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

Configure Feed

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

at 6d68a5bd212dd4eeee816828ffe4e27601cdd7f3 257 lines 8.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, 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 {useModerationOpts} from '#/state/preferences/moderation-opts' 15import {unstableCacheProfileView} from '#/state/queries/profile' 16import {android, atoms as a, platform, tokens, useTheme, web} from '#/alf' 17import {Button, ButtonIcon, ButtonText} from '#/components/Button' 18import * as Dialog from '#/components/Dialog' 19import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfoIcon} from '#/components/icons/CircleInfo' 20import {createStaticClick, SimpleInlineLinkText} from '#/components/Link' 21import {useGlobalReportDialogControl} from '#/components/moderation/ReportDialog' 22import * as ProfileCard from '#/components/ProfileCard' 23import {Text} from '#/components/Typography' 24import {useAnalytics} from '#/analytics' 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 ax = useAnalytics() 107 const {_} = useLingui() 108 const t = useTheme() 109 const queryClient = useQueryClient() 110 const openLink = useOpenLink() 111 const moderationOpts = useModerationOpts() 112 const reportDialogControl = useGlobalReportDialogControl() 113 const dialogContext = Dialog.useDialogContext() 114 115 return ( 116 <> 117 {embed.external.thumb && ( 118 <View 119 style={[ 120 t.atoms.bg_contrast_25, 121 a.w_full, 122 a.aspect_card, 123 android([ 124 a.overflow_hidden, 125 { 126 borderTopLeftRadius: a.rounded_md.borderRadius, 127 borderTopRightRadius: a.rounded_md.borderRadius, 128 }, 129 ]), 130 ]}> 131 <Image 132 source={embed.external.thumb} 133 contentFit="cover" 134 style={[a.absolute, a.inset_0]} 135 accessibilityIgnoresInvertColors 136 /> 137 <LiveIndicator 138 size="large" 139 style={[ 140 a.absolute, 141 {top: tokens.space.lg, left: tokens.space.lg}, 142 a.align_start, 143 ]} 144 /> 145 </View> 146 )} 147 <View 148 style={[ 149 a.gap_lg, 150 padding === 'xl' 151 ? [a.px_xl, !embed.external.thumb ? a.pt_2xl : a.pt_lg] 152 : a.p_lg, 153 ]}> 154 <View style={[a.w_full, a.justify_center, a.gap_2xs]}> 155 <Text 156 numberOfLines={3} 157 style={[a.leading_snug, a.font_semi_bold, a.text_xl]}> 158 {embed.external.title || embed.external.uri} 159 </Text> 160 <View style={[a.flex_row, a.align_center, a.gap_2xs]}> 161 <Globe_Stroke2_Corner0_Rounded 162 size="xs" 163 style={[t.atoms.text_contrast_medium]} 164 /> 165 <Text 166 numberOfLines={1} 167 style={[a.text_sm, a.leading_snug, t.atoms.text_contrast_medium]}> 168 {toNiceDomain(embed.external.uri)} 169 </Text> 170 </View> 171 </View> 172 <Button 173 label={_(msg`Watch now`)} 174 size={platform({native: 'large', web: 'small'})} 175 color="primary" 176 variant="solid" 177 onPress={() => { 178 ax.metric('live:card:watch', {subject: profile.did}) 179 openLink(embed.external.uri, false) 180 }}> 181 <ButtonText> 182 <Trans>Watch now</Trans> 183 </ButtonText> 184 <ButtonIcon icon={SquareArrowTopRightIcon} /> 185 </Button> 186 <View style={[t.atoms.border_contrast_low, a.border_t, a.w_full]} /> 187 {moderationOpts && ( 188 <ProfileCard.Header> 189 <ProfileCard.Avatar 190 profile={profile} 191 moderationOpts={moderationOpts} 192 disabledPreview 193 /> 194 {/* Ensure wide enough on web hover */} 195 <View style={[a.flex_1, web({minWidth: 100})]}> 196 <ProfileCard.NameAndHandle 197 profile={profile} 198 moderationOpts={moderationOpts} 199 /> 200 </View> 201 <Button 202 label={_(msg`Open profile`)} 203 size="small" 204 color="secondary" 205 variant="solid" 206 onPress={() => { 207 ax.metric('live:card:openProfile', {subject: profile.did}) 208 unstableCacheProfileView(queryClient, profile) 209 onPressOpenProfile() 210 }}> 211 <ButtonText> 212 <Trans>Open profile</Trans> 213 </ButtonText> 214 </Button> 215 </ProfileCard.Header> 216 )} 217 <View 218 style={[ 219 a.flex_row, 220 a.align_center, 221 a.justify_between, 222 a.w_full, 223 a.pt_sm, 224 ]}> 225 <View style={[a.flex_row, a.align_center, a.gap_xs, a.flex_1]}> 226 <CircleInfoIcon size="sm" fill={t.atoms.text_contrast_low.color} /> 227 <Text style={[t.atoms.text_contrast_low, a.text_sm]}> 228 <Trans>Live feature is in beta</Trans> 229 </Text> 230 </View> 231 {status && ( 232 <SimpleInlineLinkText 233 label={_(msg`Report this livestream`)} 234 {...createStaticClick(() => { 235 function open() { 236 reportDialogControl.open({ 237 subject: { 238 ...status, 239 $type: 'app.bsky.actor.defs#statusView', 240 }, 241 }) 242 } 243 if (dialogContext.isWithinDialog) { 244 dialogContext.close(open) 245 } else { 246 open() 247 } 248 })} 249 style={[a.text_sm, a.underline, t.atoms.text_contrast_medium]}> 250 <Trans>Report</Trans> 251 </SimpleInlineLinkText> 252 )} 253 </View> 254 </View> 255 </> 256 ) 257}