From df20ae237eaf434c6ed0fd032f8328cd9b8c352c Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Tue, 26 Aug 2025 09:54:19 -0500 Subject: 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 --- src/view/com/post-thread/PostThreadItem.tsx | 1036 --------------------------- 1 file changed, 1036 deletions(-) delete mode 100644 src/view/com/post-thread/PostThreadItem.tsx (limited to 'src/view/com/post-thread/PostThreadItem.tsx') 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 - } - if (richText && moderation) { - return ( - - ) - } - return null -} - -function PostThreadItemDeleted({hideTopBorder}: {hideTopBorder?: boolean}) { - const t = useTheme() - return ( - - - - This post has been deleted. - - - ) -} - -let PostThreadItemLoaded = ({ - post, - record, - richText, - moderation, - treeView, - depth, - prevPost, - nextPost, - isHighlightedPost, - hasMore, - showChildReplyLine, - showParentReplyLine, - hasPrecedingItem, - overrideBlur, - onPostReply, - onPostSuccess, - hideTopBorder, - threadgateRecord, - anchorPostSource, -}: { - post: Shadow - 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 - } - - if (isHighlightedPost) { - return ( - <> - {rootUri !== post.uri && ( - - - - - - )} - - - - - - - - - {sanitizeDisplayName( - post.author.displayName || - sanitizeHandle(post.author.handle), - moderation.ui('displayName'), - )} - - - - - - - - - - {sanitizeHandle(post.author.handle, '@')} - - - - {showFollowButton && ( - - - - )} - - - - - - {richText?.text ? ( - - ) : undefined} - {post.embed && ( - - - - )} - - - {post.repostCount !== 0 || - post.likeCount !== 0 || - post.quoteCount !== 0 ? ( - // Show this section unless we're *sure* it has no engagement. - - {post.repostCount != null && post.repostCount !== 0 ? ( - - - - {formatCount(i18n, post.repostCount)} - {' '} - - - - ) : null} - {post.quoteCount != null && - post.quoteCount !== 0 && - !post.viewer?.embeddingDisabled ? ( - - - - {formatCount(i18n, post.quoteCount)} - {' '} - - - - ) : null} - {post.likeCount != null && post.likeCount !== 0 ? ( - - - - {formatCount(i18n, post.likeCount)} - {' '} - - - - ) : null} - - ) : null} - - - - - - - - - ) - } else { - const isThreadedChild = treeView && depth > 0 - const isThreadedChildAdjacentTop = - isThreadedChild && prevPost?.ctx.depth === depth && depth !== 1 - const isThreadedChildAdjacentBot = - isThreadedChild && nextPost?.ctx.depth === depth - return ( - - - - - {!isThreadedChild && showParentReplyLine && ( - - )} - - - - - {/* If we are in threaded mode, the avatar is rendered in PostMeta */} - {!isThreadedChild && ( - - - - {showChildReplyLine && ( - - )} - - )} - - - - - - {richText?.text ? ( - - - {limitLines && ( - - )} - - ) : undefined} - {post.embed && ( - - - - )} - - - - {hasMore ? ( - - - More - - - - ) : undefined} - - - ) - } -} -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 ( - - {Array.from(Array(depth - 1)).map((_, n: number) => ( - - ))} - - - {children} - - - ) - } - return ( - - - {children} - - ) -} - -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( - 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 ( - - - - - {niceDate(i18n, post.indexedAt)} - - {isRootPost && ( - - )} - {needsTranslation && ( - <> - - · - - - - Translate - - - )} - - - ) -} - -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( - 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 ( - <> - - - - - Archived post - - - - This post claims to have been created on{' '} - {niceDate(i18n, createdAt)}, - but was first seen by Bluesky on{' '} - {niceDate(i18n, indexedAt)}. - - - - - Bluesky cannot confirm the authenticity of the claimed date. - - - - {}} /> - - - - ) -} - -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', - }, -}) -- cgit 1.4.1