about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
authorEric Bailey <git@esb.lol>2024-08-23 14:35:48 -0500
committerGitHub <noreply@github.com>2024-08-23 12:35:48 -0700
commit425dd5f27feade1abff7a8e882929ca112376210 (patch)
tree4a279fe3f60b65a6f5c9774adb6e4d36ba1b046f /src
parent5ec8761b294b6a650af9ee286df6864d6fc4f25d (diff)
downloadvoidsky-425dd5f27feade1abff7a8e882929ca112376210.tar.zst
Optimistic hidden replies (#4977)
Diffstat (limited to 'src')
-rw-r--r--src/state/cache/post-shadow.ts13
-rw-r--r--src/state/queries/post-thread.ts16
-rw-r--r--src/state/queries/threadgate/index.ts19
-rw-r--r--src/state/queries/threadgate/util.ts20
-rw-r--r--src/state/threadgate-hidden-replies.tsx16
-rw-r--r--src/view/com/post-thread/PostThread.tsx41
-rw-r--r--src/view/com/post-thread/PostThreadItem.tsx19
-rw-r--r--src/view/com/posts/FeedItem.tsx43
-rw-r--r--src/view/com/util/forms/PostDropdownBtn.tsx12
9 files changed, 70 insertions, 129 deletions
diff --git a/src/state/cache/post-shadow.ts b/src/state/cache/post-shadow.ts
index 4d848ccc4..65300a8ef 100644
--- a/src/state/cache/post-shadow.ts
+++ b/src/state/cache/post-shadow.ts
@@ -21,7 +21,6 @@ export interface PostShadow {
   repostUri: string | undefined
   isDeleted: boolean
   embed: AppBskyEmbedRecord.View | AppBskyEmbedRecordWithMedia.View | undefined
-  threadgateView: AppBskyFeedDefs.ThreadgateView | undefined
 }
 
 export const POST_TOMBSTONE = Symbol('PostTombstone')
@@ -105,16 +104,6 @@ function mergeShadow(
     }
   }
 
-  let threadgateView: typeof post.threadgate
-  if ('threadgateView' in shadow && !post.threadgate) {
-    if (
-      AppBskyFeedDefs.isThreadgateView(shadow.threadgateView) ||
-      shadow.threadgateView === undefined
-    ) {
-      threadgateView = shadow.threadgateView
-    }
-  }
-
   return castAsShadow({
     ...post,
     embed: embed || post.embed,
@@ -125,8 +114,6 @@ function mergeShadow(
       like: 'likeUri' in shadow ? shadow.likeUri : post.viewer?.like,
       repost: 'repostUri' in shadow ? shadow.repostUri : post.viewer?.repost,
     },
-    // always prefer real post data
-    threadgate: post.threadgate || threadgateView,
   })
 }
 
