Detached QPs and hidden replies (#4878)
Co-authored-by: Hailey <me@haileyok.com>
This commit is contained in:
parent
56ab5e177f
commit
6616a6467e
41 changed files with 2584 additions and 622 deletions
|
@ -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: {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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]}>·</Text>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue