Use same lock values as mobile clients
[ProtonMail-WebClient.git] / packages / shared / lib / calendar / calendar.ts
blob65da65618ce396624621c25be8e5c259d2ffcaee
1 import isTruthy from '@proton/utils/isTruthy';
2 import unary from '@proton/utils/unary';
4 import { updateCalendarSettings, updateMember } from '../api/calendars';
5 import { hasBit, toggleBit } from '../helpers/bitset';
6 import type { Address, Api } from '../interfaces';
7 import type {
8     Calendar,
9     CalendarCreateData,
10     CalendarNotificationSettings,
11     CalendarSettings,
12     CalendarUserSettings,
13     CalendarWithOwnMembers,
14     SubscribedCalendar,
15     VisualCalendar,
16 } from '../interfaces/calendar';
17 import type { GetAddressKeys } from '../interfaces/hooks/GetAddressKeys';
18 import type { GetAddresses } from '../interfaces/hooks/GetAddresses';
19 import { getHasUserReachedCalendarsLimit } from './calendarLimits';
20 import { CALENDAR_FLAGS, CALENDAR_TYPE, SETTINGS_VIEW } from './constants';
21 import { reactivateCalendarsKeys } from './crypto/keys/reactivateCalendarKeys';
22 import { getMemberAndAddress } from './members';
23 import { getCanWrite } from './permissions';
25 export const getIsCalendarActive = ({ Flags } = { Flags: 0 }) => {
26     return hasBit(Flags, CALENDAR_FLAGS.ACTIVE);
29 export const getIsCalendarDisabled = ({ Flags } = { Flags: 0 }) => {
30     return hasBit(Flags, CALENDAR_FLAGS.SELF_DISABLED) || hasBit(Flags, CALENDAR_FLAGS.SUPER_OWNER_DISABLED);
33 export const getDoesCalendarNeedReset = ({ Flags } = { Flags: 0 }) => {
34     return hasBit(Flags, CALENDAR_FLAGS.RESET_NEEDED);
37 export const getDoesCalendarHaveInactiveKeys = ({ Flags } = { Flags: 0 }) => {
38     return hasBit(Flags, CALENDAR_FLAGS.UPDATE_PASSPHRASE);
41 export const getDoesCalendarNeedUserAction = ({ Flags } = { Flags: 0 }) => {
42     return getDoesCalendarNeedReset({ Flags }) || getDoesCalendarHaveInactiveKeys({ Flags });
45 export const getIsCalendarProbablyActive = (calendar = { Flags: 0 }) => {
46     // Calendars are treated as "active" if flags are undefined, this can happen when a new calendar was created and received through the event manager.
47     // In this case, we assume everything went well and treat it as an active calendar.
48     return calendar.Flags === undefined || (!getIsCalendarDisabled(calendar) && getIsCalendarActive(calendar));
51 export const getProbablyActiveCalendars = <T extends Calendar>(calendars: T[] = []): T[] => {
52     return calendars.filter(unary(getIsCalendarProbablyActive));
55 export const getIsPersonalCalendar = (calendar: VisualCalendar | SubscribedCalendar): calendar is VisualCalendar => {
56     return calendar.Type === CALENDAR_TYPE.PERSONAL;
59 export const getIsOwnedCalendar = (calendar: CalendarWithOwnMembers) => {
60     return calendar.Owner.Email === calendar.Members[0].Email;
63 export const getIsSharedCalendar = (calendar: VisualCalendar) => {
64     return getIsPersonalCalendar(calendar) && !getIsOwnedCalendar(calendar);
67 export const getIsSubscribedCalendar = (
68     calendar: Calendar | VisualCalendar | SubscribedCalendar
69 ): calendar is SubscribedCalendar => {
70     return calendar.Type === CALENDAR_TYPE.SUBSCRIPTION;
73 export const getPersonalCalendars = <T extends Calendar>(calendars: T[] = []): T[] => {
74     return calendars.filter(unary(getIsPersonalCalendar));
77 export const getIsCalendarWritable = (calendar: VisualCalendar) => {
78     return getCanWrite(calendar.Permissions) && getIsPersonalCalendar(calendar);
81 export const getWritableCalendars = (calendars: VisualCalendar[]) => {
82     return calendars.filter(unary(getIsCalendarWritable));
85 export const getIsHolidaysCalendar = (calendar: VisualCalendar) => {
86     return calendar.Type === CALENDAR_TYPE.HOLIDAYS;
89 export const getIsUnknownCalendar = (calendar: VisualCalendar) => {
90     const knownTypes = [CALENDAR_TYPE.PERSONAL, CALENDAR_TYPE.SUBSCRIPTION, CALENDAR_TYPE.HOLIDAYS];
92     return !knownTypes.includes(calendar.Type);
95 export const getShowDuration = (calendar: VisualCalendar) => {
96     return getIsCalendarWritable(calendar) && getIsPersonalCalendar(calendar) && getIsOwnedCalendar(calendar);
99 export const groupCalendarsByTaxonomy = (calendars: VisualCalendar[] = []) => {
100     return calendars.reduce<{
101         ownedPersonalCalendars: VisualCalendar[];
102         sharedCalendars: VisualCalendar[];
103         subscribedCalendars: VisualCalendar[];
104         holidaysCalendars: VisualCalendar[];
105         unknownCalendars: VisualCalendar[];
106     }>(
107         (acc, calendar) => {
108             if (getIsSubscribedCalendar(calendar)) {
109                 acc.subscribedCalendars.push(calendar);
110             } else if (getIsPersonalCalendar(calendar)) {
111                 const calendarsGroup = getIsOwnedCalendar(calendar) ? acc.ownedPersonalCalendars : acc.sharedCalendars;
112                 calendarsGroup.push(calendar);
113             } else if (getIsHolidaysCalendar(calendar)) {
114                 acc.holidaysCalendars.push(calendar);
115             } else {
116                 acc.unknownCalendars.push(calendar);
117             }
118             return acc;
119         },
120         {
121             ownedPersonalCalendars: [],
122             sharedCalendars: [],
123             subscribedCalendars: [],
124             holidaysCalendars: [],
125             unknownCalendars: [],
126         }
127     );
130 export const getOwnedPersonalCalendars = (calendars: VisualCalendar[] = []) => {
131     return groupCalendarsByTaxonomy(calendars).ownedPersonalCalendars;
134 export const getSharedCalendars = (calendars: VisualCalendar[] = []) => {
135     return groupCalendarsByTaxonomy(calendars).sharedCalendars;
138 export const getSubscribedCalendars = (calendars: VisualCalendar[] = []) => {
139     return groupCalendarsByTaxonomy(calendars).subscribedCalendars;
142 enum CALENDAR_WEIGHT {
143     PERSONAL = 0,
144     SUBSCRIBED = 1,
145     HOLIDAYS = 2,
146     SHARED = 3,
147     UNKNOWN = 4,
150 const getCalendarWeight = (calendar: VisualCalendar) => {
151     if (getIsPersonalCalendar(calendar)) {
152         return getIsOwnedCalendar(calendar) ? CALENDAR_WEIGHT.PERSONAL : CALENDAR_WEIGHT.SHARED;
153     }
154     if (getIsSubscribedCalendar(calendar)) {
155         return CALENDAR_WEIGHT.SUBSCRIBED;
156     }
157     if (getIsHolidaysCalendar(calendar)) {
158         return CALENDAR_WEIGHT.HOLIDAYS;
159     }
160     return CALENDAR_WEIGHT.UNKNOWN;
164  * Returns calendars sorted by weight and first member priority
166  * @param calendars calendars to sort
167  * @returns sorted calendars
168  */
169 export const sortCalendars = (calendars: VisualCalendar[]) => {
170     return [...calendars].sort((cal1, cal2) => {
171         return getCalendarWeight(cal1) - getCalendarWeight(cal2) || cal1.Priority - cal2.Priority;
172     });
175 const getPreferredCalendar = (calendars: VisualCalendar[] = [], defaultCalendarID: string | null = '') => {
176     if (!calendars.length) {
177         return;
178     }
179     return calendars.find(({ ID }) => ID === defaultCalendarID) || calendars[0];
182 export const getPreferredActiveWritableCalendar = (
183     calendars: VisualCalendar[] = [],
184     defaultCalendarID: string | null = ''
185 ) => {
186     return getPreferredCalendar(getProbablyActiveCalendars(getWritableCalendars(calendars)), defaultCalendarID);
189 export const getDefaultCalendar = (calendars: VisualCalendar[] = [], defaultCalendarID: string | null = '') => {
190     // only active owned personal calendars can be default
191     return getPreferredCalendar(getProbablyActiveCalendars(getOwnedPersonalCalendars(calendars)), defaultCalendarID);
194 export const getVisualCalendar = <T>(calendar: CalendarWithOwnMembers & T): VisualCalendar & T => {
195     const [member] = calendar.Members;
197     return {
198         ...calendar,
199         Name: member.Name,
200         Description: member.Description,
201         Color: member.Color,
202         Display: member.Display,
203         Email: member.Email,
204         Flags: member.Flags,
205         Permissions: member.Permissions,
206         Priority: member.Priority,
207     };
210 export const getVisualCalendars = <T>(calendars: (CalendarWithOwnMembers & T)[]): (VisualCalendar & T)[] =>
211     calendars.map((calendar) => getVisualCalendar(calendar));
213 export const getCanCreateCalendar = ({
214     calendars,
215     ownedPersonalCalendars,
216     disabledCalendars,
217     isFreeUser,
218 }: {
219     calendars: VisualCalendar[];
220     ownedPersonalCalendars: VisualCalendar[];
221     disabledCalendars: VisualCalendar[];
222     isFreeUser: boolean;
223 }) => {
224     const { isCalendarsLimitReached } = getHasUserReachedCalendarsLimit(calendars, isFreeUser);
225     if (isCalendarsLimitReached) {
226         return false;
227     }
228     // TODO: The following if condition is very flaky. We should check that somewhere else
229     const activeCalendars = getProbablyActiveCalendars(ownedPersonalCalendars);
230     const totalActionableCalendars = activeCalendars.length + disabledCalendars.length;
231     if (totalActionableCalendars < ownedPersonalCalendars.length) {
232         // calendar keys need to be reactivated before being able to create a calendar
233         return false;
234     }
236     return true;
239 export const getCalendarWithReactivatedKeys = async ({
240     calendar,
241     api,
242     silenceApi = true,
243     addresses,
244     getAddressKeys,
245     successCallback,
246     handleError,
247 }: {
248     calendar: VisualCalendar;
249     api: Api;
250     silenceApi?: boolean;
251     addresses: Address[];
252     getAddressKeys: GetAddressKeys;
253     successCallback?: () => void;
254     handleError?: (error: any) => void;
255 }) => {
256     if (getDoesCalendarHaveInactiveKeys(calendar)) {
257         try {
258             const possiblySilentApi = <T>(config: any) => api<T>({ ...config, silence: silenceApi });
260             await reactivateCalendarsKeys({
261                 calendars: [calendar],
262                 api: possiblySilentApi,
263                 addresses,
264                 getAddressKeys,
265             });
267             successCallback?.();
269             return {
270                 ...calendar,
271                 Flags: toggleBit(calendar.Flags, CALENDAR_FLAGS.UPDATE_PASSPHRASE),
272                 Members: calendar.Members.map((member) => {
273                     const newMember = { ...member };
274                     if (newMember.Email === calendar.Email) {
275                         newMember.Flags = toggleBit(calendar.Flags, CALENDAR_FLAGS.UPDATE_PASSPHRASE);
276                     }
277                     return newMember;
278                 }),
279             };
280         } catch (e) {
281             handleError?.(e);
283             return calendar;
284         }
285     }
287     return calendar;
290 export const DEFAULT_CALENDAR_USER_SETTINGS: CalendarUserSettings = {
291     WeekLength: 7,
292     DisplayWeekNumber: 1,
293     DefaultCalendarID: null,
294     AutoDetectPrimaryTimezone: 1,
295     PrimaryTimezone: 'UTC',
296     DisplaySecondaryTimezone: 0,
297     SecondaryTimezone: null,
298     ViewPreference: SETTINGS_VIEW.WEEK,
299     InviteLocale: null,
300     AutoImportInvite: 0,
303 const getHasChangedCalendarMemberData = (calendarPayload: CalendarCreateData, calendar: VisualCalendar) => {
304     const { Name: oldName, Description: oldDescription, Color: oldColor, Display: oldDisplay } = calendar;
305     const { Name: newName, Description: newDescription, Color: newColor, Display: newDisplay } = calendarPayload;
307     return (
308         oldColor.toLowerCase() !== newColor.toLowerCase() ||
309         oldDisplay !== newDisplay ||
310         oldName !== newName ||
311         oldDescription !== newDescription
312     );
315 const getHasChangedCalendarNotifications = (
316     newNotifications: CalendarNotificationSettings[],
317     oldNotifications: CalendarNotificationSettings[]
318 ) => {
319     return (
320         newNotifications.length !== oldNotifications.length ||
321         newNotifications.some(
322             ({ Type: newType, Trigger: newTrigger }) =>
323                 !oldNotifications.find(
324                     ({ Type: oldType, Trigger: oldTrigger }) => oldType === newType && oldTrigger === newTrigger
325                 )
326         )
327     );
330 export type EditableCalendarSettings = Pick<
331     CalendarSettings,
332     'DefaultEventDuration' | 'DefaultPartDayNotifications' | 'DefaultFullDayNotifications' | 'MakesUserBusy'
335 const getHasChangedCalendarSettings = (
336     newSettings: Required<EditableCalendarSettings>,
337     oldSettings?: CalendarSettings
338 ) => {
339     if (!oldSettings) {
340         // we should not fall in here. If we do, assume changes are needed
341         return true;
342     }
343     const {
344         DefaultEventDuration: newDuration,
345         DefaultPartDayNotifications: newPartDayNotifications,
346         DefaultFullDayNotifications: newFullDayNotifications,
347         MakesUserBusy: newMakesUserBusy,
348     } = newSettings;
349     const {
350         DefaultEventDuration: oldDuration,
351         DefaultPartDayNotifications: oldPartDayNotifications,
352         DefaultFullDayNotifications: oldFullDayNotifications,
353         MakesUserBusy: oldMakesUserBusy,
354     } = oldSettings;
356     return (
357         newDuration !== oldDuration ||
358         getHasChangedCalendarNotifications(newPartDayNotifications, oldPartDayNotifications) ||
359         getHasChangedCalendarNotifications(newFullDayNotifications, oldFullDayNotifications) ||
360         newMakesUserBusy !== oldMakesUserBusy
361     );
363 export const updateCalendar = async (
364     calendar: VisualCalendar,
365     calendarPayload: CalendarCreateData,
366     calendarSettingsPayload: Required<EditableCalendarSettings>,
367     readCalendarBootstrap: (calendarID: string) => any,
368     getAddresses: GetAddresses,
369     api: Api
370 ) => {
371     const calendarID = calendar.ID;
372     const { Color, Display, Description, Name } = calendarPayload;
373     const [{ ID: memberID }] = getMemberAndAddress(await getAddresses(), calendar.Members);
374     const hasChangedMemberData = getHasChangedCalendarMemberData(calendarPayload, calendar);
375     const hasChangedSettings = getHasChangedCalendarSettings(
376         calendarSettingsPayload,
377         readCalendarBootstrap(calendarID)?.CalendarSettings
378     );
380     await Promise.all(
381         [
382             hasChangedMemberData && api(updateMember(calendarID, memberID, { Display, Color, Description, Name })),
383             hasChangedSettings && api(updateCalendarSettings(calendarID, calendarSettingsPayload)),
384         ].filter(isTruthy)
385     );