diff --git a/backend/api/src/get-notifications.ts b/backend/api/src/get-notifications.ts
index 400b0d0262..107252b229 100644
--- a/backend/api/src/get-notifications.ts
+++ b/backend/api/src/get-notifications.ts
@@ -6,17 +6,18 @@ export const getNotifications: APIHandler<'get-notifications'> = async (
props,
auth
) => {
- const { limit } = props
+ const { limit, after } = props
const pg = createSupabaseDirectClient()
const query = `
select data from user_notifications
where user_id = $1
+ and ($3 is null or (data->'createdTime')::bigint > $3)
order by (data->'createdTime')::bigint desc
limit $2
`
return await pg.map(
query,
- [auth.uid, limit],
+ [auth.uid, limit, after],
(row) => row.data as Notification
)
}
diff --git a/backend/shared/src/supabase/notifications.ts b/backend/shared/src/supabase/notifications.ts
index fc8f5427ec..e539ab5ce8 100644
--- a/backend/shared/src/supabase/notifications.ts
+++ b/backend/shared/src/supabase/notifications.ts
@@ -27,4 +27,7 @@ export const bulkInsertNotifications = async (
data: n,
}))
)
+ notifications.forEach((notification) =>
+ broadcast(`user-notifications/${notification.userId}`, { notification })
+ )
}
diff --git a/common/src/api/schema.ts b/common/src/api/schema.ts
index 81167425d4..832745caa0 100644
--- a/common/src/api/schema.ts
+++ b/common/src/api/schema.ts
@@ -1327,6 +1327,7 @@ export const API = (_apiTypeCheck = {
returns: [] as Notification[],
props: z
.object({
+ after: z.coerce.number().optional(),
limit: z.coerce.number().gte(0).lte(1000).default(100),
})
.strict(),
diff --git a/web/components/notifications-icon.tsx b/web/components/notifications-icon.tsx
index 9fd0eafb61..43870ac877 100644
--- a/web/components/notifications-icon.tsx
+++ b/web/components/notifications-icon.tsx
@@ -1,82 +1,48 @@
'use client'
import { BellIcon } from '@heroicons/react/outline'
-import { BellIcon as SolidBellIcon } from '@heroicons/react/solid'
import { Row } from 'web/components/layout/row'
-import { useEffect, useState } from 'react'
+import { useEffect } from 'react'
import { usePrivateUser } from 'web/hooks/use-user'
import { useGroupedUnseenNotifications } from 'web/hooks/use-notifications'
import { PrivateUser } from 'common/user'
import { NOTIFICATIONS_PER_PAGE } from './notifications/notification-helpers'
-import {
- notification_source_types,
- NotificationReason,
-} from 'common/notification'
+
import { usePathname } from 'next/navigation'
+import { usePersistentInMemoryState } from 'web/hooks/use-persistent-in-memory-state'
-export function NotificationsIcon(props: {
- className?: string
- selectTypes?: notification_source_types[]
- selectReasons?: NotificationReason[]
-}) {
+export function NotificationsIcon(props: { className?: string }) {
const privateUser = usePrivateUser()
- const { selectTypes, selectReasons, className } = props
+ const { className } = props
return (
- {privateUser && (
-
- )}
+ {privateUser && }
)
}
-export function SolidNotificationsIcon(props: {
- className?: string
- selectTypes?: notification_source_types[]
- selectReasons?: NotificationReason[]
-}) {
- const privateUser = usePrivateUser()
- const { selectTypes, selectReasons, className } = props
- return (
-
- {privateUser && (
-
- )}
-
-
- )
-}
-
-function UnseenNotificationsBubble(props: {
- privateUser: PrivateUser
- selectTypes?: notification_source_types[]
- selectReasons?: NotificationReason[]
-}) {
+function UnseenNotificationsBubble(props: { privateUser: PrivateUser }) {
const pathname = usePathname()
- const { privateUser, selectTypes, selectReasons } = props
- const [seen, setSeen] = useState(false)
- const unseenSourceIdsToNotificationIds =
- useGroupedUnseenNotifications(privateUser.id, selectTypes, selectReasons) ??
- []
+ const { privateUser } = props
+ const [lastSeenTime, setLastSeenTime] = usePersistentInMemoryState(
+ 0,
+ 'notifications-seen-time'
+ )
+ const unseenNotificationGroups =
+ useGroupedUnseenNotifications(privateUser.id) ?? []
- const unseenNotifs = Object.keys(unseenSourceIdsToNotificationIds).length
+ const unseenNotifs = unseenNotificationGroups.filter(
+ (ng) => ng.latestCreatedTime > lastSeenTime
+ ).length
useEffect(() => {
if (pathname?.endsWith('notifications')) {
- setSeen(pathname.endsWith('notifications'))
+ setLastSeenTime(Date.now())
}
- }, [pathname])
+ }, [pathname, unseenNotifs])
- if (unseenNotifs === 0 || seen) {
+ if (unseenNotifs === 0) {
return null
}
diff --git a/web/hooks/use-notifications.ts b/web/hooks/use-notifications.ts
index 4289762016..4e97a8be99 100644
--- a/web/hooks/use-notifications.ts
+++ b/web/hooks/use-notifications.ts
@@ -5,7 +5,7 @@ import {
notification_source_types,
NotificationReason,
} from 'common/notification'
-import { Dictionary, first, groupBy, sortBy } from 'lodash'
+import { Dictionary, first, groupBy, sortBy, uniqBy } from 'lodash'
import { useEffect, useMemo } from 'react'
import { NOTIFICATIONS_PER_PAGE } from 'web/components/notifications/notification-helpers'
import { User } from 'common/user'
@@ -18,53 +18,70 @@ export type NotificationGroup = {
notifications: Notification[]
groupedById: string
isSeen: boolean
+ latestCreatedTime: number
}
-function useNotifications(userId: string, count = 15 * NOTIFICATIONS_PER_PAGE) {
+function useNotifications(
+ userId: string,
+ count = 15 * NOTIFICATIONS_PER_PAGE,
+ newOnly?: boolean
+) {
const [notifications, setNotifications] = usePersistentLocalState<
Notification[] | undefined
>(undefined, 'notifications-' + userId)
+ const [latestCreatedTime, setLatestCreatedTime] = usePersistentLocalState<
+ number | undefined
+ >(undefined, 'latest-notification-time-' + userId)
+
useEffect(() => {
- if (userId)
- api('get-notifications', { limit: count }).then((data) => {
- setNotifications(data)
+ if (userId) {
+ const params = {
+ limit: count,
+ after: newOnly ? latestCreatedTime : undefined,
+ }
+ api('get-notifications', params).then((newData) => {
+ setNotifications((oldData) => {
+ const updatedNotifications = uniqBy(
+ [...newData, ...(oldData ?? [])],
+ 'id'
+ )
+ const newLatestCreatedTime = Math.max(
+ ...updatedNotifications.map((n) => n.createdTime),
+ latestCreatedTime ?? 0
+ )
+ setLatestCreatedTime(newLatestCreatedTime)
+ return updatedNotifications
+ })
})
- }, [userId])
+ }
+ }, [userId, count, newOnly])
useApiSubscription({
topics: [`user-notifications/${userId}`],
onBroadcast: ({ data }) => {
- setNotifications((notifs) => [
- data.notification as Notification,
- ...(notifs ?? []),
- ])
+ console.log('new notification', data)
+ setNotifications((notifs) => {
+ const newNotification = data.notification as Notification
+ setLatestCreatedTime((prevTime) =>
+ Math.max(prevTime ?? 0, newNotification.createdTime)
+ )
+ return [newNotification, ...(notifs ?? [])]
+ })
},
})
return notifications
}
-function useUnseenNotifications(
- userId: string,
- count = NOTIFICATIONS_PER_PAGE
-) {
- const notifs = useNotifications(userId, count)
- return notifs?.filter((n) => !n.isSeen)
-}
+export function useGroupedUnseenNotifications(userId: string) {
+ const unseenNotifs = useNotifications(
+ userId,
+ NOTIFICATIONS_PER_PAGE,
+ true
+ )?.filter((n) => !n.isSeen)
-export function useGroupedUnseenNotifications(
- userId: string,
- selectTypes?: notification_source_types[],
- selectReasons?: NotificationReason[]
-) {
- const notifications = useUnseenNotifications(userId)?.filter(
- (n) =>
- (selectTypes?.includes(n.sourceType) ||
- selectReasons?.includes(n.reason)) ??
- true
- )
return useMemo(() => {
- return notifications ? groupNotificationsForIcon(notifications) : undefined
- }, [notifications])
+ return unseenNotifs ? groupNotificationsForIcon(unseenNotifs) : undefined
+ }, [unseenNotifs?.length])
}
export function useGroupedNotifications(
@@ -128,6 +145,7 @@ const groupNotifications = (
notifications: value,
groupedById: key,
isSeen: value.every((n) => n.isSeen),
+ latestCreatedTime: Math.max(...value.map((n) => n.createdTime)),
}))
}
diff --git a/web/pages/notifications.tsx b/web/pages/notifications.tsx
index a786407b0c..623378ba1d 100644
--- a/web/pages/notifications.tsx
+++ b/web/pages/notifications.tsx
@@ -30,7 +30,7 @@ import {
} from 'web/hooks/use-notifications'
import { useIsPageVisible } from 'web/hooks/use-page-visible'
import { useRedirectIfSignedOut } from 'web/hooks/use-redirect-if-signed-out'
-import { usePrivateUser, useIsAuthorized, useUser } from 'web/hooks/use-user'
+import { usePrivateUser, useUser } from 'web/hooks/use-user'
import { XIcon } from '@heroicons/react/outline'
import { getNativePlatform } from 'web/lib/native/is-native'
import { AppBadgesOrGetAppButton } from 'web/components/buttons/app-badges-or-get-app-button'
@@ -240,7 +240,6 @@ export function NotificationsList(props: {
groupedNotifications,
mostRecentNotification,
} = props
- const isAuthorized = useIsAuthorized()
const [page, setPage] = useState(0)
const user = useUser()
@@ -255,12 +254,8 @@ export function NotificationsList(props: {
// Mark all notifications as seen. Rerun as new notifications come in.
useEffect(() => {
if (!privateUser || !isPageVisible) return
- if (isAuthorized) markAllNotifications({ seen: true })
- groupedNotifications
- ?.map((ng) => ng.notifications)
- .flat()
- .forEach((n) => (!n.isSeen ? (n.isSeen = true) : null))
- }, [privateUser, isPageVisible, mostRecentNotification?.id, isAuthorized])
+ markAllNotifications({ seen: true })
+ }, [privateUser?.id, isPageVisible, mostRecentNotification?.id])
return (