diff --git a/web/components/nav/sidebar.tsx b/web/components/nav/sidebar.tsx
index b9449ea0..0739da75 100644
--- a/web/components/nav/sidebar.tsx
+++ b/web/components/nav/sidebar.tsx
@@ -12,7 +12,7 @@ import {
import clsx from 'clsx'
import Link from 'next/link'
import { useRouter } from 'next/router'
-import { useUser } from 'web/hooks/use-user'
+import { usePrivateUser, useUser } from 'web/hooks/use-user'
import { firebaseLogout, User } from 'web/lib/firebase/users'
import { ManifoldLogo } from './manifold-logo'
import { MenuButton } from './menu'
@@ -26,8 +26,9 @@ import { groupPath } from 'web/lib/firebase/groups'
import { trackCallback, withTracking } from 'web/lib/service/analytics'
import { Group } from 'common/group'
import { Spacer } from '../layout/spacer'
-import { usePreferredNotifications } from 'web/hooks/use-notifications'
+import { useUnseenPreferredNotifications } from 'web/hooks/use-notifications'
import { setNotificationsAsSeen } from 'web/pages/notifications'
+import { PrivateUser } from 'common/lib/user'
function getNavigation() {
return [
@@ -186,6 +187,7 @@ export default function Sidebar(props: { className?: string }) {
const currentPage = router.pathname
const user = useUser()
+ const privateUser = usePrivateUser(user?.id)
const navigationOptions = !user ? signedOutNavigation : getNavigation()
const mobileNavigationOptions = !user
? signedOutMobileNavigation
@@ -220,11 +222,13 @@ export default function Sidebar(props: { className?: string }) {
/>
)}
-
+ {privateUser && (
+
+ )}
{/* Desktop navigation */}
@@ -243,11 +247,13 @@ export default function Sidebar(props: { className?: string }) {
)}
-
+ {privateUser && (
+
+ )}
)
@@ -256,11 +262,10 @@ export default function Sidebar(props: { className?: string }) {
function GroupsList(props: {
currentPage: string
memberItems: Item[]
- user: User | null | undefined
+ privateUser: PrivateUser
}) {
- const { currentPage, memberItems, user } = props
- const preferredNotifications = usePreferredNotifications(user?.id, {
- unseenOnly: true,
+ const { currentPage, memberItems, privateUser } = props
+ const preferredNotifications = useUnseenPreferredNotifications(privateUser, {
customHref: '/group/',
})
diff --git a/web/components/notifications-icon.tsx b/web/components/notifications-icon.tsx
index 8f45a054..5f36bb46 100644
--- a/web/components/notifications-icon.tsx
+++ b/web/components/notifications-icon.tsx
@@ -4,45 +4,57 @@ import { Row } from 'web/components/layout/row'
import { useEffect, useState } from 'react'
import { usePrivateUser, useUser } from 'web/hooks/use-user'
import { useRouter } from 'next/router'
-import { usePreferredGroupedNotifications } from 'web/hooks/use-notifications'
+import { useUnseenPreferredNotificationGroups } from 'web/hooks/use-notifications'
import { NOTIFICATIONS_PER_PAGE } from 'web/pages/notifications'
import { requestBonuses } from 'web/lib/firebase/api-call'
+import { PrivateUser } from 'common/lib/user'
export default function NotificationsIcon(props: { className?: string }) {
const user = useUser()
const privateUser = usePrivateUser(user?.id)
- const notifications = usePreferredGroupedNotifications(privateUser?.id, {
- unseenOnly: true,
- })
- const [seen, setSeen] = useState(false)
useEffect(() => {
- if (!privateUser) return
-
- if (Date.now() - (privateUser.lastTimeCheckedBonuses ?? 0) > 65 * 1000)
- requestBonuses({}).catch((error) => {
- console.log("couldn't get bonuses:", error.message)
- })
- }, [privateUser])
-
- const router = useRouter()
- useEffect(() => {
- if (router.pathname.endsWith('notifications')) return setSeen(true)
- else setSeen(false)
- }, [router.pathname])
+ if (user) {
+ const bonusChecker = setTimeout(() => {
+ requestBonuses({}).catch((error) => {
+ console.log("couldn't get bonuses:", error.message)
+ })
+ return () => {
+ clearInterval(bonusChecker)
+ }
+ }, 1000 * 120)
+ }
+ }, [user])
return (
- {!seen && notifications && notifications.length > 0 && (
-
- {notifications.length > NOTIFICATIONS_PER_PAGE
- ? `${NOTIFICATIONS_PER_PAGE}+`
- : notifications.length}
-
- )}
+ {privateUser &&
}
)
}
+function UnseenNotificationsBubble(props: { privateUser: PrivateUser }) {
+ const router = useRouter()
+ const { privateUser } = props
+ const [seen, setSeen] = useState(false)
+
+ useEffect(() => {
+ if (router.pathname.endsWith('notifications')) return setSeen(true)
+ else setSeen(false)
+ }, [router.pathname])
+
+ const notifications = useUnseenPreferredNotificationGroups(privateUser)
+ if (!notifications || notifications.length === 0 || seen) {
+ return
+ }
+
+ return (
+
+ {notifications.length > NOTIFICATIONS_PER_PAGE
+ ? `${NOTIFICATIONS_PER_PAGE}+`
+ : notifications.length}
+
+ )
+}
diff --git a/web/hooks/use-notifications.ts b/web/hooks/use-notifications.ts
index 98b0f2fd..8e68fdcb 100644
--- a/web/hooks/use-notifications.ts
+++ b/web/hooks/use-notifications.ts
@@ -1,9 +1,12 @@
import { useEffect, useState } from 'react'
-import { listenForPrivateUser } from 'web/lib/firebase/users'
import { notification_subscribe_types, PrivateUser } from 'common/user'
import { Notification } from 'common/notification'
-import { listenForNotifications } from 'web/lib/firebase/notifications'
+import {
+ getNotificationsQuery,
+ listenForNotifications,
+} from 'web/lib/firebase/notifications'
import { groupBy, map } from 'lodash'
+import { useFirestoreQuery } from '@react-query-firebase/firestore'
export type NotificationGroup = {
notifications: Notification[]
@@ -13,15 +16,36 @@ export type NotificationGroup = {
type: 'income' | 'normal'
}
-export function usePreferredGroupedNotifications(
- userId: string | undefined,
- options: { unseenOnly: boolean }
-) {
+// This doesn't listen for new notifications, use firebase listener for that
+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, false),
+ {
+ // subscribe: false,
+ // includeMetadataChanges: true,
+ }
+ )
+ useEffect(() => {
+ if (result.isLoading) return
+ if (!result.data) return setNotifications([])
+ const notifications = result.data.docs.map(
+ (doc) => doc.data() as Notification
+ )
+
+ const notificationsToShow = getAppropriateNotifications(
+ notifications,
+ privateUser.notificationPreferences
+ ).filter((n) => !n.isSeenOnHref)
+ setNotifications(notificationsToShow)
+ }, [privateUser.notificationPreferences, result.data, result.isLoading])
- const notifications = usePreferredNotifications(userId, options)
useEffect(() => {
if (!notifications) return
@@ -32,6 +56,20 @@ export function usePreferredGroupedNotifications(
return notificationGroups
}
+export function useUnseenPreferredNotificationGroups(privateUser: PrivateUser) {
+ const notifications = useUnseenPreferredNotifications(privateUser, {})
+ const [notificationGroups, setNotificationGroups] = useState<
+ NotificationGroup[] | undefined
+ >(undefined)
+ useEffect(() => {
+ if (!notifications) return
+
+ const groupedNotifications = groupNotifications(notifications)
+ setNotificationGroups(groupedNotifications)
+ }, [notifications])
+ return notificationGroups
+}
+
export function groupNotifications(notifications: Notification[]) {
let notificationGroups: NotificationGroup[] = []
const notificationGroupsByDay = groupBy(notifications, (notification) =>
@@ -85,32 +123,17 @@ export function groupNotifications(notifications: Notification[]) {
return notificationGroups
}
-export function usePreferredNotifications(
- userId: string | undefined,
- options: { unseenOnly: boolean; customHref?: string }
+export function useUnseenPreferredNotifications(
+ privateUser: PrivateUser,
+ options: { customHref?: string }
) {
- const { unseenOnly, customHref } = options
- const [privateUser, setPrivateUser] = useState(null)
+ const { customHref } = options
const [notifications, setNotifications] = useState([])
const [userAppropriateNotifications, setUserAppropriateNotifications] =
useState([])
+ listenForNotifications(privateUser.id, setNotifications, true)
useEffect(() => {
- if (userId) listenForPrivateUser(userId, setPrivateUser)
- }, [userId])
-
- useEffect(() => {
- if (privateUser)
- return listenForNotifications(
- privateUser.id,
- setNotifications,
- unseenOnly
- )
- }, [privateUser, unseenOnly])
-
- useEffect(() => {
- if (!privateUser) return
-
const notificationsToShow = getAppropriateNotifications(
notifications,
privateUser.notificationPreferences
@@ -118,7 +141,7 @@ export function usePreferredNotifications(
customHref ? n.isSeenOnHref?.includes(customHref) : !n.isSeenOnHref
)
setUserAppropriateNotifications(notificationsToShow)
- }, [privateUser, notifications, customHref])
+ }, [notifications, customHref, privateUser.notificationPreferences])
return userAppropriateNotifications
}
@@ -131,7 +154,7 @@ const lessPriorityReasons = [
// 'on_contract_with_users_shares_in',
]
-function getAppropriateNotifications(
+export function getAppropriateNotifications(
notifications: Notification[],
notificationPreferences?: notification_subscribe_types
) {
diff --git a/web/lib/firebase/notifications.ts b/web/lib/firebase/notifications.ts
index c0dca8be..302c4d55 100644
--- a/web/lib/firebase/notifications.ts
+++ b/web/lib/firebase/notifications.ts
@@ -1,12 +1,17 @@
-import { collection, query, where } from 'firebase/firestore'
+import { collection, orderBy, query, where } from 'firebase/firestore'
import { Notification } from 'common/notification'
import { db } from 'web/lib/firebase/init'
import { listenForValues } from 'web/lib/firebase/utils'
-function getNotificationsQuery(userId: string, unseenOnly?: boolean) {
+export function getNotificationsQuery(userId: string, unseenOnly?: boolean) {
const notifsCollection = collection(db, `/users/${userId}/notifications`)
- if (unseenOnly) return query(notifsCollection, where('isSeen', '==', false))
- return query(notifsCollection)
+ if (unseenOnly)
+ return query(
+ notifsCollection,
+ where('isSeen', '==', false),
+ orderBy('createdTime', 'desc')
+ )
+ return query(notifsCollection, orderBy('createdTime', 'desc'))
}
export function listenForNotifications(
@@ -19,6 +24,7 @@ export function listenForNotifications(
(notifs) => {
notifs.sort((n1, n2) => n2.createdTime - n1.createdTime)
setNotifications(notifs)
- }
+ },
+ true
)
}
diff --git a/web/lib/firebase/utils.ts b/web/lib/firebase/utils.ts
index e63c2d96..38bcb493 100644
--- a/web/lib/firebase/utils.ts
+++ b/web/lib/firebase/utils.ts
@@ -39,12 +39,13 @@ export function listenForValue(
export function listenForValues(
query: Query,
- setValues: (values: T[]) => void
+ setValues: (values: T[]) => void,
+ enableCache?: boolean
) {
// Exclude cached snapshots so we only trigger on fresh data.
// includeMetadataChanges ensures listener is called even when server data is the same as cached data.
return onSnapshot(query, { includeMetadataChanges: true }, (snapshot) => {
- if (snapshot.metadata.fromCache) return
+ if (snapshot.metadata.fromCache && !enableCache) return
const values = snapshot.docs.map((doc) => doc.data() as T)
setValues(values)
diff --git a/web/pages/notifications.tsx b/web/pages/notifications.tsx
index 08ef9bb8..c9c8ace5 100644
--- a/web/pages/notifications.tsx
+++ b/web/pages/notifications.tsx
@@ -1,5 +1,5 @@
import { Tabs } from 'web/components/layout/tabs'
-import { useUser } from 'web/hooks/use-user'
+import { usePrivateUser, useUser } from 'web/hooks/use-user'
import React, { useEffect, useState } from 'react'
import { Notification, notification_source_types } from 'common/notification'
import { Avatar, EmptyAvatar } from 'web/components/avatar'
@@ -9,7 +9,6 @@ import { Title } from 'web/components/title'
import { doc, updateDoc } from 'firebase/firestore'
import { db } from 'web/lib/firebase/init'
import { CopyLinkDateTimeComponent } from 'web/components/feed/copy-link-date-time'
-import Custom404 from 'web/pages/404'
import { UserLink } from 'web/components/user-page'
import { notification_subscribe_types, PrivateUser } from 'common/user'
import { Contract } from 'common/contract'
@@ -35,137 +34,144 @@ import { formatMoney } from 'common/util/format'
import { groupPath } from 'web/lib/firebase/groups'
import { UNIQUE_BETTOR_BONUS_AMOUNT } from 'common/numeric-constants'
import { groupBy, sum, uniq } from 'lodash'
+import Custom404 from 'web/pages/404'
+import { Col } from 'web/components/layout/col'
export const NOTIFICATIONS_PER_PAGE = 30
const MULTIPLE_USERS_KEY = 'multipleUsers'
export default function Notifications() {
const user = useUser()
- const [page, setPage] = useState(1)
-
- const groupedNotifications = usePreferredGroupedNotifications(user?.id, {
- unseenOnly: false,
- })
- const [paginatedNotificationGroups, setPaginatedNotificationGroups] =
- useState([])
- useEffect(() => {
- if (!groupedNotifications) return
- const start = (page - 1) * NOTIFICATIONS_PER_PAGE
- const end = start + NOTIFICATIONS_PER_PAGE
- const maxNotificationsToShow = groupedNotifications.slice(start, end)
- const remainingNotification = groupedNotifications.slice(end)
- for (const notification of remainingNotification) {
- if (notification.isSeen) break
- else setNotificationsAsSeen(notification.notifications)
- }
- setPaginatedNotificationGroups(maxNotificationsToShow)
- }, [groupedNotifications, page])
-
- if (user === undefined) {
- return
- }
- if (user === null) {
- return
- }
+ const privateUser = usePrivateUser(user?.id)
+ if (!user) return
return (
-
- {paginatedNotificationGroups.length === 0 &&
- "You don't have any notifications. Try changing your settings to see more."}
- {paginatedNotificationGroups.map((notification) =>
- notification.type === 'income' ? (
-
- ) : notification.notifications.length === 1 ? (
-
- ) : (
-
- )
- )}
- {groupedNotifications.length > NOTIFICATIONS_PER_PAGE && (
-
- )}
-
- ) : (
-
- ),
- },
- {
- title: 'Settings',
- content: (
-
-
-
- ),
- },
- ]}
- />
+
+
+ ) : (
+
+ ),
+ },
+ {
+ title: 'Settings',
+ content: (
+
+
+
+ ),
+ },
+ ]}
+ />
+
)
}
+function NotificationsList(props: { privateUser: PrivateUser }) {
+ const { privateUser } = props
+ const [page, setPage] = useState(1)
+ const allGroupedNotifications = usePreferredGroupedNotifications(privateUser)
+ const [paginatedGroupedNotifications, setPaginatedGroupedNotifications] =
+ useState(undefined)
+
+ useEffect(() => {
+ if (!allGroupedNotifications) return
+ const start = (page - 1) * 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) return
+
+ return (
+
+ {paginatedGroupedNotifications.length === 0 &&
+ "You don't have any notifications. Try changing your settings to see more."}
+ {paginatedGroupedNotifications.map((notification) =>
+ notification.type === 'income' ? (
+
+ ) : notification.notifications.length === 1 ? (
+
+ ) : (
+
+ )
+ )}
+ {allGroupedNotifications &&
+ allGroupedNotifications.length > NOTIFICATIONS_PER_PAGE && (
+
+ )}
+
+ )
+}
+
function IncomeNotificationGroupItem(props: {
notificationGroup: NotificationGroup
className?: string