Integrate postgates into ThreadgateEditor dialog

This commit is contained in:
Eric Bailey
2024-08-09 15:04:01 -05:00
parent f9e988c5fb
commit 26e7d31557
9 changed files with 424 additions and 167 deletions
+100 -32
View File
@@ -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
+166 -95
View File
@@ -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
View File
@@ -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.',
)
}
+25
View File
@@ -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()
+7 -5
View File
@@ -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'},
}
+1 -1
View File
@@ -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'}]
+15 -4
View File
@@ -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}
/>
</>
)
+25 -2
View File
@@ -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]}>&middot;</Text>