From 1609778ff67bbb32250edbade033f71c2d4bfadf Mon Sep 17 00:00:00 2001 From: DS Boyce <260543580+ds-boyce@users.noreply.github.com> Date: Fri, 8 May 2026 15:49:44 -0700 Subject: [PATCH] Update chat settings (#10449) --- src/components/forms/Toggle/index.tsx | 25 ++ src/screens/Messages/Settings.tsx | 327 +++++++++++++----- .../Messages/components/InviteLinkDialog.tsx | 21 +- .../queries/messages/actor-declaration.ts | 22 +- 4 files changed, 276 insertions(+), 119 deletions(-) diff --git a/src/components/forms/Toggle/index.tsx b/src/components/forms/Toggle/index.tsx index 76ab62df66..e22ede36ec 100644 --- a/src/components/forms/Toggle/index.tsx +++ b/src/components/forms/Toggle/index.tsx @@ -572,4 +572,29 @@ export function BaseRadio({ ) } +export function RadioWithLabel({ + label, + selected, +}: { + label: string + selected: boolean +}) { + const t = useTheme() + + return ( + + + + {label} + + + ) +} + export const Platform = IS_NATIVE ? Switch : Checkbox diff --git a/src/screens/Messages/Settings.tsx b/src/screens/Messages/Settings.tsx index a43da45b6d..7c96c2f995 100644 --- a/src/screens/Messages/Settings.tsx +++ b/src/screens/Messages/Settings.tsx @@ -1,23 +1,27 @@ -import {useCallback} from 'react' +import {useCallback, useState} from 'react' import {View} from 'react-native' -import {msg} from '@lingui/core/macro' -import {useLingui} from '@lingui/react' -import {Trans} from '@lingui/react/macro' +import {Trans, useLingui} from '@lingui/react/macro' import {type NativeStackScreenProps} from '@react-navigation/native-stack' +import {DM_SERVICE_HEADERS} from '#/lib/constants' +import {saveBytesToDisk} from '#/lib/media/manip' import {type CommonNavigatorParams} from '#/lib/routes/types' +import {logger} from '#/logger' import {useUpdateActorDeclaration} from '#/state/queries/messages/actor-declaration' import {useProfileQuery} from '#/state/queries/profile' -import {useSession} from '#/state/session' -import {atoms as a} from '#/alf' -import {Admonition} from '#/components/Admonition' +import {useAgent, useSession} from '#/state/session' +import {atoms as a, useTheme} from '#/alf' import {AgeRestrictedScreen} from '#/components/ageAssurance/AgeRestrictedScreen' import {useAgeAssuranceCopy} from '#/components/ageAssurance/useAgeAssuranceCopy' +import {Button, ButtonIcon, ButtonText} from '#/components/Button' import {Divider} from '#/components/Divider' import * as Toggle from '#/components/forms/Toggle' +import {Download_Stroke2_Corner0_Rounded as DownloadIcon} from '#/components/icons/Download' import * as Layout from '#/components/Layout' +import {Loader} from '#/components/Loader' import * as Toast from '#/components/Toast' import {Text} from '#/components/Typography' +import {useAnalytics} from '#/analytics' import {IS_NATIVE} from '#/env' import {useBackgroundNotificationPreferences} from '../../../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider' @@ -26,12 +30,12 @@ type AllowIncoming = 'all' | 'none' | 'following' type Props = NativeStackScreenProps export function MessagesSettingsScreen(props: Props) { - const {_} = useLingui() + const {t: l} = useLingui() const aaCopy = useAgeAssuranceCopy() return ( @@ -39,16 +43,84 @@ export function MessagesSettingsScreen(props: Props) { } export function MessagesSettingsScreenInner({}: Props) { - const {_} = useLingui() + const t = useTheme() + const {t: l} = useLingui() + const ax = useAnalytics() const {currentAccount} = useSession() + const agent = useAgent() const {data: profile} = useProfileQuery({ did: currentAccount!.did, }) const {preferences, setPref} = useBackgroundNotificationPreferences() + const isGroupChatEnabled = ax.features.enabled(ax.features.GroupChatsEnable) + + const [loading, setLoading] = useState(false) + + const allowMessagesFromOptions: {name: AllowIncoming; label: string}[] = [ + { + name: 'all', + label: l({context: 'allow messages from', message: `Everyone`}), + }, + { + name: 'following', + label: l({context: 'allow messages from', message: `Users I follow`}), + }, + { + name: 'none', + label: l({context: 'allow messages from', message: `No one`}), + }, + ] + + const allowGroupInvitesFromOptions: {name: AllowIncoming; label: string}[] = [ + { + name: 'all', + label: l({context: 'allow group chat invites from', message: `Everyone`}), + }, + { + name: 'following', + label: l({ + context: 'allow group chat invites from', + message: `Users I follow`, + }), + }, + { + name: 'none', + label: l({context: 'allow group chat invites from', message: `No one`}), + }, + ] + + const onDownloadChatData = async () => { + if (!agent.session) return + try { + setLoading(true) + const res = await agent.sessionManager.fetchHandler( + '/xrpc/chat.bsky.actor.exportAccountData', + {headers: DM_SERVICE_HEADERS}, + ) + if (!res.ok) { + throw new Error(`HTTP ${res.status}`) + } + const data = new Uint8Array(await res.arrayBuffer()) + const saveRes = await saveBytesToDisk( + 'chat.jsonl', + data, + res.headers.get('content-type') || 'application/jsonl', + ) + if (saveRes) { + Toast.show(l`File saved successfully!`) + } + } catch (e) { + logger.error('Error occurred while downloading chat data', {message: e}) + Toast.show(l`Error occurred while saving file`, {type: 'error'}) + } finally { + setLoading(false) + } + } + const {mutate: updateDeclaration} = useUpdateActorDeclaration({ onError: () => { - Toast.show(_(msg`Failed to update settings`), { + Toast.show(l`Failed to update settings`, { type: 'error', }) }, @@ -58,16 +130,23 @@ export function MessagesSettingsScreenInner({}: Props) { (keys: string[]) => { const key = keys[0] if (!key) return - updateDeclaration(key as AllowIncoming) + updateDeclaration({allowIncoming: key as AllowIncoming}) + }, + [updateDeclaration], + ) + + const onSelectGroupInvitesFrom = useCallback( + (keys: string[]) => { + const key = keys[0] + if (!key) return + updateDeclaration({allowGroupInvites: key as AllowIncoming}) }, [updateDeclaration], ) const onSelectSoundSetting = useCallback( - (keys: string[]) => { - const key = keys[0] - if (!key) return - setPref('playSoundChat', key === 'enabled') + (selected: boolean) => { + setPref('playSoundChat', selected) }, [setPref], ) @@ -84,90 +163,146 @@ export function MessagesSettingsScreenInner({}: Props) { - - - Allow new messages from - - - - - - Everyone - - - - - - Users I follow - - - - - - No one - - - - - - - - You can continue ongoing conversations regardless of which setting - you choose. - - + + + + Allow direct messages from + + + + You can continue ongoing conversations regardless of which + setting you choose. + + + + + {allowMessagesFromOptions.map(option => ( + + {({selected}) => ( + + )} + + ))} + + + + + {isGroupChatEnabled ? ( + <> + + + Allow group chat invites from + + + + You can continue ongoing conversations regardless of which + setting you choose. + + + + + {allowGroupInvitesFromOptions.map(option => ( + + {({selected}) => ( + + )} + + ))} + + + + + + ) : null} {IS_NATIVE && ( <> - - - Notification Sounds - - - - - - Enabled - - - - - - Disabled - - - - - + + + + Notification sounds + + + + + )} + + + Download chat data + + + + This file only includes chat messages that you have sent and + does not include chat messages that you have received. + + + + + diff --git a/src/screens/Messages/components/InviteLinkDialog.tsx b/src/screens/Messages/components/InviteLinkDialog.tsx index 103ee9c4ad..498b14cabf 100644 --- a/src/screens/Messages/components/InviteLinkDialog.tsx +++ b/src/screens/Messages/components/InviteLinkDialog.tsx @@ -222,7 +222,7 @@ export function InviteLinkDialog({ name={option.name} style={[a.flex_1]}> {({selected}) => ( - @@ -454,22 +454,3 @@ export function InviteLinkDialog({ ) } - -function TargetOption({label, selected}: {label: string; selected: boolean}) { - const t = useTheme() - - return ( - - - - {label} - - - ) -} diff --git a/src/state/queries/messages/actor-declaration.ts b/src/state/queries/messages/actor-declaration.ts index 5c774cd56c..53b493ab6a 100644 --- a/src/state/queries/messages/actor-declaration.ts +++ b/src/state/queries/messages/actor-declaration.ts @@ -21,8 +21,21 @@ export function useUpdateActorDeclaration({ const agent = useAgent() return useMutation({ - mutationFn: async (allowIncoming: 'all' | 'none' | 'following') => { + mutationFn: async (update: { + allowIncoming?: 'all' | 'none' | 'following' + allowGroupInvites?: 'all' | 'none' | 'following' + }) => { if (!currentAccount) throw new Error('Not signed in') + const current = + queryClient.getQueryData( + PROFILE_RKEY(currentAccount.did), + ) + const allowIncoming = + update.allowIncoming ?? + current?.associated?.chat?.allowIncoming ?? + 'following' + const allowGroupInvites = + update.allowGroupInvites ?? current?.associated?.chat?.allowGroupInvites const result = await agent.com.atproto.repo.putRecord({ repo: currentAccount.did, collection: 'chat.bsky.actor.declaration', @@ -30,11 +43,12 @@ export function useUpdateActorDeclaration({ record: { $type: 'chat.bsky.actor.declaration', allowIncoming, + ...(allowGroupInvites && {allowGroupInvites}), }, }) return result }, - onMutate: allowIncoming => { + onMutate: update => { if (!currentAccount) return queryClient.setQueryData( PROFILE_RKEY(currentAccount?.did), @@ -45,7 +59,9 @@ export function useUpdateActorDeclaration({ associated: { ...old.associated, chat: { - allowIncoming, + allowIncoming: 'following', + ...old.associated?.chat, + ...update, }, }, } satisfies AppBskyActorDefs.ProfileViewDetailed