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
This commit is contained in:
		
							parent
							
								
									2bae7dc200
								
							
						
					
					
						commit
						c236eb15b1
					
				|  | @ -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<Notification[]>([]) | ||||
|   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) { | ||||
|  |  | |||
|  | @ -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}` | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -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 <Custom404 /> | ||||
|   return ( | ||||
|  | @ -67,7 +96,16 @@ export default function Notifications() { | |||
|               { | ||||
|                 title: 'Notifications', | ||||
|                 content: privateUser ? ( | ||||
|                   <NotificationsList privateUser={privateUser} /> | ||||
|                   <NotificationsList | ||||
|                     privateUser={privateUser} | ||||
|                     cachedNotifications={localNotifications} | ||||
|                   /> | ||||
|                 ) : localNotifications && localNotifications.length > 0 ? ( | ||||
|                   <div className={'min-h-[100vh]'}> | ||||
|                     <RenderNotificationGroups | ||||
|                       notificationGroups={localNotificationGroups} | ||||
|                     /> | ||||
|                   </div> | ||||
|                 ) : ( | ||||
|                   <LoadingIndicator /> | ||||
|                 ), | ||||
|  | @ -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<NotificationGroup[] | undefined>(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 <LoadingIndicator /> | ||||
| 
 | ||||
| function RenderNotificationGroups(props: { | ||||
|   notificationGroups: NotificationGroup[] | ||||
| }) { | ||||
|   const { notificationGroups } = props | ||||
|   return ( | ||||
|     <div className={'min-h-[100vh]'}> | ||||
|       {paginatedGroupedNotifications.length === 0 && ( | ||||
|         <div className={'mt-2'}> | ||||
|           You don't have any notifications. Try changing your settings to see | ||||
|           more. | ||||
|         </div> | ||||
|       )} | ||||
| 
 | ||||
|       {paginatedGroupedNotifications.map((notification) => | ||||
|     <> | ||||
|       {notificationGroups.map((notification) => | ||||
|         notification.type === 'income' ? ( | ||||
|           <IncomeNotificationGroupItem | ||||
|             notificationGroup={notification} | ||||
|  | @ -138,6 +150,52 @@ function NotificationsList(props: { privateUser: PrivateUser }) { | |||
|           /> | ||||
|         ) | ||||
|       )} | ||||
|     </> | ||||
|   ) | ||||
| } | ||||
| 
 | ||||
| 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 <div /> | ||||
| 
 | ||||
|   return ( | ||||
|     <div className={'min-h-[100vh]'}> | ||||
|       {paginatedGroupedNotifications.length === 0 && ( | ||||
|         <div className={'mt-2'}> | ||||
|           You don't have any notifications. Try changing your settings to see | ||||
|           more. | ||||
|         </div> | ||||
|       )} | ||||
| 
 | ||||
|       <RenderNotificationGroups | ||||
|         notificationGroups={paginatedGroupedNotifications} | ||||
|       /> | ||||
|       {paginatedGroupedNotifications.length > 0 && | ||||
|         allGroupedNotifications.length > NOTIFICATIONS_PER_PAGE && ( | ||||
|           <Pagination | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	Block a user