Put "compose as new" where a thumb can find it

It was never absent on a phone: it sits in the menu behind the ⋮ at the
top of a message card. But that is not where anybody looks. On a phone you
act on a thread from the strip at the bottom -- Reply, Reply all, Forward
-- and what is not there is, for practical purposes, not there.

The strip gets an overflow of its own, with compose-as-new in it. A fourth
labelled button does not fit; this does, and it says what it is once
opened.

Measuring to place it turned up something else. Three labelled buttons
want about 390px, and with the overflow rather more: enough for a 430px
phone and not for a 390, 360 or 320 one. The strip was already over that
line on the smaller ones before today, and simply overflowed. It now
wraps, and the spacer that would push the overflow onto a line of its own
is dropped on narrow screens so the buttons wrap as a group.

Closes #181
This commit is contained in:
2026-09-01 10:53:28 -07:00
parent 0c1820e2bb
commit e380e168cd
2 changed files with 23 additions and 2 deletions
+7 -1
View File
@@ -642,7 +642,10 @@ a.menu-item:hover { color: var(--fg); }
.vcard-card { margin: 0 16px 12px; padding: 12px 16px; border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--bg-sunken); display: flex; align-items: center; gap: 12px; }
.unsubscribe-row { margin: 0 16px 8px; font-size: .88em; color: var(--fg-muted); display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.reply-box { margin: 8px 16px 24px; }
.reply-box .reply-prompt { display: flex; gap: 8px; align-items: center; padding: 12px; border: 1px solid var(--border); border-radius: var(--radius); color: var(--fg-muted); }
/* Wraps, because it does not fit. Three labelled buttons and an overflow need
about 390px of it, which a 430px phone has and a 360px one does not -- and it
was already over the line on the smaller ones before the overflow was added. */
.reply-box .reply-prompt { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; padding: 12px; border: 1px solid var(--border); border-radius: var(--radius); color: var(--fg-muted); }
.reply-box .reply-prompt button { display: inline-flex; align-items: center; gap: 6px; padding: 8px 14px; border-radius: 999px; border: 1px solid var(--border-strong); color: var(--fg); font-weight: 500; }
.reply-box .reply-prompt button:hover { background: var(--bg-hover); }
.no-thread { height: 100%; display: flex; align-items: center; justify-content: center; flex-direction: column; gap: 8px; color: var(--fg-muted); }
@@ -1214,6 +1217,9 @@ button.dp-open:disabled { cursor: default; opacity: .5; }
.dp-time:hover { background: var(--bg-hover); }
.dp-time.selected { background: var(--accent); color: var(--accent-fg); }
@media (max-width: 480px) {
/* The spacer would take the whole of the first line and push the overflow
onto a line of its own; packed together they wrap as a group instead. */
.reply-box .reply-prompt .spacer { display: none; }
.dp-datetime { flex-wrap: wrap; }
.dp-datetime .dp-time-field { flex: 1 1 100%; }
.dp-split { flex-direction: column; }
+16 -1
View File
@@ -1,5 +1,5 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { AlertOctagon, Archive, ArrowLeft, ChevronDown, ChevronUp, FolderInput, Forward, Mail, MailOpen, MoreVertical, Printer, Reply, ReplyAll, ShieldCheck, Star, Tag, Trash2, Download } from "lucide-react";
import { AlertOctagon, Archive, ArrowLeft, ChevronDown, ChevronUp, FolderInput, Forward, Mail, MailOpen, MailPlus, MoreVertical, Printer, Reply, ReplyAll, ShieldCheck, Star, Tag, Trash2, Download } from "lucide-react";
import { useMail } from "@/store/mail";
import { useSettings } from "@/store/settings";
import { useCompose } from "@/store/compose";
@@ -42,6 +42,8 @@ export function ThreadView({ threadId, mailboxId, onBack, actions, onNavigate, h
const [allExpanded, setAllExpanded] = useState(false);
const [labelAnchor, setLabelAnchor] = useState<{ x: number; y: number } | null>(null);
const moreMenu = useMenu();
/** The overflow on the reply strip, which is the only per-message menu a phone offers easily. */
const replyMore = useMenu();
const scrollRef = useRef<HTMLDivElement>(null);
const markTimer = useRef<number | null>(null);
const isTouch = useIsTouch();
@@ -289,6 +291,19 @@ export function ThreadView({ threadId, mailboxId, onBack, actions, onNavigate, h
<button onClick={() => void reply(last, "reply")}><Reply size={16} /> {t("Reply")}</button>
<button onClick={() => void reply(last, "replyAll")}><ReplyAll size={16} /> {t("Reply all")}</button>
<button onClick={() => void reply(last, "forward")}><Forward size={16} /> {t("Forward")}</button>
{/*
On a phone this strip is where a thumb goes, and the per-message
menu at the top of a card is not somewhere anybody looks for
"send this again" -- which is how compose-as-new came to be
reported missing on mobile when it was there all along (#181).
A fourth full button does not fit at 500px; this does, and it
spells the action out once opened.
*/}
<span className="spacer" />
<button className="icon-btn" onClick={replyMore.open} aria-label={t("More ways to send this")}><MoreVertical size={18} /></button>
<Popover anchor={replyMore.anchor} onClose={replyMore.close} align="end" width={220}>
<MenuItem icon={<MailPlus size={16} />} label={t("Compose as new")} onClick={() => void useCompose.getState().composeAsNew(last)} />
</Popover>
</div>
</div>
)}