fix: show attachment names without directional override characters

This commit is contained in:
Linus Rath
2026-10-19 01:32:30 +02:00
parent 546f34f47e
commit 29b33eb46c
4 changed files with 34 additions and 3 deletions
+3 -2
View File
@@ -3,6 +3,7 @@
import { useEffect, useState, type ReactNode } from "react";
import { FileText, FileSpreadsheet, FileImage, FileArchive, File as FileIcon } from "@/components/icons";
import { cn } from "@/lib/utils";
import { stripBidiControls } from "@/lib/bidi";
import type { Attachment, Email } from "@/lib/jmap/types";
import type { LoadListAttachments } from "@/lib/list-attachments";
@@ -78,11 +79,11 @@ export function AttachmentChips({ attachments, onOpen, max = 2, leading, classNa
// The row itself opens the message; a chip must not do both.
onClick={(e) => { e.stopPropagation(); onOpen(a); }}
onDoubleClick={(e) => e.stopPropagation()}
title={a.name}
title={stripBidiControls(a.name ?? "")}
className={cn(CHIP_BOX_CLASS, "max-w-[12rem] bg-background/60 text-foreground/80 hover:bg-muted hover:text-foreground")}
>
<Icon className={cn(CHIP_ICON_CLASS, iconClass)} />
<span className="truncate">{shortName(a.name ?? "")}</span>
<span className="truncate">{shortName(stripBidiControls(a.name ?? ""))}</span>
</button>
);
})}
+4 -1
View File
@@ -11,6 +11,7 @@ import { collapsePlainTextQuotes, setupQuoteCollapse } from "@/lib/quote-collaps
import { DARK_PENDING_CLASS, applyEmailDarkMode, emailColorScheme, emailDarkModeCss, emailHasNativeDarkMode as htmlHasNativeDarkMode, resolveColorSchemeMedia } from "@/lib/email-dark-mode";
import { fitEmailBodyWidth } from "@/lib/email-fit-width";
import { hasFullBleedCanvas } from "@/lib/email-full-bleed";
import { stripBidiControls } from "@/lib/bidi";
import { withBasePath } from "@/lib/browser-navigation";
import { buildContactsPath, buildMailPath } from "@/lib/deep-links";
import { useCopyLink } from "@/hooks/use-copy-link";
@@ -269,7 +270,9 @@ const MIME_TYPE_LABELS: Record<string, string> = {
};
const getAttachmentDisplayName = (name: string | null | undefined, mimeType?: string): string => {
if (name) return name;
// Without directional overrides, which would show `report‮fdp.exe`
// as `reportexe.pdf` (the download name is cleaned separately).
if (name) return stripBidiControls(name);
if (mimeType) {
const label = MIME_TYPE_LABELS[mimeType.toLowerCase()];
if (label) return label;
+15
View File
@@ -0,0 +1,15 @@
import { describe, expect, it } from 'vitest';
import { stripBidiControls } from '@/lib/bidi';
describe('stripBidiControls', () => {
it('removes the characters that reorder what the reader sees', () => {
// An RTL override makes `report‮fdp.exe` read as `reportexe.pdf`.
expect(stripBidiControls('report‮fdp.exe')).toBe('reportfdp.exe');
expect(stripBidiControls('⁦a⁩‎b‏‪‫‬‭')).toBe('ab');
});
it('leaves ordinary text, including right-to-left scripts, alone', () => {
expect(stripBidiControls('Rechnung März.pdf')).toBe('Rechnung März.pdf');
expect(stripBidiControls('تقرير.pdf')).toBe('تقرير.pdf');
});
});
+12
View File
@@ -0,0 +1,12 @@
/**
* Unicode directional formatting characters: the explicit embeddings,
* overrides and isolates (U+202A-202E, U+2066-2069) and the LRM/RLM marks.
* In a sender-controlled name they reorder what the reader sees, so
* `report‮fdp.exe` shows as `reportexe.pdf`.
*/
const BIDI_CONTROLS = /[‎‏‪-‮⁦-⁩]/g;
/** `text` without directional formatting characters, for display. */
export function stripBidiControls(text: string): string {
return text.replace(BIDI_CONTROLS, '');
}