mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-10-31 00:12:39 +00:00
fix: keep the calendars shown or hidden in the Pro interface as the user set them
This commit is contained in:
@@ -124,6 +124,8 @@ export function CalendarApp({ linkSegments: routeSegments }: CalendarAppProps =
|
||||
const isMobile = useIsMobile();
|
||||
const isDesktop = useIsDesktop();
|
||||
const isEmbedded = useIsEmbedded();
|
||||
// The Pro interface takes over this route; the Pro tab loads the data then.
|
||||
const proInterfaceActive = useProInterfaceActive();
|
||||
// Mountain View's desktop chrome: its top bar in place of the left rail.
|
||||
const mvShell = useMountainViewShell(isEmbedded);
|
||||
// When the pane (Pro shell) or window is narrower than `lg`, the sidebar
|
||||
@@ -364,14 +366,17 @@ export function CalendarApp({ linkSegments: routeSegments }: CalendarAppProps =
|
||||
// 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.
|
||||
// active client's calendars only. That includes the standard instance
|
||||
// that renders while the Pro interface takes over the route: its raw ids
|
||||
// replaced the merged list after the Pro tab loaded it, and the selection
|
||||
// the user made in between went to another account's calendar.
|
||||
useEffect(() => {
|
||||
if (isEmbedded) return;
|
||||
if (isEmbedded || proInterfaceActive) return;
|
||||
if (client && !hasFetched.current) {
|
||||
hasFetched.current = true;
|
||||
fetchCalendars(client);
|
||||
}
|
||||
}, [client, fetchCalendars, isEmbedded]);
|
||||
}, [client, fetchCalendars, isEmbedded, proInterfaceActive]);
|
||||
|
||||
// Auto-refresh iCal subscriptions
|
||||
useEffect(() => {
|
||||
@@ -496,11 +501,11 @@ export function CalendarApp({ linkSegments: routeSegments }: CalendarAppProps =
|
||||
}, [client, enableCalendarTasks, normalizedViewMode, showTasksOnCalendar, fetchTasksFn]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isEmbedded) return;
|
||||
if (isEmbedded || proInterfaceActive) return;
|
||||
if (client && calendars.length > 0 && dateRange) {
|
||||
fetchEvents(client, dateRange.start, dateRange.end);
|
||||
}
|
||||
}, [client, calendars.length, dateRange, fetchEvents, isEmbedded]);
|
||||
}, [client, calendars.length, dateRange, fetchEvents, isEmbedded, proInterfaceActive]);
|
||||
|
||||
// Pro shell only: aggregate calendars and events from every connected
|
||||
// account so the sidebar lists them all (and the views render their
|
||||
@@ -851,7 +856,6 @@ export function CalendarApp({ linkSegments: routeSegments }: CalendarAppProps =
|
||||
// The permalink for what the calendar is currently showing. In the Pro
|
||||
// shell only the focused tab writes the address bar; the standard instance
|
||||
// that renders while Pro takes over a route stays silent.
|
||||
const proInterfaceActive = useProInterfaceActive();
|
||||
const isFocusedProTab = useIsFocusedProTab();
|
||||
const isPaneScoped = useIsPaneScoped();
|
||||
const eventLinkId = showEventModal && editEvent ? editEvent.id : null;
|
||||
@@ -933,7 +937,7 @@ export function CalendarApp({ linkSegments: routeSegments }: CalendarAppProps =
|
||||
onRefresh: async () => {
|
||||
if (!client) return;
|
||||
const calendarRefresh = multiAccountEnabled && accountClients.length > 0 && activeAccountId
|
||||
? fetchAllAccountsCalendarsFn(accountClients)
|
||||
? fetchAllAccountsCalendarsFn(accountClients, activeAccountId)
|
||||
: fetchCalendars(client);
|
||||
await Promise.all([
|
||||
calendarRefresh,
|
||||
|
||||
@@ -49,7 +49,7 @@ export function useProMultiAccountCalendars(start: string | null, end: string |
|
||||
// bootstrapping).
|
||||
useEffect(() => {
|
||||
if (!enabled || !activeAccountId || accountClients.length === 0) return;
|
||||
void fetchAllAccountsCalendars(accountClients);
|
||||
void fetchAllAccountsCalendars(accountClients, activeAccountId);
|
||||
}, [enabled, activeAccountId, accountClients, fetchAllAccountsCalendars]);
|
||||
|
||||
// Fetch events for the current visible date range across all accounts.
|
||||
|
||||
@@ -115,3 +115,21 @@ describe('reconcileSelectedIds with unloaded accounts', () => {
|
||||
expect(reconcileSelectedIds(['B@h::b'], calendars)).toEqual(['A@h::b']);
|
||||
});
|
||||
});
|
||||
|
||||
// Stalwart gives every account's default calendar the same id ("b"). The
|
||||
// raw ids a single-account load selected are the active login's; turning to
|
||||
// the merged list mapped them onto whichever account sorted first, so the
|
||||
// active account's calendars went hidden and another's showed.
|
||||
describe('raw selected ids when the merged list loads', () => {
|
||||
it("map onto the active login's calendars", async () => {
|
||||
useCalendarStore.setState({ calendars: [], selectedCalendarIds: ['b', 'ca:b'] });
|
||||
const list = () => clientReturning({ calendars: [cal({ id: 'b', accountId: 'x' }), cal({ id: 'ca:b', accountId: 'ca', isShared: true })] });
|
||||
|
||||
await useCalendarStore.getState().fetchAllAccountsCalendars(
|
||||
[{ localAccountId: 'A@h', client: list() }, { localAccountId: 'B@h', client: list() }],
|
||||
'B@h',
|
||||
);
|
||||
|
||||
expect(useCalendarStore.getState().selectedCalendarIds).toEqual(['B@h::b', 'B@h::ca:b']);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -487,12 +487,15 @@ async function refetchAfterOccurrenceMutation(): Promise<void> {
|
||||
* calendar is the same. Remap by raw identity so the selection survives instead
|
||||
* of silently resetting to "all". Keeps BIRTHDAY_CALENDAR_ID and ids starting
|
||||
* with one of `unloadedPrefixes` (calendars of an account that failed to load
|
||||
* are unknown, not deleted); drops ids that resolve to no calendar.
|
||||
* are unknown, not deleted); drops ids that resolve to no calendar. A raw id
|
||||
* names a calendar of `rawOwner` (the login a single-account load was for)
|
||||
* when it has one: raw ids repeat across accounts.
|
||||
*/
|
||||
export function reconcileSelectedIds(
|
||||
selectedCalendarIds: string[],
|
||||
calendars: Calendar[],
|
||||
unloadedPrefixes: string[] = [],
|
||||
rawOwner?: string,
|
||||
): string[] {
|
||||
const byStoreId = new Set(calendars.map((c) => c.id));
|
||||
const rawToStoreId = new Map<string, string>();
|
||||
@@ -507,6 +510,9 @@ export function reconcileSelectedIds(
|
||||
if (id === BIRTHDAY_CALENDAR_ID || byStoreId.has(id)) mapped = id;
|
||||
// Before the raw remap: another account may hold the same raw id.
|
||||
else if (unloadedPrefixes.some((prefix) => id.startsWith(prefix))) mapped = id;
|
||||
else if (rawOwner && !id.includes(CROSS_ACCOUNT_ID_DELIMITER) && byStoreId.has(buildCrossAccountIdPrefix(rawOwner) + id)) {
|
||||
mapped = buildCrossAccountIdPrefix(rawOwner) + id;
|
||||
}
|
||||
else mapped = rawToStoreId.get(rawIdentityOf(id));
|
||||
if (mapped && !seen.has(mapped)) {
|
||||
out.push(mapped);
|
||||
@@ -527,8 +533,9 @@ function selectionAfterFetch(
|
||||
calendars: Calendar[],
|
||||
defaultSelected: string[],
|
||||
unloadedPrefixes?: string[],
|
||||
rawOwner?: string,
|
||||
): string[] {
|
||||
const reconciled = reconcileSelectedIds(selectedCalendarIds, calendars, unloadedPrefixes);
|
||||
const reconciled = reconcileSelectedIds(selectedCalendarIds, calendars, unloadedPrefixes, rawOwner);
|
||||
if (reconciled.some((id) => id !== BIRTHDAY_CALENDAR_ID)) return reconciled;
|
||||
return reconciled.includes(BIRTHDAY_CALENDAR_ID) ? [...defaultSelected, BIRTHDAY_CALENDAR_ID] : defaultSelected;
|
||||
}
|
||||
@@ -785,7 +792,8 @@ interface CalendarStore {
|
||||
setSupported: (supported: boolean) => void;
|
||||
fetchCalendars: (client: IJMAPClient) => Promise<void>;
|
||||
fetchEvents: (client: IJMAPClient, start: string, end: string, options?: FetchEventsOptions) => Promise<void>;
|
||||
fetchAllAccountsCalendars: (accounts: CalendarAccountClient[]) => Promise<void>;
|
||||
/** `activeLocalAccountId`: the login whose calendars un-namespaced selected ids name. */
|
||||
fetchAllAccountsCalendars: (accounts: CalendarAccountClient[], activeLocalAccountId?: string | null) => Promise<void>;
|
||||
fetchAllAccountsEvents: (accounts: CalendarAccountClient[], start: string, end: string, options?: FetchEventsOptions) => Promise<void>;
|
||||
createEvent: (client: IJMAPClient, event: Partial<CalendarEvent>, sendSchedulingMessages?: boolean) => Promise<CalendarEvent | null>;
|
||||
updateEvent: (
|
||||
@@ -975,7 +983,7 @@ export const useCalendarStore = create<CalendarStore>()(
|
||||
}
|
||||
},
|
||||
|
||||
fetchAllAccountsCalendars: async (accounts) => {
|
||||
fetchAllAccountsCalendars: async (accounts, activeLocalAccountId) => {
|
||||
const epoch = currentStoreEpoch();
|
||||
set({ isLoading: true, error: null });
|
||||
try {
|
||||
@@ -1005,6 +1013,7 @@ export const useCalendarStore = create<CalendarStore>()(
|
||||
calendars,
|
||||
calendars.map(c => c.id),
|
||||
unloadedPrefixes,
|
||||
activeLocalAccountId ?? undefined,
|
||||
),
|
||||
});
|
||||
} catch (error) {
|
||||
|
||||
Reference in New Issue
Block a user