Phone: swipe right to delete, Mailbox display with the dividing lines
On a phone, a conversation dragged to the right shows a red bin behind it; let go past a third of the row and it goes to Trash, short of that it springs back. Vertical drags still scroll (touch-action: pan-y), and a swipe never opens the conversation. "Moved to Trash · Undo" shows for 5 s and the move is made only after that, like Undo send — so Undo just shows the row again; leaving the page still deletes. Not in Trash (it would be for good) and not offline. The ⋯ menu on a phone gains Mailbox display, holding only "Show a dividing line between rows" (a phone has no layouts to choose). The corner toast takes an optional action (label + run) and duration. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -581,6 +581,50 @@ export function MailLayout() {
|
||||
}, [threadsQuery.data]);
|
||||
const listArchive = (id: string) => listMove(id, api.archiveThread);
|
||||
const listTrash = (id: string) => listMove(id, api.trashThread);
|
||||
// Phone: a conversation swiped right goes to Trash — after a short Undo
|
||||
// window, like Undo send. Until then it is only hidden, so Undo just shows it
|
||||
// again (moving it back from Trash could race the server's move).
|
||||
const SWIPE_UNDO_MS = 5000;
|
||||
const pendingSwipes = useRef(new Map<string, number>());
|
||||
const swipeTrash = (id: string) => {
|
||||
setHiddenIds((prev) => new Set(prev).add(id));
|
||||
if (id === threadId) navigate("/mail");
|
||||
const timer = window.setTimeout(() => {
|
||||
pendingSwipes.current.delete(id);
|
||||
void listMove(id, api.trashThread);
|
||||
}, SWIPE_UNDO_MS);
|
||||
pendingSwipes.current.set(id, timer);
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("webmail:toast", {
|
||||
detail: {
|
||||
text: t("mail.movedToTrash"),
|
||||
ms: SWIPE_UNDO_MS,
|
||||
action: {
|
||||
label: t("compose.undo"),
|
||||
run: () => {
|
||||
window.clearTimeout(pendingSwipes.current.get(id));
|
||||
pendingSwipes.current.delete(id);
|
||||
setHiddenIds((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.delete(id);
|
||||
return next;
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
};
|
||||
// Leaving before the toast is gone still deletes what was swiped.
|
||||
useEffect(
|
||||
() => () => {
|
||||
for (const [id, timer] of pendingSwipes.current) {
|
||||
window.clearTimeout(timer);
|
||||
void api.trashThread(id);
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
const listRestore = (id: string) => listMove(id, api.restoreThread);
|
||||
const listDeletePermanent = (id: string) => listMove(id, api.deleteThreadPermanent);
|
||||
const listTogglePin = async (id: string, pinned: boolean) => {
|
||||
@@ -1166,22 +1210,21 @@ export function MailLayout() {
|
||||
>
|
||||
{t("mail.markAllRead")}
|
||||
</button>
|
||||
{/* A phone always reads in a single panel: nothing to choose. */}
|
||||
{!mobile && (
|
||||
<button
|
||||
className="flex w-full items-center px-3 py-2 text-left text-[13px] text-app-fg hover:bg-app-hover"
|
||||
onClick={() => {
|
||||
setMoreOpen(false);
|
||||
setDisplayOpen(true);
|
||||
}}
|
||||
>
|
||||
{t("display.title")}
|
||||
</button>
|
||||
)}
|
||||
{/* On a phone it holds only the dividing lines: it always reads in a
|
||||
single panel, so there is no layout to choose. */}
|
||||
<button
|
||||
className="flex w-full items-center px-3 py-2 text-left text-[13px] text-app-fg hover:bg-app-hover"
|
||||
onClick={() => {
|
||||
setMoreOpen(false);
|
||||
setDisplayOpen(true);
|
||||
}}
|
||||
>
|
||||
{t("display.title")}
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{displayOpen && <MailboxDisplay onClose={closeDisplay} />}
|
||||
{displayOpen && <MailboxDisplay onClose={closeDisplay} layouts={!mobile} />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1247,6 +1290,9 @@ export function MailLayout() {
|
||||
dimmedIds={dimmedIds}
|
||||
onToggleCheck={toggleCheck}
|
||||
compact={mobile}
|
||||
// Phone: swipe right to delete — not in Trash (that would be for
|
||||
// good) and not offline (nothing could be moved).
|
||||
onSwipeDelete={mobile && currentFolder !== "trash" && !offline ? swipeTrash : undefined}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user