From b428f8f93c6bb27e53272021441cc98763d1f445 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Fri, 9 May 2025 09:47:32 -0500 Subject: [PATCH] WIP optimistic --- src/state/queries/useGetPostThreadV2.ts | 187 ++++++++++++++++++------ src/view/screens/PostThread.tsx | 29 +--- 2 files changed, 143 insertions(+), 73 deletions(-) diff --git a/src/state/queries/useGetPostThreadV2.ts b/src/state/queries/useGetPostThreadV2.ts index cb46865aa5..ca4fdc7a79 100644 --- a/src/state/queries/useGetPostThreadV2.ts +++ b/src/state/queries/useGetPostThreadV2.ts @@ -1,4 +1,4 @@ -import {useCallback} from 'react' +import {useCallback, useMemo, useState} from 'react' import { $Typed, AtUri, @@ -138,7 +138,7 @@ export function useGetPostThreadV2({ // - sort down muted // - sort down blurred? - const filtered = filterAndSort(query.data?.thread || [], { + const {items, insertReplies} = useThread(query.data?.thread || [], { hasSession, params, threadgateHiddenReplies: mergeThreadgateHiddenReplies( @@ -151,9 +151,10 @@ export function useGetPostThreadV2({ return { ...query, data: { - slices: filtered, + slices: items, threadgate: query.data?.threadgate, }, + insertReplies, } } @@ -265,7 +266,7 @@ function getBranch( } } -export function filterAndSort( +export function useThread( thread: AppBskyFeedGetPostThreadV2.OutputSchema['thread'], { hasSession, @@ -280,6 +281,136 @@ export function filterAndSort( moderationOpts: ModerationOpts shownHiddenReplyKinds: Set }, +) { + const isTreeView = params.view === 'tree' + const [shadowSlices, setShadowSlices] = useState< + Map + >(new Map()) + const insertReplies = useCallback((belowUri: string, posts: AppBskyFeedDefs.ThreadItemPost[]) => { + setShadowSlices(prev => { + if (prev.has(belowUri)) { + prev.set(belowUri, [...prev.get(belowUri)!, posts]) + } else { + prev.set(belowUri, [posts]) + } + return prev + }) + }, [setShadowSlices]) + + const sorted = useMemo(() => { + return sortThread(thread, { + threadgateHiddenReplies, + moderationOpts, + }) + }, [thread, threadgateHiddenReplies, moderationOpts]) + + const flattened = useMemo(() => { + const showMuted = shownHiddenReplyKinds.has(HiddenReplyKind.Muted) + const showHidden = shownHiddenReplyKinds.has(HiddenReplyKind.Hidden) + + if (sorted.hidden.length) { + if (showHidden) { + sorted.slices.push(...sorted.hidden) + + if (sorted.muted.length) { + if (showMuted) { + sorted.slices.push(...sorted.muted) + } else { + sorted.slices.push({ + type: 'showHiddenReplies', + key: 'showMutedReplies', + kind: HiddenReplyKind.Muted, + }) + } + } + } else { + sorted.slices.push({ + type: 'showHiddenReplies', + key: 'showHiddenReplies', + kind: HiddenReplyKind.Hidden, + }) + } + } else if (sorted.muted.length) { + if (showMuted) { + sorted.slices.push(...sorted.muted) + } else { + sorted.slices.push({ + type: 'showHiddenReplies', + key: 'showMutedReplies', + kind: HiddenReplyKind.Muted, + }) + } + } + return sorted.slices + }, [sorted, shownHiddenReplyKinds]) + + const optimistic = useMemo(() => { + if (!shadowSlices.size) return flattened + + for (let i = 0; i < flattened.length; i++) { + const item = flattened[i] + if (item.type !== 'threadSlice') continue + if (!shadowSlices.has(item.slice.uri)) continue + + const replyThreads = shadowSlices.get(item.slice.uri) + + // TODO linear view will work + if (!isTreeView) continue + + for (const thread of replyThreads!) { + for (let ri = 0; ri < thread.length; ri++) { + const post = thread[ri] + post.depth = item.slice.depth + 1 + ri + flattened.splice(i + 1 + ri, 0, views.post({ + item: post, + oneUp: undefined, + oneDown: undefined, + moderationOpts, + })) + } + + i = i + thread.length + } + } + + return flattened + }, [flattened, shadowSlices, isTreeView]) + + const items = useMemo(() => { + if (hasSession) { + for (let i = 0; i < optimistic.length; i++) { + const slice = optimistic[i] + if ('slice' in slice && slice.slice.depth === 0) { + optimistic.splice(i + 1, 0, { + type: 'replyComposer', + key: 'replyComposer', + }) + break + } + } + } + + return optimistic + }, [optimistic, hasSession]) + + return useMemo( + () => ({ + items, + insertReplies, + }), + [items, insertReplies], + ) +} + +export function sortThread( + thread: AppBskyFeedGetPostThreadV2.OutputSchema['thread'], + { + threadgateHiddenReplies, + moderationOpts, + }: { + threadgateHiddenReplies: Set + moderationOpts: ModerationOpts + }, ) { const slices: Slice[] = [] const hidden: Slice[] = [] @@ -313,13 +444,6 @@ export function filterAndSort( }), ) - if (hasSession) { - slices.push({ - type: 'replyComposer', - key: 'replyComposer', - }) - } - parentTraversal: for (let pi = i - 1; pi >= 0; pi--) { const parentOneDown = thread[pi + 1] const parent = thread[pi] @@ -436,44 +560,11 @@ export function filterAndSort( } } - const showMuted = shownHiddenReplyKinds.has(HiddenReplyKind.Muted) - const showHidden = shownHiddenReplyKinds.has(HiddenReplyKind.Hidden) - - if (hidden.length) { - if (showHidden) { - slices.push(...hidden) - - if (muted.length) { - if (showMuted) { - slices.push(...muted) - } else { - slices.push({ - type: 'showHiddenReplies', - key: 'showMutedReplies', - kind: HiddenReplyKind.Muted, - }) - } - } - } else { - slices.push({ - type: 'showHiddenReplies', - key: 'showHiddenReplies', - kind: HiddenReplyKind.Hidden, - }) - } - } else if (muted.length) { - if (showMuted) { - slices.push(...muted) - } else { - slices.push({ - type: 'showHiddenReplies', - key: 'showMutedReplies', - kind: HiddenReplyKind.Muted, - }) - } + return { + slices, + hidden, + muted, } - - return slices } export function getModerationState(moderation: ModerationDecision) { diff --git a/src/view/screens/PostThread.tsx b/src/view/screens/PostThread.tsx index eaf64206b0..2f05d92572 100644 --- a/src/view/screens/PostThread.tsx +++ b/src/view/screens/PostThread.tsx @@ -127,10 +127,9 @@ export function Inner({uri}: {uri: string | undefined}) { setTreeViewEnabled, } = useThreadPreferences() - const [showHiddenReplies, setShowHiddenReplies] = useState(null) const [shownHiddenReplyKinds, setShownHiddenReplyKinds] = useState>(new Set()) - const {isFetching, isPlaceholderData, error, data, refetch} = useGetPostThreadV2({ + const {isFetching, isPlaceholderData, error, data, refetch, insertReplies} = useGetPostThreadV2({ uri, enabled: isThreadPreferencesLoaded, params: { @@ -146,36 +145,14 @@ export function Inner({uri}: {uri: string | undefined}) { const ref = useRef(null) const layoutHeaderRef = useRef(null) const anchorPostRef = useRef(null) - // TODO - const [justPostedUris, setJustPostedUris] = useState( - () => new Set(), - ) - - const onPostReply = useCallback( - (postUri: string | undefined) => { - refetch() - if (postUri) { - setJustPostedUris(set => { - const nextSet = new Set(set) - nextSet.add(postUri) - return nextSet - }) - } - }, - [refetch], - ) const optimisticOnPostReply = ({ post, }: { post: AppBskyFeedDefs.ThreadItemPost, }) => (_: any, posts: AppBskyFeedDefs.ThreadItemPost[]) => { - const parentDepth = post.depth if (posts.length) { - for (let i = 0; i < posts.length; i++) { - const p = posts[i] - p.depth = parentDepth + 1 + i - } + insertReplies(post.uri, posts) } } @@ -195,6 +172,7 @@ export function Inner({uri}: {uri: string | undefined}) { embed: post.embed, moderation: anchorPost.moderation, }, + // @ts-expect-error TODO onPost: optimisticOnPostReply({post:anchorPost.slice}), }) } @@ -223,6 +201,7 @@ export function Inner({uri}: {uri: string | undefined}) { overrideBlur={ shownHiddenReplyKinds.has(HiddenReplyKind.Muted) && item.slice.depth > 0 } + // @ts-expect-error TODO onPostReply={optimisticOnPostReply({post:item.slice})} hideTopBorder={index === 0} // && !item.slice.isParentLoading} // TODO />