mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-11-03 08:15:38 +00:00
fix: show a calendar that appears after the first load instead of leaving it unticked #1372
This commit is contained in:
@@ -71,6 +71,7 @@ export function snapshotAccount(accountId: string): void {
|
||||
events: [...calendarState.events],
|
||||
selectedCalendarIds: [...calendarState.selectedCalendarIds],
|
||||
calendarSelectionChosen: calendarState.calendarSelectionChosen,
|
||||
knownCalendarIds: calendarState.knownCalendarIds,
|
||||
viewMode: calendarState.viewMode,
|
||||
supportsCalendar: calendarState.supportsCalendar,
|
||||
},
|
||||
|
||||
@@ -164,7 +164,7 @@ describe('reconcileSelectedIds with unloaded accounts', () => {
|
||||
// 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'] });
|
||||
useCalendarStore.setState({ calendars: [], selectedCalendarIds: ['b', 'ca:b'], knownCalendarIds: null });
|
||||
const list = () => clientReturning({ calendars: [cal({ id: 'b', accountId: 'x' }), cal({ id: 'ca:b', accountId: 'ca', isShared: true })] });
|
||||
|
||||
await useCalendarStore.getState().fetchAllAccountsCalendars(
|
||||
@@ -175,3 +175,66 @@ describe('raw selected ids when the merged list loads', () => {
|
||||
expect(useCalendarStore.getState().selectedCalendarIds).toEqual(['B@h::b', 'B@h::ca:b']);
|
||||
});
|
||||
});
|
||||
|
||||
// A calendar shared with the user, or made in another client, after the list
|
||||
// loaded once started hidden: the kept selection never named it (#1372).
|
||||
describe('a calendar that appears after the first load', () => {
|
||||
const shared = cal({ id: 'team:x', accountId: 'team', isShared: true });
|
||||
|
||||
beforeEach(() => {
|
||||
useCalendarStore.setState({ calendars: [], selectedCalendarIds: [], calendarSelectionChosen: false, knownCalendarIds: null, error: null });
|
||||
});
|
||||
|
||||
it('starts shown, while one the user hid stays hidden', async () => {
|
||||
await useCalendarStore.getState().fetchCalendars(clientReturning({ calendars: own }));
|
||||
useCalendarStore.getState().toggleCalendarVisibility('b');
|
||||
|
||||
await useCalendarStore.getState().fetchCalendars(clientReturning({ calendars: [...own, shared] }));
|
||||
|
||||
expect(useCalendarStore.getState().selectedCalendarIds).toEqual(['a', 'team:x']);
|
||||
});
|
||||
|
||||
it('starts shown after the user hid every calendar', async () => {
|
||||
await useCalendarStore.getState().fetchCalendars(clientReturning({ calendars: own }));
|
||||
useCalendarStore.getState().toggleCalendarVisibility('a');
|
||||
useCalendarStore.getState().toggleCalendarVisibility('b');
|
||||
|
||||
await useCalendarStore.getState().fetchCalendars(clientReturning({ calendars: [...own, shared] }));
|
||||
|
||||
expect(useCalendarStore.getState().selectedCalendarIds).toEqual(['team:x']);
|
||||
});
|
||||
|
||||
it('stays hidden when the server says it is not visible, or it holds only tasks', async () => {
|
||||
await useCalendarStore.getState().fetchCalendars(clientReturning({ calendars: own }));
|
||||
|
||||
await useCalendarStore.getState().fetchCalendars(clientReturning({ calendars: [
|
||||
...own,
|
||||
cal({ id: 'team:x', accountId: 'team', isShared: true, isVisible: false }),
|
||||
cal({ id: 'todo', accountId: 'me', isTasksOnly: true }),
|
||||
] }));
|
||||
|
||||
expect(useCalendarStore.getState().selectedCalendarIds).toEqual(['a', 'b']);
|
||||
});
|
||||
|
||||
it('leaves a selection from before the calendars were recorded as it is', async () => {
|
||||
// Saved by a version that did not record the calendars it had seen: every
|
||||
// calendar not in the selection may be one the user hid.
|
||||
useCalendarStore.setState({ selectedCalendarIds: ['a'], knownCalendarIds: null });
|
||||
|
||||
await useCalendarStore.getState().fetchCalendars(clientReturning({ calendars: [...own, shared] }));
|
||||
expect(useCalendarStore.getState().selectedCalendarIds).toEqual(['a']);
|
||||
|
||||
await useCalendarStore.getState().fetchCalendars(clientReturning({ calendars: [...own, shared, cal({ id: 'c', accountId: 'me' })] }));
|
||||
expect(useCalendarStore.getState().selectedCalendarIds).toEqual(['a', 'c']);
|
||||
});
|
||||
|
||||
it('starts shown in the merged list of several logins', async () => {
|
||||
const list = (calendars: Calendar[]) => [{ localAccountId: 'A@h', client: clientReturning({ calendars }) }];
|
||||
await useCalendarStore.getState().fetchAllAccountsCalendars(list(own), 'A@h');
|
||||
useCalendarStore.getState().toggleCalendarVisibility('A@h::b');
|
||||
|
||||
await useCalendarStore.getState().fetchAllAccountsCalendars(list([...own, shared]), 'A@h');
|
||||
|
||||
expect(useCalendarStore.getState().selectedCalendarIds).toEqual(['A@h::a', 'A@h::team:x']);
|
||||
});
|
||||
});
|
||||
|
||||
+62
-10
@@ -544,6 +544,35 @@ function selectionAfterFetch(
|
||||
return reconciled.includes(BIRTHDAY_CALENDAR_ID) ? [...defaultSelected, BIRTHDAY_CALENDAR_ID] : defaultSelected;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the calendars that appeared since the last fetch, such as one just
|
||||
* shared with the user or made in another client (#1372). They start shown
|
||||
* when the default selection would hold them and the server doesn't say
|
||||
* isVisible: false; a calendar the user hid is known and stays hidden.
|
||||
* Nothing recorded yet (`known` null) counts every calendar as known.
|
||||
* Returns the selection and the calendars known from now on.
|
||||
*/
|
||||
function withNewCalendars(
|
||||
selection: string[],
|
||||
known: string[] | null,
|
||||
calendars: Calendar[],
|
||||
defaultSelected: string[],
|
||||
unloadedPrefixes?: string[],
|
||||
rawOwner?: string,
|
||||
): { selectedCalendarIds: string[]; knownCalendarIds: string[] } {
|
||||
const ids = calendars.map((c) => c.id);
|
||||
if (!known) return { selectedCalendarIds: selection, knownCalendarIds: ids };
|
||||
const seen = new Set(reconcileSelectedIds(known, calendars, unloadedPrefixes, rawOwner));
|
||||
const eligible = new Set(defaultSelected);
|
||||
const added = calendars
|
||||
.filter((c) => !seen.has(c.id) && eligible.has(c.id) && c.isVisible !== false && !selection.includes(c.id))
|
||||
.map((c) => c.id);
|
||||
return {
|
||||
selectedCalendarIds: added.length > 0 ? [...selection, ...added] : selection,
|
||||
knownCalendarIds: [...new Set([...seen, ...ids])].filter((id) => id !== BIRTHDAY_CALENDAR_ID),
|
||||
};
|
||||
}
|
||||
|
||||
// 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.
|
||||
@@ -776,6 +805,8 @@ interface CalendarStore {
|
||||
selectedCalendarIds: string[];
|
||||
/** The user picked the visible calendars by hand: an empty pick hides them all. */
|
||||
calendarSelectionChosen: boolean;
|
||||
/** Calendars the last fetch returned: one not in it is new and starts shown (#1372). Null before the first record. */
|
||||
knownCalendarIds: string[] | null;
|
||||
selectedEventId: string | null;
|
||||
isLoading: boolean;
|
||||
isLoadingEvents: boolean;
|
||||
@@ -864,6 +895,7 @@ const initialState = {
|
||||
selectedDate: displayNow(),
|
||||
selectedCalendarIds: [] as string[],
|
||||
calendarSelectionChosen: false,
|
||||
knownCalendarIds: null as string[] | null,
|
||||
selectedEventId: null as string | null,
|
||||
isLoading: false,
|
||||
isLoadingEvents: false,
|
||||
@@ -926,17 +958,24 @@ export const useCalendarStore = create<CalendarStore>()(
|
||||
// Default the visible selection to event calendars only - tasks-only
|
||||
// calendars stay out of the event grid.
|
||||
const defaultSelected = calendars.filter(c => !c.isTasksOnly).map(c => c.id);
|
||||
// Shared calendars' ids are `<accountId>:<id>` (getAllCalendars).
|
||||
const unloadedPrefixes = failedAccountIds.map((accountId) => `${accountId}:`);
|
||||
set({
|
||||
calendars,
|
||||
isLoading: false,
|
||||
selectedCalendarIds: selectionAfterFetch(
|
||||
get().selectedCalendarIds,
|
||||
...withNewCalendars(
|
||||
selectionAfterFetch(
|
||||
get().selectedCalendarIds,
|
||||
calendars,
|
||||
defaultSelected,
|
||||
unloadedPrefixes,
|
||||
undefined,
|
||||
get().calendarSelectionChosen,
|
||||
),
|
||||
get().knownCalendarIds,
|
||||
calendars,
|
||||
defaultSelected,
|
||||
// Shared calendars' ids are `<accountId>:<id>` (getAllCalendars).
|
||||
failedAccountIds.map((accountId) => `${accountId}:`),
|
||||
undefined,
|
||||
get().calendarSelectionChosen,
|
||||
unloadedPrefixes,
|
||||
),
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -1014,16 +1053,24 @@ export const useCalendarStore = create<CalendarStore>()(
|
||||
);
|
||||
if (currentStoreEpoch() !== epoch) return;
|
||||
const calendars = sortCalendars(results.flat());
|
||||
const defaultSelected = calendars.map(c => c.id);
|
||||
set({
|
||||
calendars,
|
||||
isLoading: false,
|
||||
selectedCalendarIds: selectionAfterFetch(
|
||||
get().selectedCalendarIds,
|
||||
...withNewCalendars(
|
||||
selectionAfterFetch(
|
||||
get().selectedCalendarIds,
|
||||
calendars,
|
||||
defaultSelected,
|
||||
unloadedPrefixes,
|
||||
activeLocalAccountId ?? undefined,
|
||||
get().calendarSelectionChosen,
|
||||
),
|
||||
get().knownCalendarIds,
|
||||
calendars,
|
||||
calendars.map(c => c.id),
|
||||
defaultSelected,
|
||||
unloadedPrefixes,
|
||||
activeLocalAccountId ?? undefined,
|
||||
get().calendarSelectionChosen,
|
||||
),
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -1665,6 +1712,10 @@ export const useCalendarStore = create<CalendarStore>()(
|
||||
: state.selectedCalendarIds.filter(id => id !== calendarId),
|
||||
// Showing birthdays is a setting, not a pick of calendars.
|
||||
calendarSelectionChosen: state.calendarSelectionChosen || calendarId !== BIRTHDAY_CALENDAR_ID,
|
||||
// A calendar hidden before the next fetch recorded it stays hidden.
|
||||
knownCalendarIds: state.knownCalendarIds && !state.knownCalendarIds.includes(calendarId) && calendarId !== BIRTHDAY_CALENDAR_ID
|
||||
? [...state.knownCalendarIds, calendarId]
|
||||
: state.knownCalendarIds,
|
||||
}));
|
||||
void calendarHooks.onCalendarVisibilityToggle.emit({ calendarId, visible });
|
||||
},
|
||||
@@ -2073,6 +2124,7 @@ export const useCalendarStore = create<CalendarStore>()(
|
||||
partialize: (state) => ({
|
||||
selectedCalendarIds: state.selectedCalendarIds,
|
||||
calendarSelectionChosen: state.calendarSelectionChosen,
|
||||
knownCalendarIds: state.knownCalendarIds,
|
||||
viewMode: state.viewMode,
|
||||
icalSubscriptions: state.icalSubscriptions,
|
||||
deletedSubscriptionIds: state.deletedSubscriptionIds,
|
||||
|
||||
Reference in New Issue
Block a user