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