From 21989b558bd074bf84ac08c174d7a411fda1ffb7 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Tue, 17 Jun 2025 12:37:14 +0300 Subject: [PATCH] Granular notification settings (#8484) * add mockup screen * add notification index screen * add redirect screen * upgrade sdk * new icons * add new screens * make router typesafe, finish adding screens * add routes to go server * load settings * push notif settings * improve web * fix lockfile lint * no $type on preferences * prompt to enable push notifications * fix reply prefs * space out options * fix copy error * Update RepostsOnRepostsNotificationSettings.tsx * only send minimal diff to putPrefs * fix yarn.lock * Update Navigation.tsx Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> * Update src/screens/Settings/NotificationSettings/index.tsx Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> * add description to `syncOthers` --------- Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> --- .../bellRinging_stroke2_corner0_rounded.svg | 1 + .../likeRepost_stroke2_corner2_rounded.svg | 1 + .../phoneHaptic_stroke2_corner2_rounded.svg | 1 + .../repostRepost_stroke2_corner2_rounded.svg | 1 + bskyweb/cmd/bskyweb/server.go | 11 + package.json | 2 +- src/Navigation.tsx | 99 +++++- src/components/icons/BellRinging.tsx | 5 + src/components/icons/Heart2.tsx | 4 + src/components/icons/Phone.tsx | 4 + src/components/icons/Repost.tsx | 4 + src/lib/routes/router.ts | 6 +- src/lib/routes/types.ts | 24 +- src/routes.ts | 26 +- src/screens/Messages/Settings.tsx | 4 +- ...ngsInterests.tsx => InterestsSettings.tsx} | 5 +- .../Settings/LegacyNotificationSettings.tsx | 21 ++ src/screens/Settings/NotificationSettings.tsx | 98 ------ .../LikeNotificationSettings.tsx | 60 ++++ .../LikesOnRepostsNotificationSettings.tsx | 65 ++++ .../MentionNotificationSettings.tsx | 63 ++++ .../MiscellaneousNotificationSettings.tsx | 68 ++++ .../NewFollowerNotificationSettings.tsx | 63 ++++ .../QuoteNotificationSettings.tsx | 60 ++++ .../ReplyNotificationSettings.tsx | 66 ++++ .../RepostNotificationSettings.tsx | 63 ++++ .../RepostsOnRepostsNotificationSettings.tsx | 66 ++++ .../components/ItemTextWithSubtitle.tsx | 34 ++ .../components/PreferenceControls.tsx | 194 ++++++++++++ .../Settings/NotificationSettings/index.tsx | 293 ++++++++++++++++++ src/screens/Settings/Settings.tsx | 9 + src/state/queries/notifications/settings.ts | 99 +++--- src/view/screens/Notifications.tsx | 2 +- yarn.lock | 199 ++++++------ 34 files changed, 1434 insertions(+), 287 deletions(-) create mode 100644 assets/icons/bellRinging_stroke2_corner0_rounded.svg create mode 100644 assets/icons/likeRepost_stroke2_corner2_rounded.svg create mode 100644 assets/icons/phoneHaptic_stroke2_corner2_rounded.svg create mode 100644 assets/icons/repostRepost_stroke2_corner2_rounded.svg create mode 100644 src/components/icons/BellRinging.tsx rename src/screens/Settings/{SettingsInterests.tsx => InterestsSettings.tsx} (96%) create mode 100644 src/screens/Settings/LegacyNotificationSettings.tsx delete mode 100644 src/screens/Settings/NotificationSettings.tsx create mode 100644 src/screens/Settings/NotificationSettings/LikeNotificationSettings.tsx create mode 100644 src/screens/Settings/NotificationSettings/LikesOnRepostsNotificationSettings.tsx create mode 100644 src/screens/Settings/NotificationSettings/MentionNotificationSettings.tsx create mode 100644 src/screens/Settings/NotificationSettings/MiscellaneousNotificationSettings.tsx create mode 100644 src/screens/Settings/NotificationSettings/NewFollowerNotificationSettings.tsx create mode 100644 src/screens/Settings/NotificationSettings/QuoteNotificationSettings.tsx create mode 100644 src/screens/Settings/NotificationSettings/ReplyNotificationSettings.tsx create mode 100644 src/screens/Settings/NotificationSettings/RepostNotificationSettings.tsx create mode 100644 src/screens/Settings/NotificationSettings/RepostsOnRepostsNotificationSettings.tsx create mode 100644 src/screens/Settings/NotificationSettings/components/ItemTextWithSubtitle.tsx create mode 100644 src/screens/Settings/NotificationSettings/components/PreferenceControls.tsx create mode 100644 src/screens/Settings/NotificationSettings/index.tsx diff --git a/assets/icons/bellRinging_stroke2_corner0_rounded.svg b/assets/icons/bellRinging_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..d49a59a01a --- /dev/null +++ b/assets/icons/bellRinging_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/likeRepost_stroke2_corner2_rounded.svg b/assets/icons/likeRepost_stroke2_corner2_rounded.svg new file mode 100644 index 0000000000..f5d2da35bc --- /dev/null +++ b/assets/icons/likeRepost_stroke2_corner2_rounded.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/phoneHaptic_stroke2_corner2_rounded.svg b/assets/icons/phoneHaptic_stroke2_corner2_rounded.svg new file mode 100644 index 0000000000..ebcf89b46e --- /dev/null +++ b/assets/icons/phoneHaptic_stroke2_corner2_rounded.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/repostRepost_stroke2_corner2_rounded.svg b/assets/icons/repostRepost_stroke2_corner2_rounded.svg new file mode 100644 index 0000000000..caec8c1029 --- /dev/null +++ b/assets/icons/repostRepost_stroke2_corner2_rounded.svg @@ -0,0 +1 @@ + diff --git a/bskyweb/cmd/bskyweb/server.go b/bskyweb/cmd/bskyweb/server.go index f419212cc9..ef796920d6 100644 --- a/bskyweb/cmd/bskyweb/server.go +++ b/bskyweb/cmd/bskyweb/server.go @@ -278,6 +278,17 @@ func serve(cctx *cli.Context) error { e.GET("/settings/content-and-media", server.WebGeneric) e.GET("/settings/interests", server.WebGeneric) e.GET("/settings/about", server.WebGeneric) + e.GET("/settings/notifications", server.WebGeneric) + e.GET("/settings/notifications/replies", server.WebGeneric) + e.GET("/settings/notifications/mentions", server.WebGeneric) + e.GET("/settings/notifications/quotes", server.WebGeneric) + e.GET("/settings/notifications/likes", server.WebGeneric) + e.GET("/settings/notifications/reposts", server.WebGeneric) + e.GET("/settings/notifications/new-followers", server.WebGeneric) + e.GET("/settings/notifications/likes-on-reposts", server.WebGeneric) + e.GET("/settings/notifications/reposts-on-reposts", server.WebGeneric) + e.GET("/settings/notifications/activity", server.WebGeneric) + e.GET("/settings/notifications/miscellaneous", server.WebGeneric) e.GET("/settings/app-icon", server.WebGeneric) e.GET("/sys/debug", server.WebGeneric) e.GET("/sys/debug-mod", server.WebGeneric) diff --git a/package.json b/package.json index 691a6e94d0..fe38554141 100644 --- a/package.json +++ b/package.json @@ -218,7 +218,7 @@ "zod": "^3.20.2" }, "devDependencies": { - "@atproto/dev-env": "^0.3.133", + "@atproto/dev-env": "^0.3.142", "@babel/core": "^7.26.0", "@babel/preset-env": "^7.26.0", "@babel/runtime": "^7.26.0", diff --git a/src/Navigation.tsx b/src/Navigation.tsx index 2f26c09711..3bf1ace852 100644 --- a/src/Navigation.tsx +++ b/src/Navigation.tsx @@ -90,11 +90,10 @@ import {AppPasswordsScreen} from '#/screens/Settings/AppPasswords' import {ContentAndMediaSettingsScreen} from '#/screens/Settings/ContentAndMediaSettings' import {ExternalMediaPreferencesScreen} from '#/screens/Settings/ExternalMediaPreferences' import {FollowingFeedPreferencesScreen} from '#/screens/Settings/FollowingFeedPreferences' +import {InterestsSettingsScreen} from '#/screens/Settings/InterestsSettings' import {LanguageSettingsScreen} from '#/screens/Settings/LanguageSettings' -import {NotificationSettingsScreen} from '#/screens/Settings/NotificationSettings' import {PrivacyAndSecuritySettingsScreen} from '#/screens/Settings/PrivacyAndSecuritySettings' import {SettingsScreen} from '#/screens/Settings/Settings' -import {SettingsInterests} from '#/screens/Settings/SettingsInterests' import {ThreadPreferencesScreen} from '#/screens/Settings/ThreadPreferences' import { StarterPackScreen, @@ -110,6 +109,17 @@ import { } from '#/components/dialogs/EmailDialog' import {router} from '#/routes' import {Referrer} from '../modules/expo-bluesky-swiss-army' +import {LegacyNotificationSettingsScreen} from './screens/Settings/LegacyNotificationSettings' +import {NotificationSettingsScreen} from './screens/Settings/NotificationSettings' +import {LikeNotificationSettingsScreen} from './screens/Settings/NotificationSettings/LikeNotificationSettings' +import {LikesOnRepostsNotificationSettingsScreen} from './screens/Settings/NotificationSettings/LikesOnRepostsNotificationSettings' +import {MentionNotificationSettingsScreen} from './screens/Settings/NotificationSettings/MentionNotificationSettings' +import {MiscellaneousNotificationSettingsScreen} from './screens/Settings/NotificationSettings/MiscellaneousNotificationSettings' +import {NewFollowerNotificationSettingsScreen} from './screens/Settings/NotificationSettings/NewFollowerNotificationSettings' +import {QuoteNotificationSettingsScreen} from './screens/Settings/NotificationSettings/QuoteNotificationSettings' +import {ReplyNotificationSettingsScreen} from './screens/Settings/NotificationSettings/ReplyNotificationSettings' +import {RepostNotificationSettingsScreen} from './screens/Settings/NotificationSettings/RepostNotificationSettings' +import {RepostsOnRepostsNotificationSettingsScreen} from './screens/Settings/NotificationSettings/RepostsOnRepostsNotificationSettings' const navigationRef = createNavigationContainerRef() @@ -380,6 +390,83 @@ function commonScreens(Stack: typeof Flat, unreadCountLabel?: string) { requireAuth: true, }} /> + NotificationSettingsScreen} + options={{title: title(msg`Notification settings`), requireAuth: true}} + /> + ReplyNotificationSettingsScreen} + options={{ + title: title(msg`Reply notifications`), + requireAuth: true, + }} + /> + MentionNotificationSettingsScreen} + options={{ + title: title(msg`Mention notifications`), + requireAuth: true, + }} + /> + QuoteNotificationSettingsScreen} + options={{ + title: title(msg`Quote notifications`), + requireAuth: true, + }} + /> + LikeNotificationSettingsScreen} + options={{ + title: title(msg`Like notifications`), + requireAuth: true, + }} + /> + RepostNotificationSettingsScreen} + options={{ + title: title(msg`Repost notifications`), + requireAuth: true, + }} + /> + NewFollowerNotificationSettingsScreen} + options={{ + title: title(msg`New follower notifications`), + requireAuth: true, + }} + /> + LikesOnRepostsNotificationSettingsScreen} + options={{ + title: title(msg`Likes on your reposts notifications`), + requireAuth: true, + }} + /> + RepostsOnRepostsNotificationSettingsScreen} + options={{ + title: title(msg`Reposts on your reposts notifications`), + requireAuth: true, + }} + /> + MiscellaneousNotificationSettingsScreen} + options={{ + title: title(msg`Miscellaneous notifications`), + requireAuth: true, + }} + /> ContentAndMediaSettingsScreen} @@ -389,8 +476,8 @@ function commonScreens(Stack: typeof Flat, unreadCountLabel?: string) { }} /> SettingsInterests} + name="InterestsSettings" + getComponent={() => InterestsSettingsScreen} options={{ title: title(msg`Your interests`), requireAuth: true, @@ -438,8 +525,8 @@ function commonScreens(Stack: typeof Flat, unreadCountLabel?: string) { options={{title: title(msg`Chat request inbox`), requireAuth: true}} /> NotificationSettingsScreen} + name="LegacyNotificationSettings" + getComponent={() => LegacyNotificationSettingsScreen} options={{title: title(msg`Notification settings`), requireAuth: true}} /> > { routes: [string, Route][] = [] - constructor(description: Record) { + constructor(description: Record) { for (const [screen, pattern] of Object.entries(description)) { if (typeof pattern === 'string') { this.routes.push([screen, createRoute(pattern)]) @@ -14,7 +14,7 @@ export class Router { } } - matchName(name: string): Route | undefined { + matchName(name: keyof T | (string & {})): Route | undefined { for (const [screenName, route] of this.routes) { if (screenName === name) { return route diff --git a/src/lib/routes/types.ts b/src/lib/routes/types.ts index f587423900..c92be34c23 100644 --- a/src/lib/routes/types.ts +++ b/src/lib/routes/types.ts @@ -52,7 +52,18 @@ export type CommonNavigatorParams = { AccountSettings: undefined PrivacyAndSecuritySettings: undefined ContentAndMediaSettings: undefined - SettingsInterests: undefined + NotificationSettings: undefined + ReplyNotificationSettings: undefined + MentionNotificationSettings: undefined + QuoteNotificationSettings: undefined + LikeNotificationSettings: undefined + RepostNotificationSettings: undefined + NewFollowerNotificationSettings: undefined + LikesOnRepostsNotificationSettings: undefined + RepostsOnRepostsNotificationSettings: undefined + ActivityNotificationSettings: undefined + MiscellaneousNotificationSettings: undefined + InterestsSettings: undefined AboutSettings: undefined AppIconSettings: undefined Search: {q?: string} @@ -61,7 +72,7 @@ export type CommonNavigatorParams = { MessagesConversation: {conversation: string; embed?: string; accept?: true} MessagesSettings: undefined MessagesInbox: undefined - NotificationSettings: undefined + LegacyNotificationSettings: undefined Feeds: undefined Start: {name: string; rkey: string} StarterPack: {name: string; rkey: string; new?: boolean} @@ -104,8 +115,6 @@ export type FlatNavigatorParams = CommonNavigatorParams & { Search: {q?: string} Feeds: undefined Notifications: undefined - Hashtag: {tag: string; author?: string} - Topic: {topic: string} Messages: {pushToConversation?: string; animation?: 'push' | 'pop'} } @@ -118,15 +127,8 @@ export type AllNavigatorParams = CommonNavigatorParams & { NotificationsTab: undefined Notifications: undefined MyProfileTab: undefined - Hashtag: {tag: string; author?: string} - Topic: {topic: string} MessagesTab: undefined Messages: {animation?: 'push' | 'pop'} - Start: {name: string; rkey: string} - StarterPack: {name: string; rkey: string; new?: boolean} - StarterPackShort: {code: string} - StarterPackWizard: undefined - StarterPackEdit: {rkey?: string} } // NOTE diff --git a/src/routes.ts b/src/routes.ts index 60bb65dd5a..b66a0ae53f 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -1,11 +1,17 @@ import {Router} from '#/lib/routes/router' +import {type FlatNavigatorParams} from './lib/routes/types' -export const router = new Router({ +type AllNavigatableRoutes = Omit< + FlatNavigatorParams, + 'NotFound' | 'SharedPreferencesTester' +> + +export const router = new Router({ Home: '/', Search: '/search', Feeds: '/feeds', Notifications: '/notifications', - NotificationSettings: '/notifications/settings', + LegacyNotificationSettings: '/notifications/settings', Settings: '/settings', Lists: '/lists', // moderation @@ -42,13 +48,25 @@ export const router = new Router({ AccessibilitySettings: '/settings/accessibility', AppearanceSettings: '/settings/appearance', SavedFeeds: '/settings/saved-feeds', - // new settings AccountSettings: '/settings/account', PrivacyAndSecuritySettings: '/settings/privacy-and-security', ContentAndMediaSettings: '/settings/content-and-media', - SettingsInterests: '/settings/interests', + InterestsSettings: '/settings/interests', AboutSettings: '/settings/about', AppIconSettings: '/settings/app-icon', + NotificationSettings: '/settings/notifications', + ReplyNotificationSettings: '/settings/notifications/replies', + MentionNotificationSettings: '/settings/notifications/mentions', + QuoteNotificationSettings: '/settings/notifications/quotes', + LikeNotificationSettings: '/settings/notifications/likes', + RepostNotificationSettings: '/settings/notifications/reposts', + NewFollowerNotificationSettings: '/settings/notifications/new-followers', + LikesOnRepostsNotificationSettings: + '/settings/notifications/likes-on-reposts', + RepostsOnRepostsNotificationSettings: + '/settings/notifications/reposts-on-reposts', + ActivityNotificationSettings: '/settings/notifications/activity', + MiscellaneousNotificationSettings: '/settings/notifications/miscellaneous', // support Support: '/support', PrivacyPolicy: '/support/privacy', diff --git a/src/screens/Messages/Settings.tsx b/src/screens/Messages/Settings.tsx index f37e7a9ba1..0b8c88b9dd 100644 --- a/src/screens/Messages/Settings.tsx +++ b/src/screens/Messages/Settings.tsx @@ -2,9 +2,9 @@ import {useCallback} from 'react' import {View} from 'react-native' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' -import {NativeStackScreenProps} from '@react-navigation/native-stack' +import {type NativeStackScreenProps} from '@react-navigation/native-stack' -import {CommonNavigatorParams} from '#/lib/routes/types' +import {type CommonNavigatorParams} from '#/lib/routes/types' import {isNative} from '#/platform/detection' import {useUpdateActorDeclaration} from '#/state/queries/messages/actor-declaration' import {useProfileQuery} from '#/state/queries/profile' diff --git a/src/screens/Settings/SettingsInterests.tsx b/src/screens/Settings/InterestsSettings.tsx similarity index 96% rename from src/screens/Settings/SettingsInterests.tsx rename to src/screens/Settings/InterestsSettings.tsx index 42259e9b68..746315f7b8 100644 --- a/src/screens/Settings/SettingsInterests.tsx +++ b/src/screens/Settings/InterestsSettings.tsx @@ -2,9 +2,11 @@ import {useMemo, useState} from 'react' import {type TextStyle, View, type ViewStyle} from 'react-native' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' +import {type NativeStackScreenProps} from '@react-navigation/native-stack' import {useQueryClient} from '@tanstack/react-query' import debounce from 'lodash.debounce' +import {type CommonNavigatorParams} from '#/lib/routes/types' import { preferencesQueryKey, usePreferencesQuery, @@ -24,7 +26,8 @@ import * as Layout from '#/components/Layout' import {Loader} from '#/components/Loader' import {Text} from '#/components/Typography' -export function SettingsInterests() { +type Props = NativeStackScreenProps +export function InterestsSettingsScreen({}: Props) { const t = useTheme() const gutters = useGutters(['base']) const {data: preferences} = usePreferencesQuery() diff --git a/src/screens/Settings/LegacyNotificationSettings.tsx b/src/screens/Settings/LegacyNotificationSettings.tsx new file mode 100644 index 0000000000..a9ef5d9831 --- /dev/null +++ b/src/screens/Settings/LegacyNotificationSettings.tsx @@ -0,0 +1,21 @@ +import {useCallback} from 'react' +import {useFocusEffect} from '@react-navigation/native' + +import { + type AllNavigatorParams, + type NativeStackScreenProps, +} from '#/lib/routes/types' + +type Props = NativeStackScreenProps< + AllNavigatorParams, + 'LegacyNotificationSettings' +> +export function LegacyNotificationSettingsScreen({navigation}: Props) { + useFocusEffect( + useCallback(() => { + navigation.replace('NotificationSettings') + }, [navigation]), + ) + + return null +} diff --git a/src/screens/Settings/NotificationSettings.tsx b/src/screens/Settings/NotificationSettings.tsx deleted file mode 100644 index ebb230c2ca..0000000000 --- a/src/screens/Settings/NotificationSettings.tsx +++ /dev/null @@ -1,98 +0,0 @@ -import {Text} from 'react-native' -import {msg, Trans} from '@lingui/macro' -import {useLingui} from '@lingui/react' - -import {AllNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types' -import {useNotificationFeedQuery} from '#/state/queries/notifications/feed' -import {useNotificationSettingsMutation} from '#/state/queries/notifications/settings' -import {atoms as a} from '#/alf' -import {Admonition} from '#/components/Admonition' -import {Error} from '#/components/Error' -import * as Toggle from '#/components/forms/Toggle' -import {Beaker_Stroke2_Corner2_Rounded as BeakerIcon} from '#/components/icons/Beaker' -import * as Layout from '#/components/Layout' -import {Loader} from '#/components/Loader' -import * as SettingsList from './components/SettingsList' - -type Props = NativeStackScreenProps -export function NotificationSettingsScreen({}: Props) { - const {_} = useLingui() - - const { - data, - isError: isQueryError, - refetch, - } = useNotificationFeedQuery({ - filter: 'all', - }) - const serverPriority = data?.pages.at(0)?.priority - - const { - mutate: onChangePriority, - isPending: isMutationPending, - variables, - } = useNotificationSettingsMutation() - - const priority = isMutationPending - ? variables[0] === 'enabled' - : serverPriority - - return ( - - - - - - Notification Settings - - - - - - {isQueryError ? ( - - ) : ( - - - - - Notification filters - - - - - Enable priority notifications - - {!data ? : } - - - - - - - Experimental: When this - preference is enabled, you'll only receive reply and quote - notifications from users you follow. We'll continue to add - more controls here over time. - - - - - )} - - - ) -} diff --git a/src/screens/Settings/NotificationSettings/LikeNotificationSettings.tsx b/src/screens/Settings/NotificationSettings/LikeNotificationSettings.tsx new file mode 100644 index 0000000000..f726ab558a --- /dev/null +++ b/src/screens/Settings/NotificationSettings/LikeNotificationSettings.tsx @@ -0,0 +1,60 @@ +import {View} from 'react-native' +import {Trans} from '@lingui/macro' + +import { + type AllNavigatorParams, + type NativeStackScreenProps, +} from '#/lib/routes/types' +import {useNotificationSettingsQuery} from '#/state/queries/notifications/settings' +import {atoms as a} from '#/alf' +import {Admonition} from '#/components/Admonition' +import {Heart2_Stroke2_Corner0_Rounded as HeartIcon} from '#/components/icons/Heart2' +import * as Layout from '#/components/Layout' +import * as SettingsList from '../components/SettingsList' +import {ItemTextWithSubtitle} from './components/ItemTextWithSubtitle' +import {PreferenceControls} from './components/PreferenceControls' + +type Props = NativeStackScreenProps< + AllNavigatorParams, + 'LikeNotificationSettings' +> +export function LikeNotificationSettingsScreen({}: Props) { + const {data: preferences, isError} = useNotificationSettingsQuery() + + return ( + + + + + + Notifications + + + + + + + + + Likes} + subtitleText={ + Get notifications when people like your posts. + } + /> + + {isError ? ( + + + Failed to load notification settings. + + + ) : ( + + )} + + + + ) +} diff --git a/src/screens/Settings/NotificationSettings/LikesOnRepostsNotificationSettings.tsx b/src/screens/Settings/NotificationSettings/LikesOnRepostsNotificationSettings.tsx new file mode 100644 index 0000000000..08a05d468f --- /dev/null +++ b/src/screens/Settings/NotificationSettings/LikesOnRepostsNotificationSettings.tsx @@ -0,0 +1,65 @@ +import {View} from 'react-native' +import {Trans} from '@lingui/macro' + +import { + type AllNavigatorParams, + type NativeStackScreenProps, +} from '#/lib/routes/types' +import {useNotificationSettingsQuery} from '#/state/queries/notifications/settings' +import {atoms as a} from '#/alf' +import {Admonition} from '#/components/Admonition' +import {LikeRepost_Stroke2_Corner2_Rounded as LikeRepostIcon} from '#/components/icons/Heart2' +import * as Layout from '#/components/Layout' +import * as SettingsList from '../components/SettingsList' +import {ItemTextWithSubtitle} from './components/ItemTextWithSubtitle' +import {PreferenceControls} from './components/PreferenceControls' + +type Props = NativeStackScreenProps< + AllNavigatorParams, + 'LikesOnRepostsNotificationSettings' +> +export function LikesOnRepostsNotificationSettingsScreen({}: Props) { + const {data: preferences, isError} = useNotificationSettingsQuery() + + return ( + + + + + + Notifications + + + + + + + + + Likes on your reposts} + subtitleText={ + + Get notifications when people like posts that you've reposted. + + } + /> + + {isError ? ( + + + Failed to load notification settings. + + + ) : ( + + )} + + + + ) +} diff --git a/src/screens/Settings/NotificationSettings/MentionNotificationSettings.tsx b/src/screens/Settings/NotificationSettings/MentionNotificationSettings.tsx new file mode 100644 index 0000000000..0a770157e9 --- /dev/null +++ b/src/screens/Settings/NotificationSettings/MentionNotificationSettings.tsx @@ -0,0 +1,63 @@ +import {View} from 'react-native' +import {Trans} from '@lingui/macro' + +import { + type AllNavigatorParams, + type NativeStackScreenProps, +} from '#/lib/routes/types' +import {useNotificationSettingsQuery} from '#/state/queries/notifications/settings' +import {atoms as a} from '#/alf' +import {Admonition} from '#/components/Admonition' +import {At_Stroke2_Corner2_Rounded as AtIcon} from '#/components/icons/At' +import * as Layout from '#/components/Layout' +import * as SettingsList from '../components/SettingsList' +import {ItemTextWithSubtitle} from './components/ItemTextWithSubtitle' +import {PreferenceControls} from './components/PreferenceControls' + +type Props = NativeStackScreenProps< + AllNavigatorParams, + 'MentionNotificationSettings' +> +export function MentionNotificationSettingsScreen({}: Props) { + const {data: preferences, isError} = useNotificationSettingsQuery() + + return ( + + + + + + Notifications + + + + + + + + + Mentions} + subtitleText={ + Get notifications when people mention you. + } + /> + + {isError ? ( + + + Failed to load notification settings. + + + ) : ( + + )} + + + + ) +} diff --git a/src/screens/Settings/NotificationSettings/MiscellaneousNotificationSettings.tsx b/src/screens/Settings/NotificationSettings/MiscellaneousNotificationSettings.tsx new file mode 100644 index 0000000000..a0fe65ecf6 --- /dev/null +++ b/src/screens/Settings/NotificationSettings/MiscellaneousNotificationSettings.tsx @@ -0,0 +1,68 @@ +import {View} from 'react-native' +import {Trans} from '@lingui/macro' + +import { + type AllNavigatorParams, + type NativeStackScreenProps, +} from '#/lib/routes/types' +import {useNotificationSettingsQuery} from '#/state/queries/notifications/settings' +import {atoms as a} from '#/alf' +import {Admonition} from '#/components/Admonition' +import {Shapes_Stroke2_Corner0_Rounded as ShapesIcon} from '#/components/icons/Shapes' +import * as Layout from '#/components/Layout' +import * as SettingsList from '../components/SettingsList' +import {ItemTextWithSubtitle} from './components/ItemTextWithSubtitle' +import {PreferenceControls} from './components/PreferenceControls' + +type Props = NativeStackScreenProps< + AllNavigatorParams, + 'MiscellaneousNotificationSettings' +> +export function MiscellaneousNotificationSettingsScreen({}: Props) { + const {data: preferences, isError} = useNotificationSettingsQuery() + + return ( + + + + + + Notifications + + + + + + + + + Everything else} + subtitleText={ + + Notifications for everything else, such as when someone joins + via one of your starter packs. + + } + /> + + {isError ? ( + + + Failed to load notification settings. + + + ) : ( + + )} + + + + ) +} diff --git a/src/screens/Settings/NotificationSettings/NewFollowerNotificationSettings.tsx b/src/screens/Settings/NotificationSettings/NewFollowerNotificationSettings.tsx new file mode 100644 index 0000000000..dd603a52f5 --- /dev/null +++ b/src/screens/Settings/NotificationSettings/NewFollowerNotificationSettings.tsx @@ -0,0 +1,63 @@ +import {View} from 'react-native' +import {Trans} from '@lingui/macro' + +import { + type AllNavigatorParams, + type NativeStackScreenProps, +} from '#/lib/routes/types' +import {useNotificationSettingsQuery} from '#/state/queries/notifications/settings' +import {atoms as a} from '#/alf' +import {Admonition} from '#/components/Admonition' +import {PersonPlus_Stroke2_Corner2_Rounded as PersonPlusIcon} from '#/components/icons/Person' +import * as Layout from '#/components/Layout' +import * as SettingsList from '../components/SettingsList' +import {ItemTextWithSubtitle} from './components/ItemTextWithSubtitle' +import {PreferenceControls} from './components/PreferenceControls' + +type Props = NativeStackScreenProps< + AllNavigatorParams, + 'NewFollowerNotificationSettings' +> +export function NewFollowerNotificationSettingsScreen({}: Props) { + const {data: preferences, isError} = useNotificationSettingsQuery() + + return ( + + + + + + Notifications + + + + + + + + + New followers} + subtitleText={ + Get notifications when people follow you. + } + /> + + {isError ? ( + + + Failed to load notification settings. + + + ) : ( + + )} + + + + ) +} diff --git a/src/screens/Settings/NotificationSettings/QuoteNotificationSettings.tsx b/src/screens/Settings/NotificationSettings/QuoteNotificationSettings.tsx new file mode 100644 index 0000000000..afb3df90f5 --- /dev/null +++ b/src/screens/Settings/NotificationSettings/QuoteNotificationSettings.tsx @@ -0,0 +1,60 @@ +import {View} from 'react-native' +import {Trans} from '@lingui/macro' + +import { + type AllNavigatorParams, + type NativeStackScreenProps, +} from '#/lib/routes/types' +import {useNotificationSettingsQuery} from '#/state/queries/notifications/settings' +import {atoms as a} from '#/alf' +import {Admonition} from '#/components/Admonition' +import {CloseQuote_Stroke2_Corner0_Rounded as CloseQuoteIcon} from '#/components/icons/Quote' +import * as Layout from '#/components/Layout' +import * as SettingsList from '../components/SettingsList' +import {ItemTextWithSubtitle} from './components/ItemTextWithSubtitle' +import {PreferenceControls} from './components/PreferenceControls' + +type Props = NativeStackScreenProps< + AllNavigatorParams, + 'QuoteNotificationSettings' +> +export function QuoteNotificationSettingsScreen({}: Props) { + const {data: preferences, isError} = useNotificationSettingsQuery() + + return ( + + + + + + Notifications + + + + + + + + + Quotes} + subtitleText={ + Get notifications when people quote your posts. + } + /> + + {isError ? ( + + + Failed to load notification settings. + + + ) : ( + + )} + + + + ) +} diff --git a/src/screens/Settings/NotificationSettings/ReplyNotificationSettings.tsx b/src/screens/Settings/NotificationSettings/ReplyNotificationSettings.tsx new file mode 100644 index 0000000000..b3e7c6cff2 --- /dev/null +++ b/src/screens/Settings/NotificationSettings/ReplyNotificationSettings.tsx @@ -0,0 +1,66 @@ +import {View} from 'react-native' +import {Trans} from '@lingui/macro' + +import { + type AllNavigatorParams, + type NativeStackScreenProps, +} from '#/lib/routes/types' +import {useNotificationSettingsQuery} from '#/state/queries/notifications/settings' +import {atoms as a} from '#/alf' +import {Admonition} from '#/components/Admonition' +import {Bubble_Stroke2_Corner2_Rounded as BubbleIcon} from '#/components/icons/Bubble' +import * as Layout from '#/components/Layout' +import * as SettingsList from '../components/SettingsList' +import {ItemTextWithSubtitle} from './components/ItemTextWithSubtitle' +import {PreferenceControls} from './components/PreferenceControls' + +type Props = NativeStackScreenProps< + AllNavigatorParams, + 'ReplyNotificationSettings' +> +export function ReplyNotificationSettingsScreen({}: Props) { + const {data: preferences, isError} = useNotificationSettingsQuery() + + return ( + + + + + + Notifications + + + + + + + + + Replies} + subtitleText={ + + Get notifications when people reply to your posts. + + } + /> + + {isError ? ( + + + Failed to load notification settings. + + + ) : ( + + )} + + + + ) +} diff --git a/src/screens/Settings/NotificationSettings/RepostNotificationSettings.tsx b/src/screens/Settings/NotificationSettings/RepostNotificationSettings.tsx new file mode 100644 index 0000000000..aa9e4e32fa --- /dev/null +++ b/src/screens/Settings/NotificationSettings/RepostNotificationSettings.tsx @@ -0,0 +1,63 @@ +import {View} from 'react-native' +import {Trans} from '@lingui/macro' + +import { + type AllNavigatorParams, + type NativeStackScreenProps, +} from '#/lib/routes/types' +import {useNotificationSettingsQuery} from '#/state/queries/notifications/settings' +import {atoms as a} from '#/alf' +import {Admonition} from '#/components/Admonition' +import {Repost_Stroke2_Corner2_Rounded as RepostIcon} from '#/components/icons/Repost' +import * as Layout from '#/components/Layout' +import * as SettingsList from '../components/SettingsList' +import {ItemTextWithSubtitle} from './components/ItemTextWithSubtitle' +import {PreferenceControls} from './components/PreferenceControls' + +type Props = NativeStackScreenProps< + AllNavigatorParams, + 'RepostNotificationSettings' +> +export function RepostNotificationSettingsScreen({}: Props) { + const {data: preferences, isError} = useNotificationSettingsQuery() + + return ( + + + + + + Notifications + + + + + + + + + Reposts} + subtitleText={ + Get notifications when people repost your posts. + } + /> + + {isError ? ( + + + Failed to load notification settings. + + + ) : ( + + )} + + + + ) +} diff --git a/src/screens/Settings/NotificationSettings/RepostsOnRepostsNotificationSettings.tsx b/src/screens/Settings/NotificationSettings/RepostsOnRepostsNotificationSettings.tsx new file mode 100644 index 0000000000..13fec61682 --- /dev/null +++ b/src/screens/Settings/NotificationSettings/RepostsOnRepostsNotificationSettings.tsx @@ -0,0 +1,66 @@ +import {View} from 'react-native' +import {Trans} from '@lingui/macro' + +import { + type AllNavigatorParams, + type NativeStackScreenProps, +} from '#/lib/routes/types' +import {useNotificationSettingsQuery} from '#/state/queries/notifications/settings' +import {atoms as a} from '#/alf' +import {Admonition} from '#/components/Admonition' +import {RepostRepost_Stroke2_Corner2_Rounded as RepostRepostIcon} from '#/components/icons/Repost' +import * as Layout from '#/components/Layout' +import * as SettingsList from '../components/SettingsList' +import {ItemTextWithSubtitle} from './components/ItemTextWithSubtitle' +import {PreferenceControls} from './components/PreferenceControls' + +type Props = NativeStackScreenProps< + AllNavigatorParams, + 'RepostsOnRepostsNotificationSettings' +> +export function RepostsOnRepostsNotificationSettingsScreen({}: Props) { + const {data: preferences, isError} = useNotificationSettingsQuery() + + return ( + + + + + + Notifications + + + + + + + + + Reposts of your reposts} + subtitleText={ + + Get notifications when people repost posts that you've + reposted. + + } + /> + + {isError ? ( + + + Failed to load notification settings. + + + ) : ( + + )} + + + + ) +} diff --git a/src/screens/Settings/NotificationSettings/components/ItemTextWithSubtitle.tsx b/src/screens/Settings/NotificationSettings/components/ItemTextWithSubtitle.tsx new file mode 100644 index 0000000000..217fc33b95 --- /dev/null +++ b/src/screens/Settings/NotificationSettings/components/ItemTextWithSubtitle.tsx @@ -0,0 +1,34 @@ +import {View} from 'react-native' + +import {atoms as a, useTheme} from '#/alf' +import * as Skele from '#/components/Skeleton' +import {Text} from '#/components/Typography' +import * as SettingsList from '../../components/SettingsList' + +export function ItemTextWithSubtitle({ + titleText, + subtitleText, + bold = false, + showSkeleton = false, +}: { + titleText: React.ReactNode + subtitleText: React.ReactNode + bold?: boolean + showSkeleton?: boolean +}) { + const t = useTheme() + return ( + + + {titleText} + + {showSkeleton ? ( + + ) : ( + + {subtitleText} + + )} + + ) +} diff --git a/src/screens/Settings/NotificationSettings/components/PreferenceControls.tsx b/src/screens/Settings/NotificationSettings/components/PreferenceControls.tsx new file mode 100644 index 0000000000..336e086950 --- /dev/null +++ b/src/screens/Settings/NotificationSettings/components/PreferenceControls.tsx @@ -0,0 +1,194 @@ +import {useMemo} from 'react' +import {View} from 'react-native' +import {type AppBskyNotificationDefs} from '@atproto/api' +import {type FilterablePreference} from '@atproto/api/dist/client/types/app/bsky/notification/defs' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {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' +import {Text} from '#/components/Typography' +import {Divider} from '../../components/SettingsList' + +export function PreferenceControls({ + name, + syncOthers, + preference, + allowDisableInApp = true, +}: { + name: Exclude + /** + * 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[] + preference?: AppBskyNotificationDefs.Preference | FilterablePreference + allowDisableInApp?: boolean +}) { + if (!preference) + return ( + + + + ) + + return ( + + ) +} + +export function Inner({ + name, + syncOthers = [], + preference, + allowDisableInApp, +}: { + name: Exclude + syncOthers?: Exclude[] + preference: AppBskyNotificationDefs.Preference | FilterablePreference + allowDisableInApp: boolean +}) { + const t = useTheme() + const {_} = useLingui() + const {mutate} = useNotificationSettingsUpdateMutation() + + const channels = useMemo(() => { + const arr = [] + if (preference.list) arr.push('list') + if (preference.push) arr.push('push') + return arr + }, [preference]) + + const onChangeChannels = (change: string[]) => { + const newPreference = { + ...preference, + list: change.includes('list'), + push: change.includes('push'), + } satisfies typeof preference + + mutate({ + [name]: newPreference, + ...Object.fromEntries(syncOthers.map(key => [key, newPreference])), + }) + } + + const onChangeFilter = ([change]: string[]) => { + if (change !== 'all' && change !== 'follows') + throw new Error('Invalid filter') + + const newPreference = { + ...preference, + filter: change, + } satisfies typeof preference + + mutate({ + [name]: newPreference, + ...Object.fromEntries(syncOthers.map(key => [key, newPreference])), + }) + } + + return ( + + + + + + Push notifications + + + + {allowDisableInApp && ( + + + In-app notifications + + + + )} + + + {'filter' in preference && ( + <> + + From + + + + + 0 && t.atoms.text, + a.font_normal, + a.text_md, + ]}> + Everyone + + + + + 0 && t.atoms.text, + a.font_normal, + a.text_md, + ]}> + People I follow + + + + + + )} + + ) +} diff --git a/src/screens/Settings/NotificationSettings/index.tsx b/src/screens/Settings/NotificationSettings/index.tsx new file mode 100644 index 0000000000..a4f6dede05 --- /dev/null +++ b/src/screens/Settings/NotificationSettings/index.tsx @@ -0,0 +1,293 @@ +import {useEffect} from 'react' +import {Linking, View} from 'react-native' +import * as Notification from 'expo-notifications' +import {type AppBskyNotificationDefs} from '@atproto/api' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' +import {useQuery, useQueryClient} from '@tanstack/react-query' + +import {useAppState} from '#/lib/hooks/useAppState' +import { + type AllNavigatorParams, + type NativeStackScreenProps, +} from '#/lib/routes/types' +import {isAndroid, isIOS, isWeb} from '#/platform/detection' +import {useNotificationSettingsQuery} from '#/state/queries/notifications/settings' +import {atoms as a} from '#/alf' +import {Admonition} from '#/components/Admonition' +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 {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 {PersonPlus_Stroke2_Corner2_Rounded as PersonPlusIcon} from '#/components/icons/Person' +import {CloseQuote_Stroke2_Corner0_Rounded as CloseQuoteIcon} from '#/components/icons/Quote' +import { + Repost_Stroke2_Corner2_Rounded as RepostIcon, + RepostRepost_Stroke2_Corner2_Rounded as RepostRepostIcon, +} from '#/components/icons/Repost' +import {Shapes_Stroke2_Corner0_Rounded as ShapesIcon} from '#/components/icons/Shapes' +import * as Layout from '#/components/Layout' +import * as SettingsList from '../components/SettingsList' +import {ItemTextWithSubtitle} from './components/ItemTextWithSubtitle' + +const RQKEY = ['notification-permissions'] + +type Props = NativeStackScreenProps +export function NotificationSettingsScreen({}: Props) { + const {_} = useLingui() + const queryClient = useQueryClient() + const {data: settings, isError} = useNotificationSettingsQuery() + + const {data: permissions, refetch} = useQuery({ + queryKey: RQKEY, + queryFn: async () => { + if (isWeb) return null + return await Notification.getPermissionsAsync() + }, + }) + + const appState = useAppState() + useEffect(() => { + if (appState === 'active') { + refetch() + } + }, [appState, refetch]) + + const onRequestPermissions = async () => { + if (isWeb) return + if (permissions?.canAskAgain) { + const response = await Notification.requestPermissionsAsync() + queryClient.setQueryData(RQKEY, response) + } else { + if (isAndroid) { + try { + await Linking.sendIntent( + 'android.settings.APP_NOTIFICATION_SETTINGS', + [ + { + key: 'android.provider.extra.APP_PACKAGE', + value: 'xyz.blueskyweb.app', + }, + ], + ) + } catch { + Linking.openSettings() + } + } else if (isIOS) { + Linking.openSettings() + } + } + } + + return ( + + + + + + Notifications + + + + + + + {permissions && !permissions.granted && ( + <> + + + + Enable push notifications + + + + + )} + {isError && ( + + + Failed to load notification settings. + + + )} + + + + Replies} + subtitleText={} + showSkeleton={!settings} + /> + + + + Mentions} + subtitleText={} + showSkeleton={!settings} + /> + + + + Quotes} + subtitleText={} + showSkeleton={!settings} + /> + + + + Likes} + subtitleText={} + showSkeleton={!settings} + /> + + + + Reposts} + subtitleText={} + showSkeleton={!settings} + /> + + + + New followers} + subtitleText={} + showSkeleton={!settings} + /> + + {/* + + + Activity alerts} + subtitleText={ + + } + showSkeleton={!settings} + /> + */} + + + Likes on your reposts} + subtitleText={ + + } + showSkeleton={!settings} + /> + + + + Reposts of your reposts} + subtitleText={ + + } + showSkeleton={!settings} + /> + + + + Everything else} + // technically a bundle of several settings, but since they're set together + // and are most likely in sync we'll just show the state of one of them + subtitleText={ + + } + showSkeleton={!settings} + /> + + + + + + ) +} + +function SettingPreview({ + preference, +}: { + preference?: + | AppBskyNotificationDefs.Preference + | AppBskyNotificationDefs.FilterablePreference +}) { + const {_} = useLingui() + if (!preference) { + return null + } else { + if ('filter' in preference) { + if (preference.filter === 'all') { + if (preference.list && preference.push) { + return _(msg`In-app, Push, Everyone`) + } else if (preference.list) { + return _(msg`In-app, Everyone`) + } else if (preference.push) { + return _(msg`Push, Everyone`) + } + } else if (preference.filter === 'follows') { + if (preference.list && preference.push) { + return _(msg`In-app, Push, People you follow`) + } else if (preference.list) { + return _(msg`In-app, People you follow`) + } else if (preference.push) { + return _(msg`Push, People you follow`) + } + } + } else { + if (preference.list && preference.push) { + return _(msg`In-app, Push`) + } else if (preference.list) { + return _(msg`In-app`) + } else if (preference.push) { + return _(msg`Push`) + } + } + } + + return _(msg`Off`) +} diff --git a/src/screens/Settings/Settings.tsx b/src/screens/Settings/Settings.tsx index 9f36c27acc..6310c7c3c8 100644 --- a/src/screens/Settings/Settings.tsx +++ b/src/screens/Settings/Settings.tsx @@ -36,6 +36,7 @@ import {AvatarStackWithFetch} from '#/components/AvatarStack' import {useDialogControl} from '#/components/Dialog' import {SwitchAccountDialog} from '#/components/dialogs/SwitchAccount' import {Accessibility_Stroke2_Corner2_Rounded as AccessibilityIcon} from '#/components/icons/Accessibility' +import {Bell_Stroke2_Corner0_Rounded as NotificationIcon} from '#/components/icons/Bell' import {BubbleInfo_Stroke2_Corner2_Rounded as BubbleInfoIcon} from '#/components/icons/BubbleInfo' import {ChevronTop_Stroke2_Corner0_Rounded as ChevronUpIcon} from '#/components/icons/Chevron' import {CircleQuestion_Stroke2_Corner2_Rounded as CircleQuestionIcon} from '#/components/icons/CircleQuestion' @@ -180,6 +181,14 @@ export function SettingsScreen({}: Props) { Moderation + + + + Notifications + + diff --git a/src/state/queries/notifications/settings.ts b/src/state/queries/notifications/settings.ts index 2ac42aa328..9661bed1be 100644 --- a/src/state/queries/notifications/settings.ts +++ b/src/state/queries/notifications/settings.ts @@ -1,72 +1,63 @@ -import {msg} from '@lingui/macro' -import {useLingui} from '@lingui/react' -import {useMutation, useQueryClient} from '@tanstack/react-query' +import {type AppBskyNotificationDefs} from '@atproto/api' +import {t} from '@lingui/macro' +import { + type QueryClient, + useMutation, + useQuery, + useQueryClient, +} from '@tanstack/react-query' -import {until} from '#/lib/async/until' import {logger} from '#/logger' -import {RQKEY as RQKEY_NOTIFS} from '#/state/queries/notifications/feed' -import {invalidateCachedUnreadPage} from '#/state/queries/notifications/unread' import {useAgent} from '#/state/session' import * as Toast from '#/view/com/util/Toast' -export function useNotificationSettingsMutation() { - const {_} = useLingui() +const RQKEY_ROOT = 'notification-settings' +const RQKEY = [RQKEY_ROOT] + +export function useNotificationSettingsQuery() { + const agent = useAgent() + + return useQuery({ + queryKey: RQKEY, + queryFn: async () => { + const response = await agent.app.bsky.notification.getPreferences() + return response.data.preferences + }, + }) +} +export function useNotificationSettingsUpdateMutation() { const agent = useAgent() const queryClient = useQueryClient() return useMutation({ - mutationFn: async (keys: string[]) => { - const enabled = keys[0] === 'enabled' - - await agent.api.app.bsky.notification.putPreferences({ - priority: enabled, - }) - - await until( - 5, // 5 tries - 1e3, // 1s delay between tries - res => res.data.priority === enabled, - () => agent.api.app.bsky.notification.listNotifications({limit: 1}), + mutationFn: async ( + update: Partial, + ) => { + const response = await agent.app.bsky.notification.putPreferencesV2( + update, ) - - eagerlySetCachedPriority(queryClient, enabled) + return response.data.preferences }, - onError: err => { - logger.error('Failed to save notification preferences', { - safeMessage: err, - }) - Toast.show( - _(msg`Failed to save notification preferences, please try again`), - 'xmark', - ) + onMutate: update => { + optimisticUpdateNotificationSettings(queryClient, update) }, - onSuccess: () => { - Toast.show(_(msg({message: 'Preference saved', context: 'toast'}))) - }, - onSettled: () => { - invalidateCachedUnreadPage() - queryClient.invalidateQueries({queryKey: RQKEY_NOTIFS('all')}) - queryClient.invalidateQueries({queryKey: RQKEY_NOTIFS('mentions')}) + onError: e => { + logger.error('Could not update notification settings', {message: e}) + queryClient.invalidateQueries({queryKey: RQKEY}) + Toast.show(t`Could not update notification settings`, 'xmark') }, }) } -function eagerlySetCachedPriority( - queryClient: ReturnType, - enabled: boolean, +function optimisticUpdateNotificationSettings( + queryClient: QueryClient, + update: Partial, ) { - function updateData(old: any) { - if (!old) return old - return { - ...old, - pages: old.pages.map((page: any) => { - return { - ...page, - priority: enabled, - } - }), - } - } - queryClient.setQueryData(RQKEY_NOTIFS('all'), updateData) - queryClient.setQueryData(RQKEY_NOTIFS('mentions'), updateData) + queryClient.setQueryData( + RQKEY, + (old?: AppBskyNotificationDefs.Preferences) => { + if (!old) return old + return {...old, ...update} + }, + ) } diff --git a/src/view/screens/Notifications.tsx b/src/view/screens/Notifications.tsx index ace0de2aed..528d6be870 100644 --- a/src/view/screens/Notifications.tsx +++ b/src/view/screens/Notifications.tsx @@ -130,7 +130,7 @@ export function NotificationsScreen({}: Props) {