+ {/* "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 && (
{
if (!client) return;
@@ -154,13 +155,13 @@ export function IdentityManagerModal({ isOpen, onClose }: IdentityManagerModalPr
await refreshIdentities();
setEditingId(null);
- toast.success(tNotif('identity_updated'));
+ toast.success(tAddr('saved'));
} catch (error) {
const message = error instanceof Error ? error.message : t('validation_errors.unknown_error');
toast.error(tNotif('identity_update_failed', { error: message }));
throw error;
}
- }, [client, refreshIdentities, t, tNotif]);
+ }, [client, refreshIdentities, t, tAddr, tNotif]);
const handleDelete = useCallback(async (identity: Identity) => {
if (!client) return;
@@ -170,7 +171,7 @@ export function IdentityManagerModal({ isOpen, onClose }: IdentityManagerModalPr
}
const confirmed = await confirmDialog({
- title: t('delete_confirm_title'),
+ title: tAddr('delete_title'),
message: t('delete_confirm'),
confirmText: t('delete_button'),
variant: "destructive",
@@ -182,14 +183,14 @@ export function IdentityManagerModal({ isOpen, onClose }: IdentityManagerModalPr
try {
if (!(await deleteIdentityWithHooks(client, identity))) return;
await refreshIdentities();
- toast.success(tNotif('identity_deleted'));
+ toast.success(tAddr('deleted'));
} catch (error) {
const message = error instanceof Error ? error.message : t('validation_errors.unknown_error');
toast.error(tNotif('identity_delete_failed', { error: message }));
} finally {
setDeletingId(null);
}
- }, [client, refreshIdentities, t, tNotif, confirmDialog]);
+ }, [client, refreshIdentities, t, tAddr, tNotif, confirmDialog]);
const handleSetPrimary = useCallback((identity: Identity) => {
setPreferredPrimary(identity.id);
@@ -223,7 +224,7 @@ export function IdentityManagerModal({ isOpen, onClose }: IdentityManagerModalPr
title={
- {t('modal_title')}
+ {tAddr('modal_title')}
}
>
@@ -231,7 +232,7 @@ export function IdentityManagerModal({ isOpen, onClose }: IdentityManagerModalPr
{/* Create New Form */}
{isCreating && (
-
{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
)}
-