diff --git a/components/contacts/__tests__/contact-export.test.ts b/components/contacts/__tests__/contact-export.test.ts
index 1106e5bcc..db0734444 100644
--- a/components/contacts/__tests__/contact-export.test.ts
+++ b/components/contacts/__tests__/contact-export.test.ts
@@ -124,3 +124,12 @@ describe('vCard export of contact photos', () => {
expect(await exported!.text()).toContain('FN:Contact 1');
});
});
+
+describe('vCard file names', () => {
+ it('keep letters of any script instead of turning them into underscores', async () => {
+ const { sanitizeFilename } = await import('../contact-export');
+ expect(sanitizeFilename('Jürgen Müller')).toBe('Jürgen_Müller');
+ expect(sanitizeFilename('李 雷')).toBe('李_雷');
+ expect(sanitizeFilename('a/b:c')).toBe('a_b_c');
+ });
+});
diff --git a/components/contacts/__tests__/contact-form.test.tsx b/components/contacts/__tests__/contact-form.test.tsx
index 3ad1693e4..6550dc98b 100644
--- a/components/contacts/__tests__/contact-form.test.tsx
+++ b/components/contacts/__tests__/contact-form.test.tsx
@@ -44,7 +44,7 @@ describe('ContactForm', () => {
render();
fireEvent.submit(screen.getByText('save').closest('form')!);
await waitFor(() => {
- expect(screen.getByText('name_required')).toBeInTheDocument();
+ expect(screen.getByRole('alert')).toHaveTextContent('name_required');
});
expect(onSave).not.toHaveBeenCalled();
});
@@ -460,7 +460,7 @@ describe('ContactForm', () => {
fireEvent.submit(screen.getByText('save').closest('form')!);
await waitFor(() => {
- expect(screen.getByText('name_required')).toBeInTheDocument();
+ expect(screen.getByRole('alert')).toHaveTextContent('name_required');
});
expect(onSave).not.toHaveBeenCalled();
});
diff --git a/components/contacts/__tests__/contacts-app.test.tsx b/components/contacts/__tests__/contacts-app.test.tsx
index d7f304227..1ff1e1d2a 100644
--- a/components/contacts/__tests__/contacts-app.test.tsx
+++ b/components/contacts/__tests__/contacts-app.test.tsx
@@ -235,4 +235,12 @@ describe('ContactsApp', () => {
fireEvent.click(screen.getByRole('button', { name: /import.title/ }));
expect(screen.getByRole('dialog', { name: 'import.title' })).toBeInTheDocument();
});
+
+ it("marks a message started from Contacts as the app's own, so Mail does not ask for an account", async () => {
+ const { consumePendingMailto } = await import('@/lib/protocol-handlers/session');
+ renderContacts();
+ fireEvent.contextMenu(screen.getByText('Alice Smith'));
+ fireEvent.click(screen.getByText('context_menu.send_email'));
+ expect(consumePendingMailto()).toMatchObject({ to: [expect.stringContaining('alice@example.com')], inApp: true });
+ });
});
diff --git a/components/contacts/contact-export.tsx b/components/contacts/contact-export.tsx
index ba67c4988..3bc6bcedd 100644
--- a/components/contacts/contact-export.tsx
+++ b/components/contacts/contact-export.tsx
@@ -67,6 +67,12 @@ function downloadVcf(content: string, filename: string) {
URL.revokeObjectURL(url);
}
-function sanitizeFilename(name: string): string {
- return name.replace(/[^a-zA-Z0-9_-]/g, "_").substring(0, 50);
+// Letters of any script stay ("Jürgen_Müller.vcf"); only what file systems
+// refuse or treat specially becomes "_".
+export function sanitizeFilename(name: string): string {
+ const safe = Array.from(name.normalize("NFC"), (ch) => (ch.charCodeAt(0) < 32 || '\\/:*?"<>|.'.includes(ch) || /\s/.test(ch) ? "_" : ch))
+ .join("")
+ .replace(/_+/g, "_")
+ .replace(/^_|_$/g, "");
+ return safe.substring(0, 50) || "contact";
}
diff --git a/components/contacts/contact-form.tsx b/components/contacts/contact-form.tsx
index df0b837e5..60e1bce1e 100644
--- a/components/contacts/contact-form.tsx
+++ b/components/contacts/contact-form.tsx
@@ -821,15 +821,11 @@ export function ContactForm({ contact, addressBooks, allKeywords, defaultAddress
setPrefix(e.target.value)} placeholder={t("prefix_placeholder")} className="w-20" />
-
+
setGivenName(e.target.value)} placeholder={t("given_name")} autoFocus />
-
+
setSurname(e.target.value)} placeholder={t("surname")} />
@@ -837,6 +833,8 @@ export function ContactForm({ contact, addressBooks, allKeywords, defaultAddress
setSuffix(e.target.value)} placeholder={t("suffix_placeholder")} className="w-20" />
+ {/* Either name is enough (or an organization), so neither is starred. */}
+ {t("name_required")}
diff --git a/components/contacts/contact-list.tsx b/components/contacts/contact-list.tsx
index 211d23968..6833b8d1e 100644
--- a/components/contacts/contact-list.tsx
+++ b/components/contacts/contact-list.tsx
@@ -577,7 +577,8 @@ export function ContactList({
{/* Bulk action bar */}
{hasSelection && (
-
+ // One line at any width: below 28rem the buttons show only their icons.
+
)}
-
+
{t("bulk.selected", { count: selectedContactIds.size })}
-