diff --git a/src/state/queries/post-thread.ts b/src/state/queries/post-thread.ts
index 2c4a36c01..9d650024a 100644
--- a/src/state/queries/post-thread.ts
+++ b/src/state/queries/post-thread.ts
@@ -88,7 +88,10 @@ export type ThreadModerationCache = WeakMap<ThreadNode, ModerationDecision>
 export function usePostThreadQuery(uri: string | undefined) {
   const queryClient = useQueryClient()
   const agent = useAgent()
-  return useQuery<ThreadNode, Error>({
+  return useQuery<
+    {thread: ThreadNode; threadgate?: AppBskyFeedDefs.ThreadgateView},
+    Error
+  >({
     gcTime: 0,
     queryKey: RQKEY(uri || ''),
     async queryFn() {
@@ -99,16 +102,21 @@ export function usePostThreadQuery(uri: string | undefined) {
       if (res.success) {
         const thread = responseToThreadNodes(res.data.thread)
         annotateSelfThread(thread)
-        return thread
+        return {
+          thread,
+          threadgate: res.data.threadgate as
+            | AppBskyFeedDefs.ThreadgateView
+            | undefined,
+        }
       }
-      return {type: 'unknown', uri: uri!}
+      return {thread: {type: 'unknown', uri: uri!}}
     },
     enabled: !!uri,
     placeholderData: () => {
       if (!uri) return
       const post = findPostInQueryData(queryClient, uri)
       if (post) {
-        return post
+        return {thread: post}
       }
       return undefined
     },
diff --git a/src/state/queries/threadgate/index.ts b/src/state/queries/threadgate/index.ts
index faa166e2c..8aa932081 100644
--- a/src/state/queries/threadgate/index.ts
+++ b/src/state/queries/threadgate/index.ts
@@ -9,12 +9,10 @@ import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'
 
 import {networkRetry, retry} from '#/lib/async/retry'
 import {until} from '#/lib/async/until'
-import {updatePostShadow} from '#/state/cache/post-shadow'
 import {STALE} from '#/state/queries'
 import {RQKEY_ROOT as postThreadQueryKeyRoot} from '#/state/queries/post-thread'
 import {ThreadgateAllowUISetting} from '#/state/queries/threadgate/types'
 import {
-  createTempThreadgateView,
   createThreadgateRecord,
   mergeThreadgateRecords,
   threadgateAllowUISettingToAllowRecordValue,
@@ -33,18 +31,16 @@ export const createThreadgateRecordQueryKey = (uri: string) => [
 ]
 
 export function useThreadgateRecordQuery({
-  enabled,
   postUri,
   initialData,
 }: {
-  enabled?: boolean
   postUri?: string
   initialData?: AppBskyFeedThreadgate.Record
 } = {}) {
   const agent = useAgent()
 
   return useQuery({
-    enabled: enabled ?? !!postUri,
+    enabled: !!postUri,
     queryKey: createThreadgateRecordQueryKey(postUri || ''),
     placeholderData: initialData,
     staleTime: STALE.MINUTES.ONE,
@@ -344,26 +340,17 @@ export function useToggleReplyVisibilityMutation() {
         }
       })
     },
-    onSuccess(_, {postUri, replyUri}) {
-      updatePostShadow(queryClient, postUri, {
-        threadgateView: createTempThreadgateView({
-          postUri,
-          hiddenReplies: [replyUri],
-        }),
-      })
+    onSuccess() {
       queryClient.invalidateQueries({
         queryKey: [threadgateRecordQueryKeyRoot],
       })
     },
-    onError(_, {postUri, replyUri, action}) {
+    onError(_, {replyUri, action}) {
       if (action === 'hide') {
         hiddenReplies.removeHiddenReplyUri(replyUri)
       } else if (action === 'show') {
         hiddenReplies.addHiddenReplyUri(replyUri)
       }
-      updatePostShadow(queryClient, postUri, {
-        threadgateView: undefined,
-      })
     },
   })
 }
diff --git a/src/state/queries/threadgate/util.ts b/src/state/queries/threadgate/util.ts
index 35c33875e..09ae0a0c1 100644
--- a/src/state/queries/threadgate/util.ts
+++ b/src/state/queries/threadgate/util.ts
@@ -139,23 +139,3 @@ export function createThreadgateRecord(
     hiddenReplies: threadgate.hiddenReplies || [],
   }
 }
-
-export function createTempThreadgateView({
-  postUri,
-  hiddenReplies,
-}: Pick<AppBskyFeedThreadgate.Record, 'hiddenReplies'> & {
-  postUri: string
-}): AppBskyFeedDefs.ThreadgateView {
-  const record: AppBskyFeedThreadgate.Record = {
-    $type: 'app.bsky.feed.threadgate',
-    post: postUri,
-    allow: undefined,
-    hiddenReplies,
-    createdAt: new Date().toISOString(),
-  }
-  return {
-    $type: 'app.bsky.feed.defs#threadgateView',
-    uri: postUri,
-    record,
-  }
-}
diff --git a/src/state/threadgate-hidden-replies.tsx b/src/state/threadgate-hidden-replies.tsx
index 06fc22366..60806f570 100644
--- a/src/state/threadgate-hidden-replies.tsx
+++ b/src/state/threadgate-hidden-replies.tsx
@@ -1,4 +1,5 @@
 import React from 'react'
+import {AppBskyFeedThreadgate} from '@atproto/api'
 
 type StateContext = {
   uris: Set<string>
@@ -67,3 +68,18 @@ export function useThreadgateHiddenReplyUris() {
 export function useThreadgateHiddenReplyUrisAPI() {
   return React.useContext(ApiContext)
 }
+
+export function useMergedThreadgateHiddenReplies({
+  threadgateRecord,
+}: {
+  threadgateRecord?: AppBskyFeedThreadgate.Record
+}) {
+  const {uris, recentlyUnhiddenUris} = useThreadgateHiddenReplyUris()
+  return React.useMemo(() => {
+    const set = new Set([...(threadgateRecord?.hiddenReplies || []), ...uris])
+    for (const uri of recentlyUnhiddenUris) {
+      set.delete(uri)
+    }
+    return set
+  }, [uris, recentlyUnhiddenUris, threadgateRecord])
+}
diff --git a/src/view/com/post-thread/PostThread.tsx b/src/view/com/post-thread/PostThread.tsx
index b3196f9ba..d5740f870 100644
--- a/src/view/com/post-thread/PostThread.tsx
+++ b/src/view/com/post-thread/PostThread.tsx
@@ -3,12 +3,7 @@ import {StyleSheet, useWindowDimensions, View} from 'react-native'
 import {runOnJS} from 'react-native-reanimated'
 import Animated from 'react-native-reanimated'
 import {useSafeAreaInsets} from 'react-native-safe-area-context'
-import {
-  AppBskyFeedDefs,
-  AppBskyFeedPost,
-  AppBskyFeedThreadgate,
-  AtUri,
-} from '@atproto/api'
+import {AppBskyFeedDefs, AppBskyFeedThreadgate} from '@atproto/api'
 import {msg, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 
@@ -28,9 +23,9 @@ import {
   usePostThreadQuery,
 } from '#/state/queries/post-thread'
 import {usePreferencesQuery} from '#/state/queries/preferences'
-import {useThreadgateRecordQuery} from '#/state/queries/threadgate'
 import {useSession} from '#/state/session'
 import {useComposerControls} from '#/state/shell'
+import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies'
 import {useInitialNumToRender} from 'lib/hooks/useInitialNumToRender'
 import {useMinimalShellFabTransform} from 'lib/hooks/useMinimalShellTransform'
 import {useSetTitle} from 'lib/hooks/useSetTitle'
@@ -108,7 +103,7 @@ export function PostThread({uri}: {uri: string | undefined}) {
     isError: isThreadError,
     error: threadError,
     refetch,
-    data: thread,
+    data: {thread, threadgate} = {},
   } = usePostThreadQuery(uri)
 
   const treeView = React.useMemo(
@@ -119,26 +114,11 @@ export function PostThread({uri}: {uri: string | undefined}) {
   )
   const rootPost = thread?.type === 'post' ? thread.post : undefined
   const rootPostRecord = thread?.type === 'post' ? thread.record : undefined
-  const replyRef =
-    rootPostRecord && AppBskyFeedPost.isRecord(rootPostRecord)
-      ? rootPostRecord.reply
-      : undefined
-  const rootPostUri = replyRef ? replyRef.root.uri : rootPost?.uri
-
-  const isOP =
-    currentAccount &&
-    rootPostUri &&
-    currentAccount?.did === new AtUri(rootPostUri).host
-  const initialThreadgateRecord = rootPost?.threadgate?.record as
+  const threadgateRecord = threadgate?.record as
     | AppBskyFeedThreadgate.Record
     | undefined
-  const {data: threadgateRecord} = useThreadgateRecordQuery({
-    /**
-     * If the user is the OP and we have a root post, fetch the threadgate.
-     */
-    enabled: Boolean(isOP && rootPostUri),
-    postUri: rootPostUri,
-    initialData: initialThreadgateRecord,
+  const threadgateHiddenReplies = useMergedThreadgateHiddenReplies({
+    threadgateRecord,
   })
 
   const moderationOpts = useModerationOpts()
@@ -194,9 +174,6 @@ export function PostThread({uri}: {uri: string | undefined}) {
   const skeleton = React.useMemo(() => {
     const threadViewPrefs = preferences?.threadViewPrefs
     if (!threadViewPrefs || !thread) return null
-    const threadgateRecordHiddenReplies = new Set<string>(
-      threadgateRecord?.hiddenReplies || [],
-    )
 
     return createThreadSkeleton(
       sortThread(
@@ -205,13 +182,13 @@ export function PostThread({uri}: {uri: string | undefined}) {
         threadModerationCache,
         currentDid,
         justPostedUris,
-        threadgateRecordHiddenReplies,
+        threadgateHiddenReplies,
       ),
       currentDid,
       treeView,
       threadModerationCache,
       hiddenRepliesState !== HiddenRepliesState.Hide,
-      threadgateRecordHiddenReplies,
+      threadgateHiddenReplies,
     )
   }, [
     thread,
@@ -221,7 +198,7 @@ export function PostThread({uri}: {uri: string | undefined}) {
     threadModerationCache,
     hiddenRepliesState,
     justPostedUris,
-    threadgateRecord,
+    threadgateHiddenReplies,
   ])
 
   const error = React.useMemo(() => {
diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx
index f2cd8e85a..f2a8be598 100644
--- a/src/view/com/post-thread/PostThreadItem.tsx
+++ b/src/view/com/post-thread/PostThreadItem.tsx
@@ -17,6 +17,7 @@ import {useLanguagePrefs} from '#/state/preferences'
 import {useOpenLink} from '#/state/preferences/in-app-browser'
 import {ThreadPost} from '#/state/queries/post-thread'
 import {useComposerControls} from '#/state/shell/composer'
+import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies'
 import {MAX_POST_LINES} from 'lib/constants'
 import {usePalette} from 'lib/hooks/usePalette'
 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
@@ -206,24 +207,22 @@ let PostThreadItemLoaded = ({
     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[] = React.useMemo(() => {
-    const isPostHiddenByThreadgate = threadgateRecord?.hiddenReplies?.includes(
-      post.uri,
-    )
-    const isControlledByViewer =
-      threadgateRecord &&
-      new AtUri(threadgateRecord.post).host === currentAccount?.did
-    if (!isControlledByViewer) return []
-    return threadgateRecord && isPostHiddenByThreadgate
+    const isPostHiddenByThreadgate = threadgateHiddenReplies.has(post.uri)
+    const isControlledByViewer = new AtUri(rootUri).host === currentAccount?.did
+    return isControlledByViewer && isPostHiddenByThreadgate
       ? [
           {
             type: 'reply-hidden',
-            source: {type: 'user', did: new AtUri(threadgateRecord.post).host},
+            source: {type: 'user', did: currentAccount?.did},
             priority: 6,
           },
         ]
       : []
-  }, [post, threadgateRecord, currentAccount?.did])
+  }, [post, currentAccount?.did, threadgateHiddenReplies, rootUri])
   const quotesHref = React.useMemo(() => {
     const urip = new AtUri(post.uri)
     return makeProfileLink(post.author, 'post', urip.rkey, 'quotes')
diff --git a/src/view/com/posts/FeedItem.tsx b/src/view/com/posts/FeedItem.tsx
index e90e8b885..a5714fafe 100644
--- a/src/view/com/posts/FeedItem.tsx
+++ b/src/view/com/posts/FeedItem.tsx
@@ -22,7 +22,7 @@ import {POST_TOMBSTONE, Shadow, usePostShadow} from '#/state/cache/post-shadow'
 import {useFeedFeedbackContext} from '#/state/feed-feedback'
 import {useSession} from '#/state/session'
 import {useComposerControls} from '#/state/shell/composer'
-import {useThreadgateHiddenReplyUris} from '#/state/threadgate-hidden-replies'
+import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies'
 import {isReasonFeedSource, ReasonFeedSource} from 'lib/api/feed/types'
 import {MAX_POST_LINES} from 'lib/constants'
 import {usePalette} from 'lib/hooks/usePalette'
@@ -227,6 +227,10 @@ let FeedItemInner = ({
     AppBskyFeedDefs.isReasonRepost(reason) &&
     reason.by.did === currentAccount?.did
 
+  /**
+   * If `post[0]` in this slice is the actual root post (not an orphan thread),
+   * then we may have a threadgate record to reference
+   */
   const threadgateRecord = AppBskyFeedThreadgate.isRecord(
     rootPost.threadgate?.record,
   )
@@ -422,41 +426,26 @@ let PostContent = ({
   const [limitLines, setLimitLines] = useState(
     () => countLines(richText.text) >= MAX_POST_LINES,
   )
-  const {uris: hiddenReplyUris, recentlyUnhiddenUris} =
-    useThreadgateHiddenReplyUris()
+  const threadgateHiddenReplies = useMergedThreadgateHiddenReplies({
+    threadgateRecord,
+  })
   const additionalPostAlerts: AppModerationCause[] = React.useMemo(() => {
-    const isPostHiddenByHiddenReplyCache = hiddenReplyUris.has(post.uri)
-    const isPostHiddenByThreadgate =
-      !recentlyUnhiddenUris.has(post.uri) &&
-      !!threadgateRecord?.hiddenReplies?.includes(post.uri)
-    const isHidden = isPostHiddenByHiddenReplyCache || isPostHiddenByThreadgate
+    const isPostHiddenByThreadgate = threadgateHiddenReplies.has(post.uri)
+    const rootPostUri = AppBskyFeedPost.isRecord(post.record)
+      ? post.record?.reply?.root?.uri || post.uri
+      : undefined
     const isControlledByViewer =
-      isPostHiddenByHiddenReplyCache ||
-      (threadgateRecord &&
-        new AtUri(threadgateRecord.post).host === currentAccount?.did)
-    if (!isControlledByViewer) return []
-    const alertSource =
-      threadgateRecord && isPostHiddenByThreadgate
-        ? new AtUri(threadgateRecord.post).host
-        : isPostHiddenByHiddenReplyCache
-        ? currentAccount?.did
-        : undefined
-    return isHidden && alertSource
+      rootPostUri && new AtUri(rootPostUri).host === currentAccount?.did
+    return isControlledByViewer && isPostHiddenByThreadgate
       ? [
           {
             type: 'reply-hidden',
-            source: {type: 'user', did: alertSource},
+            source: {type: 'user', did: currentAccount?.did},
             priority: 6,
           },
         ]
       : []
-  }, [
-    post,
-    hiddenReplyUris,
-    recentlyUnhiddenUris,
-    threadgateRecord,
-    currentAccount?.did,
-  ])
+  }, [post, currentAccount?.did, threadgateHiddenReplies])
 
   const onPressShowMore = React.useCallback(() => {
     setLimitLines(false)
diff --git a/src/view/com/util/forms/PostDropdownBtn.tsx b/src/view/com/util/forms/PostDropdownBtn.tsx
index b293b0dff..03b6dd233 100644
--- a/src/view/com/util/forms/PostDropdownBtn.tsx
+++ b/src/view/com/util/forms/PostDropdownBtn.tsx
@@ -37,7 +37,7 @@ import {useToggleQuoteDetachmentMutation} from '#/state/queries/postgate'
 import {getMaybeDetachedQuoteEmbed} from '#/state/queries/postgate/util'
 import {useToggleReplyVisibilityMutation} from '#/state/queries/threadgate'
 import {useSession} from '#/state/session'
-import {useThreadgateHiddenReplyUris} from '#/state/threadgate-hidden-replies'
+import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies'
 import {getCurrentRoute} from 'lib/routes/helpers'
 import {shareUrl} from 'lib/sharing'
 import {toShareUrl} from 'lib/strings/url-helpers'
@@ -124,8 +124,6 @@ let PostDropdownBtn = ({
   const hideReplyConfirmControl = useDialogControl()
   const {mutateAsync: toggleReplyVisibility} =
     useToggleReplyVisibilityMutation()
-  const {uris: hiddenReplies, recentlyUnhiddenUris} =
-    useThreadgateHiddenReplyUris()
 
   const postUri = post.uri
   const postCid = post.cid
@@ -147,10 +145,10 @@ let PostDropdownBtn = ({
   const isPostHidden = hiddenPosts && hiddenPosts.includes(postUri)
   const isAuthor = postAuthor.did === currentAccount?.did
   const isRootPostAuthor = new AtUri(rootUri).host === currentAccount?.did
-  const isReplyHiddenByThreadgate =
-    hiddenReplies.has(postUri) ||
-    (!recentlyUnhiddenUris.has(postUri) &&
-      threadgateRecord?.hiddenReplies?.includes(postUri))
+  const threadgateHiddenReplies = useMergedThreadgateHiddenReplies({
+    threadgateRecord,
+  })
+  const isReplyHiddenByThreadgate = threadgateHiddenReplies.has(postUri)
 
   const {mutateAsync: toggleQuoteDetachment, isPending} =
     useToggleQuoteDetachmentMutation()