diff options
author | Eric Bailey <git@esb.lol> | 2025-08-26 09:54:19 -0500 |
---|---|---|
committer | GitHub <noreply@github.com> | 2025-08-26 09:54:19 -0500 |
commit | df20ae237eaf434c6ed0fd032f8328cd9b8c352c (patch) | |
tree | eecd070cf125acc908b1137a569aa369fe5fc436 /src/view/com/post-thread/PostThreadItem.tsx | |
parent | e91a6838101c9566ce2dafaa6fe8c77293a5eba6 (diff) | |
download | voidsky-df20ae237eaf434c6ed0fd032f8328cd9b8c352c.tar.zst |
Threads v2 cleanup (#8902)
* Delete root PostThread component * Remove PostThreadItem, migrate DebugMod to use new components * Remove other unused components * Move PostThreadFollowBtn to new home * Move PostThreadComposePrompt to new home * Remove gate * Keep naming in DebugMod * rm v1 prefs --------- Co-authored-by: Samuel Newman <mozzius@protonmail.com>
Diffstat (limited to 'src/view/com/post-thread/PostThreadItem.tsx')
-rw-r--r-- | src/view/com/post-thread/PostThreadItem.tsx | 1036 |
1 files changed, 0 insertions, 1036 deletions
diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx deleted file mode 100644 index 679a506b9..000000000 --- a/src/view/com/post-thread/PostThreadItem.tsx +++ /dev/null @@ -1,1036 +0,0 @@ -import {memo, useCallback, useMemo, useState} from 'react' -import { - type GestureResponderEvent, - StyleSheet, - Text as RNText, - View, -} from 'react-native' -import { - AppBskyFeedDefs, - AppBskyFeedPost, - type AppBskyFeedThreadgate, - AtUri, - type ModerationDecision, - RichText as RichTextAPI, -} from '@atproto/api' -import {msg, Plural, Trans} from '@lingui/macro' -import {useLingui} from '@lingui/react' - -import {useActorStatus} from '#/lib/actor-status' -import {MAX_POST_LINES} from '#/lib/constants' -import {useOpenComposer} from '#/lib/hooks/useOpenComposer' -import {usePalette} from '#/lib/hooks/usePalette' -import {useTranslate} from '#/lib/hooks/useTranslate' -import {makeProfileLink} from '#/lib/routes/links' -import {sanitizeDisplayName} from '#/lib/strings/display-names' -import {sanitizeHandle} from '#/lib/strings/handles' -import {countLines} from '#/lib/strings/helpers' -import {niceDate} from '#/lib/strings/time' -import {s} from '#/lib/styles' -import {getTranslatorLink, isPostInLanguage} from '#/locale/helpers' -import {logger} from '#/logger' -import { - POST_TOMBSTONE, - type Shadow, - usePostShadow, -} from '#/state/cache/post-shadow' -import {useProfileShadow} from '#/state/cache/profile-shadow' -import {FeedFeedbackProvider, useFeedFeedback} from '#/state/feed-feedback' -import {useLanguagePrefs} from '#/state/preferences' -import {type ThreadPost} from '#/state/queries/post-thread' -import {useSession} from '#/state/session' -import {type OnPostSuccessData} from '#/state/shell/composer' -import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies' -import {type PostSource} from '#/state/unstable-post-source' -import {PostThreadFollowBtn} from '#/view/com/post-thread/PostThreadFollowBtn' -import {ErrorMessage} from '#/view/com/util/error/ErrorMessage' -import {Link} from '#/view/com/util/Link' -import {formatCount} from '#/view/com/util/numeric/format' -import {PostMeta} from '#/view/com/util/PostMeta' -import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar' -import {atoms as a, useTheme} from '#/alf' -import {colors} from '#/components/Admonition' -import {Button} from '#/components/Button' -import {useInteractionState} from '#/components/hooks/useInteractionState' -import {CalendarClock_Stroke2_Corner0_Rounded as CalendarClockIcon} from '#/components/icons/CalendarClock' -import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRightIcon} from '#/components/icons/Chevron' -import {Trash_Stroke2_Corner0_Rounded as TrashIcon} from '#/components/icons/Trash' -import {InlineLinkText} from '#/components/Link' -import {ContentHider} from '#/components/moderation/ContentHider' -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} from '#/components/PostControls' -import * as Prompt from '#/components/Prompt' -import {RichText} from '#/components/RichText' -import {SubtleWebHover} from '#/components/SubtleWebHover' -import {Text} from '#/components/Typography' -import {VerificationCheckButton} from '#/components/verification/VerificationCheckButton' -import {WhoCanReply} from '#/components/WhoCanReply' -import * as bsky from '#/types/bsky' - -export function PostThreadItem({ - post, - record, - moderation, - treeView, - depth, - prevPost, - nextPost, - isHighlightedPost, - hasMore, - showChildReplyLine, - showParentReplyLine, - hasPrecedingItem, - overrideBlur, - onPostReply, - onPostSuccess, - hideTopBorder, - threadgateRecord, - anchorPostSource, -}: { - post: AppBskyFeedDefs.PostView - record: AppBskyFeedPost.Record - moderation: ModerationDecision | undefined - treeView: boolean - depth: number - prevPost: ThreadPost | undefined - nextPost: ThreadPost | undefined - isHighlightedPost?: boolean - hasMore?: boolean - showChildReplyLine?: boolean - showParentReplyLine?: boolean - hasPrecedingItem: boolean - overrideBlur: boolean - onPostReply: (postUri: string | undefined) => void - onPostSuccess?: (data: OnPostSuccessData) => void - hideTopBorder?: boolean - threadgateRecord?: AppBskyFeedThreadgate.Record - anchorPostSource?: PostSource -}) { - const postShadowed = usePostShadow(post) - const richText = useMemo( - () => - new RichTextAPI({ - text: record.text, - facets: record.facets, - }), - [record], - ) - if (postShadowed === POST_TOMBSTONE) { - return <PostThreadItemDeleted hideTopBorder={hideTopBorder} /> - } - if (richText && moderation) { - return ( - <PostThreadItemLoaded - // Safeguard from clobbering per-post state below: - key={postShadowed.uri} - post={postShadowed} - prevPost={prevPost} - nextPost={nextPost} - record={record} - richText={richText} - moderation={moderation} - treeView={treeView} - depth={depth} - isHighlightedPost={isHighlightedPost} - hasMore={hasMore} - showChildReplyLine={showChildReplyLine} - showParentReplyLine={showParentReplyLine} - hasPrecedingItem={hasPrecedingItem} - overrideBlur={overrideBlur} - onPostReply={onPostReply} - onPostSuccess={onPostSuccess} - hideTopBorder={hideTopBorder} - threadgateRecord={threadgateRecord} - anchorPostSource={anchorPostSource} - /> - ) - } - return null -} - -function PostThreadItemDeleted({hideTopBorder}: {hideTopBorder?: boolean}) { - const t = useTheme() - return ( - <View - style={[ - t.atoms.bg, - t.atoms.border_contrast_low, - a.p_xl, - a.pl_lg, - a.flex_row, - a.gap_md, - !hideTopBorder && a.border_t, - ]}> - <TrashIcon style={[t.atoms.text]} /> - <Text style={[t.atoms.text_contrast_medium, a.mt_2xs]}> - <Trans>This post has been deleted.</Trans> - </Text> - </View> - ) -} - -let PostThreadItemLoaded = ({ - post, - record, - richText, - moderation, - treeView, - depth, - prevPost, - nextPost, - isHighlightedPost, - hasMore, - showChildReplyLine, - showParentReplyLine, - hasPrecedingItem, - overrideBlur, - onPostReply, - onPostSuccess, - hideTopBorder, - threadgateRecord, - anchorPostSource, -}: { - post: Shadow<AppBskyFeedDefs.PostView> - record: AppBskyFeedPost.Record - richText: RichTextAPI - moderation: ModerationDecision - treeView: boolean - depth: number - prevPost: ThreadPost | undefined - nextPost: ThreadPost | undefined - isHighlightedPost?: boolean - hasMore?: boolean - showChildReplyLine?: boolean - showParentReplyLine?: boolean - hasPrecedingItem: boolean - overrideBlur: boolean - onPostReply: (postUri: string | undefined) => void - onPostSuccess?: (data: OnPostSuccessData) => void - hideTopBorder?: boolean - threadgateRecord?: AppBskyFeedThreadgate.Record - anchorPostSource?: PostSource -}): React.ReactNode => { - const {currentAccount, hasSession} = useSession() - const feedFeedback = useFeedFeedback(anchorPostSource?.feed, hasSession) - - const t = useTheme() - const pal = usePalette('default') - const {_, i18n} = useLingui() - const langPrefs = useLanguagePrefs() - const {openComposer} = useOpenComposer() - const [limitLines, setLimitLines] = useState( - () => countLines(richText?.text) >= MAX_POST_LINES, - ) - const shadowedPostAuthor = useProfileShadow(post.author) - const rootUri = 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 itemTitle = _(msg`Post by ${post.author.handle}`) - const authorHref = makeProfileLink(post.author) - const authorTitle = post.author.handle - const isThreadAuthor = getThreadAuthor(post, record) === currentAccount?.did - const likesHref = useMemo(() => { - const urip = new AtUri(post.uri) - return makeProfileLink(post.author, 'post', urip.rkey, 'liked-by') - }, [post.uri, post.author]) - const likesTitle = _(msg`Likes on this post`) - const repostsHref = useMemo(() => { - const urip = new AtUri(post.uri) - return makeProfileLink(post.author, 'post', urip.rkey, 'reposted-by') - }, [post.uri, post.author]) - const repostsTitle = _(msg`Reposts of this post`) - const threadgateHiddenReplies = useMergedThreadgateHiddenReplies({ - threadgateRecord, - }) - const additionalPostAlerts: AppModerationCause[] = useMemo(() => { - const isPostHiddenByThreadgate = threadgateHiddenReplies.has(post.uri) - const isControlledByViewer = new AtUri(rootUri).host === currentAccount?.did - return isControlledByViewer && isPostHiddenByThreadgate - ? [ - { - type: 'reply-hidden', - source: {type: 'user', did: currentAccount?.did}, - priority: 6, - }, - ] - : [] - }, [post, currentAccount?.did, threadgateHiddenReplies, rootUri]) - const quotesHref = useMemo(() => { - const urip = new AtUri(post.uri) - return makeProfileLink(post.author, 'post', urip.rkey, 'quotes') - }, [post.uri, post.author]) - const quotesTitle = _(msg`Quotes of this post`) - const onlyFollowersCanReply = !!threadgateRecord?.allow?.find( - rule => rule.$type === 'app.bsky.feed.threadgate#followerRule', - ) - const showFollowButton = - currentAccount?.did !== post.author.did && !onlyFollowersCanReply - - const needsTranslation = useMemo( - () => - Boolean( - langPrefs.primaryLanguage && - !isPostInLanguage(post, [langPrefs.primaryLanguage]), - ), - [post, langPrefs.primaryLanguage], - ) - - const onPressReply = () => { - if (anchorPostSource && isHighlightedPost) { - feedFeedback.sendInteraction({ - item: post.uri, - event: 'app.bsky.feed.defs#interactionReply', - feedContext: anchorPostSource.post.feedContext, - reqId: anchorPostSource.post.reqId, - }) - } - openComposer({ - replyTo: { - uri: post.uri, - cid: post.cid, - text: record.text, - author: post.author, - embed: post.embed, - moderation, - langs: record.langs, - }, - onPost: onPostReply, - onPostSuccess: onPostSuccess, - }) - } - - const onOpenAuthor = () => { - if (anchorPostSource) { - feedFeedback.sendInteraction({ - item: post.uri, - event: 'app.bsky.feed.defs#clickthroughAuthor', - feedContext: anchorPostSource.post.feedContext, - reqId: anchorPostSource.post.reqId, - }) - } - } - - const onOpenEmbed = () => { - if (anchorPostSource) { - feedFeedback.sendInteraction({ - item: post.uri, - event: 'app.bsky.feed.defs#clickthroughEmbed', - feedContext: anchorPostSource.post.feedContext, - reqId: anchorPostSource.post.reqId, - }) - } - } - - const onPressShowMore = useCallback(() => { - setLimitLines(false) - }, [setLimitLines]) - - const {isActive: live} = useActorStatus(post.author) - - const reason = anchorPostSource?.post.reason - const viaRepost = useMemo(() => { - if (AppBskyFeedDefs.isReasonRepost(reason) && reason.uri && reason.cid) { - return { - uri: reason.uri, - cid: reason.cid, - } - } - }, [reason]) - - if (!record) { - return <ErrorMessage message={_(msg`Invalid or unsupported post record`)} /> - } - - if (isHighlightedPost) { - return ( - <> - {rootUri !== post.uri && ( - <View - style={[ - a.pl_lg, - a.flex_row, - a.pb_xs, - {height: a.pt_lg.paddingTop}, - ]}> - <View style={{width: 42}}> - <View - style={[ - styles.replyLine, - a.flex_grow, - {backgroundColor: pal.colors.replyLine}, - ]} - /> - </View> - </View> - )} - - <View - testID={`postThreadItem-by-${post.author.handle}`} - style={[ - a.px_lg, - t.atoms.border_contrast_low, - // root post styles - rootUri === post.uri && [a.pt_lg], - ]}> - <View style={[a.flex_row, a.gap_md, a.pb_md]}> - <PreviewableUserAvatar - size={42} - profile={post.author} - moderation={moderation.ui('avatar')} - type={post.author.associated?.labeler ? 'labeler' : 'user'} - live={live} - onBeforePress={onOpenAuthor} - /> - <View style={[a.flex_1]}> - <View style={[a.flex_row, a.align_center]}> - <Link - style={[a.flex_shrink]} - href={authorHref} - title={authorTitle} - onBeforePress={onOpenAuthor}> - <Text - emoji - style={[ - a.text_lg, - a.font_bold, - a.leading_snug, - a.self_start, - ]} - numberOfLines={1}> - {sanitizeDisplayName( - post.author.displayName || - sanitizeHandle(post.author.handle), - moderation.ui('displayName'), - )} - </Text> - </Link> - - <View style={[{paddingLeft: 3, top: -1}]}> - <VerificationCheckButton - profile={shadowedPostAuthor} - size="md" - /> - </View> - </View> - <Link style={s.flex1} href={authorHref} title={authorTitle}> - <Text - emoji - style={[ - a.text_md, - a.leading_snug, - t.atoms.text_contrast_medium, - ]} - numberOfLines={1}> - {sanitizeHandle(post.author.handle, '@')} - </Text> - </Link> - </View> - {showFollowButton && ( - <View> - <PostThreadFollowBtn did={post.author.did} /> - </View> - )} - </View> - <View style={[a.pb_sm]}> - <LabelsOnMyPost post={post} style={[a.pb_sm]} /> - <ContentHider - modui={moderation.ui('contentView')} - ignoreMute - childContainerStyle={[a.pt_sm]}> - <PostAlerts - modui={moderation.ui('contentView')} - size="lg" - includeMute - style={[a.pb_sm]} - additionalCauses={additionalPostAlerts} - /> - {richText?.text ? ( - <RichText - enableTags - selectable - value={richText} - style={[a.flex_1, a.text_xl]} - authorHandle={post.author.handle} - shouldProxyLinks={true} - /> - ) : undefined} - {post.embed && ( - <View style={[a.py_xs]}> - <Embed - embed={post.embed} - moderation={moderation} - viewContext={PostEmbedViewContext.ThreadHighlighted} - onOpen={onOpenEmbed} - /> - </View> - )} - </ContentHider> - <ExpandedPostDetails - post={post} - record={record} - isThreadAuthor={isThreadAuthor} - needsTranslation={needsTranslation} - /> - {post.repostCount !== 0 || - post.likeCount !== 0 || - post.quoteCount !== 0 ? ( - // Show this section unless we're *sure* it has no engagement. - <View - style={[ - a.flex_row, - a.align_center, - a.gap_lg, - a.border_t, - a.border_b, - a.mt_md, - a.py_md, - t.atoms.border_contrast_low, - ]}> - {post.repostCount != null && post.repostCount !== 0 ? ( - <Link href={repostsHref} title={repostsTitle}> - <Text - testID="repostCount-expanded" - style={[a.text_md, t.atoms.text_contrast_medium]}> - <Text style={[a.text_md, a.font_bold, t.atoms.text]}> - {formatCount(i18n, post.repostCount)} - </Text>{' '} - <Plural - value={post.repostCount} - one="repost" - other="reposts" - /> - </Text> - </Link> - ) : null} - {post.quoteCount != null && - post.quoteCount !== 0 && - !post.viewer?.embeddingDisabled ? ( - <Link href={quotesHref} title={quotesTitle}> - <Text - testID="quoteCount-expanded" - style={[a.text_md, t.atoms.text_contrast_medium]}> - <Text style={[a.text_md, a.font_bold, t.atoms.text]}> - {formatCount(i18n, post.quoteCount)} - </Text>{' '} - <Plural - value={post.quoteCount} - one="quote" - other="quotes" - /> - </Text> - </Link> - ) : null} - {post.likeCount != null && post.likeCount !== 0 ? ( - <Link href={likesHref} title={likesTitle}> - <Text - testID="likeCount-expanded" - style={[a.text_md, t.atoms.text_contrast_medium]}> - <Text style={[a.text_md, a.font_bold, t.atoms.text]}> - {formatCount(i18n, post.likeCount)} - </Text>{' '} - <Plural value={post.likeCount} one="like" other="likes" /> - </Text> - </Link> - ) : null} - </View> - ) : null} - <View - style={[ - a.pt_sm, - a.pb_2xs, - { - marginLeft: -5, - }, - ]}> - <FeedFeedbackProvider value={feedFeedback}> - <PostControls - big - post={post} - record={record} - richText={richText} - onPressReply={onPressReply} - onPostReply={onPostReply} - logContext="PostThreadItem" - threadgateRecord={threadgateRecord} - feedContext={anchorPostSource?.post?.feedContext} - reqId={anchorPostSource?.post?.reqId} - viaRepost={viaRepost} - /> - </FeedFeedbackProvider> - </View> - </View> - </View> - </> - ) - } else { - const isThreadedChild = treeView && depth > 0 - const isThreadedChildAdjacentTop = - isThreadedChild && prevPost?.ctx.depth === depth && depth !== 1 - const isThreadedChildAdjacentBot = - isThreadedChild && nextPost?.ctx.depth === depth - return ( - <PostOuterWrapper - post={post} - depth={depth} - showParentReplyLine={!!showParentReplyLine} - treeView={treeView} - hasPrecedingItem={hasPrecedingItem} - hideTopBorder={hideTopBorder}> - <PostHider - testID={`postThreadItem-by-${post.author.handle}`} - href={postHref} - disabled={overrideBlur} - modui={moderation.ui('contentList')} - iconSize={isThreadedChild ? 24 : 42} - iconStyles={ - isThreadedChild ? {marginRight: 4} : {marginLeft: 2, marginRight: 2} - } - profile={post.author} - interpretFilterAsBlur> - <View - style={{ - flexDirection: 'row', - gap: 10, - paddingLeft: 8, - height: isThreadedChildAdjacentTop ? 8 : 16, - }}> - <View style={{width: 42}}> - {!isThreadedChild && showParentReplyLine && ( - <View - style={[ - styles.replyLine, - { - flexGrow: 1, - backgroundColor: pal.colors.replyLine, - marginBottom: 4, - }, - ]} - /> - )} - </View> - </View> - - <View - style={[ - a.flex_row, - a.px_sm, - a.gap_md, - { - paddingBottom: - showChildReplyLine && !isThreadedChild - ? 0 - : isThreadedChildAdjacentBot - ? 4 - : 8, - }, - ]}> - {/* If we are in threaded mode, the avatar is rendered in PostMeta */} - {!isThreadedChild && ( - <View> - <PreviewableUserAvatar - size={42} - profile={post.author} - moderation={moderation.ui('avatar')} - type={post.author.associated?.labeler ? 'labeler' : 'user'} - live={live} - /> - - {showChildReplyLine && ( - <View - style={[ - styles.replyLine, - { - flexGrow: 1, - backgroundColor: pal.colors.replyLine, - marginTop: 4, - }, - ]} - /> - )} - </View> - )} - - <View style={[a.flex_1]}> - <PostMeta - author={post.author} - moderation={moderation} - timestamp={post.indexedAt} - postHref={postHref} - showAvatar={isThreadedChild} - avatarSize={24} - style={[a.pb_xs]} - /> - <LabelsOnMyPost post={post} style={[a.pb_xs]} /> - <PostAlerts - modui={moderation.ui('contentList')} - style={[a.pb_2xs]} - additionalCauses={additionalPostAlerts} - /> - {richText?.text ? ( - <View style={[a.pb_2xs, a.pr_sm]}> - <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} - /> - )} - </View> - ) : undefined} - {post.embed && ( - <View style={[a.pb_xs]}> - <Embed - embed={post.embed} - moderation={moderation} - viewContext={PostEmbedViewContext.Feed} - /> - </View> - )} - <PostControls - post={post} - record={record} - richText={richText} - onPressReply={onPressReply} - logContext="PostThreadItem" - threadgateRecord={threadgateRecord} - /> - </View> - </View> - {hasMore ? ( - <Link - style={[ - styles.loadMore, - { - paddingLeft: treeView ? 8 : 70, - paddingTop: 0, - paddingBottom: treeView ? 4 : 12, - }, - ]} - href={postHref} - title={itemTitle} - noFeedback> - <Text - style={[t.atoms.text_contrast_medium, a.font_bold, a.text_sm]}> - <Trans>More</Trans> - </Text> - <ChevronRightIcon - size="xs" - style={[t.atoms.text_contrast_medium]} - /> - </Link> - ) : undefined} - </PostHider> - </PostOuterWrapper> - ) - } -} -PostThreadItemLoaded = memo(PostThreadItemLoaded) - -function PostOuterWrapper({ - post, - treeView, - depth, - showParentReplyLine, - hasPrecedingItem, - hideTopBorder, - children, -}: React.PropsWithChildren<{ - post: AppBskyFeedDefs.PostView - treeView: boolean - depth: number - showParentReplyLine: boolean - hasPrecedingItem: boolean - hideTopBorder?: boolean -}>) { - const t = useTheme() - const { - state: hover, - onIn: onHoverIn, - onOut: onHoverOut, - } = useInteractionState() - if (treeView && depth > 0) { - return ( - <View - style={[ - a.flex_row, - a.px_sm, - a.flex_row, - t.atoms.border_contrast_low, - styles.cursor, - depth === 1 && a.border_t, - ]} - onPointerEnter={onHoverIn} - onPointerLeave={onHoverOut}> - {Array.from(Array(depth - 1)).map((_, n: number) => ( - <View - key={`${post.uri}-padding-${n}`} - style={[ - a.ml_sm, - t.atoms.border_contrast_low, - { - borderLeftWidth: 2, - paddingLeft: a.pl_sm.paddingLeft - 2, // minus border - }, - ]} - /> - ))} - <View style={a.flex_1}> - <SubtleWebHover - hover={hover} - style={{ - left: (depth === 1 ? 0 : 2) - a.pl_sm.paddingLeft, - right: -a.pr_sm.paddingRight, - }} - /> - {children} - </View> - </View> - ) - } - return ( - <View - onPointerEnter={onHoverIn} - onPointerLeave={onHoverOut} - style={[ - a.border_t, - a.px_sm, - t.atoms.border_contrast_low, - showParentReplyLine && hasPrecedingItem && styles.noTopBorder, - hideTopBorder && styles.noTopBorder, - styles.cursor, - ]}> - <SubtleWebHover hover={hover} /> - {children} - </View> - ) -} - -function ExpandedPostDetails({ - post, - record, - isThreadAuthor, - needsTranslation, -}: { - post: AppBskyFeedDefs.PostView - record: AppBskyFeedPost.Record - isThreadAuthor: boolean - needsTranslation: boolean -}) { - const t = useTheme() - const pal = usePalette('default') - const {_, i18n} = useLingui() - const translate = useTranslate() - const isRootPost = !('reply' in post.record) - const langPrefs = useLanguagePrefs() - - const onTranslatePress = useCallback( - (e: GestureResponderEvent) => { - e.preventDefault() - translate(record.text || '', langPrefs.primaryLanguage) - - if ( - bsky.dangerousIsType<AppBskyFeedPost.Record>( - post.record, - AppBskyFeedPost.isRecord, - ) - ) { - logger.metric( - 'translate', - { - sourceLanguages: post.record.langs ?? [], - targetLanguage: langPrefs.primaryLanguage, - textLength: post.record.text.length, - }, - {statsig: false}, - ) - } - - return false - }, - [translate, record.text, langPrefs, post], - ) - - return ( - <View style={[a.gap_md, a.pt_md, a.align_start]}> - <BackdatedPostIndicator post={post} /> - <View style={[a.flex_row, a.align_center, a.flex_wrap, a.gap_sm]}> - <Text style={[a.text_sm, t.atoms.text_contrast_medium]}> - {niceDate(i18n, post.indexedAt)} - </Text> - {isRootPost && ( - <WhoCanReply post={post} isThreadAuthor={isThreadAuthor} /> - )} - {needsTranslation && ( - <> - <Text style={[a.text_sm, t.atoms.text_contrast_medium]}> - · - </Text> - - <InlineLinkText - // overridden to open an intent on android, but keep - // as anchor tag for accessibility - to={getTranslatorLink(record.text, langPrefs.primaryLanguage)} - label={_(msg`Translate`)} - style={[a.text_sm, pal.link]} - onPress={onTranslatePress}> - <Trans>Translate</Trans> - </InlineLinkText> - </> - )} - </View> - </View> - ) -} - -function BackdatedPostIndicator({post}: {post: AppBskyFeedDefs.PostView}) { - const t = useTheme() - const {_, i18n} = useLingui() - const control = Prompt.usePromptControl() - - const indexedAt = new Date(post.indexedAt) - const createdAt = bsky.dangerousIsType<AppBskyFeedPost.Record>( - post.record, - AppBskyFeedPost.isRecord, - ) - ? new Date(post.record.createdAt) - : new Date(post.indexedAt) - - // backdated if createdAt is 24 hours or more before indexedAt - const isBackdated = - indexedAt.getTime() - createdAt.getTime() > 24 * 60 * 60 * 1000 - - if (!isBackdated) return null - - const orange = t.name === 'light' ? colors.warning.dark : colors.warning.light - - return ( - <> - <Button - label={_(msg`Archived post`)} - accessibilityHint={_( - msg`Shows information about when this post was created`, - )} - onPress={e => { - e.preventDefault() - e.stopPropagation() - control.open() - }}> - {({hovered, pressed}) => ( - <View - style={[ - a.flex_row, - a.align_center, - a.rounded_full, - t.atoms.bg_contrast_25, - (hovered || pressed) && t.atoms.bg_contrast_50, - { - gap: 3, - paddingHorizontal: 6, - paddingVertical: 3, - }, - ]}> - <CalendarClockIcon fill={orange} size="sm" aria-hidden /> - <Text - style={[ - a.text_xs, - a.font_bold, - a.leading_tight, - t.atoms.text_contrast_medium, - ]}> - <Trans>Archived from {niceDate(i18n, createdAt)}</Trans> - </Text> - </View> - )} - </Button> - - <Prompt.Outer control={control}> - <Prompt.TitleText> - <Trans>Archived post</Trans> - </Prompt.TitleText> - <Prompt.DescriptionText> - <Trans> - This post claims to have been created on{' '} - <RNText style={[a.font_bold]}>{niceDate(i18n, createdAt)}</RNText>, - but was first seen by Bluesky on{' '} - <RNText style={[a.font_bold]}>{niceDate(i18n, indexedAt)}</RNText>. - </Trans> - </Prompt.DescriptionText> - <Text - style={[ - a.text_md, - a.leading_snug, - t.atoms.text_contrast_high, - a.pb_xl, - ]}> - <Trans> - Bluesky cannot confirm the authenticity of the claimed date. - </Trans> - </Text> - <Prompt.Actions> - <Prompt.Action cta={_(msg`Okay`)} onPress={() => {}} /> - </Prompt.Actions> - </Prompt.Outer> - </> - ) -} - -function getThreadAuthor( - post: AppBskyFeedDefs.PostView, - record: AppBskyFeedPost.Record, -): string { - if (!record.reply) { - return post.author.did - } - try { - return new AtUri(record.reply.root.uri).host - } catch { - return '' - } -} - -const styles = StyleSheet.create({ - outer: { - borderTopWidth: StyleSheet.hairlineWidth, - paddingLeft: 8, - }, - noTopBorder: { - borderTopWidth: 0, - }, - meta: { - flexDirection: 'row', - paddingVertical: 2, - }, - metaExpandedLine1: { - paddingVertical: 0, - }, - loadMore: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'flex-start', - gap: 4, - paddingHorizontal: 20, - }, - replyLine: { - width: 2, - marginLeft: 'auto', - marginRight: 'auto', - }, - cursor: { - // @ts-ignore web only - cursor: 'pointer', - }, -}) |