From 26e7d3155777fe20bab2529927f68570357a4c50 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Fri, 9 Aug 2024 15:04:01 -0500 Subject: [PATCH] Integrate postgates into ThreadgateEditor dialog --- src/components/WhoCanReply.tsx | 132 ++++++--- src/components/dialogs/ThreadgateEditor.tsx | 261 +++++++++++------- src/lib/api/index.ts | 54 +++- src/state/queries/postgate/index.ts | 25 ++ src/state/queries/postgate/util.ts | 12 +- src/state/queries/threadgate/util.ts | 2 +- src/view/com/composer/Composer.tsx | 19 +- .../com/composer/threadgate/ThreadgateBtn.tsx | 59 +++- src/view/com/post-thread/PostThreadItem.tsx | 27 +- 9 files changed, 424 insertions(+), 167 deletions(-) diff --git a/src/components/WhoCanReply.tsx b/src/components/WhoCanReply.tsx index 824a8de2cb..cb2f801766 100644 --- a/src/components/WhoCanReply.tsx +++ b/src/components/WhoCanReply.tsx @@ -3,6 +3,7 @@ import {Keyboard, StyleProp, View, ViewStyle} from 'react-native' import { AppBskyFeedDefs, AppBskyFeedGetPostThread, + AppBskyFeedPostgate, AppBskyGraphDefs, AtUri, BskyAgent, @@ -17,11 +18,13 @@ import {makeListLink, makeProfileLink} from '#/lib/routes/links' import {logger} from '#/logger' import {isNative} from '#/platform/detection' import {RQKEY_ROOT as POST_THREAD_RQKEY_ROOT} from '#/state/queries/post-thread' -import {updateThreadgateAllow} from '#/state/queries/threadgate' -import {threadgateRecordQueryKeyRoot} from '#/state/queries/threadgate' +import {useWritePostgateMutation} from '#/state/queries/postgate' +import {embeddingRules} from '#/state/queries/postgate/util' import { ThreadgateAllowUISetting, + threadgateRecordQueryKeyRoot, threadgateViewToAllowUISetting, + updateThreadgateAllow, } from '#/state/queries/threadgate' import {useAgent} from '#/state/session' import * as Toast from 'view/com/util/Toast' @@ -41,35 +44,47 @@ interface WhoCanReplyProps { post: AppBskyFeedDefs.PostView isThreadAuthor: boolean style?: StyleProp + postgate: AppBskyFeedPostgate.Record } -export function WhoCanReply({post, isThreadAuthor, style}: WhoCanReplyProps) { +export function WhoCanReply({ + post, + isThreadAuthor, + style, + postgate: initialPostgate, +}: WhoCanReplyProps) { const {_} = useLingui() const t = useTheme() const infoDialogControl = useDialogControl() const editDialogControl = useDialogControl() const agent = useAgent() const queryClient = useQueryClient() + const [isSaving, setIsSaving] = React.useState(false) + const {mutateAsync: writePostgateRecord} = useWritePostgateMutation() - const settings = React.useMemo( - () => threadgateViewToAllowUISetting(post.threadgate), - [post], + const [postgate, setPostgate] = React.useState(initialPostgate) + // TODO test if we get weird data back + const [settings, setSettings] = React.useState( + threadgateViewToAllowUISetting(post.threadgate), ) - const isRootPost = !('reply' in post.record) - if (!isRootPost) { - return null - } - if (!settings.length && !isThreadAuthor) { - return null - } - - const isEverybody = settings.length === 0 - const description = isEverybody - ? _(msg`Anyone can interact`) + const anyoneCanReply = + settings.length === 1 && settings[0].type === 'everybody' + const noOneCanReply = settings.length === 1 && settings[0].type === 'nobody' + const anyoneCanQuote = + !postgate.quotepostRules || postgate.quotepostRules.length === 0 + const noOneCanQuote = + postgate.quotepostRules?.length === 1 && + postgate.quotepostRules[0]?.$type === embeddingRules.disableRule.$type + const anyoneCanInteract = anyoneCanReply && anyoneCanQuote + const noOneCanInteract = noOneCanReply && noOneCanQuote + const description = anyoneCanInteract + ? _(msg`Anybody can interact`) + : noOneCanInteract + ? _(msg`Nobody can interact`) : _(msg`Interaction limited`) - const onPress = () => { + const onPressOpen = () => { if (isNative && Keyboard.isVisible()) { Keyboard.dismiss() } @@ -80,45 +95,94 @@ export function WhoCanReply({post, isThreadAuthor, style}: WhoCanReplyProps) { } } - const onEditConfirm = async (newSettings: ThreadgateAllowUISetting[]) => { - if (JSON.stringify(settings) === JSON.stringify(newSettings)) { - return - } + const onChangePostgate = React.useCallback( + (next: AppBskyFeedPostgate.Record) => { + setPostgate(next) + }, + [setPostgate], + ) + + const onChangeThreadgateAllowUISettings = React.useCallback( + (next: ThreadgateAllowUISetting[]) => { + setSettings(next) + }, + [setSettings], + ) + + const saveThreadgateAllowSettings = React.useCallback(async () => { try { await updateThreadgateAllow({ agent, postUri: post.uri, - allow: newSettings, + allow: settings, }) - // TODO await whenAppViewReady(agent, post.uri, res => { const thread = res.data.thread if (AppBskyFeedDefs.isThreadViewPost(thread)) { const fetchedSettings = threadgateViewToAllowUISetting( thread.post.threadgate, ) - return JSON.stringify(fetchedSettings) === JSON.stringify(newSettings) + return JSON.stringify(fetchedSettings) === JSON.stringify(settings) } return false }) - Toast.show(_(msg`Thread settings updated`)) + queryClient.invalidateQueries({ queryKey: [POST_THREAD_RQKEY_ROOT], }) queryClient.invalidateQueries({ queryKey: [threadgateRecordQueryKeyRoot], }) - } catch (err) { + } catch (e: any) { + logger.error('Failed to edit threadgate', {safeMessage: e.message}) Toast.show( _( msg`There was an issue. Please check your internet connection and try again.`, ), 'xmark', ) - logger.error('Failed to edit threadgate', {message: err}) } - } + }, [_, agent, post, settings, queryClient]) + + const savePostgateRecord = React.useCallback(async () => { + try { + await writePostgateRecord({postUri: post.uri, postgate}) + } catch (e: any) { + logger.error('Failed to save postgate', {safeMessage: e.message}) + Toast.show( + _( + msg`There was an issue. Please check your internet connection and try again.`, + ), + 'xmark', + ) + } + }, [_, post, postgate, writePostgateRecord]) + + const onSave = React.useCallback(async () => { + setIsSaving(true) + + try { + await Promise.all([saveThreadgateAllowSettings(), savePostgateRecord()]) + editDialogControl.close() + Toast.show(_(msg`Thread settings updated`)) + } catch (e: any) { + Toast.show( + _( + msg`There was an issue. Please check your internet connection and try again.`, + ), + 'xmark', + ) + } finally { + setIsSaving(false) + } + }, [ + _, + editDialogControl, + saveThreadgateAllowSettings, + savePostgateRecord, + setIsSaving, + ]) return ( <> @@ -126,7 +190,7 @@ export function WhoCanReply({post, isThreadAuthor, style}: WhoCanReplyProps) { label={ isThreadAuthor ? _(msg`Edit who can reply`) : _(msg`Who can reply`) } - onPress={onPress} + onPress={onPressOpen} hitSlop={HITSLOP_10}> {({hovered}) => ( @@ -154,9 +218,13 @@ export function WhoCanReply({post, isThreadAuthor, style}: WhoCanReplyProps) { {isThreadAuthor ? ( ) : ( void - onConfirmThreadgateUISettings?: (v: ThreadgateAllowUISetting[]) => void + onSave: () => void + isSaving: boolean + + postgate: AppBskyFeedPostgate.Record + onChangePostgate: (v: AppBskyFeedPostgate.Record) => void + + threadgateAllowUISettings: ThreadgateAllowUISetting[] + onChangeThreadgateAllowUISettings: (v: ThreadgateAllowUISetting[]) => void } export function ThreadgateEditorDialog({ control, - onChangeThreadgateUISettings, - onConfirmThreadgateUISettings, - threadgateUISettings, + ...rest }: Props & { control: Dialog.DialogControlProps }) { return ( - + ) } function DialogContent({ - onChangeThreadgateUISettings, - onConfirmThreadgateUISettings, - threadgateUISettings, + onSave, + isSaving, + postgate, + onChangePostgate, + threadgateAllowUISettings, + onChangeThreadgateAllowUISettings, }: Props) { + const t = useTheme() const {_} = useLingui() const control = Dialog.useDialogContext() const {data: lists} = useMyListsQuery('curate') - const [draft, setDraft] = React.useState(threadgateUISettings) - - const [prevThreadgateUISettings, setPrevThreadgateUISettings] = - React.useState(threadgateUISettings) - if (threadgateUISettings !== prevThreadgateUISettings) { - // New data flowed from above (e.g. due to update coming through). - setPrevThreadgateUISettings(threadgateUISettings) - setDraft(threadgateUISettings) // Reset draft. - } - - function updateThreadgate(nextThreadgate: ThreadgateAllowUISetting[]) { - setDraft(nextThreadgate) - onChangeThreadgateUISettings?.(nextThreadgate) - } - - const onPressEverybody = () => { - updateThreadgate([{type: 'everybody'}]) - } - - const onPressNobody = () => { - updateThreadgate([{type: 'nobody'}]) - } const onPressAudience = (setting: ThreadgateAllowUISetting) => { - // remove nobody - let newSelected: ThreadgateAllowUISetting[] = draft.filter( - v => v.type !== 'nobody', - ) + // remove boolean values + let newSelected: ThreadgateAllowUISetting[] = + threadgateAllowUISettings.filter( + v => v.type !== 'nobody' && v.type !== 'everybody', + ) // toggle const i = newSelected.findIndex(v => isEqual(v, setting)) if (i === -1) { @@ -81,79 +70,161 @@ function DialogContent({ } else { newSelected.splice(i, 1) } - updateThreadgate(newSelected) + + onChangeThreadgateAllowUISettings(newSelected) } - const doneLabel = onConfirmThreadgateUISettings ? _(msg`Save`) : _(msg`Done`) + const onChangeEmbeddingRules = React.useCallback( + (rules: AppBskyFeedPostgate.Record['quotepostRules']) => { + onChangePostgate( + createPostgateRecord({ + ...postgate, + quotepostRules: rules, + }), + ) + }, + [postgate, onChangePostgate], + ) + + const doneLabel = _(msg`Save`) return ( - Choose who can reply + Post interaction settings - - Either choose "Everybody" or "Nobody" - - - v.type === 'everybody')} - onPress={onPressEverybody} - style={{flex: 1}} - /> - v.type === 'nobody')} - onPress={onPressNobody} - style={{flex: 1}} - /> - - - Or combine these options: - - - v.type === 'mention')} - onPress={() => onPressAudience({type: 'mention'})} - /> - v.type === 'following')} - onPress={() => onPressAudience({type: 'following'})} - /> - {lists && lists.length > 0 - ? lists.map(list => ( - v.type === 'list' && v.list === list.uri) - } - onPress={() => - onPressAudience({type: 'list', list: list.uri}) - } - /> - )) - : // No loading states to avoid jumps for the common case (no lists) - null} + + + + Customize who can engage with this post. + + + + + + + Quote settings + + + + Allow quote posts from: + + + + onChangeEmbeddingRules([])} + style={{flex: 1}} + /> + v.$type === embeddingRules.disableRule.$type, + ), + )} + onPress={() => + onChangeEmbeddingRules([embeddingRules.disableRule]) + } + style={{flex: 1}} + /> + + + + + + + + Reply settings + + + + Allow replies from: + + + + v.type === 'everybody') + } + onPress={() => + onChangeThreadgateAllowUISettings([{type: 'everybody'}]) + } + style={{flex: 1}} + /> + v.type === 'nobody') + } + onPress={() => + onChangeThreadgateAllowUISettings([{type: 'nobody'}]) + } + style={{flex: 1}} + /> + + + + Or combine these options: + + + + v.type === 'mention') + } + onPress={() => onPressAudience({type: 'mention'})} + /> + v.type === 'following') + } + onPress={() => onPressAudience({type: 'following'})} + /> + {lists && lists.length > 0 + ? lists.map(list => ( + v.type === 'list' && v.list === list.uri, + ) + } + onPress={() => + onPressAudience({type: 'list', list: list.uri}) + } + /> + )) + : // No loading states to avoid jumps for the common case (no lists) + null} + + + + ) diff --git a/src/lib/api/index.ts b/src/lib/api/index.ts index 0196bb5c58..2d8bd633fd 100644 --- a/src/lib/api/index.ts +++ b/src/lib/api/index.ts @@ -3,6 +3,7 @@ import { AppBskyEmbedImages, AppBskyEmbedRecord, AppBskyEmbedRecordWithMedia, + AppBskyFeedPostgate, BskyAgent, ComAtprotoLabelDefs, ComAtprotoRepoUploadBlob, @@ -11,6 +12,7 @@ import { import {AtUri} from '@atproto/api' import {logger} from '#/logger' +import {writePostgateRecord} from '#/state/queries/postgate' import { createThreadgateRecord, ThreadgateAllowUISetting, @@ -65,7 +67,8 @@ interface PostOpts { extLink?: ExternalEmbedDraft images?: ImageModel[] labels?: string[] - threadgate?: ThreadgateAllowUISetting[] + threadgate: ThreadgateAllowUISetting[] + postgate: AppBskyFeedPostgate.Record onStateChange?: (state: string) => void langs?: string[] } @@ -253,7 +256,9 @@ export async function post(agent: BskyAgent, opts: PostOpts) { labels, }) } catch (e: any) { - console.error(`Failed to create post: ${e.toString()}`) + logger.error(`Failed to create post`, { + safeMessage: e.message, + }) if (isNetworkError(e)) { throw new Error( 'Post failed to upload. Please check your Internet connection and try again.', @@ -265,20 +270,41 @@ export async function post(agent: BskyAgent, opts: PostOpts) { try { // TODO: this needs to be batch-created with the post! - if (opts.threadgate?.length) { - await writeThreadgateRecord({ - agent, - postUri: res.uri, - threadgate: createThreadgateRecord({ - post: res.uri, - allow: threadgateAllowUISettingToAllowRecordValue(opts.threadgate), - }), - }) - } + await writeThreadgateRecord({ + agent, + postUri: res.uri, + threadgate: createThreadgateRecord({ + post: res.uri, + allow: threadgateAllowUISettingToAllowRecordValue(opts.threadgate), + }), + }) } catch (e: any) { - console.error(`Failed to create threadgate: ${e.toString()}`) + logger.error(`Failed to create threadgate`, { + context: 'composer', + safeMessage: e.message, + }) throw new Error( - 'Post reply-controls failed to be set. Your post was created but anyone can reply to it.', + 'Failed to save post interaction settings. Your post was created but users may be able to interact with it.', + ) + } + + try { + // TODO: this needs to be batch-created with the post! + await writePostgateRecord({ + agent, + postUri: res.uri, + postgate: { + ...opts.postgate, + post: res.uri, + }, + }) + } catch (e: any) { + logger.error(`Failed to create postgate`, { + context: 'composer', + safeMessage: e.message, + }) + throw new Error( + 'Failed to save post interaction settings. Your post was created but users may be able to interact with it.', ) } diff --git a/src/state/queries/postgate/index.ts b/src/state/queries/postgate/index.ts index b487ab4dda..afa8e03b0a 100644 --- a/src/state/queries/postgate/index.ts +++ b/src/state/queries/postgate/index.ts @@ -135,6 +135,31 @@ export function usePostgateQuery({postUri}: {postUri: string}) { }) } +export function useWritePostgateMutation() { + const agent = useAgent() + const queryClient = useQueryClient() + return useMutation({ + mutationFn: async ({ + postUri, + postgate, + }: { + postUri: string + postgate: AppBskyFeedPostgate.Record + }) => { + return writePostgateRecord({ + agent, + postUri, + postgate, + }) + }, + onSuccess(_, {postUri}) { + queryClient.invalidateQueries({ + queryKey: [createPostgateQueryKey(postUri)], + }) + }, + }) +} + export function useToggleQuoteDetachmentMutation() { const agent = useAgent() const queryClient = useQueryClient() diff --git a/src/state/queries/postgate/util.ts b/src/state/queries/postgate/util.ts index c351909695..ba821cf618 100644 --- a/src/state/queries/postgate/util.ts +++ b/src/state/queries/postgate/util.ts @@ -10,12 +10,10 @@ import {ViewRemoved} from '@atproto/api/dist/client/types/app/bsky/embed/record' export const POSTGATE_COLLECTION = 'app.bsky.feed.postgate' export function createPostgateRecord( - postgate: Partial, + postgate: Partial & { + post: AppBskyFeedPostgate.Record['post'] + }, ): AppBskyFeedPostgate.Record { - if (!postgate.post) { - throw new Error(`Cannot create a postgate record without a post URI`) - } - return { $type: POSTGATE_COLLECTION, createdAt: new Date().toISOString(), @@ -190,3 +188,7 @@ export function getMaybeDetachedQuoteEmbed({ } } } + +export const embeddingRules = { + disableRule: {$type: 'app.bsky.feed.postgate#disableRule'}, +} diff --git a/src/state/queries/threadgate/util.ts b/src/state/queries/threadgate/util.ts index f8896716c2..b410546fb4 100644 --- a/src/state/queries/threadgate/util.ts +++ b/src/state/queries/threadgate/util.ts @@ -24,7 +24,7 @@ export function threadgateViewToAllowUISetting( * for 'replies disabled' other than an empty array. */ if (!record || record.allow === undefined) { - return [] + return [{type: 'everybody'}] } if (record.allow.length === 0) { return [{type: 'nobody'}] diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx index 70bb5b2e97..ef238752b4 100644 --- a/src/view/com/composer/Composer.tsx +++ b/src/view/com/composer/Composer.tsx @@ -58,9 +58,11 @@ import { useLanguagePrefs, useLanguagePrefsApi, } from '#/state/preferences/languages' +import {createPostgateRecord} from '#/state/queries/postgate/util' import {useProfileQuery} from '#/state/queries/profile' import {Gif} from '#/state/queries/tenor' import {ThreadgateAllowUISetting} from '#/state/queries/threadgate' +import {threadgateViewToAllowUISetting} from '#/state/queries/threadgate/util' import {useUploadVideo} from '#/state/queries/video/video' import {useAgent, useSession} from '#/state/session' import {useComposerControls} from '#/state/shell/composer' @@ -184,7 +186,11 @@ export const ComposePost = observer(function ComposePost({ const {extLink, setExtLink} = useExternalLinkFetch({setQuote}) const [extGif, setExtGif] = useState() const [labels, setLabels] = useState([]) - const [threadgate, setThreadgate] = useState([]) + const [threadgateAllowUISettings, onChangeThreadgateAllowUISettings] = + useState( + threadgateViewToAllowUISetting(undefined), + ) + const [postgate, setPostgate] = useState(createPostgateRecord({post: ''})) const gallery = useMemo( () => new GalleryModel(initImageUris), @@ -334,7 +340,8 @@ export const ComposePost = observer(function ComposePost({ quote, extLink, labels, - threadgate, + threadgate: threadgateAllowUISettings, + postgate, onStateChange: setProcessingState, langs: toPostLanguages(langPrefs.postLanguage), }) @@ -664,8 +671,12 @@ export const ComposePost = observer(function ComposePost({ {replyTo ? null : ( )} diff --git a/src/view/com/composer/threadgate/ThreadgateBtn.tsx b/src/view/com/composer/threadgate/ThreadgateBtn.tsx index 2bf7f77014..cdd8ec7eef 100644 --- a/src/view/com/composer/threadgate/ThreadgateBtn.tsx +++ b/src/view/com/composer/threadgate/ThreadgateBtn.tsx @@ -1,10 +1,12 @@ import React from 'react' import {Keyboard, StyleProp, ViewStyle} from 'react-native' import Animated, {AnimatedStyle} from 'react-native-reanimated' +import {AppBskyFeedPostgate} from '@atproto/api' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' import {isNative} from '#/platform/detection' +import {embeddingRules} from '#/state/queries/postgate/util' import {ThreadgateAllowUISetting} from '#/state/queries/threadgate' import {useAnalytics} from 'lib/analytics/analytics' import {atoms as a, useTheme} from '#/alf' @@ -16,12 +18,18 @@ import {Earth_Stroke2_Corner0_Rounded as Earth} from '#/components/icons/Globe' import {Group3_Stroke2_Corner0_Rounded as Group} from '#/components/icons/Group' export function ThreadgateBtn({ - threadgate, - onChange, + postgate, + onChangePostgate, + threadgateAllowUISettings, + onChangeThreadgateAllowUISettings, style, }: { - threadgate: ThreadgateAllowUISetting[] - onChange: (v: ThreadgateAllowUISetting[]) => void + postgate: AppBskyFeedPostgate.Record + onChangePostgate: (v: AppBskyFeedPostgate.Record) => void + + threadgateAllowUISettings: ThreadgateAllowUISetting[] + onChangeThreadgateAllowUISettings: (v: ThreadgateAllowUISetting[]) => void + style?: StyleProp> }) { const {track} = useAnalytics() @@ -38,13 +46,24 @@ export function ThreadgateBtn({ control.open() } - const isEverybody = threadgate.length === 0 - const isNobody = !!threadgate.find(gate => gate.type === 'nobody') - const label = isEverybody - ? _(msg`Everybody can reply`) - : isNobody - ? _(msg`Nobody can reply`) - : _(msg`Some people can reply`) + const anyoneCanReply = + threadgateAllowUISettings.length === 1 && + threadgateAllowUISettings[0].type === 'everybody' + const noOneCanReply = + threadgateAllowUISettings.length === 1 && + threadgateAllowUISettings[0].type === 'nobody' + const anyoneCanQuote = + !postgate.quotepostRules || postgate.quotepostRules.length === 0 + const noOneCanQuote = + postgate.quotepostRules?.length === 1 && + postgate.quotepostRules[0]?.$type === embeddingRules.disableRule.$type + const anyoneCanInteract = anyoneCanReply && anyoneCanQuote + const noOneCanInteract = noOneCanReply && noOneCanQuote + const label = anyoneCanInteract + ? _(msg`Anybody can interact`) + : noOneCanInteract + ? _(msg`Nobody can interact`) + : _(msg`Interaction limited`) return ( <> @@ -60,15 +79,27 @@ export function ThreadgateBtn({ msg`Opens a dialog to choose who can reply to this thread`, )}> {label} { + control.close() + }} + postgate={postgate} + onChangePostgate={onChangePostgate} + threadgateAllowUISettings={threadgateAllowUISettings} + onChangeThreadgateAllowUISettings={onChangeThreadgateAllowUISettings} /> ) diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx index 0d4b15daef..031ee997ae 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, + AppBskyFeedPostgate, AtUri, ModerationDecision, RichText as RichTextAPI, @@ -15,6 +16,8 @@ import {POST_TOMBSTONE, Shadow, usePostShadow} from '#/state/cache/post-shadow' import {useLanguagePrefs} from '#/state/preferences' import {useOpenLink} from '#/state/preferences/in-app-browser' import {ThreadPost} from '#/state/queries/post-thread' +import {usePostgateQuery} from '#/state/queries/postgate' +import {createPostgateRecord} from '#/state/queries/postgate/util' import {useThreadgateRecordQuery} from '#/state/queries/threadgate' import {useComposerControls} from '#/state/shell/composer' import {MAX_POST_LINES} from 'lib/constants' @@ -80,6 +83,7 @@ export function PostThreadItem({ onPostReply: (postUri: string | undefined) => void hideTopBorder?: boolean }) { + const {data: postgate, isLoading} = usePostgateQuery({postUri: post.uri}) const postShadowed = usePostShadow(post) const richText = useMemo( () => @@ -92,7 +96,7 @@ export function PostThreadItem({ if (postShadowed === POST_TOMBSTONE) { return } - if (richText && moderation) { + if (richText && moderation && !isLoading) { return ( ) } @@ -156,6 +161,7 @@ let PostThreadItemLoaded = ({ overrideBlur, onPostReply, hideTopBorder, + postgate, }: { post: Shadow record: AppBskyFeedPost.Record @@ -173,6 +179,7 @@ let PostThreadItemLoaded = ({ overrideBlur: boolean onPostReply: (postUri: string | undefined) => void hideTopBorder?: boolean + postgate: AppBskyFeedPostgate.Record | undefined }): React.ReactNode => { const pal = usePalette('default') const {_} = useLingui() @@ -360,6 +367,7 @@ let PostThreadItemLoaded = ({ isThreadAuthor={isThreadAuthor} translatorUrl={translatorUrl} needsTranslation={needsTranslation} + postgate={postgate} /> {post.repostCount !== 0 || post.likeCount !== 0 ? ( // Show this section unless we're *sure* it has no engagement. @@ -663,15 +671,18 @@ function ExpandedPostDetails({ isThreadAuthor, needsTranslation, translatorUrl, + postgate, }: { post: AppBskyFeedDefs.PostView isThreadAuthor: boolean needsTranslation: boolean translatorUrl: string + postgate: AppBskyFeedPostgate.Record | undefined }) { const pal = usePalette('default') const {_} = useLingui() const openLink = useOpenLink() + const isRootPost = !('reply' in post.record) const onTranslatePress = React.useCallback(() => { openLink(translatorUrl) @@ -688,7 +699,19 @@ function ExpandedPostDetails({ s.mb10, ]}> {niceDate(post.indexedAt)} - + {isRootPost && ( + + )} {needsTranslation && ( <> ·