about summary refs log tree commit diff
path: root/src/view
diff options
context:
space:
mode:
Diffstat (limited to 'src/view')
-rw-r--r--src/view/com/notifications/NotificationFeed.tsx33
-rw-r--r--src/view/com/notifications/NotificationFeedItem.tsx13
-rw-r--r--src/view/com/pager/Pager.tsx14
-rw-r--r--src/view/screens/DebugMod.tsx8
-rw-r--r--src/view/screens/Notifications.tsx256
5 files changed, 211 insertions, 113 deletions
diff --git a/src/view/com/notifications/NotificationFeed.tsx b/src/view/com/notifications/NotificationFeed.tsx
index 5168933ae..0b814e68d 100644
--- a/src/view/com/notifications/NotificationFeed.tsx
+++ b/src/view/com/notifications/NotificationFeed.tsx
@@ -9,13 +9,11 @@ import {msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 
 import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
-import {usePalette} from '#/lib/hooks/usePalette'
 import {cleanError} from '#/lib/strings/errors'
 import {s} from '#/lib/styles'
 import {logger} from '#/logger'
 import {useModerationOpts} from '#/state/preferences/moderation-opts'
 import {useNotificationFeedQuery} from '#/state/queries/notifications/feed'
-import {useUnreadNotificationsApi} from '#/state/queries/notifications/unread'
 import {EmptyState} from '#/view/com/util/EmptyState'
 import {ErrorMessage} from '#/view/com/util/error/ErrorMessage'
 import {List, ListRef} from '#/view/com/util/List'
@@ -28,26 +26,26 @@ const LOAD_MORE_ERROR_ITEM = {_reactKey: '__load_more_error__'}
 const LOADING_ITEM = {_reactKey: '__loading__'}
 
 export function NotificationFeed({
+  filter,
+  enabled,
   scrollElRef,
   onPressTryAgain,
   onScrolledDownChange,
   ListHeaderComponent,
-  overridePriorityNotifications,
+  refreshNotifications,
 }: {
+  filter: 'all' | 'mentions'
+  enabled: boolean
   scrollElRef?: ListRef
   onPressTryAgain?: () => void
   onScrolledDownChange: (isScrolledDown: boolean) => void
   ListHeaderComponent?: () => JSX.Element
-  overridePriorityNotifications?: boolean
+  refreshNotifications: () => Promise<void>
 }) {
   const initialNumToRender = useInitialNumToRender()
-
   const [isPTRing, setIsPTRing] = React.useState(false)
-  const pal = usePalette('default')
-
   const {_} = useLingui()
   const moderationOpts = useModerationOpts()
-  const {checkUnread} = useUnreadNotificationsApi()
   const {
     data,
     isFetching,
@@ -58,8 +56,8 @@ export function NotificationFeed({
     isFetchingNextPage,
     fetchNextPage,
   } = useNotificationFeedQuery({
-    enabled: !!moderationOpts,
-    overridePriorityNotifications,
+    enabled: enabled && !!moderationOpts,
+    filter,
   })
   const isEmpty = !isFetching && !data?.pages[0]?.items.length
 
@@ -85,7 +83,7 @@ export function NotificationFeed({
   const onRefresh = React.useCallback(async () => {
     try {
       setIsPTRing(true)
-      await checkUnread({invalidate: true})
+      await refreshNotifications()
     } catch (err) {
       logger.error('Failed to refresh notifications feed', {
         message: err,
@@ -93,7 +91,7 @@ export function NotificationFeed({
     } finally {
       setIsPTRing(false)
     }
-  }, [checkUnread, setIsPTRing])
+  }, [refreshNotifications, setIsPTRing])
 
   const onEndReached = React.useCallback(async () => {
     if (isFetching || !hasNextPage || isError) return
@@ -129,21 +127,18 @@ export function NotificationFeed({
           />
         )
       } else if (item === LOADING_ITEM) {
-        return (
-          <View style={[pal.border]}>
-            <NotificationFeedLoadingPlaceholder />
-          </View>
-        )
+        return <NotificationFeedLoadingPlaceholder />
       }
       return (
         <NotificationFeedItem
+          highlightUnread={filter === 'all'}
           item={item}
           moderationOpts={moderationOpts!}
-          hideTopBorder={index === 0}
+          hideTopBorder={index === 0 && item.notification.isRead}
         />
       )
     },
-    [moderationOpts, _, onPressRetryLoadMore, pal.border],
+    [moderationOpts, _, onPressRetryLoadMore, filter],
   )
 
   const FeedFooter = React.useCallback(
diff --git a/src/view/com/notifications/NotificationFeedItem.tsx b/src/view/com/notifications/NotificationFeedItem.tsx
index 4902e66bc..1267ce089 100644
--- a/src/view/com/notifications/NotificationFeedItem.tsx
+++ b/src/view/com/notifications/NotificationFeedItem.tsx
@@ -79,10 +79,12 @@ interface Author {
 let NotificationFeedItem = ({
   item,
   moderationOpts,
+  highlightUnread,
   hideTopBorder,
 }: {
   item: FeedNotification
   moderationOpts: ModerationOpts
+  highlightUnread: boolean
   hideTopBorder?: boolean
 }): React.ReactNode => {
   const queryClient = useQueryClient()
@@ -151,6 +153,7 @@ let NotificationFeedItem = ({
     if (!item.subject) {
       return null
     }
+    const isHighlighted = highlightUnread && !item.notification.isRead
     return (
       <Link
         testID={`feedItem-by-${item.notification.author.handle}`}
@@ -160,12 +163,10 @@ let NotificationFeedItem = ({
         <Post
           post={item.subject}
           style={
-            item.notification.isRead
-              ? undefined
-              : {
-                  backgroundColor: pal.colors.unreadNotifBg,
-                  borderColor: pal.colors.unreadNotifBorder,
-                }
+            isHighlighted && {
+              backgroundColor: pal.colors.unreadNotifBg,
+              borderColor: pal.colors.unreadNotifBorder,
+            }
           }
           hideTopBorder={hideTopBorder}
         />
diff --git a/src/view/com/pager/Pager.tsx b/src/view/com/pager/Pager.tsx
index 2c0bbee52..b3f936ddc 100644
--- a/src/view/com/pager/Pager.tsx
+++ b/src/view/com/pager/Pager.tsx
@@ -136,12 +136,14 @@ export const Pager = forwardRef<PagerRef, React.PropsWithChildren<Props>>(
 
     return (
       <View testID={testID} style={[a.flex_1, native(a.overflow_hidden)]}>
-        {renderTabBar({
-          selectedPage,
-          onSelect: onTabBarSelect,
-          dragProgress,
-          dragState,
-        })}
+        <View style={a.z_10 /* Let tabbar bottom border cover the glimmer */}>
+          {renderTabBar({
+            selectedPage,
+            onSelect: onTabBarSelect,
+            dragProgress,
+            dragState,
+          })}
+        </View>
         <GestureDetector gesture={nativeGesture}>
           <AnimatedPagerView
             ref={pagerView}
diff --git a/src/view/screens/DebugMod.tsx b/src/view/screens/DebugMod.tsx
index 74a58a56a..4ff0a4b8b 100644
--- a/src/view/screens/DebugMod.tsx
+++ b/src/view/screens/DebugMod.tsx
@@ -872,7 +872,13 @@ function MockNotifItem({
       </P>
     )
   }
-  return <NotificationFeedItem item={notif} moderationOpts={moderationOpts} />
+  return (
+    <NotificationFeedItem
+      item={notif}
+      moderationOpts={moderationOpts}
+      highlightUnread
+    />
+  )
 }
 
 function MockAccountCard({
diff --git a/src/view/screens/Notifications.tsx b/src/view/screens/Notifications.tsx
index 70ab32db0..82c68dde6 100644
--- a/src/view/screens/Notifications.tsx
+++ b/src/view/screens/Notifications.tsx
@@ -13,7 +13,7 @@ import {
 } from '#/lib/routes/types'
 import {s} from '#/lib/styles'
 import {logger} from '#/logger'
-import {isNative, isWeb} from '#/platform/detection'
+import {isNative} from '#/platform/detection'
 import {emitSoftReset, listenSoftReset} from '#/state/events'
 import {RQKEY as NOTIFS_RQKEY} from '#/state/queries/notifications/feed'
 import {
@@ -24,35 +24,173 @@ import {truncateAndInvalidate} from '#/state/queries/util'
 import {useSetMinimalShellMode} from '#/state/shell'
 import {useComposerControls} from '#/state/shell/composer'
 import {NotificationFeed} from '#/view/com/notifications/NotificationFeed'
+import {Pager} from '#/view/com/pager/Pager'
+import {TabBar} from '#/view/com/pager/TabBar'
 import {FAB} from '#/view/com/util/fab/FAB'
 import {ListMethods} from '#/view/com/util/List'
 import {LoadLatestBtn} from '#/view/com/util/load-latest/LoadLatestBtn'
 import {MainScrollProvider} from '#/view/com/util/MainScrollProvider'
-import {atoms as a, useBreakpoints, useTheme} from '#/alf'
-import {Button, ButtonIcon} from '#/components/Button'
+import {atoms as a} from '#/alf'
+import {web} from '#/alf'
+import {ButtonIcon} from '#/components/Button'
 import {SettingsGear2_Stroke2_Corner0_Rounded as SettingsIcon} from '#/components/icons/SettingsGear2'
 import * as Layout from '#/components/Layout'
 import {Link} from '#/components/Link'
 import {Loader} from '#/components/Loader'
 
+// We don't currently persist this across reloads since
+// you gotta visit All to clear the badge anyway.
+// But let's at least persist it during the sesssion.
+let lastActiveTab = 0
+
 type Props = NativeStackScreenProps<
   NotificationsTabNavigatorParams,
   'Notifications'
 >
-export function NotificationsScreen({route: {params}}: Props) {
-  const t = useTheme()
-  const {gtTablet} = useBreakpoints()
+export function NotificationsScreen({}: Props) {
+  const {_} = useLingui()
+  const {openComposer} = useComposerControls()
+  const unreadNotifs = useUnreadNotifications()
+  const hasNew = !!unreadNotifs
+  const {checkUnread: checkUnreadAll} = useUnreadNotificationsApi()
+  const [isLoadingAll, setIsLoadingAll] = React.useState(false)
+  const [isLoadingMentions, setIsLoadingMentions] = React.useState(false)
+  const initialActiveTab = lastActiveTab
+  const [activeTab, setActiveTab] = React.useState(initialActiveTab)
+  const isLoading = activeTab === 0 ? isLoadingAll : isLoadingMentions
+
+  const onPageSelected = React.useCallback(
+    (index: number) => {
+      setActiveTab(index)
+      lastActiveTab = index
+    },
+    [setActiveTab],
+  )
+
+  const queryClient = useQueryClient()
+  const checkUnreadMentions = React.useCallback(
+    async ({invalidate}: {invalidate: boolean}) => {
+      if (invalidate) {
+        return truncateAndInvalidate(queryClient, NOTIFS_RQKEY('mentions'))
+      } else {
+        // Background polling is not implemented for the mentions tab.
+        // Just ignore it.
+      }
+    },
+    [queryClient],
+  )
+
+  const sections = React.useMemo(() => {
+    return [
+      {
+        title: _(msg`All`),
+        component: (
+          <NotificationsTab
+            filter="all"
+            isActive={activeTab === 0}
+            isLoading={isLoadingAll}
+            hasNew={hasNew}
+            setIsLoadingLatest={setIsLoadingAll}
+            checkUnread={checkUnreadAll}
+          />
+        ),
+      },
+      {
+        title: _(msg`Mentions`),
+        component: (
+          <NotificationsTab
+            filter="mentions"
+            isActive={activeTab === 1}
+            isLoading={isLoadingMentions}
+            hasNew={false /* We don't know for sure */}
+            setIsLoadingLatest={setIsLoadingMentions}
+            checkUnread={checkUnreadMentions}
+          />
+        ),
+      },
+    ]
+  }, [
+    _,
+    hasNew,
+    checkUnreadAll,
+    checkUnreadMentions,
+    activeTab,
+    isLoadingAll,
+    isLoadingMentions,
+  ])
+
+  return (
+    <Layout.Screen testID="notificationsScreen">
+      <Layout.Header.Outer noBottomBorder>
+        <Layout.Header.MenuButton />
+        <Layout.Header.Content>
+          <Layout.Header.TitleText>
+            <Trans>Notifications</Trans>
+          </Layout.Header.TitleText>
+        </Layout.Header.Content>
+        <Layout.Header.Slot>
+          <Link
+            to="/notifications/settings"
+            label={_(msg`Notification settings`)}
+            size="small"
+            variant="ghost"
+            color="secondary"
+            shape="round"
+            style={[a.justify_center]}>
+            <ButtonIcon icon={isLoading ? Loader : SettingsIcon} size="lg" />
+          </Link>
+        </Layout.Header.Slot>
+      </Layout.Header.Outer>
+      <Pager
+        onPageSelected={onPageSelected}
+        renderTabBar={props => (
+          <Layout.Center style={web([a.sticky, a.z_10, {top: 0}])}>
+            <TabBar
+              {...props}
+              items={sections.map(section => section.title)}
+              onPressSelected={() => emitSoftReset()}
+            />
+          </Layout.Center>
+        )}
+        initialPage={initialActiveTab}>
+        {sections.map((section, i) => (
+          <View key={i}>{section.component}</View>
+        ))}
+      </Pager>
+      <FAB
+        testID="composeFAB"
+        onPress={() => openComposer({})}
+        icon={<ComposeIcon2 strokeWidth={1.5} size={29} style={s.white} />}
+        accessibilityRole="button"
+        accessibilityLabel={_(msg`New post`)}
+        accessibilityHint=""
+      />
+    </Layout.Screen>
+  )
+}
+
+function NotificationsTab({
+  filter,
+  isActive,
+  isLoading,
+  hasNew,
+  checkUnread,
+  setIsLoadingLatest,
+}: {
+  filter: 'all' | 'mentions'
+  isActive: boolean
+  isLoading: boolean
+  hasNew: boolean
+  checkUnread: ({invalidate}: {invalidate: boolean}) => Promise<void>
+  setIsLoadingLatest: (v: boolean) => void
+}) {
   const {_} = useLingui()
   const setMinimalShellMode = useSetMinimalShellMode()
   const [isScrolledDown, setIsScrolledDown] = React.useState(false)
-  const [isLoadingLatest, setIsLoadingLatest] = React.useState(false)
   const scrollElRef = React.useRef<ListMethods>(null)
   const queryClient = useQueryClient()
-  const unreadNotifs = useUnreadNotifications()
-  const unreadApi = useUnreadNotificationsApi()
-  const hasNew = !!unreadNotifs
   const isScreenFocused = useIsFocused()
-  const {openComposer} = useComposerControls()
+  const isFocusedAndActive = isScreenFocused && isActive
 
   // event handlers
   // =
@@ -65,16 +203,23 @@ export function NotificationsScreen({route: {params}}: Props) {
     scrollToTop()
     if (hasNew) {
       // render what we have now
-      truncateAndInvalidate(queryClient, NOTIFS_RQKEY())
-    } else {
+      truncateAndInvalidate(queryClient, NOTIFS_RQKEY(filter))
+    } else if (!isLoading) {
       // check with the server
       setIsLoadingLatest(true)
-      unreadApi
-        .checkUnread({invalidate: true})
+      checkUnread({invalidate: true})
         .catch(() => undefined)
         .then(() => setIsLoadingLatest(false))
     }
-  }, [scrollToTop, queryClient, unreadApi, hasNew, setIsLoadingLatest])
+  }, [
+    scrollToTop,
+    queryClient,
+    checkUnread,
+    hasNew,
+    isLoading,
+    setIsLoadingLatest,
+    filter,
+  ])
 
   const onFocusCheckLatest = useNonReactiveCallback(() => {
     // on focus, check for latest, but only invalidate if the user
@@ -87,79 +232,36 @@ export function NotificationsScreen({route: {params}}: Props) {
       // we're just going to look it up synchronously.
       currentIsScrolledDown = window.scrollY > 200
     }
-    unreadApi.checkUnread({invalidate: !currentIsScrolledDown})
+    checkUnread({invalidate: !currentIsScrolledDown})
   })
 
   // on-visible setup
   // =
   useFocusEffect(
     React.useCallback(() => {
-      setMinimalShellMode(false)
-      logger.debug('NotificationsScreen: Focus')
-      onFocusCheckLatest()
-    }, [setMinimalShellMode, onFocusCheckLatest]),
+      if (isFocusedAndActive) {
+        setMinimalShellMode(false)
+        logger.debug('NotificationsScreen: Focus')
+        onFocusCheckLatest()
+      }
+    }, [setMinimalShellMode, onFocusCheckLatest, isFocusedAndActive]),
   )
   React.useEffect(() => {
-    if (!isScreenFocused) {
+    if (!isFocusedAndActive) {
       return
     }
     return listenSoftReset(onPressLoadLatest)
-  }, [onPressLoadLatest, isScreenFocused])
+  }, [onPressLoadLatest, isFocusedAndActive])
 
   return (
-    <Layout.Screen testID="notificationsScreen">
-      <Layout.Header.Outer>
-        <Layout.Header.MenuButton />
-        <Layout.Header.Content>
-          <Button
-            label={_(msg`Notifications`)}
-            accessibilityHint={_(msg`Refresh notifications`)}
-            onPress={emitSoftReset}
-            style={[a.justify_start]}>
-            {({hovered}) => (
-              <Layout.Header.TitleText
-                style={[a.w_full, hovered && a.underline]}>
-                <Trans>Notifications</Trans>
-                {isWeb && gtTablet && hasNew && (
-                  <View
-                    style={[
-                      a.rounded_full,
-                      {
-                        width: 8,
-                        height: 8,
-                        bottom: 3,
-                        left: 6,
-                        backgroundColor: t.palette.primary_500,
-                      },
-                    ]}
-                  />
-                )}
-              </Layout.Header.TitleText>
-            )}
-          </Button>
-        </Layout.Header.Content>
-        <Layout.Header.Slot>
-          <Link
-            to="/notifications/settings"
-            label={_(msg`Notification settings`)}
-            size="small"
-            variant="ghost"
-            color="secondary"
-            shape="round"
-            style={[a.justify_center]}>
-            <ButtonIcon
-              icon={isLoadingLatest ? Loader : SettingsIcon}
-              size="lg"
-            />
-          </Link>
-        </Layout.Header.Slot>
-      </Layout.Header.Outer>
-
+    <>
       <MainScrollProvider>
         <NotificationFeed
+          enabled={isFocusedAndActive}
+          filter={filter}
+          refreshNotifications={() => checkUnread({invalidate: true})}
           onScrolledDownChange={setIsScrolledDown}
           scrollElRef={scrollElRef}
-          overridePriorityNotifications={params?.show === 'all'}
         />
       </MainScrollProvider>
       {(isScrolledDown || hasNew) && (
@@ -169,14 +271,6 @@ export function NotificationsScreen({route: {params}}: Props) {
           showIndicator={hasNew}
         />
       )}
-      <FAB
-        testID="composeFAB"
-        onPress={() => openComposer({})}
-        icon={<ComposeIcon2 strokeWidth={1.5} size={29} style={s.white} />}
-        accessibilityRole="button"
-        accessibilityLabel={_(msg`New post`)}
-        accessibilityHint=""
-      />
-    </Layout.Screen>
+    </>
   )
 }