diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 876f84e..3b27ef1 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -173,6 +173,7 @@ export function App() { fetchOlderMessages, addMessageIfNew, updateMessageAck, + triggerReconcile, } = useConversationMessages(activeConversation); const { @@ -182,6 +183,7 @@ export function App() { incrementUnread, markAllRead, trackNewMessage, + refreshUnreads, } = useUnreadCounts(channels, contacts, activeConversation); // Determine if active contact is a repeater (used for routing to dashboard) @@ -227,10 +229,12 @@ export function App() { }); }, onReconnect: () => { - toast.warning('Unstable connection', { - description: 'Please refresh the page to ensure all messages are correctly loaded.', - duration: 10000, - }); + // Silently recover any data missed during the disconnect window + triggerReconcile(); + refreshUnreads(); + fetchAllContacts() + .then((data) => setContacts(data)) + .catch(console.error); }, onMessage: (msg: Message) => { const activeConv = activeConversationRef.current; @@ -299,6 +303,9 @@ export function App() { setConfig, activeConversationRef, setContacts, + triggerReconcile, + refreshUnreads, + fetchAllContacts, ] ); diff --git a/frontend/src/hooks/useConversationMessages.ts b/frontend/src/hooks/useConversationMessages.ts index ebbc617..ed81e7d 100644 --- a/frontend/src/hooks/useConversationMessages.ts +++ b/frontend/src/hooks/useConversationMessages.ts @@ -66,6 +66,7 @@ interface UseConversationMessagesResult { fetchOlderMessages: () => Promise; addMessageIfNew: (msg: Message) => boolean; updateMessageAck: (messageId: number, ackCount: number, paths?: MessagePath[]) => void; + triggerReconcile: () => void; } export function useConversationMessages( @@ -258,6 +259,15 @@ export function useConversationMessages( } }, [activeConversation, loadingOlder, hasOlderMessages, messages, applyPendingAck]); + // Trigger a background reconciliation for the current conversation. + // Used after WebSocket reconnects to silently recover any missed messages. + const triggerReconcile = useCallback(() => { + const conv = activeConversation; + if (!conv || conv.type === 'raw' || conv.type === 'map' || conv.type === 'visualizer') return; + const controller = new AbortController(); + reconcileFromBackend(conv, controller.signal); + }, [activeConversation]); // eslint-disable-line react-hooks/exhaustive-deps + // Background reconciliation: silently fetch from backend after a cache restore // and only update state if something differs (missed WS message, stale ack, etc.). // No-ops on the happy path — zero rerenders when cache is already consistent. @@ -435,5 +445,6 @@ export function useConversationMessages( fetchOlderMessages, addMessageIfNew, updateMessageAck, + triggerReconcile, }; } diff --git a/frontend/src/hooks/useUnreadCounts.ts b/frontend/src/hooks/useUnreadCounts.ts index 8821bd0..34eee7a 100644 --- a/frontend/src/hooks/useUnreadCounts.ts +++ b/frontend/src/hooks/useUnreadCounts.ts @@ -17,6 +17,7 @@ interface UseUnreadCountsResult { incrementUnread: (stateKey: string, hasMention?: boolean) => void; markAllRead: () => void; trackNewMessage: (msg: Message) => void; + refreshUnreads: () => Promise; } export function useUnreadCounts( @@ -170,5 +171,6 @@ export function useUnreadCounts( incrementUnread, markAllRead, trackNewMessage, + refreshUnreads: fetchUnreads, }; }