From 505fbc59adbc845e0033e7ae6932072cc91086bb Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Sat, 31 Oct 2026 04:21:37 +0100 Subject: [PATCH] fix: tell two accounts with the same name apart in settings --- src/components/settings/AccountSettings.tsx | 11 ++++---- src/lib/__tests__/account-row-text.test.ts | 27 +++++++++++++++++++ src/lib/account-utils.ts | 30 +++++++++++++++++++++ 3 files changed, 63 insertions(+), 5 deletions(-) create mode 100644 src/lib/__tests__/account-row-text.test.ts diff --git a/src/components/settings/AccountSettings.tsx b/src/components/settings/AccountSettings.tsx index c6cd5f9f..cd5acf67 100644 --- a/src/components/settings/AccountSettings.tsx +++ b/src/components/settings/AccountSettings.tsx @@ -15,7 +15,7 @@ import { setPendingSettingsTab } from '../../navigation/pending-settings-tab'; import { sharedAccountSettingsTabs } from '../../lib/capabilities'; import { jmapClient } from '../../api/jmap-client'; import { fetchMailQuota, type MailQuota } from '../../api/quota'; -import { MAX_ACCOUNTS } from '../../lib/account-utils'; +import { MAX_ACCOUNTS, accountRowText } from '../../lib/account-utils'; import type { RootStackParamList } from '../../navigation/types'; function formatFileSize(bytes: number): string { @@ -168,7 +168,8 @@ export function AccountSettings() { {accounts.map((a, index) => { const isActive = a.id === activeAccountId; const isDefault = a.id === defaultAccountId; - const label = a.email || a.username; + // The address leads when another account has the same name. + const rowText = accountRowText(a, accounts); return ( 0 && styles.rowBorder]}> @@ -183,10 +184,10 @@ export function AccountSettings() { : t('settings.account.accounts.switch_to', 'Switch to this account')} > - {a.displayName || a.username} + {rowText.title} {isActive && } - {label} + {rowText.subtitle ? {rowText.subtitle} : null} {isDefault && ( {t('settings.account.accounts.default_badge', 'Default account')} @@ -226,7 +227,7 @@ export function AccountSettings() { confirmRemove(a.id, label)} + onPress={() => confirmRemove(a.id, a.email || a.username)} disabled={busyId !== null} accessibilityRole="button" accessibilityLabel={t('common.remove', 'Remove')} diff --git a/src/lib/__tests__/account-row-text.test.ts b/src/lib/__tests__/account-row-text.test.ts new file mode 100644 index 00000000..f629b4d1 --- /dev/null +++ b/src/lib/__tests__/account-row-text.test.ts @@ -0,0 +1,27 @@ +import { describe, it, expect } from 'vitest'; +import { accountRowText } from '../account-utils'; + +// One person signed in to two mailboxes has the same name on both, so the +// rows in the switcher and in Settings > Account looked alike; only the +// small address line told them apart (webmail 3bb9f7df8). + +const ada = { id: 'a', displayName: 'Ada Lovelace', email: 'ada@example.com', username: 'ada@example.com' }; +const adaWork = { id: 'b', displayName: 'Ada Lovelace', email: 'ada@work.example', username: 'ada@work.example' }; +const bob = { id: 'c', displayName: 'Bob', email: 'bob@example.com', username: 'bob@example.com' }; + +describe('accountRowText', () => { + it('shows the name over the address', () => { + expect(accountRowText(ada, [ada, bob])).toEqual({ title: 'Ada Lovelace', subtitle: 'ada@example.com' }); + }); + + it('leads with the address when another account has the same name', () => { + expect(accountRowText(ada, [ada, adaWork, bob])).toEqual({ title: 'ada@example.com', subtitle: null }); + expect(accountRowText(adaWork, [ada, adaWork, bob])).toEqual({ title: 'ada@work.example', subtitle: null }); + expect(accountRowText(bob, [ada, adaWork, bob]).title).toBe('Bob'); + }); + + it('does not repeat an address that is also the name', () => { + const plain = { id: 'd', displayName: 'carol@example.com', email: 'carol@example.com', username: 'carol@example.com' }; + expect(accountRowText(plain, [plain])).toEqual({ title: 'carol@example.com', subtitle: null }); + }); +}); diff --git a/src/lib/account-utils.ts b/src/lib/account-utils.ts index ff15c030..a330ecbc 100644 --- a/src/lib/account-utils.ts +++ b/src/lib/account-utils.ts @@ -22,3 +22,33 @@ export function generateAccountId(username: string, serverUrl: string): string { } return `${username}@${host}`; } + +/** What naming an account in a list needs (structural: no AccountEntry import). */ +export interface NamedAccount { + id: string; + displayName?: string; + email?: string; + username: string; +} + +/** + * The bold title and the line under it for an account in a list (webmail + * lib/account-utils.ts accountRowText, 3bb9f7df8). Usually the name over the + * address. One person signed in to two mailboxes has the same name, and + * often the same picture, on both, so the rows only differed in the small + * address line: when another account shares the name, the address becomes + * the title and the name is left out. A name that is just the address again + * is left out too. + */ +export function accountRowText( + account: NamedAccount, + accounts: readonly NamedAccount[], +): { title: string; subtitle: string | null } { + const key = (value: string) => value.trim().toLowerCase(); + const nameOf = (a: NamedAccount) => a.displayName || a.username; + const name = nameOf(account); + const address = account.email || account.username; + const shared = accounts.some((a) => a.id !== account.id && key(nameOf(a)) === key(name)); + if (!name || shared || key(name) === key(address)) return { title: address, subtitle: null }; + return { title: name, subtitle: address }; +}