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

Configure Feed

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

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