diff --git a/app/(main)/[locale]/calendar/page.tsx b/app/(main)/[locale]/calendar/page.tsx index aae1f10e..80611f3f 100644 --- a/app/(main)/[locale]/calendar/page.tsx +++ b/app/(main)/[locale]/calendar/page.tsx @@ -46,6 +46,7 @@ import { SidebarAppsModal } from "@/components/layout/sidebar-apps-modal"; import { InlineAppView } from "@/components/layout/inline-app-view"; import { useSidebarApps } from "@/hooks/use-sidebar-apps"; import { useIsEmbedded } from "@/hooks/use-is-embedded"; +import { useProMultiAccountCalendars } from "@/hooks/use-pro-multi-account-calendars"; import { ResizeHandle } from "@/components/layout/resize-handle"; import { sanitizeOutgoingCalendarEventData } from "@/lib/calendar-event-normalization"; import { getEventStartDate } from "@/lib/calendar-utils"; @@ -263,12 +264,17 @@ export default function CalendarPage() { return subscribeToPendingWebcal(openPendingWebcal); }, [isAuthenticated, client, handleWebcalProtocolRequest]); + // Single-account fetch path. The Pro shell aggregates calendars from + // every connected account via [[useProMultiAccountCalendars]] below, so + // skip this fetch there to avoid clobbering the merged list with the + // active client's calendars only. useEffect(() => { + if (isEmbedded) return; if (client && !hasFetched.current) { hasFetched.current = true; fetchCalendars(client); } - }, [client, fetchCalendars]); + }, [client, fetchCalendars, isEmbedded]); // Auto-refresh iCal subscriptions useEffect(() => { @@ -337,10 +343,21 @@ export default function CalendarPage() { }, [client, enableCalendarTasks, normalizedViewMode, showTasksOnCalendar, fetchTasksFn]); useEffect(() => { + if (isEmbedded) return; if (client && calendars.length > 0 && dateRange) { fetchEvents(client, dateRange.start, dateRange.end); } - }, [client, calendars.length, dateRange, fetchEvents]); + }, [client, calendars.length, dateRange, fetchEvents, isEmbedded]); + + // Pro shell only: aggregate calendars and events from every connected + // account so the sidebar lists them all (and the views render their + // events together). The hook is a no-op outside the embedded shell. + const { enabled: multiAccountEnabled, accountClients } = useProMultiAccountCalendars( + isEmbedded ? dateRange?.start ?? null : null, + isEmbedded ? dateRange?.end ?? null : null, + ); + const fetchAllAccountsCalendarsFn = useCalendarStore((s) => s.fetchAllAccountsCalendars); + const fetchAllAccountsEventsFn = useCalendarStore((s) => s.fetchAllAccountsEvents); const navigatePrev = useCallback(() => { let next: Date; @@ -564,12 +581,15 @@ export default function CalendarPage() { }, [events, client]); const refetchCurrentRange = useCallback(async () => { - if (!client) return; + if (!client || !activeAccountId) return; const { dateRange: currentRange } = useCalendarStore.getState(); - if (currentRange) { - await fetchEvents(client, currentRange.start, currentRange.end); + if (!currentRange) return; + if (multiAccountEnabled && accountClients.length > 0) { + await fetchAllAccountsEventsFn(accountClients, activeAccountId, currentRange.start, currentRange.end); + return; } - }, [client, fetchEvents]); + await fetchEvents(client, currentRange.start, currentRange.end); + }, [client, fetchEvents, multiAccountEnabled, accountClients, activeAccountId, fetchAllAccountsEventsFn]); // Intercept browser refresh gestures (F5, Ctrl/Cmd+R, pull-to-refresh) // and refresh calendar data via JMAP instead of reloading the page. @@ -577,8 +597,11 @@ export default function CalendarPage() { enabled: isAuthenticated && !!client, onRefresh: async () => { if (!client) return; + const calendarRefresh = multiAccountEnabled && accountClients.length > 0 && activeAccountId + ? fetchAllAccountsCalendarsFn(accountClients, activeAccountId) + : fetchCalendars(client); await Promise.all([ - fetchCalendars(client), + calendarRefresh, refetchCurrentRange(), refreshAllSubscriptions(client), ]); @@ -1335,6 +1358,7 @@ export default function CalendarPage() { onSubscribe={() => setShowSubscriptionModal(true)} onEditSubscription={(subId) => setEditingSubscription(subId)} client={client} + multiAccountMode={multiAccountEnabled && accountClients.length > 1} /> {!isNarrow && ( diff --git a/components/calendar/calendar-sidebar-panel.tsx b/components/calendar/calendar-sidebar-panel.tsx index e4b67207..f1325a8e 100644 --- a/components/calendar/calendar-sidebar-panel.tsx +++ b/components/calendar/calendar-sidebar-panel.tsx @@ -2,13 +2,14 @@ import { useMemo, useState } from "react"; import { useTranslations } from "next-intl"; -import { Globe, ListTodo, Pencil, RefreshCw, Share2, Trash2, Cake, Users, Plus, Eraser, Palette } from "lucide-react"; +import { ChevronDown, ChevronRight, Globe, ListTodo, Pencil, RefreshCw, Share2, Trash2, Cake, User, Users, Plus, Eraser, Palette } from "lucide-react"; import { cn, formatDateTime } from "@/lib/utils"; import type { Calendar } from "@/lib/jmap/types"; import { CalendarColorPicker } from "@/components/settings/calendar-management-settings"; import { useCalendarStore } from "@/stores/calendar-store"; import { useSettingsStore } from "@/stores/settings-store"; import { useTaskStore } from "@/stores/task-store"; +import { useAccountStore } from "@/stores/account-store"; import { BIRTHDAY_CALENDAR_ID } from "@/lib/birthday-calendar"; import { toast } from "@/stores/toast-store"; import { ContextMenu, ContextMenuItem, ContextMenuSeparator, ContextMenuSubMenu } from "@/components/ui/context-menu"; @@ -28,6 +29,12 @@ interface CalendarSidebarPanelProps { onSubscribe?: () => void; onEditSubscription?: (subscriptionId: string) => void; client?: IJMAPClient | null; + /** + * When true, render one collapsible section per connected local account, + * mirroring the mail sidebar's Pro-shell layout. Calendars are bucketed + * by their `localAccountId` and the active account is shown first. + */ + multiAccountMode?: boolean; } export function CalendarSidebarPanel({ @@ -43,6 +50,7 @@ export function CalendarSidebarPanel({ onSubscribe, onEditSubscription, client, + multiAccountMode, }: CalendarSidebarPanelProps) { const t = useTranslations("calendar"); const tSub = useTranslations("calendar.subscription"); @@ -70,6 +78,26 @@ export function CalendarSidebarPanel({ const { contextMenu, openContextMenu, closeContextMenu, menuRef } = useContextMenu(); const [refreshingSubId, setRefreshingSubId] = useState(null); + // Persisted across mounts so toggle state survives tab switches in the + // Pro shell (same key family as the mail sidebar's account collapse). + const [collapsedAccountGroups, setCollapsedAccountGroups] = useState>(() => { + try { + const raw = localStorage.getItem('calendar-sidebar-collapsed-accounts'); + return raw ? new Set(JSON.parse(raw)) : new Set(); + } catch { return new Set(); } + }); + const toggleAccountGroup = (key: string) => { + setCollapsedAccountGroups((prev) => { + const next = new Set(prev); + if (next.has(key)) next.delete(key); else next.add(key); + try { localStorage.setItem('calendar-sidebar-collapsed-accounts', JSON.stringify(Array.from(next))); } catch { /* */ } + return next; + }); + }; + + const localAccounts = useAccountStore((s) => s.accounts); + const activeLocalAccountId = useAccountStore((s) => s.activeAccountId); + const personalCalendars = useMemo(() => calendars.filter(c => !c.isShared), [calendars]); const sharedAccountGroups = useMemo(() => { const shared = calendars.filter(c => c.isShared); @@ -84,6 +112,53 @@ export function CalendarSidebarPanel({ return Array.from(groups.values()); }, [calendars]); + /** + * Pro / multi-account grouping: every calendar bucketed by its owning + * local account. Active account comes first, then the rest in their + * account-store order. Calendars without a `localAccountId` (e.g. the + * birthday calendar) fall into a separate "other" bucket so they still + * render. + */ + const localAccountGroups = useMemo(() => { + if (!multiAccountMode) return []; + const byAccount = new Map(); + for (const cal of calendars) { + const key = cal.localAccountId || '__other__'; + const list = byAccount.get(key) ?? []; + list.push(cal); + byAccount.set(key, list); + } + const ordered: { key: string; label: string; calendars: Calendar[] }[] = []; + // Active account first. + if (activeLocalAccountId && byAccount.has(activeLocalAccountId)) { + const acct = localAccounts.find(a => a.id === activeLocalAccountId); + ordered.push({ + key: activeLocalAccountId, + label: acct?.label || acct?.email || acct?.username || activeLocalAccountId, + calendars: byAccount.get(activeLocalAccountId)!, + }); + byAccount.delete(activeLocalAccountId); + } + // Then the rest in account-store order so the layout matches the mail sidebar. + for (const acct of localAccounts) { + if (!byAccount.has(acct.id)) continue; + ordered.push({ + key: acct.id, + label: acct.label || acct.email || acct.username, + calendars: byAccount.get(acct.id)!, + }); + byAccount.delete(acct.id); + } + // Any leftover buckets (deleted accounts, untagged calendars). + for (const [key, list] of byAccount.entries()) { + const fallbackLabel = key === '__other__' + ? t('my_calendars') + : list[0]?.accountName || key; + ordered.push({ key, label: fallbackLabel, calendars: list }); + } + return ordered; + }, [multiAccountMode, calendars, localAccounts, activeLocalAccountId, t]); + const getSubscriptionForCalendar = (calendarId: string) => { return icalSubscriptions.find(s => s.calendarId === calendarId); }; @@ -268,37 +343,89 @@ export function CalendarSidebarPanel({ )} )} -
- {onCreateCalendar ? ( - - ) : ( -

- {t('my_calendars')} -

- )} -
-
- {personalCalendars.map(renderCalendarItem)} -
- - {sharedAccountGroups.map((group) => ( -
-

- - {group.accountName} -

-
- {group.calendars.map(renderCalendarItem)} + {multiAccountMode && localAccountGroups.length > 0 ? ( + <> + {localAccountGroups.map((group, idx) => { + const expanded = !collapsedAccountGroups.has(group.key); + const isActive = group.key === activeLocalAccountId; + return ( +
+ + {expanded && ( +
+ {group.calendars.map(renderCalendarItem)} +
+ )} +
+ ); + })} + + ) : ( + <> +
+ {onCreateCalendar ? ( + + ) : ( +

+ {t('my_calendars')} +

+ )}
-
- ))} +
+ {personalCalendars.map(renderCalendarItem)} +
+ + {sharedAccountGroups.map((group) => ( +
+

+ + {group.accountName} +

+
+ {group.calendars.map(renderCalendarItem)} +
+
+ ))} + + )} {renderCalendarMenu()}
diff --git a/hooks/use-pro-multi-account-calendars.ts b/hooks/use-pro-multi-account-calendars.ts new file mode 100644 index 00000000..5db46ede --- /dev/null +++ b/hooks/use-pro-multi-account-calendars.ts @@ -0,0 +1,63 @@ +"use client"; + +import { useEffect, useMemo } from "react"; +import { useAccountStore } from "@/stores/account-store"; +import { useAuthStore } from "@/stores/auth-store"; +import { useCalendarStore, type CalendarAccountClient } from "@/stores/calendar-store"; +import { useSettingsStore } from "@/stores/settings-store"; +import { useIsEmbedded } from "@/hooks/use-is-embedded"; + +/** + * When the Pro shell is the active interface, aggregate calendars from + * every connected account so the calendar sidebar lists them all — the + * same way [[use-pro-multi-account-mailboxes]] does for mail folders. + * + * Returns the resolved list of `{ localAccountId, client }` pairs so the + * caller (calendar page) can fetch events the same way without + * re-deriving the set. + */ +export function useProMultiAccountCalendars(start: string | null, end: string | null): { + enabled: boolean; + accountClients: CalendarAccountClient[]; +} { + const isEmbedded = useIsEmbedded(); + const proInterface = useSettingsStore((s) => s.proInterface); + const accounts = useAccountStore((s) => s.accounts); + const activeAccountId = useAuthStore((s) => s.activeAccountId); + const fetchAllAccountsCalendars = useCalendarStore((s) => s.fetchAllAccountsCalendars); + const fetchAllAccountsEvents = useCalendarStore((s) => s.fetchAllAccountsEvents); + + const enabled = proInterface || isEmbedded; + + const accountClients = useMemo(() => { + if (!enabled) return []; + const getClientForAccount = useAuthStore.getState().getClientForAccount; + const pairs: CalendarAccountClient[] = []; + for (const account of accounts) { + if (!account.isConnected) continue; + const client = getClientForAccount(account.id); + if (!client || !client.supportsCalendars()) continue; + pairs.push({ localAccountId: account.id, client }); + } + return pairs; + // accounts identity changes whenever the connected set or login states + // change, so this is the only dependency we need. + }, [enabled, accounts]); + + // Fetch calendars whenever the set of connected calendar-capable accounts + // changes. Skips when there isn't an active account yet (auth still + // bootstrapping). + useEffect(() => { + if (!enabled || !activeAccountId || accountClients.length === 0) return; + void fetchAllAccountsCalendars(accountClients, activeAccountId); + }, [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); + }, [enabled, activeAccountId, accountClients, start, end, fetchAllAccountsEvents]); + + return { enabled, accountClients }; +} diff --git a/lib/jmap/types.ts b/lib/jmap/types.ts index 0bbbeb7b..fbde2cc1 100644 --- a/lib/jmap/types.ts +++ b/lib/jmap/types.ts @@ -446,6 +446,11 @@ export interface Calendar { accountId?: string; accountName?: string; isShared?: boolean; + // Local account-store ID (per JMAP server connection). Populated when the + // Pro shell aggregates calendars from multiple connected accounts so we + // can route mutations to the right client. Distinct from `accountId` + // which is the JMAP server's own account UUID. + localAccountId?: string; } export interface CalendarRights { @@ -467,6 +472,8 @@ export interface CalendarEvent { accountId?: string; accountName?: string; isShared?: boolean; + // See `Calendar.localAccountId` — same purpose for events. + localAccountId?: string; isDraft: boolean; isOrigin: boolean; utcStart: string | null; diff --git a/stores/calendar-store.ts b/stores/calendar-store.ts index f4162593..8d890f33 100644 --- a/stores/calendar-store.ts +++ b/stores/calendar-store.ts @@ -10,6 +10,32 @@ import { expandRecurringEvents } from '@/lib/recurrence-expansion'; import { generateUUID } from '@/lib/utils'; import { apiFetch } from '@/lib/browser-navigation'; import { BIRTHDAY_CALENDAR_ID } from '@/lib/birthday-calendar'; +import { useAuthStore } from './auth-store'; + +/** + * When the Pro shell aggregates calendars/events from every connected + * account, the entity carries a `localAccountId` pointing back to the + * owning JMAP client. Mutations need to use *that* client — the active + * client (passed in by the page) could be on a different server entirely. + * Falls back to the active client when `localAccountId` is unset or no + * matching client is registered. + */ +function resolveAccountClient(active: T, localAccountId?: string): T { + if (!localAccountId) return active; + const lookup = useAuthStore.getState().getClientForAccount(localAccountId) as T | undefined; + return lookup ?? active; +} + +/** + * Strip the local-account namespace prefix from an id (if present). Used + * before passing ids back to a JMAP client, since the prefix only exists + * to keep multi-account ids unique inside the client-side store. + */ +function stripLocalAccountPrefix(id: string, localAccountId?: string): string { + if (!localAccountId) return id; + const prefix = `${localAccountId}${CROSS_ACCOUNT_ID_DELIMITER}`; + return id.startsWith(prefix) ? id.slice(prefix.length) : id; +} // In-flight refresh dedup. Concurrent callers (auto-interval + // manual refresh, two account-switch reloads, etc.) share the same @@ -24,6 +50,61 @@ export function isCalendarViewMode(value: unknown): value is CalendarViewMode { return typeof value === 'string' && CALENDAR_VIEW_MODES.includes(value as 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. + */ +const CROSS_ACCOUNT_ID_DELIMITER = '::'; + +function buildCrossAccountIdPrefix(localAccountId: string): string { + return `${localAccountId}${CROSS_ACCOUNT_ID_DELIMITER}`; +} + +function prefixCalendarsWithLocalAccount( + calendars: Calendar[], + localAccountId: string, + isActiveAccount: boolean, +): Calendar[] { + if (isActiveAccount) { + return calendars.map((cal) => ({ ...cal, localAccountId })); + } + const prefix = buildCrossAccountIdPrefix(localAccountId); + return calendars.map((cal) => ({ + ...cal, + id: `${prefix}${cal.id}`, + localAccountId, + // Make sure other accounts' calendars surface under their own section in + // the sidebar. The sidebar groups "shared" calendars by account label; + // promoting them keeps them visually separate from the active account's + // own calendars without inventing new grouping logic. + isShared: true, + })); +} + +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}`, + localAccountId, + isShared: true, + calendarIds: event.calendarIds + ? Object.fromEntries( + Object.entries(event.calendarIds).map(([calId, v]) => [`${prefix}${calId}`, v]), + ) + : event.calendarIds, + })); +} + function mapCalendarIdsToStoreIds( calendarIds: Record | undefined, calendars: Calendar[], @@ -113,6 +194,17 @@ export interface ICalSubscription { lastRefreshed: string | null; } +/** + * One connected JMAP account. When the Pro shell aggregates calendars from + * every logged-in account, the page hands the calendar store a list of + * these so we can fetch + tag each account's data with its local app-store + * accountId (used to route mutations back to the right client). + */ +export interface CalendarAccountClient { + localAccountId: string; + client: IJMAPClient; +} + interface CalendarStore { calendars: Calendar[]; events: CalendarEvent[]; @@ -129,6 +221,8 @@ interface CalendarStore { setSupported: (supported: boolean) => void; fetchCalendars: (client: IJMAPClient) => Promise; fetchEvents: (client: IJMAPClient, start: string, end: string) => Promise; + fetchAllAccountsCalendars: (accounts: CalendarAccountClient[], activeLocalAccountId: string) => Promise; + fetchAllAccountsEvents: (accounts: CalendarAccountClient[], activeLocalAccountId: string, start: string, end: string) => Promise; createEvent: (client: IJMAPClient, event: Partial, sendSchedulingMessages?: boolean) => Promise; updateEvent: (client: IJMAPClient, id: string, updates: Partial, sendSchedulingMessages?: boolean) => Promise; deleteEvent: (client: IJMAPClient, id: string, sendSchedulingMessages?: boolean) => Promise; @@ -230,25 +324,92 @@ export const useCalendarStore = create()( } }, + fetchAllAccountsCalendars: async (accounts, activeLocalAccountId) => { + 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, + ); + } catch (error) { + debug.error(`Failed to fetch calendars for account ${localAccountId}:`, error); + return [] as Calendar[]; + } + }), + ); + 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); + set({ + calendars, + isLoading: false, + selectedCalendarIds: stillValid.length > 0 ? stillValid : validIds, + }); + } catch (error) { + debug.error('Failed to fetch all-account calendars:', error); + set({ error: 'Failed to load calendars', isLoading: false }); + } + }, + + fetchAllAccountsEvents: async (accounts, activeLocalAccountId, start, end) => { + set({ isLoadingEvents: true, error: null }); + try { + const results = await Promise.all( + accounts.map(async ({ client, localAccountId }) => { + try { + const raw = await client.queryAllCalendarEvents({ after: start, before: end }); + const valid = raw.filter(e => typeof e.start === 'string' && e.start); + const expanded = expandRecurringEvents(valid, start, end); + return prefixEventsWithLocalAccount( + expanded, + localAccountId, + localAccountId === activeLocalAccountId, + ); + } catch (error) { + debug.error(`Failed to fetch events for account ${localAccountId}:`, error); + return [] as CalendarEvent[]; + } + }), + ); + set({ events: results.flat(), isLoadingEvents: false, dateRange: { start, end } }); + } catch (error) { + debug.error('Failed to fetch all-account events:', error); + set({ error: 'Failed to load events', isLoadingEvents: false }); + } + }, + createEvent: async (client, event, sendSchedulingMessages) => { set({ error: null }); try { - // Resolve shared calendar context from calendarIds + // Resolve shared calendar context from calendarIds. Also pin the + // local account from the calendar so we route through that + // server's client when in multi-account Pro mode. let targetAccountId = event.accountId; + let localAccountId = event.localAccountId; const cleanEvent = sanitizeOutgoingCalendarEventData({ ...event }); if (event.calendarIds) { const remapped: Record = {}; for (const calId of Object.keys(event.calendarIds)) { const cal = get().calendars.find(c => c.id === calId); + if (cal?.localAccountId) localAccountId = cal.localAccountId; if (cal?.isShared && cal.originalId) { targetAccountId = cal.accountId; remapped[cal.originalId] = true; + } else if (cal?.originalId) { + remapped[cal.originalId] = true; } else { remapped[calId] = true; } } cleanEvent.calendarIds = remapped; } + client = resolveAccountClient(client, localAccountId); if (event.originalCalendarIds) { cleanEvent.calendarIds = event.originalCalendarIds; } @@ -321,8 +482,9 @@ export const useCalendarStore = create()( try { // Resolve shared event IDs and client-side expanded occurrence IDs const storeEvent = get().events.find(e => e.id === id); - const realId = storeEvent?.originalId || id; + const realId = storeEvent?.originalId || stripLocalAccountPrefix(id, storeEvent?.localAccountId); const targetAccountId = storeEvent?.accountId; + client = resolveAccountClient(client, storeEvent?.localAccountId); debug.log('calendar', 'Calendar updateEvent', { storeId: id, realId, @@ -404,8 +566,9 @@ export const useCalendarStore = create()( try { // Resolve shared event IDs and client-side expanded occurrence IDs const storeEvent = get().events.find(e => e.id === eventId); - const realId = storeEvent?.originalId || eventId; + const realId = storeEvent?.originalId || stripLocalAccountPrefix(eventId, storeEvent?.localAccountId); const targetAccountId = storeEvent?.accountId; + client = resolveAccountClient(client, storeEvent?.localAccountId); // Escape per RFC 6901 (JSON Pointer): ~ → ~0, / → ~1 const escapedId = participantId.replace(/~/g, '~0').replace(/\//g, '~1'); const patchKey = `participants/${escapedId}/participationStatus`; @@ -443,8 +606,9 @@ export const useCalendarStore = create()( importEvents: async (client, events, calendarId) => { // Resolve shared calendar IDs const cal = get().calendars.find(c => c.id === calendarId); - const realCalendarId = cal?.originalId || calendarId; + const realCalendarId = cal?.originalId || stripLocalAccountPrefix(calendarId, cal?.localAccountId); const targetAccountId = cal?.accountId; + client = resolveAccountClient(client, cal?.localAccountId); // Deduplicate UIDs: Stalwart enforces UID uniqueness across all calendars. // - Events already in the target calendar → skip (true duplicates) @@ -611,8 +775,9 @@ export const useCalendarStore = create()( try { // Resolve shared event IDs and client-side expanded occurrence IDs const storeEvent = get().events.find(e => e.id === id); - const realId = storeEvent?.originalId || id; + const realId = storeEvent?.originalId || stripLocalAccountPrefix(id, storeEvent?.localAccountId); const targetAccountId = storeEvent?.accountId; + client = resolveAccountClient(client, storeEvent?.localAccountId); if (sendSchedulingMessages) { try { const event = await client.getCalendarEvent(realId, targetAccountId); @@ -649,8 +814,9 @@ export const useCalendarStore = create()( set({ error: null }); try { const cal = get().calendars.find(c => c.id === calendarId); - const realId = cal?.originalId || calendarId; + const realId = cal?.originalId || stripLocalAccountPrefix(calendarId, cal?.localAccountId); const targetAccountId = cal?.accountId; + client = resolveAccountClient(client, cal?.localAccountId); await client.updateCalendar(realId, updates, targetAccountId); set((state) => ({ calendars: state.calendars.map(c => @@ -668,8 +834,9 @@ export const useCalendarStore = create()( set({ error: null }); try { const cal = get().calendars.find(c => c.id === calendarId); - const realId = cal?.originalId || calendarId; + const realId = cal?.originalId || stripLocalAccountPrefix(calendarId, cal?.localAccountId); const targetAccountId = cal?.accountId; + client = resolveAccountClient(client, cal?.localAccountId); await client.setCalendarShare(realId, principalId, rights, targetAccountId); set((state) => ({ calendars: state.calendars.map(c => { @@ -707,8 +874,9 @@ export const useCalendarStore = create()( set({ error: null }); try { const cal = get().calendars.find(c => c.id === calendarId); - const realId = cal?.originalId || calendarId; + const realId = cal?.originalId || stripLocalAccountPrefix(calendarId, cal?.localAccountId); const targetAccountId = cal?.accountId; + client = resolveAccountClient(client, cal?.localAccountId); await client.deleteCalendar(realId, targetAccountId); set((state) => ({ calendars: state.calendars.filter(c => c.id !== calendarId), @@ -726,8 +894,9 @@ export const useCalendarStore = create()( set({ error: null }); try { const cal = get().calendars.find(c => c.id === calendarId); - const realCalId = cal?.originalId || calendarId; + const realCalId = cal?.originalId || stripLocalAccountPrefix(calendarId, cal?.localAccountId); const targetAccountId = cal?.accountId; + client = resolveAccountClient(client, cal?.localAccountId); let totalRemoved = 0; // Loop to handle pagination (getCalendarEvents has a 1000 limit) let hasMore = true;