diff --git a/services/webmail/messages/en.json b/services/webmail/messages/en.json index aabb645..a69b4b7 100644 --- a/services/webmail/messages/en.json +++ b/services/webmail/messages/en.json @@ -586,5 +586,9 @@ "hintClose": "close", "open": "Command menu", "signOut": "Sign out" + }, + "viewer": { + "asSent": "View as sent", + "adapt": "Match the theme" } } diff --git a/services/webmail/messages/es.json b/services/webmail/messages/es.json index db81fb9..b35da2a 100644 --- a/services/webmail/messages/es.json +++ b/services/webmail/messages/es.json @@ -586,5 +586,9 @@ "hintClose": "cerrar", "open": "Menú de comandos", "signOut": "Cerrar sesión" + }, + "viewer": { + "asSent": "Ver como se envió", + "adapt": "Adaptar al tema" } } diff --git a/services/webmail/messages/pt-BR.json b/services/webmail/messages/pt-BR.json index 6c9ecd6..1a8d2a8 100644 --- a/services/webmail/messages/pt-BR.json +++ b/services/webmail/messages/pt-BR.json @@ -586,5 +586,9 @@ "hintClose": "fechar", "open": "Menu de comandos", "signOut": "Sair" + }, + "viewer": { + "asSent": "Ver como enviado", + "adapt": "Adaptar ao tema" } } diff --git a/services/webmail/public/icons/icon-192.png b/services/webmail/public/icons/icon-192.png index cfb73b0..6b3bcef 100644 Binary files a/services/webmail/public/icons/icon-192.png and b/services/webmail/public/icons/icon-192.png differ diff --git a/services/webmail/public/icons/icon-512.png b/services/webmail/public/icons/icon-512.png index dbfbea3..9e3d3e5 100644 Binary files a/services/webmail/public/icons/icon-512.png and b/services/webmail/public/icons/icon-512.png differ diff --git a/services/webmail/public/icons/maskable-512.png b/services/webmail/public/icons/maskable-512.png index afe8553..49a9811 100644 Binary files a/services/webmail/public/icons/maskable-512.png and b/services/webmail/public/icons/maskable-512.png differ diff --git a/services/webmail/src/app/(user)/user/mail/[folder]/page.tsx b/services/webmail/src/app/(user)/user/mail/[folder]/page.tsx index 9fef3d5..75c9e79 100644 --- a/services/webmail/src/app/(user)/user/mail/[folder]/page.tsx +++ b/services/webmail/src/app/(user)/user/mail/[folder]/page.tsx @@ -30,6 +30,7 @@ import { } from "../../../../../lib/mailCounts"; import { plainSnippet } from "../../../../../lib/snippet"; import { useTrustedSenders } from "../../../../../lib/trustedSenders"; +import { useTheme } from "../../../../../lib/theme"; import type { InlineImages } from "../../../../../lib/emailBody"; import { messageDocument, hasBlockedRemoteContent } from "../../../../../lib/renderMessage"; import { @@ -158,6 +159,9 @@ export default function MailFolderPage({ params }: { params: Promise<{ folder: s const [messages, setMessages] = useState([]); const [selected, setSelected] = useState(null); + // A plainly written message takes the app's colours; this shows it as sent. + const [showOriginal, setShowOriginal] = useState(false); + const { theme } = useTheme(); // The open conversation, when the list is showing conversations. const [selectedThread, setSelectedThread] = useState(null); const conversations = useConversationView(); @@ -444,6 +448,7 @@ export default function MailFolderPage({ params }: { params: Promise<{ folder: s } } setLoadRemoteImages(false); + setShowOriginal(false); setReaderHeight(420); try { const res = await fetch(`/api/v1/mail/message/${uid}?folder=${encodeURIComponent(folder)}`, { @@ -478,13 +483,13 @@ export default function MailFolderPage({ params }: { params: Promise<{ folder: s * markup, then resolve the message's own inline parts, then block or restore * remote sources - which is the only part the reader chooses. */ - const readerDocument = useMemo(() => { + const readerBody = useMemo(() => { if (!selected) return null; // A trusted sender's images load without asking; anyone else's wait for // the reader's choice, one message at a time. const showImages = loadRemoteImages || trustedSenders.isTrusted(selected.from ?? ""); - return messageDocument(selected, showImages); - }, [selected, loadRemoteImages, trustedSenders]); + return messageDocument(selected, showImages, { theme, original: showOriginal }); + }, [selected, loadRemoteImages, trustedSenders, theme, showOriginal]); // Whether anything was actually held back, so the privacy banner is not shown // over a message that has no remote content at all. @@ -794,7 +799,11 @@ export default function MailFolderPage({ params }: { params: Promise<{ folder: s name: target.name, label: FOLDER_LABELS[(target.special_use || target.name).toLowerCase()] ?? target.name, }))} - document={readerDocument} + document={readerBody?.document ?? null} + paper={readerBody?.paper ?? true} + canAdapt={readerBody?.canAdapt ?? false} + showingOriginal={showOriginal} + onToggleOriginal={() => setShowOriginal((v) => !v)} frameHeight={readerHeight} hasBlockedContent={hasBlockedContent} imagesLoaded={loadRemoteImages || senderTrusted} diff --git a/services/webmail/src/app/apple-icon.png b/services/webmail/src/app/apple-icon.png index 0a694a0..90a2674 100644 Binary files a/services/webmail/src/app/apple-icon.png and b/services/webmail/src/app/apple-icon.png differ diff --git a/services/webmail/src/app/favicon.ico b/services/webmail/src/app/favicon.ico index f8553e0..db11bde 100644 Binary files a/services/webmail/src/app/favicon.ico and b/services/webmail/src/app/favicon.ico differ diff --git a/services/webmail/src/app/icon.svg b/services/webmail/src/app/icon.svg index b3c661c..7b2fa2c 100644 --- a/services/webmail/src/app/icon.svg +++ b/services/webmail/src/app/icon.svg @@ -1,14 +1,5 @@ - - - - - - - - - - - - + + + diff --git a/services/webmail/src/components/Logo.tsx b/services/webmail/src/components/Logo.tsx index a883f0f..35705d3 100644 --- a/services/webmail/src/components/Logo.tsx +++ b/services/webmail/src/components/Logo.tsx @@ -1,36 +1,20 @@ -import React, { useId } from "react"; +import React from "react"; /** - * The LambdaMail mark: a lambda on an ultramarine tile, with the flap of an - * envelope lit across the top. The same drawing as app/icon.svg, inline so it - * costs no request and takes any size. + * The LambdaMail mark: a lambda built as two square-cut strokes of one weight, + * white on an ultramarine tile with continuous corners. The same drawing as + * app/icon.svg and the generated icons, inline so it costs no request and takes + * any size. Flat on purpose: it has to hold at 16 pixels and on either theme. */ +const TILE = "M64.00 32.00 L64.00 38.34 L63.99 40.36 L63.98 41.83 L63.97 43.03 L63.95 44.06 L63.93 44.97 L63.90 45.79 L63.88 46.54 L63.84 47.24 L63.80 47.89 L63.76 48.50 L63.72 49.07 L63.67 49.62 L63.62 50.14 L63.56 50.64 L63.50 51.11 L63.43 51.57 L63.36 52.01 L63.29 52.43 L63.21 52.83 L63.13 53.23 L63.05 53.61 L62.96 53.97 L62.86 54.33 L62.77 54.67 L62.66 55.01 L62.56 55.33 L62.45 55.65 L62.33 55.95 L62.21 56.25 L62.09 56.54 L61.96 56.82 L61.83 57.10 L61.69 57.36 L61.55 57.62 L61.40 57.87 L61.25 58.12 L61.09 58.36 L60.93 58.59 L60.76 58.81 L60.59 59.04 L60.42 59.25 L60.24 59.46 L60.05 59.66 L59.86 59.86 L59.66 60.05 L59.46 60.24 L59.25 60.42 L59.04 60.59 L58.81 60.76 L58.59 60.93 L58.36 61.09 L58.12 61.25 L57.87 61.40 L57.62 61.55 L57.36 61.69 L57.10 61.83 L56.82 61.96 L56.54 62.09 L56.25 62.21 L55.95 62.33 L55.65 62.45 L55.33 62.56 L55.01 62.66 L54.67 62.77 L54.33 62.86 L53.97 62.96 L53.61 63.05 L53.23 63.13 L52.83 63.21 L52.43 63.29 L52.01 63.36 L51.57 63.43 L51.11 63.50 L50.64 63.56 L50.14 63.62 L49.62 63.67 L49.07 63.72 L48.50 63.76 L47.89 63.80 L47.24 63.84 L46.54 63.88 L45.79 63.90 L44.97 63.93 L44.06 63.95 L43.03 63.97 L41.83 63.98 L40.36 63.99 L38.34 64.00 L32.00 64.00 L25.66 64.00 L23.64 63.99 L22.17 63.98 L20.97 63.97 L19.94 63.95 L19.03 63.93 L18.21 63.90 L17.46 63.88 L16.76 63.84 L16.11 63.80 L15.50 63.76 L14.93 63.72 L14.38 63.67 L13.86 63.62 L13.36 63.56 L12.89 63.50 L12.43 63.43 L11.99 63.36 L11.57 63.29 L11.17 63.21 L10.77 63.13 L10.39 63.05 L10.03 62.96 L9.67 62.86 L9.33 62.77 L8.99 62.66 L8.67 62.56 L8.35 62.45 L8.05 62.33 L7.75 62.21 L7.46 62.09 L7.18 61.96 L6.90 61.83 L6.64 61.69 L6.38 61.55 L6.13 61.40 L5.88 61.25 L5.64 61.09 L5.41 60.93 L5.19 60.76 L4.96 60.59 L4.75 60.42 L4.54 60.24 L4.34 60.05 L4.14 59.86 L3.95 59.66 L3.76 59.46 L3.58 59.25 L3.41 59.04 L3.24 58.81 L3.07 58.59 L2.91 58.36 L2.75 58.12 L2.60 57.87 L2.45 57.62 L2.31 57.36 L2.17 57.10 L2.04 56.82 L1.91 56.54 L1.79 56.25 L1.67 55.95 L1.55 55.65 L1.44 55.33 L1.34 55.01 L1.23 54.67 L1.14 54.33 L1.04 53.97 L0.95 53.61 L0.87 53.23 L0.79 52.83 L0.71 52.43 L0.64 52.01 L0.57 51.57 L0.50 51.11 L0.44 50.64 L0.38 50.14 L0.33 49.62 L0.28 49.07 L0.24 48.50 L0.20 47.89 L0.16 47.24 L0.12 46.54 L0.10 45.79 L0.07 44.97 L0.05 44.06 L0.03 43.03 L0.02 41.83 L0.01 40.36 L0.00 38.34 L0.00 32.00 L0.00 25.66 L0.01 23.64 L0.02 22.17 L0.03 20.97 L0.05 19.94 L0.07 19.03 L0.10 18.21 L0.12 17.46 L0.16 16.76 L0.20 16.11 L0.24 15.50 L0.28 14.93 L0.33 14.38 L0.38 13.86 L0.44 13.36 L0.50 12.89 L0.57 12.43 L0.64 11.99 L0.71 11.57 L0.79 11.17 L0.87 10.77 L0.95 10.39 L1.04 10.03 L1.14 9.67 L1.23 9.33 L1.34 8.99 L1.44 8.67 L1.55 8.35 L1.67 8.05 L1.79 7.75 L1.91 7.46 L2.04 7.18 L2.17 6.90 L2.31 6.64 L2.45 6.38 L2.60 6.13 L2.75 5.88 L2.91 5.64 L3.07 5.41 L3.24 5.19 L3.41 4.96 L3.58 4.75 L3.76 4.54 L3.95 4.34 L4.14 4.14 L4.34 3.95 L4.54 3.76 L4.75 3.58 L4.96 3.41 L5.19 3.24 L5.41 3.07 L5.64 2.91 L5.88 2.75 L6.13 2.60 L6.38 2.45 L6.64 2.31 L6.90 2.17 L7.18 2.04 L7.46 1.91 L7.75 1.79 L8.05 1.67 L8.35 1.55 L8.67 1.44 L8.99 1.34 L9.33 1.23 L9.67 1.14 L10.03 1.04 L10.39 0.95 L10.77 0.87 L11.17 0.79 L11.57 0.71 L11.99 0.64 L12.43 0.57 L12.89 0.50 L13.36 0.44 L13.86 0.38 L14.38 0.33 L14.93 0.28 L15.50 0.24 L16.11 0.20 L16.76 0.16 L17.46 0.12 L18.21 0.10 L19.03 0.07 L19.94 0.05 L20.97 0.03 L22.17 0.02 L23.64 0.01 L25.66 0.00 L32.00 0.00 L38.34 0.00 L40.36 0.01 L41.83 0.02 L43.03 0.03 L44.06 0.05 L44.97 0.07 L45.79 0.10 L46.54 0.12 L47.24 0.16 L47.89 0.20 L48.50 0.24 L49.07 0.28 L49.62 0.33 L50.14 0.38 L50.64 0.44 L51.11 0.50 L51.57 0.57 L52.01 0.64 L52.43 0.71 L52.83 0.79 L53.23 0.87 L53.61 0.95 L53.97 1.04 L54.33 1.14 L54.67 1.23 L55.01 1.34 L55.33 1.44 L55.65 1.55 L55.95 1.67 L56.25 1.79 L56.54 1.91 L56.82 2.04 L57.10 2.17 L57.36 2.31 L57.62 2.45 L57.87 2.60 L58.12 2.75 L58.36 2.91 L58.59 3.07 L58.81 3.24 L59.04 3.41 L59.25 3.58 L59.46 3.76 L59.66 3.95 L59.86 4.14 L60.05 4.34 L60.24 4.54 L60.42 4.75 L60.59 4.96 L60.76 5.19 L60.93 5.41 L61.09 5.64 L61.25 5.88 L61.40 6.13 L61.55 6.38 L61.69 6.64 L61.83 6.90 L61.96 7.18 L62.09 7.46 L62.21 7.75 L62.33 8.05 L62.45 8.35 L62.56 8.67 L62.66 8.99 L62.77 9.33 L62.86 9.67 L62.96 10.03 L63.05 10.39 L63.13 10.77 L63.21 11.17 L63.29 11.57 L63.36 11.99 L63.43 12.43 L63.50 12.89 L63.56 13.36 L63.62 13.86 L63.67 14.38 L63.72 14.93 L63.76 15.50 L63.80 16.11 L63.84 16.76 L63.88 17.46 L63.90 18.21 L63.93 19.03 L63.95 19.94 L63.97 20.97 L63.98 22.17 L63.99 23.64 L64.00 25.66Z"; +const LAMBDA = "M15.27 10.00 L23.13 10.00 L48.73 54.00 L40.87 54.00Z M29.96 28.49 L34.04 35.51 L24.80 54.00 L17.20 54.00Z"; + export function Logo({ size = 32, className = "" }: { size?: number; className?: string }) { - // Several of these can be on one page, and a gradient id must be unique. - const id = useId().replace(/:/g, ""); return ( - - - - - - - - - - - + + + {/* Shifted left a little so the glyph, not its bounding box, is centred. */} + ); } diff --git a/services/webmail/src/components/MessageFrame.tsx b/services/webmail/src/components/MessageFrame.tsx index a8f73a8..5834c84 100644 --- a/services/webmail/src/components/MessageFrame.tsx +++ b/services/webmail/src/components/MessageFrame.tsx @@ -11,7 +11,7 @@ import { useTranslations } from "../i18n/provider"; * that sent it. The sandbox has no allow-same-origin: the frame stays on an * opaque origin and can read nothing of this page. */ -export function MessageFrame({ document: html }: { document: string }) { +export function MessageFrame({ document: html, paper = true }: { document: string; paper?: boolean }) { const t = useTranslations(); const frame = useRef(null); const [height, setHeight] = useState(160); @@ -30,7 +30,7 @@ export function MessageFrame({ document: html }: { document: string }) { }, []); return ( -
+