"use client"; import { useEffect, useMemo, useState } from "react"; import { useAccountStore } from "@/stores/account-store"; import { useAuthStore } from "@/stores/auth-store"; import { useIdentityStore } from "@/stores/identity-store"; import { useSettingsStore } from "@/stores/settings-store"; import { useIsEmbedded } from "@/hooks/use-is-embedded"; import type { Identity } from "@/lib/jmap/types"; interface AccountIdentityGroup { localAccountId: string; accountLabel: string; identities: Identity[]; } const CROSS_ACCOUNT_IDENTITY_DELIMITER = '::'; /** Cross-account identity IDs are namespaced to avoid collisions between * JMAP servers that happen to issue the same opaque ID. The active * account's IDs are left untouched so existing single-account code paths * (reply-identity resolution, S/MIME bindings) keep working unchanged. */ export function isCrossAccountIdentityId(id: string): boolean { return id.includes(CROSS_ACCOUNT_IDENTITY_DELIMITER); } export function stripCrossAccountIdentityPrefix(id: string): { localAccountId: string | null; rawId: string } { const idx = id.indexOf(CROSS_ACCOUNT_IDENTITY_DELIMITER); if (idx < 0) return { localAccountId: null, rawId: id }; return { localAccountId: id.slice(0, idx), rawId: id.slice(idx + CROSS_ACCOUNT_IDENTITY_DELIMITER.length), }; } /** * Pro shell only: load identities from every connected account and group * them by local account so the composer's From dropdown can render an * per account - mirrors [[useProMultiAccountCalendars]] and * [[useProMultiAccountContacts]]. * * Outside Pro / embedded mode the hook returns `enabled: false` and the * caller falls back to the active account's identities from * [[useIdentityStore]]. */ export function useProMultiAccountIdentities(): { enabled: boolean; groups: AccountIdentityGroup[]; /** Flat list across all accounts, useful for lookup-by-id. */ allIdentities: Identity[]; } { const isEmbedded = useIsEmbedded(); const proInterface = useSettingsStore((s) => s.proInterface); const accounts = useAccountStore((s) => s.accounts); const activeAccountId = useAuthStore((s) => s.activeAccountId); const activeIdentities = useIdentityStore((s) => s.identities); const enabled = (proInterface || isEmbedded) && accounts.filter(a => a.isConnected).length > 1; const [remoteIdentities, setRemoteIdentities] = useState>({}); // Cache identities fetched per non-active account. Active account's // identities come live from useIdentityStore so signature/alias edits // there are reflected immediately without an extra round-trip. useEffect(() => { if (!enabled) { setRemoteIdentities({}); return; } let cancelled = false; const getClientForAccount = useAuthStore.getState().getClientForAccount; (async () => { const next: Record = {}; await Promise.all( accounts .filter((a) => a.isConnected && a.id !== activeAccountId) .map(async (account) => { const client = getClientForAccount(account.id); if (!client) return; try { const list = await client.getIdentities(); if (!cancelled) next[account.id] = list; } catch { // Skip accounts that fail to load identities - one bad // account shouldn't blank the whole dropdown. } }), ); if (!cancelled) setRemoteIdentities(next); })(); return () => { cancelled = true; }; }, [enabled, accounts, activeAccountId]); const groups = useMemo(() => { if (!enabled) return []; const out: AccountIdentityGroup[] = []; if (activeAccountId) { const active = accounts.find((a) => a.id === activeAccountId); const label = active?.label || active?.email || active?.username || activeAccountId; out.push({ localAccountId: activeAccountId, accountLabel: label, identities: activeIdentities.map((id) => ({ ...id, localAccountId: activeAccountId, accountName: label, })), }); } for (const account of accounts) { if (!account.isConnected || account.id === activeAccountId) continue; const list = remoteIdentities[account.id]; if (!list || list.length === 0) continue; const label = account.label || account.email || account.username; out.push({ localAccountId: account.id, accountLabel: label, identities: list.map((id) => ({ ...id, id: `${account.id}${CROSS_ACCOUNT_IDENTITY_DELIMITER}${id.id}`, localAccountId: account.id, accountName: label, })), }); } return out; }, [enabled, accounts, activeAccountId, activeIdentities, remoteIdentities]); const allIdentities = useMemo(() => groups.flatMap((g) => g.identities), [groups]); return { enabled, groups, allIdentities }; }