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 colorStarredSearchFilterIcon = Color(0xFFFFCC00);
static const colorMobileSearchFilterButton = Color(0xFFEBEDF0);
static const colorContactViewClearFilterButton = Color(0x001C3D0D);
static const mapGradientColor = [
[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/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<Session>();
final listContactSearched = RxList<EmailAddress>();
final searchedContactList = RxList<EmailAddress>();
final selectedContactList = RxList<EmailAddress>();
final contactArguments = Rxn<ContactArguments>();
final searchStatus = SearchStatus.INACTIVE.obs;
final searchViewState = Rx<Either<Failure, Success>>(Right(UIState.idle));
GetAllAutoCompleteInteractor? _getAllAutoCompleteInteractor;
GetAutoCompleteInteractor? _getAutoCompleteInteractor;
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;
EmailAddress? contactSelected;
SelectedContactCallbackAction? onSelectedContactCallback;
VoidCallback? onDismissContactView;
StreamSubscription<String>? _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<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() {
textInputSearchController.clear();
searchQuery.value = SearchQuery.initial();
searchedContactList.clear();
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');
final listContact = await _getAutoCompleteSuggestion(query);
listContactSearched.value = listContact;
searchedContactList.value = listContact;
}
Future<List<EmailAddress>> _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() {
textInputSearchController.clear();
searchQuery.value = SearchQuery.initial();
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();
searchStatus.value = SearchStatus.INACTIVE;
}
}
@@ -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<ContactController> {
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<ContactController> {
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),
),
],
),
);
Obx(() => ContactLoadingBarWidget(
viewState: controller.searchViewState.value,
)),
Expanded(
child: Obx(() {
if (controller.searchStatus.value == SearchStatus.ACTIVE) {
return _buildSearchedContactListView();
} else {
return const SizedBox.shrink();
return _buildSelectedContactListView();
}
}),
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),
);
}
)
);
} 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();
}
})),
})
),
ContactListActionWidget(
onClearFilterAction: controller.handleOnClearFilterAction,
onDoneAction: controller.handleOnDoneAction,
),
]
),
)
@@ -183,11 +110,65 @@ class ContactView extends GetWidget<ContactController> {
);
}
SuggestionEmailAddress _toSuggestionEmailAddress(EmailAddress item, List<EmailAddress> 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,
);
}
);
});
}
}
@@ -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<String> listContactSelected;
final Set<String> 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<Object?> get props => [
accountId,
session,
listContactSelected,
selectedContactList,
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(
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);
}
}
}
@@ -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());
}
Future<void> selectFromSearchFilter(BuildContext context) async {
Future<void> 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<EmailAddress>) {
final listMailAddress = newListContact
.map((emailAddress) => emailAddress.emailAddress)
.toSet();
searchController.updateFilterEmail(fromOption: Some(listMailAddress));
dispatchAction(StartSearchEmailBySearchFilterAction(QuickSearchFilter.from));
}
}
Future<void> selectToSearchFilter(BuildContext context) async {
Future<void> 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<EmailAddress>) {
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;
@@ -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
)
);
}
@@ -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<EmailAddress> && 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<String> 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;
@@ -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,
@@ -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);