From 5db35dc629c45c307ded2e1afa0c47938d64f87f Mon Sep 17 00:00:00 2001 From: Tutitoos Date: Fri, 11 Sep 2026 17:08:38 +0200 Subject: [PATCH] fix(web): render resilient inbox rows --- apps/web/src/pages.tsx | 63 ++++++++++--------- apps/web/tests/inbox.spec.ts | 8 ++- .../modules/translations/catalogs.json | 14 +++++ 3 files changed, 54 insertions(+), 31 deletions(-) diff --git a/apps/web/src/pages.tsx b/apps/web/src/pages.tsx index cc08817..08c687c 100644 --- a/apps/web/src/pages.tsx +++ b/apps/web/src/pages.tsx @@ -735,10 +735,7 @@ function MessageRow({ onTrash, onReadToggle, onImportant, - readActionLabel, - readActionText, - openLabel, - openActionText, + t, actionsEnabled, }: { message: InboxThread; @@ -751,14 +748,17 @@ function MessageRow({ onTrash: () => void; onReadToggle: () => void; onImportant: () => void; - readActionLabel: string; - readActionText: string; - openLabel: string; - openActionText: string; + t: Translator; actionsEnabled: boolean; }) { const [menu, setMenu] = useState<{ x: number; y: number } | null>(null); const menuRef = useRef(null); + const senderText = + message.senderName.trim() || message.senderAddress.trim() || t("unknownSender"); + const subjectText = message.subject.trim() || t("noSubject"); + const previewText = message.preview.trim() || t("noPreview"); + const actionIdentity = message.subject.trim() || senderText; + const readActionText = t(message.isRead ? "markUnread" : "markRead"); useEffect(() => { if (!menu) return; menuRef.current?.querySelector('[role="menuitem"]')?.focus(); @@ -792,7 +792,7 @@ function MessageRow({ className={selected ? "select-box checked" : "select-box"} type="button" onClick={onSelect} - aria-label={`Select ${message.subject || message.senderName}`} + aria-label={`${t("selectMessage")} ${actionIdentity}`} > {selected ? "✓" : ""} @@ -800,7 +800,7 @@ function MessageRow({ className={starred ? "row-icon starred" : "row-icon"} type="button" onClick={onStar} - aria-label={`${starred ? "Unstar" : "Star"} ${message.subject || message.senderName}`} + aria-label={`${t(starred ? "unstarMessage" : "starMessage")} ${actionIdentity}`} disabled={!actionsEnabled} > @@ -808,17 +808,22 @@ function MessageRow({ - {actionsEnabled && (
- - -
@@ -854,7 +863,7 @@ function MessageRow({ ref={menuRef} className="message-context-menu ui-menu" role="menu" - aria-label={`Actions for ${message.subject || message.senderName}`} + aria-label={`${t("actionsFor")} ${actionIdentity}`} style={{ left: menu.x, top: menu.y }} onPointerDown={(event) => event.stopPropagation()} onKeyDown={(event) => { @@ -887,7 +896,7 @@ function MessageRow({ setMenu(null); }} > - {openActionText} + {t("openMessage")} {actionsEnabled && ( <> @@ -900,7 +909,7 @@ function MessageRow({ setMenu(null); }} > - Archive + {t("archive")} )} @@ -1002,12 +1012,7 @@ function VirtualMessageList({ message.id, ]) } - readActionLabel={`${t(message.isRead ? "markUnread" : "markRead")} ${ - message.subject || message.senderName - }`} - readActionText={t(message.isRead ? "markUnread" : "markRead")} - openLabel={`${t("openMessage")} ${message.subject || message.senderName}`} - openActionText={t("openMessage")} + t={t} actionsEnabled={actionsEnabled} /> diff --git a/apps/web/tests/inbox.spec.ts b/apps/web/tests/inbox.spec.ts index cc2b4de..fd77506 100644 --- a/apps/web/tests/inbox.spec.ts +++ b/apps/web/tests/inbox.spec.ts @@ -19,8 +19,8 @@ test("live inbox virtualizes large account-scoped pages and preserves selection" const syntheticThreads = Array.from({ length: itemCount }, (_, index) => ({ id: `10000000-0000-7000-8000-${index.toString(16).padStart(12, "0")}`, accountId: account.id, - senderName: `Sender ${index}`, - senderAddress: `sender-${index}@example.test`, + senderName: index === 1 ? "" : `Sender ${index}`, + senderAddress: index === 1 ? "" : `sender-${index}@example.test`, subject: "", preview: "", lastMessageAt: "2026-09-07T17:00:00Z", @@ -397,6 +397,10 @@ test("live inbox virtualizes large account-scoped pages and preserves selection" await page.getByRole("button", { name: "Refresh" }).click(); await expect(firstSelection).toBeVisible({ timeout: 20_000 }); await expect(firstSelection).toHaveClass(/checked/); + await expect(page.locator(".message-row").first()).toContainText("(No subject)"); + await expect(page.locator(".message-row").first()).toContainText("No preview"); + await expect(page.locator(".message-row").first()).not.toContainText("— No preview"); + await expect(page.locator(".message-row").nth(1)).toContainText("Unknown sender"); await page.getByRole("button", { name: "Open Sender 0" }).focus(); await page.keyboard.press("Enter"); diff --git a/services/api/internal/modules/translations/catalogs.json b/services/api/internal/modules/translations/catalogs.json index 4f8cfab..ae73c6f 100644 --- a/services/api/internal/modules/translations/catalogs.json +++ b/services/api/internal/modules/translations/catalogs.json @@ -65,6 +65,13 @@ "justNow": "Just now", "noMessages": "No messages here", "noSubject": "(No subject)", + "unknownSender": "Unknown sender", + "noPreview": "No preview", + "selectMessage": "Select", + "starMessage": "Star", + "unstarMessage": "Unstar", + "actionsFor": "Actions for", + "markUnimportant": "Mark unimportant", "openMessage": "Open", "toRecipients": "to", "messageFrom": "Message from", @@ -414,6 +421,13 @@ "justNow": "Ahora mismo", "noMessages": "No hay mensajes aquí", "noSubject": "(Sin asunto)", + "unknownSender": "Remitente desconocido", + "noPreview": "Sin vista previa", + "selectMessage": "Seleccionar", + "starMessage": "Destacar", + "unstarMessage": "Quitar destacado", + "actionsFor": "Acciones para", + "markUnimportant": "Marcar como no importante", "openMessage": "Abrir", "toRecipients": "para", "messageFrom": "Mensaje de",