diff --git a/frontend/src/routes/MailLayout.tsx b/frontend/src/routes/MailLayout.tsx index d123ecf..9ce048c 100644 --- a/frontend/src/routes/MailLayout.tsx +++ b/frontend/src/routes/MailLayout.tsx @@ -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).