Add new chat notification preferences (#10972)
This commit is contained in:
@@ -946,14 +946,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/lib/hooks/useNotificationHandler.ts": {
|
||||
"@typescript-eslint/no-floating-promises": {
|
||||
"count": 16
|
||||
},
|
||||
"@typescript-eslint/require-await": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/lib/hooks/useOTAUpdates.ts": {
|
||||
"@typescript-eslint/no-explicit-any": {
|
||||
"count": 2
|
||||
@@ -2183,14 +2175,6 @@
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"src/view/com/modals/Modal.tsx": {
|
||||
"@typescript-eslint/no-misused-promises": {
|
||||
"count": 1
|
||||
},
|
||||
"@typescript-eslint/require-await": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/view/com/notifications/NotificationFeed.tsx": {
|
||||
"@typescript-eslint/no-explicit-any": {
|
||||
"count": 2
|
||||
|
||||
+1
-1
@@ -94,7 +94,7 @@
|
||||
"update-actions": "pnpm dlx actions-up --min-age 7"
|
||||
},
|
||||
"dependencies": {
|
||||
"@atproto/api": "0.20.15",
|
||||
"@atproto/api": "0.20.20",
|
||||
"@atproto/syntax": "0.6.1",
|
||||
"@bitdrift/react-native": "^0.6.8",
|
||||
"@braintree/sanitize-url": "^6.0.2",
|
||||
|
||||
Generated
+40
-31
@@ -241,8 +241,8 @@ importers:
|
||||
.:
|
||||
dependencies:
|
||||
'@atproto/api':
|
||||
specifier: 0.20.15
|
||||
version: 0.20.15
|
||||
specifier: 0.20.20
|
||||
version: 0.20.20
|
||||
'@atproto/syntax':
|
||||
specifier: 0.6.1
|
||||
version: 0.6.1
|
||||
@@ -876,32 +876,36 @@ packages:
|
||||
graphql:
|
||||
optional: true
|
||||
|
||||
'@atproto/api@0.20.15':
|
||||
resolution: {integrity: sha512-b9TuVNY9iWIaRXAeKegNCqRsK9tSpB68DE/j/ytVTxEMK+/m43B0DycJND9tnhRiNIY+i7MhwLqNHPak9YaDJg==}
|
||||
'@atproto/api@0.20.20':
|
||||
resolution: {integrity: sha512-+yqxLCvMu36xRxwc8/x0mhptZBC+/cbfPwdzeOtlBUeVi4ZB/5RWg2L4lWWdhBSiQBZw0AgaPg/2zsx4lDFgjg==}
|
||||
engines: {node: '>=22'}
|
||||
|
||||
'@atproto/common-web@0.5.0':
|
||||
resolution: {integrity: sha512-ReWnkuZdDU/74/I47gaI26uxQjHmpq4edp41NnZZQ5vIIKGb7Ei6pZHzDTUD9JURo109SKrPx9RMP2IQm0fOKA==}
|
||||
'@atproto/common-web@0.5.1':
|
||||
resolution: {integrity: sha512-nru02gLyzjMQn4ZFgms9ry3kIAKwMaCAvjeFhB9mU6h1ABiSho7aRDbGvnU50CC88TROXuZlgRiEkBjnuiHEtA==}
|
||||
engines: {node: '>=22'}
|
||||
|
||||
'@atproto/lex-data@0.1.1':
|
||||
resolution: {integrity: sha512-/xza8nU/YhtzhETnHL3QKKofaJ28/0NCzhT7LaYoUkm8EgypWp5ykEtmW52yLhQM2JF6fVa25g1soQmNTGqtSg==}
|
||||
'@atproto/lex-data@0.1.2':
|
||||
resolution: {integrity: sha512-NZ4iZvNaqTM6pz+9VRDyEjtozrrf2EDHySNi3Xa8PCzS8gRMCvsnnsvM7r264v4eSqNIDhBB8fr9hfWCHMspXg==}
|
||||
engines: {node: '>=22'}
|
||||
|
||||
'@atproto/lex-json@0.1.0':
|
||||
resolution: {integrity: sha512-oWUrRMwFyWpmi/5k1Se3xBTbP06XdxBS5iFuUz9LmqItaPXwrWRD87a9ldPvINQ/A2/mn7J6/qug8sDVlhD+vQ==}
|
||||
'@atproto/lex-json@0.1.1':
|
||||
resolution: {integrity: sha512-FC/NsKHm8TDzWikvf/268T3mMAh6+f31yfCApWC3SvrhWc9c06cSYPp7qzpXcdV0OdB+I5dqHScuTB5OC7HrXg==}
|
||||
engines: {node: '>=22'}
|
||||
|
||||
'@atproto/lexicon@0.7.1':
|
||||
resolution: {integrity: sha512-voNfNED5KUxn3vpo7N5DMRblBDfWf7kSfdKhJFC1RrLCxg38YbBzzURNVQJ32bp13Oot8kYfyXBWxTgtKLvw8w==}
|
||||
'@atproto/lexicon@0.7.2':
|
||||
resolution: {integrity: sha512-LVZcTr5+9Qh0okZnNmfRiIDPfbL/6rRxf4goiQxPxwDzaeUDhn4M209i1drmiitbCO1qRLJA2hHF54yNA75Cig==}
|
||||
engines: {node: '>=22'}
|
||||
|
||||
'@atproto/syntax@0.6.1':
|
||||
resolution: {integrity: sha512-kA4dQDoMPpWCH8N0Q4KoSq024u5MkVfDVa8DdhyLjGA72z/khbOf1jXKPv7NIL2oEc9aj7geKELdvqyf4ogopA==}
|
||||
engines: {node: '>=22'}
|
||||
|
||||
'@atproto/xrpc@0.8.0':
|
||||
resolution: {integrity: sha512-NJy02bIKrWlE2NQkRV1kT0Cj0ixbuxlF/MejBdo4cPWAa9v3oZexvAcjjb0zaOYeABkaU14iyIhvn2G4e/oLpw==}
|
||||
'@atproto/syntax@0.6.2':
|
||||
resolution: {integrity: sha512-h3njTNFl/jv5kTbDqfamQGOJfGbulqLDuAiLbasKwVdBhFyQanpRLa6vE2WqTwv1XEFWLYNMH0KCVsYwT2+aww==}
|
||||
engines: {node: '>=22'}
|
||||
|
||||
'@atproto/xrpc@0.8.1':
|
||||
resolution: {integrity: sha512-sKopRG3an6LN6NfHnRNIEX1fBx3CRtxbNFWQxyse2f86wMcTuXM+/+olN4lVXgFQgv4AWvKTxRXWyuIF2G4YxQ==}
|
||||
engines: {node: '>=22'}
|
||||
|
||||
'@babel/code-frame@7.10.4':
|
||||
@@ -9472,40 +9476,40 @@ snapshots:
|
||||
|
||||
'@0no-co/graphql.web@1.2.0': {}
|
||||
|
||||
'@atproto/api@0.20.15':
|
||||
'@atproto/api@0.20.20':
|
||||
dependencies:
|
||||
'@atproto/common-web': 0.5.0
|
||||
'@atproto/lexicon': 0.7.1
|
||||
'@atproto/syntax': 0.6.1
|
||||
'@atproto/xrpc': 0.8.0
|
||||
'@atproto/common-web': 0.5.1
|
||||
'@atproto/lexicon': 0.7.2
|
||||
'@atproto/syntax': 0.6.2
|
||||
'@atproto/xrpc': 0.8.1
|
||||
await-lock: 3.0.0
|
||||
multiformats: 13.4.2
|
||||
tlds: 1.261.0
|
||||
zod: 3.25.76
|
||||
|
||||
'@atproto/common-web@0.5.0':
|
||||
'@atproto/common-web@0.5.1':
|
||||
dependencies:
|
||||
'@atproto/lex-data': 0.1.1
|
||||
'@atproto/lex-json': 0.1.0
|
||||
'@atproto/syntax': 0.6.1
|
||||
'@atproto/lex-data': 0.1.2
|
||||
'@atproto/lex-json': 0.1.1
|
||||
'@atproto/syntax': 0.6.2
|
||||
zod: 3.25.76
|
||||
|
||||
'@atproto/lex-data@0.1.1':
|
||||
'@atproto/lex-data@0.1.2':
|
||||
dependencies:
|
||||
multiformats: 13.4.2
|
||||
tslib: 2.8.1
|
||||
uint8arrays: 5.1.1
|
||||
unicode-segmenter: 0.14.5
|
||||
|
||||
'@atproto/lex-json@0.1.0':
|
||||
'@atproto/lex-json@0.1.1':
|
||||
dependencies:
|
||||
'@atproto/lex-data': 0.1.1
|
||||
'@atproto/lex-data': 0.1.2
|
||||
tslib: 2.8.1
|
||||
|
||||
'@atproto/lexicon@0.7.1':
|
||||
'@atproto/lexicon@0.7.2':
|
||||
dependencies:
|
||||
'@atproto/common-web': 0.5.0
|
||||
'@atproto/syntax': 0.6.1
|
||||
'@atproto/common-web': 0.5.1
|
||||
'@atproto/syntax': 0.6.2
|
||||
multiformats: 13.4.2
|
||||
zod: 3.25.76
|
||||
|
||||
@@ -9514,9 +9518,14 @@ snapshots:
|
||||
iso-datestring-validator: 2.2.2
|
||||
tslib: 2.8.1
|
||||
|
||||
'@atproto/xrpc@0.8.0':
|
||||
'@atproto/syntax@0.6.2':
|
||||
dependencies:
|
||||
'@atproto/lexicon': 0.7.1
|
||||
iso-datestring-validator: 2.2.2
|
||||
tslib: 2.8.1
|
||||
|
||||
'@atproto/xrpc@0.8.1':
|
||||
dependencies:
|
||||
'@atproto/lexicon': 0.7.2
|
||||
zod: 3.25.76
|
||||
|
||||
'@babel/code-frame@7.10.4':
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import {View} from 'react-native'
|
||||
import {type AppBskyNotificationDefs} from '@atproto/api'
|
||||
import {Trans, useLingui} from '@lingui/react/macro'
|
||||
|
||||
import {useNotificationSettingsQuery} from '#/state/queries/notifications/settings'
|
||||
import {
|
||||
isChatPreferenceName,
|
||||
type NotificationSettingsPreferenceName,
|
||||
useChatNotificationSettingsQuery,
|
||||
useNotificationSettingsQuery,
|
||||
} from '#/state/queries/notifications/settings'
|
||||
import * as SettingsList from '#/screens/Settings/components/SettingsList'
|
||||
import {PreferenceControls} from '#/screens/Settings/NotificationSettings/components/PreferenceControls'
|
||||
import {atoms as a, useTheme, web} from '#/alf'
|
||||
@@ -15,8 +19,8 @@ import {IS_NATIVE} from '#/env'
|
||||
|
||||
type NotificationSettingsDialogProps = {
|
||||
control: Dialog.DialogControlProps
|
||||
name: Exclude<keyof AppBskyNotificationDefs.Preferences, '$type'>
|
||||
syncOthers?: Exclude<keyof AppBskyNotificationDefs.Preferences, '$type'>[]
|
||||
name: NotificationSettingsPreferenceName
|
||||
syncOthers?: NotificationSettingsPreferenceName[]
|
||||
icon: React.ComponentType<SVGIconProps>
|
||||
titleText: React.ReactNode
|
||||
subtitleText: React.ReactNode
|
||||
@@ -55,7 +59,11 @@ function NotificationSettingsDialogInner({
|
||||
}: Omit<NotificationSettingsDialogProps, 'icon'>) {
|
||||
const t = useTheme()
|
||||
const {t: l} = useLingui()
|
||||
const {data: preferences, isError} = useNotificationSettingsQuery()
|
||||
const isChat = isChatPreferenceName(name)
|
||||
const appQuery = useNotificationSettingsQuery({enabled: !isChat})
|
||||
const chatQuery = useChatNotificationSettingsQuery({enabled: isChat})
|
||||
const isError = isChat ? chatQuery.isError : appQuery.isError
|
||||
const preference = isChat ? chatQuery.data?.[name] : appQuery.data?.[name]
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -81,7 +89,7 @@ function NotificationSettingsDialogInner({
|
||||
<PreferenceControls
|
||||
name={name}
|
||||
syncOthers={syncOthers}
|
||||
preference={preferences?.[name]}
|
||||
preference={preference}
|
||||
allowDisableInApp={allowDisableInApp}
|
||||
/>
|
||||
)}
|
||||
@@ -89,7 +97,7 @@ function NotificationSettingsDialogInner({
|
||||
<Dialog.Close />
|
||||
{IS_NATIVE && (
|
||||
<Button
|
||||
color="secondary"
|
||||
color="primary"
|
||||
size="large"
|
||||
label={l`Close dialog`}
|
||||
onPress={() => control.close()}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import {useEffect} from 'react'
|
||||
import * as Notifications from 'expo-notifications'
|
||||
import {AtUri} from '@atproto/api'
|
||||
import {msg} from '@lingui/core/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useLingui} from '@lingui/react/macro'
|
||||
import {CommonActions, useNavigation} from '@react-navigation/native'
|
||||
import {useQueryClient} from '@tanstack/react-query'
|
||||
|
||||
@@ -108,6 +107,7 @@ let lastHandledNotificationDateDedupe = 0
|
||||
|
||||
export function useNotificationsHandler() {
|
||||
const ax = useAnalytics()
|
||||
// eslint-disable-next-line react-compiler/react-compiler
|
||||
const logger = ax.logger.useChild(ax.logger.Context.Notifications)
|
||||
const queryClient = useQueryClient()
|
||||
const {currentAccount, accounts} = useSession()
|
||||
@@ -116,7 +116,7 @@ export function useNotificationsHandler() {
|
||||
const {currentConvoId} = useCurrentConvoId()
|
||||
const {setShowLoggedOut} = useLoggedOutViewControls()
|
||||
const closeAllActiveElements = useCloseAllActiveElements()
|
||||
const {_} = useLingui()
|
||||
const {t: l} = useLingui()
|
||||
|
||||
// On Android, we cannot control which sound is used for a notification on Android
|
||||
// 28 or higher. Instead, we have to configure a notification channel ahead of time
|
||||
@@ -129,14 +129,12 @@ export function useNotificationsHandler() {
|
||||
// NOTE: I don't think that it will retroactively move them into the group
|
||||
// if the channels already exist. no big deal imo -sfn
|
||||
const CHAT_GROUP = 'chat'
|
||||
Notifications.setNotificationChannelGroupAsync(CHAT_GROUP, {
|
||||
name: _(msg`Chat`),
|
||||
description: _(
|
||||
msg`You can choose whether chat notifications have sound in the chat settings within the app`,
|
||||
),
|
||||
void Notifications.setNotificationChannelGroupAsync(CHAT_GROUP, {
|
||||
name: l`Chat`,
|
||||
description: l`You can choose whether chat notifications have sound in the chat settings within the app`,
|
||||
})
|
||||
Notifications.setNotificationChannelAsync('chat-messages', {
|
||||
name: _(msg`Chat messages - sound`),
|
||||
void Notifications.setNotificationChannelAsync('chat-messages', {
|
||||
name: l`Chat messages - sound`,
|
||||
groupId: CHAT_GROUP,
|
||||
importance: Notifications.AndroidImportance.MAX,
|
||||
sound: 'dm.mp3',
|
||||
@@ -144,8 +142,8 @@ export function useNotificationsHandler() {
|
||||
vibrationPattern: [250],
|
||||
lockscreenVisibility: Notifications.AndroidNotificationVisibility.PRIVATE,
|
||||
})
|
||||
Notifications.setNotificationChannelAsync('chat-messages-muted', {
|
||||
name: _(msg`Chat messages - silent`),
|
||||
void Notifications.setNotificationChannelAsync('chat-messages-muted', {
|
||||
name: l`Chat messages - silent`,
|
||||
groupId: CHAT_GROUP,
|
||||
importance: Notifications.AndroidImportance.MAX,
|
||||
sound: null,
|
||||
@@ -154,70 +152,70 @@ export function useNotificationsHandler() {
|
||||
lockscreenVisibility: Notifications.AndroidNotificationVisibility.PRIVATE,
|
||||
})
|
||||
|
||||
Notifications.setNotificationChannelAsync(
|
||||
void Notifications.setNotificationChannelAsync(
|
||||
'like' satisfies NotificationReason,
|
||||
{
|
||||
name: _(msg`Likes`),
|
||||
name: l`Likes`,
|
||||
importance: Notifications.AndroidImportance.HIGH,
|
||||
},
|
||||
)
|
||||
Notifications.setNotificationChannelAsync(
|
||||
void Notifications.setNotificationChannelAsync(
|
||||
'repost' satisfies NotificationReason,
|
||||
{
|
||||
name: _(msg`Reposts`),
|
||||
name: l`Reposts`,
|
||||
importance: Notifications.AndroidImportance.HIGH,
|
||||
},
|
||||
)
|
||||
Notifications.setNotificationChannelAsync(
|
||||
void Notifications.setNotificationChannelAsync(
|
||||
'reply' satisfies NotificationReason,
|
||||
{
|
||||
name: _(msg`Replies`),
|
||||
name: l`Replies`,
|
||||
importance: Notifications.AndroidImportance.HIGH,
|
||||
},
|
||||
)
|
||||
Notifications.setNotificationChannelAsync(
|
||||
void Notifications.setNotificationChannelAsync(
|
||||
'mention' satisfies NotificationReason,
|
||||
{
|
||||
name: _(msg`Mentions`),
|
||||
name: l`Mentions`,
|
||||
importance: Notifications.AndroidImportance.HIGH,
|
||||
},
|
||||
)
|
||||
Notifications.setNotificationChannelAsync(
|
||||
void Notifications.setNotificationChannelAsync(
|
||||
'quote' satisfies NotificationReason,
|
||||
{
|
||||
name: _(msg`Quotes`),
|
||||
name: l`Quotes`,
|
||||
importance: Notifications.AndroidImportance.HIGH,
|
||||
},
|
||||
)
|
||||
Notifications.setNotificationChannelAsync(
|
||||
void Notifications.setNotificationChannelAsync(
|
||||
'follow' satisfies NotificationReason,
|
||||
{
|
||||
name: _(msg`New followers`),
|
||||
name: l`New followers`,
|
||||
importance: Notifications.AndroidImportance.HIGH,
|
||||
},
|
||||
)
|
||||
Notifications.setNotificationChannelAsync(
|
||||
void Notifications.setNotificationChannelAsync(
|
||||
'like-via-repost' satisfies NotificationReason,
|
||||
{
|
||||
name: _(msg`Likes of your reposts`),
|
||||
name: l`Likes of your reposts`,
|
||||
importance: Notifications.AndroidImportance.HIGH,
|
||||
},
|
||||
)
|
||||
Notifications.setNotificationChannelAsync(
|
||||
void Notifications.setNotificationChannelAsync(
|
||||
'repost-via-repost' satisfies NotificationReason,
|
||||
{
|
||||
name: _(msg`Reposts of your reposts`),
|
||||
name: l`Reposts of your reposts`,
|
||||
importance: Notifications.AndroidImportance.HIGH,
|
||||
},
|
||||
)
|
||||
Notifications.setNotificationChannelAsync(
|
||||
void Notifications.setNotificationChannelAsync(
|
||||
'subscribed-post' satisfies NotificationReason,
|
||||
{
|
||||
name: _(msg`Activity from others`),
|
||||
name: l`Activity from others`,
|
||||
importance: Notifications.AndroidImportance.HIGH,
|
||||
},
|
||||
)
|
||||
}, [_])
|
||||
}, [l])
|
||||
|
||||
useEffect(() => {
|
||||
const handleNotification = (payload?: NotificationPayload) => {
|
||||
@@ -237,7 +235,7 @@ export function useNotificationsHandler() {
|
||||
|
||||
const account = accounts.find(a => a.did === payload.recipientDid)
|
||||
if (account) {
|
||||
onPressSwitchAccount(account, 'Notification')
|
||||
void onPressSwitchAccount(account, 'Notification')
|
||||
} else {
|
||||
setShowLoggedOut(true)
|
||||
}
|
||||
@@ -305,10 +303,10 @@ export function useNotificationsHandler() {
|
||||
}
|
||||
|
||||
Notifications.setNotificationHandler({
|
||||
handleNotification: async e => {
|
||||
handleNotification: e => {
|
||||
const payload = getNotificationPayload(e)
|
||||
|
||||
if (!payload) return DEFAULT_HANDLER_OPTIONS
|
||||
if (!payload) return Promise.resolve(DEFAULT_HANDLER_OPTIONS)
|
||||
|
||||
logger.debug('useNotificationsHandler: incoming', {e, payload})
|
||||
|
||||
@@ -323,17 +321,17 @@ export function useNotificationsHandler() {
|
||||
payload.reason === 'chat-removed-from-group' ||
|
||||
payload.reason === 'chat-join-request-rejected' ||
|
||||
payload.convoId !== currentConvoId
|
||||
return {
|
||||
return Promise.resolve({
|
||||
shouldShowList: shouldAlert,
|
||||
shouldShowBanner: shouldAlert,
|
||||
shouldPlaySound: false,
|
||||
shouldSetBadge: false,
|
||||
} satisfies Notifications.NotificationBehavior
|
||||
} satisfies Notifications.NotificationBehavior)
|
||||
}
|
||||
|
||||
// Any notification other than a chat message should invalidate the unread page
|
||||
invalidateCachedUnreadPage()
|
||||
return DEFAULT_HANDLER_OPTIONS
|
||||
return Promise.resolve(DEFAULT_HANDLER_OPTIONS)
|
||||
},
|
||||
})
|
||||
|
||||
@@ -363,14 +361,14 @@ export function useNotificationsHandler() {
|
||||
})
|
||||
|
||||
invalidateCachedUnreadPage()
|
||||
truncateAndInvalidate(queryClient, RQKEY_NOTIFS('all'))
|
||||
void truncateAndInvalidate(queryClient, RQKEY_NOTIFS('all'))
|
||||
|
||||
if (
|
||||
payload.reason === 'mention' ||
|
||||
payload.reason === 'quote' ||
|
||||
payload.reason === 'reply'
|
||||
) {
|
||||
truncateAndInvalidate(queryClient, RQKEY_NOTIFS('mentions'))
|
||||
void truncateAndInvalidate(queryClient, RQKEY_NOTIFS('mentions'))
|
||||
}
|
||||
|
||||
logger.debug('Notifications: handleNotification', {
|
||||
@@ -379,7 +377,7 @@ export function useNotificationsHandler() {
|
||||
})
|
||||
|
||||
handleNotification(payload)
|
||||
Notifications.dismissAllNotificationsAsync()
|
||||
void Notifications.dismissAllNotificationsAsync()
|
||||
// Also clear the native `lastResponse` cache. Otherwise a subsequent
|
||||
// `getLastNotificationResponse()` (e.g. on an account-switch remount,
|
||||
// which re-runs `handlePushNotificationEntry`) would replay this
|
||||
|
||||
@@ -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`
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import {type AppBskyNotificationDefs} from '@atproto/api'
|
||||
import {
|
||||
type AppBskyNotificationDefs,
|
||||
type ChatBskyNotificationDefs,
|
||||
} from '@atproto/api'
|
||||
import {t} from '@lingui/core/macro'
|
||||
import {
|
||||
type QueryClient,
|
||||
@@ -7,12 +10,59 @@ import {
|
||||
useQueryClient,
|
||||
} from '@tanstack/react-query'
|
||||
|
||||
import {DM_SERVICE_HEADERS} from '#/lib/constants'
|
||||
import {logger} from '#/logger'
|
||||
import {useAgent} from '#/state/session'
|
||||
import * as Toast from '#/components/Toast'
|
||||
|
||||
const RQKEY_ROOT = 'notification-settings'
|
||||
const RQKEY = [RQKEY_ROOT]
|
||||
const RQKEY_APP = [RQKEY_ROOT, 'app']
|
||||
const RQKEY_CHAT = [RQKEY_ROOT, 'chat']
|
||||
|
||||
// App notification preferences live on the appview. Chat preferences live on a
|
||||
// separate chat service proxy that can be up or down independently, so they are
|
||||
// fetched and cached separately. This combined type names every preference for
|
||||
// the generic settings dialog, but it is never the shape of a query response.
|
||||
export type NotificationSettingsPreferences = Omit<
|
||||
AppBskyNotificationDefs.Preferences,
|
||||
'chat'
|
||||
> &
|
||||
Partial<Pick<ChatBskyNotificationDefs.Preferences, 'chat' | 'chatRequest'>>
|
||||
|
||||
export type AppNotificationSettingsPreferences = Omit<
|
||||
AppBskyNotificationDefs.Preferences,
|
||||
'chat'
|
||||
>
|
||||
|
||||
export type ChatNotificationSettingsPreferences = Pick<
|
||||
ChatBskyNotificationDefs.Preferences,
|
||||
'chat' | 'chatRequest'
|
||||
>
|
||||
|
||||
export type NotificationSettingsPreferenceName = Exclude<
|
||||
keyof NotificationSettingsPreferences,
|
||||
'$type'
|
||||
>
|
||||
|
||||
export type NotificationSettingsPreference =
|
||||
| AppBskyNotificationDefs.Preference
|
||||
| AppBskyNotificationDefs.FilterablePreference
|
||||
| ChatBskyNotificationDefs.ChatPreference
|
||||
|
||||
export function isChatPreferenceName(
|
||||
name: NotificationSettingsPreferenceName,
|
||||
): name is 'chat' | 'chatRequest' {
|
||||
return name === 'chat' || name === 'chatRequest'
|
||||
}
|
||||
|
||||
type NotificationSettingsUpdate = Partial<NotificationSettingsPreferences>
|
||||
|
||||
type AppNotificationSettingsUpdate = Partial<
|
||||
Omit<AppBskyNotificationDefs.Preferences, '$type' | 'chat'>
|
||||
>
|
||||
|
||||
type ChatNotificationSettingsUpdate =
|
||||
Partial<ChatNotificationSettingsPreferences>
|
||||
|
||||
export function useNotificationSettingsQuery({
|
||||
enabled,
|
||||
@@ -20,10 +70,27 @@ export function useNotificationSettingsQuery({
|
||||
const agent = useAgent()
|
||||
|
||||
return useQuery({
|
||||
queryKey: RQKEY,
|
||||
queryFn: async () => {
|
||||
const response = await agent.app.bsky.notification.getPreferences()
|
||||
return response.data.preferences
|
||||
queryKey: RQKEY_APP,
|
||||
queryFn: async (): Promise<AppNotificationSettingsPreferences> => {
|
||||
const res = await agent.app.bsky.notification.getPreferences()
|
||||
return appPreferencesWithoutChat(res.data.preferences)
|
||||
},
|
||||
enabled,
|
||||
})
|
||||
}
|
||||
|
||||
export function useChatNotificationSettingsQuery({
|
||||
enabled,
|
||||
}: {enabled?: boolean} = {}) {
|
||||
const agent = useAgent()
|
||||
|
||||
return useQuery({
|
||||
queryKey: RQKEY_CHAT,
|
||||
queryFn: async (): Promise<ChatNotificationSettingsPreferences> => {
|
||||
const res = await agent.chat.bsky.notification.getPreferences(undefined, {
|
||||
headers: DM_SERVICE_HEADERS,
|
||||
})
|
||||
return chatPreferencesForSettings(res.data.preferences)
|
||||
},
|
||||
enabled,
|
||||
})
|
||||
@@ -33,19 +100,27 @@ export function useNotificationSettingsUpdateMutation() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (
|
||||
update: Partial<AppBskyNotificationDefs.Preferences>,
|
||||
) => {
|
||||
const response =
|
||||
await agent.app.bsky.notification.putPreferencesV2(update)
|
||||
return response.data.preferences
|
||||
mutationFn: async (update: NotificationSettingsUpdate) => {
|
||||
const {appUpdate, chatUpdate} = splitNotificationSettingsUpdate(update)
|
||||
await Promise.all([
|
||||
hasUpdates(appUpdate)
|
||||
? agent.app.bsky.notification.putPreferencesV2(appUpdate)
|
||||
: undefined,
|
||||
hasUpdates(chatUpdate)
|
||||
? agent.chat.bsky.notification.putPreferences(chatUpdate, {
|
||||
headers: DM_SERVICE_HEADERS,
|
||||
encoding: 'application/json',
|
||||
})
|
||||
: undefined,
|
||||
])
|
||||
},
|
||||
onMutate: update => {
|
||||
optimisticUpdateNotificationSettings(queryClient, update)
|
||||
},
|
||||
onError: e => {
|
||||
logger.error('Could not update notification settings', {message: e})
|
||||
void queryClient.invalidateQueries({queryKey: RQKEY})
|
||||
void queryClient.invalidateQueries({queryKey: RQKEY_APP})
|
||||
void queryClient.invalidateQueries({queryKey: RQKEY_CHAT})
|
||||
Toast.show(t`Could not update notification settings`, {
|
||||
type: 'error',
|
||||
})
|
||||
@@ -55,13 +130,62 @@ export function useNotificationSettingsUpdateMutation() {
|
||||
|
||||
function optimisticUpdateNotificationSettings(
|
||||
queryClient: QueryClient,
|
||||
update: Partial<AppBskyNotificationDefs.Preferences>,
|
||||
update: NotificationSettingsUpdate,
|
||||
) {
|
||||
queryClient.setQueryData(
|
||||
RQKEY,
|
||||
(old?: AppBskyNotificationDefs.Preferences) => {
|
||||
if (!old) return old
|
||||
return {...old, ...update}
|
||||
},
|
||||
)
|
||||
const {appUpdate, chatUpdate} = splitNotificationSettingsUpdate(update)
|
||||
|
||||
if (hasUpdates(appUpdate)) {
|
||||
queryClient.setQueryData(
|
||||
RQKEY_APP,
|
||||
(old?: AppNotificationSettingsPreferences) => {
|
||||
if (!old) return old
|
||||
return {...old, ...appUpdate}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
if (hasUpdates(chatUpdate)) {
|
||||
queryClient.setQueryData(
|
||||
RQKEY_CHAT,
|
||||
(old?: ChatNotificationSettingsPreferences) => {
|
||||
if (!old) return old
|
||||
return {...old, ...chatUpdate}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function appPreferencesWithoutChat(
|
||||
preferences: AppBskyNotificationDefs.Preferences,
|
||||
): Omit<AppBskyNotificationDefs.Preferences, 'chat'> {
|
||||
const {chat: _ignoredChat, ...appPreferences} = preferences
|
||||
return appPreferences
|
||||
}
|
||||
|
||||
function chatPreferencesForSettings(
|
||||
preferences: ChatBskyNotificationDefs.Preferences,
|
||||
): Pick<ChatBskyNotificationDefs.Preferences, 'chat' | 'chatRequest'> {
|
||||
return {
|
||||
chat: preferences.chat,
|
||||
chatRequest: preferences.chatRequest,
|
||||
}
|
||||
}
|
||||
|
||||
function splitNotificationSettingsUpdate(update: NotificationSettingsUpdate): {
|
||||
appUpdate: AppNotificationSettingsUpdate
|
||||
chatUpdate: ChatNotificationSettingsUpdate
|
||||
} {
|
||||
const {chat, chatRequest, $type: _type, ...appUpdate} = update
|
||||
|
||||
return {
|
||||
appUpdate: appUpdate,
|
||||
chatUpdate: {
|
||||
...(chat !== undefined ? {chat} : {}),
|
||||
...(chatRequest !== undefined ? {chatRequest} : {}),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function hasUpdates(update: object) {
|
||||
return Object.keys(update).length > 0
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user