Add new chat notification preferences (#10972)
This commit is contained in:
@@ -946,14 +946,6 @@
|
|||||||
"count": 1
|
"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": {
|
"src/lib/hooks/useOTAUpdates.ts": {
|
||||||
"@typescript-eslint/no-explicit-any": {
|
"@typescript-eslint/no-explicit-any": {
|
||||||
"count": 2
|
"count": 2
|
||||||
@@ -2183,14 +2175,6 @@
|
|||||||
"count": 2
|
"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": {
|
"src/view/com/notifications/NotificationFeed.tsx": {
|
||||||
"@typescript-eslint/no-explicit-any": {
|
"@typescript-eslint/no-explicit-any": {
|
||||||
"count": 2
|
"count": 2
|
||||||
|
|||||||
+1
-1
@@ -94,7 +94,7 @@
|
|||||||
"update-actions": "pnpm dlx actions-up --min-age 7"
|
"update-actions": "pnpm dlx actions-up --min-age 7"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@atproto/api": "0.20.15",
|
"@atproto/api": "0.20.20",
|
||||||
"@atproto/syntax": "0.6.1",
|
"@atproto/syntax": "0.6.1",
|
||||||
"@bitdrift/react-native": "^0.6.8",
|
"@bitdrift/react-native": "^0.6.8",
|
||||||
"@braintree/sanitize-url": "^6.0.2",
|
"@braintree/sanitize-url": "^6.0.2",
|
||||||
|
|||||||
Generated
+40
-31
@@ -241,8 +241,8 @@ importers:
|
|||||||
.:
|
.:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@atproto/api':
|
'@atproto/api':
|
||||||
specifier: 0.20.15
|
specifier: 0.20.20
|
||||||
version: 0.20.15
|
version: 0.20.20
|
||||||
'@atproto/syntax':
|
'@atproto/syntax':
|
||||||
specifier: 0.6.1
|
specifier: 0.6.1
|
||||||
version: 0.6.1
|
version: 0.6.1
|
||||||
@@ -876,32 +876,36 @@ packages:
|
|||||||
graphql:
|
graphql:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@atproto/api@0.20.15':
|
'@atproto/api@0.20.20':
|
||||||
resolution: {integrity: sha512-b9TuVNY9iWIaRXAeKegNCqRsK9tSpB68DE/j/ytVTxEMK+/m43B0DycJND9tnhRiNIY+i7MhwLqNHPak9YaDJg==}
|
resolution: {integrity: sha512-+yqxLCvMu36xRxwc8/x0mhptZBC+/cbfPwdzeOtlBUeVi4ZB/5RWg2L4lWWdhBSiQBZw0AgaPg/2zsx4lDFgjg==}
|
||||||
engines: {node: '>=22'}
|
engines: {node: '>=22'}
|
||||||
|
|
||||||
'@atproto/common-web@0.5.0':
|
'@atproto/common-web@0.5.1':
|
||||||
resolution: {integrity: sha512-ReWnkuZdDU/74/I47gaI26uxQjHmpq4edp41NnZZQ5vIIKGb7Ei6pZHzDTUD9JURo109SKrPx9RMP2IQm0fOKA==}
|
resolution: {integrity: sha512-nru02gLyzjMQn4ZFgms9ry3kIAKwMaCAvjeFhB9mU6h1ABiSho7aRDbGvnU50CC88TROXuZlgRiEkBjnuiHEtA==}
|
||||||
engines: {node: '>=22'}
|
engines: {node: '>=22'}
|
||||||
|
|
||||||
'@atproto/lex-data@0.1.1':
|
'@atproto/lex-data@0.1.2':
|
||||||
resolution: {integrity: sha512-/xza8nU/YhtzhETnHL3QKKofaJ28/0NCzhT7LaYoUkm8EgypWp5ykEtmW52yLhQM2JF6fVa25g1soQmNTGqtSg==}
|
resolution: {integrity: sha512-NZ4iZvNaqTM6pz+9VRDyEjtozrrf2EDHySNi3Xa8PCzS8gRMCvsnnsvM7r264v4eSqNIDhBB8fr9hfWCHMspXg==}
|
||||||
engines: {node: '>=22'}
|
engines: {node: '>=22'}
|
||||||
|
|
||||||
'@atproto/lex-json@0.1.0':
|
'@atproto/lex-json@0.1.1':
|
||||||
resolution: {integrity: sha512-oWUrRMwFyWpmi/5k1Se3xBTbP06XdxBS5iFuUz9LmqItaPXwrWRD87a9ldPvINQ/A2/mn7J6/qug8sDVlhD+vQ==}
|
resolution: {integrity: sha512-FC/NsKHm8TDzWikvf/268T3mMAh6+f31yfCApWC3SvrhWc9c06cSYPp7qzpXcdV0OdB+I5dqHScuTB5OC7HrXg==}
|
||||||
engines: {node: '>=22'}
|
engines: {node: '>=22'}
|
||||||
|
|
||||||
'@atproto/lexicon@0.7.1':
|
'@atproto/lexicon@0.7.2':
|
||||||
resolution: {integrity: sha512-voNfNED5KUxn3vpo7N5DMRblBDfWf7kSfdKhJFC1RrLCxg38YbBzzURNVQJ32bp13Oot8kYfyXBWxTgtKLvw8w==}
|
resolution: {integrity: sha512-LVZcTr5+9Qh0okZnNmfRiIDPfbL/6rRxf4goiQxPxwDzaeUDhn4M209i1drmiitbCO1qRLJA2hHF54yNA75Cig==}
|
||||||
engines: {node: '>=22'}
|
engines: {node: '>=22'}
|
||||||
|
|
||||||
'@atproto/syntax@0.6.1':
|
'@atproto/syntax@0.6.1':
|
||||||
resolution: {integrity: sha512-kA4dQDoMPpWCH8N0Q4KoSq024u5MkVfDVa8DdhyLjGA72z/khbOf1jXKPv7NIL2oEc9aj7geKELdvqyf4ogopA==}
|
resolution: {integrity: sha512-kA4dQDoMPpWCH8N0Q4KoSq024u5MkVfDVa8DdhyLjGA72z/khbOf1jXKPv7NIL2oEc9aj7geKELdvqyf4ogopA==}
|
||||||
engines: {node: '>=22'}
|
engines: {node: '>=22'}
|
||||||
|
|
||||||
'@atproto/xrpc@0.8.0':
|
'@atproto/syntax@0.6.2':
|
||||||
resolution: {integrity: sha512-NJy02bIKrWlE2NQkRV1kT0Cj0ixbuxlF/MejBdo4cPWAa9v3oZexvAcjjb0zaOYeABkaU14iyIhvn2G4e/oLpw==}
|
resolution: {integrity: sha512-h3njTNFl/jv5kTbDqfamQGOJfGbulqLDuAiLbasKwVdBhFyQanpRLa6vE2WqTwv1XEFWLYNMH0KCVsYwT2+aww==}
|
||||||
|
engines: {node: '>=22'}
|
||||||
|
|
||||||
|
'@atproto/xrpc@0.8.1':
|
||||||
|
resolution: {integrity: sha512-sKopRG3an6LN6NfHnRNIEX1fBx3CRtxbNFWQxyse2f86wMcTuXM+/+olN4lVXgFQgv4AWvKTxRXWyuIF2G4YxQ==}
|
||||||
engines: {node: '>=22'}
|
engines: {node: '>=22'}
|
||||||
|
|
||||||
'@babel/code-frame@7.10.4':
|
'@babel/code-frame@7.10.4':
|
||||||
@@ -9472,40 +9476,40 @@ snapshots:
|
|||||||
|
|
||||||
'@0no-co/graphql.web@1.2.0': {}
|
'@0no-co/graphql.web@1.2.0': {}
|
||||||
|
|
||||||
'@atproto/api@0.20.15':
|
'@atproto/api@0.20.20':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@atproto/common-web': 0.5.0
|
'@atproto/common-web': 0.5.1
|
||||||
'@atproto/lexicon': 0.7.1
|
'@atproto/lexicon': 0.7.2
|
||||||
'@atproto/syntax': 0.6.1
|
'@atproto/syntax': 0.6.2
|
||||||
'@atproto/xrpc': 0.8.0
|
'@atproto/xrpc': 0.8.1
|
||||||
await-lock: 3.0.0
|
await-lock: 3.0.0
|
||||||
multiformats: 13.4.2
|
multiformats: 13.4.2
|
||||||
tlds: 1.261.0
|
tlds: 1.261.0
|
||||||
zod: 3.25.76
|
zod: 3.25.76
|
||||||
|
|
||||||
'@atproto/common-web@0.5.0':
|
'@atproto/common-web@0.5.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@atproto/lex-data': 0.1.1
|
'@atproto/lex-data': 0.1.2
|
||||||
'@atproto/lex-json': 0.1.0
|
'@atproto/lex-json': 0.1.1
|
||||||
'@atproto/syntax': 0.6.1
|
'@atproto/syntax': 0.6.2
|
||||||
zod: 3.25.76
|
zod: 3.25.76
|
||||||
|
|
||||||
'@atproto/lex-data@0.1.1':
|
'@atproto/lex-data@0.1.2':
|
||||||
dependencies:
|
dependencies:
|
||||||
multiformats: 13.4.2
|
multiformats: 13.4.2
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
uint8arrays: 5.1.1
|
uint8arrays: 5.1.1
|
||||||
unicode-segmenter: 0.14.5
|
unicode-segmenter: 0.14.5
|
||||||
|
|
||||||
'@atproto/lex-json@0.1.0':
|
'@atproto/lex-json@0.1.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@atproto/lex-data': 0.1.1
|
'@atproto/lex-data': 0.1.2
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
|
||||||
'@atproto/lexicon@0.7.1':
|
'@atproto/lexicon@0.7.2':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@atproto/common-web': 0.5.0
|
'@atproto/common-web': 0.5.1
|
||||||
'@atproto/syntax': 0.6.1
|
'@atproto/syntax': 0.6.2
|
||||||
multiformats: 13.4.2
|
multiformats: 13.4.2
|
||||||
zod: 3.25.76
|
zod: 3.25.76
|
||||||
|
|
||||||
@@ -9514,9 +9518,14 @@ snapshots:
|
|||||||
iso-datestring-validator: 2.2.2
|
iso-datestring-validator: 2.2.2
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
|
||||||
'@atproto/xrpc@0.8.0':
|
'@atproto/syntax@0.6.2':
|
||||||
dependencies:
|
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
|
zod: 3.25.76
|
||||||
|
|
||||||
'@babel/code-frame@7.10.4':
|
'@babel/code-frame@7.10.4':
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
import {View} from 'react-native'
|
import {View} from 'react-native'
|
||||||
import {type AppBskyNotificationDefs} from '@atproto/api'
|
|
||||||
import {Trans, useLingui} from '@lingui/react/macro'
|
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 * as SettingsList from '#/screens/Settings/components/SettingsList'
|
||||||
import {PreferenceControls} from '#/screens/Settings/NotificationSettings/components/PreferenceControls'
|
import {PreferenceControls} from '#/screens/Settings/NotificationSettings/components/PreferenceControls'
|
||||||
import {atoms as a, useTheme, web} from '#/alf'
|
import {atoms as a, useTheme, web} from '#/alf'
|
||||||
@@ -15,8 +19,8 @@ import {IS_NATIVE} from '#/env'
|
|||||||
|
|
||||||
type NotificationSettingsDialogProps = {
|
type NotificationSettingsDialogProps = {
|
||||||
control: Dialog.DialogControlProps
|
control: Dialog.DialogControlProps
|
||||||
name: Exclude<keyof AppBskyNotificationDefs.Preferences, '$type'>
|
name: NotificationSettingsPreferenceName
|
||||||
syncOthers?: Exclude<keyof AppBskyNotificationDefs.Preferences, '$type'>[]
|
syncOthers?: NotificationSettingsPreferenceName[]
|
||||||
icon: React.ComponentType<SVGIconProps>
|
icon: React.ComponentType<SVGIconProps>
|
||||||
titleText: React.ReactNode
|
titleText: React.ReactNode
|
||||||
subtitleText: React.ReactNode
|
subtitleText: React.ReactNode
|
||||||
@@ -55,7 +59,11 @@ function NotificationSettingsDialogInner({
|
|||||||
}: Omit<NotificationSettingsDialogProps, 'icon'>) {
|
}: Omit<NotificationSettingsDialogProps, 'icon'>) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {t: l} = useLingui()
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -81,7 +89,7 @@ function NotificationSettingsDialogInner({
|
|||||||
<PreferenceControls
|
<PreferenceControls
|
||||||
name={name}
|
name={name}
|
||||||
syncOthers={syncOthers}
|
syncOthers={syncOthers}
|
||||||
preference={preferences?.[name]}
|
preference={preference}
|
||||||
allowDisableInApp={allowDisableInApp}
|
allowDisableInApp={allowDisableInApp}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -89,7 +97,7 @@ function NotificationSettingsDialogInner({
|
|||||||
<Dialog.Close />
|
<Dialog.Close />
|
||||||
{IS_NATIVE && (
|
{IS_NATIVE && (
|
||||||
<Button
|
<Button
|
||||||
color="secondary"
|
color="primary"
|
||||||
size="large"
|
size="large"
|
||||||
label={l`Close dialog`}
|
label={l`Close dialog`}
|
||||||
onPress={() => control.close()}
|
onPress={() => control.close()}
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import {useEffect} from 'react'
|
import {useEffect} from 'react'
|
||||||
import * as Notifications from 'expo-notifications'
|
import * as Notifications from 'expo-notifications'
|
||||||
import {AtUri} from '@atproto/api'
|
import {AtUri} from '@atproto/api'
|
||||||
import {msg} from '@lingui/core/macro'
|
import {useLingui} from '@lingui/react/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
|
||||||
import {CommonActions, useNavigation} from '@react-navigation/native'
|
import {CommonActions, useNavigation} from '@react-navigation/native'
|
||||||
import {useQueryClient} from '@tanstack/react-query'
|
import {useQueryClient} from '@tanstack/react-query'
|
||||||
|
|
||||||
@@ -108,6 +107,7 @@ let lastHandledNotificationDateDedupe = 0
|
|||||||
|
|
||||||
export function useNotificationsHandler() {
|
export function useNotificationsHandler() {
|
||||||
const ax = useAnalytics()
|
const ax = useAnalytics()
|
||||||
|
// eslint-disable-next-line react-compiler/react-compiler
|
||||||
const logger = ax.logger.useChild(ax.logger.Context.Notifications)
|
const logger = ax.logger.useChild(ax.logger.Context.Notifications)
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const {currentAccount, accounts} = useSession()
|
const {currentAccount, accounts} = useSession()
|
||||||
@@ -116,7 +116,7 @@ export function useNotificationsHandler() {
|
|||||||
const {currentConvoId} = useCurrentConvoId()
|
const {currentConvoId} = useCurrentConvoId()
|
||||||
const {setShowLoggedOut} = useLoggedOutViewControls()
|
const {setShowLoggedOut} = useLoggedOutViewControls()
|
||||||
const closeAllActiveElements = useCloseAllActiveElements()
|
const closeAllActiveElements = useCloseAllActiveElements()
|
||||||
const {_} = useLingui()
|
const {t: l} = useLingui()
|
||||||
|
|
||||||
// On Android, we cannot control which sound is used for a notification on Android
|
// 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
|
// 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
|
// NOTE: I don't think that it will retroactively move them into the group
|
||||||
// if the channels already exist. no big deal imo -sfn
|
// if the channels already exist. no big deal imo -sfn
|
||||||
const CHAT_GROUP = 'chat'
|
const CHAT_GROUP = 'chat'
|
||||||
Notifications.setNotificationChannelGroupAsync(CHAT_GROUP, {
|
void Notifications.setNotificationChannelGroupAsync(CHAT_GROUP, {
|
||||||
name: _(msg`Chat`),
|
name: l`Chat`,
|
||||||
description: _(
|
description: l`You can choose whether chat notifications have sound in the chat settings within the app`,
|
||||||
msg`You can choose whether chat notifications have sound in the chat settings within the app`,
|
|
||||||
),
|
|
||||||
})
|
})
|
||||||
Notifications.setNotificationChannelAsync('chat-messages', {
|
void Notifications.setNotificationChannelAsync('chat-messages', {
|
||||||
name: _(msg`Chat messages - sound`),
|
name: l`Chat messages - sound`,
|
||||||
groupId: CHAT_GROUP,
|
groupId: CHAT_GROUP,
|
||||||
importance: Notifications.AndroidImportance.MAX,
|
importance: Notifications.AndroidImportance.MAX,
|
||||||
sound: 'dm.mp3',
|
sound: 'dm.mp3',
|
||||||
@@ -144,8 +142,8 @@ export function useNotificationsHandler() {
|
|||||||
vibrationPattern: [250],
|
vibrationPattern: [250],
|
||||||
lockscreenVisibility: Notifications.AndroidNotificationVisibility.PRIVATE,
|
lockscreenVisibility: Notifications.AndroidNotificationVisibility.PRIVATE,
|
||||||
})
|
})
|
||||||
Notifications.setNotificationChannelAsync('chat-messages-muted', {
|
void Notifications.setNotificationChannelAsync('chat-messages-muted', {
|
||||||
name: _(msg`Chat messages - silent`),
|
name: l`Chat messages - silent`,
|
||||||
groupId: CHAT_GROUP,
|
groupId: CHAT_GROUP,
|
||||||
importance: Notifications.AndroidImportance.MAX,
|
importance: Notifications.AndroidImportance.MAX,
|
||||||
sound: null,
|
sound: null,
|
||||||
@@ -154,70 +152,70 @@ export function useNotificationsHandler() {
|
|||||||
lockscreenVisibility: Notifications.AndroidNotificationVisibility.PRIVATE,
|
lockscreenVisibility: Notifications.AndroidNotificationVisibility.PRIVATE,
|
||||||
})
|
})
|
||||||
|
|
||||||
Notifications.setNotificationChannelAsync(
|
void Notifications.setNotificationChannelAsync(
|
||||||
'like' satisfies NotificationReason,
|
'like' satisfies NotificationReason,
|
||||||
{
|
{
|
||||||
name: _(msg`Likes`),
|
name: l`Likes`,
|
||||||
importance: Notifications.AndroidImportance.HIGH,
|
importance: Notifications.AndroidImportance.HIGH,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
Notifications.setNotificationChannelAsync(
|
void Notifications.setNotificationChannelAsync(
|
||||||
'repost' satisfies NotificationReason,
|
'repost' satisfies NotificationReason,
|
||||||
{
|
{
|
||||||
name: _(msg`Reposts`),
|
name: l`Reposts`,
|
||||||
importance: Notifications.AndroidImportance.HIGH,
|
importance: Notifications.AndroidImportance.HIGH,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
Notifications.setNotificationChannelAsync(
|
void Notifications.setNotificationChannelAsync(
|
||||||
'reply' satisfies NotificationReason,
|
'reply' satisfies NotificationReason,
|
||||||
{
|
{
|
||||||
name: _(msg`Replies`),
|
name: l`Replies`,
|
||||||
importance: Notifications.AndroidImportance.HIGH,
|
importance: Notifications.AndroidImportance.HIGH,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
Notifications.setNotificationChannelAsync(
|
void Notifications.setNotificationChannelAsync(
|
||||||
'mention' satisfies NotificationReason,
|
'mention' satisfies NotificationReason,
|
||||||
{
|
{
|
||||||
name: _(msg`Mentions`),
|
name: l`Mentions`,
|
||||||
importance: Notifications.AndroidImportance.HIGH,
|
importance: Notifications.AndroidImportance.HIGH,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
Notifications.setNotificationChannelAsync(
|
void Notifications.setNotificationChannelAsync(
|
||||||
'quote' satisfies NotificationReason,
|
'quote' satisfies NotificationReason,
|
||||||
{
|
{
|
||||||
name: _(msg`Quotes`),
|
name: l`Quotes`,
|
||||||
importance: Notifications.AndroidImportance.HIGH,
|
importance: Notifications.AndroidImportance.HIGH,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
Notifications.setNotificationChannelAsync(
|
void Notifications.setNotificationChannelAsync(
|
||||||
'follow' satisfies NotificationReason,
|
'follow' satisfies NotificationReason,
|
||||||
{
|
{
|
||||||
name: _(msg`New followers`),
|
name: l`New followers`,
|
||||||
importance: Notifications.AndroidImportance.HIGH,
|
importance: Notifications.AndroidImportance.HIGH,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
Notifications.setNotificationChannelAsync(
|
void Notifications.setNotificationChannelAsync(
|
||||||
'like-via-repost' satisfies NotificationReason,
|
'like-via-repost' satisfies NotificationReason,
|
||||||
{
|
{
|
||||||
name: _(msg`Likes of your reposts`),
|
name: l`Likes of your reposts`,
|
||||||
importance: Notifications.AndroidImportance.HIGH,
|
importance: Notifications.AndroidImportance.HIGH,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
Notifications.setNotificationChannelAsync(
|
void Notifications.setNotificationChannelAsync(
|
||||||
'repost-via-repost' satisfies NotificationReason,
|
'repost-via-repost' satisfies NotificationReason,
|
||||||
{
|
{
|
||||||
name: _(msg`Reposts of your reposts`),
|
name: l`Reposts of your reposts`,
|
||||||
importance: Notifications.AndroidImportance.HIGH,
|
importance: Notifications.AndroidImportance.HIGH,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
Notifications.setNotificationChannelAsync(
|
void Notifications.setNotificationChannelAsync(
|
||||||
'subscribed-post' satisfies NotificationReason,
|
'subscribed-post' satisfies NotificationReason,
|
||||||
{
|
{
|
||||||
name: _(msg`Activity from others`),
|
name: l`Activity from others`,
|
||||||
importance: Notifications.AndroidImportance.HIGH,
|
importance: Notifications.AndroidImportance.HIGH,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}, [_])
|
}, [l])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleNotification = (payload?: NotificationPayload) => {
|
const handleNotification = (payload?: NotificationPayload) => {
|
||||||
@@ -237,7 +235,7 @@ export function useNotificationsHandler() {
|
|||||||
|
|
||||||
const account = accounts.find(a => a.did === payload.recipientDid)
|
const account = accounts.find(a => a.did === payload.recipientDid)
|
||||||
if (account) {
|
if (account) {
|
||||||
onPressSwitchAccount(account, 'Notification')
|
void onPressSwitchAccount(account, 'Notification')
|
||||||
} else {
|
} else {
|
||||||
setShowLoggedOut(true)
|
setShowLoggedOut(true)
|
||||||
}
|
}
|
||||||
@@ -305,10 +303,10 @@ export function useNotificationsHandler() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Notifications.setNotificationHandler({
|
Notifications.setNotificationHandler({
|
||||||
handleNotification: async e => {
|
handleNotification: e => {
|
||||||
const payload = getNotificationPayload(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})
|
logger.debug('useNotificationsHandler: incoming', {e, payload})
|
||||||
|
|
||||||
@@ -323,17 +321,17 @@ export function useNotificationsHandler() {
|
|||||||
payload.reason === 'chat-removed-from-group' ||
|
payload.reason === 'chat-removed-from-group' ||
|
||||||
payload.reason === 'chat-join-request-rejected' ||
|
payload.reason === 'chat-join-request-rejected' ||
|
||||||
payload.convoId !== currentConvoId
|
payload.convoId !== currentConvoId
|
||||||
return {
|
return Promise.resolve({
|
||||||
shouldShowList: shouldAlert,
|
shouldShowList: shouldAlert,
|
||||||
shouldShowBanner: shouldAlert,
|
shouldShowBanner: shouldAlert,
|
||||||
shouldPlaySound: false,
|
shouldPlaySound: false,
|
||||||
shouldSetBadge: false,
|
shouldSetBadge: false,
|
||||||
} satisfies Notifications.NotificationBehavior
|
} satisfies Notifications.NotificationBehavior)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Any notification other than a chat message should invalidate the unread page
|
// Any notification other than a chat message should invalidate the unread page
|
||||||
invalidateCachedUnreadPage()
|
invalidateCachedUnreadPage()
|
||||||
return DEFAULT_HANDLER_OPTIONS
|
return Promise.resolve(DEFAULT_HANDLER_OPTIONS)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -363,14 +361,14 @@ export function useNotificationsHandler() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
invalidateCachedUnreadPage()
|
invalidateCachedUnreadPage()
|
||||||
truncateAndInvalidate(queryClient, RQKEY_NOTIFS('all'))
|
void truncateAndInvalidate(queryClient, RQKEY_NOTIFS('all'))
|
||||||
|
|
||||||
if (
|
if (
|
||||||
payload.reason === 'mention' ||
|
payload.reason === 'mention' ||
|
||||||
payload.reason === 'quote' ||
|
payload.reason === 'quote' ||
|
||||||
payload.reason === 'reply'
|
payload.reason === 'reply'
|
||||||
) {
|
) {
|
||||||
truncateAndInvalidate(queryClient, RQKEY_NOTIFS('mentions'))
|
void truncateAndInvalidate(queryClient, RQKEY_NOTIFS('mentions'))
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.debug('Notifications: handleNotification', {
|
logger.debug('Notifications: handleNotification', {
|
||||||
@@ -379,7 +377,7 @@ export function useNotificationsHandler() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
handleNotification(payload)
|
handleNotification(payload)
|
||||||
Notifications.dismissAllNotificationsAsync()
|
void Notifications.dismissAllNotificationsAsync()
|
||||||
// Also clear the native `lastResponse` cache. Otherwise a subsequent
|
// Also clear the native `lastResponse` cache. Otherwise a subsequent
|
||||||
// `getLastNotificationResponse()` (e.g. on an account-switch remount,
|
// `getLastNotificationResponse()` (e.g. on an account-switch remount,
|
||||||
// which re-runs `handlePushNotificationEntry`) would replay this
|
// which re-runs `handlePushNotificationEntry`) would replay this
|
||||||
|
|||||||
@@ -1,13 +1,19 @@
|
|||||||
import {useCallback} from 'react'
|
import {useCallback} from 'react'
|
||||||
import {View} from 'react-native'
|
import {Pressable, View} from 'react-native'
|
||||||
import {Trans, useLingui} from '@lingui/react/macro'
|
import {Trans, useLingui} from '@lingui/react/macro'
|
||||||
import {type NativeStackScreenProps} from '@react-navigation/native-stack'
|
import {type NativeStackScreenProps} from '@react-navigation/native-stack'
|
||||||
|
|
||||||
import {type CommonNavigatorParams} from '#/lib/routes/types'
|
import {type CommonNavigatorParams} from '#/lib/routes/types'
|
||||||
import {useUpdateActorDeclaration} from '#/state/queries/messages/actor-declaration'
|
import {useUpdateActorDeclaration} from '#/state/queries/messages/actor-declaration'
|
||||||
|
import {
|
||||||
|
type NotificationSettingsPreference,
|
||||||
|
useChatNotificationSettingsQuery,
|
||||||
|
} from '#/state/queries/notifications/settings'
|
||||||
import {useProfileQuery} from '#/state/queries/profile'
|
import {useProfileQuery} from '#/state/queries/profile'
|
||||||
import {useSession} from '#/state/session'
|
import {useSession} from '#/state/session'
|
||||||
import {ExportCarDialog} from '#/screens/Settings/components/ExportCarDialog'
|
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 {atoms as a, useTheme} from '#/alf'
|
||||||
import {AgeRestrictedScreen} from '#/components/ageAssurance/AgeRestrictedScreen'
|
import {AgeRestrictedScreen} from '#/components/ageAssurance/AgeRestrictedScreen'
|
||||||
import {useAgeAssuranceCopy} from '#/components/ageAssurance/useAgeAssuranceCopy'
|
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 {Bell_Stroke2_Corner0_Rounded as BellIcon} from '#/components/icons/Bell'
|
||||||
import {Car_Stroke2_Corner2_Rounded as CarIcon} from '#/components/icons/Car'
|
import {Car_Stroke2_Corner2_Rounded as CarIcon} from '#/components/icons/Car'
|
||||||
import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRightIcon} from '#/components/icons/Chevron'
|
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 Layout from '#/components/Layout'
|
||||||
|
import * as Skele from '#/components/Skeleton'
|
||||||
import * as Toast from '#/components/Toast'
|
import * as Toast from '#/components/Toast'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
import {useAgeAssurance} from '#/ageAssurance'
|
import {useAgeAssurance} from '#/ageAssurance'
|
||||||
@@ -52,8 +61,12 @@ export function MessagesSettingsScreenInner({}: Props) {
|
|||||||
const {data: profile} = useProfileQuery({
|
const {data: profile} = useProfileQuery({
|
||||||
did: currentAccount!.did,
|
did: currentAccount!.did,
|
||||||
})
|
})
|
||||||
|
const {data: chatNotificationSettings, isError: chatSettingsError} =
|
||||||
|
useChatNotificationSettingsQuery()
|
||||||
const {preferences, setPref} = useBackgroundNotificationPreferences()
|
const {preferences, setPref} = useBackgroundNotificationPreferences()
|
||||||
|
|
||||||
|
const chatDialogControl = Dialog.useDialogControl()
|
||||||
|
const chatRequestDialogControl = Dialog.useDialogControl()
|
||||||
const exportCarControl = Dialog.useDialogControl()
|
const exportCarControl = Dialog.useDialogControl()
|
||||||
|
|
||||||
const isGroupChatEnabled = !ax.features.enabled(ax.features.GroupChatsDisable)
|
const isGroupChatEnabled = !ax.features.enabled(ax.features.GroupChatsDisable)
|
||||||
@@ -237,6 +250,54 @@ export function MessagesSettingsScreenInner({}: Props) {
|
|||||||
<Divider style={{marginVertical: 10}} />
|
<Divider style={{marginVertical: 10}} />
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : 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 && (
|
{IS_NATIVE && (
|
||||||
<>
|
<>
|
||||||
<View style={[a.px_xl]}>
|
<View style={[a.px_xl]}>
|
||||||
@@ -263,12 +324,12 @@ export function MessagesSettingsScreenInner({}: Props) {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<View style={[a.px_xl]}>
|
<View style={[a.px_xl]}>
|
||||||
<Toggle.Item
|
<Pressable
|
||||||
label={l`Export my chat data`}
|
accessibilityRole="button"
|
||||||
name="playSoundChat"
|
accessibilityLabel={l`Export my chat data`}
|
||||||
value={preferences.playSoundChat}
|
accessibilityHint={undefined}
|
||||||
style={[a.flex_row, a.align_center, a.justify_between]}
|
style={[a.flex_row, a.align_center, a.justify_between, a.gap_sm]}
|
||||||
onChange={() => {
|
onPress={() => {
|
||||||
exportCarControl.open()
|
exportCarControl.open()
|
||||||
}}>
|
}}>
|
||||||
<CarIcon style={[a.mr_2xs, t.atoms.text]} size="lg" />
|
<CarIcon style={[a.mr_2xs, t.atoms.text]} size="lg" />
|
||||||
@@ -277,12 +338,46 @@ export function MessagesSettingsScreenInner({}: Props) {
|
|||||||
<Trans>Export my chat data</Trans>
|
<Trans>Export my chat data</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
<ChevronRightIcon style={[a.ml_2xs, t.atoms.text]} size="lg" />
|
<ChevronRightIcon style={[a.ml_2xs, t.atoms.text]} size="lg" />
|
||||||
</Toggle.Item>
|
</Pressable>
|
||||||
</View>
|
</View>
|
||||||
<Divider style={{marginVertical: 10}} />
|
<Divider style={{marginVertical: 10}} />
|
||||||
</View>
|
</View>
|
||||||
</Layout.Content>
|
</Layout.Content>
|
||||||
|
<ChatNotificationDialogs
|
||||||
|
chatControl={chatDialogControl}
|
||||||
|
chatRequestControl={chatRequestDialogControl}
|
||||||
|
/>
|
||||||
<ExportCarDialog control={exportCarControl} />
|
<ExportCarDialog control={exportCarControl} />
|
||||||
</Layout.Screen>
|
</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 {useMemo} from 'react'
|
||||||
import {View} from 'react-native'
|
import {View} from 'react-native'
|
||||||
import {type AppBskyNotificationDefs} from '@atproto/api'
|
|
||||||
import {Trans, useLingui} from '@lingui/react/macro'
|
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 {atoms as a, platform, useTheme} from '#/alf'
|
||||||
import * as Toggle from '#/components/forms/Toggle'
|
import * as Toggle from '#/components/forms/Toggle'
|
||||||
import {Loader} from '#/components/Loader'
|
import {Loader} from '#/components/Loader'
|
||||||
@@ -17,16 +20,13 @@ export function PreferenceControls({
|
|||||||
preference,
|
preference,
|
||||||
allowDisableInApp = true,
|
allowDisableInApp = true,
|
||||||
}: {
|
}: {
|
||||||
name: Exclude<keyof AppBskyNotificationDefs.Preferences, '$type'>
|
name: NotificationSettingsPreferenceName
|
||||||
/**
|
/**
|
||||||
* Keep other prefs in sync with `name`. For use in the "everything else" category
|
* 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.
|
* which groups starterpack joins + verified + unverified notifications into a single toggle.
|
||||||
*/
|
*/
|
||||||
syncOthers?: Exclude<keyof AppBskyNotificationDefs.Preferences, '$type'>[]
|
syncOthers?: NotificationSettingsPreferenceName[]
|
||||||
preference?:
|
preference?: NotificationSettingsPreference
|
||||||
| AppBskyNotificationDefs.Preference
|
|
||||||
| AppBskyNotificationDefs.FilterablePreference
|
|
||||||
| AppBskyNotificationDefs.ChatPreference
|
|
||||||
allowDisableInApp?: boolean
|
allowDisableInApp?: boolean
|
||||||
}) {
|
}) {
|
||||||
if (!preference)
|
if (!preference)
|
||||||
@@ -52,12 +52,9 @@ export function Inner({
|
|||||||
preference,
|
preference,
|
||||||
allowDisableInApp,
|
allowDisableInApp,
|
||||||
}: {
|
}: {
|
||||||
name: Exclude<keyof AppBskyNotificationDefs.Preferences, '$type'>
|
name: NotificationSettingsPreferenceName
|
||||||
syncOthers?: Exclude<keyof AppBskyNotificationDefs.Preferences, '$type'>[]
|
syncOthers?: NotificationSettingsPreferenceName[]
|
||||||
preference:
|
preference: NotificationSettingsPreference
|
||||||
| AppBskyNotificationDefs.Preference
|
|
||||||
| AppBskyNotificationDefs.FilterablePreference
|
|
||||||
| AppBskyNotificationDefs.ChatPreference
|
|
||||||
allowDisableInApp: boolean
|
allowDisableInApp: boolean
|
||||||
}) {
|
}) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
@@ -95,7 +92,7 @@ export function Inner({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const onChangeFilter = ([change]: string[]) => {
|
const onChangeFilter = ([change]: string[]) => {
|
||||||
if (change !== 'all' && change !== 'follows' && change !== 'accepted')
|
if (change !== 'all' && change !== 'follows')
|
||||||
throw new Error('Invalid filter')
|
throw new Error('Invalid filter')
|
||||||
|
|
||||||
const newPreference = {
|
const newPreference = {
|
||||||
@@ -135,7 +132,7 @@ export function Inner({
|
|||||||
</Toggle.LabelText>
|
</Toggle.LabelText>
|
||||||
<Toggle.Platform />
|
<Toggle.Platform />
|
||||||
</Toggle.Item>
|
</Toggle.Item>
|
||||||
{allowDisableInApp && (
|
{allowDisableInApp && 'list' in preference && (
|
||||||
<Toggle.Item
|
<Toggle.Item
|
||||||
label={l`Receive in-app notifications`}
|
label={l`Receive in-app notifications`}
|
||||||
name="list"
|
name="list"
|
||||||
@@ -176,31 +173,17 @@ export function Inner({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Toggle.Item>
|
</Toggle.Item>
|
||||||
{name === 'chat' ? (
|
<Toggle.Item
|
||||||
<Toggle.Item
|
highlightRow
|
||||||
highlightRow
|
label={l`People I follow`}
|
||||||
label={l`Accepted conversations`}
|
name="follows">
|
||||||
name="accepted">
|
{({selected}) => (
|
||||||
{({selected}) => (
|
<Toggle.RadioWithLabel
|
||||||
<Toggle.RadioWithLabel
|
label={l`People I follow`}
|
||||||
label={l`Accepted conversations`}
|
selected={selected}
|
||||||
selected={selected}
|
/>
|
||||||
/>
|
)}
|
||||||
)}
|
</Toggle.Item>
|
||||||
</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>
|
</View>
|
||||||
</Toggle.Group>
|
</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 {useEffect} from 'react'
|
||||||
import {Linking, View} from 'react-native'
|
import {Linking, View} from 'react-native'
|
||||||
import * as Notification from 'expo-notifications'
|
import * as Notification from 'expo-notifications'
|
||||||
import {type AppBskyNotificationDefs} from '@atproto/api'
|
|
||||||
import {Trans, useLingui} from '@lingui/react/macro'
|
import {Trans, useLingui} from '@lingui/react/macro'
|
||||||
import {useQuery, useQueryClient} from '@tanstack/react-query'
|
import {useQuery, useQueryClient} from '@tanstack/react-query'
|
||||||
|
|
||||||
@@ -10,7 +9,10 @@ import {
|
|||||||
type AllNavigatorParams,
|
type AllNavigatorParams,
|
||||||
type NativeStackScreenProps,
|
type NativeStackScreenProps,
|
||||||
} from '#/lib/routes/types'
|
} 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 {atoms as a} from '#/alf'
|
||||||
import {Admonition} from '#/components/Admonition'
|
import {Admonition} from '#/components/Admonition'
|
||||||
import * as Dialog from '#/components/Dialog'
|
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 {At_Stroke2_Corner2_Rounded as AtIcon} from '#/components/icons/At'
|
||||||
import {BellRinging_Stroke2_Corner0_Rounded as BellRingingIcon} from '#/components/icons/BellRinging'
|
import {BellRinging_Stroke2_Corner0_Rounded as BellRingingIcon} from '#/components/icons/BellRinging'
|
||||||
import {Bubble_Stroke2_Corner2_Rounded as BubbleIcon} from '#/components/icons/Bubble'
|
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 {Haptic_Stroke2_Corner2_Rounded as HapticIcon} from '#/components/icons/Haptic'
|
||||||
import {
|
import {
|
||||||
Heart2_Stroke2_Corner0_Rounded as HeartIcon,
|
Heart2_Stroke2_Corner0_Rounded as HeartIcon,
|
||||||
LikeRepost_Stroke2_Corner2_Rounded as LikeRepostIcon,
|
LikeRepost_Stroke2_Corner2_Rounded as LikeRepostIcon,
|
||||||
} from '#/components/icons/Heart2'
|
} 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 {PersonPlus_Stroke2_Corner2_Rounded as PersonPlusIcon} from '#/components/icons/Person'
|
||||||
import {CloseQuote_Stroke2_Corner0_Rounded as CloseQuoteIcon} from '#/components/icons/Quote'
|
import {CloseQuote_Stroke2_Corner0_Rounded as CloseQuoteIcon} from '#/components/icons/Quote'
|
||||||
import {
|
import {
|
||||||
@@ -33,7 +37,9 @@ import {Shapes_Stroke2_Corner0_Rounded as ShapesIcon} from '#/components/icons/S
|
|||||||
import * as Layout from '#/components/Layout'
|
import * as Layout from '#/components/Layout'
|
||||||
import {IS_ANDROID, IS_IOS, IS_WEB} from '#/env'
|
import {IS_ANDROID, IS_IOS, IS_WEB} from '#/env'
|
||||||
import * as SettingsList from '../components/SettingsList'
|
import * as SettingsList from '../components/SettingsList'
|
||||||
|
import {ChatNotificationDialogs} from './components/ChatNotificationDialogs'
|
||||||
import {ItemTextWithSubtitle} from './components/ItemTextWithSubtitle'
|
import {ItemTextWithSubtitle} from './components/ItemTextWithSubtitle'
|
||||||
|
import {SettingPreview} from './components/SettingPreview'
|
||||||
|
|
||||||
const RQKEY = ['notification-permissions']
|
const RQKEY = ['notification-permissions']
|
||||||
|
|
||||||
@@ -42,6 +48,8 @@ export function NotificationSettingsScreen({}: Props) {
|
|||||||
const {t: l} = useLingui()
|
const {t: l} = useLingui()
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const {data: settings, isError} = useNotificationSettingsQuery()
|
const {data: settings, isError} = useNotificationSettingsQuery()
|
||||||
|
const {data: chatSettings, isError: chatError} =
|
||||||
|
useChatNotificationSettingsQuery()
|
||||||
|
|
||||||
const likeDialogControl = Dialog.useDialogControl()
|
const likeDialogControl = Dialog.useDialogControl()
|
||||||
const followDialogControl = Dialog.useDialogControl()
|
const followDialogControl = Dialog.useDialogControl()
|
||||||
@@ -52,6 +60,8 @@ export function NotificationSettingsScreen({}: Props) {
|
|||||||
const activityDialogControl = Dialog.useDialogControl()
|
const activityDialogControl = Dialog.useDialogControl()
|
||||||
const likeRepostDialogControl = Dialog.useDialogControl()
|
const likeRepostDialogControl = Dialog.useDialogControl()
|
||||||
const repostRepostDialogControl = Dialog.useDialogControl()
|
const repostRepostDialogControl = Dialog.useDialogControl()
|
||||||
|
const chatDialogControl = Dialog.useDialogControl()
|
||||||
|
const chatRequestDialogControl = Dialog.useDialogControl()
|
||||||
const miscDialogControl = Dialog.useDialogControl()
|
const miscDialogControl = Dialog.useDialogControl()
|
||||||
|
|
||||||
const {data: permissions, refetch} = useQuery({
|
const {data: permissions, refetch} = useQuery({
|
||||||
@@ -234,6 +244,40 @@ export function NotificationSettingsScreen({}: Props) {
|
|||||||
showSkeleton={!settings}
|
showSkeleton={!settings}
|
||||||
/>
|
/>
|
||||||
</SettingsList.PressableItem>
|
</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
|
<SettingsList.PressableItem
|
||||||
label={l`Settings for notifications for everything else`}
|
label={l`Settings for notifications for everything else`}
|
||||||
onPress={miscDialogControl.open}
|
onPress={miscDialogControl.open}
|
||||||
@@ -333,6 +377,10 @@ export function NotificationSettingsScreen({}: Props) {
|
|||||||
<Trans>Get notifications when people repost your reposts.</Trans>
|
<Trans>Get notifications when people repost your reposts.</Trans>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<ChatNotificationDialogs
|
||||||
|
chatControl={chatDialogControl}
|
||||||
|
chatRequestControl={chatRequestDialogControl}
|
||||||
|
/>
|
||||||
<NotificationSettingsDialog
|
<NotificationSettingsDialog
|
||||||
control={miscDialogControl}
|
control={miscDialogControl}
|
||||||
name="starterpackJoined"
|
name="starterpackJoined"
|
||||||
@@ -350,46 +398,3 @@ export function NotificationSettingsScreen({}: Props) {
|
|||||||
</Layout.Screen>
|
</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 {t} from '@lingui/core/macro'
|
||||||
import {
|
import {
|
||||||
type QueryClient,
|
type QueryClient,
|
||||||
@@ -7,12 +10,59 @@ import {
|
|||||||
useQueryClient,
|
useQueryClient,
|
||||||
} from '@tanstack/react-query'
|
} from '@tanstack/react-query'
|
||||||
|
|
||||||
|
import {DM_SERVICE_HEADERS} from '#/lib/constants'
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {useAgent} from '#/state/session'
|
import {useAgent} from '#/state/session'
|
||||||
import * as Toast from '#/components/Toast'
|
import * as Toast from '#/components/Toast'
|
||||||
|
|
||||||
const RQKEY_ROOT = 'notification-settings'
|
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({
|
export function useNotificationSettingsQuery({
|
||||||
enabled,
|
enabled,
|
||||||
@@ -20,10 +70,27 @@ export function useNotificationSettingsQuery({
|
|||||||
const agent = useAgent()
|
const agent = useAgent()
|
||||||
|
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: RQKEY,
|
queryKey: RQKEY_APP,
|
||||||
queryFn: async () => {
|
queryFn: async (): Promise<AppNotificationSettingsPreferences> => {
|
||||||
const response = await agent.app.bsky.notification.getPreferences()
|
const res = await agent.app.bsky.notification.getPreferences()
|
||||||
return response.data.preferences
|
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,
|
enabled,
|
||||||
})
|
})
|
||||||
@@ -33,19 +100,27 @@ export function useNotificationSettingsUpdateMutation() {
|
|||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: async (
|
mutationFn: async (update: NotificationSettingsUpdate) => {
|
||||||
update: Partial<AppBskyNotificationDefs.Preferences>,
|
const {appUpdate, chatUpdate} = splitNotificationSettingsUpdate(update)
|
||||||
) => {
|
await Promise.all([
|
||||||
const response =
|
hasUpdates(appUpdate)
|
||||||
await agent.app.bsky.notification.putPreferencesV2(update)
|
? agent.app.bsky.notification.putPreferencesV2(appUpdate)
|
||||||
return response.data.preferences
|
: undefined,
|
||||||
|
hasUpdates(chatUpdate)
|
||||||
|
? agent.chat.bsky.notification.putPreferences(chatUpdate, {
|
||||||
|
headers: DM_SERVICE_HEADERS,
|
||||||
|
encoding: 'application/json',
|
||||||
|
})
|
||||||
|
: undefined,
|
||||||
|
])
|
||||||
},
|
},
|
||||||
onMutate: update => {
|
onMutate: update => {
|
||||||
optimisticUpdateNotificationSettings(queryClient, update)
|
optimisticUpdateNotificationSettings(queryClient, update)
|
||||||
},
|
},
|
||||||
onError: e => {
|
onError: e => {
|
||||||
logger.error('Could not update notification settings', {message: 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`, {
|
Toast.show(t`Could not update notification settings`, {
|
||||||
type: 'error',
|
type: 'error',
|
||||||
})
|
})
|
||||||
@@ -55,13 +130,62 @@ export function useNotificationSettingsUpdateMutation() {
|
|||||||
|
|
||||||
function optimisticUpdateNotificationSettings(
|
function optimisticUpdateNotificationSettings(
|
||||||
queryClient: QueryClient,
|
queryClient: QueryClient,
|
||||||
update: Partial<AppBskyNotificationDefs.Preferences>,
|
update: NotificationSettingsUpdate,
|
||||||
) {
|
) {
|
||||||
queryClient.setQueryData(
|
const {appUpdate, chatUpdate} = splitNotificationSettingsUpdate(update)
|
||||||
RQKEY,
|
|
||||||
(old?: AppBskyNotificationDefs.Preferences) => {
|
if (hasUpdates(appUpdate)) {
|
||||||
if (!old) return old
|
queryClient.setQueryData(
|
||||||
return {...old, ...update}
|
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