From 10f0066ed15e03ed6a29adefd6a34ae7a4b0506e Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Sat, 31 Oct 2026 20:44:42 +0100 Subject: [PATCH] fix: name the server when two accounts with one address sync to this device --- .../device-sync/DeviceSyncAccountRow.tsx | 6 ++-- .../__tests__/account-label.test.ts | 31 +++++++++++++++++++ .../settings/device-sync/account-label.ts | 18 +++++++++++ 3 files changed, 53 insertions(+), 2 deletions(-) create mode 100644 src/components/settings/device-sync/__tests__/account-label.test.ts create mode 100644 src/components/settings/device-sync/account-label.ts diff --git a/src/components/settings/device-sync/DeviceSyncAccountRow.tsx b/src/components/settings/device-sync/DeviceSyncAccountRow.tsx index 814783cb..602f8cfd 100644 --- a/src/components/settings/device-sync/DeviceSyncAccountRow.tsx +++ b/src/components/settings/device-sync/DeviceSyncAccountRow.tsx @@ -15,7 +15,7 @@ import { spacing, typography, type ThemePalette } from '../../../theme/tokens'; import { useColors } from '../../../theme/colors'; import { useLocaleStore, type TranslateFn } from '../../../stores/locale-store'; import { useSettingsStore } from '../../../stores/settings-store'; -import type { AccountEntry } from '../../../stores/account-store'; +import { useAccountStore, type AccountEntry } from '../../../stores/account-store'; import { effectiveSelection, emptyAccountDeviceSync, @@ -41,6 +41,7 @@ import { describeSyncStatus, type StatusAction } from './status'; import { useAndroidSync } from './use-android-sync'; import { useSyncCollections } from './use-sync-collections'; import { nameWithAccount } from '../../../lib/address-book-label'; +import { deviceSyncAccountLabel } from './account-label'; interface Props { account: AccountEntry; @@ -117,7 +118,8 @@ export function DeviceSyncAccountRow({ account, authority }: Props) { [collections, entry, authority], ); - const accountLabel = account.email || account.username; + const accounts = useAccountStore((s) => s.accounts); + const accountLabel = React.useMemo(() => deviceSyncAccountLabel(account, accounts), [account, accounts]); const status = describeSyncStatus( { authority, diff --git a/src/components/settings/device-sync/__tests__/account-label.test.ts b/src/components/settings/device-sync/__tests__/account-label.test.ts new file mode 100644 index 00000000..637826c8 --- /dev/null +++ b/src/components/settings/device-sync/__tests__/account-label.test.ts @@ -0,0 +1,31 @@ +import { describe, expect, it } from 'vitest'; +import fs from 'node:fs'; +import path from 'node:path'; +import { deviceSyncAccountLabel } from '../account-label'; + +const account = (id: string, email: string, serverUrl: string) => ({ id, email, username: email, serverUrl }); + +describe('the account name in Sync to this device', () => { + const a = account('usera@10.10.10.62:8016', 'usera@example.org', 'http://10.10.10.62:8016'); + const b = account('usera@10.10.10.62:8120', 'usera@example.org', 'http://10.10.10.62:8120'); + const c = account('userb@mail.example.org', 'userb@example.org', 'https://mail.example.org'); + + it('tells two accounts with the same address apart by their server', () => { + expect(deviceSyncAccountLabel(a, [a, b, c])).toBe('usera@example.org (10.10.10.62:8016)'); + expect(deviceSyncAccountLabel(b, [a, b, c])).toBe('usera@example.org (10.10.10.62:8120)'); + }); + + it('is just the address when it is the only one with it', () => { + expect(deviceSyncAccountLabel(c, [a, b, c])).toBe('userb@example.org'); + expect(deviceSyncAccountLabel(a, [a, c])).toBe('usera@example.org'); + // A default port is left out of the server. + const d = account('usera@mail.example.org', 'USERA@example.org', 'https://mail.example.org'); + expect(deviceSyncAccountLabel(d, [a, d])).toBe('USERA@example.org (mail.example.org)'); + }); + + it('is what the row and its switch are labelled with', () => { + const src = fs.readFileSync(path.resolve(__dirname, '..', 'DeviceSyncAccountRow.tsx'), 'utf8'); + expect(src).toMatch(/const accountLabel = React\.useMemo\(\(\) => deviceSyncAccountLabel\(account, accounts\), \[account, accounts\]\);/); + expect(src).toMatch(/'Sync \{account\} to this device', \{ account: accountLabel \}/); + }); +}); diff --git a/src/components/settings/device-sync/account-label.ts b/src/components/settings/device-sync/account-label.ts new file mode 100644 index 00000000..a452eeca --- /dev/null +++ b/src/components/settings/device-sync/account-label.ts @@ -0,0 +1,18 @@ +// The account's name in "Sync to this device": its address, plus the server +// when another signed-in account has the same address (usera@example.org on +// two servers showed two identical "Sync usera@example.org to this device"). +import { serverHostLabel } from '../../../lib/account-utils'; + +interface LabelledAccount { + id: string; + email: string; + username: string; + serverUrl: string; +} + +export function deviceSyncAccountLabel(account: LabelledAccount, accounts: readonly LabelledAccount[]): string { + const address = account.email || account.username; + const key = address.trim().toLowerCase(); + const twin = accounts.some((a) => a.id !== account.id && (a.email || a.username).trim().toLowerCase() === key); + return twin ? `${address} (${serverHostLabel(account.serverUrl)})` : address; +}