fix(calendar,contacts): namespace all accounts consistently on switch

Calendar/contact aggregation left the ACTIVE account's ids raw while
namespacing background accounts (<localAccountId>::id). So an entity's id
depended on which account was active; switching flipped the id form and
invalidated persisted references (selectedCalendarIds), broke isSubscription
matching, and destabilised selection - the account-switch bug users saw in
Calendar and Address Book (mail was fine: it keys per account + stable
virtual ids).

Invariant now: an entity is namespaced iff it carries a localAccountId;
originalId always holds the raw JMAP id. Consumers/mutations already resolve
the raw id via `originalId || stripLocalAccountPrefix`, so they are unchanged.

- prefix* functions (calendar + contacts) always namespace + set originalId;
  drop the isActiveAccount branch and the now-dead activeLocalAccountId params
  (callers in the hooks + calendar-app/contacts-app updated).
- mapServerEventToStoreEvent namespaces the event id with the matched calendar's
  localAccountId, so createEvent/updateEvent results are consistent.
- reconcileSelectedIds(): remap selectedCalendarIds by raw identity on every
  fetch, so a raw->namespaced transition (switch, just-created calendar, or the
  one-time migration from old persisted raw ids) keeps the selection instead of
  resetting to "all".
- isSubscriptionCalendar(): namespace-robust and collision-safe - resolve the
  calendar and match by raw id scoped to the owning JMAP accountId (Stalwart's
  raw ids like "b"/"c" collide across accounts).

