diff --git a/core/lib/presentation/extensions/color_extension.dart b/core/lib/presentation/extensions/color_extension.dart index 32e301689..7ee0efdd9 100644 --- a/core/lib/presentation/extensions/color_extension.dart +++ b/core/lib/presentation/extensions/color_extension.dart @@ -120,6 +120,7 @@ extension AppColor on Color { static const colorBgMenuItemDropDownSelected = Color(0x80DEE2E7); static const colorButtonCancelDialog = Color(0x0D000000); static const colorShadowComposerButton = Color(0x99007AFF); + static const colorBackgroundTagFilter = Color(0x6D7885); static const mapGradientColor = [ [Color(0xFF21D4FD), Color(0xFFB721FF)], diff --git a/docs/adr/0007-fix-show-dropdown-can-not-over-overlay.md b/docs/adr/0007-fix-show-dropdown-can-not-over-overlay.md new file mode 100644 index 000000000..89a7bd025 --- /dev/null +++ b/docs/adr/0007-fix-show-dropdown-can-not-over-overlay.md @@ -0,0 +1,22 @@ +# 7. Fix show Dropdown can't not over overlayUI (#685) + +Date: 2022-06-29 + +## Status + +- Issue: DropDownButton menu shows behind Overlay + +## Context + +- Root cause: Add package flutter_portal to fix this issue + +## Decision + +- When add Dropdown button to overlayUI, it can't lying above overlayUI. + +## Consequences + +- The app Navigator has a list of routes (_history) and each route has its list of OverlayEntry's. +When you push a new route using Navigator.push() you are inserting the route overlays above the overlays of the last route. +So, when you add an independent Overlay with just Overlay.of(context).insert(myOverlay); +it will always be above any route/overlay managed by the app Navigator (if what you receive from the of() method is the Navigator Overlay). diff --git a/lib/features/mailbox_dashboard/presentation/bindings/advanced_filter_bindings.dart b/lib/features/mailbox_dashboard/presentation/bindings/advanced_filter_bindings.dart new file mode 100644 index 000000000..e81a1e4a8 --- /dev/null +++ b/lib/features/mailbox_dashboard/presentation/bindings/advanced_filter_bindings.dart @@ -0,0 +1,63 @@ +import 'package:contact/contact_module.dart'; +import 'package:get/get.dart'; +import 'package:tmail_ui_user/features/base/base_bindings.dart'; +import 'package:tmail_ui_user/features/composer/data/datasource/contact_datasource.dart'; +import 'package:tmail_ui_user/features/composer/data/datasource_impl/contact_datasource_impl.dart'; +import 'package:tmail_ui_user/features/composer/data/repository/auto_complete_repository_impl.dart'; +import 'package:tmail_ui_user/features/composer/data/repository/contact_repository_impl.dart'; +import 'package:tmail_ui_user/features/composer/domain/repository/auto_complete_repository.dart'; +import 'package:tmail_ui_user/features/composer/domain/repository/contact_repository.dart'; +import 'package:tmail_ui_user/features/composer/domain/usecases/get_autocomplete_with_device_contact_interactor.dart'; +import 'package:tmail_ui_user/features/composer/domain/usecases/get_device_contact_suggestions_interactor.dart'; +import 'package:tmail_ui_user/features/composer/domain/usecases/get_autocomplete_interactor.dart'; +import 'package:tmail_ui_user/features/composer/presentation/extensions/session_extension.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart'; +import 'package:jmap_dart_client/http/http_client.dart' as jmap_http_client; + +class AdvancedFilterBindings extends BaseBindings { + + final mailboxDashBoardController = Get.find(); + final Set dataSources = {}; + + @override + void bindingsDataSourceImpl() { + if (mailboxDashBoardController.sessionCurrent?.hasSupportAutoComplete == true) { + Get.lazyPut(() => ContactAPI(Get.find())); + Get.lazyPut(() => TMailContactDataSourceImpl(Get.find())); + dataSources.add(Get.find()); + } + + Get.lazyPut(() => ContactDataSourceImpl()); + } + + @override + void bindingsDataSource() { + Get.lazyPut(() => Get.find()); + } + + @override + void bindingsRepositoryImpl() { + Get.lazyPut(() => AutoCompleteRepositoryImpl(dataSources)); + Get.lazyPut(() => ContactRepositoryImpl(Get.find())); + } + + @override + void bindingsRepository() { + Get.lazyPut(() => Get.find()); + Get.lazyPut(() => Get.find()); + } + + @override + void bindingsInteractor() { + Get.lazyPut(() => GetAutoCompleteInteractor(Get.find())); + Get.lazyPut(() => GetDeviceContactSuggestionsInteractor(Get.find())); + Get.lazyPut(() => GetAutoCompleteWithDeviceContactInteractor( + Get.find(), + Get.find() + )); + } + + @override + void bindingsController() { + } +} \ No newline at end of file diff --git a/lib/features/mailbox_dashboard/presentation/mailbox_dashboard_bindings.dart b/lib/features/mailbox_dashboard/presentation/bindings/mailbox_dashboard_bindings.dart similarity index 90% rename from lib/features/mailbox_dashboard/presentation/mailbox_dashboard_bindings.dart rename to lib/features/mailbox_dashboard/presentation/bindings/mailbox_dashboard_bindings.dart index 6b9b57ea2..fcc233a56 100644 --- a/lib/features/mailbox_dashboard/presentation/mailbox_dashboard_bindings.dart +++ b/lib/features/mailbox_dashboard/presentation/bindings/mailbox_dashboard_bindings.dart @@ -3,6 +3,13 @@ import 'package:get/get.dart'; import 'package:tmail_ui_user/features/base/base_bindings.dart'; import 'package:tmail_ui_user/features/caching/recent_search_cache_client.dart'; import 'package:tmail_ui_user/features/caching/state_cache_client.dart'; +import 'package:tmail_ui_user/features/composer/data/repository/auto_complete_repository_impl.dart'; +import 'package:tmail_ui_user/features/composer/data/repository/contact_repository_impl.dart'; +import 'package:tmail_ui_user/features/composer/domain/repository/auto_complete_repository.dart'; +import 'package:tmail_ui_user/features/composer/domain/repository/contact_repository.dart'; +import 'package:tmail_ui_user/features/composer/domain/usecases/get_autocomplete_interactor.dart'; +import 'package:tmail_ui_user/features/composer/domain/usecases/get_autocomplete_with_device_contact_interactor.dart'; +import 'package:tmail_ui_user/features/composer/domain/usecases/get_device_contact_suggestions_interactor.dart'; import 'package:tmail_ui_user/features/email/data/datasource/email_datasource.dart'; import 'package:tmail_ui_user/features/email/data/datasource/html_datasource.dart'; import 'package:tmail_ui_user/features/email/data/datasource_impl/email_datasource_impl.dart'; @@ -129,6 +136,12 @@ class MailboxDashBoardBindings extends BaseBindings { @override void bindingsInteractor() { + Get.lazyPut(() => GetAutoCompleteInteractor(Get.find())); + Get.lazyPut(() => GetDeviceContactSuggestionsInteractor(Get.find())); + Get.lazyPut(() => GetAutoCompleteWithDeviceContactInteractor( + Get.find(), + Get.find() + )); Get.lazyPut(() => GetUserProfileInteractor(Get.find())); Get.lazyPut(() => RemoveEmailDraftsInteractor(Get.find())); Get.lazyPut(() => MoveToMailboxInteractor(Get.find())); @@ -156,6 +169,8 @@ class MailboxDashBoardBindings extends BaseBindings { @override void bindingsRepository() { + Get.lazyPut(() => Get.find()); + Get.lazyPut(() => Get.find()); Get.lazyPut(() => Get.find()); Get.lazyPut(() => Get.find()); Get.lazyPut(() => Get.find()); diff --git a/lib/features/mailbox_dashboard/presentation/controller/advanced_filter_controller.dart b/lib/features/mailbox_dashboard/presentation/controller/advanced_filter_controller.dart index f3369784e..5998c2d86 100644 --- a/lib/features/mailbox_dashboard/presentation/controller/advanced_filter_controller.dart +++ b/lib/features/mailbox_dashboard/presentation/controller/advanced_filter_controller.dart @@ -2,10 +2,16 @@ import 'package:collection/collection.dart'; import 'package:core/core.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:model/mailbox/presentation_mailbox.dart'; -import 'package:tmail_ui_user/features/base/base_controller.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; +import 'package:model/model.dart'; +import 'package:permission_handler/permission_handler.dart'; +import 'package:tmail_ui_user/features/composer/domain/model/contact_suggestion_source.dart'; +import 'package:tmail_ui_user/features/composer/domain/state/get_autocomplete_state.dart'; +import 'package:tmail_ui_user/features/composer/domain/usecases/get_autocomplete_interactor.dart'; +import 'package:tmail_ui_user/features/composer/domain/usecases/get_autocomplete_with_device_contact_interactor.dart'; import 'package:tmail_ui_user/features/destination_picker/presentation/model/destination_picker_arguments.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_actions.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/bindings/advanced_filter_bindings.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/search_controller.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/search/email_receive_time_type.dart'; @@ -13,33 +19,59 @@ import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/sear import 'package:tmail_ui_user/main/routes/app_routes.dart'; import 'package:tmail_ui_user/main/routes/route_navigation.dart'; -class AdvancedFilterController extends BaseController { +class AdvancedFilterController extends GetxController { + late GetAutoCompleteWithDeviceContactInteractor + _getAutoCompleteWithDeviceContactInteractor; + late GetAutoCompleteInteractor _getAutoCompleteInteractor; final dateFilterSelectedFormAdvancedSearch = EmailReceiveTimeType.allTime.obs; final hasAttachment = false.obs; + final listEmailAddressInit = RxList(); + final listTagFromSelected = RxList(); + final listTagToSelected = RxList(); - TextEditingController fromFilterInputController = TextEditingController(); - TextEditingController toFilterInputController = TextEditingController(); TextEditingController subjectFilterInputController = TextEditingController(); - TextEditingController hasKeyWordFilterInputController = TextEditingController(); - TextEditingController notKeyWordFilterInputController = TextEditingController(); + TextEditingController hasKeyWordFilterInputController = + TextEditingController(); + TextEditingController notKeyWordFilterInputController = + TextEditingController(); TextEditingController dateFilterInputController = TextEditingController(); TextEditingController mailBoxFilterInputController = TextEditingController(); + ContactSuggestionSource _contactSuggestionSource = + ContactSuggestionSource.tMailContact; final SearchController _searchController = Get.find(); - final MailboxDashBoardController _mailboxDashBoardController = Get.find(); + final MailboxDashBoardController _mailboxDashBoardController = + Get.find(); - SearchEmailFilter get _searchEmailFilter => _searchController.searchEmailFilter.value; + AdvancedFilterController() { + AdvancedFilterBindings().dependencies(); + _getAutoCompleteWithDeviceContactInteractor = + Get.find(); + _getAutoCompleteInteractor = Get.find(); + } - selectOpenAdvanceSearch() { + SearchEmailFilter get searchEmailFilter => + _searchController.searchEmailFilter.value; + + @override + void onReady() async { + if (!BuildUtils.isWeb) { + Future.delayed( + const Duration(milliseconds: 500), () => _checkContactPermission()); + } + super.onReady(); + } + + void selectOpenAdvanceSearch() { _searchController.isAdvancedSearchViewOpen.toggle(); } - cleanSearchFilter(BuildContext context) { + void cleanSearchFilter(BuildContext context) { _searchController.cleanSearchFilter(); dateFilterSelectedFormAdvancedSearch.value = EmailReceiveTimeType.allTime; - fromFilterInputController.text = ''; - toFilterInputController.text = ''; + listTagFromSelected.clear(); + listTagToSelected.clear(); subjectFilterInputController.text = ''; hasKeyWordFilterInputController.text = ''; notKeyWordFilterInputController.text = ''; @@ -47,56 +79,102 @@ class AdvancedFilterController extends BaseController { hasAttachment.value = false; _searchController.isAdvancedSearchHasApply.value = false; _searchController.isAdvancedSearchViewOpen.toggle(); - _mailboxDashBoardController.searchEmail(context, StringConvert.writeNullToEmpty(_searchEmailFilter.text?.value)); + _mailboxDashBoardController.searchEmail( + context, StringConvert.writeNullToEmpty(searchEmailFilter.text?.value)); } void _updateFilterEmailFromAdvancedSearchView() { - if (fromFilterInputController.text.isNotEmpty) { - _searchController.updateFilterEmail(from: fromFilterInputController.text.split(',').toSet()); + if (listTagFromSelected.isNotEmpty) { + _searchController.updateFilterEmail( + from: listTagFromSelected.toSet()); } else { _searchController.updateFilterEmail(from: {}); } - if (toFilterInputController.text.isNotEmpty) { - _searchController.updateFilterEmail(to: toFilterInputController.text.split(',').toSet()); + if (listTagToSelected.isNotEmpty) { + _searchController.updateFilterEmail( + to: listTagToSelected.toSet()); } else { _searchController.updateFilterEmail(to: {}); } if (hasKeyWordFilterInputController.text.isNotEmpty) { - _searchController.updateFilterEmail(hasKeyword: hasKeyWordFilterInputController.text.split(',').toSet()); + _searchController.updateFilterEmail( + hasKeyword: hasKeyWordFilterInputController.text.split(',').toSet()); } else { _searchController.updateFilterEmail(hasKeyword: {}); } if (notKeyWordFilterInputController.text.isNotEmpty) { - _searchController.updateFilterEmail(notKeyword: notKeyWordFilterInputController.text.split(',').toSet()); + _searchController.updateFilterEmail( + notKeyword: notKeyWordFilterInputController.text.split(',').toSet()); } else { _searchController.updateFilterEmail(notKeyword: {}); } _searchController.updateFilterEmail( - subject: StringConvert.writeEmptyToNull(subjectFilterInputController.text), + subject: + StringConvert.writeEmptyToNull(subjectFilterInputController.text), emailReceiveTimeType: dateFilterSelectedFormAdvancedSearch.value, hasAttachment: hasAttachment.value, ); } - Future selectedMailBox() async{ - final PresentationMailbox destinationMailbox = await push( - AppRoutes.DESTINATION_PICKER, - arguments: DestinationPickerArguments(_mailboxDashBoardController.accountId.value!, MailboxActions.moveEmail)); - _searchController.updateFilterEmail(mailbox: destinationMailbox); - mailBoxFilterInputController.text = StringConvert.writeNullToEmpty(destinationMailbox.name?.name); - } - - applyAdvancedSearchFilter(BuildContext context){ - _updateFilterEmailFromAdvancedSearchView(); - _mailboxDashBoardController.searchEmail(context, StringConvert.writeNullToEmpty(_searchEmailFilter.text?.value)); - _searchController.isAdvancedSearchViewOpen.toggle(); - _searchController.isAdvancedSearchHasApply.value = _checkAdvancedSearchHasApply(); + Future selectedMailBox() async { + final PresentationMailbox destinationMailbox = await push( + AppRoutes.DESTINATION_PICKER, + arguments: DestinationPickerArguments( + _mailboxDashBoardController.accountId.value!, + MailboxActions.moveEmail)); + _searchController.updateFilterEmail(mailbox: destinationMailbox); + mailBoxFilterInputController.text = + StringConvert.writeNullToEmpty(destinationMailbox.name?.name); } - bool _checkAdvancedSearchHasApply() { - return fromFilterInputController.text.isNotEmpty || - toFilterInputController.text.isNotEmpty || + void applyAdvancedSearchFilter(BuildContext context) { + _updateFilterEmailFromAdvancedSearchView(); + _mailboxDashBoardController.searchEmail( + context, StringConvert.writeNullToEmpty(searchEmailFilter.text?.value)); + _searchController.isAdvancedSearchViewOpen.toggle(); + _searchController.isAdvancedSearchHasApply.value = + _checkAdvancedSearchHasApply(); + } + + void _checkContactPermission() async { + final permissionStatus = await Permission.contacts.status; + if (permissionStatus.isGranted) { + _contactSuggestionSource = ContactSuggestionSource.all; + } else if (!permissionStatus.isPermanentlyDenied) { + final requestedPermission = await Permission.contacts.request(); + _contactSuggestionSource = requestedPermission == PermissionStatus.granted + ? ContactSuggestionSource.all + : _contactSuggestionSource; + } + } + + Future> getAutoCompleteSuggestion( + {required String word}) async { + if (_contactSuggestionSource == ContactSuggestionSource.all) { + return await _getAutoCompleteWithDeviceContactInteractor + .execute(AutoCompletePattern( + word: word, + accountId: _mailboxDashBoardController.accountId.value)) + .then((value) => value.fold( + (failure) => [], + (success) => success is GetAutoCompleteSuccess + ? success.listEmailAddress + : [])); + } + return await _getAutoCompleteInteractor + .execute(AutoCompletePattern( + word: word, accountId: _mailboxDashBoardController.accountId.value)) + .then((value) => value.fold( + (failure) => [], + (success) => success is GetAutoCompleteSuccess + ? success.listEmailAddress + : [])); + } + + bool _checkAdvancedSearchHasApply() { + return listTagFromSelected.isNotEmpty || + listTagToSelected.isNotEmpty || subjectFilterInputController.text.isNotEmpty || hasKeyWordFilterInputController.text.isNotEmpty || notKeyWordFilterInputController.text.isNotEmpty || @@ -105,32 +183,33 @@ class AdvancedFilterController extends BaseController { hasAttachment.isTrue; } - initSearchFilterField(BuildContext context) { - _searchController.updateFilterEmail(mailbox: _mailboxDashBoardController.selectedMailbox.value); - fromFilterInputController.text = StringConvert.writeNullToEmpty(_searchEmailFilter.from.firstOrNull); - toFilterInputController.text = StringConvert.writeNullToEmpty(_searchEmailFilter.to.firstOrNull); - subjectFilterInputController.text = StringConvert.writeNullToEmpty(_searchEmailFilter.subject); - hasKeyWordFilterInputController.text = StringConvert.writeNullToEmpty(_searchEmailFilter.hasKeyword.firstOrNull); - notKeyWordFilterInputController.text = StringConvert.writeNullToEmpty(_searchEmailFilter.notKeyword.firstOrNull); - dateFilterInputController.text = StringConvert.writeNullToEmpty(_searchEmailFilter.emailReceiveTimeType.getTitle(context)); - mailBoxFilterInputController.text = StringConvert.writeNullToEmpty(_searchEmailFilter.mailbox?.name?.name); - dateFilterSelectedFormAdvancedSearch.value = _searchEmailFilter.emailReceiveTimeType; - hasAttachment.value = _searchEmailFilter.hasAttachment; + void initSearchFilterField(BuildContext context) { + _searchController.updateFilterEmail( + mailbox: _mailboxDashBoardController.selectedMailbox.value); + subjectFilterInputController.text = + StringConvert.writeNullToEmpty(searchEmailFilter.subject); + hasKeyWordFilterInputController.text = StringConvert.writeNullToEmpty( + searchEmailFilter.hasKeyword.firstOrNull); + notKeyWordFilterInputController.text = StringConvert.writeNullToEmpty( + searchEmailFilter.notKeyword.firstOrNull); + dateFilterInputController.text = StringConvert.writeNullToEmpty( + searchEmailFilter.emailReceiveTimeType.getTitle(context)); + mailBoxFilterInputController.text = + StringConvert.writeNullToEmpty(searchEmailFilter.mailbox?.name?.name); + dateFilterSelectedFormAdvancedSearch.value = + searchEmailFilter.emailReceiveTimeType; + hasAttachment.value = searchEmailFilter.hasAttachment; } @override - void onDone() { - } + void onDone() {} @override - void onError(error) { - } + void onError(error) {} @override void onClose() { - fromFilterInputController.dispose(); subjectFilterInputController.dispose(); - toFilterInputController.dispose(); hasKeyWordFilterInputController.dispose(); notKeyWordFilterInputController.dispose(); mailBoxFilterInputController.dispose(); 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 bf81c07be..4e65aacbe 100644 --- a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart +++ b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart @@ -241,7 +241,6 @@ class MailboxDashBoardController extends ReloadableController { void setNewFirstSelectedMailbox(PresentationMailbox? newPresentationMailbox) { selectedMailbox.firstRebuild = true; selectedMailbox.value = newPresentationMailbox; - } void setSelectedEmail(PresentationEmail? newPresentationEmail) { diff --git a/lib/features/mailbox_dashboard/presentation/controller/search_controller.dart b/lib/features/mailbox_dashboard/presentation/controller/search_controller.dart index a923a6401..99ada3bec 100644 --- a/lib/features/mailbox_dashboard/presentation/controller/search_controller.dart +++ b/lib/features/mailbox_dashboard/presentation/controller/search_controller.dart @@ -92,7 +92,7 @@ class SearchController extends BaseController { return; case QuickSearchFilter.fromMe: quickSearchFilterSelected - ? searchEmailFilter.value.from.remove(userProfile.email) + ? searchEmailFilter.value.from.removeWhere((e) => e == userProfile.email) : searchEmailFilter.value.from.add(userProfile.email); updateFilterEmail(from: searchEmailFilter.value.from); return; @@ -195,7 +195,7 @@ class SearchController extends BaseController { } return searchEmailFilter.value.emailReceiveTimeType == EmailReceiveTimeType.last7Days; case QuickSearchFilter.fromMe: - return searchEmailFilter.value.from.contains(userProfile.email) && searchEmailFilter.value.from.length == 1; + return searchEmailFilter.value.from.contains( userProfile.email) && searchEmailFilter.value.from.length == 1; } } diff --git a/lib/features/mailbox_dashboard/presentation/mailbox_dashboard_view_web.dart b/lib/features/mailbox_dashboard/presentation/mailbox_dashboard_view_web.dart index ebbcdb0d1..709765b0f 100644 --- a/lib/features/mailbox_dashboard/presentation/mailbox_dashboard_view_web.dart +++ b/lib/features/mailbox_dashboard/presentation/mailbox_dashboard_view_web.dart @@ -98,7 +98,7 @@ class MailboxDashBoardView extends BaseMailboxDashBoardView { tabletLarge: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SizedBox(child: MailboxView(), width: responsiveUtils.defaultSizeDrawer), + SizedBox(child: MailboxView(), width: responsiveUtils.defaultSizeMenu), Expanded(child: _wrapContainerForThreadAndEmail(context)) ], ), @@ -423,8 +423,8 @@ class MailboxDashBoardView extends BaseMailboxDashBoardView { target: Alignment.bottomLeft, widthFactor: 1, backup: Aligned( - follower: Alignment.bottomLeft, - target: Alignment.topLeft, + follower: Alignment.topLeft, + target: Alignment.bottomLeft, widthFactor: 1, ), ), diff --git a/lib/features/mailbox_dashboard/presentation/widgets/advanced_search/advanced_search_filter_form.dart b/lib/features/mailbox_dashboard/presentation/widgets/advanced_search/advanced_search_filter_form.dart index 89f6ac7c3..06bade9e6 100644 --- a/lib/features/mailbox_dashboard/presentation/widgets/advanced_search/advanced_search_filter_form.dart +++ b/lib/features/mailbox_dashboard/presentation/widgets/advanced_search/advanced_search_filter_form.dart @@ -8,6 +8,7 @@ import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/sear import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/search/email_receive_time_type.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/advanced_search/advanced_search_filter_form_bottom_view.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/advanced_search/drop_down_button_filter_widget.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/advanced_search/text_field_auto_complete_email_adress.dart'; class AdvancedSearchInputForm extends GetWidget with PopupContextMenuActionMixin { @@ -24,15 +25,17 @@ class AdvancedSearchInputForm extends GetWidget color: Colors.white, child: Column( children: [ - _buildFilterField( - textEditingController: controller.fromFilterInputController, - context: context, - advancedSearchFilterField: AdvancedSearchFilterField.form, + _buildSuggestionFilterField( + listTagSelected: controller.listTagFromSelected, + context: context, + advancedSearchFilterField: AdvancedSearchFilterField.form, + listTagInitial: controller.searchEmailFilter.from.toList() ), - _buildFilterField( - textEditingController: controller.toFilterInputController, - context: context, - advancedSearchFilterField: AdvancedSearchFilterField.to, + _buildSuggestionFilterField( + listTagSelected: controller.listTagToSelected, + context: context, + advancedSearchFilterField: AdvancedSearchFilterField.to, + listTagInitial: controller.searchEmailFilter.to.toList() ), _buildFilterField( textEditingController: controller.subjectFilterInputController, @@ -50,14 +53,13 @@ class AdvancedSearchInputForm extends GetWidget advancedSearchFilterField: AdvancedSearchFilterField.notKeyword, ), _buildFilterField( - textEditingController: controller.mailBoxFilterInputController, - context: context, - advancedSearchFilterField: AdvancedSearchFilterField.mailBox, - isSelectFormList: true, - onTap: () async { - await controller.selectedMailBox(); - } - ), + textEditingController: controller.mailBoxFilterInputController, + context: context, + advancedSearchFilterField: AdvancedSearchFilterField.mailBox, + isSelectFormList: true, + onTap: () async { + await controller.selectedMailBox(); + }), _buildFilterField( textEditingController: controller.dateFilterInputController, context: context, @@ -70,7 +72,7 @@ class AdvancedSearchInputForm extends GetWidget ); }, ), - const AdvancedSearchFilterFormBottomView() + const AdvancedSearchFilterFormBottomView() ], ), ); @@ -177,6 +179,57 @@ class AdvancedSearchInputForm extends GetWidget } } + Widget _buildSuggestionFilterField({ + required AdvancedSearchFilterField advancedSearchFilterField, + required BuildContext context, + required List listTagSelected, + required List listTagInitial, + }) { + final child = [ + SizedBox( + width: _responsiveUtils.isMobile(context) ? null : 112, + child: Text( + advancedSearchFilterField.getTitle(context), + style: const TextStyle( + fontSize: 14, + color: AppColor.colorContentEmail, + ), + ), + ), + const Padding(padding: EdgeInsets.all(4)), + _responsiveUtils.isMobile(context) + ? TextFieldAutoCompleteEmailAddress( + optionsBuilder: (word) async { + return controller.getAutoCompleteSuggestion(word: word); + }, + advancedSearchFilterField: advancedSearchFilterField, + initialTags: listTagInitial, + listTagSelected: listTagSelected, + ) + : Expanded( + child: TextFieldAutoCompleteEmailAddress( + optionsBuilder: (word) async { + return controller.getAutoCompleteSuggestion(word: word); + }, + advancedSearchFilterField: advancedSearchFilterField, + initialTags: listTagInitial, + listTagSelected: listTagSelected, + ), + ) + ]; + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: _responsiveUtils.isMobile(context) + ? Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: child, + ) + : SizedBox(height: 44, child: Row(children: child)), + ); + } + Widget _buildTextField({ required BuildContext context, required AdvancedSearchFilterField advancedSearchFilterField, @@ -195,6 +248,15 @@ class AdvancedSearchInputForm extends GetWidget right: 8, left: 12, ), + enabledBorder: const OutlineInputBorder( + borderRadius: BorderRadius.all( + Radius.circular(10), + ), + borderSide: BorderSide( + width: 0.5, + color: AppColor.colorInputBorderCreateMailbox, + ), + ), border: const OutlineInputBorder( borderRadius: BorderRadius.all( Radius.circular(10), diff --git a/lib/features/mailbox_dashboard/presentation/widgets/advanced_search/advanced_search_filter_form_bottom_view.dart b/lib/features/mailbox_dashboard/presentation/widgets/advanced_search/advanced_search_filter_form_bottom_view.dart index bd2587047..e986f02d9 100644 --- a/lib/features/mailbox_dashboard/presentation/widgets/advanced_search/advanced_search_filter_form_bottom_view.dart +++ b/lib/features/mailbox_dashboard/presentation/widgets/advanced_search/advanced_search_filter_form_bottom_view.dart @@ -33,8 +33,8 @@ class AdvancedSearchFilterFormBottomView controller.cleanSearchFilter(context); popBack(); }, - colorButton: AppColor.primaryColor.withOpacity(0.14), - colorText: AppColor.primaryColor, + colorButton: Colors.transparent, + colorText: AppColor.colorMessageConfirmDialog, text: AppLocalizations.of(context).clearFilter, ), const SizedBox(width: 12), @@ -80,8 +80,8 @@ class AdvancedSearchFilterFormBottomView onAction.call(); }, child: Container( - constraints: const BoxConstraints(maxWidth: 154), - padding: const EdgeInsets.symmetric(vertical: 16), + height: 44, + constraints: const BoxConstraints(maxWidth: 144), alignment: Alignment.center, decoration: BoxDecoration( borderRadius: BorderRadius.circular(10), color: colorButton), diff --git a/lib/features/mailbox_dashboard/presentation/widgets/advanced_search/advanced_search_filter_overlay.dart b/lib/features/mailbox_dashboard/presentation/widgets/advanced_search/advanced_search_filter_overlay.dart index 3fefce21f..ae73993f3 100644 --- a/lib/features/mailbox_dashboard/presentation/widgets/advanced_search/advanced_search_filter_overlay.dart +++ b/lib/features/mailbox_dashboard/presentation/widgets/advanced_search/advanced_search_filter_overlay.dart @@ -11,7 +11,7 @@ class AdvancedSearchFilterOverlay extends StatelessWidget { padding: const EdgeInsets.only(top: 8), child: Container( width: 660, - height: 558, + height: MediaQuery.of(context).size.height, padding: const EdgeInsets.all(32), decoration: BoxDecoration( color: Colors.white, @@ -22,7 +22,12 @@ class AdvancedSearchFilterOverlay extends StatelessWidget { spreadRadius: 1, blurRadius: 1, offset: Offset(0, 0.5)), ] ), - child: AdvancedSearchInputForm(), + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 28, vertical: 12), + child: AdvancedSearchInputForm(), + ), + ), ), ); } diff --git a/lib/features/mailbox_dashboard/presentation/widgets/advanced_search/text_field_auto_complete_email_adress.dart b/lib/features/mailbox_dashboard/presentation/widgets/advanced_search/text_field_auto_complete_email_adress.dart new file mode 100644 index 000000000..1f6b5c7a2 --- /dev/null +++ b/lib/features/mailbox_dashboard/presentation/widgets/advanced_search/text_field_auto_complete_email_adress.dart @@ -0,0 +1,286 @@ +import 'package:collection/collection.dart'; +import 'package:core/core.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:get/get.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; +import 'package:model/extensions/email_address_extension.dart'; +import 'package:textfield_tags/textfield_tags.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/search/advanced_search_filter.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; + +class TextFieldAutoCompleteEmailAddress extends StatefulWidget { + const TextFieldAutoCompleteEmailAddress({ + Key? key, + required this.advancedSearchFilterField, + required this.listTagSelected, + required this.initialTags, + required this.optionsBuilder, + }) : super(key: key); + final AdvancedSearchFilterField advancedSearchFilterField; + final List listTagSelected; + final List initialTags; + final Future> Function(String) optionsBuilder; + + @override + State createState() => + _TextFieldAutoCompleteEmailAddressState(); +} + +class _TextFieldAutoCompleteEmailAddressState + extends State { + final double _distanceToField = 380; + final ImagePaths _imagePaths = Get.find(); + late TextfieldTagsController _controller; + + @override + void initState() { + _controller = TextfieldTagsController(); + _controller.addListener(() { + if(_controller.hasTags){ + widget.listTagSelected.addAll(_controller.getTags!); + } + }); + super.initState(); + } + + + @override + Widget build(BuildContext context) { + return Autocomplete( + optionsViewBuilder: (context, onSelected, options) { + return Container( + margin: const EdgeInsets.symmetric(horizontal: 10.0, vertical: 4.0), + child: Align( + alignment: Alignment.topLeft, + child: Material( + elevation: 4.0, + child: ConstrainedBox( + constraints: + const BoxConstraints(maxHeight: 200, maxWidth: 200), + child: ListView.builder( + shrinkWrap: true, + itemCount: options.length, + itemBuilder: (BuildContext context, int index) { + final dynamic option = options.elementAt(index); + return GestureDetector( + onTap: () { + onSelected(option); + }, + child: _buildSuggestionItem(context, option), + ); + }, + ), + ), + ), + ), + ); + }, + optionsBuilder: (TextEditingValue textEditingValue) { + return widget.optionsBuilder.call(textEditingValue.text); + }, + onSelected: (EmailAddress selectedTag) { + _controller.addTag = selectedTag.asString(); + }, + fieldViewBuilder: (context, ttec, tfn, onFieldSubmitted) { + return TextFieldTags( + initialTags: widget.initialTags, + textEditingController: ttec, + focusNode: tfn, + textfieldTagsController: _controller, + textSeparators: const [' ', ','], + letterCase: LetterCase.normal, + validator: (String tag) { + if (_controller.getTags!.contains(tag)) { + return AppLocalizations.of(context).messageDuplicateTagFilterMail; + } + return null; + }, + inputfieldBuilder: (context, tec, fn, error, onChanged, onSubmitted) { + return ((context, sc, tags, onTagDelete) { + return TextField( + controller: tec, + focusNode: fn, + decoration: InputDecoration( + filled: true, + fillColor: AppColor.loginTextFieldBackgroundColor, + contentPadding: const EdgeInsets.only( + right: 8, + left: 12, + ), + enabledBorder: const OutlineInputBorder( + borderRadius: BorderRadius.all( + Radius.circular(10), + ), + borderSide: BorderSide( + width: 0.5, + color: AppColor.colorInputBorderCreateMailbox, + ), + ), + border: const OutlineInputBorder( + borderRadius: BorderRadius.all( + Radius.circular(10), + ), + ), + hintText: + widget.advancedSearchFilterField.getHintText(context), + hintStyle: const TextStyle( + fontSize: 14, + color: AppColor.colorHintSearchBar, + ), + prefixIconConstraints: + BoxConstraints(maxWidth: _distanceToField * 0.74), + prefixIcon: tags.isNotEmpty + ? SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 10), + controller: sc, + scrollDirection: Axis.horizontal, + child: Row( + children: tags.map((String tag) { + return _buildTagItem(context, tag, onTagDelete); + }).toList()), + ) + : null, + ), + onChanged: onChanged, + onSubmitted: (tag){ + onSubmitted?.call(tag); + }, + ); + }); + }, + ); + }, + ); + } + + Widget _buildSuggestionItem( + BuildContext context, + EmailAddress emailAddress, + ) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 40, + height: 40, + alignment: Alignment.center, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: AppColor.avatarColor, + border: Border.all( + color: AppColor.colorShadowBgContentEmail, width: 1.0)), + child: Text( + emailAddress.asString().isNotEmpty + ? emailAddress.asString()[0].toUpperCase() + : '', + style: const TextStyle( + color: Colors.black, + fontSize: 16, + fontWeight: FontWeight.w600))), + Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text(emailAddress.asString(), + maxLines: 1, + overflow: kIsWeb ? null : TextOverflow.ellipsis, + style: const TextStyle( + color: Colors.black, + fontSize: 16, + fontWeight: FontWeight.normal)), + if (emailAddress.displayName.isNotEmpty && + emailAddress.emailAddress.isNotEmpty) + Text(emailAddress.emailAddress, + maxLines: 1, + overflow: kIsWeb ? null : TextOverflow.ellipsis, + style: const TextStyle( + color: AppColor.colorHintSearchBar, + fontSize: 13, + fontWeight: FontWeight.normal)) + ], + ) + ], + ); + } + + Widget _buildTagItem( + BuildContext context, + String tag, + Function(String tag) onTagDelete, + ) { + return Container( + decoration: BoxDecoration( + borderRadius: const BorderRadius.all( + Radius.circular(10), + ), + color: AppColor.colorBackgroundTagFilter.withOpacity(0.08), + ), + margin: const EdgeInsets.only(right: 8), + padding: const EdgeInsets.fromLTRB(8, 4, 4, 4), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 24, + height: 24, + alignment: Alignment.center, + decoration: BoxDecoration( + shape: BoxShape.circle, + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: AppColor.mapGradientColor[_generateIndex(tag)], + ), + border: Border.all( + color: AppColor.colorShadowBgContentEmail, width: 0.21)), + child: Text(tag[0].toUpperCase(), + style: const TextStyle( + color: Colors.white, + fontSize: 8.57, + fontWeight: FontWeight.w600))), + const SizedBox(width: 4), + Text(tag, + maxLines: 1, + overflow: kIsWeb ? null : TextOverflow.ellipsis, + style: const TextStyle( + color: Colors.black, + fontSize: 17, + fontWeight: FontWeight.normal)), + const SizedBox(width: 4), + GestureDetector( + onTap: (){ + onTagDelete.call(tag); + widget.listTagSelected.remove(tag); + }, + child: SvgPicture.asset( + _imagePaths.icClose, + width: 28, + height: 28, + fit: BoxFit.fill, + ), + ) + ], + ), + ); + } + + int _generateIndex(String tag) { + if (tag.isNotEmpty) { + final codeUnits = tag.codeUnits; + if (codeUnits.isNotEmpty) { + final sumCodeUnits = codeUnits.sum; + final index = sumCodeUnits % AppColor.mapGradientColor.length; + return index; + } + } + return 0; + } + + @override + void dispose() { + super.dispose(); + _controller.dispose(); + } +} diff --git a/lib/features/thread/presentation/thread_controller.dart b/lib/features/thread/presentation/thread_controller.dart index b39999952..2fb2c6caa 100644 --- a/lib/features/thread/presentation/thread_controller.dart +++ b/lib/features/thread/presentation/thread_controller.dart @@ -289,8 +289,8 @@ class ThreadController extends BaseController { if (viewState is Either) { viewState.map((success) { if (success is SearchEmailNewQuery){ - _searchEmail(); mailboxDashBoardController.clearState(); + _searchEmail(); } else if (success is MarkAsEmailReadSuccess || success is MoveToMailboxSuccess || success is MarkAsStarEmailSuccess diff --git a/lib/features/thread/presentation/thread_view.dart b/lib/features/thread/presentation/thread_view.dart index f89b78320..ab1bda11f 100644 --- a/lib/features/thread/presentation/thread_view.dart +++ b/lib/features/thread/presentation/thread_view.dart @@ -190,8 +190,8 @@ class ThreadView extends GetWidget with AppLoaderMixin, target: Alignment.bottomLeft, widthFactor: 1, backup: Aligned( - follower: Alignment.bottomLeft, - target: Alignment.topLeft, + follower: Alignment.topLeft, + target: Alignment.bottomLeft, widthFactor: 1, ), ), diff --git a/lib/l10n/intl_messages.arb b/lib/l10n/intl_messages.arb index e46196a56..71af1b938 100644 --- a/lib/l10n/intl_messages.arb +++ b/lib/l10n/intl_messages.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2022-06-24T14:16:53.170960", + "@@last_modified": "2022-06-29T12:21:47.830137", "initializing_data": "Initializing data...", "@initializing_data": { "type": "text", diff --git a/lib/main/localizations/app_localizations.dart b/lib/main/localizations/app_localizations.dart index 774c48910..62e527bad 100644 --- a/lib/main/localizations/app_localizations.dart +++ b/lib/main/localizations/app_localizations.dart @@ -1545,4 +1545,11 @@ class AppLocalizations { name: 'selectMailbox', ); } + + String get messageDuplicateTagFilterMail { + return Intl.message( + 'you already entered that', + name: 'selectMailbox', + ); + } } \ No newline at end of file diff --git a/lib/main/pages/app_pages.dart b/lib/main/pages/app_pages.dart index 867298c45..42849b727 100644 --- a/lib/main/pages/app_pages.dart +++ b/lib/main/pages/app_pages.dart @@ -14,7 +14,7 @@ import 'package:tmail_ui_user/features/login/presentation/login_view.dart' if (dart.library.html) 'package:tmail_ui_user/features/login/presentation/login_view_web.dart' deferred as login; import 'package:tmail_ui_user/features/mailbox_creator/presentation/mailbox_creator_bindings.dart'; import 'package:tmail_ui_user/features/mailbox_creator/presentation/mailbox_creator_view.dart' deferred as mailbox_creator; -import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/mailbox_dashboard_bindings.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/bindings/mailbox_dashboard_bindings.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/mailbox_dashboard_view.dart' if (dart.library.html) 'package:tmail_ui_user/features/mailbox_dashboard/presentation/mailbox_dashboard_view_web.dart' deferred as mailbox_dashboard; import 'package:tmail_ui_user/features/manage_account/presentation/manage_account_dashboard_bindings.dart'; diff --git a/pubspec.yaml b/pubspec.yaml index 11112af63..88ec11824 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -69,6 +69,8 @@ dependencies: built_collection: 5.1.1 + textfield_tags: ^2.0.1 + # jmap_dart_client jmap_dart_client: git: diff --git a/test/features/thread/domain/usecases/get_emails_in_mailbox_interactor_test.mocks.dart b/test/features/thread/domain/usecases/get_emails_in_mailbox_interactor_test.mocks.dart index dc82f464a..12f294734 100644 --- a/test/features/thread/domain/usecases/get_emails_in_mailbox_interactor_test.mocks.dart +++ b/test/features/thread/domain/usecases/get_emails_in_mailbox_interactor_test.mocks.dart @@ -1,4 +1,4 @@ -// Mocks generated by Mockito 5.0.17 from annotations +// Mocks generated by Mockito 5.2.0 from annotations // in tmail_ui_user/test/features/thread/domain/usecases/get_emails_in_mailbox_interactor_test.dart. // Do not manually edit this file. @@ -24,6 +24,7 @@ import 'package:tmail_ui_user/features/thread/domain/model/get_email_request.dar import 'package:tmail_ui_user/features/thread/domain/repository/thread_repository.dart' as _i2; +// ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values // ignore_for_file: avoid_setters_without_getters // ignore_for_file: comment_references diff --git a/test/features/thread/domain/usecases/refresh_changes_emails_in_mailbox_interactor_test.mocks.dart b/test/features/thread/domain/usecases/refresh_changes_emails_in_mailbox_interactor_test.mocks.dart index b2a94e9d3..824dfe96a 100644 --- a/test/features/thread/domain/usecases/refresh_changes_emails_in_mailbox_interactor_test.mocks.dart +++ b/test/features/thread/domain/usecases/refresh_changes_emails_in_mailbox_interactor_test.mocks.dart @@ -1,4 +1,4 @@ -// Mocks generated by Mockito 5.0.17 from annotations +// Mocks generated by Mockito 5.2.0 from annotations // in tmail_ui_user/test/features/thread/domain/usecases/refresh_changes_emails_in_mailbox_interactor_test.dart. // Do not manually edit this file. @@ -24,6 +24,7 @@ import 'package:tmail_ui_user/features/thread/domain/model/get_email_request.dar import 'package:tmail_ui_user/features/thread/domain/repository/thread_repository.dart' as _i2; +// ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values // ignore_for_file: avoid_setters_without_getters // ignore_for_file: comment_references