about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/Navigation.tsx6
-rw-r--r--src/components/Lists.tsx2
-rw-r--r--src/components/forms/Toggle.tsx3
-rw-r--r--src/components/icons/Gear.tsx5
-rw-r--r--src/lib/async/until.ts10
-rw-r--r--src/lib/routes/types.ts15
-rw-r--r--src/routes.ts1
-rw-r--r--src/screens/Messages/Conversation/index.tsx1
-rw-r--r--src/screens/Messages/List/index.tsx2
-rw-r--r--src/screens/Messages/Settings.tsx2
-rw-r--r--src/state/queries/notifications/feed.ts37
-rw-r--r--src/state/queries/notifications/settings.ts67
-rw-r--r--src/state/queries/notifications/types.ts1
-rw-r--r--src/state/queries/notifications/util.ts8
-rw-r--r--src/view/com/notifications/Feed.tsx7
-rw-r--r--src/view/screens/Notifications.tsx117
-rw-r--r--src/view/screens/NotificationsSettings.tsx94
17 files changed, 299 insertions, 79 deletions
diff --git a/src/Navigation.tsx b/src/Navigation.tsx
index 9e9b49443..8646577c8 100644
--- a/src/Navigation.tsx
+++ b/src/Navigation.tsx
@@ -76,6 +76,7 @@ import {LogScreen} from './view/screens/Log'
 import {ModerationModlistsScreen} from './view/screens/ModerationModlists'
 import {NotFoundScreen} from './view/screens/NotFound'
 import {NotificationsScreen} from './view/screens/Notifications'
+import {NotificationsSettingsScreen} from './view/screens/NotificationsSettings'
 import {PostLikedByScreen} from './view/screens/PostLikedBy'
 import {PostRepostedByScreen} from './view/screens/PostRepostedBy'
 import {PostThreadScreen} from './view/screens/PostThread'
@@ -325,6 +326,11 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
         options={{title: title(msg`Chat settings`), requireAuth: true}}
       />
       <Stack.Screen
+        name="NotificationsSettings"
+        getComponent={() => NotificationsSettingsScreen}
+        options={{title: title(msg`Notification settings`), requireAuth: true}}
+      />
+      <Stack.Screen
         name="Feeds"
         getComponent={() => FeedsScreen}
         options={{title: title(msg`Feeds`)}}
