wacrm/src/hooks/use-total-unread.ts
Arnas Donauskas c2ef80c7df
fix(inbox): reliable unread reset + sidebar green-dot indicator
Two issues:

1. Unread count didn't always clear when opening / viewing a
   conversation. The reset effect keyed off conversationId only, so
   a new inbound message arriving while the thread was already open
   would get the server to bump unread_count to 1, but the reset
   effect wouldn't re-fire. Split the fetch and unread-reset effects:
   fetch stays keyed on conversationId (no unnecessary refetches on
   every new message), and a second effect keyed on [conversationId,
   hasUnread] flips unread back to 0 whenever it surfaces on the
   active conversation. The hasUnread gate still prevents the
   reset→update→realtime→re-run loop.

2. No indicator elsewhere in the app when new messages arrive. Added
   useTotalUnread() hook — one-time query + realtime subscription on
   the conversations table — and a pulsing green dot on the sidebar
   Inbox entry when totalUnread > 0. Dot is hidden while on /inbox
   itself (redundant with the per-conversation badges there).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 21:17:28 +03:00

75 lines
2.3 KiB
TypeScript

"use client";
import { useEffect, useRef, useState } from "react";
import { createClient } from "@/lib/supabase/client";
import type { Conversation } from "@/types";
/**
* Count of conversations with at least one unread inbound message for
* the current user. Used by the sidebar to surface a green dot on the
* Inbox nav entry when the user is elsewhere in the app.
*
* Lives on its own realtime channel (distinct from the inbox page's
* "inbox-realtime") so both can coexist without sharing state.
*/
export function useTotalUnread(): number {
const [total, setTotal] = useState(0);
// Keep a live local mirror of {id: unread_count} so INSERT/UPDATE/DELETE
// events can adjust the total in O(1) without refetching.
const countsRef = useRef<Map<string, number>>(new Map());
useEffect(() => {
const supabase = createClient();
let cancelled = false;
// Initial load. RLS scopes this to the signed-in user automatically —
// no explicit user_id filter needed here.
(async () => {
const { data, error } = await supabase
.from("conversations")
.select("id, unread_count");
if (cancelled || error || !data) return;
const map = new Map<string, number>();
let sum = 0;
for (const row of data as { id: string; unread_count: number }[]) {
const n = row.unread_count ?? 0;
map.set(row.id, n);
if (n > 0) sum += 1;
}
countsRef.current = map;
setTotal(sum);
})();
const channel = supabase
.channel("total-unread-realtime")
.on(
"postgres_changes",
{ event: "*", schema: "public", table: "conversations" },
(payload) => {
const map = countsRef.current;
if (payload.eventType === "DELETE") {
const oldRow = payload.old as Partial<Conversation>;
if (oldRow.id) map.delete(oldRow.id);
} else {
const row = payload.new as Conversation;
map.set(row.id, row.unread_count ?? 0);
}
// Recompute — cheap, conversations per user stay small.
let sum = 0;
for (const n of map.values()) if (n > 0) sum += 1;
setTotal(sum);
},
)
.subscribe();
return () => {
cancelled = true;
supabase.removeChannel(channel);
};
}, []);
return total;
}