Compose: close on Send and offer Undo from a toast; phone-size touch targets
Clicking Send now closes the compose window right away. The undo offer moved out of the composer into a toast: bottom right on a desktop, a small "Undo" pill with the countdown bottom left on a phone, where sending also returns to the message list. An undone send reopens the draft in a composer. The popped-out compose window keeps its inline undo bar, since closing it would lose the offer. Also: drop the "Reply" label from the From row; on a phone show the Send button as its icon alone, enlarge the Send/Attach/Close icons by half, put the logo beside the menu button, make the reading view's back arrow bolder, and make the list's pagination bar half again as big. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -4,9 +4,9 @@ import { useEffect, useRef, useState } from "react";
|
||||
import { useT } from "../../i18n";
|
||||
import { api, ApiError } from "../../lib/api";
|
||||
import type { AccountOut, AttachmentOut, DraftInput, DraftOut } from "../../lib/types";
|
||||
import { Icon } from "../Icon";
|
||||
import { RecipientChips } from "./RecipientChips";
|
||||
import { RichTextEditor, type RichTextEditorHandle } from "./RichTextEditor";
|
||||
import { hasUndoHost, offerUndo } from "./undoSend";
|
||||
|
||||
const MAX_ATTACHMENT_BYTES = 25 * 1024 * 1024; // 25 MB per file
|
||||
|
||||
@@ -102,6 +102,9 @@ function HeaderButton({
|
||||
);
|
||||
}
|
||||
|
||||
// Send / Attach / Close icons, half again as big at phone width for the thumb.
|
||||
const PHONE_ICON = "max-md:h-[22.5px] max-md:w-[22.5px]";
|
||||
|
||||
const caret = (
|
||||
<svg width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5">
|
||||
<path d="M4 6l4 4 4-4" strokeLinecap="round" strokeLinejoin="round" />
|
||||
@@ -437,6 +440,12 @@ export function Composer({
|
||||
const res = await api.send(draftId.current);
|
||||
// Drive the countdown + auto-close off the server's undo deadline.
|
||||
const until = new Date(res.undo_until).getTime();
|
||||
// With a toast to carry the undo offer, the window closes right away.
|
||||
if (hasUndoHost()) {
|
||||
offerUndo({ outboxId: res.outbox_id, draftId: draftId.current, until });
|
||||
onClose();
|
||||
return;
|
||||
}
|
||||
const remaining = Math.max(0, until - Date.now());
|
||||
setUndo({ outboxId: res.outbox_id, until });
|
||||
setUndoLeft(Math.ceil(remaining / 1000));
|
||||
@@ -609,14 +618,17 @@ export function Composer({
|
||||
disabled={status === "sending" || (!to && !cc)}
|
||||
className="flex items-center gap-2 rounded-md bg-app-accent px-3 py-1.5 text-sm font-medium text-app-accent-fg hover:opacity-90 disabled:opacity-50"
|
||||
>
|
||||
<svg width="15" height="15" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round">
|
||||
<svg width="15" height="15" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round" className={PHONE_ICON}>
|
||||
<path d="M14.5 1.5L7.2 8.8" />
|
||||
<path d="M14.5 1.5L9.8 14.5 7.2 8.8 1.5 6.2 14.5 1.5z" />
|
||||
</svg>
|
||||
{status === "sending" ? t("compose.sending") : t("compose.send")}
|
||||
{/* Phone width: the icon alone. */}
|
||||
<span className="max-md:hidden">
|
||||
{status === "sending" ? t("compose.sending") : t("compose.send")}
|
||||
</span>
|
||||
</button>
|
||||
<HeaderButton onClick={() => fileInputRef.current?.click()} title={t("compose.attach")}>
|
||||
<svg width="15" height="15" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round">
|
||||
<svg width="15" height="15" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round" className={PHONE_ICON}>
|
||||
<path d="M12.5 6.5l-5.2 5.2a2.5 2.5 0 0 1-3.5-3.5l5.4-5.4a1.6 1.6 0 0 1 2.3 2.3l-5.3 5.3a0.8 0.8 0 0 1-1.1-1.1l4.9-4.9" />
|
||||
</svg>
|
||||
{/* Phone width: the paperclip alone, so the close button still fits. */}
|
||||
@@ -724,7 +736,7 @@ export function Composer({
|
||||
</div>
|
||||
)}
|
||||
<HeaderButton onClick={requestClose} title={t("common.close")}>
|
||||
<svg width="15" height="15" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round">
|
||||
<svg width="15" height="15" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" className={PHONE_ICON}>
|
||||
<path d="M4 4l8 8M12 4l-8 8" />
|
||||
</svg>
|
||||
</HeaderButton>
|
||||
@@ -749,10 +761,6 @@ export function Composer({
|
||||
<span className="text-app-muted">{fromName ? `<${fromEmail}>` : fromEmail}</span>
|
||||
{accounts.length > 1 && <span className="text-app-faint">{caret}</span>}
|
||||
</button>
|
||||
<div className="ml-auto flex items-center gap-1 text-xs text-app-muted">
|
||||
<Icon name="reply" size={13} />
|
||||
{t("compose.replyLabel")}
|
||||
</div>
|
||||
{fromOpen && (
|
||||
<div className="absolute left-12 top-full z-30 mt-1 w-80 overflow-hidden rounded-lg border border-app-border bg-app-elevated py-1 shadow-xl">
|
||||
{accounts.map((a) => (
|
||||
|
||||
Reference in New Issue
Block a user