From c236eb15b167fd6bdf8b409f65662301afe0b874 Mon Sep 17 00:00:00 2001 From: Ian Philips Date: Tue, 19 Jul 2022 09:04:47 -0600 Subject: [PATCH] Cache notifs in local, gives instant load of old notifs (#662) * Cache notifs in local, gives instant load of old notifs * Small refactor, add ss auth * unused vars * Add back in replaceAll * Save all notifs * Memoize paginated notifs * Replace all => replace with regexp --- web/hooks/use-notifications.ts | 52 ++++++------- web/lib/firebase/auth.ts | 2 +- web/pages/notifications.tsx | 132 ++++++++++++++++++++++++--------- 3 files changed, 123 insertions(+), 63 deletions(-) diff --git a/web/hooks/use-notifications.ts b/web/hooks/use-notifications.ts index f5502b85..b9bef469 100644 --- a/web/hooks/use-notifications.ts +++ b/web/hooks/use-notifications.ts @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react' +import { useEffect, useMemo, useState } from 'react' import { notification_subscribe_types, PrivateUser } from 'common/user' import { Notification } from 'common/notification' import { @@ -6,7 +6,7 @@ import { listenForNotifications, } from 'web/lib/firebase/notifications' import { groupBy, map } from 'lodash' -import { useFirestoreQuery } from '@react-query-firebase/firestore' +import { useFirestoreQueryData } from '@react-query-firebase/firestore' import { NOTIFICATIONS_PER_PAGE } from 'web/pages/notifications' export type NotificationGroup = { @@ -19,36 +19,38 @@ export type NotificationGroup = { // For some reason react-query subscriptions don't actually listen for notifications // Use useUnseenPreferredNotificationGroups to listen for new notifications -export function usePreferredGroupedNotifications(privateUser: PrivateUser) { - const [notificationGroups, setNotificationGroups] = useState< - NotificationGroup[] | undefined - >(undefined) - const [notifications, setNotifications] = useState([]) - const key = `notifications-${privateUser.id}-all` - - const result = useFirestoreQuery([key], getNotificationsQuery(privateUser.id)) - useEffect(() => { - if (result.isLoading) return - if (!result.data) return setNotifications([]) - const notifications = result.data.docs.map( - (doc) => doc.data() as Notification - ) +export function usePreferredGroupedNotifications( + privateUser: PrivateUser, + cachedNotifications?: Notification[] +) { + const result = useFirestoreQueryData( + ['notifications-all', privateUser.id], + getNotificationsQuery(privateUser.id) + ) + const notifications = useMemo(() => { + if (result.isLoading) return cachedNotifications ?? [] + if (!result.data) return cachedNotifications ?? [] + const notifications = result.data as Notification[] const notificationsToShow = getAppropriateNotifications( notifications, privateUser.notificationPreferences ).filter((n) => !n.isSeenOnHref) - setNotifications(notificationsToShow) - }, [privateUser.notificationPreferences, result.data, result.isLoading]) + const cachedIds = cachedNotifications?.map((n) => n.id) + if (notificationsToShow.some((n) => !cachedIds?.includes(n.id))) { + return notificationsToShow + } + return cachedNotifications + }, [ + cachedNotifications, + privateUser.notificationPreferences, + result.data, + result.isLoading, + ]) - useEffect(() => { - if (!notifications) return - - const groupedNotifications = groupNotifications(notifications) - setNotificationGroups(groupedNotifications) + return useMemo(() => { + if (notifications) return groupNotifications(notifications) }, [notifications]) - - return notificationGroups } export function useUnseenPreferredNotificationGroups(privateUser: PrivateUser) { diff --git a/web/lib/firebase/auth.ts b/web/lib/firebase/auth.ts index d1c440ec..b6daea6e 100644 --- a/web/lib/firebase/auth.ts +++ b/web/lib/firebase/auth.ts @@ -6,7 +6,7 @@ const TOKEN_KINDS = ['refresh', 'id'] as const type TokenKind = typeof TOKEN_KINDS[number] const getAuthCookieName = (kind: TokenKind) => { - const suffix = `${PROJECT_ID}_${kind}`.toUpperCase().replaceAll('-', '_') + const suffix = `${PROJECT_ID}_${kind}`.toUpperCase().replace(/-/g, '_') return `FIREBASE_TOKEN_${suffix}` } diff --git a/web/pages/notifications.tsx b/web/pages/notifications.tsx index 9166109f..7500c2a8 100644 --- a/web/pages/notifications.tsx +++ b/web/pages/notifications.tsx @@ -1,6 +1,6 @@ import { Tabs } from 'web/components/layout/tabs' import { usePrivateUser, useUser } from 'web/hooks/use-user' -import React, { useEffect, useState } from 'react' +import React, { useEffect, useMemo, useState } from 'react' import { Notification, notification_source_types } from 'common/notification' import { Avatar, EmptyAvatar } from 'web/components/avatar' import { Row } from 'web/components/layout/row' @@ -14,9 +14,14 @@ import { MANIFOLD_USERNAME, notification_subscribe_types, PrivateUser, + User, } from 'common/user' import { ChoicesToggleGroup } from 'web/components/choices-toggle-group' -import { listenForPrivateUser, updatePrivateUser } from 'web/lib/firebase/users' +import { + getUser, + listenForPrivateUser, + updatePrivateUser, +} from 'web/lib/firebase/users' import { LoadingIndicator } from 'web/components/loading-indicator' import clsx from 'clsx' import { RelativeTimestamp } from 'web/components/relative-timestamp' @@ -43,14 +48,38 @@ import { track } from '@amplitude/analytics-browser' import { Pagination } from 'web/components/pagination' import { useWindowSize } from 'web/hooks/use-window-size' import Router from 'next/router' +import { safeLocalStorage } from 'web/lib/util/local' +import { + getServerAuthenticatedUid, + redirectIfLoggedOut, +} from 'web/lib/firebase/server-auth' export const NOTIFICATIONS_PER_PAGE = 30 const MULTIPLE_USERS_KEY = 'multipleUsers' const HIGHLIGHT_CLASS = 'bg-indigo-50' -export default function Notifications() { - const user = useUser() +export const getServerSideProps = redirectIfLoggedOut('/', async (ctx) => { + const uid = await getServerAuthenticatedUid(ctx) + if (!uid) { + return { props: { user: null } } + } + const user = await getUser(uid) + return { props: { user } } +}) + +export default function Notifications(props: { user: User }) { + const { user } = props const privateUser = usePrivateUser(user?.id) + const local = safeLocalStorage() + let localNotifications = [] as Notification[] + const localSavedNotificationGroups = local?.getItem('notification-groups') + let localNotificationGroups = [] as NotificationGroup[] + if (localSavedNotificationGroups) { + localNotificationGroups = JSON.parse(localSavedNotificationGroups) + localNotifications = localNotificationGroups + .map((g) => g.notifications) + .flat() + } if (!user) return return ( @@ -67,7 +96,16 @@ export default function Notifications() { { title: 'Notifications', content: privateUser ? ( - + + ) : localNotifications && localNotifications.length > 0 ? ( +
+ +
) : ( ), @@ -88,39 +126,13 @@ export default function Notifications() { ) } -function NotificationsList(props: { privateUser: PrivateUser }) { - const { privateUser } = props - const [page, setPage] = useState(0) - const allGroupedNotifications = usePreferredGroupedNotifications(privateUser) - const [paginatedGroupedNotifications, setPaginatedGroupedNotifications] = - useState(undefined) - - useEffect(() => { - if (!allGroupedNotifications) return - const start = page * NOTIFICATIONS_PER_PAGE - const end = start + NOTIFICATIONS_PER_PAGE - const maxNotificationsToShow = allGroupedNotifications.slice(start, end) - const remainingNotification = allGroupedNotifications.slice(end) - for (const notification of remainingNotification) { - if (notification.isSeen) break - else setNotificationsAsSeen(notification.notifications) - } - setPaginatedGroupedNotifications(maxNotificationsToShow) - }, [allGroupedNotifications, page]) - - if (!paginatedGroupedNotifications || !allGroupedNotifications) - return - +function RenderNotificationGroups(props: { + notificationGroups: NotificationGroup[] +}) { + const { notificationGroups } = props return ( -
- {paginatedGroupedNotifications.length === 0 && ( -
- You don't have any notifications. Try changing your settings to see - more. -
- )} - - {paginatedGroupedNotifications.map((notification) => + <> + {notificationGroups.map((notification) => notification.type === 'income' ? ( ) )} + + ) +} + +function NotificationsList(props: { + privateUser: PrivateUser + cachedNotifications: Notification[] +}) { + const { privateUser, cachedNotifications } = props + const [page, setPage] = useState(0) + const allGroupedNotifications = usePreferredGroupedNotifications( + privateUser, + cachedNotifications + ) + const paginatedGroupedNotifications = useMemo(() => { + if (!allGroupedNotifications) return + const start = page * NOTIFICATIONS_PER_PAGE + const end = start + NOTIFICATIONS_PER_PAGE + const maxNotificationsToShow = allGroupedNotifications.slice(start, end) + const remainingNotification = allGroupedNotifications.slice(end) + for (const notification of remainingNotification) { + if (notification.isSeen) break + else setNotificationsAsSeen(notification.notifications) + } + const local = safeLocalStorage() + local?.setItem( + 'notification-groups', + JSON.stringify(maxNotificationsToShow) + ) + return maxNotificationsToShow + }, [allGroupedNotifications, page]) + + if (!paginatedGroupedNotifications || !allGroupedNotifications) return
+ + return ( +
+ {paginatedGroupedNotifications.length === 0 && ( +
+ You don't have any notifications. Try changing your settings to see + more. +
+ )} + + {paginatedGroupedNotifications.length > 0 && allGroupedNotifications.length > NOTIFICATIONS_PER_PAGE && (