Merge branch 'feat/inda-383-daily-stat' into 'main'
[ProtonMail-WebClient.git] / applications / calendar / src / app / components / eventModal / eventForm / state.ts
blob2e0740a888215beacf61a50e333f02e691f32185
1 import {
2     DEFAULT_FULL_DAY_NOTIFICATION,
3     DEFAULT_FULL_DAY_NOTIFICATIONS,
4     DEFAULT_PART_DAY_NOTIFICATION,
5     DEFAULT_PART_DAY_NOTIFICATIONS,
6 } from '@proton/shared/lib/calendar/alarms/notificationDefaults';
7 import { apiNotificationsToModel, notificationsToModel } from '@proton/shared/lib/calendar/alarms/notificationsToModel';
8 import {
9     getIsCalendarWritable,
10     getIsOwnedCalendar,
11     getIsSubscribedCalendar,
12     getIsUnknownCalendar,
13 } from '@proton/shared/lib/calendar/calendar';
14 import {
15     DAILY_TYPE,
16     DEFAULT_EVENT_DURATION,
17     END_TYPE,
18     EVENT_VERIFICATION_STATUS,
19     FREQUENCY,
20     ICAL_EVENT_STATUS,
21     MONTHLY_TYPE,
22     WEEKLY_TYPE,
23     YEARLY_TYPE,
24 } from '@proton/shared/lib/calendar/constants';
25 import { stripAllTags } from '@proton/shared/lib/calendar/sanitize';
26 import { getIsAllDay, getRecurrenceId } from '@proton/shared/lib/calendar/veventHelper';
27 import { fromLocalDate, toUTCDate } from '@proton/shared/lib/date/timezone';
28 import type { Address, RequireOnly, Address as tsAddress } from '@proton/shared/lib/interfaces';
29 import type {
30     AttendeeModel,
31     CalendarMember,
32     CalendarSettings,
33     DateTimeModel,
34     EventModel,
35     FrequencyModel,
36     SelfAddressData,
37     VisualCalendar,
38     CalendarSettings as tsCalendarSettings,
39 } from '@proton/shared/lib/interfaces/calendar';
40 import type { VcalVeventComponent } from '@proton/shared/lib/interfaces/calendar/VcalModel';
42 import type { SharedVcalVeventComponent } from '../../../containers/calendar/eventStore/interface';
43 import { getSnappedDate } from '../../calendar/mouseHelpers/dateHelpers';
44 import getFrequencyModelChange from './getFrequencyModelChange';
45 import { propertiesToModel } from './propertiesToModel';
46 import { propertiesToNotificationModel } from './propertiesToNotificationModel';
47 import { getDateTimeState } from './time';
49 export const getNotificationModels = ({
50     DefaultPartDayNotifications = DEFAULT_PART_DAY_NOTIFICATIONS,
51     DefaultFullDayNotifications = DEFAULT_FULL_DAY_NOTIFICATIONS,
52 }) => {
53     return {
54         defaultPartDayNotification: DEFAULT_PART_DAY_NOTIFICATION,
55         defaultFullDayNotification: DEFAULT_FULL_DAY_NOTIFICATION,
56         partDayNotifications: notificationsToModel(DefaultPartDayNotifications, false),
57         fullDayNotifications: notificationsToModel(DefaultFullDayNotifications, true),
58     };
61 export const getInitialDateTimeModel = (initialDate: Date, defaultEventDuration: number, tzid: string) => {
62     const snapInterval = 30;
64     const start = getSnappedDate(
65         new Date(
66             Date.UTC(
67                 initialDate.getUTCFullYear(),
68                 initialDate.getUTCMonth(),
69                 initialDate.getUTCDate(),
70                 initialDate.getUTCHours(),
71                 initialDate.getUTCMinutes() + snapInterval
72             )
73         ),
74         snapInterval
75     );
77     const end = new Date(
78         Date.UTC(
79             start.getUTCFullYear(),
80             start.getUTCMonth(),
81             start.getUTCDate(),
82             start.getUTCHours(),
83             start.getUTCMinutes() + defaultEventDuration
84         )
85     );
87     return {
88         start: getDateTimeState(start, tzid),
89         end: getDateTimeState(end, tzid),
90     };
93 export const getInitialFrequencyModel = (startDate: Date): FrequencyModel => {
94     return {
95         type: FREQUENCY.ONCE,
96         frequency: FREQUENCY.WEEKLY,
97         interval: 1,
98         daily: { type: DAILY_TYPE.ALL_DAYS },
99         weekly: { type: WEEKLY_TYPE.ON_DAYS, days: [startDate.getDay()] },
100         monthly: { type: MONTHLY_TYPE.ON_MONTH_DAY },
101         yearly: { type: YEARLY_TYPE.BY_MONTH_ON_MONTH_DAY },
102         ends: { type: END_TYPE.NEVER, count: 2 },
103     };
106 export const getInitialMemberModel = (
107     Addresses: tsAddress[],
108     Members: CalendarMember[],
109     Member: CalendarMember,
110     Address: tsAddress
111 ) => {
112     const { ID: addressID } = Address;
113     const { ID: memberID } = Member;
114     return {
115         member: {
116             addressID,
117             memberID,
118         },
119     };
122 const getCalendarsModel = (Calendar: VisualCalendar, Calendars: VisualCalendar[] = []) => {
123     if (!Calendars.some(({ ID }) => ID === Calendar.ID)) {
124         throw new Error('Calendar not found');
125     }
126     return {
127         calendars: Calendars.map((calendar) => ({
128             text: calendar.Name,
129             value: calendar.ID,
130             color: calendar.Color,
131             permissions: calendar.Permissions,
132             isSubscribed: getIsSubscribedCalendar(calendar),
133             isOwned: getIsOwnedCalendar(calendar),
134             isWritable: getIsCalendarWritable(calendar),
135             isUnknown: getIsUnknownCalendar(calendar),
136         })),
137         calendar: {
138             id: Calendar.ID,
139             color: Calendar.Color,
140             permissions: Calendar.Permissions,
141             isSubscribed: getIsSubscribedCalendar(Calendar),
142             isOwned: getIsOwnedCalendar(Calendar),
143             isWritable: getIsCalendarWritable(Calendar),
144             isUnknown: getIsUnknownCalendar(Calendar),
145         },
146     };
149 export const getOrganizerAndSelfAddressModel = ({
150     attendees,
151     addressID,
152     addresses,
153     isAttendee,
154 }: {
155     attendees: AttendeeModel[];
156     addressID: string;
157     addresses: Address[];
158     isAttendee: boolean;
159 }) => {
160     if (!attendees.length || isAttendee) {
161         return {};
162     }
164     const organizerAddress = addresses.find(({ ID }) => ID === addressID);
166     if (!organizerAddress) {
167         return {};
168     }
170     return {
171         organizer: { email: organizerAddress.Email, cn: organizerAddress.DisplayName },
172         selfAddress: organizerAddress,
173     };
176 interface GetInitialModelArguments {
177     initialDate: Date;
178     CalendarSettings: tsCalendarSettings;
179     Calendar: VisualCalendar;
180     Calendars: VisualCalendar[];
181     Members: CalendarMember[];
182     Member: CalendarMember;
183     Addresses: tsAddress[];
184     Address: tsAddress;
185     isAllDay: boolean;
186     verificationStatus?: EVENT_VERIFICATION_STATUS;
187     tzid: string;
188     attendees?: AttendeeModel[];
191 export const getInitialModel = ({
192     initialDate = toUTCDate(fromLocalDate(new Date())), // Needs to be in fake utc time
193     CalendarSettings,
194     Calendar,
195     Calendars,
196     Members,
197     Member,
198     Addresses,
199     Address,
200     isAllDay,
201     verificationStatus = EVENT_VERIFICATION_STATUS.NOT_VERIFIED,
202     tzid,
203     attendees = [],
204 }: GetInitialModelArguments): EventModel => {
205     const { DefaultEventDuration: defaultEventDuration = DEFAULT_EVENT_DURATION } = CalendarSettings;
206     const dateTimeModel = getInitialDateTimeModel(initialDate, defaultEventDuration, tzid);
207     const frequencyModel = getInitialFrequencyModel(dateTimeModel.start.date);
208     const notificationModel = getNotificationModels(CalendarSettings);
209     const memberModel = getInitialMemberModel(Addresses, Members, Member, Address);
210     const calendarsModel = getCalendarsModel(Calendar, Calendars);
211     const organizerModel = getOrganizerAndSelfAddressModel({
212         attendees,
213         addressID: memberModel.member.addressID,
214         addresses: Addresses,
215         isAttendee: false,
216     });
218     return {
219         type: 'event',
220         title: '',
221         location: '',
222         description: '',
223         attendees,
224         ...organizerModel,
225         initialDate,
226         initialTzid: tzid,
227         isAllDay,
228         verificationStatus,
229         isOrganizer: !!attendees.length,
230         isAttendee: false,
231         isProtonProtonInvite: false,
232         hasDefaultNotifications: true,
233         status: ICAL_EVENT_STATUS.CONFIRMED,
234         defaultEventDuration,
235         frequencyModel,
236         hasTouchedRrule: false,
237         ...notificationModel,
238         hasPartDayDefaultNotifications: true,
239         hasFullDayDefaultNotifications: true,
240         ...memberModel,
241         ...dateTimeModel,
242         ...calendarsModel,
243     };
246 const getParentMerge = ({
247     veventComponentParentPartial,
248     recurrenceStart,
249     hasDefaultNotifications,
250     isProtonProtonInvite,
251     tzid,
252 }: {
253     veventComponentParentPartial: SharedVcalVeventComponent;
254     recurrenceStart: DateTimeModel;
255     hasDefaultNotifications: boolean;
256     isProtonProtonInvite: boolean;
257     tzid: string;
258 }) => {
259     const isAllDay = getIsAllDay(veventComponentParentPartial);
260     const parentModel = propertiesToModel({
261         veventComponent: veventComponentParentPartial,
262         isAllDay,
263         hasDefaultNotifications,
264         isProtonProtonInvite,
265         tzid,
266     });
267     const { frequencyModel, start } = parentModel;
268     return {
269         frequencyModel: getFrequencyModelChange(start, recurrenceStart, frequencyModel),
270     };
273 interface GetExistingEventArguments {
274     veventComponent: VcalVeventComponent;
275     hasDefaultNotifications: boolean;
276     veventComponentParentPartial?: SharedVcalVeventComponent;
277     isProtonProtonInvite: boolean;
278     tzid: string;
279     selfAddressData: SelfAddressData;
280     calendarSettings: CalendarSettings;
281     color?: string;
284 export const getExistingEvent = ({
285     veventComponent,
286     hasDefaultNotifications,
287     veventComponentParentPartial,
288     isProtonProtonInvite,
289     tzid,
290     selfAddressData,
291     calendarSettings,
292 }: GetExistingEventArguments): RequireOnly<EventModel, 'isAllDay' | 'description'> => {
293     const isAllDay = getIsAllDay(veventComponent);
294     const recurrenceId = getRecurrenceId(veventComponent);
296     const newModel = propertiesToModel({
297         veventComponent,
298         hasDefaultNotifications,
299         selfAddressData,
300         isAllDay,
301         isProtonProtonInvite,
302         tzid,
303     });
304     const strippedDescription = stripAllTags(newModel.description);
306     const notifications = hasDefaultNotifications
307         ? apiNotificationsToModel({ notifications: null, isAllDay, calendarSettings })
308         : propertiesToNotificationModel(veventComponent, isAllDay);
310     const parentMerge =
311         veventComponentParentPartial && recurrenceId
312             ? getParentMerge({
313                   veventComponentParentPartial,
314                   recurrenceStart: newModel.start,
315                   hasDefaultNotifications,
316                   isProtonProtonInvite,
317                   tzid,
318               })
319             : {};
321     return {
322         ...newModel,
323         description: strippedDescription,
324         isAllDay,
325         ...parentMerge,
326         ...(isAllDay
327             ? {
328                   fullDayNotifications: notifications,
329               }
330             : {
331                   partDayNotifications: notifications,
332               }),
333     };