Bluesky app fork with some witchin' additions 馃挮
at jean/pds-label 283 lines 9.2 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} from '@lingui/core/macro' 6import {useLingui} from '@lingui/react' 7import {Trans} from '@lingui/react/macro' 8import {useNavigation} from '@react-navigation/native' 9import {useQueryClient} from '@tanstack/react-query' 10 11import {useOpenLink} from '#/lib/hooks/useOpenLink' 12import {type NavigationProp} from '#/lib/routes/types' 13import {sanitizeHandle} from '#/lib/strings/handles' 14import {toNiceDomain} from '#/lib/strings/url-helpers' 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 {CircleInfo_Stroke2_Corner0_Rounded as CircleInfoIcon} from '#/components/icons/CircleInfo' 21import {Globe_Stroke2_Corner0_Rounded} from '#/components/icons/Globe' 22import {SquareArrowTopRight_Stroke2_Corner0_Rounded as SquareArrowTopRightIcon} from '#/components/icons/SquareArrowTopRight' 23import {createStaticClick, SimpleInlineLinkText} from '#/components/Link' 24import {useGlobalReportDialogControl} from '#/components/moderation/ReportDialog' 25import * as ProfileCard from '#/components/ProfileCard' 26import {Text} from '#/components/Typography' 27import {useAnalytics} from '#/analytics' 28import {LiveIndicator} from '#/features/liveNow/components/LiveIndicator' 29import type * as bsky from '#/types/bsky' 30 31export function LiveStatusDialog({ 32 control, 33 profile, 34 embed, 35 status, 36 onPressViewAvatar, 37}: { 38 control: Dialog.DialogControlProps 39 profile: bsky.profile.AnyProfileView 40 status: AppBskyActorDefs.StatusView 41 embed: AppBskyEmbedExternal.View 42 onPressViewAvatar?: () => void 43}) { 44 const navigation = useNavigation<NavigationProp>() 45 return ( 46 <Dialog.Outer control={control} nativeOptions={{preventExpansion: true}}> 47 <Dialog.Handle difference={!!embed.external.thumb} /> 48 <DialogInner 49 status={status} 50 profile={profile} 51 embed={embed} 52 navigation={navigation} 53 onPressViewAvatar={onPressViewAvatar} 54 /> 55 </Dialog.Outer> 56 ) 57} 58 59function DialogInner({ 60 profile, 61 embed, 62 navigation, 63 status, 64 onPressViewAvatar, 65}: { 66 profile: bsky.profile.AnyProfileView 67 embed: AppBskyEmbedExternal.View 68 navigation: NavigationProp 69 status: AppBskyActorDefs.StatusView 70 onPressViewAvatar?: () => void 71}) { 72 const {_} = useLingui() 73 const control = Dialog.useDialogContext() 74 75 const onPressOpenProfile = useCallback(() => { 76 control.close(() => { 77 navigation.push('Profile', { 78 name: profile.handle, 79 }) 80 }) 81 }, [navigation, profile.handle, control]) 82 83 const handlePressViewAvatar = useCallback(() => { 84 control.close(onPressViewAvatar) 85 }, [control, onPressViewAvatar]) 86 87 return ( 88 <Dialog.ScrollableInner 89 label={_(msg`${sanitizeHandle(profile.handle)} is live`)} 90 contentContainerStyle={[a.pt_0, a.px_0]} 91 style={[web({maxWidth: 420}), a.overflow_hidden]}> 92 <LiveStatus 93 status={status} 94 profile={profile} 95 embed={embed} 96 onPressOpenProfile={onPressOpenProfile} 97 {...(onPressViewAvatar 98 ? {onPressViewAvatar: handlePressViewAvatar} 99 : {})} 100 /> 101 <Dialog.Close /> 102 </Dialog.ScrollableInner> 103 ) 104} 105 106export function LiveStatus({ 107 status, 108 profile, 109 embed, 110 padding = 'xl', 111 onPressOpenProfile, 112 onPressViewAvatar, 113}: { 114 status: AppBskyActorDefs.StatusView 115 profile: bsky.profile.AnyProfileView 116 embed: AppBskyEmbedExternal.View 117 padding?: 'lg' | 'xl' 118 onPressOpenProfile: () => void 119 onPressViewAvatar?: () => void 120}) { 121 const ax = useAnalytics() 122 const {_} = useLingui() 123 const t = useTheme() 124 const queryClient = useQueryClient() 125 const openLink = useOpenLink() 126 const moderationOpts = useModerationOpts() 127 const reportDialogControl = useGlobalReportDialogControl() 128 const dialogContext = Dialog.useDialogContext() 129 130 return ( 131 <> 132 {embed.external.thumb && ( 133 <View 134 style={[ 135 t.atoms.bg_contrast_25, 136 a.w_full, 137 a.aspect_card, 138 android([ 139 a.overflow_hidden, 140 { 141 borderTopLeftRadius: a.rounded_md.borderRadius, 142 borderTopRightRadius: a.rounded_md.borderRadius, 143 }, 144 ]), 145 ]}> 146 <Image 147 source={embed.external.thumb} 148 contentFit="cover" 149 style={[a.absolute, a.inset_0]} 150 accessibilityIgnoresInvertColors 151 /> 152 <LiveIndicator 153 size="large" 154 style={[ 155 a.absolute, 156 {top: tokens.space.lg, left: tokens.space.lg}, 157 a.align_start, 158 ]} 159 /> 160 </View> 161 )} 162 <View 163 style={[ 164 a.gap_lg, 165 padding === 'xl' 166 ? [a.px_xl, !embed.external.thumb ? a.pt_2xl : a.pt_lg] 167 : a.p_lg, 168 ]}> 169 <View style={[a.w_full, a.justify_center, a.gap_2xs]}> 170 <Text 171 numberOfLines={3} 172 style={[a.leading_snug, a.font_semi_bold, a.text_xl]}> 173 {embed.external.title || embed.external.uri} 174 </Text> 175 <View style={[a.flex_row, a.align_center, a.gap_2xs]}> 176 <Globe_Stroke2_Corner0_Rounded 177 size="xs" 178 style={[t.atoms.text_contrast_medium]} 179 /> 180 <Text 181 numberOfLines={1} 182 style={[a.text_sm, a.leading_snug, t.atoms.text_contrast_medium]}> 183 {toNiceDomain(embed.external.uri)} 184 </Text> 185 </View> 186 </View> 187 <Button 188 label={_(msg`Watch now`)} 189 size={platform({native: 'large', web: 'small'})} 190 color="primary" 191 variant="solid" 192 onPress={() => { 193 ax.metric('live:card:watch', {subject: profile.did}) 194 openLink(embed.external.uri, false) 195 }}> 196 <ButtonText> 197 <Trans>Watch now</Trans> 198 </ButtonText> 199 <ButtonIcon icon={SquareArrowTopRightIcon} /> 200 </Button> 201 <View style={[t.atoms.border_contrast_low, a.border_t, a.w_full]} /> 202 {moderationOpts && ( 203 <ProfileCard.Header> 204 <ProfileCard.Avatar 205 profile={profile} 206 moderationOpts={moderationOpts} 207 disabledPreview 208 /> 209 {/* Ensure wide enough on web hover */} 210 <View style={[a.flex_1, web({minWidth: 100})]}> 211 <ProfileCard.NameAndHandle 212 profile={profile} 213 moderationOpts={moderationOpts} 214 /> 215 </View> 216 <Button 217 label={ 218 onPressViewAvatar ? _(msg`View avatar`) : _(msg`Open profile`) 219 } 220 size="small" 221 color="secondary" 222 variant="solid" 223 onPress={() => { 224 if (onPressViewAvatar) { 225 ax.metric('live:card:viewAvatar', {subject: profile.did}) 226 onPressViewAvatar() 227 } else { 228 ax.metric('live:card:openProfile', {subject: profile.did}) 229 unstableCacheProfileView(queryClient, profile) 230 onPressOpenProfile() 231 } 232 }}> 233 <ButtonText> 234 {onPressViewAvatar ? ( 235 <Trans>View avatar</Trans> 236 ) : ( 237 <Trans>Open profile</Trans> 238 )} 239 </ButtonText> 240 </Button> 241 </ProfileCard.Header> 242 )} 243 <View 244 style={[ 245 a.flex_row, 246 a.align_center, 247 a.justify_between, 248 a.w_full, 249 a.pt_sm, 250 ]}> 251 <View style={[a.flex_row, a.align_center, a.gap_xs, a.flex_1]}> 252 <CircleInfoIcon size="sm" fill={t.atoms.text_contrast_low.color} /> 253 <Text style={[t.atoms.text_contrast_low, a.text_sm]}> 254 <Trans>Live feature is in beta</Trans> 255 </Text> 256 </View> 257 {status && ( 258 <SimpleInlineLinkText 259 label={_(msg`Report this livestream`)} 260 {...createStaticClick(() => { 261 function open() { 262 reportDialogControl.open({ 263 subject: { 264 ...status, 265 $type: 'app.bsky.actor.defs#statusView', 266 }, 267 }) 268 } 269 if (dialogContext.isWithinDialog) { 270 dialogContext.close(open) 271 } else { 272 open() 273 } 274 })} 275 style={[a.text_sm, a.underline, t.atoms.text_contrast_medium]}> 276 <Trans>Report</Trans> 277 </SimpleInlineLinkText> 278 )} 279 </View> 280 </View> 281 </> 282 ) 283}