TF-2035 Show calendar event banner in email view

(cherry picked from commit 130f575da86e23095d2b626ff8af133a7122ab2e)
This commit is contained in:
dab246
2023-07-24 19:40:09 +07:00
committed by Dat Vu
parent 41e0f841da
commit 588614f034
16 changed files with 570 additions and 22 deletions
@@ -1,8 +1,17 @@
import 'package:collection/collection.dart';
import 'package:jmap_dart_client/jmap/core/id.dart';
import 'package:model/email/attachment.dart';
import 'package:tmail_ui_user/features/email/domain/extensions/attachment_extension.dart';
import 'package:tmail_ui_user/features/offline_mode/model/attachment_hive_cache.dart';
extension ListAttachmentsExtension on List<Attachment> {
List<AttachmentHiveCache> toHiveCache() => map((attachment) => attachment.toHiveCache()).toList();
Set<Attachment> get calendarAttachments => where((attachment) => attachment.isCalendarEvent).toSet();
Set<Id> get calendarEventBlobIds => calendarAttachments
.map((attachment) => attachment.blobId)
.whereNotNull()
.toSet();
}
@@ -1,4 +1,5 @@
import 'package:get/get.dart';
import 'package:jmap_dart_client/http/http_client.dart';
import 'package:tmail_ui_user/features/base/interactors_bindings.dart';
import 'package:tmail_ui_user/features/email/data/datasource/calendar_event_datasource.dart';
import 'package:tmail_ui_user/features/email/data/datasource_impl/calendar_event_datasource_impl.dart';
@@ -17,6 +18,7 @@ class CalendarEventInteractorBindings extends InteractorsBindings {
@override
void bindingsDataSourceImpl() {
Get.lazyPut(() => CalendarEventAPI(Get.find<HttpClient>()));
Get.lazyPut(() => CalendarEventDataSourceImpl(
Get.find<CalendarEventAPI>(),
Get.find<RemoteExceptionThrower>()));
@@ -6,7 +6,6 @@ import 'package:dartz/dartz.dart';
import 'package:dio/dio.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:get/get.dart';
import 'package:jmap_dart_client/jmap/account_id.dart';
@@ -14,6 +13,7 @@ import 'package:jmap_dart_client/jmap/core/capability/capability_identifier.dart
import 'package:jmap_dart_client/jmap/core/id.dart';
import 'package:jmap_dart_client/jmap/core/session/session.dart';
import 'package:jmap_dart_client/jmap/identities/identity.dart';
import 'package:jmap_dart_client/jmap/mail/calendar/calendar_event.dart';
import 'package:jmap_dart_client/jmap/mail/email/email.dart';
import 'package:jmap_dart_client/jmap/mail/email/email_address.dart';
import 'package:jmap_dart_client/jmap/mdn/disposition.dart';
@@ -27,7 +27,9 @@ import 'package:tmail_ui_user/features/base/base_controller.dart';
import 'package:tmail_ui_user/features/base/mixin/app_loader_mixin.dart';
import 'package:tmail_ui_user/features/composer/presentation/extensions/email_action_type_extension.dart';
import 'package:tmail_ui_user/features/destination_picker/presentation/model/destination_picker_arguments.dart';
import 'package:tmail_ui_user/features/email/domain/extensions/list_attachments_extension.dart';
import 'package:tmail_ui_user/features/email/domain/model/detailed_email.dart';
import 'package:tmail_ui_user/features/email/domain/state/parse_calendar_event_state.dart';
import 'package:tmail_ui_user/features/email/domain/usecases/parse_calendar_event_interactor.dart';
import 'package:tmail_ui_user/features/email/domain/usecases/store_opened_email_interactor.dart';
import 'package:tmail_ui_user/features/email/presentation/bindings/calendar_event_interactor_bindings.dart';
@@ -109,6 +111,8 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
final attachmentsExpandMode = ExpandMode.COLLAPSE.obs;
final emailContents = RxnString();
final attachments = <Attachment>[].obs;
final calendarEvent = Rxn<CalendarEvent>();
EmailId? _currentEmailId;
Identity? _identitySelected;
String? initialEmailContents;
@@ -175,6 +179,8 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
_sendReceiptToSenderSuccess(success);
} else if (success is CreateNewRuleFilterSuccess) {
_createNewRuleFilterSuccess(success);
} else if (success is ParseCalendarEventSuccess) {
_handleParseCalendarEventSuccess(success);
}
}
@@ -391,6 +397,8 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
initialEmailContents = success.emailContent;
attachments.value = success.attachments;
_loadCalendarEventAction(success.attachments.calendarEventBlobIds);
final isShowMessageReadReceipt = success.emailCurrent?.hasReadReceipt(mailboxDashBoardController.mapMailboxById) == true;
if (isShowMessageReadReceipt) {
_handleReadReceipt();
@@ -416,6 +424,8 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
initialEmailContents = success.emailContent;
attachments.value = success.attachments;
_loadCalendarEventAction(success.attachments.calendarEventBlobIds);
if (PlatformInfo.isMobile) {
final detailedEmail = DetailedEmail(
emailId: currentEmail!.id!,
@@ -461,6 +471,7 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
emailContents.value = null;
initialEmailContents = null;
attachments.clear();
calendarEvent.value = null;
}
PresentationMailbox? getMailboxContain(PresentationEmail email) {
@@ -1229,4 +1240,45 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
consumeState(_storeOpenedEmailInteractor.execute(session, accountId, detailedEmail));
}
}
void _loadCalendarEventAction(Set<Id> blobIds) {
log('SingleEmailController::_loadCalendarEventAction:blobIds: $blobIds');
if (_isCalendarEventSupported) {
if (currentEmail?.hasCalendarEvent == true &&
blobIds.isNotEmpty &&
mailboxDashBoardController.accountId.value != null) {
_parseCalendarEventAction(
mailboxDashBoardController.accountId.value!,
blobIds
);
} else {
logError('SingleEmailController::_loadCalendarEventAction: not found calendar event header');
}
} else {
logError('SingleEmailController::_loadCalendarEventAction: calendar event not supported');
}
}
bool get _isCalendarEventSupported {
final accountId = mailboxDashBoardController.accountId.value;
final session = mailboxDashBoardController.sessionCurrent;
return session != null &&
accountId != null &&
CapabilityIdentifier.jamesCalendarEvent.isSupported(session, accountId);
}
void _parseCalendarEventAction(AccountId accountId, Set<Id> blobIds) {
log("SingleEmailController::_parseCalendarEventAction:blobIds: $blobIds");
if (_parseCalendarEventInteractor != null) {
consumeState(_parseCalendarEventInteractor!.execute(accountId, blobIds));
} else {
logError("SingleEmailController::_parseCalendarEventAction: _parseCalendarEventInteractor is NULL");
}
}
void _handleParseCalendarEventSuccess(ParseCalendarEventSuccess success) {
if (success.calendarEventList.isNotEmpty) {
calendarEvent.value = success.calendarEventList.first;
}
}
}
+42 -20
View File
@@ -1,6 +1,5 @@
import 'package:core/presentation/extensions/color_extension.dart';
import 'package:core/presentation/resources/image_paths.dart';
import 'package:core/presentation/state/success.dart';
import 'package:core/presentation/utils/icon_utils.dart';
import 'package:core/presentation/utils/responsive_utils.dart';
import 'package:core/presentation/views/button/icon_button_web.dart';
@@ -11,7 +10,6 @@ import 'package:core/utils/app_logger.dart';
import 'package:core/utils/direction_utils.dart';
import 'package:core/utils/platform_info.dart';
import 'package:filesize/filesize.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:get/get.dart';
@@ -21,12 +19,17 @@ import 'package:model/email/presentation_email.dart';
import 'package:model/extensions/list_attachment_extension.dart';
import 'package:model/extensions/presentation_email_extension.dart';
import 'package:model/mailbox/presentation_mailbox.dart';
import 'package:tmail_ui_user/features/base/mixin/app_loader_mixin.dart';
import 'package:tmail_ui_user/features/base/widget/custom_scroll_behavior.dart';
import 'package:tmail_ui_user/features/base/widget/popup_item_widget.dart';
import 'package:tmail_ui_user/features/email/domain/state/get_email_content_state.dart';
import 'package:tmail_ui_user/features/email/domain/state/parse_calendar_event_state.dart';
import 'package:tmail_ui_user/features/email/presentation/controller/single_email_controller.dart';
import 'package:tmail_ui_user/features/email/presentation/styles/email_view_styles.dart';
import 'package:tmail_ui_user/features/email/presentation/widgets/app_bar_mail_widget_builder.dart';
import 'package:tmail_ui_user/features/email/presentation/widgets/attachment_file_tile_builder.dart';
import 'package:tmail_ui_user/features/email/presentation/widgets/bottom_bar_mail_widget_builder.dart';
import 'package:tmail_ui_user/features/email/presentation/widgets/calendar_event_action_banner_widget.dart';
import 'package:tmail_ui_user/features/email/presentation/widgets/email_action_cupertino_action_sheet_action_builder.dart';
import 'package:tmail_ui_user/features/email/presentation/widgets/information_sender_and_receiver_builder.dart';
import 'package:tmail_ui_user/features/manage_account/presentation/extensions/vacation_response_extension.dart';
@@ -34,7 +37,7 @@ import 'package:tmail_ui_user/features/manage_account/presentation/vacation/widg
import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
import 'package:tmail_ui_user/main/utils/app_utils.dart';
class EmailView extends GetWidget<SingleEmailController> {
class EmailView extends GetWidget<SingleEmailController> with AppLoaderMixin {
final responsiveUtils = Get.find<ResponsiveUtils>();
final imagePaths = Get.find<ImagePaths>();
@@ -278,38 +281,57 @@ class EmailView extends GetWidget<SingleEmailController> {
imagePaths: imagePaths,
responsiveUtils: responsiveUtils,
),
_buildLoadingView(),
_buildLoadingContentView(),
_buildAttachments(context),
Obx(() {
if (controller.calendarEvent.value != null) {
return CalendarEventActionBannerWidget(
calendarEvent: controller.calendarEvent.value!,
listFromEmailAddress: controller.currentEmail?.from
);
} else {
return _buildLoadingCalendarEventBanner();
}
}),
if (PlatformInfo.isWeb)
Expanded(child: Padding(
padding: EdgeInsets.only(
left: AppUtils.isDirectionRTL(context) ? 0 : 16,
right: AppUtils.isDirectionRTL(context) ? 16 : 0,
bottom: 16
),
padding: const EdgeInsetsDirectional.only(start: 16, bottom: 16),
child: _buildEmailContent(context, constraints, email)
))
else
Padding(
padding: const EdgeInsets.all(16),
child: _buildEmailContent(context, constraints, email))
padding: const EdgeInsetsDirectional.symmetric(
vertical: EmailViewStyles.mobileContentVerticalMargin,
horizontal: EmailViewStyles.mobileContentHorizontalMargin
),
child: _buildEmailContent(context, constraints, email)
)
],
);
});
}
Widget _buildLoadingView() {
Widget _buildLoadingContentView() {
return Obx(() {
return controller.viewState.value.fold(
(failure) => const SizedBox.shrink(),
(success) {
if (success is LoadingState) {
return const Align(alignment: Alignment.topCenter, child: Padding(
padding: EdgeInsets.all(16),
child: SizedBox(
width: 30,
height: 30,
child: CupertinoActivityIndicator(color: AppColor.colorLoading))));
if (success is GetEmailContentLoading) {
return loadingWidget;
} else {
return const SizedBox.shrink();
}
});
});
}
Widget _buildLoadingCalendarEventBanner() {
return Obx(() {
return controller.viewState.value.fold(
(failure) => const SizedBox.shrink(),
(success) {
if (success is ParseCalendarEventLoading) {
return loadingWidget;
} else {
return const SizedBox.shrink();
}
@@ -329,7 +351,7 @@ class EmailView extends GetWidget<SingleEmailController> {
Widget _buildAttachmentsBody(BuildContext context, List<Attachment> attachments) {
return Container(
color: Colors.white,
padding: const EdgeInsets.only(left: 16, right: 16, bottom: 12, top: 10),
padding: const EdgeInsetsDirectional.symmetric(vertical: 12, horizontal: 16),
child: Column(crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildAttachmentsHeader(context, attachments),
@@ -0,0 +1,178 @@
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:flutter/material.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/event_method.dart';
import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
extension CalendarEventExtension on CalendarEvent {
Color getColorEventActionBanner(String senderEmailAddress) {
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(senderEmailAddress);
if (matchedAttendee != null) {
return getAttendeeMessageTextColor(matchedAttendee.participationStatus);
} else {
return Colors.transparent;
}
default:
return Colors.transparent;
}
}
Color getColorEventActionText(String senderEmailAddress) {
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(senderEmailAddress);
if (matchedAttendee != null) {
return getAttendeeMessageTextColor(matchedAttendee.participationStatus);
} else {
return Colors.transparent;
}
default:
return Colors.transparent;
}
}
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, String senderEmailAddress) {
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(senderEmailAddress);
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 getSubTitleEventAction(BuildContext context) {
switch(method) {
case EventMethod.refresh:
return AppLocalizations.of(context).subMessageEventActionBannerUpdated;
case EventMethod.cancel:
return AppLocalizations.of(context).subMessageEventActionBannerCanceled;
default:
return '';
}
}
String getUserNameEventAction({
required BuildContext context,
required ImagePaths imagePaths,
required String senderEmailAddress
}) {
switch(method) {
case EventMethod.request:
case EventMethod.add:
case EventMethod.refresh:
case EventMethod.cancel:
case EventMethod.declineCounter:
return getOrganizerName(context);
case EventMethod.reply:
case EventMethod.counter:
return getAttendeeName(context, senderEmailAddress);
default:
return '';
}
}
String getOrganizerName(BuildContext context) => organizer?.name ?? AppLocalizations.of(context).you;
String getAttendeeName(BuildContext context, String senderEmailAddress) {
final matchedAttendee = findAttendeeHasUpdatedStatus(senderEmailAddress);
if (matchedAttendee != null) {
return matchedAttendee.name?.name ?? AppLocalizations.of(context).anAttendee;
} else {
return AppLocalizations.of(context).anAttendee;
}
}
CalendarAttendee? findAttendeeHasUpdatedStatus(String senderEmailAddress) {
if (participants?.isNotEmpty == true) {
final listMatchedAttendee = participants
!.where((attendee) => attendee.mailto?.mailAddress.value == senderEmailAddress)
.whereNotNull();
log('CalendarEventExtension::findAttendeeHasUpdatedStatus:listMatchedAttendee: $listMatchedAttendee');
if (listMatchedAttendee.isNotEmpty) {
return listMatchedAttendee.first;
}
}
return null;
}
String getAttendeeMessageStatus(BuildContext context, CalendarAttendeeParticipationStatus? status) {
if (status == CalendarAttendeeParticipationStatus('ACCEPTED')) {
return AppLocalizations.of(context).messageEventActionBannerAttendeeAccepted;
} else if (status == CalendarAttendeeParticipationStatus('TENTATIVE')) {
return AppLocalizations.of(context).messageEventActionBannerAttendeeTentative;
} else if (status == CalendarAttendeeParticipationStatus('DECLINED')) {
return AppLocalizations.of(context).messageEventActionBannerAttendeeDeclined;
} else {
return '';
}
}
Color getAttendeeMessageTextColor(CalendarAttendeeParticipationStatus? status) {
if (status == CalendarAttendeeParticipationStatus('ACCEPTED')) {
return AppColor.colorUpdatedEventActionText;
} else if (status == CalendarAttendeeParticipationStatus('TENTATIVE')) {
return AppColor.colorMaybeEventActionText;
} else if (status == CalendarAttendeeParticipationStatus('DECLINED')) {
return AppColor.colorCanceledEventActionText;
} else {
return Colors.transparent;
}
}
}
@@ -0,0 +1,7 @@
class CalendarEventActionBannerStyles {
static const double borderRadius = 16;
static const double contentPadding = 16;
static const double viewHorizontalMargin = 16;
static const double viewVerticalMargin = 12;
}
@@ -0,0 +1,5 @@
class EmailViewStyles {
static const double mobileContentHorizontalMargin = 16;
static const double mobileContentVerticalMargin = 12;
}
@@ -0,0 +1,104 @@
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:flutter/cupertino.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:get/get.dart';
import 'package:jmap_dart_client/jmap/mail/calendar/calendar_event.dart';
import 'package:jmap_dart_client/jmap/mail/email/email_address.dart';
import 'package:model/extensions/email_address_extension.dart';
import 'package:tmail_ui_user/features/email/presentation/extensions/calendar_event_extension.dart';
import 'package:tmail_ui_user/features/email/presentation/styles/calendar_event_action_banner_styles.dart';
class CalendarEventActionBannerWidget extends StatelessWidget {
final CalendarEvent calendarEvent;
final Set<EmailAddress>? listFromEmailAddress;
const CalendarEventActionBannerWidget({
super.key,
required this.calendarEvent,
required this.listFromEmailAddress,
});
@override
Widget build(BuildContext context) {
final imagePaths = Get.find<ImagePaths>();
return Container(
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(CalendarEventActionBannerStyles.borderRadius)),
color: calendarEvent.getColorEventActionBanner(_getSenderEmailAddress()).withOpacity(0.12)
),
padding: const EdgeInsets.all(CalendarEventActionBannerStyles.contentPadding),
margin: const EdgeInsets.symmetric(
vertical: CalendarEventActionBannerStyles.viewVerticalMargin,
horizontal: CalendarEventActionBannerStyles.viewHorizontalMargin,
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (calendarEvent.getIconEventAction(imagePaths).isNotEmpty)
Padding(
padding: const EdgeInsetsDirectional.only(end: 8),
child: SvgPicture.asset(
calendarEvent.getIconEventAction(imagePaths),
width: 24,
height: 24,
fit: BoxFit.fill,
),
),
Expanded(child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
RichText(
text: TextSpan(
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w400,
color: calendarEvent.getColorEventActionText(_getSenderEmailAddress())
),
children: [
TextSpan(
text: calendarEvent.getUserNameEventAction(
context: context,
imagePaths: imagePaths,
senderEmailAddress: _getSenderEmailAddress()
),
style: TextStyle(
color: calendarEvent.getColorEventActionText(_getSenderEmailAddress()),
fontSize: 20,
fontWeight: FontWeight.w700
),
),
TextSpan(text: calendarEvent.getTitleEventAction(context, _getSenderEmailAddress()))
]
)
),
if (calendarEvent.getSubTitleEventAction(context).isNotEmpty)
Text(
calendarEvent.getSubTitleEventAction(context),
style: const TextStyle(
color: AppColor.colorSubTitleEventActionText,
fontSize: 16,
fontWeight: FontWeight.w400
),
)
]
))
]
),
);
}
String _getSenderEmailAddress() {
if (listFromEmailAddress?.isNotEmpty == true) {
final senderEmailAddress = listFromEmailAddress!.first.emailAddress;
log('CalendarEventActionBannerWidget::getSenderEmailAddress: $senderEmailAddress');
return senderEmailAddress;
} else {
return '';
}
}
}