Merge branch 'feat/inda-383-daily-stat' into 'main'
[ProtonMail-WebClient.git] / applications / calendar / src / app / components / events / EventPopover.tsx
blob8cd7a90bc41c93a093d6481379bf14a44a55fa40
1 import { useMemo, useRef } from 'react';
3 import { c } from 'ttag';
5 import { useUser } from '@proton/account/user/hooks';
6 import { useCalendarBootstrap } from '@proton/calendar/calendarBootstrap/hooks';
7 import { Badge, CalendarEventDateHeader, CalendarInviteButtons, Loader } from '@proton/components';
8 import { useLoading } from '@proton/hooks';
9 import {
10     getIsCalendarDisabled,
11     getIsCalendarWritable,
12     getIsOwnedCalendar,
13     getIsSubscribedCalendar,
14     getIsUnknownCalendar,
15 } from '@proton/shared/lib/calendar/calendar';
16 import { ICAL_ATTENDEE_STATUS, VIEWS } from '@proton/shared/lib/calendar/constants';
17 import { naiveGetIsDecryptionError } from '@proton/shared/lib/calendar/helper';
18 import { getTimezonedFrequencyString } from '@proton/shared/lib/calendar/recurrence/getFrequencyString';
19 import type { WeekStartsOn } from '@proton/shared/lib/date-fns-utc/interface';
20 import { wait } from '@proton/shared/lib/helpers/promise';
21 import { dateLocale } from '@proton/shared/lib/i18n';
22 import type { CalendarEventSharedData, VcalVeventComponent } from '@proton/shared/lib/interfaces/calendar';
23 import type { SimpleMap } from '@proton/shared/lib/interfaces/utils';
24 import clsx from '@proton/utils/clsx';
26 import type {
27     CalendarViewEvent,
28     CalendarViewEventTemporaryEvent,
29     DisplayNameEmail,
30 } from '../../containers/calendar/interface';
31 import { getCanDeleteEvent, getCanDuplicateEvent, getCanEditEvent, getCanReplyToEvent } from '../../helpers/event';
32 import { getIsCalendarAppInDrawer } from '../../helpers/views';
33 import type { InviteActions } from '../../interfaces/Invite';
34 import { INVITE_ACTION_TYPES } from '../../interfaces/Invite';
35 import PopoverContainer from './PopoverContainer';
36 import PopoverEventContent from './PopoverEventContent';
37 import PopoverFooter from './PopoverFooter';
38 import PopoverHeader from './PopoverHeader';
39 import { getEventErrorMessage } from './error';
40 import {
41     EventReloadErrorAction,
42     PopoverDeleteButton,
43     PopoverDuplicateButton,
44     PopoverEditButton,
45     PopoverViewButton,
46 } from './eventPopoverButtons/EventPopoverButtons';
47 import getEventInformation from './getEventInformation';
48 import useReadEvent from './useReadEvent';
50 import './EventPopover.scss';
52 const { ACCEPTED, TENTATIVE } = ICAL_ATTENDEE_STATUS;
54 interface Props {
55     formatTime: (date: Date) => string;
56     /**
57      * On edit event callback
58      * @param userCanDuplicateEvent - used for busy slots to know if we should fetch them or not
59      * @returns
60      */
61     onEdit: (userCanDuplicateEvent: boolean) => void;
62     onRefresh: () => Promise<void>;
63     onDuplicate?: () => void;
64     onChangePartstat: (inviteActions: InviteActions) => Promise<void>;
65     onDelete: (inviteActions: InviteActions) => Promise<void>;
66     onClose: () => void;
67     onNavigateToEventFromSearch?: (
68         eventData: CalendarEventSharedData,
69         eventComponent: VcalVeventComponent,
70         occurrence?: { localStart: Date; occurrenceNumber: number }
71     ) => void;
72     style: any;
73     popoverRef: any;
74     event: CalendarViewEvent | CalendarViewEventTemporaryEvent;
75     view: VIEWS;
76     tzid: string;
77     weekStartsOn: WeekStartsOn;
78     isSmallViewport: boolean;
79     displayNameEmailMap: SimpleMap<DisplayNameEmail>;
82 const EventPopover = ({
83     formatTime,
84     onEdit,
85     onRefresh,
86     onDuplicate,
87     onChangePartstat,
88     onDelete,
89     onClose,
90     onNavigateToEventFromSearch,
91     style,
92     popoverRef,
93     event: targetEvent,
94     event: { start, end, isAllDay, isAllPartDay },
95     view,
96     tzid,
97     weekStartsOn,
98     isSmallViewport,
99     displayNameEmailMap,
100 }: Props) => {
101     const isDrawerApp = getIsCalendarAppInDrawer(view);
102     const popoverEventContentRef = useRef<HTMLDivElement>(null);
103     const [{ hasPaidMail }] = useUser();
105     const [loadingDelete, withLoadingDelete] = useLoading();
106     const [loadingRefresh, withLoadingRefresh] = useLoading();
108     const targetEventData = targetEvent?.data || {};
109     const { eventReadResult, eventData, calendarData, eventRecurrence } = targetEventData;
110     const [calendarBootstrap] = useCalendarBootstrap(calendarData.ID);
111     const [{ veventComponent }] = eventReadResult?.result || [{}];
112     const isCalendarDisabled = getIsCalendarDisabled(calendarData);
113     const isSubscribedCalendar = getIsSubscribedCalendar(calendarData);
114     const isOwnedCalendar = getIsOwnedCalendar(calendarData);
115     const isUnknownCalendar = getIsUnknownCalendar(calendarData);
116     const isCalendarWritable = getIsCalendarWritable(calendarData);
118     const isSearchView = view === VIEWS.SEARCH;
119     const model = useReadEvent(targetEventData, tzid, calendarBootstrap?.CalendarSettings);
120     const {
121         eventReadError,
122         isEventReadLoading,
123         eventTitleSafe,
124         isInvitation,
125         isCancelled,
126         isUnanswered,
127         userPartstat,
128         isSelfAddressActive,
129         color,
130     } = getEventInformation(targetEvent, model, hasPaidMail);
131     const canDuplicateEvent =
132         !isSearchView &&
133         onDuplicate &&
134         getCanDuplicateEvent({
135             isUnknownCalendar,
136             isSubscribedCalendar,
137             isOwnedCalendar,
138             isOrganizer: model.isOrganizer,
139             isInvitation,
140         });
142     const handleDelete = () => {
143         const sendCancellationNotice =
144             !eventReadError && !isCalendarDisabled && !isCancelled && [ACCEPTED, TENTATIVE].includes(userPartstat);
146         if (model.isAttendee) {
147             return withLoadingDelete(
148                 onDelete({
149                     type: isSelfAddressActive
150                         ? INVITE_ACTION_TYPES.DECLINE_INVITATION
151                         : INVITE_ACTION_TYPES.DECLINE_DISABLED,
152                     isProtonProtonInvite: model.isProtonProtonInvite,
153                     sendCancellationNotice,
154                     selfAddress: model.selfAddress,
155                     selfAttendeeIndex: model.selfAttendeeIndex,
156                     partstat: ICAL_ATTENDEE_STATUS.DECLINED,
157                 })
158             );
159         }
161         return withLoadingDelete(
162             onDelete({
163                 type: isSelfAddressActive ? INVITE_ACTION_TYPES.CANCEL_INVITATION : INVITE_ACTION_TYPES.CANCEL_DISABLED,
164                 isProtonProtonInvite: model.isProtonProtonInvite,
165                 selfAddress: model.selfAddress,
166                 selfAttendeeIndex: model.selfAttendeeIndex,
167             })
168         );
169     };
171     const handleChangePartstat = (partstat: ICAL_ATTENDEE_STATUS) => {
172         return onChangePartstat({
173             isProtonProtonInvite: model.isProtonProtonInvite,
174             type: INVITE_ACTION_TYPES.CHANGE_PARTSTAT,
175             partstat,
176             selfAddress: model.selfAddress,
177             selfAttendeeIndex: model.selfAttendeeIndex,
178         });
179     };
181     const dateHeader = useMemo(
182         () => (
183             <CalendarEventDateHeader
184                 startDate={start}
185                 endDate={end}
186                 isAllDay={isAllDay && !isAllPartDay}
187                 formatTime={formatTime}
188                 hasFakeUtcDates
189                 hasModifiedAllDayEndDate
190                 className="text-lg m-0"
191             />
192         ),
193         [start, end, isAllDay, isAllPartDay, formatTime]
194     );
196     const showEditButton = !isSearchView && getCanEditEvent({ isUnknownCalendar, isCalendarDisabled });
197     const showDeleteButton = !isSearchView && getCanDeleteEvent({ isOwnedCalendar, isCalendarWritable, isInvitation });
198     const showDuplicateButton = !!canDuplicateEvent;
199     const showViewEventButton = isSearchView || isDrawerApp;
201     const mergedStyle = isSmallViewport ? undefined : style;
202     const frequencyString = useMemo(() => {
203         if (!veventComponent) {
204             return;
205         }
206         return getTimezonedFrequencyString(veventComponent.rrule, veventComponent.dtstart, {
207             currentTzid: tzid,
208             weekStartsOn,
209             locale: dateLocale,
210         });
211     }, [veventComponent, tzid]);
213     const commonContainerProps = {
214         style: mergedStyle,
215         ref: popoverRef,
216         onClose,
217     };
218     const commonHeaderProps = {
219         onClose,
220         className: 'shrink-0',
221     };
223     if (eventReadError) {
224         const showReload = !isSearchView && !naiveGetIsDecryptionError(eventReadError);
225         return (
226             <PopoverContainer {...commonContainerProps} className="eventpopover flex flex-column flex-nowrap">
227                 <PopoverHeader
228                     {...commonHeaderProps}
229                     actions={
230                         <EventReloadErrorAction
231                             showDeleteButton={showDeleteButton}
232                             showReloadButton={showReload}
233                             loadingDelete={loadingDelete}
234                             loadingRefresh={loadingRefresh}
235                             onDelete={handleDelete}
236                             onRefresh={() => withLoadingRefresh(onRefresh())}
237                         />
238                     }
239                 >
240                     <h1 className="h3">{c('Error').t`Error`}</h1>
241                 </PopoverHeader>
242                 <span>{getEventErrorMessage(eventReadError)}</span>
243             </PopoverContainer>
244         );
245     }
247     if (isEventReadLoading) {
248         return (
249             <PopoverContainer {...commonContainerProps} className="eventpopover p-4">
250                 <PopoverHeader {...commonHeaderProps}>
251                     <Loader />
252                 </PopoverHeader>
253             </PopoverContainer>
254         );
255     }
257     return (
258         <PopoverContainer {...commonContainerProps} className="eventpopover flex flex-column flex-nowrap">
259             <PopoverHeader
260                 {...commonHeaderProps}
261                 actions={
262                     <>
263                         <PopoverEditButton
264                             showButton={showEditButton}
265                             loading={loadingDelete}
266                             onEdit={() => onEdit(!!canDuplicateEvent)}
267                         />
268                         <PopoverDuplicateButton
269                             showButton={showDuplicateButton}
270                             loading={loadingDelete}
271                             onDuplicate={onDuplicate}
272                         />
273                         <PopoverDeleteButton
274                             showButton={showDeleteButton}
275                             loading={loadingDelete}
276                             onDelete={handleDelete}
277                         />
278                         <PopoverViewButton
279                             showButton={showViewEventButton}
280                             start={start}
281                             isSearchView={isSearchView}
282                             eventData={eventData}
283                             onViewClick={() => {
284                                 if (!eventData || !veventComponent) {
285                                     return;
286                                 }
287                                 onNavigateToEventFromSearch?.(eventData, veventComponent, eventRecurrence);
288                             }}
289                         />
290                     </>
291                 }
292             >
293                 {isCancelled && (
294                     <Badge
295                         type="light"
296                         tooltip={c('Calendar invite info').t`This event has been canceled`}
297                         className="mb-1"
298                     >
299                         <span className="text-uppercase">{c('Event canceled status badge').t`canceled`}</span>
300                     </Badge>
301                 )}
302                 <div className="flex mb-4 flex-nowrap">
303                     <span
304                         className={clsx(
305                             'event-popover-calendar-border relative shrink-0 my-1',
306                             isUnanswered && !isCancelled && 'isUnanswered'
307                         )}
308                         style={{ '--calendar-color': color }}
309                     />
310                     <div className="pt-2">
311                         <h1 className="eventpopover-title lh-rg text-hyphens overflow-auto mb-0" title={eventTitleSafe}>
312                             {eventTitleSafe}
313                         </h1>
314                         <div className={clsx([!!frequencyString ? 'mb-2' : 'mb-3'])}>
315                             {dateHeader}
316                             {!!frequencyString && <div className="color-weak">{frequencyString}</div>}
317                         </div>
318                     </div>
319                 </div>
320             </PopoverHeader>
321             <div className="overflow-auto mb-4" ref={popoverEventContentRef}>
322                 <PopoverEventContent
323                     key={targetEvent.uniqueId}
324                     calendar={calendarData}
325                     model={model}
326                     isDrawerApp={isDrawerApp}
327                     formatTime={formatTime}
328                     displayNameEmailMap={displayNameEmailMap}
329                     popoverEventContentRef={popoverEventContentRef}
330                 />
331             </div>
332             {getCanReplyToEvent({ isOwnedCalendar, isCalendarWritable, isAttendee: model.isAttendee, isCancelled }) && (
333                 <PopoverFooter
334                     className="shrink-0 items-start md:items-center justify-space-between gap-4 flex-column md:flex-row"
335                     key={targetEvent.uniqueId}
336                 >
337                     <div>
338                         <strong>{c('Calendar invite buttons label').t`Attending?`}</strong>
339                     </div>
340                     <div className="ml-0 md:ml-auto">
341                         <CalendarInviteButtons
342                             actions={{
343                                 accept: () => handleChangePartstat(ICAL_ATTENDEE_STATUS.ACCEPTED),
344                                 acceptTentatively: () => handleChangePartstat(ICAL_ATTENDEE_STATUS.TENTATIVE),
345                                 decline: () => handleChangePartstat(ICAL_ATTENDEE_STATUS.DECLINED),
346                                 retryCreateEvent: () => wait(0),
347                                 retryUpdateEvent: () => wait(0),
348                             }}
349                             partstat={userPartstat}
350                             disabled={isCalendarDisabled || !isSelfAddressActive || isSearchView}
351                         />
352                     </div>
353                 </PopoverFooter>
354             )}
355         </PopoverContainer>
356     );
359 export default EventPopover;