Open the composer full screen, as a setting (#401) (#404)

Settings > General > Composing has a new switch, "Open the composer full
screen". With it on, every new composer, whether a new message, reply,
forward or reopened draft, starts maximized. Restore still shrinks it to
a window. A draft put back after an undone or failed send keeps the size
it had. It's off by default, and on a phone, where the composer already
fills the screen, it changes nothing.

One new string, translated in all nine catalogs. The count falling back
to English stays at 16 in every language.

Fixes #401

Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
jcoffey
2026-09-19 14:06:18 -07:00
committed by GitHub
co-authored by Claude Opus 5
parent 05d1645ab7
commit c201d34377
14 changed files with 61 additions and 2 deletions
+3 -1
View File
@@ -482,6 +482,8 @@ minimizable and maximizable; full-screen on mobile.
uploaded, which it was not being.
- **Attachment reminder** when the text mentions an attachment and none is there.
- **Spell check** toggle.
- **Open the composer full screen**, as a setting, for anyone whose first
move is always Maximize. Off by default; on a phone it changes nothing.
- **Drafts** save as you type and on close, with the save state shown.
- **Quoting** on reply, with the signature placed above or below it, and
reply-all as an optional default.
@@ -921,7 +923,7 @@ not reach another that already has ihasmail open until it signs in again.
| Section | Holds |
| --- | --- |
| **General** | Reading pane, mark-as-read delay, auto-advance, conversation view, snippets, avatars; compose format, quoting, signature placement, spell check; time zone, week start, language & region, date format, time format; `mailto:` handler; export / import / reset |
| **General** | Reading pane, mark-as-read delay, auto-advance, conversation view, snippets, avatars; compose format, quoting, signature placement, spell check, full-screen composer; time zone, week start, language & region, date format, time format; `mailto:` handler; export / import / reset |
| **Privacy & safety** | Remote images and the senders trusted with them, read receipts asked for and answered; the three warnings and the domains they measure against; undo-send window, attachment reminder, confirm-before-delete |
| **Appearance** | Theme, accent color, density, font size, sidebar, swipe actions, interface language |
| **Identities & signatures** | Addresses, names, Reply-To, HTML signatures, the default, and which to hide from the picker |
+1
View File
@@ -754,6 +754,7 @@ export const catalog: Catalog = {
"Attachment reminder": "Anhang-Erinnerung",
"Warn when the message mentions an attachment but none is attached.": "Warnen, wenn die Nachricht einen Anhang erwähnt, aber keiner angehängt ist.",
"Spell check while typing": "Rechtschreibprüfung während der Eingabe",
"Open the composer full screen": "Nachrichten im Vollbild verfassen",
"Confirm before deleting": "Vor dem Löschen bestätigen",
"Show message snippets": "Nachrichtenvorschau anzeigen",
"Preview the first line of each message in the list.": "Die erste Zeile jeder Nachricht in der Liste anzeigen.",
+1
View File
@@ -749,6 +749,7 @@ export const catalog: Catalog = {
"Attachment reminder": "Aviso de adjunto",
"Warn when the message mentions an attachment but none is attached.": "Avisar cuando el mensaje menciona un adjunto pero no hay ninguno.",
"Spell check while typing": "Corrección ortográfica al escribir",
"Open the composer full screen": "Redactar mensajes a pantalla completa",
"Confirm before deleting": "Confirmar antes de eliminar",
"Show message snippets": "Mostrar un fragmento de los mensajes",
"Preview the first line of each message in the list.": "Mostrar la primera línea de cada mensaje en la lista.",
+1
View File
@@ -755,6 +755,7 @@ export const catalog: Catalog = {
"Attachment reminder": "Rappel de pièce jointe",
"Warn when the message mentions an attachment but none is attached.": "Avertir lorsque le message mentionne une pièce jointe alors qu'aucune n'est jointe.",
"Spell check while typing": "Vérification orthographique pendant la saisie",
"Open the composer full screen": "Rédiger les messages en plein écran",
"Confirm before deleting": "Confirmer avant de supprimer",
"Show message snippets": "Afficher un aperçu des messages",
"Preview the first line of each message in the list.": "Afficher la première ligne de chaque message dans la liste.",
+1
View File
@@ -749,6 +749,7 @@ export const catalog: Catalog = {
"Attachment reminder": "添付忘れの確認",
"Warn when the message mentions an attachment but none is attached.": "本文で添付に触れているのにファイルが添付されていないとき警告します。",
"Spell check while typing": "入力中にスペルチェックする",
"Open the composer full screen": "メールを全画面で作成",
"Confirm before deleting": "削除前に確認する",
"Show message snippets": "本文の抜粋を表示する",
"Preview the first line of each message in the list.": "一覧に各メールの 1 行目を表示します。",
+1
View File
@@ -747,6 +747,7 @@ export const catalog: Catalog = {
"Attachment reminder": "Bijlageherinnering",
"Warn when the message mentions an attachment but none is attached.": "Waarschuwen wanneer het bericht een bijlage noemt maar er geen is bijgevoegd.",
"Spell check while typing": "Spellingcontrole tijdens het typen",
"Open the composer full screen": "Berichten opstellen op volledig scherm",
"Confirm before deleting": "Bevestigen voor verwijderen",
"Show message snippets": "Berichtfragmenten tonen",
"Preview the first line of each message in the list.": "De eerste regel van elk bericht in de lijst tonen.",
+1
View File
@@ -752,6 +752,7 @@ export const catalog: Catalog = {
"Attachment reminder": "Aviso de anexo",
"Warn when the message mentions an attachment but none is attached.": "Avisar quando a mensagem mencionar um anexo mas nenhum estiver anexado.",
"Spell check while typing": "Verificação ortográfica ao digitar",
"Open the composer full screen": "Escrever mensagens em tela cheia",
"Confirm before deleting": "Confirmar antes de excluir",
"Show message snippets": "Mostrar um trecho das mensagens",
"Preview the first line of each message in the list.": "Mostrar a primeira linha de cada mensagem na lista.",
+1
View File
@@ -752,6 +752,7 @@ export const catalog: Catalog = {
"Attachment reminder": "Напоминание о вложении",
"Warn when the message mentions an attachment but none is attached.": "Предупреждать, если письмо упоминает вложение, но его нет.",
"Spell check while typing": "Проверять орфографию при вводе",
"Open the composer full screen": "Писать письма во весь экран",
"Confirm before deleting": "Спрашивать перед удалением",
"Show message snippets": "Показывать начало письма",
"Preview the first line of each message in the list.": "Показывать первую строку каждого письма в списке.",
+1
View File
@@ -746,6 +746,7 @@ export const catalog: Catalog = {
"Attachment reminder": "Нагадування про вкладення",
"Warn when the message mentions an attachment but none is attached.": "Попереджати, якщо лист згадує вкладення, але його немає.",
"Spell check while typing": "Перевіряти орфографію під час введення",
"Open the composer full screen": "Писати листи на весь екран",
"Confirm before deleting": "Питати перед видаленням",
"Show message snippets": "Показувати початок листа",
"Preview the first line of each message in the list.": "Показувати перший рядок кожного листа у списку.",
+1
View File
@@ -748,6 +748,7 @@ export const catalog: Catalog = {
"Attachment reminder": "附件提醒",
"Warn when the message mentions an attachment but none is attached.": "邮件提到附件但未添加时提醒。",
"Spell check while typing": "输入时检查拼写",
"Open the composer full screen": "全屏写邮件",
"Confirm before deleting": "删除前确认",
"Show message snippets": "显示邮件摘要",
"Preview the first line of each message in the list.": "在列表中显示每封邮件的首行。",
@@ -0,0 +1,41 @@
import { beforeEach, describe, expect, it } from "vitest";
import { useCompose } from "@/store/compose";
import { useMail } from "@/store/mail";
import { DEFAULT_SETTINGS, useSettings } from "@/store/settings";
/**
* "Open the composer full screen" (#401): the size a new composer starts at.
*
* Only new composers follow it. A draft put back after an undone or failed
* send keeps the size it had, since that is the window somebody was already
* looking at.
*/
beforeEach(() => {
useCompose.setState({ drafts: [], activeKey: null, pendingSends: {} });
useMail.setState({ accountId: "a1", identities: [] as never });
useSettings.setState({ settings: { ...DEFAULT_SETTINGS } });
});
const opened = () => {
const key = useCompose.getState().open();
return useCompose.getState().drafts.find((d) => d.key === key)!;
};
describe("the size a new composer opens at", () => {
it("is the usual window by default", () => {
expect(opened().maximized).toBe(false);
});
it("is full screen with the setting on", () => {
useSettings.setState((s) => ({ settings: { ...s.settings, composeMaximized: true } }));
expect(opened().maximized).toBe(true);
});
it("can still be restored to a window once open", () => {
useSettings.setState((s) => ({ settings: { ...s.settings, composeMaximized: true } }));
const d = opened();
useCompose.getState().update(d.key, { maximized: false });
expect(useCompose.getState().drafts.find((x) => x.key === d.key)!.maximized).toBe(false);
});
});
+1 -1
View File
@@ -137,7 +137,7 @@ function blankDraft(init: Partial<Draft> = {}): Draft {
showBcc: false,
showReplyTo: false,
minimized: false,
maximized: false,
maximized: s.composeMaximized,
dirty: false,
savedAt: null,
saving: false,
+6
View File
@@ -251,6 +251,11 @@ export interface Settings {
archiveOnReply: boolean;
autoAdvance: "newer" | "older" | "list";
spellcheck: boolean;
/**
* Open every new composer full screen (#401). Desktop only: on a phone the
* composer fills the screen already and has no size to choose.
*/
composeMaximized: boolean;
sendAndArchive: boolean;
/** Width (px) of the message list when the reading pane is on the right. */
listPaneWidth: number;
@@ -376,6 +381,7 @@ export const DEFAULT_SETTINGS: Settings = {
archiveOnReply: false,
autoAdvance: "list",
spellcheck: true,
composeMaximized: false,
sendAndArchive: false,
listPaneWidth: 520,
listPaneHeight: 340,
@@ -183,6 +183,7 @@ export function GeneralSettings() {
<Switch locked={isEnforced("includeQuote")} checked={s.includeQuote} onChange={(v) => update({ includeQuote: v })} label={t("Quote original message in replies")} />
<Switch locked={isEnforced("signatureAboveQuote")} checked={s.signatureAboveQuote} onChange={(v) => update({ signatureAboveQuote: v })} label={t("Place signature above quoted text")} />
<Switch locked={isEnforced("spellcheck")} checked={s.spellcheck} onChange={(v) => update({ spellcheck: v })} label={t("Spell check while typing")} />
<Switch locked={isEnforced("composeMaximized")} checked={s.composeMaximized} onChange={(v) => update({ composeMaximized: v })} label={t("Open the composer full screen")} />
<h2>{t("Locale")}</h2>
<div className="field-row">