Update all non-major dependencies
[ProtonMail-WebClient.git] / applications / calendar / src / app / store / events / eventsSlice.ts
blob80ae6f3ac37470cf94b0a585fb057ee5561c1c39
1 import { type PayloadAction, createSlice } from '@reduxjs/toolkit';
2 import cloneDeep from 'lodash/cloneDeep';
4 import { ICAL_ATTENDEE_STATUS, RECURRING_TYPES, TMP_UID, TMP_UNIQUE_ID } from '@proton/shared/lib/calendar/constants';
5 import { getHasRecurrenceId } from '@proton/shared/lib/calendar/vcalHelper';
6 import type { CalendarEvent } from '@proton/shared/lib/interfaces/calendar';
8 import type { CalendarViewEvent } from '../../containers/calendar/interface';
9 import { getEventReadResult, setEventReadResult, setPartstat } from './eventsCache';
11 export interface CalendarViewEventStore extends CalendarViewEvent {
12     isDeleted?: boolean;
13     isSaved?: boolean;
14     isSaving?: boolean;
15     isDeleting?: boolean;
18 interface EventsState {
19     events: CalendarViewEventStore[];
20     isTmpEventSaving: boolean;
23 const initialState: EventsState = {
24     events: [],
25     isTmpEventSaving: false,
28 export const eventsSliceName = 'events';
30 const slice = createSlice({
31     name: eventsSliceName,
32     initialState,
33     reducers: {
34         synchronizeEvents(state, action: PayloadAction<CalendarViewEvent[]>) {
35             const oldEventsMap = new Map<string, CalendarViewEventStore>(
36                 state.events.map((event) => [event.uniqueId, event])
37             );
39             state.events = action.payload.map((event) => {
40                 const oldEvent = oldEventsMap.get(event.uniqueId);
41                 const { eventReadResult, calendarData, ...restData } = event.data;
43                 // Store eventReadResult in cache if it exists
44                 if (eventReadResult) {
45                     setEventReadResult(event.uniqueId, eventReadResult);
46                 }
48                 // State is immutable but if we add the event as is in Redux, the payload content will also become
49                 // immutable. This is causing issues in other places in the app where we are using the same object
50                 // to do some computations.
51                 // To avoid this, we need to deep copy the object so that we don't mess up the references
52                 const tmpEvent = {
53                     ...event,
54                     data: restData, // Omit eventReadResult && calendarData
55                 };
57                 const eventCopy = cloneDeep(tmpEvent);
59                 // Return the updated event, preserving some old event properties
60                 return {
61                     ...eventCopy,
62                     isDeleted: oldEvent?.isDeleted,
63                     isSaved: oldEvent?.isSaved,
64                     isSaving: oldEvent?.isSaving,
65                     isDeleting: oldEvent?.isDeleting,
66                 } as CalendarViewEventStore;
67             });
68         },
69         markAsDeleted(
70             state,
71             action: PayloadAction<{
72                 targetEvent: CalendarViewEvent;
73                 isDeleted: boolean;
74                 recurringType?: RECURRING_TYPES;
75             }>
76         ) {
77             const { targetEvent, isDeleted, recurringType } = action.payload;
78             const { uniqueId: targetUniqueId } = targetEvent;
79             const targetUID = (targetEvent.data?.eventData as CalendarEvent)?.UID;
81             if (recurringType === RECURRING_TYPES.SINGLE) {
82                 // Find and update only the target event (single edit case included)
83                 const event = state.events.find((event) => {
84                     return event.uniqueId === targetUniqueId;
85                 });
87                 if (event) {
88                     event.isDeleted = isDeleted;
89                 }
90             } else if (recurringType === RECURRING_TYPES.ALL) {
91                 let needRefresh = true;
92                 // Update all instances of the recurring event
93                 state.events.forEach((event) => {
94                     const UID = (event.data?.eventData as CalendarEvent)?.UID;
96                     if (UID && UID === targetUID) {
97                         const eventReadResult = getEventReadResult(event.uniqueId);
98                         const { isAttendee, selfAttendee } = eventReadResult?.result?.[0]?.selfAddressData || {};
99                         const isSingleEdit = getHasRecurrenceId(eventReadResult?.result?.[0]?.veventComponent);
101                         // When deleting a recurring series as an attendee, single are not deleted.
102                         if (isAttendee && isSingleEdit) {
103                             // If the user responded to the single edit invite with YES or MAYBE (ACCEPTED OR TENTATIVE),
104                             // the user participation status is being reset
105                             if (
106                                 selfAttendee?.parameters?.partstat === ICAL_ATTENDEE_STATUS.ACCEPTED ||
107                                 selfAttendee?.parameters?.partstat === ICAL_ATTENDEE_STATUS.TENTATIVE
108                             ) {
109                                 eventReadResult?.result?.[0]?.veventComponent.attendee?.forEach((attendee) => {
110                                     if (attendee.value === selfAttendee?.value) {
111                                         if (attendee.parameters) {
112                                             attendee.parameters.partstat = ICAL_ATTENDEE_STATUS.NEEDS_ACTION;
113                                         }
114                                     }
115                                 });
116                             }
117                             // Do not mark event as deleted
118                             setEventReadResult(event.uniqueId, eventReadResult);
119                             return;
120                         }
122                         event.isDeleted = isDeleted;
123                         needRefresh = false;
124                     }
125                 });
127                 if (needRefresh) {
128                     state.events = [...state.events];
129                 }
130             } else if (recurringType === RECURRING_TYPES.FUTURE) {
131                 // Update all future instances of the recurring event
132                 state.events.forEach((event) => {
133                     if (event.start >= targetEvent.start) {
134                         const UID = (event.data?.eventData as CalendarEvent)?.UID;
136                         if (UID && UID === targetUID) {
137                             event.isDeleted = isDeleted;
138                         }
139                     }
140                 });
141             }
142         },
143         markEventAsDeleting(
144             state,
145             action: PayloadAction<{
146                 isDeleting: boolean;
147                 targetEvent: CalendarViewEvent;
148                 recurringType?: RECURRING_TYPES;
149             }>
150         ) {
151             const { isDeleting, targetEvent, recurringType } = action.payload;
152             const { uniqueId: targetUniqueId } = targetEvent;
153             const targetUID = (targetEvent.data?.eventData as CalendarEvent)?.UID;
155             state.events.forEach((event) => {
156                 const UID = (event.data?.eventData as CalendarEvent)?.UID;
158                 if (UID === targetUID) {
159                     /**
160                      * Mark the events that actually needs to be deleted as deleting
161                      */
162                     if (recurringType === RECURRING_TYPES.SINGLE) {
163                         if (event.uniqueId === targetUniqueId) {
164                             event.isDeleting = isDeleting;
165                         }
166                     } else if (recurringType === RECURRING_TYPES.ALL) {
167                         const eventReadResult = getEventReadResult(event.uniqueId);
168                         const { isAttendee } = eventReadResult?.result?.[0]?.selfAddressData || {};
169                         const isSingleEdit = getHasRecurrenceId(eventReadResult?.result?.[0]?.veventComponent);
171                         // Single edits are not deleted, but we will reset their partstat
172                         if (isAttendee && isSingleEdit) {
173                             return;
174                         }
176                         event.isDeleting = isDeleting;
177                     } else if (recurringType === RECURRING_TYPES.FUTURE) {
178                         if (event.start >= targetEvent.start) {
179                             const UID = (event.data?.eventData as CalendarEvent)?.UID;
181                             if (UID && UID === targetUID) {
182                                 event.isDeleting = isDeleting;
183                             }
184                         }
185                     }
186                 }
187             });
188         },
189         markEventAsSaving(state, action: PayloadAction<{ uniqueId: string; isSaving: boolean }>) {
190             // tmp event doesn't not exist in events, so we need to handle it separately
191             if (action.payload.uniqueId === TMP_UNIQUE_ID) {
192                 state.isTmpEventSaving = action.payload.isSaving;
193                 return;
194             }
195             state.events.forEach((event) => {
196                 if (event.uniqueId === action.payload.uniqueId) {
197                     event.isSaving = action.payload.isSaving;
198                 }
199             });
200         },
201         markEventsAsSaving(state, action: PayloadAction<{ UID: string; isSaving: boolean }>) {
202             // tmp event doesn't not exist in events, so we need to handle it separately
203             if (action.payload.UID === TMP_UID) {
204                 state.isTmpEventSaving = action.payload.isSaving;
205                 return;
206             }
207             state.events.forEach((event) => {
208                 const UID = (event.data?.eventData as CalendarEvent)?.UID;
209                 if (UID === action.payload.UID) {
210                     event.isSaving = action.payload.isSaving;
211                 }
212             });
213         },
214         updateInvite(state, action: PayloadAction<{ ID: string; selfEmail: string; partstat: string }>) {
215             let needRefresh = false;
217             state.events.forEach((event) => {
218                 const ID = (event.data?.eventData as CalendarEvent)?.ID;
219                 // We use the event ID instead of the event UID since single edit must not change for invite.
220                 if (ID && ID === action.payload.ID) {
221                     const result = setPartstat(event.uniqueId, action.payload.selfEmail, action.payload.partstat);
223                     if (!needRefresh && result) {
224                         needRefresh = true;
225                     }
226                 }
227             });
229             if (needRefresh) {
230                 // Force refresh since changes are saved in a different cache than the Redux store
231                 // However, the interface calculates events to display from both caches
232                 state.events = [...state.events];
233             }
234         },
235     },
238 export const eventsActions = slice.actions;
239 export const eventsReducer = { [eventsSliceName]: slice.reducer };