fix(web): only announce a state change after the server confirms it

The 'Marked watched' / 'Hidden' notices (and resolving a stale notice on
unwatch/unhide) fired synchronously, before the api.setState call resolved.
When the write failed — e.g. the API was unreachable — the optimistic card
override was rolled back in .catch, but the notice had already been emitted
and lingered, falsely claiming a change that never persisted.

Move all three branches into the .then() so the notice is produced only once
the server has actually applied the change; on failure the card reverts and
nothing is announced (the connection-lost status already explains why).
This commit is contained in:
npeter83 2026-06-18 23:27:59 +02:00
parent 0ab6742b44
commit 9f9d2544ef

View file

@ -144,18 +144,14 @@ export default function Feed({
(id: string, status: string) => { (id: string, status: string) => {
setOverrides((o) => ({ ...o, [id]: status })); setOverrides((o) => ({ ...o, [id]: status }));
// Refetch once the server has the change so other views (e.g. Hidden) are in sync. // Refetch once the server has the change so other views (e.g. Hidden) are in sync.
// Announce the change only AFTER the server confirms it: a "Marked watched"/"Hidden"
// notice (or resolving a stale one) is a claim of success, so firing it optimistically
// would falsely report a change that the .catch below is about to roll back (e.g. when
// the API is unreachable).
api api
.setState(id, status) .setState(id, status)
.then(() => qc.invalidateQueries({ queryKey: ["feed"] })) .then(() => {
.catch(() => qc.invalidateQueries({ queryKey: ["feed"] });
// Server rejected the change — drop the optimistic override so the card
// reverts to the real (unchanged) state instead of staying phantom-hidden.
setOverrides((o) => {
const next = { ...o };
delete next[id];
return next;
})
);
if (status === "hidden") { if (status === "hidden") {
const v = loadedRef.current.find((x) => x.id === id); const v = loadedRef.current.find((x) => x.id === id);
notify({ notify({
@ -181,10 +177,20 @@ export default function Feed({
meta: { kind: "video-watched", videoId: id, title: v?.title ?? "this video" }, meta: { kind: "video-watched", videoId: id, title: v?.title ?? "this video" },
}); });
} else if (status === "new") { } else if (status === "new") {
// Unhide / unwatch (from a card, the toast's Undo, or the center): quietly resolve any // Unhide / unwatch (from a card, the toast's Undo, or the center): quietly resolve
// stale hide/watch notice for this video so it doesn't linger with a dead action. // any stale hide/watch notice for this video so it doesn't linger with a dead action.
resolveVideo(id); resolveVideo(id);
} }
})
.catch(() =>
// Server rejected the change — drop the optimistic override so the card
// reverts to the real (unchanged) state instead of staying phantom-hidden.
setOverrides((o) => {
const next = { ...o };
delete next[id];
return next;
})
);
}, },
[qc] [qc]
); );