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 {
|
||||
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<ViewStyle>
|
||||
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}) => (
|
||||
<View style={[a.flex_row, a.align_center, a.gap_xs, style]}>
|
||||
@@ -154,9 +218,13 @@ export function WhoCanReply({post, isThreadAuthor, style}: WhoCanReplyProps) {
|
||||
|
||||
{isThreadAuthor ? (
|
||||
<ThreadgateEditorDialog
|
||||
onSave={onSave}
|
||||
isSaving={isSaving}
|
||||
postgate={postgate}
|
||||
onChangePostgate={onChangePostgate}
|
||||
threadgateAllowUISettings={settings}
|
||||
onChangeThreadgateAllowUISettings={onChangeThreadgateAllowUISettings}
|
||||
control={editDialogControl}
|
||||
threadgateUISettings={settings}
|
||||
onConfirmThreadgateUISettings={onEditConfirm}
|
||||
/>
|
||||
) : (
|
||||
<WhoCanReplyDialog
|
||||
|
||||
@@ -1,79 +1,68 @@
|
||||
import React from 'react'
|
||||
import {StyleProp, View, ViewStyle} from 'react-native'
|
||||
import {AppBskyFeedPostgate} from '@atproto/api'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import isEqual from 'lodash.isequal'
|
||||
|
||||
import {useMyListsQuery} from '#/state/queries/my-lists'
|
||||
import {
|
||||
createPostgateRecord,
|
||||
embeddingRules,
|
||||
} from '#/state/queries/postgate/util'
|
||||
import {ThreadgateAllowUISetting} from '#/state/queries/threadgate'
|
||||
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 {Divider} from '#/components/Divider'
|
||||
import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
|
||||
import {Loader} from '#/components/Loader'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
type Props = {
|
||||
threadgateUISettings: ThreadgateAllowUISetting[]
|
||||
onChangeThreadgateUISettings?: (v: ThreadgateAllowUISetting[]) => 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 (
|
||||
<Dialog.Outer control={control}>
|
||||
<Dialog.Handle />
|
||||
<DialogContent
|
||||
onChangeThreadgateUISettings={onChangeThreadgateUISettings}
|
||||
onConfirmThreadgateUISettings={onConfirmThreadgateUISettings}
|
||||
threadgateUISettings={threadgateUISettings}
|
||||
/>
|
||||
<DialogContent {...rest} />
|
||||
</Dialog.Outer>
|
||||
)
|
||||
}
|
||||
|
||||
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 (
|
||||
<Dialog.ScrollableInner
|
||||
label={_(msg`Choose who can reply`)}
|
||||
label={_(msg`Edit post interaction settings`)}
|
||||
style={[{maxWidth: 500}, a.w_full]}>
|
||||
<View style={[a.flex_1, a.gap_md]}>
|
||||
<Text style={[a.text_2xl, a.font_bold]}>
|
||||
<Trans>Choose who can reply</Trans>
|
||||
<Trans>Post interaction settings</Trans>
|
||||
</Text>
|
||||
<Text style={a.mt_xs}>
|
||||
<Trans>Either choose "Everybody" or "Nobody"</Trans>
|
||||
</Text>
|
||||
<View style={[a.flex_row, a.gap_sm]}>
|
||||
<Selectable
|
||||
label={_(msg`Everybody`)}
|
||||
isSelected={!!draft.find(v => v.type === 'everybody')}
|
||||
onPress={onPressEverybody}
|
||||
style={{flex: 1}}
|
||||
/>
|
||||
<Selectable
|
||||
label={_(msg`Nobody`)}
|
||||
isSelected={!!draft.find(v => v.type === 'nobody')}
|
||||
onPress={onPressNobody}
|
||||
style={{flex: 1}}
|
||||
/>
|
||||
</View>
|
||||
<Text style={a.mt_md}>
|
||||
<Trans>Or combine these options:</Trans>
|
||||
</Text>
|
||||
<View style={[a.gap_sm]}>
|
||||
<Selectable
|
||||
label={_(msg`Mentioned users`)}
|
||||
isSelected={!!draft.find(v => v.type === 'mention')}
|
||||
onPress={() => onPressAudience({type: 'mention'})}
|
||||
/>
|
||||
<Selectable
|
||||
label={_(msg`Followed users`)}
|
||||
isSelected={!!draft.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={
|
||||
!!draft.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 style={[a.gap_lg]}>
|
||||
<Text style={[a.text_md]}>
|
||||
<Trans>Customize who can engage with this post.</Trans>
|
||||
</Text>
|
||||
|
||||
<Divider />
|
||||
|
||||
<View style={[a.gap_sm]}>
|
||||
<Text style={[a.font_bold, a.text_lg]}>
|
||||
<Trans>Quote settings</Trans>
|
||||
</Text>
|
||||
|
||||
<Text style={[a.pt_sm, t.atoms.text_contrast_medium]}>
|
||||
<Trans>Allow quote posts from:</Trans>
|
||||
</Text>
|
||||
|
||||
<View style={[a.flex_row, a.gap_sm]}>
|
||||
<Selectable
|
||||
label={_(msg`Everybody`)}
|
||||
isSelected={
|
||||
!postgate.quotepostRules ||
|
||||
postgate.quotepostRules?.length === 0
|
||||
}
|
||||
onPress={() => onChangeEmbeddingRules([])}
|
||||
style={{flex: 1}}
|
||||
/>
|
||||
<Selectable
|
||||
label={_(msg`Nobody`)}
|
||||
isSelected={Boolean(
|
||||
postgate.quotepostRules &&
|
||||
postgate.quotepostRules.find(
|
||||
v => v.$type === embeddingRules.disableRule.$type,
|
||||
),
|
||||
)}
|
||||
onPress={() =>
|
||||
onChangeEmbeddingRules([embeddingRules.disableRule])
|
||||
}
|
||||
style={{flex: 1}}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<Divider />
|
||||
|
||||
<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>
|
||||
|
||||
<Button
|
||||
label={doneLabel}
|
||||
onPress={() => {
|
||||
control.close()
|
||||
onConfirmThreadgateUISettings?.(draft)
|
||||
}}
|
||||
onPress={onSave}
|
||||
onAccessibilityEscape={control.close}
|
||||
color="primary"
|
||||
size="medium"
|
||||
variant="solid"
|
||||
style={a.mt_xl}>
|
||||
<ButtonText>{doneLabel}</ButtonText>
|
||||
{isSaving && <ButtonIcon icon={Loader} position="right" />}
|
||||
</Button>
|
||||
|
||||
<Dialog.Close />
|
||||
</Dialog.ScrollableInner>
|
||||
)
|
||||
|
||||
+40
-14
@@ -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.',
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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<AppBskyFeedPostgate.Record>,
|
||||
postgate: Partial<AppBskyFeedPostgate.Record> & {
|
||||
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'},
|
||||
}
|
||||
|
||||
@@ -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'}]
|
||||
|
||||
@@ -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<Gif>()
|
||||
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(
|
||||
() => 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 : (
|
||||
<ThreadgateBtn
|
||||
threadgate={threadgate}
|
||||
onChange={setThreadgate}
|
||||
postgate={postgate}
|
||||
onChangePostgate={setPostgate}
|
||||
threadgateAllowUISettings={threadgateAllowUISettings}
|
||||
onChangeThreadgateAllowUISettings={
|
||||
onChangeThreadgateAllowUISettings
|
||||
}
|
||||
style={bottomBarAnimatedStyle}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -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<AnimatedStyle<ViewStyle>>
|
||||
}) {
|
||||
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`,
|
||||
)}>
|
||||
<ButtonIcon
|
||||
icon={isEverybody ? Earth : isNobody ? CircleBanSign : Group}
|
||||
icon={
|
||||
anyoneCanInteract
|
||||
? Earth
|
||||
: noOneCanInteract
|
||||
? CircleBanSign
|
||||
: Group
|
||||
}
|
||||
/>
|
||||
<ButtonText>{label}</ButtonText>
|
||||
</Button>
|
||||
</Animated.View>
|
||||
<ThreadgateEditorDialog
|
||||
control={control}
|
||||
threadgateUISettings={threadgate}
|
||||
onChangeThreadgateUISettings={onChange}
|
||||
isSaving={false}
|
||||
onSave={() => {
|
||||
control.close()
|
||||
}}
|
||||
postgate={postgate}
|
||||
onChangePostgate={onChangePostgate}
|
||||
threadgateAllowUISettings={threadgateAllowUISettings}
|
||||
onChangeThreadgateAllowUISettings={onChangeThreadgateAllowUISettings}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -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 <PostThreadItemDeleted hideTopBorder={hideTopBorder} />
|
||||
}
|
||||
if (richText && moderation) {
|
||||
if (richText && moderation && !isLoading) {
|
||||
return (
|
||||
<PostThreadItemLoaded
|
||||
// Safeguard from clobbering per-post state below:
|
||||
@@ -113,6 +117,7 @@ export function PostThreadItem({
|
||||
overrideBlur={overrideBlur}
|
||||
onPostReply={onPostReply}
|
||||
hideTopBorder={hideTopBorder}
|
||||
postgate={postgate}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -156,6 +161,7 @@ let PostThreadItemLoaded = ({
|
||||
overrideBlur,
|
||||
onPostReply,
|
||||
hideTopBorder,
|
||||
postgate,
|
||||
}: {
|
||||
post: Shadow<AppBskyFeedDefs.PostView>
|
||||
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,
|
||||
]}>
|
||||
<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 && (
|
||||
<>
|
||||
<Text style={[a.text_sm, pal.textLight]}>·</Text>
|
||||
|
||||
Reference in New Issue
Block a user