mirror of
https://github.com/multica-ai/multica.git
synced 2026-08-13 11:30:58 +02:00
The inbox auto-marks a notification read the moment it is selected, so
"opened" and "handled" were the same signal — a row you glanced at and
meant to come back to was gone from the unread count with no way back.
Right-click any inbox row for a shared context menu: Mark as read /
Mark as unread, plus Archive (Unarchive in the archived view).
- POST /api/inbox/{id}/unread + MarkInboxUnread query, publishing
inbox:unread. Item-scoped, mirroring mark-read: the list renders one
row per issue carrying that group's newest item, so flipping the whole
group would resurrect siblings the user already dealt with.
- useMarkInboxUnread patches both lists optimistically and re-pulls the
cross-workspace unread summary on settle.
- One shared menu per list rather than a Base UI root per row (the same
shape IssueContextMenuProvider uses): only one is ever open, and a
per-row root would unmount with its menu when the row scrolls out of
the virtualized viewport.
- The read toggle is main-view only — archived rows deliberately render
as read and the unread count excludes them, so a toggle there would
report success and change nothing on screen.
- Parking the row that is currently open holds the auto-read effect off
that one item while it stays selected; re-opening it later marks it
read again.
- Mobile subscribes to inbox:unread so the unread dots agree across
clients.
Co-authored-by: Lambda <lambda@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
79 lines
3.0 KiB
TypeScript
79 lines
3.0 KiB
TypeScript
/**
|
|
* Inbox realtime — Layer 3 of the realtime stack.
|
|
*
|
|
* Two subscription groups:
|
|
*
|
|
* 1. `inbox:*` events → invalidate the inbox query. inbox payloads are
|
|
* small and (apart from inbox:new) rare, so refetching is cheaper than
|
|
* maintaining per-event patchers. Multi-device parity: subscribing to
|
|
* inbox:read / inbox:archived means a read/archive on web reaches
|
|
* mobile within the next WS frame (web's use-realtime-sync deliberately
|
|
* DOESN'T subscribe to those, but mobile's stricter freshness wins for
|
|
* multi-device users).
|
|
*
|
|
* 2. `issue:*` events → patch the inbox cache directly via the dedicated
|
|
* updaters (inbox-ws-updaters.ts). Required because:
|
|
* - `issue:updated` with a new status must flip the inbox row's
|
|
* StatusIcon inline — otherwise the row keeps showing stale status.
|
|
* - `issue:deleted` must strip every inbox item pointing at that
|
|
* issue, otherwise tapping the orphan row 404s on issue/[id].
|
|
* Web does the same in `packages/core/inbox/ws-updaters.ts`.
|
|
*
|
|
* Reconnect: invalidate the list (we may have missed events while down;
|
|
* no replay buffer in v1).
|
|
*/
|
|
import { useQueryClient } from "@tanstack/react-query";
|
|
import { inboxKeys } from "@/data/queries/inbox";
|
|
import { useWSSubscriptions } from "@/lib/use-ws-subscriptions";
|
|
import {
|
|
dropInboxItemsByIssue,
|
|
patchInboxIssueStatus,
|
|
} from "./inbox-ws-updaters";
|
|
|
|
export function useInboxRealtime() {
|
|
const qc = useQueryClient();
|
|
|
|
useWSSubscriptions(
|
|
(ws, wsId) => {
|
|
const invalidate = () =>
|
|
qc.invalidateQueries({ queryKey: inboxKeys.list(wsId) });
|
|
|
|
return [
|
|
// Inbox-domain events: refetch the small inbox list.
|
|
ws.on("inbox:new", invalidate),
|
|
ws.on("inbox:read", invalidate),
|
|
// Mobile has no mark-unread affordance yet (web/desktop right-click
|
|
// only), but a mark-unread there must un-read the row here too —
|
|
// otherwise the phone keeps showing it read and the unread dots
|
|
// disagree across clients.
|
|
ws.on("inbox:unread", invalidate),
|
|
ws.on("inbox:archived", invalidate),
|
|
// Mobile has no archived view yet (web/desktop only, MUL-3736), but an
|
|
// unarchive there restores the item to THIS list — without refetching,
|
|
// mobile keeps showing the pre-restore list.
|
|
ws.on("inbox:unarchived", invalidate),
|
|
ws.on("inbox:batch-read", invalidate),
|
|
ws.on("inbox:batch-archived", invalidate),
|
|
|
|
// Cross-cutting: issue events that need to patch inbox state.
|
|
ws.on("issue:updated", (payload) => {
|
|
patchInboxIssueStatus(
|
|
qc,
|
|
wsId,
|
|
payload.issue.id,
|
|
payload.issue.status,
|
|
);
|
|
}),
|
|
ws.on("issue:deleted", (payload) => {
|
|
dropInboxItemsByIssue(qc, wsId, payload.issue_id);
|
|
}),
|
|
|
|
// After a reconnect we don't know what we missed during the
|
|
// downtime — refresh from server.
|
|
ws.onReconnect(invalidate),
|
|
];
|
|
},
|
|
[qc],
|
|
);
|
|
}
|