chore: consolidate platform split checks
Some checks failed
Dependency Audit / dependency-audit (push) Has been cancelled
Module Matrix / module-matrix (push) Has been cancelled

This commit is contained in:
2026-07-10 12:51:19 +02:00
parent 150b720f12
commit 635d25c74c
216 changed files with 23336 additions and 4077 deletions

View File

@@ -1,5 +1,7 @@
import { useEffect, useMemo, useState, type ReactNode } from "react";
import { Archive, LockKeyhole, Paperclip } from "lucide-react";
import { i18nMessage, usePlatformLanguage } from "../i18n/LanguageContext";
import SegmentedControl from "./SegmentedControl";
export type MessageDisplayField = {
label: string;
@@ -43,8 +45,9 @@ export default function MessageDisplayPanel({
deriveTextFromHtml = true,
headers = {},
attachments = [],
emptyText = "Select an item to inspect its content."
emptyText = "i18n:govoplan-core.select_an_item_to_inspect_its_content.1f67f131"
}: MessageDisplayPanelProps) {
const { translateText } = usePlatformLanguage();
const visibleFields = fields.filter((field) => hasRenderableValue(field.value));
const headerEntries = Object.entries(headers);
const hasHtml = Boolean(bodyHtml?.trim());
@@ -60,92 +63,99 @@ export default function MessageDisplayPanel({
}, [defaultBodyMode, title, bodyText, bodyHtml, bodyPreview]);
if (!title && visibleFields.length === 0 && !hasText && !hasHtml && attachments.length === 0 && headerEntries.length === 0) {
return <p className="muted">{emptyText}</p>;
return <p className="muted">{translateText(emptyText)}</p>;
}
const activeBodyMode = bodyMode === "html" && hasHtml ? "html" : "text";
const showBodySwitch = hasHtml && hasText;
const htmlBodyFallback = translateText("i18n:govoplan-core.p_no_html_body_content_p.c305bfc6");
const textBodyFallback = translateText("i18n:govoplan-core.no_readable_body_content.37643a01");
return (
<div className="message-display-panel">
<div className="message-display-header">
<h3>{title || "(no subject)"}</h3>
{visibleFields.length > 0 && (
<dl className="message-display-fields">
{visibleFields.map((field) => (
<div key={field.label}>
<h3>{title || "i18n:govoplan-core.no_subject.49b20da0"}</h3>
{visibleFields.length > 0 &&
<dl className="message-display-fields">
{visibleFields.map((field) =>
<div key={field.label}>
<dt>{field.label}</dt>
<dd>{field.value}</dd>
</div>
))}
)}
</dl>
)}
}
</div>
<section className="message-display-body-section" aria-label="Message body">
<section className="message-display-body-section" aria-label="i18n:govoplan-core.message_body.daee2627">
<div className="message-display-section-heading">
<h4>Message</h4>
{showBodySwitch && (
<div className="message-display-body-switch" role="tablist" aria-label="Message body format">
<button type="button" className={activeBodyMode === "html" ? "active" : ""} onClick={() => setBodyMode("html")}>HTML</button>
<button type="button" className={activeBodyMode === "text" ? "active" : ""} onClick={() => setBodyMode("text")}>Text</button>
</div>
)}
<h4>i18n:govoplan-core.message.68f4145f</h4>
{showBodySwitch &&
<SegmentedControl
ariaLabel="i18n:govoplan-core.message_body_format.5fec42d2"
value={activeBodyMode}
onChange={setBodyMode}
options={[
{ id: "html", label: "i18n:govoplan-core.html.9f738ce8" },
{ id: "text", label: "i18n:govoplan-core.text.c3328c39" }
]}
/>
}
</div>
{activeBodyMode === "html" ? (
<iframe className="message-display-html-frame" title="Rendered HTML message body" sandbox="" srcDoc={bodyHtml || "<p>No HTML body content.</p>"} />
) : (
<pre className="message-display-body">{textBody || "No readable body content."}</pre>
)}
{activeBodyMode === "html" ?
<iframe className="message-display-html-frame" title="i18n:govoplan-core.rendered_html_message_body.8638b634" sandbox="" srcDoc={bodyHtml || htmlBodyFallback} /> :
<pre className="message-display-body">{textBody || textBodyFallback}</pre>
}
</section>
{attachments.length ? (
<div className="message-display-attachments">
<h4>Attachments</h4>
{attachments.length ?
<div className="message-display-attachments">
<h4>i18n:govoplan-core.attachments.6771ade6</h4>
<div className="message-display-attachments-scroll">
{groupedAttachments.direct.length > 0 && (
<div className="message-display-attachment-list">
{groupedAttachments.direct.length > 0 &&
<div className="message-display-attachment-list">
{groupedAttachments.direct.map((attachment, index) => <AttachmentRow key={attachmentKey(attachment, index)} attachment={attachment} index={index} />)}
</div>
)}
{groupedAttachments.archives.map((archive) => (
<section className="message-display-attachment-archive" key={archive.key}>
}
{groupedAttachments.archives.map((archive) =>
<section className="message-display-attachment-archive" key={archive.key}>
<header>
<div>
<strong><Archive size={15} aria-hidden="true" /> {archive.label}</strong>
<span>{archive.items.length} file{archive.items.length === 1 ? "" : "s"} inside ZIP</span>
<span>{archive.items.length} file{archive.items.length === 1 ? "" : "s"} i18n:govoplan-core.inside_zip.68c3fd85</span>
</div>
{archive.protected && (
<div className="message-display-attachment-protection">
<small><LockKeyhole size={13} aria-hidden="true" /> Password protected</small>
{archive.protected &&
<div className="message-display-attachment-protection">
<small><LockKeyhole size={13} aria-hidden="true" /> i18n:govoplan-core.password_protected.09d9e174</small>
{archive.protectionNote && <small className="message-display-attachment-protection-note">{formatProtectionNote(archive.protectionNote)}</small>}
</div>
)}
}
</header>
<div className="message-display-attachment-list">
{archive.items.map((attachment, index) => <AttachmentRow key={attachmentKey(attachment, index)} attachment={attachment} index={index} />)}
</div>
</section>
))}
)}
</div>
</div>
) : null}
</div> :
null}
{headerEntries.length > 0 && (
<details className="message-display-headers">
<summary>Headers</summary>
{headerEntries.length > 0 &&
<details className="message-display-headers">
<summary>i18n:govoplan-core.headers.520de744</summary>
<dl>
{headerEntries.map(([key, value]) => (
<div key={key}><dt>{key}</dt><dd>{value}</dd></div>
))}
{headerEntries.map(([key, value]) =>
<div key={key}><dt>{key}</dt><dd>{value}</dd></div>
)}
</dl>
</details>
)}
</div>
);
}
</div>);
}
function AttachmentRow({ attachment, index }: { attachment: MessageDisplayAttachment; index: number }) {
function AttachmentRow({ attachment, index }: {attachment: MessageDisplayAttachment;index: number;}) {
const contentType = formatContentType(attachment.contentType);
const size = formatBytes(attachment.sizeBytes);
const hasMeta = Boolean(contentType || size);
@@ -153,17 +163,17 @@ function AttachmentRow({ attachment, index }: { attachment: MessageDisplayAttach
<div className="message-display-attachment-row">
<Paperclip size={14} aria-hidden="true" />
<span>
<strong>{attachment.filename || `Attachment ${index + 1}`}</strong>
<strong>{attachment.filename || i18nMessage("i18n:govoplan-core.attachment_value.01801a54", { value0: index + 1 })}</strong>
{attachment.detail && <small className="message-display-attachment-detail">{attachment.detail}</small>}
{hasMeta && (
<small className="message-display-attachment-meta">
{hasMeta &&
<small className="message-display-attachment-meta">
{contentType && <span title={contentType.full}>{contentType.label}</span>}
{size && <span>{size}</span>}
</small>
)}
}
</span>
</div>
);
</div>);
}
function groupAttachments(attachments: MessageDisplayAttachment[]) {
@@ -198,45 +208,51 @@ function hasRenderableValue(value: ReactNode | null | undefined): boolean {
}
function formatProtectionNote(value: string): string {
return value
.replace(/^Password-protected ZIP(?: using)?\s*/i, "")
.replace(/^Password protected(?: using)?\s*/i, "")
.replace(/^using\s+/i, "")
.replace(/\.\s*Encryption:/i, " · Encryption:")
.trim();
const normalized = value.
replace(/^Password-protected ZIP(?: using)?\s*/i, "").
replace(/^Password protected(?: using)?\s*/i, "").
replace(/^using\s+/i, "").
trim();
const encryptionMatch = normalized.match(/^(.*?)\.\s*Encryption:\s*(.*)$/i);
if (!encryptionMatch) return normalized;
const source = encryptionMatch[1].trim();
const method = encryptionMatch[2].trim();
return source ?
i18nMessage("i18n:govoplan-core.value_encryption_value", { value0: source, value1: method }) :
i18nMessage("i18n:govoplan-core.encryption_value", { value0: method });
}
function formatContentType(value?: string | null): { label: string; full: string } | null {
function formatContentType(value?: string | null): {label: string;full: string;} | null {
const full = value?.trim();
if (!full) return null;
const normalized = full.toLowerCase();
const known: Record<string, string> = {
"application/pdf": "PDF",
"application/zip": "ZIP archive",
"application/x-zip-compressed": "ZIP archive",
"application/octet-stream": "Binary file",
"text/plain": "Plain text",
"text/html": "HTML",
"text/csv": "CSV",
"application/json": "JSON",
"image/jpeg": "JPEG image",
"image/png": "PNG image",
"image/gif": "GIF image",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": "XLSX spreadsheet",
"application/vnd.ms-excel": "Excel spreadsheet",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": "DOCX document",
"application/msword": "Word document",
"application/vnd.openxmlformats-officedocument.presentationml.presentation": "PPTX presentation",
"application/vnd.ms-powerpoint": "PowerPoint presentation"
"application/pdf": "i18n:govoplan-core.pdf.d613d88c",
"application/zip": "i18n:govoplan-core.zip_archive.5a2430dd",
"application/x-zip-compressed": "i18n:govoplan-core.zip_archive.5a2430dd",
"application/octet-stream": "i18n:govoplan-core.binary_file.a4de1904",
"text/plain": "i18n:govoplan-core.plain_text.9580fcbc",
"text/html": "i18n:govoplan-core.html.9f738ce8",
"text/csv": "i18n:govoplan-core.csv.32811883",
"application/json": "i18n:govoplan-core.json.031a4e76",
"image/jpeg": "i18n:govoplan-core.jpeg_image.6876b7ff",
"image/png": "i18n:govoplan-core.png_image.6715d4b8",
"image/gif": "i18n:govoplan-core.gif_image.d092a779",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": "i18n:govoplan-core.xlsx_spreadsheet.db8c2fc9",
"application/vnd.ms-excel": "i18n:govoplan-core.excel_spreadsheet.7107fea2",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": "i18n:govoplan-core.docx_document.f0b09822",
"application/msword": "i18n:govoplan-core.word_document.f593629d",
"application/vnd.openxmlformats-officedocument.presentationml.presentation": "i18n:govoplan-core.pptx_presentation.855d193b",
"application/vnd.ms-powerpoint": "i18n:govoplan-core.powerpoint_presentation.b32b7115"
};
if (known[normalized]) return { label: known[normalized], full };
const subtype = normalized.split("/")[1]?.split(";")[0] || normalized;
const cleaned = subtype
.replace(/^vnd\./, "")
.replace(/^x-/, "")
.replace(/openxmlformats-officedocument\./g, "")
.replace(/[.+_-]+/g, " ")
.trim();
const cleaned = subtype.
replace(/^vnd\./, "").
replace(/^x-/, "").
replace(/openxmlformats-officedocument\./g, "").
replace(/[.+_-]+/g, " ").
trim();
const label = titleCase(cleaned || normalized);
return { label: label.length > 34 ? `${label.slice(0, 31)}...` : label, full };
}
@@ -247,9 +263,9 @@ function titleCase(value: string): string {
function formatBytes(value?: number | null): string {
if (!value) return "";
if (value < 1024) return `${value} B`;
if (value < 1024 * 1024) return `${(value / 1024).toFixed(1)} KB`;
return `${(value / 1024 / 1024).toFixed(1)} MB`;
if (value < 1024) return i18nMessage("i18n:govoplan-core.bytes_b", { value0: value });
if (value < 1024 * 1024) return i18nMessage("i18n:govoplan-core.bytes_kb", { value0: (value / 1024).toFixed(1) });
return i18nMessage("i18n:govoplan-core.bytes_mb", { value0: (value / 1024 / 1024).toFixed(1) });
}
function stripHtml(value: string): string {