feat: group composer From dropdown by account in Pro shell
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
"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 };
|
||||
}
|
||||
Reference in New Issue
Block a user