Add new chat notification preferences (#10972)

This commit is contained in:
DS Boyce
2026-06-25 12:01:33 -07:00
committed by GitHub
parent 1ff27bfd3b
commit 3b2e609761
11 changed files with 492 additions and 210 deletions
+103 -8
View File
@@ -1,13 +1,19 @@
import {useCallback} from 'react'
import {View} from 'react-native'
import {Pressable, View} from 'react-native'
import {Trans, useLingui} from '@lingui/react/macro'
import {type NativeStackScreenProps} from '@react-navigation/native-stack'
import {type CommonNavigatorParams} from '#/lib/routes/types'
import {useUpdateActorDeclaration} from '#/state/queries/messages/actor-declaration'
import {
type NotificationSettingsPreference,
useChatNotificationSettingsQuery,
} from '#/state/queries/notifications/settings'
import {useProfileQuery} from '#/state/queries/profile'
import {useSession} from '#/state/session'
import {ExportCarDialog} from '#/screens/Settings/components/ExportCarDialog'
import {ChatNotificationDialogs} from '#/screens/Settings/NotificationSettings/components/ChatNotificationDialogs'
import {SettingPreview} from '#/screens/Settings/NotificationSettings/components/SettingPreview'
import {atoms as a, useTheme} from '#/alf'
import {AgeRestrictedScreen} from '#/components/ageAssurance/AgeRestrictedScreen'
import {useAgeAssuranceCopy} from '#/components/ageAssurance/useAgeAssuranceCopy'
@@ -18,7 +24,10 @@ import * as Toggle from '#/components/forms/Toggle'
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 {Envelope_Stroke2_Corner2_Rounded as EnvelopeIcon} from '#/components/icons/Envelope'
import {Message_Stroke2_Corner0_Rounded as MessageIcon} from '#/components/icons/Message'
import * as Layout from '#/components/Layout'
import * as Skele from '#/components/Skeleton'
import * as Toast from '#/components/Toast'
import {Text} from '#/components/Typography'
import {useAgeAssurance} from '#/ageAssurance'
@@ -52,8 +61,12 @@ export function MessagesSettingsScreenInner({}: Props) {
const {data: profile} = useProfileQuery({
did: currentAccount!.did,
})
const {data: chatNotificationSettings, isError: chatSettingsError} =
useChatNotificationSettingsQuery()
const {preferences, setPref} = useBackgroundNotificationPreferences()
const chatDialogControl = Dialog.useDialogControl()
const chatRequestDialogControl = Dialog.useDialogControl()
const exportCarControl = Dialog.useDialogControl()
const isGroupChatEnabled = !ax.features.enabled(ax.features.GroupChatsDisable)
@@ -237,6 +250,54 @@ export function MessagesSettingsScreenInner({}: Props) {
<Divider style={{marginVertical: 10}} />
</>
) : null}
<View style={[a.px_xl, a.gap_lg]}>
<Text style={[a.pb_xs, a.text_md, a.font_semi_bold, t.atoms.text]}>
<Trans>Notifications</Trans>
</Text>
<Pressable
accessibilityRole="button"
accessibilityLabel={l`Settings for notifications for new messages`}
accessibilityHint={undefined}
style={[a.flex_row, a.align_start, a.justify_between, a.gap_sm]}
onPress={() => {
chatDialogControl.open()
}}>
<MessageIcon style={[a.mr_2xs, t.atoms.text]} size="lg" />
<View style={[a.flex_1, a.flex_grow]}>
<Text style={[a.text_md, a.font_semi_bold, t.atoms.text]}>
<Trans>New messages</Trans>
</Text>
<NotificationPreferenceSubtitle
preference={chatNotificationSettings?.chat}
isLoading={!chatNotificationSettings}
isError={chatSettingsError}
/>
</View>
<ChevronRightIcon style={[a.ml_2xs, t.atoms.text]} size="lg" />
</Pressable>
<Pressable
accessibilityRole="button"
accessibilityLabel={l`Settings for notifications for new message requests`}
accessibilityHint={undefined}
style={[a.flex_row, a.align_start, a.justify_between, a.gap_sm]}
onPress={() => {
chatRequestDialogControl.open()
}}>
<EnvelopeIcon style={[a.mr_2xs, t.atoms.text]} size="lg" />
<View style={[a.flex_1, a.flex_grow]}>
<Text style={[a.text_md, a.font_semi_bold, t.atoms.text]}>
<Trans>New message requests</Trans>
</Text>
<NotificationPreferenceSubtitle
preference={chatNotificationSettings?.chatRequest}
isLoading={!chatNotificationSettings}
isError={chatSettingsError}
/>
</View>
<ChevronRightIcon style={[a.ml_2xs, t.atoms.text]} size="lg" />
</Pressable>
</View>
<Divider style={{marginVertical: 10}} />
{IS_NATIVE && (
<>
<View style={[a.px_xl]}>
@@ -263,12 +324,12 @@ export function MessagesSettingsScreenInner({}: Props) {
</>
)}
<View style={[a.px_xl]}>
<Toggle.Item
label={l`Export my chat data`}
name="playSoundChat"
value={preferences.playSoundChat}
style={[a.flex_row, a.align_center, a.justify_between]}
onChange={() => {
<Pressable
accessibilityRole="button"
accessibilityLabel={l`Export my chat data`}
accessibilityHint={undefined}
style={[a.flex_row, a.align_center, a.justify_between, a.gap_sm]}
onPress={() => {
exportCarControl.open()
}}>
<CarIcon style={[a.mr_2xs, t.atoms.text]} size="lg" />
@@ -277,12 +338,46 @@ export function MessagesSettingsScreenInner({}: Props) {
<Trans>Export my chat data</Trans>
</Text>
<ChevronRightIcon style={[a.ml_2xs, t.atoms.text]} size="lg" />
</Toggle.Item>
</Pressable>
</View>
<Divider style={{marginVertical: 10}} />
</View>
</Layout.Content>
<ChatNotificationDialogs
chatControl={chatDialogControl}
chatRequestControl={chatRequestDialogControl}
/>
<ExportCarDialog control={exportCarControl} />
</Layout.Screen>
)
}
function NotificationPreferenceSubtitle({
preference,
isLoading,
isError,
}: {
preference?: NotificationSettingsPreference
isLoading: boolean
isError: boolean
}) {
const t = useTheme()
if (isError) {
return (
<Text style={[a.text_sm, t.atoms.text_contrast_medium, a.leading_snug]}>
<Trans>Failed to load notification settings.</Trans>
</Text>
)
}
if (isLoading) {
return <Skele.Text style={[a.text_sm, {width: 120}]} />
}
return (
<Text style={[a.text_sm, t.atoms.text_contrast_medium, a.leading_snug]}>
<SettingPreview preference={preference} />
</Text>
)
}
@@ -0,0 +1,41 @@
import {Trans} from '@lingui/react/macro'
import type * as Dialog from '#/components/Dialog'
import {NotificationSettingsDialog} from '#/components/dialogs/NotificationSettingsDialog'
import {Envelope_Stroke2_Corner2_Rounded as EnvelopeIcon} from '#/components/icons/Envelope'
import {Message_Stroke2_Corner0_Rounded as MessageIcon} from '#/components/icons/Message'
export function ChatNotificationDialogs({
chatControl,
chatRequestControl,
}: {
chatControl: Dialog.DialogControlProps
chatRequestControl: Dialog.DialogControlProps
}) {
return (
<>
<NotificationSettingsDialog
control={chatControl}
name="chat"
icon={MessageIcon}
titleText={<Trans>New messages</Trans>}
subtitleText={
<Trans>Get notifications when people send you messages.</Trans>
}
allowDisableInApp={false}
/>
<NotificationSettingsDialog
control={chatRequestControl}
name="chatRequest"
icon={EnvelopeIcon}
titleText={<Trans>New message requests</Trans>}
subtitleText={
<Trans>
Get notifications when people send you message requests.
</Trans>
}
allowDisableInApp={false}
/>
</>
)
}
@@ -1,9 +1,12 @@
import {useMemo} from 'react'
import {View} from 'react-native'
import {type AppBskyNotificationDefs} from '@atproto/api'
import {Trans, useLingui} from '@lingui/react/macro'
import {useNotificationSettingsUpdateMutation} from '#/state/queries/notifications/settings'
import {
type NotificationSettingsPreference,
type NotificationSettingsPreferenceName,
useNotificationSettingsUpdateMutation,
} from '#/state/queries/notifications/settings'
import {atoms as a, platform, useTheme} from '#/alf'
import * as Toggle from '#/components/forms/Toggle'
import {Loader} from '#/components/Loader'
@@ -17,16 +20,13 @@ export function PreferenceControls({
preference,
allowDisableInApp = true,
}: {
name: Exclude<keyof AppBskyNotificationDefs.Preferences, '$type'>
name: NotificationSettingsPreferenceName
/**
* Keep other prefs in sync with `name`. For use in the "everything else" category
* which groups starterpack joins + verified + unverified notifications into a single toggle.
*/
syncOthers?: Exclude<keyof AppBskyNotificationDefs.Preferences, '$type'>[]
preference?:
| AppBskyNotificationDefs.Preference
| AppBskyNotificationDefs.FilterablePreference
| AppBskyNotificationDefs.ChatPreference
syncOthers?: NotificationSettingsPreferenceName[]
preference?: NotificationSettingsPreference
allowDisableInApp?: boolean
}) {
if (!preference)
@@ -52,12 +52,9 @@ export function Inner({
preference,
allowDisableInApp,
}: {
name: Exclude<keyof AppBskyNotificationDefs.Preferences, '$type'>
syncOthers?: Exclude<keyof AppBskyNotificationDefs.Preferences, '$type'>[]
preference:
| AppBskyNotificationDefs.Preference
| AppBskyNotificationDefs.FilterablePreference
| AppBskyNotificationDefs.ChatPreference
name: NotificationSettingsPreferenceName
syncOthers?: NotificationSettingsPreferenceName[]
preference: NotificationSettingsPreference
allowDisableInApp: boolean
}) {
const t = useTheme()
@@ -95,7 +92,7 @@ export function Inner({
}
const onChangeFilter = ([change]: string[]) => {
if (change !== 'all' && change !== 'follows' && change !== 'accepted')
if (change !== 'all' && change !== 'follows')
throw new Error('Invalid filter')
const newPreference = {
@@ -135,7 +132,7 @@ export function Inner({
</Toggle.LabelText>
<Toggle.Platform />
</Toggle.Item>
{allowDisableInApp && (
{allowDisableInApp && 'list' in preference && (
<Toggle.Item
label={l`Receive in-app notifications`}
name="list"
@@ -176,31 +173,17 @@ export function Inner({
/>
)}
</Toggle.Item>
{name === 'chat' ? (
<Toggle.Item
highlightRow
label={l`Accepted conversations`}
name="accepted">
{({selected}) => (
<Toggle.RadioWithLabel
label={l`Accepted conversations`}
selected={selected}
/>
)}
</Toggle.Item>
) : (
<Toggle.Item
highlightRow
label={l`People I follow`}
name="follows">
{({selected}) => (
<Toggle.RadioWithLabel
label={l`People I follow`}
selected={selected}
/>
)}
</Toggle.Item>
)}
<Toggle.Item
highlightRow
label={l`People I follow`}
name="follows">
{({selected}) => (
<Toggle.RadioWithLabel
label={l`People I follow`}
selected={selected}
/>
)}
</Toggle.Item>
</View>
</Toggle.Group>
</>
@@ -0,0 +1,35 @@
import {useLingui} from '@lingui/react/macro'
import {type NotificationSettingsPreference} from '#/state/queries/notifications/settings'
export function SettingPreview({
preference,
}: {
preference?: NotificationSettingsPreference
}) {
const {t: l} = useLingui()
if (!preference) {
return null
}
if ('include' in preference) {
const list = 'list' in preference && preference.list
if (preference.include === 'all') {
if (list && preference.push) return l`In-app, push, everyone`
if (list) return l`In-app, everyone`
if (preference.push) return l`Push, everyone`
} else if (preference.include === 'follows') {
if (list && preference.push) return l`In-app, push, people you follow`
if (list) return l`In-app, people you follow`
if (preference.push) return l`Push, people you follow`
}
} else {
if (preference.list && preference.push) return l`In-app, push`
if (preference.list) return l`In-app`
if (preference.push) return l`Push`
}
return l`Off`
}
@@ -1,7 +1,6 @@
import {useEffect} from 'react'
import {Linking, View} from 'react-native'
import * as Notification from 'expo-notifications'
import {type AppBskyNotificationDefs} from '@atproto/api'
import {Trans, useLingui} from '@lingui/react/macro'
import {useQuery, useQueryClient} from '@tanstack/react-query'
@@ -10,7 +9,10 @@ import {
type AllNavigatorParams,
type NativeStackScreenProps,
} from '#/lib/routes/types'
import {useNotificationSettingsQuery} from '#/state/queries/notifications/settings'
import {
useChatNotificationSettingsQuery,
useNotificationSettingsQuery,
} from '#/state/queries/notifications/settings'
import {atoms as a} from '#/alf'
import {Admonition} from '#/components/Admonition'
import * as Dialog from '#/components/Dialog'
@@ -18,11 +20,13 @@ import {NotificationSettingsDialog} from '#/components/dialogs/NotificationSetti
import {At_Stroke2_Corner2_Rounded as AtIcon} from '#/components/icons/At'
import {BellRinging_Stroke2_Corner0_Rounded as BellRingingIcon} from '#/components/icons/BellRinging'
import {Bubble_Stroke2_Corner2_Rounded as BubbleIcon} from '#/components/icons/Bubble'
import {Envelope_Stroke2_Corner2_Rounded as EnvelopeIcon} from '#/components/icons/Envelope'
import {Haptic_Stroke2_Corner2_Rounded as HapticIcon} from '#/components/icons/Haptic'
import {
Heart2_Stroke2_Corner0_Rounded as HeartIcon,
LikeRepost_Stroke2_Corner2_Rounded as LikeRepostIcon,
} from '#/components/icons/Heart2'
import {Message_Stroke2_Corner0_Rounded as MessageIcon} from '#/components/icons/Message'
import {PersonPlus_Stroke2_Corner2_Rounded as PersonPlusIcon} from '#/components/icons/Person'
import {CloseQuote_Stroke2_Corner0_Rounded as CloseQuoteIcon} from '#/components/icons/Quote'
import {
@@ -33,7 +37,9 @@ import {Shapes_Stroke2_Corner0_Rounded as ShapesIcon} from '#/components/icons/S
import * as Layout from '#/components/Layout'
import {IS_ANDROID, IS_IOS, IS_WEB} from '#/env'
import * as SettingsList from '../components/SettingsList'
import {ChatNotificationDialogs} from './components/ChatNotificationDialogs'
import {ItemTextWithSubtitle} from './components/ItemTextWithSubtitle'
import {SettingPreview} from './components/SettingPreview'
const RQKEY = ['notification-permissions']
@@ -42,6 +48,8 @@ export function NotificationSettingsScreen({}: Props) {
const {t: l} = useLingui()
const queryClient = useQueryClient()
const {data: settings, isError} = useNotificationSettingsQuery()
const {data: chatSettings, isError: chatError} =
useChatNotificationSettingsQuery()
const likeDialogControl = Dialog.useDialogControl()
const followDialogControl = Dialog.useDialogControl()
@@ -52,6 +60,8 @@ export function NotificationSettingsScreen({}: Props) {
const activityDialogControl = Dialog.useDialogControl()
const likeRepostDialogControl = Dialog.useDialogControl()
const repostRepostDialogControl = Dialog.useDialogControl()
const chatDialogControl = Dialog.useDialogControl()
const chatRequestDialogControl = Dialog.useDialogControl()
const miscDialogControl = Dialog.useDialogControl()
const {data: permissions, refetch} = useQuery({
@@ -234,6 +244,40 @@ export function NotificationSettingsScreen({}: Props) {
showSkeleton={!settings}
/>
</SettingsList.PressableItem>
<SettingsList.PressableItem
label={l`Settings for notifications for new messages`}
onPress={chatDialogControl.open}
contentContainerStyle={[a.align_start]}>
<SettingsList.ItemIcon icon={MessageIcon} />
<ItemTextWithSubtitle
titleText={<Trans>New messages</Trans>}
subtitleText={
chatError ? (
<Trans>Failed to load notification settings.</Trans>
) : (
<SettingPreview preference={chatSettings?.chat} />
)
}
showSkeleton={!chatSettings && !chatError}
/>
</SettingsList.PressableItem>
<SettingsList.PressableItem
label={l`Settings for notifications for new message requests`}
onPress={chatRequestDialogControl.open}
contentContainerStyle={[a.align_start]}>
<SettingsList.ItemIcon icon={EnvelopeIcon} />
<ItemTextWithSubtitle
titleText={<Trans>New message requests</Trans>}
subtitleText={
chatError ? (
<Trans>Failed to load notification settings.</Trans>
) : (
<SettingPreview preference={chatSettings?.chatRequest} />
)
}
showSkeleton={!chatSettings && !chatError}
/>
</SettingsList.PressableItem>
<SettingsList.PressableItem
label={l`Settings for notifications for everything else`}
onPress={miscDialogControl.open}
@@ -333,6 +377,10 @@ export function NotificationSettingsScreen({}: Props) {
<Trans>Get notifications when people repost your reposts.</Trans>
}
/>
<ChatNotificationDialogs
chatControl={chatDialogControl}
chatRequestControl={chatRequestDialogControl}
/>
<NotificationSettingsDialog
control={miscDialogControl}
name="starterpackJoined"
@@ -350,46 +398,3 @@ export function NotificationSettingsScreen({}: Props) {
</Layout.Screen>
)
}
function SettingPreview({
preference,
}: {
preference?:
| AppBskyNotificationDefs.Preference
| AppBskyNotificationDefs.FilterablePreference
}) {
const {t: l} = useLingui()
if (!preference) {
return null
} else {
if ('include' in preference) {
if (preference.include === 'all') {
if (preference.list && preference.push) {
return l`In-app, push, everyone`
} else if (preference.list) {
return l`In-app, everyone`
} else if (preference.push) {
return l`Push, everyone`
}
} else if (preference.include === 'follows') {
if (preference.list && preference.push) {
return l`In-app, push, people you follow`
} else if (preference.list) {
return l`In-app, people you follow`
} else if (preference.push) {
return l`Push, people you follow`
}
}
} else {
if (preference.list && preference.push) {
return l`In-app, push`
} else if (preference.list) {
return l`In-app`
} else if (preference.push) {
return l`Push`
}
}
}
return l`Off`
}