Snooze the open conversation from its toolbar, and with b

Every row offered a snooze menu and the conversation itself did not,
so "not now" after reading a mail meant going back to the list to
find its row. It was also the last key in #36 with nothing to press:
b did nothing in an open conversation.

The toolbar gets the clock button and the same menu, handled by
mail--message-actions. Snoozing closes the pane, as archiving does,
since the conversation has left the list it was opened from; clearing
a snooze leaves the reader where they are. Offered where the row
offers it, not for mail in the bin or in Spam.

The menu opens from the button's left edge there. It opened from the
right everywhere, which suits a button at the end of a row and would
run off the pane from one at the start of a toolbar.

Refs #36

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Paul LütznerandClaude Opus 5.5 committed 2026-10-10 10:40:06 +02:00
1 parent 7d54acf4d2
commit 1bae047d77
9 files changed
+96 -5

No files matched your search

+2 -1
View File
@@ -19,7 +19,8 @@ them off per person. No migration.
`a` replies on a message that has only one recipient, where there is no Reply to all to press. The
toolbar above a selection has a **Star** button, and `s` presses it; before, `s` did nothing with
rows ticked.
rows ticked. An open conversation has a **Snooze** button in its toolbar, which is what `b` presses
there.
**A config backup now carries how each person sorts their mail.** Settings → Categories — rules or
the assistant, whether that overrules the provider's own categories, and whether the inbox has tabs
@@ -77,6 +77,30 @@ export default class extends Controller {
this._closePane();
}
/**
* Snooze the open conversation, from its own toolbar.
*
* The wake time arrives as a param from mail--snooze-menu, which works it
* out in the browser — the server has no timezone for the session. Absent,
* this clears the snooze, exactly as the row's does.
*
* A snoozed conversation has left the list it was opened from, so the pane
* closes, as it does after Archive. Clearing a snooze moves nothing the
* reader is looking at, and they stay where they are.
*/
async snooze(event) {
const { until = null } = event.params;
event.stopPropagation();
if (false === await this._post(this._url("snooze"), { until })) {
return;
}
if (null !== until) {
this._closePane();
}
}
async archive(event) {
event.stopPropagation();
this._close();
@@ -51,6 +51,7 @@ export default class extends Controller {
// than teaching this controller which surface it is on.
element.setAttribute("data-mail--message-row-until-param", at.toISOString());
element.setAttribute("data-mail--list-toolbar-until-param", at.toISOString());
element.setAttribute("data-mail--message-actions-until-param", at.toISOString());
const when = element.querySelector("[data-snooze-when]");
@@ -69,6 +69,7 @@ const OPEN = {
star: "[data-action*='mail--message-actions#star']",
label: "[data-action='click->mail--label-menu#toggle']",
move: "[data-action='click->mail--move-menu#toggle']",
snooze: "[data-shortcut='snooze']",
back: "[data-action='click->mail--mail-pane#close']",
reply: "[data-shortcut='reply']",
replyAll: "[data-shortcut='reply-all']",
+3 -2
View File
@@ -1,4 +1,4 @@
<!-- translated-from: features/mail.md sha1:15c2febd1a3f8601aa57a89a38c29946ef9718cb -->
<!-- translated-from: features/mail.md sha1:52f9fcdc53048f91fd103d949726d5cb242ac80f -->
# Mail
@@ -656,7 +656,8 @@ der Anbieter. Ein Filter ist der Weg, es hier dauerhaft zu machen.
Zurückstellen ist Archivieren mit Wecker. Die Konversation verlässt den Posteingang sofort —
auch beim Anbieter, nicht nur in plMails Sicht darauf —, bekommt das Label Zurückgestellt und
kommt zurück, wenn ihre Zeit um ist.
kommt zurück, wenn ihre Zeit um ist. Den Uhr-Knopf gibt es in jeder Zeile, über einer Auswahl und
in der Leiste einer geöffneten Unterhaltung.
Das Menü bietet **Später heute**, **Morgen**, **Dieses Wochenende**, **Nächste Woche**, **Datum
und Uhrzeit wählen** und, bei bereits Zurückgestelltem, **Zurückholen**. Später heute ist 18:00
+2 -1
View File
@@ -598,7 +598,8 @@ is how you make it stick here.
## Snooze
Snoozing is archiving with a timer. The conversation leaves the Inbox now — at the provider too,
not only in plMail's view of it — gains the Snoozed label, and comes back when its time is up.
not only in plMail's view of it — gains the Snoozed label, and comes back when its time is up. The
clock button is on every row, above a selection and in the toolbar of an open conversation.
The menu offers **Later today**, **Tomorrow**, **This weekend**, **Next week**, **Pick a date and
time**, and **Unsnooze** on something already snoozed. Later today is 18:00 and is offered only
+4 -1
View File
@@ -15,6 +15,9 @@
derives per-row URLs itself
snoozed — whether the target is currently snoozed, which is the only
thing that puts "Unsnooze" on the menu
align — "left" to open from the button's left edge. The default
opens from its right, which suits a button at the end of a
row and runs off the pane from one at the start of a toolbar
#}
{% set snoozed = snoozed|default(false) %}
{% set url = url|default(null) %}
@@ -23,7 +26,7 @@
<div data-ui--dropdown-target="menu" hidden
data-controller="mail--snooze-menu"
data-action="ui--dropdown:opened->mail--snooze-menu#refresh"
class="absolute top-full mt-1 right-0 z-20 min-w-56 popover py-1">
class="absolute top-full mt-1 {{ 'left' == align|default('right') ? 'left-0' : 'right-0' }} z-20 min-w-56 popover py-1">
<p class="px-3 py-1 text-[10px] font-semibold uppercase tracking-wide text-ink-faint">
{{ 'thread_row.actions.snooze'|trans }}
+31
View File
@@ -64,6 +64,37 @@ self-contained: no {% extends %}, no block dependencies.
{% endif %}
{{ tbtn('click->mail--message-actions#markUnread', 'fa-regular fa-envelope', 'toolbar.actions.mark_unread'|trans) }}
{# Snooze, which every row offered and the conversation itself did not:
deciding "not now" is something done after reading a mail at least
as often as before opening it, and from here it meant going back to
the list to find the row again. It is also what `b` presses — see
ui--shortcuts, whose keys do nothing where there is no control.
Where the row offers it and for the row's reason: not for mail that
is already put away. Written out rather than through tbtn() for the
one attribute the macro has no room for. #}
{% if placement == 'normal' %}
<div class="relative" data-controller="ui--dropdown">
<button type="button"
data-action="click->ui--dropdown#toggle"
data-shortcut="snooze"
aria-label="{{ 'toolbar.actions.snooze'|trans }}"
title="{{ 'toolbar.actions.snooze'|trans }}"
class="flex items-center justify-center w-8 h-8 rounded-full
text-ink-muted hover:text-ink hover:bg-hover
transition-colors duration-100 cursor-pointer">
<i class="fa-solid fa-clock text-[15px]" aria-hidden="true"></i>
</button>
{% include '_partials/_snooze_menu.html.twig' with {
controller: 'mail--message-actions',
method: 'snooze',
snoozed: thread.isSnoozed,
align: 'left',
} only %}
</div>
{% endif %}
</div>
{% include '_partials/_label_menu.html.twig' with {
+28
View File
@@ -211,6 +211,34 @@ test.describe("keyboard shortcuts", () => {
await expect(page.locator(".compose-window").first()).toBeVisible();
});
/**
* `b` snoozes the conversation that is open.
*
* It did nothing there: every row had a snooze menu and the open
* conversation had none, so there was no control for the key to press. The
* toolbar has one now, and the mail leaves the inbox for Snoozed.
*/
test("b snoozes the open conversation", async ({ page }) => {
await inbox(page);
const subject = INBOX_SUBJECTS.read;
await mailRow(page, subject).click();
await expect(page.locator('[data-shortcut="reply"]')).toBeVisible();
await page.keyboard.press("b");
const option = page.locator('[data-surface="reading"] [data-snooze-key="tomorrow"]');
await expect(option).toBeVisible();
await option.click();
// Back on the list, without it.
await expect(page.locator(ROWS).first()).toBeVisible();
await expect(mailRow(page, subject)).toHaveCount(0);
await page.goto("/mail/snoozed");
await expect(mailRow(page, subject)).toHaveCount(1);
});
test("switched off, a key is only a key", async ({ page }) => {
await setShortcuts(page, "Off");