fix(identity): sync default sender identity per account (#507)

The default sender identity (`preferredPrimaryId`) lived only in the
browser-local `identity-storage` Zustand store and was never written to
the server-side synced settings. As a result the choice was lost when
clearing site data or switching browsers, and never appeared in the
exported settings JSON.

Persist the default identity in the synced settings store, keyed per
account (`preferredIdentityIds: Record<accountId, identityId>`), mirroring
the existing per-account `allMailFolderIds`. Per-account keying is required
because JMAP identity ids are account-scoped and would otherwise collide
across accounts / the unified mailbox.

- settings-store: add `preferredIdentityIds` to state, defaults, export
  (so it shows in exported JSON), import (with a non-record guard),
  rehydrate coercion, and a v6->v7 migration.
- auth-store: add `applyPreferredIdentity()`, invoked in every
  `loadFromServer().finally()` (login / OAuth / SSO / switch / restore) so
  the synced default reorders the active account's identities once server
  settings load (the composer defaults From to identities[0]).
- identity-manager-modal: the star action also writes the choice to the
  synced per-account map, triggering server sync + export inclusion.
- identity-store: keep `preferredPrimaryId` in local persist as a sync-off
  fallback; synced settings are the durable cross-device source of truth.
- tests: per-account independence, export/import round-trip, non-record
  import guard, and v6->v7 migration.
This commit is contained in:
Stefan Hildebrandt
2026-07-11 21:14:56 +02:00
parent 2e42693228
commit 034f7a4b9b
5 changed files with 154 additions and 10 deletions
+41
View File
@@ -233,6 +233,40 @@ function loadIdentities(rawIdentities: Identity[], username: string): { identiti
return { identities, primaryIdentity };
}
/**
* Re-apply the per-account default sender identity once synced settings are
* available (issue #507). The choice is stored server-side in the settings
* store (`preferredIdentityIds`, keyed by AccountEntry.id), so it can only be
* applied after `loadFromServer` resolves. It reorders the account's identities
* so the preferred one is primary - the composer defaults its `From` to
* identities[0]. No-op when nothing is configured for the account.
*
* @param accountId The account to apply for; defaults to the active account.
*/
export function applyPreferredIdentity(accountId?: string | null): void {
const targetId = accountId ?? useAccountStore.getState().activeAccountId;
if (!targetId) return;
const preferred = useSettingsStore.getState().preferredIdentityIds[targetId];
if (!preferred) return;
const idStore = useIdentityStore.getState();
// Only touch the live identity store when it currently holds this account's
// identities (true for the active account). Switching snapshots/restores the
// ordering per account, so a background account's order is restored later.
if (useAccountStore.getState().activeAccountId !== targetId) return;
idStore.setPreferredPrimary(preferred);
const ids = [...idStore.identities];
const idx = ids.findIndex((i) => i.id === preferred);
if (idx > 0) {
const [p] = ids.splice(idx, 1);
ids.unshift(p);
idStore.setIdentities(ids);
}
useAuthStore.setState({ identities: ids, primaryIdentity: ids[0] ?? null });
}
function getLocaleLoginPath(): string {
if (typeof window === 'undefined') return '/en/login';
@@ -638,6 +672,7 @@ export const useAuthStore = create<AuthState>()(
if (!config.settingsSyncEnabled) return;
useSettingsStore.getState().loadFromServer(username, serverUrl).finally(() => {
useSettingsStore.getState().enableSync(username, serverUrl);
applyPreferredIdentity(accountId);
});
}).catch(() => {});
@@ -840,6 +875,7 @@ export const useAuthStore = create<AuthState>()(
if (!config.settingsSyncEnabled) return;
useSettingsStore.getState().loadFromServer(username, serverUrl).finally(() => {
useSettingsStore.getState().enableSync(username, serverUrl);
applyPreferredIdentity(accountId);
});
}).catch(() => {});
@@ -977,6 +1013,7 @@ export const useAuthStore = create<AuthState>()(
if (!cfg.settingsSyncEnabled) return;
useSettingsStore.getState().loadFromServer(username, ssoServerUrl).finally(() => {
useSettingsStore.getState().enableSync(username, ssoServerUrl);
applyPreferredIdentity(accountId);
});
}).catch(() => {});
@@ -1363,6 +1400,7 @@ export const useAuthStore = create<AuthState>()(
if (!config.settingsSyncEnabled) return;
useSettingsStore.getState().loadFromServer(targetAccount.username, targetAccount.serverUrl).finally(() => {
useSettingsStore.getState().enableSync(targetAccount.username, targetAccount.serverUrl);
applyPreferredIdentity(targetAccount.id);
});
}).catch(() => {});
},
@@ -1534,6 +1572,7 @@ export const useAuthStore = create<AuthState>()(
if (!config.settingsSyncEnabled) return;
useSettingsStore.getState().loadFromServer(targetAccount.username, targetAccount.serverUrl).finally(() => {
useSettingsStore.getState().enableSync(targetAccount.username, targetAccount.serverUrl);
applyPreferredIdentity(targetAccount.id);
});
}).catch(() => {});
return;
@@ -1647,6 +1686,7 @@ export const useAuthStore = create<AuthState>()(
if (!config.settingsSyncEnabled) return;
useSettingsStore.getState().loadFromServer(state.username || '', state.serverUrl!).finally(() => {
useSettingsStore.getState().enableSync(state.username || '', state.serverUrl!);
applyPreferredIdentity(accountId);
});
}).catch(() => {});
return;
@@ -1718,6 +1758,7 @@ export const useAuthStore = create<AuthState>()(
if (!config.settingsSyncEnabled) return;
useSettingsStore.getState().loadFromServer(username, serverUrl).finally(() => {
useSettingsStore.getState().enableSync(username, serverUrl);
applyPreferredIdentity(accountId);
});
}).catch(() => {});
return;