From 21989b558bd074bf84ac08c174d7a411fda1ffb7 Mon Sep 17 00:00:00 2001
From: Samuel Newman <mozzius@protonmail.com>
Date: Tue, 17 Jun 2025 12:37:14 +0300
Subject: 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>
---
 .../components/PreferenceControls.tsx              | 194 +++++++++++++++++++++
 1 file changed, 194 insertions(+)
 create mode 100644 src/screens/Settings/NotificationSettings/components/PreferenceControls.tsx

(limited to 'src/screens/Settings/NotificationSettings/components/PreferenceControls.tsx')

diff --git a/src/screens/Settings/NotificationSettings/components/PreferenceControls.tsx b/src/screens/Settings/NotificationSettings/components/PreferenceControls.tsx
new file mode 100644
index 000000000..336e08695
--- /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<keyof AppBskyNotificationDefs.Preferences, '$type'>
+  /**
+   * Keep other prefs in sync with `name`. For use in the "everything else" category
+   * which groups starterpack joins + verified + unverified notifications into a single toggle.
+   */
+  syncOthers?: Exclude<keyof AppBskyNotificationDefs.Preferences, '$type'>[]
+  preference?: AppBskyNotificationDefs.Preference | FilterablePreference
+  allowDisableInApp?: boolean
+}) {
+  if (!preference)
+    return (
+      <View style={[a.w_full, a.pt_5xl, a.align_center]}>
+        <Loader size="xl" />
+      </View>
+    )
+
+  return (
+    <Inner
+      name={name}
+      syncOthers={syncOthers}
+      preference={preference}
+      allowDisableInApp={allowDisableInApp}
+    />
+  )
+}
+
+export function Inner({
+  name,
+  syncOthers = [],
+  preference,
+  allowDisableInApp,
+}: {
+  name: Exclude<keyof AppBskyNotificationDefs.Preferences, '$type'>
+  syncOthers?: Exclude<keyof AppBskyNotificationDefs.Preferences, '$type'>[]
+  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 (
+    <View style={[a.px_xl, a.pt_md, a.gap_sm]}>
+      <Toggle.Group
+        type="checkbox"
+        label={_(`Select your preferred notification channels`)}
+        values={channels}
+        onChange={onChangeChannels}>
+        <View style={[a.gap_sm]}>
+          <Toggle.Item
+            label={_(msg`Receive push notifications`)}
+            name="push"
+            style={[
+              a.py_xs,
+              platform({
+                native: [a.justify_between],
+                web: [a.flex_row_reverse, a.gap_md],
+              }),
+            ]}>
+            <Toggle.LabelText
+              style={[t.atoms.text, a.font_normal, a.text_md, a.flex_1]}>
+              <Trans>Push notifications</Trans>
+            </Toggle.LabelText>
+            <Toggle.Platform />
+          </Toggle.Item>
+          {allowDisableInApp && (
+            <Toggle.Item
+              label={_(msg`Receive in-app notifications`)}
+              name="list"
+              style={[
+                a.py_xs,
+                platform({
+                  native: [a.justify_between],
+                  web: [a.flex_row_reverse, a.gap_md],
+                }),
+              ]}>
+              <Toggle.LabelText
+                style={[t.atoms.text, a.font_normal, a.text_md, a.flex_1]}>
+                <Trans>In-app notifications</Trans>
+              </Toggle.LabelText>
+              <Toggle.Platform />
+            </Toggle.Item>
+          )}
+        </View>
+      </Toggle.Group>
+      {'filter' in preference && (
+        <>
+          <Divider />
+          <Text style={[a.font_bold, a.text_md]}>From</Text>
+          <Toggle.Group
+            type="radio"
+            label={_('Filter who you receive notifications from')}
+            values={[preference.filter]}
+            onChange={onChangeFilter}
+            disabled={channels.length === 0}>
+            <View style={[a.gap_sm]}>
+              <Toggle.Item
+                label={_(msg`Everyone`)}
+                name="all"
+                style={[
+                  a.flex_row,
+                  a.py_xs,
+                  platform({native: [a.gap_sm], web: [a.gap_md]}),
+                ]}>
+                <Toggle.Radio />
+                <Toggle.LabelText
+                  style={[
+                    channels.length > 0 && t.atoms.text,
+                    a.font_normal,
+                    a.text_md,
+                  ]}>
+                  <Trans>Everyone</Trans>
+                </Toggle.LabelText>
+              </Toggle.Item>
+              <Toggle.Item
+                label={_(msg`People I follow`)}
+                name="follows"
+                style={[
+                  a.flex_row,
+                  a.py_xs,
+                  platform({native: [a.gap_sm], web: [a.gap_md]}),
+                ]}>
+                <Toggle.Radio />
+                <Toggle.LabelText
+                  style={[
+                    channels.length > 0 && t.atoms.text,
+                    a.font_normal,
+                    a.text_md,
+                  ]}>
+                  <Trans>People I follow</Trans>
+                </Toggle.LabelText>
+              </Toggle.Item>
+            </View>
+          </Toggle.Group>
+        </>
+      )}
+    </View>
+  )
+}
-- 
cgit 1.4.1

