TF-3005 Apply new design search result list contact view

This commit is contained in:
dab246
2024-10-08 18:37:18 +07:00
committed by Dat H. Pham
parent 4cdd55ed82
commit 6d0c7ed7f5
16 changed files with 461 additions and 277 deletions
@@ -233,6 +233,7 @@ extension AppColor on Color {
static const colorFilterMessageTitle = Color(0xFF686E76); static const colorFilterMessageTitle = Color(0xFF686E76);
static const colorStarredSearchFilterIcon = Color(0xFFFFCC00); static const colorStarredSearchFilterIcon = Color(0xFFFFCC00);
static const colorMobileSearchFilterButton = Color(0xFFEBEDF0); static const colorMobileSearchFilterButton = Color(0xFFEBEDF0);
static const colorContactViewClearFilterButton = Color(0x001C3D0D);
static const mapGradientColor = [ static const mapGradientColor = [
[Color(0xFF21D4FD), Color(0xFFB721FF)], [Color(0xFF21D4FD), Color(0xFFB721FF)],
@@ -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/presentation/utils/theme_utils.dart';
import 'package:core/utils/app_logger.dart'; import 'package:core/utils/app_logger.dart';
import 'package:core/utils/platform_info.dart'; import 'package:core/utils/platform_info.dart';
import 'package:dartz/dartz.dart';
import 'package:debounce_throttle/debounce_throttle.dart'; import 'package:debounce_throttle/debounce_throttle.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:jmap_dart_client/jmap/account_id.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:jmap_dart_client/jmap/mail/email/email_address.dart';
import 'package:model/autocomplete/auto_complete_pattern.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:permission_handler/permission_handler.dart';
import 'package:tmail_ui_user/features/base/base_controller.dart'; import 'package:tmail_ui_user/features/base/base_controller.dart';
import 'package:tmail_ui_user/features/composer/domain/model/contact_suggestion_source.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/model/contact_arguments.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_suggestion_box_item.dart';
import 'package:tmail_ui_user/features/thread/domain/model/search_query.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'; import 'package:tmail_ui_user/main/routes/route_navigation.dart';
class ContactController extends BaseController { class ContactController extends BaseController {
@@ -30,20 +36,24 @@ class ContactController extends BaseController {
ContactSuggestionSource _contactSuggestionSource = ContactSuggestionSource.tMailContact; ContactSuggestionSource _contactSuggestionSource = ContactSuggestionSource.tMailContact;
final searchQuery = SearchQuery.initial().obs; final searchQuery = SearchQuery.initial().obs;
final session = Rxn<Session>(); final searchedContactList = RxList<EmailAddress>();
final listContactSearched = RxList<EmailAddress>(); final selectedContactList = RxList<EmailAddress>();
final contactArguments = Rxn<ContactArguments>(); final contactArguments = Rxn<ContactArguments>();
final searchStatus = SearchStatus.INACTIVE.obs;
final searchViewState = Rx<Either<Failure, Success>>(Right(UIState.idle));
GetAllAutoCompleteInteractor? _getAllAutoCompleteInteractor; GetAllAutoCompleteInteractor? _getAllAutoCompleteInteractor;
GetAutoCompleteInteractor? _getAutoCompleteInteractor; GetAutoCompleteInteractor? _getAutoCompleteInteractor;
GetDeviceContactSuggestionsInteractor? _getDeviceContactSuggestionsInteractor; GetDeviceContactSuggestionsInteractor? _getDeviceContactSuggestionsInteractor;
final Debouncer<String> _deBouncerTime = Debouncer<String>(const Duration(milliseconds: 300), initialValue: ''); final Debouncer<String> _deBouncerTime = Debouncer<String>(
const Duration(milliseconds: 300),
initialValue: ''
);
AccountId? _accountId; AccountId? _accountId;
EmailAddress? contactSelected;
SelectedContactCallbackAction? onSelectedContactCallback; SelectedContactCallbackAction? onSelectedContactCallback;
VoidCallback? onDismissContactView; VoidCallback? onDismissContactView;
StreamSubscription<String>? _deBouncerTimeStreamSubscription;
@override @override
void onInit() { void onInit() {
@@ -51,25 +61,23 @@ class ContactController extends BaseController {
ThemeUtils.setStatusBarTransparentColor(); ThemeUtils.setStatusBarTransparentColor();
log('ContactController::onInit():arguments: ${Get.arguments}'); log('ContactController::onInit():arguments: ${Get.arguments}');
contactArguments.value = Get.arguments; contactArguments.value = Get.arguments;
_deBouncerTime.values.listen((value) { _deBouncerTimeStreamSubscription = _deBouncerTime.values.listen(_handleDeBounceTimeSearchContact);
searchQuery.value = SearchQuery(value); textInputSearchFocus.addListener(_onSearchInputTextFocusListener);
_searchContactByNameOrEmail(searchQuery.value.value);
});
} }
@override @override
void onReady() { void onReady() {
super.onReady(); super.onReady();
textInputSearchFocus.requestFocus();
if (contactArguments.value != null) { if (contactArguments.value != null) {
_accountId = contactArguments.value!.accountId; _accountId = contactArguments.value!.accountId;
session.value = contactArguments.value!.session; selectedContactList.value = contactArguments.value!.selectedContactList
final listContactSelected = contactArguments.value!.listContactSelected; .map((mailAddress) => EmailAddress(null, mailAddress))
log('ContactController::onReady(): listContactSelected: $listContactSelected'); .toList();
if (listContactSelected.isNotEmpty) { injectAutoCompleteBindings(contactArguments.value!.session, _accountId);
contactSelected = EmailAddress(listContactSelected.first, listContactSelected.first);
if (selectedContactList.isEmpty) {
textInputSearchFocus.requestFocus();
} }
injectAutoCompleteBindings(session.value, _accountId);
} }
if (PlatformInfo.isMobile) { if (PlatformInfo.isMobile) {
Future.delayed( Future.delayed(
@@ -81,12 +89,21 @@ class ContactController extends BaseController {
@override @override
void onClose() { void onClose() {
log('ContactController::onClose():'); log('ContactController::onClose():');
searchViewState.value = Right(UIState.idle);
textInputSearchFocus.removeListener(_onSearchInputTextFocusListener);
textInputSearchFocus.dispose(); textInputSearchFocus.dispose();
textInputSearchController.dispose(); textInputSearchController.dispose();
_deBouncerTimeStreamSubscription?.cancel();
_deBouncerTime.cancel(); _deBouncerTime.cancel();
super.onClose(); super.onClose();
} }
void _onSearchInputTextFocusListener() {
if (textInputSearchFocus.hasFocus && textInputSearchController.text.trim().isEmpty) {
searchStatus.value = SearchStatus.INACTIVE;
}
}
void onTextSearchChange(String text) { void onTextSearchChange(String text) {
_deBouncerTime.value = text; _deBouncerTime.value = text;
} }
@@ -95,9 +112,23 @@ class ContactController extends BaseController {
_deBouncerTime.value = text; _deBouncerTime.value = text;
} }
Future<void> _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() { void clearAllTextInputSearchForm() {
textInputSearchController.clear(); textInputSearchController.clear();
searchQuery.value = SearchQuery.initial(); searchQuery.value = SearchQuery.initial();
searchedContactList.clear();
textInputSearchFocus.requestFocus(); textInputSearchFocus.requestFocus();
} }
@@ -113,10 +144,10 @@ class ContactController extends BaseController {
} }
} }
void _searchContactByNameOrEmail(String query) async { Future<void> _searchContactByNameOrEmail(String query) async {
log('ContactController::_searchContactByNameOrEmail(): query: $query'); log('ContactController::_searchContactByNameOrEmail(): query: $query');
final listContact = await _getAutoCompleteSuggestion(query); final listContact = await _getAutoCompleteSuggestion(query);
listContactSearched.value = listContact; searchedContactList.value = listContact;
} }
Future<List<EmailAddress>> _getAutoCompleteSuggestion(String query) async { Future<List<EmailAddress>> _getAutoCompleteSuggestion(String query) async {
@@ -162,16 +193,46 @@ class ContactController extends BaseController {
} }
} }
void selectContact(BuildContext context, EmailAddress emailAddress) { void handleOnSelectContactAction(EmailAddress emailAddress) {
KeyboardUtils.hideKeyboard(context); log('ContactController::selectContact:emailAddress = $emailAddress');
popBack(result: 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() { void closeContactView() {
textInputSearchController.clear();
searchQuery.value = SearchQuery.initial();
textInputSearchFocus.unfocus(); textInputSearchFocus.unfocus();
FocusManager.instance.primaryFocus?.unfocus(); FocusManager.instance.primaryFocus?.unfocus();
popBack(); 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();
searchStatus.value = SearchStatus.INACTIVE;
}
} }
@@ -1,16 +1,14 @@
import 'package:core/presentation/extensions/color_extension.dart'; import 'package:core/presentation/extensions/color_extension.dart';
import 'package:core/utils/platform_info.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.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: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/contact_controller.dart';
import 'package:tmail_ui_user/features/contact/presentation/styles/contact_view_style.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/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/features/thread/presentation/widgets/search_app_bar_widget.dart';
import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
@@ -68,10 +66,11 @@ class ContactView extends GetWidget<ContactController> {
hasBackButton: false, hasBackButton: false,
hasSearchButton: true, hasSearchButton: true,
searchIconSize: 20, searchIconSize: 20,
autoFocus: false,
searchIconColor: AppColor.colorHintSearchBar, searchIconColor: AppColor.colorHintSearchBar,
margin: ContactViewStyle.getSearchInputFormMargin( margin: ContactViewStyle.getSearchInputFormMargin(
context, context,
controller.responsiveUtils controller.responsiveUtils,
), ),
decoration: const BoxDecoration( decoration: const BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(12)), borderRadius: BorderRadius.all(Radius.circular(12)),
@@ -84,94 +83,22 @@ class ContactView extends GetWidget<ContactController> {
onTextChangeSearchAction: controller.onTextSearchChange, onTextChangeSearchAction: controller.onTextSearchChange,
onSearchTextAction: controller.onSearchTextAction, onSearchTextAction: controller.onSearchTextAction,
)), )),
if (PlatformInfo.isWeb) Obx(() => ContactLoadingBarWidget(
Obx(() { viewState: controller.searchViewState.value,
final username = controller.session.value?.username.value ?? ''; )),
if (username.isNotEmpty) { Expanded(
final userEmailAddress = EmailAddress( child: Obx(() {
AppLocalizations.of(context).me, if (controller.searchStatus.value == SearchStatus.ACTIVE) {
username); return _buildSearchedContactListView();
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 { } else {
return const SizedBox.shrink(); return _buildSelectedContactListView();
} }
}), })
Expanded(child: Obx(() { ),
if (controller.listContactSearched.isNotEmpty) { ContactListActionWidget(
if (PlatformInfo.isMobile) { onClearFilterAction: controller.handleOnClearFilterAction,
return Container( onDoneAction: controller.handleOnDoneAction,
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),
);
}
)
);
} 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),
);
}
)
);
}
} else {
return const SizedBox.shrink();
}
})),
] ]
), ),
) )
@@ -183,11 +110,65 @@ class ContactView extends GetWidget<ContactController> {
); );
} }
SuggestionEmailAddress _toSuggestionEmailAddress(EmailAddress item, List<EmailAddress> addedEmailAddresses) { Widget _buildSearchedContactListView() {
if (addedEmailAddresses.contains(item)) { return Obx(() {
return SuggestionEmailAddress(item, state: SuggestionEmailState.duplicated); if (controller.searchedContactList.isEmpty) {
} else { return const SizedBox.shrink();
return SuggestionEmailAddress(item); }
}
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,
);
}
);
});
} }
} }
@@ -6,13 +6,13 @@ import 'package:jmap_dart_client/jmap/core/session/session.dart';
class ContactArguments with EquatableMixin { class ContactArguments with EquatableMixin {
final AccountId accountId; final AccountId accountId;
final Session session; final Session session;
final Set<String> listContactSelected; final Set<String> selectedContactList;
final String? contactViewTitle; final String? contactViewTitle;
ContactArguments({ ContactArguments({
required this.accountId, required this.accountId,
required this.session, required this.session,
required this.listContactSelected, required this.selectedContactList,
this.contactViewTitle this.contactViewTitle
}); });
@@ -20,7 +20,7 @@ class ContactArguments with EquatableMixin {
List<Object?> get props => [ List<Object?> get props => [
accountId, accountId,
session, session,
listContactSelected, selectedContactList,
contactViewTitle, contactViewTitle,
]; ];
} }
@@ -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);
}
}
}
@@ -75,7 +75,7 @@ class ContactViewStyle {
static EdgeInsetsGeometry getSearchInputFormMargin( static EdgeInsetsGeometry getSearchInputFormMargin(
BuildContext context, BuildContext context,
ResponsiveUtils responsiveUtils ResponsiveUtils responsiveUtils,
) { ) {
if (PlatformInfo.isWeb || responsiveUtils.isScreenWithShortestSide(context)) { if (PlatformInfo.isWeb || responsiveUtils.isScreenWithShortestSide(context)) {
return const EdgeInsets.symmetric(horizontal: 16, vertical: 10); return const EdgeInsets.symmetric(horizontal: 16, vertical: 10);
@@ -83,4 +83,15 @@ class ContactViewStyle {
return const EdgeInsets.symmetric(horizontal: 32, vertical: 10); 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);
}
}
} }
@@ -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);
}
}
}
}
@@ -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);
}
}
@@ -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<EmailAddress> 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);
}
@@ -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,
),
),
],
),
);
}
}
@@ -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<Failure, Success> 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();
}
}
);
}
}
@@ -1696,42 +1696,48 @@ class MailboxDashBoardController extends ReloadableController with UserSettingPo
dispatchAction(StartSearchEmailAction()); dispatchAction(StartSearchEmailAction());
} }
Future<void> selectFromSearchFilter(BuildContext context) async { Future<void> selectFromSearchFilter({required AppLocalizations appLocalizations}) async {
if (accountId.value == null || sessionCurrent == null) return; if (accountId.value == null || sessionCurrent == null) return;
final contactArgument = ContactArguments( final contactArgument = ContactArguments(
accountId: accountId.value!, accountId: accountId.value!,
session: sessionCurrent!, session: sessionCurrent!,
listContactSelected: searchController.searchEmailFilter.value.from, selectedContactList: searchController.searchEmailFilter.value.from,
contactViewTitle: '${AppLocalizations.of(context).findEmails} ${AppLocalizations.of(context).from_email_address_prefix.toLowerCase()}' contactViewTitle: '${appLocalizations.findEmails} ${appLocalizations.from_email_address_prefix.toLowerCase()}'
); );
final newContact = await DialogRouter.pushGeneralDialog( final newListContact = await DialogRouter.pushGeneralDialog(
routeName: AppRoutes.contact, routeName: AppRoutes.contact,
arguments: contactArgument); arguments: contactArgument);
if (newContact is EmailAddress) { if (newListContact is List<EmailAddress>) {
searchController.updateFilterEmail(fromOption: Some({newContact.emailAddress})); final listMailAddress = newListContact
.map((emailAddress) => emailAddress.emailAddress)
.toSet();
searchController.updateFilterEmail(fromOption: Some(listMailAddress));
dispatchAction(StartSearchEmailBySearchFilterAction(QuickSearchFilter.from)); dispatchAction(StartSearchEmailBySearchFilterAction(QuickSearchFilter.from));
} }
} }
Future<void> selectToSearchFilter(BuildContext context) async { Future<void> selectToSearchFilter({required AppLocalizations appLocalizations}) async {
if (accountId.value == null || sessionCurrent == null) return; if (accountId.value == null || sessionCurrent == null) return;
final contactArgument = ContactArguments( final contactArgument = ContactArguments(
accountId: accountId.value!, accountId: accountId.value!,
session: sessionCurrent!, session: sessionCurrent!,
listContactSelected: searchController.searchEmailFilter.value.to, selectedContactList: searchController.searchEmailFilter.value.to,
contactViewTitle: '${AppLocalizations.of(context).findEmails} ${AppLocalizations.of(context).to_email_address_prefix.toLowerCase()}' contactViewTitle: '${appLocalizations.findEmails} ${appLocalizations.to_email_address_prefix.toLowerCase()}'
); );
final newContact = await DialogRouter.pushGeneralDialog( final newListContact = await DialogRouter.pushGeneralDialog(
routeName: AppRoutes.contact, routeName: AppRoutes.contact,
arguments: contactArgument); arguments: contactArgument);
if (newContact is EmailAddress) { if (newListContact is List<EmailAddress>) {
searchController.updateFilterEmail(toOption: Some({newContact.emailAddress})); final listMailAddress = newListContact
.map((emailAddress) => emailAddress.emailAddress)
.toSet();
searchController.updateFilterEmail(toOption: Some(listMailAddress));
dispatchAction(StartSearchEmailBySearchFilterAction(QuickSearchFilter.to)); 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'); log('MailboxDashBoardController::selectSortOrderQuickSearchFilter():sortOrder: $sortOrder');
popBack(); popBack();
searchController.sortOrderFiltered.value = sortOrder; searchController.sortOrderFiltered.value = sortOrder;
@@ -591,13 +591,17 @@ class MailboxDashBoardView extends BaseMailboxDashBoardView {
} }
break; break;
case QuickSearchFilter.from: case QuickSearchFilter.from:
controller.selectFromSearchFilter(context); controller.selectFromSearchFilter(
appLocalizations: AppLocalizations.of(context)
);
break; break;
case QuickSearchFilter.hasAttachment: case QuickSearchFilter.hasAttachment:
controller.selectHasAttachmentSearchFilter(); controller.selectHasAttachmentSearchFilter();
break; break;
case QuickSearchFilter.to: case QuickSearchFilter.to:
controller.selectToSearchFilter(context); controller.selectToSearchFilter(
appLocalizations: AppLocalizations.of(context)
);
break; break;
case QuickSearchFilter.folder: case QuickSearchFilter.folder:
controller.selectFolderSearchFilter(); controller.selectFolderSearchFilter();
@@ -665,7 +669,7 @@ class MailboxDashBoardView extends BaseMailboxDashBoardView {
popupMenuEmailSortOrderType( popupMenuEmailSortOrderType(
context, context,
controller.searchController.sortOrderFiltered.value, controller.searchController.sortOrderFiltered.value,
onCallBack: (sortOrder) => controller.selectSortOrderQuickSearchFilter(context, sortOrder) onCallBack: controller.selectSortOrderQuickSearchFilter
) )
); );
} }
@@ -640,22 +640,26 @@ class SearchEmailController extends BaseController
if (accountId == null || session == null) return; if (accountId == null || session == null) return;
final listContactSelected = searchEmailFilter.value.getContactApplied(prefixEmailAddress); final selectedContactList = searchEmailFilter.value.getContactApplied(prefixEmailAddress);
final arguments = ContactArguments( final arguments = ContactArguments(
accountId: accountId!, accountId: accountId!,
session: session!, session: session!,
listContactSelected: listContactSelected, selectedContactList: selectedContactList,
contactViewTitle: '${AppLocalizations.of(context).findEmails} ${prefixEmailAddress.asName(context).toLowerCase()}' 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<EmailAddress> && context.mounted) {
final listMailAddress = newListContact
.map((emailAddress) => emailAddress.emailAddress)
.toSet();
if (newContact is EmailAddress && context.mounted) {
_dispatchApplyContactAction( _dispatchApplyContactAction(
context, context,
listContactSelected, listMailAddress,
prefixEmailAddress, prefixEmailAddress
newContact); );
} }
} }
@@ -663,48 +667,21 @@ class SearchEmailController extends BaseController
BuildContext context, BuildContext context,
Set<String> listContactSelected, Set<String> listContactSelected,
PrefixEmailAddress prefixEmailAddress, 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) { switch(prefixEmailAddress) {
case PrefixEmailAddress.from: case PrefixEmailAddress.from:
_updateSimpleSearchFilter(fromOption: Some(searchEmailFilter.value.from)); _updateSimpleSearchFilter(
fromOption: listContactSelected.isNotEmpty
? Some(listContactSelected)
: const None()
);
break; break;
case PrefixEmailAddress.to: case PrefixEmailAddress.to:
_updateSimpleSearchFilter(toOption: Some(searchEmailFilter.value.to)); _updateSimpleSearchFilter(
toOption: listContactSelected.isNotEmpty
? Some(listContactSelected)
: const None()
);
break; break;
default: default:
break; break;
@@ -32,6 +32,7 @@ class SearchAppBarWidget extends StatelessWidget {
final double? searchIconSize; final double? searchIconSize;
final TextStyle? inputHintTextStyle; final TextStyle? inputHintTextStyle;
final TextStyle? inputTextStyle; final TextStyle? inputTextStyle;
final bool? autoFocus;
final OnCancelSearchPressed? onCancelSearchPressed; final OnCancelSearchPressed? onCancelSearchPressed;
final OnTextChangeSearchAction? onTextChangeSearchAction; final OnTextChangeSearchAction? onTextChangeSearchAction;
final OnClearTextSearchAction? onClearTextSearchAction; final OnClearTextSearchAction? onClearTextSearchAction;
@@ -55,6 +56,7 @@ class SearchAppBarWidget extends StatelessWidget {
this.searchIconSize, this.searchIconSize,
this.inputHintTextStyle, this.inputHintTextStyle,
this.inputTextStyle, this.inputTextStyle,
this.autoFocus,
this.onCancelSearchPressed, this.onCancelSearchPressed,
this.onTextChangeSearchAction, this.onTextChangeSearchAction,
this.onClearTextSearchAction, this.onClearTextSearchAction,
@@ -122,7 +124,7 @@ class SearchAppBarWidget extends StatelessWidget {
cursorColor: AppColor.colorTextButton, cursorColor: AppColor.colorTextButton,
maxLines: 1, maxLines: 1,
textDirection: DirectionUtils.getDirectionByLanguage(context), textDirection: DirectionUtils.getDirectionByLanguage(context),
autoFocus: true, autoFocus: autoFocus ?? true,
focusNode: searchFocusNode, focusNode: searchFocusNode,
textStyle: inputTextStyle ?? const TextStyle( textStyle: inputTextStyle ?? const TextStyle(
color: AppColor.colorNameEmail, color: AppColor.colorNameEmail,
@@ -399,7 +399,6 @@ void main() {
// expect sort in search controller update as expected // expect sort in search controller update as expected
mailboxDashboardController.selectSortOrderQuickSearchFilter( mailboxDashboardController.selectSortOrderQuickSearchFilter(
context,
EmailSortOrderType.oldest); EmailSortOrderType.oldest);
expect(searchController.sortOrderFiltered.value, EmailSortOrderType.oldest); expect(searchController.sortOrderFiltered.value, EmailSortOrderType.oldest);