This commit is contained in:
Eric Bailey
2024-08-12 15:33:50 -05:00
parent 7adaf159c1
commit 5d78f69d95
8 changed files with 260 additions and 345 deletions
+27 -151
View File
@@ -2,32 +2,20 @@ import React from 'react'
import {Keyboard, StyleProp, View, ViewStyle} from 'react-native' import {Keyboard, StyleProp, View, ViewStyle} from 'react-native'
import { import {
AppBskyFeedDefs, AppBskyFeedDefs,
AppBskyFeedGetPostThread, AppBskyFeedPost,
AppBskyFeedPostgate,
AppBskyGraphDefs, AppBskyGraphDefs,
AtUri, AtUri,
BskyAgent,
} from '@atproto/api' } from '@atproto/api'
import {msg, Trans} from '@lingui/macro' import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useQueryClient} from '@tanstack/react-query'
import {until} from '#/lib/async/until'
import {HITSLOP_10} from '#/lib/constants' import {HITSLOP_10} from '#/lib/constants'
import {makeListLink, makeProfileLink} from '#/lib/routes/links' import {makeListLink, makeProfileLink} from '#/lib/routes/links'
import {logger} from '#/logger'
import {isNative} from '#/platform/detection' import {isNative} from '#/platform/detection'
import {RQKEY_ROOT as POST_THREAD_RQKEY_ROOT} from '#/state/queries/post-thread'
import {useWritePostgateMutation} from '#/state/queries/postgate'
import {embeddingRules} from '#/state/queries/postgate/util'
import { import {
ThreadgateAllowUISetting, ThreadgateAllowUISetting,
threadgateRecordQueryKeyRoot,
threadgateViewToAllowUISetting, threadgateViewToAllowUISetting,
updateThreadgateAllow,
} from '#/state/queries/threadgate' } from '#/state/queries/threadgate'
import {useAgent} from '#/state/session'
import * as Toast from 'view/com/util/Toast'
import {atoms as a, useTheme} from '#/alf' import {atoms as a, useTheme} from '#/alf'
import {Button} from '#/components/Button' import {Button} from '#/components/Button'
import * as Dialog from '#/components/Dialog' import * as Dialog from '#/components/Dialog'
@@ -44,29 +32,25 @@ interface WhoCanReplyProps {
post: AppBskyFeedDefs.PostView post: AppBskyFeedDefs.PostView
isThreadAuthor: boolean isThreadAuthor: boolean
style?: StyleProp<ViewStyle> style?: StyleProp<ViewStyle>
postgate: AppBskyFeedPostgate.Record
} }
export function WhoCanReply({ export function WhoCanReply({post, isThreadAuthor, style}: WhoCanReplyProps) {
post,
isThreadAuthor,
style,
postgate: initialPostgate,
}: WhoCanReplyProps) {
const {_} = useLingui() const {_} = useLingui()
const t = useTheme() const t = useTheme()
const infoDialogControl = useDialogControl() const infoDialogControl = useDialogControl()
const editDialogControl = useDialogControl() const editDialogControl = useDialogControl()
const agent = useAgent()
const queryClient = useQueryClient()
const [isSaving, setIsSaving] = React.useState(false)
const {mutateAsync: writePostgateRecord} = useWritePostgateMutation()
const [postgate, setPostgate] = React.useState(initialPostgate) /*
// TODO test if we get weird data back * `WhoCanReply` is only used for root posts atm, in case this changes
const [settings, setSettings] = React.useState( * unexpectedly, we should check to make sure it's for sure the root URI.
threadgateViewToAllowUISetting(post.threadgate), */
) const rootUri =
AppBskyFeedPost.isRecord(post.record) && post.record.reply?.root
? post.record.reply.root.uri
: post.uri
const settings = React.useMemo(() => {
return threadgateViewToAllowUISetting(post.threadgate)
}, [post.threadgate])
const anyoneCanReply = const anyoneCanReply =
settings.length === 1 && settings[0].type === 'everybody' settings.length === 1 && settings[0].type === 'everybody'
@@ -88,95 +72,6 @@ export function WhoCanReply({
} }
} }
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: settings,
})
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(settings)
}
return false
})
queryClient.invalidateQueries({
queryKey: [POST_THREAD_RQKEY_ROOT],
})
queryClient.invalidateQueries({
queryKey: [threadgateRecordQueryKeyRoot],
})
} 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',
)
}
}, [_, 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 ( return (
<> <>
<Button <Button
@@ -211,20 +106,17 @@ export function WhoCanReply({
{isThreadAuthor ? ( {isThreadAuthor ? (
<PostInteractionSettingsDialog <PostInteractionSettingsDialog
onSave={onSave} postUri={post.uri}
isSaving={isSaving} rootPostUri={rootUri}
postgate={postgate}
onChangePostgate={onChangePostgate}
threadgateAllowUISettings={settings}
onChangeThreadgateAllowUISettings={onChangeThreadgateAllowUISettings}
control={editDialogControl} control={editDialogControl}
initialThreadgateView={post.threadgate}
/> />
) : ( ) : (
<WhoCanReplyDialog <WhoCanReplyDialog
control={infoDialogControl} control={infoDialogControl}
post={post} post={post}
settings={settings} settings={settings}
postgate={postgate} embeddingDisabled={Boolean(post.viewer?.embeddingDisabled)}
/> />
)} )}
</> </>
@@ -250,12 +142,12 @@ function WhoCanReplyDialog({
control, control,
post, post,
settings, settings,
postgate, embeddingDisabled,
}: { }: {
control: Dialog.DialogControlProps control: Dialog.DialogControlProps
post: AppBskyFeedDefs.PostView post: AppBskyFeedDefs.PostView
settings: ThreadgateAllowUISetting[] settings: ThreadgateAllowUISetting[]
postgate: AppBskyFeedPostgate.Record embeddingDisabled: boolean
}) { }) {
const {_} = useLingui() const {_} = useLingui()
return ( return (
@@ -268,7 +160,11 @@ function WhoCanReplyDialog({
<Text style={[a.font_bold, a.text_xl, a.pb_sm]}> <Text style={[a.font_bold, a.text_xl, a.pb_sm]}>
<Trans>Who can interact with this post?</Trans> <Trans>Who can interact with this post?</Trans>
</Text> </Text>
<Rules post={post} settings={settings} postgate={postgate} /> <Rules
post={post}
settings={settings}
embeddingDisabled={embeddingDisabled}
/>
</View> </View>
</Dialog.ScrollableInner> </Dialog.ScrollableInner>
</Dialog.Outer> </Dialog.Outer>
@@ -278,16 +174,13 @@ function WhoCanReplyDialog({
function Rules({ function Rules({
post, post,
settings, settings,
postgate, embeddingDisabled,
}: { }: {
post: AppBskyFeedDefs.PostView post: AppBskyFeedDefs.PostView
settings: ThreadgateAllowUISetting[] settings: ThreadgateAllowUISetting[]
postgate: AppBskyFeedPostgate.Record embeddingDisabled: boolean
}) { }) {
const t = useTheme() const t = useTheme()
const noOneCanQuote =
postgate.embeddingRules?.length === 1 &&
postgate.embeddingRules[0]?.$type === embeddingRules.disableRule.$type
return ( return (
<> <>
@@ -320,7 +213,7 @@ function Rules({
</Trans> </Trans>
)}{' '} )}{' '}
</Text> </Text>
{noOneCanQuote && ( {embeddingDisabled && (
<Text <Text
style={[ style={[
a.text_sm, a.text_sm,
@@ -392,20 +285,3 @@ function Separator({i, length}: {i: number; length: number}) {
} }
return <>, </> return <>, </>
} }
async function whenAppViewReady(
agent: BskyAgent,
uri: string,
fn: (res: AppBskyFeedGetPostThread.Response) => boolean,
) {
await until(
5, // 5 tries
1e3, // 1s delay between tries
fn,
() =>
agent.app.bsky.feed.getPostThread({
uri,
depth: 0,
}),
)
}
@@ -1,28 +1,46 @@
import React from 'react' import React from 'react'
import {StyleProp, View, ViewStyle} from 'react-native' import {StyleProp, View, ViewStyle} from 'react-native'
import {AppBskyFeedPostgate} from '@atproto/api' import {
AppBskyFeedDefs,
AppBskyFeedPostgate,
AppBskyFeedThreadgate,
AtUri,
} from '@atproto/api'
import {msg, Trans} from '@lingui/macro' import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import isEqual from 'lodash.isequal' import isEqual from 'lodash.isequal'
import {logger} from '#/logger'
import {useMyListsQuery} from '#/state/queries/my-lists' import {useMyListsQuery} from '#/state/queries/my-lists'
import {
usePostgateQuery,
useWritePostgateMutation,
} from '#/state/queries/postgate'
import { import {
createPostgateRecord, createPostgateRecord,
embeddingRules, embeddingRules,
} from '#/state/queries/postgate/util' } from '#/state/queries/postgate/util'
import {ThreadgateAllowUISetting} from '#/state/queries/threadgate' import {
ThreadgateAllowUISetting,
threadgateViewToAllowUISetting,
useSetThreadgateAllowMutation,
useThreadgateViewQuery,
} from '#/state/queries/threadgate'
import {useSession} from '#/state/session'
import * as Toast from '#/view/com/util/toast'
import {atoms as a, useTheme} from '#/alf' import {atoms as a, useTheme} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button' import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog' import * as Dialog from '#/components/Dialog'
import {Divider} from '#/components/Divider' import {Divider} from '#/components/Divider'
import {useDelayedLoading} from '#/components/hooks/useDelayedLoading'
import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check' import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo' import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
import {Loader} from '#/components/Loader' import {Loader} from '#/components/Loader'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
export type Props = { export type PostInteractionSettingsFormProps = {
onSave: () => void onSave: () => void
isSaving: boolean isSaving?: boolean
postgate: AppBskyFeedPostgate.Record postgate: AppBskyFeedPostgate.Record
onChangePostgate: (v: AppBskyFeedPostgate.Record) => void onChangePostgate: (v: AppBskyFeedPostgate.Record) => void
@@ -33,10 +51,10 @@ export type Props = {
replySettingsDisabled?: boolean replySettingsDisabled?: boolean
} }
export function PostInteractionSettingsDialog({ export function PostInteractionSettingsControlledDialog({
control, control,
...rest ...rest
}: Props & { }: PostInteractionSettingsFormProps & {
control: Dialog.DialogControlProps control: Dialog.DialogControlProps
}) { }) {
const {_} = useLingui() const {_} = useLingui()
@@ -46,14 +64,168 @@ export function PostInteractionSettingsDialog({
<Dialog.ScrollableInner <Dialog.ScrollableInner
label={_(msg`Edit post interaction settings`)} label={_(msg`Edit post interaction settings`)}
style={[{maxWidth: 500}, a.w_full]}> style={[{maxWidth: 500}, a.w_full]}>
<PostInteractionSettingsDialogInner {...rest} /> <PostInteractionSettingsForm {...rest} />
<Dialog.Close /> <Dialog.Close />
</Dialog.ScrollableInner> </Dialog.ScrollableInner>
</Dialog.Outer> </Dialog.Outer>
) )
} }
export function PostInteractionSettingsDialogInner({ export type PostInteractionSettingsDialogProps = {
control: Dialog.DialogControlProps
/**
* URI of the post to edit the interaction settings for. Could be a root post
* or could be a reply.
*/
postUri: string
/**
* The URI of the root post in the thread. Used to determine if the viewer
* owns the threadgate record and can therefore edit it.
*/
rootPostUri: string
/**
* Optional initial {@link AppBskyFeedDefs.ThreadgateView} to use if we
* happen to have one before opening the settings dialog.
*/
initialThreadgateView?: AppBskyFeedDefs.ThreadgateView
}
export function PostInteractionSettingsDialog(
props: PostInteractionSettingsDialogProps,
) {
return (
<Dialog.Outer control={props.control}>
<Dialog.Handle />
<PostInteractionSettingsDialogControlledInner {...props} />
</Dialog.Outer>
)
}
export function PostInteractionSettingsDialogControlledInner(
props: PostInteractionSettingsDialogProps,
) {
const {_} = useLingui()
const {currentAccount} = useSession()
const [isSaving, setIsSaving] = React.useState(false)
const {data: threadgateViewLoaded, isLoading: isLoadingThreadgate} =
useThreadgateViewQuery({postUri: props.rootPostUri})
const {data: postgate, isLoading: isLoadingPostgate} = usePostgateQuery({
postUri: props.postUri,
})
const {mutateAsync: writePostgateRecord} = useWritePostgateMutation()
const {mutateAsync: setThreadgateAllow} = useSetThreadgateAllowMutation()
const naturalLoading = useDelayedLoading(500) // TODO
const [editedPostgate, setEditedPostgate] =
React.useState<AppBskyFeedPostgate.Record>()
const [editedAllowUISettings, setEditedAllowUISettings] =
React.useState<ThreadgateAllowUISetting[]>()
const isLoading = isLoadingThreadgate || isLoadingPostgate || naturalLoading
const threadgateView = threadgateViewLoaded || props.initialThreadgateView
const isThreadgateOwnedByViewer = React.useMemo(() => {
if (AppBskyFeedThreadgate.isRecord(threadgateView?.record)) {
return (
currentAccount?.did === new AtUri(threadgateView?.record?.post).host
)
}
return false
}, [threadgateView, currentAccount?.did])
const postgateValue = React.useMemo(() => {
return (
editedPostgate || postgate || createPostgateRecord({post: props.postUri})
)
}, [postgate, editedPostgate, props.postUri])
const allowUIValue = React.useMemo(() => {
return (
editedAllowUISettings || threadgateViewToAllowUISetting(threadgateView)
)
}, [threadgateView, editedAllowUISettings])
const onSave = React.useCallback(async () => {
if (!editedPostgate && !editedAllowUISettings) {
props.control.close()
return
}
setIsSaving(true)
try {
const requests = []
if (editedPostgate) {
requests.push(
writePostgateRecord({
postUri: props.postUri,
postgate: editedPostgate,
}),
)
}
if (editedAllowUISettings && isThreadgateOwnedByViewer) {
requests.push(
setThreadgateAllow({
postUri: props.rootPostUri,
allow: editedAllowUISettings,
}),
)
}
await Promise.all(requests)
props.control.close()
} catch (e: any) {
logger.error(`Failed to save post interaction settings`, {
context: 'PostInteractionSettingsDialogControlledInner',
safeMessage: e.message,
})
Toast.show(
_(
msg`There was an issue. Please check your internet connection and try again.`,
),
'xmark',
)
} finally {
setIsSaving(false)
}
}, [
_,
props.postUri,
props.rootPostUri,
props.control,
editedPostgate,
editedAllowUISettings,
setIsSaving,
writePostgateRecord,
setThreadgateAllow,
isThreadgateOwnedByViewer,
])
return (
<Dialog.ScrollableInner
label={_(msg`Edit post interaction settings`)}
style={[{maxWidth: 500}, a.w_full]}>
{isLoading ? (
<Loader size="xl" />
) : (
<PostInteractionSettingsForm
replySettingsDisabled={!isThreadgateOwnedByViewer}
isSaving={isSaving}
onSave={onSave}
postgate={postgateValue}
onChangePostgate={setEditedPostgate}
threadgateAllowUISettings={allowUIValue}
onChangeThreadgateAllowUISettings={setEditedAllowUISettings}
/>
)}
</Dialog.ScrollableInner>
)
}
export function PostInteractionSettingsForm({
onSave, onSave,
isSaving, isSaving,
postgate, postgate,
@@ -61,7 +233,7 @@ export function PostInteractionSettingsDialogInner({
threadgateAllowUISettings, threadgateAllowUISettings,
onChangeThreadgateAllowUISettings, onChangeThreadgateAllowUISettings,
replySettingsDisabled, replySettingsDisabled,
}: Props) { }: PostInteractionSettingsFormProps) {
const t = useTheme() const t = useTheme()
const {_} = useLingui() const {_} = useLingui()
const control = Dialog.useDialogContext() const control = Dialog.useDialogContext()
+1
View File
@@ -76,6 +76,7 @@ export function useThreadgateViewQuery({
uri: postUri!, uri: postUri!,
depth: 0, depth: 0,
}) })
console.log(data.thread)
if (AppBskyFeedDefs.isThreadViewPost(data.thread)) { if (AppBskyFeedDefs.isThreadViewPost(data.thread)) {
return data.thread.post.threadgate ?? null return data.thread.post.threadgate ?? null
@@ -11,7 +11,7 @@ import {useAnalytics} from 'lib/analytics/analytics'
import {atoms as a, useTheme} from '#/alf' import {atoms as a, useTheme} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button' import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog' import * as Dialog from '#/components/Dialog'
import {PostInteractionSettingsDialog} from '#/components/dialogs/PostInteractionSettingsDialog' import {PostInteractionSettingsControlledDialog} from '#/components/dialogs/PostInteractionSettingsDialog'
import {Earth_Stroke2_Corner0_Rounded as Earth} from '#/components/icons/Globe' import {Earth_Stroke2_Corner0_Rounded as Earth} from '#/components/icons/Globe'
import {Group3_Stroke2_Corner0_Rounded as Group} from '#/components/icons/Group' import {Group3_Stroke2_Corner0_Rounded as Group} from '#/components/icons/Group'
@@ -71,9 +71,8 @@ export function ThreadgateBtn({
<ButtonText>{label}</ButtonText> <ButtonText>{label}</ButtonText>
</Button> </Button>
</Animated.View> </Animated.View>
<PostInteractionSettingsDialog <PostInteractionSettingsControlledDialog
control={control} control={control}
isSaving={false}
onSave={() => { onSave={() => {
control.close() control.close()
}} }}
+1
View File
@@ -443,6 +443,7 @@ export function PostThread({uri}: {uri: string | undefined}) {
<PostThreadItem <PostThreadItem
post={item.post} post={item.post}
record={item.record} record={item.record}
threadgateRecord={threadgateRecord ?? undefined}
moderation={threadModerationCache.get(item)} moderation={threadModerationCache.get(item)}
treeView={treeView} treeView={treeView}
depth={item.ctx.depth} depth={item.ctx.depth}
+10 -30
View File
@@ -3,7 +3,7 @@ import {StyleSheet, View} from 'react-native'
import { import {
AppBskyFeedDefs, AppBskyFeedDefs,
AppBskyFeedPost, AppBskyFeedPost,
AppBskyFeedPostgate, AppBskyFeedThreadgate,
AtUri, AtUri,
ModerationDecision, ModerationDecision,
RichText as RichTextAPI, RichText as RichTextAPI,
@@ -16,9 +16,6 @@ import {POST_TOMBSTONE, Shadow, usePostShadow} from '#/state/cache/post-shadow'
import {useLanguagePrefs} from '#/state/preferences' import {useLanguagePrefs} from '#/state/preferences'
import {useOpenLink} from '#/state/preferences/in-app-browser' import {useOpenLink} from '#/state/preferences/in-app-browser'
import {ThreadPost} from '#/state/queries/post-thread' 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 {useComposerControls} from '#/state/shell/composer'
import {MAX_POST_LINES} from 'lib/constants' import {MAX_POST_LINES} from 'lib/constants'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
@@ -66,6 +63,7 @@ export function PostThreadItem({
overrideBlur, overrideBlur,
onPostReply, onPostReply,
hideTopBorder, hideTopBorder,
threadgateRecord,
}: { }: {
post: AppBskyFeedDefs.PostView post: AppBskyFeedDefs.PostView
record: AppBskyFeedPost.Record record: AppBskyFeedPost.Record
@@ -82,9 +80,9 @@ export function PostThreadItem({
overrideBlur: boolean overrideBlur: boolean
onPostReply: (postUri: string | undefined) => void onPostReply: (postUri: string | undefined) => void
hideTopBorder?: boolean hideTopBorder?: boolean
threadgateRecord?: AppBskyFeedThreadgate.Record
}) { }) {
// TODO should load this for every post, ugh // TODO should load this for every post, ugh
const {data: postgate, isLoading} = usePostgateQuery({postUri: post.uri})
const postShadowed = usePostShadow(post) const postShadowed = usePostShadow(post)
const richText = useMemo( const richText = useMemo(
() => () =>
@@ -97,7 +95,7 @@ export function PostThreadItem({
if (postShadowed === POST_TOMBSTONE) { if (postShadowed === POST_TOMBSTONE) {
return <PostThreadItemDeleted hideTopBorder={hideTopBorder} /> return <PostThreadItemDeleted hideTopBorder={hideTopBorder} />
} }
if (richText && moderation && !isLoading) { if (richText && moderation) {
return ( return (
<PostThreadItemLoaded <PostThreadItemLoaded
// Safeguard from clobbering per-post state below: // Safeguard from clobbering per-post state below:
@@ -118,7 +116,7 @@ export function PostThreadItem({
overrideBlur={overrideBlur} overrideBlur={overrideBlur}
onPostReply={onPostReply} onPostReply={onPostReply}
hideTopBorder={hideTopBorder} hideTopBorder={hideTopBorder}
postgate={postgate} threadgateRecord={threadgateRecord}
/> />
) )
} }
@@ -162,7 +160,7 @@ let PostThreadItemLoaded = ({
overrideBlur, overrideBlur,
onPostReply, onPostReply,
hideTopBorder, hideTopBorder,
postgate, threadgateRecord,
}: { }: {
post: Shadow<AppBskyFeedDefs.PostView> post: Shadow<AppBskyFeedDefs.PostView>
record: AppBskyFeedPost.Record record: AppBskyFeedPost.Record
@@ -180,7 +178,7 @@ let PostThreadItemLoaded = ({
overrideBlur: boolean overrideBlur: boolean
onPostReply: (postUri: string | undefined) => void onPostReply: (postUri: string | undefined) => void
hideTopBorder?: boolean hideTopBorder?: boolean
postgate: AppBskyFeedPostgate.Record | undefined threadgateRecord?: AppBskyFeedThreadgate.Record
}): React.ReactNode => { }): React.ReactNode => {
const pal = usePalette('default') const pal = usePalette('default')
const {_} = useLingui() const {_} = useLingui()
@@ -209,9 +207,6 @@ let PostThreadItemLoaded = ({
return makeProfileLink(post.author, 'post', urip.rkey, 'reposted-by') return makeProfileLink(post.author, 'post', urip.rkey, 'reposted-by')
}, [post.uri, post.author]) }, [post.uri, post.author])
const repostsTitle = _(msg`Reposts of this post`) const repostsTitle = _(msg`Reposts of this post`)
const {data: threadgateRecord} = useThreadgateRecordQuery({
postUri: rootUri,
})
const isPostHiddenByThreadgate = threadgateRecord?.hiddenReplies?.includes( const isPostHiddenByThreadgate = threadgateRecord?.hiddenReplies?.includes(
post.uri, post.uri,
) )
@@ -370,7 +365,6 @@ let PostThreadItemLoaded = ({
isThreadAuthor={isThreadAuthor} isThreadAuthor={isThreadAuthor}
translatorUrl={translatorUrl} translatorUrl={translatorUrl}
needsTranslation={needsTranslation} needsTranslation={needsTranslation}
postgate={postgate}
/> />
{post.repostCount !== 0 || post.likeCount !== 0 ? ( {post.repostCount !== 0 || post.likeCount !== 0 ? (
// Show this section unless we're *sure* it has no engagement. // Show this section unless we're *sure* it has no engagement.
@@ -421,8 +415,7 @@ let PostThreadItemLoaded = ({
richText={richText} richText={richText}
onPressReply={onPressReply} onPressReply={onPressReply}
logContext="PostThreadItem" logContext="PostThreadItem"
rootPostUri={rootUri} threadgateRecord={threadgateRecord}
threadgateRecord={threadgateRecord ?? undefined}
/> />
</View> </View>
</View> </View>
@@ -575,8 +568,7 @@ let PostThreadItemLoaded = ({
richText={richText} richText={richText}
onPressReply={onPressReply} onPressReply={onPressReply}
logContext="PostThreadItem" logContext="PostThreadItem"
rootPostUri={rootUri} threadgateRecord={threadgateRecord}
threadgateRecord={threadgateRecord ?? undefined}
/> />
</View> </View>
</View> </View>
@@ -674,13 +666,11 @@ function ExpandedPostDetails({
isThreadAuthor, isThreadAuthor,
needsTranslation, needsTranslation,
translatorUrl, translatorUrl,
postgate,
}: { }: {
post: AppBskyFeedDefs.PostView post: AppBskyFeedDefs.PostView
isThreadAuthor: boolean isThreadAuthor: boolean
needsTranslation: boolean needsTranslation: boolean
translatorUrl: string translatorUrl: string
postgate: AppBskyFeedPostgate.Record | undefined
}) { }) {
const pal = usePalette('default') const pal = usePalette('default')
const {_} = useLingui() const {_} = useLingui()
@@ -703,17 +693,7 @@ function ExpandedPostDetails({
]}> ]}>
<Text style={[a.text_sm, pal.textLight]}>{niceDate(post.indexedAt)}</Text> <Text style={[a.text_sm, pal.textLight]}>{niceDate(post.indexedAt)}</Text>
{isRootPost && ( {isRootPost && (
<WhoCanReply <WhoCanReply post={post} isThreadAuthor={isThreadAuthor} />
post={post}
postgate={
// TODO maybe define at query
postgate ||
createPostgateRecord({
post: post.uri,
})
}
isThreadAuthor={isThreadAuthor}
/>
)} )}
{needsTranslation && ( {needsTranslation && (
<> <>
+38 -149
View File
@@ -9,7 +9,6 @@ import * as Clipboard from 'expo-clipboard'
import { import {
AppBskyFeedDefs, AppBskyFeedDefs,
AppBskyFeedPost, AppBskyFeedPost,
AppBskyFeedPostgate,
AppBskyFeedThreadgate, AppBskyFeedThreadgate,
AtUri, AtUri,
RichText as RichTextAPI, RichText as RichTextAPI,
@@ -33,17 +32,9 @@ import {
usePostDeleteMutation, usePostDeleteMutation,
useThreadMuteMutationQueue, useThreadMuteMutationQueue,
} from '#/state/queries/post' } from '#/state/queries/post'
import { import {useToggleQuoteDetachmentMutation} from '#/state/queries/postgate'
usePostgateQuery, import {getMaybeDetachedQuoteEmbed} from '#/state/queries/postgate/util'
useToggleQuoteDetachmentMutation,
useWritePostgateMutation,
} from '#/state/queries/postgate'
import {
createPostgateRecord,
getMaybeDetachedQuoteEmbed,
} from '#/state/queries/postgate/util'
import {useToggleReplyVisibilityMutation} from '#/state/queries/threadgate' import {useToggleReplyVisibilityMutation} from '#/state/queries/threadgate'
import {threadgateRecordToAllowUISetting} from '#/state/queries/threadgate/util'
import {useSession} from '#/state/session' import {useSession} from '#/state/session'
import {getCurrentRoute} from 'lib/routes/helpers' import {getCurrentRoute} from 'lib/routes/helpers'
import {shareUrl} from 'lib/sharing' import {shareUrl} from 'lib/sharing'
@@ -51,10 +42,9 @@ import {toShareUrl} from 'lib/strings/url-helpers'
import {useTheme} from 'lib/ThemeContext' import {useTheme} from 'lib/ThemeContext'
import {atoms as a, useBreakpoints, useTheme as useAlf} from '#/alf' import {atoms as a, useBreakpoints, useTheme as useAlf} from '#/alf'
import {useDialogControl} from '#/components/Dialog' import {useDialogControl} from '#/components/Dialog'
import * as Dialog from '#/components/Dialog'
import {useGlobalDialogsControlContext} from '#/components/dialogs/Context' import {useGlobalDialogsControlContext} from '#/components/dialogs/Context'
import {EmbedDialog} from '#/components/dialogs/Embed' import {EmbedDialog} from '#/components/dialogs/Embed'
import {PostInteractionSettingsDialogInner} from '#/components/dialogs/PostInteractionSettingsDialog' import {PostInteractionSettingsDialog} from '#/components/dialogs/PostInteractionSettingsDialog'
import {SendViaChatDialog} from '#/components/dms/dialogs/ShareViaChatDialog' import {SendViaChatDialog} from '#/components/dms/dialogs/ShareViaChatDialog'
import {ArrowOutOfBox_Stroke2_Corner0_Rounded as Share} from '#/components/icons/ArrowOutOfBox' import {ArrowOutOfBox_Stroke2_Corner0_Rounded as Share} from '#/components/icons/ArrowOutOfBox'
import {BubbleQuestion_Stroke2_Corner0_Rounded as Translate} from '#/components/icons/Bubble' import {BubbleQuestion_Stroke2_Corner0_Rounded as Translate} from '#/components/icons/Bubble'
@@ -91,7 +81,6 @@ let PostDropdownBtn = ({
hitSlop, hitSlop,
size, size,
timestamp, timestamp,
rootPostUri,
threadgateRecord, threadgateRecord,
}: { }: {
testID: string testID: string
@@ -103,7 +92,6 @@ let PostDropdownBtn = ({
hitSlop?: PressableProps['hitSlop'] hitSlop?: PressableProps['hitSlop']
size?: 'lg' | 'md' | 'sm' size?: 'lg' | 'md' | 'sm'
timestamp: string timestamp: string
rootPostUri?: string
threadgateRecord?: AppBskyFeedThreadgate.Record threadgateRecord?: AppBskyFeedThreadgate.Record
}): React.ReactNode => { }): React.ReactNode => {
const {hasSession, currentAccount} = useSession() const {hasSession, currentAccount} = useSession()
@@ -282,36 +270,6 @@ let PostDropdownBtn = ({
[navigation, postUri], [navigation, postUri],
) )
const onToggleReplyVisibility = React.useCallback(async () => {
// TODO no threadgate?
if (!rootPostUri) return
const action = isReplyHiddenByThreadgate ? 'show' : 'hide'
const isHide = action === 'hide'
try {
await toggleReplyVisibility({
postUri: rootPostUri,
replyUri: postUri,
action,
})
Toast.show(
isHide
? _(msg`Reply was successfully hidden`)
: _(msg`Reply visibility updated`),
)
} catch (e: any) {
Toast.show(_(msg`Updating reply visibility failed`))
logger.error(`Failed to ${action} reply`, {safeMessage: e.message})
}
}, [
_,
isReplyHiddenByThreadgate,
rootPostUri,
postUri,
toggleReplyVisibility,
])
const onToggleQuotePostAttachment = React.useCallback(async () => { const onToggleQuotePostAttachment = React.useCallback(async () => {
if (!quoteEmbed) return if (!quoteEmbed) return
@@ -336,10 +294,40 @@ let PostDropdownBtn = ({
}, [_, quoteEmbed, post, toggleQuoteDetachment]) }, [_, quoteEmbed, post, toggleQuoteDetachment])
const canEmbed = isWeb && gtMobile && !hideInPWI const canEmbed = isWeb && gtMobile && !hideInPWI
const canHideReply = const canHideReply = !isAuthor && isRootPostAuthor && !isPostHidden && isReply
!isAuthor && isRootPostAuthor && !isPostHidden && rootPostUri && isReply
const canDetachQuote = quoteEmbed && quoteEmbed.isOwnedByViewer const canDetachQuote = quoteEmbed && quoteEmbed.isOwnedByViewer
const onToggleReplyVisibility = React.useCallback(async () => {
// TODO no threadgate?
if (!canHideReply) return
const action = isReplyHiddenByThreadgate ? 'show' : 'hide'
const isHide = action === 'hide'
try {
await toggleReplyVisibility({
postUri: rootUri,
replyUri: postUri,
action,
})
Toast.show(
isHide
? _(msg`Reply was successfully hidden`)
: _(msg`Reply visibility updated`),
)
} catch (e: any) {
Toast.show(_(msg`Updating reply visibility failed`))
logger.error(`Failed to ${action} reply`, {safeMessage: e.message})
}
}, [
_,
isReplyHiddenByThreadgate,
rootUri,
postUri,
canHideReply,
toggleReplyVisibility,
])
return ( return (
<EventStopper onKeyDown={false}> <EventStopper onKeyDown={false}>
<Menu.Root> <Menu.Root>
@@ -643,114 +631,15 @@ let PostDropdownBtn = ({
onSelectChat={onSelectChatToShareTo} onSelectChat={onSelectChatToShareTo}
/> />
<PostInteractionSettingsDialogControlled <PostInteractionSettingsDialog
control={postInteractionSettingsDialogControl} control={postInteractionSettingsDialogControl}
postUri={post.uri} postUri={post.uri}
threadgateRecord={threadgateRecord} rootPostUri={rootUri}
replySettingsDisabled={!isRootPostAuthor} initialThreadgateView={post.threadgate}
/> />
</EventStopper> </EventStopper>
) )
} }
export function PostInteractionSettingsDialogControlled(props: {
control: Dialog.DialogControlProps
postUri: string
threadgateRecord?: AppBskyFeedThreadgate.Record
replySettingsDisabled: boolean
}) {
return (
<Dialog.Outer control={props.control}>
<Dialog.Handle />
<PostInteractionSettingsDialogControlledInner {...props} />
</Dialog.Outer>
)
}
function PostInteractionSettingsDialogControlledInner(props: {
control: Dialog.DialogControlProps
postUri: string
threadgateRecord?: AppBskyFeedThreadgate.Record
replySettingsDisabled: boolean
}) {
const {_} = useLingui()
const threadgateAllowUISettings = React.useMemo(() => {
return threadgateRecordToAllowUISetting(props.threadgateRecord)
}, [props.threadgateRecord])
const [isSaving, setIsSaving] = React.useState(false)
const {mutateAsync: writePostgateRecord} = useWritePostgateMutation()
const {data: postgate, isLoading} = usePostgateQuery({postUri: props.postUri})
const [editedPostgate, setEditedPostgate] =
React.useState<AppBskyFeedPostgate.Record>()
const onChangePostgate = React.useCallback(
(next: AppBskyFeedPostgate.Record) => {
setEditedPostgate(next)
},
[setEditedPostgate],
)
const onSave = React.useCallback(async () => {
if (!editedPostgate) {
props.control.close()
return
}
setIsSaving(true)
try {
await writePostgateRecord({
postUri: props.postUri,
postgate: editedPostgate,
})
props.control.close()
} catch (e: any) {
logger.error(`Failed to save post interaction settings`, {
context: 'PostInteractionSettingsDialogControlledInner',
safeMessage: e.message,
})
Toast.show(
_(
msg`There was an issue. Please check your internet connection and try again.`,
),
'xmark',
)
} finally {
setIsSaving(false)
}
}, [
_,
props.postUri,
props.control,
editedPostgate,
setIsSaving,
writePostgateRecord,
])
return (
<Dialog.ScrollableInner
label={_(msg`Edit post interaction settings`)}
style={[{maxWidth: 500}, a.w_full]}>
{isLoading ? (
<Loader size="xl" />
) : (
<PostInteractionSettingsDialogInner
replySettingsDisabled={props.replySettingsDisabled}
isSaving={isSaving}
onSave={onSave}
postgate={
editedPostgate ||
postgate ||
createPostgateRecord({post: props.postUri})
}
onChangePostgate={onChangePostgate}
threadgateAllowUISettings={threadgateAllowUISettings}
onChangeThreadgateAllowUISettings={() => {}}
/>
)}
</Dialog.ScrollableInner>
)
}
PostDropdownBtn = memo(PostDropdownBtn) PostDropdownBtn = memo(PostDropdownBtn)
export {PostDropdownBtn} export {PostDropdownBtn}
@@ -60,7 +60,6 @@ let PostCtrls = ({
style, style,
onPressReply, onPressReply,
logContext, logContext,
rootPostUri,
threadgateRecord, threadgateRecord,
}: { }: {
big?: boolean big?: boolean
@@ -71,7 +70,6 @@ let PostCtrls = ({
style?: StyleProp<ViewStyle> style?: StyleProp<ViewStyle>
onPressReply: () => void onPressReply: () => void
logContext: 'FeedItem' | 'PostThreadItem' | 'Post' logContext: 'FeedItem' | 'PostThreadItem' | 'Post'
rootPostUri?: string
threadgateRecord?: AppBskyFeedThreadgate.Record threadgateRecord?: AppBskyFeedThreadgate.Record
}): React.ReactNode => { }): React.ReactNode => {
const t = useTheme() const t = useTheme()
@@ -344,7 +342,6 @@ let PostCtrls = ({
style={{padding: 5}} style={{padding: 5}}
hitSlop={POST_CTRL_HITSLOP} hitSlop={POST_CTRL_HITSLOP}
timestamp={post.indexedAt} timestamp={post.indexedAt}
rootPostUri={rootPostUri}
threadgateRecord={threadgateRecord} threadgateRecord={threadgateRecord}
/> />
</View> </View>