Cleanup - unused files / unused exports / duplicate exports
[ProtonMail-WebClient.git] / applications / calendar / src / app / containers / alarms / AlarmWatcher.tsx
blob234fca1462c0f77656c4ac4731ef08ece91d82d2
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 }) => {
22     return create(
23         title,
24         {
25             body: text,
26             icon: notificationIcon,
27             onClick() {
28                 window.focus();
29             },
30             ...rest,
31         },
32         // Used for Electron notifications on the Calendar desktop app
33         { title, body: text, app: 'calendar' }
34     );
37 const getFirstUnseenAlarm = (alarms: CalendarAlarm[] = [], set: Set<string>) => {
38     return alarms.find(({ ID }) => {
39         return !set.has(ID);
40     });
43 interface Props {
44     alarms: CalendarAlarm[];
45     tzid: string;
46     calendarsEventsCacheRef: MutableRefObject<CalendarsEventsCache>;
48 const AlarmWatcher = ({ alarms = [], tzid, calendarsEventsCacheRef }: Props) => {
49     const api = useApi();
50     const hasSuspendedCounter = useHasSuspendedCounter({ refreshInterval: MINUTE, tolerance: MINUTE / 2 });
51     const { contactEmailsMap } = useContactEmailsCache();
52     const getCalendarEventRaw = useGetCalendarEventRaw(contactEmailsMap);
53     const cacheRef = useRef<Set<string>>();
55     useEffect(() => {
56         let timeoutHandle = 0;
57         let unmounted = false;
59         const run = () => {
60             if (unmounted) {
61                 return;
62             }
63             if (!cacheRef.current) {
64                 cacheRef.current = new Set();
65             }
67             const firstUnseenAlarm = getFirstUnseenAlarm(alarms, cacheRef.current);
68             if (!firstUnseenAlarm) {
69                 return;
70             }
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);
81                 if (cachedEvent) {
82                     return Promise.resolve(cachedEvent);
83                 }
84                 return api<{ Event: CalendarEvent }>({ ...getEventRoute(CalendarID, EventID), silence: true }).then(
85                     ({ Event }) => Event
86                 );
87             };
89             timeoutHandle = window.setTimeout(() => {
90                 if (unmounted || !cacheRef.current) {
91                     return;
92                 }
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);
99                     return;
100                 }
102                 getEvent()
103                     .then((Event) => getCalendarEventRaw(Event))
104                     .then((eventRaw) => {
105                         if (unmounted) {
106                             return;
107                         }
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 });
115                     })
116                     .catch(noop);
118                 window.setTimeout(run, 0);
119             }, delay);
120         };
122         run();
124         return () => {
125             unmounted = true;
126             if (timeoutHandle) {
127                 window.clearTimeout(timeoutHandle);
128             }
129         };
130     }, [alarms, tzid, getCalendarEventRaw, hasSuspendedCounter]);
132     return null;
135 export default AlarmWatcher;