fix(notifications): Mark all read empties a band of read rows, and a read landing mid-write cannot refill it

This commit is contained in:
2026-10-28 21:54:00 +01:00
parent ee658b8bfc
commit 0469812499
2 changed files with 74 additions and 9 deletions
@@ -108,9 +108,6 @@ export default function NotificationsPanel() {
);
const [openedAt] = useState(() => Date.now());
const [earlierOpen, setEarlierOpen] = useState(false);
const order = orderInbox(serverItems, clientItems, { now: openedAt, held });
const asking = order.needsYou.map(entryKey).filter((k) => !held.has(k));
if (asking.length > 0) setHeld(new Set([...held, ...asking]));
// Opening the center marks the (ephemeral) client items read, matching the old bell. Server
// items stay unread until acted on individually.
@@ -129,7 +126,9 @@ export default function NotificationsPanel() {
});
const readAllMut = useMutation({
mutationFn: api.markAllNotificationsRead,
onSuccess: refresh,
// Settled, not only succeeded: a refused write brings the rows back unread at once, rather than
// leaving them read until the next poll moves them under the reader.
onSettled: refresh,
meta: { failTitle: "errors.writeFailed.notifReadAll" },
});
const dismissMut = useMutation({
@@ -143,14 +142,25 @@ export default function NotificationsPanel() {
meta: { failTitle: "errors.writeFailed.notifClear" },
});
// While Mark all read is on its way, a read that lands before the server commits it (the poll, or
// another row's write refreshing the list) still says "unread": those rows are read already, or
// the band would take them back and hold them for the rest of the visit.
const shownServer = readAllMut.isPending
? serverItems.map((n) => (n.read ? n : { ...n, read: true }))
: serverItems;
const order = orderInbox(shownServer, clientItems, { now: openedAt, held });
const asking = order.needsYou.map(entryKey).filter((k) => !held.has(k));
if (asking.length > 0) setHeld(new Set([...held, ...asking]));
const serverUnread = serverItems.some((n) => !n.read);
const hasAny = serverItems.length > 0 || clientItems.length > 0;
const hasUnread = serverUnread || clientUnread > 0;
// A band of rows already read still offers the gesture that empties it.
const canMarkAll = serverUnread || clientUnread > 0 || order.needsYou.length > 0;
function markAll() {
markAllClientRead();
// Read at once, not when the write lands: until then the rows still read unread, and the band
// would take them straight back. A failed write refetches the truth (refresh, then the poll).
// would take them straight back. A failed write refetches the truth when it settles.
void qc.cancelQueries({ queryKey: qk.notifications() });
qc.setQueryData<{ items: AppNotification[]; total: number }>(qk.notifications(), (d) =>
d ? { ...d, items: d.items.map((n) => (n.read ? n : { ...n, read: true })) } : d,
@@ -404,7 +414,7 @@ export default function NotificationsPanel() {
id: "read-all",
label: t("inbox.markAllRead"),
icon: icon(CheckCheck),
disabled: !hasUnread || readAllMut.isPending,
disabled: !canMarkAll || readAllMut.isPending,
onSelect: markAll,
},
{
@@ -438,7 +448,7 @@ export default function NotificationsPanel() {
variant="ghost"
size="sm"
onClick={markAll}
disabled={!hasUnread || readAllMut.isPending}
disabled={!canMarkAll || readAllMut.isPending}
icon={<CheckCheck />}
>
{t("inbox.markAllRead")}