import 'package:collection/collection.dart'; import 'package:core/presentation/extensions/color_extension.dart'; import 'package:core/presentation/resources/image_paths.dart'; import 'package:core/utils/app_logger.dart'; import 'package:date_format/date_format.dart' as date_format; import 'package:flutter/material.dart'; import 'package:jmap_dart_client/jmap/core/utc_date.dart'; import 'package:jmap_dart_client/jmap/mail/calendar/calendar_event.dart'; import 'package:jmap_dart_client/jmap/mail/calendar/properties/attendee/calendar_attendee.dart'; import 'package:jmap_dart_client/jmap/mail/calendar/properties/attendee/calendar_attendee_participation_status.dart'; import 'package:jmap_dart_client/jmap/mail/calendar/properties/calendar_duration.dart'; import 'package:jmap_dart_client/jmap/mail/calendar/properties/calendar_event_status.dart'; import 'package:jmap_dart_client/jmap/mail/calendar/properties/calendar_extension_fields.dart'; import 'package:jmap_dart_client/jmap/mail/calendar/properties/calendar_free_busy_status.dart'; import 'package:jmap_dart_client/jmap/mail/calendar/properties/calendar_organizer.dart'; import 'package:jmap_dart_client/jmap/mail/calendar/properties/calendar_priority.dart'; import 'package:jmap_dart_client/jmap/mail/calendar/properties/calendar_privacy.dart'; import 'package:jmap_dart_client/jmap/mail/calendar/properties/calendar_sequence.dart'; import 'package:jmap_dart_client/jmap/mail/calendar/properties/event_id.dart'; import 'package:jmap_dart_client/jmap/mail/calendar/properties/event_method.dart'; import 'package:jmap_dart_client/jmap/mail/calendar/properties/recurrence_rule/recurrence_rule.dart'; import 'package:tmail_ui_user/features/email/domain/model/event_action.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; import 'package:tmail_ui_user/main/utils/app_utils.dart'; extension CalendarEventExtension on CalendarEvent { static const String acceptedParticipationStatus = 'ACCEPTED'; static const String tentativeParticipationStatus = 'TENTATIVE'; static const String declinedParticipationStatus = 'DECLINED'; Color? getColorEventActionBanner(List listEmailAddressSender) { switch(method) { case EventMethod.request: case EventMethod.add: return AppColor.colorInvitedEventActionBanner; case EventMethod.refresh: case EventMethod.counter: return AppColor.colorUpdatedEventActionBanner; case EventMethod.cancel: case EventMethod.declineCounter: return AppColor.colorCanceledEventActionBanner; case EventMethod.reply: final matchedAttendee = findAttendeeHasUpdatedStatus(listEmailAddressSender); if (matchedAttendee != null) { return getAttendeeMessageBannerColor(matchedAttendee.participationStatus); } else { return null; } default: return null; } } Color? getColorEventActionText(List listEmailAddressSender) { switch(method) { case EventMethod.request: case EventMethod.add: return AppColor.colorInvitedEventActionText; case EventMethod.refresh: case EventMethod.counter: return AppColor.colorUpdatedEventActionText; case EventMethod.cancel: case EventMethod.declineCounter: return AppColor.colorCanceledEventActionText; case EventMethod.reply: final matchedAttendee = findAttendeeHasUpdatedStatus(listEmailAddressSender); if (matchedAttendee != null) { return getAttendeeMessageTextColor(matchedAttendee.participationStatus); } else { return null; } default: return null; } } String getIconEventAction(ImagePaths imagePaths) { switch(method) { case EventMethod.request: case EventMethod.add: return imagePaths.icEventInvited; case EventMethod.refresh: return imagePaths.icEventUpdated; case EventMethod.cancel: return imagePaths.icEventCanceled; default: return ''; } } String getTitleEventAction(BuildContext context, List listEmailAddressSender) { switch(method) { case EventMethod.request: case EventMethod.add: return AppLocalizations.of(context).messageEventActionBannerOrganizerInvited; case EventMethod.refresh: return AppLocalizations.of(context).messageEventActionBannerOrganizerUpdated; case EventMethod.cancel: return AppLocalizations.of(context).messageEventActionBannerOrganizerCanceled; case EventMethod.reply: final matchedAttendee = findAttendeeHasUpdatedStatus(listEmailAddressSender); if (matchedAttendee != null) { return getAttendeeMessageStatus(context, matchedAttendee.participationStatus); } else { return ''; } case EventMethod.counter: return AppLocalizations.of(context).messageEventActionBannerAttendeeCounter; case EventMethod.declineCounter: return AppLocalizations.of(context).messageEventActionBannerAttendeeCounterDeclined; default: return ''; } } String getUserNameEventAction({ required BuildContext context, required ImagePaths imagePaths, required List listEmailAddressSender }) { switch(method) { case EventMethod.request: case EventMethod.add: case EventMethod.refresh: case EventMethod.cancel: case EventMethod.declineCounter: return organizerName; case EventMethod.reply: case EventMethod.counter: return getAttendeeNameEvent(context, listEmailAddressSender); default: return ''; } } String get organizerName => organizer?.name ?? organizer?.mailto?.value ?? ''; String getAttendeeNameEvent(BuildContext context, List listEmailAddressSender) { final matchedAttendee = findAttendeeHasUpdatedStatus(listEmailAddressSender); if (matchedAttendee != null) { return matchedAttendee.name?.name ?? AppLocalizations.of(context).anAttendee; } else { return AppLocalizations.of(context).anAttendee; } } CalendarAttendee? findAttendeeHasUpdatedStatus(List listEmailAddressSender) { if (participants?.isNotEmpty == true) { final listMatchedAttendee = participants !.where((attendee) => attendee.mailto != null && listEmailAddressSender.contains(attendee.mailto!.mailAddress.value)) .nonNulls; log('CalendarEventExtension::findAttendeeHasUpdatedStatus:listMatchedAttendee: $listMatchedAttendee'); if (listMatchedAttendee.isNotEmpty) { return listMatchedAttendee.first; } } return null; } String getAttendeeMessageStatus(BuildContext context, CalendarAttendeeParticipationStatus? status) { if (status == CalendarAttendeeParticipationStatus(acceptedParticipationStatus)) { return AppLocalizations.of(context).messageEventActionBannerAttendeeAccepted; } else if (status == CalendarAttendeeParticipationStatus(tentativeParticipationStatus)) { return AppLocalizations.of(context).messageEventActionBannerAttendeeTentative; } else if (status == CalendarAttendeeParticipationStatus(declinedParticipationStatus)) { return AppLocalizations.of(context).messageEventActionBannerAttendeeDeclined; } else { return ''; } } Color? getAttendeeMessageBannerColor(CalendarAttendeeParticipationStatus? status) { if (status == CalendarAttendeeParticipationStatus(acceptedParticipationStatus)) { return AppColor.colorUpdatedEventActionBanner; } else if (status == CalendarAttendeeParticipationStatus(tentativeParticipationStatus)) { return AppColor.colorMaybeEventActionBanner; } else if (status == CalendarAttendeeParticipationStatus(declinedParticipationStatus)) { return AppColor.colorCanceledEventActionBanner; } else { return null; } } Color? getAttendeeMessageTextColor(CalendarAttendeeParticipationStatus? status) { if (status == CalendarAttendeeParticipationStatus(acceptedParticipationStatus)) { return AppColor.colorUpdatedEventActionText; } else if (status == CalendarAttendeeParticipationStatus(tentativeParticipationStatus)) { return AppColor.colorMaybeEventActionText; } else if (status == CalendarAttendeeParticipationStatus(declinedParticipationStatus)) { return AppColor.colorCanceledEventActionText; } else { return null; } } DateTime? get localStartDate => startUtcDate?.value.toLocal(); DateTime? get localEndDate => endUtcDate?.value.toLocal(); String get monthStartDateAsString { if (localStartDate != null) { return date_format.formatDate( localStartDate!, [date_format.M], locale: AppUtils.getCurrentDateLocale() ); } else { return ''; } } String get dayStartDateAsString { if (localStartDate != null) { return date_format.formatDate( localStartDate!, [date_format.d], locale: AppUtils.getCurrentDateLocale() ); } else { return ''; } } String get weekDayStartDateAsString { if (localStartDate != null) { return date_format.formatDate( localStartDate!, [date_format.D], locale: AppUtils.getCurrentDateLocale() ); } else { return ''; } } String formatDateTime(date_format.DateLocale locale, DateTime dateTime) { return date_format.formatDate( dateTime, [ date_format.DD, ', ', date_format.MM, ' ', date_format.dd, ', ', date_format.yyyy, ' ', date_format.hh, ':', date_format.nn, ' ', date_format.am ], locale: locale ); } String formatTime(date_format.DateLocale locale, DateTime dateTime) { return date_format.formatDate( dateTime, [ date_format.hh, ':', date_format.nn, ' ', date_format.am ], locale: locale ); } String formatDate(date_format.DateLocale locale, DateTime dateTime) { return date_format.formatDate( dateTime, [ date_format.DD, ', ', date_format.MM, ' ', date_format.dd, ', ', date_format.yyyy ], locale: locale ); } bool get isAllDayEvent { if (startUtcDate != null && endUtcDate != null) { final startDateValue = startUtcDate!.value; final endDateValue = endUtcDate!.value; final eventDurationInHours = startDateValue.difference(endDateValue).inHours; final startHour = startDateValue.hour; final startMinute = startDateValue.minute; final startSecond = startDateValue.second; final endHour = endDateValue.hour; final endMinute = endDateValue.minute; final endSecond = endDateValue.second; return startHour == 0 && startMinute == 0 && startSecond == 0 && endHour == 0 && endMinute == 0 && endSecond == 0 && eventDurationInHours % 24 == 0; } return false; } String dateTimeStringForAllDayEvent({ required DateTime startDate, required DateTime endDate, required date_format.DateLocale dateLocale, required String timeZone }) { final dateStart = formatDate(dateLocale, startDate); final endDateToDisplay = endDate.subtract(const Duration(days: 1)); final dateEnd = formatDate(dateLocale, endDateToDisplay); if (DateUtils.isSameDay(startDate, endDateToDisplay)) { return '$dateStart ($timeZone)'; } else { return '$dateStart - $dateEnd ($timeZone)'; } } String getDateTimeEvent({ required date_format.DateLocale dateLocale, required String timeZone }) { if (isAllDayEvent) { return dateTimeStringForAllDayEvent( startDate: startUtcDate!.value, endDate: endUtcDate!.value, dateLocale: dateLocale, timeZone: timeZone, ); } if (localStartDate != null && localEndDate != null) { final timeStart = formatDateTime(dateLocale, localStartDate!); final timeEnd = DateUtils.isSameDay(localStartDate, localEndDate) ? formatTime(dateLocale, localEndDate!) : formatDateTime(dateLocale, localEndDate!); return '$timeStart - $timeEnd'; } else if (localStartDate != null) { return formatDateTime(dateLocale, localStartDate!); } else if (localEndDate != null) { return formatDateTime(dateLocale, localEndDate!); } else { return ''; } } List get videoConferences { if (extensionFields != null && extensionFields!.mapFields.isNotEmpty) { final videoConferences = List.empty(growable: true); final openPaasVideoConferences = extensionFields?.mapFields['X-OPENPAAS-VIDEOCONFERENCE'] ?.nonNulls .where((link) => link.isNotEmpty) .toList() ?? []; log('CalendarEventExtension::openPaasVideoConferences: $openPaasVideoConferences'); final googleVideoConferences = extensionFields!.mapFields['X-GOOGLE-CONFERENCE'] ?.nonNulls .where((link) => link.isNotEmpty) .toList() ?? []; log('CalendarEventExtension::googleVideoConferences: $googleVideoConferences'); if (openPaasVideoConferences.isNotEmpty) { videoConferences.addAll(openPaasVideoConferences); } if (googleVideoConferences.isNotEmpty) { videoConferences.addAll(googleVideoConferences); } return videoConferences; } return []; } bool isDisplayedEventReplyAction(String ownerEmailAddress) => method != null && _methodIsRepliable && organizer != null && participants?.isNotEmpty == true && userIsListedInParticipants(ownerEmailAddress); bool get _methodIsRepliable => method == EventMethod.request || method == EventMethod.add || method == EventMethod.counter; bool userIsListedInParticipants(String ownEmailAddress) { final participant = participants?.firstWhereOrNull((participant) => participant.mailto?.mailAddress.value == ownEmailAddress); return participant != null; } bool userIsOrganizer(String ownEmailAddress) { return organizer?.mailto?.value == ownEmailAddress; } bool isDisplayedWarningMessage(String ownerEmailAddress) { return !userIsListedInParticipants(ownerEmailAddress) && !userIsOrganizer(ownerEmailAddress); } bool get isDisplayedMailToAttendees => organizer != null || participants?.isNotEmpty == true; List getEventActionTypesIsDisplayed( String ownerEmailAddress, ) { if (isDisplayedEventReplyAction(ownerEmailAddress)) { return [ if (method == EventMethod.counter) EventActionType.acceptCounter else ...[ EventActionType.yes, EventActionType.maybe, EventActionType.no, ], EventActionType.mailToAttendees, ]; } else if (isDisplayedMailToAttendees) { return [ EventActionType.mailToAttendees, ]; } else { return []; } } bool isIMIPResponsesAvailable(List listEmailAddressSender) { final participationStatus = findAttendeeHasUpdatedStatus( listEmailAddressSender, )?.participationStatus?.value; return method == EventMethod.reply && (participationStatus == acceptedParticipationStatus || participationStatus == tentativeParticipationStatus || participationStatus == declinedParticipationStatus); } CalendarEvent copyWith({ EventId? eventId, String? title, String? description, DateTime? startDate, DateTime? endDate, UTCDate? startUtcDate, UTCDate? endUtcDate, CalendarDuration? duration, String? timeZone, String? location, EventMethod? method, CalendarSequence? sequence, CalendarPrivacy? privacy, CalendarPriority? priority, CalendarFreeBusyStatus? freeBusyStatus, CalendarEventStatus? status, CalendarOrganizer? organizer, List? participants, CalendarExtensionFields? extensionFields, List? recurrenceRules, List? excludedCalendarEvents, }) { return CalendarEvent( eventId: eventId ?? this.eventId, title: title ?? this.title, description: description ?? this.description, startDate: startDate ?? this.startDate, endDate: endDate ?? this.endDate, startUtcDate: startUtcDate ?? this.startUtcDate, endUtcDate: endUtcDate ?? this.endUtcDate, duration: duration ?? this.duration, timeZone: timeZone ?? this.timeZone, location: location ?? this.location, method: method ?? this.method, sequence: sequence ?? this.sequence, privacy: privacy ?? this.privacy, priority: priority ?? this.priority, freeBusyStatus: freeBusyStatus ?? this.freeBusyStatus, status: status ?? this.status, organizer: organizer ?? this.organizer, participants: participants ?? this.participants, extensionFields: extensionFields ?? this.extensionFields, recurrenceRules: recurrenceRules ?? this.recurrenceRules, excludedCalendarEvents: excludedCalendarEvents ?? this.excludedCalendarEvents, ); } }