Update chat settings (#10449)

This commit is contained in:
DS Boyce
2026-05-08 15:49:44 -07:00
committed by GitHub
parent 75d8fb3dbd
commit 1609778ff6
4 changed files with 276 additions and 119 deletions
+25
View File
@@ -572,4 +572,29 @@ export function BaseRadio({
)
}
export function RadioWithLabel({
label,
selected,
}: {
label: string
selected: boolean
}) {
const t = useTheme()
return (
<View style={[a.flex_1, a.flex_row, a.align_center, a.gap_sm]}>
<Radio />
<LabelText
style={[
a.font_medium,
a.flex_1,
a.leading_tight,
selected ? t.atoms.text : t.atoms.text_contrast_high,
]}>
{label}
</LabelText>
</View>
)
}
export const Platform = IS_NATIVE ? Switch : Checkbox
+231 -96
View File
@@ -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<CommonNavigatorParams, 'MessagesSettings'>
export function MessagesSettingsScreen(props: Props) {
const {_} = useLingui()
const {t: l} = useLingui()
const aaCopy = useAgeAssuranceCopy()
return (
<AgeRestrictedScreen
screenTitle={_(msg`Chat settings`)}
screenTitle={l`Chat settings`}
infoText={aaCopy.chatsInfoText}>
<MessagesSettingsScreenInner {...props} />
</AgeRestrictedScreen>
@@ -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) {
<Layout.Header.Slot />
</Layout.Header.Outer>
<Layout.Content>
<View style={[a.p_lg, a.gap_md]}>
<Text style={[a.text_lg, a.font_semi_bold]}>
<Trans>Allow new messages from</Trans>
</Text>
<Toggle.Group
label={_(msg`Allow new messages from`)}
type="radio"
values={[
(profile?.associated?.chat?.allowIncoming as AllowIncoming) ??
'following',
]}
onChange={onSelectMessagesFrom}>
<View>
<Toggle.Item
name="all"
label={_(msg`Everyone`)}
style={[a.justify_between, a.py_sm]}>
<Toggle.LabelText>
<Trans>Everyone</Trans>
</Toggle.LabelText>
<Toggle.Radio />
</Toggle.Item>
<Toggle.Item
name="following"
label={_(msg`Users I follow`)}
style={[a.justify_between, a.py_sm]}>
<Toggle.LabelText>
<Trans>Users I follow</Trans>
</Toggle.LabelText>
<Toggle.Radio />
</Toggle.Item>
<Toggle.Item
name="none"
label={_(
msg({context: 'allow messages from', message: `No one`}),
)}
style={[a.justify_between, a.py_sm]}>
<Toggle.LabelText>
<Trans context="allow messages from">No one</Trans>
</Toggle.LabelText>
<Toggle.Radio />
</Toggle.Item>
</View>
</Toggle.Group>
<Admonition type="tip">
<Trans>
You can continue ongoing conversations regardless of which setting
you choose.
</Trans>
</Admonition>
<View style={[a.py_xl, a.gap_md]}>
<View style={[a.px_xl]}>
<Text style={[a.pb_xs, a.text_md, a.font_semi_bold, t.atoms.text]}>
<Trans>Allow direct messages from</Trans>
</Text>
<Text
style={[
a.pb_md,
a.text_sm,
a.leading_snug,
t.atoms.text_contrast_high,
]}>
<Trans>
You can continue ongoing conversations regardless of which
setting you choose.
</Trans>
</Text>
<Toggle.Group
label={l`Allow direct messages from`}
type="radio"
values={[
(profile?.associated?.chat?.allowIncoming as AllowIncoming) ??
'following',
]}
onChange={onSelectMessagesFrom}>
<View>
{allowMessagesFromOptions.map(option => (
<Toggle.Item
key={option.name}
highlightRow
name={option.name}
label={option.label}>
{({selected}) => (
<Toggle.RadioWithLabel
label={option.label}
selected={selected}
/>
)}
</Toggle.Item>
))}
</View>
</Toggle.Group>
</View>
<Divider style={{marginVertical: 10}} />
{isGroupChatEnabled ? (
<>
<View style={[a.px_xl]}>
<Text
style={[a.pb_xs, a.text_md, a.font_semi_bold, t.atoms.text]}>
<Trans>Allow group chat invites from</Trans>
</Text>
<Text
style={[
a.pb_md,
a.text_sm,
a.leading_snug,
t.atoms.text_contrast_high,
]}>
<Trans>
You can continue ongoing conversations regardless of which
setting you choose.
</Trans>
</Text>
<Toggle.Group
label={l`Allow group chat invites from`}
type="radio"
values={[
(profile?.associated?.chat
?.allowGroupInvites as AllowIncoming) ?? 'following',
]}
onChange={onSelectGroupInvitesFrom}>
<View>
{allowGroupInvitesFromOptions.map(option => (
<Toggle.Item
key={option.name}
highlightRow
name={option.name}
label={option.label}>
{({selected}) => (
<Toggle.RadioWithLabel
label={option.label}
selected={selected}
/>
)}
</Toggle.Item>
))}
</View>
</Toggle.Group>
</View>
<Divider style={{marginVertical: 10}} />
</>
) : null}
{IS_NATIVE && (
<>
<Divider style={a.my_md} />
<Text style={[a.text_lg, a.font_semi_bold]}>
<Trans>Notification Sounds</Trans>
</Text>
<Toggle.Group
label={_(msg`Notification sounds`)}
type="radio"
values={[preferences.playSoundChat ? 'enabled' : 'disabled']}
onChange={onSelectSoundSetting}>
<View>
<Toggle.Item
name="enabled"
label={_(msg`Enabled`)}
style={[a.justify_between, a.py_sm]}>
<Toggle.LabelText>
<Trans>Enabled</Trans>
</Toggle.LabelText>
<Toggle.Radio />
</Toggle.Item>
<Toggle.Item
name="disabled"
label={_(msg`Disabled`)}
style={[a.justify_between, a.py_sm]}>
<Toggle.LabelText>
<Trans>Disabled</Trans>
</Toggle.LabelText>
<Toggle.Radio />
</Toggle.Item>
</View>
</Toggle.Group>
<View style={[a.px_xl]}>
<Toggle.Item
label={l`Notification sounds`}
name="playSoundChat"
value={preferences.playSoundChat}
style={[a.flex_row, a.align_center, a.justify_between]}
onChange={onSelectSoundSetting}>
<Text style={[a.text_md, a.font_semi_bold, t.atoms.text]}>
<Trans>Notification sounds</Trans>
</Text>
<Toggle.Switch />
</Toggle.Item>
</View>
<Divider style={{marginVertical: 10}} />
</>
)}
<View style={[a.px_xl]}>
<Text style={[a.pb_xs, a.text_md, a.font_semi_bold, t.atoms.text]}>
<Trans>Download chat data</Trans>
</Text>
<Text
style={[
a.pb_md,
a.text_sm,
a.leading_snug,
t.atoms.text_contrast_high,
]}>
<Trans>
This file only includes chat messages that you have sent and
does not include chat messages that you have received.
</Trans>
</Text>
<Button
disabled={loading}
color="primary"
size="small"
label={l`Download .jsonl file`}
style={[a.self_start]}
onPress={() => void onDownloadChatData()}>
<ButtonIcon icon={loading ? Loader : DownloadIcon} />
<ButtonText>
<Trans>Download .jsonl file</Trans>
</ButtonText>
</Button>
</View>
<Divider style={{marginVertical: 10}} />
</View>
</Layout.Content>
</Layout.Screen>
@@ -222,7 +222,7 @@ export function InviteLinkDialog({
name={option.name}
style={[a.flex_1]}>
{({selected}) => (
<TargetOption
<Toggle.RadioWithLabel
label={isOwner ? option.owner : option.member}
selected={selected}
/>
@@ -454,22 +454,3 @@ export function InviteLinkDialog({
</Dialog.Outer>
)
}
function TargetOption({label, selected}: {label: string; selected: boolean}) {
const t = useTheme()
return (
<View style={[a.flex_1, a.flex_row, a.align_center, a.gap_sm]}>
<Toggle.Radio />
<Toggle.LabelText
style={[
a.font_normal,
a.flex_1,
a.leading_tight,
selected ? t.atoms.text : t.atoms.text_contrast_high,
]}>
{label}
</Toggle.LabelText>
</View>
)
}
@@ -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<AppBskyActorDefs.ProfileViewDetailed>(
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