Something went wrong. Try again.
A Witchsky fork that is groovy and NEVER glooby!! tenna.party
Something went wrong. Try again.
13 kB · 443 lines
TSX
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444import {memo, useCallback, useMemo, useState} from 'react'import {View} from 'react-native'import { type AppBskyFeedDefs, type AppBskyFeedThreadgate, AtUri, RichText as RichTextAPI,} from '@atproto/api'import {Trans} from '@lingui/macro'
import {MAX_POST_LINES} from '#/lib/constants'import {useOpenComposer} from '#/lib/hooks/useOpenComposer'import {makeProfileLink} from '#/lib/routes/links'import {countLines} from '#/lib/strings/helpers'import { POST_TOMBSTONE, type Shadow, usePostShadow,} from '#/state/cache/post-shadow'import {type ThreadItem} from '#/state/queries/usePostThread/types'import {useSession} from '#/state/session'import {type OnPostSuccessData} from '#/state/shell/composer'import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies'import {PostMeta} from '#/view/com/util/PostMeta'import { OUTER_SPACE, REPLY_LINE_WIDTH, TREE_AVI_WIDTH, TREE_INDENT,} from '#/screens/PostThread/const'import {atoms as a, useTheme} from '#/alf'import {DebugFieldDisplay} from '#/components/DebugFieldDisplay'import {useInteractionState} from '#/components/hooks/useInteractionState'import {Trash_Stroke2_Corner0_Rounded as TrashIcon} from '#/components/icons/Trash'import {LabelsOnMyPost} from '#/components/moderation/LabelsOnMe'import {PostAlerts} from '#/components/moderation/PostAlerts'import {PostHider} from '#/components/moderation/PostHider'import {type AppModerationCause} from '#/components/Pills'import {Embed, PostEmbedViewContext} from '#/components/Post/Embed'import {ShowMoreTextButton} from '#/components/Post/ShowMoreTextButton'import {PostControls, PostControlsSkeleton} from '#/components/PostControls'import {RichText} from '#/components/RichText'import * as Skele from '#/components/Skeleton'import {SubtleHover} from '#/components/SubtleHover'import {Text} from '#/components/Typography'
/** * Mimic the space in PostMeta */const TREE_AVI_PLUS_SPACE = TREE_AVI_WIDTH + a.gap_xs.gap
export function ThreadItemTreePost({ item, overrides, onPostSuccess, threadgateRecord,}: { item: Extract<ThreadItem, {type: 'threadPost'}> overrides?: { moderation?: boolean topBorder?: boolean } onPostSuccess?: (data: OnPostSuccessData) => void threadgateRecord?: AppBskyFeedThreadgate.Record}) { const postShadow = usePostShadow(item.value.post)
if (postShadow === POST_TOMBSTONE) { return <ThreadItemTreePostDeleted item={item} /> }
return ( <ThreadItemTreePostInner // Safeguard from clobbering per-post state below: key={postShadow.uri} item={item} postShadow={postShadow} threadgateRecord={threadgateRecord} overrides={overrides} onPostSuccess={onPostSuccess} /> )}
function ThreadItemTreePostDeleted({ item,}: { item: Extract<ThreadItem, {type: 'threadPost'}>}) { const t = useTheme() return ( <ThreadItemTreePostOuterWrapper item={item}> <ThreadItemTreePostInnerWrapper item={item}> <View style={[ a.flex_row, a.align_center, a.rounded_sm, t.atoms.bg_contrast_25, { gap: 6, paddingHorizontal: OUTER_SPACE / 2, height: TREE_AVI_WIDTH, }, ]}> <TrashIcon style={[t.atoms.text]} width={14} /> <Text style={[t.atoms.text_contrast_medium, a.mt_2xs]}> <Trans>Skeet has been deleted</Trans> </Text> </View> {item.ui.isLastChild && !item.ui.precedesChildReadMore && ( <View style={{height: OUTER_SPACE / 2}} /> )} </ThreadItemTreePostInnerWrapper> </ThreadItemTreePostOuterWrapper> )}
const ThreadItemTreePostOuterWrapper = memo( function ThreadItemTreePostOuterWrapper({ item, children, }: { item: Extract<ThreadItem, {type: 'threadPost'}> children: React.ReactNode }) { const t = useTheme() const indents = Math.max(0, item.ui.indent - 1)
return ( <View style={[ a.flex_row, item.ui.indent === 1 && !item.ui.showParentReplyLine && [ a.border_t, t.atoms.border_contrast_low, ], ]}> {Array.from(Array(indents)).map((_, n: number) => { const isSkipped = item.ui.skippedIndentIndices.has(n) return ( <View key={`${item.value.post.uri}-padding-${n}`} style={[ t.atoms.border_contrast_low, { borderRightWidth: isSkipped ? 0 : REPLY_LINE_WIDTH, width: TREE_INDENT + TREE_AVI_WIDTH / 2, left: 1, }, ]} /> ) })} {children} </View> ) },)
const ThreadItemTreePostInnerWrapper = memo( function ThreadItemTreePostInnerWrapper({ item, children, }: { item: Extract<ThreadItem, {type: 'threadPost'}> children: React.ReactNode }) { const t = useTheme() return ( <View style={[ a.flex_1, // TODO check on ios { paddingHorizontal: OUTER_SPACE, paddingTop: OUTER_SPACE / 2, }, item.ui.indent === 1 && [ !item.ui.showParentReplyLine && {paddingTop: OUTER_SPACE / 1.5}, !item.ui.showChildReplyLine && a.pb_sm, ], item.ui.isLastChild && !item.ui.precedesChildReadMore && [ { paddingBottom: OUTER_SPACE / 2, }, ], ]}> {item.ui.indent > 1 && ( <View style={[ a.absolute, t.atoms.border_contrast_low, { left: -1, top: 0, height: TREE_AVI_WIDTH / 2 + REPLY_LINE_WIDTH / 2 + OUTER_SPACE / 2, width: OUTER_SPACE, borderLeftWidth: REPLY_LINE_WIDTH, borderBottomWidth: REPLY_LINE_WIDTH, borderBottomLeftRadius: a.rounded_sm.borderRadius, }, ]} /> )} {children} </View> ) },)
const ThreadItemTreeReplyChildReplyLine = memo( function ThreadItemTreeReplyChildReplyLine({ item, }: { item: Extract<ThreadItem, {type: 'threadPost'}> }) { const t = useTheme() return ( <View style={[a.relative, a.pt_2xs, {width: TREE_AVI_PLUS_SPACE}]}> {item.ui.showChildReplyLine && ( <View style={[ a.flex_1, t.atoms.border_contrast_low, {borderRightWidth: 2, width: '50%', left: -1}, ]} /> )} </View> ) },)
const ThreadItemTreePostInner = memo(function ThreadItemTreePostInner({ item, postShadow, overrides, onPostSuccess, threadgateRecord,}: { item: Extract<ThreadItem, {type: 'threadPost'}> postShadow: Shadow<AppBskyFeedDefs.PostView> overrides?: { moderation?: boolean topBorder?: boolean } onPostSuccess?: (data: OnPostSuccessData) => void threadgateRecord?: AppBskyFeedThreadgate.Record}): React.ReactNode { const {openComposer} = useOpenComposer() const {currentAccount} = useSession()
const post = item.value.post const record = item.value.post.record const moderation = item.moderation const richText = useMemo( () => new RichTextAPI({ text: record.text, facets: record.facets, }), [record], ) const [limitLines, setLimitLines] = useState( () => countLines(richText?.text) >= MAX_POST_LINES, ) const threadRootUri = record.reply?.root?.uri || post.uri const postHref = useMemo(() => { const urip = new AtUri(post.uri) return makeProfileLink(post.author, 'post', urip.rkey) }, [post.uri, post.author]) const threadgateHiddenReplies = useMergedThreadgateHiddenReplies({ threadgateRecord, }) const additionalPostAlerts: AppModerationCause[] = useMemo(() => { const isPostHiddenByThreadgate = threadgateHiddenReplies.has(post.uri) const isControlledByViewer = new AtUri(threadRootUri).host === currentAccount?.did return isControlledByViewer && isPostHiddenByThreadgate ? [ { type: 'reply-hidden', source: {type: 'user', did: currentAccount?.did}, priority: 6, }, ] : [] }, [post, currentAccount?.did, threadgateHiddenReplies, threadRootUri])
const onPressReply = useCallback(() => { openComposer({ replyTo: { uri: post.uri, cid: post.cid, text: record.text, author: post.author, embed: post.embed, moderation, langs: post.record.langs, }, onPostSuccess: onPostSuccess, }) }, [openComposer, post, record, onPostSuccess, moderation])
const onPressShowMore = useCallback(() => { setLimitLines(false) }, [setLimitLines])
return ( <ThreadItemTreePostOuterWrapper item={item}> <SubtleHoverWrapper> <PostHider testID={`postThreadItem-by-${post.author.handle}`} href={postHref} disabled={overrides?.moderation === true} modui={moderation.ui('contentList')} iconSize={42} iconStyles={{marginLeft: 2, marginRight: 2}} profile={post.author} interpretFilterAsBlur> <ThreadItemTreePostInnerWrapper item={item}> <View style={[a.flex_1]}> <PostMeta author={post.author} moderation={moderation} timestamp={post.indexedAt} postHref={postHref} avatarSize={TREE_AVI_WIDTH} style={[a.pb_0]} showAvatar /> <View style={[a.flex_row]}> <ThreadItemTreeReplyChildReplyLine item={item} /> <View style={[a.flex_1, a.pl_2xs]}> <LabelsOnMyPost post={post} style={[a.pb_2xs]} /> <PostAlerts modui={moderation.ui('contentList')} style={[a.pb_2xs]} additionalCauses={additionalPostAlerts} /> {richText?.text ? ( <> <RichText enableTags value={richText} style={[a.flex_1, a.text_md]} numberOfLines={limitLines ? MAX_POST_LINES : undefined} authorHandle={post.author.handle} shouldProxyLinks={true} /> {limitLines && ( <ShowMoreTextButton style={[a.text_md]} onPress={onPressShowMore} /> )} </> ) : null} {post.embed && ( <View style={[a.pb_xs]}> <Embed embed={post.embed} moderation={moderation} viewContext={PostEmbedViewContext.Feed} /> </View> )} <PostControls variant="compact" post={postShadow} record={record} richText={richText} onPressReply={onPressReply} logContext="PostThreadItem" threadgateRecord={threadgateRecord} /> <DebugFieldDisplay subject={post} /> </View> </View> </View> </ThreadItemTreePostInnerWrapper> </PostHider> </SubtleHoverWrapper> </ThreadItemTreePostOuterWrapper> )})
function SubtleHoverWrapper({children}: {children: React.ReactNode}) { const { state: hover, onIn: onHoverIn, onOut: onHoverOut, } = useInteractionState() return ( <View onPointerEnter={onHoverIn} onPointerLeave={onHoverOut} style={[a.flex_1, a.pointer]}> <SubtleHover hover={hover} /> {children} </View> )}
export function ThreadItemTreePostSkeleton({index}: {index: number}) { const t = useTheme() const even = index % 2 === 0 return ( <View style={[ {paddingHorizontal: OUTER_SPACE, paddingVertical: OUTER_SPACE / 1.5}, a.border_t, t.atoms.border_contrast_low, ]}> <Skele.Row style={[a.align_start, a.gap_xs]}> <Skele.Circle size={TREE_AVI_WIDTH} />
<Skele.Col style={[a.gap_xs]}> <Skele.Row style={[a.gap_sm]}> <Skele.Text style={[a.text_md, {width: '20%'}]} /> <Skele.Text blend style={[a.text_md, {width: '30%'}]} /> </Skele.Row>
<Skele.Col> {even ? ( <> <Skele.Text blend style={[a.text_md, {width: '100%'}]} /> <Skele.Text blend style={[a.text_md, {width: '60%'}]} /> </> ) : ( <Skele.Text blend style={[a.text_md, {width: '60%'}]} /> )} </Skele.Col>
<PostControlsSkeleton /> </Skele.Col> </Skele.Row> </View> )}