diff --git a/core/lib/presentation/extensions/color_extension.dart b/core/lib/presentation/extensions/color_extension.dart index afc54f1105..1ca116c7f0 100644 --- a/core/lib/presentation/extensions/color_extension.dart +++ b/core/lib/presentation/extensions/color_extension.dart @@ -233,6 +233,7 @@ extension AppColor on Color { static const colorFilterMessageTitle = Color(0xFF686E76); static const colorStarredSearchFilterIcon = Color(0xFFFFCC00); static const colorMobileSearchFilterButton = Color(0xFFEBEDF0); + static const colorContactViewClearFilterButton = Color(0x001C3D0D); static const mapGradientColor = [ [Color(0xFF21D4FD), Color(0xFFB721FF)], diff --git a/lib/features/contact/presentation/contact_controller.dart b/lib/features/contact/presentation/contact_controller.dart index af32226f39..283285585a 100644 --- a/lib/features/contact/presentation/contact_controller.dart +++ b/lib/features/contact/presentation/contact_controller.dart @@ -1,15 +1,19 @@ -import 'package:core/presentation/utils/keyboard_utils.dart'; +import 'dart:async'; + +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; import 'package:core/presentation/utils/theme_utils.dart'; import 'package:core/utils/app_logger.dart'; import 'package:core/utils/platform_info.dart'; +import 'package:dartz/dartz.dart'; import 'package:debounce_throttle/debounce_throttle.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; -import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; import 'package:model/autocomplete/auto_complete_pattern.dart'; +import 'package:model/extensions/email_address_extension.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:tmail_ui_user/features/base/base_controller.dart'; import 'package:tmail_ui_user/features/composer/domain/model/contact_suggestion_source.dart'; @@ -21,6 +25,8 @@ import 'package:tmail_ui_user/features/composer/domain/usecases/get_device_conta import 'package:tmail_ui_user/features/contact/presentation/model/contact_arguments.dart'; import 'package:tmail_ui_user/features/contact/presentation/widgets/contact_suggestion_box_item.dart'; import 'package:tmail_ui_user/features/thread/domain/model/search_query.dart'; +import 'package:tmail_ui_user/features/thread/domain/state/search_email_state.dart'; +import 'package:tmail_ui_user/features/thread/presentation/model/search_status.dart'; import 'package:tmail_ui_user/main/routes/route_navigation.dart'; class ContactController extends BaseController { @@ -30,20 +36,24 @@ class ContactController extends BaseController { ContactSuggestionSource _contactSuggestionSource = ContactSuggestionSource.tMailContact; final searchQuery = SearchQuery.initial().obs; - final session = Rxn(); - final listContactSearched = RxList(); + final searchedContactList = RxList(); + final selectedContactList = RxList(); final contactArguments = Rxn(); + final searchStatus = SearchStatus.INACTIVE.obs; + final searchViewState = Rx>(Right(UIState.idle)); GetAllAutoCompleteInteractor? _getAllAutoCompleteInteractor; GetAutoCompleteInteractor? _getAutoCompleteInteractor; GetDeviceContactSuggestionsInteractor? _getDeviceContactSuggestionsInteractor; - final Debouncer _deBouncerTime = Debouncer(const Duration(milliseconds: 300), initialValue: ''); + final Debouncer _deBouncerTime = Debouncer( + const Duration(milliseconds: 300), + initialValue: '' + ); AccountId? _accountId; - - EmailAddress? contactSelected; SelectedContactCallbackAction? onSelectedContactCallback; VoidCallback? onDismissContactView; + StreamSubscription? _deBouncerTimeStreamSubscription; @override void onInit() { @@ -51,25 +61,23 @@ class ContactController extends BaseController { ThemeUtils.setStatusBarTransparentColor(); log('ContactController::onInit():arguments: ${Get.arguments}'); contactArguments.value = Get.arguments; - _deBouncerTime.values.listen((value) { - searchQuery.value = SearchQuery(value); - _searchContactByNameOrEmail(searchQuery.value.value); - }); + _deBouncerTimeStreamSubscription = _deBouncerTime.values.listen(_handleDeBounceTimeSearchContact); + textInputSearchFocus.addListener(_onSearchInputTextFocusListener); } @override void onReady() { super.onReady(); - textInputSearchFocus.requestFocus(); if (contactArguments.value != null) { _accountId = contactArguments.value!.accountId; - session.value = contactArguments.value!.session; - final listContactSelected = contactArguments.value!.listContactSelected; - log('ContactController::onReady(): listContactSelected: $listContactSelected'); - if (listContactSelected.isNotEmpty) { - contactSelected = EmailAddress(listContactSelected.first, listContactSelected.first); + selectedContactList.value = contactArguments.value!.selectedContactList + .map((mailAddress) => EmailAddress(null, mailAddress)) + .toList(); + injectAutoCompleteBindings(contactArguments.value!.session, _accountId); + + if (selectedContactList.isEmpty) { + textInputSearchFocus.requestFocus(); } - injectAutoCompleteBindings(session.value, _accountId); } if (PlatformInfo.isMobile) { Future.delayed( @@ -81,12 +89,21 @@ class ContactController extends BaseController { @override void onClose() { log('ContactController::onClose():'); + searchViewState.value = Right(UIState.idle); + textInputSearchFocus.removeListener(_onSearchInputTextFocusListener); textInputSearchFocus.dispose(); textInputSearchController.dispose(); + _deBouncerTimeStreamSubscription?.cancel(); _deBouncerTime.cancel(); super.onClose(); } + void _onSearchInputTextFocusListener() { + if (textInputSearchFocus.hasFocus && textInputSearchController.text.trim().isEmpty) { + searchStatus.value = SearchStatus.INACTIVE; + } + } + void onTextSearchChange(String text) { _deBouncerTime.value = text; } @@ -95,9 +112,23 @@ class ContactController extends BaseController { _deBouncerTime.value = text; } + Future _handleDeBounceTimeSearchContact(String value) async { + if (value.trim().isEmpty) { + searchStatus.value = SearchStatus.INACTIVE; + return; + } + + searchStatus.value = SearchStatus.ACTIVE; + searchViewState.value = Right(SearchingState()); + searchQuery.value = SearchQuery(value); + await _searchContactByNameOrEmail(searchQuery.value.value); + searchViewState.value = Right(UIState.idle); + } + void clearAllTextInputSearchForm() { textInputSearchController.clear(); searchQuery.value = SearchQuery.initial(); + searchedContactList.clear(); textInputSearchFocus.requestFocus(); } @@ -113,10 +144,10 @@ class ContactController extends BaseController { } } - void _searchContactByNameOrEmail(String query) async { + Future _searchContactByNameOrEmail(String query) async { log('ContactController::_searchContactByNameOrEmail(): query: $query'); final listContact = await _getAutoCompleteSuggestion(query); - listContactSearched.value = listContact; + searchedContactList.value = listContact; } Future> _getAutoCompleteSuggestion(String query) async { @@ -162,16 +193,46 @@ class ContactController extends BaseController { } } - void selectContact(BuildContext context, EmailAddress emailAddress) { - KeyboardUtils.hideKeyboard(context); - popBack(result: emailAddress); + void handleOnSelectContactAction(EmailAddress emailAddress) { + log('ContactController::selectContact:emailAddress = $emailAddress'); + final isEmailAddressExist = selectedContactList + .any((contact) => contact.emailAddress == emailAddress.emailAddress); + log('ContactController::selectContact:isEmailAddressExist = $isEmailAddressExist'); + if (!isEmailAddressExist) { + selectedContactList.add(emailAddress); + } + } + + void handleOnDeleteContactAction(EmailAddress emailAddress) { + log('ContactController::handleOnDeleteContactAction:emailAddress = $emailAddress'); + selectedContactList.removeWhere((contact) => contact.emailAddress == emailAddress.emailAddress); } void closeContactView() { + textInputSearchFocus.unfocus(); + FocusManager.instance.primaryFocus?.unfocus(); + popBack(); + } + + void handleOnClearFilterAction() { + selectedContactList.clear(); + textInputSearchFocus.unfocus(); + FocusManager.instance.primaryFocus?.unfocus(); + popBack(result: selectedContactList); + } + + void handleOnDoneAction() { + textInputSearchFocus.unfocus(); + FocusManager.instance.primaryFocus?.unfocus(); + popBack(result: selectedContactList); + } + + void handleOnSearchBackAction() { textInputSearchController.clear(); searchQuery.value = SearchQuery.initial(); + searchedContactList.clear(); textInputSearchFocus.unfocus(); FocusManager.instance.primaryFocus?.unfocus(); - popBack(); + searchStatus.value = SearchStatus.INACTIVE; } } \ No newline at end of file diff --git a/lib/features/contact/presentation/contact_view.dart b/lib/features/contact/presentation/contact_view.dart index 5495488db7..ae1da616f2 100644 --- a/lib/features/contact/presentation/contact_view.dart +++ b/lib/features/contact/presentation/contact_view.dart @@ -1,16 +1,14 @@ - import 'package:core/presentation/extensions/color_extension.dart'; -import 'package:core/utils/platform_info.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; import 'package:pointer_interceptor/pointer_interceptor.dart'; -import 'package:tmail_ui_user/features/composer/presentation/model/suggestion_email_address.dart'; import 'package:tmail_ui_user/features/contact/presentation/contact_controller.dart'; import 'package:tmail_ui_user/features/contact/presentation/styles/contact_view_style.dart'; -import 'package:tmail_ui_user/features/contact/presentation/utils/contact_utils.dart'; import 'package:tmail_ui_user/features/contact/presentation/widgets/app_bar_contact_widget.dart'; -import 'package:tmail_ui_user/features/contact/presentation/widgets/contact_suggestion_box_item.dart'; +import 'package:tmail_ui_user/features/contact/presentation/widgets/contact_item_widget.dart'; +import 'package:tmail_ui_user/features/contact/presentation/widgets/contact_list_action_widget.dart'; +import 'package:tmail_ui_user/features/contact/presentation/widgets/contact_loading_bar_widget.dart'; +import 'package:tmail_ui_user/features/thread/presentation/model/search_status.dart'; import 'package:tmail_ui_user/features/thread/presentation/widgets/search_app_bar_widget.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; @@ -68,10 +66,11 @@ class ContactView extends GetWidget { hasBackButton: false, hasSearchButton: true, searchIconSize: 20, + autoFocus: false, searchIconColor: AppColor.colorHintSearchBar, margin: ContactViewStyle.getSearchInputFormMargin( context, - controller.responsiveUtils + controller.responsiveUtils, ), decoration: const BoxDecoration( borderRadius: BorderRadius.all(Radius.circular(12)), @@ -84,94 +83,22 @@ class ContactView extends GetWidget { onTextChangeSearchAction: controller.onTextSearchChange, onSearchTextAction: controller.onSearchTextAction, )), - if (PlatformInfo.isWeb) - Obx(() { - final username = controller.session.value?.username.value ?? ''; - if (username.isNotEmpty) { - final userEmailAddress = EmailAddress( - AppLocalizations.of(context).me, - username); - final fromMeSuggestionEmailAddress = SuggestionEmailAddress(userEmailAddress, state: SuggestionEmailState.valid); - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 4), - child: Column( - children: [ - ContactSuggestionBoxItem( - fromMeSuggestionEmailAddress, - padding: ContactUtils.getPaddingSearchResultList(context, controller.responsiveUtils), - selectedContactCallbackAction: (contact) { - controller.selectContact(context, contact); - }, - ), - Padding( - padding: ContactUtils.getPaddingDividerSearchResultList(context, controller.responsiveUtils), - child: const Divider(height: 1, color: AppColor.colorDivider), - ), - ], - ), - ); - } else { - return const SizedBox.shrink(); - } - }), - Expanded(child: Obx(() { - if (controller.listContactSearched.isNotEmpty) { - if (PlatformInfo.isMobile) { - return Container( - color: Colors.white, - child: ListView.separated( - itemCount: controller.listContactSearched.length, - separatorBuilder: (context, index) { - return Padding( - padding: ContactUtils.getPaddingDividerSearchResultList(context, controller.responsiveUtils), - child: const Divider(height: 1, color: AppColor.colorDivider), - ); - }, - itemBuilder: (context, index) { - final emailAddress = controller.listContactSearched[index]; - final suggestionEmailAddress = _toSuggestionEmailAddress( - emailAddress, - controller.contactSelected != null ? [controller.contactSelected!] : [] - ); - return ContactSuggestionBoxItem( - suggestionEmailAddress, - padding: ContactUtils.getPaddingSearchResultList(context, controller.responsiveUtils), - selectedContactCallbackAction: (contact) => controller.selectContact(context, contact), - ); - } - ) - ); + Obx(() => ContactLoadingBarWidget( + viewState: controller.searchViewState.value, + )), + Expanded( + child: Obx(() { + if (controller.searchStatus.value == SearchStatus.ACTIVE) { + return _buildSearchedContactListView(); } else { - return Container( - color: Colors.white, - padding: const EdgeInsets.symmetric(horizontal: 4), - child: ListView.separated( - itemCount: controller.listContactSearched.length, - separatorBuilder: (context, index) { - return Padding( - padding: ContactUtils.getPaddingDividerSearchResultList(context, controller.responsiveUtils), - child: const Divider(height: 1, color: AppColor.colorDivider), - ); - }, - itemBuilder: (context, index) { - final emailAddress = controller.listContactSearched[index]; - final suggestionEmailAddress = _toSuggestionEmailAddress( - emailAddress, - controller.contactSelected != null ? [controller.contactSelected!] : [] - ); - return ContactSuggestionBoxItem( - suggestionEmailAddress, - padding: ContactUtils.getPaddingSearchResultList(context, controller.responsiveUtils), - selectedContactCallbackAction: (contact) => controller.selectContact(context, contact), - ); - } - ) - ); + return _buildSelectedContactListView(); } - } else { - return const SizedBox.shrink(); - } - })), + }) + ), + ContactListActionWidget( + onClearFilterAction: controller.handleOnClearFilterAction, + onDoneAction: controller.handleOnDoneAction, + ), ] ), ) @@ -183,11 +110,65 @@ class ContactView extends GetWidget { ); } - SuggestionEmailAddress _toSuggestionEmailAddress(EmailAddress item, List addedEmailAddresses) { - if (addedEmailAddresses.contains(item)) { - return SuggestionEmailAddress(item, state: SuggestionEmailState.duplicated); - } else { - return SuggestionEmailAddress(item); - } + Widget _buildSearchedContactListView() { + return Obx(() { + if (controller.searchedContactList.isEmpty) { + return const SizedBox.shrink(); + } + + return ListView.separated( + itemCount: controller.searchedContactList.length, + separatorBuilder: (context, index) { + return Padding( + padding: ContactViewStyle.getDividerSearchResultListPadding( + context, + controller.responsiveUtils + ), + child: const Divider(height: 1, color: AppColor.colorDivider), + ); + }, + itemBuilder: (context, index) { + return ContactItemWidget( + emailAddress: controller.searchedContactList[index], + selectedContactList: controller.selectedContactList, + imagePaths: controller.imagePaths, + responsiveUtils: controller.responsiveUtils, + onSelectContactAction: controller.handleOnSelectContactAction, + onDeleteContactAction: controller.handleOnDeleteContactAction, + ); + } + ); + }); + } + + Widget _buildSelectedContactListView() { + return Obx(() { + if (controller.selectedContactList.isEmpty) { + return const SizedBox.shrink(); + } + + return ListView.separated( + itemCount: controller.selectedContactList.length, + separatorBuilder: (context, index) { + return Padding( + padding: ContactViewStyle.getDividerSearchResultListPadding( + context, + controller.responsiveUtils + ), + child: const Divider(height: 1, color: AppColor.colorDivider), + ); + }, + itemBuilder: (context, index) { + return ContactItemWidget( + emailAddress: controller.selectedContactList[index], + selectedContactList: controller.selectedContactList, + imagePaths: controller.imagePaths, + responsiveUtils: controller.responsiveUtils, + onSelectContactAction: controller.handleOnSelectContactAction, + onDeleteContactAction: controller.handleOnDeleteContactAction, + ); + } + ); + }); } } \ No newline at end of file diff --git a/lib/features/contact/presentation/model/contact_arguments.dart b/lib/features/contact/presentation/model/contact_arguments.dart index 1136062893..5abdc58652 100644 --- a/lib/features/contact/presentation/model/contact_arguments.dart +++ b/lib/features/contact/presentation/model/contact_arguments.dart @@ -6,13 +6,13 @@ import 'package:jmap_dart_client/jmap/core/session/session.dart'; class ContactArguments with EquatableMixin { final AccountId accountId; final Session session; - final Set listContactSelected; + final Set selectedContactList; final String? contactViewTitle; ContactArguments({ required this.accountId, required this.session, - required this.listContactSelected, + required this.selectedContactList, this.contactViewTitle }); @@ -20,7 +20,7 @@ class ContactArguments with EquatableMixin { List get props => [ accountId, session, - listContactSelected, + selectedContactList, contactViewTitle, ]; } \ No newline at end of file diff --git a/lib/features/contact/presentation/styles/contact_item_widget_style.dart b/lib/features/contact/presentation/styles/contact_item_widget_style.dart new file mode 100644 index 0000000000..06d7e41bf4 --- /dev/null +++ b/lib/features/contact/presentation/styles/contact_item_widget_style.dart @@ -0,0 +1,30 @@ + +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/presentation/utils/responsive_utils.dart'; +import 'package:core/utils/platform_info.dart'; +import 'package:flutter/material.dart'; + +class ContactItemWidgetStyle { + static const TextStyle nameAddressTextStyle = TextStyle( + color: Colors.black, + fontSize: 15, + fontWeight: FontWeight.w600 + ); + + static const TextStyle emailAddressTextStyle = TextStyle( + color: AppColor.colorSubtitle, + fontSize: 13, + fontWeight: FontWeight.w400 + ); + + static EdgeInsetsGeometry getItemPadding( + BuildContext context, + ResponsiveUtils responsiveUtils + ) { + if (PlatformInfo.isWeb || responsiveUtils.isScreenWithShortestSide(context)) { + return const EdgeInsets.symmetric(horizontal: 16, vertical: 10); + } else { + return const EdgeInsets.symmetric(horizontal: 32, vertical: 10); + } + } +} \ No newline at end of file diff --git a/lib/features/contact/presentation/styles/contact_view_style.dart b/lib/features/contact/presentation/styles/contact_view_style.dart index aa7d77939f..05ad6776d4 100644 --- a/lib/features/contact/presentation/styles/contact_view_style.dart +++ b/lib/features/contact/presentation/styles/contact_view_style.dart @@ -75,7 +75,7 @@ class ContactViewStyle { static EdgeInsetsGeometry getSearchInputFormMargin( BuildContext context, - ResponsiveUtils responsiveUtils + ResponsiveUtils responsiveUtils, ) { if (PlatformInfo.isWeb || responsiveUtils.isScreenWithShortestSide(context)) { return const EdgeInsets.symmetric(horizontal: 16, vertical: 10); @@ -83,4 +83,15 @@ class ContactViewStyle { return const EdgeInsets.symmetric(horizontal: 32, vertical: 10); } } + + static EdgeInsets getDividerSearchResultListPadding( + BuildContext context, + ResponsiveUtils responsiveUtils + ) { + if (PlatformInfo.isWeb || responsiveUtils.isScreenWithShortestSide(context)) { + return const EdgeInsets.symmetric(horizontal: 16); + } else { + return const EdgeInsets.symmetric(horizontal: 32); + } + } } \ No newline at end of file diff --git a/lib/features/contact/presentation/utils/contact_utils.dart b/lib/features/contact/presentation/utils/contact_utils.dart deleted file mode 100644 index 5c7d919c85..0000000000 --- a/lib/features/contact/presentation/utils/contact_utils.dart +++ /dev/null @@ -1,30 +0,0 @@ - -import 'package:core/presentation/utils/responsive_utils.dart'; -import 'package:core/utils/platform_info.dart'; -import 'package:flutter/material.dart'; - -class ContactUtils { - static EdgeInsets getPaddingSearchResultList(BuildContext context, ResponsiveUtils responsiveUtils) { - if (PlatformInfo.isWeb) { - return const EdgeInsets.symmetric(horizontal: 16, vertical: 10); - } else { - if (responsiveUtils.isScreenWithShortestSide(context)) { - return const EdgeInsets.symmetric(horizontal: 16, vertical: 10); - } else { - return const EdgeInsets.symmetric(horizontal: 32, vertical: 10); - } - } - } - - static EdgeInsets getPaddingDividerSearchResultList(BuildContext context, ResponsiveUtils responsiveUtils) { - if (PlatformInfo.isWeb) { - return const EdgeInsets.symmetric(horizontal: 16); - } else { - if (responsiveUtils.isScreenWithShortestSide(context)) { - return const EdgeInsets.symmetric(horizontal: 16); - } else { - return const EdgeInsets.symmetric(horizontal: 32); - } - } - } -} \ No newline at end of file diff --git a/lib/features/contact/presentation/widgets/contact_input_decoration_builder.dart b/lib/features/contact/presentation/widgets/contact_input_decoration_builder.dart deleted file mode 100644 index 0f5794ac6e..0000000000 --- a/lib/features/contact/presentation/widgets/contact_input_decoration_builder.dart +++ /dev/null @@ -1,57 +0,0 @@ - -import 'package:core/presentation/extensions/color_extension.dart'; -import 'package:core/presentation/views/text/input_decoration_builder.dart'; -import 'package:core/utils/platform_info.dart'; -import 'package:flutter/material.dart'; - -class ContactInputDecorationBuilder extends InputDecorationBuilder { - - @override - InputDecoration build() { - return InputDecoration( - enabledBorder: enabledBorder ?? const OutlineInputBorder( - borderRadius: BorderRadius.all(Radius.circular(12)), - borderSide: BorderSide( - width: 0.5, - color: AppColor.colorInputBorderCreateMailbox)), - focusedBorder: enabledBorder ?? const OutlineInputBorder( - borderRadius: BorderRadius.all(Radius.circular(12)), - borderSide: BorderSide( - width: 1, - color: AppColor.colorTextButton)), - errorBorder: errorBorder ?? const OutlineInputBorder( - borderRadius: BorderRadius.all(Radius.circular(12)), - borderSide: BorderSide( - width: 1, - color: AppColor.colorInputBorderErrorVerifyName)), - focusedErrorBorder: errorBorder ?? const OutlineInputBorder( - borderRadius: BorderRadius.all(Radius.circular(12)), - borderSide: BorderSide( - width: 0.5, - color: AppColor.colorInputBorderErrorVerifyName)), - prefixText: prefixText, - labelText: labelText, - floatingLabelBehavior: FloatingLabelBehavior.never, - labelStyle: labelStyle ?? const TextStyle( - color: Colors.black, - fontSize: 16, - fontWeight: FontWeight.w500), - hintText: hintText, - isDense: true, - hintStyle: hintStyle ?? const TextStyle( - color: AppColor.colorEmailAddressFull, - fontWeight: FontWeight.w500, - fontSize: 16), - contentPadding: EdgeInsets.symmetric( - vertical: PlatformInfo.isWeb ? 16 : 12, - horizontal: 12), - errorText: errorText, - errorStyle: errorTextStyle ?? const TextStyle( - color: AppColor.colorInputBorderErrorVerifyName, - fontSize: 13), - filled: true, - fillColor: errorText?.isNotEmpty == true - ? AppColor.colorInputBackgroundErrorVerifyName - : AppColor.colorInputBackgroundCreateMailbox); - } -} \ No newline at end of file diff --git a/lib/features/contact/presentation/widgets/contact_item_widget.dart b/lib/features/contact/presentation/widgets/contact_item_widget.dart new file mode 100644 index 0000000000..3da80b23b5 --- /dev/null +++ b/lib/features/contact/presentation/widgets/contact_item_widget.dart @@ -0,0 +1,102 @@ + +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/presentation/resources/image_paths.dart'; +import 'package:core/presentation/utils/responsive_utils.dart'; +import 'package:core/presentation/views/avatar/gradient_circle_avatar_icon.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.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/contact/presentation/styles/contact_item_widget_style.dart'; + +typedef OnSelectContactAction = Function(EmailAddress emailAddress); +typedef OnDeleteContactAction = Function(EmailAddress emailAddress); + +class ContactItemWidget extends StatelessWidget { + + final EmailAddress emailAddress; + final List selectedContactList; + final ImagePaths imagePaths; + final ResponsiveUtils responsiveUtils; + final OnSelectContactAction onSelectContactAction; + final OnDeleteContactAction onDeleteContactAction; + + const ContactItemWidget({ + Key? key, + required this.emailAddress, + required this.selectedContactList, + required this.imagePaths, + required this.responsiveUtils, + required this.onSelectContactAction, + required this.onDeleteContactAction, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final childWidget = Padding( + padding: ContactItemWidgetStyle.getItemPadding( + context, + responsiveUtils + ), + child: Row( + children: [ + GradientCircleAvatarIcon( + colors: emailAddress.avatarColors, + label: emailAddress.labelAvatar, + ), + const SizedBox(width: 12), + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + emailAddress.asString(), + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: ContactItemWidgetStyle.nameAddressTextStyle + ), + if (emailAddress.displayName.isNotEmpty) + Padding( + padding: const EdgeInsets.only(top: 2), + child: Text( + emailAddress.emailAddress, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: ContactItemWidgetStyle.emailAddressTextStyle + ) + ) + ] + ) + ), + Padding( + padding: const EdgeInsetsDirectional.symmetric(horizontal: 12), + child: SvgPicture.asset( + _isSelectedEmailAddress + ? imagePaths.icCheckboxSelected + : imagePaths.icCheckboxUnselected, + width: 20, + height: 20, + colorFilter: _isSelectedEmailAddress + ? AppColor.primaryColor.asFilter() + : AppColor.colorEmailTileCheckboxUnhover.asFilter(), + ), + ), + ] + ), + ); + + return Material( + color: Colors.transparent, + child: InkWell( + onTap: () => _isSelectedEmailAddress + ? onDeleteContactAction(emailAddress) + : onSelectContactAction(emailAddress), + child: childWidget, + ), + ); + } + + bool get _isSelectedEmailAddress => selectedContactList + .any((contact) => contact.emailAddress == emailAddress.emailAddress); +} \ No newline at end of file diff --git a/lib/features/contact/presentation/widgets/contact_list_action_widget.dart b/lib/features/contact/presentation/widgets/contact_list_action_widget.dart new file mode 100644 index 0000000000..92ac556c4e --- /dev/null +++ b/lib/features/contact/presentation/widgets/contact_list_action_widget.dart @@ -0,0 +1,66 @@ + +import 'package:core/presentation/extensions/capitalize_extension.dart'; +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/presentation/views/button/tmail_button_widget.dart'; +import 'package:flutter/material.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; + +class ContactListActionWidget extends StatelessWidget { + + final VoidCallback onClearFilterAction; + final VoidCallback onDoneAction; + + const ContactListActionWidget({ + super.key, + required this.onClearFilterAction, + required this.onDoneAction + }); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(16), + width: double.infinity, + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Flexible( + child: TMailButtonWidget.fromText( + text: AppLocalizations.of(context).clearFilter.capitalizeFirstEach, + backgroundColor: AppColor.colorContactViewClearFilterButton.withOpacity(0.05), + borderRadius: 10, + maxHeight: 44, + minWidth: 156, + maxLines: 1, + textAlign: TextAlign.center, + textStyle: const TextStyle( + fontWeight: FontWeight.w500, + fontSize: 17, + color: AppColor.primaryColor + ), + onTapActionCallback: onClearFilterAction, + ), + ), + const SizedBox(width: 12), + Flexible( + child: TMailButtonWidget.fromText( + text: AppLocalizations.of(context).done, + backgroundColor: AppColor.primaryColor, + borderRadius: 10, + maxHeight: 44, + minWidth: 156, + maxLines: 1, + textAlign: TextAlign.center, + textStyle: const TextStyle( + fontWeight: FontWeight.w500, + fontSize: 17, + color: Colors.white + ), + onTapActionCallback: onDoneAction, + ), + ), + ], + ), + ); + } +} diff --git a/lib/features/contact/presentation/widgets/contact_loading_bar_widget.dart b/lib/features/contact/presentation/widgets/contact_loading_bar_widget.dart new file mode 100644 index 0000000000..55f909ff97 --- /dev/null +++ b/lib/features/contact/presentation/widgets/contact_loading_bar_widget.dart @@ -0,0 +1,31 @@ + +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:core/presentation/views/loading/cupertino_loading_widget.dart'; +import 'package:dartz/dartz.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:tmail_ui_user/features/thread/domain/state/search_email_state.dart'; + +class ContactLoadingBarWidget extends StatelessWidget { + + final Either viewState; + + const ContactLoadingBarWidget({super.key, required this.viewState}); + + @override + Widget build(BuildContext context) { + return viewState.fold( + (failure) => const SizedBox.shrink(), + (success) { + if (success is SearchingState) { + return const CupertinoLoadingWidget( + padding: EdgeInsets.symmetric(vertical: 12) + ); + } else { + return const SizedBox.shrink(); + } + } + ); + } +} \ No newline at end of file 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 a548e33749..75e48815ea 100644 --- a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart +++ b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart @@ -1696,42 +1696,48 @@ class MailboxDashBoardController extends ReloadableController with UserSettingPo dispatchAction(StartSearchEmailAction()); } - Future selectFromSearchFilter(BuildContext context) async { + Future selectFromSearchFilter({required AppLocalizations appLocalizations}) async { if (accountId.value == null || sessionCurrent == null) return; final contactArgument = ContactArguments( accountId: accountId.value!, session: sessionCurrent!, - listContactSelected: searchController.searchEmailFilter.value.from, - contactViewTitle: '${AppLocalizations.of(context).findEmails} ${AppLocalizations.of(context).from_email_address_prefix.toLowerCase()}' + selectedContactList: searchController.searchEmailFilter.value.from, + contactViewTitle: '${appLocalizations.findEmails} ${appLocalizations.from_email_address_prefix.toLowerCase()}' ); - final newContact = await DialogRouter.pushGeneralDialog( + final newListContact = await DialogRouter.pushGeneralDialog( routeName: AppRoutes.contact, arguments: contactArgument); - if (newContact is EmailAddress) { - searchController.updateFilterEmail(fromOption: Some({newContact.emailAddress})); + if (newListContact is List) { + final listMailAddress = newListContact + .map((emailAddress) => emailAddress.emailAddress) + .toSet(); + searchController.updateFilterEmail(fromOption: Some(listMailAddress)); dispatchAction(StartSearchEmailBySearchFilterAction(QuickSearchFilter.from)); } } - Future selectToSearchFilter(BuildContext context) async { + Future selectToSearchFilter({required AppLocalizations appLocalizations}) async { if (accountId.value == null || sessionCurrent == null) return; final contactArgument = ContactArguments( accountId: accountId.value!, session: sessionCurrent!, - listContactSelected: searchController.searchEmailFilter.value.to, - contactViewTitle: '${AppLocalizations.of(context).findEmails} ${AppLocalizations.of(context).to_email_address_prefix.toLowerCase()}' + selectedContactList: searchController.searchEmailFilter.value.to, + contactViewTitle: '${appLocalizations.findEmails} ${appLocalizations.to_email_address_prefix.toLowerCase()}' ); - final newContact = await DialogRouter.pushGeneralDialog( + final newListContact = await DialogRouter.pushGeneralDialog( routeName: AppRoutes.contact, arguments: contactArgument); - if (newContact is EmailAddress) { - searchController.updateFilterEmail(toOption: Some({newContact.emailAddress})); + if (newListContact is List) { + final listMailAddress = newListContact + .map((emailAddress) => emailAddress.emailAddress) + .toSet(); + searchController.updateFilterEmail(toOption: Some(listMailAddress)); dispatchAction(StartSearchEmailBySearchFilterAction(QuickSearchFilter.to)); } } @@ -1796,7 +1802,7 @@ class MailboxDashBoardController extends ReloadableController with UserSettingPo } } - void selectSortOrderQuickSearchFilter(BuildContext context, EmailSortOrderType sortOrder) { + void selectSortOrderQuickSearchFilter(EmailSortOrderType sortOrder) { log('MailboxDashBoardController::selectSortOrderQuickSearchFilter():sortOrder: $sortOrder'); popBack(); searchController.sortOrderFiltered.value = sortOrder; 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 021c5d7f97..13b013db16 100644 --- a/lib/features/mailbox_dashboard/presentation/mailbox_dashboard_view_web.dart +++ b/lib/features/mailbox_dashboard/presentation/mailbox_dashboard_view_web.dart @@ -591,13 +591,17 @@ class MailboxDashBoardView extends BaseMailboxDashBoardView { } break; case QuickSearchFilter.from: - controller.selectFromSearchFilter(context); + controller.selectFromSearchFilter( + appLocalizations: AppLocalizations.of(context) + ); break; case QuickSearchFilter.hasAttachment: controller.selectHasAttachmentSearchFilter(); break; case QuickSearchFilter.to: - controller.selectToSearchFilter(context); + controller.selectToSearchFilter( + appLocalizations: AppLocalizations.of(context) + ); break; case QuickSearchFilter.folder: controller.selectFolderSearchFilter(); @@ -665,7 +669,7 @@ class MailboxDashBoardView extends BaseMailboxDashBoardView { popupMenuEmailSortOrderType( context, controller.searchController.sortOrderFiltered.value, - onCallBack: (sortOrder) => controller.selectSortOrderQuickSearchFilter(context, sortOrder) + onCallBack: controller.selectSortOrderQuickSearchFilter ) ); } diff --git a/lib/features/search/email/presentation/search_email_controller.dart b/lib/features/search/email/presentation/search_email_controller.dart index b137d776af..1f46f5b367 100644 --- a/lib/features/search/email/presentation/search_email_controller.dart +++ b/lib/features/search/email/presentation/search_email_controller.dart @@ -640,22 +640,26 @@ class SearchEmailController extends BaseController if (accountId == null || session == null) return; - final listContactSelected = searchEmailFilter.value.getContactApplied(prefixEmailAddress); + final selectedContactList = searchEmailFilter.value.getContactApplied(prefixEmailAddress); final arguments = ContactArguments( accountId: accountId!, session: session!, - listContactSelected: listContactSelected, + selectedContactList: selectedContactList, contactViewTitle: '${AppLocalizations.of(context).findEmails} ${prefixEmailAddress.asName(context).toLowerCase()}' ); - final newContact = await push(AppRoutes.contact, arguments: arguments); + final newListContact = await push(AppRoutes.contact, arguments: arguments); + + if (newListContact is List && context.mounted) { + final listMailAddress = newListContact + .map((emailAddress) => emailAddress.emailAddress) + .toSet(); - if (newContact is EmailAddress && context.mounted) { _dispatchApplyContactAction( context, - listContactSelected, - prefixEmailAddress, - newContact); + listMailAddress, + prefixEmailAddress + ); } } @@ -663,48 +667,21 @@ class SearchEmailController extends BaseController BuildContext context, Set listContactSelected, PrefixEmailAddress prefixEmailAddress, - EmailAddress newContact ) { - if (listContactSelected.isNotEmpty) { - switch(prefixEmailAddress) { - case PrefixEmailAddress.from: - if (listContactSelected.first == newContact.email) { - searchEmailFilter.value.from.clear(); - } else { - searchEmailFilter.value.from.clear(); - searchEmailFilter.value.from.add(newContact.email!); - } - break; - case PrefixEmailAddress.to: - if (listContactSelected.first == newContact.email) { - searchEmailFilter.value.to.clear(); - } else { - searchEmailFilter.value.to.clear(); - searchEmailFilter.value.to.add(newContact.email!); - } - break; - default: - break; - } - } else { - switch(prefixEmailAddress) { - case PrefixEmailAddress.from: - searchEmailFilter.value.from.add(newContact.email!); - break; - case PrefixEmailAddress.to: - searchEmailFilter.value.to.add(newContact.email!); - break; - default: - break; - } - } - switch(prefixEmailAddress) { case PrefixEmailAddress.from: - _updateSimpleSearchFilter(fromOption: Some(searchEmailFilter.value.from)); + _updateSimpleSearchFilter( + fromOption: listContactSelected.isNotEmpty + ? Some(listContactSelected) + : const None() + ); break; case PrefixEmailAddress.to: - _updateSimpleSearchFilter(toOption: Some(searchEmailFilter.value.to)); + _updateSimpleSearchFilter( + toOption: listContactSelected.isNotEmpty + ? Some(listContactSelected) + : const None() + ); break; default: break; diff --git a/lib/features/thread/presentation/widgets/search_app_bar_widget.dart b/lib/features/thread/presentation/widgets/search_app_bar_widget.dart index 5e24742f4a..39ede36f8a 100644 --- a/lib/features/thread/presentation/widgets/search_app_bar_widget.dart +++ b/lib/features/thread/presentation/widgets/search_app_bar_widget.dart @@ -32,6 +32,7 @@ class SearchAppBarWidget extends StatelessWidget { final double? searchIconSize; final TextStyle? inputHintTextStyle; final TextStyle? inputTextStyle; + final bool? autoFocus; final OnCancelSearchPressed? onCancelSearchPressed; final OnTextChangeSearchAction? onTextChangeSearchAction; final OnClearTextSearchAction? onClearTextSearchAction; @@ -55,6 +56,7 @@ class SearchAppBarWidget extends StatelessWidget { this.searchIconSize, this.inputHintTextStyle, this.inputTextStyle, + this.autoFocus, this.onCancelSearchPressed, this.onTextChangeSearchAction, this.onClearTextSearchAction, @@ -122,7 +124,7 @@ class SearchAppBarWidget extends StatelessWidget { cursorColor: AppColor.colorTextButton, maxLines: 1, textDirection: DirectionUtils.getDirectionByLanguage(context), - autoFocus: true, + autoFocus: autoFocus ?? true, focusNode: searchFocusNode, textStyle: inputTextStyle ?? const TextStyle( color: AppColor.colorNameEmail, diff --git a/test/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller_test.dart b/test/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller_test.dart index e36ff127d0..55cd94908d 100644 --- a/test/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller_test.dart +++ b/test/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller_test.dart @@ -399,7 +399,6 @@ void main() { // expect sort in search controller update as expected mailboxDashboardController.selectSortOrderQuickSearchFilter( - context, EmailSortOrderType.oldest); expect(searchController.sortOrderFiltered.value, EmailSortOrderType.oldest);