Bluesky app fork with some witchin' additions 馃挮
at post-text-option 220 lines 6.8 kB view raw
1import {useCallback} from 'react' 2import {View} from 'react-native' 3import {Image} from 'expo-image' 4import {type AppBskyActorDefs, type AppBskyEmbedExternal} from '@atproto/api' 5import {msg, Trans} from '@lingui/macro' 6import {useLingui} from '@lingui/react' 7import {useNavigation} from '@react-navigation/native' 8import {useQueryClient} from '@tanstack/react-query' 9 10import {useOpenLink} from '#/lib/hooks/useOpenLink' 11import {type NavigationProp} from '#/lib/routes/types' 12import {sanitizeHandle} from '#/lib/strings/handles' 13import {toNiceDomain} from '#/lib/strings/url-helpers' 14import {logger} from '#/logger' 15import {useModerationOpts} from '#/state/preferences/moderation-opts' 16import {unstableCacheProfileView} from '#/state/queries/profile' 17import {android, atoms as a, platform, tokens, useTheme, web} from '#/alf' 18import {Button, ButtonIcon, ButtonText} from '#/components/Button' 19import * as Dialog from '#/components/Dialog' 20import * as ProfileCard from '#/components/ProfileCard' 21import {Text} from '#/components/Typography' 22import type * as bsky from '#/types/bsky' 23import {Globe_Stroke2_Corner0_Rounded} from '../icons/Globe' 24import {SquareArrowTopRight_Stroke2_Corner0_Rounded as SquareArrowTopRightIcon} from '../icons/SquareArrowTopRight' 25import {LiveIndicator} from './LiveIndicator' 26 27export function LiveStatusDialog({ 28 control, 29 profile, 30 embed, 31}: { 32 control: Dialog.DialogControlProps 33 profile: bsky.profile.AnyProfileView 34 status: AppBskyActorDefs.StatusView 35 embed: AppBskyEmbedExternal.View 36}) { 37 const navigation = useNavigation<NavigationProp>() 38 return ( 39 <Dialog.Outer control={control} nativeOptions={{preventExpansion: true}}> 40 <Dialog.Handle difference={!!embed.external.thumb} /> 41 <DialogInner profile={profile} embed={embed} navigation={navigation} /> 42 </Dialog.Outer> 43 ) 44} 45 46function DialogInner({ 47 profile, 48 embed, 49 navigation, 50}: { 51 profile: bsky.profile.AnyProfileView 52 embed: AppBskyEmbedExternal.View 53 navigation: NavigationProp 54}) { 55 const {_} = useLingui() 56 const control = Dialog.useDialogContext() 57 58 const onPressOpenProfile = useCallback(() => { 59 control.close(() => { 60 navigation.push('Profile', { 61 name: profile.handle, 62 }) 63 }) 64 }, [navigation, profile.handle, control]) 65 66 return ( 67 <Dialog.ScrollableInner 68 label={_(msg`${sanitizeHandle(profile.handle)} is live`)} 69 contentContainerStyle={[a.pt_0, a.px_0]} 70 style={[web({maxWidth: 420}), a.overflow_hidden]}> 71 <LiveStatus 72 profile={profile} 73 embed={embed} 74 onPressOpenProfile={onPressOpenProfile} 75 /> 76 <Dialog.Close /> 77 </Dialog.ScrollableInner> 78 ) 79} 80 81export function LiveStatus({ 82 profile, 83 embed, 84 padding = 'xl', 85 onPressOpenProfile, 86}: { 87 profile: bsky.profile.AnyProfileView 88 embed: AppBskyEmbedExternal.View 89 padding?: 'lg' | 'xl' 90 onPressOpenProfile: () => void 91}) { 92 const {_} = useLingui() 93 const t = useTheme() 94 const queryClient = useQueryClient() 95 const openLink = useOpenLink() 96 const moderationOpts = useModerationOpts() 97 98 return ( 99 <> 100 {embed.external.thumb && ( 101 <View 102 style={[ 103 t.atoms.bg_contrast_25, 104 a.w_full, 105 a.aspect_card, 106 android([ 107 a.overflow_hidden, 108 { 109 borderTopLeftRadius: a.rounded_md.borderRadius, 110 borderTopRightRadius: a.rounded_md.borderRadius, 111 }, 112 ]), 113 ]}> 114 <Image 115 source={embed.external.thumb} 116 contentFit="cover" 117 style={[a.absolute, a.inset_0]} 118 accessibilityIgnoresInvertColors 119 /> 120 <LiveIndicator 121 size="large" 122 style={[ 123 a.absolute, 124 {top: tokens.space.lg, left: tokens.space.lg}, 125 a.align_start, 126 ]} 127 /> 128 </View> 129 )} 130 <View 131 style={[ 132 a.gap_lg, 133 padding === 'xl' 134 ? [a.px_xl, !embed.external.thumb ? a.pt_2xl : a.pt_lg] 135 : a.p_lg, 136 ]}> 137 <View style={[a.w_full, a.justify_center, a.gap_2xs]}> 138 <Text 139 numberOfLines={3} 140 style={[a.leading_snug, a.font_semi_bold, a.text_xl]}> 141 {embed.external.title || embed.external.uri} 142 </Text> 143 <View style={[a.flex_row, a.align_center, a.gap_2xs]}> 144 <Globe_Stroke2_Corner0_Rounded 145 size="xs" 146 style={[t.atoms.text_contrast_medium]} 147 /> 148 <Text 149 numberOfLines={1} 150 style={[a.text_sm, a.leading_snug, t.atoms.text_contrast_medium]}> 151 {toNiceDomain(embed.external.uri)} 152 </Text> 153 </View> 154 </View> 155 <Button 156 label={_(msg`Watch now`)} 157 size={platform({native: 'large', web: 'small'})} 158 color="primary" 159 variant="solid" 160 onPress={() => { 161 logger.metric( 162 'live:card:watch', 163 {subject: profile.did}, 164 {statsig: true}, 165 ) 166 openLink(embed.external.uri, false) 167 }}> 168 <ButtonText> 169 <Trans>Watch now</Trans> 170 </ButtonText> 171 <ButtonIcon icon={SquareArrowTopRightIcon} /> 172 </Button> 173 <View style={[t.atoms.border_contrast_low, a.border_t, a.w_full]} /> 174 {moderationOpts && ( 175 <ProfileCard.Header> 176 <ProfileCard.Avatar 177 profile={profile} 178 moderationOpts={moderationOpts} 179 disabledPreview 180 /> 181 {/* Ensure wide enough on web hover */} 182 <View style={[a.flex_1, web({minWidth: 100})]}> 183 <ProfileCard.NameAndHandle 184 profile={profile} 185 moderationOpts={moderationOpts} 186 /> 187 </View> 188 <Button 189 label={_(msg`Open profile`)} 190 size="small" 191 color="secondary" 192 variant="solid" 193 onPress={() => { 194 logger.metric( 195 'live:card:openProfile', 196 {subject: profile.did}, 197 {statsig: true}, 198 ) 199 unstableCacheProfileView(queryClient, profile) 200 onPressOpenProfile() 201 }}> 202 <ButtonText> 203 <Trans>Open profile</Trans> 204 </ButtonText> 205 </Button> 206 </ProfileCard.Header> 207 )} 208 <Text 209 style={[ 210 a.w_full, 211 a.text_center, 212 t.atoms.text_contrast_low, 213 a.text_sm, 214 ]}> 215 <Trans>Live feature is in beta testing</Trans> 216 </Text> 217 </View> 218 </> 219 ) 220}