diff --git a/components/identity/identity-form.tsx b/components/identity/identity-form.tsx index e48c83d54..b1717b352 100644 --- a/components/identity/identity-form.tsx +++ b/components/identity/identity-form.tsx @@ -34,6 +34,7 @@ export function IdentityForm({ identity, onSave, onCancel, onDirtyChange }: Iden const t = useTranslations('identities.form'); const tValidation = useTranslations('identities.validation_errors'); const tOrg = useTranslations('org_signature'); + const tName = useTranslations('uxs2_ident.your_name'); const isEditing = !!identity; // Composing > Plain text only: the signature is plain text too. const plainTextMode = useSettingsStore((s) => s.plainTextMode); @@ -156,8 +157,9 @@ export function IdentityForm({ identity, onSave, onCancel, onDirtyChange }: Iden
{/* Name */}
+ {/* "Your name", like the Account page: the one name recipients see. */} setFormData({ ...formData, name: e.target.value })} - placeholder={t('name_placeholder')} disabled={isSubmitting} className={errors.name ? 'border-destructive' : ''} - aria-describedby={errors.name ? 'name-error' : undefined} + aria-describedby={errors.name ? 'name-error' : 'identity-name-hint'} aria-invalid={!!errors.name} /> +

{tName('description')}

{errors.name && (
-

{t('create_new')}

+

{tAddr('add')}

void cancelForm()} @@ -247,7 +248,7 @@ export function IdentityManagerModal({ isOpen, onClose }: IdentityManagerModalPr className="mb-6 w-full sm:w-auto" > - {t('create_new')} + {tAddr('add')} )} @@ -261,7 +262,7 @@ export function IdentityManagerModal({ isOpen, onClose }: IdentityManagerModalPr {editingId === identity.id ? (

- {t('edit_identity')} + {tAddr('edit')}

)} setEditingId(identity.id)} disabled={!!editingId || isCreating} > @@ -358,7 +359,7 @@ export function IdentityManagerModal({ isOpen, onClose }: IdentityManagerModalPr {identities.length === 0 && !isCreating && (
-

{t('no_identities')}

+

{tAddr('count', { count: 0 })}

)}
diff --git a/components/layout/__tests__/account-switcher-remove.test.tsx b/components/layout/__tests__/account-switcher-remove.test.tsx index f287edb60..a93fdcacd 100644 --- a/components/layout/__tests__/account-switcher-remove.test.tsx +++ b/components/layout/__tests__/account-switcher-remove.test.tsx @@ -1,9 +1,8 @@ -import { act, fireEvent, render, screen } from '@testing-library/react'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { fireEvent, render, screen } from '@testing-library/react'; +import { beforeEach, describe, expect, it } from 'vitest'; -// Removing an account from the popover: the app's confirm dialog instead of -// the browser's, a control a touch screen can reach, and sign-out rows in -// the menu's plain text colour. +// The popover's rows: no remove button (settings-p2-11), whole addresses, +// and sign-out rows in the menu's plain text colour. const { AccountSwitcher } = await import('../account-switcher'); const { useAccountStore } = await import('@/stores/account-store'); @@ -18,35 +17,34 @@ function account(id: string) { }; } -const removeAccount = vi.fn(); - beforeEach(() => { - removeAccount.mockClear(); useAccountStore.setState({ accounts: [account('a'), account('b')] } as never); - useAuthStore.setState({ activeAccountId: 'a', removeAccount } as never); + useAuthStore.setState({ activeAccountId: 'a' } as never); useSettingsStore.setState({ interfaceLayout: 'default' } as never); }); describe('removing an account from the popover', () => { - it('asks with the app dialog, not the browser, and removes on confirm', async () => { - const browserConfirm = vi.spyOn(window, 'confirm').mockReturnValue(true); + // settings-p2-11: each row had a hidden X next to "Sign out of …" below. + // An account you're not in is removed under Settings > Account, where + // "Manage accounts…" leads. + it('has no remove button on the rows', () => { render(); fireEvent.click(screen.getByTestId('account-switcher')); - fireEvent.click(screen.getByRole('menuitem', { name: 'remove_account' })); - - expect(browserConfirm).not.toHaveBeenCalled(); - const dialog = screen.getByRole('alertdialog', { name: 'remove_account_title' }); - expect(removeAccount).not.toHaveBeenCalled(); - await act(async () => { fireEvent.click(screen.getByRole('button', { name: 'remove_account' })); }); - expect(removeAccount).toHaveBeenCalledWith('b'); - expect(dialog).not.toBeInTheDocument(); - browserConfirm.mockRestore(); + expect(screen.queryByRole('menuitem', { name: 'remove_account' })).toBeNull(); + expect(screen.getAllByTestId('account-option')).toHaveLength(2); }); - it('keeps the remove control visible on a touch screen', () => { + // The address is what tells two same-named accounts apart, and it was cut + // ("dso-ux-settings@example…"). + it('shows the whole address, wrapped, with the full text as a tooltip', () => { + const long = (id: string, email: string) => ({ ...account(id), email, username: email, label: 'Sam', displayName: 'Sam' }); + useAccountStore.setState({ accounts: [long('a', 'dso-ux-settings@example.org'), long('b', 'dso-ux-settings-b@example.org')] } as never); render(); fireEvent.click(screen.getByTestId('account-switcher')); - expect(screen.getByRole('menuitem', { name: 'remove_account' }).className).toContain('pointer-coarse:opacity-100'); + const title = screen.getByText('dso-ux-settings-b@example.org'); + expect(title.className).not.toContain('truncate'); + expect(title.className).toContain('wrap-anywhere'); + expect(title.getAttribute('title')).toBe('dso-ux-settings-b@example.org'); }); it('draws "sign out of all accounts" like the other rows, start-aligned', () => { diff --git a/components/layout/account-switcher.tsx b/components/layout/account-switcher.tsx index 57e4df9a9..aa1292cde 100644 --- a/components/layout/account-switcher.tsx +++ b/components/layout/account-switcher.tsx @@ -12,7 +12,7 @@ import { useAuthStore } from "@/stores/auth-store"; import { useEmailStore } from "@/stores/email-store"; import { useSettingsStore } from "@/stores/settings-store"; import { HelpMenuItems } from "@/components/layout/help-links"; -import { getMaxAccounts, sortDefaultFirst, reorderNonDefaultIds, accountRowText, accountAvatarName } from "@/lib/account-utils"; +import { getMaxAccounts, sortDefaultFirst, reorderNonDefaultIds, accountRowText, accountAvatarName, accountSharesAddress } from "@/lib/account-utils"; import { inboxUnread, totalInboxUnread } from "@/lib/account-inbox-unread"; import { isDocumentRTL } from "@/i18n/direction"; import { cn, formatFileSize } from "@/lib/utils"; @@ -20,9 +20,6 @@ import { useRouter } from "@/i18n/navigation"; import { onRefusedLoginsChanged, siblingWaits, waitDate } from "@/lib/devices/wipe-state"; import { Avatar } from "@/components/ui/avatar"; import { useMenuNavigation } from "@/hooks/use-menu-navigation"; -import { useConfirmDialog } from "@/hooks/use-confirm-dialog"; -import { ConfirmDialog } from "@/components/ui/confirm-dialog"; -import { IconButton } from "@/components/ui/icon-button"; import { toUnicodeDomain, toUnicodeEmail } from "@/lib/idn"; import { buildSettingsPath } from "@/lib/deep-links"; @@ -118,9 +115,7 @@ export function AccountSwitcher({ const activeAccount = accounts.find((a) => a.id === activeAccountId); const switchAccount = useAuthStore((s) => s.switchAccount); const logout = useAuthStore((s) => s.logout); - const removeAccount = useAuthStore((s) => s.removeAccount); const logoutAll = useAuthStore((s) => s.logoutAll); - const { dialogProps: confirmDialogProps, confirm } = useConfirmDialog(); const tWipe = useTranslations("offlineWipe"); // An account that signed out by itself beside this one: its data waits. const [, noteWaitsChanged] = useState(0); @@ -218,20 +213,6 @@ export function AccountSwitcher({ router.push(buildSettingsPath("account", "browser") as never); }; - const handleRemove = async (e: React.MouseEvent, account: AccountEntry) => { - e.stopPropagation(); - const label = toUnicodeEmail(account.email || account.username); - // The popover closes first: the dialog is the only thing asking now. - setOpen(false); - const ok = await confirm({ - title: t("remove_account_title"), - message: t("remove_account_confirm", { account: label }), - confirmText: t("remove_account"), - variant: "destructive", - }); - if (ok) removeAccount(account.id); - }; - const handleLogout = () => { setOpen(false); logout(); @@ -316,9 +297,7 @@ export function AccountSwitcher({ const unreadFor = (account: AccountEntry) => mvPopover || severalAccounts ? inboxUnread(unreadByAccount[account.id]) : undefined; // The server is only worth a line when it is what tells two rows apart. - const addressKey = (account: AccountEntry) => toUnicodeEmail(account.email || account.username).trim().toLowerCase(); - const sharesAddress = (account: AccountEntry) => - accounts.some((other) => other.id !== account.id && addressKey(other) === addressKey(account)); + const sharesAddress = (account: AccountEntry) => accountSharesAddress(account, accounts); const connectedIds = accounts.filter((account) => account.isConnected).map((account) => account.id); const allInboxesUnread = totalInboxUnread(connectedIds, unreadByAccount); // What waits in the accounts not on screen: a dot on the avatar says so. @@ -341,7 +320,7 @@ export function AccountSwitcher({ : "w-full px-2 py-1.5 hover:bg-muted text-start min-w-0", className )} - title={variant !== "expanded" ? [displayName, othersUnreadText].filter(Boolean).join("\n") : undefined} + title={[displayName, displaySubtitle, variant !== "expanded" ? othersUnreadText : null].filter(Boolean).join("\n")} aria-label={othersUnreadText ? `${t("switch_account")}, ${othersUnreadText}` : t("switch_account")} aria-expanded={open} aria-haspopup="menu" @@ -414,8 +393,8 @@ export function AccountSwitcher({
-

{displayName}

- {displaySubtitle &&

{displaySubtitle}

} +

{displayName}

+ {displaySubtitle &&

{displaySubtitle}

}
)} @@ -479,7 +458,7 @@ export function AccountSwitcher({ className={cn( "w-full flex items-start gap-3 px-3 py-2.5 text-start transition-colors", isActive ? "bg-accent/50" : "hover:bg-muted", - (!isActive && !account.isDefault) ? (isDraggable ? "pe-16" : "pe-10") : (isDraggable && "pe-7") + isDraggable && "pe-7" )} role="menuitem" disabled={isActive} @@ -494,7 +473,7 @@ export function AccountSwitcher({
- + {toUnicodeEmail(text.title)} {account.isDefault && accounts.length > 1 && ( @@ -505,7 +484,7 @@ export function AccountSwitcher({
{text.subtitle && ( -

+

{toUnicodeEmail(text.subtitle)}

)} @@ -524,25 +503,11 @@ export function AccountSwitcher({ {isDraggable && ( )} - {!isActive && !account.isDefault && ( - // Shown on hover and focus, and always on a touch screen (R7). - void handleRemove(e, account)} - role="menuitem" - label={t("remove_account")} - className="absolute end-1.5 top-1/2 -translate-y-1/2 text-muted-foreground opacity-0 transition-opacity group-hover/acct:opacity-100 focus-visible:opacity-100 pointer-coarse:opacity-100 hover:bg-destructive/10 hover:text-destructive" - > - - - )}
); })} @@ -718,7 +683,6 @@ export function AccountSwitcher({
, document.body )} - ); } diff --git a/components/settings/__tests__/account-settings-name-and-accounts.test.tsx b/components/settings/__tests__/account-settings-name-and-accounts.test.tsx new file mode 100644 index 000000000..0aca17da6 --- /dev/null +++ b/components/settings/__tests__/account-settings-name-and-accounts.test.tsx @@ -0,0 +1,112 @@ +import { act, fireEvent, render, screen, within } from '@testing-library/react'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { AccountSettings } from '../account-settings'; +import { useAccountStore } from '@/stores/account-store'; +import { useAuthStore } from '@/stores/auth-store'; +import { useIdentityStore } from '@/stores/identity-store'; +import type { Identity } from '@/lib/jmap/types'; + +// settings-p2-2: Account had "Display name" (Stalwart's account description, +// "DSO UX audit settings (throwaway…)") while mail went out as the identity's +// "Sam Settings". There is one name now: the address's, "Your name". +// settings-p2-11: the server host stood under every account, and the drag +// hint showed with one account. + +vi.mock('next-intl', () => { + const t = (key: string, values?: Record) => (values ? `${key} ${JSON.stringify(values)}` : key); + return { useTranslations: () => t, useLocale: () => 'en' }; +}); +vi.mock('@/lib/stalwart/principal', () => ({ fetchPrincipalDisplayName: vi.fn(async () => 'Admin note (throwaway)') })); +vi.mock('../identity-settings', () => ({ EmailAddressesSection: () => null })); +vi.mock('../layout-settings', () => ({ MultipleAccountsSection: () => null })); +vi.mock('../sign-out-row', () => ({ SignOutRow: () => null })); +vi.mock('../account-archive-settings', () => ({ AccountArchiveSettings: () => null })); + +const sam: Identity = { + id: 'i1', name: 'Sam Settings', email: 'sam@x.test', textSignature: '', htmlSignature: '', mayDelete: false, +}; + +function entry(id: string, email: string, serverUrl = 'https://mail.x.test') { + return { + id, label: 'Admin note (throwaway)', displayName: 'Admin note (throwaway)', email, username: email, serverUrl, + authMode: 'basic' as const, rememberMe: true, cookieSlot: 0, isConnected: true, hasError: false, lastLoginAt: 1, + avatarColor: '#2563eb', isDefault: id === 'a', + }; +} + +const updateIdentity = vi.fn(); +const removeAccount = vi.fn(); + +function signIn(accounts: ReturnType[]) { + useAccountStore.setState({ accounts: accounts as never, activeAccountId: 'a', defaultAccountId: 'a' }); + useIdentityStore.setState({ identities: [sam] }); + useAuthStore.setState({ + username: 'sam@x.test', serverUrl: 'https://mail.x.test', isDemoMode: false, authMode: 'basic', + client: { updateIdentity, getSharedAccounts: () => [] } as never, + activeAccountId: 'a', identities: [sam], primaryIdentity: sam, removeAccount, + } as never); +} + +beforeEach(() => { + updateIdentity.mockReset().mockResolvedValue(undefined); + removeAccount.mockReset(); +}); + +describe('Account: your name', () => { + it("shows the address's name, not the server's account description, and saves it to the address", async () => { + signIn([entry('a', 'sam@x.test')]); + render(); + + const row = screen.getByText('label').closest('[data-search-label]') as HTMLElement; + const input = within(row).getByRole('textbox'); + expect(input).toHaveValue('Sam Settings'); + const profile = document.querySelector('[data-settings-section="profile"]') as HTMLElement; + expect(within(profile).queryByText('Admin note (throwaway)')).toBeNull(); + expect(within(profile).queryByDisplayValue('Admin note (throwaway)')).toBeNull(); + expect(screen.queryByText('name_label')).toBeNull(); + + fireEvent.change(input, { target: { value: 'Sam Lee' } }); + await act(async () => { fireEvent.submit(input.closest('form')!); }); + + expect(updateIdentity).toHaveBeenCalledWith('i1', { name: 'Sam Lee' }); + expect(useAuthStore.getState().primaryIdentity?.name).toBe('Sam Lee'); + // The switcher and the avatar follow the name people see. + await vi.waitFor(() => expect(useAccountStore.getState().getAccountById('a')?.displayName).toBe('Sam Lee')); + }); +}); + +describe('Account: signed-in accounts', () => { + it('shows no server line and no drag hint for one account', () => { + signIn([entry('a', 'sam@x.test')]); + render(); + expect(screen.queryByTestId('account-server')).toBeNull(); + expect(screen.queryByText('mail.x.test')).toBeNull(); + expect(screen.queryByText('accounts.description')).toBeNull(); + expect(screen.queryByRole('button', { name: 'accounts.move_up' })).toBeNull(); + }); + + it('shows the server only when two accounts share an address', () => { + signIn([entry('a', 'sam@x.test'), entry('b', 'sam@y.test', 'https://mail.y.test')]); + const { unmount } = render(); + expect(screen.queryByTestId('account-server')).toBeNull(); + expect(screen.getByText('accounts.description')).toBeTruthy(); + unmount(); + + signIn([entry('a', 'sam@x.test'), entry('b', 'sam@x.test', 'https://mail.y.test')]); + render(); + expect(screen.getAllByTestId('account-server').map((el) => el.textContent)).toEqual(['mail.x.test', 'mail.y.test']); + }); + + it('removes an account you are not in after asking', async () => { + signIn([entry('a', 'sam@x.test'), entry('b', 'sam@y.test')]); + render(); + // Only the other account has a remove button. + const buttons = screen.getAllByRole('button', { name: 'remove_account' }); + expect(buttons).toHaveLength(1); + fireEvent.click(buttons[0]); + const dialog = await screen.findByRole('alertdialog'); + expect(removeAccount).not.toHaveBeenCalled(); + await act(async () => { fireEvent.click(within(dialog).getByRole('button', { name: 'remove_account' })); }); + expect(removeAccount).toHaveBeenCalledWith('b'); + }); +}); diff --git a/components/settings/__tests__/identity-settings.test.tsx b/components/settings/__tests__/identity-settings.test.tsx index 21847d0a9..2908c8973 100644 --- a/components/settings/__tests__/identity-settings.test.tsx +++ b/components/settings/__tests__/identity-settings.test.tsx @@ -1,5 +1,6 @@ import { fireEvent, render, screen, within } from '@testing-library/react'; import { describe, expect, it, vi } from 'vitest'; +import en from '@/locales/en/common.json'; import { EmailAddressesSection } from '../identity-settings'; // SET-19: "Learn more" next to Sub-Addressing opened the identity manager, @@ -21,4 +22,17 @@ describe('Email addresses section', () => { fireEvent.click(screen.getByRole('button', { name: 'manage' })); expect(screen.getByRole('dialog')).toBeTruthy(); }); + + // settings-p2-18: "Email addresses: The email addresses you can send from" + // over "Your sender addresses: Addresses you can send mail from". The + // section says it once; the row only counts them, without "identity". + it('says what the addresses are once, and counts them as addresses', () => { + render(); + const row = screen.getByText('label').closest('[data-search-label]') as HTMLElement; + expect(row.querySelector('p')).toBeNull(); + expect(within(row).getByText('count')).toBeTruthy(); + expect(screen.queryByText('identities_count.description')).toBeNull(); + expect(en.uxs2_ident.addresses.count).not.toMatch(/identit/i); + expect(en.uxs2_ident.addresses.manage).toBe('Edit addresses…'); + }); }); diff --git a/components/settings/__tests__/settings-editors-unsaved.test.tsx b/components/settings/__tests__/settings-editors-unsaved.test.tsx index 89af29a1c..aff1fbda7 100644 --- a/components/settings/__tests__/settings-editors-unsaved.test.tsx +++ b/components/settings/__tests__/settings-editors-unsaved.test.tsx @@ -102,8 +102,9 @@ describe('identity dialog', () => { // In a portal on body, not inside the settings page. expect(screen.getByRole('dialog').parentElement?.parentElement).toBe(document.body); - fireEvent.click(screen.getByRole('button', { name: 'create_new' })); - fireEvent.change(screen.getByPlaceholderText('name_placeholder'), { target: { value: 'Work' } }); + // "Add address" and "Your name" (settings-p2-2, p2-3), no placeholder. + fireEvent.click(screen.getByRole('button', { name: 'add' })); + fireEvent.change(screen.getByLabelText('label'), { target: { value: 'Work' } }); clickOutside(); expect(onClose).not.toHaveBeenCalled(); diff --git a/components/settings/account-security-settings.tsx b/components/settings/account-security-settings.tsx index ec1194a30..1391ef407 100644 --- a/components/settings/account-security-settings.tsx +++ b/components/settings/account-security-settings.tsx @@ -174,60 +174,6 @@ function PasswordChangeSection() { ); } -/** - * Editable display name (the Stalwart principal's name). Rendered by the - * Account page's Profile section; the Security page no longer repeats it. - */ -export function DisplayNameSection() { - const t = useTranslations('settings.security'); - const { displayName, updateDisplayName, isSaving, isLoadingPrincipal } = useAccountSecurityStore(); - const [name, setName] = useState(displayName); - const [saved, setSaved] = useState(false); - - useEffect(() => { - setName(displayName); - }, [displayName]); - - const handleSave = async () => { - try { - await updateDisplayName(name); - setSaved(true); - setTimeout(() => setSaved(false), 2000); - toast.success(t('display_name.success')); - } catch (err) { - toast.error(t('display_name.error'), err instanceof Error ? err.message : undefined); - } - }; - - if (isLoadingPrincipal) { - return ( - - - - ); - } - - return ( - -
- setName(e.target.value)} - placeholder={displayName || t('display_name.placeholder')} - className="w-48" - /> - -
-
- ); -} - function generateTotp(accountLabel: string, issuer: string): { totp: OTPAuth.TOTP; url: string } { const totp = new OTPAuth.TOTP({ issuer, diff --git a/components/settings/account-settings.tsx b/components/settings/account-settings.tsx index 1914fa4d8..cea4ed09b 100644 --- a/components/settings/account-settings.tsx +++ b/components/settings/account-settings.tsx @@ -1,8 +1,8 @@ "use client"; -import { useState, useRef, useCallback, useMemo, useEffect } from 'react'; +import { useState, useRef, useCallback, useMemo } from 'react'; import { useTranslations } from 'next-intl'; -import { Check, GripVertical, Plus, Star, AlertCircle, ChevronRight, ChevronUp, ChevronDown } from '@/components/icons'; +import { Check, GripVertical, Plus, Star, AlertCircle, ChevronRight, ChevronUp, ChevronDown, X } from '@/components/icons'; import { useAuthStore } from '@/stores/auth-store'; import { useEmailStore } from '@/stores/email-store'; import { useAccountStore, type AccountEntry } from '@/stores/account-store'; @@ -12,18 +12,18 @@ import { SettingsSection, SettingItem } from './settings-section'; import { Avatar } from '@/components/ui/avatar'; import { Button } from '@/components/ui/button'; import { useRouter } from '@/i18n/navigation'; -import { getMaxAccounts, accountRowText } from '@/lib/account-utils'; +import { getMaxAccounts, accountRowText, accountSharesAddress } from '@/lib/account-utils'; import { formatFileSize, cn } from '@/lib/utils'; import { toUnicodeDomain, toUnicodeEmail } from '@/lib/idn'; import { IS_LITE } from '@/lib/lite'; -import { isPasswordSession } from '@/lib/auth/password-session'; import { usePolicyStore } from '@/stores/policy-store'; import { AccountArchiveSettings } from './account-archive-settings'; import { SignOutRow } from './sign-out-row'; -import { DisplayNameSection } from './account-security-settings'; +import { YourNameRow } from './your-name-row'; import { EmailAddressesSection } from './identity-settings'; import { MultipleAccountsSection } from './layout-settings'; -import { useAccountSecurityStore } from '@/stores/account-security-store'; +import { ConfirmDialog } from '@/components/ui/confirm-dialog'; +import { useConfirmDialog } from '@/hooks/use-confirm-dialog'; function hostnameOf(serverUrl: string): string { try { return toUnicodeDomain(new URL(serverUrl).hostname); } catch { return serverUrl; } @@ -42,30 +42,15 @@ function firstScopedTab(caps: SharedAccount['capabilities']): string | null { export function AccountSettings() { const t = useTranslations('settings.account'); const tCommon = useTranslations('common'); + const tSidebar = useTranslations('sidebar'); const router = useRouter(); + const removeAccount = useAuthStore((s) => s.removeAccount); + const { dialogProps: confirmDialogProps, confirm } = useConfirmDialog(); const { username, serverUrl, isDemoMode, primaryIdentity, authMode, client } = useAuthStore(); const activeAccountId = useAuthStore((s) => s.activeAccountId); const switchAccount = useAuthStore((s) => s.switchAccount); const setManagedAccount = useManagedAccountStore((s) => s.setManagedAccount); const archiveEnabled = usePolicyStore((s) => s.isFeatureEnabled('accountArchiveEnabled')) && !IS_LITE && !isDemoMode; - const isAuthenticated = useAuthStore((s) => s.isAuthenticated); - // The display name is editable on Stalwart (the principal's name) for - // password sign-ins; identity-provider and token sessions cannot manage - // the principal. - const { isStalwart, probe, fetchPrincipal } = useAccountSecurityStore(); - const activeAccount = useAccountStore((s) => (activeAccountId ? s.getAccountById(activeAccountId) : undefined)); - const canEditDisplayName = isStalwart === true && isPasswordSession(authMode, activeAccount); - // Wait for `client` before probing, as the Security page does: on reload - // the persisted session flips authenticated before the client reconnects. - useEffect(() => { - if (!isAuthenticated || !client) return; - if (isStalwart === null) { - void probe().then((detected) => { if (detected) void fetchPrincipal(); }); - } else if (isStalwart) { - void fetchPrincipal(); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isAuthenticated, client, isStalwart]); // Shared/group accounts delegated to this session (excludes the user's own // primary account). These can be drilled into for scoped settings editing. @@ -83,10 +68,8 @@ export function AccountSettings() { const draggedIndexRef = useRef(null); const quotaPercentage = quota && quota.total > 0 ? Math.min(Math.round((quota.used / quota.total) * 100), 100) : 0; - // `account.displayName` is refreshed from the server on every login/restore - // (Stalwart principal "Full name" when available - #900); the identity name - // is only a fallback until that entry exists. - const displayName = account?.displayName || primaryIdentity?.name || (isDemoMode ? 'Nora Lindqvist' : undefined); + // Shown when there is no identity to edit: the name the app knows. + const displayName = primaryIdentity?.name || account?.displayName || (isDemoMode ? 'Nora Lindqvist' : undefined); const email = primaryIdentity?.email || account?.email || username; const max = getMaxAccounts(); @@ -131,6 +114,18 @@ export function AccountSettings() { void switchAccount(id); }, [activeAccountId, switchAccount]); + // The switcher has no remove button on its rows; an account you're not in + // is removed here ("Manage accounts…" leads to this list). + const handleRemove = useCallback(async (target: AccountEntry) => { + const ok = await confirm({ + title: tSidebar('remove_account_title'), + message: tSidebar('remove_account_confirm', { account: toUnicodeEmail(target.email || target.username) }), + confirmText: tSidebar('remove_account'), + variant: 'destructive', + }); + if (ok) removeAccount(target.id); + }, [confirm, removeAccount, tSidebar]); + const handleAddAccount = useCallback(() => { router.push(`/login?mode=add-account` as never); }, [router]); @@ -148,14 +143,9 @@ export function AccountSettings() { return (
- {/* Display Name: editable where the server lets us, read-only otherwise. */} - {canEditDisplayName ? ( - - ) : ( - - {displayName || tCommon('unknown')} - - )} + {/* One name: the primary address's, what recipients see (settings-p2-2). + Stalwart's principal description is an admin's field and isn't shown. */} + {/* Email Address */} @@ -225,13 +215,20 @@ export function AccountSettings() { {/* Logged-in accounts list */} {accounts.length > 0 && ( - + 1 ? t('accounts.description') : undefined} + >
{accounts.map((a, index) => ( 1} index={index} isActive={a.id === activeAccountId} isFirst={index === 0} @@ -245,6 +242,7 @@ export function AccountSettings() { onMoveDown={() => moveAccount(index, index + 1)} onSwitch={() => handleSwitch(a.id)} onSetDefault={() => setDefaultAccount(a.id)} + onRemove={() => void handleRemove(a)} labels={{ active: t('accounts.active'), default: t('accounts.default_badge'), @@ -253,6 +251,7 @@ export function AccountSettings() { moveUp: t('accounts.move_up'), moveDown: t('accounts.move_down'), dragHandle: t('accounts.drag_handle'), + remove: tSidebar('remove_account'), }} /> ))} @@ -272,6 +271,8 @@ export function AccountSettings() { )} + + {archiveEnabled && } @@ -320,6 +321,10 @@ export function AccountSettings() { interface AccountRowProps { account: AccountEntry; text: { title: string; subtitle: string | null }; + /** The server line: only for an account in trouble or a shared address. */ + showServer: boolean; + /** Dragging and the arrows: only with two accounts or more. */ + canReorder: boolean; index: number; isActive: boolean; isFirst: boolean; @@ -333,6 +338,7 @@ interface AccountRowProps { onMoveDown: () => void; onSwitch: () => void; onSetDefault: () => void; + onRemove: () => void; labels: { active: string; default: string; @@ -341,12 +347,15 @@ interface AccountRowProps { moveUp: string; moveDown: string; dragHandle: string; + remove: string; }; } function AccountRow({ account, text, + showServer, + canReorder, index, isActive, isFirst, @@ -360,11 +369,12 @@ function AccountRow({ onMoveDown, onSwitch, onSetDefault, + onRemove, labels, }: AccountRowProps) { return (
onDragStart(e, index)} onDragOver={(e) => onDragOver(e, index)} onDrop={(e) => onDrop(e, index)} @@ -378,12 +388,14 @@ function AccountRow({ : 'border-border hover:bg-muted/50' )} > -
- -
+ {canReorder && ( +
+ +
+ )}
- + {toUnicodeEmail(text.title)} {account.isDefault && ( @@ -420,23 +432,18 @@ function AccountRow({ )}
{text.subtitle && ( -

+

{toUnicodeEmail(text.subtitle)}

)} -
- {account.hasError ? ( - - ) : ( - - )} - - {hostnameOf(account.serverUrl)} - -
+ {showServer && ( +
+ {account.hasError && } + + {hostnameOf(account.serverUrl)} + +
+ )}
@@ -451,6 +458,7 @@ function AccountRow({ )} + {canReorder && (<> + )} + {!isActive && ( + + )}
); diff --git a/components/settings/identity-settings.tsx b/components/settings/identity-settings.tsx index 3601bf851..68e21be11 100644 --- a/components/settings/identity-settings.tsx +++ b/components/settings/identity-settings.tsx @@ -27,29 +27,23 @@ export function EmailAddressesSection() { const t = useTranslations('settings.identities'); const tAccount = useTranslations('settings.account'); const tEmail = useTranslations('settings.email_behavior'); + const tAddr = useTranslations('uxs2_ident.addresses'); const { identities } = useIdentityStore(); const { subAddressDelimiter, updateSetting } = useSettingsStore(); const [showModal, setShowModal] = useState(false); return ( <> - - {/* Identity Count */} - + {/* The section says what the addresses are; the row under it doesn't + say it again (settings-p2-18). No "identity": a developer word. */} + +
- {identities.length === 0 - ? t('identities_count.count_zero') - : identities.length === 1 - ? t('identities_count.count_one') - : t('identities_count.count_other', { count: identities.length })} + {tAddr('count', { count: identities.length })}
diff --git a/components/settings/your-name-row.tsx b/components/settings/your-name-row.tsx new file mode 100644 index 000000000..97a817159 --- /dev/null +++ b/components/settings/your-name-row.tsx @@ -0,0 +1,76 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { useTranslations } from 'next-intl'; +import { Check, Loader2 } from '@/components/icons'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { SettingItem } from './settings-section'; +import { useAuthStore } from '@/stores/auth-store'; +import { saveIdentityChanges } from '@/stores/identity-store'; +import { toast } from '@/stores/toast-store'; + +/** + * "Your name" on the Account page: the name on the primary sending address, + * which is what recipients see next to it. It saves to that identity, so the + * Account page and the address editor change the same name (settings-p2-2). + * Without an identity there is nothing to save to, and the row only shows + * the name the app knows. + */ +export function YourNameRow({ fallbackName }: { fallbackName?: string }) { + const t = useTranslations('uxs2_ident.your_name'); + const tForm = useTranslations('identities.form'); + const tCommon = useTranslations('common'); + const client = useAuthStore((s) => s.client); + const identity = useAuthStore((s) => s.primaryIdentity); + const current = identity?.name ?? ''; + const [name, setName] = useState(current); + const [saving, setSaving] = useState(false); + const [saved, setSaved] = useState(false); + + useEffect(() => { setName(current); }, [current]); + + if (!identity || !client) { + return ( + + {fallbackName || tCommon('unknown')} + + ); + } + + const save = async () => { + setSaving(true); + try { + if (!(await saveIdentityChanges(client, identity, { name: name.trim() }))) return; + useAuthStore.getState().syncIdentities(); + setSaved(true); + setTimeout(() => setSaved(false), 2000); + toast.success(t('saved')); + } catch (error) { + toast.error(t('error'), error instanceof Error ? error.message : undefined); + } finally { + setSaving(false); + } + }; + + return ( + + { e.preventDefault(); void save(); }} + > + setName(e.target.value)} + maxLength={256} + autoComplete="name" + className="w-48" + disabled={saving} + /> + + + + ); +} diff --git a/lib/__tests__/settings-search-addresses.test.ts b/lib/__tests__/settings-search-addresses.test.ts new file mode 100644 index 000000000..47f1f1af1 --- /dev/null +++ b/lib/__tests__/settings-search-addresses.test.ts @@ -0,0 +1,34 @@ +import { describe, it, expect } from 'vitest'; +import en from '@/locales/en/common.json'; +import de from '@/locales/de/common.json'; +import { + type SettingsSearchTab, + matchesAllWords, + settingsSubResults, + subResultText, + tabSearchEntries, +} from '../settings-search'; + +// settings-p2-3: "identity", "alias", "sender" and "from address" found +// nothing, and "signature" led to a row that only linked elsewhere. The +// signatures now sit on Composing, and these words find them. +const everywhere = (messages: unknown, query: string) => (Object.keys(tabSearchEntries) as SettingsSearchTab[]).flatMap((tab) => + settingsSubResults(messages, tab, 'Bulwark') + .filter((r) => matchesAllWords(subResultText(r), query)) + .map((r) => `${tab}: ${r.label}`)); + +describe('settings search: addresses and your name', () => { + it('finds the sending addresses on Account for identity and alias', () => { + for (const query of ['identity', 'alias', 'sender address']) { + expect(everywhere(en, query), query).toContain('account: Email addresses'); + } + }); + + it('finds "Your name" for display name', () => { + expect(everywhere(en, 'display name')).toContain('account: Your name'); + }); + + it('finds them in German too', () => { + expect(everywhere(de, 'Identität')).toContain('account: E-Mail-Adressen'); + }); +}); diff --git a/lib/account-utils.ts b/lib/account-utils.ts index 2081326cf..00e537362 100644 --- a/lib/account-utils.ts +++ b/lib/account-utils.ts @@ -1,4 +1,5 @@ import { IS_DESKTOP_BUILD } from '@/lib/desktop/flag'; +import { toUnicodeEmail } from '@/lib/idn'; /** * Utilities for multi-account support: @@ -174,6 +175,19 @@ export function accountRowText( return { title: name, subtitle: address }; } +/** + * Whether another signed-in account has the same address (one mailbox on two + * servers). Only then is the server worth a line under an account: in every + * other case it says nothing the person needs (settings-14, settings-p2-11). + */ +export function accountSharesAddress( + account: NamedAccount, + accounts: NamedAccount[], +): boolean { + const key = (a: NamedAccount) => toUnicodeEmail(a.email || a.username).trim().toLowerCase(); + return accounts.some((other) => other.id !== account.id && key(other) === key(account)); +} + /** * The name an account's avatar takes its initials from. Usually the account's * name. When another account has the same name, both would show the same two diff --git a/lib/settings-search.ts b/lib/settings-search.ts index a6d7cd9f2..ec46be264 100644 --- a/lib/settings-search.ts +++ b/lib/settings-search.ts @@ -64,12 +64,12 @@ const syn = (id: string) => `uxs_structure.search.${id}`; // the label (see descriptionKeyFor) and searched too. export const tabSearchEntries: Record = { account: [ - { label: 'settings.account.name_label' }, + { label: 'uxs2_ident.your_name.label', synonyms: 'uxs2_ident.search.your_name' }, { label: 'settings.account.email.label' }, { label: 'settings.account.auth_method_label' }, { label: 'settings.account.server.label', advanced: true }, - { label: 'settings.account.sections.addresses', synonyms: syn('signature') }, - { label: 'settings.identities.identities_count.label' }, + { label: 'settings.account.sections.addresses', synonyms: 'uxs2_ident.search.addresses' }, + { label: 'uxs2_ident.addresses.label' }, { label: 'settings.identities.sub_addressing.label' }, { label: 'settings.email_behavior.sub_address_delimiter.label', advanced: true }, { label: 'settings.account.accounts.title', when: 'multiAccount' }, diff --git a/locales/ar/common.json b/locales/ar/common.json index 999cafaf0..aa0e9d63c 100644 --- a/locales/ar/common.json +++ b/locales/ar/common.json @@ -7146,6 +7146,39 @@ "app_lock_timeout_hours": "{count, plural, zero {# ساعة} one {ساعة واحدة} two {ساعتان} few {# ساعات} many {# ساعة} other {# ساعة}}", "desktop_attention_title": "على هذا الكمبيوتر" }, + "uxs2_ident": { + "your_name": { + "label": "اسمك", + "description": "يرى الناس هذا الاسم بجانب عنوانك عندما ترسل إليهم رسالة.", + "saved": "تم حفظ اسمك.", + "error": "تعذّر حفظ اسمك. حاول مرة أخرى." + }, + "addresses": { + "description": "العناوين التي يمكنك الإرسال منها، لكل منها اسمه وتوقيعه.", + "label": "عناوينك", + "count": "{count, plural, =0 {لا توجد عناوين بعد} zero {# عنوان} one {عنوان واحد} two {عنوانان} few {# عناوين} many {# عنوانًا} other {# عنوان}}", + "manage": "تعديل العناوين…", + "modal_title": "عناوين بريدك الإلكتروني", + "add": "إضافة عنوان", + "edit": "تعديل العنوان", + "added": "تمت إضافة العنوان.", + "saved": "تم حفظ العنوان.", + "deleted": "تم حذف العنوان.", + "delete_title": "حذف هذا العنوان؟" + }, + "signatures": { + "title": "التوقيعات", + "description": "يُضاف في نهاية الرسائل التي تكتبها. لكل عنوان توقيعه الخاص.", + "saved": "تم حفظ التوقيع.", + "error": "تعذّر حفظ التوقيع. حاول مرة أخرى.", + "no_addresses": "لا يوجد عنوان للإرسال منه بعد." + }, + "search": { + "addresses": "هوية هويات اسم مستعار مرسل عنوان المرسل عنوان من identity alias sender", + "signatures": "توقيع توقيع البريد تذييل هوية مرسل عنوان من signature", + "your_name": "الاسم المعروض الاسم الكامل اسم المرسل display name" + } + }, "desktop_sign_in": { "server_needed": "أدخل عنوان الخادم أولًا.", "server_declined": "لم تسمح بهذا الخادم، لذا لم يُرسل إليه أي شيء.", diff --git a/locales/ca/common.json b/locales/ca/common.json index 091cd601c..08ad74d20 100644 --- a/locales/ca/common.json +++ b/locales/ca/common.json @@ -7146,6 +7146,39 @@ "app_lock_timeout_hours": "{count, plural, one {# hora} other {# hores}}", "desktop_attention_title": "En aquest ordinador" }, + "uxs2_ident": { + "your_name": { + "label": "El vostre nom", + "description": "La gent veu aquest nom al costat de la vostra adreça quan els envieu correu.", + "saved": "S'ha desat el vostre nom.", + "error": "No s'ha pogut desar el vostre nom. Torneu-ho a provar." + }, + "addresses": { + "description": "Les adreces des de les quals podeu enviar, cadascuna amb el seu nom i la seva signatura.", + "label": "Les vostres adreces", + "count": "{count, plural, =0 {Encara no hi ha adreces} one {# adreça} other {# adreces}}", + "manage": "Edita les adreces…", + "modal_title": "Les vostres adreces de correu", + "add": "Afegeix una adreça", + "edit": "Edita l'adreça", + "added": "S'ha afegit l'adreça.", + "saved": "S'ha desat l'adreça.", + "deleted": "S'ha suprimit l'adreça.", + "delete_title": "Voleu suprimir aquesta adreça?" + }, + "signatures": { + "title": "Signatures", + "description": "S'afegeix al final dels missatges que escriviu. Cada adreça té la seva.", + "saved": "S'ha desat la signatura.", + "error": "No s'ha pogut desar la signatura. Torneu-ho a provar.", + "no_addresses": "Encara no teniu cap adreça per enviar." + }, + "search": { + "addresses": "identitat identitats àlies remitent adreça del remitent adreça de identity alias sender", + "signatures": "signatura signatura de correu peu identitat remitent adreça de signature", + "your_name": "nom visible nom complet nom del remitent display name" + } + }, "desktop_sign_in": { "server_needed": "Primer escriu l'adreça del servidor.", "server_declined": "No has permès aquest servidor, així que no s'hi ha enviat res.", diff --git a/locales/cs/common.json b/locales/cs/common.json index b2ae541cf..2a0cb7035 100644 --- a/locales/cs/common.json +++ b/locales/cs/common.json @@ -7146,6 +7146,39 @@ "app_lock_timeout_hours": "{count, plural, one {# hodina} few {# hodiny} many {# hodiny} other {# hodin}}", "desktop_attention_title": "Na tomto počítači" }, + "uxs2_ident": { + "your_name": { + "label": "Vaše jméno", + "description": "Toto jméno lidé vidí vedle vaší adresy, když jim pošlete e-mail.", + "saved": "Vaše jméno je uloženo.", + "error": "Vaše jméno se nepodařilo uložit. Zkuste to znovu." + }, + "addresses": { + "description": "Adresy, ze kterých můžete odesílat, každá s vlastním jménem a podpisem.", + "label": "Vaše adresy", + "count": "{count, plural, =0 {Zatím žádné adresy} one {# adresa} few {# adresy} many {# adresy} other {# adres}}", + "manage": "Upravit adresy…", + "modal_title": "Vaše e-mailové adresy", + "add": "Přidat adresu", + "edit": "Upravit adresu", + "added": "Adresa přidána.", + "saved": "Adresa uložena.", + "deleted": "Adresa smazána.", + "delete_title": "Smazat tuto adresu?" + }, + "signatures": { + "title": "Podpisy", + "description": "Přidá se na konec zpráv, které píšete. Každá adresa má svůj vlastní.", + "saved": "Podpis uložen.", + "error": "Podpis se nepodařilo uložit. Zkuste to znovu.", + "no_addresses": "Zatím nemáte adresu, ze které lze odesílat." + }, + "search": { + "addresses": "identita identity alias aliasy odesílatel adresa odesílatele adresa od identity alias sender", + "signatures": "podpis e-mailový podpis zápatí identita odesílatel adresa od signature", + "your_name": "zobrazované jméno celé jméno jméno odesílatele display name" + } + }, "desktop_sign_in": { "server_needed": "Nejdřív zadejte adresu serveru.", "server_declined": "Tento server jste nepovolili, proto se na něj nic neodeslalo.", diff --git a/locales/da/common.json b/locales/da/common.json index 7d48f4b15..7fddd6fd1 100644 --- a/locales/da/common.json +++ b/locales/da/common.json @@ -7146,6 +7146,39 @@ "app_lock_timeout_hours": "{count, plural, one {# time} other {# timer}}", "desktop_attention_title": "På denne computer" }, + "uxs2_ident": { + "your_name": { + "label": "Dit navn", + "description": "Folk ser dette navn ved siden af din adresse, når du sender dem mail.", + "saved": "Dit navn er gemt.", + "error": "Dit navn kunne ikke gemmes. Prøv igen." + }, + "addresses": { + "description": "De adresser, du kan sende fra, hver med sit eget navn og sin egen signatur.", + "label": "Dine adresser", + "count": "{count, plural, =0 {Ingen adresser endnu} one {# adresse} other {# adresser}}", + "manage": "Rediger adresser…", + "modal_title": "Dine e-mailadresser", + "add": "Tilføj adresse", + "edit": "Rediger adresse", + "added": "Adressen er tilføjet.", + "saved": "Adressen er gemt.", + "deleted": "Adressen er slettet.", + "delete_title": "Slet denne adresse?" + }, + "signatures": { + "title": "Signaturer", + "description": "Tilføjes i slutningen af de beskeder, du skriver. Hver adresse har sin egen.", + "saved": "Signaturen er gemt.", + "error": "Signaturen kunne ikke gemmes. Prøv igen.", + "no_addresses": "Der er endnu ingen adresse at sende fra." + }, + "search": { + "addresses": "identitet identiteter alias afsender afsenderadresse fra-adresse identity alias sender", + "signatures": "signatur e-mailsignatur sidefod identitet afsender fra-adresse signature", + "your_name": "visningsnavn fulde navn afsendernavn display name" + } + }, "desktop_sign_in": { "server_needed": "Angiv først serverens adresse.", "server_declined": "Du tillod ikke denne server, så der blev ikke sendt noget til den.", diff --git a/locales/de/common.json b/locales/de/common.json index e1e33c934..caa48891b 100644 --- a/locales/de/common.json +++ b/locales/de/common.json @@ -7146,6 +7146,39 @@ "app_lock_timeout_hours": "{count, plural, one {# Stunde} other {# Stunden}}", "desktop_attention_title": "Auf diesem Computer" }, + "uxs2_ident": { + "your_name": { + "label": "Ihr Name", + "description": "Diesen Namen sehen andere neben Ihrer Adresse, wenn Sie ihnen schreiben.", + "saved": "Ihr Name ist gespeichert.", + "error": "Ihr Name konnte nicht gespeichert werden. Versuchen Sie es noch einmal." + }, + "addresses": { + "description": "Die Adressen, von denen Sie senden können, jede mit eigenem Namen und eigener Signatur.", + "label": "Ihre Adressen", + "count": "{count, plural, =0 {Noch keine Adressen} one {# Adresse} other {# Adressen}}", + "manage": "Adressen bearbeiten…", + "modal_title": "Ihre E-Mail-Adressen", + "add": "Adresse hinzufügen", + "edit": "Adresse bearbeiten", + "added": "Adresse hinzugefügt.", + "saved": "Adresse gespeichert.", + "deleted": "Adresse gelöscht.", + "delete_title": "Diese Adresse löschen?" + }, + "signatures": { + "title": "Signaturen", + "description": "Steht am Ende der Nachrichten, die Sie schreiben. Jede Adresse hat ihre eigene.", + "saved": "Signatur gespeichert.", + "error": "Die Signatur konnte nicht gespeichert werden. Versuchen Sie es noch einmal.", + "no_addresses": "Es gibt noch keine Adresse zum Senden." + }, + "search": { + "addresses": "Identität Identitäten Alias Aliasse Absender Absenderadresse Von-Adresse Absendername identity alias sender", + "signatures": "Signatur E-Mail-Signatur Grußformel Fußzeile Identität Absender Von-Adresse signature", + "your_name": "Anzeigename vollständiger Name Absendername display name" + } + }, "desktop_sign_in": { "server_needed": "Geben Sie zuerst die Adresse Ihres Servers ein.", "server_declined": "Sie haben diesen Server nicht zugelassen, daher wurde nichts an ihn gesendet.", diff --git a/locales/en/common.json b/locales/en/common.json index ae08611b3..01e55b7a3 100644 --- a/locales/en/common.json +++ b/locales/en/common.json @@ -7146,6 +7146,39 @@ "app_lock_timeout_hours": "{count, plural, one {# hour} other {# hours}}", "desktop_attention_title": "On this computer" }, + "uxs2_ident": { + "your_name": { + "label": "Your name", + "description": "People see this name next to your address when you send them mail.", + "saved": "Your name is saved.", + "error": "Your name could not be saved. Try again." + }, + "addresses": { + "description": "The addresses you can send from, each with its own name and signature.", + "label": "Your addresses", + "count": "{count, plural, =0 {No addresses yet} one {# address} other {# addresses}}", + "manage": "Edit addresses…", + "modal_title": "Your email addresses", + "add": "Add address", + "edit": "Edit address", + "added": "Address added.", + "saved": "Address saved.", + "deleted": "Address deleted.", + "delete_title": "Delete this address?" + }, + "signatures": { + "title": "Signatures", + "description": "Added at the end of the messages you write. Each address has its own.", + "saved": "Signature saved.", + "error": "The signature could not be saved. Try again.", + "no_addresses": "There is no address to send from yet." + }, + "search": { + "addresses": "identity identities alias aliases sender sender address from address from name", + "signatures": "signature email signature sign-off footer identity sender from address", + "your_name": "display name full name sender name from name" + } + }, "desktop_sign_in": { "server_needed": "Enter your server's address first.", "server_declined": "You didn't allow this server, so nothing was sent to it.", diff --git a/locales/es/common.json b/locales/es/common.json index e4c6f39e9..7db263487 100644 --- a/locales/es/common.json +++ b/locales/es/common.json @@ -7146,6 +7146,39 @@ "app_lock_timeout_hours": "{count, plural, one {# hora} other {# horas}}", "desktop_attention_title": "En este ordenador" }, + "uxs2_ident": { + "your_name": { + "label": "Tu nombre", + "description": "La gente ve este nombre junto a tu dirección cuando les envías un correo.", + "saved": "Tu nombre se ha guardado.", + "error": "No se ha podido guardar tu nombre. Inténtalo de nuevo." + }, + "addresses": { + "description": "Las direcciones desde las que puedes enviar, cada una con su propio nombre y firma.", + "label": "Tus direcciones", + "count": "{count, plural, =0 {Todavía no hay direcciones} one {# dirección} other {# direcciones}}", + "manage": "Editar direcciones…", + "modal_title": "Tus direcciones de correo", + "add": "Añadir dirección", + "edit": "Editar dirección", + "added": "Dirección añadida.", + "saved": "Dirección guardada.", + "deleted": "Dirección eliminada.", + "delete_title": "¿Eliminar esta dirección?" + }, + "signatures": { + "title": "Firmas", + "description": "Se añade al final de los mensajes que escribes. Cada dirección tiene la suya.", + "saved": "Firma guardada.", + "error": "No se ha podido guardar la firma. Inténtalo de nuevo.", + "no_addresses": "Todavía no hay ninguna dirección desde la que enviar." + }, + "search": { + "addresses": "identidad identidades alias remitente dirección del remitente dirección de envío identity alias sender", + "signatures": "firma firma de correo pie identidad remitente dirección de envío signature", + "your_name": "nombre visible nombre completo nombre del remitente display name" + } + }, "desktop_sign_in": { "server_needed": "Primero escribe la dirección del servidor.", "server_declined": "No permitiste este servidor, así que no se le envió nada.", diff --git a/locales/et/common.json b/locales/et/common.json index 30ac1ee96..b37427db9 100644 --- a/locales/et/common.json +++ b/locales/et/common.json @@ -7146,6 +7146,39 @@ "app_lock_timeout_hours": "{count, plural, one {# tund} other {# tundi}}", "desktop_attention_title": "Selles arvutis" }, + "uxs2_ident": { + "your_name": { + "label": "Teie nimi", + "description": "Seda nime näevad inimesed teie aadressi kõrval, kui saadate neile kirja.", + "saved": "Teie nimi on salvestatud.", + "error": "Teie nime ei õnnestunud salvestada. Proovige uuesti." + }, + "addresses": { + "description": "Aadressid, millelt saate saata, igaühel oma nimi ja allkiri.", + "label": "Teie aadressid", + "count": "{count, plural, =0 {Aadresse veel pole} one {# aadress} other {# aadressi}}", + "manage": "Muuda aadresse…", + "modal_title": "Teie e-posti aadressid", + "add": "Lisa aadress", + "edit": "Muuda aadressi", + "added": "Aadress lisatud.", + "saved": "Aadress salvestatud.", + "deleted": "Aadress kustutatud.", + "delete_title": "Kas kustutada see aadress?" + }, + "signatures": { + "title": "Allkirjad", + "description": "Lisatakse teie kirjutatud kirjade lõppu. Igal aadressil on oma.", + "saved": "Allkiri salvestatud.", + "error": "Allkirja ei õnnestunud salvestada. Proovige uuesti.", + "no_addresses": "Teil pole veel aadressi, millelt saata." + }, + "search": { + "addresses": "identiteet identiteedid alias saatja saatja aadress saatja nimi identity alias sender", + "signatures": "allkiri e-kirja allkiri jalus identiteet saatja saatja aadress signature", + "your_name": "kuvatav nimi täisnimi saatja nimi display name" + } + }, "desktop_sign_in": { "server_needed": "Sisesta kõigepealt serveri aadress.", "server_declined": "Sa ei lubanud seda serverit, nii et sellele ei saadetud midagi.", diff --git a/locales/fa/common.json b/locales/fa/common.json index 117abe12f..72b21f212 100644 --- a/locales/fa/common.json +++ b/locales/fa/common.json @@ -7146,6 +7146,39 @@ "app_lock_timeout_hours": "{count, plural, one {# ساعت} other {# ساعت}}", "desktop_attention_title": "روی این رایانه" }, + "uxs2_ident": { + "your_name": { + "label": "نام شما", + "description": "وقتی برای دیگران نامه می‌فرستید، این نام را کنار نشانی شما می‌بینند.", + "saved": "نام شما ذخیره شد.", + "error": "نام شما ذخیره نشد. دوباره امتحان کنید." + }, + "addresses": { + "description": "نشانی‌هایی که می‌توانید از آن‌ها نامه بفرستید، هر کدام با نام و امضای خودش.", + "label": "نشانی‌های شما", + "count": "{count, plural, =0 {هنوز نشانی‌ای نیست} one {# نشانی} other {# نشانی}}", + "manage": "ویرایش نشانی‌ها…", + "modal_title": "نشانی‌های ایمیل شما", + "add": "افزودن نشانی", + "edit": "ویرایش نشانی", + "added": "نشانی افزوده شد.", + "saved": "نشانی ذخیره شد.", + "deleted": "نشانی حذف شد.", + "delete_title": "این نشانی حذف شود؟" + }, + "signatures": { + "title": "امضاها", + "description": "به انتهای پیام‌هایی که می‌نویسید افزوده می‌شود. هر نشانی امضای خودش را دارد.", + "saved": "امضا ذخیره شد.", + "error": "امضا ذخیره نشد. دوباره امتحان کنید.", + "no_addresses": "هنوز نشانی‌ای برای فرستادن ندارید." + }, + "search": { + "addresses": "هویت هویت‌ها نام مستعار فرستنده نشانی فرستنده نشانی از identity alias sender", + "signatures": "امضا امضای ایمیل پانویس هویت فرستنده نشانی از signature", + "your_name": "نام نمایشی نام کامل نام فرستنده display name" + } + }, "desktop_sign_in": { "server_needed": "ابتدا نشانی سرور را وارد کنید.", "server_declined": "شما این سرور را مجاز نکردید، پس چیزی به آن فرستاده نشد.", diff --git a/locales/fr/common.json b/locales/fr/common.json index 920bc103e..fd00f005c 100644 --- a/locales/fr/common.json +++ b/locales/fr/common.json @@ -7146,6 +7146,39 @@ "app_lock_timeout_hours": "{count, plural, one {# heure} other {# heures}}", "desktop_attention_title": "Sur cet ordinateur" }, + "uxs2_ident": { + "your_name": { + "label": "Votre nom", + "description": "Les gens voient ce nom à côté de votre adresse quand vous leur écrivez.", + "saved": "Votre nom est enregistré.", + "error": "Votre nom n'a pas pu être enregistré. Réessayez." + }, + "addresses": { + "description": "Les adresses depuis lesquelles vous pouvez envoyer, chacune avec son nom et sa signature.", + "label": "Vos adresses", + "count": "{count, plural, =0 {Pas encore d'adresse} one {# adresse} other {# adresses}}", + "manage": "Modifier les adresses…", + "modal_title": "Vos adresses e-mail", + "add": "Ajouter une adresse", + "edit": "Modifier l'adresse", + "added": "Adresse ajoutée.", + "saved": "Adresse enregistrée.", + "deleted": "Adresse supprimée.", + "delete_title": "Supprimer cette adresse ?" + }, + "signatures": { + "title": "Signatures", + "description": "Ajoutée à la fin des messages que vous écrivez. Chaque adresse a la sienne.", + "saved": "Signature enregistrée.", + "error": "La signature n'a pas pu être enregistrée. Réessayez.", + "no_addresses": "Vous n'avez encore aucune adresse d'envoi." + }, + "search": { + "addresses": "identité identités alias expéditeur adresse d'expédition adresse de l'expéditeur identity alias sender", + "signatures": "signature signature e-mail pied de page identité expéditeur adresse d'expédition", + "your_name": "nom affiché nom complet nom de l'expéditeur display name" + } + }, "desktop_sign_in": { "server_needed": "Saisissez d'abord l'adresse du serveur.", "server_declined": "Vous n'avez pas autorisé ce serveur, donc rien ne lui a été envoyé.", diff --git a/locales/he/common.json b/locales/he/common.json index 2ddbebc18..56e1df697 100644 --- a/locales/he/common.json +++ b/locales/he/common.json @@ -7147,6 +7147,39 @@ "app_lock_timeout_hours": "{count, plural, one {שעה אחת} two {שעתיים} other {# שעות}}", "desktop_attention_title": "במחשב הזה" }, + "uxs2_ident": { + "your_name": { + "label": "השם שלכם", + "description": "אנשים רואים את השם הזה ליד הכתובת שלכם כשאתם שולחים להם דואר.", + "saved": "השם שלכם נשמר.", + "error": "לא ניתן היה לשמור את השם שלכם. נסו שוב." + }, + "addresses": { + "description": "הכתובות שמהן אפשר לשלוח, לכל אחת שם וחתימה משלה.", + "label": "הכתובות שלכם", + "count": "{count, plural, =0 {עדיין אין כתובות} one {כתובת אחת} two {# כתובות} other {# כתובות}}", + "manage": "עריכת כתובות…", + "modal_title": "כתובות הדואר שלכם", + "add": "הוספת כתובת", + "edit": "עריכת כתובת", + "added": "הכתובת נוספה.", + "saved": "הכתובת נשמרה.", + "deleted": "הכתובת נמחקה.", + "delete_title": "למחוק את הכתובת הזו?" + }, + "signatures": { + "title": "חתימות", + "description": "נוספת בסוף ההודעות שאתם כותבים. לכל כתובת חתימה משלה.", + "saved": "החתימה נשמרה.", + "error": "לא ניתן היה לשמור את החתימה. נסו שוב.", + "no_addresses": "עדיין אין כתובת לשליחה." + }, + "search": { + "addresses": "זהות זהויות כינוי שולח כתובת השולח כתובת מאת identity alias sender", + "signatures": "חתימה חתימת דואר כותרת תחתונה זהות שולח כתובת מאת signature", + "your_name": "שם תצוגה שם מלא שם השולח display name" + } + }, "desktop_sign_in": { "server_needed": "קודם צריך להזין את כתובת השרת.", "server_declined": "לא אישרת את השרת הזה, לכן לא נשלח אליו דבר.", diff --git a/locales/hu/common.json b/locales/hu/common.json index 40440385b..870f35bd4 100644 --- a/locales/hu/common.json +++ b/locales/hu/common.json @@ -7146,6 +7146,39 @@ "app_lock_timeout_hours": "{count, plural, one {# óra} other {# óra}}", "desktop_attention_title": "Ezen a számítógépen" }, + "uxs2_ident": { + "your_name": { + "label": "Az Ön neve", + "description": "Ezt a nevet látják mások a címe mellett, amikor levelet küld nekik.", + "saved": "A neve mentve.", + "error": "A nevét nem sikerült menteni. Próbálja újra." + }, + "addresses": { + "description": "A címek, amelyekről küldhet, mindegyik saját névvel és aláírással.", + "label": "Az Ön címei", + "count": "{count, plural, =0 {Még nincs cím} one {# cím} other {# cím}}", + "manage": "Címek szerkesztése…", + "modal_title": "Az Ön e-mail-címei", + "add": "Cím hozzáadása", + "edit": "Cím szerkesztése", + "added": "Cím hozzáadva.", + "saved": "Cím mentve.", + "deleted": "Cím törölve.", + "delete_title": "Törli ezt a címet?" + }, + "signatures": { + "title": "Aláírások", + "description": "Az Ön által írt üzenetek végére kerül. Minden címnek saját aláírása van.", + "saved": "Aláírás mentve.", + "error": "Az aláírást nem sikerült menteni. Próbálja újra.", + "no_addresses": "Még nincs cím, amelyről küldhetne." + }, + "search": { + "addresses": "identitás identitások álnév alias feladó feladó címe feladócím identity alias sender", + "signatures": "aláírás e-mail aláírás lábléc identitás feladó feladócím signature", + "your_name": "megjelenített név teljes név feladó neve display name" + } + }, "desktop_sign_in": { "server_needed": "Először adja meg a szerver címét.", "server_declined": "Nem engedélyezte ezt a szervert, ezért semmit sem küldtünk neki.", diff --git a/locales/it/common.json b/locales/it/common.json index 4c928e274..7a9ec1c94 100644 --- a/locales/it/common.json +++ b/locales/it/common.json @@ -7146,6 +7146,39 @@ "app_lock_timeout_hours": "{count, plural, one {# ora} other {# ore}}", "desktop_attention_title": "Su questo computer" }, + "uxs2_ident": { + "your_name": { + "label": "Il tuo nome", + "description": "Le persone vedono questo nome accanto al tuo indirizzo quando scrivi loro.", + "saved": "Il tuo nome è stato salvato.", + "error": "Non è stato possibile salvare il tuo nome. Riprova." + }, + "addresses": { + "description": "Gli indirizzi da cui puoi inviare, ognuno con il proprio nome e la propria firma.", + "label": "I tuoi indirizzi", + "count": "{count, plural, =0 {Ancora nessun indirizzo} one {# indirizzo} other {# indirizzi}}", + "manage": "Modifica indirizzi…", + "modal_title": "I tuoi indirizzi email", + "add": "Aggiungi indirizzo", + "edit": "Modifica indirizzo", + "added": "Indirizzo aggiunto.", + "saved": "Indirizzo salvato.", + "deleted": "Indirizzo eliminato.", + "delete_title": "Eliminare questo indirizzo?" + }, + "signatures": { + "title": "Firme", + "description": "Aggiunta alla fine dei messaggi che scrivi. Ogni indirizzo ha la sua.", + "saved": "Firma salvata.", + "error": "Non è stato possibile salvare la firma. Riprova.", + "no_addresses": "Non c'è ancora un indirizzo da cui inviare." + }, + "search": { + "addresses": "identità alias mittente indirizzo del mittente indirizzo da identity sender", + "signatures": "firma firma email piè di pagina identità mittente indirizzo da signature", + "your_name": "nome visualizzato nome completo nome del mittente display name" + } + }, "desktop_sign_in": { "server_needed": "Inserisci prima l'indirizzo del server.", "server_declined": "Non hai autorizzato questo server, quindi non gli è stato inviato nulla.", diff --git a/locales/ja/common.json b/locales/ja/common.json index a4788a73e..242f2d0f0 100644 --- a/locales/ja/common.json +++ b/locales/ja/common.json @@ -7146,6 +7146,39 @@ "app_lock_timeout_hours": "{count, plural, other {#時間}}", "desktop_attention_title": "このコンピューター" }, + "uxs2_ident": { + "your_name": { + "label": "あなたの名前", + "description": "メールを送ると、相手にはこの名前がアドレスの横に表示されます。", + "saved": "名前を保存しました。", + "error": "名前を保存できませんでした。もう一度お試しください。" + }, + "addresses": { + "description": "送信に使えるアドレスです。アドレスごとに名前と署名があります。", + "label": "あなたのアドレス", + "count": "{count, plural, =0 {まだアドレスはありません} other {# 件のアドレス}}", + "manage": "アドレスを編集…", + "modal_title": "あなたのメールアドレス", + "add": "アドレスを追加", + "edit": "アドレスを編集", + "added": "アドレスを追加しました。", + "saved": "アドレスを保存しました。", + "deleted": "アドレスを削除しました。", + "delete_title": "このアドレスを削除しますか?" + }, + "signatures": { + "title": "署名", + "description": "書いたメッセージの最後に付きます。アドレスごとに別の署名を使えます。", + "saved": "署名を保存しました。", + "error": "署名を保存できませんでした。もう一度お試しください。", + "no_addresses": "送信に使えるアドレスがまだありません。" + }, + "search": { + "addresses": "ID アイデンティティ エイリアス 差出人 差出人アドレス 送信元アドレス identity alias sender", + "signatures": "署名 メール署名 フッター アイデンティティ 差出人 送信元アドレス signature", + "your_name": "表示名 氏名 差出人名 display name" + } + }, "desktop_sign_in": { "server_needed": "先にサーバーのアドレスを入力してください。", "server_declined": "このサーバーを許可しなかったため、何も送信していません。", diff --git a/locales/ko/common.json b/locales/ko/common.json index 8692e53c6..f0ccdb32b 100644 --- a/locales/ko/common.json +++ b/locales/ko/common.json @@ -7146,6 +7146,39 @@ "app_lock_timeout_hours": "{count, plural, other {#시간}}", "desktop_attention_title": "이 컴퓨터에서" }, + "uxs2_ident": { + "your_name": { + "label": "내 이름", + "description": "메일을 보내면 받는 사람에게 주소 옆에 이 이름이 보입니다.", + "saved": "이름을 저장했습니다.", + "error": "이름을 저장하지 못했습니다. 다시 시도하세요." + }, + "addresses": { + "description": "보낼 때 쓸 수 있는 주소입니다. 주소마다 이름과 서명이 따로 있습니다.", + "label": "내 주소", + "count": "{count, plural, =0 {아직 주소가 없습니다} other {주소 #개}}", + "manage": "주소 편집…", + "modal_title": "내 이메일 주소", + "add": "주소 추가", + "edit": "주소 편집", + "added": "주소를 추가했습니다.", + "saved": "주소를 저장했습니다.", + "deleted": "주소를 삭제했습니다.", + "delete_title": "이 주소를 삭제할까요?" + }, + "signatures": { + "title": "서명", + "description": "작성한 메시지 끝에 붙습니다. 주소마다 서명이 따로 있습니다.", + "saved": "서명을 저장했습니다.", + "error": "서명을 저장하지 못했습니다. 다시 시도하세요.", + "no_addresses": "아직 보낼 수 있는 주소가 없습니다." + }, + "search": { + "addresses": "ID 아이덴티티 별칭 보낸 사람 보낸 사람 주소 발신 주소 identity alias sender", + "signatures": "서명 이메일 서명 바닥글 아이덴티티 보낸 사람 발신 주소 signature", + "your_name": "표시 이름 전체 이름 보낸 사람 이름 display name" + } + }, "desktop_sign_in": { "server_needed": "먼저 서버 주소를 입력하세요.", "server_declined": "이 서버를 허용하지 않아 아무것도 보내지 않았습니다.", diff --git a/locales/lv/common.json b/locales/lv/common.json index 30ae51e41..c4445ae9f 100644 --- a/locales/lv/common.json +++ b/locales/lv/common.json @@ -7146,6 +7146,39 @@ "app_lock_timeout_hours": "{count, plural, zero {# stundu} one {# stunda} other {# stundas}}", "desktop_attention_title": "Šajā datorā" }, + "uxs2_ident": { + "your_name": { + "label": "Jūsu vārds", + "description": "Šo vārdu cilvēki redz blakus jūsu adresei, kad sūtāt viņiem vēstuli.", + "saved": "Jūsu vārds ir saglabāts.", + "error": "Jūsu vārdu neizdevās saglabāt. Mēģiniet vēlreiz." + }, + "addresses": { + "description": "Adreses, no kurām varat sūtīt, katrai savs vārds un paraksts.", + "label": "Jūsu adreses", + "count": "{count, plural, =0 {Vēl nav adrešu} zero {# adrešu} one {# adrese} other {# adreses}}", + "manage": "Rediģēt adreses…", + "modal_title": "Jūsu e-pasta adreses", + "add": "Pievienot adresi", + "edit": "Rediģēt adresi", + "added": "Adrese pievienota.", + "saved": "Adrese saglabāta.", + "deleted": "Adrese izdzēsta.", + "delete_title": "Vai dzēst šo adresi?" + }, + "signatures": { + "title": "Paraksti", + "description": "Tiek pievienots jūsu rakstīto ziņojumu beigās. Katrai adresei ir savs.", + "saved": "Paraksts saglabāts.", + "error": "Parakstu neizdevās saglabāt. Mēģiniet vēlreiz.", + "no_addresses": "Vēl nav adreses, no kuras sūtīt." + }, + "search": { + "addresses": "identitāte identitātes aizstājvārds sūtītājs sūtītāja adrese no adrese identity alias sender", + "signatures": "paraksts e-pasta paraksts kājene identitāte sūtītājs no adrese signature", + "your_name": "parādāmais vārds pilns vārds sūtītāja vārds display name" + } + }, "desktop_sign_in": { "server_needed": "Vispirms ievadiet servera adresi.", "server_declined": "Jūs neatļāvāt šo serveri, tāpēc tam nekas netika nosūtīts.", diff --git a/locales/mn/common.json b/locales/mn/common.json index 677689faa..516a36c98 100644 --- a/locales/mn/common.json +++ b/locales/mn/common.json @@ -7146,6 +7146,39 @@ "app_lock_timeout_hours": "{count, plural, one {# цаг} other {# цаг}}", "desktop_attention_title": "Энэ компьютер дээр" }, + "uxs2_ident": { + "your_name": { + "label": "Таны нэр", + "description": "Та хүмүүст захидал илгээхэд энэ нэр таны хаягийн хажууд харагдана.", + "saved": "Таны нэр хадгалагдлаа.", + "error": "Таны нэрийг хадгалж чадсангүй. Дахин оролдоно уу." + }, + "addresses": { + "description": "Таны илгээж болох хаягууд, тус бүр өөрийн нэр, гарын үсэгтэй.", + "label": "Таны хаягууд", + "count": "{count, plural, =0 {Одоогоор хаяг алга} one {# хаяг} other {# хаяг}}", + "manage": "Хаягуудыг засах…", + "modal_title": "Таны имэйл хаягууд", + "add": "Хаяг нэмэх", + "edit": "Хаягийг засах", + "added": "Хаяг нэмэгдлээ.", + "saved": "Хаяг хадгалагдлаа.", + "deleted": "Хаяг устгагдлаа.", + "delete_title": "Энэ хаягийг устгах уу?" + }, + "signatures": { + "title": "Гарын үсгүүд", + "description": "Таны бичсэн захидлын төгсгөлд нэмэгдэнэ. Хаяг бүр өөрийн гарын үсэгтэй.", + "saved": "Гарын үсэг хадгалагдлаа.", + "error": "Гарын үсгийг хадгалж чадсангүй. Дахин оролдоно уу.", + "no_addresses": "Илгээх хаяг одоогоор алга." + }, + "search": { + "addresses": "хувийн мэдээлэл таних тэмдэг өөр нэр илгээгч илгээгчийн хаяг хэнээс хаяг identity alias sender", + "signatures": "гарын үсэг имэйлийн гарын үсэг хөл хэсэг илгээгч хэнээс хаяг signature", + "your_name": "харагдах нэр бүтэн нэр илгээгчийн нэр display name" + } + }, "desktop_sign_in": { "server_needed": "Эхлээд серверийн хаягаа оруулна уу.", "server_declined": "Та энэ серверийг зөвшөөрөөгүй тул юу ч илгээгээгүй.", diff --git a/locales/nb/common.json b/locales/nb/common.json index bc849a939..c6c2e8bf7 100644 --- a/locales/nb/common.json +++ b/locales/nb/common.json @@ -7146,6 +7146,39 @@ "app_lock_timeout_hours": "{count, plural, one {# time} other {# timer}}", "desktop_attention_title": "På denne datamaskinen" }, + "uxs2_ident": { + "your_name": { + "label": "Navnet ditt", + "description": "Folk ser dette navnet ved siden av adressen din når du sender dem e-post.", + "saved": "Navnet ditt er lagret.", + "error": "Navnet ditt kunne ikke lagres. Prøv igjen." + }, + "addresses": { + "description": "Adressene du kan sende fra, hver med sitt eget navn og sin egen signatur.", + "label": "Adressene dine", + "count": "{count, plural, =0 {Ingen adresser ennå} one {# adresse} other {# adresser}}", + "manage": "Rediger adresser…", + "modal_title": "E-postadressene dine", + "add": "Legg til adresse", + "edit": "Rediger adresse", + "added": "Adressen er lagt til.", + "saved": "Adressen er lagret.", + "deleted": "Adressen er slettet.", + "delete_title": "Vil du slette denne adressen?" + }, + "signatures": { + "title": "Signaturer", + "description": "Legges til på slutten av meldingene du skriver. Hver adresse har sin egen.", + "saved": "Signaturen er lagret.", + "error": "Signaturen kunne ikke lagres. Prøv igjen.", + "no_addresses": "Det finnes ingen adresse å sende fra ennå." + }, + "search": { + "addresses": "identitet identiteter alias avsender avsenderadresse fra-adresse identity sender", + "signatures": "signatur e-postsignatur bunntekst identitet avsender fra-adresse signature", + "your_name": "visningsnavn fullt navn avsendernavn display name" + } + }, "desktop_sign_in": { "server_needed": "Skriv inn serveradressen først.", "server_declined": "Du tillot ikke denne serveren, så ingenting ble sendt til den.", diff --git a/locales/nl/common.json b/locales/nl/common.json index 40c5b7aba..3cbcd086b 100644 --- a/locales/nl/common.json +++ b/locales/nl/common.json @@ -7146,6 +7146,39 @@ "app_lock_timeout_hours": "{count, plural, one {# uur} other {# uur}}", "desktop_attention_title": "Op deze computer" }, + "uxs2_ident": { + "your_name": { + "label": "Uw naam", + "description": "Mensen zien deze naam naast uw adres als u ze mailt.", + "saved": "Uw naam is opgeslagen.", + "error": "Uw naam kon niet worden opgeslagen. Probeer het opnieuw." + }, + "addresses": { + "description": "De adressen waarvanaf u kunt verzenden, elk met een eigen naam en handtekening.", + "label": "Uw adressen", + "count": "{count, plural, =0 {Nog geen adressen} one {# adres} other {# adressen}}", + "manage": "Adressen bewerken…", + "modal_title": "Uw e-mailadressen", + "add": "Adres toevoegen", + "edit": "Adres bewerken", + "added": "Adres toegevoegd.", + "saved": "Adres opgeslagen.", + "deleted": "Adres verwijderd.", + "delete_title": "Dit adres verwijderen?" + }, + "signatures": { + "title": "Handtekeningen", + "description": "Komt onder de berichten die u schrijft. Elk adres heeft een eigen handtekening.", + "saved": "Handtekening opgeslagen.", + "error": "De handtekening kon niet worden opgeslagen. Probeer het opnieuw.", + "no_addresses": "Er is nog geen adres om vanaf te verzenden." + }, + "search": { + "addresses": "identiteit identiteiten alias afzender afzenderadres van-adres identity sender", + "signatures": "handtekening e-mailhandtekening voettekst identiteit afzender van-adres signature", + "your_name": "weergavenaam volledige naam afzendernaam display name" + } + }, "desktop_sign_in": { "server_needed": "Vul eerst het adres van de server in.", "server_declined": "Je hebt deze server niet toegestaan, dus er is niets naartoe gestuurd.", diff --git a/locales/pl/common.json b/locales/pl/common.json index 977c2f348..5f7fd6fd0 100644 --- a/locales/pl/common.json +++ b/locales/pl/common.json @@ -7146,6 +7146,39 @@ "app_lock_timeout_hours": "{count, plural, one {# godzina} few {# godziny} many {# godzin} other {# godziny}}", "desktop_attention_title": "Na tym komputerze" }, + "uxs2_ident": { + "your_name": { + "label": "Twoje imię i nazwisko", + "description": "Ludzie widzą tę nazwę obok Twojego adresu, gdy wysyłasz im wiadomość.", + "saved": "Zapisano Twoje imię i nazwisko.", + "error": "Nie udało się zapisać Twojego imienia i nazwiska. Spróbuj ponownie." + }, + "addresses": { + "description": "Adresy, z których możesz wysyłać, każdy z własną nazwą i podpisem.", + "label": "Twoje adresy", + "count": "{count, plural, =0 {Jeszcze brak adresów} one {# adres} few {# adresy} many {# adresów} other {# adresu}}", + "manage": "Edytuj adresy…", + "modal_title": "Twoje adresy e-mail", + "add": "Dodaj adres", + "edit": "Edytuj adres", + "added": "Dodano adres.", + "saved": "Zapisano adres.", + "deleted": "Usunięto adres.", + "delete_title": "Usunąć ten adres?" + }, + "signatures": { + "title": "Podpisy", + "description": "Dodawany na końcu pisanych wiadomości. Każdy adres ma własny.", + "saved": "Zapisano podpis.", + "error": "Nie udało się zapisać podpisu. Spróbuj ponownie.", + "no_addresses": "Nie masz jeszcze adresu, z którego można wysyłać." + }, + "search": { + "addresses": "tożsamość tożsamości alias nadawca adres nadawcy adres od identity sender", + "signatures": "podpis podpis e-mail stopka tożsamość nadawca adres od signature", + "your_name": "nazwa wyświetlana imię i nazwisko nazwa nadawcy display name" + } + }, "desktop_sign_in": { "server_needed": "Najpierw wpisz adres serwera.", "server_declined": "Ten serwer nie został dozwolony, więc nic do niego nie wysłano.", diff --git a/locales/pt/common.json b/locales/pt/common.json index 4b3b27d99..4b8048722 100644 --- a/locales/pt/common.json +++ b/locales/pt/common.json @@ -7146,6 +7146,39 @@ "app_lock_timeout_hours": "{count, plural, one {# hora} other {# horas}}", "desktop_attention_title": "Neste computador" }, + "uxs2_ident": { + "your_name": { + "label": "O seu nome", + "description": "As pessoas veem este nome ao lado do seu endereço quando lhes envia e-mail.", + "saved": "O seu nome foi guardado.", + "error": "Não foi possível guardar o seu nome. Tente novamente." + }, + "addresses": { + "description": "Os endereços a partir dos quais pode enviar, cada um com o seu nome e assinatura.", + "label": "Os seus endereços", + "count": "{count, plural, =0 {Ainda sem endereços} one {# endereço} other {# endereços}}", + "manage": "Editar endereços…", + "modal_title": "Os seus endereços de e-mail", + "add": "Adicionar endereço", + "edit": "Editar endereço", + "added": "Endereço adicionado.", + "saved": "Endereço guardado.", + "deleted": "Endereço eliminado.", + "delete_title": "Eliminar este endereço?" + }, + "signatures": { + "title": "Assinaturas", + "description": "É acrescentada no fim das mensagens que escreve. Cada endereço tem a sua.", + "saved": "Assinatura guardada.", + "error": "Não foi possível guardar a assinatura. Tente novamente.", + "no_addresses": "Ainda não há nenhum endereço a partir do qual enviar." + }, + "search": { + "addresses": "identidade identidades alias remetente endereço do remetente endereço de envio identity sender", + "signatures": "assinatura assinatura de e-mail rodapé identidade remetente endereço de envio signature", + "your_name": "nome a mostrar nome completo nome do remetente display name" + } + }, "desktop_sign_in": { "server_needed": "Digite primeiro o endereço do servidor.", "server_declined": "Você não permitiu este servidor, então nada foi enviado a ele.", diff --git a/locales/ro/common.json b/locales/ro/common.json index 60c77c966..ebbc9ad4e 100644 --- a/locales/ro/common.json +++ b/locales/ro/common.json @@ -7146,6 +7146,39 @@ "app_lock_timeout_hours": "{count, plural, one {# oră} few {# ore} other {# de ore}}", "desktop_attention_title": "Pe acest computer" }, + "uxs2_ident": { + "your_name": { + "label": "Numele dvs.", + "description": "Oamenii văd acest nume lângă adresa dvs. când le trimiteți e-mail.", + "saved": "Numele dvs. a fost salvat.", + "error": "Numele dvs. nu a putut fi salvat. Încercați din nou." + }, + "addresses": { + "description": "Adresele de pe care puteți trimite, fiecare cu numele și semnătura ei.", + "label": "Adresele dvs.", + "count": "{count, plural, =0 {Încă nicio adresă} one {# adresă} few {# adrese} other {# de adrese}}", + "manage": "Editați adresele…", + "modal_title": "Adresele dvs. de e-mail", + "add": "Adăugați o adresă", + "edit": "Editați adresa", + "added": "Adresa a fost adăugată.", + "saved": "Adresa a fost salvată.", + "deleted": "Adresa a fost ștearsă.", + "delete_title": "Ștergeți această adresă?" + }, + "signatures": { + "title": "Semnături", + "description": "Se adaugă la sfârșitul mesajelor pe care le scrieți. Fiecare adresă are semnătura ei.", + "saved": "Semnătura a fost salvată.", + "error": "Semnătura nu a putut fi salvată. Încercați din nou.", + "no_addresses": "Nu există încă nicio adresă de pe care să trimiteți." + }, + "search": { + "addresses": "identitate identități alias expeditor adresa expeditorului adresa de la identity sender", + "signatures": "semnătură semnătură e-mail subsol identitate expeditor adresa de la signature", + "your_name": "nume afișat nume complet numele expeditorului display name" + } + }, "desktop_sign_in": { "server_needed": "Introdu mai întâi adresa serverului.", "server_declined": "Nu ai permis acest server, așa că nu i s-a trimis nimic.", diff --git a/locales/ru/common.json b/locales/ru/common.json index 3a0c9fcec..954c8992e 100644 --- a/locales/ru/common.json +++ b/locales/ru/common.json @@ -7146,6 +7146,39 @@ "app_lock_timeout_hours": "{count, plural, one {# час} few {# часа} many {# часов} other {# часа}}", "desktop_attention_title": "На этом компьютере" }, + "uxs2_ident": { + "your_name": { + "label": "Ваше имя", + "description": "Это имя люди видят рядом с вашим адресом, когда вы им пишете.", + "saved": "Ваше имя сохранено.", + "error": "Не удалось сохранить ваше имя. Попробуйте ещё раз." + }, + "addresses": { + "description": "Адреса, с которых вы можете отправлять почту, у каждого своё имя и своя подпись.", + "label": "Ваши адреса", + "count": "{count, plural, =0 {Адресов пока нет} one {# адрес} few {# адреса} many {# адресов} other {# адреса}}", + "manage": "Изменить адреса…", + "modal_title": "Ваши адреса электронной почты", + "add": "Добавить адрес", + "edit": "Изменить адрес", + "added": "Адрес добавлен.", + "saved": "Адрес сохранён.", + "deleted": "Адрес удалён.", + "delete_title": "Удалить этот адрес?" + }, + "signatures": { + "title": "Подписи", + "description": "Добавляется в конце ваших писем. У каждого адреса своя.", + "saved": "Подпись сохранена.", + "error": "Не удалось сохранить подпись. Попробуйте ещё раз.", + "no_addresses": "Пока нет адреса, с которого можно отправлять." + }, + "search": { + "addresses": "идентичность личность псевдоним алиас отправитель адрес отправителя адрес от identity alias sender", + "signatures": "подпись подпись письма нижний колонтитул отправитель адрес от signature", + "your_name": "отображаемое имя полное имя имя отправителя display name" + } + }, "desktop_sign_in": { "server_needed": "Сначала введите адрес сервера.", "server_declined": "Вы не разрешили этот сервер, поэтому ему ничего не отправлено.", diff --git a/locales/sk/common.json b/locales/sk/common.json index 502164d6d..a90922544 100644 --- a/locales/sk/common.json +++ b/locales/sk/common.json @@ -7146,6 +7146,39 @@ "app_lock_timeout_hours": "{count, plural, one {# hodina} few {# hodiny} many {# hodiny} other {# hodín}}", "desktop_attention_title": "Na tomto počítači" }, + "uxs2_ident": { + "your_name": { + "label": "Vaše meno", + "description": "Toto meno ľudia vidia vedľa vašej adresy, keď im pošlete e-mail.", + "saved": "Vaše meno je uložené.", + "error": "Vaše meno sa nepodarilo uložiť. Skúste to znova." + }, + "addresses": { + "description": "Adresy, z ktorých môžete odosielať, každá s vlastným menom a podpisom.", + "label": "Vaše adresy", + "count": "{count, plural, =0 {Zatiaľ žiadne adresy} one {# adresa} few {# adresy} many {# adresy} other {# adries}}", + "manage": "Upraviť adresy…", + "modal_title": "Vaše e-mailové adresy", + "add": "Pridať adresu", + "edit": "Upraviť adresu", + "added": "Adresa pridaná.", + "saved": "Adresa uložená.", + "deleted": "Adresa odstránená.", + "delete_title": "Odstrániť túto adresu?" + }, + "signatures": { + "title": "Podpisy", + "description": "Pridá sa na koniec správ, ktoré píšete. Každá adresa má vlastný.", + "saved": "Podpis uložený.", + "error": "Podpis sa nepodarilo uložiť. Skúste to znova.", + "no_addresses": "Zatiaľ nemáte adresu, z ktorej sa dá odosielať." + }, + "search": { + "addresses": "identita identity alias odosielateľ adresa odosielateľa adresa od identity sender", + "signatures": "podpis e-mailový podpis päta identita odosielateľ adresa od signature", + "your_name": "zobrazované meno celé meno meno odosielateľa display name" + } + }, "desktop_sign_in": { "server_needed": "Najprv zadajte adresu servera.", "server_declined": "Tento server ste nepovolili, preto sa naň nič neodoslalo.", diff --git a/locales/th/common.json b/locales/th/common.json index 852be7f21..87a04b350 100644 --- a/locales/th/common.json +++ b/locales/th/common.json @@ -7154,6 +7154,39 @@ "app_lock_timeout_hours": "{count, plural, other {# ชั่วโมง}}", "desktop_attention_title": "บนคอมพิวเตอร์เครื่องนี้" }, + "uxs2_ident": { + "your_name": { + "label": "ชื่อของคุณ", + "description": "ผู้คนจะเห็นชื่อนี้ข้างที่อยู่ของคุณเมื่อคุณส่งอีเมลถึงพวกเขา", + "saved": "บันทึกชื่อของคุณแล้ว", + "error": "บันทึกชื่อของคุณไม่ได้ ลองอีกครั้ง" + }, + "addresses": { + "description": "ที่อยู่ที่คุณใช้ส่งได้ แต่ละที่อยู่มีชื่อและลายเซ็นของตัวเอง", + "label": "ที่อยู่ของคุณ", + "count": "{count, plural, =0 {ยังไม่มีที่อยู่} other {# ที่อยู่}}", + "manage": "แก้ไขที่อยู่…", + "modal_title": "ที่อยู่อีเมลของคุณ", + "add": "เพิ่มที่อยู่", + "edit": "แก้ไขที่อยู่", + "added": "เพิ่มที่อยู่แล้ว", + "saved": "บันทึกที่อยู่แล้ว", + "deleted": "ลบที่อยู่แล้ว", + "delete_title": "ลบที่อยู่นี้หรือไม่" + }, + "signatures": { + "title": "ลายเซ็น", + "description": "ใส่ไว้ท้ายข้อความที่คุณเขียน แต่ละที่อยู่มีลายเซ็นของตัวเอง", + "saved": "บันทึกลายเซ็นแล้ว", + "error": "บันทึกลายเซ็นไม่ได้ ลองอีกครั้ง", + "no_addresses": "ยังไม่มีที่อยู่สำหรับส่ง" + }, + "search": { + "addresses": "ข้อมูลประจำตัว นามแฝง ผู้ส่ง ที่อยู่ผู้ส่ง ที่อยู่จาก identity alias sender", + "signatures": "ลายเซ็น ลายเซ็นอีเมล ท้ายกระดาษ ผู้ส่ง ที่อยู่จาก signature", + "your_name": "ชื่อที่แสดง ชื่อเต็ม ชื่อผู้ส่ง display name" + } + }, "desktop_sign_in": { "server_needed": "ป้อนที่อยู่เซิร์ฟเวอร์ก่อน", "server_declined": "คุณไม่ได้อนุญาตเซิร์ฟเวอร์นี้ จึงไม่มีการส่งข้อมูลใดไป", diff --git a/locales/tr/common.json b/locales/tr/common.json index 8e246b38f..5d5c5a157 100644 --- a/locales/tr/common.json +++ b/locales/tr/common.json @@ -7146,6 +7146,39 @@ "app_lock_timeout_hours": "{count, plural, one {# saat} other {# saat}}", "desktop_attention_title": "Bu bilgisayarda" }, + "uxs2_ident": { + "your_name": { + "label": "Adınız", + "description": "Birine e-posta gönderdiğinizde bu adı adresinizin yanında görür.", + "saved": "Adınız kaydedildi.", + "error": "Adınız kaydedilemedi. Yeniden deneyin." + }, + "addresses": { + "description": "Gönderebileceğiniz adresler; her birinin kendi adı ve imzası var.", + "label": "Adresleriniz", + "count": "{count, plural, =0 {Henüz adres yok} one {# adres} other {# adres}}", + "manage": "Adresleri düzenle…", + "modal_title": "E-posta adresleriniz", + "add": "Adres ekle", + "edit": "Adresi düzenle", + "added": "Adres eklendi.", + "saved": "Adres kaydedildi.", + "deleted": "Adres silindi.", + "delete_title": "Bu adres silinsin mi?" + }, + "signatures": { + "title": "İmzalar", + "description": "Yazdığınız iletilerin sonuna eklenir. Her adresin kendi imzası vardır.", + "saved": "İmza kaydedildi.", + "error": "İmza kaydedilemedi. Yeniden deneyin.", + "no_addresses": "Henüz gönderebileceğiniz bir adres yok." + }, + "search": { + "addresses": "kimlik kimlikler takma ad gönderen gönderen adresi kimden adresi identity alias sender", + "signatures": "imza e-posta imzası alt bilgi kimlik gönderen kimden adresi signature", + "your_name": "görünen ad tam ad gönderen adı display name" + } + }, "desktop_sign_in": { "server_needed": "Önce sunucu adresini girin.", "server_declined": "Bu sunucuya izin vermediniz, bu yüzden ona hiçbir şey gönderilmedi.", diff --git a/locales/uk/common.json b/locales/uk/common.json index 52bf3725a..2028ab3b8 100644 --- a/locales/uk/common.json +++ b/locales/uk/common.json @@ -7146,6 +7146,39 @@ "app_lock_timeout_hours": "{count, plural, one {# година} few {# години} many {# годин} other {# години}}", "desktop_attention_title": "На цьому комп'ютері" }, + "uxs2_ident": { + "your_name": { + "label": "Ваше ім'я", + "description": "Це ім'я люди бачать поруч із вашою адресою, коли ви їм пишете.", + "saved": "Ваше ім'я збережено.", + "error": "Не вдалося зберегти ваше ім'я. Спробуйте ще раз." + }, + "addresses": { + "description": "Адреси, з яких ви можете надсилати пошту, кожна зі своїм ім'ям і підписом.", + "label": "Ваші адреси", + "count": "{count, plural, =0 {Адрес поки немає} one {# адреса} few {# адреси} many {# адрес} other {# адреси}}", + "manage": "Змінити адреси…", + "modal_title": "Ваші адреси електронної пошти", + "add": "Додати адресу", + "edit": "Змінити адресу", + "added": "Адресу додано.", + "saved": "Адресу збережено.", + "deleted": "Адресу видалено.", + "delete_title": "Видалити цю адресу?" + }, + "signatures": { + "title": "Підписи", + "description": "Додається в кінці ваших листів. Кожна адреса має свій.", + "saved": "Підпис збережено.", + "error": "Не вдалося зберегти підпис. Спробуйте ще раз.", + "no_addresses": "Поки немає адреси, з якої можна надсилати." + }, + "search": { + "addresses": "ідентичність особа псевдонім аліас відправник адреса відправника адреса від identity alias sender", + "signatures": "підпис підпис листа нижній колонтитул відправник адреса від signature", + "your_name": "відображуване ім'я повне ім'я ім'я відправника display name" + } + }, "desktop_sign_in": { "server_needed": "Спочатку введіть адресу сервера.", "server_declined": "Ви не дозволили цей сервер, тому йому нічого не надіслано.", diff --git a/locales/vi/common.json b/locales/vi/common.json index a02cf42d1..f9d2ca0f0 100644 --- a/locales/vi/common.json +++ b/locales/vi/common.json @@ -7158,6 +7158,39 @@ "app_lock_timeout_hours": "{count, plural, other {# giờ}}", "desktop_attention_title": "Trên máy tính này" }, + "uxs2_ident": { + "your_name": { + "label": "Tên của bạn", + "description": "Mọi người thấy tên này cạnh địa chỉ của bạn khi bạn gửi thư cho họ.", + "saved": "Đã lưu tên của bạn.", + "error": "Không lưu được tên của bạn. Hãy thử lại." + }, + "addresses": { + "description": "Các địa chỉ bạn có thể gửi thư, mỗi địa chỉ có tên và chữ ký riêng.", + "label": "Địa chỉ của bạn", + "count": "{count, plural, =0 {Chưa có địa chỉ nào} other {# địa chỉ}}", + "manage": "Sửa địa chỉ…", + "modal_title": "Địa chỉ email của bạn", + "add": "Thêm địa chỉ", + "edit": "Sửa địa chỉ", + "added": "Đã thêm địa chỉ.", + "saved": "Đã lưu địa chỉ.", + "deleted": "Đã xóa địa chỉ.", + "delete_title": "Xóa địa chỉ này?" + }, + "signatures": { + "title": "Chữ ký", + "description": "Được thêm vào cuối thư bạn viết. Mỗi địa chỉ có chữ ký riêng.", + "saved": "Đã lưu chữ ký.", + "error": "Không lưu được chữ ký. Hãy thử lại.", + "no_addresses": "Chưa có địa chỉ nào để gửi." + }, + "search": { + "addresses": "danh tính bí danh người gửi địa chỉ người gửi địa chỉ gửi từ identity alias sender", + "signatures": "chữ ký chữ ký email chân trang danh tính người gửi địa chỉ gửi từ signature", + "your_name": "tên hiển thị họ tên tên người gửi display name" + } + }, "desktop_sign_in": { "server_needed": "Trước tiên hãy nhập địa chỉ máy chủ.", "server_declined": "Bạn không cho phép máy chủ này nên không có gì được gửi đến đó.", diff --git a/locales/zh-TW/common.json b/locales/zh-TW/common.json index afe32e3ed..960b63520 100644 --- a/locales/zh-TW/common.json +++ b/locales/zh-TW/common.json @@ -7146,6 +7146,39 @@ "app_lock_timeout_hours": "{count, plural, other {# 小時}}", "desktop_attention_title": "在這台電腦上" }, + "uxs2_ident": { + "your_name": { + "label": "你的名稱", + "description": "你寄信給別人時,對方會在你的地址旁看到這個名稱。", + "saved": "你的名稱已儲存。", + "error": "無法儲存你的名稱。請再試一次。" + }, + "addresses": { + "description": "你可以用來寄信的地址,每個地址都有自己的名稱和簽名。", + "label": "你的地址", + "count": "{count, plural, =0 {還沒有地址} other {# 個地址}}", + "manage": "編輯地址…", + "modal_title": "你的電子郵件地址", + "add": "新增地址", + "edit": "編輯地址", + "added": "已新增地址。", + "saved": "已儲存地址。", + "deleted": "已刪除地址。", + "delete_title": "要刪除這個地址嗎?" + }, + "signatures": { + "title": "簽名", + "description": "加在你所寫郵件的結尾。每個地址都有自己的簽名。", + "saved": "已儲存簽名。", + "error": "無法儲存簽名。請再試一次。", + "no_addresses": "還沒有可以寄信的地址。" + }, + "search": { + "addresses": "身分 別名 寄件者 寄件者地址 寄件地址 identity alias sender", + "signatures": "簽名 郵件簽名 頁尾 身分 寄件者 寄件地址 signature", + "your_name": "顯示名稱 全名 寄件者名稱 display name" + } + }, "desktop_sign_in": { "server_needed": "請先輸入伺服器位址。", "server_declined": "你沒有允許此伺服器,因此沒有傳送任何內容給它。", diff --git a/locales/zh/common.json b/locales/zh/common.json index a5fd459b2..ac15e6f16 100644 --- a/locales/zh/common.json +++ b/locales/zh/common.json @@ -7146,6 +7146,39 @@ "app_lock_timeout_hours": "{count, plural, other {# 小时}}", "desktop_attention_title": "在这台电脑上" }, + "uxs2_ident": { + "your_name": { + "label": "你的名字", + "description": "你给别人发邮件时,对方会在你的地址旁看到这个名字。", + "saved": "你的名字已保存。", + "error": "无法保存你的名字。请重试。" + }, + "addresses": { + "description": "你可以用来发信的地址,每个地址都有自己的名字和签名。", + "label": "你的地址", + "count": "{count, plural, =0 {还没有地址} other {# 个地址}}", + "manage": "编辑地址…", + "modal_title": "你的电子邮件地址", + "add": "添加地址", + "edit": "编辑地址", + "added": "地址已添加。", + "saved": "地址已保存。", + "deleted": "地址已删除。", + "delete_title": "删除这个地址?" + }, + "signatures": { + "title": "签名", + "description": "添加在你所写邮件的末尾。每个地址都有自己的签名。", + "saved": "签名已保存。", + "error": "无法保存签名。请重试。", + "no_addresses": "还没有可以发信的地址。" + }, + "search": { + "addresses": "身份 别名 发件人 发件人地址 发件地址 identity alias sender", + "signatures": "签名 邮件签名 页脚 身份 发件人 发件地址 signature", + "your_name": "显示名称 全名 发件人姓名 display name" + } + }, "desktop_sign_in": { "server_needed": "请先输入服务器地址。", "server_declined": "你没有允许此服务器,因此没有向它发送任何内容。", diff --git a/stores/__tests__/auth-store-display-name.test.ts b/stores/__tests__/auth-store-display-name.test.ts index 40aef4126..36eab0668 100644 --- a/stores/__tests__/auth-store-display-name.test.ts +++ b/stores/__tests__/auth-store-display-name.test.ts @@ -32,35 +32,39 @@ function seedAccount(displayName = 'Old Name', label = displayName): string { } // Issue #900: the registry entry is written once by addAccount and never -// refreshed, so a Stalwart admin renaming the user ("Full name") was never -// reflected in the account switcher / settings header. -describe('syncAccountDisplayName (#900)', () => { +// refreshed, so a renamed user kept the old name in the account switcher. +// settings-p2-2: the name that counts is the primary identity's, the one +// recipients see and Settings > Account edits. Stalwart's principal +// description is only the fallback for an account without one. +describe('syncAccountDisplayName (#900, settings-p2-2)', () => { beforeEach(() => { mockedPrincipal.mockReset(); useAccountStore.setState({ accounts: [], activeAccountId: null, defaultAccountId: null }); }); - it('replaces the cached name with the live Stalwart principal name', async () => { + it('takes the identity name over the Stalwart principal description', async () => { + const id = seedAccount('Old Name'); + mockedPrincipal.mockResolvedValue('Admin note (throwaway)'); + + await syncAccountDisplayName(id, client, 'Sam Settings'); + + const account = useAccountStore.getState().getAccountById(id)!; + expect(account.displayName).toBe('Sam Settings'); + expect(account.label).toBe('Sam Settings'); + expect(mockedPrincipal).not.toHaveBeenCalled(); + }); + + it('falls back to the principal name when the identity has no name', async () => { const id = seedAccount('Old Name'); mockedPrincipal.mockResolvedValueOnce('New Name'); - await syncAccountDisplayName(id, client, 'Old Name'); + await syncAccountDisplayName(id, client, ' '); const account = useAccountStore.getState().getAccountById(id)!; expect(account.displayName).toBe('New Name'); - expect(account.label).toBe('New Name'); expect(mockedPrincipal).toHaveBeenCalledWith(client, account.cookieSlot); }); - it('falls back to the identity name when the principal is unavailable', async () => { - const id = seedAccount('Old Name'); - mockedPrincipal.mockResolvedValueOnce(null); - - await syncAccountDisplayName(id, client, 'Identity Name'); - - expect(useAccountStore.getState().getAccountById(id)!.displayName).toBe('Identity Name'); - }); - it('keeps the cached name when neither source yields one', async () => { const id = seedAccount('Old Name'); mockedPrincipal.mockResolvedValueOnce(null); @@ -70,12 +74,30 @@ describe('syncAccountDisplayName (#900)', () => { expect(useAccountStore.getState().getAccountById(id)!.displayName).toBe('Old Name'); }); - it('leaves a diverged label alone', async () => { - const id = seedAccount('Old Name', 'Work'); + it('keeps the name when the caller does not know the identities', async () => { + const id = seedAccount('Sam Settings'); + mockedPrincipal.mockResolvedValue('Admin note (throwaway)'); + + await syncAccountDisplayName(id, client); + + expect(useAccountStore.getState().getAccountById(id)!.displayName).toBe('Sam Settings'); + expect(mockedPrincipal).not.toHaveBeenCalled(); + }); + + it('names an unnamed account from the principal when the identities are unknown', async () => { + const id = seedAccount(''); mockedPrincipal.mockResolvedValueOnce('New Name'); await syncAccountDisplayName(id, client); + expect(useAccountStore.getState().getAccountById(id)!.displayName).toBe('New Name'); + }); + + it('leaves a diverged label alone', async () => { + const id = seedAccount('Old Name', 'Work'); + + await syncAccountDisplayName(id, client, 'New Name'); + const account = useAccountStore.getState().getAccountById(id)!; expect(account.displayName).toBe('New Name'); expect(account.label).toBe('Work'); @@ -86,9 +108,8 @@ describe('syncAccountDisplayName (#900)', () => { expect(mockedPrincipal).not.toHaveBeenCalled(); const id = seedAccount('Same'); - mockedPrincipal.mockResolvedValueOnce('Same'); const before = useAccountStore.getState().getAccountById(id); - await syncAccountDisplayName(id, client); + await syncAccountDisplayName(id, client, 'Same'); expect(useAccountStore.getState().getAccountById(id)).toBe(before); }); }); diff --git a/stores/auth-store.ts b/stores/auth-store.ts index 4fb3b9f6c..88483e511 100644 --- a/stores/auth-store.ts +++ b/stores/auth-store.ts @@ -295,9 +295,9 @@ function getClientRateLimitState(client: IJMAPClient | null): Pick Account edits. The Stalwart principal name is only the + * fallback for an account without one. * * Best-effort and never throws: a failed lookup keeps the cached name. */ @@ -309,9 +309,16 @@ export async function syncAccountDisplayName( const account = useAccountStore.getState().getAccountById(accountId); if (!account) return; - const name = (await fetchPrincipalDisplayName(client, account.cookieSlot)) - || fallbackName?.trim() - || ''; + // The name people see on sent mail is the sending identity's, and that is + // the one name Settings lets them change (settings-p2-2). Stalwart's + // principal description ("Full name") is often an admin's note, so it only + // names an account that has no identity name. A caller that doesn't know + // the identities keeps the name the account already has. + let name = fallbackName?.trim() || ''; + if (!name) { + if (fallbackName === undefined && account.displayName) return; + name = (await fetchPrincipalDisplayName(client, account.cookieSlot)) || ''; + } if (!name) return; // Re-read: the entry may have changed (or been removed) during the request. diff --git a/stores/identity-store.ts b/stores/identity-store.ts index 3f22669b6..6dbb80f3e 100644 --- a/stores/identity-store.ts +++ b/stores/identity-store.ts @@ -182,7 +182,7 @@ export async function createIdentityWithHooks(client: IJMAPClient, data: Identit export async function updateIdentityWithHooks( client: IJMAPClient, identity: Identity, - updates: Omit, + updates: Partial>, ): Promise { const next = identityToView({ ...identity, ...updates } as Identity); if (!(await identityHooks.onBeforeIdentityChange.intercept({ identityId: identity.id, change: 'update', identity: next }))) { @@ -193,6 +193,22 @@ export async function updateIdentityWithHooks( return true; } +/** + * Change some fields of one identity (the Account page's name, a signature + * on Composing) and keep the local list in step, without reloading every + * identity. Callers then run the auth store's syncIdentities so the From + * line and the account switcher follow. + */ +export async function saveIdentityChanges( + client: IJMAPClient, + identity: Identity, + updates: Partial>, +): Promise { + if (!(await updateIdentityWithHooks(client, identity, updates))) return false; + useIdentityStore.getState().updateIdentityLocal(identity.id, updates as Partial); + return true; +} + export async function deleteIdentityWithHooks(client: IJMAPClient, identity: Identity): Promise { if (!(await identityHooks.onBeforeIdentityChange.intercept({ identityId: identity.id, change: 'delete', identity: identityToView(identity),