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';
10 CalendarNotificationSettings,
13 CalendarWithOwnMembers,
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[];
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);
116 acc.unknownCalendars.push(calendar);
121 ownedPersonalCalendars: [],
123 subscribedCalendars: [],
124 holidaysCalendars: [],
125 unknownCalendars: [],
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 {
150 const getCalendarWeight = (calendar: VisualCalendar) => {
151 if (getIsPersonalCalendar(calendar)) {
152 return getIsOwnedCalendar(calendar) ? CALENDAR_WEIGHT.PERSONAL : CALENDAR_WEIGHT.SHARED;
154 if (getIsSubscribedCalendar(calendar)) {
155 return CALENDAR_WEIGHT.SUBSCRIBED;
157 if (getIsHolidaysCalendar(calendar)) {
158 return CALENDAR_WEIGHT.HOLIDAYS;
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
169 export const sortCalendars = (calendars: VisualCalendar[]) => {
170 return [...calendars].sort((cal1, cal2) => {
171 return getCalendarWeight(cal1) - getCalendarWeight(cal2) || cal1.Priority - cal2.Priority;
175 const getPreferredCalendar = (calendars: VisualCalendar[] = [], defaultCalendarID: string | null = '') => {
176 if (!calendars.length) {
179 return calendars.find(({ ID }) => ID === defaultCalendarID) || calendars[0];
182 export const getPreferredActiveWritableCalendar = (
183 calendars: VisualCalendar[] = [],
184 defaultCalendarID: string | null = ''
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;
200 Description: member.Description,
202 Display: member.Display,
205 Permissions: member.Permissions,
206 Priority: member.Priority,
210 export const getVisualCalendars = <T>(calendars: (CalendarWithOwnMembers & T)[]): (VisualCalendar & T)[] =>
211 calendars.map((calendar) => getVisualCalendar(calendar));
213 export const getCanCreateCalendar = ({
215 ownedPersonalCalendars,
219 calendars: VisualCalendar[];
220 ownedPersonalCalendars: VisualCalendar[];
221 disabledCalendars: VisualCalendar[];
224 const { isCalendarsLimitReached } = getHasUserReachedCalendarsLimit(calendars, isFreeUser);
225 if (isCalendarsLimitReached) {
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
239 export const getCalendarWithReactivatedKeys = async ({
248 calendar: VisualCalendar;
250 silenceApi?: boolean;
251 addresses: Address[];
252 getAddressKeys: GetAddressKeys;
253 successCallback?: () => void;
254 handleError?: (error: any) => void;
256 if (getDoesCalendarHaveInactiveKeys(calendar)) {
258 const possiblySilentApi = <T>(config: any) => api<T>({ ...config, silence: silenceApi });
260 await reactivateCalendarsKeys({
261 calendars: [calendar],
262 api: possiblySilentApi,
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);
290 export const DEFAULT_CALENDAR_USER_SETTINGS: CalendarUserSettings = {
292 DisplayWeekNumber: 1,
293 DefaultCalendarID: null,
294 AutoDetectPrimaryTimezone: 1,
295 PrimaryTimezone: 'UTC',
296 DisplaySecondaryTimezone: 0,
297 SecondaryTimezone: null,
298 ViewPreference: SETTINGS_VIEW.WEEK,
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;
308 oldColor.toLowerCase() !== newColor.toLowerCase() ||
309 oldDisplay !== newDisplay ||
310 oldName !== newName ||
311 oldDescription !== newDescription
315 const getHasChangedCalendarNotifications = (
316 newNotifications: CalendarNotificationSettings[],
317 oldNotifications: CalendarNotificationSettings[]
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
330 export type EditableCalendarSettings = Pick<
332 'DefaultEventDuration' | 'DefaultPartDayNotifications' | 'DefaultFullDayNotifications' | 'MakesUserBusy'
335 const getHasChangedCalendarSettings = (
336 newSettings: Required<EditableCalendarSettings>,
337 oldSettings?: CalendarSettings
340 // we should not fall in here. If we do, assume changes are needed
344 DefaultEventDuration: newDuration,
345 DefaultPartDayNotifications: newPartDayNotifications,
346 DefaultFullDayNotifications: newFullDayNotifications,
347 MakesUserBusy: newMakesUserBusy,
350 DefaultEventDuration: oldDuration,
351 DefaultPartDayNotifications: oldPartDayNotifications,
352 DefaultFullDayNotifications: oldFullDayNotifications,
353 MakesUserBusy: oldMakesUserBusy,
357 newDuration !== oldDuration ||
358 getHasChangedCalendarNotifications(newPartDayNotifications, oldPartDayNotifications) ||
359 getHasChangedCalendarNotifications(newFullDayNotifications, oldFullDayNotifications) ||
360 newMakesUserBusy !== oldMakesUserBusy
363 export const updateCalendar = async (
364 calendar: VisualCalendar,
365 calendarPayload: CalendarCreateData,
366 calendarSettingsPayload: Required<EditableCalendarSettings>,
367 readCalendarBootstrap: (calendarID: string) => any,
368 getAddresses: GetAddresses,
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
382 hasChangedMemberData && api(updateMember(calendarID, memberID, { Display, Color, Description, Name })),
383 hasChangedSettings && api(updateCalendarSettings(calendarID, calendarSettingsPayload)),