Something went wrong. Try again.
Bluesky app fork with some witchin' additions 💫 forked from jollywhoppers.com/witchsky.app
Something went wrong. Try again.
16 kB · 566 lines
TSX
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567import {useMemo} from 'react'import {View} from 'react-native'import {Image} from 'expo-image'import {LinearGradient} from 'expo-linear-gradient'import { type AppBskyActorDefs, AppBskyEmbedVideo, type AppBskyFeedDefs, AppBskyFeedPost, type ModerationDecision,} from '@atproto/api'import {msg} from '@lingui/macro'import {useLingui} from '@lingui/react'
import {sanitizeHandle} from '#/lib/strings/handles'import {formatCount} from '#/view/com/util/numeric/format'import {UserAvatar} from '#/view/com/util/UserAvatar'import {type VideoFeedSourceContext} from '#/screens/VideoFeed/types'import {atoms as a, useTheme} from '#/alf'import {BLUE_HUE} from '#/alf/util/colorGeneration'import {select} from '#/alf/util/themeSelector'import {useInteractionState} from '#/components/hooks/useInteractionState'import {EyeSlash_Stroke2_Corner0_Rounded as Eye} from '#/components/icons/EyeSlash'import {Heart2_Stroke2_Corner0_Rounded as Heart} from '#/components/icons/Heart2'import {Repost_Stroke2_Corner2_Rounded as Repost} from '#/components/icons/Repost'import {Link} from '#/components/Link'import {MediaInsetBorder} from '#/components/MediaInsetBorder'import * as Hider from '#/components/moderation/Hider'import {Text} from '#/components/Typography'import * as bsky from '#/types/bsky'
function getBlackColor(t: ReturnType<typeof useTheme>) { return select(t.name, { light: t.palette.black, dark: t.atoms.bg_contrast_25.backgroundColor, dim: `hsl(${BLUE_HUE}, 28%, 6%)`, })}
export function VideoPostCard({ post, sourceContext, moderation, onInteract,}: { post: AppBskyFeedDefs.PostView sourceContext: VideoFeedSourceContext moderation: ModerationDecision /** * Callback for metrics etc */ onInteract?: () => void}) { const t = useTheme() const {_, i18n} = useLingui() const embed = post.embed const { state: pressed, onIn: onPressIn, onOut: onPressOut, } = useInteractionState()
const listModUi = moderation.ui('contentList')
const mergedModui = useMemo(() => { const modui = moderation.ui('contentList') const mediaModui = moderation.ui('contentMedia') modui.alerts = [...modui.alerts, ...mediaModui.alerts] modui.blurs = [...modui.blurs, ...mediaModui.blurs] modui.filters = [...modui.filters, ...mediaModui.filters] modui.informs = [...modui.informs, ...mediaModui.informs] return modui }, [moderation])
/** * Filtering should be done at a higher level, such as `PostFeed` or * `PostFeedVideoGridRow`, but we need to protect here as well. */ if (!AppBskyEmbedVideo.isView(embed)) return null
const author = post.author const text = bsky.dangerousIsType<AppBskyFeedPost.Record>( post.record, AppBskyFeedPost.isRecord, ) ? post.record?.text : '' const likeCount = post?.likeCount ?? 0 const repostCount = post?.repostCount ?? 0 const {thumbnail} = embed const black = getBlackColor(t)
const textAndAuthor = ( <View style={[a.pr_xs, {paddingTop: 6, gap: 4}]}> {text && ( <Text style={[a.text_md, a.leading_snug]} numberOfLines={2} emoji> {text} </Text> )} <View style={[a.flex_row, a.gap_xs, a.align_center]}> <View style={[a.relative, a.rounded_full, {width: 20, height: 20}]}> <UserAvatar type="user" size={20} avatar={post.author.avatar} /> <MediaInsetBorder /> </View> <Text style={[ a.flex_1, a.text_sm, a.leading_tight, t.atoms.text_contrast_medium, ]} numberOfLines={1}> {sanitizeHandle(post.author.handle, '@')} </Text> </View> </View> )
return ( <Link accessibilityHint={_(msg`Views video in immersive mode`)} label={_(msg`Video from ${author.handle}: ${text}`)} to={{ screen: 'VideoFeed', params: { ...sourceContext, initialPostUri: post.uri, }, }} onPress={() => { onInteract?.() }} onPressIn={onPressIn} onPressOut={onPressOut} style={[ a.flex_col, { alignItems: undefined, justifyContent: undefined, }, ]}> <Hider.Outer modui={mergedModui}> <Hider.Mask> <View style={[ a.justify_center, a.rounded_md, a.overflow_hidden, { backgroundColor: black, aspectRatio: 9 / 16, }, ]}> <Image source={{uri: thumbnail}} style={[a.w_full, a.h_full, {opacity: pressed ? 0.8 : 1}]} accessibilityIgnoresInvertColors blurRadius={100} /> <MediaInsetBorder /> <View style={[a.absolute, a.inset_0, a.justify_center, a.align_center]}> <View style={[ a.absolute, a.inset_0, a.justify_center, a.align_center, { backgroundColor: 'black', opacity: 0.2, }, ]} /> <View style={[a.align_center, a.gap_xs]}> <Eye size="lg" fill="white" /> <Text style={[a.text_sm, {color: 'white'}]}> {_(msg`Hidden`)} </Text> </View> </View> </View> {listModUi.blur ? ( <VideoPostCardTextPlaceholder author={post.author} /> ) : ( textAndAuthor )} </Hider.Mask> <Hider.Content> <View style={[ a.justify_center, a.rounded_md, a.overflow_hidden, { backgroundColor: black, aspectRatio: 9 / 16, }, ]}> <Image source={{uri: thumbnail}} style={[a.w_full, a.h_full, {opacity: pressed ? 0.8 : 1}]} accessibilityIgnoresInvertColors /> <MediaInsetBorder />
<View style={[a.absolute, a.inset_0]}> <View style={[ a.absolute, a.inset_0, a.pt_2xl, { top: 'auto', }, ]}> <LinearGradient colors={[black, 'rgba(0, 0, 0, 0)']} locations={[0.02, 1]} start={{x: 0, y: 1}} end={{x: 0, y: 0}} style={[a.absolute, a.inset_0, {opacity: 0.9}]} />
<View style={[a.relative, a.z_10, a.p_md, a.flex_row, a.gap_md]}> {likeCount > 0 && ( <View style={[a.flex_row, a.align_center, a.gap_xs]}> <Heart size="sm" fill="white" /> <Text style={[a.text_sm, a.font_semi_bold, {color: 'white'}]}> {formatCount(i18n, likeCount)} </Text> </View> )} {repostCount > 0 && ( <View style={[a.flex_row, a.align_center, a.gap_xs]}> <Repost size="sm" fill="white" /> <Text style={[a.text_sm, a.font_semi_bold, {color: 'white'}]}> {formatCount(i18n, repostCount)} </Text> </View> )} </View> </View> </View> </View> {textAndAuthor} </Hider.Content> </Hider.Outer> </Link> )}
export function VideoPostCardPlaceholder() { const t = useTheme() const black = getBlackColor(t)
return ( <View style={[a.flex_1]}> <View style={[ a.rounded_md, a.overflow_hidden, { backgroundColor: black, aspectRatio: 9 / 16, }, ]}> <MediaInsetBorder /> </View> <VideoPostCardTextPlaceholder /> </View> )}
export function VideoPostCardTextPlaceholder({ author,}: { author?: AppBskyActorDefs.ProfileViewBasic}) { const t = useTheme()
return ( <View style={[a.flex_1]}> <View style={[a.pr_xs, {paddingTop: 8, gap: 6}]}> <View style={[ a.w_full, a.rounded_xs, t.atoms.bg_contrast_50, { height: 14, }, ]} /> <View style={[ a.w_full, a.rounded_xs, t.atoms.bg_contrast_50, { height: 14, width: '70%', }, ]} /> {author ? ( <View style={[a.flex_row, a.gap_xs, a.align_center]}> <View style={[a.relative, a.rounded_full, {width: 20, height: 20}]}> <UserAvatar type="user" size={20} avatar={author.avatar} /> <MediaInsetBorder /> </View> <Text style={[ a.flex_1, a.text_sm, a.leading_tight, t.atoms.text_contrast_medium, ]} numberOfLines={1}> {sanitizeHandle(author.handle, '@')} </Text> </View> ) : ( <View style={[a.flex_row, a.gap_xs, a.align_center]}> <View style={[ a.rounded_full, t.atoms.bg_contrast_50, { width: 20, height: 20, }, ]} /> <View style={[ a.rounded_xs, t.atoms.bg_contrast_25, { height: 12, width: '75%', }, ]} /> </View> )} </View> </View> )}
export function CompactVideoPostCard({ post, sourceContext, moderation, onInteract,}: { post: AppBskyFeedDefs.PostView sourceContext: VideoFeedSourceContext moderation: ModerationDecision /** * Callback for metrics etc */ onInteract?: () => void}) { const t = useTheme() const {_, i18n} = useLingui() const embed = post.embed const { state: pressed, onIn: onPressIn, onOut: onPressOut, } = useInteractionState()
const mergedModui = useMemo(() => { const modui = moderation.ui('contentList') const mediaModui = moderation.ui('contentMedia') modui.alerts = [...modui.alerts, ...mediaModui.alerts] modui.blurs = [...modui.blurs, ...mediaModui.blurs] modui.filters = [...modui.filters, ...mediaModui.filters] modui.informs = [...modui.informs, ...mediaModui.informs] return modui }, [moderation])
/** * Filtering should be done at a higher level, such as `PostFeed` or * `PostFeedVideoGridRow`, but we need to protect here as well. */ if (!AppBskyEmbedVideo.isView(embed)) return null
const likeCount = post?.likeCount ?? 0 const showLikeCount = false const {thumbnail} = embed const black = getBlackColor(t)
return ( <Link label={_(msg`View video`)} to={{ screen: 'VideoFeed', params: { ...sourceContext, initialPostUri: post.uri, }, }} onPress={() => { onInteract?.() }} onPressIn={onPressIn} onPressOut={onPressOut} style={[ a.flex_col, t.atoms.shadow_sm, { alignItems: undefined, justifyContent: undefined, }, ]}> <Hider.Outer modui={mergedModui}> <Hider.Mask> <View style={[ a.justify_center, a.rounded_lg, a.overflow_hidden, a.border, t.atoms.border_contrast_low, { backgroundColor: black, aspectRatio: 9 / 16, }, ]}> <Image source={{uri: thumbnail}} style={[a.w_full, a.h_full, {opacity: pressed ? 0.8 : 1}]} accessibilityIgnoresInvertColors blurRadius={100} /> <MediaInsetBorder /> <View style={[a.absolute, a.inset_0, a.justify_center, a.align_center]}> <View style={[ a.absolute, a.inset_0, a.justify_center, a.align_center, a.border, t.atoms.border_contrast_low, { backgroundColor: 'black', opacity: 0.2, }, ]} /> <View style={[a.align_center, a.gap_xs]}> <Eye size="lg" fill="white" /> <Text style={[a.text_sm, {color: 'white'}]}> {_(msg`Hidden`)} </Text> </View> </View> </View> </Hider.Mask> <Hider.Content> <View style={[ a.justify_center, a.rounded_lg, a.overflow_hidden, a.border, t.atoms.border_contrast_low, { backgroundColor: black, aspectRatio: 9 / 16, }, ]}> <Image source={{uri: thumbnail}} style={[a.w_full, a.h_full, {opacity: pressed ? 0.8 : 1}]} accessibilityIgnoresInvertColors /> <MediaInsetBorder />
<View style={[a.absolute, a.inset_0, t.atoms.shadow_sm]}> <View style={[a.absolute, a.inset_0, a.p_sm, {bottom: 'auto'}]}> <View style={[a.relative, a.rounded_full, {width: 24, height: 24}]}> <UserAvatar type="user" size={24} avatar={post.author.avatar} /> <MediaInsetBorder /> </View> </View>
{showLikeCount && ( <View style={[ a.absolute, a.inset_0, a.pt_2xl, { top: 'auto', }, ]}> <LinearGradient colors={[black, 'rgba(0, 0, 0, 0)']} locations={[0.02, 1]} start={{x: 0, y: 1}} end={{x: 0, y: 0}} style={[a.absolute, a.inset_0, {opacity: 0.9}]} />
<View style={[a.relative, a.z_10, a.p_sm, a.flex_row, a.gap_md]}> {likeCount > 0 && ( <View style={[a.flex_row, a.align_center, a.gap_xs]}> <Heart size="sm" fill="white" /> <Text style={[ a.text_sm, a.font_semi_bold, {color: 'white'}, ]}> {formatCount(i18n, likeCount)} </Text> </View> )} </View> </View> )} </View> </View> </Hider.Content> </Hider.Outer> </Link> )}
export function CompactVideoPostCardPlaceholder() { const t = useTheme() const black = getBlackColor(t)
return ( <View style={[a.flex_1, t.atoms.shadow_sm]}> <View style={[ a.rounded_lg, a.overflow_hidden, a.border, t.atoms.border_contrast_low, { backgroundColor: black, aspectRatio: 9 / 16, }, ]}> <MediaInsetBorder /> </View> </View> )}