diff options
author | Eric Bailey <git@esb.lol> | 2025-02-06 11:51:40 -0600 |
---|---|---|
committer | GitHub <noreply@github.com> | 2025-02-06 11:51:40 -0600 |
commit | 9cd4f92027774029234e38980fac3a12f136166f (patch) | |
tree | 52805dd7ba11a128bc0cf582c984d89d9a7c390d /src | |
parent | 1db2668a96208046ffe316114f65d432e57db994 (diff) | |
download | voidsky-9cd4f92027774029234e38980fac3a12f136166f.tar.zst |
[APP-1013] Configure and apply default post interaction settings from user preferences (#7664)
* Add interaction settings screen * Move header out of interaction settings form * WIP hook it up * Thread through default settings into composer * Update copy pasta * Handle edited state * Copy feedback * Sentence case Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> * Update copy * Bump SDK * Fix new type error * Less in your face * Remove new dep * Add slot * Copy edit --------- Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>
Diffstat (limited to 'src')
-rw-r--r-- | src/Navigation.tsx | 9 | ||||
-rw-r--r-- | src/components/dialogs/PostInteractionSettingsDialog.tsx | 82 | ||||
-rw-r--r-- | src/lib/routes/types.ts | 1 | ||||
-rw-r--r-- | src/routes.ts | 1 | ||||
-rw-r--r-- | src/screens/Moderation/index.tsx | 16 | ||||
-rw-r--r-- | src/screens/ModerationInteractionSettings/index.tsx | 127 | ||||
-rw-r--r-- | src/screens/Settings/components/ExportCarDialog.tsx | 2 | ||||
-rw-r--r-- | src/state/queries/post-interaction-settings.ts | 20 | ||||
-rw-r--r-- | src/state/queries/preferences/const.ts | 4 | ||||
-rw-r--r-- | src/view/com/composer/Composer.tsx | 10 | ||||
-rw-r--r-- | src/view/com/composer/state/composer.ts | 25 |
11 files changed, 265 insertions, 32 deletions
diff --git a/src/Navigation.tsx b/src/Navigation.tsx index a6332c5d8..0dcce98bf 100644 --- a/src/Navigation.tsx +++ b/src/Navigation.tsx @@ -70,6 +70,7 @@ import {MessagesScreen} from '#/screens/Messages/ChatList' import {MessagesConversationScreen} from '#/screens/Messages/Conversation' import {MessagesSettingsScreen} from '#/screens/Messages/Settings' import {ModerationScreen} from '#/screens/Moderation' +import {Screen as ModerationInteractionSettings} from '#/screens/ModerationInteractionSettings' import {PostLikedByScreen} from '#/screens/Post/PostLikedBy' import {PostQuotesScreen} from '#/screens/Post/PostQuotes' import {PostRepostedByScreen} from '#/screens/Post/PostRepostedBy' @@ -156,6 +157,14 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) { options={{title: title(msg`Blocked Accounts`), requireAuth: true}} /> <Stack.Screen + name="ModerationInteractionSettings" + getComponent={() => ModerationInteractionSettings} + options={{ + title: title(msg`Post Interaction Settings`), + requireAuth: true, + }} + /> + <Stack.Screen name="Settings" getComponent={() => SettingsScreen} options={{title: title(msg`Settings`), requireAuth: true}} diff --git a/src/components/dialogs/PostInteractionSettingsDialog.tsx b/src/components/dialogs/PostInteractionSettingsDialog.tsx index a698574a4..b443d59f2 100644 --- a/src/components/dialogs/PostInteractionSettingsDialog.tsx +++ b/src/components/dialogs/PostInteractionSettingsDialog.tsx @@ -40,6 +40,7 @@ import {Loader} from '#/components/Loader' import {Text} from '#/components/Typography' export type PostInteractionSettingsFormProps = { + canSave?: boolean onSave: () => void isSaving?: boolean @@ -58,20 +59,53 @@ export function PostInteractionSettingsControlledDialog({ }: PostInteractionSettingsFormProps & { control: Dialog.DialogControlProps }) { + const t = useTheme() const {_} = useLingui() + return ( <Dialog.Outer control={control}> <Dialog.Handle /> <Dialog.ScrollableInner label={_(msg`Edit post interaction settings`)} style={[{maxWidth: 500}, a.w_full]}> - <PostInteractionSettingsForm {...rest} /> + <View style={[a.gap_md]}> + <Header /> + <PostInteractionSettingsForm {...rest} /> + <Text + style={[ + a.pt_sm, + a.text_sm, + a.leading_snug, + t.atoms.text_contrast_medium, + ]}> + <Trans> + You can set default interaction settings in{' '} + <Text style={[a.font_bold, t.atoms.text_contrast_medium]}> + Settings → Moderation → Interaction settings. + </Text> + </Trans> + </Text> + </View> <Dialog.Close /> </Dialog.ScrollableInner> </Dialog.Outer> ) } +export function Header() { + return ( + <View style={[a.gap_md, a.pb_sm]}> + <Text style={[a.text_2xl, a.font_bold]}> + <Trans>Post interaction settings</Trans> + </Text> + <Text style={[a.text_md, a.pb_xs]}> + <Trans>Customize who can interact with this post.</Trans> + </Text> + <Divider /> + </View> + ) +} + export type PostInteractionSettingsDialogProps = { control: Dialog.DialogControlProps /** @@ -203,26 +237,31 @@ export function PostInteractionSettingsDialogControlledInner( <Dialog.ScrollableInner label={_(msg`Edit post interaction settings`)} style={[{maxWidth: 500}, a.w_full]}> - {isLoading ? ( - <View style={[a.flex_1, a.py_4xl, a.align_center, a.justify_center]}> - <Loader size="xl" /> - </View> - ) : ( - <PostInteractionSettingsForm - replySettingsDisabled={!isThreadgateOwnedByViewer} - isSaving={isSaving} - onSave={onSave} - postgate={postgateValue} - onChangePostgate={setEditedPostgate} - threadgateAllowUISettings={allowUIValue} - onChangeThreadgateAllowUISettings={setEditedAllowUISettings} - /> - )} + <View style={[a.gap_md]}> + <Header /> + + {isLoading ? ( + <View style={[a.flex_1, a.py_4xl, a.align_center, a.justify_center]}> + <Loader size="xl" /> + </View> + ) : ( + <PostInteractionSettingsForm + replySettingsDisabled={!isThreadgateOwnedByViewer} + isSaving={isSaving} + onSave={onSave} + postgate={postgateValue} + onChangePostgate={setEditedPostgate} + threadgateAllowUISettings={allowUIValue} + onChangeThreadgateAllowUISettings={setEditedAllowUISettings} + /> + )} + </View> </Dialog.ScrollableInner> ) } export function PostInteractionSettingsForm({ + canSave = true, onSave, isSaving, postgate, @@ -283,17 +322,7 @@ export function PostInteractionSettingsForm({ return ( <View> <View style={[a.flex_1, a.gap_md]}> - <Text style={[a.text_2xl, a.font_bold]}> - <Trans>Post interaction settings</Trans> - </Text> - <View style={[a.gap_lg]}> - <Text style={[a.text_md]}> - <Trans>Customize who can interact with this post.</Trans> - </Text> - - <Divider /> - <View style={[a.gap_sm]}> <Text style={[a.font_bold, a.text_lg]}> <Trans>Quote settings</Trans> @@ -435,6 +464,7 @@ export function PostInteractionSettingsForm({ </View> <Button + disabled={!canSave || isSaving} label={_(msg`Save`)} onPress={onSave} color="primary" diff --git a/src/lib/routes/types.ts b/src/lib/routes/types.ts index 66ee7bffa..8b69a66c4 100644 --- a/src/lib/routes/types.ts +++ b/src/lib/routes/types.ts @@ -12,6 +12,7 @@ export type CommonNavigatorParams = { ModerationModlists: undefined ModerationMutedAccounts: undefined ModerationBlockedAccounts: undefined + ModerationInteractionSettings: undefined Settings: undefined Profile: {name: string; hideBackButton?: boolean} ProfileFollowers: {name: string} diff --git a/src/routes.ts b/src/routes.ts index 8541d4254..576ac92d1 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -13,6 +13,7 @@ export const router = new Router({ ModerationModlists: '/moderation/modlists', ModerationMutedAccounts: '/moderation/muted-accounts', ModerationBlockedAccounts: '/moderation/blocked-accounts', + ModerationInteractionSettings: '/moderation/interaction-settings', // profiles, threads, lists Profile: ['/profile/:name', '/profile/:name/rss'], ProfileFollowers: '/profile/:name/followers', diff --git a/src/screens/Moderation/index.tsx b/src/screens/Moderation/index.tsx index 6b4dd06bc..55cc67f8c 100644 --- a/src/screens/Moderation/index.tsx +++ b/src/screens/Moderation/index.tsx @@ -28,6 +28,7 @@ import * as Toggle from '#/components/forms/Toggle' import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron' import {CircleBanSign_Stroke2_Corner0_Rounded as CircleBanSign} from '#/components/icons/CircleBanSign' import {Props as SVGIconProps} from '#/components/icons/common' +import {EditBig_Stroke2_Corner0_Rounded as EditBig} from '#/components/icons/EditBig' import {Filter_Stroke2_Corner0_Rounded as Filter} from '#/components/icons/Filter' import {Group3_Stroke2_Corner0_Rounded as Group} from '#/components/icons/Group' import {Person_Stroke2_Corner0_Rounded as Person} from '#/components/icons/Person' @@ -199,6 +200,21 @@ export function ModerationScreenInner({ a.overflow_hidden, t.atoms.bg_contrast_25, ]}> + <Link + label={_(msg`View your default post interaction settings`)} + testID="interactionSettingsBtn" + to="/moderation/interaction-settings"> + {state => ( + <SubItem + title={_(msg`Interaction settings`)} + icon={EditBig} + style={[ + (state.hovered || state.pressed) && [t.atoms.bg_contrast_50], + ]} + /> + )} + </Link> + <Divider /> <Button testID="mutedWordsBtn" label={_(msg`Open muted words and tags settings`)} diff --git a/src/screens/ModerationInteractionSettings/index.tsx b/src/screens/ModerationInteractionSettings/index.tsx new file mode 100644 index 000000000..99b29d950 --- /dev/null +++ b/src/screens/ModerationInteractionSettings/index.tsx @@ -0,0 +1,127 @@ +import React from 'react' +import {View} from 'react-native' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' +import deepEqual from 'lodash.isequal' + +import {logger} from '#/logger' +import {usePostInteractionSettingsMutation} from '#/state/queries/post-interaction-settings' +import {createPostgateRecord} from '#/state/queries/postgate/util' +import { + usePreferencesQuery, + UsePreferencesQueryResponse, +} from '#/state/queries/preferences' +import { + threadgateAllowUISettingToAllowRecordValue, + threadgateRecordToAllowUISetting, +} from '#/state/queries/threadgate' +import * as Toast from '#/view/com/util/Toast' +import {atoms as a, useGutters} from '#/alf' +import {Admonition} from '#/components/Admonition' +import {PostInteractionSettingsForm} from '#/components/dialogs/PostInteractionSettingsDialog' +import * as Layout from '#/components/Layout' +import {Loader} from '#/components/Loader' + +export function Screen() { + const gutters = useGutters(['base']) + const {data: preferences} = usePreferencesQuery() + return ( + <Layout.Screen testID="ModerationInteractionSettingsScreen"> + <Layout.Header.Outer> + <Layout.Header.BackButton /> + <Layout.Header.Content> + <Layout.Header.TitleText> + <Trans>Post Interaction Settings</Trans> + </Layout.Header.TitleText> + </Layout.Header.Content> + <Layout.Header.Slot /> + </Layout.Header.Outer> + <Layout.Content> + <View style={[gutters, a.gap_xl]}> + <Admonition type="tip"> + <Trans> + The following settings will be used as your defaults when creating + new posts. You can edit these for a specific post from the + composer. + </Trans> + </Admonition> + {preferences ? ( + <Inner preferences={preferences} /> + ) : ( + <View style={[gutters, a.justify_center, a.align_center]}> + <Loader size="xl" /> + </View> + )} + </View> + </Layout.Content> + </Layout.Screen> + ) +} + +function Inner({preferences}: {preferences: UsePreferencesQueryResponse}) { + const {_} = useLingui() + const {mutateAsync: setPostInteractionSettings, isPending} = + usePostInteractionSettingsMutation() + const [error, setError] = React.useState<string | undefined>(undefined) + + const allowUI = React.useMemo(() => { + return threadgateRecordToAllowUISetting({ + $type: 'app.bsky.feed.threadgate', + post: '', + createdAt: new Date().toString(), + allow: preferences.postInteractionSettings.threadgateAllowRules, + }) + }, [preferences.postInteractionSettings.threadgateAllowRules]) + const postgate = React.useMemo(() => { + return createPostgateRecord({ + post: '', + embeddingRules: + preferences.postInteractionSettings.postgateEmbeddingRules, + }) + }, [preferences.postInteractionSettings.postgateEmbeddingRules]) + + const [maybeEditedAllowUI, setAllowUI] = React.useState(allowUI) + const [maybeEditedPostgate, setEditedPostgate] = React.useState(postgate) + + const wasEdited = React.useMemo(() => { + return ( + !deepEqual(allowUI, maybeEditedAllowUI) || + !deepEqual(postgate.embeddingRules, maybeEditedPostgate.embeddingRules) + ) + }, [postgate, allowUI, maybeEditedAllowUI, maybeEditedPostgate]) + + const onSave = React.useCallback(async () => { + setError('') + + try { + await setPostInteractionSettings({ + threadgateAllowRules: + threadgateAllowUISettingToAllowRecordValue(maybeEditedAllowUI), + postgateEmbeddingRules: maybeEditedPostgate.embeddingRules ?? [], + }) + Toast.show(_(msg`Settings saved`)) + } catch (e: any) { + logger.error(`Failed to save post interaction settings`, { + context: 'ModerationInteractionSettingsScreen', + safeMessage: e.message, + }) + setError(_(msg`Failed to save settings. Please try again.`)) + } + }, [_, maybeEditedPostgate, maybeEditedAllowUI, setPostInteractionSettings]) + + return ( + <> + <PostInteractionSettingsForm + canSave={wasEdited} + isSaving={isPending} + onSave={onSave} + postgate={maybeEditedPostgate} + onChangePostgate={setEditedPostgate} + threadgateAllowUISettings={maybeEditedAllowUI} + onChangeThreadgateAllowUISettings={setAllowUI} + /> + + {error && <Admonition type="error">{error}</Admonition>} + </> + ) +} diff --git a/src/screens/Settings/components/ExportCarDialog.tsx b/src/screens/Settings/components/ExportCarDialog.tsx index 2de3895d3..685707259 100644 --- a/src/screens/Settings/components/ExportCarDialog.tsx +++ b/src/screens/Settings/components/ExportCarDialog.tsx @@ -36,7 +36,7 @@ export function ExportCarDialog({ const saveRes = await saveBytesToDisk( 'repo.car', downloadRes.data, - downloadRes.headers['content-type'], + downloadRes.headers['content-type'] || 'application/vnd.ipld.car', ) if (saveRes) { diff --git a/src/state/queries/post-interaction-settings.ts b/src/state/queries/post-interaction-settings.ts new file mode 100644 index 000000000..a256f2956 --- /dev/null +++ b/src/state/queries/post-interaction-settings.ts @@ -0,0 +1,20 @@ +import {AppBskyActorDefs} from '@atproto/api' +import {useMutation, useQueryClient} from '@tanstack/react-query' + +import {preferencesQueryKey} from '#/state/queries/preferences' +import {useAgent} from '#/state/session' + +export function usePostInteractionSettingsMutation() { + const qc = useQueryClient() + const agent = useAgent() + return useMutation({ + async mutationFn(props: AppBskyActorDefs.PostInteractionSettingsPref) { + await agent.setPostInteractionSettings(props) + }, + async onSuccess() { + await qc.invalidateQueries({ + queryKey: preferencesQueryKey, + }) + }, + }) +} diff --git a/src/state/queries/preferences/const.ts b/src/state/queries/preferences/const.ts index 549f7ce29..3c1fead5e 100644 --- a/src/state/queries/preferences/const.ts +++ b/src/state/queries/preferences/const.ts @@ -39,4 +39,8 @@ export const DEFAULT_LOGGED_OUT_PREFERENCES: UsePreferencesQueryResponse = { activeProgressGuide: undefined, nuxs: [], }, + postInteractionSettings: { + threadgateAllowRules: undefined, + postgateEmbeddingRules: [], + }, } diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx index 0e9b52ce0..78293f618 100644 --- a/src/view/com/composer/Composer.tsx +++ b/src/view/com/composer/Composer.tsx @@ -83,6 +83,7 @@ import { useLanguagePrefs, useLanguagePrefsApi, } from '#/state/preferences/languages' +import {usePreferencesQuery} from '#/state/queries/preferences' import {useProfileQuery} from '#/state/queries/profile' import {Gif} from '#/state/queries/tenor' import {useAgent, useSession} from '#/state/session' @@ -169,6 +170,7 @@ export const ComposePost = ({ const discardPromptControl = Prompt.usePromptControl() const {closeAllDialogs} = useDialogStateControlContext() const {closeAllModals} = useModalControls() + const {data: preferences} = usePreferencesQuery() const [isKeyboardVisible] = useIsKeyboardVisible({iosUseWillEvents: true}) const [isPublishing, setIsPublishing] = useState(false) @@ -177,7 +179,13 @@ export const ComposePost = ({ const [composerState, composerDispatch] = useReducer( composerReducer, - {initImageUris, initQuoteUri: initQuote?.uri, initText, initMention}, + { + initImageUris, + initQuoteUri: initQuote?.uri, + initText, + initMention, + initInteractionSettings: preferences?.postInteractionSettings, + }, createComposerState, ) diff --git a/src/view/com/composer/state/composer.ts b/src/view/com/composer/state/composer.ts index 6d4f10297..f5a55f175 100644 --- a/src/view/com/composer/state/composer.ts +++ b/src/view/com/composer/state/composer.ts @@ -1,5 +1,10 @@ import {ImagePickerAsset} from 'expo-image-picker' -import {AppBskyFeedPostgate, AppBskyRichtextFacet, RichText} from '@atproto/api' +import { + AppBskyFeedPostgate, + AppBskyRichtextFacet, + BskyPreferences, + RichText, +} from '@atproto/api' import {nanoid} from 'nanoid/non-secure' import {SelfLabel} from '#/lib/moderation' @@ -13,7 +18,7 @@ import { import {ComposerImage, createInitialImages} from '#/state/gallery' import {createPostgateRecord} from '#/state/queries/postgate/util' import {Gif} from '#/state/queries/tenor' -import {threadgateViewToAllowUISetting} from '#/state/queries/threadgate' +import {threadgateRecordToAllowUISetting} from '#/state/queries/threadgate' import {ThreadgateAllowUISetting} from '#/state/queries/threadgate' import {ComposerOpts} from '#/state/shell/composer' import { @@ -477,11 +482,15 @@ export function createComposerState({ initMention, initImageUris, initQuoteUri, + initInteractionSettings, }: { initText: string | undefined initMention: string | undefined initImageUris: ComposerOpts['imageUris'] initQuoteUri: string | undefined + initInteractionSettings: + | BskyPreferences['postInteractionSettings'] + | undefined }): ComposerState { let media: ImagesMedia | undefined if (initImageUris?.length) { @@ -591,8 +600,16 @@ export function createComposerState({ }, }, ], - postgate: createPostgateRecord({post: ''}), - threadgate: threadgateViewToAllowUISetting(undefined), + postgate: createPostgateRecord({ + post: '', + embeddingRules: initInteractionSettings?.postgateEmbeddingRules || [], + }), + threadgate: threadgateRecordToAllowUISetting({ + $type: 'app.bsky.feed.threadgate', + post: '', + createdAt: new Date().toString(), + allow: initInteractionSettings?.threadgateAllowRules, + }), }, } } |