Notifications
Notifications use a polling strategy rather than WebSocket subscriptions for reliability.
NotificationContext
File: contexts/NotificationContext.tsx
Provides notification state across the application:
| State | Type | Purpose |
|---|---|---|
unreadCount | number | Total unread notifications |
isConnected | boolean | Polling active flag |
lastNotification | Notification | Most recent notification |
Polling Behavior
UNREAD_NOTIFICATION_COUNTquery polled every 30 seconds- Document title updated with unread badge count (e.g.,
(3) AIQLick) - Toast notification shown when new notifications arrive — historical unread items are silenced on cold load (see below)
- Activity monitoring pauses polling when the user is idle
Cold-load backlog suppression
When the page loads with a non-zero unreadCount, those items are part of the user's backlog, not real-time arrivals. Replaying them as toasts on every refresh produced visible-but-stale popups (e.g. "Support Reply on SUP-XXXXX" appearing on the public welcome page).
The current behaviour is:
| Source | Toast on cold load? | Toast in real time? | Auto-marked read? |
|---|---|---|---|
| Pre-existing unread (backlog) | No | n/a | No — stays unread, badge keeps the count |
| Notification arriving while page is open (poll or SSE) | n/a | Yes | Yes — MARK_NOTIFICATION_AS_READ fires when the toast surfaces |
| DND enabled | No | No | No |
Implementation:
- The
NotificationContextmount effect callsrefetchUnreadCount()(network-only) before any polling-effect side effects fire. The fresh count is treated as the backlog baseline. - A
suppressToastsRefflag tells theuseLazyQueryonCompletedhandler to seed the dedup set without invokingsetLastNotification. - The polling effect early-returns until
isInitialLoadRef.current === false, so neither the cached emission (count=0from a previous session) nor the first network emission triggers the new-arrival branch. - When
NotificationToastHandleractually surfaces a toast, it fires the existingMARK_NOTIFICATION_AS_READmutation and refetchesUnreadNotificationCountso the badge stays in sync. DND short-circuits before the mark-read call.
Individual Notifications
Notifications are not pre-fetched. They are loaded on demand when the user opens the notification panel.
Badge Counts
| Hook | File | Purpose |
|---|---|---|
useBadgeCounts | lib/hooks/useBadgeCounts.ts | Global badge count management |
useCompanyBadgeCounts | lib/hooks/useCompanyBadgeCounts.ts | Company-specific badge counts (unread, pending) |
useUnreadMessageCount | lib/hooks/useUnreadMessageCount.ts | Unread message count for messaging badge |
Notification UI
Component: components/navigation/Notification.tsx
The notification panel is accessible from the navbar bell icon. It shows:
- Unread count badge on the bell
- List of recent notifications (fetched on open)
- Mark as read / mark all as read actions
Notification Settings
Page: app/(shared)/notifications/settings/
Users can configure notification preferences (which event types to receive, delivery channels).
Toast System
The application uses a custom toast system (TWToast) for transient notifications:
- Success / error / warning / info variants
- Auto-dismiss with configurable duration
- Stacks multiple toasts vertically
BillingErrorToastHandlerauto-shows billing-related toasts