Add post interaction settings to dropdown for all users
This commit is contained in:
@@ -16,10 +16,11 @@ 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 {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
|
||||
import {Loader} from '#/components/Loader'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
type Props = {
|
||||
export type Props = {
|
||||
onSave: () => void
|
||||
isSaving: boolean
|
||||
|
||||
@@ -28,6 +29,8 @@ type Props = {
|
||||
|
||||
threadgateAllowUISettings: ThreadgateAllowUISetting[]
|
||||
onChangeThreadgateAllowUISettings: (v: ThreadgateAllowUISetting[]) => void
|
||||
|
||||
replySettingsDisabled?: boolean
|
||||
}
|
||||
|
||||
export function PostInteractionSettingsDialog({
|
||||
@@ -36,21 +39,28 @@ export function PostInteractionSettingsDialog({
|
||||
}: Props & {
|
||||
control: Dialog.DialogControlProps
|
||||
}) {
|
||||
const {_} = useLingui()
|
||||
return (
|
||||
<Dialog.Outer control={control}>
|
||||
<Dialog.Handle />
|
||||
<DialogContent {...rest} />
|
||||
<Dialog.ScrollableInner
|
||||
label={_(msg`Edit post interaction settings`)}
|
||||
style={[{maxWidth: 500}, a.w_full]}>
|
||||
<PostInteractionSettingsDialogInner {...rest} />
|
||||
<Dialog.Close />
|
||||
</Dialog.ScrollableInner>
|
||||
</Dialog.Outer>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogContent({
|
||||
export function PostInteractionSettingsDialogInner({
|
||||
onSave,
|
||||
isSaving,
|
||||
postgate,
|
||||
onChangePostgate,
|
||||
threadgateAllowUISettings,
|
||||
onChangeThreadgateAllowUISettings,
|
||||
replySettingsDisabled,
|
||||
}: Props) {
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
@@ -91,9 +101,7 @@ function DialogContent({
|
||||
)
|
||||
|
||||
return (
|
||||
<Dialog.ScrollableInner
|
||||
label={_(msg`Edit post interaction settings`)}
|
||||
style={[{maxWidth: 500}, a.w_full]}>
|
||||
<View>
|
||||
<View style={[a.flex_1, a.gap_md]}>
|
||||
<Text style={[a.text_2xl, a.font_bold]}>
|
||||
<Trans>Post interaction settings</Trans>
|
||||
@@ -143,7 +151,31 @@ function DialogContent({
|
||||
|
||||
<Divider />
|
||||
|
||||
<View style={[a.gap_sm]}>
|
||||
{replySettingsDisabled && (
|
||||
<View
|
||||
style={[
|
||||
a.px_md,
|
||||
a.py_sm,
|
||||
a.rounded_sm,
|
||||
a.flex_row,
|
||||
a.align_center,
|
||||
a.gap_sm,
|
||||
t.atoms.bg_contrast_25,
|
||||
]}>
|
||||
<CircleInfo fill={t.atoms.text_contrast_low.color} />
|
||||
<Text style={[a.leading_snug, t.atoms.text_contrast_medium]}>
|
||||
<Trans>Reply settings are inherited from the root post</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<View
|
||||
style={[
|
||||
a.gap_sm,
|
||||
{
|
||||
opacity: replySettingsDisabled ? 0.3 : 1,
|
||||
},
|
||||
]}>
|
||||
<Text style={[a.font_bold, a.text_lg]}>
|
||||
<Trans>Reply settings</Trans>
|
||||
</Text>
|
||||
@@ -162,6 +194,7 @@ function DialogContent({
|
||||
onChangeThreadgateAllowUISettings([{type: 'everybody'}])
|
||||
}
|
||||
style={{flex: 1}}
|
||||
disabled={replySettingsDisabled}
|
||||
/>
|
||||
<Selectable
|
||||
label={_(msg`Nobody`)}
|
||||
@@ -170,6 +203,7 @@ function DialogContent({
|
||||
onChangeThreadgateAllowUISettings([{type: 'nobody'}])
|
||||
}
|
||||
style={{flex: 1}}
|
||||
disabled={replySettingsDisabled}
|
||||
/>
|
||||
</View>
|
||||
|
||||
@@ -188,6 +222,7 @@ function DialogContent({
|
||||
)
|
||||
}
|
||||
onPress={() => onPressAudience({type: 'mention'})}
|
||||
disabled={replySettingsDisabled}
|
||||
/>
|
||||
<Selectable
|
||||
label={_(msg`Followed users`)}
|
||||
@@ -197,6 +232,7 @@ function DialogContent({
|
||||
)
|
||||
}
|
||||
onPress={() => onPressAudience({type: 'following'})}
|
||||
disabled={replySettingsDisabled}
|
||||
/>
|
||||
{lists && lists.length > 0
|
||||
? lists.map(list => (
|
||||
@@ -211,6 +247,7 @@ function DialogContent({
|
||||
onPress={() =>
|
||||
onPressAudience({type: 'list', list: list.uri})
|
||||
}
|
||||
disabled={replySettingsDisabled}
|
||||
/>
|
||||
))
|
||||
: // No loading states to avoid jumps for the common case (no lists)
|
||||
@@ -233,9 +270,7 @@ function DialogContent({
|
||||
<ButtonText>{_(msg`Save`)}</ButtonText>
|
||||
{isSaving && <ButtonIcon icon={Loader} position="right" />}
|
||||
</Button>
|
||||
|
||||
<Dialog.Close />
|
||||
</Dialog.ScrollableInner>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -244,15 +279,18 @@ function Selectable({
|
||||
isSelected,
|
||||
onPress,
|
||||
style,
|
||||
disabled,
|
||||
}: {
|
||||
label: string
|
||||
isSelected: boolean
|
||||
onPress: () => void
|
||||
style?: StyleProp<ViewStyle>
|
||||
disabled?: boolean
|
||||
}) {
|
||||
const t = useTheme()
|
||||
return (
|
||||
<Button
|
||||
disabled={disabled}
|
||||
onPress={onPress}
|
||||
label={label}
|
||||
accessibilityHint="Select this option"
|
||||
|
||||
@@ -2,35 +2,41 @@ import {AppBskyFeedDefs, AppBskyFeedThreadgate} from '@atproto/api'
|
||||
|
||||
import {ThreadgateAllowUISetting} from '#/state/queries/threadgate/types'
|
||||
|
||||
export function threadgateViewToAllowUISetting(
|
||||
threadgateView: AppBskyFeedDefs.ThreadgateView | undefined,
|
||||
): ThreadgateAllowUISetting[] {
|
||||
const threadgate =
|
||||
threadgateView &&
|
||||
AppBskyFeedThreadgate.isRecord(threadgateView.record) &&
|
||||
AppBskyFeedThreadgate.validateRecord(threadgateView.record).success
|
||||
? threadgateView.record
|
||||
: undefined
|
||||
return threadgateRecordToAllowUISetting(threadgate)
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a full {@link AppBskyFeedThreadgate.Record} to a list of
|
||||
* {@link ThreadgateAllowUISetting}, for use by app UI.
|
||||
*/
|
||||
export function threadgateViewToAllowUISetting(
|
||||
threadgate: AppBskyFeedDefs.ThreadgateView | undefined,
|
||||
export function threadgateRecordToAllowUISetting(
|
||||
threadgate: AppBskyFeedThreadgate.Record | undefined,
|
||||
): ThreadgateAllowUISetting[] {
|
||||
const record =
|
||||
threadgate &&
|
||||
AppBskyFeedThreadgate.isRecord(threadgate.record) &&
|
||||
AppBskyFeedThreadgate.validateRecord(threadgate.record).success
|
||||
? threadgate.record
|
||||
: null
|
||||
/*
|
||||
* If record doesn't exist (default), or if `record.allow === undefined`, it means
|
||||
* If `threadgate` doesn't exist (default), or if `threadgate.allow === undefined`, it means
|
||||
* anyone can reply.
|
||||
*
|
||||
* If `record.allow === []` it means no one can reply, and we translate to UI code
|
||||
* If `threadgate.allow === []` it means no one can reply, and we translate to UI code
|
||||
* here. This was a historical choice, and we have no lexicon representation
|
||||
* for 'replies disabled' other than an empty array.
|
||||
*/
|
||||
if (!record || record.allow === undefined) {
|
||||
if (!threadgate || threadgate.allow === undefined) {
|
||||
return [{type: 'everybody'}]
|
||||
}
|
||||
if (record.allow.length === 0) {
|
||||
if (threadgate.allow.length === 0) {
|
||||
return [{type: 'nobody'}]
|
||||
}
|
||||
|
||||
const settings: ThreadgateAllowUISetting[] = record.allow
|
||||
const settings: ThreadgateAllowUISetting[] = threadgate.allow
|
||||
.map(allow => {
|
||||
let setting: ThreadgateAllowUISetting | undefined
|
||||
if (allow.$type === 'app.bsky.feed.threadgate#mentionRule') {
|
||||
|
||||
@@ -83,6 +83,7 @@ export function PostThreadItem({
|
||||
onPostReply: (postUri: string | undefined) => void
|
||||
hideTopBorder?: boolean
|
||||
}) {
|
||||
// TODO should load this for every post, ugh
|
||||
const {data: postgate, isLoading} = usePostgateQuery({postUri: post.uri})
|
||||
const postShadowed = usePostShadow(post)
|
||||
const richText = useMemo(
|
||||
|
||||
@@ -9,6 +9,7 @@ import * as Clipboard from 'expo-clipboard'
|
||||
import {
|
||||
AppBskyFeedDefs,
|
||||
AppBskyFeedPost,
|
||||
AppBskyFeedPostgate,
|
||||
AppBskyFeedThreadgate,
|
||||
AtUri,
|
||||
RichText as RichTextAPI,
|
||||
@@ -32,9 +33,17 @@ import {
|
||||
usePostDeleteMutation,
|
||||
useThreadMuteMutationQueue,
|
||||
} from '#/state/queries/post'
|
||||
import {useToggleQuoteDetachmentMutation} from '#/state/queries/postgate'
|
||||
import {getMaybeDetachedQuoteEmbed} from '#/state/queries/postgate/util'
|
||||
import {
|
||||
usePostgateQuery,
|
||||
useToggleQuoteDetachmentMutation,
|
||||
useWritePostgateMutation,
|
||||
} from '#/state/queries/postgate'
|
||||
import {
|
||||
createPostgateRecord,
|
||||
getMaybeDetachedQuoteEmbed,
|
||||
} from '#/state/queries/postgate/util'
|
||||
import {useToggleReplyVisibilityMutation} from '#/state/queries/threadgate'
|
||||
import {threadgateRecordToAllowUISetting} from '#/state/queries/threadgate/util'
|
||||
import {useSession} from '#/state/session'
|
||||
import {getCurrentRoute} from 'lib/routes/helpers'
|
||||
import {shareUrl} from 'lib/sharing'
|
||||
@@ -42,8 +51,10 @@ import {toShareUrl} from 'lib/strings/url-helpers'
|
||||
import {useTheme} from 'lib/ThemeContext'
|
||||
import {atoms as a, useBreakpoints, useTheme as useAlf} from '#/alf'
|
||||
import {useDialogControl} from '#/components/Dialog'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import {useGlobalDialogsControlContext} from '#/components/dialogs/Context'
|
||||
import {EmbedDialog} from '#/components/dialogs/Embed'
|
||||
import {PostInteractionSettingsDialogInner} from '#/components/dialogs/PostInteractionSettingsDialog'
|
||||
import {SendViaChatDialog} from '#/components/dms/dialogs/ShareViaChatDialog'
|
||||
import {ArrowOutOfBox_Stroke2_Corner0_Rounded as Share} from '#/components/icons/ArrowOutOfBox'
|
||||
import {BubbleQuestion_Stroke2_Corner0_Rounded as Translate} from '#/components/icons/Bubble'
|
||||
@@ -59,6 +70,7 @@ import {EyeSlash_Stroke2_Corner0_Rounded as EyeSlash} from '#/components/icons/E
|
||||
import {Filter_Stroke2_Corner0_Rounded as Filter} from '#/components/icons/Filter'
|
||||
import {Mute_Stroke2_Corner0_Rounded as Mute} from '#/components/icons/Mute'
|
||||
import {PaperPlane_Stroke2_Corner0_Rounded as Send} from '#/components/icons/PaperPlane'
|
||||
import {SettingsGear2_Stroke2_Corner0_Rounded as Gear} from '#/components/icons/SettingsGear2'
|
||||
import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as Unmute} from '#/components/icons/Speaker'
|
||||
import {Trash_Stroke2_Corner0_Rounded as Trash} from '#/components/icons/Trash'
|
||||
import {Warning_Stroke2_Corner0_Rounded as Warning} from '#/components/icons/Warning'
|
||||
@@ -114,6 +126,7 @@ let PostDropdownBtn = ({
|
||||
const loggedOutWarningPromptControl = useDialogControl()
|
||||
const embedPostControl = useDialogControl()
|
||||
const sendViaChatControl = useDialogControl()
|
||||
const postInteractionSettingsDialogControl = useDialogControl()
|
||||
const {mutateAsync: toggleReplyVisibility} =
|
||||
useToggleReplyVisibilityMutation()
|
||||
|
||||
@@ -136,6 +149,8 @@ let PostDropdownBtn = ({
|
||||
)
|
||||
const isPostHidden = hiddenPosts && hiddenPosts.includes(postUri)
|
||||
const isAuthor = postAuthor.did === currentAccount?.did
|
||||
const isRootPostAuthor =
|
||||
rootPostUri && new AtUri(rootPostUri).host === currentAccount?.did
|
||||
const isReplyHiddenByThreadgate =
|
||||
threadgateRecord?.hiddenReplies?.includes(postUri)
|
||||
|
||||
@@ -322,7 +337,8 @@ let PostDropdownBtn = ({
|
||||
}, [_, quoteEmbed, post, toggleQuoteDetachment])
|
||||
|
||||
const canEmbed = isWeb && gtMobile && !hideInPWI
|
||||
const canHideReply = !isAuthor && !isPostHidden && rootPostUri && isReply
|
||||
const canHideReply =
|
||||
!isAuthor && isRootPostAuthor && !isPostHidden && rootPostUri && isReply
|
||||
const canDetachQuote = quoteEmbed && quoteEmbed.isOwnedByViewer
|
||||
|
||||
return (
|
||||
@@ -549,13 +565,24 @@ let PostDropdownBtn = ({
|
||||
)}
|
||||
|
||||
{isAuthor && (
|
||||
<Menu.Item
|
||||
testID="postDropdownDeleteBtn"
|
||||
label={_(msg`Delete post`)}
|
||||
onPress={deletePromptControl.open}>
|
||||
<Menu.ItemText>{_(msg`Delete post`)}</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={Trash} position="right" />
|
||||
</Menu.Item>
|
||||
<>
|
||||
<Menu.Item
|
||||
testID="postDropdownEditPostInteractions"
|
||||
label={_(msg`Edit interaction settings`)}
|
||||
onPress={postInteractionSettingsDialogControl.open}>
|
||||
<Menu.ItemText>
|
||||
{_(msg`Edit interaction settings`)}
|
||||
</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={Gear} position="right" />
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
testID="postDropdownDeleteBtn"
|
||||
label={_(msg`Delete post`)}
|
||||
onPress={deletePromptControl.open}>
|
||||
<Menu.ItemText>{_(msg`Delete post`)}</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={Trash} position="right" />
|
||||
</Menu.Item>
|
||||
</>
|
||||
)}
|
||||
</Menu.Group>
|
||||
</>
|
||||
@@ -616,9 +643,112 @@ let PostDropdownBtn = ({
|
||||
control={sendViaChatControl}
|
||||
onSelectChat={onSelectChatToShareTo}
|
||||
/>
|
||||
|
||||
<PostInteractionSettingsDialogControlled
|
||||
control={postInteractionSettingsDialogControl}
|
||||
postUri={post.uri}
|
||||
threadgateRecord={threadgateRecord}
|
||||
/>
|
||||
</EventStopper>
|
||||
)
|
||||
}
|
||||
|
||||
export function PostInteractionSettingsDialogControlled(props: {
|
||||
control: Dialog.DialogControlProps
|
||||
postUri: string
|
||||
threadgateRecord?: AppBskyFeedThreadgate.Record
|
||||
}) {
|
||||
return (
|
||||
<Dialog.Outer control={props.control}>
|
||||
<Dialog.Handle />
|
||||
<PostInteractionSettingsDialogControlledInner {...props} />
|
||||
</Dialog.Outer>
|
||||
)
|
||||
}
|
||||
|
||||
function PostInteractionSettingsDialogControlledInner(props: {
|
||||
control: Dialog.DialogControlProps
|
||||
postUri: string
|
||||
threadgateRecord?: AppBskyFeedThreadgate.Record
|
||||
}) {
|
||||
const {_} = useLingui()
|
||||
const threadgateAllowUISettings = React.useMemo(() => {
|
||||
return threadgateRecordToAllowUISetting(props.threadgateRecord)
|
||||
}, [props.threadgateRecord])
|
||||
const [isSaving, setIsSaving] = React.useState(false)
|
||||
const {mutateAsync: writePostgateRecord} = useWritePostgateMutation()
|
||||
const {data: postgate, isLoading} = usePostgateQuery({postUri: props.postUri})
|
||||
const [editedPostgate, setEditedPostgate] =
|
||||
React.useState<AppBskyFeedPostgate.Record>()
|
||||
|
||||
const onChangePostgate = React.useCallback(
|
||||
(next: AppBskyFeedPostgate.Record) => {
|
||||
setEditedPostgate(next)
|
||||
},
|
||||
[setEditedPostgate],
|
||||
)
|
||||
|
||||
const onSave = React.useCallback(async () => {
|
||||
if (!editedPostgate) {
|
||||
props.control.close()
|
||||
return
|
||||
}
|
||||
|
||||
setIsSaving(true)
|
||||
|
||||
try {
|
||||
await writePostgateRecord({
|
||||
postUri: props.postUri,
|
||||
postgate: editedPostgate,
|
||||
})
|
||||
props.control.close()
|
||||
} catch (e: any) {
|
||||
logger.error(`Failed to save post interaction settings`, {
|
||||
context: 'PostInteractionSettingsDialogControlledInner',
|
||||
safeMessage: e.message,
|
||||
})
|
||||
Toast.show(
|
||||
_(
|
||||
msg`There was an issue. Please check your internet connection and try again.`,
|
||||
),
|
||||
'xmark',
|
||||
)
|
||||
} finally {
|
||||
setIsSaving(false)
|
||||
}
|
||||
}, [
|
||||
_,
|
||||
props.postUri,
|
||||
props.control,
|
||||
editedPostgate,
|
||||
setIsSaving,
|
||||
writePostgateRecord,
|
||||
])
|
||||
|
||||
return (
|
||||
<Dialog.ScrollableInner
|
||||
label={_(msg`Edit post interaction settings`)}
|
||||
style={[{maxWidth: 500}, a.w_full]}>
|
||||
{isLoading ? (
|
||||
<Loader size="xl" />
|
||||
) : (
|
||||
<PostInteractionSettingsDialogInner
|
||||
replySettingsDisabled
|
||||
isSaving={isSaving}
|
||||
onSave={onSave}
|
||||
postgate={
|
||||
editedPostgate ||
|
||||
postgate ||
|
||||
createPostgateRecord({post: props.postUri})
|
||||
}
|
||||
onChangePostgate={onChangePostgate}
|
||||
threadgateAllowUISettings={threadgateAllowUISettings}
|
||||
onChangeThreadgateAllowUISettings={() => {}}
|
||||
/>
|
||||
)}
|
||||
</Dialog.ScrollableInner>
|
||||
)
|
||||
}
|
||||
|
||||
PostDropdownBtn = memo(PostDropdownBtn)
|
||||
export {PostDropdownBtn}
|
||||
|
||||
Reference in New Issue
Block a user