Mail list: a hidden row flickered back before its move landed

The server only queues a thread move, so the refetch right after the request
still carried the row, and un-hiding it then brought it back until the live
update removed it for good. A row now stays hidden until a fetched list no
longer contains it, with a 30 s fallback in case the move never lands.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-30 19:55:18 +02:00
co-authored by Claude Opus 5.5
parent e9c36c47af
commit 8c463921f7
+20 -3
View File
@@ -45,6 +45,9 @@ function quotaLabel(q: QuotaOut): string {
// Threads shown per page in the mail list.
const PAGE_SIZE = 50;
// How long a row the list hid for a queued move waits for the move to land
// before it is shown again.
const HIDE_FALLBACK_MS = 30_000;
// Collapsed sidebar width: just wide enough for the icons.
const COLLAPSED_W = 60;
@@ -480,13 +483,26 @@ export function MailLayout() {
unmark();
return;
}
queryClient.invalidateQueries({ queryKey: ["threads"] });
queryClient.invalidateQueries({ queryKey: ["search"] }); // keep search results in sync
queryClient.invalidateQueries({ queryKey: ["quota"] });
queryClient.invalidateQueries({ queryKey: ["folder-counts"] });
// Once the list has refetched without the row, it no longer needs hiding.
await queryClient.invalidateQueries({ queryKey: ["threads"] });
if (!searching) unmark();
// The server only queues the move, so the refetch above can still carry the
// row; the effect below un-hides it once a list arrives without it. Should
// the move never land, the row comes back rather than staying lost.
if (!searching) window.setTimeout(unmark, HIDE_FALLBACK_MS);
};
// A row stays hidden until a fetched list no longer contains it.
useEffect(() => {
const rows = threadsQuery.data?.threads;
if (!rows) return;
setHiddenIds((prev) => {
if (prev.size === 0) return prev;
const present = new Set(rows.map((th) => th.id));
const next = new Set([...prev].filter((id) => present.has(id)));
return next.size === prev.size ? prev : next;
});
}, [threadsQuery.data]);
const listArchive = (id: string) => listMove(id, api.archiveThread);
const listTrash = (id: string) => listMove(id, api.trashThread);
const listRestore = (id: string) => listMove(id, api.restoreThread);
@@ -501,6 +517,7 @@ export function MailLayout() {
// Selection resets when you leave the folder.
useEffect(() => setCheckedIds(new Set()), [view]);
useEffect(() => setDimmedIds(new Set()), [view, debouncedTerm]);
useEffect(() => setHiddenIds(new Set()), [view]);
// Bulk actions run the given operation over every selected thread, then clear the
// selection and refresh (navigating away if the open thread was among them).