diff --git a/components/identity/identity-manager-modal.tsx b/components/identity/identity-manager-modal.tsx index 59ef035d..3c4a4280 100644 --- a/components/identity/identity-manager-modal.tsx +++ b/components/identity/identity-manager-modal.tsx @@ -9,6 +9,7 @@ import { ConfirmDialog } from '@/components/ui/confirm-dialog'; import { IdentityForm } from './identity-form'; import { useIdentityStore } from '@/stores/identity-store'; import { useAuthStore } from '@/stores/auth-store'; +import { useSettingsStore } from '@/stores/settings-store'; function useSyncIdentities() { const syncIdentities = useAuthStore((state) => state.syncIdentities); @@ -206,6 +207,17 @@ export function IdentityManagerModal({ isOpen, onClose }: IdentityManagerModalPr const handleSetPrimary = useCallback((identity: Identity) => { setPreferredPrimary(identity.id); + // Persist to the synced settings (keyed by username, matching how + // loadIdentities reads it back) so the choice survives a new browser / + // cleared site data and reaches other devices (#507). + const username = useAuthStore.getState().username || ''; + if (username) { + const current = useSettingsStore.getState().preferredIdentityIds; + useSettingsStore.getState().updateSetting('preferredIdentityIds', { + ...current, + [username]: identity.id, + }); + } // Re-sort: move the preferred identity to the front const reordered = [identity, ...identities.filter((id) => id.id !== identity.id)]; useIdentityStore.getState().setIdentities(reordered); diff --git a/stores/auth-store.ts b/stores/auth-store.ts index 956250d8..d61e4a1c 100644 --- a/stores/auth-store.ts +++ b/stores/auth-store.ts @@ -49,6 +49,7 @@ interface AuthState { clearError: () => void; syncIdentities: () => void; refreshIdentities: () => Promise; + applyPreferredIdentityOrdering: () => void; getClientForAccount: (accountId: string) => JMAPClient | undefined; getAllConnectedClients: () => Map; } @@ -171,7 +172,22 @@ function sortIdentities(rawIdentities: Identity[], username: string): Identity[] } function loadIdentities(rawIdentities: Identity[], username: string): { identities: Identity[]; primaryIdentity: Identity | null } { - const preferredPrimaryId = useIdentityStore.getState().preferredPrimaryId; + const settings = useSettingsStore.getState(); + const preferredMap = settings.preferredIdentityIds || {}; + let preferredPrimaryId = preferredMap[username] ?? null; + + // One-time migration: builds before #507 stored the preferred identity only + // in the browser-local identity-storage (never synced). If the synced + // settings have no entry for this account yet, adopt that legacy local value + // and write it into the synced settings so it persists across devices. + if (preferredPrimaryId == null) { + const legacy = useIdentityStore.getState().preferredPrimaryId; + if (legacy) { + preferredPrimaryId = legacy; + settings.updateSetting('preferredIdentityIds', { ...preferredMap, [username]: legacy }); + } + } + const identities = sortIdentities(rawIdentities, username); // If user has a preferred primary, move it to front @@ -185,6 +201,9 @@ function loadIdentities(rawIdentities: Identity[], username: string): { identiti const primaryIdentity = identities[0] ?? null; useIdentityStore.getState().setIdentities(identities); + // Mirror the resolved choice into the identity store so the identity-manager + // UI (the ⭐ marker) reflects the active account's preferred identity. + useIdentityStore.setState({ preferredPrimaryId }); return { identities, primaryIdentity }; } @@ -1645,6 +1664,25 @@ export const useAuthStore = create()( set({ identities, primaryIdentity }); }, + // Re-sort the already-loaded identities to honor the active account's + // synced preferred-primary identity, without a network round-trip. Used + // after settings load from the server so a fresh browser reflects the + // synced default (#507). + applyPreferredIdentityOrdering: () => { + const { username, identities } = get(); + if (!username || identities.length === 0) return; + const preferredId = useSettingsStore.getState().preferredIdentityIds?.[username] ?? null; + useIdentityStore.setState({ preferredPrimaryId: preferredId }); + if (!preferredId) return; + const idx = identities.findIndex((id) => id.id === preferredId); + if (idx <= 0) return; // already first, or not present + const reordered = [...identities]; + const [preferred] = reordered.splice(idx, 1); + reordered.unshift(preferred); + useIdentityStore.getState().setIdentities(reordered); + set({ identities: reordered, primaryIdentity: reordered[0] ?? null }); + }, + refreshIdentities: async () => { const { client, username } = get(); if (!client || !username) return; diff --git a/stores/settings-store.ts b/stores/settings-store.ts index 3a13aa34..260a2d26 100644 --- a/stores/settings-store.ts +++ b/stores/settings-store.ts @@ -168,6 +168,13 @@ interface SettingsState { requestReadReceiptDefault: boolean; // Pre-check "request read receipt" in the composer readReceiptResponse: ReadReceiptResponse; // How to respond to incoming read-receipt requests + // Identities + // Per-account default ("preferred primary") sender identity, keyed by + // username (the same key settings sync uses). A JMAP identity id is only + // meaningful within its own account, so this must be account-scoped. Synced + // so the choice survives a new browser / cleared site data (#507). + preferredIdentityIds: Record; + // Privacy & Security sessionTimeout: number; // minutes (0 = never) trustedSenders: string[]; // Email addresses that can load external content @@ -371,6 +378,9 @@ const DEFAULT_SETTINGS = { requestReadReceiptDefault: false, readReceiptResponse: 'ask' as ReadReceiptResponse, + // Identities + preferredIdentityIds: {} as Record, + // Privacy & Security sessionTimeout: 0, // Never trustedSenders: [] as string[], @@ -577,6 +587,7 @@ export const useSettingsStore = create()( signatureSeparatorEnabled: state.signatureSeparatorEnabled, requestReadReceiptDefault: state.requestReadReceiptDefault, readReceiptResponse: state.readReceiptResponse, + preferredIdentityIds: state.preferredIdentityIds, sessionTimeout: state.sessionTimeout, emailNotificationsEnabled: state.emailNotificationsEnabled, emailNotificationSound: state.emailNotificationSound, @@ -659,6 +670,11 @@ export const useSettingsStore = create()( if (key === 'allMailFolderIds' && !isPlainRecord(settings[key])) { return; } + // Defensive: a non-record (e.g. a legacy scalar) would break the + // per-account map lookups - ignore it. + if (key === 'preferredIdentityIds' && !isPlainRecord(settings[key])) { + return; + } if (DEVICE_LOCAL_SETTING_KEYS.has(key)) { return; } @@ -837,6 +853,14 @@ export const useSettingsStore = create()( get().importSettings(JSON.stringify(settings)); isLoadingFromServer = false; syncLog('Settings loaded from server successfully'); + // Re-apply the (possibly server-updated) per-account preferred + // sender identity to the already-loaded identities, so a fresh + // browser reflects the synced default without waiting for the next + // identity refresh. Dynamic import avoids a static import cycle + // (auth-store imports this store). (#507) + import('./auth-store') + .then(({ useAuthStore }) => useAuthStore.getState().applyPreferredIdentityOrdering()) + .catch(() => {}); return true; } return false; @@ -888,6 +912,9 @@ export const useSettingsStore = create()( if (!isPlainRecord(state.allMailFolderIds)) { state.allMailFolderIds = {}; } + if (!isPlainRecord(state.preferredIdentityIds)) { + state.preferredIdentityIds = {}; + } applyFontSize(state.fontSize); applyDensity(state.density); applyAnimations(state.animationsEnabled);