From 3e4db20bcb4e5e4fb73ee412f2fb7dfac34a02c2 Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Fri, 6 Mar 2026 20:21:28 +0530 Subject: [PATCH] fix: update baseline logic in useDocuments and useInbox hooks to accurately track new items and unread counts, addressing timing issues with Electric updates --- surfsense_web/hooks/use-documents.ts | 14 ++++++---- surfsense_web/hooks/use-inbox.ts | 41 +++++++++++++++++++++------- surfsense_web/lib/electric/client.ts | 3 +- 3 files changed, 41 insertions(+), 17 deletions(-) diff --git a/surfsense_web/hooks/use-documents.ts b/surfsense_web/hooks/use-documents.ts index 40f729c7c..50c2f73ce 100644 --- a/surfsense_web/hooks/use-documents.ts +++ b/surfsense_web/hooks/use-documents.ts @@ -351,12 +351,14 @@ export function useDocuments( const liveIds = new Set(validItems.map((d) => d.id)); const prevIds = new Set(prev.map((d) => d.id)); - // First callback: snapshot all Electric IDs as the baseline. - // Everything in this set existed before the sidebar opened and - // should only appear via API pagination, not Electric. - if (electricBaselineIdsRef.current === null) { - electricBaselineIdsRef.current = new Set(liveIds); - } + // Only baseline items already rendered from API. + // Items in Electric but NOT in prev are genuinely new + // (created between the API fetch and Electric's first callback). + if (electricBaselineIdsRef.current === null) { + electricBaselineIdsRef.current = new Set( + [...liveIds].filter((id) => prevIds.has(id)) + ); + } // Genuinely new = not in rendered list, not in baseline snapshot. // These are docs created AFTER the sidebar opened. diff --git a/surfsense_web/hooks/use-inbox.ts b/surfsense_web/hooks/use-inbox.ts index 97cadd579..9c20a1cda 100644 --- a/surfsense_web/hooks/use-inbox.ts +++ b/surfsense_web/hooks/use-inbox.ts @@ -203,7 +203,11 @@ export function useInbox( const prevIds = new Set(prev.map((d) => d.id)); if (electricBaselineIdsRef.current === null) { - electricBaselineIdsRef.current = new Set(liveIds); + // Only baseline items already rendered from API. + // Items in Electric but NOT in prev are genuinely new. + electricBaselineIdsRef.current = new Set( + [...liveIds].filter((id) => prevIds.has(id)) + ); } const baseline = electricBaselineIdsRef.current; @@ -237,11 +241,35 @@ export function useInbox( return updated; }); + + // Calibrate the older-unread offset using baseline items + // (items present in both Electric and the API-loaded list). + // This avoids the timing bug where new items arriving between + // the API fetch and Electric's first callback would be absorbed + // into the offset, making the count appear unchanged. + const baseline = electricBaselineIdsRef.current; + if (olderUnreadOffsetRef.current === null && baseline !== null) { + const baselineUnreadCount = validItems.filter( + (item) => baseline.has(item.id) && !item.read + ).length; + olderUnreadOffsetRef.current = Math.max( + 0, + apiUnreadTotalRef.current - baselineUnreadCount + ); + } + + // Derive unread count from all Electric items + the older offset + if (olderUnreadOffsetRef.current !== null) { + const electricUnreadCount = validItems.filter((item) => !item.read).length; + setUnreadCount(olderUnreadOffsetRef.current + electricUnreadCount); + } }); liveQueryRef.current = liveQuery; - // Per-instance unread count live query filtered by category types + // Per-instance unread count live query filtered by category types. + // Acts as a secondary reactive path for read-status changes that + // may not trigger the items live query in all edge cases. const countQuery = `SELECT COUNT(*) as count FROM notifications WHERE user_id = $1 AND (search_space_id = $2 OR search_space_id IS NULL) @@ -258,15 +286,8 @@ export function useInbox( countLiveQuery.subscribe((result: { rows: Array<{ count: number | string }> }) => { if (!mounted || !result.rows?.[0] || !initialLoadDoneRef.current) return; + if (olderUnreadOffsetRef.current === null) return; const liveRecentUnread = Number(result.rows[0].count) || 0; - - if (olderUnreadOffsetRef.current === null) { - olderUnreadOffsetRef.current = Math.max( - 0, - apiUnreadTotalRef.current - liveRecentUnread - ); - } - setUnreadCount(olderUnreadOffsetRef.current + liveRecentUnread); }); diff --git a/surfsense_web/lib/electric/client.ts b/surfsense_web/lib/electric/client.ts index 6dda662d7..09ef5e300 100644 --- a/surfsense_web/lib/electric/client.ts +++ b/surfsense_web/lib/electric/client.ts @@ -71,7 +71,8 @@ const pendingSyncs = new Map>(); // real-time documents table with title/created_by_id/status columns, // consolidated single documents sync, pending state for document queue visibility // v6: added enable_summary column to search_source_connectors -const SYNC_VERSION = 6; +// v7: fixed connector-popup using invalid category for useInbox +const SYNC_VERSION = 7; // Database name prefix for identifying SurfSense databases const DB_PREFIX = "surfsense-";