fix: sync default identity (preferredPrimaryId) to server settings #507
This commit is contained in:
@@ -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);
|
||||
|
||||
+39
-1
@@ -49,6 +49,7 @@ interface AuthState {
|
||||
clearError: () => void;
|
||||
syncIdentities: () => void;
|
||||
refreshIdentities: () => Promise<void>;
|
||||
applyPreferredIdentityOrdering: () => void;
|
||||
getClientForAccount: (accountId: string) => JMAPClient | undefined;
|
||||
getAllConnectedClients: () => Map<string, JMAPClient>;
|
||||
}
|
||||
@@ -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<AuthState>()(
|
||||
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;
|
||||
|
||||
@@ -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<string, string | null>;
|
||||
|
||||
// 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<string, string | null>,
|
||||
|
||||
// Privacy & Security
|
||||
sessionTimeout: 0, // Never
|
||||
trustedSenders: [] as string[],
|
||||
@@ -577,6 +587,7 @@ export const useSettingsStore = create<SettingsState>()(
|
||||
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<SettingsState>()(
|
||||
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<SettingsState>()(
|
||||
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<SettingsState>()(
|
||||
if (!isPlainRecord(state.allMailFolderIds)) {
|
||||
state.allMailFolderIds = {};
|
||||
}
|
||||
if (!isPlainRecord(state.preferredIdentityIds)) {
|
||||
state.preferredIdentityIds = {};
|
||||
}
|
||||
applyFontSize(state.fontSize);
|
||||
applyDensity(state.density);
|
||||
applyAnimations(state.animationsEnabled);
|
||||
|
||||
Reference in New Issue
Block a user