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
This commit is contained in:
jcoffey
2026-09-19 14:06:18 -07:00
committed by GitHub
parent 05d1645ab7
commit 05df758d0a
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. uploaded, which it was not being.
- **Attachment reminder** when the text mentions an attachment and none is there. - **Attachment reminder** when the text mentions an attachment and none is there.
- **Spell check** toggle. - **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. - **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 - **Quoting** on reply, with the signature placed above or below it, and
reply-all as an optional default. 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 | | 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 | | **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 | | **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 | | **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", "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.", "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", "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", "Confirm before deleting": "Vor dem Löschen bestätigen",
"Show message snippets": "Nachrichtenvorschau anzeigen", "Show message snippets": "Nachrichtenvorschau anzeigen",
"Preview the first line of each message in the list.": "Die erste Zeile jeder Nachricht in der Liste 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", "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.", "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", "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", "Confirm before deleting": "Confirmar antes de eliminar",
"Show message snippets": "Mostrar un fragmento de los mensajes", "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.", "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", "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.", "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", "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", "Confirm before deleting": "Confirmer avant de supprimer",
"Show message snippets": "Afficher un aperçu des messages", "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.", "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": "添付忘れの確認", "Attachment reminder": "添付忘れの確認",
"Warn when the message mentions an attachment but none is attached.": "本文で添付に触れているのにファイルが添付されていないとき警告します。", "Warn when the message mentions an attachment but none is attached.": "本文で添付に触れているのにファイルが添付されていないとき警告します。",
"Spell check while typing": "入力中にスペルチェックする", "Spell check while typing": "入力中にスペルチェックする",
"Open the composer full screen": "メールを全画面で作成",
"Confirm before deleting": "削除前に確認する", "Confirm before deleting": "削除前に確認する",
"Show message snippets": "本文の抜粋を表示する", "Show message snippets": "本文の抜粋を表示する",
"Preview the first line of each message in the list.": "一覧に各メールの 1 行目を表示します。", "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", "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.", "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", "Spell check while typing": "Spellingcontrole tijdens het typen",
"Open the composer full screen": "Berichten opstellen op volledig scherm",
"Confirm before deleting": "Bevestigen voor verwijderen", "Confirm before deleting": "Bevestigen voor verwijderen",
"Show message snippets": "Berichtfragmenten tonen", "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.", "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", "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.", "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", "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", "Confirm before deleting": "Confirmar antes de excluir",
"Show message snippets": "Mostrar um trecho das mensagens", "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.", "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": "Напоминание о вложении", "Attachment reminder": "Напоминание о вложении",
"Warn when the message mentions an attachment but none is attached.": "Предупреждать, если письмо упоминает вложение, но его нет.", "Warn when the message mentions an attachment but none is attached.": "Предупреждать, если письмо упоминает вложение, но его нет.",
"Spell check while typing": "Проверять орфографию при вводе", "Spell check while typing": "Проверять орфографию при вводе",
"Open the composer full screen": "Писать письма во весь экран",
"Confirm before deleting": "Спрашивать перед удалением", "Confirm before deleting": "Спрашивать перед удалением",
"Show message snippets": "Показывать начало письма", "Show message snippets": "Показывать начало письма",
"Preview the first line of each message in the list.": "Показывать первую строку каждого письма в списке.", "Preview the first line of each message in the list.": "Показывать первую строку каждого письма в списке.",
+1
View File
@@ -746,6 +746,7 @@ export const catalog: Catalog = {
"Attachment reminder": "Нагадування про вкладення", "Attachment reminder": "Нагадування про вкладення",
"Warn when the message mentions an attachment but none is attached.": "Попереджати, якщо лист згадує вкладення, але його немає.", "Warn when the message mentions an attachment but none is attached.": "Попереджати, якщо лист згадує вкладення, але його немає.",
"Spell check while typing": "Перевіряти орфографію під час введення", "Spell check while typing": "Перевіряти орфографію під час введення",
"Open the composer full screen": "Писати листи на весь екран",
"Confirm before deleting": "Питати перед видаленням", "Confirm before deleting": "Питати перед видаленням",
"Show message snippets": "Показувати початок листа", "Show message snippets": "Показувати початок листа",
"Preview the first line of each message in the list.": "Показувати перший рядок кожного листа у списку.", "Preview the first line of each message in the list.": "Показувати перший рядок кожного листа у списку.",
+1
View File
@@ -748,6 +748,7 @@ export const catalog: Catalog = {
"Attachment reminder": "附件提醒", "Attachment reminder": "附件提醒",
"Warn when the message mentions an attachment but none is attached.": "邮件提到附件但未添加时提醒。", "Warn when the message mentions an attachment but none is attached.": "邮件提到附件但未添加时提醒。",
"Spell check while typing": "输入时检查拼写", "Spell check while typing": "输入时检查拼写",
"Open the composer full screen": "全屏写邮件",
"Confirm before deleting": "删除前确认", "Confirm before deleting": "删除前确认",
"Show message snippets": "显示邮件摘要", "Show message snippets": "显示邮件摘要",
"Preview the first line of each message in the list.": "在列表中显示每封邮件的首行。", "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, showBcc: false,
showReplyTo: false, showReplyTo: false,
minimized: false, minimized: false,
maximized: false, maximized: s.composeMaximized,
dirty: false, dirty: false,
savedAt: null, savedAt: null,
saving: false, saving: false,
+6
View File
@@ -251,6 +251,11 @@ export interface Settings {
archiveOnReply: boolean; archiveOnReply: boolean;
autoAdvance: "newer" | "older" | "list"; autoAdvance: "newer" | "older" | "list";
spellcheck: boolean; 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; sendAndArchive: boolean;
/** Width (px) of the message list when the reading pane is on the right. */ /** Width (px) of the message list when the reading pane is on the right. */
listPaneWidth: number; listPaneWidth: number;
@@ -376,6 +381,7 @@ export const DEFAULT_SETTINGS: Settings = {
archiveOnReply: false, archiveOnReply: false,
autoAdvance: "list", autoAdvance: "list",
spellcheck: true, spellcheck: true,
composeMaximized: false,
sendAndArchive: false, sendAndArchive: false,
listPaneWidth: 520, listPaneWidth: 520,
listPaneHeight: 340, 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("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("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("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> <h2>{t("Locale")}</h2>
<div className="field-row"> <div className="field-row">