From 8287f5d8b46bbaf4bc8fbb585dd3907594a60aaa Mon Sep 17 00:00:00 2001 From: Matthew Meszaros Date: Sun, 7 Jun 2026 11:07:19 +0200 Subject: [PATCH] feat: mark opened unibox threads seen --- web/src/components/app/unibox/ThreadView.tsx | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/web/src/components/app/unibox/ThreadView.tsx b/web/src/components/app/unibox/ThreadView.tsx index 2ec53bc6..f433c03e 100644 --- a/web/src/components/app/unibox/ThreadView.tsx +++ b/web/src/components/app/unibox/ThreadView.tsx @@ -33,6 +33,7 @@ import ContactContextPanel from "./ContactContextPanel"; import { CategoryChip } from "@/components/app/contacts/CategoryPicker"; import { SectionBar } from "@/components/layout/Page"; import useThread from "@/lib/api/hooks/app/unibox/useThread"; +import useMarkSeen from "@/lib/api/hooks/app/unibox/useMarkSeen"; import useThreadLabels from "@/lib/api/hooks/app/unibox/useThreadLabels"; import useThreadScheduled from "@/lib/api/hooks/app/unibox/useThreadScheduled"; import cancelScheduled from "@/lib/api/client/app/unibox/cancelScheduled"; @@ -198,6 +199,20 @@ export function ThreadView({ threadId, emailId }: ThreadViewProps) { setReplyState(null); }, [threadId]); + // Opening a thread marks its unseen messages as read. The hook invalidates + // ["unibox"], so the unread badge, the collapsed list, and the overview all + // refresh. Once everything is seen the id list is empty and this no-ops, so + // it self-terminates after the post-mark refetch (no loop). + const markSeen = useMarkSeen(); + const markSeenMutate = markSeen.mutate; + React.useEffect(() => { + const unseenIds = (q.data?.data ?? []) + .filter((m) => !m.seen) + .map((m) => m.id); + if (unseenIds.length === 0) return; + markSeenMutate({ ids: unseenIds }); + }, [threadId, q.data, markSeenMutate]); + const snooze = useMutation({ mutationFn: (until: Date) => snoozeThread({ thread_id: threadId, snoozed_until: until.toISOString() }),