Mail list: hide a row the moment its archive/delete button is clicked
The hover actions waited for the server before the list reacted. The row now leaves the list at once and the request runs behind it; a failed request puts it back. In search the row greys out immediately instead, since it may still match the query. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -90,6 +90,9 @@ export function MailLayout() {
|
||||
// Rows just archived/trashed/deleted from the list: greyed out and unclickable
|
||||
// for immediate feedback (search results don't always drop the row on refetch).
|
||||
const [dimmedIds, setDimmedIds] = useState<Set<string>>(new Set());
|
||||
// Rows a hover action is moving out of the folder: gone from the list the
|
||||
// moment the button is clicked, while the request runs behind it.
|
||||
const [hiddenIds, setHiddenIds] = useState<Set<string>>(new Set());
|
||||
const toggleCheck = (id: string) =>
|
||||
setCheckedIds((prev) => {
|
||||
const next = new Set(prev);
|
||||
@@ -223,8 +226,9 @@ export function MailLayout() {
|
||||
total = ordered.length;
|
||||
pageThreads = ordered.slice(page * PAGE_SIZE, page * PAGE_SIZE + PAGE_SIZE);
|
||||
} else {
|
||||
pageThreads = threadsQuery.data?.threads ?? [];
|
||||
total = threadsQuery.data?.total ?? 0;
|
||||
const all = threadsQuery.data?.threads ?? [];
|
||||
pageThreads = hiddenIds.size ? all.filter((th) => !hiddenIds.has(th.id)) : all;
|
||||
total = Math.max(0, (threadsQuery.data?.total ?? 0) - (all.length - pageThreads.length));
|
||||
}
|
||||
const pageStart = page * PAGE_SIZE;
|
||||
const pageEnd = pageStart + pageThreads.length;
|
||||
@@ -455,32 +459,38 @@ export function MailLayout() {
|
||||
|
||||
// Per-thread actions for the mail-list hover buttons (act on a given id, not the
|
||||
// currently-open thread). Close the reading pane only if the acted thread is open.
|
||||
const listAfterMove = (id: string) => {
|
||||
// In a folder the refetch below removes the row (it left the folder's scope).
|
||||
// In search the row may still match a global query, so grey it out for feedback.
|
||||
if (searching) setDimmedIds((prev) => new Set(prev).add(id));
|
||||
queryClient.invalidateQueries({ queryKey: ["threads"] });
|
||||
//
|
||||
// The feedback is immediate and the request runs behind it. In a folder the
|
||||
// row is hidden straight away (it is leaving the folder's scope); in search it
|
||||
// may still match a global query, so it is greyed out instead. A failed
|
||||
// request puts the row back.
|
||||
const listMove = async (id: string, action: (id: string) => Promise<unknown>) => {
|
||||
const mark = searching ? setDimmedIds : setHiddenIds;
|
||||
const unmark = () =>
|
||||
mark((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.delete(id);
|
||||
return next;
|
||||
});
|
||||
mark((prev) => new Set(prev).add(id));
|
||||
if (id === threadId) navigate("/mail");
|
||||
try {
|
||||
await action(id);
|
||||
} catch {
|
||||
unmark();
|
||||
return;
|
||||
}
|
||||
queryClient.invalidateQueries({ queryKey: ["search"] }); // keep search results in sync
|
||||
queryClient.invalidateQueries({ queryKey: ["quota"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["folder-counts"] });
|
||||
if (id === threadId) navigate("/mail");
|
||||
};
|
||||
const listArchive = async (id: string) => {
|
||||
await api.archiveThread(id);
|
||||
listAfterMove(id);
|
||||
};
|
||||
const listTrash = async (id: string) => {
|
||||
await api.trashThread(id);
|
||||
listAfterMove(id);
|
||||
};
|
||||
const listRestore = async (id: string) => {
|
||||
await api.restoreThread(id);
|
||||
listAfterMove(id);
|
||||
};
|
||||
const listDeletePermanent = async (id: string) => {
|
||||
await api.deleteThreadPermanent(id);
|
||||
listAfterMove(id);
|
||||
// Once the list has refetched without the row, it no longer needs hiding.
|
||||
await queryClient.invalidateQueries({ queryKey: ["threads"] });
|
||||
if (!searching) unmark();
|
||||
};
|
||||
const listArchive = (id: string) => listMove(id, api.archiveThread);
|
||||
const listTrash = (id: string) => listMove(id, api.trashThread);
|
||||
const listRestore = (id: string) => listMove(id, api.restoreThread);
|
||||
const listDeletePermanent = (id: string) => listMove(id, api.deleteThreadPermanent);
|
||||
const listTogglePin = async (id: string, pinned: boolean) => {
|
||||
await api.threadFlags(id, pinned ? [] : ["\\Flagged"], pinned ? ["\\Flagged"] : []);
|
||||
queryClient.invalidateQueries({ queryKey: ["threads"] });
|
||||
|
||||
Reference in New Issue
Block a user