From ce4efc511b57788780eb00ec485bf80f7c1c0de4 Mon Sep 17 00:00:00 2001 From: DS Boyce <260543580+ds-boyce@users.noreply.github.com> Date: Fri, 29 May 2026 09:35:14 -0700 Subject: [PATCH] Refactor notification settings as dialogs (#10646) Co-authored-by: Samuel Newman --- bskyweb/cmd/bskyweb/server.go | 10 - src/Navigation.tsx | 101 +------ src/analytics/metrics/types.ts | 2 +- .../dialogs/NotificationSettingsDialog.tsx | 105 +++++++ src/lib/routes/types.ts | 10 - src/routes.ts | 12 - src/screens/Messages/Settings.tsx | 11 +- .../ActivityNotificationSettings.tsx | 269 ------------------ .../LikeNotificationSettings.tsx | 60 ---- .../LikesOnRepostsNotificationSettings.tsx | 65 ----- .../MentionNotificationSettings.tsx | 63 ---- .../MiscellaneousNotificationSettings.tsx | 68 ----- .../NewFollowerNotificationSettings.tsx | 63 ---- .../QuoteNotificationSettings.tsx | 60 ---- .../ReplyNotificationSettings.tsx | 62 ---- .../RepostNotificationSettings.tsx | 63 ---- .../RepostsOnRepostsNotificationSettings.tsx | 66 ----- .../components/PreferenceControls.tsx | 94 +++--- .../Settings/NotificationSettings/index.tsx | 230 ++++++++++----- .../Settings/components/ExportCarDialog.tsx | 4 +- src/state/queries/notifications/settings.ts | 2 +- 21 files changed, 338 insertions(+), 1082 deletions(-) create mode 100644 src/components/dialogs/NotificationSettingsDialog.tsx delete mode 100644 src/screens/Settings/NotificationSettings/ActivityNotificationSettings.tsx delete mode 100644 src/screens/Settings/NotificationSettings/LikeNotificationSettings.tsx delete mode 100644 src/screens/Settings/NotificationSettings/LikesOnRepostsNotificationSettings.tsx delete mode 100644 src/screens/Settings/NotificationSettings/MentionNotificationSettings.tsx delete mode 100644 src/screens/Settings/NotificationSettings/MiscellaneousNotificationSettings.tsx delete mode 100644 src/screens/Settings/NotificationSettings/NewFollowerNotificationSettings.tsx delete mode 100644 src/screens/Settings/NotificationSettings/QuoteNotificationSettings.tsx delete mode 100644 src/screens/Settings/NotificationSettings/ReplyNotificationSettings.tsx delete mode 100644 src/screens/Settings/NotificationSettings/RepostNotificationSettings.tsx delete mode 100644 src/screens/Settings/NotificationSettings/RepostsOnRepostsNotificationSettings.tsx diff --git a/bskyweb/cmd/bskyweb/server.go b/bskyweb/cmd/bskyweb/server.go index 6edae1bebe..44e79bd3fb 100644 --- a/bskyweb/cmd/bskyweb/server.go +++ b/bskyweb/cmd/bskyweb/server.go @@ -308,16 +308,6 @@ func serve(cctx *cli.Context) error { 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("/sys/debug", server.WebGeneric) e.GET("/sys/debug-mod", server.WebGeneric) e.GET("/sys/log", server.WebGeneric) diff --git a/src/Navigation.tsx b/src/Navigation.tsx index 5f8d715f53..6284aff34b 100644 --- a/src/Navigation.tsx +++ b/src/Navigation.tsx @@ -114,16 +114,6 @@ import {InterestsSettingsScreen} from '#/screens/Settings/InterestsSettings' import {LanguageSettingsScreen} from '#/screens/Settings/LanguageSettings' import {LegacyNotificationSettingsScreen} from '#/screens/Settings/LegacyNotificationSettings' import {NotificationSettingsScreen} from '#/screens/Settings/NotificationSettings' -import {ActivityNotificationSettingsScreen} from '#/screens/Settings/NotificationSettings/ActivityNotificationSettings' -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' import {PrivacyAndSecuritySettingsScreen} from '#/screens/Settings/PrivacyAndSecuritySettings' import {SettingsScreen} from '#/screens/Settings/Settings' import {ThreadPreferencesScreen} from '#/screens/Settings/ThreadPreferences' @@ -444,86 +434,6 @@ function commonScreens(Stack: typeof Flat, unreadCountLabel?: string) { getComponent={() => 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 of your reposts notifications`), - requireAuth: true, - }} - /> - RepostsOnRepostsNotificationSettingsScreen} - options={{ - title: title(msg`Reposts of your reposts notifications`), - requireAuth: true, - }} - /> - ActivityNotificationSettingsScreen} - options={{ - title: title(msg`Activity notifications`), - requireAuth: true, - }} - /> - MiscellaneousNotificationSettingsScreen} - options={{ - title: title(msg`Miscellaneous notifications`), - requireAuth: true, - }} - /> ContentAndMediaSettingsScreen} @@ -916,6 +826,7 @@ const LINKING = { function RoutesContainer({children}: React.PropsWithChildren<{}>) { const ax = useAnalytics() + // eslint-disable-next-line react-compiler/react-compiler const notyLogger = ax.logger.useChild(ax.logger.Context.Notifications) const theme = useColorSchemeStyle(DefaultTheme, DarkTheme) const {currentAccount, accounts} = useSession() @@ -943,7 +854,7 @@ function RoutesContainer({children}: React.PropsWithChildren<{}>) { const account = accounts.find(a => a.did === payload.recipientDid) if (account) { - onPressSwitchAccount(account, 'Notification') + void onPressSwitchAccount(account, 'Notification') } else { setShowLoggedOut(true) } @@ -955,7 +866,7 @@ function RoutesContainer({children}: React.PropsWithChildren<{}>) { // chat-added-to-group routes to the convo because the recipient was // just added and now has access. // @ts-expect-error nested navigators aren't typed -sfn - navigate('MessagesTab', { + void navigate('MessagesTab', { screen: 'Messages', params: { pushToConversation: payload.convoId, @@ -1012,7 +923,7 @@ function RoutesContainer({children}: React.PropsWithChildren<{}>) { } else if (path) { const [screen, params] = router.matchPath(path) // @ts-expect-error nested navigators aren't typed -sfn - navigate('HomeTab', {screen, params}) + void navigate('HomeTab', {screen, params}) notyLogger.debug(`handlePushNotificationEntry: navigate`, { screen, params, @@ -1118,7 +1029,7 @@ function navigate( } navigationRef.addListener('state', handler) - // @ts-ignore I dont know what would make typescript happy but I have a life -prf + // @ts-ignore I don't know what would make typescript happy but I have a life -prf navigationRef.navigate(name, params) }), timeout(1e3), @@ -1131,7 +1042,7 @@ function resetToTab( tabName: 'HomeTab' | 'SearchTab' | 'MessagesTab' | 'NotificationsTab', ) { if (navigationRef.isReady()) { - navigate(tabName) + void navigate(tabName) if (navigationRef.canGoBack()) { navigationRef.dispatch(StackActions.popToTop()) //we need to check .canGoBack() before calling it } diff --git a/src/analytics/metrics/types.ts b/src/analytics/metrics/types.ts index 7c9f1bffb9..90dbc4f983 100644 --- a/src/analytics/metrics/types.ts +++ b/src/analytics/metrics/types.ts @@ -1017,7 +1017,7 @@ export type Events = { 'activityPreference:changeChannels': { name: string push: boolean - list: boolean + list?: boolean } 'activityPreference:changeFilter': { name: string diff --git a/src/components/dialogs/NotificationSettingsDialog.tsx b/src/components/dialogs/NotificationSettingsDialog.tsx new file mode 100644 index 0000000000..c62f6a7d98 --- /dev/null +++ b/src/components/dialogs/NotificationSettingsDialog.tsx @@ -0,0 +1,105 @@ +import {View} from 'react-native' +import {type AppBskyNotificationDefs} from '@atproto/api' +import {Trans, useLingui} from '@lingui/react/macro' + +import {useNotificationSettingsQuery} from '#/state/queries/notifications/settings' +import * as SettingsList from '#/screens/Settings/components/SettingsList' +import {PreferenceControls} from '#/screens/Settings/NotificationSettings/components/PreferenceControls' +import {atoms as a, useTheme, web} from '#/alf' +import {Admonition} from '#/components/Admonition' +import {Button, ButtonText} from '#/components/Button' +import * as Dialog from '#/components/Dialog' +import {type Props as SVGIconProps} from '#/components/icons/common' +import {Text} from '#/components/Typography' +import {IS_NATIVE} from '#/env' + +type NotificationSettingsDialogProps = { + control: Dialog.DialogControlProps + name: Exclude + syncOthers?: Exclude[] + icon: React.ComponentType + titleText: React.ReactNode + subtitleText: React.ReactNode + allowDisableInApp?: boolean +} + +export function NotificationSettingsDialog({ + control, + name, + syncOthers, + titleText, + subtitleText, + allowDisableInApp = true, +}: NotificationSettingsDialogProps) { + return ( + + + + ) +} + +function NotificationSettingsDialogInner({ + control, + name, + syncOthers, + titleText, + subtitleText, + allowDisableInApp, +}: Omit) { + const t = useTheme() + const {t: l} = useLingui() + const {data: preferences, isError} = useNotificationSettingsQuery() + + return ( + <> + + + + + {titleText} + + {subtitleText} + + + {isError ? ( + + + Failed to load notification settings. + + + ) : ( + + )} + + + {IS_NATIVE && ( + + )} + + + ) +} diff --git a/src/lib/routes/types.ts b/src/lib/routes/types.ts index 1786040568..3c30b740da 100644 --- a/src/lib/routes/types.ts +++ b/src/lib/routes/types.ts @@ -55,16 +55,6 @@ export type CommonNavigatorParams = { ActivityPrivacySettings: undefined ContentAndMediaSettings: 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 diff --git a/src/routes.ts b/src/routes.ts index 387b1ca410..7ae73dd6e1 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -58,18 +58,6 @@ export const router = new Router({ 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', FindContactsSettings: '/settings/find-contacts', // support Support: '/support', diff --git a/src/screens/Messages/Settings.tsx b/src/screens/Messages/Settings.tsx index c070ff3c91..f49311b0da 100644 --- a/src/screens/Messages/Settings.tsx +++ b/src/screens/Messages/Settings.tsx @@ -50,6 +50,7 @@ export function MessagesSettingsScreenInner({}: Props) { did: currentAccount!.did, }) const {preferences, setPref} = useBackgroundNotificationPreferences() + const exportCarControl = Dialog.useDialogControl() const isGroupChatEnabled = ax.features.enabled(ax.features.GroupChatsEnable) @@ -233,7 +234,7 @@ export function MessagesSettingsScreenInner({}: Props) { value={preferences.playSoundChat} style={[a.flex_row, a.align_center, a.justify_between]} onChange={onSelectSoundSetting}> - + { exportCarControl.open() }}> - + - Export chat data + Export my chat data - + diff --git a/src/screens/Settings/NotificationSettings/ActivityNotificationSettings.tsx b/src/screens/Settings/NotificationSettings/ActivityNotificationSettings.tsx deleted file mode 100644 index c8b944d03b..0000000000 --- a/src/screens/Settings/NotificationSettings/ActivityNotificationSettings.tsx +++ /dev/null @@ -1,269 +0,0 @@ -import {useCallback, useMemo} from 'react' -import {type ListRenderItemInfo, Text as RNText, View} from 'react-native' -import {type ModerationOpts} from '@atproto/api' -import {msg} from '@lingui/core/macro' -import {useLingui} from '@lingui/react' -import {Trans} from '@lingui/react/macro' - -import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name' -import { - type AllNavigatorParams, - type NativeStackScreenProps, -} from '#/lib/routes/types' -import {cleanError} from '#/lib/strings/errors' -import {logger} from '#/logger' -import {useProfileShadow} from '#/state/cache/profile-shadow' -import {useModerationOpts} from '#/state/preferences/moderation-opts' -import {useActivitySubscriptionsQuery} from '#/state/queries/activity-subscriptions' -import {useNotificationSettingsQuery} from '#/state/queries/notifications/settings' -import {List} from '#/view/com/util/List' -import {atoms as a, useTheme} from '#/alf' -import {SubscribeProfileDialog} from '#/components/activity-notifications/SubscribeProfileDialog' -import * as Admonition from '#/components/Admonition' -import {Button, ButtonText} from '#/components/Button' -import {useDialogControl} from '#/components/Dialog' -import { - BellRinging_Filled_Corner0_Rounded as BellRingingFilledIcon, - BellRinging_Stroke2_Corner0_Rounded as BellRingingIcon, -} from '#/components/icons/BellRinging' -import * as Layout from '#/components/Layout' -import {InlineLinkText} from '#/components/Link' -import {ListFooter} from '#/components/Lists' -import {Loader} from '#/components/Loader' -import * as ProfileCard from '#/components/ProfileCard' -import {Text} from '#/components/Typography' -import type * as bsky from '#/types/bsky' -import * as SettingsList from '../components/SettingsList' -import {ItemTextWithSubtitle} from './components/ItemTextWithSubtitle' -import {PreferenceControls} from './components/PreferenceControls' - -type Props = NativeStackScreenProps< - AllNavigatorParams, - 'ActivityNotificationSettings' -> -export function ActivityNotificationSettingsScreen({}: Props) { - const t = useTheme() - const {_} = useLingui() - const {data: preferences, isError} = useNotificationSettingsQuery() - - const moderationOpts = useModerationOpts() - - const { - data: subscriptions, - isPending, - error, - isFetchingNextPage, - fetchNextPage, - hasNextPage, - } = useActivitySubscriptionsQuery() - - const items = useMemo(() => { - if (!subscriptions) return [] - return subscriptions?.pages.flatMap(page => page.subscriptions) - }, [subscriptions]) - - const renderItem = useCallback( - ({item}: ListRenderItemInfo) => { - if (!moderationOpts) return null - return ( - - ) - }, - [moderationOpts], - ) - - const onEndReached = useCallback(async () => { - if (isFetchingNextPage || !hasNextPage || isError) return - try { - await fetchNextPage() - } catch (err) { - logger.error('Failed to load more likes', {message: err}) - } - }, [isFetchingNextPage, hasNextPage, isError, fetchNextPage]) - - return ( - - - - - - Notifications - - - - - - - - Activity from others} - subtitleText={ - - Get notified about posts and replies from accounts you - choose. - - } - /> - - {isError ? ( - - - Failed to load notification settings. - - - ) : ( - - )} - - } - data={items} - keyExtractor={keyExtractor} - renderItem={renderItem} - onEndReached={onEndReached} - onEndReachedThreshold={4} - ListEmptyComponent={ - error ? null : ( - - {!isPending ? ( - - - - - - - Enable notifications for an account by visiting their - profile and pressing the{' '} - - bell icon - {' '} - - . - - - - - If you want to restrict who can receive notifications - for your account's activity, you can change this in{' '} - - Settings → Privacy and Security - - . - - - - - - ) : ( - - - - )} - - ) - } - ListFooterComponent={ - - } - windowSize={11} - /> - - ) -} - -function keyExtractor(item: bsky.profile.AnyProfileView) { - return item.did -} - -function ActivitySubscriptionCard({ - profile: profileUnshadowed, - moderationOpts, -}: { - profile: bsky.profile.AnyProfileView - moderationOpts: ModerationOpts -}) { - const profile = useProfileShadow(profileUnshadowed) - const control = useDialogControl() - const {_} = useLingui() - const t = useTheme() - - const preview = useMemo(() => { - const actSub = profile.viewer?.activitySubscription - if (actSub?.post && actSub?.reply) { - return _(msg`Posts, Replies`) - } else if (actSub?.post) { - return _(msg`Posts`) - } else if (actSub?.reply) { - return _(msg`Replies`) - } - return _(msg`None`) - }, [_, profile.viewer?.activitySubscription]) - - return ( - - - - - - - - {preview} - - - - - - - - - ) -} diff --git a/src/screens/Settings/NotificationSettings/LikeNotificationSettings.tsx b/src/screens/Settings/NotificationSettings/LikeNotificationSettings.tsx deleted file mode 100644 index 5c03d040a1..0000000000 --- a/src/screens/Settings/NotificationSettings/LikeNotificationSettings.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import {View} from 'react-native' -import {Trans} from '@lingui/react/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 deleted file mode 100644 index 738b4c6af4..0000000000 --- a/src/screens/Settings/NotificationSettings/LikesOnRepostsNotificationSettings.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import {View} from 'react-native' -import {Trans} from '@lingui/react/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 of 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 deleted file mode 100644 index 7aafedaef4..0000000000 --- a/src/screens/Settings/NotificationSettings/MentionNotificationSettings.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import {View} from 'react-native' -import {Trans} from '@lingui/react/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 deleted file mode 100644 index c5d678f300..0000000000 --- a/src/screens/Settings/NotificationSettings/MiscellaneousNotificationSettings.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import {View} from 'react-native' -import {Trans} from '@lingui/react/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 deleted file mode 100644 index 3526895529..0000000000 --- a/src/screens/Settings/NotificationSettings/NewFollowerNotificationSettings.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import {View} from 'react-native' -import {Trans} from '@lingui/react/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 deleted file mode 100644 index 023ccc29b9..0000000000 --- a/src/screens/Settings/NotificationSettings/QuoteNotificationSettings.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import {View} from 'react-native' -import {Trans} from '@lingui/react/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 deleted file mode 100644 index 552dbd4c85..0000000000 --- a/src/screens/Settings/NotificationSettings/ReplyNotificationSettings.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import {View} from 'react-native' -import {Trans} from '@lingui/react/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 deleted file mode 100644 index a2e59e58d9..0000000000 --- a/src/screens/Settings/NotificationSettings/RepostNotificationSettings.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import {View} from 'react-native' -import {Trans} from '@lingui/react/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 deleted file mode 100644 index 53a5610522..0000000000 --- a/src/screens/Settings/NotificationSettings/RepostsOnRepostsNotificationSettings.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import {View} from 'react-native' -import {Trans} from '@lingui/react/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/PreferenceControls.tsx b/src/screens/Settings/NotificationSettings/components/PreferenceControls.tsx index 76a3f77d1f..14ee7054aa 100644 --- a/src/screens/Settings/NotificationSettings/components/PreferenceControls.tsx +++ b/src/screens/Settings/NotificationSettings/components/PreferenceControls.tsx @@ -1,9 +1,7 @@ import {useMemo} from 'react' import {View} from 'react-native' import {type AppBskyNotificationDefs} from '@atproto/api' -import {msg} from '@lingui/core/macro' -import {useLingui} from '@lingui/react' -import {Trans} from '@lingui/react/macro' +import {Trans, useLingui} from '@lingui/react/macro' import {useNotificationSettingsUpdateMutation} from '#/state/queries/notifications/settings' import {atoms as a, platform, useTheme} from '#/alf' @@ -28,6 +26,7 @@ export function PreferenceControls({ preference?: | AppBskyNotificationDefs.Preference | AppBskyNotificationDefs.FilterablePreference + | AppBskyNotificationDefs.ChatPreference allowDisableInApp?: boolean }) { if (!preference) @@ -58,16 +57,17 @@ export function Inner({ preference: | AppBskyNotificationDefs.Preference | AppBskyNotificationDefs.FilterablePreference + | AppBskyNotificationDefs.ChatPreference allowDisableInApp: boolean }) { const t = useTheme() - const {_} = useLingui() + const {t: l} = useLingui() const ax = useAnalytics() const {mutate} = useNotificationSettingsUpdateMutation() const channels = useMemo(() => { const arr = [] - if (preference.list) arr.push('list') + if ('list' in preference && preference.list) arr.push('list') if (preference.push) arr.push('push') return arr }, [preference]) @@ -75,15 +75,18 @@ export function Inner({ const onChangeChannels = (change: string[]) => { const newPreference = { ...preference, - list: change.includes('list'), + ...('list' in preference ? {list: change.includes('list')} : {}), push: change.includes('push'), - } satisfies typeof preference + } as typeof preference - ax.metric('activityPreference:changeChannels', { + const metrics: {name: string; push: boolean; list?: boolean} = { name, push: newPreference.push, - list: newPreference.list, - }) + } + if ('list' in newPreference) { + metrics.list = newPreference.list + } + ax.metric('activityPreference:changeChannels', metrics) mutate({ [name]: newPreference, @@ -92,7 +95,7 @@ export function Inner({ } const onChangeFilter = ([change]: string[]) => { - if (change !== 'all' && change !== 'follows') + if (change !== 'all' && change !== 'follows' && change !== 'accepted') throw new Error('Invalid filter') const newPreference = { @@ -109,15 +112,15 @@ export function Inner({ } return ( - + {allowDisableInApp && ( - - - 0 && t.atoms.text, - a.font_normal, - a.text_md, - ]}> - Everyone - - - - - 0 && t.atoms.text, - a.font_normal, - a.text_md, - ]}> - People I follow - + + {({selected}) => ( + + )} + {name === 'chat' ? ( + + {({selected}) => ( + + )} + + ) : ( + + {({selected}) => ( + + )} + + )} diff --git a/src/screens/Settings/NotificationSettings/index.tsx b/src/screens/Settings/NotificationSettings/index.tsx index 96fee77218..29d27ce661 100644 --- a/src/screens/Settings/NotificationSettings/index.tsx +++ b/src/screens/Settings/NotificationSettings/index.tsx @@ -2,9 +2,7 @@ import {useEffect} from 'react' import {Linking, View} from 'react-native' import * as Notification from 'expo-notifications' import {type AppBskyNotificationDefs} from '@atproto/api' -import {msg} from '@lingui/core/macro' -import {useLingui} from '@lingui/react' -import {Trans} from '@lingui/react/macro' +import {Trans, useLingui} from '@lingui/react/macro' import {useQuery, useQueryClient} from '@tanstack/react-query' import {useAppState} from '#/lib/appState' @@ -15,6 +13,8 @@ import { import {useNotificationSettingsQuery} from '#/state/queries/notifications/settings' import {atoms as a} from '#/alf' import {Admonition} from '#/components/Admonition' +import * as Dialog from '#/components/Dialog' +import {NotificationSettingsDialog} from '#/components/dialogs/NotificationSettingsDialog' 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' @@ -39,10 +39,21 @@ const RQKEY = ['notification-permissions'] type Props = NativeStackScreenProps export function NotificationSettingsScreen({}: Props) { - const {_} = useLingui() + const {t: l} = useLingui() const queryClient = useQueryClient() const {data: settings, isError} = useNotificationSettingsQuery() + const likeDialogControl = Dialog.useDialogControl() + const followDialogControl = Dialog.useDialogControl() + const replyDialogControl = Dialog.useDialogControl() + const mentionDialogControl = Dialog.useDialogControl() + const quoteDialogControl = Dialog.useDialogControl() + const repostDialogControl = Dialog.useDialogControl() + const activityDialogControl = Dialog.useDialogControl() + const likeRepostDialogControl = Dialog.useDialogControl() + const repostRepostDialogControl = Dialog.useDialogControl() + const miscDialogControl = Dialog.useDialogControl() + const {data: permissions, refetch} = useQuery({ queryKey: RQKEY, queryFn: async () => { @@ -54,7 +65,7 @@ export function NotificationSettingsScreen({}: Props) { const appState = useAppState() useEffect(() => { if (appState === 'active') { - refetch() + void refetch() } }, [appState, refetch]) @@ -76,10 +87,10 @@ export function NotificationSettingsScreen({}: Props) { ], ) } catch { - Linking.openSettings() + void Linking.openSettings() } } else if (IS_IOS) { - Linking.openSettings() + void Linking.openSettings() } } } @@ -100,8 +111,8 @@ export function NotificationSettingsScreen({}: Props) { {permissions && !permissions.granted && ( <> + label={l`Enable push notifications`} + onPress={() => void onRequestPermissions()}> Enable push notifications @@ -118,9 +129,9 @@ export function NotificationSettingsScreen({}: Props) { )} - } showSkeleton={!settings} /> - - + } showSkeleton={!settings} /> - - + } showSkeleton={!settings} /> - - + } showSkeleton={!settings} /> - - + } showSkeleton={!settings} /> - - + } showSkeleton={!settings} /> - - + - - + - - + - - + - + + Likes} + subtitleText={ + Get notifications when people like your posts. + } + /> + New followers} + subtitleText={Get notifications when people follow you.} + /> + Replies} + subtitleText={ + Get notifications when people reply to your posts. + } + /> + Mentions} + subtitleText={Get notifications when people mention you.} + /> + Quotes} + subtitleText={ + Get notifications when people quote your posts. + } + /> + Reposts} + subtitleText={ + Get notifications when people repost your posts. + } + /> + Activity from others} + subtitleText={ + + Get notifications when there's activity on posts you're subscribed + to. + + } + allowDisableInApp={false} + /> + Likes of your reposts} + subtitleText={ + Get notifications when people like your reposts. + } + /> + Reposts of your reposts} + subtitleText={ + Get notifications when people repost your reposts. + } + /> + Everything else} + subtitleText={ + + Get notifications for starter pack joins, verification, and other + activity. + + } + allowDisableInApp={false} + /> ) } @@ -256,38 +358,38 @@ function SettingPreview({ | AppBskyNotificationDefs.Preference | AppBskyNotificationDefs.FilterablePreference }) { - const {_} = useLingui() + const {t: l} = useLingui() if (!preference) { return null } else { if ('include' in preference) { if (preference.include === 'all') { if (preference.list && preference.push) { - return _(msg`In-app, Push, Everyone`) + return l`In-app, push, everyone` } else if (preference.list) { - return _(msg`In-app, Everyone`) + return l`In-app, everyone` } else if (preference.push) { - return _(msg`Push, Everyone`) + return l`Push, everyone` } } else if (preference.include === 'follows') { if (preference.list && preference.push) { - return _(msg`In-app, Push, People you follow`) + return l`In-app, push, people you follow` } else if (preference.list) { - return _(msg`In-app, People you follow`) + return l`In-app, people you follow` } else if (preference.push) { - return _(msg`Push, People you follow`) + return l`Push, people you follow` } } } else { if (preference.list && preference.push) { - return _(msg`In-app, Push`) + return l`In-app, push` } else if (preference.list) { - return _(msg`In-app`) + return l`In-app` } else if (preference.push) { - return _(msg`Push`) + return l`Push` } } } - return _(msg`Off`) + return l`Off` } diff --git a/src/screens/Settings/components/ExportCarDialog.tsx b/src/screens/Settings/components/ExportCarDialog.tsx index f0b2a8f54d..13b60dfdaa 100644 --- a/src/screens/Settings/components/ExportCarDialog.tsx +++ b/src/screens/Settings/components/ExportCarDialog.tsx @@ -115,12 +115,12 @@ export function ExportCarDialog({ diff --git a/src/state/queries/notifications/settings.ts b/src/state/queries/notifications/settings.ts index d00a4ae5ed..7c26491602 100644 --- a/src/state/queries/notifications/settings.ts +++ b/src/state/queries/notifications/settings.ts @@ -45,7 +45,7 @@ export function useNotificationSettingsUpdateMutation() { }, onError: e => { logger.error('Could not update notification settings', {message: e}) - queryClient.invalidateQueries({queryKey: RQKEY}) + void queryClient.invalidateQueries({queryKey: RQKEY}) Toast.show(t`Could not update notification settings`, { type: 'error', })