about summary refs log tree commit diff
path: root/src/view/com/post-thread
diff options
context:
space:
mode:
authorEric Bailey <git@esb.lol>2024-08-21 21:20:45 -0500
committerGitHub <noreply@github.com>2024-08-21 19:20:45 -0700
commit6616a6467ec53aa71e5f823c2d8c46dc01442703 (patch)
tree5e49d6916bc9b9fc71a475cf0d02f169c744bf59 /src/view/com/post-thread
parent56ab5e177fa2b24d0e5d9d969aa37532b96128da (diff)
downloadvoidsky-6616a6467ec53aa71e5f823c2d8c46dc01442703.tar.zst
Detached QPs and hidden replies (#4878)
Co-authored-by: Hailey <me@haileyok.com>
Diffstat (limited to 'src/view/com/post-thread')
-rw-r--r--src/view/com/post-thread/PostQuotes.tsx5
-rw-r--r--src/view/com/post-thread/PostThread.tsx65
-rw-r--r--src/view/com/post-thread/PostThreadItem.tsx34
3 files changed, 91 insertions, 13 deletions
diff --git a/src/view/com/post-thread/PostQuotes.tsx b/src/view/com/post-thread/PostQuotes.tsx
index d573d27a1..f91a041d7 100644
--- a/src/view/com/post-thread/PostQuotes.tsx
+++ b/src/view/com/post-thread/PostQuotes.tsx
@@ -10,7 +10,6 @@ import {useLingui} from '@lingui/react'
 import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped'
 import {cleanError} from '#/lib/strings/errors'
 import {logger} from '#/logger'
-import {isWeb} from '#/platform/detection'
 import {useModerationOpts} from '#/state/preferences/moderation-opts'
 import {usePostQuotesQuery} from '#/state/queries/post-quotes'
 import {useResolveUriQuery} from '#/state/queries/resolve-uri'
@@ -25,16 +24,14 @@ import {List} from '../util/List'
 
 function renderItem({
   item,
-  index,
 }: {
   item: {
     post: AppBskyFeedDefs.PostView
     moderation: ModerationDecision
     record: AppBskyFeedPost.Record
   }
-  index: number
 }) {
-  return <Post post={item.post} hideTopBorder={index === 0 && !isWeb} />
+  return <Post post={item.post} />
 }
 
 function keyExtractor(item: {
diff --git a/src/view/com/post-thread/PostThread.tsx b/src/view/com/post-thread/PostThread.tsx
index c64be8d67..bd778fd98 100644
--- a/src/view/com/post-thread/PostThread.tsx
+++ b/src/view/com/post-thread/PostThread.tsx
@@ -3,7 +3,12 @@ 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} from '@atproto/api'
+import {
+  AppBskyFeedDefs,
+  AppBskyFeedPost,
+  AppBskyFeedThreadgate,
+  AtUri,
+} from '@atproto/api'
 import {msg, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 
@@ -23,6 +28,7 @@ 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 {useInitialNumToRender} from 'lib/hooks/useInitialNumToRender'
@@ -113,6 +119,28 @@ 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 {data: threadgateRecord} = useThreadgateRecordQuery({
+    /**
+     * If the user is the OP and the root post has a threadgate, we should load
+     * the threadgate record. Otherwise, fallback to initialData, which is taken
+     * from the response from `getPostThread`.
+     */
+    enabled: Boolean(isOP && rootPostUri),
+    postUri: rootPostUri,
+    initialData: rootPost?.threadgate?.record as
+      | AppBskyFeedThreadgate.Record
+      | undefined,
+  })
 
   const moderationOpts = useModerationOpts()
   const isNoPwi = React.useMemo(() => {
@@ -167,6 +195,9 @@ 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(
@@ -175,11 +206,13 @@ export function PostThread({uri}: {uri: string | undefined}) {
         threadModerationCache,
         currentDid,
         justPostedUris,
+        threadgateRecordHiddenReplies,
       ),
-      !!currentDid,
+      currentDid,
       treeView,
       threadModerationCache,
       hiddenRepliesState !== HiddenRepliesState.Hide,
+      threadgateRecordHiddenReplies,
     )
   }, [
     thread,
@@ -189,6 +222,7 @@ export function PostThread({uri}: {uri: string | undefined}) {
     threadModerationCache,
     hiddenRepliesState,
     justPostedUris,
+    threadgateRecord,
   ])
 
   const error = React.useMemo(() => {
@@ -425,6 +459,7 @@ export function PostThread({uri}: {uri: string | undefined}) {
           <PostThreadItem
             post={item.post}
             record={item.record}
+            threadgateRecord={threadgateRecord ?? undefined}
             moderation={threadModerationCache.get(item)}
             treeView={treeView}
             depth={item.ctx.depth}
@@ -545,23 +580,25 @@ function isThreadBlocked(v: unknown): v is ThreadBlocked {
 
 function createThreadSkeleton(
   node: ThreadNode,
-  hasSession: boolean,
+  currentDid: string | undefined,
   treeView: boolean,
   modCache: ThreadModerationCache,
   showHiddenReplies: boolean,
+  threadgateRecordHiddenReplies: Set<string>,
 ): ThreadSkeletonParts | null {
   if (!node) return null
 
   return {
-    parents: Array.from(flattenThreadParents(node, hasSession)),
+    parents: Array.from(flattenThreadParents(node, !!currentDid)),
     highlightedPost: node,
     replies: Array.from(
       flattenThreadReplies(
         node,
-        hasSession,
+        currentDid,
         treeView,
         modCache,
         showHiddenReplies,
+        threadgateRecordHiddenReplies,
       ),
     ),
   }
@@ -594,14 +631,15 @@ enum HiddenReplyType {
 
 function* flattenThreadReplies(
   node: ThreadNode,
-  hasSession: boolean,
+  currentDid: string | undefined,
   treeView: boolean,
   modCache: ThreadModerationCache,
   showHiddenReplies: boolean,
+  threadgateRecordHiddenReplies: Set<string>,
 ): Generator<YieldedItem, HiddenReplyType> {
   if (node.type === 'post') {
     // dont show pwi-opted-out posts to logged out users
-    if (!hasSession && hasPwiOptOut(node)) {
+    if (!currentDid && hasPwiOptOut(node)) {
       return HiddenReplyType.None
     }
 
@@ -616,6 +654,16 @@ function* flattenThreadReplies(
           return HiddenReplyType.Hidden
         }
       }
+
+      if (!showHiddenReplies) {
+        const hiddenByThreadgate = threadgateRecordHiddenReplies.has(
+          node.post.uri,
+        )
+        const authorIsViewer = node.post.author.did === currentDid
+        if (hiddenByThreadgate && !authorIsViewer) {
+          return HiddenReplyType.Hidden
+        }
+      }
     }
 
     if (!node.ctx.isHighlightedPost) {
@@ -627,10 +675,11 @@ function* flattenThreadReplies(
       for (const reply of node.replies) {
         let hiddenReply = yield* flattenThreadReplies(
           reply,
-          hasSession,
+          currentDid,
           treeView,
           modCache,
           showHiddenReplies,
+          threadgateRecordHiddenReplies,
         )
         if (hiddenReply > hiddenReplies) {
           hiddenReplies = hiddenReply
diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx
index 26a5f2f03..da187f5d9 100644
--- a/src/view/com/post-thread/PostThreadItem.tsx
+++ b/src/view/com/post-thread/PostThreadItem.tsx
@@ -3,6 +3,7 @@ import {StyleSheet, View} from 'react-native'
 import {
   AppBskyFeedDefs,
   AppBskyFeedPost,
+  AppBskyFeedThreadgate,
   AtUri,
   ModerationDecision,
   RichText as RichTextAPI,
@@ -29,6 +30,7 @@ import {isWeb} from 'platform/detection'
 import {useSession} from 'state/session'
 import {PostThreadFollowBtn} from 'view/com/post-thread/PostThreadFollowBtn'
 import {atoms as a} from '#/alf'
+import {AppModerationCause} from '#/components/Pills'
 import {RichText} from '#/components/RichText'
 import {ContentHider} from '../../../components/moderation/ContentHider'
 import {LabelsOnMyPost} from '../../../components/moderation/LabelsOnMe'
@@ -61,6 +63,7 @@ export function PostThreadItem({
   overrideBlur,
   onPostReply,
   hideTopBorder,
+  threadgateRecord,
 }: {
   post: AppBskyFeedDefs.PostView
   record: AppBskyFeedPost.Record
@@ -77,6 +80,7 @@ export function PostThreadItem({
   overrideBlur: boolean
   onPostReply: (postUri: string | undefined) => void
   hideTopBorder?: boolean
+  threadgateRecord?: AppBskyFeedThreadgate.Record
 }) {
   const postShadowed = usePostShadow(post)
   const richText = useMemo(
@@ -111,6 +115,7 @@ export function PostThreadItem({
         overrideBlur={overrideBlur}
         onPostReply={onPostReply}
         hideTopBorder={hideTopBorder}
+        threadgateRecord={threadgateRecord}
       />
     )
   }
@@ -154,6 +159,7 @@ let PostThreadItemLoaded = ({
   overrideBlur,
   onPostReply,
   hideTopBorder,
+  threadgateRecord,
 }: {
   post: Shadow<AppBskyFeedDefs.PostView>
   record: AppBskyFeedPost.Record
@@ -171,6 +177,7 @@ let PostThreadItemLoaded = ({
   overrideBlur: boolean
   onPostReply: (postUri: string | undefined) => void
   hideTopBorder?: boolean
+  threadgateRecord?: AppBskyFeedThreadgate.Record
 }): React.ReactNode => {
   const pal = usePalette('default')
   const {_} = useLingui()
@@ -199,6 +206,24 @@ let PostThreadItemLoaded = ({
     return makeProfileLink(post.author, 'post', urip.rkey, 'reposted-by')
   }, [post.uri, post.author])
   const repostsTitle = _(msg`Reposts of this post`)
+  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
+      ? [
+          {
+            type: 'reply-hidden',
+            source: {type: 'user', did: new AtUri(threadgateRecord.post).host},
+            priority: 6,
+          },
+        ]
+      : []
+  }, [post, threadgateRecord, currentAccount?.did])
   const quotesHref = React.useMemo(() => {
     const urip = new AtUri(post.uri)
     return makeProfileLink(post.author, 'post', urip.rkey, 'quotes')
@@ -320,6 +345,7 @@ let PostThreadItemLoaded = ({
                 size="lg"
                 includeMute
                 style={[a.pt_2xs, a.pb_sm]}
+                additionalCauses={additionalPostAlerts}
               />
               {richText?.text ? (
                 <View
@@ -420,6 +446,7 @@ let PostThreadItemLoaded = ({
                 onPressReply={onPressReply}
                 onPostReply={onPostReply}
                 logContext="PostThreadItem"
+                threadgateRecord={threadgateRecord}
               />
             </View>
           </View>
@@ -540,6 +567,7 @@ let PostThreadItemLoaded = ({
               <PostAlerts
                 modui={moderation.ui('contentList')}
                 style={[a.pt_2xs, a.pb_2xs]}
+                additionalCauses={additionalPostAlerts}
               />
               {richText?.text ? (
                 <View style={styles.postTextContainer}>
@@ -571,6 +599,7 @@ let PostThreadItemLoaded = ({
                 richText={richText}
                 onPressReply={onPressReply}
                 logContext="PostThreadItem"
+                threadgateRecord={threadgateRecord}
               />
             </View>
           </View>
@@ -677,6 +706,7 @@ function ExpandedPostDetails({
   const pal = usePalette('default')
   const {_} = useLingui()
   const openLink = useOpenLink()
+  const isRootPost = !('reply' in post.record)
 
   const onTranslatePress = React.useCallback(() => {
     openLink(translatorUrl)
@@ -693,7 +723,9 @@ function ExpandedPostDetails({
         s.mb10,
       ]}>
       <Text style={[a.text_sm, pal.textLight]}>{niceDate(post.indexedAt)}</Text>
-      <WhoCanReply post={post} isThreadAuthor={isThreadAuthor} />
+      {isRootPost && (
+        <WhoCanReply post={post} isThreadAuthor={isThreadAuthor} />
+      )}
       {needsTranslation && (
         <>
           <Text style={[a.text_sm, pal.textLight]}>&middot;</Text>