Integrate postgates into ThreadgateEditor dialog
This commit is contained in:
+100
-32
@@ -3,6 +3,7 @@ import {Keyboard, StyleProp, View, ViewStyle} from 'react-native'
|
|||||||
import {
|
import {
|
||||||
AppBskyFeedDefs,
|
AppBskyFeedDefs,
|
||||||
AppBskyFeedGetPostThread,
|
AppBskyFeedGetPostThread,
|
||||||
|
AppBskyFeedPostgate,
|
||||||
AppBskyGraphDefs,
|
AppBskyGraphDefs,
|
||||||
AtUri,
|
AtUri,
|
||||||
BskyAgent,
|
BskyAgent,
|
||||||
@@ -17,11 +18,13 @@ import {makeListLink, makeProfileLink} from '#/lib/routes/links'
|
|||||||
import {logger} from '#/logger'
|
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 {RQKEY_ROOT as POST_THREAD_RQKEY_ROOT} from '#/state/queries/post-thread'
|
||||||
import {updateThreadgateAllow} from '#/state/queries/threadgate'
|
import {useWritePostgateMutation} from '#/state/queries/postgate'
|
||||||
import {threadgateRecordQueryKeyRoot} from '#/state/queries/threadgate'
|
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 {useAgent} from '#/state/session'
|
||||||
import * as Toast from 'view/com/util/Toast'
|
import * as Toast from 'view/com/util/Toast'
|
||||||
@@ -41,35 +44,47 @@ interface WhoCanReplyProps {
|
|||||||
post: AppBskyFeedDefs.PostView
|
post: AppBskyFeedDefs.PostView
|
||||||
isThreadAuthor: boolean
|
isThreadAuthor: boolean
|
||||||
style?: StyleProp<ViewStyle>
|
style?: StyleProp<ViewStyle>
|
||||||
|
postgate: AppBskyFeedPostgate.Record
|
||||||
}
|
}
|
||||||
|
|
||||||
export function WhoCanReply({post, isThreadAuthor, style}: WhoCanReplyProps) {
|
export function WhoCanReply({
|
||||||
|
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 agent = useAgent()
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
|
const [isSaving, setIsSaving] = React.useState(false)
|
||||||
|
const {mutateAsync: writePostgateRecord} = useWritePostgateMutation()
|
||||||
|
|
||||||
const settings = React.useMemo(
|
const [postgate, setPostgate] = React.useState(initialPostgate)
|
||||||
() => threadgateViewToAllowUISetting(post.threadgate),
|
// TODO test if we get weird data back
|
||||||
[post],
|
const [settings, setSettings] = React.useState(
|
||||||
|
threadgateViewToAllowUISetting(post.threadgate),
|
||||||
)
|
)
|
||||||
const isRootPost = !('reply' in post.record)
|
|
||||||
|
|
||||||
if (!isRootPost) {
|
const anyoneCanReply =
|
||||||
return null
|
settings.length === 1 && settings[0].type === 'everybody'
|
||||||
}
|
const noOneCanReply = settings.length === 1 && settings[0].type === 'nobody'
|
||||||
if (!settings.length && !isThreadAuthor) {
|
const anyoneCanQuote =
|
||||||
return null
|
!postgate.quotepostRules || postgate.quotepostRules.length === 0
|
||||||
}
|
const noOneCanQuote =
|
||||||
|
postgate.quotepostRules?.length === 1 &&
|
||||||
const isEverybody = settings.length === 0
|
postgate.quotepostRules[0]?.$type === embeddingRules.disableRule.$type
|
||||||
const description = isEverybody
|
const anyoneCanInteract = anyoneCanReply && anyoneCanQuote
|
||||||
? _(msg`Anyone can interact`)
|
const noOneCanInteract = noOneCanReply && noOneCanQuote
|
||||||
|
const description = anyoneCanInteract
|
||||||
|
? _(msg`Anybody can interact`)
|
||||||
|
: noOneCanInteract
|
||||||
|
? _(msg`Nobody can interact`)
|
||||||
: _(msg`Interaction limited`)
|
: _(msg`Interaction limited`)
|
||||||
|
|
||||||
const onPress = () => {
|
const onPressOpen = () => {
|
||||||
if (isNative && Keyboard.isVisible()) {
|
if (isNative && Keyboard.isVisible()) {
|
||||||
Keyboard.dismiss()
|
Keyboard.dismiss()
|
||||||
}
|
}
|
||||||
@@ -80,45 +95,94 @@ export function WhoCanReply({post, isThreadAuthor, style}: WhoCanReplyProps) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const onEditConfirm = async (newSettings: ThreadgateAllowUISetting[]) => {
|
const onChangePostgate = React.useCallback(
|
||||||
if (JSON.stringify(settings) === JSON.stringify(newSettings)) {
|
(next: AppBskyFeedPostgate.Record) => {
|
||||||
return
|
setPostgate(next)
|
||||||
}
|
},
|
||||||
|
[setPostgate],
|
||||||
|
)
|
||||||
|
|
||||||
|
const onChangeThreadgateAllowUISettings = React.useCallback(
|
||||||
|
(next: ThreadgateAllowUISetting[]) => {
|
||||||
|
setSettings(next)
|
||||||
|
},
|
||||||
|
[setSettings],
|
||||||
|
)
|
||||||
|
|
||||||
|
const saveThreadgateAllowSettings = React.useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
await updateThreadgateAllow({
|
await updateThreadgateAllow({
|
||||||
agent,
|
agent,
|
||||||
postUri: post.uri,
|
postUri: post.uri,
|
||||||
allow: newSettings,
|
allow: settings,
|
||||||
})
|
})
|
||||||
|
|
||||||
// TODO
|
|
||||||
await whenAppViewReady(agent, post.uri, res => {
|
await whenAppViewReady(agent, post.uri, res => {
|
||||||
const thread = res.data.thread
|
const thread = res.data.thread
|
||||||
if (AppBskyFeedDefs.isThreadViewPost(thread)) {
|
if (AppBskyFeedDefs.isThreadViewPost(thread)) {
|
||||||
const fetchedSettings = threadgateViewToAllowUISetting(
|
const fetchedSettings = threadgateViewToAllowUISetting(
|
||||||
thread.post.threadgate,
|
thread.post.threadgate,
|
||||||
)
|
)
|
||||||
return JSON.stringify(fetchedSettings) === JSON.stringify(newSettings)
|
return JSON.stringify(fetchedSettings) === JSON.stringify(settings)
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
Toast.show(_(msg`Thread settings updated`))
|
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: [POST_THREAD_RQKEY_ROOT],
|
queryKey: [POST_THREAD_RQKEY_ROOT],
|
||||||
})
|
})
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: [threadgateRecordQueryKeyRoot],
|
queryKey: [threadgateRecordQueryKeyRoot],
|
||||||
})
|
})
|
||||||
} catch (err) {
|
} catch (e: any) {
|
||||||
|
logger.error('Failed to edit threadgate', {safeMessage: e.message})
|
||||||
Toast.show(
|
Toast.show(
|
||||||
_(
|
_(
|
||||||
msg`There was an issue. Please check your internet connection and try again.`,
|
msg`There was an issue. Please check your internet connection and try again.`,
|
||||||
),
|
),
|
||||||
'xmark',
|
'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 (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -126,7 +190,7 @@ export function WhoCanReply({post, isThreadAuthor, style}: WhoCanReplyProps) {
|
|||||||
label={
|
label={
|
||||||
isThreadAuthor ? _(msg`Edit who can reply`) : _(msg`Who can reply`)
|
isThreadAuthor ? _(msg`Edit who can reply`) : _(msg`Who can reply`)
|
||||||
}
|
}
|
||||||
onPress={onPress}
|
onPress={onPressOpen}
|
||||||
hitSlop={HITSLOP_10}>
|
hitSlop={HITSLOP_10}>
|
||||||
{({hovered}) => (
|
{({hovered}) => (
|
||||||
<View style={[a.flex_row, a.align_center, a.gap_xs, style]}>
|
<View style={[a.flex_row, a.align_center, a.gap_xs, style]}>
|
||||||
@@ -154,9 +218,13 @@ export function WhoCanReply({post, isThreadAuthor, style}: WhoCanReplyProps) {
|
|||||||
|
|
||||||
{isThreadAuthor ? (
|
{isThreadAuthor ? (
|
||||||
<ThreadgateEditorDialog
|
<ThreadgateEditorDialog
|
||||||
|
onSave={onSave}
|
||||||
|
isSaving={isSaving}
|
||||||
|
postgate={postgate}
|
||||||
|
onChangePostgate={onChangePostgate}
|
||||||
|
threadgateAllowUISettings={settings}
|
||||||
|
onChangeThreadgateAllowUISettings={onChangeThreadgateAllowUISettings}
|
||||||
control={editDialogControl}
|
control={editDialogControl}
|
||||||
threadgateUISettings={settings}
|
|
||||||
onConfirmThreadgateUISettings={onEditConfirm}
|
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<WhoCanReplyDialog
|
<WhoCanReplyDialog
|
||||||
|
|||||||
@@ -1,79 +1,68 @@
|
|||||||
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 {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 {useMyListsQuery} from '#/state/queries/my-lists'
|
import {useMyListsQuery} from '#/state/queries/my-lists'
|
||||||
|
import {
|
||||||
|
createPostgateRecord,
|
||||||
|
embeddingRules,
|
||||||
|
} from '#/state/queries/postgate/util'
|
||||||
import {ThreadgateAllowUISetting} from '#/state/queries/threadgate'
|
import {ThreadgateAllowUISetting} from '#/state/queries/threadgate'
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
import {Button, 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 {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
|
import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
|
||||||
|
import {Loader} from '#/components/Loader'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
threadgateUISettings: ThreadgateAllowUISetting[]
|
onSave: () => void
|
||||||
onChangeThreadgateUISettings?: (v: ThreadgateAllowUISetting[]) => void
|
isSaving: boolean
|
||||||
onConfirmThreadgateUISettings?: (v: ThreadgateAllowUISetting[]) => void
|
|
||||||
|
postgate: AppBskyFeedPostgate.Record
|
||||||
|
onChangePostgate: (v: AppBskyFeedPostgate.Record) => void
|
||||||
|
|
||||||
|
threadgateAllowUISettings: ThreadgateAllowUISetting[]
|
||||||
|
onChangeThreadgateAllowUISettings: (v: ThreadgateAllowUISetting[]) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ThreadgateEditorDialog({
|
export function ThreadgateEditorDialog({
|
||||||
control,
|
control,
|
||||||
onChangeThreadgateUISettings,
|
...rest
|
||||||
onConfirmThreadgateUISettings,
|
|
||||||
threadgateUISettings,
|
|
||||||
}: Props & {
|
}: Props & {
|
||||||
control: Dialog.DialogControlProps
|
control: Dialog.DialogControlProps
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<Dialog.Outer control={control}>
|
<Dialog.Outer control={control}>
|
||||||
<Dialog.Handle />
|
<Dialog.Handle />
|
||||||
<DialogContent
|
<DialogContent {...rest} />
|
||||||
onChangeThreadgateUISettings={onChangeThreadgateUISettings}
|
|
||||||
onConfirmThreadgateUISettings={onConfirmThreadgateUISettings}
|
|
||||||
threadgateUISettings={threadgateUISettings}
|
|
||||||
/>
|
|
||||||
</Dialog.Outer>
|
</Dialog.Outer>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function DialogContent({
|
function DialogContent({
|
||||||
onChangeThreadgateUISettings,
|
onSave,
|
||||||
onConfirmThreadgateUISettings,
|
isSaving,
|
||||||
threadgateUISettings,
|
postgate,
|
||||||
|
onChangePostgate,
|
||||||
|
threadgateAllowUISettings,
|
||||||
|
onChangeThreadgateAllowUISettings,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
|
const t = useTheme()
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const control = Dialog.useDialogContext()
|
const control = Dialog.useDialogContext()
|
||||||
const {data: lists} = useMyListsQuery('curate')
|
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) => {
|
const onPressAudience = (setting: ThreadgateAllowUISetting) => {
|
||||||
// remove nobody
|
// remove boolean values
|
||||||
let newSelected: ThreadgateAllowUISetting[] = draft.filter(
|
let newSelected: ThreadgateAllowUISetting[] =
|
||||||
v => v.type !== 'nobody',
|
threadgateAllowUISettings.filter(
|
||||||
)
|
v => v.type !== 'nobody' && v.type !== 'everybody',
|
||||||
|
)
|
||||||
// toggle
|
// toggle
|
||||||
const i = newSelected.findIndex(v => isEqual(v, setting))
|
const i = newSelected.findIndex(v => isEqual(v, setting))
|
||||||
if (i === -1) {
|
if (i === -1) {
|
||||||
@@ -81,79 +70,161 @@ function DialogContent({
|
|||||||
} else {
|
} else {
|
||||||
newSelected.splice(i, 1)
|
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 (
|
return (
|
||||||
<Dialog.ScrollableInner
|
<Dialog.ScrollableInner
|
||||||
label={_(msg`Choose who can reply`)}
|
label={_(msg`Edit post interaction settings`)}
|
||||||
style={[{maxWidth: 500}, a.w_full]}>
|
style={[{maxWidth: 500}, a.w_full]}>
|
||||||
<View style={[a.flex_1, a.gap_md]}>
|
<View style={[a.flex_1, a.gap_md]}>
|
||||||
<Text style={[a.text_2xl, a.font_bold]}>
|
<Text style={[a.text_2xl, a.font_bold]}>
|
||||||
<Trans>Choose who can reply</Trans>
|
<Trans>Post interaction settings</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
<Text style={a.mt_xs}>
|
|
||||||
<Trans>Either choose "Everybody" or "Nobody"</Trans>
|
<View style={[a.gap_lg]}>
|
||||||
</Text>
|
<Text style={[a.text_md]}>
|
||||||
<View style={[a.flex_row, a.gap_sm]}>
|
<Trans>Customize who can engage with this post.</Trans>
|
||||||
<Selectable
|
</Text>
|
||||||
label={_(msg`Everybody`)}
|
|
||||||
isSelected={!!draft.find(v => v.type === 'everybody')}
|
<Divider />
|
||||||
onPress={onPressEverybody}
|
|
||||||
style={{flex: 1}}
|
<View style={[a.gap_sm]}>
|
||||||
/>
|
<Text style={[a.font_bold, a.text_lg]}>
|
||||||
<Selectable
|
<Trans>Quote settings</Trans>
|
||||||
label={_(msg`Nobody`)}
|
</Text>
|
||||||
isSelected={!!draft.find(v => v.type === 'nobody')}
|
|
||||||
onPress={onPressNobody}
|
<Text style={[a.pt_sm, t.atoms.text_contrast_medium]}>
|
||||||
style={{flex: 1}}
|
<Trans>Allow quote posts from:</Trans>
|
||||||
/>
|
</Text>
|
||||||
</View>
|
|
||||||
<Text style={a.mt_md}>
|
<View style={[a.flex_row, a.gap_sm]}>
|
||||||
<Trans>Or combine these options:</Trans>
|
<Selectable
|
||||||
</Text>
|
label={_(msg`Everybody`)}
|
||||||
<View style={[a.gap_sm]}>
|
isSelected={
|
||||||
<Selectable
|
!postgate.quotepostRules ||
|
||||||
label={_(msg`Mentioned users`)}
|
postgate.quotepostRules?.length === 0
|
||||||
isSelected={!!draft.find(v => v.type === 'mention')}
|
}
|
||||||
onPress={() => onPressAudience({type: 'mention'})}
|
onPress={() => onChangeEmbeddingRules([])}
|
||||||
/>
|
style={{flex: 1}}
|
||||||
<Selectable
|
/>
|
||||||
label={_(msg`Followed users`)}
|
<Selectable
|
||||||
isSelected={!!draft.find(v => v.type === 'following')}
|
label={_(msg`Nobody`)}
|
||||||
onPress={() => onPressAudience({type: 'following'})}
|
isSelected={Boolean(
|
||||||
/>
|
postgate.quotepostRules &&
|
||||||
{lists && lists.length > 0
|
postgate.quotepostRules.find(
|
||||||
? lists.map(list => (
|
v => v.$type === embeddingRules.disableRule.$type,
|
||||||
<Selectable
|
),
|
||||||
key={list.uri}
|
)}
|
||||||
label={_(msg`Users in "${list.name}"`)}
|
onPress={() =>
|
||||||
isSelected={
|
onChangeEmbeddingRules([embeddingRules.disableRule])
|
||||||
!!draft.find(v => v.type === 'list' && v.list === list.uri)
|
}
|
||||||
}
|
style={{flex: 1}}
|
||||||
onPress={() =>
|
/>
|
||||||
onPressAudience({type: 'list', list: list.uri})
|
</View>
|
||||||
}
|
</View>
|
||||||
/>
|
|
||||||
))
|
<Divider />
|
||||||
: // No loading states to avoid jumps for the common case (no lists)
|
|
||||||
null}
|
<View style={[a.gap_sm]}>
|
||||||
|
<Text style={[a.font_bold, a.text_lg]}>
|
||||||
|
<Trans>Reply settings</Trans>
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Text style={[a.pt_sm, t.atoms.text_contrast_medium]}>
|
||||||
|
<Trans>Allow replies from:</Trans>
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<View style={[a.flex_row, a.gap_sm]}>
|
||||||
|
<Selectable
|
||||||
|
label={_(msg`Everybody`)}
|
||||||
|
isSelected={
|
||||||
|
!!threadgateAllowUISettings.find(v => v.type === 'everybody')
|
||||||
|
}
|
||||||
|
onPress={() =>
|
||||||
|
onChangeThreadgateAllowUISettings([{type: 'everybody'}])
|
||||||
|
}
|
||||||
|
style={{flex: 1}}
|
||||||
|
/>
|
||||||
|
<Selectable
|
||||||
|
label={_(msg`Nobody`)}
|
||||||
|
isSelected={
|
||||||
|
!!threadgateAllowUISettings.find(v => v.type === 'nobody')
|
||||||
|
}
|
||||||
|
onPress={() =>
|
||||||
|
onChangeThreadgateAllowUISettings([{type: 'nobody'}])
|
||||||
|
}
|
||||||
|
style={{flex: 1}}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<Text style={[a.pt_sm, t.atoms.text_contrast_medium]}>
|
||||||
|
<Trans>Or combine these options:</Trans>
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<View style={[a.gap_sm]}>
|
||||||
|
<Selectable
|
||||||
|
label={_(msg`Mentioned users`)}
|
||||||
|
isSelected={
|
||||||
|
!!threadgateAllowUISettings.find(v => v.type === 'mention')
|
||||||
|
}
|
||||||
|
onPress={() => onPressAudience({type: 'mention'})}
|
||||||
|
/>
|
||||||
|
<Selectable
|
||||||
|
label={_(msg`Followed users`)}
|
||||||
|
isSelected={
|
||||||
|
!!threadgateAllowUISettings.find(v => v.type === 'following')
|
||||||
|
}
|
||||||
|
onPress={() => onPressAudience({type: 'following'})}
|
||||||
|
/>
|
||||||
|
{lists && lists.length > 0
|
||||||
|
? lists.map(list => (
|
||||||
|
<Selectable
|
||||||
|
key={list.uri}
|
||||||
|
label={_(msg`Users in "${list.name}"`)}
|
||||||
|
isSelected={
|
||||||
|
!!threadgateAllowUISettings.find(
|
||||||
|
v => 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}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
label={doneLabel}
|
label={doneLabel}
|
||||||
onPress={() => {
|
onPress={onSave}
|
||||||
control.close()
|
|
||||||
onConfirmThreadgateUISettings?.(draft)
|
|
||||||
}}
|
|
||||||
onAccessibilityEscape={control.close}
|
onAccessibilityEscape={control.close}
|
||||||
color="primary"
|
color="primary"
|
||||||
size="medium"
|
size="medium"
|
||||||
variant="solid"
|
variant="solid"
|
||||||
style={a.mt_xl}>
|
style={a.mt_xl}>
|
||||||
<ButtonText>{doneLabel}</ButtonText>
|
<ButtonText>{doneLabel}</ButtonText>
|
||||||
|
{isSaving && <ButtonIcon icon={Loader} position="right" />}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Dialog.Close />
|
<Dialog.Close />
|
||||||
</Dialog.ScrollableInner>
|
</Dialog.ScrollableInner>
|
||||||
)
|
)
|
||||||
|
|||||||
+40
-14
@@ -3,6 +3,7 @@ import {
|
|||||||
AppBskyEmbedImages,
|
AppBskyEmbedImages,
|
||||||
AppBskyEmbedRecord,
|
AppBskyEmbedRecord,
|
||||||
AppBskyEmbedRecordWithMedia,
|
AppBskyEmbedRecordWithMedia,
|
||||||
|
AppBskyFeedPostgate,
|
||||||
BskyAgent,
|
BskyAgent,
|
||||||
ComAtprotoLabelDefs,
|
ComAtprotoLabelDefs,
|
||||||
ComAtprotoRepoUploadBlob,
|
ComAtprotoRepoUploadBlob,
|
||||||
@@ -11,6 +12,7 @@ import {
|
|||||||
import {AtUri} from '@atproto/api'
|
import {AtUri} from '@atproto/api'
|
||||||
|
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
|
import {writePostgateRecord} from '#/state/queries/postgate'
|
||||||
import {
|
import {
|
||||||
createThreadgateRecord,
|
createThreadgateRecord,
|
||||||
ThreadgateAllowUISetting,
|
ThreadgateAllowUISetting,
|
||||||
@@ -65,7 +67,8 @@ interface PostOpts {
|
|||||||
extLink?: ExternalEmbedDraft
|
extLink?: ExternalEmbedDraft
|
||||||
images?: ImageModel[]
|
images?: ImageModel[]
|
||||||
labels?: string[]
|
labels?: string[]
|
||||||
threadgate?: ThreadgateAllowUISetting[]
|
threadgate: ThreadgateAllowUISetting[]
|
||||||
|
postgate: AppBskyFeedPostgate.Record
|
||||||
onStateChange?: (state: string) => void
|
onStateChange?: (state: string) => void
|
||||||
langs?: string[]
|
langs?: string[]
|
||||||
}
|
}
|
||||||
@@ -253,7 +256,9 @@ export async function post(agent: BskyAgent, opts: PostOpts) {
|
|||||||
labels,
|
labels,
|
||||||
})
|
})
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error(`Failed to create post: ${e.toString()}`)
|
logger.error(`Failed to create post`, {
|
||||||
|
safeMessage: e.message,
|
||||||
|
})
|
||||||
if (isNetworkError(e)) {
|
if (isNetworkError(e)) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'Post failed to upload. Please check your Internet connection and try again.',
|
'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 {
|
try {
|
||||||
// TODO: this needs to be batch-created with the post!
|
// TODO: this needs to be batch-created with the post!
|
||||||
if (opts.threadgate?.length) {
|
await writeThreadgateRecord({
|
||||||
await writeThreadgateRecord({
|
agent,
|
||||||
agent,
|
postUri: res.uri,
|
||||||
postUri: res.uri,
|
threadgate: createThreadgateRecord({
|
||||||
threadgate: createThreadgateRecord({
|
post: res.uri,
|
||||||
post: res.uri,
|
allow: threadgateAllowUISettingToAllowRecordValue(opts.threadgate),
|
||||||
allow: threadgateAllowUISettingToAllowRecordValue(opts.threadgate),
|
}),
|
||||||
}),
|
})
|
||||||
})
|
|
||||||
}
|
|
||||||
} catch (e: any) {
|
} 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(
|
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.',
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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() {
|
export function useToggleQuoteDetachmentMutation() {
|
||||||
const agent = useAgent()
|
const agent = useAgent()
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
|
|||||||
@@ -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 const POSTGATE_COLLECTION = 'app.bsky.feed.postgate'
|
||||||
|
|
||||||
export function createPostgateRecord(
|
export function createPostgateRecord(
|
||||||
postgate: Partial<AppBskyFeedPostgate.Record>,
|
postgate: Partial<AppBskyFeedPostgate.Record> & {
|
||||||
|
post: AppBskyFeedPostgate.Record['post']
|
||||||
|
},
|
||||||
): AppBskyFeedPostgate.Record {
|
): AppBskyFeedPostgate.Record {
|
||||||
if (!postgate.post) {
|
|
||||||
throw new Error(`Cannot create a postgate record without a post URI`)
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
$type: POSTGATE_COLLECTION,
|
$type: POSTGATE_COLLECTION,
|
||||||
createdAt: new Date().toISOString(),
|
createdAt: new Date().toISOString(),
|
||||||
@@ -190,3 +188,7 @@ export function getMaybeDetachedQuoteEmbed({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const embeddingRules = {
|
||||||
|
disableRule: {$type: 'app.bsky.feed.postgate#disableRule'},
|
||||||
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export function threadgateViewToAllowUISetting(
|
|||||||
* for 'replies disabled' other than an empty array.
|
* for 'replies disabled' other than an empty array.
|
||||||
*/
|
*/
|
||||||
if (!record || record.allow === undefined) {
|
if (!record || record.allow === undefined) {
|
||||||
return []
|
return [{type: 'everybody'}]
|
||||||
}
|
}
|
||||||
if (record.allow.length === 0) {
|
if (record.allow.length === 0) {
|
||||||
return [{type: 'nobody'}]
|
return [{type: 'nobody'}]
|
||||||
|
|||||||
@@ -58,9 +58,11 @@ import {
|
|||||||
useLanguagePrefs,
|
useLanguagePrefs,
|
||||||
useLanguagePrefsApi,
|
useLanguagePrefsApi,
|
||||||
} from '#/state/preferences/languages'
|
} from '#/state/preferences/languages'
|
||||||
|
import {createPostgateRecord} from '#/state/queries/postgate/util'
|
||||||
import {useProfileQuery} from '#/state/queries/profile'
|
import {useProfileQuery} from '#/state/queries/profile'
|
||||||
import {Gif} from '#/state/queries/tenor'
|
import {Gif} from '#/state/queries/tenor'
|
||||||
import {ThreadgateAllowUISetting} from '#/state/queries/threadgate'
|
import {ThreadgateAllowUISetting} from '#/state/queries/threadgate'
|
||||||
|
import {threadgateViewToAllowUISetting} from '#/state/queries/threadgate/util'
|
||||||
import {useUploadVideo} from '#/state/queries/video/video'
|
import {useUploadVideo} from '#/state/queries/video/video'
|
||||||
import {useAgent, useSession} from '#/state/session'
|
import {useAgent, useSession} from '#/state/session'
|
||||||
import {useComposerControls} from '#/state/shell/composer'
|
import {useComposerControls} from '#/state/shell/composer'
|
||||||
@@ -184,7 +186,11 @@ export const ComposePost = observer(function ComposePost({
|
|||||||
const {extLink, setExtLink} = useExternalLinkFetch({setQuote})
|
const {extLink, setExtLink} = useExternalLinkFetch({setQuote})
|
||||||
const [extGif, setExtGif] = useState<Gif>()
|
const [extGif, setExtGif] = useState<Gif>()
|
||||||
const [labels, setLabels] = useState<string[]>([])
|
const [labels, setLabels] = useState<string[]>([])
|
||||||
const [threadgate, setThreadgate] = useState<ThreadgateAllowUISetting[]>([])
|
const [threadgateAllowUISettings, onChangeThreadgateAllowUISettings] =
|
||||||
|
useState<ThreadgateAllowUISetting[]>(
|
||||||
|
threadgateViewToAllowUISetting(undefined),
|
||||||
|
)
|
||||||
|
const [postgate, setPostgate] = useState(createPostgateRecord({post: ''}))
|
||||||
|
|
||||||
const gallery = useMemo(
|
const gallery = useMemo(
|
||||||
() => new GalleryModel(initImageUris),
|
() => new GalleryModel(initImageUris),
|
||||||
@@ -334,7 +340,8 @@ export const ComposePost = observer(function ComposePost({
|
|||||||
quote,
|
quote,
|
||||||
extLink,
|
extLink,
|
||||||
labels,
|
labels,
|
||||||
threadgate,
|
threadgate: threadgateAllowUISettings,
|
||||||
|
postgate,
|
||||||
onStateChange: setProcessingState,
|
onStateChange: setProcessingState,
|
||||||
langs: toPostLanguages(langPrefs.postLanguage),
|
langs: toPostLanguages(langPrefs.postLanguage),
|
||||||
})
|
})
|
||||||
@@ -664,8 +671,12 @@ export const ComposePost = observer(function ComposePost({
|
|||||||
|
|
||||||
{replyTo ? null : (
|
{replyTo ? null : (
|
||||||
<ThreadgateBtn
|
<ThreadgateBtn
|
||||||
threadgate={threadgate}
|
postgate={postgate}
|
||||||
onChange={setThreadgate}
|
onChangePostgate={setPostgate}
|
||||||
|
threadgateAllowUISettings={threadgateAllowUISettings}
|
||||||
|
onChangeThreadgateAllowUISettings={
|
||||||
|
onChangeThreadgateAllowUISettings
|
||||||
|
}
|
||||||
style={bottomBarAnimatedStyle}
|
style={bottomBarAnimatedStyle}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {Keyboard, StyleProp, ViewStyle} from 'react-native'
|
import {Keyboard, StyleProp, ViewStyle} from 'react-native'
|
||||||
import Animated, {AnimatedStyle} from 'react-native-reanimated'
|
import Animated, {AnimatedStyle} from 'react-native-reanimated'
|
||||||
|
import {AppBskyFeedPostgate} from '@atproto/api'
|
||||||
import {msg} from '@lingui/macro'
|
import {msg} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
import {isNative} from '#/platform/detection'
|
import {isNative} from '#/platform/detection'
|
||||||
|
import {embeddingRules} from '#/state/queries/postgate/util'
|
||||||
import {ThreadgateAllowUISetting} from '#/state/queries/threadgate'
|
import {ThreadgateAllowUISetting} from '#/state/queries/threadgate'
|
||||||
import {useAnalytics} from 'lib/analytics/analytics'
|
import {useAnalytics} from 'lib/analytics/analytics'
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
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'
|
import {Group3_Stroke2_Corner0_Rounded as Group} from '#/components/icons/Group'
|
||||||
|
|
||||||
export function ThreadgateBtn({
|
export function ThreadgateBtn({
|
||||||
threadgate,
|
postgate,
|
||||||
onChange,
|
onChangePostgate,
|
||||||
|
threadgateAllowUISettings,
|
||||||
|
onChangeThreadgateAllowUISettings,
|
||||||
style,
|
style,
|
||||||
}: {
|
}: {
|
||||||
threadgate: ThreadgateAllowUISetting[]
|
postgate: AppBskyFeedPostgate.Record
|
||||||
onChange: (v: ThreadgateAllowUISetting[]) => void
|
onChangePostgate: (v: AppBskyFeedPostgate.Record) => void
|
||||||
|
|
||||||
|
threadgateAllowUISettings: ThreadgateAllowUISetting[]
|
||||||
|
onChangeThreadgateAllowUISettings: (v: ThreadgateAllowUISetting[]) => void
|
||||||
|
|
||||||
style?: StyleProp<AnimatedStyle<ViewStyle>>
|
style?: StyleProp<AnimatedStyle<ViewStyle>>
|
||||||
}) {
|
}) {
|
||||||
const {track} = useAnalytics()
|
const {track} = useAnalytics()
|
||||||
@@ -38,13 +46,24 @@ export function ThreadgateBtn({
|
|||||||
control.open()
|
control.open()
|
||||||
}
|
}
|
||||||
|
|
||||||
const isEverybody = threadgate.length === 0
|
const anyoneCanReply =
|
||||||
const isNobody = !!threadgate.find(gate => gate.type === 'nobody')
|
threadgateAllowUISettings.length === 1 &&
|
||||||
const label = isEverybody
|
threadgateAllowUISettings[0].type === 'everybody'
|
||||||
? _(msg`Everybody can reply`)
|
const noOneCanReply =
|
||||||
: isNobody
|
threadgateAllowUISettings.length === 1 &&
|
||||||
? _(msg`Nobody can reply`)
|
threadgateAllowUISettings[0].type === 'nobody'
|
||||||
: _(msg`Some people can reply`)
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -60,15 +79,27 @@ export function ThreadgateBtn({
|
|||||||
msg`Opens a dialog to choose who can reply to this thread`,
|
msg`Opens a dialog to choose who can reply to this thread`,
|
||||||
)}>
|
)}>
|
||||||
<ButtonIcon
|
<ButtonIcon
|
||||||
icon={isEverybody ? Earth : isNobody ? CircleBanSign : Group}
|
icon={
|
||||||
|
anyoneCanInteract
|
||||||
|
? Earth
|
||||||
|
: noOneCanInteract
|
||||||
|
? CircleBanSign
|
||||||
|
: Group
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<ButtonText>{label}</ButtonText>
|
<ButtonText>{label}</ButtonText>
|
||||||
</Button>
|
</Button>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
<ThreadgateEditorDialog
|
<ThreadgateEditorDialog
|
||||||
control={control}
|
control={control}
|
||||||
threadgateUISettings={threadgate}
|
isSaving={false}
|
||||||
onChangeThreadgateUISettings={onChange}
|
onSave={() => {
|
||||||
|
control.close()
|
||||||
|
}}
|
||||||
|
postgate={postgate}
|
||||||
|
onChangePostgate={onChangePostgate}
|
||||||
|
threadgateAllowUISettings={threadgateAllowUISettings}
|
||||||
|
onChangeThreadgateAllowUISettings={onChangeThreadgateAllowUISettings}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {StyleSheet, View} from 'react-native'
|
|||||||
import {
|
import {
|
||||||
AppBskyFeedDefs,
|
AppBskyFeedDefs,
|
||||||
AppBskyFeedPost,
|
AppBskyFeedPost,
|
||||||
|
AppBskyFeedPostgate,
|
||||||
AtUri,
|
AtUri,
|
||||||
ModerationDecision,
|
ModerationDecision,
|
||||||
RichText as RichTextAPI,
|
RichText as RichTextAPI,
|
||||||
@@ -15,6 +16,8 @@ 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 {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'
|
||||||
@@ -80,6 +83,7 @@ export function PostThreadItem({
|
|||||||
onPostReply: (postUri: string | undefined) => void
|
onPostReply: (postUri: string | undefined) => void
|
||||||
hideTopBorder?: boolean
|
hideTopBorder?: boolean
|
||||||
}) {
|
}) {
|
||||||
|
const {data: postgate, isLoading} = usePostgateQuery({postUri: post.uri})
|
||||||
const postShadowed = usePostShadow(post)
|
const postShadowed = usePostShadow(post)
|
||||||
const richText = useMemo(
|
const richText = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@@ -92,7 +96,7 @@ export function PostThreadItem({
|
|||||||
if (postShadowed === POST_TOMBSTONE) {
|
if (postShadowed === POST_TOMBSTONE) {
|
||||||
return <PostThreadItemDeleted hideTopBorder={hideTopBorder} />
|
return <PostThreadItemDeleted hideTopBorder={hideTopBorder} />
|
||||||
}
|
}
|
||||||
if (richText && moderation) {
|
if (richText && moderation && !isLoading) {
|
||||||
return (
|
return (
|
||||||
<PostThreadItemLoaded
|
<PostThreadItemLoaded
|
||||||
// Safeguard from clobbering per-post state below:
|
// Safeguard from clobbering per-post state below:
|
||||||
@@ -113,6 +117,7 @@ export function PostThreadItem({
|
|||||||
overrideBlur={overrideBlur}
|
overrideBlur={overrideBlur}
|
||||||
onPostReply={onPostReply}
|
onPostReply={onPostReply}
|
||||||
hideTopBorder={hideTopBorder}
|
hideTopBorder={hideTopBorder}
|
||||||
|
postgate={postgate}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -156,6 +161,7 @@ let PostThreadItemLoaded = ({
|
|||||||
overrideBlur,
|
overrideBlur,
|
||||||
onPostReply,
|
onPostReply,
|
||||||
hideTopBorder,
|
hideTopBorder,
|
||||||
|
postgate,
|
||||||
}: {
|
}: {
|
||||||
post: Shadow<AppBskyFeedDefs.PostView>
|
post: Shadow<AppBskyFeedDefs.PostView>
|
||||||
record: AppBskyFeedPost.Record
|
record: AppBskyFeedPost.Record
|
||||||
@@ -173,6 +179,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
|
||||||
}): React.ReactNode => {
|
}): React.ReactNode => {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
@@ -360,6 +367,7 @@ 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.
|
||||||
@@ -663,15 +671,18 @@ 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()
|
||||||
const openLink = useOpenLink()
|
const openLink = useOpenLink()
|
||||||
|
const isRootPost = !('reply' in post.record)
|
||||||
|
|
||||||
const onTranslatePress = React.useCallback(() => {
|
const onTranslatePress = React.useCallback(() => {
|
||||||
openLink(translatorUrl)
|
openLink(translatorUrl)
|
||||||
@@ -688,7 +699,19 @@ function ExpandedPostDetails({
|
|||||||
s.mb10,
|
s.mb10,
|
||||||
]}>
|
]}>
|
||||||
<Text style={[a.text_sm, pal.textLight]}>{niceDate(post.indexedAt)}</Text>
|
<Text style={[a.text_sm, pal.textLight]}>{niceDate(post.indexedAt)}</Text>
|
||||||
<WhoCanReply post={post} isThreadAuthor={isThreadAuthor} />
|
{isRootPost && (
|
||||||
|
<WhoCanReply
|
||||||
|
post={post}
|
||||||
|
postgate={
|
||||||
|
// TODO maybe define at query
|
||||||
|
postgate ||
|
||||||
|
createPostgateRecord({
|
||||||
|
post: post.uri,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
isThreadAuthor={isThreadAuthor}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{needsTranslation && (
|
{needsTranslation && (
|
||||||
<>
|
<>
|
||||||
<Text style={[a.text_sm, pal.textLight]}>·</Text>
|
<Text style={[a.text_sm, pal.textLight]}>·</Text>
|
||||||
|
|||||||
Reference in New Issue
Block a user