1 import { addDays, isSameDay } from 'date-fns';
4 addZoomInfoToDescription,
5 removeZoomInfoFromDescription,
6 } from '@proton/calendar/components/videoConferencing/zoom/zoomHelpers';
7 import { dedupeNotifications } from '@proton/shared/lib/calendar/alarms';
8 import { modelToValarmComponent } from '@proton/shared/lib/calendar/alarms/modelToValarm';
9 import { ICAL_EVENT_STATUS, MAX_CHARS_API } from '@proton/shared/lib/calendar/constants';
15 } from '@proton/shared/lib/calendar/vcalConverter';
16 import { withRequiredProperties } from '@proton/shared/lib/calendar/veventHelper';
17 import { omit } from '@proton/shared/lib/helpers/object';
18 import type { DateTimeModel, EventModel } from '@proton/shared/lib/interfaces/calendar';
19 import type { VcalVeventComponent } from '@proton/shared/lib/interfaces/calendar/VcalModel';
21 import modelToFrequencyProperties from './modelToFrequencyProperties';
23 export const modelToDateProperty = ({ date, time, tzid }: DateTimeModel, isAllDay: boolean) => {
25 year: date.getFullYear(),
26 month: date.getMonth() + 1,
31 return getDateProperty(dateObject);
34 const dateTimeObject = {
36 hours: time.getHours(),
37 minutes: time.getMinutes(),
41 return getDateTimeProperty(dateTimeObject, tzid);
44 const modelToDateProperties = ({ start, end, isAllDay }: EventModel) => {
45 const dtstart = modelToDateProperty(start, isAllDay);
47 // All day events date ranges are stored non-inclusively, so add a full day from the selected date to the end date
48 const modifiedEnd = isAllDay ? { ...end, date: addDays(end.date, 1) } : end;
49 const dtend = modelToDateProperty(modifiedEnd, isAllDay);
51 const ignoreDtend = isAllDay
52 ? isSameDay(start.date, end.date)
53 : +propertyToUTCDate(dtstart) === +propertyToUTCDate(dtend);
55 return ignoreDtend ? { dtstart } : { dtstart, dtend };
58 export const modelToGeneralProperties = ({
65 }: Partial<EventModel>): Omit<VcalVeventComponent, 'dtstart' | 'dtend'> => {
66 const properties = omit(rest, ['dtstart', 'dtend']);
69 properties.summary = { value: title.trim().slice(0, MAX_CHARS_API.TITLE) };
73 properties.uid = { value: uid };
77 properties.location = { value: location.slice(0, MAX_CHARS_API.LOCATION) };
81 properties.color = { value: color };
84 properties.status = { value: status || ICAL_EVENT_STATUS.CONFIRMED };
89 const modelToOrganizerProperties = ({ organizer }: EventModel) => {
90 const organizerEmail = organizer?.email;
91 if (!organizerEmail) {
95 organizer: buildVcalOrganizer(organizerEmail, organizer?.cn || organizerEmail),
99 const modelToAttendeeProperties = ({ attendees }: EventModel) => {
100 if (!Array.isArray(attendees) || !attendees.length) {
104 attendee: attendees.map(({ email, rsvp, role, token, partstat }) => ({
107 // cutype: 'INDIVIDUAL',
118 const modelToVideoConferenceProperties = ({
123 }: Partial<EventModel>) => {
124 if (!conferenceId || !conferenceUrl) {
129 'x-pm-conference-id': {
135 'x-pm-conference-url': {
136 value: conferenceUrl,
138 ...(conferencePassword && { password: conferencePassword }),
139 ...(conferenceHost && { host: conferenceHost }),
145 const modelToDescriptionProperties = ({
151 }: Partial<EventModel>) => {
152 const hasZoom = !!(conferenceUrl && conferenceId);
154 // Return an empty object if there is no description and no Zoom meeting
155 if (!description && !hasZoom) {
159 // Return the description if there is no Zoom meeting
160 if (description && !hasZoom) {
161 const cleanedDescription = removeZoomInfoFromDescription(description ?? '');
162 return { description: { value: cleanedDescription?.slice(0, MAX_CHARS_API.EVENT_DESCRIPTION) } };
165 // We remove the Zoom info from the description to avoid saving it twice
166 const cleanedDescription = removeZoomInfoFromDescription(description ?? '');
167 // We slice the description smaller to avoid too long descriptions with the generated Zoom info
168 const slicedDescription = cleanedDescription?.slice(0, MAX_CHARS_API.EVENT_DESCRIPTION);
169 const newDescription = addZoomInfoToDescription({
170 host: conferenceHost,
171 meedingURL: conferenceUrl,
172 password: conferencePassword,
173 meetingId: conferenceId,
174 description: slicedDescription,
178 description: { value: newDescription },
182 export const modelToValarmComponents = ({ isAllDay, fullDayNotifications, partDayNotifications }: EventModel) =>
183 dedupeNotifications(isAllDay ? fullDayNotifications : partDayNotifications).map((notification) =>
184 modelToValarmComponent(notification)
187 export const modelToVeventComponent = (model: EventModel) => {
188 const dateProperties = modelToDateProperties(model);
189 const frequencyProperties = modelToFrequencyProperties(model);
190 const organizerProperties = modelToOrganizerProperties(model);
191 const attendeeProperties = modelToAttendeeProperties(model);
192 const generalProperties = modelToGeneralProperties(model);
193 const valarmComponents = modelToValarmComponents(model);
194 const descriptionProperties = modelToDescriptionProperties(model);
195 const videoConferenceProperties = modelToVideoConferenceProperties(model);
197 return withRequiredProperties({
198 ...generalProperties,
199 ...frequencyProperties,
201 ...organizerProperties,
202 ...attendeeProperties,
203 ...videoConferenceProperties,
204 ...descriptionProperties,
206 components: [...valarmComponents],