Bluesky app fork with some witchin' additions 馃挮
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

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