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 { create } from '@proton/shared/lib/helpers/desktopNotification';
12 import { dateLocale } from '@proton/shared/lib/i18n';
13 import type { CalendarAlarm, CalendarEvent } from '@proton/shared/lib/interfaces/calendar';
14 import noop from '@proton/utils/noop';
16 import notificationIcon from '../../../assets/notification.png';
17 import type { CalendarsEventsCache } from '../calendar/eventStore/interface';
19 const MIN_CUTOFF = -MINUTE;
21 export const displayNotification = ({ title = c('Title').t`Calendar alarm`, text = '', ...rest }) => {
26 icon: notificationIcon,
32 // Used for Electron notifications on the Calendar desktop app
33 { title, body: text, app: 'calendar' }
37 const getFirstUnseenAlarm = (alarms: CalendarAlarm[] = [], set: Set<string>) => {
38 return alarms.find(({ ID }) => {
44 alarms: CalendarAlarm[];
46 calendarsEventsCacheRef: MutableRefObject<CalendarsEventsCache>;
48 const AlarmWatcher = ({ alarms = [], tzid, calendarsEventsCacheRef }: Props) => {
50 const hasSuspendedCounter = useHasSuspendedCounter({ refreshInterval: MINUTE, tolerance: MINUTE / 2 });
51 const { contactEmailsMap } = useContactEmailsCache();
52 const getCalendarEventRaw = useGetCalendarEventRaw(contactEmailsMap);
53 const cacheRef = useRef<Set<string>>();
56 let timeoutHandle = 0;
57 let unmounted = false;
63 if (!cacheRef.current) {
64 cacheRef.current = new Set();
67 const firstUnseenAlarm = getFirstUnseenAlarm(alarms, cacheRef.current);
68 if (!firstUnseenAlarm) {
72 const { ID, Occurrence, Trigger, CalendarID, EventID } = firstUnseenAlarm;
74 const nextAlarmTime = fromUnixTime(Occurrence);
75 const now = Date.now();
76 const diff = differenceInMilliseconds(nextAlarmTime, now);
77 const delay = Math.max(diff, 0);
79 const getEvent = () => {
80 const cachedEvent = calendarsEventsCacheRef.current.getCachedEvent(CalendarID, EventID);
82 return Promise.resolve(cachedEvent);
84 return api<{ Event: CalendarEvent }>({ ...getEventRoute(CalendarID, EventID), silence: true }).then(
89 timeoutHandle = window.setTimeout(() => {
90 if (unmounted || !cacheRef.current) {
93 // Eagerly add the event to seen, ignore if it would fail
94 cacheRef.current.add(ID);
96 // Ignore the event if it's in the past after the cutoff
97 if (diff < MIN_CUTOFF) {
98 window.setTimeout(run, 0);
103 .then((Event) => getCalendarEventRaw(Event))
104 .then((eventRaw) => {
108 const { veventComponent: component } = eventRaw;
109 // compute event start time based on trigger, as we cannot rely on dtstart for recurring events
110 const start = new Date(getNextEventTime({ Occurrence, Trigger, tzid }));
111 const now = new Date();
112 const formatOptions = { locale: dateLocale };
113 const text = getAlarmMessage({ component, start, now, tzid, formatOptions });
114 return displayNotification({ text, tag: ID });
118 window.setTimeout(run, 0);
127 window.clearTimeout(timeoutHandle);
130 }, [alarms, tzid, getCalendarEventRaw, hasSuspendedCounter]);
135 export default AlarmWatcher;