Merge bug/optimistic-state-notify-on-success: gate state-change notices on server confirmation

This commit is contained in:
npeter83 2026-06-18 23:27:59 +02:00
commit d61e844f6b

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]
); );