1 import { updateCalendarSettings, updateMember } from '@proton/shared/lib/api/calendars';
2 import { getHasUserReachedCalendarsLimit } from '@proton/shared/lib/calendar/calendarLimits';
3 import { CALENDAR_FLAGS, CALENDAR_TYPE, SETTINGS_VIEW } from '@proton/shared/lib/calendar/constants';
4 import { reactivateCalendarsKeys } from '@proton/shared/lib/calendar/crypto/keys/reactivateCalendarKeys';
5 import { getMemberAndAddress } from '@proton/shared/lib/calendar/members';
6 import { getCanWrite } from '@proton/shared/lib/calendar/permissions';
7 import { hasBit, toggleBit } from '@proton/shared/lib/helpers/bitset';
8 import type { Api } from '@proton/shared/lib/interfaces';
9 import type { Address } from '@proton/shared/lib/interfaces/Address';
10 import type { CalendarCreateData } from '@proton/shared/lib/interfaces/calendar/Api';
14 CalendarNotificationSettings,
17 CalendarWithOwnMembers,
19 } from '@proton/shared/lib/interfaces/calendar/Calendar';
20 import type { SubscribedCalendar } from '@proton/shared/lib/interfaces/calendar/Subscription';
21 import type { GetAddressKeys } from '@proton/shared/lib/interfaces/hooks/GetAddressKeys';
22 import type { GetAddresses } from '@proton/shared/lib/interfaces/hooks/GetAddresses';
23 import isTruthy from '@proton/utils/isTruthy';
24 import unary from '@proton/utils/unary';
26 export const getIsCalendarActive = ({ Flags } = { Flags: 0 }) => {
27 return hasBit(Flags, CALENDAR_FLAGS.ACTIVE);
30 export const getIsCalendarDisabled = ({ Flags } = { Flags: 0 }) => {
31 return hasBit(Flags, CALENDAR_FLAGS.SELF_DISABLED) || hasBit(Flags, CALENDAR_FLAGS.SUPER_OWNER_DISABLED);
34 export const getDoesCalendarNeedReset = ({ Flags } = { Flags: 0 }) => {
35 return hasBit(Flags, CALENDAR_FLAGS.RESET_NEEDED);
38 export const getDoesCalendarHaveInactiveKeys = ({ Flags } = { Flags: 0 }) => {
39 return hasBit(Flags, CALENDAR_FLAGS.UPDATE_PASSPHRASE);
42 export const getDoesCalendarNeedUserAction = ({ Flags } = { Flags: 0 }) => {
43 return getDoesCalendarNeedReset({ Flags }) || getDoesCalendarHaveInactiveKeys({ Flags });
46 export const getIsCalendarProbablyActive = (calendar = { Flags: 0 }) => {
47 // Calendars are treated as "active" if flags are undefined, this can happen when a new calendar was created and received through the event manager.
48 // In this case, we assume everything went well and treat it as an active calendar.
49 return calendar.Flags === undefined || (!getIsCalendarDisabled(calendar) && getIsCalendarActive(calendar));
52 export const getProbablyActiveCalendars = <T extends Calendar>(calendars: T[] = []): T[] => {
53 return calendars.filter(unary(getIsCalendarProbablyActive));
56 export const getIsPersonalCalendar = (calendar: VisualCalendar | SubscribedCalendar): calendar is VisualCalendar => {
57 return calendar.Type === CALENDAR_TYPE.PERSONAL;
60 export const getIsOwnedCalendar = (calendar: CalendarWithOwnMembers) => {
61 return calendar.Owner.Email === calendar.Members[0].Email;
64 export const getIsSharedCalendar = (calendar: VisualCalendar) => {
65 return getIsPersonalCalendar(calendar) && !getIsOwnedCalendar(calendar);
68 export const getIsSubscribedCalendar = (
69 calendar: Calendar | VisualCalendar | SubscribedCalendar
70 ): calendar is SubscribedCalendar => {
71 return calendar.Type === CALENDAR_TYPE.SUBSCRIPTION;
74 export const getPersonalCalendars = <T extends Calendar>(calendars: T[] = []): T[] => {
75 return calendars.filter(unary(getIsPersonalCalendar));
78 export const getIsCalendarWritable = (calendar: VisualCalendar) => {
79 return getCanWrite(calendar.Permissions) && getIsPersonalCalendar(calendar);
82 export const getWritableCalendars = (calendars: VisualCalendar[]) => {
83 return calendars.filter(unary(getIsCalendarWritable));
86 export const getIsHolidaysCalendar = (calendar: VisualCalendar) => {
87 return calendar.Type === CALENDAR_TYPE.HOLIDAYS;
90 export const getIsUnknownCalendar = (calendar: VisualCalendar) => {
91 const knownTypes = [CALENDAR_TYPE.PERSONAL, CALENDAR_TYPE.SUBSCRIPTION, CALENDAR_TYPE.HOLIDAYS];
93 return !knownTypes.includes(calendar.Type);
96 export const getShowDuration = (calendar: VisualCalendar) => {
97 return getIsCalendarWritable(calendar) && getIsPersonalCalendar(calendar) && getIsOwnedCalendar(calendar);
100 export const groupCalendarsByTaxonomy = (calendars: VisualCalendar[] = []) => {
101 return calendars.reduce<{
102 ownedPersonalCalendars: VisualCalendar[];
103 sharedCalendars: VisualCalendar[];
104 subscribedCalendars: VisualCalendar[];
105 holidaysCalendars: VisualCalendar[];
106 unknownCalendars: VisualCalendar[];
109 if (getIsSubscribedCalendar(calendar)) {
110 acc.subscribedCalendars.push(calendar);
111 } else if (getIsPersonalCalendar(calendar)) {
112 const calendarsGroup = getIsOwnedCalendar(calendar) ? acc.ownedPersonalCalendars : acc.sharedCalendars;
113 calendarsGroup.push(calendar);
114 } else if (getIsHolidaysCalendar(calendar)) {
115 acc.holidaysCalendars.push(calendar);
117 acc.unknownCalendars.push(calendar);
122 ownedPersonalCalendars: [],
124 subscribedCalendars: [],
125 holidaysCalendars: [],
126 unknownCalendars: [],
131 export const getOwnedPersonalCalendars = (calendars: VisualCalendar[] = []) => {
132 return groupCalendarsByTaxonomy(calendars).ownedPersonalCalendars;
135 export const getSharedCalendars = (calendars: VisualCalendar[] = []) => {
136 return groupCalendarsByTaxonomy(calendars).sharedCalendars;
139 export const getSubscribedCalendars = (calendars: VisualCalendar[] = []) => {
140 return groupCalendarsByTaxonomy(calendars).subscribedCalendars;
143 enum CALENDAR_WEIGHT {
151 const getCalendarWeight = (calendar: VisualCalendar) => {
152 if (getIsPersonalCalendar(calendar)) {
153 return getIsOwnedCalendar(calendar) ? CALENDAR_WEIGHT.PERSONAL : CALENDAR_WEIGHT.SHARED;
155 if (getIsSubscribedCalendar(calendar)) {
156 return CALENDAR_WEIGHT.SUBSCRIBED;
158 if (getIsHolidaysCalendar(calendar)) {
159 return CALENDAR_WEIGHT.HOLIDAYS;
161 return CALENDAR_WEIGHT.UNKNOWN;
165 * Returns calendars sorted by weight and first member priority
167 * @param calendars calendars to sort
168 * @returns sorted calendars
170 export const sortCalendars = (calendars: VisualCalendar[]) => {
171 return [...calendars].sort((cal1, cal2) => {
172 return getCalendarWeight(cal1) - getCalendarWeight(cal2) || cal1.Priority - cal2.Priority;
176 const getPreferredCalendar = (calendars: VisualCalendar[] = [], defaultCalendarID: string | null = '') => {
177 if (!calendars.length) {
180 return calendars.find(({ ID }) => ID === defaultCalendarID) || calendars[0];
183 export const getPreferredActiveWritableCalendar = (
184 calendars: VisualCalendar[] = [],
185 defaultCalendarID: string | null = ''
187 return getPreferredCalendar(getProbablyActiveCalendars(getWritableCalendars(calendars)), defaultCalendarID);
190 export const getDefaultCalendar = (calendars: VisualCalendar[] = [], defaultCalendarID: string | null = '') => {
191 // only active owned personal calendars can be default
192 return getPreferredCalendar(getProbablyActiveCalendars(getOwnedPersonalCalendars(calendars)), defaultCalendarID);
195 export const getVisualCalendar = <T>(calendar: CalendarWithOwnMembers & T): VisualCalendar & T => {
196 const [member] = calendar.Members;
201 Description: member.Description,
203 Display: member.Display,
206 Permissions: member.Permissions,
207 Priority: member.Priority,
211 export const getVisualCalendars = <T>(calendars: (CalendarWithOwnMembers & T)[]): (VisualCalendar & T)[] =>
212 calendars.map((calendar) => getVisualCalendar(calendar));
214 export const getCanCreateCalendar = ({
216 ownedPersonalCalendars,
220 calendars: VisualCalendar[];
221 ownedPersonalCalendars: VisualCalendar[];
222 disabledCalendars: VisualCalendar[];
225 const { isCalendarsLimitReached } = getHasUserReachedCalendarsLimit(calendars, isFreeUser);
226 if (isCalendarsLimitReached) {
229 // TODO: The following if condition is very flaky. We should check that somewhere else
230 const activeCalendars = getProbablyActiveCalendars(ownedPersonalCalendars);
231 const totalActionableCalendars = activeCalendars.length + disabledCalendars.length;
232 if (totalActionableCalendars < ownedPersonalCalendars.length) {
233 // calendar keys need to be reactivated before being able to create a calendar
240 export const getCalendarWithReactivatedKeys = async ({
249 calendar: VisualCalendar;
251 silenceApi?: boolean;
252 addresses: Address[];
253 getAddressKeys: GetAddressKeys;
254 successCallback?: () => void;
255 handleError?: (error: any) => void;
257 if (getDoesCalendarHaveInactiveKeys(calendar)) {
259 const possiblySilentApi = <T>(config: any) => api<T>({ ...config, silence: silenceApi });
261 await reactivateCalendarsKeys({
262 calendars: [calendar],
263 api: possiblySilentApi,
272 Flags: toggleBit(calendar.Flags, CALENDAR_FLAGS.UPDATE_PASSPHRASE),
273 Members: calendar.Members.map((member) => {
274 const newMember = { ...member };
275 if (newMember.Email === calendar.Email) {
276 newMember.Flags = toggleBit(calendar.Flags, CALENDAR_FLAGS.UPDATE_PASSPHRASE);
291 export const DEFAULT_CALENDAR_USER_SETTINGS: CalendarUserSettings = {
293 DisplayWeekNumber: 1,
294 DefaultCalendarID: null,
295 AutoDetectPrimaryTimezone: 1,
296 PrimaryTimezone: 'UTC',
297 DisplaySecondaryTimezone: 0,
298 SecondaryTimezone: null,
299 ViewPreference: SETTINGS_VIEW.WEEK,
304 const getHasChangedCalendarMemberData = (calendarPayload: CalendarCreateData, calendar: VisualCalendar) => {
305 const { Name: oldName, Description: oldDescription, Color: oldColor, Display: oldDisplay } = calendar;
306 const { Name: newName, Description: newDescription, Color: newColor, Display: newDisplay } = calendarPayload;
309 oldColor.toLowerCase() !== newColor.toLowerCase() ||
310 oldDisplay !== newDisplay ||
311 oldName !== newName ||
312 oldDescription !== newDescription
316 const getHasChangedCalendarNotifications = (
317 newNotifications: CalendarNotificationSettings[],
318 oldNotifications: CalendarNotificationSettings[]
321 newNotifications.length !== oldNotifications.length ||
322 newNotifications.some(
323 ({ Type: newType, Trigger: newTrigger }) =>
324 !oldNotifications.find(
325 ({ Type: oldType, Trigger: oldTrigger }) => oldType === newType && oldTrigger === newTrigger
331 export type EditableCalendarSettings = Pick<
333 'DefaultEventDuration' | 'DefaultPartDayNotifications' | 'DefaultFullDayNotifications' | 'MakesUserBusy'
336 const getHasChangedCalendarSettings = (
337 newSettings: Required<EditableCalendarSettings>,
338 oldSettings?: CalendarSettings
341 // we should not fall in here. If we do, assume changes are needed
345 DefaultEventDuration: newDuration,
346 DefaultPartDayNotifications: newPartDayNotifications,
347 DefaultFullDayNotifications: newFullDayNotifications,
348 MakesUserBusy: newMakesUserBusy,
351 DefaultEventDuration: oldDuration,
352 DefaultPartDayNotifications: oldPartDayNotifications,
353 DefaultFullDayNotifications: oldFullDayNotifications,
354 MakesUserBusy: oldMakesUserBusy,
358 newDuration !== oldDuration ||
359 getHasChangedCalendarNotifications(newPartDayNotifications, oldPartDayNotifications) ||
360 getHasChangedCalendarNotifications(newFullDayNotifications, oldFullDayNotifications) ||
361 newMakesUserBusy !== oldMakesUserBusy
364 export const updateCalendar = async (
365 calendar: VisualCalendar,
366 calendarPayload: CalendarCreateData,
367 calendarSettingsPayload: Required<EditableCalendarSettings>,
368 getCalendarBootstrap: (calendarID: string) => Promise<CalendarBootstrap>,
369 getAddresses: GetAddresses,
372 const calendarID = calendar.ID;
373 const { Color, Display, Description, Name } = calendarPayload;
374 const [{ ID: memberID }] = getMemberAndAddress(await getAddresses(), calendar.Members);
375 const hasChangedMemberData = getHasChangedCalendarMemberData(calendarPayload, calendar);
376 const calendarBootstrap = await getCalendarBootstrap(calendarID);
377 const hasChangedSettings = getHasChangedCalendarSettings(
378 calendarSettingsPayload,
379 calendarBootstrap.CalendarSettings
384 hasChangedMemberData && api(updateMember(calendarID, memberID, { Display, Color, Description, Name })),
385 hasChangedSettings && api(updateCalendarSettings(calendarID, calendarSettingsPayload)),