1 import { createNextState } from '@reduxjs/toolkit';
3 import { selectAddresses } from '@proton/account/addresses';
4 import { serverEvent } from '@proton/account/eventLoop';
7 getIsCalendarEventManagerCreate,
8 getIsCalendarEventManagerDelete,
9 getIsCalendarEventManagerUpdate,
10 getIsCalendarMemberEventManagerCreate,
11 getIsCalendarMemberEventManagerDelete,
12 getIsCalendarMemberEventManagerUpdate,
13 } from '@proton/shared/lib/eventManager/calendar/helpers';
16 type CalendarsBootstrapState,
17 calendarsBootstrapActions,
18 findCalendarBootstrapID,
19 selectCalendarsBootstrap,
20 } from '../calendarBootstrap';
21 import { deleteCalendarFromKeyCache } from '../calendarBootstrap/keys';
22 import { calendarServerEvent } from '../calendarServerEvent';
23 import type { CalendarStartListening } from '../interface';
24 import type { CalendarsState } from './index';
25 import { calendarsActions, selectCalendars } from './index';
27 // This calendar event listener needs to access the global state to get the addresses, so it's a listener and not a reducer.
28 export const startCalendarEventListener = (
29 startListening: CalendarStartListening<CalendarsState & CalendarsBootstrapState>
32 actionCreator: serverEvent,
33 effect: async (action, listenerApi) => {
34 const updateMembers = action.payload.CalendarMembers;
35 if (updateMembers?.length) {
36 updateMembers.forEach((event) => {
37 listenerApi.dispatch(calendarsBootstrapActions.memberEvent({ event }));
41 const updateCalendars = action.payload.Calendars;
42 if (updateCalendars?.length) {
43 updateCalendars.forEach((event) => {
44 if (getIsCalendarEventManagerDelete(event)) {
45 const calendarID = event.ID;
46 listenerApi.dispatch(calendarsBootstrapActions.remove({ id: calendarID }));
47 deleteCalendarFromKeyCache(calendarID);
55 actionCreator: calendarServerEvent,
56 effect: async (action, listenerApi) => {
57 const calendarKeysUpdate = action.payload.CalendarKeys;
58 if (calendarKeysUpdate?.length) {
59 const deleteCalendarFromCache = (calendarID: string) => {
60 listenerApi.dispatch(calendarsBootstrapActions.remove({ id: calendarID }));
61 deleteCalendarFromKeyCache(calendarID);
63 const state = selectCalendarsBootstrap(listenerApi.getState());
64 calendarKeysUpdate.forEach(({ ID: KeyID, Key }) => {
65 // When a new calendar key is received, the entire calendar cache is invalidated.
66 // TODO: Merge the bootstrapped version.
67 if (Key && Key.CalendarID) {
68 deleteCalendarFromCache(Key.CalendarID);
71 const calendarID = findCalendarBootstrapID(state, ({ Keys }) => {
72 return Boolean(Array.isArray(Keys) && Keys.find(({ ID: otherID }) => otherID === KeyID));
75 deleteCalendarFromCache(calendarID);
83 actionCreator: serverEvent,
84 effect: async (action, listenerApi) => {
85 if (!action.payload.Calendars && !action.payload.CalendarMembers) {
89 const state = listenerApi.getState();
90 const currentCalendarsWithMembers = selectCalendars(state).value;
91 if (!currentCalendarsWithMembers) {
95 const updateCalendars = action.payload.Calendars;
96 const updateMembers = action.payload.CalendarMembers;
97 const calendarEventModelManager = listenerApi.extra.calendarModelEventManager;
99 const oldCalendarsWithMembers = [...currentCalendarsWithMembers];
100 const newCalendarsWithMembers = createNextState(oldCalendarsWithMembers, (newCalendarsWithMembers) => {
101 if (updateCalendars?.length) {
102 for (const event of updateCalendars) {
103 if (getIsCalendarEventManagerDelete(event)) {
104 const index = newCalendarsWithMembers.findIndex(({ ID }) => ID === event.ID);
106 newCalendarsWithMembers.splice(index, 1);
108 calendarEventModelManager.reset([event.ID]);
109 } else if (getIsCalendarEventManagerCreate(event)) {
110 const { ID: calendarID, Calendar } = event;
111 const index = newCalendarsWithMembers.findIndex(({ ID }) => ID === calendarID);
113 // The calendar already exists for a creation event. Ignore it.
116 newCalendarsWithMembers.push({ ...Calendar });
117 } else if (getIsCalendarEventManagerUpdate(event)) {
118 const { ID: calendarID, Calendar } = event;
119 const index = newCalendarsWithMembers.findIndex(({ ID }) => ID === calendarID);
121 // update only the calendar part. Members updated below if needed
122 const oldCalendarWithMembers = oldCalendarsWithMembers[index];
123 newCalendarsWithMembers.splice(index, 1, { ...oldCalendarWithMembers, ...Calendar });
129 if (updateMembers?.length) {
130 const ownAddressIDs = (selectAddresses(state)?.value || []).map(({ ID }) => ID);
132 for (const event of updateMembers) {
133 if (getIsCalendarMemberEventManagerDelete(event)) {
134 const [calendarIndex, memberIndex] = findMemberIndices(event.ID, newCalendarsWithMembers);
135 if (calendarIndex !== -1 && memberIndex !== -1) {
136 const { CalendarID, AddressID } =
137 newCalendarsWithMembers[calendarIndex].Members[memberIndex]!;
138 if (ownAddressIDs.includes(AddressID)) {
139 // the user is the member removed -> remove the calendar
140 newCalendarsWithMembers.splice(calendarIndex, 1);
141 calendarEventModelManager.reset([CalendarID]);
143 // otherwise a member of one of an owned calendar got removed -> remove the member
144 newCalendarsWithMembers[calendarIndex].Members.splice(memberIndex, 1);
148 const [calendarIndex, memberIndex] = findMemberIndices(
150 newCalendarsWithMembers,
151 event.Member.CalendarID
153 // If the targeted calendar cannot be found, ignore this update. It will be dealt with when the calendar update happens.
154 if (calendarIndex === -1) {
157 if (getIsCalendarMemberEventManagerCreate(event)) {
158 if (memberIndex !== -1) {
161 newCalendarsWithMembers[calendarIndex].Members.push(event.Member);
162 } else if (getIsCalendarMemberEventManagerUpdate(event)) {
163 if (memberIndex === -1) {
166 newCalendarsWithMembers[calendarIndex].Members.splice(memberIndex, 1, event.Member);
173 if (newCalendarsWithMembers !== oldCalendarsWithMembers) {
174 listenerApi.dispatch(calendarsActions.updateCalendars(newCalendarsWithMembers));