diff --git a/CHANGELOG.md b/CHANGELOG.md index 949e144abd..41c2579039 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +## [0.8.9] - 2023-07-31 +### Added +- Translate Russian and French +- Apply new view calendar event + +### Fixed +- \#2052 Fix TeamMailbox email address alignment is incorrect +- If no refreshToken return, maybe the old refreshToken still available + ## [0.8.8] - 2023-07-20 ### Fixed - \#2046 App crashes when login account information is incorrect on web @@ -199,6 +208,7 @@ - \#1512 remove plain text input for signature - \#1469 remove animation when navigating screen +[0.8.9]: https://github.com/linagora/tmail-flutter/releases/tag/v0.8.9 [0.8.8]: https://github.com/linagora/tmail-flutter/releases/tag/v0.8.8 [0.8.7]: https://github.com/linagora/tmail-flutter/releases/tag/v0.8.7 [0.8.6]: https://github.com/linagora/tmail-flutter/releases/tag/v0.8.6 diff --git a/assets/images/ic_calendar_event.svg b/assets/images/ic_calendar_event.svg new file mode 100644 index 0000000000..be965e4fdb --- /dev/null +++ b/assets/images/ic_calendar_event.svg @@ -0,0 +1,5 @@ + + + diff --git a/assets/images/ic_event_canceled.svg b/assets/images/ic_event_canceled.svg new file mode 100644 index 0000000000..7b011b5a95 --- /dev/null +++ b/assets/images/ic_event_canceled.svg @@ -0,0 +1,5 @@ + + + diff --git a/assets/images/ic_event_invited.svg b/assets/images/ic_event_invited.svg new file mode 100644 index 0000000000..4497cc4d99 --- /dev/null +++ b/assets/images/ic_event_invited.svg @@ -0,0 +1,5 @@ + + + diff --git a/assets/images/ic_event_updated.svg b/assets/images/ic_event_updated.svg new file mode 100644 index 0000000000..c339cd6a42 --- /dev/null +++ b/assets/images/ic_event_updated.svg @@ -0,0 +1,5 @@ + + + diff --git a/assets/images/ic_format_quote.svg b/assets/images/ic_format_quote.svg new file mode 100644 index 0000000000..6806d928a4 --- /dev/null +++ b/assets/images/ic_format_quote.svg @@ -0,0 +1,5 @@ + + + diff --git a/contact/pubspec.lock b/contact/pubspec.lock index f7d16f0fb9..9e14d21c45 100644 --- a/contact/pubspec.lock +++ b/contact/pubspec.lock @@ -492,7 +492,7 @@ packages: description: path: "." ref: master - resolved-ref: "973a57d1ffbc5237d63ea960b46e8921c6ac6a66" + resolved-ref: "63d47945f676adca3bf48b3940b21b77546e309e" url: "https://github.com/linagora/jmap-dart-client.git" source: git version: "0.0.1" diff --git a/core/lib/presentation/extensions/color_extension.dart b/core/lib/presentation/extensions/color_extension.dart index 7bb309437f..34b2157184 100644 --- a/core/lib/presentation/extensions/color_extension.dart +++ b/core/lib/presentation/extensions/color_extension.dart @@ -184,6 +184,19 @@ extension AppColor on Color { static const colorBackgroundDeliveringState = Color(0xFFF2F3F5); static const colorNetworkConnectionBannerBackground = Color(0x99EBEDF0); static const colorNetworkConnectionLabel = Color(0xFF818C99); + static const colorCalendarEventRead = Color(0xFF818C99); + static const colorCalendarEventUnread = Color(0xFF1C1B1F); + static const colorMaybeEventActionText = Color(0xFFFFC107); + static const colorInvitedEventActionText = Color(0xFF007AFF); + static const colorUpdatedEventActionText = Color(0xFF4BB34B); + static const colorCanceledEventActionText = Color(0xFFFF3347); + static const colorSubTitleEventActionText = Color(0xFF939393); + static const colorCalendarEventInformationBackground = Color(0x0A000000); + static const colorCalendarEventInformationStroke = Color(0x1F000000); + static const colorShadowCalendarDateIcon = Color(0x26000000); + static const colorOrganizerMailto = Color(0xFFB3B3B3); + static const colorMailto = Color(0xFFB3B3B3); + static const colorEventDescriptionBackground = Color(0x05000000); static const mapGradientColor = [ [Color(0xFF21D4FD), Color(0xFFB721FF)], diff --git a/core/lib/presentation/resources/image_paths.dart b/core/lib/presentation/resources/image_paths.dart index 39bc83c9e6..cea00d9f59 100644 --- a/core/lib/presentation/resources/image_paths.dart +++ b/core/lib/presentation/resources/image_paths.dart @@ -194,6 +194,11 @@ class ImagePaths { String get icArrowLeft => _getImagePath('ic_arrow_left.svg'); String get icArrowRight => _getImagePath('ic_arrow_right.svg'); String get icAddPicture => _getImagePath('ic_add_picture.svg'); + String get icCalendarEvent => _getImagePath('ic_calendar_event.svg'); + String get icEventInvited => _getImagePath('ic_event_invited.svg'); + String get icEventUpdated => _getImagePath('ic_event_updated.svg'); + String get icEventCanceled => _getImagePath('ic_event_canceled.svg'); + String get icFormatQuote => _getImagePath('ic_format_quote.svg'); String _getImagePath(String imageName) { return AssetsPaths.images + imageName; diff --git a/fcm/pubspec.lock b/fcm/pubspec.lock index 1273272758..22fedc3afe 100644 --- a/fcm/pubspec.lock +++ b/fcm/pubspec.lock @@ -296,7 +296,7 @@ packages: description: path: "." ref: master - resolved-ref: "973a57d1ffbc5237d63ea960b46e8921c6ac6a66" + resolved-ref: "63d47945f676adca3bf48b3940b21b77546e309e" url: "https://github.com/linagora/jmap-dart-client.git" source: git version: "0.0.1" diff --git a/forward/pubspec.lock b/forward/pubspec.lock index 1273272758..22fedc3afe 100644 --- a/forward/pubspec.lock +++ b/forward/pubspec.lock @@ -296,7 +296,7 @@ packages: description: path: "." ref: master - resolved-ref: "973a57d1ffbc5237d63ea960b46e8921c6ac6a66" + resolved-ref: "63d47945f676adca3bf48b3940b21b77546e309e" url: "https://github.com/linagora/jmap-dart-client.git" source: git version: "0.0.1" diff --git a/lib/features/base/base_controller.dart b/lib/features/base/base_controller.dart index 6ca202abc2..b3555b5b1d 100644 --- a/lib/features/base/base_controller.dart +++ b/lib/features/base/base_controller.dart @@ -22,7 +22,7 @@ import 'package:rule_filter/rule_filter/capability_rule_filter.dart'; import 'package:tmail_ui_user/features/base/mixin/message_dialog_action_mixin.dart'; import 'package:tmail_ui_user/features/base/mixin/popup_context_menu_action_mixin.dart'; import 'package:tmail_ui_user/features/caching/caching_manager.dart'; -import 'package:tmail_ui_user/features/email/presentation/mdn_interactor_bindings.dart'; +import 'package:tmail_ui_user/features/email/presentation/bindings/mdn_interactor_bindings.dart'; import 'package:tmail_ui_user/features/login/data/network/config/authorization_interceptors.dart'; import 'package:tmail_ui_user/features/login/domain/usecases/delete_authority_oidc_interactor.dart'; import 'package:tmail_ui_user/features/login/domain/usecases/delete_credential_interactor.dart'; diff --git a/lib/features/base/reloadable/reloadable_controller.dart b/lib/features/base/reloadable/reloadable_controller.dart index 35684e353b..e72d65fd79 100644 --- a/lib/features/base/reloadable/reloadable_controller.dart +++ b/lib/features/base/reloadable/reloadable_controller.dart @@ -140,7 +140,7 @@ abstract class ReloadableController extends BaseController { } void updateAuthenticationAccount(Session session, AccountId accountId, UserName userName) { - final apiUrl = session.getQualifiedApiUrl(baseUrl: _dynamicUrlInterceptors.jmapUrl);; + final apiUrl = session.getQualifiedApiUrl(baseUrl: _dynamicUrlInterceptors.jmapUrl); log('ReloadableController::updateAuthenticationAccount():apiUrl: $apiUrl'); if (apiUrl.isNotEmpty) { consumeState(_updateAuthenticationAccountInteractor.execute(accountId, apiUrl, userName)); diff --git a/lib/features/base/styles/cupertino_loading_widget_styles.dart b/lib/features/base/styles/cupertino_loading_widget_styles.dart new file mode 100644 index 0000000000..3000ad3694 --- /dev/null +++ b/lib/features/base/styles/cupertino_loading_widget_styles.dart @@ -0,0 +1,7 @@ +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:flutter/material.dart'; + +class CupertinoLoadingWidgetStyles { + static const Color progressColor = AppColor.colorLoading; + static const double size = 24; +} \ No newline at end of file diff --git a/lib/features/base/styles/hyper_link_widget_styles.dart b/lib/features/base/styles/hyper_link_widget_styles.dart new file mode 100644 index 0000000000..c8668cb476 --- /dev/null +++ b/lib/features/base/styles/hyper_link_widget_styles.dart @@ -0,0 +1,7 @@ +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:flutter/material.dart'; + +class HyperLinkWidgetStyles { + static const Color textColor = AppColor.primaryColor; + static const double textSize = 16; +} \ No newline at end of file diff --git a/lib/features/base/widget/cupertino_loading_widget.dart b/lib/features/base/widget/cupertino_loading_widget.dart new file mode 100644 index 0000000000..0c8d3e72a9 --- /dev/null +++ b/lib/features/base/widget/cupertino_loading_widget.dart @@ -0,0 +1,17 @@ +import 'package:flutter/cupertino.dart'; +import 'package:tmail_ui_user/features/base/styles/cupertino_loading_widget_styles.dart'; + +class CupertinoLoadingWidget extends StatelessWidget { + const CupertinoLoadingWidget({super.key}); + + @override + Widget build(BuildContext context) { + return const Center( + child: SizedBox( + width: CupertinoLoadingWidgetStyles.size, + height: CupertinoLoadingWidgetStyles.size, + child: CupertinoActivityIndicator(color: CupertinoLoadingWidgetStyles.progressColor) + ) + ); + } +} \ No newline at end of file diff --git a/lib/features/base/widget/hyper_link_widget.dart b/lib/features/base/widget/hyper_link_widget.dart new file mode 100644 index 0000000000..f0249179ea --- /dev/null +++ b/lib/features/base/widget/hyper_link_widget.dart @@ -0,0 +1,33 @@ +import 'package:flutter/gestures.dart'; +import 'package:flutter/widgets.dart'; +import 'package:tmail_ui_user/features/base/styles/hyper_link_widget_styles.dart'; +import 'package:url_launcher/url_launcher_string.dart'; + +class HyperLinkWidget extends StatelessWidget { + + final String urlString; + + const HyperLinkWidget({Key? key, required this.urlString}) : super(key: key); + + @override + Widget build(BuildContext context) { + return RichText( + text: TextSpan( + text: urlString, + style: const TextStyle( + color: HyperLinkWidgetStyles.textColor, + fontSize: HyperLinkWidgetStyles.textSize, + decoration: TextDecoration.underline + ), + recognizer: TapGestureRecognizer()..onTap = () async { + if (await canLaunchUrlString(urlString)) { + launchUrlString( + urlString, + mode: LaunchMode.externalApplication + ); + } + } + ) + ); + } +} diff --git a/lib/features/email/data/datasource/calendar_event_datasource.dart b/lib/features/email/data/datasource/calendar_event_datasource.dart new file mode 100644 index 0000000000..3f34688d1b --- /dev/null +++ b/lib/features/email/data/datasource/calendar_event_datasource.dart @@ -0,0 +1,8 @@ + +import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/id.dart'; +import 'package:jmap_dart_client/jmap/mail/calendar/calendar_event.dart'; + +abstract class CalendarEventDataSource { + Future> parse(AccountId accountId, Set blobIds); +} \ No newline at end of file diff --git a/lib/features/email/data/datasource_impl/calendar_event_datasource_impl.dart b/lib/features/email/data/datasource_impl/calendar_event_datasource_impl.dart new file mode 100644 index 0000000000..aabcc5193a --- /dev/null +++ b/lib/features/email/data/datasource_impl/calendar_event_datasource_impl.dart @@ -0,0 +1,22 @@ + +import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/id.dart'; +import 'package:jmap_dart_client/jmap/mail/calendar/calendar_event.dart'; +import 'package:tmail_ui_user/features/email/data/datasource/calendar_event_datasource.dart'; +import 'package:tmail_ui_user/features/email/data/network/calendar_event_api.dart'; +import 'package:tmail_ui_user/main/exceptions/exception_thrower.dart'; + +class CalendarEventDataSourceImpl extends CalendarEventDataSource { + + final CalendarEventAPI _calendarEventAPI; + final ExceptionThrower _exceptionThrower; + + CalendarEventDataSourceImpl(this._calendarEventAPI, this._exceptionThrower); + + @override + Future> parse(AccountId accountId, Set blobIds) { + return Future.sync(() async { + return await _calendarEventAPI.parse(accountId, blobIds); + }).catchError(_exceptionThrower.throwException); + } +} \ No newline at end of file diff --git a/lib/features/email/data/network/calendar_event_api.dart b/lib/features/email/data/network/calendar_event_api.dart new file mode 100644 index 0000000000..19ce720465 --- /dev/null +++ b/lib/features/email/data/network/calendar_event_api.dart @@ -0,0 +1,41 @@ +import 'dart:async'; + +import 'package:jmap_dart_client/http/http_client.dart'; +import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/id.dart'; +import 'package:jmap_dart_client/jmap/jmap_request.dart'; +import 'package:jmap_dart_client/jmap/mail/calendar/calendar_event.dart'; +import 'package:jmap_dart_client/jmap/mail/calendar/parse/calendar_event_parse_method.dart'; +import 'package:jmap_dart_client/jmap/mail/calendar/parse/calendar_event_parse_response.dart'; +import 'package:tmail_ui_user/features/email/domain/exceptions/calendar_event_exceptions.dart'; + +class CalendarEventAPI { + + final HttpClient _httpClient; + + CalendarEventAPI(this._httpClient); + + Future> parse(AccountId accountId, Set blobIds) async { + final requestBuilder = JmapRequestBuilder(_httpClient, ProcessingInvocation()); + final calendarEventParseMethod = CalendarEventParseMethod(accountId, blobIds); + final calendarEventParseInvocation = requestBuilder.invocation(calendarEventParseMethod); + final response = await (requestBuilder + ..usings(calendarEventParseMethod.requiredCapabilities)) + .build() + .execute(); + + final calendarEventParseResponse = response.parse( + calendarEventParseInvocation.methodCallId, + CalendarEventParseResponse.deserialize); + + if (calendarEventParseResponse?.parsed?.isNotEmpty == true) { + return calendarEventParseResponse!.parsed!.values.toList(); + } else if (calendarEventParseResponse?.notParsable?.isNotEmpty == true) { + throw NotParsableCalendarEventException(); + } else if (calendarEventParseResponse?.notFound?.isNotEmpty == true) { + throw NotFoundCalendarEventException(); + } else { + throw NotParsableCalendarEventException(); + } + } +} \ No newline at end of file diff --git a/lib/features/email/data/repository/calendar_event_repository_impl.dart b/lib/features/email/data/repository/calendar_event_repository_impl.dart new file mode 100644 index 0000000000..9d94ecc749 --- /dev/null +++ b/lib/features/email/data/repository/calendar_event_repository_impl.dart @@ -0,0 +1,18 @@ + +import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/id.dart'; +import 'package:jmap_dart_client/jmap/mail/calendar/calendar_event.dart'; +import 'package:tmail_ui_user/features/email/data/datasource/calendar_event_datasource.dart'; +import 'package:tmail_ui_user/features/email/domain/repository/calendar_event_repository.dart'; + +class CalendarEventRepositoryImpl extends CalendarEventRepository { + + final CalendarEventDataSource _calendarEventDataSource; + + CalendarEventRepositoryImpl(this._calendarEventDataSource); + + @override + Future> parse(AccountId accountId, Set blobIds) { + return _calendarEventDataSource.parse(accountId, blobIds); + } +} \ No newline at end of file diff --git a/lib/features/email/domain/exceptions/calendar_event_exceptions.dart b/lib/features/email/domain/exceptions/calendar_event_exceptions.dart new file mode 100644 index 0000000000..1129d36f55 --- /dev/null +++ b/lib/features/email/domain/exceptions/calendar_event_exceptions.dart @@ -0,0 +1,4 @@ + +class NotFoundCalendarEventException implements Exception {} + +class NotParsableCalendarEventException implements Exception {} \ No newline at end of file diff --git a/lib/features/email/domain/extensions/list_attachments_extension.dart b/lib/features/email/domain/extensions/list_attachments_extension.dart index 1cbfc601a3..bd9a0fc08a 100644 --- a/lib/features/email/domain/extensions/list_attachments_extension.dart +++ b/lib/features/email/domain/extensions/list_attachments_extension.dart @@ -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 { List toHiveCache() => map((attachment) => attachment.toHiveCache()).toList(); + + Set get calendarAttachments => where((attachment) => attachment.isCalendarEvent).toSet(); + + Set get calendarEventBlobIds => calendarAttachments + .map((attachment) => attachment.blobId) + .whereNotNull() + .toSet(); } \ No newline at end of file diff --git a/lib/features/email/domain/repository/calendar_event_repository.dart b/lib/features/email/domain/repository/calendar_event_repository.dart new file mode 100644 index 0000000000..809b12c7e3 --- /dev/null +++ b/lib/features/email/domain/repository/calendar_event_repository.dart @@ -0,0 +1,8 @@ + +import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/id.dart'; +import 'package:jmap_dart_client/jmap/mail/calendar/calendar_event.dart'; + +abstract class CalendarEventRepository { + Future> parse(AccountId accountId, Set blobIds); +} \ No newline at end of file diff --git a/lib/features/email/domain/state/parse_calendar_event_state.dart b/lib/features/email/domain/state/parse_calendar_event_state.dart new file mode 100644 index 0000000000..4d1be20264 --- /dev/null +++ b/lib/features/email/domain/state/parse_calendar_event_state.dart @@ -0,0 +1,19 @@ +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:jmap_dart_client/jmap/mail/calendar/calendar_event.dart'; + +class ParseCalendarEventLoading extends LoadingState {} + +class ParseCalendarEventSuccess extends UIState { + + final List calendarEventList; + + ParseCalendarEventSuccess(this.calendarEventList); + + @override + List get props => [calendarEventList]; +} + +class ParseCalendarEventFailure extends FeatureFailure { + ParseCalendarEventFailure(dynamic exception) : super(exception: exception); +} \ No newline at end of file diff --git a/lib/features/email/domain/usecases/parse_calendar_event_interactor.dart b/lib/features/email/domain/usecases/parse_calendar_event_interactor.dart new file mode 100644 index 0000000000..2c6c9adb13 --- /dev/null +++ b/lib/features/email/domain/usecases/parse_calendar_event_interactor.dart @@ -0,0 +1,23 @@ +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:dartz/dartz.dart'; +import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/id.dart'; +import 'package:tmail_ui_user/features/email/domain/repository/calendar_event_repository.dart'; +import 'package:tmail_ui_user/features/email/domain/state/parse_calendar_event_state.dart'; + +class ParseCalendarEventInteractor { + final CalendarEventRepository _calendarEventRepository; + + ParseCalendarEventInteractor(this._calendarEventRepository); + + Stream> execute(AccountId accountId, Set blobIds) async* { + try { + yield Right(ParseCalendarEventLoading()); + final calendarEventList = await _calendarEventRepository.parse(accountId, blobIds); + yield Right(ParseCalendarEventSuccess(calendarEventList)); + } catch (e) { + yield Left(ParseCalendarEventFailure(e)); + } + } +} \ No newline at end of file diff --git a/lib/features/email/presentation/bindings/calendar_event_interactor_bindings.dart b/lib/features/email/presentation/bindings/calendar_event_interactor_bindings.dart new file mode 100644 index 0000000000..dc1fa15f5a --- /dev/null +++ b/lib/features/email/presentation/bindings/calendar_event_interactor_bindings.dart @@ -0,0 +1,41 @@ +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'; +import 'package:tmail_ui_user/features/email/data/network/calendar_event_api.dart'; +import 'package:tmail_ui_user/features/email/data/repository/calendar_event_repository_impl.dart'; +import 'package:tmail_ui_user/features/email/domain/repository/calendar_event_repository.dart'; +import 'package:tmail_ui_user/features/email/domain/usecases/parse_calendar_event_interactor.dart'; +import 'package:tmail_ui_user/main/exceptions/remote_exception_thrower.dart'; + +class CalendarEventInteractorBindings extends InteractorsBindings { + + @override + void bindingsDataSource() { + Get.lazyPut(() => Get.find()); + } + + @override + void bindingsDataSourceImpl() { + Get.lazyPut(() => CalendarEventAPI(Get.find())); + Get.lazyPut(() => CalendarEventDataSourceImpl( + Get.find(), + Get.find())); + } + + @override + void bindingsInteractor() { + Get.lazyPut(() => ParseCalendarEventInteractor(Get.find())); + } + + @override + void bindingsRepository() { + Get.lazyPut(() => Get.find()); + } + + @override + void bindingsRepositoryImpl() { + Get.lazyPut(() => CalendarEventRepositoryImpl(Get.find())); + } +} \ No newline at end of file diff --git a/lib/features/email/presentation/email_bindings.dart b/lib/features/email/presentation/bindings/email_bindings.dart similarity index 100% rename from lib/features/email/presentation/email_bindings.dart rename to lib/features/email/presentation/bindings/email_bindings.dart diff --git a/lib/features/email/presentation/mdn_interactor_bindings.dart b/lib/features/email/presentation/bindings/mdn_interactor_bindings.dart similarity index 100% rename from lib/features/email/presentation/mdn_interactor_bindings.dart rename to lib/features/email/presentation/bindings/mdn_interactor_bindings.dart diff --git a/lib/features/email/presentation/controller/single_email_controller.dart b/lib/features/email/presentation/controller/single_email_controller.dart index cb7208ea46..8ad0ce6041 100644 --- a/lib/features/email/presentation/controller/single_email_controller.dart +++ b/lib/features/email/presentation/controller/single_email_controller.dart @@ -6,13 +6,14 @@ 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'; +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'; @@ -26,8 +27,12 @@ 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'; import 'package:tmail_ui_user/features/email/presentation/controller/email_supervisor_controller.dart'; import 'package:tmail_ui_user/features/email/presentation/model/email_loaded.dart'; import 'package:tmail_ui_user/features/email/domain/model/move_action.dart'; @@ -66,6 +71,7 @@ import 'package:tmail_ui_user/features/manage_account/presentation/extensions/da import 'package:tmail_ui_user/features/rules_filter_creator/presentation/model/rules_filter_creator_arguments.dart'; import 'package:tmail_ui_user/features/thread/domain/constants/thread_constants.dart'; import 'package:tmail_ui_user/features/thread/presentation/model/delete_action_type.dart'; +import 'package:tmail_ui_user/main/error/capability_validator.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; import 'package:tmail_ui_user/main/routes/app_routes.dart'; import 'package:tmail_ui_user/main/routes/dialog_router.dart'; @@ -99,11 +105,14 @@ class SingleEmailController extends BaseController with AppLoaderMixin { CreateNewEmailRuleFilterInteractor? _createNewEmailRuleFilterInteractor; SendReceiptToSenderInteractor? _sendReceiptToSenderInteractor; + ParseCalendarEventInteractor? _parseCalendarEventInteractor; final emailAddressExpandMode = ExpandMode.COLLAPSE.obs; final attachmentsExpandMode = ExpandMode.COLLAPSE.obs; final emailContents = RxnString(); final attachments = [].obs; + final calendarEvent = Rxn(); + EmailId? _currentEmailId; Identity? _identitySelected; String? initialEmailContents; @@ -170,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); } } @@ -291,12 +302,18 @@ class SingleEmailController extends BaseController with AppLoaderMixin { void _injectAndGetInteractorBindings(Session? session, AccountId accountId) { injectRuleFilterBindings(session, accountId); injectMdnBindings(session, accountId); + _injectCalendarEventBindings(session, accountId); - if (Get.isRegistered()) { - _createNewEmailRuleFilterInteractor = Get.find(); - } - if (Get.isRegistered()) { - _sendReceiptToSenderInteractor = Get.find(); + _createNewEmailRuleFilterInteractor = getBinding(); + _sendReceiptToSenderInteractor = getBinding(); + _parseCalendarEventInteractor = getBinding(); + } + + void _injectCalendarEventBindings(Session? session, AccountId? accountId) { + if (session != null && accountId != null) { + if (CapabilityIdentifier.jamesCalendarEvent.isSupported(session, accountId)) { + CalendarEventInteractorBindings().dependencies(); + } } } @@ -380,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(); @@ -405,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!, @@ -450,6 +471,7 @@ class SingleEmailController extends BaseController with AppLoaderMixin { emailContents.value = null; initialEmailContents = null; attachments.clear(); + calendarEvent.value = null; } PresentationMailbox? getMailboxContain(PresentationEmail email) { @@ -1218,4 +1240,50 @@ class SingleEmailController extends BaseController with AppLoaderMixin { consumeState(_storeOpenedEmailInteractor.execute(session, accountId, detailedEmail)); } } + + void _loadCalendarEventAction(Set 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 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; + _enableScrollPageView(); + } + } + + void _enableScrollPageView() { + emailSupervisorController.scrollPhysicsPageView.value = null; + } } \ No newline at end of file diff --git a/lib/features/email/presentation/email_view.dart b/lib/features/email/presentation/email_view.dart index c862183658..e3594bf0f8 100644 --- a/lib/features/email/presentation/email_view.dart +++ b/lib/features/email/presentation/email_view.dart @@ -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,20 +19,26 @@ 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/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/calendar_event_action_banner_widget.dart'; +import 'package:tmail_ui_user/features/email/presentation/widgets/calendar_event/calendar_event_detail_widget.dart'; +import 'package:tmail_ui_user/features/email/presentation/widgets/calendar_event/calendar_event_information_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/email_view_loading_bar_widget.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'; import 'package:tmail_ui_user/features/manage_account/presentation/vacation/widgets/vacation_notification_message_widget.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; import 'package:tmail_ui_user/main/utils/app_utils.dart'; -class EmailView extends GetWidget { +class EmailView extends GetWidget with AppLoaderMixin { final responsiveUtils = Get.find(); final imagePaths = Get.find(); @@ -225,7 +229,7 @@ class EmailView extends GetWidget { } Widget _buildEmailBody(BuildContext context, PresentationEmail email) { - if (PlatformInfo.isWeb) { + if (PlatformInfo.isWeb && !email.hasCalendarEvent) { return _buildEmailMessage(context, email); } else { return SingleChildScrollView( @@ -278,45 +282,41 @@ class EmailView extends GetWidget { imagePaths: imagePaths, responsiveUtils: responsiveUtils, ), - _buildLoadingView(), _buildAttachments(context), - if (PlatformInfo.isWeb) - Expanded(child: Padding( - padding: EdgeInsets.only( - left: AppUtils.isDirectionRTL(context) ? 0 : 16, - right: AppUtils.isDirectionRTL(context) ? 16 : 0, - bottom: 16 - ), - child: _buildEmailContent(context, constraints, email) - )) + Obx(() => EmailViewLoadingBarWidget( + viewState: controller.viewState.value, + selectedEmail: email + )), + if (email.hasCalendarEvent) + Obx(() { + if (controller.calendarEvent.value != null) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + CalendarEventInformationWidget( + calendarEvent: controller.calendarEvent.value! + ), + CalendarEventActionBannerWidget( + calendarEvent: controller.calendarEvent.value!, + listFromEmailAddress: controller.currentEmail?.from + ), + CalendarEventDetailWidget( + calendarEvent: controller.calendarEvent.value! + ), + ], + ); + } else { + return const SizedBox.shrink(); + } + }) else - Padding( - padding: const EdgeInsets.all(16), - child: _buildEmailContent(context, constraints, email)) + _buildEmailContent(context, constraints, email) ], ); }); } - Widget _buildLoadingView() { - 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)))); - } else { - return const SizedBox.shrink(); - } - }); - }); - } - Widget _buildAttachments(BuildContext context) { return Obx(() { final attachments = controller.attachments.listAttachmentsDisplayedOutSide; @@ -329,7 +329,7 @@ class EmailView extends GetWidget { Widget _buildAttachmentsBody(BuildContext context, List 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), @@ -446,25 +446,36 @@ class EmailView extends GetWidget { final allEmailContents = controller.emailContents.value; if (PlatformInfo.isWeb) { - return HtmlContentViewerOnWeb( - widthContent: constraints.maxWidth, - heightContent: responsiveUtils.getSizeScreenHeight(context), - contentHtml: allEmailContents ?? "", - controller: HtmlViewerControllerForWeb(), - mailtoDelegate: (uri) => controller.openMailToLink(uri), - direction: AppUtils.getCurrentDirection(context), + return Expanded( + child: Padding( + padding: const EdgeInsetsDirectional.only(start: 16, bottom: 16), + child: HtmlContentViewerOnWeb( + widthContent: constraints.maxWidth, + heightContent: responsiveUtils.getSizeScreenHeight(context), + contentHtml: allEmailContents ?? "", + controller: HtmlViewerControllerForWeb(), + mailtoDelegate: (uri) => controller.openMailToLink(uri), + direction: AppUtils.getCurrentDirection(context), + ), + ), ); } else { - return HtmlContentViewer( - heightContent: responsiveUtils.getSizeScreenHeight(context), - contentHtml: allEmailContents ?? "", - mailtoDelegate: (uri) async => controller.openMailToLink(uri), - onScrollHorizontalEnd: controller.toggleScrollPhysicsPagerView, - onWebViewLoaded: (isScrollPageViewActivated) { - log('EmailView::_buildEmailContent(): isScrollPageViewActivated: $isScrollPageViewActivated'); - controller.emailSupervisorController.updateScrollPhysicPageView(isScrollPageViewActivated: isScrollPageViewActivated); - }, - direction: AppUtils.getCurrentDirection(context), + return Padding( + padding: const EdgeInsetsDirectional.symmetric( + vertical: EmailViewStyles.mobileContentVerticalMargin, + horizontal: EmailViewStyles.mobileContentHorizontalMargin + ), + child: HtmlContentViewer( + heightContent: responsiveUtils.getSizeScreenHeight(context), + contentHtml: allEmailContents ?? "", + mailtoDelegate: (uri) async => controller.openMailToLink(uri), + onScrollHorizontalEnd: controller.toggleScrollPhysicsPagerView, + onWebViewLoaded: (isScrollPageViewActivated) { + log('EmailView::_buildEmailContent(): isScrollPageViewActivated: $isScrollPageViewActivated'); + controller.emailSupervisorController.updateScrollPhysicPageView(isScrollPageViewActivated: isScrollPageViewActivated); + }, + direction: AppUtils.getCurrentDirection(context), + ), ); } } else { diff --git a/lib/features/email/presentation/extensions/calendar_event_extension.dart b/lib/features/email/presentation/extensions/calendar_event_extension.dart new file mode 100644 index 0000000000..164c4d6a9a --- /dev/null +++ b/lib/features/email/presentation/extensions/calendar_event_extension.dart @@ -0,0 +1,288 @@ + +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/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'; +import 'package:tmail_ui_user/main/utils/app_utils.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; + } + } + + 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(DateTime dateTime) { + return date_format.formatDate( + dateTime, + [ + date_format.DD, + ', ', + date_format.MM, + ' ', + date_format.dd, + ', ', + date_format.yyyy, + ' ', + date_format.hh, + ':', + date_format.ss, + ' ', + date_format.am + ], + locale: AppUtils.getCurrentDateLocale() + ); + } + + String formatTime(DateTime dateTime) { + return date_format.formatDate( + dateTime, + [ + date_format.hh, + ':', + date_format.ss, + ' ', + date_format.am + ], + locale: AppUtils.getCurrentDateLocale() + ); + } + + String get dateTimeEventAsString { + if (localStartDate != null && localEndDate != null) { + final timeStart = formatDateTime(localStartDate!); + final timeEnd = DateUtils.isSameDay(localStartDate, localEndDate) + ? formatTime(localEndDate!) + : formatDateTime(localEndDate!); + return '$timeStart - $timeEnd'; + } else if (localStartDate != null) { + return formatDateTime(localStartDate!); + } else if (localEndDate != null) { + return formatDateTime(localEndDate!); + } else { + return ''; + } + } + + List get videoConferences { + if (extensionFields != null && extensionFields!.mapFields.isNotEmpty) { + final videoConferences = extensionFields!.mapFields['X-OPENPAAS-VIDEOCONFERENCE']; + if (videoConferences != null) { + final videoConferencesNotEmpty = videoConferences + .whereNotNull() + .where((link) => link.isNotEmpty) + .toList(); + log('CalendarEventExtension::getListVideoConference: $videoConferencesNotEmpty'); + return videoConferencesNotEmpty; + } + } + + return []; + } +} \ No newline at end of file diff --git a/lib/features/email/presentation/extensions/list_attendee_extension.dart b/lib/features/email/presentation/extensions/list_attendee_extension.dart new file mode 100644 index 0000000000..248fd32b48 --- /dev/null +++ b/lib/features/email/presentation/extensions/list_attendee_extension.dart @@ -0,0 +1,19 @@ + +import 'package:collection/collection.dart'; +import 'package:jmap_dart_client/jmap/mail/calendar/properties/attendee/calendar_attendee.dart'; +import 'package:jmap_dart_client/jmap/mail/calendar/properties/calendar_organizer.dart'; + +extension ListAttendeeExtension on List { + + String get mailtoAsString { + return map((attendee) => attendee.mailto?.mailAddress.value) + .whereNotNull() + .join(', '); + } + + List withoutOrganizer(CalendarOrganizer organizer) { + return where((attendee) => attendee.mailto?.mailAddress != organizer.mailto) + .whereNotNull() + .toList(); + } +} \ No newline at end of file diff --git a/lib/features/email/presentation/styles/attendee_widget_styles.dart b/lib/features/email/presentation/styles/attendee_widget_styles.dart new file mode 100644 index 0000000000..0a66107c97 --- /dev/null +++ b/lib/features/email/presentation/styles/attendee_widget_styles.dart @@ -0,0 +1,10 @@ + +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:flutter/material.dart'; + +class AttendeeWidgetStyles { + static const double maxWidth = 100; + static const double textSize = 16; + static const Color textColor = Colors.black; + static const Color mailtoColor = AppColor.colorMailto; +} \ No newline at end of file diff --git a/lib/features/email/presentation/styles/calendar_date_icon_widget_styles.dart b/lib/features/email/presentation/styles/calendar_date_icon_widget_styles.dart new file mode 100644 index 0000000000..a2e1854960 --- /dev/null +++ b/lib/features/email/presentation/styles/calendar_date_icon_widget_styles.dart @@ -0,0 +1,11 @@ + +class CalendarIconWidgetStyles { + static const double borderRadius = 8; + static const double headerVerticalContentPadding = 4; + static const double headerHorizontalContentPadding = 8; + static const double headerTextSize = 14; + static const double bodyDayTextSize = 40; + static const double bodyWeekDayTextSize = 14; + static const double bodyContentPadding = 8; + static const double margin = 16; +} \ No newline at end of file diff --git a/lib/features/email/presentation/styles/calendar_event_action_banner_styles.dart b/lib/features/email/presentation/styles/calendar_event_action_banner_styles.dart new file mode 100644 index 0000000000..fc4203ca57 --- /dev/null +++ b/lib/features/email/presentation/styles/calendar_event_action_banner_styles.dart @@ -0,0 +1,10 @@ + +class CalendarEventActionBannerStyles { + static const double borderRadius = 12; + static const double contentPadding = 12; + static const double viewHorizontalMargin = 16; + static const double viewVerticalMargin = 12; + static const double titleTextSize = 16; + static const double subTileTextSize = 13; + static const double iconSize = 20; +} \ No newline at end of file diff --git a/lib/features/email/presentation/styles/calendar_event_detail_widget_styles.dart b/lib/features/email/presentation/styles/calendar_event_detail_widget_styles.dart new file mode 100644 index 0000000000..80d262e2d9 --- /dev/null +++ b/lib/features/email/presentation/styles/calendar_event_detail_widget_styles.dart @@ -0,0 +1,14 @@ + +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:flutter/material.dart'; + +class CalendarEventDetailWidgetStyles { + static const double textSize = 24; + static const double borderRadius = 16; + static const double verticalMargin = 12; + static const double horizontalMargin = 16; + static const double contentPadding = 16; + static const double fieldTopPadding = 16; + static const Color borderStrokeColor = AppColor.colorCalendarEventInformationStroke; + static const double borderStrokeWidth = 0.5; +} \ No newline at end of file diff --git a/lib/features/email/presentation/styles/calendar_event_information_widget_styles.dart b/lib/features/email/presentation/styles/calendar_event_information_widget_styles.dart new file mode 100644 index 0000000000..507a7b643c --- /dev/null +++ b/lib/features/email/presentation/styles/calendar_event_information_widget_styles.dart @@ -0,0 +1,18 @@ + +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:flutter/material.dart'; + +class CalendarEventInformationWidgetStyles { + static const double borderRadius = 16; + static const double contentPadding = 16; + static const double verticalMargin = 12; + static const double horizontalMargin = 16; + static const double calendarDateIconMargin = 16; + static const Color calendarDateIconBackgroundColor = AppColor.colorCalendarEventInformationBackground; + static const double calendarInformationMargin = 16; + static const double invitationMessageTextSize = 16; + static const double fieldTopPadding = 16; + static const double space = 8; + static const Color titleColor = Colors.black; + static const Color invitationMessageColor = Colors.black; +} \ No newline at end of file diff --git a/lib/features/email/presentation/styles/email_view_styles.dart b/lib/features/email/presentation/styles/email_view_styles.dart new file mode 100644 index 0000000000..1f331a7560 --- /dev/null +++ b/lib/features/email/presentation/styles/email_view_styles.dart @@ -0,0 +1,5 @@ + +class EmailViewStyles { + static const double mobileContentHorizontalMargin = 16; + static const double mobileContentVerticalMargin = 12; +} \ No newline at end of file diff --git a/lib/features/email/presentation/styles/event_attendee_detail_widget_styles.dart b/lib/features/email/presentation/styles/event_attendee_detail_widget_styles.dart new file mode 100644 index 0000000000..f510184b20 --- /dev/null +++ b/lib/features/email/presentation/styles/event_attendee_detail_widget_styles.dart @@ -0,0 +1,11 @@ + +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:flutter/material.dart'; + +class EventAttendeeDetailWidgetStyles { + static const double maxWidth = 100; + static const double textSize = 16; + static const double fieldTopPadding = 8; + static const Color labelColor = AppColor.colorSubTitleEventActionText; + static const Color valueColor = Colors.black; +} \ No newline at end of file diff --git a/lib/features/email/presentation/styles/event_attendee_information_widget_styles.dart b/lib/features/email/presentation/styles/event_attendee_information_widget_styles.dart new file mode 100644 index 0000000000..f76346d0a2 --- /dev/null +++ b/lib/features/email/presentation/styles/event_attendee_information_widget_styles.dart @@ -0,0 +1,11 @@ + +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:flutter/material.dart'; + +class EventAttendeeInformationWidgetStyles { + static const double maxWidth = 100; + static const double textSize = 16; + static const Color labelColor = AppColor.colorSubTitleEventActionText; + static const Color valueColor = Colors.black; + static const Color valueOrganizerColor = AppColor.colorOrganizerMailto; +} \ No newline at end of file diff --git a/lib/features/email/presentation/styles/event_description_detail_widget_styles.dart b/lib/features/email/presentation/styles/event_description_detail_widget_styles.dart new file mode 100644 index 0000000000..82b3591bd9 --- /dev/null +++ b/lib/features/email/presentation/styles/event_description_detail_widget_styles.dart @@ -0,0 +1,9 @@ + +import 'package:flutter/material.dart'; + +class EventDescriptionDetailWidgetStyles { + static const double textSize = 16; + static const double borderRadius = 16; + static const double contentPadding = 16; + static const Color valueColor = Colors.black; +} \ No newline at end of file diff --git a/lib/features/email/presentation/styles/event_link_detail_widget_styles.dart b/lib/features/email/presentation/styles/event_link_detail_widget_styles.dart new file mode 100644 index 0000000000..e91c8ba779 --- /dev/null +++ b/lib/features/email/presentation/styles/event_link_detail_widget_styles.dart @@ -0,0 +1,10 @@ + +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:flutter/material.dart'; + +class EventLinkDetailWidgetStyles { + static const double maxWidth = 100; + static const double textSize = 16; + static const Color labelColor = AppColor.colorSubTitleEventActionText; + static const Color valueColor = Colors.black; +} \ No newline at end of file diff --git a/lib/features/email/presentation/styles/event_location_detail_widget_styles.dart b/lib/features/email/presentation/styles/event_location_detail_widget_styles.dart new file mode 100644 index 0000000000..8abe0cdf06 --- /dev/null +++ b/lib/features/email/presentation/styles/event_location_detail_widget_styles.dart @@ -0,0 +1,10 @@ + +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:flutter/material.dart'; + +class EventLocationDetailWidgetStyles { + static const double maxWidth = 100; + static const double textSize = 16; + static const Color labelColor = AppColor.colorSubTitleEventActionText; + static const Color valueColor = Colors.black; +} \ No newline at end of file diff --git a/lib/features/email/presentation/styles/event_location_information_widget_styles.dart b/lib/features/email/presentation/styles/event_location_information_widget_styles.dart new file mode 100644 index 0000000000..c6208b5e5e --- /dev/null +++ b/lib/features/email/presentation/styles/event_location_information_widget_styles.dart @@ -0,0 +1,10 @@ + +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:flutter/material.dart'; + +class EventLocationInformationWidgetStyles { + static const double maxWidth = 100; + static const double textSize = 16; + static const Color labelColor = AppColor.colorSubTitleEventActionText; + static const Color valueColor = Colors.black; +} \ No newline at end of file diff --git a/lib/features/email/presentation/styles/event_time_detail_widget_styles.dart b/lib/features/email/presentation/styles/event_time_detail_widget_styles.dart new file mode 100644 index 0000000000..af54d01924 --- /dev/null +++ b/lib/features/email/presentation/styles/event_time_detail_widget_styles.dart @@ -0,0 +1,10 @@ + +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:flutter/material.dart'; + +class EventTimeDetailWidgetStyles { + static const double maxWidth = 100; + static const double textSize = 16; + static const Color labelColor = AppColor.colorSubTitleEventActionText; + static const Color valueColor = Colors.black; +} \ No newline at end of file diff --git a/lib/features/email/presentation/styles/event_time_information_widget_styles.dart b/lib/features/email/presentation/styles/event_time_information_widget_styles.dart new file mode 100644 index 0000000000..874e389f22 --- /dev/null +++ b/lib/features/email/presentation/styles/event_time_information_widget_styles.dart @@ -0,0 +1,10 @@ + +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:flutter/material.dart'; + +class EventTimeInformationWidgetStyles { + static const double maxWidth = 100; + static const double textSize = 16; + static const Color labelColor = AppColor.colorSubTitleEventActionText; + static const Color valueColor = Colors.black; +} \ No newline at end of file diff --git a/lib/features/email/presentation/styles/event_title_widget_styles.dart b/lib/features/email/presentation/styles/event_title_widget_styles.dart new file mode 100644 index 0000000000..f53abaf925 --- /dev/null +++ b/lib/features/email/presentation/styles/event_title_widget_styles.dart @@ -0,0 +1,7 @@ + +import 'package:flutter/material.dart'; + +class EventTitleWidgetStyles { + static const double textSize = 24; + static const Color textColor = Colors.black; +} \ No newline at end of file diff --git a/lib/features/email/presentation/styles/organizer_widget_styles.dart b/lib/features/email/presentation/styles/organizer_widget_styles.dart new file mode 100644 index 0000000000..1ea478c4bc --- /dev/null +++ b/lib/features/email/presentation/styles/organizer_widget_styles.dart @@ -0,0 +1,10 @@ + +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:flutter/material.dart'; + +class OrganizerWidgetStyles { + static const double maxWidth = 100; + static const double textSize = 16; + static const Color textColor = Colors.black; + static const Color mailtoColor = AppColor.colorMailto; +} \ No newline at end of file diff --git a/lib/features/email/presentation/styles/see_all_attendees_button_widget_styles.dart b/lib/features/email/presentation/styles/see_all_attendees_button_widget_styles.dart new file mode 100644 index 0000000000..a64f3ce54e --- /dev/null +++ b/lib/features/email/presentation/styles/see_all_attendees_button_widget_styles.dart @@ -0,0 +1,12 @@ + +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:flutter/material.dart'; + +class SeeAllAttendeesButtonWidgetStyles { + static const double textSize = 16; + static const double horizontalPadding = 8; + static const double verticalPadding = 4; + static const double borderRadius = 20; + static const double horizontalMargin = -8; + static const Color textColor = AppColor.primaryColor; +} \ No newline at end of file diff --git a/lib/features/email/presentation/utils/email_utils.dart b/lib/features/email/presentation/utils/email_utils.dart new file mode 100644 index 0000000000..ed0cd8c8b2 --- /dev/null +++ b/lib/features/email/presentation/utils/email_utils.dart @@ -0,0 +1,18 @@ + +import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/capability/capability_identifier.dart'; +import 'package:jmap_dart_client/jmap/core/properties/properties.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; +import 'package:tmail_ui_user/features/thread/domain/constants/thread_constants.dart'; +import 'package:tmail_ui_user/main/error/capability_validator.dart'; + +class EmailUtils { + + static Properties getPropertiesForEmailGetMethod(Session session, AccountId accountId) { + if (CapabilityIdentifier.jamesCalendarEvent.isSupported(session, accountId)) { + return ThreadConstants.propertiesCalendarEvent; + } else { + return ThreadConstants.propertiesDefault; + } + } +} \ No newline at end of file diff --git a/lib/features/email/presentation/widgets/calendar_event/attendee_widget.dart b/lib/features/email/presentation/widgets/calendar_event/attendee_widget.dart new file mode 100644 index 0000000000..03ebcf3d3d --- /dev/null +++ b/lib/features/email/presentation/widgets/calendar_event/attendee_widget.dart @@ -0,0 +1,41 @@ + +import 'package:flutter/material.dart'; +import 'package:jmap_dart_client/jmap/mail/calendar/properties/attendee/calendar_attendee.dart'; +import 'package:tmail_ui_user/features/email/presentation/styles/attendee_widget_styles.dart'; + +class AttendeeWidget extends StatelessWidget { + + final CalendarAttendee attendee; + + const AttendeeWidget({ + super.key, + required this.attendee + }); + + @override + Widget build(BuildContext context) { + return RichText( + text: TextSpan( + style: const TextStyle( + fontSize: AttendeeWidgetStyles.textSize, + fontWeight: FontWeight.w500, + color: AttendeeWidgetStyles.textColor + ), + children: [ + if (attendee.name?.name.isNotEmpty == true) + TextSpan(text: attendee.name!.name), + if (attendee.mailto?.mailAddress.value.isNotEmpty == true) + TextSpan( + text: ' <${attendee.mailto!.mailAddress.value}> ', + style: const TextStyle( + color: AttendeeWidgetStyles.mailtoColor, + fontSize: AttendeeWidgetStyles.textSize, + fontWeight: FontWeight.w500 + ), + ), + const TextSpan(text: ', '), + ] + ) + ); + } +} \ No newline at end of file diff --git a/lib/features/email/presentation/widgets/calendar_event/calendar_date_icon_widget.dart b/lib/features/email/presentation/widgets/calendar_event/calendar_date_icon_widget.dart new file mode 100644 index 0000000000..ffe82b4203 --- /dev/null +++ b/lib/features/email/presentation/widgets/calendar_event/calendar_date_icon_widget.dart @@ -0,0 +1,93 @@ + +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:flutter/material.dart'; +import 'package:jmap_dart_client/jmap/mail/calendar/calendar_event.dart'; +import 'package:tmail_ui_user/features/email/presentation/extensions/calendar_event_extension.dart'; +import 'package:tmail_ui_user/features/email/presentation/styles/calendar_date_icon_widget_styles.dart'; + +class CalendarDateIconWidget extends StatelessWidget { + + final CalendarEvent calendarEvent; + final double width; + + const CalendarDateIconWidget({ + super.key, + required this.calendarEvent, + this.width = 100 + }); + + @override + Widget build(BuildContext context) { + return Container( + clipBehavior: Clip.antiAlias, + width: width, + margin: const EdgeInsets.all(CalendarIconWidgetStyles.margin), + decoration: const ShapeDecoration( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(CalendarIconWidgetStyles.borderRadius))), + color: Colors.white, + shadows: [ + BoxShadow( + color: AppColor.colorShadowBgContentEmail, + blurRadius: 80, + offset: Offset(0, 1), + spreadRadius: 0, + ), + BoxShadow( + color: AppColor.colorShadowCalendarDateIcon, + blurRadius: 3, + offset: Offset(0, 1), + spreadRadius: 1, + ) + ], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + color: AppColor.primaryColor, + padding: const EdgeInsetsDirectional.symmetric( + vertical: CalendarIconWidgetStyles.headerVerticalContentPadding, + horizontal: CalendarIconWidgetStyles.headerHorizontalContentPadding + ), + width: width, + child: Text( + calendarEvent.monthStartDateAsString, + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: CalendarIconWidgetStyles.headerTextSize, + fontWeight: FontWeight.w400, + color: Colors.white + ), + ), + ), + Padding( + padding: const EdgeInsets.all(CalendarIconWidgetStyles.bodyContentPadding), + child: Text( + calendarEvent.dayStartDateAsString, + style: const TextStyle( + fontSize: CalendarIconWidgetStyles.bodyDayTextSize, + fontWeight: FontWeight.w700, + color: Colors.black + ), + ), + ), + const Padding( + padding: EdgeInsets.symmetric(horizontal: CalendarIconWidgetStyles.bodyContentPadding), + child: Divider(color: AppColor.colorCalendarEventInformationStroke, height: 2) + ), + Padding( + padding: const EdgeInsets.all(CalendarIconWidgetStyles.bodyContentPadding), + child: Text( + calendarEvent.weekDayStartDateAsString, + style: const TextStyle( + fontSize: CalendarIconWidgetStyles.bodyWeekDayTextSize, + fontWeight: FontWeight.w400, + color: Colors.black + ), + ), + ), + ], + ), + ); + } +} \ No newline at end of file diff --git a/lib/features/email/presentation/widgets/calendar_event/calendar_event_action_banner_widget.dart b/lib/features/email/presentation/widgets/calendar_event/calendar_event_action_banner_widget.dart new file mode 100644 index 0000000000..b270e63a58 --- /dev/null +++ b/lib/features/email/presentation/widgets/calendar_event/calendar_event_action_banner_widget.dart @@ -0,0 +1,101 @@ + +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/presentation/resources/image_paths.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? listFromEmailAddress; + + const CalendarEventActionBannerWidget({ + super.key, + required this.calendarEvent, + required this.listFromEmailAddress, + }); + + @override + Widget build(BuildContext context) { + final imagePaths = Get.find(); + 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( + horizontal: CalendarEventActionBannerStyles.viewHorizontalMargin, + vertical: CalendarEventActionBannerStyles.viewVerticalMargin, + ), + 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: CalendarEventActionBannerStyles.iconSize, + height: CalendarEventActionBannerStyles.iconSize, + fit: BoxFit.fill, + ), + ), + Expanded(child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + RichText( + text: TextSpan( + style: TextStyle( + fontSize: CalendarEventActionBannerStyles.titleTextSize, + 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: CalendarEventActionBannerStyles.titleTextSize, + 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: CalendarEventActionBannerStyles.subTileTextSize, + fontWeight: FontWeight.w400 + ), + ) + ] + )) + ] + ), + ); + } + + String _getSenderEmailAddress() { + if (listFromEmailAddress?.isNotEmpty == true) { + return listFromEmailAddress!.first.emailAddress; + } else { + return ''; + } + } +} \ No newline at end of file diff --git a/lib/features/email/presentation/widgets/calendar_event/calendar_event_detail_widget.dart b/lib/features/email/presentation/widgets/calendar_event/calendar_event_detail_widget.dart new file mode 100644 index 0000000000..0d6c39bf21 --- /dev/null +++ b/lib/features/email/presentation/widgets/calendar_event/calendar_event_detail_widget.dart @@ -0,0 +1,78 @@ + +import 'package:flutter/material.dart'; +import 'package:jmap_dart_client/jmap/mail/calendar/calendar_event.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_detail_widget_styles.dart'; +import 'package:tmail_ui_user/features/email/presentation/widgets/calendar_event/event_attendee_detail_widget.dart'; +import 'package:tmail_ui_user/features/email/presentation/widgets/calendar_event/event_description_detail_widget.dart'; +import 'package:tmail_ui_user/features/email/presentation/widgets/calendar_event/event_link_detail_widget.dart'; +import 'package:tmail_ui_user/features/email/presentation/widgets/calendar_event/event_location_detail_widget.dart'; +import 'package:tmail_ui_user/features/email/presentation/widgets/calendar_event/event_time_detail_widget.dart'; +import 'package:tmail_ui_user/features/email/presentation/widgets/calendar_event/event_title_widget.dart'; + +class CalendarEventDetailWidget extends StatelessWidget { + + final CalendarEvent calendarEvent; + + const CalendarEventDetailWidget({ + super.key, + required this.calendarEvent + }); + + @override + Widget build(BuildContext context) { + return Container( + clipBehavior: Clip.antiAlias, + decoration: const ShapeDecoration( + color: Colors.white, + shape: RoundedRectangleBorder( + side: BorderSide( + width: CalendarEventDetailWidgetStyles.borderStrokeWidth, + color: CalendarEventDetailWidgetStyles.borderStrokeColor, + ), + borderRadius: BorderRadius.all(Radius.circular(CalendarEventDetailWidgetStyles.borderRadius)), + ), + ), + margin: const EdgeInsetsDirectional.symmetric( + vertical: CalendarEventDetailWidgetStyles.verticalMargin, + horizontal: CalendarEventDetailWidgetStyles.horizontalMargin), + padding: const EdgeInsets.all(CalendarEventDetailWidgetStyles.contentPadding), + width: double.infinity, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (calendarEvent.title?.isNotEmpty == true) + EventTitleWidget(title: calendarEvent.title!), + if (calendarEvent.description?.isNotEmpty == true) + Padding( + padding: const EdgeInsets.only(top: CalendarEventDetailWidgetStyles.fieldTopPadding), + child: EventDescriptionDetailWidget(description: calendarEvent.description!) + ), + if (calendarEvent.dateTimeEventAsString.isNotEmpty) + Padding( + padding: const EdgeInsets.only(top: CalendarEventDetailWidgetStyles.fieldTopPadding), + child: EventTimeWidgetWidget(timeEvent: calendarEvent.dateTimeEventAsString), + ), + if (calendarEvent.videoConferences.isNotEmpty) + Padding( + padding: const EdgeInsets.only(top: CalendarEventDetailWidgetStyles.fieldTopPadding), + child: EventLinkDetailWidget(listHyperLink: calendarEvent.videoConferences), + ), + if (calendarEvent.location?.isNotEmpty == true) + Padding( + padding: const EdgeInsets.only(top: CalendarEventDetailWidgetStyles.fieldTopPadding), + child: EventLocationDetailWidget(locationEvent: calendarEvent.location!), + ), + if (calendarEvent.participants?.isNotEmpty == true && calendarEvent.organizer != null) + Padding( + padding: const EdgeInsets.only(top: CalendarEventDetailWidgetStyles.fieldTopPadding), + child: EventAttendeeDetailWidget( + attendees: calendarEvent.participants!, + organizer: calendarEvent.organizer!, + ), + ), + ], + ), + ); + } +} diff --git a/lib/features/email/presentation/widgets/calendar_event/calendar_event_information_widget.dart b/lib/features/email/presentation/widgets/calendar_event/calendar_event_information_widget.dart new file mode 100644 index 0000000000..62062c4102 --- /dev/null +++ b/lib/features/email/presentation/widgets/calendar_event/calendar_event_information_widget.dart @@ -0,0 +1,178 @@ + +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/presentation/utils/responsive_utils.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:jmap_dart_client/jmap/mail/calendar/calendar_event.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_information_widget_styles.dart'; +import 'package:tmail_ui_user/features/email/presentation/widgets/calendar_event/event_attendee_information_widget.dart'; +import 'package:tmail_ui_user/features/email/presentation/widgets/calendar_event/calendar_date_icon_widget.dart'; +import 'package:tmail_ui_user/features/email/presentation/widgets/calendar_event/event_location_information_widget.dart'; +import 'package:tmail_ui_user/features/email/presentation/widgets/calendar_event/event_time_information_widget.dart'; +import 'package:tmail_ui_user/features/email/presentation/widgets/calendar_event/event_title_widget.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; + +class CalendarEventInformationWidget extends StatelessWidget { + + final CalendarEvent calendarEvent; + + const CalendarEventInformationWidget({ + super.key, + required this.calendarEvent + }); + + @override + Widget build(BuildContext context) { + final responsiveUtils = Get.find(); + return Container( + clipBehavior: Clip.antiAlias, + decoration: const ShapeDecoration( + color: AppColor.colorCalendarEventInformationBackground, + shape: RoundedRectangleBorder( + side: BorderSide( + width: 0.5, + color: AppColor.colorCalendarEventInformationStroke, + ), + borderRadius: BorderRadius.all(Radius.circular(CalendarEventInformationWidgetStyles.borderRadius)), + ), + ), + margin: const EdgeInsetsDirectional.symmetric( + vertical: CalendarEventInformationWidgetStyles.verticalMargin, + horizontal: CalendarEventInformationWidgetStyles.horizontalMargin), + child: responsiveUtils.isPortraitMobile(context) + ? Column( + children: [ + CalendarDateIconWidget( + calendarEvent: calendarEvent, + width: double.infinity, + ), + Container( + decoration: const ShapeDecoration( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.only( + bottomLeft: Radius.circular(CalendarEventInformationWidgetStyles.borderRadius), + bottomRight: Radius.circular(CalendarEventInformationWidgetStyles.borderRadius) + ) + ), + color: Colors.white + ), + clipBehavior: Clip.antiAlias, + padding: const EdgeInsets.all(CalendarEventInformationWidgetStyles.calendarInformationMargin), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + RichText( + text: TextSpan( + style: const TextStyle( + fontSize: CalendarEventInformationWidgetStyles.invitationMessageTextSize, + fontWeight: FontWeight.w500, + color: CalendarEventInformationWidgetStyles.invitationMessageColor + ), + children: [ + TextSpan( + text: calendarEvent.organizer?.name, + style: const TextStyle( + color: CalendarEventInformationWidgetStyles.invitationMessageColor, + fontSize: CalendarEventInformationWidgetStyles.invitationMessageTextSize, + fontWeight: FontWeight.w700 + ), + ), + TextSpan(text: AppLocalizations.of(context).invitationMessageCalendarInformation) + ] + ) + ), + const SizedBox(height: CalendarEventInformationWidgetStyles.space), + if (calendarEvent.title?.isNotEmpty == true) + EventTitleWidget(title: calendarEvent.title!), + if (calendarEvent.dateTimeEventAsString.isNotEmpty) + Padding( + padding: const EdgeInsets.only(top: CalendarEventInformationWidgetStyles.fieldTopPadding), + child: EventTimeInformationWidget(timeEvent: calendarEvent.dateTimeEventAsString), + ), + if (calendarEvent.location?.isNotEmpty == true) + Padding( + padding: const EdgeInsets.only(top: CalendarEventInformationWidgetStyles.fieldTopPadding), + child: EventLocationInformationWidget(locationEvent: calendarEvent.location!), + ), + if (calendarEvent.participants?.isNotEmpty == true && calendarEvent.organizer != null) + Padding( + padding: const EdgeInsets.only(top: CalendarEventInformationWidgetStyles.fieldTopPadding), + child: EventAttendeeInformationWidget( + attendees: calendarEvent.participants!, + organizer: calendarEvent.organizer!, + ), + ), + ], + ), + ) + ], + ) + : Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CalendarDateIconWidget(calendarEvent: calendarEvent), + Expanded(child: Container( + clipBehavior: Clip.antiAlias, + decoration: const ShapeDecoration( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topRight: Radius.circular(CalendarEventInformationWidgetStyles.borderRadius), + bottomRight: Radius.circular(CalendarEventInformationWidgetStyles.borderRadius) + ) + ), + color: Colors.white + ), + padding: const EdgeInsets.all(CalendarEventInformationWidgetStyles.calendarInformationMargin), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + RichText( + text: TextSpan( + style: const TextStyle( + fontSize: CalendarEventInformationWidgetStyles.invitationMessageTextSize, + fontWeight: FontWeight.w500, + color: CalendarEventInformationWidgetStyles.invitationMessageColor + ), + children: [ + TextSpan( + text: calendarEvent.organizer?.name, + style: const TextStyle( + color: CalendarEventInformationWidgetStyles.invitationMessageColor, + fontSize: CalendarEventInformationWidgetStyles.invitationMessageTextSize, + fontWeight: FontWeight.w700 + ), + ), + TextSpan(text: AppLocalizations.of(context).invitationMessageCalendarInformation) + ] + ) + ), + const SizedBox(height: CalendarEventInformationWidgetStyles.space), + if (calendarEvent.title?.isNotEmpty == true) + EventTitleWidget(title: calendarEvent.title!), + if (calendarEvent.dateTimeEventAsString.isNotEmpty) + Padding( + padding: const EdgeInsets.only(top: CalendarEventInformationWidgetStyles.fieldTopPadding), + child: EventTimeInformationWidget(timeEvent: calendarEvent.dateTimeEventAsString), + ), + if (calendarEvent.location?.isNotEmpty == true) + Padding( + padding: const EdgeInsets.only(top: CalendarEventInformationWidgetStyles.fieldTopPadding), + child: EventLocationInformationWidget(locationEvent: calendarEvent.location!), + ), + if (calendarEvent.participants?.isNotEmpty == true && calendarEvent.organizer != null) + Padding( + padding: const EdgeInsets.only(top: CalendarEventInformationWidgetStyles.fieldTopPadding), + child: EventAttendeeInformationWidget( + attendees: calendarEvent.participants!, + organizer: calendarEvent.organizer!, + ), + ), + ], + ), + )) + ], + ), + ); + } +} \ No newline at end of file diff --git a/lib/features/email/presentation/widgets/calendar_event/event_attendee_detail_widget.dart b/lib/features/email/presentation/widgets/calendar_event/event_attendee_detail_widget.dart new file mode 100644 index 0000000000..13f3026e6d --- /dev/null +++ b/lib/features/email/presentation/widgets/calendar_event/event_attendee_detail_widget.dart @@ -0,0 +1,90 @@ + +import 'package:core/utils/app_logger.dart'; +import 'package:flutter/material.dart'; +import 'package:jmap_dart_client/jmap/mail/calendar/properties/attendee/calendar_attendee.dart'; +import 'package:jmap_dart_client/jmap/mail/calendar/properties/calendar_organizer.dart'; +import 'package:tmail_ui_user/features/email/presentation/extensions/list_attendee_extension.dart'; +import 'package:tmail_ui_user/features/email/presentation/styles/event_attendee_detail_widget_styles.dart'; +import 'package:tmail_ui_user/features/email/presentation/widgets/calendar_event/attendee_widget.dart'; +import 'package:tmail_ui_user/features/email/presentation/widgets/calendar_event/organizer_widget.dart'; +import 'package:tmail_ui_user/features/email/presentation/widgets/calendar_event/see_all_attendees_button_widget.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; + +class EventAttendeeDetailWidget extends StatefulWidget { + + static const int maxAttendeeDisplayed = 6; + + final List attendees; + final CalendarOrganizer organizer; + + const EventAttendeeDetailWidget({ + super.key, + required this.attendees, + required this.organizer + }); + + @override + State createState() => _EventAttendeeDetailWidgetState(); +} + +class _EventAttendeeDetailWidgetState extends State { + + late List _attendeesDisplayed; + late bool _isShowAllAttendee; + + @override + void initState() { + super.initState(); + _attendeesDisplayed = _splitAttendees(widget.attendees); + _isShowAllAttendee = widget.attendees.length <= EventAttendeeDetailWidget.maxAttendeeDisplayed; + log('_EventAttendeeDetailWidgetState::initState:attendees: ${widget.attendees.length} | _isShowAllAttendee: $_isShowAllAttendee'); + } + + @override + Widget build(BuildContext context) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: EventAttendeeDetailWidgetStyles.maxWidth, + child: Text( + AppLocalizations.of(context).attendees, + style: const TextStyle( + fontSize: EventAttendeeDetailWidgetStyles.textSize, + fontWeight: FontWeight.w500, + color: EventAttendeeDetailWidgetStyles.labelColor + ), + ), + ), + Expanded(child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + OrganizerWidget(organizer: widget.organizer), + ..._attendeesDisplayed + .map((attendee) => AttendeeWidget(attendee: attendee)) + .toList(), + if (!_isShowAllAttendee) + Padding( + padding: const EdgeInsets.only(top: EventAttendeeDetailWidgetStyles.fieldTopPadding), + child: SeeAllAttendeesButtonWidget( + onTap: () { + setState(() { + _attendeesDisplayed = widget.attendees.withoutOrganizer(widget.organizer); + _isShowAllAttendee = true; + }); + } + ), + ) + ] + )) + ], + ); + } + + List _splitAttendees(List attendees) { + final attendeesWithoutOrganizer = attendees.withoutOrganizer(widget.organizer); + return attendeesWithoutOrganizer.length > EventAttendeeDetailWidget.maxAttendeeDisplayed + ? attendeesWithoutOrganizer.sublist(0, EventAttendeeDetailWidget.maxAttendeeDisplayed - 1) + : attendeesWithoutOrganizer; + } +} \ No newline at end of file diff --git a/lib/features/email/presentation/widgets/calendar_event/event_attendee_information_widget.dart b/lib/features/email/presentation/widgets/calendar_event/event_attendee_information_widget.dart new file mode 100644 index 0000000000..f38edc797a --- /dev/null +++ b/lib/features/email/presentation/widgets/calendar_event/event_attendee_information_widget.dart @@ -0,0 +1,67 @@ + +import 'package:core/presentation/utils/responsive_utils.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:jmap_dart_client/jmap/mail/calendar/properties/attendee/calendar_attendee.dart'; +import 'package:jmap_dart_client/jmap/mail/calendar/properties/calendar_organizer.dart'; +import 'package:tmail_ui_user/features/email/presentation/extensions/list_attendee_extension.dart'; +import 'package:tmail_ui_user/features/email/presentation/styles/event_attendee_information_widget_styles.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; + +class EventAttendeeInformationWidget extends StatelessWidget { + + final List attendees; + final CalendarOrganizer organizer; + + const EventAttendeeInformationWidget({ + super.key, + required this.attendees, + required this.organizer + }); + + @override + Widget build(BuildContext context) { + final responsiveUtils = Get.find(); + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: EventAttendeeInformationWidgetStyles.maxWidth, + child: Text( + AppLocalizations.of(context).who, + style: const TextStyle( + fontSize: EventAttendeeInformationWidgetStyles.textSize, + fontWeight: FontWeight.w500, + color: EventAttendeeInformationWidgetStyles.labelColor + ), + ), + ), + Expanded(child: RichText( + text: TextSpan( + style: const TextStyle( + fontSize: EventAttendeeInformationWidgetStyles.textSize, + fontWeight: FontWeight.w500, + color: EventAttendeeInformationWidgetStyles.valueColor + ), + children: [ + TextSpan( + text: '${organizer.mailto?.value} (${AppLocalizations.of(context).organizer})', + style: const TextStyle( + color: EventAttendeeInformationWidgetStyles.valueOrganizerColor, + fontSize: EventAttendeeInformationWidgetStyles.textSize, + fontWeight: FontWeight.w500 + ), + ), + const TextSpan(text: ', '), + TextSpan(text: attendees.withoutOrganizer(organizer).mailtoAsString) + ] + ), + overflow: responsiveUtils.isPortraitMobile(context) + ? TextOverflow.clip + : TextOverflow.ellipsis, + maxLines: responsiveUtils.isPortraitMobile(context) ? null : 2, + )) + ], + ); + } +} \ No newline at end of file diff --git a/lib/features/email/presentation/widgets/calendar_event/event_description_detail_widget.dart b/lib/features/email/presentation/widgets/calendar_event/event_description_detail_widget.dart new file mode 100644 index 0000000000..298a5465e7 --- /dev/null +++ b/lib/features/email/presentation/widgets/calendar_event/event_description_detail_widget.dart @@ -0,0 +1,48 @@ + +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/presentation/resources/image_paths.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:get/get.dart'; +import 'package:tmail_ui_user/features/email/presentation/styles/event_description_detail_widget_styles.dart'; + +class EventDescriptionDetailWidget extends StatelessWidget { + + final String description; + + const EventDescriptionDetailWidget({ + super.key, + required this.description + }); + + @override + Widget build(BuildContext context) { + final imagePath = Get.find(); + return Container( + clipBehavior: Clip.antiAlias, + decoration: const BoxDecoration( + color: AppColor.colorEventDescriptionBackground, + borderRadius: BorderRadius.all(Radius.circular(EventDescriptionDetailWidgetStyles.borderRadius)), + ), + width: double.infinity, + padding: const EdgeInsetsDirectional.all(EventDescriptionDetailWidgetStyles.contentPadding), + child: Stack( + children: [ + Text( + description, + style: const TextStyle( + fontWeight: FontWeight.w500, + fontSize: EventDescriptionDetailWidgetStyles.textSize, + color: EventDescriptionDetailWidgetStyles.valueColor + ) + ), + PositionedDirectional( + top: 0, + end: 0, + child: SvgPicture.asset(imagePath.icFormatQuote) + ) + ], + ), + ); + } +} \ No newline at end of file diff --git a/lib/features/email/presentation/widgets/calendar_event/event_link_detail_widget.dart b/lib/features/email/presentation/widgets/calendar_event/event_link_detail_widget.dart new file mode 100644 index 0000000000..32bbccd945 --- /dev/null +++ b/lib/features/email/presentation/widgets/calendar_event/event_link_detail_widget.dart @@ -0,0 +1,39 @@ + +import 'package:flutter/material.dart'; +import 'package:tmail_ui_user/features/base/widget/hyper_link_widget.dart'; +import 'package:tmail_ui_user/features/email/presentation/styles/event_link_detail_widget_styles.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; + +class EventLinkDetailWidget extends StatelessWidget { + + final List listHyperLink; + + const EventLinkDetailWidget({ + super.key, + required this.listHyperLink + }); + + @override + Widget build(BuildContext context) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: EventLinkDetailWidgetStyles.maxWidth, + child: Text( + AppLocalizations.of(context).link, + style: const TextStyle( + fontSize: EventLinkDetailWidgetStyles.textSize, + fontWeight: FontWeight.w500, + color: EventLinkDetailWidgetStyles.labelColor + ), + ), + ), + Expanded(child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: listHyperLink.map((link) => HyperLinkWidget(urlString: link)).toList(), + )) + ], + ); + } +} \ No newline at end of file diff --git a/lib/features/email/presentation/widgets/calendar_event/event_location_detail_widget.dart b/lib/features/email/presentation/widgets/calendar_event/event_location_detail_widget.dart new file mode 100644 index 0000000000..36b046848d --- /dev/null +++ b/lib/features/email/presentation/widgets/calendar_event/event_location_detail_widget.dart @@ -0,0 +1,42 @@ + +import 'package:flutter/material.dart'; +import 'package:tmail_ui_user/features/email/presentation/styles/event_location_detail_widget_styles.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; + +class EventLocationDetailWidget extends StatelessWidget { + + final String locationEvent; + + const EventLocationDetailWidget({ + super.key, + required this.locationEvent + }); + + @override + Widget build(BuildContext context) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: EventLocationDetailWidgetStyles.maxWidth, + child: Text( + AppLocalizations.of(context).location, + style: const TextStyle( + fontSize: EventLocationDetailWidgetStyles.textSize, + fontWeight: FontWeight.w500, + color: EventLocationDetailWidgetStyles.labelColor + ), + ), + ), + Expanded(child: Text( + locationEvent, + style: const TextStyle( + fontSize: EventLocationDetailWidgetStyles.textSize, + fontWeight: FontWeight.w500, + color: EventLocationDetailWidgetStyles.valueColor + ), + )) + ], + ); + } +} \ No newline at end of file diff --git a/lib/features/email/presentation/widgets/calendar_event/event_location_information_widget.dart b/lib/features/email/presentation/widgets/calendar_event/event_location_information_widget.dart new file mode 100644 index 0000000000..3aa35ff053 --- /dev/null +++ b/lib/features/email/presentation/widgets/calendar_event/event_location_information_widget.dart @@ -0,0 +1,49 @@ + +import 'package:core/presentation/utils/responsive_utils.dart'; +import 'package:core/presentation/utils/style_utils.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:tmail_ui_user/features/email/presentation/styles/event_location_information_widget_styles.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; + +class EventLocationInformationWidget extends StatelessWidget { + + final String locationEvent; + + const EventLocationInformationWidget({ + super.key, + required this.locationEvent + }); + + @override + Widget build(BuildContext context) { + final responsiveUtils = Get.find(); + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: EventLocationInformationWidgetStyles.maxWidth, + child: Text( + AppLocalizations.of(context).where, + style: const TextStyle( + fontSize: EventLocationInformationWidgetStyles.textSize, + fontWeight: FontWeight.w500, + color: EventLocationInformationWidgetStyles.labelColor + ), + ), + ), + Expanded(child: Text( + locationEvent, + overflow: responsiveUtils.isPortraitMobile(context) ? null : CommonTextStyle.defaultTextOverFlow, + softWrap: responsiveUtils.isPortraitMobile(context) ? null : CommonTextStyle.defaultSoftWrap, + maxLines: responsiveUtils.isPortraitMobile(context) ? null : 1, + style: const TextStyle( + fontSize: EventLocationInformationWidgetStyles.textSize, + fontWeight: FontWeight.w500, + color: EventLocationInformationWidgetStyles.valueColor + ), + )) + ], + ); + } +} \ No newline at end of file diff --git a/lib/features/email/presentation/widgets/calendar_event/event_time_detail_widget.dart b/lib/features/email/presentation/widgets/calendar_event/event_time_detail_widget.dart new file mode 100644 index 0000000000..1af9a6ca94 --- /dev/null +++ b/lib/features/email/presentation/widgets/calendar_event/event_time_detail_widget.dart @@ -0,0 +1,42 @@ + +import 'package:flutter/material.dart'; +import 'package:tmail_ui_user/features/email/presentation/styles/event_time_detail_widget_styles.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; + +class EventTimeWidgetWidget extends StatelessWidget { + + final String timeEvent; + + const EventTimeWidgetWidget({ + super.key, + required this.timeEvent + }); + + @override + Widget build(BuildContext context) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: EventTimeDetailWidgetStyles.maxWidth, + child: Text( + AppLocalizations.of(context).time, + style: const TextStyle( + fontSize: EventTimeDetailWidgetStyles.textSize, + fontWeight: FontWeight.w500, + color: EventTimeDetailWidgetStyles.labelColor + ), + ), + ), + Expanded(child: Text( + timeEvent, + style: const TextStyle( + fontSize: EventTimeDetailWidgetStyles.textSize, + fontWeight: FontWeight.w500, + color: EventTimeDetailWidgetStyles.valueColor + ), + )) + ], + ); + } +} \ No newline at end of file diff --git a/lib/features/email/presentation/widgets/calendar_event/event_time_information_widget.dart b/lib/features/email/presentation/widgets/calendar_event/event_time_information_widget.dart new file mode 100644 index 0000000000..1130e5a23e --- /dev/null +++ b/lib/features/email/presentation/widgets/calendar_event/event_time_information_widget.dart @@ -0,0 +1,49 @@ + +import 'package:core/presentation/utils/responsive_utils.dart'; +import 'package:core/presentation/utils/style_utils.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:tmail_ui_user/features/email/presentation/styles/event_time_information_widget_styles.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; + +class EventTimeInformationWidget extends StatelessWidget { + + final String timeEvent; + + const EventTimeInformationWidget({ + super.key, + required this.timeEvent + }); + + @override + Widget build(BuildContext context) { + final responsiveUtils = Get.find(); + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: EventTimeInformationWidgetStyles.maxWidth, + child: Text( + AppLocalizations.of(context).when, + style: const TextStyle( + fontSize: EventTimeInformationWidgetStyles.textSize, + fontWeight: FontWeight.w500, + color: EventTimeInformationWidgetStyles.labelColor + ), + ), + ), + Expanded(child: Text( + timeEvent, + overflow: responsiveUtils.isPortraitMobile(context) ? null : CommonTextStyle.defaultTextOverFlow, + softWrap: responsiveUtils.isPortraitMobile(context) ? null : CommonTextStyle.defaultSoftWrap, + maxLines: responsiveUtils.isPortraitMobile(context) ? null : 1, + style: const TextStyle( + fontSize: EventTimeInformationWidgetStyles.textSize, + fontWeight: FontWeight.w500, + color: EventTimeInformationWidgetStyles.valueColor + ), + )) + ], + ); + } +} \ No newline at end of file diff --git a/lib/features/email/presentation/widgets/calendar_event/event_title_widget.dart b/lib/features/email/presentation/widgets/calendar_event/event_title_widget.dart new file mode 100644 index 0000000000..99284c95c1 --- /dev/null +++ b/lib/features/email/presentation/widgets/calendar_event/event_title_widget.dart @@ -0,0 +1,22 @@ + +import 'package:flutter/material.dart'; +import 'package:tmail_ui_user/features/email/presentation/styles/event_title_widget_styles.dart'; + +class EventTitleWidget extends StatelessWidget { + + final String title; + + const EventTitleWidget({super.key, required this.title}); + + @override + Widget build(BuildContext context) { + return Text( + title, + style: const TextStyle( + fontWeight: FontWeight.w500, + fontSize: EventTitleWidgetStyles.textSize, + color: EventTitleWidgetStyles.textColor + ) + ); + } +} \ No newline at end of file diff --git a/lib/features/email/presentation/widgets/calendar_event/organizer_widget.dart b/lib/features/email/presentation/widgets/calendar_event/organizer_widget.dart new file mode 100644 index 0000000000..4ac0dce035 --- /dev/null +++ b/lib/features/email/presentation/widgets/calendar_event/organizer_widget.dart @@ -0,0 +1,43 @@ + +import 'package:flutter/material.dart'; +import 'package:jmap_dart_client/jmap/mail/calendar/properties/calendar_organizer.dart'; +import 'package:tmail_ui_user/features/email/presentation/styles/organizer_widget_styles.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; + +class OrganizerWidget extends StatelessWidget { + + final CalendarOrganizer organizer; + + const OrganizerWidget({ + super.key, + required this.organizer + }); + + @override + Widget build(BuildContext context) { + return RichText( + text: TextSpan( + style: const TextStyle( + fontSize: OrganizerWidgetStyles.textSize, + fontWeight: FontWeight.w500, + color: OrganizerWidgetStyles.textColor + ), + children: [ + if (organizer.name?.isNotEmpty == true) + TextSpan(text: organizer.name!), + if (organizer.mailto?.value.isNotEmpty == true) + TextSpan( + text: ' <${organizer.mailto!.value}> ', + style: const TextStyle( + color: OrganizerWidgetStyles.mailtoColor, + fontSize: OrganizerWidgetStyles.textSize, + fontWeight: FontWeight.w500 + ), + ), + TextSpan(text: '(${AppLocalizations.of(context).organizer})'), + const TextSpan(text: ', '), + ] + ) + ); + } +} \ No newline at end of file diff --git a/lib/features/email/presentation/widgets/calendar_event/see_all_attendees_button_widget.dart b/lib/features/email/presentation/widgets/calendar_event/see_all_attendees_button_widget.dart new file mode 100644 index 0000000000..78ce4d4dcf --- /dev/null +++ b/lib/features/email/presentation/widgets/calendar_event/see_all_attendees_button_widget.dart @@ -0,0 +1,35 @@ + +import 'package:flutter/cupertino.dart'; +import 'package:tmail_ui_user/features/base/widget/material_text_button.dart'; +import 'package:tmail_ui_user/features/email/presentation/styles/see_all_attendees_button_widget_styles.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; + +class SeeAllAttendeesButtonWidget extends StatelessWidget { + + final VoidCallback onTap; + + const SeeAllAttendeesButtonWidget({ + super.key, + required this.onTap + }); + + @override + Widget build(BuildContext context) { + return Transform( + transform: Matrix4.translationValues(SeeAllAttendeesButtonWidgetStyles.horizontalMargin, 0.0, 0.0), + child: MaterialTextButton( + label: AppLocalizations.of(context).seeAllAttendees, + onTap: onTap, + borderRadius: SeeAllAttendeesButtonWidgetStyles.borderRadius, + padding: const EdgeInsetsDirectional.symmetric( + horizontal: SeeAllAttendeesButtonWidgetStyles.horizontalPadding, + vertical: SeeAllAttendeesButtonWidgetStyles.verticalPadding + ), + customStyle: const TextStyle( + fontSize: SeeAllAttendeesButtonWidgetStyles.textSize, + color: SeeAllAttendeesButtonWidgetStyles.textColor + ), + ), + ); + } +} diff --git a/lib/features/email/presentation/widgets/email_view_loading_bar_widget.dart b/lib/features/email/presentation/widgets/email_view_loading_bar_widget.dart new file mode 100644 index 0000000000..4d92cf4785 --- /dev/null +++ b/lib/features/email/presentation/widgets/email_view_loading_bar_widget.dart @@ -0,0 +1,34 @@ +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:dartz/dartz.dart'; +import 'package:flutter/material.dart'; +import 'package:model/email/presentation_email.dart'; +import 'package:tmail_ui_user/features/base/widget/cupertino_loading_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'; + +class EmailViewLoadingBarWidget extends StatelessWidget { + + final PresentationEmail selectedEmail; + final Either viewState; + + const EmailViewLoadingBarWidget({ + super.key, + required this.viewState, + required this.selectedEmail + }); + + @override + Widget build(BuildContext context) { + return viewState.fold( + (failure) => const SizedBox.shrink(), + (success) { + if (success is GetEmailContentLoading || (success is ParseCalendarEventLoading && selectedEmail.hasCalendarEvent)) { + return const CupertinoLoadingWidget(); + } else { + return const SizedBox.shrink(); + } + } + ); + } +} diff --git a/lib/features/login/data/extensions/token_response_extension.dart b/lib/features/login/data/extensions/token_response_extension.dart index 3807bdc7a1..fbfff79109 100644 --- a/lib/features/login/data/extensions/token_response_extension.dart +++ b/lib/features/login/data/extensions/token_response_extension.dart @@ -4,11 +4,11 @@ import 'package:model/model.dart'; extension TokenResponseExtension on TokenResponse { - TokenOIDC toTokenOIDC() { + TokenOIDC toTokenOIDC({String? maybeAvailableRefreshToken}) { return TokenOIDC( accessToken ?? '', TokenId(idToken ?? ''), - refreshToken ?? '', + refreshToken ?? maybeAvailableRefreshToken ?? '', expiredTime: accessTokenExpirationDateTime ?? DateTime.now()); } } \ No newline at end of file diff --git a/lib/features/login/data/network/authentication_client/authentication_client_mobile.dart b/lib/features/login/data/network/authentication_client/authentication_client_mobile.dart index 2f20337af9..9bfe112830 100644 --- a/lib/features/login/data/network/authentication_client/authentication_client_mobile.dart +++ b/lib/features/login/data/network/authentication_client/authentication_client_mobile.dart @@ -7,10 +7,10 @@ import 'package:model/oidc/response/oidc_discovery_response.dart'; import 'package:model/oidc/token_id.dart'; import 'package:model/oidc/token_oidc.dart'; import 'package:tmail_ui_user/features/login/data/extensions/authentication_token_extension.dart'; -import 'package:tmail_ui_user/features/login/domain/extensions/oidc_configuration_extensions.dart'; import 'package:tmail_ui_user/features/login/data/extensions/token_response_extension.dart'; import 'package:tmail_ui_user/features/login/data/network/authentication_client/authentication_client_base.dart'; import 'package:tmail_ui_user/features/login/domain/exceptions/authentication_exception.dart'; +import 'package:tmail_ui_user/features/login/domain/extensions/oidc_configuration_extensions.dart'; class AuthenticationClientMobile implements AuthenticationClientBase { @@ -75,7 +75,7 @@ class AuthenticationClientMobile implements AuthenticationClientBase { log('AuthenticationClientMobile::refreshingTokensOIDC(): refreshToken: ${tokenResponse?.accessToken}'); if (tokenResponse != null) { - final tokenOIDC = tokenResponse.toTokenOIDC(); + final tokenOIDC = tokenResponse.toTokenOIDC(maybeAvailableRefreshToken: refreshToken); if (tokenOIDC.isTokenValid()) { return tokenOIDC; } else { diff --git a/lib/features/login/data/network/authentication_client/authentication_client_web.dart b/lib/features/login/data/network/authentication_client/authentication_client_web.dart index ae134abcc6..1544923c0b 100644 --- a/lib/features/login/data/network/authentication_client/authentication_client_web.dart +++ b/lib/features/login/data/network/authentication_client/authentication_client_web.dart @@ -1,19 +1,19 @@ import 'package:core/utils/app_logger.dart'; +import 'package:flutter_appauth_platform_interface/flutter_appauth_platform_interface.dart'; import 'package:get/get.dart'; +import 'package:model/oidc/oidc_configuration.dart'; import 'package:model/oidc/response/oidc_discovery_response.dart'; +import 'package:model/oidc/token_id.dart'; +import 'package:model/oidc/token_oidc.dart'; import 'package:tmail_ui_user/features/login/data/extensions/authentication_token_extension.dart'; -import 'package:tmail_ui_user/features/login/domain/extensions/oidc_configuration_extensions.dart'; import 'package:tmail_ui_user/features/login/data/extensions/token_response_extension.dart'; +import 'package:tmail_ui_user/features/login/data/network/authentication_client/authentication_client_base.dart'; import 'package:tmail_ui_user/features/login/data/network/config/oidc_constant.dart'; import 'package:tmail_ui_user/features/login/data/utils/library_platform/app_auth_plugin/app_auth_plugin.dart'; import 'package:tmail_ui_user/features/login/domain/exceptions/authentication_exception.dart'; +import 'package:tmail_ui_user/features/login/domain/extensions/oidc_configuration_extensions.dart'; import 'package:universal_html/html.dart' as html; -import 'package:tmail_ui_user/features/login/data/network/authentication_client/authentication_client_base.dart'; -import 'package:flutter_appauth_platform_interface/flutter_appauth_platform_interface.dart'; -import 'package:model/oidc/oidc_configuration.dart'; -import 'package:model/oidc/token_id.dart'; -import 'package:model/oidc/token_oidc.dart'; class AuthenticationClientWeb implements AuthenticationClientBase { @@ -74,7 +74,7 @@ class AuthenticationClientWeb implements AuthenticationClientBase { scopes: scopes)); if (tokenResponse != null) { - final tokenOIDC = tokenResponse.toTokenOIDC(); + final tokenOIDC = tokenResponse.toTokenOIDC(maybeAvailableRefreshToken: refreshToken); if (tokenOIDC.isTokenValid()) { return tokenOIDC; } else { diff --git a/lib/features/mailbox/presentation/widgets/mailbox_folder_tile_builder.dart b/lib/features/mailbox/presentation/widgets/mailbox_folder_tile_builder.dart index d98054b4fb..db559de88f 100644 --- a/lib/features/mailbox/presentation/widgets/mailbox_folder_tile_builder.dart +++ b/lib/features/mailbox/presentation/widgets/mailbox_folder_tile_builder.dart @@ -282,6 +282,7 @@ class MailBoxFolderTileBuilder { Widget _buildTitleFolderItem(BuildContext context, {bool showTrailingItem = true}) { return Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ diff --git a/lib/features/mailbox_dashboard/presentation/bindings/mailbox_dashboard_bindings.dart b/lib/features/mailbox_dashboard/presentation/bindings/mailbox_dashboard_bindings.dart index 49f8b85dda..fc8f47a963 100644 --- a/lib/features/mailbox_dashboard/presentation/bindings/mailbox_dashboard_bindings.dart +++ b/lib/features/mailbox_dashboard/presentation/bindings/mailbox_dashboard_bindings.dart @@ -25,7 +25,7 @@ import 'package:tmail_ui_user/features/email/domain/usecases/mark_as_star_email_ import 'package:tmail_ui_user/features/email/domain/usecases/move_to_mailbox_interactor.dart'; import 'package:tmail_ui_user/features/email/presentation/controller/email_supervisor_controller.dart'; import 'package:tmail_ui_user/features/email/presentation/controller/single_email_controller.dart'; -import 'package:tmail_ui_user/features/email/presentation/email_bindings.dart'; +import 'package:tmail_ui_user/features/email/presentation/bindings/email_bindings.dart'; import 'package:tmail_ui_user/features/login/data/datasource/account_datasource.dart'; import 'package:tmail_ui_user/features/login/data/datasource/authentication_oidc_datasource.dart'; import 'package:tmail_ui_user/features/login/data/datasource_impl/authentication_oidc_datasource_impl.dart'; diff --git a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart index e1b29772df..8817425f5a 100644 --- a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart +++ b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart @@ -46,6 +46,7 @@ import 'package:tmail_ui_user/features/email/domain/usecases/mark_as_star_email_ import 'package:tmail_ui_user/features/email/domain/usecases/move_to_mailbox_interactor.dart'; import 'package:tmail_ui_user/features/email/presentation/action/email_ui_action.dart'; import 'package:tmail_ui_user/features/email/presentation/model/composer_arguments.dart'; +import 'package:tmail_ui_user/features/email/presentation/utils/email_utils.dart'; import 'package:tmail_ui_user/features/login/domain/usecases/delete_authority_oidc_interactor.dart'; import 'package:tmail_ui_user/features/login/domain/usecases/get_authenticated_account_interactor.dart'; import 'package:tmail_ui_user/features/login/domain/usecases/update_authentication_account_interactor.dart'; @@ -105,7 +106,6 @@ import 'package:tmail_ui_user/features/sending_queue/domain/usecases/update_send import 'package:tmail_ui_user/features/sending_queue/presentation/model/sending_email_action_type.dart'; import 'package:tmail_ui_user/features/sending_queue/presentation/model/sending_email_arguments.dart'; import 'package:tmail_ui_user/features/session/domain/usecases/store_session_interactor.dart'; -import 'package:tmail_ui_user/features/thread/domain/constants/thread_constants.dart'; import 'package:tmail_ui_user/features/thread/domain/model/filter_message_option.dart'; import 'package:tmail_ui_user/features/thread/domain/model/search_query.dart'; import 'package:tmail_ui_user/features/thread/domain/state/empty_trash_folder_state.dart'; @@ -1598,7 +1598,7 @@ class MailboxDashBoardController extends ReloadableController { session, accountId, emailId, - properties: ThreadConstants.propertiesDefault + properties: EmailUtils.getPropertiesForEmailGetMethod(session, accountId) )); } diff --git a/lib/features/mailbox_dashboard/presentation/mailbox_dashboard_view.dart b/lib/features/mailbox_dashboard/presentation/mailbox_dashboard_view.dart index 569bc19399..e1af349057 100644 --- a/lib/features/mailbox_dashboard/presentation/mailbox_dashboard_view.dart +++ b/lib/features/mailbox_dashboard/presentation/mailbox_dashboard_view.dart @@ -61,7 +61,7 @@ class MailboxDashBoardView extends BaseMailboxDashBoardView { children: [ SizedBox( width: ResponsiveUtils.defaultSizeLeftMenuMobile, - child: _buildScaffoldHaveDrawer(body: SendingQueueView())), + child: _buildScaffoldHaveDrawer(body: const SendingQueueView())), Expanded(child: EmailView()), ], ); @@ -70,7 +70,7 @@ class MailboxDashBoardView extends BaseMailboxDashBoardView { desktop: bodyView, tabletLarge: bodyView, landscapeTablet: bodyView, - mobile: _buildScaffoldHaveDrawer(body: SendingQueueView())); + mobile: _buildScaffoldHaveDrawer(body: const SendingQueueView())); case DashboardRoutes.waiting: return const Center( child: SizedBox( diff --git a/lib/features/push_notification/presentation/listener/email_change_listener.dart b/lib/features/push_notification/presentation/listener/email_change_listener.dart index 8621ba2b7c..7535b70d94 100644 --- a/lib/features/push_notification/presentation/listener/email_change_listener.dart +++ b/lib/features/push_notification/presentation/listener/email_change_listener.dart @@ -29,6 +29,7 @@ import 'package:tmail_ui_user/features/email/domain/usecases/get_list_detailed_e import 'package:tmail_ui_user/features/email/domain/usecases/get_stored_email_state_interactor.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/store_list_new_email_interator.dart'; import 'package:tmail_ui_user/features/email/presentation/action/email_ui_action.dart'; +import 'package:tmail_ui_user/features/email/presentation/utils/email_utils.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart'; import 'package:tmail_ui_user/features/push_notification/domain/exceptions/fcm_exception.dart'; import 'package:tmail_ui_user/features/push_notification/domain/state/get_email_changes_to_push_notification_state.dart'; @@ -175,7 +176,7 @@ class EmailChangeListener extends ChangeListener { _accountId!, _userName!, state, - propertiesCreated: ThreadConstants.propertiesDefault, + propertiesCreated: EmailUtils.getPropertiesForEmailGetMethod(_session!, _accountId!), propertiesUpdated: ThreadConstants.propertiesUpdatedDefault, )); } diff --git a/lib/features/search/email/presentation/search_email_controller.dart b/lib/features/search/email/presentation/search_email_controller.dart index 722d0fe33c..8d9612be19 100644 --- a/lib/features/search/email/presentation/search_email_controller.dart +++ b/lib/features/search/email/presentation/search_email_controller.dart @@ -35,6 +35,7 @@ import 'package:tmail_ui_user/features/email/domain/state/delete_multiple_emails import 'package:tmail_ui_user/features/email/domain/state/mark_as_email_read_state.dart'; import 'package:tmail_ui_user/features/email/domain/state/mark_as_email_star_state.dart'; import 'package:tmail_ui_user/features/email/domain/state/move_to_mailbox_state.dart'; +import 'package:tmail_ui_user/features/email/presentation/utils/email_utils.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/action/mailbox_ui_action.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_actions.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/model/recent_search.dart'; @@ -226,7 +227,7 @@ class SearchEmailController extends BaseController ..add(EmailComparator(EmailComparatorProperty.receivedAt) ..setIsAscending(false)), filter: simpleSearchFilter.value.mappingToEmailFilterCondition(), - properties: ThreadConstants.propertiesDefault, + properties: EmailUtils.getPropertiesForEmailGetMethod(session!, accountId!), )); } } @@ -299,7 +300,7 @@ class SearchEmailController extends BaseController ..add(EmailComparator(EmailComparatorProperty.receivedAt) ..setIsAscending(false)), filter: simpleSearchFilter.value.mappingToEmailFilterCondition(), - properties: ThreadConstants.propertiesDefault, + properties: EmailUtils.getPropertiesForEmailGetMethod(session!, accountId!), )); } } @@ -344,7 +345,7 @@ class SearchEmailController extends BaseController ..add(EmailComparator(EmailComparatorProperty.receivedAt) ..setIsAscending(false)), filter: simpleSearchFilter.value.mappingToEmailFilterCondition(), - properties: ThreadConstants.propertiesDefault, + properties: EmailUtils.getPropertiesForEmailGetMethod(session!, accountId!), lastEmailId: lastEmail.id )); } diff --git a/lib/features/thread/data/extensions/email_cache_extension.dart b/lib/features/thread/data/extensions/email_cache_extension.dart index 914f57612a..5d101f04ab 100644 --- a/lib/features/thread/data/extensions/email_cache_extension.dart +++ b/lib/features/thread/data/extensions/email_cache_extension.dart @@ -4,6 +4,7 @@ import 'package:jmap_dart_client/jmap/core/id.dart'; import 'package:jmap_dart_client/jmap/core/unsigned_int.dart'; import 'package:jmap_dart_client/jmap/core/utc_date.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; +import 'package:jmap_dart_client/jmap/mail/email/individual_header_identifier.dart'; import 'package:jmap_dart_client/jmap/mail/email/keyword_identifier.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; import 'package:tmail_ui_user/features/cleanup/domain/model/email_cleanup_rule.dart'; @@ -31,6 +32,9 @@ extension EmailCacheExtension on EmailCache { mailboxIds: mailboxIds != null ? Map.fromIterables(mailboxIds!.keys.map((value) => MailboxId(Id(value))), mailboxIds!.values) : null, + headerCalendarEvent: headerCalendarEvent != null + ? Map.fromIterables(headerCalendarEvent!.keys.map((value) => IndividualHeaderIdentifier(value)), headerCalendarEvent!.values) + : null ); } diff --git a/lib/features/thread/data/extensions/email_extension.dart b/lib/features/thread/data/extensions/email_extension.dart index 037e9182d7..d6a02dd60d 100644 --- a/lib/features/thread/data/extensions/email_extension.dart +++ b/lib/features/thread/data/extensions/email_extension.dart @@ -1,5 +1,6 @@ import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; +import 'package:tmail_ui_user/features/thread/data/extensions/map_header_identifier_id_extension.dart'; import 'package:tmail_ui_user/features/thread/data/model/email_cache.dart'; import 'package:tmail_ui_user/features/thread/data/extensions/map_keywords_extension.dart'; import 'package:tmail_ui_user/features/thread/data/extensions/email_address_extension.dart'; @@ -23,6 +24,7 @@ extension EmailExtension on Email { bcc: bcc?.map((emailAddress) => emailAddress.toEmailAddressHiveCache()).toList(), replyTo: replyTo?.map((emailAddress) => emailAddress.toEmailAddressHiveCache()).toList(), mailboxIds: mailboxIds?.toMapString(), + headerCalendarEvent: headerCalendarEvent?.toMapString(), ); } diff --git a/lib/features/thread/data/extensions/map_header_identifier_id_extension.dart b/lib/features/thread/data/extensions/map_header_identifier_id_extension.dart new file mode 100644 index 0000000000..081749cc40 --- /dev/null +++ b/lib/features/thread/data/extensions/map_header_identifier_id_extension.dart @@ -0,0 +1,7 @@ + +import 'package:jmap_dart_client/jmap/mail/email/individual_header_identifier.dart'; + +extension MapHeaderIdentifierExtension on Map { + + Map toMapString() => Map.fromIterables(keys.map((identifier) => identifier.value), values); +} \ No newline at end of file diff --git a/lib/features/thread/data/model/email_cache.dart b/lib/features/thread/data/model/email_cache.dart index a407d5125c..6db80e1d50 100644 --- a/lib/features/thread/data/model/email_cache.dart +++ b/lib/features/thread/data/model/email_cache.dart @@ -51,6 +51,9 @@ class EmailCache extends HiveObject with EquatableMixin { @HiveField(13) Map? mailboxIds; + @HiveField(14) + Map? headerCalendarEvent; + EmailCache( this.id, { @@ -67,6 +70,7 @@ class EmailCache extends HiveObject with EquatableMixin { this.bcc, this.replyTo, this.mailboxIds, + this.headerCalendarEvent, } ); @@ -86,5 +90,6 @@ class EmailCache extends HiveObject with EquatableMixin { preview, hasAttachment, mailboxIds, + headerCalendarEvent, ]; } \ No newline at end of file diff --git a/lib/features/thread/domain/constants/thread_constants.dart b/lib/features/thread/domain/constants/thread_constants.dart index 0bcc4f4064..a60335f331 100644 --- a/lib/features/thread/domain/constants/thread_constants.dart +++ b/lib/features/thread/domain/constants/thread_constants.dart @@ -1,5 +1,6 @@ import 'package:jmap_dart_client/jmap/core/properties/properties.dart'; import 'package:jmap_dart_client/jmap/core/unsigned_int.dart'; +import 'package:jmap_dart_client/jmap/mail/email/individual_header_identifier.dart'; import 'package:model/email/email_property.dart'; class ThreadConstants { @@ -67,4 +68,22 @@ class ThreadConstants { EmailProperty.attachments, EmailProperty.headers }); + + static final propertiesCalendarEvent = Properties({ + EmailProperty.id, + EmailProperty.subject, + EmailProperty.from, + EmailProperty.to, + EmailProperty.cc, + EmailProperty.bcc, + EmailProperty.keywords, + EmailProperty.size, + EmailProperty.receivedAt, + EmailProperty.sentAt, + EmailProperty.preview, + EmailProperty.hasAttachment, + EmailProperty.replyTo, + EmailProperty.mailboxIds, + IndividualHeaderIdentifier.headerCalendarEvent.value, + }); } \ No newline at end of file diff --git a/lib/features/thread/presentation/mixin/base_email_item_tile.dart b/lib/features/thread/presentation/mixin/base_email_item_tile.dart index 3e95e18356..5484ed1412 100644 --- a/lib/features/thread/presentation/mixin/base_email_item_tile.dart +++ b/lib/features/thread/presentation/mixin/base_email_item_tile.dart @@ -18,6 +18,7 @@ import 'package:model/mailbox/presentation_mailbox.dart'; import 'package:model/mailbox/select_mode.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/extensions/presentation_mailbox_extension.dart'; import 'package:tmail_ui_user/features/thread/domain/model/search_query.dart'; +import 'package:tmail_ui_user/features/thread/presentation/styles/item_email_tile_styles.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; typedef OnPressEmailActionClick = void Function(EmailActionType, PresentationEmail); @@ -330,4 +331,22 @@ mixin BaseEmailItemTile { return null; } } + + Widget buildCalendarEventIcon({ + required BuildContext context, + required PresentationEmail presentationEmail + }) { + return Padding( + padding: ItemEmailTileStyles.getSpaceCalendarEventIcon(context, responsiveUtils), + child: SvgPicture.asset( + imagePaths.icCalendarEvent, + width: 20, + height: 20, + fit: BoxFit.fill, + colorFilter: presentationEmail.hasRead + ? AppColor.colorCalendarEventRead.asFilter() + : AppColor.colorCalendarEventUnread.asFilter(), + ), + ); + } } \ No newline at end of file diff --git a/lib/features/thread/presentation/styles/item_email_tile_styles.dart b/lib/features/thread/presentation/styles/item_email_tile_styles.dart new file mode 100644 index 0000000000..2c2c6c83f6 --- /dev/null +++ b/lib/features/thread/presentation/styles/item_email_tile_styles.dart @@ -0,0 +1,16 @@ + +import 'package:core/presentation/utils/responsive_utils.dart'; +import 'package:flutter/cupertino.dart'; + +class ItemEmailTileStyles { + + static EdgeInsetsGeometry getSpaceCalendarEventIcon(BuildContext context, ResponsiveUtils responsiveUtils) { + if (responsiveUtils.isScreenWithShortestSide(context)) { + return const EdgeInsetsDirectional.only(end: 4); + } else if (responsiveUtils.isWebDesktop(context)) { + return const EdgeInsetsDirectional.only(end: 12); + } else { + return const EdgeInsetsDirectional.only(end: 8); + } + } +} \ No newline at end of file diff --git a/lib/features/thread/presentation/thread_controller.dart b/lib/features/thread/presentation/thread_controller.dart index 5cc26b2de1..aa392a8056 100644 --- a/lib/features/thread/presentation/thread_controller.dart +++ b/lib/features/thread/presentation/thread_controller.dart @@ -30,6 +30,7 @@ import 'package:tmail_ui_user/features/email/domain/state/mark_as_email_read_sta import 'package:tmail_ui_user/features/email/domain/state/mark_as_email_star_state.dart'; import 'package:tmail_ui_user/features/email/domain/state/move_to_mailbox_state.dart'; import 'package:tmail_ui_user/features/email/presentation/action/email_ui_action.dart'; +import 'package:tmail_ui_user/features/email/presentation/utils/email_utils.dart'; import 'package:tmail_ui_user/features/mailbox/domain/state/mark_as_mailbox_read_state.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/state/remove_email_drafts_state.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/action/dashboard_action.dart'; @@ -417,7 +418,7 @@ class ThreadController extends BaseController with EmailActionController { filterOption: mailboxDashBoardController.filterMessageOption.value, mailboxId: _currentMailboxId ), - propertiesCreated: ThreadConstants.propertiesDefault, + propertiesCreated: EmailUtils.getPropertiesForEmailGetMethod(_session!, _accountId!), propertiesUpdated: ThreadConstants.propertiesUpdatedDefault, )); } @@ -485,7 +486,7 @@ class ThreadController extends BaseController with EmailActionController { _accountId!, newEmailState, sort: _sortOrder, - propertiesCreated: ThreadConstants.propertiesDefault, + propertiesCreated: EmailUtils.getPropertiesForEmailGetMethod(_session!, _accountId!), propertiesUpdated: ThreadConstants.propertiesUpdatedDefault, emailFilter: EmailFilter( filter: _getFilterCondition(mailboxIdSelected: _currentMailboxId), @@ -511,7 +512,7 @@ class ThreadController extends BaseController with EmailActionController { sort: _sortOrder, filterOption: mailboxDashBoardController.filterMessageOption.value, filter: _getFilterCondition(oldestEmail: oldestEmail, mailboxIdSelected: _currentMailboxId), - properties: ThreadConstants.propertiesDefault, + properties: EmailUtils.getPropertiesForEmailGetMethod(_session!, _accountId!), lastEmailId: oldestEmail?.id ) )); @@ -704,7 +705,7 @@ class ThreadController extends BaseController with EmailActionController { limit: limit ?? ThreadConstants.defaultLimit, sort: _sortOrder, filter: _searchEmailFilter.mappingToEmailFilterCondition(moreFilterCondition: _getFilterCondition()), - properties: ThreadConstants.propertiesDefault, + properties: EmailUtils.getPropertiesForEmailGetMethod(_session!, _accountId!), )); } } @@ -751,7 +752,7 @@ class ThreadController extends BaseController with EmailActionController { limit: ThreadConstants.defaultLimit, sort: _sortOrder, filter: searchController.searchEmailFilter.value.mappingToEmailFilterCondition(moreFilterCondition: _getFilterCondition()), - properties: ThreadConstants.propertiesDefault, + properties: EmailUtils.getPropertiesForEmailGetMethod(_session!, _accountId!), lastEmailId: oldestEmail?.id )); } @@ -966,7 +967,8 @@ class ThreadController extends BaseController with EmailActionController { _session!, _accountId!, emailId, - properties: ThreadConstants.propertiesDefault)); + properties: EmailUtils.getPropertiesForEmailGetMethod(_session!, _accountId!) + )); } } diff --git a/lib/features/thread/presentation/widgets/email_tile_builder.dart b/lib/features/thread/presentation/widgets/email_tile_builder.dart index d65145d9d4..d507267d61 100644 --- a/lib/features/thread/presentation/widgets/email_tile_builder.dart +++ b/lib/features/thread/presentation/widgets/email_tile_builder.dart @@ -107,6 +107,8 @@ class EmailTileBuilder with BaseEmailItemTile { child: Row( mainAxisSize: MainAxisSize.min, children: [ + if (_presentationEmail.hasCalendarEvent) + buildCalendarEventIcon(context: _context, presentationEmail: _presentationEmail), Expanded(child: buildEmailTitle( _context, _presentationEmail, diff --git a/lib/features/thread/presentation/widgets/email_tile_web_builder.dart b/lib/features/thread/presentation/widgets/email_tile_web_builder.dart index 8269aed940..82a06ba552 100644 --- a/lib/features/thread/presentation/widgets/email_tile_web_builder.dart +++ b/lib/features/thread/presentation/widgets/email_tile_web_builder.dart @@ -172,6 +172,8 @@ class EmailTileBuilder with BaseEmailItemTile { Row( mainAxisSize: MainAxisSize.min, children: [ + if (_presentationEmail.hasCalendarEvent) + buildCalendarEventIcon(context: _context, presentationEmail: _presentationEmail), Expanded(child: buildEmailTitle( _context, _presentationEmail, @@ -255,6 +257,8 @@ class EmailTileBuilder with BaseEmailItemTile { Row( mainAxisSize: MainAxisSize.min, children: [ + if (_presentationEmail.hasCalendarEvent) + buildCalendarEventIcon(context: _context, presentationEmail: _presentationEmail), Expanded(child: buildEmailTitle( _context, _presentationEmail, @@ -509,6 +513,8 @@ class EmailTileBuilder with BaseEmailItemTile { Widget _buildSubjectAndContent() { return LayoutBuilder(builder: (context, constraints) { return Row(children: [ + if (_presentationEmail.hasCalendarEvent) + buildCalendarEventIcon(context: _context, presentationEmail: _presentationEmail), if (_presentationEmail.getEmailTitle().isNotEmpty) Container( constraints: BoxConstraints(maxWidth: constraints.maxWidth / 2), diff --git a/lib/l10n/intl_fr.arb b/lib/l10n/intl_fr.arb index cc57c24430..a624a7f2af 100644 --- a/lib/l10n/intl_fr.arb +++ b/lib/l10n/intl_fr.arb @@ -2897,5 +2897,219 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "sentMailboxDisplayName": "Envoyés", + "@sentMailboxDisplayName": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "inboxMailboxDisplayName": "Boîte de réception", + "@inboxMailboxDisplayName": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "messageDialogDeleteSendingEmail": "La suppression d'un message hors ligne effacera définitivement son contenu. Vous ne pourrez pas annuler cette action ni récupérer le message de la corbeille.", + "@messageDialogDeleteSendingEmail": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "messageHaveBeenDeletedSuccessfully": "Les messages ont été supprimés avec succès", + "@messageHaveBeenDeletedSuccessfully": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "error": "Erreur", + "@error": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "connectedToTheInternet": "Connecté à Internet", + "@connectedToTheInternet": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "emptyTrash": "Vider la corbeille", + "@emptyTrash": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "cannotSelectThisImage": "Impossible de sélectionner cette image.", + "@cannotSelectThisImage": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "messageHasBeenSavedToTheSendingQueue": "Le message a été enregistré dans la file d'attente.", + "@messageHasBeenSavedToTheSendingQueue": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "bannerMessageSendingQueueView": "Les messages de la file d'attente seront envoyés ou planifiés lorsqu'ils seront en ligne.", + "@bannerMessageSendingQueueView": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "proceed": "Procéder", + "@proceed": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "youAreInOfflineMode": "Vous êtes en mode hors ligne", + "@youAreInOfflineMode": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "messageDialogWhenStoreSendingEmailSecond": " envoyer, répondre ou transférer ", + "@messageDialogWhenStoreSendingEmailSecond": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "messageDialogWhenStoreSendingEmailFirst": "Heureusement, vous pouvez toujours", + "@messageDialogWhenStoreSendingEmailFirst": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "messageDialogWhenStoreSendingEmailThird": "des messages. Ils seront délivrés lorsque vous vous connecterez à Internet. Pour modifier ces messages avant de les envoyer, rendez-vous sur ", + "@messageDialogWhenStoreSendingEmailThird": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "titleRecipientSendingEmail": "À: {recipients}", + "@titleRecipientSendingEmail": { + "type": "text", + "placeholders_order": [ + "recipients" + ], + "placeholders": { + "recipients": {} + } + }, + "openMailboxMenu": "Ouvrir le menu de la boîte aux lettres", + "@openMailboxMenu": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "messageHasBeenSentSuccessfully": "Le message a été envoyé avec succès.", + "@messageHasBeenSentSuccessfully": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "deleteOfflineEmail": "Supprimer les messages hors-ligne", + "@deleteOfflineEmail": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "sendingQueue": "File d'attente", + "@sendingQueue": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "messageDialogWhenStoreSendingEmailTail": " boîte aux lettres.", + "@messageDialogWhenStoreSendingEmailTail": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "delivering": "Livraison en cours", + "@delivering": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "resend": "Renvoyer", + "@resend": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "messagesHaveBeenResent": "Les messages ont été renvoyés", + "@messagesHaveBeenResent": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "connectionError": "Erreur de connexion", + "@connectionError": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "spamMailboxDisplayName": "Spam", + "@spamMailboxDisplayName": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "draftsMailboxDisplayName": "Brouillons", + "@draftsMailboxDisplayName": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "trashMailboxDisplayName": "Corbeille", + "@trashMailboxDisplayName": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "templatesMailboxDisplayName": "Modèles", + "@templatesMailboxDisplayName": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "archiveMailboxDisplayName": "Archives", + "@archiveMailboxDisplayName": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "outboxMailboxDisplayName": "Boîte d'envoi", + "@outboxMailboxDisplayName": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "forwardedMessage": "Message transféré", + "@forwardedMessage": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "repliedMessage": "Message répondu", + "@repliedMessage": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "repliedAndForwardedMessage": "Message répondu et transféré", + "@repliedAndForwardedMessage": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "emptyTrashMessageDialog": "Vous êtes sur le point de supprimer définitivement tous les éléments de la corbeille . Voulez-vous continuer?", + "@emptyTrashMessageDialog": { + "type": "text", + "placeholders_order": [], + "placeholders": {} } } diff --git a/lib/l10n/intl_messages.arb b/lib/l10n/intl_messages.arb index b5af622736..7dfb0a04b5 100644 --- a/lib/l10n/intl_messages.arb +++ b/lib/l10n/intl_messages.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2023-07-12T18:53:48.611802", + "@@last_modified": "2023-07-28T02:46:38.430701", "initializing_data": "Initializing data...", "@initializing_data": { "type": "text", @@ -2975,5 +2975,137 @@ "placeholders": { "maxSize": {} } + }, + "messageEventActionBannerOrganizerInvited": " has invited you in to a meeting", + "@messageEventActionBannerOrganizerInvited": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "messageEventActionBannerOrganizerUpdated": " has updated a meeting", + "@messageEventActionBannerOrganizerUpdated": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "messageEventActionBannerOrganizerCanceled": " has canceled a meeting", + "@messageEventActionBannerOrganizerCanceled": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "subMessageEventActionBannerUpdated": "\"The time has been updated to better suit all of you\"", + "@subMessageEventActionBannerUpdated": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "subMessageEventActionBannerCanceled": "\"We are canceling the event due to bad weather.\"", + "@subMessageEventActionBannerCanceled": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "anAttendee": "An attendee", + "@anAttendee": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "you": "You", + "@you": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "messageEventActionBannerAttendeeAccepted": " has accepted this invitation", + "@messageEventActionBannerAttendeeAccepted": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "messageEventActionBannerAttendeeTentative": " has replied \"Maybe\" to this invitation", + "@messageEventActionBannerAttendeeTentative": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "messageEventActionBannerAttendeeDeclined": " has declined this invitation", + "@messageEventActionBannerAttendeeDeclined": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "messageEventActionBannerAttendeeCounter": " has proposed changes to the event", + "@messageEventActionBannerAttendeeCounter": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "messageEventActionBannerAttendeeCounterDeclined": "Your counter proposal was declined", + "@messageEventActionBannerAttendeeCounterDeclined": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "invitationMessageCalendarInformation": " has invited you in to a meeting:", + "@invitationMessageCalendarInformation": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "when": "When", + "@when": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "where": "Where", + "@where": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "who": "Who", + "@who": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "organizer": "Organizer", + "@organizer": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "time": "Time", + "@time": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "location": "Location", + "@location": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "attendees": "Attendees", + "@attendees": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "seeAllAttendees": "See all attendees", + "@seeAllAttendees": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "link": "Link", + "@link": { + "type": "text", + "placeholders_order": [], + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_ru.arb b/lib/l10n/intl_ru.arb index 90e17c88d0..2f7a58e205 100644 --- a/lib/l10n/intl_ru.arb +++ b/lib/l10n/intl_ru.arb @@ -2909,5 +2909,231 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "forwardedMessage": "Пересланное сообщение", + "@forwardedMessage": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "emptyTrashMessageDialog": "Вы собираетесь безвозвратно удалить все элементы из корзины. Продолжить?", + "@emptyTrashMessageDialog": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "repliedMessage": "Сообщение с ответом", + "@repliedMessage": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "repliedAndForwardedMessage": "Сообщение с ответом и пересланное сообщение", + "@repliedAndForwardedMessage": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "emptyTrash": "Очистить Корзину", + "@emptyTrash": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "messageHasBeenSavedToTheSendingQueue": "Сообщение сохранено в очереди отправки.", + "@messageHasBeenSavedToTheSendingQueue": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "bannerMessageSendingQueueView": "Сообщения в очереди будут отправлены, когда вы вернетесь онлайн.", + "@bannerMessageSendingQueueView": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "proceed": "Продолжить", + "@proceed": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "youAreInOfflineMode": "Вы в оффлайн-режиме", + "@youAreInOfflineMode": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "messageDialogWhenStoreSendingEmailSecond": " отправлять, отвечать или пересылать ", + "@messageDialogWhenStoreSendingEmailSecond": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "messageDialogWhenStoreSendingEmailTail": " почтовый ящик.", + "@messageDialogWhenStoreSendingEmailTail": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "openMailboxMenu": "Открыть меню почтового ящика", + "@openMailboxMenu": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "deleteOfflineEmail": "Удалить оффлайн-письмо", + "@deleteOfflineEmail": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "error": "Ошибка", + "@error": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "messagesHaveBeenResent": "Сообщения отправлены еще раз", + "@messagesHaveBeenResent": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "cannotSelectThisImage": "Невозможно выбрать это изображение.", + "@cannotSelectThisImage": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "sendingQueue": "Очередь отправки", + "@sendingQueue": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "messageDialogWhenStoreSendingEmailFirst": "К счастью, вы все еще можете", + "@messageDialogWhenStoreSendingEmailFirst": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "messageDialogWhenStoreSendingEmailThird": "письма. Они будут доставлены при подключении к Интернету. Чтобы отредактировать эти электронные письма перед отправкой, перейдите в ", + "@messageDialogWhenStoreSendingEmailThird": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "titleRecipientSendingEmail": "Кому: {recipients}", + "@titleRecipientSendingEmail": { + "type": "text", + "placeholders_order": [ + "recipients" + ], + "placeholders": { + "recipients": {} + } + }, + "messageHasBeenSentSuccessfully": "Сообщение успешно отправлено.", + "@messageHasBeenSentSuccessfully": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "messageDialogDeleteSendingEmail": "Удаление оффлайн-письма приведет к безвозвратному удалению его содержимого. Вы не сможете отменить это действие или восстановить письмо из «Корзины».", + "@messageDialogDeleteSendingEmail": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "resend": "Отправить еще раз", + "@resend": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "trashMailboxDisplayName": "Корзина", + "@trashMailboxDisplayName": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "templatesMailboxDisplayName": "Шаблоны", + "@templatesMailboxDisplayName": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "messageHaveBeenDeletedSuccessfully": "Сообщения успешно удалены", + "@messageHaveBeenDeletedSuccessfully": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "delivering": "Доставляется", + "@delivering": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "connectedToTheInternet": "Подключен к Интернету", + "@connectedToTheInternet": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "inboxMailboxDisplayName": "Входящие", + "@inboxMailboxDisplayName": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "connectionError": "Ошибка подключения", + "@connectionError": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "sentMailboxDisplayName": "Отправленные", + "@sentMailboxDisplayName": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "archiveMailboxDisplayName": "Архив", + "@archiveMailboxDisplayName": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "languageItalian": "Итальянский", + "@languageItalian": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "emptyListEmailForward": "Укажите хотя бы одного получателя", + "@emptyListEmailForward": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "outboxMailboxDisplayName": "Исходящие", + "@outboxMailboxDisplayName": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "spamMailboxDisplayName": "Спам", + "@spamMailboxDisplayName": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "draftsMailboxDisplayName": "Черновики", + "@draftsMailboxDisplayName": { + "type": "text", + "placeholders_order": [], + "placeholders": {} } } diff --git a/lib/main/error/capability_validator.dart b/lib/main/error/capability_validator.dart index cce8db5ff3..abf09b54ea 100644 --- a/lib/main/error/capability_validator.dart +++ b/lib/main/error/capability_validator.dart @@ -26,12 +26,25 @@ void requireCapability(Session session, AccountId accountId, List { +extension ListCapabilityIdentifierExtension on List { bool isSupported(Session session, AccountId accountId) { try { requireCapability(session, accountId, this); return true; + } catch (error) { + logError('ListCapabilityIdentifierExtension::isSupported(): $error'); + return false; + } + } +} + +extension CapabilityIdentifierExtension on CapabilityIdentifier { + + bool isSupported(Session session, AccountId accountId) { + try { + requireCapability(session, accountId, [this]); + return true; } catch (error) { logError('CapabilityIdentifierExtension::isSupported(): $error'); return false; diff --git a/lib/main/localizations/app_localizations.dart b/lib/main/localizations/app_localizations.dart index 98215ff52f..29e81d2090 100644 --- a/lib/main/localizations/app_localizations.dart +++ b/lib/main/localizations/app_localizations.dart @@ -3071,4 +3071,137 @@ class AppLocalizations { name: 'pleaseChooseAnImageSizeCorrectly', args: [maxSize]); } + + String get messageEventActionBannerOrganizerInvited { + return Intl.message( + ' has invited you in to a meeting', + name: 'messageEventActionBannerOrganizerInvited'); + } + + String get messageEventActionBannerOrganizerUpdated { + return Intl.message( + ' has updated a meeting', + name: 'messageEventActionBannerOrganizerUpdated'); + } + + String get messageEventActionBannerOrganizerCanceled { + return Intl.message( + ' has canceled a meeting', + name: 'messageEventActionBannerOrganizerCanceled'); + } + + String get subMessageEventActionBannerUpdated { + return Intl.message( + '"The time has been updated to better suit all of you"', + name: 'subMessageEventActionBannerUpdated'); + } + + String get subMessageEventActionBannerCanceled { + return Intl.message( + '"We are canceling the event due to bad weather."', + name: 'subMessageEventActionBannerCanceled'); + } + + String get anAttendee { + return Intl.message( + 'An attendee', + name: 'anAttendee'); + } + + String get you { + return Intl.message( + 'You', + name: 'you'); + } + + String get messageEventActionBannerAttendeeAccepted { + return Intl.message( + ' has accepted this invitation', + name: 'messageEventActionBannerAttendeeAccepted'); + } + + String get messageEventActionBannerAttendeeTentative { + return Intl.message( + ' has replied "Maybe" to this invitation', + name: 'messageEventActionBannerAttendeeTentative'); + } + + String get messageEventActionBannerAttendeeDeclined { + return Intl.message( + ' has declined this invitation', + name: 'messageEventActionBannerAttendeeDeclined'); + } + + String get messageEventActionBannerAttendeeCounter { + return Intl.message( + ' has proposed changes to the event', + name: 'messageEventActionBannerAttendeeCounter'); + } + + String get messageEventActionBannerAttendeeCounterDeclined { + return Intl.message( + 'Your counter proposal was declined', + name: 'messageEventActionBannerAttendeeCounterDeclined'); + } + + String get invitationMessageCalendarInformation { + return Intl.message( + ' has invited you in to a meeting:', + name: 'invitationMessageCalendarInformation'); + } + + String get when { + return Intl.message( + 'When', + name: 'when'); + } + + String get where { + return Intl.message( + 'Where', + name: 'where'); + } + + String get who { + return Intl.message( + 'Who', + name: 'who'); + } + + String get organizer { + return Intl.message( + 'Organizer', + name: 'organizer'); + } + + String get time { + return Intl.message( + 'Time', + name: 'time', + ); + } + + String get location { + return Intl.message( + 'Location', + name: 'location'); + } + + String get attendees { + return Intl.message( + 'Attendees', + name: 'attendees'); + } + + String get seeAllAttendees { + return Intl.message( + 'See all attendees', + name: 'seeAllAttendees'); + } + + String get link { + return Intl.message( + 'Link', + name: 'link'); + } } \ No newline at end of file diff --git a/lib/main/localizations/language_code_constants.dart b/lib/main/localizations/language_code_constants.dart new file mode 100644 index 0000000000..7b84b16be2 --- /dev/null +++ b/lib/main/localizations/language_code_constants.dart @@ -0,0 +1,9 @@ + +class LanguageCodeConstants { + static const String english = 'en'; + static const String french = 'fr'; + static const String vietnamese = 'vi'; + static const String italian = 'it'; + static const String russian = 'ru'; + static const String arabic = 'ar'; +} diff --git a/lib/main/localizations/localization_service.dart b/lib/main/localizations/localization_service.dart index 18f1e48c88..9d17d32bf4 100644 --- a/lib/main/localizations/localization_service.dart +++ b/lib/main/localizations/localization_service.dart @@ -4,28 +4,29 @@ import 'dart:ui'; import 'package:core/utils/app_logger.dart'; import 'package:get/get.dart'; import 'package:tmail_ui_user/features/manage_account/data/local/language_cache_manager.dart'; +import 'package:tmail_ui_user/main/localizations/language_code_constants.dart'; class LocalizationService extends Translations { - static const defaultLocale = Locale('en', 'US'); - static const fallbackLocale = Locale('en', 'US'); + static const defaultLocale = Locale(LanguageCodeConstants.english, 'US'); + static const fallbackLocale = Locale(LanguageCodeConstants.english, 'US'); static final supportedLanguageCodes = [ - 'fr', - 'en', - 'vi', - 'ru', - 'ar', - 'it' + LanguageCodeConstants.french, + LanguageCodeConstants.english, + LanguageCodeConstants.vietnamese, + LanguageCodeConstants.russian, + LanguageCodeConstants.arabic, + LanguageCodeConstants.italian ]; static const List supportedLocales = [ - Locale('fr', 'FR'), - Locale('en', 'US'), - Locale('vi', 'VN'), - Locale('ru', 'RU'), - Locale('ar', 'TN'), - Locale('it', 'IT') + Locale(LanguageCodeConstants.french, 'FR'), + Locale(LanguageCodeConstants.english, 'US'), + Locale(LanguageCodeConstants.vietnamese, 'VN'), + Locale(LanguageCodeConstants.russian, 'RU'), + Locale(LanguageCodeConstants.arabic, 'TN'), + Locale(LanguageCodeConstants.italian, 'IT') ]; static final locale = _getLocaleFromLanguage(); diff --git a/lib/main/utils/app_utils.dart b/lib/main/utils/app_utils.dart index 4f62d697e2..6314a1f178 100644 --- a/lib/main/utils/app_utils.dart +++ b/lib/main/utils/app_utils.dart @@ -2,10 +2,13 @@ import 'package:core/utils/platform_info.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:get/get.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; +import 'package:tmail_ui_user/main/localizations/language_code_constants.dart'; import 'package:tmail_ui_user/main/utils/app_config.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:intl/intl.dart' as intl; +import 'package:date_format/date_format.dart' as date_format; class AppUtils { @@ -48,4 +51,23 @@ class AppUtils { ); }); } + + static date_format.DateLocale getCurrentDateLocale() { + final currentLanguageCode = Get.locale?.languageCode; + if (currentLanguageCode == LanguageCodeConstants.french) { + return const date_format.FrenchDateLocale(); + } else if (currentLanguageCode == LanguageCodeConstants.english) { + return const date_format.EnglishDateLocale(); + } else if (currentLanguageCode == LanguageCodeConstants.vietnamese) { + return const date_format.VietnameseDateLocale(); + } else if (currentLanguageCode == LanguageCodeConstants.russian) { + return const date_format.RussianDateLocale(); + } else if (currentLanguageCode == LanguageCodeConstants.arabic) { + return const date_format.ArabicDateLocale(); + } else if (currentLanguageCode == LanguageCodeConstants.italian) { + return const date_format.ItalianDateLocale(); + } else { + return const date_format.EnglishDateLocale(); + } + } } \ No newline at end of file diff --git a/model/lib/email/attachment.dart b/model/lib/email/attachment.dart index 2d56989d2a..2e78df7719 100644 --- a/model/lib/email/attachment.dart +++ b/model/lib/email/attachment.dart @@ -50,6 +50,8 @@ class Attachment with EquatableMixin { } } + bool get isCalendarEvent => type?.subtype == 'ics' || type?.subtype == 'calendar'; + @override List get props => [partId, blobId, size, name, type, cid, disposition]; } diff --git a/model/lib/email/presentation_email.dart b/model/lib/email/presentation_email.dart index 2c8962919d..2d49452393 100644 --- a/model/lib/email/presentation_email.dart +++ b/model/lib/email/presentation_email.dart @@ -8,6 +8,7 @@ import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; import 'package:jmap_dart_client/jmap/mail/email/email_body_part.dart'; import 'package:jmap_dart_client/jmap/mail/email/email_body_value.dart'; import 'package:jmap_dart_client/jmap/mail/email/email_header.dart'; +import 'package:jmap_dart_client/jmap/mail/email/individual_header_identifier.dart'; import 'package:jmap_dart_client/jmap/mail/email/keyword_identifier.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; import 'package:model/email/email_content.dart'; @@ -38,6 +39,7 @@ class PresentationEmail with EquatableMixin { final List? emailHeader; final Set? htmlBody; final Map? bodyValues; + final Map? headerCalendarEvent; PresentationEmail({ this.id, @@ -60,6 +62,7 @@ class PresentationEmail with EquatableMixin { this.emailHeader, this.htmlBody, this.bodyValues, + this.headerCalendarEvent, }); String getSenderName() { @@ -99,6 +102,8 @@ class PresentationEmail with EquatableMixin { bool get pushNotificationActivated => !isDraft && !hasRead; + bool get hasCalendarEvent => headerCalendarEvent?[IndividualHeaderIdentifier.headerCalendarEvent]?.isNotEmpty == true; + List get emailContentList { final newHtmlBody = htmlBody ?.where((emailBody) => emailBody.partId != null && emailBody.type != null) @@ -116,22 +121,25 @@ class PresentationEmail with EquatableMixin { @override List get props => [ id, + keywords, + size, + receivedAt, + hasAttachment, + preview, subject, + sentAt, from, to, cc, bcc, - keywords, - size, - receivedAt, - sentAt, replyTo, - preview, - hasAttachment, mailboxIds, selectMode, routeWeb, mailboxContain, - emailHeader + emailHeader, + htmlBody, + bodyValues, + headerCalendarEvent, ]; } \ No newline at end of file diff --git a/model/lib/extensions/email_extension.dart b/model/lib/extensions/email_extension.dart index 254a32e9a4..e6b4f119b8 100644 --- a/model/lib/extensions/email_extension.dart +++ b/model/lib/extensions/email_extension.dart @@ -4,6 +4,7 @@ import 'dart:convert'; import 'package:jmap_dart_client/jmap/core/properties/properties.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:jmap_dart_client/jmap/mail/email/email_body_part.dart'; +import 'package:jmap_dart_client/jmap/mail/email/individual_header_identifier.dart'; import 'package:jmap_dart_client/jmap/mail/email/keyword_identifier.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; import 'package:model/model.dart'; @@ -38,24 +39,25 @@ extension EmailExtension on Email { Email updatedEmail({Map? newKeywords, Map? newMailboxIds}) { return Email( - id: id, - keywords: newKeywords ?? keywords, - size: size, - receivedAt: receivedAt, - hasAttachment: hasAttachment, - preview: preview, - subject: subject, - sentAt: sentAt, - from: from, - to: to, - cc: cc, - bcc: bcc, - replyTo: replyTo, - mailboxIds: newMailboxIds ?? mailboxIds, - htmlBody: htmlBody, - bodyValues: bodyValues, - headerUserAgent: headerUserAgent, - attachments: attachments + id: id, + keywords: newKeywords ?? keywords, + size: size, + receivedAt: receivedAt, + hasAttachment: hasAttachment, + preview: preview, + subject: subject, + sentAt: sentAt, + from: from, + to: to, + cc: cc, + bcc: bcc, + replyTo: replyTo, + mailboxIds: newMailboxIds ?? mailboxIds, + htmlBody: htmlBody, + bodyValues: bodyValues, + headerUserAgent: headerUserAgent, + attachments: attachments, + headerCalendarEvent: headerCalendarEvent ); } @@ -76,7 +78,8 @@ extension EmailExtension on Email { replyTo: replyTo, mailboxIds: mailboxIds, selectMode: selectMode, - emailHeader: headers?.toList() + emailHeader: headers?.toList(), + headerCalendarEvent: headerCalendarEvent ); } @@ -96,6 +99,7 @@ extension EmailExtension on Email { bcc: updatedProperties.contain(EmailProperty.bcc) ? newEmail.bcc : bcc, replyTo: updatedProperties.contain(EmailProperty.replyTo) ? newEmail.replyTo : replyTo, mailboxIds: updatedProperties.contain(EmailProperty.mailboxIds) ? newEmail.mailboxIds : mailboxIds, + headerCalendarEvent: updatedProperties.contain(IndividualHeaderIdentifier.headerCalendarEvent.value) ? newEmail.headerCalendarEvent : headerCalendarEvent, ); } @@ -160,7 +164,8 @@ extension EmailExtension on Email { selectMode: selectMode, emailHeader: headers?.toList(), bodyValues: bodyValues, - htmlBody: htmlBody + htmlBody: htmlBody, + headerCalendarEvent: headerCalendarEvent ); } } \ No newline at end of file diff --git a/model/lib/extensions/presentation_email_extension.dart b/model/lib/extensions/presentation_email_extension.dart index 58379e4c53..24c6b9f973 100644 --- a/model/lib/extensions/presentation_email_extension.dart +++ b/model/lib/extensions/presentation_email_extension.dart @@ -52,7 +52,8 @@ extension PresentationEmailExtension on PresentationEmail { mailboxIds: mailboxIds, selectMode: selectMode == SelectMode.INACTIVE ? SelectMode.ACTIVE : SelectMode.INACTIVE, routeWeb: routeWeb, - mailboxContain: mailboxContain + mailboxContain: mailboxContain, + headerCalendarEvent: headerCalendarEvent ); } @@ -74,7 +75,8 @@ extension PresentationEmailExtension on PresentationEmail { mailboxIds: mailboxIds, selectMode: selectMode, routeWeb: routeWeb, - mailboxContain: mailboxContain + mailboxContain: mailboxContain, + headerCalendarEvent: headerCalendarEvent ); } @@ -95,6 +97,9 @@ extension PresentationEmailExtension on PresentationEmail { replyTo: replyTo, htmlBody: htmlBody, bodyValues: bodyValues, + mailboxIds: mailboxIds, + headers: emailHeader?.toSet(), + headerCalendarEvent: headerCalendarEvent ); } @@ -150,7 +155,8 @@ extension PresentationEmailExtension on PresentationEmail { mailboxIds: mailboxIds, selectMode: selectMode, routeWeb: routeWeb, - mailboxContain: matchedMailbox + mailboxContain: matchedMailbox, + headerCalendarEvent: headerCalendarEvent ); } @@ -185,7 +191,8 @@ extension PresentationEmailExtension on PresentationEmail { mailboxIds: mailboxIds, selectMode: selectMode, routeWeb: routeWeb, - mailboxContain: mailboxContain + mailboxContain: mailboxContain, + headerCalendarEvent: headerCalendarEvent ); } @@ -207,7 +214,8 @@ extension PresentationEmailExtension on PresentationEmail { mailboxIds: mailboxIds, selectMode: selectMode, routeWeb: routeWeb, - mailboxContain: mailboxContain + mailboxContain: mailboxContain, + headerCalendarEvent: headerCalendarEvent ); } @@ -229,7 +237,8 @@ extension PresentationEmailExtension on PresentationEmail { mailboxIds: mailboxIds, selectMode: selectMode, routeWeb: routeWeb, - mailboxContain: mailboxContain + mailboxContain: mailboxContain, + headerCalendarEvent: headerCalendarEvent ); } diff --git a/model/pubspec.lock b/model/pubspec.lock index 133620eabc..8b5e80c348 100644 --- a/model/pubspec.lock +++ b/model/pubspec.lock @@ -484,7 +484,7 @@ packages: description: path: "." ref: master - resolved-ref: "973a57d1ffbc5237d63ea960b46e8921c6ac6a66" + resolved-ref: "63d47945f676adca3bf48b3940b21b77546e309e" url: "https://github.com/linagora/jmap-dart-client.git" source: git version: "0.0.1" diff --git a/pubspec.lock b/pubspec.lock index fb2e693b82..a271466280 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -312,6 +312,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.10.1" + date_format: + dependency: "direct main" + description: + name: date_format + sha256: "8e5154ca363411847220c8cbc43afcf69c08e8debe40ba09d57710c25711760c" + url: "https://pub.dev" + source: hosted + version: "2.0.7" dbus: dependency: transitive description: @@ -971,7 +979,7 @@ packages: description: path: "." ref: master - resolved-ref: "973a57d1ffbc5237d63ea960b46e8921c6ac6a66" + resolved-ref: "63d47945f676adca3bf48b3940b21b77546e309e" url: "https://github.com/linagora/jmap-dart-client.git" source: git version: "0.0.1" diff --git a/pubspec.yaml b/pubspec.yaml index a2990bb897..f19988148f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -15,7 +15,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 0.8.8 +version: 0.8.9 environment: sdk: ">=2.19.2 <3.0.0" @@ -203,6 +203,8 @@ dependencies: flutter_app_badger: 1.5.0 + date_format: 2.0.7 + dev_dependencies: flutter_test: sdk: flutter diff --git a/rule_filter/pubspec.lock b/rule_filter/pubspec.lock index 1273272758..22fedc3afe 100644 --- a/rule_filter/pubspec.lock +++ b/rule_filter/pubspec.lock @@ -296,7 +296,7 @@ packages: description: path: "." ref: master - resolved-ref: "973a57d1ffbc5237d63ea960b46e8921c6ac6a66" + resolved-ref: "63d47945f676adca3bf48b3940b21b77546e309e" url: "https://github.com/linagora/jmap-dart-client.git" source: git version: "0.0.1"