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

Configure Feed

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

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