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

Configure Feed

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

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