Add export data dialog to chat settings (#10478)

This commit is contained in:
DS Boyce
2026-05-13 12:30:00 -07:00
committed by GitHub
parent 34b3c52729
commit 9e55bb71ef
2 changed files with 77 additions and 99 deletions
+32 -67
View File
@@ -1,24 +1,23 @@
import {useCallback, useState} from 'react'
import {useCallback} from 'react'
import {View} from 'react-native'
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 {useAgent, useSession} from '#/state/session'
import {useSession} from '#/state/session'
import {ExportCarDialog} from '#/screens/Settings/components/ExportCarDialog'
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 * as Dialog from '#/components/Dialog'
import {Divider} from '#/components/Divider'
import * as Toggle from '#/components/forms/Toggle'
import {Download_Stroke2_Corner0_Rounded as DownloadIcon} from '#/components/icons/Download'
import {Bell_Stroke2_Corner0_Rounded as BellIcon} from '#/components/icons/Bell'
import {Car_Stroke2_Corner2_Rounded as CarIcon} from '#/components/icons/Car'
import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRightIcon} from '#/components/icons/Chevron'
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'
@@ -47,16 +46,14 @@ export function MessagesSettingsScreenInner({}: Props) {
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 exportCarControl = Dialog.useDialogControl()
const isGroupChatEnabled = ax.features.enabled(ax.features.GroupChatsEnable)
const [loading, setLoading] = useState(false)
const allowMessagesFromOptions: {name: AllowIncoming; label: string}[] = [
{
name: 'all',
@@ -90,34 +87,6 @@ export function MessagesSettingsScreenInner({}: Props) {
},
]
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(l`Failed to update settings`, {
@@ -264,7 +233,14 @@ export function MessagesSettingsScreenInner({}: Props) {
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]}>
<BellIcon style={[a.mr_2xs, t.atoms.text]} size="md" />
<Text
style={[
a.flex_1,
a.text_md,
a.font_semi_bold,
t.atoms.text,
]}>
<Trans>Notification sounds</Trans>
</Text>
<Toggle.Switch />
@@ -274,37 +250,26 @@ export function MessagesSettingsScreenInner({}: Props) {
</>
)}
<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>
<Toggle.Item
label={l`Export chat data`}
name="playSoundChat"
value={preferences.playSoundChat}
style={[a.flex_row, a.align_center, a.justify_between]}
onChange={() => {
exportCarControl.open()
}}>
<CarIcon style={[a.mr_2xs, t.atoms.text]} size="md" />
<Text
style={[a.flex_1, a.text_md, a.font_semi_bold, t.atoms.text]}>
<Trans>Export chat data</Trans>
</Text>
<ChevronRightIcon style={[a.ml_2xs, t.atoms.text]} size="md" />
</Toggle.Item>
</View>
<Divider style={{marginVertical: 10}} />
</View>
</Layout.Content>
<ExportCarDialog control={exportCarControl} />
</Layout.Screen>
)
}
@@ -1,8 +1,6 @@
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 {DM_SERVICE_HEADERS} from '#/lib/constants'
import {saveBytesToDisk} from '#/lib/media/manip'
@@ -22,14 +20,14 @@ export function ExportCarDialog({
}: {
control: Dialog.DialogControlProps
}) {
const {_} = useLingui()
const {t: l} = useLingui()
const t = useTheme()
const agent = useAgent()
const [loading, setLoading] = useState<'repo' | 'chat' | false>(false)
const download = useCallback(async () => {
if (!agent.session) {
return // shouldnt ever happen
return // shouldn't ever happen
}
try {
setLoading('repo')
@@ -42,16 +40,15 @@ export function ExportCarDialog({
)
if (saveRes) {
Toast.show(_(msg`File saved successfully!`))
Toast.show(l`File saved successfully!`)
}
} catch (e) {
logger.error('Error occurred while downloading CAR file', {message: e})
Toast.show(_(msg`Error occurred while saving file`), {type: 'error'})
Toast.show(l`Error occurred while saving file`, {type: 'error'})
} finally {
setLoading(false)
control.close()
}
}, [_, control, agent])
}, [l, agent])
const downloadChatData = useCallback(async () => {
if (!agent.session) {
@@ -76,16 +73,15 @@ export function ExportCarDialog({
)
if (saveRes) {
Toast.show(_(msg`File saved successfully!`))
Toast.show(l`File saved successfully!`)
}
} catch (e) {
logger.error('Error occurred while downloading chat data', {message: e})
Toast.show(_(msg`Error occurred while saving file`), {type: 'error'})
Toast.show(l`Error occurred while saving file`, {type: 'error'})
} finally {
setLoading(false)
control.close()
}
}, [_, control, agent])
}, [l, agent])
return (
<Dialog.Outer control={control} nativeOptions={{preventExpansion: true}}>
@@ -94,13 +90,20 @@ export function ExportCarDialog({
accessibilityDescribedBy="dialog-description"
accessibilityLabelledBy="dialog-title"
style={web({maxWidth: 500})}>
<View style={[a.relative, a.gap_lg, a.w_full]}>
<Text nativeID="dialog-title" style={[a.text_2xl, a.font_bold]}>
<Trans>Export My Data</Trans>
<View style={[a.relative, a.w_full]}>
<Text
nativeID="dialog-title"
style={[a.mb_sm, a.text_2xl, a.font_bold]}>
<Trans>Export my profile data</Trans>
</Text>
<Text
nativeID="dialog-description"
style={[a.text_sm, a.leading_snug, t.atoms.text_contrast_high]}>
style={[
a.mb_lg,
a.text_sm,
a.leading_snug,
t.atoms.text_contrast_high,
]}>
<Trans>
Your account repository, containing all public data records, can
be downloaded as a "CAR" file. This file does not include media
@@ -112,17 +115,27 @@ export function ExportCarDialog({
<Button
color="primary"
size="large"
label={_(msg`Download CAR file`)}
label={l`Download CAR file`}
disabled={!!loading}
onPress={download}>
<ButtonIcon icon={DownloadIcon} />
onPress={() => void download()}>
<ButtonIcon icon={loading === 'repo' ? Loader : DownloadIcon} />
<ButtonText>
<Trans>Download CAR file</Trans>
<Trans context="button">Download CAR file</Trans>
</ButtonText>
{loading === 'repo' && <ButtonIcon icon={Loader} />}
</Button>
<Text style={[a.text_sm, a.leading_snug, t.atoms.text_contrast_high]}>
<Text
nativeID="dialog-title"
style={[a.mt_2xl, a.mb_sm, a.text_2xl, a.font_bold]}>
<Trans>Export my chat data</Trans>
</Text>
<Text
style={[
a.mb_lg,
a.text_sm,
a.leading_snug,
t.atoms.text_contrast_high,
]}>
<Trans>
You can also download your chat data as a "JSONL" file. This file
only includes chat messages that you have sent and does not
@@ -131,30 +144,30 @@ export function ExportCarDialog({
</Text>
<Button
color="secondary"
color="primary"
size="large"
label={_(msg`Download chat data`)}
label={l`Download chat data`}
disabled={!!loading}
onPress={downloadChatData}>
<ButtonIcon icon={DownloadIcon} />
onPress={() => void downloadChatData()}>
<ButtonIcon icon={loading === 'chat' ? Loader : DownloadIcon} />
<ButtonText>
<Trans>Download chat data</Trans>
<Trans context="button">Download chat data</Trans>
</ButtonText>
{loading === 'chat' && <ButtonIcon icon={Loader} />}
</Button>
<Text
style={[
t.atoms.text_contrast_medium,
a.flex_1,
a.mt_2xl,
a.text_sm,
a.leading_snug,
a.flex_1,
t.atoms.text_contrast_medium,
]}>
<Trans>
This feature is in beta. You can read more about repository
exports in{' '}
<InlineLinkText
label={_(msg`View blogpost for more details`)}
label={l`View blogpost for more details`}
to="https://docs.bsky.app/blog/repo-export"
style={[a.text_sm]}>
this blogpost