about summary refs log tree commit diff
path: root/src/screens/Settings/NotificationSettings/index.tsx
diff options
context:
space:
mode:
authorSamuel Newman <mozzius@protonmail.com>2025-06-17 12:37:14 +0300
committerGitHub <noreply@github.com>2025-06-17 02:37:14 -0700
commit21989b558bd074bf84ac08c174d7a411fda1ffb7 (patch)
treef5f28510cf5a592b83bcfc581a57e992823eb402 /src/screens/Settings/NotificationSettings/index.tsx
parent7dc6bb57a6666db3e507630c13448487acceadc5 (diff)
downloadvoidsky-21989b558bd074bf84ac08c174d7a411fda1ffb7.tar.zst
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>
Diffstat (limited to 'src/screens/Settings/NotificationSettings/index.tsx')
-rw-r--r--src/screens/Settings/NotificationSettings/index.tsx293
1 files changed, 293 insertions, 0 deletions
diff --git a/src/screens/Settings/NotificationSettings/index.tsx b/src/screens/Settings/NotificationSettings/index.tsx
new file mode 100644
index 000000000..a4f6dede0
--- /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<AllNavigatorParams, 'NotificationSettings'>
+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 (
+    <Layout.Screen>
+      <Layout.Header.Outer>
+        <Layout.Header.BackButton />
+        <Layout.Header.Content>
+          <Layout.Header.TitleText>
+            <Trans>Notifications</Trans>
+          </Layout.Header.TitleText>
+        </Layout.Header.Content>
+        <Layout.Header.Slot />
+      </Layout.Header.Outer>
+      <Layout.Content>
+        <SettingsList.Container>
+          {permissions && !permissions.granted && (
+            <>
+              <SettingsList.PressableItem
+                label={_(msg`Enable push notifications`)}
+                onPress={onRequestPermissions}>
+                <SettingsList.ItemIcon icon={HapticIcon} />
+                <SettingsList.ItemText>
+                  <Trans>Enable push notifications</Trans>
+                </SettingsList.ItemText>
+              </SettingsList.PressableItem>
+              <SettingsList.Divider />
+            </>
+          )}
+          {isError && (
+            <View style={[a.px_lg, a.pb_md]}>
+              <Admonition type="error">
+                <Trans>Failed to load notification settings.</Trans>
+              </Admonition>
+            </View>
+          )}
+          <View style={[a.gap_sm]}>
+            <SettingsList.LinkItem
+              label={_(msg`Settings for reply notifications`)}
+              to={{screen: 'ReplyNotificationSettings'}}
+              contentContainerStyle={[a.align_start]}>
+              <SettingsList.ItemIcon icon={BubbleIcon} />
+              <ItemTextWithSubtitle
+                titleText={<Trans>Replies</Trans>}
+                subtitleText={<SettingPreview preference={settings?.reply} />}
+                showSkeleton={!settings}
+              />
+            </SettingsList.LinkItem>
+            <SettingsList.LinkItem
+              label={_(msg`Settings for mention notifications`)}
+              to={{screen: 'MentionNotificationSettings'}}
+              contentContainerStyle={[a.align_start]}>
+              <SettingsList.ItemIcon icon={AtIcon} />
+              <ItemTextWithSubtitle
+                titleText={<Trans>Mentions</Trans>}
+                subtitleText={<SettingPreview preference={settings?.mention} />}
+                showSkeleton={!settings}
+              />
+            </SettingsList.LinkItem>
+            <SettingsList.LinkItem
+              label={_(msg`Settings for quote notifications`)}
+              to={{screen: 'QuoteNotificationSettings'}}
+              contentContainerStyle={[a.align_start]}>
+              <SettingsList.ItemIcon icon={CloseQuoteIcon} />
+              <ItemTextWithSubtitle
+                titleText={<Trans>Quotes</Trans>}
+                subtitleText={<SettingPreview preference={settings?.quote} />}
+                showSkeleton={!settings}
+              />
+            </SettingsList.LinkItem>
+            <SettingsList.LinkItem
+              label={_(msg`Settings for like notifications`)}
+              to={{screen: 'LikeNotificationSettings'}}
+              contentContainerStyle={[a.align_start]}>
+              <SettingsList.ItemIcon icon={HeartIcon} />
+              <ItemTextWithSubtitle
+                titleText={<Trans>Likes</Trans>}
+                subtitleText={<SettingPreview preference={settings?.like} />}
+                showSkeleton={!settings}
+              />
+            </SettingsList.LinkItem>
+            <SettingsList.LinkItem
+              label={_(msg`Settings for repost notifications`)}
+              to={{screen: 'RepostNotificationSettings'}}
+              contentContainerStyle={[a.align_start]}>
+              <SettingsList.ItemIcon icon={RepostIcon} />
+              <ItemTextWithSubtitle
+                titleText={<Trans>Reposts</Trans>}
+                subtitleText={<SettingPreview preference={settings?.repost} />}
+                showSkeleton={!settings}
+              />
+            </SettingsList.LinkItem>
+            <SettingsList.LinkItem
+              label={_(msg`Settings for new follower notifications`)}
+              to={{screen: 'NewFollowerNotificationSettings'}}
+              contentContainerStyle={[a.align_start]}>
+              <SettingsList.ItemIcon icon={PersonPlusIcon} />
+              <ItemTextWithSubtitle
+                titleText={<Trans>New followers</Trans>}
+                subtitleText={<SettingPreview preference={settings?.follow} />}
+                showSkeleton={!settings}
+              />
+            </SettingsList.LinkItem>
+            {/* <SettingsList.LinkItem
+              label={_(msg`Settings for activity alerts`)}
+              to={{screen: 'ActivityNotificationSettings'}}
+              contentContainerStyle={[a.align_start]}>
+              <SettingsList.ItemIcon icon={BellRingingIcon} />
+
+              <ItemTextWithSubtitle
+                titleText={<Trans>Activity alerts</Trans>}
+                subtitleText={
+                  <SettingPreview preference={settings?.subscribedPost} />
+                }
+                showSkeleton={!settings}
+              />
+            </SettingsList.LinkItem> */}
+            <SettingsList.LinkItem
+              label={_(
+                msg`Settings for notifications for likes on your reposts`,
+              )}
+              to={{screen: 'LikesOnRepostsNotificationSettings'}}
+              contentContainerStyle={[a.align_start]}>
+              <SettingsList.ItemIcon icon={LikeRepostIcon} />
+              <ItemTextWithSubtitle
+                titleText={<Trans>Likes on your reposts</Trans>}
+                subtitleText={
+                  <SettingPreview preference={settings?.likeViaRepost} />
+                }
+                showSkeleton={!settings}
+              />
+            </SettingsList.LinkItem>
+            <SettingsList.LinkItem
+              label={_(
+                msg`Settings for notifications for reposts of your reposts`,
+              )}
+              to={{screen: 'RepostsOnRepostsNotificationSettings'}}
+              contentContainerStyle={[a.align_start]}>
+              <SettingsList.ItemIcon icon={RepostRepostIcon} />
+              <ItemTextWithSubtitle
+                titleText={<Trans>Reposts of your reposts</Trans>}
+                subtitleText={
+                  <SettingPreview preference={settings?.repostViaRepost} />
+                }
+                showSkeleton={!settings}
+              />
+            </SettingsList.LinkItem>
+            <SettingsList.LinkItem
+              label={_(msg`Settings for notifications for everything else`)}
+              to={{screen: 'MiscellaneousNotificationSettings'}}
+              contentContainerStyle={[a.align_start]}>
+              <SettingsList.ItemIcon icon={ShapesIcon} />
+              <ItemTextWithSubtitle
+                titleText={<Trans>Everything else</Trans>}
+                // 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={
+                  <SettingPreview preference={settings?.starterpackJoined} />
+                }
+                showSkeleton={!settings}
+              />
+            </SettingsList.LinkItem>
+          </View>
+        </SettingsList.Container>
+      </Layout.Content>
+    </Layout.Screen>
+  )
+}
+
+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`)
+}