diff --git a/src/components/Lists.tsx b/src/components/Lists.tsx
index 3368d076f..e706e101f 100644
--- a/src/components/Lists.tsx
+++ b/src/components/Lists.tsx
@@ -189,7 +189,7 @@ let ListMaybePlaceholder = ({
     return (
       <Error
         title={errorTitle ?? _(msg`Oops!`)}
-        message={errorMessage ?? _(`Something went wrong!`)}
+        message={errorMessage ?? _(msg`Something went wrong!`)}
         onRetry={onRetry}
         onGoBack={onGoBack}
         sideBorders={sideBorders}
diff --git a/src/components/forms/Toggle.tsx b/src/components/forms/Toggle.tsx
index cbdbf1c60..391b1c8b7 100644
--- a/src/components/forms/Toggle.tsx
+++ b/src/components/forms/Toggle.tsx
@@ -2,6 +2,7 @@ import React from 'react'
 import {Pressable, View, ViewStyle} from 'react-native'
 import Animated, {LinearTransition} from 'react-native-reanimated'
 
+import {isNative} from '#/platform/detection'
 import {HITSLOP_10} from 'lib/constants'
 import {
   atoms as a,
@@ -459,3 +460,5 @@ export function Radio() {
     </View>
   )
 }
+
+export const Platform = isNative ? Switch : Checkbox
diff --git a/src/components/icons/Gear.tsx b/src/components/icons/Gear.tsx
deleted file mode 100644
index 980b7413b..000000000
--- a/src/components/icons/Gear.tsx
+++ /dev/null
@@ -1,5 +0,0 @@
-import {createSinglePathSVG} from './TEMPLATE'
-
-export const SettingsGear2_Stroke2_Corner0_Rounded = createSinglePathSVG({
-  path: 'M11.1 2a1 1 0 0 0-.832.445L8.851 4.57 6.6 4.05a1 1 0 0 0-.932.268l-1.35 1.35a1 1 0 0 0-.267.932l.52 2.251-2.126 1.417A1 1 0 0 0 2 11.1v1.8a1 1 0 0 0 .445.832l2.125 1.417-.52 2.251a1 1 0 0 0 .268.932l1.35 1.35a1 1 0 0 0 .932.267l2.251-.52 1.417 2.126A1 1 0 0 0 11.1 22h1.8a1 1 0 0 0 .832-.445l1.417-2.125 2.251.52a1 1 0 0 0 .932-.268l1.35-1.35a1 1 0 0 0 .267-.932l-.52-2.251 2.126-1.417A1 1 0 0 0 22 12.9v-1.8a1 1 0 0 0-.445-.832L19.43 8.851l.52-2.251a1 1 0 0 0-.268-.932l-1.35-1.35a1 1 0 0 0-.932-.267l-2.251.52-1.417-2.126A1 1 0 0 0 12.9 2h-1.8Zm-.968 4.255L11.635 4h.73l1.503 2.255a1 1 0 0 0 1.057.42l2.385-.551.566.566-.55 2.385a1 1 0 0 0 .42 1.057L20 11.635v.73l-2.255 1.503a1 1 0 0 0-.42 1.057l.551 2.385-.566.566-2.385-.55a1 1 0 0 0-1.057.42L12.365 20h-.73l-1.503-2.255a1 1 0 0 0-1.057-.42l-2.385.551-.566-.566.55-2.385a1 1 0 0 0-.42-1.057L4 12.365v-.73l2.255-1.503a1 1 0 0 0 .42-1.057L6.123 6.69l.566-.566 2.385.55a1 1 0 0 0 1.057-.42ZM8 12a4 4 0 1 1 8 0 4 4 0 0 1-8 0Zm4-2a2 2 0 1 0 0 4 2 2 0 0 0 0-4Z',
-})
diff --git a/src/lib/async/until.ts b/src/lib/async/until.ts
index db53c9218..1b7a57633 100644
--- a/src/lib/async/until.ts
+++ b/src/lib/async/until.ts
@@ -1,10 +1,10 @@
 import {timeout} from './timeout'
 
-export async function until(
+export async function until<T>(
   retries: number,
   delay: number,
-  cond: (v: any, err: any) => boolean,
-  fn: () => Promise<any>,
+  cond: (v: T, err: any) => boolean,
+  fn: () => Promise<T>,
 ): Promise<boolean> {
   while (retries > 0) {
     try {
@@ -13,7 +13,9 @@ export async function until(
         return true
       }
     } catch (e: any) {
-      if (cond(undefined, e)) {
+      // TODO: change the type signature of cond to accept undefined
+      // however this breaks every existing usage of until -sfn
+      if (cond(undefined as unknown as T, e)) {
         return true
       }
     }
diff --git a/src/lib/routes/types.ts b/src/lib/routes/types.ts
index bda93fb40..fbb66c9e9 100644
--- a/src/lib/routes/types.ts
+++ b/src/lib/routes/types.ts
@@ -42,14 +42,13 @@ export type CommonNavigatorParams = {
   Hashtag: {tag: string; author?: string}
   MessagesConversation: {conversation: string; embed?: string}
   MessagesSettings: undefined
+  NotificationsSettings: undefined
   Feeds: undefined
   Start: {name: string; rkey: string}
   StarterPack: {name: string; rkey: string; new?: boolean}
   StarterPackShort: {code: string}
   StarterPackWizard: undefined
-  StarterPackEdit: {
-    rkey?: string
-  }
+  StarterPackEdit: {rkey?: string}
 }
 
 export type BottomTabNavigatorParams = CommonNavigatorParams & {
@@ -69,7 +68,7 @@ export type SearchTabNavigatorParams = CommonNavigatorParams & {
 }
 
 export type NotificationsTabNavigatorParams = CommonNavigatorParams & {
-  Notifications: undefined
+  Notifications: {show?: 'all'}
 }
 
 export type MyProfileTabNavigatorParams = CommonNavigatorParams & {
@@ -84,7 +83,7 @@ export type FlatNavigatorParams = CommonNavigatorParams & {
   Home: undefined
   Search: {q?: string}
   Feeds: undefined
-  Notifications: undefined
+  Notifications: {show?: 'all'}
   Hashtag: {tag: string; author?: string}
   Messages: {pushToConversation?: string; animation?: 'push' | 'pop'}
 }
@@ -96,7 +95,7 @@ export type AllNavigatorParams = CommonNavigatorParams & {
   Search: {q?: string}
   Feeds: undefined
   NotificationsTab: undefined
-  Notifications: undefined
+  Notifications: {show?: 'all'}
   MyProfileTab: undefined
   Hashtag: {tag: string; author?: string}
   MessagesTab: undefined
@@ -105,9 +104,7 @@ export type AllNavigatorParams = CommonNavigatorParams & {
   StarterPack: {name: string; rkey: string; new?: boolean}
   StarterPackShort: {code: string}
   StarterPackWizard: undefined
-  StarterPackEdit: {
-    rkey?: string
-  }
+  StarterPackEdit: {rkey?: string}
 }
 
 // NOTE
diff --git a/src/routes.ts b/src/routes.ts
index a76d8c4ce..ddf4fb39f 100644
--- a/src/routes.ts
+++ b/src/routes.ts
@@ -5,6 +5,7 @@ export const router = new Router({
   Search: '/search',
   Feeds: '/feeds',
   Notifications: '/notifications',
+  NotificationsSettings: '/notifications/settings',
   Settings: '/settings',
   LanguageSettings: '/settings/language',
   Lists: '/lists',
diff --git a/src/screens/Messages/Conversation/index.tsx b/src/screens/Messages/Conversation/index.tsx
index a99ef8d4d..d14ed160a 100644
--- a/src/screens/Messages/Conversation/index.tsx
+++ b/src/screens/Messages/Conversation/index.tsx
@@ -106,6 +106,7 @@ function Inner() {
           title={_(msg`Something went wrong`)}
           message={_(msg`We couldn't load this conversation`)}
           onRetry={() => convoState.error.retry()}
+          sideBorders={false}
         />
       </CenteredView>
     )
diff --git a/src/screens/Messages/List/index.tsx b/src/screens/Messages/List/index.tsx
index 0b1fe2a95..2fd9990c7 100644
--- a/src/screens/Messages/List/index.tsx
+++ b/src/screens/Messages/List/index.tsx
@@ -309,7 +309,7 @@ function DesktopHeader({
         a.gap_lg,
         a.px_lg,
         a.pr_md,
-        a.py_md,
+        a.py_sm,
         a.border_b,
         t.atoms.border_contrast_low,
       ]}>
diff --git a/src/screens/Messages/Settings.tsx b/src/screens/Messages/Settings.tsx
index 3d7e60130..df469d13f 100644
--- a/src/screens/Messages/Settings.tsx
+++ b/src/screens/Messages/Settings.tsx
@@ -107,7 +107,7 @@ export function MessagesSettingsScreen({}: Props) {
             a.rounded_md,
             t.atoms.bg_contrast_25,
           ]}>
-          <Text style={[t.atoms.text_contrast_high]}>
+          <Text style={[t.atoms.text_contrast_high, a.leading_snug]}>
             <Trans>
               You can continue ongoing conversations regardless of which setting
               you choose.
diff --git a/src/state/queries/notifications/feed.ts b/src/state/queries/notifications/feed.ts
index 17ee90929..3cafcb716 100644
--- a/src/state/queries/notifications/feed.ts
+++ b/src/state/queries/notifications/feed.ts
@@ -46,11 +46,14 @@ const PAGE_SIZE = 30
 type RQPageParam = string | undefined
 
 const RQKEY_ROOT = 'notification-feed'
-export function RQKEY() {
-  return [RQKEY_ROOT]
+export function RQKEY(priority?: false) {
+  return [RQKEY_ROOT, priority]
 }
 
-export function useNotificationFeedQuery(opts?: {enabled?: boolean}) {
+export function useNotificationFeedQuery(opts?: {
+  enabled?: boolean
+  overridePriorityNotifications?: boolean
+}) {
   const agent = useAgent()
   const queryClient = useQueryClient()
   const moderationOpts = useModerationOpts()
@@ -59,6 +62,10 @@ export function useNotificationFeedQuery(opts?: {enabled?: boolean}) {
   const lastPageCountRef = useRef(0)
   const gate = useGate()
 
+  // false: force showing all notifications
+  // undefined: let the server decide
+  const priority = opts?.overridePriorityNotifications ? false : undefined
+
   const query = useInfiniteQuery<
     FeedPage,
     Error,
@@ -67,7 +74,7 @@ export function useNotificationFeedQuery(opts?: {enabled?: boolean}) {
     RQPageParam
   >({
     staleTime: STALE.INFINITY,
-    queryKey: RQKEY(),
+    queryKey: RQKEY(priority),
     async queryFn({pageParam}: {pageParam: RQPageParam}) {
       let page
       if (!pageParam) {
@@ -75,17 +82,17 @@ export function useNotificationFeedQuery(opts?: {enabled?: boolean}) {
         page = unreads.getCachedUnreadPage()
       }
       if (!page) {
-        page = (
-          await fetchPage({
-            agent,
-            limit: PAGE_SIZE,
-            cursor: pageParam,
-            queryClient,
-            moderationOpts,
-            fetchAdditionalData: true,
-            shouldUngroupFollowBacks: () => gate('ungroup_follow_backs'),
-          })
-        ).page
+        const {page: fetchedPage} = await fetchPage({
+          agent,
+          limit: PAGE_SIZE,
+          cursor: pageParam,
+          queryClient,
+          moderationOpts,
+          fetchAdditionalData: true,
+          shouldUngroupFollowBacks: () => gate('ungroup_follow_backs'),
+          priority,
+        })
+        page = fetchedPage
       }
 
       // if the first page has an unread, mark all read
diff --git a/src/state/queries/notifications/settings.ts b/src/state/queries/notifications/settings.ts
new file mode 100644
index 000000000..78ecbd9f7
--- /dev/null
+++ b/src/state/queries/notifications/settings.ts
@@ -0,0 +1,67 @@
+import {msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+import {useMutation, 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 {useAgent} from '#/state/session'
+import * as Toast from '#/view/com/util/Toast'
+
+export function useNotificationsSettingsMutation() {
+  const {_} = useLingui()
+  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}),
+      )
+
+      eagerlySetCachedPriority(queryClient, enabled)
+    },
+    onError: err => {
+      logger.error('Failed to save notification preferences', {
+        safeMessage: err,
+      })
+      Toast.show(
+        _(msg`Failed to save notification preferences, please try again`),
+        'xmark',
+      )
+    },
+    onSuccess: () => {
+      Toast.show(_(msg`Preference saved`))
+    },
+    onSettled: () => {
+      queryClient.invalidateQueries({queryKey: RQKEY_NOTIFS()})
+    },
+  })
+}
+
+function eagerlySetCachedPriority(
+  queryClient: ReturnType<typeof useQueryClient>,
+  enabled: boolean,
+) {
+  queryClient.setQueryData(RQKEY_NOTIFS(), (old: any) => {
+    if (!old) return old
+    return {
+      ...old,
+      pages: old.pages.map((page: any) => {
+        return {
+          ...page,
+          priority: enabled,
+        }
+      }),
+    }
+  })
+}
diff --git a/src/state/queries/notifications/types.ts b/src/state/queries/notifications/types.ts
index d40a07b12..c96374eb8 100644
--- a/src/state/queries/notifications/types.ts
+++ b/src/state/queries/notifications/types.ts
@@ -22,6 +22,7 @@ export interface FeedPage {
   cursor: string | undefined
   seenAt: Date
   items: FeedNotification[]
+  priority: boolean
 }
 
 export interface CachedFeedPage {
diff --git a/src/state/queries/notifications/util.ts b/src/state/queries/notifications/util.ts
index 2f2c242d8..7651e414a 100644
--- a/src/state/queries/notifications/util.ts
+++ b/src/state/queries/notifications/util.ts
@@ -39,10 +39,15 @@ export async function fetchPage({
   moderationOpts: ModerationOpts | undefined
   fetchAdditionalData: boolean
   shouldUngroupFollowBacks?: () => boolean
-}): Promise<{page: FeedPage; indexedAt: string | undefined}> {
+  priority?: boolean
+}): Promise<{
+  page: FeedPage
+  indexedAt: string | undefined
+}> {
   const res = await agent.listNotifications({
     limit,
     cursor,
+    // priority,
   })
 
   const indexedAt = res.data.notifications[0]?.indexedAt
@@ -88,6 +93,7 @@ export async function fetchPage({
       cursor: res.data.cursor,
       seenAt,
       items: notifsGrouped,
+      priority: res.data.priority ?? false,
     },
     indexedAt,
   }
diff --git a/src/view/com/notifications/Feed.tsx b/src/view/com/notifications/Feed.tsx
index e2f12e84f..3e7fdfc71 100644
--- a/src/view/com/notifications/Feed.tsx
+++ b/src/view/com/notifications/Feed.tsx
@@ -35,11 +35,13 @@ export function Feed({
   onPressTryAgain,
   onScrolledDownChange,
   ListHeaderComponent,
+  overridePriorityNotifications,
 }: {
   scrollElRef?: ListRef
   onPressTryAgain?: () => void
   onScrolledDownChange: (isScrolledDown: boolean) => void
   ListHeaderComponent?: () => JSX.Element
+  overridePriorityNotifications?: boolean
 }) {
   const initialNumToRender = useInitialNumToRender()
 
@@ -59,7 +61,10 @@ export function Feed({
     hasNextPage,
     isFetchingNextPage,
     fetchNextPage,
-  } = useNotificationFeedQuery({enabled: !!moderationOpts})
+  } = useNotificationFeedQuery({
+    enabled: !!moderationOpts,
+    overridePriorityNotifications,
+  })
   const isEmpty = !isFetching && !data?.pages[0]?.items.length
 
   const items = React.useMemo(() => {
diff --git a/src/view/screens/Notifications.tsx b/src/view/screens/Notifications.tsx
index f1ae7945a..073e91c45 100644
--- a/src/view/screens/Notifications.tsx
+++ b/src/view/screens/Notifications.tsx
@@ -1,11 +1,19 @@
-import React from 'react'
+import React, {useCallback} from 'react'
 import {View} from 'react-native'
 import {msg, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 import {useFocusEffect, useIsFocused} from '@react-navigation/native'
 import {useQueryClient} from '@tanstack/react-query'
 
+import {useAnalytics} from '#/lib/analytics/analytics'
 import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
+import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
+import {ComposeIcon2} from '#/lib/icons'
+import {
+  NativeStackScreenProps,
+  NotificationsTabNavigatorParams,
+} from '#/lib/routes/types'
+import {s} from '#/lib/styles'
 import {logger} from '#/logger'
 import {isNative} from '#/platform/detection'
 import {emitSoftReset, listenSoftReset} from '#/state/events'
@@ -17,37 +25,32 @@ import {
 import {truncateAndInvalidate} from '#/state/queries/util'
 import {useSetMinimalShellMode} from '#/state/shell'
 import {useComposerControls} from '#/state/shell/composer'
-import {useAnalytics} from 'lib/analytics/analytics'
-import {usePalette} from 'lib/hooks/usePalette'
-import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
-import {ComposeIcon2} from 'lib/icons'
-import {
-  NativeStackScreenProps,
-  NotificationsTabNavigatorParams,
-} from 'lib/routes/types'
-import {colors, s} from 'lib/styles'
-import {TextLink} from 'view/com/util/Link'
+import {Feed} from '#/view/com/notifications/Feed'
+import {FAB} from '#/view/com/util/fab/FAB'
+import {MainScrollProvider} from '#/view/com/util/MainScrollProvider'
+import {ViewHeader} from '#/view/com/util/ViewHeader'
 import {ListMethods} from 'view/com/util/List'
 import {LoadLatestBtn} from 'view/com/util/load-latest/LoadLatestBtn'
 import {CenteredView} from 'view/com/util/Views'
+import {atoms as a, useTheme} from '#/alf'
+import {Button} from '#/components/Button'
+import {SettingsGear2_Stroke2_Corner0_Rounded as SettingsIcon} from '#/components/icons/SettingsGear2'
+import {Link} from '#/components/Link'
 import {Loader} from '#/components/Loader'
-import {Feed} from '../com/notifications/Feed'
-import {FAB} from '../com/util/fab/FAB'
-import {MainScrollProvider} from '../com/util/MainScrollProvider'
-import {ViewHeader} from '../com/util/ViewHeader'
+import {Text} from '#/components/Typography'
 
 type Props = NativeStackScreenProps<
   NotificationsTabNavigatorParams,
   'Notifications'
 >
-export function NotificationsScreen({}: Props) {
+export function NotificationsScreen({route: {params}}: Props) {
   const {_} = useLingui()
   const setMinimalShellMode = useSetMinimalShellMode()
   const [isScrolledDown, setIsScrolledDown] = React.useState(false)
   const [isLoadingLatest, setIsLoadingLatest] = React.useState(false)
   const scrollElRef = React.useRef<ListMethods>(null)
   const {screen} = useAnalytics()
-  const pal = usePalette('default')
+  const t = useTheme()
   const {isDesktop} = useWebMediaQueries()
   const queryClient = useQueryClient()
   const unreadNotifs = useUnreadNotifications()
@@ -109,56 +112,87 @@ export function NotificationsScreen({}: Props) {
     return listenSoftReset(onPressLoadLatest)
   }, [onPressLoadLatest, isScreenFocused])
 
+  const renderButton = useCallback(() => {
+    return (
+      <Link
+        to="/notifications/settings"
+        label={_(msg`Notification settings`)}
+        size="small"
+        variant="ghost"
+        color="secondary"
+        shape="square"
+        style={[a.justify_center]}>
+        <SettingsIcon size="md" style={t.atoms.text_contrast_medium} />
+      </Link>
+    )
+  }, [_, t])
+
   const ListHeaderComponent = React.useCallback(() => {
     if (isDesktop) {
       return (
         <View
           style={[
-            pal.view,
-            {
-              flexDirection: 'row',
-              alignItems: 'center',
-              justifyContent: 'space-between',
-              paddingHorizontal: 18,
-              paddingVertical: 12,
-            },
+            t.atoms.bg,
+            a.flex_row,
+            a.align_center,
+            a.justify_between,
+            a.gap_lg,
+            a.px_lg,
+            a.pr_md,
+            a.py_sm,
           ]}>
-          <TextLink
-            type="title-lg"
-            href="/notifications"
-            style={[pal.text, {fontWeight: 'bold'}]}
-            text={
-              <>
-                <Trans>Notifications</Trans>{' '}
+          <Button
+            label={_(msg`Notifications`)}
+            accessibilityHint={_(msg`Refresh notifications`)}
+            onPress={emitSoftReset}>
+            {({hovered, pressed}) => (
+              <Text
+                style={[
+                  a.text_2xl,
+                  a.font_bold,
+                  (hovered || pressed) && a.underline,
+                ]}>
+                <Trans>Notifications</Trans>
                 {hasNew && (
                   <View
                     style={{
+                      left: 4,
                       top: -8,
-                      backgroundColor: colors.blue3,
+                      backgroundColor: t.palette.primary_500,
                       width: 8,
                       height: 8,
                       borderRadius: 4,
                     }}
                   />
                 )}
-              </>
-            }
-            onPress={emitSoftReset}
-          />
-          {isLoadingLatest ? <Loader size="md" /> : <></>}
+              </Text>
+            )}
+          </Button>
+          <View style={[a.flex_row, a.align_center, a.gap_sm]}>
+            {isLoadingLatest ? <Loader size="md" /> : <></>}
+            {renderButton()}
+          </View>
         </View>
       )
     }
     return <></>
-  }, [isDesktop, pal, hasNew, isLoadingLatest])
+  }, [isDesktop, t, hasNew, renderButton, _, isLoadingLatest])
 
   const renderHeaderSpinner = React.useCallback(() => {
     return (
-      <View style={{width: 30, height: 20, alignItems: 'flex-end'}}>
+      <View
+        style={[
+          {width: 30, height: 20},
+          a.flex_row,
+          a.align_center,
+          a.justify_end,
+          a.gap_md,
+        ]}>
         {isLoadingLatest ? <Loader width={20} /> : <></>}
+        {renderButton()}
       </View>
     )
-  }, [isLoadingLatest])
+  }, [renderButton, isLoadingLatest])
 
   return (
     <CenteredView
@@ -176,6 +210,7 @@ export function NotificationsScreen({}: Props) {
           onScrolledDownChange={setIsScrolledDown}
           scrollElRef={scrollElRef}
           ListHeaderComponent={ListHeaderComponent}
+          overridePriorityNotifications={params?.show === 'all'}
         />
       </MainScrollProvider>
       {(isScrolledDown || hasNew) && (
diff --git a/src/view/screens/NotificationsSettings.tsx b/src/view/screens/NotificationsSettings.tsx
new file mode 100644
index 000000000..2716a07f9
--- /dev/null
+++ b/src/view/screens/NotificationsSettings.tsx
@@ -0,0 +1,94 @@
+import React from 'react'
+import {View} from 'react-native'
+import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
+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 {useNotificationsSettingsMutation} from '#/state/queries/notifications/settings'
+import {ViewHeader} from '#/view/com/util/ViewHeader'
+import {CenteredView} from '#/view/com/util/Views'
+import {atoms as a, useTheme} from '#/alf'
+import {Error} from '#/components/Error'
+import * as Toggle from '#/components/forms/Toggle'
+import {Loader} from '#/components/Loader'
+import {Text} from '#/components/Typography'
+
+type Props = NativeStackScreenProps<AllNavigatorParams, 'NotificationsSettings'>
+export function NotificationsSettingsScreen({}: Props) {
+  const {_} = useLingui()
+  const t = useTheme()
+
+  const {data, isError: isQueryError, refetch} = useNotificationFeedQuery()
+  const serverPriority = data?.pages.at(0)?.priority
+
+  const {
+    mutate: onChangePriority,
+    isPending: isMutationPending,
+    variables,
+  } = useNotificationsSettingsMutation()
+
+  const priority = isMutationPending
+    ? variables[0] === 'enabled'
+    : serverPriority
+
+  return (
+    <CenteredView style={a.flex_1} sideBorders>
+      <ViewHeader
+        title={_(msg`Notification Settings`)}
+        showOnDesktop
+        showBorder
+      />
+      {isQueryError ? (
+        <Error
+          title={_(msg`Oops!`)}
+          message={_(msg`Something went wrong!`)}
+          onRetry={refetch}
+          sideBorders={false}
+        />
+      ) : (
+        <View style={[a.p_lg, a.gap_md]}>
+          <Text style={[a.text_lg, a.font_bold]}>
+            <FontAwesomeIcon icon="flask" style={t.atoms.text} />{' '}
+            <Trans>Notification filters</Trans>
+          </Text>
+          <Toggle.Group
+            label={_(msg`Priority notifications`)}
+            type="checkbox"
+            values={priority ? ['enabled'] : []}
+            onChange={onChangePriority}
+            disabled={typeof priority !== 'boolean' || isMutationPending}>
+            <View>
+              <Toggle.Item
+                name="enabled"
+                label={_(msg`Enable priority notifications`)}
+                style={[a.justify_between, a.py_sm]}>
+                <Toggle.LabelText>
+                  <Trans>Enable priority notifications</Trans>
+                </Toggle.LabelText>
+                {!data ? <Loader size="md" /> : <Toggle.Platform />}
+              </Toggle.Item>
+            </View>
+          </Toggle.Group>
+          <View
+            style={[
+              a.mt_sm,
+              a.px_xl,
+              a.py_lg,
+              a.rounded_md,
+              t.atoms.bg_contrast_25,
+            ]}>
+            <Text style={[t.atoms.text_contrast_high, a.leading_snug]}>
+              <Trans>
+                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.
+              </Trans>
+            </Text>
+          </View>
+        </View>
+      )}
+    </CenteredView>
+  )
+}