+9 unit tests (selection stability across switch/create, subscription
robustness, no cross-account false positives). tsc + eslint clean; 368 store
tests green.
This commit is contained in:
Stefan Hildebrandt
2026-08-10 22:45:21 +02:00
parent eef184f3d3
commit 583e20d9dc
7 changed files with 203 additions and 69 deletions
+2 -2
View File
@@ -656,7 +656,7 @@ export function CalendarApp({ linkSegments }: CalendarAppProps = {}) {
const { dateRange: currentRange } = useCalendarStore.getState();
if (!currentRange) return;
if (multiAccountEnabled && accountClients.length > 0) {
await fetchAllAccountsEventsFn(accountClients, activeAccountId, currentRange.start, currentRange.end);
await fetchAllAccountsEventsFn(accountClients, currentRange.start, currentRange.end);
return;
}
await fetchEvents(client, currentRange.start, currentRange.end);
@@ -669,7 +669,7 @@ export function CalendarApp({ linkSegments }: CalendarAppProps = {}) {
onRefresh: async () => {
if (!client) return;
const calendarRefresh = multiAccountEnabled && accountClients.length > 0 && activeAccountId
? fetchAllAccountsCalendarsFn(accountClients, activeAccountId)
? fetchAllAccountsCalendarsFn(accountClients)
: fetchCalendars(client);
await Promise.all([
calendarRefresh,
+3 -3
View File
@@ -224,8 +224,8 @@ export function ContactsApp({ linkSegments }: ContactsAppProps = {}) {
if (activeId) {
const { fetchAllAccountsContacts, fetchAllAccountsAddressBooks } = useContactStore.getState();
await Promise.all([
fetchAllAccountsAddressBooks(accountClients, activeId),
fetchAllAccountsContacts(accountClients, activeId),
fetchAllAccountsAddressBooks(accountClients),
fetchAllAccountsContacts(accountClients),
]);
return;
}
@@ -331,7 +331,7 @@ export function ContactsApp({ linkSegments }: ContactsAppProps = {}) {
const activeId = useAuthStore.getState().activeAccountId;
if (activeId) {
const { fetchAllAccountsAddressBooks } = useContactStore.getState();
await fetchAllAccountsAddressBooks(accountClients, activeId);
await fetchAllAccountsAddressBooks(accountClients);
return;
}
}
+2 -2
View File
@@ -49,14 +49,14 @@ export function useProMultiAccountCalendars(start: string | null, end: string |
// bootstrapping).
useEffect(() => {
if (!enabled || !activeAccountId || accountClients.length === 0) return;
void fetchAllAccountsCalendars(accountClients, activeAccountId);
void fetchAllAccountsCalendars(accountClients);
}, [enabled, activeAccountId, accountClients, fetchAllAccountsCalendars]);
// Fetch events for the current visible date range across all accounts.
useEffect(() => {
if (!enabled || !activeAccountId || accountClients.length === 0) return;
if (!start || !end) return;
void fetchAllAccountsEvents(accountClients, activeAccountId, start, end);
void fetchAllAccountsEvents(accountClients, start, end);
}, [enabled, activeAccountId, accountClients, start, end, fetchAllAccountsEvents]);
return { enabled, accountClients };
+2 -2
View File
@@ -40,8 +40,8 @@ export function useProMultiAccountContacts(): {
useEffect(() => {
if (!enabled || !activeAccountId || accountClients.length === 0) return;
void fetchAllAccountsAddressBooks(accountClients, activeAccountId);
void fetchAllAccountsContacts(accountClients, activeAccountId);
void fetchAllAccountsAddressBooks(accountClients);
void fetchAllAccountsContacts(accountClients);
}, [enabled, activeAccountId, accountClients, fetchAllAccountsAddressBooks, fetchAllAccountsContacts]);
return { enabled, accountClients };
@@ -0,0 +1,93 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { useCalendarStore, reconcileSelectedIds } from '../calendar-store';
import type { Calendar } from '@/lib/jmap/types';
function cal(overrides: Partial<Calendar>): Calendar {
return {
id: 'c', name: 'Cal', color: null, sortOrder: 0, isSubscribed: true, isVisible: true,
isDefault: false, includeInAvailability: 'all', timeZone: null, shareWith: null,
myRights: {
mayReadFreeBusy: true, mayReadItems: true, mayWriteAll: true, mayWriteOwn: true,
mayUpdatePrivate: true, mayRSVP: true, mayShare: true, mayDelete: true,
},
...overrides,
} as Calendar;
}
describe('reconcileSelectedIds (account-switch selection stability)', () => {
it('keeps a selection whose id form flipped raw -> namespaced', () => {
// Persisted while account A was active (raw "b"); after the fix A is
// namespaced. The calendar is the same, only the id string changed.
const calendars = [cal({ id: 'A@h::b', originalId: 'b', localAccountId: 'A@h' })];
expect(reconcileSelectedIds(['b'], calendars)).toEqual(['A@h::b']);
});
it('keeps an already-namespaced selection unchanged across a switch', () => {
const calendars = [
cal({ id: 'A@h::b', originalId: 'b', localAccountId: 'A@h' }),
cal({ id: 'B@h::b', originalId: 'b', localAccountId: 'B@h' }),
];
// Both accounts have raw id "b"; the namespaced selection is unambiguous.
expect(reconcileSelectedIds(['B@h::b'], calendars)).toEqual(['B@h::b']);
});
it('remaps a just-created raw id to its namespaced form after refetch', () => {
const calendars = [
cal({ id: 'A@h::b', originalId: 'b', localAccountId: 'A@h' }),
cal({ id: 'A@h::f', originalId: 'f', localAccountId: 'A@h' }),
];
// createCalendar added the raw "f"; the refetch namespaced it.
expect(reconcileSelectedIds(['A@h::b', 'f'], calendars)).toEqual(['A@h::b', 'A@h::f']);
});
it('drops ids that resolve to no calendar and de-dupes', () => {
const calendars = [cal({ id: 'A@h::b', originalId: 'b', localAccountId: 'A@h' })];
expect(reconcileSelectedIds(['b', 'A@h::b', 'gone'], calendars)).toEqual(['A@h::b']);
});
it('preserves the birthday calendar id verbatim', () => {
const calendars = [cal({ id: 'A@h::b', originalId: 'b', localAccountId: 'A@h' })];
expect(reconcileSelectedIds(['__birthday-calendar__', 'b'], calendars)).toEqual(['__birthday-calendar__', 'A@h::b']);
});
it('leaves single-account (raw, no localAccountId) selections untouched', () => {
const calendars = [cal({ id: 'b' }), cal({ id: 'c' })];
expect(reconcileSelectedIds(['b', 'c'], calendars)).toEqual(['b', 'c']);
});
});
describe('isSubscriptionCalendar (namespace-robust, collision-safe)', () => {
beforeEach(() => {
useCalendarStore.setState({ calendars: [], icalSubscriptions: [] });
});
it('matches a subscription across the raw->namespaced boundary', () => {
useCalendarStore.setState({
calendars: [cal({ id: 'A@h::sub1', originalId: 'sub1', localAccountId: 'A@h', accountId: 'acctA' })],
// subs store the raw JMAP calendar id
icalSubscriptions: [{ id: 's', url: 'x', calendarId: 'sub1', accountId: 'acctA', name: 'Feed', color: '#000', refreshInterval: 60, lastRefreshed: null }],
});
expect(useCalendarStore.getState().isSubscriptionCalendar('A@h::sub1')).toBe(true);
});
it('does NOT false-positive when another account has the same raw id', () => {
useCalendarStore.setState({
calendars: [
cal({ id: 'A@h::b', originalId: 'b', localAccountId: 'A@h', accountId: 'acctA' }), // A's subscription calendar
cal({ id: 'B@h::b', originalId: 'b', localAccountId: 'B@h', accountId: 'acctB' }), // B's normal calendar, same raw id
],
icalSubscriptions: [{ id: 's', url: 'x', calendarId: 'b', accountId: 'acctA', name: 'Feed', color: '#000', refreshInterval: 60, lastRefreshed: null }],
});
expect(useCalendarStore.getState().isSubscriptionCalendar('A@h::b')).toBe(true);
expect(useCalendarStore.getState().isSubscriptionCalendar('B@h::b')).toBe(false);
});
it('matches a single-account (raw) subscription exactly', () => {
useCalendarStore.setState({
calendars: [cal({ id: 'sub1' })],
icalSubscriptions: [{ id: 's', url: 'x', calendarId: 'sub1', name: 'Feed', color: '#000', refreshInterval: 60, lastRefreshed: null }],
});
expect(useCalendarStore.getState().isSubscriptionCalendar('sub1')).toBe(true);
expect(useCalendarStore.getState().isSubscriptionCalendar('other')).toBe(false);
});
});
+84 -34
View File
@@ -42,6 +42,42 @@ function stripLocalAccountPrefix(id: string, localAccountId?: string): string {
return id.startsWith(prefix) ? id.slice(prefix.length) : id;
}
/** The raw identity of a store id, independent of any `<localAccountId>::` prefix. */
function rawIdentityOf(id: string): string {
const idx = id.indexOf(CROSS_ACCOUNT_ID_DELIMITER);
return idx >= 0 ? id.slice(idx + CROSS_ACCOUNT_ID_DELIMITER.length) : id;
}
/**
* Reconcile persisted selectedCalendarIds against a freshly fetched calendars
* list. A selected id can be stale in id-*form* (not existence): the raw->
* namespaced transition (aggregation switched on, an account switch, or a
* just-created calendar added with its raw id) changes the id string while the
* calendar is the same. Remap by raw identity so the selection survives instead
* of silently resetting to "all". Keeps BIRTHDAY_CALENDAR_ID; drops ids that
* resolve to no calendar.
*/
export function reconcileSelectedIds(selectedCalendarIds: string[], calendars: Calendar[]): string[] {
const byStoreId = new Set(calendars.map((c) => c.id));
const rawToStoreId = new Map<string, string>();
for (const c of calendars) {
const raw = c.originalId ?? stripLocalAccountPrefix(c.id, c.localAccountId);
if (!rawToStoreId.has(raw)) rawToStoreId.set(raw, c.id);
}
const out: string[] = [];
const seen = new Set<string>();
for (const id of selectedCalendarIds) {
let mapped: string | undefined;
if (id === BIRTHDAY_CALENDAR_ID || byStoreId.has(id)) mapped = id;
else mapped = rawToStoreId.get(rawIdentityOf(id));
if (mapped && !seen.has(mapped)) {
out.push(mapped);
seen.add(mapped);
}
}
return out;
}
// In-flight refresh dedup. Concurrent callers (auto-interval +
// manual refresh, two account-switch reloads, etc.) share the same
// promise instead of double-fetching and racing the diff/import phase.
@@ -56,10 +92,11 @@ export function isCalendarViewMode(value: unknown): value is CalendarViewMode {
}
/**
* Prefix used to namespace calendar/event IDs that belong to a non-active
* JMAP account when the Pro shell aggregates across accounts. The active
* account's IDs are left untouched so existing single-account code paths
* (links, deep-links, JMAP mutations) keep working unchanged.
* Prefix used to namespace calendar/event IDs when the Pro shell aggregates
* across accounts. EVERY aggregated account is namespaced (including the active
* one) so an id stably identifies (account, entity) regardless of which account
* is active - otherwise switching accounts flipped the id form. Single-account
* (non-aggregated) code paths carry no localAccountId and keep raw ids.
*/
const CROSS_ACCOUNT_ID_DELIMITER = '::';
@@ -67,14 +104,18 @@ function buildCrossAccountIdPrefix(localAccountId: string): string {
return `${localAccountId}${CROSS_ACCOUNT_ID_DELIMITER}`;
}
// EVERY aggregated account is namespaced, including the active one. Leaving the
// active account's IDs raw made an entity's ID depend on *which* account is
// active, so switching accounts flipped the ID form and invalidated persisted
// references (selectedCalendarIds, colors, subscriptions) - the account-switch
// bug. The invariant is now: an entity is namespaced iff it carries a
// `localAccountId`; `originalId` always holds the raw JMAP ID. Mutation/consumer
// code already resolves the raw ID via `originalId || stripLocalAccountPrefix`,
// so it keeps working unchanged.
function prefixCalendarsWithLocalAccount(
calendars: Calendar[],
localAccountId: string,
isActiveAccount: boolean,
): Calendar[] {
if (isActiveAccount) {
return calendars.map((cal) => ({ ...cal, localAccountId }));
}
const prefix = buildCrossAccountIdPrefix(localAccountId);
// Preserve each calendar's original `isShared` flag - it distinguishes
// the user's own calendars on the other account from calendars shared
@@ -83,6 +124,7 @@ function prefixCalendarsWithLocalAccount(
return calendars.map((cal) => ({
...cal,
id: `${prefix}${cal.id}`,
originalId: cal.originalId ?? cal.id,
localAccountId,
}));
}
@@ -90,15 +132,12 @@ function prefixCalendarsWithLocalAccount(
function prefixEventsWithLocalAccount(
events: CalendarEvent[],
localAccountId: string,
isActiveAccount: boolean,
): CalendarEvent[] {
if (isActiveAccount) {
return events.map((event) => ({ ...event, localAccountId }));
}
const prefix = buildCrossAccountIdPrefix(localAccountId);
return events.map((event) => ({
...event,
id: `${prefix}${event.id}`,
originalId: event.originalId ?? event.id,
localAccountId,
calendarIds: event.calendarIds
? Object.fromEntries(
@@ -143,15 +182,21 @@ function mapServerEventToStoreEvent(
.find((calendar): calendar is Calendar => Boolean(calendar));
const resolvedAccountId = matchedCalendar?.accountId || targetAccountId;
const isShared = matchedCalendar?.isShared || false;
// Namespace the event id with the owning account when the store is aggregated
// (the matched calendar carries a localAccountId). `calendarIds` are already
// mapped to store ids above; `originalId` keeps the raw id for mutations.
const localAccountId = matchedCalendar?.localAccountId;
const baseId = isShared && resolvedAccountId ? `${resolvedAccountId}:${event.id}` : event.id;
return {
...event,
id: isShared && resolvedAccountId ? `${resolvedAccountId}:${event.id}` : event.id,
id: localAccountId ? `${localAccountId}${CROSS_ACCOUNT_ID_DELIMITER}${baseId}` : baseId,
originalId: event.id,
originalCalendarIds: event.calendarIds,
calendarIds: mappedCalendarIds,
accountId: resolvedAccountId,
accountName: matchedCalendar?.accountName,
localAccountId,
isShared,
};
}
@@ -224,8 +269,8 @@ interface CalendarStore {
setSupported: (supported: boolean) => void;
fetchCalendars: (client: IJMAPClient) => Promise<void>;
fetchEvents: (client: IJMAPClient, start: string, end: string) => Promise<void>;
fetchAllAccountsCalendars: (accounts: CalendarAccountClient[], activeLocalAccountId: string) => Promise<void>;
fetchAllAccountsEvents: (accounts: CalendarAccountClient[], activeLocalAccountId: string, start: string, end: string) => Promise<void>;
fetchAllAccountsCalendars: (accounts: CalendarAccountClient[]) => Promise<void>;
fetchAllAccountsEvents: (accounts: CalendarAccountClient[], start: string, end: string) => Promise<void>;
createEvent: (client: IJMAPClient, event: Partial<CalendarEvent>, sendSchedulingMessages?: boolean) => Promise<CalendarEvent | null>;
updateEvent: (client: IJMAPClient, id: string, updates: Partial<CalendarEvent>, sendSchedulingMessages?: boolean) => Promise<void>;
deleteEvent: (client: IJMAPClient, id: string, sendSchedulingMessages?: boolean) => Promise<void>;
@@ -284,12 +329,11 @@ export const useCalendarStore = create<CalendarStore>()(
try {
const calendars = await client.getAllCalendars();
const { selectedCalendarIds } = get();
const validIds = calendars.map(c => c.id);
const stillValid = selectedCalendarIds.filter(id => validIds.includes(id) || id === BIRTHDAY_CALENDAR_ID);
const stillValid = reconcileSelectedIds(selectedCalendarIds, calendars);
set({
calendars,
isLoading: false,
selectedCalendarIds: stillValid.length > 0 ? stillValid : validIds,
selectedCalendarIds: stillValid.length > 0 ? stillValid : calendars.map(c => c.id),
});
} catch (error) {
debug.error('Failed to fetch calendars:', error);
@@ -332,18 +376,14 @@ export const useCalendarStore = create<CalendarStore>()(
}
},
fetchAllAccountsCalendars: async (accounts, activeLocalAccountId) => {
fetchAllAccountsCalendars: async (accounts) => {
set({ isLoading: true, error: null });
try {
const results = await Promise.all(
accounts.map(async ({ client, localAccountId }) => {
try {
const list = await client.getAllCalendars();
return prefixCalendarsWithLocalAccount(
list,
localAccountId,
localAccountId === activeLocalAccountId,
);
return prefixCalendarsWithLocalAccount(list, localAccountId);
} catch (error) {
debug.error(`Failed to fetch calendars for account ${localAccountId}:`, error);
return [] as Calendar[];
@@ -352,12 +392,11 @@ export const useCalendarStore = create<CalendarStore>()(
);
const calendars = results.flat();
const { selectedCalendarIds } = get();
const validIds = calendars.map(c => c.id);
const stillValid = selectedCalendarIds.filter(id => validIds.includes(id) || id === BIRTHDAY_CALENDAR_ID);
const stillValid = reconcileSelectedIds(selectedCalendarIds, calendars);
set({
calendars,
isLoading: false,
selectedCalendarIds: stillValid.length > 0 ? stillValid : validIds,
selectedCalendarIds: stillValid.length > 0 ? stillValid : calendars.map(c => c.id),
});
} catch (error) {
debug.error('Failed to fetch all-account calendars:', error);
@@ -365,7 +404,7 @@ export const useCalendarStore = create<CalendarStore>()(
}
},
fetchAllAccountsEvents: async (accounts, activeLocalAccountId, start, end) => {
fetchAllAccountsEvents: async (accounts, start, end) => {
set({ isLoadingEvents: true, error: null });
try {
const results = await Promise.all(
@@ -376,11 +415,7 @@ export const useCalendarStore = create<CalendarStore>()(
typeof e.start === 'string' && e.start && !isNaN(parseISO(e.start).getTime())
);
const expanded = expandRecurringEvents(valid, start, end);
return prefixEventsWithLocalAccount(
expanded,
localAccountId,
localAccountId === activeLocalAccountId,
);
return prefixEventsWithLocalAccount(expanded, localAccountId);
} catch (error) {
debug.error(`Failed to fetch events for account ${localAccountId}:`, error);
return [] as CalendarEvent[];
@@ -884,6 +919,8 @@ export const useCalendarStore = create<CalendarStore>()(
set({ error: null });
try {
const created = await client.createCalendar(calendar);
// Added with its raw id; the next aggregated refetch namespaces it and
// reconcileSelectedIds() remaps the selection so it stays visible.
set((state) => ({
calendars: [...state.calendars, created],
selectedCalendarIds: [...state.selectedCalendarIds, created.id],
@@ -997,7 +1034,20 @@ export const useCalendarStore = create<CalendarStore>()(
// iCal subscriptions
isSubscriptionCalendar: (calendarId) => {
return get().icalSubscriptions.some(s => s.calendarId === calendarId);
const subs = get().icalSubscriptions;
// Fast path: exact match (single-account, or a sub stored with the same
// id form as the query).
if (subs.some((s) => s.calendarId === calendarId)) return true;
// Aggregated: the query is a namespaced store id while subs store the raw
// JMAP calendar id. Resolve the calendar and match by raw id, scoped to
// the owning JMAP account so raw ids that collide across accounts (e.g.
// Stalwart's "b"/"c") don't cause false positives.
const cal = get().calendars.find((c) => c.id === calendarId);
if (!cal) return false;
const rawId = cal.originalId ?? stripLocalAccountPrefix(cal.id, cal.localAccountId);
return subs.some(
(s) => s.calendarId === rawId && (!s.accountId || !cal.accountId || s.accountId === cal.accountId),
);
},
addICalSubscription: async (client, url, name, color, refreshInterval = 60) => {
+17 -26
View File
@@ -55,10 +55,9 @@ export interface ContactAccountClient {
}
/**
* Prefix used to namespace contact/address-book IDs that belong to a
* non-active JMAP account when the Pro shell aggregates across accounts.
* The active account's IDs are left untouched so existing single-account
* code paths keep working unchanged.
* Prefix used to namespace contact/address-book IDs when the Pro shell
* aggregates across accounts. EVERY aggregated account is namespaced (including
* the active one); single-account paths carry no localAccountId and stay raw.
*/
const CROSS_ACCOUNT_ID_DELIMITER = '::';
@@ -66,18 +65,21 @@ function buildCrossAccountIdPrefix(localAccountId: string): string {
return `${localAccountId}${CROSS_ACCOUNT_ID_DELIMITER}`;
}
// EVERY aggregated account is namespaced, including the active one, so an id
// stably identifies (account, entity) regardless of which account is active -
// otherwise switching accounts flipped the id form and broke mutation routing /
// selection. Invariant: namespaced iff `localAccountId` is set; `originalId`
// holds the raw id. Mutations already resolve raw via `originalId ||
// stripLocalAccountPrefix`, so they keep working. Mirrors the calendar store.
function prefixAddressBooksWithLocalAccount(
books: AddressBook[],
localAccountId: string,
isActiveAccount: boolean,
): AddressBook[] {
if (isActiveAccount) {
return books.map((b) => ({ ...b, localAccountId }));
}
const prefix = buildCrossAccountIdPrefix(localAccountId);
return books.map((b) => ({
...b,
id: `${prefix}${b.id}`,
originalId: b.originalId ?? b.id,
localAccountId,
}));
}
@@ -85,15 +87,12 @@ function prefixAddressBooksWithLocalAccount(
function prefixContactsWithLocalAccount(
contacts: ContactCard[],
localAccountId: string,
isActiveAccount: boolean,
): ContactCard[] {
if (isActiveAccount) {
return contacts.map((c) => ({ ...c, localAccountId }));
}
const prefix = buildCrossAccountIdPrefix(localAccountId);
return contacts.map((c) => ({
...c,
id: `${prefix}${c.id}`,
originalId: c.originalId ?? c.id,
localAccountId,
addressBookIds: c.addressBookIds
? Object.fromEntries(
@@ -277,8 +276,8 @@ interface ContactStore {
fetchContacts: (client: IJMAPClient) => Promise<void>;
fetchDirectory: (client: IJMAPClient) => Promise<void>;
fetchAddressBooks: (client: IJMAPClient) => Promise<void>;
fetchAllAccountsContacts: (accounts: ContactAccountClient[], activeLocalAccountId: string) => Promise<void>;
fetchAllAccountsAddressBooks: (accounts: ContactAccountClient[], activeLocalAccountId: string) => Promise<void>;
fetchAllAccountsContacts: (accounts: ContactAccountClient[]) => Promise<void>;
fetchAllAccountsAddressBooks: (accounts: ContactAccountClient[]) => Promise<void>;
createContact: (client: IJMAPClient, contact: Partial<ContactCard>) => Promise<void>;
updateContact: (client: IJMAPClient, id: string, updates: Partial<ContactCard>) => Promise<void>;
deleteContact: (client: IJMAPClient, id: string) => Promise<void>;
@@ -429,18 +428,14 @@ export const useContactStore = create<ContactStore>()(
}
},
fetchAllAccountsContacts: async (accounts, activeLocalAccountId) => {
fetchAllAccountsContacts: async (accounts) => {
set({ isLoading: true, error: null });
try {
const results = await Promise.all(
accounts.map(async ({ client, localAccountId }) => {
try {
const list = await client.getAllContacts();
return prefixContactsWithLocalAccount(
list,
localAccountId,
localAccountId === activeLocalAccountId,
);
return prefixContactsWithLocalAccount(list, localAccountId);
} catch (error) {
debug.error(`Failed to fetch contacts for account ${localAccountId}:`, error);
return [] as ContactCard[];
@@ -454,17 +449,13 @@ export const useContactStore = create<ContactStore>()(
}
},
fetchAllAccountsAddressBooks: async (accounts, activeLocalAccountId) => {
fetchAllAccountsAddressBooks: async (accounts) => {
try {
const results = await Promise.all(
accounts.map(async ({ client, localAccountId }) => {
try {
const list = await client.getAllAddressBooks();
return prefixAddressBooksWithLocalAccount(
list,
localAccountId,
localAccountId === activeLocalAccountId,
);
return prefixAddressBooksWithLocalAccount(list, localAccountId);
} catch (error) {
debug.error(`Failed to fetch address books for account ${localAccountId}:`, error);
return [] as AddressBook[];