/* eslint-disable react-hooks/rules-of-hooks */ import { useAppDispatch, useAppSelector } from "@/app/hooks"; import { CalendarName } from "@/components/Calendar/CalendarName"; import { getTimezoneOffset } from "@/utils/timezone"; import { formatEventChipTitle } from "@/components/Calendar/utils/calendarUtils"; import ResponsiveDialog from "@/components/Dialog/ResponsiveDialog"; import { EditModeDialog } from "@/components/Event/EditModeDialog"; import EventDuplication from "@/components/Event/EventDuplicate"; import { handleDelete } from "@/components/Event/eventHandlers/eventHandlers"; import { InfoRow } from "@/components/Event/InfoRow"; import { renderAttendeeBadge } from "@/components/Event/utils/eventUtils"; import { browserDefaultTimeZone } from "@/utils/timezone"; import { DateSelectArg } from "@fullcalendar/core"; import { AvatarGroup, Box, Button, Chip, IconButton, Menu, MenuItem, Tooltip, Typography, useTheme, } from "@linagora/twake-mui"; import { alpha } from "@mui/material/styles"; import CalendarTodayIcon from "@mui/icons-material/CalendarToday"; import CircleIcon from "@mui/icons-material/Circle"; import CloseIcon from "@mui/icons-material/Close"; import EditIcon from "@mui/icons-material/Edit"; import ErrorOutlineIcon from "@mui/icons-material/ErrorOutline"; import FileDownloadOutlinedIcon from "@mui/icons-material/FileDownloadOutlined"; import LocationOnOutlinedIcon from "@mui/icons-material/LocationOnOutlined"; import LockOutlineIcon from "@mui/icons-material/LockOutline"; import MoreVertIcon from "@mui/icons-material/MoreVert"; import NotificationsNoneIcon from "@mui/icons-material/NotificationsNone"; import PeopleAltOutlinedIcon from "@mui/icons-material/PeopleAltOutlined"; import RepeatIcon from "@mui/icons-material/Repeat"; import SubjectIcon from "@mui/icons-material/Subject"; import VideocamOutlinedIcon from "@mui/icons-material/VideocamOutlined"; import { useEffect, useRef, useState } from "react"; import { useI18n } from "twake-i18n"; import { deleteEventAsync } from "../Calendars/services"; import { userAttendee } from "../User/models/attendee"; import { AttendanceValidation } from "./AttendanceValidation/AttendanceValidation"; import { createEventContext } from "./createEventContext"; import { dlEvent } from "./EventApi"; import EventPopover from "./EventModal"; import { CalendarEvent } from "./EventsTypes"; import EventUpdateModal from "./EventUpdateModal"; export default function EventPreviewModal({ eventId, calId, tempEvent, open, onClose, }: { eventId: string; calId: string; tempEvent?: boolean; open: boolean; onClose: (event: unknown, reason: "backdropClick" | "escapeKeyDown") => void; }) { const { t } = useI18n(); const dispatch = useAppDispatch(); const calendars = useAppSelector((state) => state.calendars); const timezone = useAppSelector((state) => state.settings.timeZone) ?? browserDefaultTimeZone; const calendar = tempEvent ? calendars.templist[calId] : calendars.list[calId]; const event = calendar.events[eventId]; const user = useAppSelector((state) => state.user.userData); const theme = useTheme(); const infoIconColor = alpha(theme.palette.grey[900], 0.9); const infoIconSx = { minWidth: "25px", marginRight: 2, color: infoIconColor }; if (!user) return null; const isRecurring = event?.uid?.includes("/"); const isOwn = calendar.ownerEmails?.includes(user.email); const isOrganizer = event.organizer ? user.email === event.organizer.cal_address : isOwn; const [showAllAttendees, setShowAllAttendees] = useState(false); const [openUpdateModal, setOpenUpdateModal] = useState(false); const [openDuplicateModal, setOpenDuplicateModal] = useState(false); const [hidePreview, setHidePreview] = useState(false); const [openEditModePopup, setOpenEditModePopup] = useState( null ); const [typeOfAction, setTypeOfAction] = useState<"solo" | "all" | undefined>( undefined ); const [afterChoiceFunc, setAfterChoiceFunc] = useState< ((type: "solo" | "all" | undefined) => void) | undefined >(); const attendeePreview = makeAttendeePreview(event.attendee, t); const hasCheckedSessionStorageRef = useRef(false); const [toggleActionMenu, setToggleActionMenu] = useState( null ); const mailSpaUrl = window.MAIL_SPA_URL ?? null; useEffect(() => { if (!event || !calendar) { onClose({}, "backdropClick"); } }, [event, calendar, onClose]); // Check sessionStorage when component mounts or when open becomes true (only once per open) useEffect(() => { if (!open) { hasCheckedSessionStorageRef.current = false; return; } // Only check once when open becomes true if (hasCheckedSessionStorageRef.current) return; hasCheckedSessionStorageRef.current = true; const checkAndReopen = () => { try { const stored = sessionStorage.getItem("eventUpdateModalReopen"); if (stored) { const data = JSON.parse(stored); // Check if stored data matches current preview // For recurring events, typeOfAction from sessionStorage should be used // Allow undefined to match undefined, or use stored typeOfAction if current is undefined const typeOfActionMatch = data.typeOfAction === typeOfAction || (data.typeOfAction === undefined && typeOfAction === undefined) || (data.typeOfAction !== undefined && typeOfAction === undefined); // Allow stored typeOfAction to match when current is undefined if ( data.eventId === eventId && data.calId === calId && typeOfActionMatch ) { // Restore typeOfAction from sessionStorage if it exists if (data.typeOfAction !== undefined && typeOfAction === undefined) { setTypeOfAction(data.typeOfAction); // Open modal immediately with the typeOfAction from sessionStorage // Use setTimeout to ensure state is set before opening setTimeout(() => { setOpenUpdateModal(true); setHidePreview(true); sessionStorage.removeItem("eventUpdateModalReopen"); }, 50); } else { // Small delay to ensure component is fully mounted setTimeout(() => { setOpenUpdateModal(true); setHidePreview(true); sessionStorage.removeItem("eventUpdateModalReopen"); }, 100); } } } } catch { // Ignore sessionStorage errors } }; checkAndReopen(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [open, eventId, calId]); // Removed typeOfAction from dependencies to prevent re-checking when typeOfAction changes // Listen for eventUpdateModalReopen event to reopen update modal on API failure useEffect(() => { const handleUpdateModalReopen = (event: CustomEvent) => { const detail = event.detail; // Check if this event matches current preview // For recurring events, typeOfAction from event should be used // Allow undefined to match undefined, or use event typeOfAction if current is undefined const typeOfActionMatch = detail?.typeOfAction === typeOfAction || (detail?.typeOfAction === undefined && typeOfAction === undefined) || (detail?.typeOfAction !== undefined && typeOfAction === undefined); // Allow event typeOfAction to match when current is undefined if ( detail?.eventId === eventId && detail?.calId === calId && typeOfActionMatch ) { // Restore typeOfAction from event if it exists if (detail?.typeOfAction !== undefined && typeOfAction === undefined) { setTypeOfAction(detail.typeOfAction); // Open modal immediately with the typeOfAction from event setTimeout(() => { setOpenUpdateModal(true); setHidePreview(true); }, 50); } else { setOpenUpdateModal(true); setHidePreview(true); } // Clear sessionStorage after reopening try { sessionStorage.removeItem("eventUpdateModalReopen"); } catch { // Ignore sessionStorage errors } } }; // Also check sessionStorage on mount or when eventId/calId changes const checkSessionStorage = () => { try { const stored = sessionStorage.getItem("eventUpdateModalReopen"); if (stored) { const data = JSON.parse(stored); // Check if stored data matches current preview // For recurring events, typeOfAction from sessionStorage should be used // Allow undefined to match undefined, or use stored typeOfAction if current is undefined const typeOfActionMatch = data.typeOfAction === typeOfAction || (data.typeOfAction === undefined && typeOfAction === undefined) || (data.typeOfAction !== undefined && typeOfAction === undefined); // Allow stored typeOfAction to match when current is undefined if ( data.eventId === eventId && data.calId === calId && typeOfActionMatch ) { // Restore typeOfAction from sessionStorage if it exists if (data.typeOfAction !== undefined && typeOfAction === undefined) { setTypeOfAction(data.typeOfAction); // Open modal immediately with the typeOfAction from sessionStorage setTimeout(() => { setOpenUpdateModal(true); setHidePreview(true); sessionStorage.removeItem("eventUpdateModalReopen"); }, 50); } else { setOpenUpdateModal(true); setHidePreview(true); sessionStorage.removeItem("eventUpdateModalReopen"); } } } } catch { // Ignore sessionStorage errors } }; // Check on mount and when relevant props change checkSessionStorage(); window.addEventListener( "eventUpdateModalReopen", handleUpdateModalReopen as EventListener ); return () => { window.removeEventListener( "eventUpdateModalReopen", handleUpdateModalReopen as EventListener ); }; }, [eventId, calId, typeOfAction]); if (!event || !calendar) return null; const attendeeDisplayLimit = 3; const attendees = event.attendee?.filter( (a) => a.cal_address !== event.organizer?.cal_address ) || []; const currentUserAttendee = event.attendee?.find( (person) => person.cal_address === user.email ); const contextualizedEvent = createEventContext(event, calendar, user); const organizer = event.attendee?.find( (a) => a.cal_address === event.organizer?.cal_address ); return ( <> onClose({}, "backdropClick")} showHeaderActions={false} actionsBorderTop={currentUserAttendee && isOwn} actionsJustifyContent="center" style={{ overflow: "auto" }} title={ {window.DEBUG && ( { let url: string | null = null; try { const icsContent = await dlEvent(event); const blob = new Blob([icsContent], { type: "text/calendar", }); url = URL.createObjectURL(blob); const link = document.createElement("a"); link.href = url; link.download = `${eventId}.ics`; document.body.appendChild(link); link.click(); document.body.removeChild(link); } catch (error) { console.error("Failed to download ICS file:", error); } finally { if (url) URL.revokeObjectURL(url); } }} > )} {isOrganizer && isOwn && ( { if (isRecurring) { setAfterChoiceFunc(() => () => { setHidePreview(true); setOpenUpdateModal(true); }); setOpenEditModePopup("edit"); } else { setHidePreview(true); setOpenUpdateModal(true); } }} > )} {((event.class !== "PRIVATE" && !isOwn) || isOwn) && ( setToggleActionMenu(e.currentTarget)} > )} setToggleActionMenu(null)} anchorEl={toggleActionMenu} > {mailSpaUrl && attendees.length > 0 && ( window.open( `${mailSpaUrl}/mailto/?uri=mailto:${event.attendee .map((a) => a.cal_address) .filter((mail) => mail !== user.email) .join( "," )}&subject=${encodeURIComponent(event.title ?? "")}` ) } > {t("eventPreview.emailAttendees")} )} { setToggleActionMenu(null); setHidePreview(true); setOpenDuplicateModal(true); }} /> {isOwn && ( { if (isRecurring) { setAfterChoiceFunc( () => (typeOfAction?: "solo" | "all" | undefined) => handleDelete( isRecurring, typeOfAction, onClose, dispatch, calendar, event, calId, eventId ) ); setOpenEditModePopup("delete"); } else { onClose({}, "backdropClick"); try { const result = await dispatch( deleteEventAsync({ calId, eventId, eventURL: event.URL, }) ); // For compatibility with tests that may not mock unwrap if (result && typeof result.unwrap === "function") { await result.unwrap(); } } catch (error) { console.error("Failed to delete event:", error); } } }} > {t("eventPreview.deleteEvent")} )} onClose({}, "backdropClick")} > } actions={ } > {event.class === "PRIVATE" && (isOwn ? ( ) : ( ))} {formatEventChipTitle(event, t)} {event.transp === "TRANSPARENT" && ( } label={t("eventPreview.free.label")} /> )} {formatDate(event.start, t, timezone, event.allday)} {event.end && formatEnd(event.start, event.end, t, timezone, event.allday) && ` – ${formatEnd(event.start, event.end, t, timezone, event.allday)} ${!event.allday ? getTimezoneOffset(timezone, new Date(event.start)) : ""}`} {((event.class !== "PRIVATE" && !isOwn) || isOwn) && ( <> {/* Video */} {event.x_openpass_videoconference && ( } content={ } /> )} {/* Attendees */} {attendees?.length > 0 && ( <> } content={ {t("eventPreview.guests", { count: event.attendee.length, })} {attendeePreview} {!showAllAttendees && ( {organizer && renderAttendeeBadge( organizer, "org", t, showAllAttendees, true )} {attendees.map((a, idx) => renderAttendeeBadge( a, idx.toString(), t, showAllAttendees ) )} )} setShowAllAttendees(!showAllAttendees)} > {showAllAttendees ? t("eventPreview.showLess") : t("eventPreview.showMore")} } /> )} {showAllAttendees && organizer && renderAttendeeBadge(organizer, "org", t, showAllAttendees, true)} {showAllAttendees && attendees.map((a, idx) => renderAttendeeBadge(a, idx.toString(), t, showAllAttendees) )} {/* Location */} {event.location && ( } text={event.location} style={{ fontSize: "16px", fontFamily: "'Inter', sans-serif", }} /> )} {/* Description */} {event.description && ( } text={event.description} style={{ fontSize: "16px", fontFamily: "'Inter', sans-serif", }} /> )} {/* ALARM */} {event.alarm && ( } text={t("eventPreview.alarmText", { trigger: t(`event.form.notifications.${event.alarm.trigger}`), action: (() => { if (!event.alarm.action) return ""; const translationKey = `event.form.notifications.${event.alarm.action}`; const translated = t(translationKey); // If translation returns the key itself, it means translation not found, use raw value return translated === translationKey ? event.alarm.action : translated; })(), })} style={{ fontSize: "16px", fontFamily: "'Inter', sans-serif", }} /> )} {/* Repetition */} {event.repetition && ( } text={makeRecurrenceString(event, t)} style={{ fontSize: "16px", fontFamily: "'Inter', sans-serif", }} /> )} )} {event.class === "PRIVATE" && !isOwn && ( {t("eventPreview.privateEvent.hiddenDetails")} )} {/* Error */} {event.error && ( } text={event.error} style={{ fontSize: "16px", fontFamily: "'Inter', sans-serif", }} error /> )} {/* Calendar color dot */} { setTypeOfAction(type); if (afterChoiceFunc) { afterChoiceFunc(type); } }} /> { setOpenUpdateModal(false); setHidePreview(false); }} onCloseAll={() => { setOpenUpdateModal(false); onClose({}, "backdropClick"); }} eventId={eventId} calId={calId} typeOfAction={(() => { // Get typeOfAction from state or sessionStorage if (typeOfAction) { return typeOfAction; } // Fallback: try to get typeOfAction from sessionStorage if state is not set yet try { const stored = sessionStorage.getItem("eventUpdateModalReopen"); if (stored) { const data = JSON.parse(stored); if ( data.eventId === eventId && data.calId === calId && data.typeOfAction ) { return data.typeOfAction; } } } catch { // Ignore } return undefined; })()} /> {}} calendarRef={{ current: null }} onClose={() => { setOpenDuplicateModal(false); onClose({}, "backdropClick"); // Close preview modal as well }} event={event} /> ); } function makeRecurrenceString( event: CalendarEvent, t: (k: string, p?: string | object) => string ): string | undefined { if (!event.repetition) return; const recur: string[] = [ `${t("eventPreview.recurrentEvent")} · ${t( `eventPreview.freq.${event.repetition.freq}`, event.repetition.freq )}`, ]; const recurType: Record = { daily: t("eventPreview.days"), monthly: t("eventPreview.months"), yearly: t("eventPreview.years"), }; if (event.repetition.byday) { recur.push( t("eventPreview.recurrenceOnDays", { days: event.repetition.byday .map((s) => t(`eventPreview.onDays.${s}`)) .join(", "), }) ); } if (event.repetition.interval && event.repetition.interval > 1) { recur.push( t("eventPreview.everyInterval", { interval: event.repetition.interval, unit: recurType[event.repetition.freq], }) ); } if (event.repetition.occurrences) { recur.push( t("eventPreview.forOccurrences", { count: event.repetition.occurrences, }) ); } if (event.repetition.endDate) { recur.push( t("eventPreview.until", { date: new Date(event.repetition.endDate).toLocaleDateString( t("locale"), { year: "numeric", month: "long", day: "numeric", } ), }) ); } return recur.join(", "); } function formatDate( date: Date | string, t: (k: string, p?: string | object) => string, timeZone: string, allday?: boolean ) { if (allday) { return new Date(date).toLocaleDateString(t("locale"), { year: "numeric", month: "long", weekday: "long", day: "numeric", timeZone, }); } else { return new Date(date).toLocaleString(t("locale"), { year: "numeric", month: "long", weekday: "long", day: "numeric", hour: "2-digit", minute: "2-digit", hour12: false, timeZone, }); } } function formatEnd( start: Date | string, end: Date | string, t: (k: string, p?: string | object) => string, timeZone: string, allday?: boolean ) { const startDate = new Date(start); const endDate = new Date(end); const formatDatePart = (d: Date) => d.toLocaleDateString("en-CA", { timeZone }); // YYYY-MM-DD format if (allday) { const inclusiveEndDate = new Date(endDate); inclusiveEndDate.setDate(inclusiveEndDate.getDate() - 1); const sameDay = formatDatePart(startDate) === formatDatePart(inclusiveEndDate); return sameDay ? null : inclusiveEndDate.toLocaleDateString(t("locale"), { year: "numeric", month: "short", day: "numeric", timeZone, }); } else { const sameDay = formatDatePart(startDate) === formatDatePart(endDate); if (sameDay) { return endDate.toLocaleTimeString(t("locale"), { hour: "2-digit", minute: "2-digit", hour12: false, timeZone, }); } return endDate.toLocaleString(t("locale"), { year: "numeric", month: "short", day: "numeric", hour: "2-digit", minute: "2-digit", hour12: false, timeZone, }); } } export function makeAttendeePreview( attendees: userAttendee[], t: (k: string, p?: string | object) => string ) { const attendeePreview = []; const yesCount = attendees?.filter((a) => a.partstat === "ACCEPTED").length; const noCount = attendees?.filter((a) => a.partstat === "DECLINED").length; const maybeCount = attendees?.filter( (a) => a.partstat === "TENTATIVE" ).length; const needActionCount = attendees?.filter( (a) => a.partstat === "NEEDS-ACTION" ).length; if (yesCount) { attendeePreview.push(t("eventPreview.yesCount", { count: yesCount })); } if (maybeCount) { attendeePreview.push(t("eventPreview.maybeCount", { count: maybeCount })); } if (needActionCount) { attendeePreview.push( t("eventPreview.needActionCount", { count: needActionCount }) ); } if (noCount) { attendeePreview.push(t("eventPreview.noCount", { count: noCount })); } return attendeePreview.join(", "); }