From 0b50e9f10995d78b71327bea30d9d7b1a0d10f61 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Mon, 13 Oct 2025 18:36:11 -0500 Subject: [PATCH 1/2] Remove remaining usages of old post thread query (#9184) * Remove remaining usages of old post thread query * Add PostThreadContext, cache mutator for threadgates on threads, pipe it through * Replace getPostThread in threadgate query * Replace in initQuote handling, which isn't even used rn... * Missing import * Revert ext change --- .../dialogs/PostInteractionSettingsDialog.tsx | 19 +- src/screens/Post/PostLikedBy.tsx | 8 +- src/screens/Post/PostQuotes.tsx | 8 +- src/screens/Post/PostRepostedBy.tsx | 8 +- src/screens/PostThread/index.tsx | 10 +- src/state/cache/post-shadow.ts | 6 - src/state/cache/profile-shadow.ts | 2 - src/state/queries/post-thread.ts | 631 ------------------ src/state/queries/threadgate/index.ts | 76 +-- src/state/queries/usePostThread/context.tsx | 43 ++ src/state/queries/usePostThread/index.ts | 39 +- src/state/queries/usePostThread/queryCache.ts | 52 +- src/view/com/composer/Composer.tsx | 19 +- 13 files changed, 191 insertions(+), 730 deletions(-) delete mode 100644 src/state/queries/post-thread.ts create mode 100644 src/state/queries/usePostThread/context.tsx diff --git a/src/components/dialogs/PostInteractionSettingsDialog.tsx b/src/components/dialogs/PostInteractionSettingsDialog.tsx index 01194ef65..5b9fc262d 100644 --- a/src/components/dialogs/PostInteractionSettingsDialog.tsx +++ b/src/components/dialogs/PostInteractionSettingsDialog.tsx @@ -13,6 +13,7 @@ import isEqual from 'lodash.isequal' import {logger} from '#/logger' import {STALE} from '#/state/queries' import {useMyListsQuery} from '#/state/queries/my-lists' +import {useGetPost} from '#/state/queries/post' import { createPostgateQueryKey, getPostgateRecord, @@ -25,12 +26,15 @@ import { } from '#/state/queries/postgate/util' import { createThreadgateViewQueryKey, - getThreadgateView, type ThreadgateAllowUISetting, threadgateViewToAllowUISetting, useSetThreadgateAllowMutation, useThreadgateViewQuery, } from '#/state/queries/threadgate' +import { + PostThreadContextProvider, + usePostThreadContext, +} from '#/state/queries/usePostThread' import {useAgent, useSession} from '#/state/session' import * as Toast from '#/view/com/util/Toast' import {atoms as a, useTheme} from '#/alf' @@ -133,10 +137,13 @@ export type PostInteractionSettingsDialogProps = { export function PostInteractionSettingsDialog( props: PostInteractionSettingsDialogProps, ) { + const postThreadContext = usePostThreadContext() return ( - + + + ) } @@ -558,6 +565,7 @@ export function usePrefetchPostInteractionSettings({ }) { const queryClient = useQueryClient() const agent = useAgent() + const getPost = useGetPost() return React.useCallback(async () => { try { @@ -570,7 +578,10 @@ export function usePrefetchPostInteractionSettings({ }), queryClient.prefetchQuery({ queryKey: createThreadgateViewQueryKey(rootPostUri), - queryFn: () => getThreadgateView({agent, postUri: rootPostUri}), + queryFn: async () => { + const post = await getPost({uri: rootPostUri}) + return post.threadgate ?? null + }, staleTime: STALE.SECONDS.THIRTY, }), ]) @@ -579,5 +590,5 @@ export function usePrefetchPostInteractionSettings({ safeMessage: e.message, }) } - }, [queryClient, agent, postUri, rootPostUri]) + }, [queryClient, agent, postUri, rootPostUri, getPost]) } diff --git a/src/screens/Post/PostLikedBy.tsx b/src/screens/Post/PostLikedBy.tsx index e1a482452..b8a230246 100644 --- a/src/screens/Post/PostLikedBy.tsx +++ b/src/screens/Post/PostLikedBy.tsx @@ -7,7 +7,7 @@ import { type NativeStackScreenProps, } from '#/lib/routes/types' import {makeRecordUri} from '#/lib/strings/url-helpers' -import {usePostThreadQuery} from '#/state/queries/post-thread' +import {usePostQuery} from '#/state/queries/post' import {useSetMinimalShellMode} from '#/state/shell' import {PostLikedBy as PostLikedByComponent} from '#/view/com/post-thread/PostLikedBy' import * as Layout from '#/components/Layout' @@ -17,11 +17,11 @@ export const PostLikedByScreen = ({route}: Props) => { const setMinimalShellMode = useSetMinimalShellMode() const {name, rkey} = route.params const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey) - const {data: post} = usePostThreadQuery(uri) + const {data: post} = usePostQuery(uri) let likeCount - if (post?.thread.type === 'post') { - likeCount = post.thread.post.likeCount + if (post) { + likeCount = post.likeCount } useFocusEffect( diff --git a/src/screens/Post/PostQuotes.tsx b/src/screens/Post/PostQuotes.tsx index 3fa5d8027..33ebe94f3 100644 --- a/src/screens/Post/PostQuotes.tsx +++ b/src/screens/Post/PostQuotes.tsx @@ -7,7 +7,7 @@ import { type NativeStackScreenProps, } from '#/lib/routes/types' import {makeRecordUri} from '#/lib/strings/url-helpers' -import {usePostThreadQuery} from '#/state/queries/post-thread' +import {usePostQuery} from '#/state/queries/post' import {useSetMinimalShellMode} from '#/state/shell' import {PostQuotes as PostQuotesComponent} from '#/view/com/post-thread/PostQuotes' import * as Layout from '#/components/Layout' @@ -17,11 +17,11 @@ export const PostQuotesScreen = ({route}: Props) => { const setMinimalShellMode = useSetMinimalShellMode() const {name, rkey} = route.params const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey) - const {data: post} = usePostThreadQuery(uri) + const {data: post} = usePostQuery(uri) let quoteCount - if (post?.thread.type === 'post') { - quoteCount = post.thread.post.quoteCount + if (post) { + quoteCount = post.quoteCount } useFocusEffect( diff --git a/src/screens/Post/PostRepostedBy.tsx b/src/screens/Post/PostRepostedBy.tsx index d79321d0a..37f31ce22 100644 --- a/src/screens/Post/PostRepostedBy.tsx +++ b/src/screens/Post/PostRepostedBy.tsx @@ -7,7 +7,7 @@ import { type NativeStackScreenProps, } from '#/lib/routes/types' import {makeRecordUri} from '#/lib/strings/url-helpers' -import {usePostThreadQuery} from '#/state/queries/post-thread' +import {usePostQuery} from '#/state/queries/post' import {useSetMinimalShellMode} from '#/state/shell' import {PostRepostedBy as PostRepostedByComponent} from '#/view/com/post-thread/PostRepostedBy' import * as Layout from '#/components/Layout' @@ -17,11 +17,11 @@ export const PostRepostedByScreen = ({route}: Props) => { const {name, rkey} = route.params const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey) const setMinimalShellMode = useSetMinimalShellMode() - const {data: post} = usePostThreadQuery(uri) + const {data: post} = usePostQuery(uri) let quoteCount - if (post?.thread.type === 'post') { - quoteCount = post.thread.post.repostCount + if (post) { + quoteCount = post.repostCount } useFocusEffect( diff --git a/src/screens/PostThread/index.tsx b/src/screens/PostThread/index.tsx index 7918f4474..64a6f0f29 100644 --- a/src/screens/PostThread/index.tsx +++ b/src/screens/PostThread/index.tsx @@ -7,7 +7,11 @@ import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender' import {useOpenComposer} from '#/lib/hooks/useOpenComposer' import {useFeedFeedback} from '#/state/feed-feedback' import {type ThreadViewOption} from '#/state/queries/preferences/useThreadPreferences' -import {type ThreadItem, usePostThread} from '#/state/queries/usePostThread' +import { + PostThreadContextProvider, + type ThreadItem, + usePostThread, +} from '#/state/queries/usePostThread' import {useSession} from '#/state/session' import {type OnPostSuccessData} from '#/state/shell/composer' import {useShellLayout} from '#/state/shell/shell-layout' @@ -492,7 +496,7 @@ export function PostThread({uri}: {uri: string}) { const defaultListFooterHeight = hasParents ? windowHeight - 200 : undefined return ( - <> + @@ -575,7 +579,7 @@ export function PostThread({uri}: {uri: string}) { {!gtMobile && canReply && hasSession && ( )} - + ) } diff --git a/src/state/cache/post-shadow.ts b/src/state/cache/post-shadow.ts index 312bc6df8..05a702cd5 100644 --- a/src/state/cache/post-shadow.ts +++ b/src/state/cache/post-shadow.ts @@ -12,7 +12,6 @@ import {findAllPostsInQueryData as findAllPostsInExploreFeedPreviewsQueryData} f import {findAllPostsInQueryData as findAllPostsInNotifsQueryData} from '#/state/queries/notifications/feed' import {findAllPostsInQueryData as findAllPostsInFeedQueryData} from '#/state/queries/post-feed' import {findAllPostsInQueryData as findAllPostsInQuoteQueryData} from '#/state/queries/post-quotes' -import {findAllPostsInQueryData as findAllPostsInThreadQueryData} from '#/state/queries/post-thread' import {findAllPostsInQueryData as findAllPostsInSearchQueryData} from '#/state/queries/search-posts' import {findAllPostsInQueryData as findAllPostsInThreadV2QueryData} from '#/state/queries/usePostThread/queryCache' import {castAsShadow, type Shadow} from './types' @@ -176,11 +175,6 @@ function* findPostsInCache( for (let post of findAllPostsInNotifsQueryData(queryClient, uri)) { yield post } - for (let node of findAllPostsInThreadQueryData(queryClient, uri)) { - if (node.type === 'post') { - yield node.post - } - } for (let post of findAllPostsInThreadV2QueryData(queryClient, uri)) { yield post } diff --git a/src/state/cache/profile-shadow.ts b/src/state/cache/profile-shadow.ts index 1489e65fd..168661e0d 100644 --- a/src/state/cache/profile-shadow.ts +++ b/src/state/cache/profile-shadow.ts @@ -16,7 +16,6 @@ import {findAllProfilesInQueryData as findAllProfilesInFeedsQueryData} from '#/s import {findAllProfilesInQueryData as findAllProfilesInPostLikedByQueryData} from '#/state/queries/post-liked-by' import {findAllProfilesInQueryData as findAllProfilesInPostQuotesQueryData} from '#/state/queries/post-quotes' import {findAllProfilesInQueryData as findAllProfilesInPostRepostedByQueryData} from '#/state/queries/post-reposted-by' -import {findAllProfilesInQueryData as findAllProfilesInPostThreadQueryData} from '#/state/queries/post-thread' import {findAllProfilesInQueryData as findAllProfilesInProfileQueryData} from '#/state/queries/profile' import {findAllProfilesInQueryData as findAllProfilesInProfileFollowersQueryData} from '#/state/queries/profile-followers' import {findAllProfilesInQueryData as findAllProfilesInProfileFollowsQueryData} from '#/state/queries/profile-follows' @@ -173,7 +172,6 @@ function* findProfilesInCache( yield* findAllProfilesInActorSearchQueryData(queryClient, did) yield* findAllProfilesInListConvosQueryData(queryClient, did) yield* findAllProfilesInFeedsQueryData(queryClient, did) - yield* findAllProfilesInPostThreadQueryData(queryClient, did) yield* findAllProfilesInPostThreadV2QueryData(queryClient, did) yield* findAllProfilesInKnownFollowersQueryData(queryClient, did) yield* findAllProfilesInExploreFeedPreviewsQueryData(queryClient, did) diff --git a/src/state/queries/post-thread.ts b/src/state/queries/post-thread.ts deleted file mode 100644 index 551fedc8b..000000000 --- a/src/state/queries/post-thread.ts +++ /dev/null @@ -1,631 +0,0 @@ -import { - type AppBskyActorDefs, - type AppBskyEmbedRecord, - AppBskyFeedDefs, - type AppBskyFeedGetPostThread, - AppBskyFeedPost, - AtUri, - moderatePost, - type ModerationDecision, - type ModerationOpts, -} from '@atproto/api' -import {type QueryClient, useQuery, useQueryClient} from '@tanstack/react-query' - -import { - findAllPostsInQueryData as findAllPostsInExploreFeedPreviewsQueryData, - findAllProfilesInQueryData as findAllProfilesInExploreFeedPreviewsQueryData, -} from '#/state/queries/explore-feed-previews' -import {findAllPostsInQueryData as findAllPostsInQuoteQueryData} from '#/state/queries/post-quotes' -import {type UsePreferencesQueryResponse} from '#/state/queries/preferences/types' -import { - findAllPostsInQueryData as findAllPostsInSearchQueryData, - findAllProfilesInQueryData as findAllProfilesInSearchQueryData, -} from '#/state/queries/search-posts' -import {useAgent} from '#/state/session' -import * as bsky from '#/types/bsky' -import { - findAllPostsInQueryData as findAllPostsInNotifsQueryData, - findAllProfilesInQueryData as findAllProfilesInNotifsQueryData, -} from './notifications/feed' -import { - findAllPostsInQueryData as findAllPostsInFeedQueryData, - findAllProfilesInQueryData as findAllProfilesInFeedQueryData, -} from './post-feed' -import { - didOrHandleUriMatches, - embedViewRecordToPostView, - getEmbeddedPost, -} from './util' - -const REPLY_TREE_DEPTH = 10 -export const RQKEY_ROOT = 'post-thread' -export const RQKEY = (uri: string) => [RQKEY_ROOT, uri] -type ThreadViewNode = AppBskyFeedGetPostThread.OutputSchema['thread'] - -export interface ThreadCtx { - depth: number - isHighlightedPost?: boolean - hasMore?: boolean - isParentLoading?: boolean - isChildLoading?: boolean - isSelfThread?: boolean - hasMoreSelfThread?: boolean -} - -export type ThreadPost = { - type: 'post' - _reactKey: string - uri: string - post: AppBskyFeedDefs.PostView - record: AppBskyFeedPost.Record - parent: ThreadNode | undefined - replies: ThreadNode[] | undefined - hasOPLike: boolean | undefined - ctx: ThreadCtx -} - -export type ThreadNotFound = { - type: 'not-found' - _reactKey: string - uri: string - ctx: ThreadCtx -} - -export type ThreadBlocked = { - type: 'blocked' - _reactKey: string - uri: string - ctx: ThreadCtx -} - -export type ThreadUnknown = { - type: 'unknown' - uri: string -} - -export type ThreadNode = - | ThreadPost - | ThreadNotFound - | ThreadBlocked - | ThreadUnknown - -export type ThreadModerationCache = WeakMap - -export type PostThreadQueryData = { - thread: ThreadNode - threadgate?: AppBskyFeedDefs.ThreadgateView -} - -export function usePostThreadQuery(uri: string | undefined) { - const queryClient = useQueryClient() - const agent = useAgent() - return useQuery({ - gcTime: 0, - queryKey: RQKEY(uri || ''), - async queryFn() { - const res = await agent.getPostThread({ - uri: uri!, - depth: REPLY_TREE_DEPTH, - }) - if (res.success) { - const thread = responseToThreadNodes(res.data.thread) - annotateSelfThread(thread) - return { - thread, - threadgate: res.data.threadgate as - | AppBskyFeedDefs.ThreadgateView - | undefined, - } - } - return {thread: {type: 'unknown', uri: uri!}} - }, - enabled: !!uri, - placeholderData: () => { - if (!uri) return - const post = findPostInQueryData(queryClient, uri) - if (post) { - return {thread: post} - } - return undefined - }, - }) -} - -export function fillThreadModerationCache( - cache: ThreadModerationCache, - node: ThreadNode, - moderationOpts: ModerationOpts, -) { - if (node.type === 'post') { - cache.set(node, moderatePost(node.post, moderationOpts)) - if (node.parent) { - fillThreadModerationCache(cache, node.parent, moderationOpts) - } - if (node.replies) { - for (const reply of node.replies) { - fillThreadModerationCache(cache, reply, moderationOpts) - } - } - } -} - -export function sortThread( - node: ThreadNode, - opts: UsePreferencesQueryResponse['threadViewPrefs'], - modCache: ThreadModerationCache, - currentDid: string | undefined, - justPostedUris: Set, - threadgateRecordHiddenReplies: Set, - fetchedAtCache: Map, - fetchedAt: number, - randomCache: Map, -): ThreadNode { - if (node.type !== 'post') { - return node - } - if (node.replies) { - node.replies.sort((a: ThreadNode, b: ThreadNode) => { - if (a.type !== 'post') { - return 1 - } - if (b.type !== 'post') { - return -1 - } - - if (node.ctx.isHighlightedPost || opts.lab_treeViewEnabled) { - const aIsJustPosted = - a.post.author.did === currentDid && justPostedUris.has(a.post.uri) - const bIsJustPosted = - b.post.author.did === currentDid && justPostedUris.has(b.post.uri) - if (aIsJustPosted && bIsJustPosted) { - return a.post.indexedAt.localeCompare(b.post.indexedAt) // oldest - } else if (aIsJustPosted) { - return -1 // reply while onscreen - } else if (bIsJustPosted) { - return 1 // reply while onscreen - } - } - - const aIsByOp = a.post.author.did === node.post?.author.did - const bIsByOp = b.post.author.did === node.post?.author.did - if (aIsByOp && bIsByOp) { - return a.post.indexedAt.localeCompare(b.post.indexedAt) // oldest - } else if (aIsByOp) { - return -1 // op's own reply - } else if (bIsByOp) { - return 1 // op's own reply - } - - const aIsBySelf = a.post.author.did === currentDid - const bIsBySelf = b.post.author.did === currentDid - if (aIsBySelf && bIsBySelf) { - return a.post.indexedAt.localeCompare(b.post.indexedAt) // oldest - } else if (aIsBySelf) { - return -1 // current account's reply - } else if (bIsBySelf) { - return 1 // current account's reply - } - - const aHidden = threadgateRecordHiddenReplies.has(a.uri) - const bHidden = threadgateRecordHiddenReplies.has(b.uri) - if (aHidden && !aIsBySelf && !bHidden) { - return 1 - } else if (bHidden && !bIsBySelf && !aHidden) { - return -1 - } - - const aBlur = Boolean(modCache.get(a)?.ui('contentList').blur) - const bBlur = Boolean(modCache.get(b)?.ui('contentList').blur) - if (aBlur !== bBlur) { - if (aBlur) { - return 1 - } - if (bBlur) { - return -1 - } - } - - const aPin = Boolean(a.record.text.trim() === '📌') - const bPin = Boolean(b.record.text.trim() === '📌') - if (aPin !== bPin) { - if (aPin) { - return 1 - } - if (bPin) { - return -1 - } - } - - if (opts.prioritizeFollowedUsers) { - const af = a.post.author.viewer?.following - const bf = b.post.author.viewer?.following - if (af && !bf) { - return -1 - } else if (!af && bf) { - return 1 - } - } - - // Split items from different fetches into separate generations. - let aFetchedAt = fetchedAtCache.get(a.uri) - if (aFetchedAt === undefined) { - fetchedAtCache.set(a.uri, fetchedAt) - aFetchedAt = fetchedAt - } - let bFetchedAt = fetchedAtCache.get(b.uri) - if (bFetchedAt === undefined) { - fetchedAtCache.set(b.uri, fetchedAt) - bFetchedAt = fetchedAt - } - - if (aFetchedAt !== bFetchedAt) { - return aFetchedAt - bFetchedAt // older fetches first - } else if (opts.sort === 'hotness') { - const aHotness = getHotness(a, aFetchedAt) - const bHotness = getHotness(b, bFetchedAt /* same as aFetchedAt */) - return bHotness - aHotness - } else if (opts.sort === 'oldest') { - return a.post.indexedAt.localeCompare(b.post.indexedAt) - } else if (opts.sort === 'newest') { - return b.post.indexedAt.localeCompare(a.post.indexedAt) - } else if (opts.sort === 'most-likes') { - if (a.post.likeCount === b.post.likeCount) { - return b.post.indexedAt.localeCompare(a.post.indexedAt) // newest - } else { - return (b.post.likeCount || 0) - (a.post.likeCount || 0) // most likes - } - } else if (opts.sort === 'random') { - let aRandomScore = randomCache.get(a.uri) - if (aRandomScore === undefined) { - aRandomScore = Math.random() - randomCache.set(a.uri, aRandomScore) - } - let bRandomScore = randomCache.get(b.uri) - if (bRandomScore === undefined) { - bRandomScore = Math.random() - randomCache.set(b.uri, bRandomScore) - } - // this is vaguely criminal but we can get away with it - return aRandomScore - bRandomScore - } else { - return b.post.indexedAt.localeCompare(a.post.indexedAt) - } - }) - node.replies.forEach(reply => - sortThread( - reply, - opts, - modCache, - currentDid, - justPostedUris, - threadgateRecordHiddenReplies, - fetchedAtCache, - fetchedAt, - randomCache, - ), - ) - } - return node -} - -// internal methods -// = - -// Inspired by https://join-lemmy.org/docs/contributors/07-ranking-algo.html -// We want to give recent comments a real chance (and not bury them deep below the fold) -// while also surfacing well-liked comments from the past. In the future, we can explore -// something more sophisticated, but we don't have much data on the client right now. -function getHotness(threadPost: ThreadPost, fetchedAt: number) { - const {post, hasOPLike} = threadPost - const hoursAgo = Math.max( - 0, - (new Date(fetchedAt).getTime() - new Date(post.indexedAt).getTime()) / - (1000 * 60 * 60), - ) - const likeCount = post.likeCount ?? 0 - const likeOrder = Math.log(3 + likeCount) * (hasOPLike ? 1.45 : 1.0) - const timePenaltyExponent = 1.5 + 1.5 / (1 + Math.log(1 + likeCount)) - const opLikeBoost = hasOPLike ? 0.8 : 1.0 - const timePenalty = Math.pow(hoursAgo + 2, timePenaltyExponent * opLikeBoost) - return likeOrder / timePenalty -} - -function responseToThreadNodes( - node: ThreadViewNode, - depth = 0, - direction: 'up' | 'down' | 'start' = 'start', -): ThreadNode { - if ( - AppBskyFeedDefs.isThreadViewPost(node) && - bsky.dangerousIsType( - node.post.record, - AppBskyFeedPost.isRecord, - ) - ) { - const post = node.post - // These should normally be present. They're missing only for - // posts that were *just* created. Ideally, the backend would - // know to return zeros. Fill them in manually to compensate. - post.replyCount ??= 0 - post.likeCount ??= 0 - post.repostCount ??= 0 - return { - type: 'post', - _reactKey: node.post.uri, - uri: node.post.uri, - post: post, - record: node.post.record, - parent: - node.parent && direction !== 'down' - ? responseToThreadNodes(node.parent, depth - 1, 'up') - : undefined, - replies: - node.replies?.length && direction !== 'up' - ? node.replies - .map(reply => responseToThreadNodes(reply, depth + 1, 'down')) - // do not show blocked posts in replies - .filter(node => node.type !== 'blocked') - : undefined, - hasOPLike: Boolean(node?.threadContext?.rootAuthorLike), - ctx: { - depth, - isHighlightedPost: depth === 0, - hasMore: - direction === 'down' && !node.replies?.length && !!post.replyCount, - isSelfThread: false, // populated `annotateSelfThread` - hasMoreSelfThread: false, // populated in `annotateSelfThread` - }, - } - } else if (AppBskyFeedDefs.isBlockedPost(node)) { - return {type: 'blocked', _reactKey: node.uri, uri: node.uri, ctx: {depth}} - } else if (AppBskyFeedDefs.isNotFoundPost(node)) { - return {type: 'not-found', _reactKey: node.uri, uri: node.uri, ctx: {depth}} - } else { - return {type: 'unknown', uri: ''} - } -} - -function annotateSelfThread(thread: ThreadNode) { - if (thread.type !== 'post') { - return - } - const selfThreadNodes: ThreadPost[] = [thread] - - let parent: ThreadNode | undefined = thread.parent - while (parent) { - if ( - parent.type !== 'post' || - parent.post.author.did !== thread.post.author.did - ) { - // not a self-thread - return - } - selfThreadNodes.unshift(parent) - parent = parent.parent - } - - let node = thread - for (let i = 0; i < 10; i++) { - const reply = node.replies?.find( - r => r.type === 'post' && r.post.author.did === thread.post.author.did, - ) - if (reply?.type !== 'post') { - break - } - selfThreadNodes.push(reply) - node = reply - } - - if (selfThreadNodes.length > 1) { - for (const selfThreadNode of selfThreadNodes) { - selfThreadNode.ctx.isSelfThread = true - } - const last = selfThreadNodes[selfThreadNodes.length - 1] - if ( - last && - last.ctx.depth === REPLY_TREE_DEPTH && // at the edge of the tree depth - last.post.replyCount && // has replies - !last.replies?.length // replies were not hydrated - ) { - last.ctx.hasMoreSelfThread = true - } - } -} - -function findPostInQueryData( - queryClient: QueryClient, - uri: string, -): ThreadNode | void { - let partial - for (let item of findAllPostsInQueryData(queryClient, uri)) { - if (item.type === 'post') { - // Currently, the backend doesn't send full post info in some cases - // (for example, for quoted posts). We use missing `likeCount` - // as a way to detect that. In the future, we should fix this on - // the backend, which will let us always stop on the first result. - const hasAllInfo = item.post.likeCount != null - if (hasAllInfo) { - return item - } else { - partial = item - // Keep searching, we might still find a full post in the cache. - } - } - } - return partial -} - -export function* findAllPostsInQueryData( - queryClient: QueryClient, - uri: string, -): Generator { - const atUri = new AtUri(uri) - - const queryDatas = queryClient.getQueriesData({ - queryKey: [RQKEY_ROOT], - }) - for (const [_queryKey, queryData] of queryDatas) { - if (!queryData) { - continue - } - const {thread} = queryData - for (const item of traverseThread(thread)) { - if (item.type === 'post' && didOrHandleUriMatches(atUri, item.post)) { - const placeholder = threadNodeToPlaceholderThread(item) - if (placeholder) { - yield placeholder - } - } - const quotedPost = - item.type === 'post' ? getEmbeddedPost(item.post.embed) : undefined - if (quotedPost && didOrHandleUriMatches(atUri, quotedPost)) { - yield embedViewRecordToPlaceholderThread(quotedPost) - } - } - } - for (let post of findAllPostsInNotifsQueryData(queryClient, uri)) { - // Check notifications first. If you have a post in notifications, - // it's often due to a like or a repost, and we want to prioritize - // a post object with >0 likes/reposts over a stale version with no - // metrics in order to avoid a notification->post scroll jump. - yield postViewToPlaceholderThread(post) - } - for (let post of findAllPostsInFeedQueryData(queryClient, uri)) { - yield postViewToPlaceholderThread(post) - } - for (let post of findAllPostsInQuoteQueryData(queryClient, uri)) { - yield postViewToPlaceholderThread(post) - } - for (let post of findAllPostsInSearchQueryData(queryClient, uri)) { - yield postViewToPlaceholderThread(post) - } - for (let post of findAllPostsInExploreFeedPreviewsQueryData( - queryClient, - uri, - )) { - yield postViewToPlaceholderThread(post) - } -} - -export function* findAllProfilesInQueryData( - queryClient: QueryClient, - did: string, -): Generator { - const queryDatas = queryClient.getQueriesData({ - queryKey: [RQKEY_ROOT], - }) - for (const [_queryKey, queryData] of queryDatas) { - if (!queryData) { - continue - } - const {thread} = queryData - for (const item of traverseThread(thread)) { - if (item.type === 'post' && item.post.author.did === did) { - yield item.post.author - } - const quotedPost = - item.type === 'post' ? getEmbeddedPost(item.post.embed) : undefined - if (quotedPost?.author.did === did) { - yield quotedPost?.author - } - } - } - for (let profile of findAllProfilesInFeedQueryData(queryClient, did)) { - yield profile - } - for (let profile of findAllProfilesInNotifsQueryData(queryClient, did)) { - yield profile - } - for (let profile of findAllProfilesInSearchQueryData(queryClient, did)) { - yield profile - } - for (let profile of findAllProfilesInExploreFeedPreviewsQueryData( - queryClient, - did, - )) { - yield profile - } -} - -function* traverseThread(node: ThreadNode): Generator { - if (node.type === 'post') { - if (node.parent) { - yield* traverseThread(node.parent) - } - yield node - if (node.replies?.length) { - for (const reply of node.replies) { - yield* traverseThread(reply) - } - } - } -} - -function threadNodeToPlaceholderThread( - node: ThreadNode, -): ThreadNode | undefined { - if (node.type !== 'post') { - return undefined - } - return { - type: node.type, - _reactKey: node._reactKey, - uri: node.uri, - post: node.post, - record: node.record, - parent: undefined, - replies: undefined, - hasOPLike: undefined, - ctx: { - depth: 0, - isHighlightedPost: true, - hasMore: false, - isParentLoading: !!node.record.reply, - isChildLoading: !!node.post.replyCount, - }, - } -} - -function postViewToPlaceholderThread( - post: AppBskyFeedDefs.PostView, -): ThreadNode { - return { - type: 'post', - _reactKey: post.uri, - uri: post.uri, - post: post, - record: post.record as AppBskyFeedPost.Record, // validated in notifs - parent: undefined, - replies: undefined, - hasOPLike: undefined, - ctx: { - depth: 0, - isHighlightedPost: true, - hasMore: false, - isParentLoading: !!(post.record as AppBskyFeedPost.Record).reply, - isChildLoading: true, // assume yes (show the spinner) just in case - }, - } -} - -function embedViewRecordToPlaceholderThread( - record: AppBskyEmbedRecord.ViewRecord, -): ThreadNode { - return { - type: 'post', - _reactKey: record.uri, - uri: record.uri, - post: embedViewRecordToPostView(record), - record: record.value as AppBskyFeedPost.Record, // validated in getEmbeddedPost - parent: undefined, - replies: undefined, - hasOPLike: undefined, - ctx: { - depth: 0, - isHighlightedPost: true, - hasMore: false, - isParentLoading: !!(record.value as AppBskyFeedPost.Record).reply, - isChildLoading: true, // not available, so assume yes (to show the spinner) - }, - } -} diff --git a/src/state/queries/threadgate/index.ts b/src/state/queries/threadgate/index.ts index 305acc5d0..5a34eb310 100644 --- a/src/state/queries/threadgate/index.ts +++ b/src/state/queries/threadgate/index.ts @@ -1,6 +1,5 @@ import { - AppBskyFeedDefs, - type AppBskyFeedGetPostThread, + type AppBskyFeedDefs, AppBskyFeedThreadgate, AtUri, type BskyAgent, @@ -8,9 +7,8 @@ import { import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query' import {networkRetry, retry} from '#/lib/async/retry' -import {until} from '#/lib/async/until' import {STALE} from '#/state/queries' -import {RQKEY_ROOT as postThreadQueryKeyRoot} from '#/state/queries/post-thread' +import {useGetPost} from '#/state/queries/post' import {type ThreadgateAllowUISetting} from '#/state/queries/threadgate/types' import { createThreadgateRecord, @@ -18,6 +16,7 @@ import { threadgateAllowUISettingToAllowRecordValue, threadgateViewToAllowUISetting, } from '#/state/queries/threadgate/util' +import {useUpdatePostThreadThreadgateQueryCache} from '#/state/queries/usePostThread' import {useAgent} from '#/state/session' import {useThreadgateHiddenReplyUrisAPI} from '#/state/threadgate-hidden-replies' import * as bsky from '#/types/bsky' @@ -71,7 +70,7 @@ export function useThreadgateViewQuery({ postUri?: string initialData?: AppBskyFeedDefs.ThreadgateView } = {}) { - const agent = useAgent() + const getPost = useGetPost() return useQuery({ enabled: !!postUri, @@ -79,33 +78,12 @@ export function useThreadgateViewQuery({ placeholderData: initialData, staleTime: STALE.MINUTES.ONE, async queryFn() { - return getThreadgateView({ - agent, - postUri: postUri!, - }) + const post = await getPost({uri: postUri!}) + return post.threadgate ?? null }, }) } -export async function getThreadgateView({ - agent, - postUri, -}: { - agent: BskyAgent - postUri: string -}) { - const {data} = await agent.app.bsky.feed.getPostThread({ - uri: postUri!, - depth: 0, - }) - - if (AppBskyFeedDefs.isThreadViewPost(data.thread)) { - return data.thread.post.threadgate ?? null - } - - return null -} - export async function getThreadgateRecord({ agent, postUri, @@ -248,6 +226,8 @@ export async function updateThreadgateAllow({ export function useSetThreadgateAllowMutation() { const agent = useAgent() const queryClient = useQueryClient() + const getPost = useGetPost() + const updatePostThreadThreadgate = useUpdatePostThreadThreadgateQueryCache() return useMutation({ mutationFn: async ({ @@ -272,30 +252,32 @@ export function useSetThreadgateAllowMutation() { }) }, async onSuccess(_, {postUri, allow}) { - await until( + const data = await retry( 5, // 5 tries - 1e3, // 1s delay between tries - (res: AppBskyFeedGetPostThread.Response) => { - const thread = res.data.thread - if (AppBskyFeedDefs.isThreadViewPost(thread)) { - const fetchedSettings = threadgateViewToAllowUISetting( - thread.post.threadgate, + _e => true, + async () => { + const post = await getPost({uri: postUri}) + const threadgate = post.threadgate + if (!threadgate) { + throw new Error( + `useSetThreadgateAllowMutation: could not fetch threadgate, appview may not be ready yet`, ) - return JSON.stringify(fetchedSettings) === JSON.stringify(allow) } - return false - }, - () => { - return agent.app.bsky.feed.getPostThread({ - uri: postUri, - depth: 0, - }) + const fetchedSettings = threadgateViewToAllowUISetting(threadgate) + const isReady = + JSON.stringify(fetchedSettings) === JSON.stringify(allow) + if (!isReady) { + throw new Error( + `useSetThreadgateAllowMutation: appview isn't ready yet`, + ) // try again + } + return threadgate }, - ) + 1e3, // 1s delay between tries + ).catch(() => {}) + + if (data) updatePostThreadThreadgate(data) - queryClient.invalidateQueries({ - queryKey: [postThreadQueryKeyRoot], - }) queryClient.invalidateQueries({ queryKey: [threadgateRecordQueryKeyRoot], }) diff --git a/src/state/queries/usePostThread/context.tsx b/src/state/queries/usePostThread/context.tsx new file mode 100644 index 000000000..f82596541 --- /dev/null +++ b/src/state/queries/usePostThread/context.tsx @@ -0,0 +1,43 @@ +import {createContext, useContext} from 'react' + +import { + type createPostThreadOtherQueryKey, + type createPostThreadQueryKey, +} from '#/state/queries/usePostThread/types' + +/** + * Contains static metadata about the post thread query, suitable for + * context e.g. query keys and other things that don't update frequently. + * + * Be careful adding things here, as it could cause unnecessary re-renders. + */ +export type PostThreadContextType = { + postThreadQueryKey: ReturnType + postThreadOtherQueryKey: ReturnType +} + +const PostThreadContext = createContext( + undefined, +) + +/** + * Use the current {@link PostThreadContext}, if one is available. If not, + * returns `undefined`. + */ +export function usePostThreadContext() { + return useContext(PostThreadContext) +} + +export function PostThreadContextProvider({ + children, + context, +}: { + children: React.ReactNode + context?: PostThreadContextType +}) { + return ( + + {children} + + ) +} diff --git a/src/state/queries/usePostThread/index.ts b/src/state/queries/usePostThread/index.ts index 8495df04c..e00250f74 100644 --- a/src/state/queries/usePostThread/index.ts +++ b/src/state/queries/usePostThread/index.ts @@ -11,6 +11,7 @@ import { TREE_VIEW_BELOW_DESKTOP, TREE_VIEW_BF, } from '#/state/queries/usePostThread/const' +import {type PostThreadContextType} from '#/state/queries/usePostThread/context' import { createCacheMutator, getThreadPlaceholder, @@ -31,6 +32,8 @@ import {useAgent, useSession} from '#/state/session' import {useMergeThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies' import {useBreakpoints} from '#/alf' +export * from '#/state/queries/usePostThread/context' +export {useUpdatePostThreadThreadgateQueryCache} from '#/state/queries/usePostThread/queryCache' export * from '#/state/queries/usePostThread/types' export function usePostThread({anchor}: {anchor?: string}) { @@ -277,8 +280,13 @@ export function usePostThread({anchor}: {anchor?: string}) { setOtherItemsVisible, ]) - return useMemo( - () => ({ + return useMemo(() => { + const context: PostThreadContextType = { + postThreadQueryKey, + postThreadOtherQueryKey, + } + return { + context, state: { /* * Copy in any query state that is useful @@ -309,17 +317,18 @@ export function usePostThread({anchor}: {anchor?: string}) { setSort, setView, }, - }), - [ - query, - mutator.insertReplies, - otherItemsVisible, - sort, - view, - setSort, - setView, - threadgate, - items, - ], - ) + } + }, [ + query, + mutator.insertReplies, + otherItemsVisible, + sort, + view, + setSort, + setView, + threadgate, + items, + postThreadQueryKey, + postThreadOtherQueryKey, + ]) } diff --git a/src/state/queries/usePostThread/queryCache.ts b/src/state/queries/usePostThread/queryCache.ts index 5e27ebb87..5f9f26328 100644 --- a/src/state/queries/usePostThread/queryCache.ts +++ b/src/state/queries/usePostThread/queryCache.ts @@ -1,3 +1,4 @@ +import {useCallback} from 'react' import { type $Typed, type AppBskyActorDefs, @@ -7,7 +8,7 @@ import { type AppBskyUnspeccedGetPostThreadV2, AtUri, } from '@atproto/api' -import {type QueryClient} from '@tanstack/react-query' +import {type QueryClient, useQueryClient} from '@tanstack/react-query' import { dangerousGetPostShadow, @@ -18,6 +19,7 @@ import {findAllPostsInQueryData as findAllPostsInNotifsQueryData} from '#/state/ import {findAllPostsInQueryData as findAllPostsInFeedQueryData} from '#/state/queries/post-feed' import {findAllPostsInQueryData as findAllPostsInQuoteQueryData} from '#/state/queries/post-quotes' import {findAllPostsInQueryData as findAllPostsInSearchQueryData} from '#/state/queries/search-posts' +import {usePostThreadContext} from '#/state/queries/usePostThread' import {getBranch} from '#/state/queries/usePostThread/traversal' import { type ApiThreadItem, @@ -322,3 +324,51 @@ export function* findAllProfilesInQueryData( } } } + +export function useUpdatePostThreadThreadgateQueryCache() { + const qc = useQueryClient() + const context = usePostThreadContext() + + return useCallback( + (threadgate: AppBskyFeedDefs.ThreadgateView) => { + if (!context) return + + function mutator(thread: ApiThreadItem[]): T[] { + for (let i = 0; i < thread.length; i++) { + const item = thread[i] + + if (!AppBskyUnspeccedDefs.isThreadItemPost(item.value)) continue + + if (item.depth === 0) { + thread.splice(i, 1, { + ...item, + value: { + ...item.value, + post: { + ...item.value.post, + threadgate, + }, + }, + }) + } + } + + return thread as T[] + } + + qc.setQueryData( + context.postThreadQueryKey, + data => { + if (!data) return + return { + ...data, + thread: mutator([ + ...data.thread, + ]), + } + }, + ) + }, + [qc, context], + ) +} diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx index 8cbb2d37b..305790cb1 100644 --- a/src/view/com/composer/Composer.tsx +++ b/src/view/com/composer/Composer.tsx @@ -44,9 +44,8 @@ import Animated, { import {useSafeAreaInsets} from 'react-native-safe-area-context' import {type ImagePickerAsset} from 'expo-image-picker' import { - AppBskyFeedDefs, - type AppBskyFeedGetPostThread, AppBskyUnspeccedDefs, + type AppBskyUnspeccedGetPostThreadV2, AtUri, type BskyAgent, type RichText, @@ -549,10 +548,10 @@ export const ComposePost = ({ if (initQuote) { // We want to wait for the quote count to update before we call `onPost`, which will refetch data whenAppViewReady(agent, initQuote.uri, res => { - const quotedThread = res.data.thread + const anchor = res.data.thread.at(0) if ( - AppBskyFeedDefs.isThreadViewPost(quotedThread) && - quotedThread.post.quoteCount !== initQuote.quoteCount + AppBskyUnspeccedDefs.isThreadItemPost(anchor?.value) && + anchor.value.post.quoteCount !== initQuote.quoteCount ) { onPost?.(postUri) onPostSuccess?.(postSuccessData) @@ -1661,16 +1660,18 @@ function useKeyboardVerticalOffset() { async function whenAppViewReady( agent: BskyAgent, uri: string, - fn: (res: AppBskyFeedGetPostThread.Response) => boolean, + fn: (res: AppBskyUnspeccedGetPostThreadV2.Response) => boolean, ) { await until( 5, // 5 tries 1e3, // 1s delay between tries fn, () => - agent.app.bsky.feed.getPostThread({ - uri, - depth: 0, + agent.app.bsky.unspecced.getPostThreadV2({ + anchor: uri, + above: false, + below: 0, + branchingFactor: 0, }), ) } -- 2.51.2 From d2bc22e9be3e9a1aad807cf0bfad430f57e5c125 Mon Sep 17 00:00:00 2001 From: pfrazee <1270099+pfrazee@users.noreply.github.com> Date: Tue, 14 Oct 2025 02:36:22 +0000 Subject: [PATCH 2/2] Nightly source-language update --- src/locale/locales/en/messages.po | 152 +++++++++++++++--------------- 1 file changed, 78 insertions(+), 74 deletions(-) diff --git a/src/locale/locales/en/messages.po b/src/locale/locales/en/messages.po index 260929cad..489ad0475 100644 --- a/src/locale/locales/en/messages.po +++ b/src/locale/locales/en/messages.po @@ -712,11 +712,11 @@ msgstr "" msgid "Add another account" msgstr "" -#: src/view/com/composer/Composer.tsx:853 +#: src/view/com/composer/Composer.tsx:852 msgid "Add another post" msgstr "" -#: src/view/com/composer/Composer.tsx:1490 +#: src/view/com/composer/Composer.tsx:1489 msgid "Add another post to thread" msgstr "" @@ -911,11 +911,11 @@ msgstr "" msgid "Allow others to be notified of your posts" msgstr "" -#: src/components/dialogs/PostInteractionSettingsDialog.tsx:348 +#: src/components/dialogs/PostInteractionSettingsDialog.tsx:355 msgid "Allow quote posts" msgstr "" -#: src/components/dialogs/PostInteractionSettingsDialog.tsx:393 +#: src/components/dialogs/PostInteractionSettingsDialog.tsx:400 msgid "Allow replies from:" msgstr "" @@ -1225,11 +1225,11 @@ msgstr "" msgid "Are you sure you want to remove this from your feeds?" msgstr "" -#: src/view/com/composer/Composer.tsx:802 +#: src/view/com/composer/Composer.tsx:801 msgid "Are you sure you'd like to discard this draft?" msgstr "" -#: src/view/com/composer/Composer.tsx:992 +#: src/view/com/composer/Composer.tsx:991 msgid "Are you sure you'd like to discard this post?" msgstr "" @@ -1630,8 +1630,8 @@ msgstr "" #: src/screens/Settings/Settings.tsx:289 #: src/screens/Takendown.tsx:108 #: src/screens/Takendown.tsx:111 -#: src/view/com/composer/Composer.tsx:1047 -#: src/view/com/composer/Composer.tsx:1058 +#: src/view/com/composer/Composer.tsx:1046 +#: src/view/com/composer/Composer.tsx:1057 #: src/view/com/composer/photos/EditImageDialog.web.tsx:43 #: src/view/com/composer/photos/EditImageDialog.web.tsx:52 #: src/view/shell/desktop/LeftNav.tsx:213 @@ -1884,11 +1884,11 @@ msgstr "" msgid "Click here to update your email" msgstr "" -#: src/components/dialogs/PostInteractionSettingsDialog.tsx:341 +#: src/components/dialogs/PostInteractionSettingsDialog.tsx:348 msgid "Click to disable quote posts of this post." msgstr "" -#: src/components/dialogs/PostInteractionSettingsDialog.tsx:342 +#: src/components/dialogs/PostInteractionSettingsDialog.tsx:349 msgid "Click to enable quote posts of this post." msgstr "" @@ -2007,7 +2007,7 @@ msgstr "" msgid "Closes password update alert" msgstr "" -#: src/view/com/composer/Composer.tsx:1055 +#: src/view/com/composer/Composer.tsx:1054 msgid "Closes post composer and discards post draft" msgstr "" @@ -2066,7 +2066,7 @@ msgstr "" msgid "Compose new post" msgstr "" -#: src/view/com/composer/Composer.tsx:956 +#: src/view/com/composer/Composer.tsx:955 msgid "Compose posts up to {0, plural, other {# characters}} in length" msgstr "" @@ -2074,7 +2074,7 @@ msgstr "" msgid "Compose reply" msgstr "" -#: src/view/com/composer/Composer.tsx:1883 +#: src/view/com/composer/Composer.tsx:1884 msgid "Compressing video..." msgstr "" @@ -2512,7 +2512,7 @@ msgstr "" msgid "Customization options" msgstr "" -#: src/components/dialogs/PostInteractionSettingsDialog.tsx:107 +#: src/components/dialogs/PostInteractionSettingsDialog.tsx:111 msgid "Customize who can interact with this post." msgstr "" @@ -2637,7 +2637,7 @@ msgstr "" #: src/components/PostControls/PostMenu/PostMenuItems.tsx:710 #: src/components/PostControls/PostMenu/PostMenuItems.tsx:712 -#: src/view/com/composer/Composer.tsx:966 +#: src/view/com/composer/Composer.tsx:965 msgid "Delete post" msgstr "" @@ -2750,8 +2750,8 @@ msgstr "" #: src/components/dialogs/lists/CreateOrEditListDialog.tsx:92 #: src/screens/Profile/Header/EditProfileDialog.tsx:82 -#: src/view/com/composer/Composer.tsx:804 -#: src/view/com/composer/Composer.tsx:999 +#: src/view/com/composer/Composer.tsx:803 +#: src/view/com/composer/Composer.tsx:998 msgid "Discard" msgstr "" @@ -2760,11 +2760,11 @@ msgstr "" msgid "Discard changes?" msgstr "" -#: src/view/com/composer/Composer.tsx:801 +#: src/view/com/composer/Composer.tsx:800 msgid "Discard draft?" msgstr "" -#: src/view/com/composer/Composer.tsx:991 +#: src/view/com/composer/Composer.tsx:990 msgid "Discard post?" msgstr "" @@ -2787,7 +2787,7 @@ msgstr "" msgid "Dismiss" msgstr "" -#: src/view/com/composer/Composer.tsx:1807 +#: src/view/com/composer/Composer.tsx:1808 msgid "Dismiss error" msgstr "" @@ -3009,8 +3009,8 @@ msgstr "" msgid "Edit People" msgstr "" -#: src/components/dialogs/PostInteractionSettingsDialog.tsx:73 -#: src/components/dialogs/PostInteractionSettingsDialog.tsx:243 +#: src/components/dialogs/PostInteractionSettingsDialog.tsx:77 +#: src/components/dialogs/PostInteractionSettingsDialog.tsx:250 msgid "Edit post interaction settings" msgstr "" @@ -3229,7 +3229,7 @@ msgstr "" msgid "Entertainment" msgstr "" -#: src/view/com/composer/Composer.tsx:1892 +#: src/view/com/composer/Composer.tsx:1893 #: src/view/com/util/error/ErrorScreen.tsx:42 msgid "Error" msgstr "" @@ -3262,7 +3262,7 @@ msgstr "" msgid "Error: {error}" msgstr "" -#: src/components/dialogs/PostInteractionSettingsDialog.tsx:398 +#: src/components/dialogs/PostInteractionSettingsDialog.tsx:405 msgid "Everybody" msgstr "" @@ -3964,7 +3964,7 @@ msgstr "" msgid "From @{sanitizedAuthor}" msgstr "" -#: src/view/com/posts/PostFeedItem.tsx:345 +#: src/view/com/posts/PostFeedReason.tsx:47 msgctxt "from-feed" msgid "From <0/>" msgstr "" @@ -4136,6 +4136,10 @@ msgstr "" msgid "Go to conversation with {0}" msgstr "" +#: src/view/com/posts/PostFeedReason.tsx:38 +msgid "Go to feed" +msgstr "" + #: src/screens/Login/ForgotPasswordForm.tsx:165 msgid "Go to next" msgstr "" @@ -4609,7 +4613,7 @@ msgstr "" msgid "It's just you right now! Add more people to your starter pack by searching above." msgstr "" -#: src/view/com/composer/Composer.tsx:1826 +#: src/view/com/composer/Composer.tsx:1827 msgid "Job ID: {0}" msgstr "" @@ -5138,7 +5142,7 @@ msgstr "" msgid "mentioned users" msgstr "" -#: src/components/dialogs/PostInteractionSettingsDialog.tsx:427 +#: src/components/dialogs/PostInteractionSettingsDialog.tsx:434 msgid "Mentioned users" msgstr "" @@ -5726,7 +5730,7 @@ msgstr "" msgid "No thanks" msgstr "" -#: src/components/dialogs/PostInteractionSettingsDialog.tsx:409 +#: src/components/dialogs/PostInteractionSettingsDialog.tsx:416 msgid "Nobody" msgstr "" @@ -5890,11 +5894,11 @@ msgstr "" msgid "Onboarding reset" msgstr "" -#: src/view/com/composer/Composer.tsx:398 +#: src/view/com/composer/Composer.tsx:397 msgid "One or more GIFs is missing alt text." msgstr "" -#: src/view/com/composer/Composer.tsx:395 +#: src/view/com/composer/Composer.tsx:394 msgid "One or more images is missing alt text." msgstr "" @@ -5906,7 +5910,7 @@ msgstr "" msgid "One or more of your selected files are too large. Maximum size is 100 MB." msgstr "" -#: src/view/com/composer/Composer.tsx:405 +#: src/view/com/composer/Composer.tsx:404 msgid "One or more videos is missing alt text." msgstr "" @@ -5963,7 +5967,7 @@ msgid "Open drawer menu" msgstr "" #: src/screens/Messages/components/MessageInput.web.tsx:181 -#: src/view/com/composer/Composer.tsx:1475 +#: src/view/com/composer/Composer.tsx:1474 msgid "Open emoji picker" msgstr "" @@ -6063,7 +6067,7 @@ msgstr "" msgid "Opens device gallery to select up to {MAX_IMAGES, plural, other {# images}}, or a single video or GIF." msgstr "" -#: src/view/com/composer/Composer.tsx:1476 +#: src/view/com/composer/Composer.tsx:1475 msgid "Opens emoji picker" msgstr "" @@ -6115,7 +6119,7 @@ msgstr "" msgid "Options:" msgstr "" -#: src/components/dialogs/PostInteractionSettingsDialog.tsx:422 +#: src/components/dialogs/PostInteractionSettingsDialog.tsx:429 msgid "Or combine these options:" msgstr "" @@ -6273,7 +6277,7 @@ msgstr "" msgid "Pin to your profile" msgstr "" -#: src/view/com/posts/PostFeedItem.tsx:426 +#: src/view/com/posts/PostFeedReason.tsx:125 msgid "Pinned" msgstr "" @@ -6475,17 +6479,17 @@ msgstr "" msgid "Porn" msgstr "" -#: src/screens/PostThread/index.tsx:500 +#: src/screens/PostThread/index.tsx:504 msgctxt "description" msgid "Post" msgstr "" -#: src/view/com/composer/Composer.tsx:1118 +#: src/view/com/composer/Composer.tsx:1117 msgctxt "action" msgid "Post" msgstr "" -#: src/view/com/composer/Composer.tsx:1116 +#: src/view/com/composer/Composer.tsx:1115 msgctxt "action" msgid "Post All" msgstr "" @@ -6527,7 +6531,7 @@ msgstr "" msgid "Post Hidden by You" msgstr "" -#: src/components/dialogs/PostInteractionSettingsDialog.tsx:104 +#: src/components/dialogs/PostInteractionSettingsDialog.tsx:108 msgid "Post interaction settings" msgstr "" @@ -6650,7 +6654,7 @@ msgstr "" msgid "Privacy Policy" msgstr "" -#: src/view/com/composer/Composer.tsx:1889 +#: src/view/com/composer/Composer.tsx:1890 msgid "Processing video..." msgstr "" @@ -6689,22 +6693,22 @@ msgid "Public, sharable lists which can be used to drive feeds." msgstr "" #. Accessibility label for button to publish a single post -#: src/view/com/composer/Composer.tsx:1098 +#: src/view/com/composer/Composer.tsx:1097 msgid "Publish post" msgstr "" #. Accessibility label for button to publish multiple posts in a thread -#: src/view/com/composer/Composer.tsx:1091 +#: src/view/com/composer/Composer.tsx:1090 msgid "Publish posts" msgstr "" #. Accessibility label for button to publish multiple replies in a thread -#: src/view/com/composer/Composer.tsx:1076 +#: src/view/com/composer/Composer.tsx:1075 msgid "Publish replies" msgstr "" #. Accessibility label for button to publish a single reply -#: src/view/com/composer/Composer.tsx:1083 +#: src/view/com/composer/Composer.tsx:1082 msgid "Publish reply" msgstr "" @@ -6762,7 +6766,7 @@ msgstr "" msgid "Quote posts disabled" msgstr "" -#: src/components/dialogs/PostInteractionSettingsDialog.tsx:333 +#: src/components/dialogs/PostInteractionSettingsDialog.tsx:340 msgid "Quote settings" msgstr "" @@ -7094,7 +7098,7 @@ msgstr "" msgid "Replies to this post are disabled." msgstr "" -#: src/view/com/composer/Composer.tsx:1114 +#: src/view/com/composer/Composer.tsx:1113 msgctxt "action" msgid "Reply" msgstr "" @@ -7118,11 +7122,11 @@ msgstr "" msgid "Reply notifications" msgstr "" -#: src/components/dialogs/PostInteractionSettingsDialog.tsx:389 +#: src/components/dialogs/PostInteractionSettingsDialog.tsx:396 msgid "Reply settings" msgstr "" -#: src/components/dialogs/PostInteractionSettingsDialog.tsx:374 +#: src/components/dialogs/PostInteractionSettingsDialog.tsx:381 msgid "Reply settings are chosen by the author of the thread" msgstr "" @@ -7256,16 +7260,16 @@ msgstr "" msgid "Reposted By" msgstr "" -#: src/view/com/posts/PostFeedItem.tsx:366 -msgid "Reposted by {0}" +#: src/view/com/posts/PostFeedReason.tsx:77 +msgid "Reposted by {reposter}" msgstr "" -#: src/view/com/posts/PostFeedItem.tsx:385 -msgid "Reposted by <0><1/>" +#: src/view/com/posts/PostFeedReason.tsx:91 +msgid "Reposted by <0><1>{reposter}" msgstr "" -#: src/view/com/posts/PostFeedItem.tsx:364 -#: src/view/com/posts/PostFeedItem.tsx:383 +#: src/view/com/posts/PostFeedReason.tsx:77 +#: src/view/com/posts/PostFeedReason.tsx:89 msgid "Reposted by you" msgstr "" @@ -7410,8 +7414,8 @@ msgstr "" #: src/components/dialogs/BirthDateSettings.tsx:156 #: src/components/dialogs/lists/CreateOrEditListDialog.tsx:292 #: src/components/dialogs/lists/CreateOrEditListDialog.tsx:307 -#: src/components/dialogs/PostInteractionSettingsDialog.tsx:483 -#: src/components/dialogs/PostInteractionSettingsDialog.tsx:489 +#: src/components/dialogs/PostInteractionSettingsDialog.tsx:490 +#: src/components/dialogs/PostInteractionSettingsDialog.tsx:496 #: src/components/live/EditLiveDialog.tsx:216 #: src/components/live/EditLiveDialog.tsx:223 #: src/components/StarterPack/QrCodeDialog.tsx:204 @@ -8880,7 +8884,7 @@ msgstr "" msgid "There was an issue! {0}" msgstr "" -#: src/components/dialogs/PostInteractionSettingsDialog.tsx:221 +#: src/components/dialogs/PostInteractionSettingsDialog.tsx:228 #: src/screens/List/ListHiddenScreen.tsx:63 #: src/screens/List/ListHiddenScreen.tsx:77 #: src/screens/List/ListHiddenScreen.tsx:99 @@ -9075,7 +9079,7 @@ msgstr "" msgid "This post will be hidden from feeds and threads. This cannot be undone." msgstr "" -#: src/view/com/composer/Composer.tsx:514 +#: src/view/com/composer/Composer.tsx:513 msgid "This post's author has disabled quote posts." msgstr "" @@ -9503,7 +9507,7 @@ msgstr "" msgid "Unsubscribed from list" msgstr "" -#: src/view/com/composer/Composer.tsx:894 +#: src/view/com/composer/Composer.tsx:893 msgid "Unsupported video type: {mimeType}" msgstr "" @@ -9589,7 +9593,7 @@ msgstr "" msgid "Uploading link thumbnail..." msgstr "" -#: src/view/com/composer/Composer.tsx:1886 +#: src/view/com/composer/Composer.tsx:1887 msgid "Uploading video..." msgstr "" @@ -9705,11 +9709,11 @@ msgstr "" msgid "Users I follow" msgstr "" -#: src/components/dialogs/PostInteractionSettingsDialog.tsx:460 +#: src/components/dialogs/PostInteractionSettingsDialog.tsx:467 msgid "Users in \"{0}\"" msgstr "" -#: src/components/dialogs/PostInteractionSettingsDialog.tsx:437 +#: src/components/dialogs/PostInteractionSettingsDialog.tsx:444 msgid "Users you follow" msgstr "" @@ -9847,7 +9851,7 @@ msgstr "" msgid "Video settings" msgstr "" -#: src/view/com/composer/Composer.tsx:1896 +#: src/view/com/composer/Composer.tsx:1897 msgid "Video uploaded" msgstr "" @@ -9863,7 +9867,7 @@ msgstr "" msgid "Videos must be less than 3 minutes long." msgstr "" -#: src/view/com/composer/Composer.tsx:585 +#: src/view/com/composer/Composer.tsx:584 msgctxt "Action to view the post the user just created" msgid "View" msgstr "" @@ -9925,7 +9929,7 @@ msgstr "" msgid "View more trending videos" msgstr "" -#: src/view/com/composer/Composer.tsx:580 +#: src/view/com/composer/Composer.tsx:579 msgid "View post" msgstr "" @@ -10153,7 +10157,7 @@ msgstr "" msgid "We're sorry, but your search could not be completed. Please try again in a few minutes." msgstr "" -#: src/view/com/composer/Composer.tsx:511 +#: src/view/com/composer/Composer.tsx:510 msgid "We're sorry! The post you are replying to has been deleted." msgstr "" @@ -10204,7 +10208,7 @@ msgstr "" #: src/view/com/auth/SplashScreen.tsx:51 #: src/view/com/auth/SplashScreen.web.tsx:103 -#: src/view/com/composer/Composer.tsx:854 +#: src/view/com/composer/Composer.tsx:853 msgid "What's up?" msgstr "" @@ -10282,12 +10286,12 @@ msgstr "" msgid "Write a message" msgstr "" -#: src/view/com/composer/Composer.tsx:954 +#: src/view/com/composer/Composer.tsx:953 msgid "Write post" msgstr "" #: src/screens/PostThread/components/ThreadComposePrompt.tsx:90 -#: src/view/com/composer/Composer.tsx:852 +#: src/view/com/composer/Composer.tsx:851 msgid "Write your reply" msgstr "" @@ -10443,7 +10447,7 @@ msgstr "" msgid "You can select up to {MAX_IMAGES, plural, other {# images}} in total." msgstr "" -#: src/components/dialogs/PostInteractionSettingsDialog.tsx:85 +#: src/components/dialogs/PostInteractionSettingsDialog.tsx:89 msgid "You can set default interaction settings in <0>Settings → Moderation → Interaction settings." msgstr "" @@ -10825,7 +10829,7 @@ msgstr "" msgid "Your first like!" msgstr "" -#: src/components/dialogs/PostInteractionSettingsDialog.tsx:447 +#: src/components/dialogs/PostInteractionSettingsDialog.tsx:454 msgid "Your followers" msgstr "" @@ -10870,11 +10874,11 @@ msgstr "" msgid "Your password must be at least 8 characters long." msgstr "" -#: src/view/com/composer/Composer.tsx:576 +#: src/view/com/composer/Composer.tsx:575 msgid "Your post was sent" msgstr "" -#: src/view/com/composer/Composer.tsx:573 +#: src/view/com/composer/Composer.tsx:572 msgid "Your posts were sent" msgstr "" @@ -10899,7 +10903,7 @@ msgstr "" msgid "Your profile, posts, feeds, and lists will no longer be visible to other Bluesky users. You can reactivate your account at any time by logging in." msgstr "" -#: src/view/com/composer/Composer.tsx:575 +#: src/view/com/composer/Composer.tsx:574 msgid "Your reply was sent" msgstr "" -- 2.51.2