1 import type { MutableRefObject } from 'react';
2 import { useEffect, useRef } from 'react';
4 import { differenceInMilliseconds, fromUnixTime } from 'date-fns';
5 import { c } from 'ttag';
7 import { useApi, useContactEmailsCache, useGetCalendarEventRaw, useHasSuspendedCounter } from '@proton/components';
8 import { getEvent as getEventRoute } from '@proton/shared/lib/api/calendars';
9 import { getAlarmMessage, getNextEventTime } from '@proton/shared/lib/calendar/alarms';
10 import { MINUTE } from '@proton/shared/lib/constants';
11 import { isElectronMail } from '@proton/shared/lib/helpers/desktop';
12 import { create, createElectronNotification } from '@proton/shared/lib/helpers/desktopNotification';
13 import { dateLocale } from '@proton/shared/lib/i18n';
14 import type { CalendarAlarm, CalendarEvent } from '@proton/shared/lib/interfaces/calendar';
15 import noop from '@proton/utils/noop';
17 import notificationIcon from '../../../assets/notification.png';
18 import type { CalendarsEventsCache } from '../calendar/eventStore/interface';
20 const MIN_CUTOFF = -MINUTE;
22 export const displayNotification = ({ title = c('Title').t`Calendar alarm`, text = '', ...rest }) => {
24 return createElectronNotification({ title, body: text, app: 'calendar' });
26 return create(title, {
28 icon: notificationIcon,
36 const getFirstUnseenAlarm = (alarms: CalendarAlarm[] = [], set: Set<string>) => {
37 return alarms.find(({ ID }) => {
43 alarms: CalendarAlarm[];
45 calendarsEventsCacheRef: MutableRefObject<CalendarsEventsCache>;
47 const AlarmWatcher = ({ alarms = [], tzid, calendarsEventsCacheRef }: Props) => {
49 const hasSuspendedCounter = useHasSuspendedCounter({ refreshInterval: MINUTE, tolerance: MINUTE / 2 });
50 const { contactEmailsMap } = useContactEmailsCache();
51 const getCalendarEventRaw = useGetCalendarEventRaw(contactEmailsMap);
52 const cacheRef = useRef<Set<string>>();
55 let timeoutHandle = 0;
56 let unmounted = false;
62 if (!cacheRef.current) {
63 cacheRef.current = new Set();
66 const firstUnseenAlarm = getFirstUnseenAlarm(alarms, cacheRef.current);
67 if (!firstUnseenAlarm) {
71 const { ID, Occurrence, Trigger, CalendarID, EventID } = firstUnseenAlarm;
73 const nextAlarmTime = fromUnixTime(Occurrence);
74 const now = Date.now();
75 const diff = differenceInMilliseconds(nextAlarmTime, now);
76 const delay = Math.max(diff, 0);
78 const getEvent = () => {
79 const cachedEvent = calendarsEventsCacheRef.current.getCachedEvent(CalendarID, EventID);
81 return Promise.resolve(cachedEvent);
83 return api<{ Event: CalendarEvent }>({ ...getEventRoute(CalendarID, EventID), silence: true }).then(
88 timeoutHandle = window.setTimeout(() => {
89 if (unmounted || !cacheRef.current) {
92 // Eagerly add the event to seen, ignore if it would fail
93 cacheRef.current.add(ID);
95 // Ignore the event if it's in the past after the cutoff
96 if (diff < MIN_CUTOFF) {
97 window.setTimeout(run, 0);
102 .then((Event) => getCalendarEventRaw(Event))
103 .then((eventRaw) => {
107 const { veventComponent: component } = eventRaw;
108 // compute event start time based on trigger, as we cannot rely on dtstart for recurring events
109 const start = new Date(getNextEventTime({ Occurrence, Trigger, tzid }));
110 const now = new Date();
111 const formatOptions = { locale: dateLocale };
112 const text = getAlarmMessage({ component, start, now, tzid, formatOptions });
113 void displayNotification({ text, tag: ID });
117 window.setTimeout(run, 0);
126 window.clearTimeout(timeoutHandle);
129 }, [alarms, tzid, getCalendarEventRaw, hasSuspendedCounter]);
134 export default AlarmWatcher;