diff --git a/frontend/public/icon-maskable-192.png b/frontend/public/icon-maskable-192.png index 395f73c..32e778b 100644 Binary files a/frontend/public/icon-maskable-192.png and b/frontend/public/icon-maskable-192.png differ diff --git a/frontend/public/icon-maskable-512.png b/frontend/public/icon-maskable-512.png index 43336b8..f224a78 100644 Binary files a/frontend/public/icon-maskable-512.png and b/frontend/public/icon-maskable-512.png differ diff --git a/frontend/src/components/thread/ThreadView.tsx b/frontend/src/components/thread/ThreadView.tsx index 6ef64a7..15caa4e 100644 --- a/frontend/src/components/thread/ThreadView.tsx +++ b/frontend/src/components/thread/ThreadView.tsx @@ -464,6 +464,8 @@ function MessageItem({ const t = useT(); const { locale } = useLocale(); const [showDetails, setShowDetails] = useState(false); + // On a phone the reply / translate / actions buttons get a row of their own. + const mobile = useIsMobile(); // Translation (app/translation.py): the message's languages, as the server // found them ("zh-Hans,pl,en"), and the language it is shown in (null: as sent). const [language, setLanguage] = useState(message.content_language ?? ""); @@ -511,6 +513,73 @@ function MessageItem({ const allTargets = replyTargets(message, ownAddresses, "reply_all"); const multiRecipient = allTargets.to.length + allTargets.cc.length > 1; + // The full header (From, To, Cc, Subject, Date, Size), opened with ▼. + const details = ( +
+
{t("mail.from")}
+
{formatList(message.from_addr)}
+
{t("mail.to")}
+
{formatList(message.to_addr)}
+ {message.cc_addr.length > 0 && ( + <> +
Cc
+
{formatList(message.cc_addr)}
+ + )} +
{t("mail.subjectLabel")}
+
{message.subject || "(no subject)"}
+
{t("mail.dateLabel")}
+
{fmtFull(message.date)}
+
{t("mail.sizeLabel")}
+
{fmtSize(message.size_bytes)}
+
+ ); + + // Reply, Translate, Actions and plugin verbs: beside the recipients, or on + // a phone on a row below them. + const actions = ( + <> + {canReply && ( + + )} + {offerTranslate && (target || translateTo) && ( + + )} + + {/* Plugin-contributed per-message actions (SPEC §11.3 message.actions). */} + + + ); + return (
{t("mail.to")} {recipientSummary(message.to_addr)} - {showDetails ? "▲" : "▼"} + {showDetails ? "▲" : "▼"}
{location && {location}} @@ -592,73 +661,22 @@ function MessageItem({ }`} /> - {canReply && ( - - )} - {offerTranslate && (target || translateTo) && ( - - )} - - {/* Plugin-contributed per-message actions (SPEC §11.3 message.actions). */} - + {!mobile && actions}
+ {mobile && ( +
{actions}
+ )} {alsoIn.length > 0 && (
{t("mail.alsoIn")} {alsoIn.join(", ")}
)} - {showDetails && ( -
-
{t("mail.from")}
-
{formatList(message.from_addr)}
-
{t("mail.to")}
-
{formatList(message.to_addr)}
- {message.cc_addr.length > 0 && ( - <> -
Cc
-
{formatList(message.cc_addr)}
- - )} -
{t("mail.subjectLabel")}
-
{message.subject || "(no subject)"}
-
{t("mail.dateLabel")}
-
{fmtFull(message.date)}
-
{t("mail.sizeLabel")}
-
{fmtSize(message.size_bytes)}
-
- )} + {showDetails && !mobile && details} + {/* On a phone the full header takes the card's width, not the column beside the avatar. */} + {showDetails && mobile &&
{details}
} {/* Plugin-contributed per-message banner (SPEC §11.3 message.banner). */} -
+
-

{subject || "(no subject)"}

+

+ {subject || "(no subject)"} +

removeThreadLabel.mutate(l)} />
-
+ {/* On a phone the icons get twice the room between them. */} +
{/* Dark message mode (lib/darkMessage.ts) — only the dark theme has it. */} {resolvedTheme === "dark" && ( );