134 lines
4.9 KiB
TypeScript
134 lines
4.9 KiB
TypeScript
"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
|
|
* <optgroup> 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<Record<string, Identity[]>>({});
|
|
|
|
// 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<string, Identity[]> = {};
|
|
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<AccountIdentityGroup[]>(() => {
|
|
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 };
|
|
}
|