WIP optimistic

This commit is contained in:
Eric Bailey
2025-05-09 09:47:32 -05:00
parent 21601390e4
commit b428f8f93c
2 changed files with 143 additions and 73 deletions
+139 -48
View File
@@ -1,4 +1,4 @@
import {useCallback} from 'react' import {useCallback, useMemo, useState} from 'react'
import { import {
$Typed, $Typed,
AtUri, AtUri,
@@ -138,7 +138,7 @@ export function useGetPostThreadV2({
// - sort down muted // - sort down muted
// - sort down blurred? // - sort down blurred?
const filtered = filterAndSort(query.data?.thread || [], { const {items, insertReplies} = useThread(query.data?.thread || [], {
hasSession, hasSession,
params, params,
threadgateHiddenReplies: mergeThreadgateHiddenReplies( threadgateHiddenReplies: mergeThreadgateHiddenReplies(
@@ -151,9 +151,10 @@ export function useGetPostThreadV2({
return { return {
...query, ...query,
data: { data: {
slices: filtered, slices: items,
threadgate: query.data?.threadgate, threadgate: query.data?.threadgate,
}, },
insertReplies,
} }
} }
@@ -265,7 +266,7 @@ function getBranch(
} }
} }
export function filterAndSort( export function useThread(
thread: AppBskyFeedGetPostThreadV2.OutputSchema['thread'], thread: AppBskyFeedGetPostThreadV2.OutputSchema['thread'],
{ {
hasSession, hasSession,
@@ -280,6 +281,136 @@ export function filterAndSort(
moderationOpts: ModerationOpts moderationOpts: ModerationOpts
shownHiddenReplyKinds: Set<HiddenReplyKind> shownHiddenReplyKinds: Set<HiddenReplyKind>
}, },
) {
const isTreeView = params.view === 'tree'
const [shadowSlices, setShadowSlices] = useState<
Map<string, AppBskyFeedDefs.ThreadItemPost[][]>
>(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<string>
moderationOpts: ModerationOpts
},
) { ) {
const slices: Slice[] = [] const slices: Slice[] = []
const hidden: 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--) { parentTraversal: for (let pi = i - 1; pi >= 0; pi--) {
const parentOneDown = thread[pi + 1] const parentOneDown = thread[pi + 1]
const parent = thread[pi] const parent = thread[pi]
@@ -436,44 +560,11 @@ export function filterAndSort(
} }
} }
const showMuted = shownHiddenReplyKinds.has(HiddenReplyKind.Muted) return {
const showHidden = shownHiddenReplyKinds.has(HiddenReplyKind.Hidden) slices,
hidden,
if (hidden.length) { muted,
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
} }
export function getModerationState(moderation: ModerationDecision) { export function getModerationState(moderation: ModerationDecision) {
+4 -25
View File
@@ -127,10 +127,9 @@ export function Inner({uri}: {uri: string | undefined}) {
setTreeViewEnabled, setTreeViewEnabled,
} = useThreadPreferences() } = useThreadPreferences()
const [showHiddenReplies, setShowHiddenReplies] = useState<HiddenReplyKind | null>(null)
const [shownHiddenReplyKinds, setShownHiddenReplyKinds] = useState<Set<HiddenReplyKind>>(new Set()) const [shownHiddenReplyKinds, setShownHiddenReplyKinds] = useState<Set<HiddenReplyKind>>(new Set())
const {isFetching, isPlaceholderData, error, data, refetch} = useGetPostThreadV2({ const {isFetching, isPlaceholderData, error, data, refetch, insertReplies} = useGetPostThreadV2({
uri, uri,
enabled: isThreadPreferencesLoaded, enabled: isThreadPreferencesLoaded,
params: { params: {
@@ -146,36 +145,14 @@ export function Inner({uri}: {uri: string | undefined}) {
const ref = useRef<ListMethods>(null) const ref = useRef<ListMethods>(null)
const layoutHeaderRef = useRef<View | null>(null) const layoutHeaderRef = useRef<View | null>(null)
const anchorPostRef = useRef<View | null>(null) const anchorPostRef = useRef<View | null>(null)
// TODO
const [justPostedUris, setJustPostedUris] = useState(
() => new Set<string>(),
)
const onPostReply = useCallback(
(postUri: string | undefined) => {
refetch()
if (postUri) {
setJustPostedUris(set => {
const nextSet = new Set(set)
nextSet.add(postUri)
return nextSet
})
}
},
[refetch],
)
const optimisticOnPostReply = ({ const optimisticOnPostReply = ({
post, post,
}: { }: {
post: AppBskyFeedDefs.ThreadItemPost, post: AppBskyFeedDefs.ThreadItemPost,
}) => (_: any, posts: AppBskyFeedDefs.ThreadItemPost[]) => { }) => (_: any, posts: AppBskyFeedDefs.ThreadItemPost[]) => {
const parentDepth = post.depth
if (posts.length) { if (posts.length) {
for (let i = 0; i < posts.length; i++) { insertReplies(post.uri, posts)
const p = posts[i]
p.depth = parentDepth + 1 + i
}
} }
} }
@@ -195,6 +172,7 @@ export function Inner({uri}: {uri: string | undefined}) {
embed: post.embed, embed: post.embed,
moderation: anchorPost.moderation, moderation: anchorPost.moderation,
}, },
// @ts-expect-error TODO
onPost: optimisticOnPostReply({post:anchorPost.slice}), onPost: optimisticOnPostReply({post:anchorPost.slice}),
}) })
} }
@@ -223,6 +201,7 @@ export function Inner({uri}: {uri: string | undefined}) {
overrideBlur={ overrideBlur={
shownHiddenReplyKinds.has(HiddenReplyKind.Muted) && item.slice.depth > 0 shownHiddenReplyKinds.has(HiddenReplyKind.Muted) && item.slice.depth > 0
} }
// @ts-expect-error TODO
onPostReply={optimisticOnPostReply({post:item.slice})} onPostReply={optimisticOnPostReply({post:item.slice})}
hideTopBorder={index === 0} // && !item.slice.isParentLoading} // TODO hideTopBorder={index === 0} // && !item.slice.isParentLoading} // TODO
/> />