From 11c9a8713a7df5b644283a5d9eb6cc5c246136f9 Mon Sep 17 00:00:00 2001 From: dab246 Date: Tue, 24 May 2022 11:56:50 +0700 Subject: [PATCH] TF-524 Apply quick search for mobile and tablet --- core/lib/core.dart | 2 +- .../typeahead/typeahead_for_quick_search.dart | 1847 ----------------- .../presentation/views/typeahead/typedef.dart | 15 - .../presentation/views/typeahead/utils.dart | 5 - .../domain/model/recent_search.dart | 4 + .../presentation/action/dashboard_action.dart | 10 +- .../filter_message_option_extension.dart | 34 - .../mailbox_dashboard_controller.dart | 65 +- .../mailbox_dashboard_view_web.dart | 70 +- .../mixin/filter_email_popup_menu_mixin.dart | 22 +- .../data/datasource/thread_datasource.dart | 2 +- .../local_thread_datasource_impl.dart | 2 +- .../thread_datasource_impl.dart | 2 +- .../data/local/email_cache_manager.dart | 1 + .../repository/thread_repository_impl.dart | 2 + .../thread/domain/model}/email_filter.dart | 2 +- .../model/filter_message_option.dart} | 62 +- .../domain/repository/thread_repository.dart | 3 +- .../get_emails_in_mailbox_interactor.dart | 1 + ..._changes_emails_in_mailbox_interactor.dart | 1 + .../presentation/thread_controller.dart | 175 +- .../thread/presentation/thread_view.dart | 544 +++-- .../app_bar_thread_widget_builder.dart | 19 +- ...cupertino_action_sheet_action_builder.dart | 5 +- .../widgets/suggestion_box_widget.dart | 88 - model/lib/email/filter_message_option.dart | 40 - model/lib/model.dart | 2 - ...get_emails_in_mailbox_interactor_test.dart | 5 +- ...ails_in_mailbox_interactor_test.mocks.dart | 23 +- ...ails_in_mailbox_interactor_test.mocks.dart | 23 +- 30 files changed, 647 insertions(+), 2429 deletions(-) delete mode 100644 core/lib/presentation/views/typeahead/typeahead_for_quick_search.dart delete mode 100644 core/lib/presentation/views/typeahead/typedef.dart delete mode 100644 core/lib/presentation/views/typeahead/utils.dart delete mode 100644 lib/features/mailbox_dashboard/presentation/extensions/filter_message_option_extension.dart rename {model/lib/email => lib/features/thread/domain/model}/email_filter.dart (83%) rename lib/features/thread/{presentation/extensions/filter_message_option_extension.dart => domain/model/filter_message_option.dart} (50%) delete mode 100644 lib/features/thread/presentation/widgets/suggestion_box_widget.dart delete mode 100644 model/lib/email/filter_message_option.dart diff --git a/core/lib/core.dart b/core/lib/core.dart index 97a865cbd..b3fb31444 100644 --- a/core/lib/core.dart +++ b/core/lib/core.dart @@ -62,7 +62,7 @@ export 'presentation/views/modal_sheets/edit_text_modal_sheet_builder.dart'; export 'presentation/views/search/search_bar_view.dart'; export 'presentation/views/popup_menu/popup_menu_item_widget.dart'; export 'presentation/views/tab_bar/custom_tab_indicator.dart'; -export 'presentation/views/typeahead/typeahead_for_quick_search.dart'; +export 'presentation/views/quick_search/quick_search_input_form.dart'; // Resources export 'presentation/resources/assets_paths.dart'; diff --git a/core/lib/presentation/views/typeahead/typeahead_for_quick_search.dart b/core/lib/presentation/views/typeahead/typeahead_for_quick_search.dart deleted file mode 100644 index 9f712c50e..000000000 --- a/core/lib/presentation/views/typeahead/typeahead_for_quick_search.dart +++ /dev/null @@ -1,1847 +0,0 @@ -/// # Flutter TypeAhead -/// A TypeAhead widget for Flutter, where you can show suggestions to -/// users as they type -import 'dart:async'; -import 'dart:math'; - -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart'; -import 'package:pointer_interceptor/pointer_interceptor.dart'; - -import 'typedef.dart'; -import 'utils.dart'; - -/// A [FormField](https://docs.flutter.io/flutter/widgets/FormField-class.html) -/// implementation of [TypeAheadFieldQuickSearch], that allows the value to be saved, -/// validated, etc. -/// -/// See also: -/// -/// * [TypeAheadFieldQuickSearch], A [TextField](https://docs.flutter.io/flutter/material/TextField-class.html) -/// that displays a list of suggestions as the user types -class TypeAheadFormFieldQuickSearch extends FormField { - /// The configuration of the [TextField](https://docs.flutter.io/flutter/material/TextField-class.html) - /// that the TypeAhead widget displays - final QuickSearchTextFieldConfiguration textFieldConfiguration; - - /// Creates a [TypeAheadFormFieldQuickSearch] - TypeAheadFormFieldQuickSearch( - {Key? key, - String? initialValue, - bool getImmediateSuggestions: false, - @Deprecated('Use autovalidateMode parameter which provides more specific ' - 'behavior related to auto validation. ' - 'This feature was deprecated after Flutter v1.19.0.') - bool autovalidate: false, - bool enabled: true, - AutovalidateMode autovalidateMode: AutovalidateMode.disabled, - FormFieldSetter? onSaved, - FormFieldValidator? validator, - ErrorBuilder? errorBuilder, - WidgetBuilder? noItemsFoundBuilder, - WidgetBuilder? loadingBuilder, - Duration debounceDuration: const Duration(milliseconds: 300), - QuickSearchSuggestionsBoxDecoration suggestionsBoxDecoration: - const QuickSearchSuggestionsBoxDecoration(), - QuickSearchSuggestionsBoxController? suggestionsBoxController, - required SuggestionSelectionCallback onSuggestionSelected, - required ItemBuilder itemBuilder, - required SuggestionsCallback suggestionsCallback, - double suggestionsBoxVerticalOffset: 5.0, - this.textFieldConfiguration: const QuickSearchTextFieldConfiguration(), - AnimationTransitionBuilder? transitionBuilder, - Duration animationDuration: const Duration(milliseconds: 500), - double animationStart: 0.25, - AxisDirection direction: AxisDirection.down, - bool hideOnLoading: false, - bool hideOnEmpty: false, - bool hideOnError: false, - bool hideSuggestionsOnKeyboardHide: true, - bool keepSuggestionsOnLoading: true, - bool keepSuggestionsOnSuggestionSelected: false, - bool autoFlipDirection: false, - bool hideKeyboard: false, - int minCharsForSuggestions: 0, - List? listActionButton, - ButtonActionBuilder? actionButtonBuilder, - ButtonActionCallback? buttonActionCallback, - ButtonActionBuilder? buttonShowAllResult, - Widget? titleHeaderRecent, - ItemBuilder? itemRecentBuilder, - FetchRecentActionCallback? fetchRecentActionCallback, - RecentSelectionCallback? onRecentSelected, - EdgeInsets? listActionPadding, - bool hideSuggestionsBox: false, - }) : assert( - initialValue == null || textFieldConfiguration.controller == null), - assert(minCharsForSuggestions >= 0), - super( - key: key, - onSaved: onSaved, - validator: validator, - initialValue: textFieldConfiguration.controller != null - ? textFieldConfiguration.controller!.text - : (initialValue ?? ''), - enabled: enabled, - autovalidateMode: autovalidateMode, - builder: (FormFieldState field) { - final _TypeAheadFormFieldState state = - field as _TypeAheadFormFieldState; - - return TypeAheadFieldQuickSearch( - getImmediateSuggestions: getImmediateSuggestions, - transitionBuilder: transitionBuilder, - errorBuilder: errorBuilder, - noItemsFoundBuilder: noItemsFoundBuilder, - loadingBuilder: loadingBuilder, - debounceDuration: debounceDuration, - suggestionsBoxDecoration: suggestionsBoxDecoration, - suggestionsBoxController: suggestionsBoxController, - textFieldConfiguration: textFieldConfiguration.copyWith( - decoration: textFieldConfiguration.decoration - .copyWith(errorText: state.errorText), - onChanged: (text) { - state.didChange(text); - textFieldConfiguration.onChanged?.call(text); - }, - controller: state._effectiveController, - ), - suggestionsBoxVerticalOffset: suggestionsBoxVerticalOffset, - onSuggestionSelected: onSuggestionSelected, - itemBuilder: itemBuilder, - suggestionsCallback: suggestionsCallback, - animationStart: animationStart, - animationDuration: animationDuration, - direction: direction, - hideOnLoading: hideOnLoading, - hideOnEmpty: hideOnEmpty, - hideOnError: hideOnError, - hideSuggestionsOnKeyboardHide: hideSuggestionsOnKeyboardHide, - keepSuggestionsOnLoading: keepSuggestionsOnLoading, - keepSuggestionsOnSuggestionSelected: - keepSuggestionsOnSuggestionSelected, - autoFlipDirection: autoFlipDirection, - hideKeyboard: hideKeyboard, - minCharsForSuggestions: minCharsForSuggestions, - listActionButton: listActionButton, - actionButtonBuilder: actionButtonBuilder, - buttonActionCallback: buttonActionCallback, - buttonShowAllResult: buttonShowAllResult, - titleHeaderRecent: titleHeaderRecent, - itemRecentBuilder: itemRecentBuilder, - fetchRecentActionCallback: fetchRecentActionCallback, - onRecentSelected: onRecentSelected, - listActionPadding: listActionPadding, - hideSuggestionsBox: hideSuggestionsBox, - ); - }); - - @override - _TypeAheadFormFieldState createState() => _TypeAheadFormFieldState(); -} - -class _TypeAheadFormFieldState extends FormFieldState { - TextEditingController? _controller; - - TextEditingController? get _effectiveController => - widget.textFieldConfiguration.controller ?? _controller; - - @override - TypeAheadFormFieldQuickSearch get widget => super.widget as TypeAheadFormFieldQuickSearch; - - @override - void initState() { - super.initState(); - if (widget.textFieldConfiguration.controller == null) { - _controller = TextEditingController(text: widget.initialValue); - } else { - widget.textFieldConfiguration.controller! - .addListener(_handleControllerChanged); - } - } - - @override - void didUpdateWidget(TypeAheadFormFieldQuickSearch oldWidget) { - super.didUpdateWidget(oldWidget); - if (widget.textFieldConfiguration.controller != - oldWidget.textFieldConfiguration.controller) { - oldWidget.textFieldConfiguration.controller - ?.removeListener(_handleControllerChanged); - widget.textFieldConfiguration.controller - ?.addListener(_handleControllerChanged); - - if (oldWidget.textFieldConfiguration.controller != null && - widget.textFieldConfiguration.controller == null) - _controller = TextEditingController.fromValue( - oldWidget.textFieldConfiguration.controller!.value); - if (widget.textFieldConfiguration.controller != null) { - setValue(widget.textFieldConfiguration.controller!.text); - if (oldWidget.textFieldConfiguration.controller == null) - _controller = null; - } - } - } - - @override - void dispose() { - widget.textFieldConfiguration.controller - ?.removeListener(_handleControllerChanged); - super.dispose(); - } - - @override - void reset() { - super.reset(); - setState(() { - _effectiveController!.text = widget.initialValue!; - }); - } - - void _handleControllerChanged() { - // Suppress changes that originated from within this class. - // - // In the case where a controller has been passed in to this widget, we - // register this change listener. In these cases, we'll also receive change - // notifications for changes originating from within this class -- for - // example, the reset() method. In such cases, the FormField value will - // already have been set. - if (_effectiveController!.text != value) - didChange(_effectiveController!.text); - } -} - -/// A [TextField](https://docs.flutter.io/flutter/material/TextField-class.html) -/// that displays a list of suggestions as the user types -/// -/// See also: -/// -/// * [TypeAheadFormFieldQuickSearch], a [FormField](https://docs.flutter.io/flutter/widgets/FormField-class.html) -/// implementation of [TypeAheadFieldQuickSearch] that allows the value to be saved, -/// validated, etc. -class TypeAheadFieldQuickSearch extends StatefulWidget { - /// Called with the search pattern to get the search suggestions. - /// - /// This callback must not be null. It is be called by the TypeAhead widget - /// and provided with the search pattern. It should return a [List](https://api.dartlang.org/stable/2.0.0/dart-core/List-class.html) - /// of suggestions either synchronously, or asynchronously (as the result of a - /// [Future](https://api.dartlang.org/stable/dart-async/Future-class.html)). - /// Typically, the list of suggestions should not contain more than 4 or 5 - /// entries. These entries will then be provided to [itemBuilder] to display - /// the suggestions. - /// - /// Example: - /// ```dart - /// suggestionsCallback: (pattern) async { - /// return await _getSuggestions(pattern); - /// } - /// ``` - final SuggestionsCallback suggestionsCallback; - - /// Called when a suggestion is tapped. - /// - /// This callback must not be null. It is called by the TypeAhead widget and - /// provided with the value of the tapped suggestion. - /// - /// For example, you might want to navigate to a specific view when the user - /// tabs a suggestion: - /// ```dart - /// onSuggestionSelected: (suggestion) { - /// Navigator.of(context).push(MaterialPageRoute( - /// builder: (context) => SearchResult( - /// searchItem: suggestion - /// ) - /// )); - /// } - /// ``` - /// - /// Or to set the value of the text field: - /// ```dart - /// onSuggestionSelected: (suggestion) { - /// _controller.text = suggestion['name']; - /// } - /// ``` - final SuggestionSelectionCallback onSuggestionSelected; - - /// Called for each suggestion returned by [suggestionsCallback] to build the - /// corresponding widget. - /// - /// This callback must not be null. It is called by the TypeAhead widget for - /// each suggestion, and expected to build a widget to display this - /// suggestion's info. For example: - /// - /// ```dart - /// itemBuilder: (context, suggestion) { - /// return ListTile( - /// title: Text(suggestion['name']), - /// subtitle: Text('USD' + suggestion['price'].toString()) - /// ); - /// } - /// ``` - final ItemBuilder itemBuilder; - - /// used to control the scroll behavior of item-builder list - final ScrollController? scrollController; - - /// The decoration of the material sheet that contains the suggestions. - /// - /// If null, default decoration with an elevation of 4.0 is used - /// - - final QuickSearchSuggestionsBoxDecoration suggestionsBoxDecoration; - - /// Used to control the `_SuggestionsBox`. Allows manual control to - /// open, close, toggle, or resize the `_SuggestionsBox`. - final QuickSearchSuggestionsBoxController? suggestionsBoxController; - - /// The duration to wait after the user stops typing before calling - /// [suggestionsCallback] - /// - /// This is useful, because, if not set, a request for suggestions will be - /// sent for every character that the user types. - /// - /// This duration is set by default to 300 milliseconds - final Duration debounceDuration; - - /// Called when waiting for [suggestionsCallback] to return. - /// - /// It is expected to return a widget to display while waiting. - /// For example: - /// ```dart - /// (BuildContext context) { - /// return Text('Loading...'); - /// } - /// ``` - /// - /// If not specified, a [CircularProgressIndicator](https://docs.flutter.io/flutter/material/CircularProgressIndicator-class.html) is shown - final WidgetBuilder? loadingBuilder; - - /// Called when [suggestionsCallback] returns an empty array. - /// - /// It is expected to return a widget to display when no suggestions are - /// avaiable. - /// For example: - /// ```dart - /// (BuildContext context) { - /// return Text('No Items Found!'); - /// } - /// ``` - /// - /// If not specified, a simple text is shown - final WidgetBuilder? noItemsFoundBuilder; - - /// Called when [suggestionsCallback] throws an exception. - /// - /// It is called with the error object, and expected to return a widget to - /// display when an exception is thrown - /// For example: - /// ```dart - /// (BuildContext context, error) { - /// return Text('$error'); - /// } - /// ``` - /// - /// If not specified, the error is shown in [ThemeData.errorColor](https://docs.flutter.io/flutter/material/ThemeData/errorColor.html) - final ErrorBuilder? errorBuilder; - - /// Called to display animations when [suggestionsCallback] returns suggestions - /// - /// It is provided with the suggestions box instance and the animation - /// controller, and expected to return some animation that uses the controller - /// to display the suggestion box. - /// - /// For example: - /// ```dart - /// transitionBuilder: (context, suggestionsBox, animationController) { - /// return FadeTransition( - /// child: suggestionsBox, - /// opacity: CurvedAnimation( - /// parent: animationController, - /// curve: Curves.fastOutSlowIn - /// ), - /// ); - /// } - /// ``` - /// This argument is best used with [animationDuration] and [animationStart] - /// to fully control the animation. - /// - /// To fully remove the animation, just return `suggestionsBox` - /// - /// If not specified, a [SizeTransition](https://docs.flutter.io/flutter/widgets/SizeTransition-class.html) is shown. - final AnimationTransitionBuilder? transitionBuilder; - - /// The duration that [transitionBuilder] animation takes. - /// - /// This argument is best used with [transitionBuilder] and [animationStart] - /// to fully control the animation. - /// - /// Defaults to 500 milliseconds. - final Duration animationDuration; - - /// Determine the [SuggestionBox]'s direction. - /// - /// If [AxisDirection.down], the [SuggestionBox] will be below the [TextField] - /// and the [_SuggestionsList] will grow **down**. - /// - /// If [AxisDirection.up], the [SuggestionBox] will be above the [TextField] - /// and the [_SuggestionsList] will grow **up**. - /// - /// [AxisDirection.left] and [AxisDirection.right] are not allowed. - final AxisDirection direction; - - /// The value at which the [transitionBuilder] animation starts. - /// - /// This argument is best used with [transitionBuilder] and [animationDuration] - /// to fully control the animation. - /// - /// Defaults to 0.25. - final double animationStart; - - /// The configuration of the [TextField](https://docs.flutter.io/flutter/material/TextField-class.html) - /// that the TypeAhead widget displays - final QuickSearchTextFieldConfiguration textFieldConfiguration; - - /// How far below the text field should the suggestions box be - /// - /// Defaults to 5.0 - final double suggestionsBoxVerticalOffset; - - /// If set to true, suggestions will be fetched immediately when the field is - /// added to the view. - /// - /// But the suggestions box will only be shown when the field receives focus. - /// To make the field receive focus immediately, you can set the `autofocus` - /// property in the [textFieldConfiguration] to true - /// - /// Defaults to false - final bool getImmediateSuggestions; - - /// If set to true, no loading box will be shown while suggestions are - /// being fetched. [loadingBuilder] will also be ignored. - /// - /// Defaults to false. - final bool hideOnLoading; - - /// If set to true, nothing will be shown if there are no results. - /// [noItemsFoundBuilder] will also be ignored. - /// - /// Defaults to false. - final bool hideOnEmpty; - - /// If set to true, nothing will be shown if there is an error. - /// [errorBuilder] will also be ignored. - /// - /// Defaults to false. - final bool hideOnError; - - /// If set to false, the suggestions box will stay opened after - /// the keyboard is closed. - /// - /// Defaults to true. - final bool hideSuggestionsOnKeyboardHide; - - /// If set to false, the suggestions box will show a circular - /// progress indicator when retrieving suggestions. - /// - /// Defaults to true. - final bool keepSuggestionsOnLoading; - - /// If set to true, the suggestions box will remain opened even after - /// selecting a suggestion. - /// - /// Note that if this is enabled, the only way - /// to close the suggestions box is either manually via the - /// `QuickSearchSuggestionsBoxController` or when the user closes the software - /// keyboard if `hideSuggestionsOnKeyboardHide` is set to true. Users - /// with a physical keyboard will be unable to close the - /// box without a manual way via `QuickSearchSuggestionsBoxController`. - /// - /// Defaults to false. - final bool keepSuggestionsOnSuggestionSelected; - - /// If set to true, in the case where the suggestions box has less than - /// _SuggestionsBoxController.minOverlaySpace to grow in the desired [direction], the direction axis - /// will be temporarily flipped if there's more room available in the opposite - /// direction. - /// - /// Defaults to false - final bool autoFlipDirection; - final bool hideKeyboard; - - /// The minimum number of characters which must be entered before - /// [suggestionsCallback] is triggered. - /// - /// Defaults to 0. - final int minCharsForSuggestions; - - /// List button action for suggestion box - final List? listActionButton; - final ButtonActionBuilder? actionButtonBuilder; - final ButtonActionCallback? buttonActionCallback; - - /// Button show all result - final ButtonActionBuilder? buttonShowAllResult; - - /// Title header recent suggestion box - final Widget? titleHeaderRecent; - /// Widget item recent - final ItemBuilder? itemRecentBuilder; - /// Get all recent callback - final FetchRecentActionCallback? fetchRecentActionCallback; - /// On listen select recent - final RecentSelectionCallback? onRecentSelected; - /// Padding button action - final EdgeInsets? listActionPadding; - final bool hideSuggestionsBox; - - /// Creates a [TypeAheadFieldQuickSearch] - TypeAheadFieldQuickSearch( - {Key? key, - required this.suggestionsCallback, - required this.itemBuilder, - required this.onSuggestionSelected, - this.textFieldConfiguration: const QuickSearchTextFieldConfiguration(), - this.suggestionsBoxDecoration: const QuickSearchSuggestionsBoxDecoration(), - this.debounceDuration: const Duration(milliseconds: 300), - this.suggestionsBoxController, - this.scrollController, - this.loadingBuilder, - this.noItemsFoundBuilder, - this.errorBuilder, - this.transitionBuilder, - this.animationStart: 0.25, - this.animationDuration: const Duration(milliseconds: 500), - this.getImmediateSuggestions: false, - this.suggestionsBoxVerticalOffset: 5.0, - this.direction: AxisDirection.down, - this.hideOnLoading: false, - this.hideOnEmpty: false, - this.hideOnError: false, - this.hideSuggestionsOnKeyboardHide: true, - this.keepSuggestionsOnLoading: true, - this.keepSuggestionsOnSuggestionSelected: false, - this.autoFlipDirection: false, - this.hideKeyboard: false, - this.minCharsForSuggestions: 0, - this.listActionButton, - this.actionButtonBuilder, - this.buttonActionCallback, - this.buttonShowAllResult, - this.titleHeaderRecent, - this.itemRecentBuilder, - this.fetchRecentActionCallback, - this.onRecentSelected, - this.listActionPadding, - this.hideSuggestionsBox = false, - }) : assert(animationStart >= 0.0 && animationStart <= 1.0), - assert( - direction == AxisDirection.down || direction == AxisDirection.up), - assert(minCharsForSuggestions >= 0), - super(key: key); - - @override - _TypeAheadFieldQuickSearchState createState() => _TypeAheadFieldQuickSearchState(); -} - -class _TypeAheadFieldQuickSearchState extends State> - with WidgetsBindingObserver { - FocusNode? _focusNode; - TextEditingController? _textEditingController; - _SuggestionsBox? _suggestionsBox; - - TextEditingController? get _effectiveController => - widget.textFieldConfiguration.controller ?? _textEditingController; - FocusNode? get _effectiveFocusNode => - widget.textFieldConfiguration.focusNode ?? _focusNode; - late VoidCallback _focusNodeListener; - - final LayerLink _layerLink = LayerLink(); - - // Timer that resizes the suggestion box on each tick. Only active when the user is scrolling. - Timer? _resizeOnScrollTimer; - // The rate at which the suggestion box will resize when the user is scrolling - final Duration _resizeOnScrollRefreshRate = const Duration(milliseconds: 500); - // Will have a value if the typeahead is inside a scrollable widget - ScrollPosition? _scrollPosition; - - // Keyboard detection - final Stream? _keyboardVisibility = - (supportedPlatform) ? KeyboardVisibilityController().onChange : null; - late StreamSubscription? _keyboardVisibilitySubscription; - - @override - void didChangeMetrics() { - // Catch keyboard event and orientation change; resize suggestions list - this._suggestionsBox!.onChangeMetrics(); - } - - @override - void dispose() { - this._suggestionsBox!.close(); - this._suggestionsBox!.widgetMounted = false; - WidgetsBinding.instance?.removeObserver(this); - _keyboardVisibilitySubscription?.cancel(); - _effectiveFocusNode!.removeListener(_focusNodeListener); - _focusNode?.dispose(); - _resizeOnScrollTimer?.cancel(); - _scrollPosition?.removeListener(_scrollResizeListener); - _textEditingController?.dispose(); - super.dispose(); - } - - @override - void initState() { - super.initState(); - WidgetsBinding.instance?.addObserver(this); - - if (widget.textFieldConfiguration.controller == null) { - this._textEditingController = TextEditingController(); - } - - if (widget.textFieldConfiguration.focusNode == null) { - this._focusNode = FocusNode(); - } - - this._suggestionsBox = _SuggestionsBox( - context, - widget.direction, - widget.autoFlipDirection, - widget.hideSuggestionsBox); - widget.suggestionsBoxController?._suggestionsBox = this._suggestionsBox; - widget.suggestionsBoxController?._effectiveFocusNode = - this._effectiveFocusNode; - - this._focusNodeListener = () { - if (_effectiveFocusNode!.hasFocus) { - this._suggestionsBox!.open(); - } else { - if (widget.hideSuggestionsOnKeyboardHide){ - this._suggestionsBox!.close(); - } - } - }; - - this._effectiveFocusNode!.addListener(_focusNodeListener); - - // hide suggestions box on keyboard closed - this._keyboardVisibilitySubscription = - _keyboardVisibility?.listen((bool isVisible) { - if (widget.hideSuggestionsOnKeyboardHide && !isVisible) { - _effectiveFocusNode!.unfocus(); - } - }); - - WidgetsBinding.instance?.addPostFrameCallback((duration) { - if (mounted) { - this._initOverlayEntry(); - // calculate initial suggestions list size - this._suggestionsBox!.resize(); - - // in case we already missed the focus event - if (this._effectiveFocusNode!.hasFocus) { - this._suggestionsBox!.open(); - } - } - }); - } - - @override - void didChangeDependencies() { - super.didChangeDependencies(); - ScrollableState? scrollableState = Scrollable.of(context); - if (scrollableState != null) { - // The TypeAheadFieldQuickSearch is inside a scrollable widget - _scrollPosition = scrollableState.position; - - _scrollPosition!.removeListener(_scrollResizeListener); - _scrollPosition!.isScrollingNotifier.addListener(_scrollResizeListener); - } - } - - void _scrollResizeListener() { - bool isScrolling = _scrollPosition!.isScrollingNotifier.value; - _resizeOnScrollTimer?.cancel(); - if (isScrolling) { - // Scroll started - _resizeOnScrollTimer = - Timer.periodic(_resizeOnScrollRefreshRate, (timer) { - _suggestionsBox!.resize(); - }); - } else { - // Scroll finished - _suggestionsBox!.resize(); - } - } - - void _initOverlayEntry() { - this._suggestionsBox!._overlayEntry = OverlayEntry(builder: (context) { - final suggestionsList = _SuggestionsList( - suggestionsBox: _suggestionsBox, - decoration: widget.suggestionsBoxDecoration, - debounceDuration: widget.debounceDuration, - controller: this._effectiveController, - loadingBuilder: widget.loadingBuilder, - scrollController: widget.scrollController, - noItemsFoundBuilder: widget.noItemsFoundBuilder, - errorBuilder: widget.errorBuilder, - transitionBuilder: widget.transitionBuilder, - suggestionsCallback: widget.suggestionsCallback, - animationDuration: widget.animationDuration, - animationStart: widget.animationStart, - getImmediateSuggestions: widget.getImmediateSuggestions, - onSuggestionSelected: (T selection) { - if (!widget.keepSuggestionsOnSuggestionSelected) { - this._effectiveFocusNode!.unfocus(); - this._suggestionsBox!.close(); - } - widget.onSuggestionSelected(selection); - }, - itemBuilder: widget.itemBuilder, - direction: _suggestionsBox!.direction, - hideOnLoading: widget.hideOnLoading, - hideOnEmpty: widget.hideOnEmpty, - hideOnError: widget.hideOnError, - keepSuggestionsOnLoading: widget.keepSuggestionsOnLoading, - minCharsForSuggestions: widget.minCharsForSuggestions, - listActionButton: widget.listActionButton, - actionButtonBuilder: widget.actionButtonBuilder, - buttonActionCallback: widget.buttonActionCallback, - buttonShowAllResult: widget.buttonShowAllResult, - titleHeaderRecent: widget.titleHeaderRecent, - itemRecentBuilder: widget.itemRecentBuilder, - fetchRecentActionCallback: widget.fetchRecentActionCallback, - onRecentSelected: (R selection) { - if (!widget.keepSuggestionsOnSuggestionSelected) { - this._effectiveFocusNode!.unfocus(); - this._suggestionsBox!.close(); - } - if (widget.onRecentSelected != null) { - widget.onRecentSelected!(selection); - } - }, - listActionPadding: widget.listActionPadding, - hideSuggestionsBox: widget.hideSuggestionsBox, - ); - - double w = _suggestionsBox!.textBoxWidth; - if (widget.suggestionsBoxDecoration.constraints != null) { - if (widget.suggestionsBoxDecoration.constraints!.minWidth != 0.0 && - widget.suggestionsBoxDecoration.constraints!.maxWidth != - double.infinity) { - w = (widget.suggestionsBoxDecoration.constraints!.minWidth + - widget.suggestionsBoxDecoration.constraints!.maxWidth) / - 2; - } else if (widget.suggestionsBoxDecoration.constraints!.minWidth != - 0.0 && - widget.suggestionsBoxDecoration.constraints!.minWidth > w) { - w = widget.suggestionsBoxDecoration.constraints!.minWidth; - } else if (widget.suggestionsBoxDecoration.constraints!.maxWidth != - double.infinity && - widget.suggestionsBoxDecoration.constraints!.maxWidth < w) { - w = widget.suggestionsBoxDecoration.constraints!.maxWidth; - } - } - - return Positioned( - width: w, - child: CompositedTransformFollower( - link: this._layerLink, - showWhenUnlinked: false, - offset: Offset( - widget.suggestionsBoxDecoration.offsetX, - _suggestionsBox!.direction == AxisDirection.down - ? _suggestionsBox!.textBoxHeight + - widget.suggestionsBoxVerticalOffset - : _suggestionsBox!.directionUpOffset), - child: _suggestionsBox!.direction == AxisDirection.down - ? suggestionsList - : FractionalTranslation( - translation: - Offset(0.0, -1.0), // visually flips list to go up - child: suggestionsList, - ), - ), - ); - }); - } - - @override - Widget build(BuildContext context) { - return CompositedTransformTarget( - link: this._layerLink, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - if (widget.textFieldConfiguration.leftButton != null) - widget.textFieldConfiguration.leftButton!, - Expanded( - child: TextField( - focusNode: this._effectiveFocusNode, - controller: this._effectiveController, - decoration: widget.textFieldConfiguration.decoration, - style: widget.textFieldConfiguration.style, - textAlign: widget.textFieldConfiguration.textAlign, - enabled: widget.textFieldConfiguration.enabled, - keyboardType: widget.textFieldConfiguration.keyboardType, - autofocus: widget.textFieldConfiguration.autofocus, - inputFormatters: widget.textFieldConfiguration.inputFormatters, - autocorrect: widget.textFieldConfiguration.autocorrect, - maxLines: widget.textFieldConfiguration.maxLines, - textAlignVertical: widget.textFieldConfiguration.textAlignVertical, - minLines: widget.textFieldConfiguration.minLines, - maxLength: widget.textFieldConfiguration.maxLength, - maxLengthEnforcement: widget.textFieldConfiguration.maxLengthEnforcement, - obscureText: widget.textFieldConfiguration.obscureText, - onChanged: widget.textFieldConfiguration.onChanged, - onSubmitted: widget.textFieldConfiguration.onSubmitted, - onEditingComplete: widget.textFieldConfiguration.onEditingComplete, - onTap: widget.textFieldConfiguration.onTap, - scrollPadding: widget.textFieldConfiguration.scrollPadding, - textInputAction: widget.textFieldConfiguration.textInputAction, - textCapitalization: widget.textFieldConfiguration.textCapitalization, - keyboardAppearance: widget.textFieldConfiguration.keyboardAppearance, - cursorWidth: widget.textFieldConfiguration.cursorWidth, - cursorRadius: widget.textFieldConfiguration.cursorRadius, - cursorColor: widget.textFieldConfiguration.cursorColor, - textDirection: widget.textFieldConfiguration.textDirection, - enableInteractiveSelection: - widget.textFieldConfiguration.enableInteractiveSelection, - readOnly: widget.hideKeyboard, - ), - ), - if (widget.textFieldConfiguration.clearTextButton != null - && this._effectiveController?.text.isNotEmpty == true) - widget.textFieldConfiguration.clearTextButton!, - if (widget.textFieldConfiguration.rightButton != null) - widget.textFieldConfiguration.rightButton!, - ], - ), - ); - } -} - -class _SuggestionsList extends StatefulWidget { - final _SuggestionsBox? suggestionsBox; - final TextEditingController? controller; - final bool getImmediateSuggestions; - final SuggestionSelectionCallback? onSuggestionSelected; - final SuggestionsCallback? suggestionsCallback; - final ItemBuilder? itemBuilder; - final ScrollController? scrollController; - final QuickSearchSuggestionsBoxDecoration? decoration; - final Duration? debounceDuration; - final WidgetBuilder? loadingBuilder; - final WidgetBuilder? noItemsFoundBuilder; - final ErrorBuilder? errorBuilder; - final AnimationTransitionBuilder? transitionBuilder; - final Duration? animationDuration; - final double? animationStart; - final AxisDirection? direction; - final bool? hideOnLoading; - final bool? hideOnEmpty; - final bool? hideOnError; - final bool? keepSuggestionsOnLoading; - final int? minCharsForSuggestions; - final List? listActionButton; - final ButtonActionBuilder? actionButtonBuilder; - final ButtonActionCallback? buttonActionCallback; - final ButtonActionBuilder? buttonShowAllResult; - final Widget? titleHeaderRecent; - final ItemBuilder? itemRecentBuilder; - final FetchRecentActionCallback? fetchRecentActionCallback; - final RecentSelectionCallback? onRecentSelected; - final EdgeInsets? listActionPadding; - final bool hideSuggestionsBox; - - _SuggestionsList({ - required this.suggestionsBox, - this.controller, - this.getImmediateSuggestions: false, - this.onSuggestionSelected, - this.suggestionsCallback, - this.itemBuilder, - this.scrollController, - this.decoration, - this.debounceDuration, - this.loadingBuilder, - this.noItemsFoundBuilder, - this.errorBuilder, - this.transitionBuilder, - this.animationDuration, - this.animationStart, - this.direction, - this.hideOnLoading, - this.hideOnEmpty, - this.hideOnError, - this.keepSuggestionsOnLoading, - this.minCharsForSuggestions, - this.listActionButton, - this.actionButtonBuilder, - this.buttonActionCallback, - this.buttonShowAllResult, - this.titleHeaderRecent, - this.itemRecentBuilder, - this.fetchRecentActionCallback, - this.onRecentSelected, - this.listActionPadding, - this.hideSuggestionsBox: false, - }); - - @override - _SuggestionsListState createState() => _SuggestionsListState(); -} - -class _SuggestionsListState extends State<_SuggestionsList> - with SingleTickerProviderStateMixin { - Iterable? _suggestions; - Iterable? _recentItems; - late bool _suggestionsValid; - late VoidCallback _controllerListener; - Timer? _debounceTimer; - bool? _isLoading, _isQueued; - AnimationController? _animationController; - String? _lastTextValue; - late final ScrollController _scrollController = - widget.scrollController ?? ScrollController(); - - _SuggestionsListState() { - this._controllerListener = () async { - // If we came here because of a change in selected text, not because of - // actual change in text - if (widget.controller!.text == this._lastTextValue) return; - - this._lastTextValue = widget.controller!.text; - - this._debounceTimer?.cancel(); - if (widget.controller!.text.length <= widget.minCharsForSuggestions!) { - if (mounted) { - Iterable? recentItems; - try { - if (widget.fetchRecentActionCallback != null) { - recentItems = await widget.fetchRecentActionCallback!(widget.controller!.text); - } - } catch (e) {} - - setState(() { - _isLoading = false; - _suggestions = null; - _recentItems = recentItems; - _suggestionsValid = true; - }); - } - return; - } else { - this._debounceTimer = Timer(widget.debounceDuration!, () async { - if (this._debounceTimer!.isActive) return; - if (_isLoading!) { - _isQueued = true; - return; - } - - await this.invalidateSuggestions(); - while (_isQueued!) { - _isQueued = false; - await this.invalidateSuggestions(); - } - }); - } - }; - } - - @override - void didUpdateWidget(_SuggestionsList oldWidget) { - super.didUpdateWidget(oldWidget); - widget.controller!.addListener(this._controllerListener); - _getSuggestions(); - } - - @override - void didChangeDependencies() { - super.didChangeDependencies(); - _getSuggestions(); - } - - @override - void initState() { - super.initState(); - - this._animationController = AnimationController( - vsync: this, - duration: widget.animationDuration, - ); - - this._suggestionsValid = widget.minCharsForSuggestions! > 0 ? true : false; - this._isLoading = false; - this._isQueued = false; - this._lastTextValue = widget.controller!.text; - - if (widget.getImmediateSuggestions) { - this._getSuggestions(); - } - - widget.controller!.addListener(this._controllerListener); - } - - Future invalidateSuggestions() async { - if (widget.hideSuggestionsBox) return; - _suggestionsValid = false; - await _getSuggestions(); - } - - Future _getSuggestions() async { - if (_suggestionsValid || widget.hideSuggestionsBox) return; - _suggestionsValid = true; - - if (mounted) { - setState(() { - this._animationController!.forward(from: 1.0); - - this._isLoading = true; - }); - - Iterable? suggestions; - Iterable? recentItems; - Object? error; - - try { - suggestions = await widget.suggestionsCallback!(widget.controller!.text); - if (widget.fetchRecentActionCallback != null) { - recentItems = await widget.fetchRecentActionCallback!(widget.controller!.text); - } - } catch (e) { - error = e; - } - - if (this.mounted) { - // if it wasn't removed in the meantime - setState(() { - double? animationStart = widget.animationStart; - // allow suggestionsCallback to return null and not throw error here - if (error != null || suggestions?.isEmpty == true) { - animationStart = 1.0; - } - this._animationController!.forward(from: animationStart); - - this._isLoading = false; - this._suggestions = suggestions; - this._recentItems = recentItems; - }); - } - } - } - - @override - void dispose() { - _animationController!.dispose(); - _debounceTimer?.cancel(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - if (widget.hideSuggestionsBox) { - return SizedBox.shrink(); - } - - Widget child; - - if (this._suggestions?.isNotEmpty == true && widget.controller?.text.isNotEmpty == true) { - child = createSuggestionsWidget(); - } else { - child = createRecentWidget(); - } - - final animationChild = widget.transitionBuilder != null - ? widget.transitionBuilder!(context, child, this._animationController) - : SizeTransition( - axisAlignment: -1.0, - sizeFactor: CurvedAnimation( - parent: this._animationController!, - curve: Curves.fastOutSlowIn), - child: child, - ); - - BoxConstraints constraints; - if (widget.decoration!.constraints == null) { - constraints = BoxConstraints( - maxHeight: widget.suggestionsBox!.maxHeight, - ); - } else { - double maxHeight = min(widget.decoration!.constraints!.maxHeight, - widget.suggestionsBox!.maxHeight); - constraints = widget.decoration!.constraints!.copyWith( - minHeight: min(widget.decoration!.constraints!.minHeight, maxHeight), - maxHeight: maxHeight, - ); - } - - var container = Material( - elevation: widget.decoration!.elevation, - color: widget.decoration!.color, - shape: widget.decoration!.shape, - borderRadius: widget.decoration!.borderRadius, - shadowColor: widget.decoration!.shadowColor, - clipBehavior: widget.decoration!.clipBehavior, - child: ConstrainedBox( - constraints: constraints, - child: animationChild, - ), - ); - - return PointerInterceptor(child: container); - } - - Widget createSuggestionsWidget() { - final listItemSuggestionWidget = this._suggestions?.map((T suggestion) { - if ( widget.itemBuilder != null) { - return InkWell( - child: widget.itemBuilder!(context, suggestion), - onTap: () { - widget.onSuggestionSelected!(suggestion); - }, - ); - } else { - return SizedBox.shrink(); - } - }).toList() ?? []; - - final loadingWidget = widget.loadingBuilder != null - ? widget.loadingBuilder!(context) - : Align( - alignment: Alignment.center, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0), - child: CircularProgressIndicator(), - ), - ); - - final listAction = Row( - mainAxisSize: MainAxisSize.min, - children: widget.listActionButton!.map((dynamic action) { - if (widget.actionButtonBuilder != null) { - return Padding( - padding: const EdgeInsets.only(right: 8), - child: InkWell( - child: widget.actionButtonBuilder!(context, action), - borderRadius: BorderRadius.all(Radius.circular(10)), - onTap: () { - if (widget.buttonActionCallback != null) { - widget.buttonActionCallback!(action); - this.invalidateSuggestions(); - } - }, - ), - ); - } else { - return SizedBox.shrink(); - } - }).toList()); - - Widget child = ListView( - padding: EdgeInsets.zero, - primary: false, - shrinkWrap: true, - controller: _scrollController, - reverse: widget.suggestionsBox!.direction == AxisDirection.down ? false : true, // reverses the list to start at the bottom - children: [ - if (widget.listActionButton != null && widget.listActionButton?.isNotEmpty == true) - Padding( - padding: widget.listActionPadding ?? EdgeInsets.zero, - child: listAction, - ), - if (this._isLoading == true && widget.hideOnLoading == false && widget.keepSuggestionsOnLoading == false) - loadingWidget, - if (widget.buttonShowAllResult != null && widget.controller?.text.isNotEmpty == true) - widget.buttonShowAllResult!(context, widget.controller?.text), - if (listItemSuggestionWidget.isNotEmpty) - ... [ - ... listItemSuggestionWidget, - SizedBox(height: 16) - ], - ], - ); - - if (widget.decoration!.hasScrollbar) { - child = Scrollbar( - controller: _scrollController, - child: child, - ); - } - - return child; - } - - Widget createRecentWidget() { - final listItemRecent = this._recentItems?.map((R recent) { - if (widget.itemRecentBuilder != null) { - return InkWell( - child: widget.itemRecentBuilder!(context, recent), - onTap: () { - if (widget.onRecentSelected != null) { - widget.onRecentSelected!(recent); - } - }, - ); - } else { - return SizedBox.shrink(); - } - }).toList() ?? []; - - final loadingWidget = widget.loadingBuilder != null - ? widget.loadingBuilder!(context) - : Align( - alignment: Alignment.center, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0), - child: CircularProgressIndicator(), - ), - ); - - final listAction = Row( - mainAxisSize: MainAxisSize.min, - children: widget.listActionButton!.map((dynamic action) { - if (widget.actionButtonBuilder != null) { - return Padding( - padding: const EdgeInsets.only(right: 8), - child: InkWell( - child: widget.actionButtonBuilder!(context, action), - borderRadius: BorderRadius.all(Radius.circular(10)), - onTap: () { - if (widget.buttonActionCallback != null) { - widget.buttonActionCallback!(action); - this.invalidateSuggestions(); - } - }, - ), - ); - } else { - return SizedBox.shrink(); - } - }).toList()); - - Widget child = ListView( - padding: EdgeInsets.zero, - primary: false, - shrinkWrap: true, - controller: _scrollController, - reverse: widget.suggestionsBox!.direction == AxisDirection.down ? false : true, // reverses the list to start at the bottom - children: [ - if (widget.listActionButton != null && widget.listActionButton?.isNotEmpty == true) - Padding( - padding: widget.listActionPadding ?? EdgeInsets.zero, - child: listAction, - ), - if (this._isLoading == true && widget.hideOnLoading == false && widget.keepSuggestionsOnLoading == false) - loadingWidget, - if (widget.buttonShowAllResult != null && widget.controller?.text.isNotEmpty == true) - widget.buttonShowAllResult!(context, widget.controller?.text), - if (_recentItems?.isNotEmpty == true && widget.itemRecentBuilder != null && widget.titleHeaderRecent != null) - widget.titleHeaderRecent!, - if (listItemRecent.isNotEmpty) - ... [ - ... listItemRecent, - SizedBox(height: 16) - ], - ], - ); - - if (widget.decoration!.hasScrollbar) { - child = Scrollbar( - controller: _scrollController, - child: child, - ); - } - - return child; - } -} - -/// Supply an instance of this class to the [TypeAhead.suggestionsBoxDecoration] -/// property to configure the suggestions box decoration -class QuickSearchSuggestionsBoxDecoration { - /// The z-coordinate at which to place the suggestions box. This controls the size - /// of the shadow below the box. - /// - /// Same as [Material.elevation](https://docs.flutter.io/flutter/material/Material/elevation.html) - final double elevation; - - /// The color to paint the suggestions box. - /// - /// Same as [Material.color](https://docs.flutter.io/flutter/material/Material/color.html) - final Color? color; - - /// Defines the material's shape as well its shadow. - /// - /// Same as [Material.shape](https://docs.flutter.io/flutter/material/Material/shape.html) - final ShapeBorder? shape; - - /// Defines if a scrollbar will be displayed or not. - final bool hasScrollbar; - - /// If non-null, the corners of this box are rounded by this [BorderRadius](https://docs.flutter.io/flutter/painting/BorderRadius-class.html). - /// - /// Same as [Material.borderRadius](https://docs.flutter.io/flutter/material/Material/borderRadius.html) - final BorderRadius? borderRadius; - - /// The color to paint the shadow below the material. - /// - /// Same as [Material.shadowColor](https://docs.flutter.io/flutter/material/Material/shadowColor.html) - final Color shadowColor; - - /// The constraints to be applied to the suggestions box - final BoxConstraints? constraints; - - /// Adds an offset to the suggestions box - final double offsetX; - - /// The content will be clipped (or not) according to this option. - /// - /// Same as [Material.clipBehavior](https://api.flutter.dev/flutter/material/Material/clipBehavior.html) - final Clip clipBehavior; - - /// Creates a QuickSearchSuggestionsBoxDecoration - const QuickSearchSuggestionsBoxDecoration( - {this.elevation: 4.0, - this.color, - this.shape, - this.hasScrollbar: true, - this.borderRadius, - this.shadowColor: const Color(0xFF000000), - this.constraints, - this.clipBehavior: Clip.none, - this.offsetX: 0.0}); -} - -/// Supply an instance of this class to the [TypeAhead.textFieldConfiguration] -/// property to configure the displayed text field -class QuickSearchTextFieldConfiguration { - /// The decoration to show around the text field. - /// - /// Same as [TextField.decoration](https://docs.flutter.io/flutter/material/TextField/decoration.html) - final InputDecoration decoration; - - /// Controls the text being edited. - /// - /// If null, this widget will create its own [TextEditingController](https://docs.flutter.io/flutter/widgets/TextEditingController-class.html). - /// A typical use case for this field in the TypeAhead widget is to set the - /// text of the widget when a suggestion is selected. For example: - /// - /// ```dart - /// final _controller = TextEditingController(); - /// ... - /// ... - /// TypeAheadFieldQuickSearch( - /// controller: _controller, - /// ... - /// ... - /// onSuggestionSelected: (suggestion) { - /// _controller.text = suggestion['city_name']; - /// } - /// ) - /// ``` - final TextEditingController? controller; - - /// Controls whether this widget has keyboard focus. - /// - /// Same as [TextField.focusNode](https://docs.flutter.io/flutter/material/TextField/focusNode.html) - final FocusNode? focusNode; - - /// The style to use for the text being edited. - /// - /// Same as [TextField.style](https://docs.flutter.io/flutter/material/TextField/style.html) - final TextStyle? style; - - /// How the text being edited should be aligned horizontally. - /// - /// Same as [TextField.textAlign](https://docs.flutter.io/flutter/material/TextField/textAlign.html) - final TextAlign textAlign; - - /// Same as [TextField.textDirection](https://docs.flutter.io/flutter/material/TextField/textDirection.html) - /// - /// Defaults to null - final TextDirection? textDirection; - - /// Same as [TextField.textAlignVertical](https://api.flutter.dev/flutter/material/TextField/textAlignVertical.html) - final TextAlignVertical? textAlignVertical; - - /// If false the textfield is "disabled": it ignores taps and its - /// [decoration] is rendered in grey. - /// - /// Same as [TextField.enabled](https://docs.flutter.io/flutter/material/TextField/enabled.html) - final bool enabled; - - /// Whether to show input suggestions as the user types. - /// - /// Same as [TextField.enableSuggestions](https://api.flutter.dev/flutter/material/TextField/enableSuggestions.html) - final bool enableSuggestions; - - /// The type of keyboard to use for editing the text. - /// - /// Same as [TextField.keyboardType](https://docs.flutter.io/flutter/material/TextField/keyboardType.html) - final TextInputType keyboardType; - - /// Whether this text field should focus itself if nothing else is already - /// focused. - /// - /// Same as [TextField.autofocus](https://docs.flutter.io/flutter/material/TextField/autofocus.html) - final bool autofocus; - - /// Optional input validation and formatting overrides. - /// - /// Same as [TextField.inputFormatters](https://docs.flutter.io/flutter/material/TextField/inputFormatters.html) - final List? inputFormatters; - - /// Whether to enable autocorrection. - /// - /// Same as [TextField.autocorrect](https://docs.flutter.io/flutter/material/TextField/autocorrect.html) - final bool autocorrect; - - /// The maximum number of lines for the text to span, wrapping if necessary. - /// - /// Same as [TextField.maxLines](https://docs.flutter.io/flutter/material/TextField/maxLines.html) - final int? maxLines; - - /// The minimum number of lines to occupy when the content spans fewer lines. - /// - /// Same as [TextField.minLines](https://docs.flutter.io/flutter/material/TextField/minLines.html) - final int? minLines; - - /// The maximum number of characters (Unicode scalar values) to allow in the - /// text field. - /// - /// Same as [TextField.maxLength](https://docs.flutter.io/flutter/material/TextField/maxLength.html) - final int? maxLength; - - /// If true, prevents the field from allowing more than [maxLength] - /// characters. - /// - /// Same as [TextField.maxLengthEnforcement](https://api.flutter.dev/flutter/material/TextField/maxLengthEnforcement.html) - final MaxLengthEnforcement? maxLengthEnforcement; - - /// Whether to hide the text being edited (e.g., for passwords). - /// - /// Same as [TextField.obscureText](https://docs.flutter.io/flutter/material/TextField/obscureText.html) - final bool obscureText; - - /// Called when the text being edited changes. - /// - /// Same as [TextField.onChanged](https://docs.flutter.io/flutter/material/TextField/onChanged.html) - final ValueChanged? onChanged; - - /// Called when the user indicates that they are done editing the text in the - /// field. - /// - /// Same as [TextField.onSubmitted](https://docs.flutter.io/flutter/material/TextField/onSubmitted.html) - final ValueChanged? onSubmitted; - - /// The color to use when painting the cursor. - /// - /// Same as [TextField.cursorColor](https://docs.flutter.io/flutter/material/TextField/cursorColor.html) - final Color? cursorColor; - - /// How rounded the corners of the cursor should be. By default, the cursor has a null Radius - /// - /// Same as [TextField.cursorRadius](https://docs.flutter.io/flutter/material/TextField/cursorRadius.html) - final Radius? cursorRadius; - - /// How thick the cursor will be. - /// - /// Same as [TextField.cursorWidth](https://docs.flutter.io/flutter/material/TextField/cursorWidth.html) - final double cursorWidth; - - /// The appearance of the keyboard. - /// - /// Same as [TextField.keyboardAppearance](https://docs.flutter.io/flutter/material/TextField/keyboardAppearance.html) - final Brightness? keyboardAppearance; - - /// Called when the user submits editable content (e.g., user presses the "done" button on the keyboard). - /// - /// Same as [TextField.onEditingComplete](https://docs.flutter.io/flutter/material/TextField/onEditingComplete.html) - final VoidCallback? onEditingComplete; - - /// Called for each distinct tap except for every second tap of a double tap. - /// - /// Same as [TextField.onTap](https://docs.flutter.io/flutter/material/TextField/onTap.html) - final GestureTapCallback? onTap; - - /// Configures padding to edges surrounding a Scrollable when the Textfield scrolls into view. - /// - /// Same as [TextField.scrollPadding](https://docs.flutter.io/flutter/material/TextField/scrollPadding.html) - final EdgeInsets scrollPadding; - - /// Configures how the platform keyboard will select an uppercase or lowercase keyboard. - /// - /// Same as [TextField.TextCapitalization](https://docs.flutter.io/flutter/material/TextField/textCapitalization.html) - final TextCapitalization textCapitalization; - - /// The type of action button to use for the keyboard. - /// - /// Same as [TextField.textInputAction](https://docs.flutter.io/flutter/material/TextField/textInputAction.html) - final TextInputAction? textInputAction; - - final bool enableInteractiveSelection; - - final Widget? leftButton, rightButton; - final Widget? clearTextButton; - - /// Creates a QuickSearchTextFieldConfiguration - const QuickSearchTextFieldConfiguration({ - this.decoration: const InputDecoration(), - this.style, - this.controller, - this.onChanged, - this.onSubmitted, - this.obscureText: false, - this.maxLengthEnforcement, - this.maxLength, - this.maxLines: 1, - this.minLines, - this.textAlignVertical, - this.autocorrect: true, - this.inputFormatters, - this.autofocus: false, - this.keyboardType: TextInputType.text, - this.enabled: true, - this.enableSuggestions: true, - this.textAlign: TextAlign.start, - this.focusNode, - this.cursorColor, - this.cursorRadius, - this.textInputAction, - this.textCapitalization: TextCapitalization.none, - this.cursorWidth: 2.0, - this.keyboardAppearance, - this.onEditingComplete, - this.onTap, - this.textDirection, - this.scrollPadding: const EdgeInsets.all(20.0), - this.enableInteractiveSelection: true, - this.leftButton, - this.rightButton, - this.clearTextButton, - }); - - /// Copies the [QuickSearchTextFieldConfiguration] and only changes the specified - /// properties - QuickSearchTextFieldConfiguration copyWith( - {InputDecoration? decoration, - TextStyle? style, - TextEditingController? controller, - ValueChanged? onChanged, - ValueChanged? onSubmitted, - bool? obscureText, - MaxLengthEnforcement? maxLengthEnforcement, - int? maxLength, - int? maxLines, - int? minLines, - bool? autocorrect, - List? inputFormatters, - bool? autofocus, - TextInputType? keyboardType, - bool? enabled, - bool? enableSuggestions, - TextAlign? textAlign, - FocusNode? focusNode, - Color? cursorColor, - TextAlignVertical? textAlignVertical, - Radius? cursorRadius, - double? cursorWidth, - Brightness? keyboardAppearance, - VoidCallback? onEditingComplete, - GestureTapCallback? onTap, - EdgeInsets? scrollPadding, - TextCapitalization? textCapitalization, - TextDirection? textDirection, - TextInputAction? textInputAction, - bool? enableInteractiveSelection, - Widget? leftButton, - Widget? rightButton, - Widget? clearTextButton}) { - return QuickSearchTextFieldConfiguration( - decoration: decoration ?? this.decoration, - style: style ?? this.style, - controller: controller ?? this.controller, - onChanged: onChanged ?? this.onChanged, - onSubmitted: onSubmitted ?? this.onSubmitted, - obscureText: obscureText ?? this.obscureText, - maxLengthEnforcement: maxLengthEnforcement ?? this.maxLengthEnforcement, - maxLength: maxLength ?? this.maxLength, - maxLines: maxLines ?? this.maxLines, - minLines: minLines ?? this.minLines, - autocorrect: autocorrect ?? this.autocorrect, - inputFormatters: inputFormatters ?? this.inputFormatters, - autofocus: autofocus ?? this.autofocus, - keyboardType: keyboardType ?? this.keyboardType, - enabled: enabled ?? this.enabled, - enableSuggestions: enableSuggestions ?? this.enableSuggestions, - textAlign: textAlign ?? this.textAlign, - textAlignVertical: textAlignVertical ?? this.textAlignVertical, - focusNode: focusNode ?? this.focusNode, - cursorColor: cursorColor ?? this.cursorColor, - cursorRadius: cursorRadius ?? this.cursorRadius, - cursorWidth: cursorWidth ?? this.cursorWidth, - keyboardAppearance: keyboardAppearance ?? this.keyboardAppearance, - onEditingComplete: onEditingComplete ?? this.onEditingComplete, - onTap: onTap ?? this.onTap, - scrollPadding: scrollPadding ?? this.scrollPadding, - textCapitalization: textCapitalization ?? this.textCapitalization, - textInputAction: textInputAction ?? this.textInputAction, - textDirection: textDirection ?? this.textDirection, - enableInteractiveSelection: - enableInteractiveSelection ?? this.enableInteractiveSelection, - leftButton: leftButton ?? this.leftButton, - rightButton: rightButton ?? this.rightButton, - clearTextButton: clearTextButton ?? this.clearTextButton, - ); - } -} - -class _SuggestionsBox { - static const int waitMetricsTimeoutMillis = 1000; - static const double minOverlaySpace = 64.0; - - final BuildContext context; - final AxisDirection desiredDirection; - final bool autoFlipDirection; - final bool hideSuggestionBox; - - OverlayEntry? _overlayEntry; - AxisDirection direction; - - bool isOpened = false; - bool widgetMounted = true; - double maxHeight = 300.0; - double textBoxWidth = 100.0; - double textBoxHeight = 100.0; - late double directionUpOffset; - - _SuggestionsBox( - this.context, - this.direction, - this.autoFlipDirection, - this.hideSuggestionBox - ) : desiredDirection = direction; - - void open() { - if (this.hideSuggestionBox) return; - if (this.isOpened) return; - assert(this._overlayEntry != null); - resize(); - Overlay.of(context)!.insert(this._overlayEntry!); - this.isOpened = true; - } - - void close() { - if (!this.isOpened) return; - assert(this._overlayEntry != null); - this._overlayEntry!.remove(); - this.isOpened = false; - } - - void toggle() { - if (this.isOpened) { - this.close(); - } else { - this.open(); - } - } - - MediaQuery? _findRootMediaQuery() { - MediaQuery? rootMediaQuery; - context.visitAncestorElements((element) { - if (element.widget is MediaQuery) { - rootMediaQuery = element.widget as MediaQuery; - } - return true; - }); - - return rootMediaQuery; - } - - /// Delays until the keyboard has toggled or the orientation has fully changed - Future _waitChangeMetrics() async { - if (widgetMounted) { - // initial viewInsets which are before the keyboard is toggled - EdgeInsets initial = MediaQuery.of(context).viewInsets; - // initial MediaQuery for orientation change - MediaQuery? initialRootMediaQuery = _findRootMediaQuery(); - - int timer = 0; - // viewInsets or MediaQuery have changed once keyboard has toggled or orientation has changed - while (widgetMounted && timer < waitMetricsTimeoutMillis) { - // reduce delay if showDialog ever exposes detection of animation end - await Future.delayed(const Duration(milliseconds: 170)); - timer += 170; - - if (widgetMounted && - (MediaQuery.of(context).viewInsets != initial || - _findRootMediaQuery() != initialRootMediaQuery)) { - return true; - } - } - } - - return false; - } - - void resize() { - // check to see if widget is still mounted - // user may have closed the widget with the keyboard still open - if (widgetMounted) { - _adjustMaxHeightAndOrientation(); - _overlayEntry!.markNeedsBuild(); - } - } - - // See if there's enough room in the desired direction for the overlay to display - // correctly. If not, try the opposite direction if things look more roomy there - void _adjustMaxHeightAndOrientation() { - TypeAheadFieldQuickSearch widget = context.widget as TypeAheadFieldQuickSearch; - - RenderBox? box = context.findRenderObject() as RenderBox?; - if (box == null || box.hasSize == false) { - return; - } - - textBoxWidth = box.size.width; - textBoxHeight = box.size.height; - - // top of text box - double textBoxAbsY = box.localToGlobal(Offset.zero).dy; - - // height of window - double windowHeight = MediaQuery.of(context).size.height; - - // we need to find the root MediaQuery for the unsafe area height - // we cannot use BuildContext.ancestorWidgetOfExactType because - // widgets like SafeArea creates a new MediaQuery with the padding removed - MediaQuery rootMediaQuery = _findRootMediaQuery()!; - - // height of keyboard - double keyboardHeight = rootMediaQuery.data.viewInsets.bottom; - - double maxHDesired = _calculateMaxHeight(desiredDirection, box, widget, - windowHeight, rootMediaQuery, keyboardHeight, textBoxAbsY); - - // if there's enough room in the desired direction, update the direction and the max height - if (maxHDesired >= minOverlaySpace || !autoFlipDirection) { - direction = desiredDirection; - maxHeight = maxHDesired; - } else { - // There's not enough room in the desired direction so see how much room is in the opposite direction - AxisDirection flipped = flipAxisDirection(desiredDirection); - double maxHFlipped = _calculateMaxHeight(flipped, box, widget, - windowHeight, rootMediaQuery, keyboardHeight, textBoxAbsY); - - // if there's more room in this opposite direction, update the direction and maxHeight - if (maxHFlipped > maxHDesired) { - direction = flipped; - maxHeight = maxHFlipped; - } - } - - if (maxHeight < 0) maxHeight = 0; - } - - double _calculateMaxHeight( - AxisDirection direction, - RenderBox box, - TypeAheadFieldQuickSearch widget, - double windowHeight, - MediaQuery rootMediaQuery, - double keyboardHeight, - double textBoxAbsY) { - return direction == AxisDirection.down - ? _calculateMaxHeightDown(box, widget, windowHeight, rootMediaQuery, - keyboardHeight, textBoxAbsY) - : _calculateMaxHeightUp(box, widget, windowHeight, rootMediaQuery, - keyboardHeight, textBoxAbsY); - } - - double _calculateMaxHeightDown( - RenderBox box, - TypeAheadFieldQuickSearch widget, - double windowHeight, - MediaQuery rootMediaQuery, - double keyboardHeight, - double textBoxAbsY) { - // unsafe area, ie: iPhone X 'home button' - // keyboardHeight includes unsafeAreaHeight, if keyboard is showing, set to 0 - double unsafeAreaHeight = - keyboardHeight == 0 ? rootMediaQuery.data.padding.bottom : 0; - - return windowHeight - - keyboardHeight - - unsafeAreaHeight - - textBoxHeight - - textBoxAbsY - - 2 * widget.suggestionsBoxVerticalOffset; - } - - double _calculateMaxHeightUp( - RenderBox box, - TypeAheadFieldQuickSearch widget, - double windowHeight, - MediaQuery rootMediaQuery, - double keyboardHeight, - double textBoxAbsY) { - // recalculate keyboard absolute y value - double keyboardAbsY = windowHeight - keyboardHeight; - - directionUpOffset = textBoxAbsY > keyboardAbsY - ? keyboardAbsY - textBoxAbsY - widget.suggestionsBoxVerticalOffset - : -widget.suggestionsBoxVerticalOffset; - - // unsafe area, ie: iPhone X notch - double unsafeAreaHeight = rootMediaQuery.data.padding.top; - - return textBoxAbsY > keyboardAbsY - ? keyboardAbsY - - unsafeAreaHeight - - 2 * widget.suggestionsBoxVerticalOffset - : textBoxAbsY - - unsafeAreaHeight - - 2 * widget.suggestionsBoxVerticalOffset; - } - - Future onChangeMetrics() async { - if (await _waitChangeMetrics()) { - resize(); - } - } -} - -/// Supply an instance of this class to the [TypeAhead.suggestionsBoxController] -/// property to manually control the suggestions box -class QuickSearchSuggestionsBoxController { - _SuggestionsBox? _suggestionsBox; - FocusNode? _effectiveFocusNode; - - /// Opens the suggestions box - void open() { - _effectiveFocusNode!.requestFocus(); - } - - bool isOpened() { - return _suggestionsBox!.isOpened; - } - - /// Closes the suggestions box - void close() { - _effectiveFocusNode!.unfocus(); - } - - /// Opens the suggestions box if closed and vice-versa - void toggle() { - if (_suggestionsBox!.isOpened) { - close(); - } else { - open(); - } - } - - /// Recalculates the height of the suggestions box - void resize() { - _suggestionsBox!.resize(); - } -} diff --git a/core/lib/presentation/views/typeahead/typedef.dart b/core/lib/presentation/views/typeahead/typedef.dart deleted file mode 100644 index 5fc9dd714..000000000 --- a/core/lib/presentation/views/typeahead/typedef.dart +++ /dev/null @@ -1,15 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/widgets.dart'; - -typedef FutureOr> SuggestionsCallback(String pattern); -typedef FutureOr> FetchRecentActionCallback(String pattern); -typedef Widget ItemBuilder(BuildContext context, T itemData); -typedef void SuggestionSelectionCallback(T suggestion); -typedef void RecentSelectionCallback(R recent); -typedef Widget ErrorBuilder(BuildContext context, Object? error); -typedef Widget ButtonActionBuilder(BuildContext context, dynamic action); -typedef void ButtonActionCallback(dynamic action); - -typedef Widget AnimationTransitionBuilder( - BuildContext context, Widget child, AnimationController? controller); diff --git a/core/lib/presentation/views/typeahead/utils.dart b/core/lib/presentation/views/typeahead/utils.dart deleted file mode 100644 index f8a3062c3..000000000 --- a/core/lib/presentation/views/typeahead/utils.dart +++ /dev/null @@ -1,5 +0,0 @@ -import 'dart:io'; - -import 'package:flutter/foundation.dart'; - -final supportedPlatform = (kIsWeb || Platform.isAndroid || Platform.isIOS); diff --git a/lib/features/mailbox_dashboard/domain/model/recent_search.dart b/lib/features/mailbox_dashboard/domain/model/recent_search.dart index 4d226109b..02f1d7f87 100644 --- a/lib/features/mailbox_dashboard/domain/model/recent_search.dart +++ b/lib/features/mailbox_dashboard/domain/model/recent_search.dart @@ -7,6 +7,10 @@ class RecentSearch with EquatableMixin { RecentSearch(this.value, this.creationDate); + factory RecentSearch.now(String word) { + return RecentSearch(word, DateTime.now()); + } + @override List get props => [value, creationDate]; } \ No newline at end of file diff --git a/lib/features/mailbox_dashboard/presentation/action/dashboard_action.dart b/lib/features/mailbox_dashboard/presentation/action/dashboard_action.dart index c6ee6f0d5..965490da7 100644 --- a/lib/features/mailbox_dashboard/presentation/action/dashboard_action.dart +++ b/lib/features/mailbox_dashboard/presentation/action/dashboard_action.dart @@ -1,9 +1,9 @@ import 'package:flutter/cupertino.dart'; import 'package:model/email/email_action_type.dart'; -import 'package:model/email/filter_message_option.dart'; import 'package:model/email/presentation_email.dart'; import 'package:tmail_ui_user/features/base/action/ui_action.dart'; +import 'package:tmail_ui_user/features/thread/domain/model/filter_message_option.dart'; import 'package:tmail_ui_user/main/routes/router_arguments.dart'; class DashBoardAction extends UIAction { @@ -97,4 +97,12 @@ class OpenEmailDetailedAction extends DashBoardAction { @override List get props => [presentationEmail]; +} + +class DisableSearchEmailAction extends DashBoardAction { + + DisableSearchEmailAction(); + + @override + List get props => []; } \ No newline at end of file diff --git a/lib/features/mailbox_dashboard/presentation/extensions/filter_message_option_extension.dart b/lib/features/mailbox_dashboard/presentation/extensions/filter_message_option_extension.dart deleted file mode 100644 index cd5932287..000000000 --- a/lib/features/mailbox_dashboard/presentation/extensions/filter_message_option_extension.dart +++ /dev/null @@ -1,34 +0,0 @@ - -import 'package:core/core.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:model/model.dart'; -import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; - -extension FilterMessageOptionExtension on FilterMessageOption { - - String getName(BuildContext context) { - switch(this) { - case FilterMessageOption.all: - return ''; - case FilterMessageOption.unread: - return AppLocalizations.of(context).unread; - case FilterMessageOption.attachments: - return AppLocalizations.of(context).with_attachments; - case FilterMessageOption.starred: - return AppLocalizations.of(context).starred; - } - } - - String getIcon(ImagePaths imagePaths) { - switch(this) { - case FilterMessageOption.all: - return ''; - case FilterMessageOption.unread: - return imagePaths.icUnread; - case FilterMessageOption.attachments: - return imagePaths.icAttachment; - case FilterMessageOption.starred: - return imagePaths.icStar; - } - } -} \ No newline at end of file diff --git a/lib/features/mailbox_dashboard/presentation/mailbox_dashboard_controller.dart b/lib/features/mailbox_dashboard/presentation/mailbox_dashboard_controller.dart index 6337a252e..7dfcda945 100644 --- a/lib/features/mailbox_dashboard/presentation/mailbox_dashboard_controller.dart +++ b/lib/features/mailbox_dashboard/presentation/mailbox_dashboard_controller.dart @@ -50,6 +50,7 @@ import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/emai import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/quick_search_filter.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/model/manage_account_arguments.dart'; import 'package:tmail_ui_user/features/thread/domain/constants/thread_constants.dart'; +import 'package:tmail_ui_user/features/thread/domain/model/filter_message_option.dart'; import 'package:tmail_ui_user/features/thread/domain/model/search_query.dart'; import 'package:tmail_ui_user/features/thread/domain/state/search_email_state.dart'; import 'package:tmail_ui_user/features/thread/presentation/model/search_state.dart'; @@ -82,7 +83,6 @@ class MailboxDashBoardController extends ReloadableController { final accountId = Rxn(); final userProfile = Rxn(); final searchState = SearchState.initial().obs; - final suggestionSearch = [].obs; final dashBoardAction = Rxn(); final routePath = AppRoutes.MAILBOX_DASHBOARD.obs; final appInformation = Rxn(); @@ -286,35 +286,40 @@ class MailboxDashBoardController extends ReloadableController { void disableSearch() { searchState.value = searchState.value.disableSearchState(); searchQuery = SearchQuery.initial(); - clearSuggestionSearch(); searchInputController.clear(); listFilterQuickSearch.clear(); + emailReceiveTimeType.value = null; FocusManager.instance.primaryFocus?.unfocus(); } - void clearSearchText() { + void clearTextSearch() { searchQuery = SearchQuery.initial(); - clearSuggestionSearch(); + searchInputController.clear(); searchFocus.requestFocus(); } - void clearSuggestionSearch() { - suggestionSearch.clear(); - } - - void addSuggestionSearch(String query) { - if (query.trim().isNotEmpty) { - suggestionSearch.value = [query]; - } else { - clearSearchText(); - } + void updateTextSearch(String value) { + searchInputController.text = value; } void searchEmail(BuildContext context, String value) { searchQuery = SearchQuery(value); dispatchState(Right(SearchEmailNewQuery(searchQuery ?? SearchQuery.initial()))); - clearSuggestionSearch(); FocusScope.of(context).unfocus(); + if (searchInsideEmailDetailedViewIsActive(context)) { + closeEmailDetailedView(); + } + } + + bool searchInsideEmailDetailedViewIsActive(BuildContext context) { + return BuildUtils.isWeb + && _responsiveUtils.isDesktop(context) + && routePath.value == AppRoutes.EMAIL; + } + + void closeEmailDetailedView() { + dispatchRoute(AppRoutes.THREAD); + clearSelectedEmail(); } void _saveEmailAsDraftsSuccess(SaveEmailAsDraftsSuccess success) { @@ -472,6 +477,36 @@ class MailboxDashBoardController extends ReloadableController { emailReceiveTimeType.value = receiveTimeType; } + bool quickSearchFilterForLast7DaysIsActive () { + return listFilterQuickSearch.contains(QuickSearchFilter.last7Days) + && emailReceiveTimeType.value != null; + } + + bool quickSearchFilterForHasAttachmentIsActive () { + return listFilterQuickSearch.contains(QuickSearchFilter.hasAttachment) + || filterMessageOption.value == FilterMessageOption.attachments; + } + + bool quickSearchFilterForFromMeIsActive () { + return listFilterQuickSearch.contains(QuickSearchFilter.fromMe); + } + + bool filterForHasAttachmentIsActive () { + return quickSearchFilterForHasAttachmentIsActive() || filterMessageWithAttachmentIsActive(); + } + + bool filterMessageWithAttachmentIsActive () { + return filterMessageOption.value == FilterMessageOption.attachments; + } + + bool filterMessageUnreadIsActive () { + return filterMessageOption.value == FilterMessageOption.unread; + } + + bool filterMessageStarredIsActive () { + return filterMessageOption.value == FilterMessageOption.starred; + } + void composeEmailAction() { if (kIsWeb) { if (dashBoardAction.value is! ComposeEmailAction) { 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 3a4085da4..7eba59240 100644 --- a/lib/features/mailbox_dashboard/presentation/mailbox_dashboard_view_web.dart +++ b/lib/features/mailbox_dashboard/presentation/mailbox_dashboard_view_web.dart @@ -1,7 +1,6 @@ import 'package:core/core.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; -import 'package:flutter_typeahead/flutter_typeahead.dart'; import 'package:get/get.dart'; import 'package:model/model.dart'; import 'package:tmail_ui_user/features/composer/presentation/composer_view_web.dart'; @@ -15,7 +14,7 @@ import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/em import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/recent_search_item_tile_widget.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/model/app_setting.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/model/reading_pane.dart'; -import 'package:tmail_ui_user/features/thread/presentation/extensions/filter_message_option_extension.dart'; +import 'package:tmail_ui_user/features/thread/domain/model/filter_message_option.dart'; import 'package:tmail_ui_user/features/thread/presentation/thread_view.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; import 'package:tmail_ui_user/main/routes/app_routes.dart'; @@ -396,17 +395,17 @@ class MailboxDashBoardView extends BaseMailboxDashBoardView { return Row( children: [ buildIconWeb( - icon: SvgPicture.asset(imagePaths.icBack, color: AppColor.colorTextButton, fit: BoxFit.fill), - onTap: () { - controller.disableSearch(); - controller.dispatchAction(CancelSelectionAllEmailAction()); - }), + icon: SvgPicture.asset( + imagePaths.icBack, + color: AppColor.colorTextButton, + fit: BoxFit.fill), + onTap: () => controller.dispatchAction(DisableSearchEmailAction())), Expanded(child: Container( decoration: const BoxDecoration( borderRadius: BorderRadius.all(Radius.circular(10)), color: AppColor.colorBgSearchBar), height: 45, - child: TypeAheadFormFieldQuickSearch( + child: QuickSearchInputForm( textFieldConfiguration: QuickSearchTextFieldConfiguration( controller: controller.searchInputController, autofocus: true, @@ -414,7 +413,7 @@ class MailboxDashBoardView extends BaseMailboxDashBoardView { textInputAction: TextInputAction.done, onSubmitted: (keyword) { if (keyword.trim().isNotEmpty) { - controller.saveRecentSearch(RecentSearch(keyword, DateTime.now())); + controller.saveRecentSearch(RecentSearch.now(keyword)); } controller.searchEmail(context, keyword); }, @@ -424,30 +423,43 @@ class MailboxDashBoardView extends BaseMailboxDashBoardView { enabledBorder: InputBorder.none, contentPadding: EdgeInsets.zero, hintText: AppLocalizations.of(context).search_mail, - hintStyle: const TextStyle(color: AppColor.colorHintSearchBar, fontSize: 17.0), - labelStyle: const TextStyle(color: AppColor.colorHintSearchBar, fontSize: 17.0) + hintStyle: const TextStyle( + color: AppColor.colorHintSearchBar, + fontSize: 17.0), + labelStyle: const TextStyle( + color: AppColor.colorHintSearchBar, + fontSize: 17.0) ), leftButton: buildIconWeb( - icon: SvgPicture.asset(imagePaths.icSearchBar, width: 16, height: 16, fit: BoxFit.fill), + icon: SvgPicture.asset( + imagePaths.icSearchBar, + width: 16, + height: 16, + fit: BoxFit.fill), onTap: () { final keyword = controller.searchInputController.text; if (keyword.trim().isNotEmpty) { - controller.saveRecentSearch(RecentSearch(keyword, DateTime.now())); + controller.saveRecentSearch(RecentSearch.now(keyword)); } controller.searchEmail(context, keyword); }), clearTextButton: buildIconWeb( - icon: SvgPicture.asset(imagePaths.icClearTextSearch, width: 16, height: 16, fit: BoxFit.fill), + icon: SvgPicture.asset( + imagePaths.icClearTextSearch, + width: 16, + height: 16, + fit: BoxFit.fill), onTap: () { - controller.searchInputController.clear(); - controller.clearSearchText(); + controller.clearTextSearch(); }) ), suggestionsBoxDecoration: QuickSearchSuggestionsBoxDecoration( color: Colors.white, borderRadius: const BorderRadius.all(Radius.circular(16)), constraints: BoxConstraints( - maxWidth: responsiveUtils.isDesktop(context) ? 556 : double.infinity), + maxWidth: responsiveUtils.isDesktop(context) + ? 556 + : double.infinity), ), debounceDuration: const Duration(milliseconds: 500), listActionButton: const [ @@ -463,12 +475,18 @@ class MailboxDashBoardView extends BaseMailboxDashBoardView { }, buttonActionCallback: (filterAction) { if (filterAction is QuickSearchFilter) { - controller.selectQuickSearchFilter(filterAction, fromSuggestionBox: true); + controller.selectQuickSearchFilter( + filterAction, + fromSuggestionBox: true); } }, - listActionPadding: const EdgeInsets.only(left: 24, top: 24, right: 24, bottom: 16), + listActionPadding: const EdgeInsets.only( + left: 24, + top: 24, + right: 24, + bottom: 16), titleHeaderRecent: Padding( - padding: const EdgeInsets.only(left: 24, right: 24, bottom: 8, top: 8), + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 8), child: Text(AppLocalizations.of(context).recent, style: const TextStyle(fontSize: 13.0, color: AppColor.colorTextButtonHeaderThread, @@ -480,12 +498,14 @@ class MailboxDashBoardView extends BaseMailboxDashBoardView { return InkWell( onTap: () { if (keyword.trim().isNotEmpty) { - controller.saveRecentSearch(RecentSearch(keyword, DateTime.now())); + controller.saveRecentSearch(RecentSearch.now(keyword)); } controller.searchEmail(context, keyword); }, child: Padding( - padding: const EdgeInsets.only(left: 24, right: 24, top: 14, bottom: 14), + padding: const EdgeInsets.symmetric( + horizontal: 24, + vertical: 14), child: Row(children: [ Text(AppLocalizations.of(context).showingResultsFor, style: const TextStyle(fontSize: 13.0, @@ -523,10 +543,12 @@ class MailboxDashBoardView extends BaseMailboxDashBoardView { return controller.quickSearchEmailsAction(pattern); }, itemBuilder: (context, email) { - return EmailQuickSearchItemTileWidget(email, controller.selectedMailbox.value); + return EmailQuickSearchItemTileWidget( + email, controller.selectedMailbox.value); }, onSuggestionSelected: (presentationEmail) async { - controller.dispatchAction(OpenEmailDetailedAction(context, presentationEmail)); + controller.dispatchAction( + OpenEmailDetailedAction(context, presentationEmail)); }), )), const SizedBox(width: 16), diff --git a/lib/features/mailbox_dashboard/presentation/mixin/filter_email_popup_menu_mixin.dart b/lib/features/mailbox_dashboard/presentation/mixin/filter_email_popup_menu_mixin.dart index f48b52ace..2049254f7 100644 --- a/lib/features/mailbox_dashboard/presentation/mixin/filter_email_popup_menu_mixin.dart +++ b/lib/features/mailbox_dashboard/presentation/mixin/filter_email_popup_menu_mixin.dart @@ -3,8 +3,7 @@ import 'package:core/core.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; -import 'package:model/model.dart'; -import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/filter_message_option_extension.dart'; +import 'package:tmail_ui_user/features/thread/domain/model/filter_message_option.dart'; mixin FilterEmailPopupMenuMixin { final _imagePaths = Get.find(); @@ -38,15 +37,24 @@ mixin FilterEmailPopupMenuMixin { width: 20, height: 20, fit: BoxFit.fill, - color: option != FilterMessageOption.starred ? AppColor.colorTextButton : null), + color: option != FilterMessageOption.starred + ? AppColor.colorTextButton + : null), const SizedBox(width: 12), Expanded(child: Text( option.getName(context), - style: const TextStyle(fontSize: 15, color: Colors.black, fontWeight: FontWeight.w500))), + style: const TextStyle( + fontSize: 15, + color: Colors.black, + fontWeight: FontWeight.w500))), if (optionSelected == option) - const SizedBox(width: 12), - if (optionSelected == option) - SvgPicture.asset(_imagePaths.icFilterSelected, width: 16, height: 16, fit: BoxFit.fill), + ... [ + const SizedBox(width: 12), + SvgPicture.asset(_imagePaths.icFilterSelected, + width: 16, + height: 16, + fit: BoxFit.fill), + ] ]) ), ) diff --git a/lib/features/thread/data/datasource/thread_datasource.dart b/lib/features/thread/data/datasource/thread_datasource.dart index bba7bf94e..a235df0af 100644 --- a/lib/features/thread/data/datasource/thread_datasource.dart +++ b/lib/features/thread/data/datasource/thread_datasource.dart @@ -6,9 +6,9 @@ import 'package:jmap_dart_client/jmap/core/state.dart'; import 'package:jmap_dart_client/jmap/core/unsigned_int.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; -import 'package:model/model.dart'; import 'package:tmail_ui_user/features/thread/data/model/email_change_response.dart'; import 'package:tmail_ui_user/features/thread/domain/model/email_response.dart'; +import 'package:tmail_ui_user/features/thread/domain/model/filter_message_option.dart'; abstract class ThreadDataSource { Future getAllEmail( diff --git a/lib/features/thread/data/datasource_impl/local_thread_datasource_impl.dart b/lib/features/thread/data/datasource_impl/local_thread_datasource_impl.dart index 8d1f65828..1b765dcc4 100644 --- a/lib/features/thread/data/datasource_impl/local_thread_datasource_impl.dart +++ b/lib/features/thread/data/datasource_impl/local_thread_datasource_impl.dart @@ -6,11 +6,11 @@ import 'package:jmap_dart_client/jmap/core/state.dart'; import 'package:jmap_dart_client/jmap/core/unsigned_int.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; -import 'package:model/model.dart'; import 'package:tmail_ui_user/features/thread/data/datasource/thread_datasource.dart'; import 'package:tmail_ui_user/features/thread/data/local/email_cache_manager.dart'; import 'package:tmail_ui_user/features/thread/data/model/email_change_response.dart'; import 'package:tmail_ui_user/features/thread/domain/model/email_response.dart'; +import 'package:tmail_ui_user/features/thread/domain/model/filter_message_option.dart'; class LocalThreadDataSourceImpl extends ThreadDataSource { diff --git a/lib/features/thread/data/datasource_impl/thread_datasource_impl.dart b/lib/features/thread/data/datasource_impl/thread_datasource_impl.dart index 82d262a8a..9bd2a56ff 100644 --- a/lib/features/thread/data/datasource_impl/thread_datasource_impl.dart +++ b/lib/features/thread/data/datasource_impl/thread_datasource_impl.dart @@ -6,11 +6,11 @@ import 'package:jmap_dart_client/jmap/core/state.dart'; import 'package:jmap_dart_client/jmap/core/unsigned_int.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; -import 'package:model/model.dart'; import 'package:tmail_ui_user/features/thread/data/datasource/thread_datasource.dart'; import 'package:tmail_ui_user/features/thread/data/model/email_change_response.dart'; import 'package:tmail_ui_user/features/thread/domain/model/email_response.dart'; import 'package:tmail_ui_user/features/thread/data/network/thread_api.dart'; +import 'package:tmail_ui_user/features/thread/domain/model/filter_message_option.dart'; class ThreadDataSourceImpl extends ThreadDataSource { diff --git a/lib/features/thread/data/local/email_cache_manager.dart b/lib/features/thread/data/local/email_cache_manager.dart index f06d4dc42..f1db0e4e2 100644 --- a/lib/features/thread/data/local/email_cache_manager.dart +++ b/lib/features/thread/data/local/email_cache_manager.dart @@ -10,6 +10,7 @@ import 'package:tmail_ui_user/features/thread/data/extensions/email_extension.da import 'package:tmail_ui_user/features/thread/data/extensions/list_email_cache_extension.dart'; import 'package:tmail_ui_user/features/thread/data/model/email_cache.dart'; import 'package:jmap_dart_client/jmap/core/sort/comparator.dart'; +import 'package:tmail_ui_user/features/thread/domain/model/filter_message_option.dart'; class EmailCacheManager { diff --git a/lib/features/thread/data/repository/thread_repository_impl.dart b/lib/features/thread/data/repository/thread_repository_impl.dart index b18b70925..987668698 100644 --- a/lib/features/thread/data/repository/thread_repository_impl.dart +++ b/lib/features/thread/data/repository/thread_repository_impl.dart @@ -21,7 +21,9 @@ import 'package:tmail_ui_user/features/mailbox/data/model/state_type.dart'; import 'package:tmail_ui_user/features/thread/data/datasource/thread_datasource.dart'; import 'package:tmail_ui_user/features/thread/data/model/email_change_response.dart'; import 'package:tmail_ui_user/features/thread/domain/constants/thread_constants.dart'; +import 'package:tmail_ui_user/features/thread/domain/model/email_filter.dart'; import 'package:tmail_ui_user/features/thread/domain/model/email_response.dart'; +import 'package:tmail_ui_user/features/thread/domain/model/filter_message_option.dart'; import 'package:tmail_ui_user/features/thread/domain/model/get_email_request.dart'; import 'package:tmail_ui_user/features/thread/domain/repository/thread_repository.dart'; diff --git a/model/lib/email/email_filter.dart b/lib/features/thread/domain/model/email_filter.dart similarity index 83% rename from model/lib/email/email_filter.dart rename to lib/features/thread/domain/model/email_filter.dart index e225c576c..3ec21aaef 100644 --- a/model/lib/email/email_filter.dart +++ b/lib/features/thread/domain/model/email_filter.dart @@ -2,7 +2,7 @@ import 'package:equatable/equatable.dart'; import 'package:jmap_dart_client/jmap/core/filter/filter.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; -import 'package:model/email/filter_message_option.dart'; +import 'package:tmail_ui_user/features/thread/domain/model/filter_message_option.dart'; class EmailFilter with EquatableMixin { diff --git a/lib/features/thread/presentation/extensions/filter_message_option_extension.dart b/lib/features/thread/domain/model/filter_message_option.dart similarity index 50% rename from lib/features/thread/presentation/extensions/filter_message_option_extension.dart rename to lib/features/thread/domain/model/filter_message_option.dart index 6f5eb77f6..43b0283d6 100644 --- a/lib/features/thread/presentation/extensions/filter_message_option_extension.dart +++ b/lib/features/thread/domain/model/filter_message_option.dart @@ -1,9 +1,17 @@ import 'package:core/core.dart'; -import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:model/model.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; +enum FilterMessageOption { + all, + unread, + attachments, + starred, +} + extension FilterMessageOptionExtension on FilterMessageOption { String getIconToast(ImagePaths imagePaths) { @@ -44,4 +52,56 @@ extension FilterMessageOptionExtension on FilterMessageOption { return AppLocalizations.of(context).with_starred; } } + + bool filterPresentationEmail(PresentationEmail email) { + switch(this) { + case FilterMessageOption.all: + return true; + case FilterMessageOption.unread: + return !email.hasRead; + case FilterMessageOption.attachments: + return email.withAttachments; + case FilterMessageOption.starred: + return email.hasStarred; + } + } + + bool filterEmail(Email email) { + switch(this) { + case FilterMessageOption.all: + return true; + case FilterMessageOption.unread: + return !email.hasRead; + case FilterMessageOption.attachments: + return email.withAttachments; + case FilterMessageOption.starred: + return email.hasStarred; + } + } + + String getName(BuildContext context) { + switch(this) { + case FilterMessageOption.all: + return ''; + case FilterMessageOption.unread: + return AppLocalizations.of(context).unread; + case FilterMessageOption.attachments: + return AppLocalizations.of(context).with_attachments; + case FilterMessageOption.starred: + return AppLocalizations.of(context).starred; + } + } + + String getIcon(ImagePaths imagePaths) { + switch(this) { + case FilterMessageOption.all: + return ''; + case FilterMessageOption.unread: + return imagePaths.icUnread; + case FilterMessageOption.attachments: + return imagePaths.icAttachment; + case FilterMessageOption.starred: + return imagePaths.icStar; + } + } } \ No newline at end of file diff --git a/lib/features/thread/domain/repository/thread_repository.dart b/lib/features/thread/domain/repository/thread_repository.dart index 002557d3a..43641327f 100644 --- a/lib/features/thread/domain/repository/thread_repository.dart +++ b/lib/features/thread/domain/repository/thread_repository.dart @@ -6,8 +6,9 @@ import 'package:jmap_dart_client/jmap/core/state.dart' as jmap; import 'package:jmap_dart_client/jmap/core/unsigned_int.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; -import 'package:model/model.dart'; +import 'package:tmail_ui_user/features/thread/domain/model/email_filter.dart'; import 'package:tmail_ui_user/features/thread/domain/model/email_response.dart'; +import 'package:tmail_ui_user/features/thread/domain/model/filter_message_option.dart'; import 'package:tmail_ui_user/features/thread/domain/model/get_email_request.dart'; abstract class ThreadRepository { diff --git a/lib/features/thread/domain/usecases/get_emails_in_mailbox_interactor.dart b/lib/features/thread/domain/usecases/get_emails_in_mailbox_interactor.dart index c67405a22..a28b1f9e8 100644 --- a/lib/features/thread/domain/usecases/get_emails_in_mailbox_interactor.dart +++ b/lib/features/thread/domain/usecases/get_emails_in_mailbox_interactor.dart @@ -4,6 +4,7 @@ import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/properties/properties.dart'; import 'package:jmap_dart_client/jmap/core/sort/comparator.dart'; import 'package:jmap_dart_client/jmap/core/unsigned_int.dart'; +import 'package:tmail_ui_user/features/thread/domain/model/email_filter.dart'; import 'package:tmail_ui_user/features/thread/domain/model/email_response.dart'; import 'package:tmail_ui_user/features/thread/domain/repository/thread_repository.dart'; import 'package:tmail_ui_user/features/thread/domain/state/get_all_email_state.dart'; diff --git a/lib/features/thread/domain/usecases/refresh_changes_emails_in_mailbox_interactor.dart b/lib/features/thread/domain/usecases/refresh_changes_emails_in_mailbox_interactor.dart index 7a440fa8e..fb99ebaef 100644 --- a/lib/features/thread/domain/usecases/refresh_changes_emails_in_mailbox_interactor.dart +++ b/lib/features/thread/domain/usecases/refresh_changes_emails_in_mailbox_interactor.dart @@ -5,6 +5,7 @@ import 'package:jmap_dart_client/jmap/core/properties/properties.dart'; import 'package:jmap_dart_client/jmap/core/sort/comparator.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; import 'package:tmail_ui_user/features/thread/domain/model/email_response.dart'; +import 'package:tmail_ui_user/features/thread/domain/model/filter_message_option.dart'; import 'package:tmail_ui_user/features/thread/domain/repository/thread_repository.dart'; import 'package:model/model.dart'; import 'package:jmap_dart_client/jmap/core/state.dart' as jmap; diff --git a/lib/features/thread/presentation/thread_controller.dart b/lib/features/thread/presentation/thread_controller.dart index 2d1a026dd..a16920edb 100644 --- a/lib/features/thread/presentation/thread_controller.dart +++ b/lib/features/thread/presentation/thread_controller.dart @@ -42,6 +42,8 @@ import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/mailbox_da import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/email_receive_time_type.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/quick_search_filter.dart'; import 'package:tmail_ui_user/features/thread/domain/constants/thread_constants.dart'; +import 'package:tmail_ui_user/features/thread/domain/model/email_filter.dart'; +import 'package:tmail_ui_user/features/thread/domain/model/filter_message_option.dart'; import 'package:tmail_ui_user/features/thread/domain/model/get_email_request.dart'; import 'package:tmail_ui_user/features/thread/domain/model/search_query.dart'; import 'package:tmail_ui_user/features/thread/domain/state/empty_trash_folder_state.dart'; @@ -62,7 +64,6 @@ import 'package:tmail_ui_user/features/thread/domain/usecases/move_multiple_emai import 'package:tmail_ui_user/features/thread/domain/usecases/refresh_changes_emails_in_mailbox_interactor.dart'; import 'package:tmail_ui_user/features/thread/domain/usecases/search_email_interactor.dart'; import 'package:tmail_ui_user/features/thread/domain/usecases/search_more_email_interactor.dart'; -import 'package:tmail_ui_user/features/thread/presentation/extensions/filter_message_option_extension.dart'; import 'package:tmail_ui_user/features/thread/presentation/model/delete_action_type.dart'; import 'package:tmail_ui_user/features/thread/presentation/model/search_state.dart'; import 'package:tmail_ui_user/features/thread/presentation/model/search_status.dart'; @@ -92,7 +93,7 @@ class ThreadController extends BaseController { final MoveToMailboxInteractor _moveToMailboxInteractor; final emailList = [].obs; - final emailListSearch = [].obs; + final searchIsActive = RxBool(false); bool canLoadMore = true; bool canSearchMore = true; @@ -113,6 +114,10 @@ class ThreadController extends BaseController { PresentationMailbox? get currentMailbox => mailboxDashBoardController.selectedMailbox.value; + TextEditingController get searchController => mailboxDashBoardController.searchInputController; + + String get currentTextSearch => mailboxDashBoardController.searchInputController.text; + ThreadController( this._getEmailsInMailboxInteractor, this._markAsMultipleEmailReadInteractor, @@ -154,7 +159,7 @@ class ThreadController extends BaseController { newState.fold( (failure) { if (failure is SearchEmailFailure) { - emailListSearch.clear(); + emailList.clear(); } else if (failure is SearchMoreEmailFailure || failure is LoadMoreEmailsFailure) { _isLoadingMore = false; } @@ -181,7 +186,6 @@ class ThreadController extends BaseController { void onDone() { viewState.value.fold( (failure) { - log('ThreadController::onDone(): failure: $failure'); if (failure is MarkAsMultipleEmailReadAllFailure || failure is MarkAsMultipleEmailReadFailure) { _markAsReadSelectedMultipleEmailFailure(failure); @@ -197,7 +201,6 @@ class ThreadController extends BaseController { (success) { if (success is MarkAsMultipleEmailReadAllSuccess || success is MarkAsMultipleEmailReadHasSomeEmailFailure) { - log('ThreadController::onDone(): success: $success'); _markAsReadSelectedMultipleEmailSuccess(success); } else if (success is MoveMultipleEmailToMailboxAllSuccess || success is MoveMultipleEmailToMailboxHasSomeEmailFailure) { @@ -239,9 +242,7 @@ class ThreadController extends BaseController { searchWorker = ever(mailboxDashBoardController.searchState, (searchState) { if (searchState is SearchState) { - if (searchState.searchStatus == SearchStatus.INACTIVE) { - emailListSearch.clear(); - } else if (searchState.searchStatus == SearchStatus.ACTIVE) { + if (searchState.searchStatus == SearchStatus.ACTIVE) { cancelSelectEmail(); } } @@ -270,6 +271,9 @@ class ThreadController extends BaseController { } if (action is OpenEmailDetailedAction) { pressEmailAction(action.context, EmailActionType.preview, action.presentationEmail); mailboxDashBoardController.clearDashBoardAction(); + } if (action is DisableSearchEmailAction) { + closeSearchEmailAction(); + mailboxDashBoardController.clearDashBoardAction(); } } }); @@ -318,7 +322,6 @@ class ThreadController extends BaseController { } void _getAllEmailSuccess(GetAllEmailSuccess success) { - log('ThreadController::_getAllEmailSuccess(): ${success.emailList.length}'); _currentEmailState = success.currentEmailState; emailList.value = success.emailList; if (listEmailController.hasClients) { @@ -327,7 +330,6 @@ class ThreadController extends BaseController { } void _refreshChangesAllEmailSuccess(RefreshChangesAllEmailSuccess success) { - log('ThreadController::_refreshChangesAllEmailSuccess(): ${success.emailList.length}'); _currentEmailState = success.currentEmailState; final emailsBeforeChanges = emailList; @@ -341,7 +343,6 @@ class ThreadController extends BaseController { } void _getAllEmailAction(AccountId accountId, {MailboxId? mailboxId}) { - log('ThreadController::_getAllEmailAction(): mailboxId = $mailboxId'); consumeState(_getEmailsInMailboxInteractor.execute( accountId, limit: ThreadConstants.defaultLimit, @@ -394,37 +395,30 @@ class ThreadController extends BaseController { } void _refreshEmailChanges() { - log('ThreadController::_refreshEmailChanges(): '); if (isSearchActive()) { if (_accountId != null && searchQuery != null) { - final limit = emailListSearch.isNotEmpty ? UnsignedInt(emailListSearch.length) : ThreadConstants.defaultLimit; - consumeState(_searchEmailInteractor.execute( - _accountId!, - limit: limit, - sort: _sortOrder, - filter: EmailFilterCondition( - text: searchQuery!.value, - inMailbox: _currentMailboxId), - properties: ThreadConstants.propertiesDefault, + final limit = emailList.isNotEmpty + ? UnsignedInt(emailList.length) + : ThreadConstants.defaultLimit; + _searchEmail(limit: limit); + } + } else { + if (_accountId != null && _currentEmailState != null) { + consumeState(_refreshChangesEmailsInMailboxInteractor.execute( + _accountId!, + _currentEmailState!, + sort: _sortOrder, + propertiesCreated: ThreadConstants.propertiesDefault, + propertiesUpdated: ThreadConstants.propertiesUpdatedDefault, + inMailboxId: _currentMailboxId, + filterOption: mailboxDashBoardController.filterMessageOption.value )); } } - if (_accountId != null && _currentEmailState != null) { - consumeState(_refreshChangesEmailsInMailboxInteractor.execute( - _accountId!, - _currentEmailState!, - sort: _sortOrder, - propertiesCreated: ThreadConstants.propertiesDefault, - propertiesUpdated: ThreadConstants.propertiesUpdatedDefault, - inMailboxId: _currentMailboxId, - filterOption: mailboxDashBoardController.filterMessageOption.value - )); - } } void loadMoreEmails() { if (canLoadMore && _accountId != null) { - log('ThreadController::loadMoreEmails(): latest: ${emailList.last.receivedAt}'); startFpsMeter(); consumeState(_loadMoreEmailsInMailboxInteractor.execute( GetEmailRequest( @@ -439,11 +433,8 @@ class ThreadController extends BaseController { } void _loadMoreEmailsSuccess(LoadMoreEmailsSuccess success) { - log('ThreadController::_loadMoreEmailsSuccess(): [BEFORE] totalEmailList = ${emailList.length}'); if (success.emailList.isNotEmpty) { - log('ThreadController::_loadMoreEmailsSuccess(): add success: ${success.emailList.length}'); emailList.addAll(success.emailList); - log('ThreadController::_loadMoreEmailsSuccess(): [AFTER] totalEmailList = ${emailList.length}'); } else { canLoadMore = false; } @@ -466,15 +457,9 @@ class ThreadController extends BaseController { } void selectEmail(BuildContext context, PresentationEmail presentationEmailSelected) { - if (isSearchActive()) { - emailListSearch.value = emailListSearch - .map((email) => email.id == presentationEmailSelected.id ? email.toggleSelect() : email) - .toList(); - } else { - emailList.value = emailList - .map((email) => email.id == presentationEmailSelected.id ? email.toggleSelect() : email) - .toList(); - } + emailList.value = emailList + .map((email) => email.id == presentationEmailSelected.id ? email.toggleSelect() : email) + .toList(); if (_isUnSelectedAll()) { mailboxDashBoardController.currentSelectMode.value = SelectMode.INACTIVE; @@ -492,41 +477,27 @@ class ThreadController extends BaseController { } void setSelectAllEmailAction() { - if (isSearchActive()) { - emailListSearch.value = emailListSearch.map((email) => email.toSelectedEmail(selectMode: SelectMode.ACTIVE)).toList(); - } else { - emailList.value = emailList.map((email) => email.toSelectedEmail(selectMode: SelectMode.ACTIVE)).toList(); - } + emailList.value = emailList.map((email) => email.toSelectedEmail(selectMode: SelectMode.ACTIVE)).toList(); mailboxDashBoardController.currentSelectMode.value = SelectMode.ACTIVE; mailboxDashBoardController.listEmailSelected.value = listEmailSelected; } List get listEmailSelected { - if (isSearchActive()) { - return emailListSearch.where((email) => email.selectMode == SelectMode.ACTIVE).toList(); - } else { - return emailList.where((email) => email.selectMode == SelectMode.ACTIVE).toList(); - } + return emailList.where((email) => email.selectMode == SelectMode.ACTIVE).toList(); } bool _isUnSelectedAll() { - if (isSearchActive()) { - return emailListSearch.every((email) => email.selectMode == SelectMode.INACTIVE); - } else { - return emailList.every((email) => email.selectMode == SelectMode.INACTIVE); - } + return emailList.every((email) => email.selectMode == SelectMode.INACTIVE); } void cancelSelectEmail() { - emailListSearch.value = emailListSearch.map((email) => email.toSelectedEmail(selectMode: SelectMode.INACTIVE)).toList(); emailList.value = emailList.map((email) => email.toSelectedEmail(selectMode: SelectMode.INACTIVE)).toList(); mailboxDashBoardController.currentSelectMode.value = SelectMode.INACTIVE; mailboxDashBoardController.listEmailSelected.clear(); } void markAsReadAllEmails() { - final listEmail = isSearchActive() ? emailListSearch.allEmailUnread : emailList.allEmailUnread; - log('ThreadController::markAsReadAllEmails(): ${listEmail.length}'); + final listEmail = emailList.allEmailUnread; if (listEmail.isNotEmpty) { markAsReadSelectedMultipleEmail(listEmail); } @@ -534,12 +505,10 @@ class ThreadController extends BaseController { void markAsReadSelectedMultipleEmail(List listPresentationEmail) { final listEmail = listPresentationEmail.map((presentationEmail) => presentationEmail.toEmail()).toList(); - log('ThreadController::markAsReadSelectedMultipleEmail(): listPresentationEmail: ${listPresentationEmail.length}'); final readAction = listPresentationEmail.isAllEmailRead ? ReadActions.markAsUnread : ReadActions.markAsRead; final mailboxCurrent = mailboxDashBoardController.selectedMailbox.value; if (_accountId != null && mailboxCurrent != null) { cancelSelectEmail(); - log('ThreadController::markAsReadSelectedMultipleEmail(): listEmail: ${listEmail.length}'); consumeState(_markAsMultipleEmailReadInteractor.execute(_accountId!, listEmail, readAction)); } } @@ -564,7 +533,6 @@ class ThreadController extends BaseController { message: message, icon: readActions == ReadActions.markAsUnread ? _imagePaths.icUnreadToast : _imagePaths.icReadToast); } - log('ThreadController::_markAsReadSelectedMultipleEmailSuccess(): _refreshEmailChanges'); _refreshEmailChanges(); } @@ -602,18 +570,18 @@ class ThreadController extends BaseController { ? FilterMessageOption.all : filterOption; - log('ThreadController::filterMessagesAction(): newFilterOption: $newFilterOption'); - mailboxDashBoardController.filterMessageOption.value = newFilterOption; - log('ThreadController::filterMessagesAction(): filterMessageOption: ${mailboxDashBoardController.filterMessageOption}'); - _appToast.showToastWithIcon( currentOverlayContext!, message: newFilterOption.getMessageToast(context), icon: newFilterOption.getIconToast(_imagePaths)); - refreshAllEmail(); + if (isSearchActive()) { + _searchEmail(); + } else { + refreshAllEmail(); + } } void moveSelectedMultipleEmailToMailbox(List listEmail) async { @@ -839,34 +807,62 @@ class ThreadController extends BaseController { } void disableSearch() { - emailListSearch.clear(); + searchIsActive.value = false; mailboxDashBoardController.disableSearch(); - cancelSelectEmail(); } - void _searchEmail() { - emailListSearch.clear(); + void closeSearchEmailAction() { + disableSearch(); + cancelSelectEmail(); + refreshAllEmail(); + } + void clearTextSearch() { + mailboxDashBoardController.clearTextSearch(); + } + + void _searchEmail({UnsignedInt? limit}) { if (_accountId != null && searchQuery != null) { + searchIsActive.value = true; + UTCDate? afterTime; - if (mailboxDashBoardController.listFilterQuickSearch.contains(QuickSearchFilter.last7Days) - && mailboxDashBoardController.emailReceiveTimeType.value != null) { + bool? hasAttachment; + String? fromAddress; + String? keywordUnread; + String? keywordStarred; + + if (mailboxDashBoardController.quickSearchFilterForLast7DaysIsActive()) { afterTime = mailboxDashBoardController.emailReceiveTimeType.value!.toUTCDate(); } + if (mailboxDashBoardController.filterForHasAttachmentIsActive()) { + hasAttachment = true; + } + + if (mailboxDashBoardController.quickSearchFilterForFromMeIsActive()) { + fromAddress = mailboxDashBoardController.userProfile.value?.email; + } + + if (mailboxDashBoardController.filterMessageUnreadIsActive()) { + keywordUnread = KeyWordIdentifier.emailSeen.value; + } + + if (mailboxDashBoardController.filterMessageStarredIsActive()) { + keywordStarred = KeyWordIdentifier.emailFlagged.value; + } + final emailFilterCondition = EmailFilterCondition( text: searchQuery!.value, - hasAttachment: mailboxDashBoardController.listFilterQuickSearch.contains(QuickSearchFilter.hasAttachment) - ? true : null, - from: mailboxDashBoardController.listFilterQuickSearch.contains(QuickSearchFilter.fromMe) - ? mailboxDashBoardController.userProfile.value!.email - : null, + hasAttachment: hasAttachment, + from: fromAddress, after: afterTime, + hasKeyword: keywordStarred, + notKeyword: keywordUnread, inMailbox: _currentMailboxId); consumeState(_searchEmailInteractor.execute( _accountId!, - limit: ThreadConstants.defaultLimit, + limit: limit ?? ThreadConstants.defaultLimit, sort: _sortOrder, filter: emailFilterCondition, properties: ThreadConstants.propertiesDefault, @@ -879,10 +875,10 @@ class ThreadController extends BaseController { .map((email) => email.toSearchPresentationEmail(mailboxDashBoardController.mapMailbox)) .toList(); - final emailsSearchBeforeChanges = emailListSearch; + final emailsSearchBeforeChanges = emailList; final emailsSearchAfterChanges = resultEmailSearchList; final newListEmailSearch = emailsSearchAfterChanges.combine(emailsSearchBeforeChanges); - emailListSearch.value = newListEmailSearch; + emailList.value = newListEmailSearch; } void searchMoreEmails() { @@ -893,9 +889,9 @@ class ThreadController extends BaseController { sort: _sortOrder, filter: EmailFilterCondition( text: searchQuery!.value, - before: emailListSearch.last.receivedAt), + before: emailList.last.receivedAt), properties: ThreadConstants.propertiesDefault, - lastEmailId: emailListSearch.last.id + lastEmailId: emailList.last.id )); } } @@ -904,9 +900,9 @@ class ThreadController extends BaseController { if (success.emailList.isNotEmpty) { final resultEmailSearchList = success.emailList .map((email) => email.toSearchPresentationEmail(mailboxDashBoardController.mapMailbox)) - .where((email) => !emailListSearch.contains(email)) + .where((email) => !emailList.contains(email)) .toList(); - emailListSearch.addAll(resultEmailSearchList); + emailList.addAll(resultEmailSearchList); } else { canSearchMore = false; } @@ -916,7 +912,6 @@ class ThreadController extends BaseController { bool isSelectionEnabled() => mailboxDashBoardController.isSelectionEnabled(); void pressEmailSelectionAction(BuildContext context, EmailActionType actionType, List selectionEmail) { - log('ThreadController::pressEmailSelectionAction(): selectionEmail: ${selectionEmail.length}'); switch(actionType) { case EmailActionType.markAsRead: case EmailActionType.markAsUnread: diff --git a/lib/features/thread/presentation/thread_view.dart b/lib/features/thread/presentation/thread_view.dart index 86969a0c8..cfc6f9ec9 100644 --- a/lib/features/thread/presentation/thread_view.dart +++ b/lib/features/thread/presentation/thread_view.dart @@ -5,8 +5,13 @@ import 'package:get/get.dart'; import 'package:model/model.dart'; import 'package:tmail_ui_user/features/base/mixin/app_loader_mixin.dart'; import 'package:tmail_ui_user/features/email/presentation/widgets/email_action_cupertino_action_sheet_action_builder.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/domain/model/recent_search.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/mixin/filter_email_popup_menu_mixin.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/email_receive_time_type.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/quick_search_filter.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/email_quick_search_item_tile_widget.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/recent_search_item_tile_widget.dart'; +import 'package:tmail_ui_user/features/thread/domain/model/filter_message_option.dart'; import 'package:tmail_ui_user/features/thread/domain/state/search_email_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/search_more_email_state.dart'; import 'package:tmail_ui_user/features/thread/presentation/model/delete_action_type.dart'; @@ -16,11 +21,10 @@ import 'package:tmail_ui_user/features/thread/presentation/widgets/bottom_bar_th import 'package:tmail_ui_user/features/thread/presentation/widgets/email_tile_builder.dart' if (dart.library.html) 'package:tmail_ui_user/features/thread/presentation/widgets/email_tile_web_builder.dart'; import 'package:tmail_ui_user/features/thread/presentation/widgets/filter_message_cupertino_action_sheet_action_builder.dart'; -import 'package:tmail_ui_user/features/thread/presentation/widgets/search_app_bar_widget.dart'; -import 'package:tmail_ui_user/features/thread/presentation/widgets/suggestion_box_widget.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; -class ThreadView extends GetWidget with AppLoaderMixin { +class ThreadView extends GetWidget with AppLoaderMixin, + FilterEmailPopupMenuMixin { final _responsiveUtils = Get.find(); final _imagePaths = Get.find(); @@ -46,50 +50,74 @@ class ThreadView extends GetWidget with AppLoaderMixin { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Obx(() => !controller.isSearchActive() - && ((!_responsiveUtils.isDesktop(context) && BuildUtils.isWeb) || !BuildUtils.isWeb) - ? _buildAppBarNormal(context) - : const SizedBox.shrink()), - _buildSearchInputFormForMobile(context), - Container( - color: BuildUtils.isWeb ? AppColor.colorBgDesktop : Colors.white, - padding: EdgeInsets.zero, - child: _buildSearchButtonViewForMobile(context)), - Obx(() => controller.isMailboxTrash && controller.emailList.isNotEmpty && !controller.isSearchActive() - ? _buildEmptyTrashButton(context) - : const SizedBox.shrink()), - Obx(() => controller.isSearchActive() && _responsiveUtils.isDesktop(context) && BuildUtils.isWeb - ? _buildListButtonQuickSearchFilter(context) - : const SizedBox.shrink()), + if ((!_responsiveUtils.isDesktop(context) && BuildUtils.isWeb) || !BuildUtils.isWeb) + ... [ + _buildAppBarNormal(context), + Obx(() { + return Stack(children: [ + if (!controller.isSearchActive()) + Container( + color: Colors.white, + padding: EdgeInsets.symmetric( + horizontal: 16, + vertical: BuildUtils.isWeb && _responsiveUtils.isTabletLarge(context) ? 8 : 0), + margin: const EdgeInsets.only( + bottom: !BuildUtils.isWeb ? 16 : 0), + child: _buildSearchFormInActive(context)) + else + Container( + color: Colors.white, + padding: EdgeInsets.symmetric( + horizontal: 8, + vertical: BuildUtils.isWeb && _responsiveUtils.isTabletLarge(context) ? 8 : 0), + margin: const EdgeInsets.only( + bottom: !BuildUtils.isWeb ? 16 : 0), + child: _buildSearchFormActive(context)) + ]); + }) + ] + else + const SizedBox.shrink(), + Obx(() { + if (controller.isMailboxTrash && controller.emailList.isNotEmpty + && !controller.isSearchActive()) { + return _buildEmptyTrashButton(context); + } else { + return const SizedBox.shrink(); + } + }), + Obx(() { + if (controller.isSearchActive() + && controller.searchIsActive.isTrue + && _responsiveUtils.isDesktop(context) + && BuildUtils.isWeb) { + return _buildListButtonQuickSearchFilter(context); + } else { + return const SizedBox.shrink(); + } + }), Expanded(child: Container( - color: BuildUtils.isWeb ? AppColor.colorBgDesktop : Colors.white, - padding: BuildUtils.isWeb && _responsiveUtils.isDesktop(context) + alignment: Alignment.center, + margin: BuildUtils.isWeb && _responsiveUtils.isDesktop(context) ? const EdgeInsets.only(left: 32, right: 24, top: 16, bottom: 24) : EdgeInsets.zero, - child: Stack(children: [ - Container( - alignment: Alignment.center, - decoration: BoxDecoration( - borderRadius: BuildUtils.isWeb && _responsiveUtils.isDesktop(context) - ? BorderRadius.circular(20) - : null, - border: BuildUtils.isWeb && _responsiveUtils.isDesktop(context) - ? Border.all(color: AppColor.colorBorderBodyThread, width: 1) - : null, - color: Colors.white), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (_responsiveUtils.isMobile(context)) - _buildStatusResultSearch(context), - _buildLoadingView(), - Expanded(child: _buildListEmail(context)), - _buildLoadingViewLoadMore(), - ] - ) - ), - _buildSuggestionBox(context) - ]))), + decoration: BoxDecoration( + borderRadius: BuildUtils.isWeb && _responsiveUtils.isDesktop(context) + ? BorderRadius.circular(20) + : null, + border: BuildUtils.isWeb && _responsiveUtils.isDesktop(context) + ? Border.all(color: AppColor.colorBorderBodyThread, width: 1) + : null, + color: Colors.white), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildLoadingView(), + Expanded(child: _buildListEmail(context)), + _buildLoadingViewLoadMore(), + ] + ) + )), _buildListButtonSelectionForMobile(context), ] ) @@ -125,62 +153,189 @@ class ThreadView extends GetWidget with AppLoaderMixin { }); } - Widget _buildSearchButtonViewForMobile(BuildContext context) { - return Obx(() { - if (!controller.isSearchActive() && - ((!_responsiveUtils.isDesktop(context) && BuildUtils.isWeb) || !BuildUtils.isWeb)) { - return Container( - color: Colors.white, - padding: const EdgeInsets.only(left: 16, right: 16, bottom: 10), - child: (SearchBarView(_imagePaths) - ..hintTextSearch(BuildUtils.isWeb ? AppLocalizations.of(context).search_emails : AppLocalizations.of(context).hint_search_emails) - ..addOnOpenSearchViewAction(() => controller.enableSearch(context))) - .build()); - } else { - return const SizedBox.shrink(); - } - }); + Widget _buildSearchFormInActive(BuildContext context) { + return (SearchBarView(_imagePaths) + ..hintTextSearch(AppLocalizations.of(context).search_emails) + ..addOnOpenSearchViewAction(() => controller.enableSearch(context))) + .build(); } - Widget _buildSearchInputFormForMobile(BuildContext context) { - return Obx(() { - if (controller.isSearchActive() && - ((!_responsiveUtils.isDesktop(context) && BuildUtils.isWeb) || !BuildUtils.isWeb)) { - return Column(children: [ - _buildSearchForm(context), - const Divider(color: AppColor.lineItemListColor, height: 1, thickness: 0.2), - ]); - } else { - return const SizedBox.shrink(); - } - }); - } - - Widget _buildSearchForm(BuildContext context) { - return (SearchAppBarWidget( - context, - _imagePaths, - _responsiveUtils, - controller.searchQuery, - controller.mailboxDashBoardController.searchFocus, - controller.mailboxDashBoardController.searchInputController, - suggestionSearch: controller.mailboxDashBoardController.suggestionSearch,) - ..addDecoration(const BoxDecoration(color: Colors.white)) - ..setMargin(const EdgeInsets.only(right: 10)) - ..setHintText(AppLocalizations.of(context).search_mail) - ..addOnCancelSearchPressed(() => controller.disableSearch()) - ..addOnClearTextSearchAction(() => controller.mailboxDashBoardController.clearSearchText()) - ..addOnTextChangeSearchAction((query) => controller.mailboxDashBoardController.addSuggestionSearch(query)) - ..addOnSearchTextAction((query) => controller.mailboxDashBoardController.searchEmail(context, query))) - .build(); + Widget _buildSearchFormActive(BuildContext context) { + return Row( + children: [ + buildIconWeb( + icon: SvgPicture.asset( + _imagePaths.icBack, + color: AppColor.colorTextButton, + fit: BoxFit.fill), + onTap: () => controller.closeSearchEmailAction()), + Expanded(child: Container( + decoration: const BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(10)), + color: AppColor.colorBgSearchBar), + height: 45, + child: QuickSearchInputForm( + textFieldConfiguration: QuickSearchTextFieldConfiguration( + controller: controller.searchController, + autofocus: true, + focusNode: controller.mailboxDashBoardController.searchFocus, + textInputAction: TextInputAction.done, + onSubmitted: (keyword) { + if (keyword.trim().isNotEmpty) { + controller.mailboxDashBoardController + .saveRecentSearch(RecentSearch.now(keyword)); + } + controller.mailboxDashBoardController.searchEmail(context, keyword); + }, + decoration: InputDecoration( + border: InputBorder.none, + focusedBorder: InputBorder.none, + enabledBorder: InputBorder.none, + contentPadding: EdgeInsets.zero, + hintText: AppLocalizations.of(context).search_mail, + hintStyle: const TextStyle( + color: AppColor.colorHintSearchBar, + fontSize: 17.0), + labelStyle: const TextStyle( + color: AppColor.colorHintSearchBar, + fontSize: 17.0) + ), + leftButton: buildIconWeb( + icon: SvgPicture.asset( + _imagePaths.icSearchBar, + width: 16, + height: 16, + fit: BoxFit.fill), + onTap: () { + final keyword = controller.currentTextSearch; + if (keyword.trim().isNotEmpty) { + controller.mailboxDashBoardController + .saveRecentSearch(RecentSearch.now(keyword)); + } + controller.mailboxDashBoardController.searchEmail(context, keyword); + }), + clearTextButton: buildIconWeb( + icon: SvgPicture.asset( + _imagePaths.icClearTextSearch, + width: 16, + height: 16, + fit: BoxFit.fill), + onTap: () { + controller.clearTextSearch(); + }) + ), + suggestionsBoxDecoration: const QuickSearchSuggestionsBoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.all(Radius.circular(16)), + ), + debounceDuration: const Duration(milliseconds: 500), + hideSuggestionsBox: _responsiveUtils.isScreenWithShortestSide(context), + listActionButton: const [ + QuickSearchFilter.hasAttachment, + QuickSearchFilter.last7Days, + QuickSearchFilter.fromMe, + ], + actionButtonBuilder: (context, filterAction) { + if (filterAction is QuickSearchFilter) { + return _buildQuickSearchFilterButtonSuggestionBox( + context, + filterAction); + } + return const SizedBox.shrink(); + }, + buttonActionCallback: (filterAction) { + if (filterAction is QuickSearchFilter) { + controller.mailboxDashBoardController + .selectQuickSearchFilter(filterAction, fromSuggestionBox: true); + } + }, + listActionPadding: const EdgeInsets.only( + left: 24, + top: 24, + right: 24, + bottom: 16), + titleHeaderRecent: Padding( + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 8), + child: Text(AppLocalizations.of(context).recent, + style: const TextStyle(fontSize: 13.0, + color: AppColor.colorTextButtonHeaderThread, + fontWeight: FontWeight.w500) + ) + ), + buttonShowAllResult: (context, keyword) { + if (keyword is String) { + return InkWell( + onTap: () { + if (keyword.trim().isNotEmpty) { + controller.mailboxDashBoardController + .saveRecentSearch(RecentSearch.now(keyword)); + } + controller.mailboxDashBoardController.searchEmail(context, keyword); + }, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 14), + child: Row(children: [ + Text(AppLocalizations.of(context).showingResultsFor, + style: const TextStyle(fontSize: 13.0, + color: AppColor.colorTextButtonHeaderThread, + fontWeight: FontWeight.w500) + ), + const SizedBox(width: 4), + Expanded(child: Text('"$keyword"', + style: const TextStyle( + fontSize: 13.0, + color: Colors.black, + fontWeight: FontWeight.w500))) + ]) + ), + ); + } else { + return const SizedBox.shrink(); + } + }, + loadingBuilder: (context) => Padding( + padding: const EdgeInsets.only(bottom: 16), + child: loadingWidget, + ), + fetchRecentActionCallback: (pattern) async { + return controller.mailboxDashBoardController + .getAllRecentSearchAction(pattern); + }, + itemRecentBuilder: (context, recent) { + return RecentSearchItemTileWidget(recent); + }, + onRecentSelected: (recent) { + controller.mailboxDashBoardController + .updateTextSearch(recent.value); + controller.mailboxDashBoardController + .searchEmail(context, recent.value); + }, + suggestionsCallback: (pattern) async { + return controller.mailboxDashBoardController + .quickSearchEmailsAction(pattern); + }, + itemBuilder: (context, email) { + return EmailQuickSearchItemTileWidget( + email, + controller.currentMailbox); + }, + onSuggestionSelected: (presentationEmail) async { + controller.pressEmailAction( + context, + EmailActionType.preview, + presentationEmail); + }), + )), + const SizedBox(width: 16), + ] + ); } Widget _buildAppBarNormal(BuildContext context) { - return (AppBarThreadWidgetBuilder( + return Obx(() { + return (AppBarThreadWidgetBuilder( context, - _imagePaths, - _responsiveUtils, - controller.mailboxDashBoardController.selectedMailbox.value, + controller.currentMailbox, controller.listEmailSelected, controller.mailboxDashBoardController.currentSelectMode.value, controller.mailboxDashBoardController.filterMessageOption.value) @@ -188,17 +343,24 @@ class ThreadView extends GetWidget with AppLoaderMixin { ..addOnEditThreadAction(() => controller.enableSelectionEmail()) ..addOnEmailSelectionAction((actionType, selectionEmail) => controller.pressEmailSelectionAction(context, actionType, selectionEmail)) - ..addOnFilterEmailAction((filterMessageOption, position) => - _responsiveUtils.isScreenWithShortestSide(context) - ? controller.openContextMenuAction( - context, - _filterMessagesCupertinoActionTile(context, filterMessageOption)) - : controller.openPopupMenuAction( - context, - position, - _popupMenuEmailActionTile(context, filterMessageOption))) + ..addOnFilterEmailAction((filterMessageOption, position) { + if (_responsiveUtils.isScreenWithShortestSide(context)) { + controller.openContextMenuAction( + context, + _filterMessagesCupertinoActionTile(context, filterMessageOption)); + } else { + controller.openPopupMenuAction( + context, + position, + popupMenuFilterEmailActionTile( + context, + filterMessageOption, + (option) => controller.filterMessagesAction(context, option))); + } + }) ..addOnCancelEditThread(() => controller.cancelSelectEmail())) .build(); + }); } Widget _buildFloatingButtonCompose(BuildContext context) { @@ -232,75 +394,37 @@ class ThreadView extends GetWidget with AppLoaderMixin { } List _filterMessagesCupertinoActionTile(BuildContext context, FilterMessageOption optionCurrent) { - return [ - _filterMessageWithAttachmentsAction(context, optionCurrent), - _filterMessagesUnreadAction(context, optionCurrent), - _filterMessageStarredAction(context, optionCurrent), - ]; - } - - Widget _filterMessageWithAttachmentsAction(BuildContext context, FilterMessageOption optionCurrent) { - return (FilterMessageCupertinoActionSheetActionBuilder( - const Key('filter_attachment_action'), - SvgPicture.asset(_imagePaths.icAttachment, width: 28, height: 28, fit: BoxFit.fill, color: AppColor.colorTextButton), - AppLocalizations.of(context).with_attachments, - FilterMessageOption.attachments, - optionCurrent: optionCurrent, - iconLeftPadding: _responsiveUtils.isMobile(context) - ? const EdgeInsets.only(left: 12, right: 16) - : const EdgeInsets.only(right: 12), - iconRightPadding: _responsiveUtils.isMobile(context) - ? const EdgeInsets.only(right: 12) - : EdgeInsets.zero, - actionSelected: SvgPicture.asset(_imagePaths.icFilterSelected, fit: BoxFit.fill)) - ..onActionClick((option) => controller.filterMessagesAction(context, option))) - .build(); - } - - Widget _filterMessagesUnreadAction(BuildContext context, FilterMessageOption optionCurrent) { - return (FilterMessageCupertinoActionSheetActionBuilder( - const Key('filter_unread_action'), - SvgPicture.asset(_imagePaths.icUnread, width: 28, height: 28, fit: BoxFit.fill), - AppLocalizations.of(context).unread, - FilterMessageOption.unread, - optionCurrent: optionCurrent, - iconLeftPadding: _responsiveUtils.isMobile(context) - ? const EdgeInsets.only(left: 12, right: 16) - : const EdgeInsets.only(right: 12), - iconRightPadding: _responsiveUtils.isMobile(context) - ? const EdgeInsets.only(right: 12) - : EdgeInsets.zero, - actionSelected: SvgPicture.asset(_imagePaths.icFilterSelected, fit: BoxFit.fill)) - ..onActionClick((option) => controller.filterMessagesAction(context, option))) - .build(); - } - - Widget _filterMessageStarredAction(BuildContext context, FilterMessageOption optionCurrent) { - return (FilterMessageCupertinoActionSheetActionBuilder( - const Key('filter_starred_action'), - SvgPicture.asset(_imagePaths.icStar, width: 28, height: 28, fit: BoxFit.fill), - AppLocalizations.of(context).starred, - FilterMessageOption.starred, - optionCurrent: optionCurrent, - iconLeftPadding: _responsiveUtils.isMobile(context) - ? const EdgeInsets.only(left: 12, right: 16) - : const EdgeInsets.only(right: 12), - iconRightPadding: _responsiveUtils.isMobile(context) - ? const EdgeInsets.only(right: 12) - : EdgeInsets.zero, - actionSelected: SvgPicture.asset(_imagePaths.icFilterSelected, fit: BoxFit.fill)) - ..onActionClick((option) => controller.filterMessagesAction(context, option))) - .build(); - } - - List _popupMenuEmailActionTile(BuildContext context, FilterMessageOption option) { - return [ - PopupMenuItem(padding: const EdgeInsets.symmetric(horizontal: 8), child: _filterMessageWithAttachmentsAction(context, option)), - const PopupMenuDivider(height: 0.5), - PopupMenuItem(padding: const EdgeInsets.symmetric(horizontal: 8), child: _filterMessagesUnreadAction(context, option)), - const PopupMenuDivider(height: 0.5), - PopupMenuItem(padding: const EdgeInsets.symmetric(horizontal: 8), child: _filterMessageStarredAction(context, option)), + final listFilter = [ + FilterMessageOption.attachments, + FilterMessageOption.unread, + FilterMessageOption.starred, ]; + + return listFilter.map((filter) => (FilterMessageCupertinoActionSheetActionBuilder( + Key('filter_email_${filter.name}'), + SvgPicture.asset( + filter.getIcon(_imagePaths), + width: 20, + height: 20, + fit: BoxFit.fill, + color: filter == FilterMessageOption.attachments + ? AppColor.colorTextButton : null), + filter.getName(context), + filter, + optionCurrent: optionCurrent, + iconLeftPadding: _responsiveUtils.isMobile(context) + ? const EdgeInsets.only(left: 12, right: 16) + : const EdgeInsets.only(right: 12), + iconRightPadding: _responsiveUtils.isMobile(context) + ? const EdgeInsets.only(right: 12) + : EdgeInsets.zero, + actionSelected: SvgPicture.asset( + _imagePaths.icFilterSelected, + width: 20, + height: 20, + fit: BoxFit.fill)) + ..onActionClick((option) => controller.filterMessagesAction(context, option))) + .build()).toList(); } Widget _buildLoadingView() { @@ -344,25 +468,11 @@ class ThreadView extends GetWidget with AppLoaderMixin { padding: EdgeInsets.zero, color: Colors.white, child: Obx(() { - if (controller.isSearchActive()) { - if (controller.mailboxDashBoardController.suggestionSearch.isNotEmpty) { - return const SizedBox.shrink(); - } else { - return _buildResultSearchEmails(context, controller.emailListSearch); - } - } else { - return _buildResultListEmail(context, controller.emailList); - } + return _buildResultListEmail(context, controller.emailList); }) ); } - Widget _buildResultSearchEmails(BuildContext context, List listPresentationEmail) { - return listPresentationEmail.isNotEmpty - ? _buildListEmailBody(context, listPresentationEmail) - : _buildEmptyEmail(context); - } - Widget _buildResultListEmail(BuildContext context, List listPresentationEmail) { if (controller.mailboxDashBoardController.currentSelectMode.value == SelectMode.INACTIVE) { return listPresentationEmail.isNotEmpty @@ -433,42 +543,6 @@ class ThreadView extends GetWidget with AppLoaderMixin { ); } - Widget _buildSuggestionBox(BuildContext context) { - return Obx(() => controller.mailboxDashBoardController.suggestionSearch.isNotEmpty - ? (SuggestionBoxWidget( - context, - _imagePaths, - controller.mailboxDashBoardController.suggestionSearch) - ..addOnSelectedSuggestion((suggestion) => - controller.mailboxDashBoardController.searchEmail(context, suggestion))) - .build() - : const SizedBox.shrink() - ); - } - - Widget _buildStatusResultSearch(BuildContext context) { - return Obx(() { - if (controller.isSearchActive()) { - return controller.emailListSearch.isNotEmpty - ? Container( - width: double.infinity, - padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 24), - decoration: BoxDecoration( - borderRadius: BuildUtils.isWeb && _responsiveUtils.isDesktop(context) - ? const BorderRadius.only(topLeft: Radius.circular(20), topRight: Radius.circular(20)) - : null, - color: AppColor.bgStatusResultSearch), - child: Text( - AppLocalizations.of(context).results, - style: const TextStyle(color: AppColor.baseTextColor, fontSize: 14, fontWeight: FontWeight.w500), - )) - : const SizedBox.shrink(); - } else { - return const SizedBox.shrink(); - } - }); - } - Widget _buildEmptyTrashButton(BuildContext context) { return Container( decoration: BoxDecoration( @@ -661,4 +735,30 @@ class ThreadView extends GetWidget with AppLoaderMixin { ) ); } + + Widget _buildQuickSearchFilterButtonSuggestionBox( + BuildContext context, QuickSearchFilter filter) { + return Obx(() { + return Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + color: filter.getBackgroundColor(controller.mailboxDashBoardController.listFilterQuickSearch)), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + child: Row(mainAxisAlignment: MainAxisAlignment.center, children: [ + SvgPicture.asset( + filter.getIcon(_imagePaths, controller.mailboxDashBoardController.listFilterQuickSearch), + width: 16, + height: 16, + fit: BoxFit.fill), + const SizedBox(width: 4), + Text( + filter.getTitle(context, + receiveTimeType: controller.mailboxDashBoardController.emailReceiveTimeType.value), + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: filter.getTextStyle(controller.mailboxDashBoardController.listFilterQuickSearch), + ) + ])); + }); + } } \ No newline at end of file diff --git a/lib/features/thread/presentation/widgets/app_bar_thread_widget_builder.dart b/lib/features/thread/presentation/widgets/app_bar_thread_widget_builder.dart index bd7769e33..a2aa02b16 100644 --- a/lib/features/thread/presentation/widgets/app_bar_thread_widget_builder.dart +++ b/lib/features/thread/presentation/widgets/app_bar_thread_widget_builder.dart @@ -5,7 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; import 'package:model/model.dart'; -import 'package:tmail_ui_user/features/thread/presentation/extensions/filter_message_option_extension.dart'; +import 'package:tmail_ui_user/features/thread/domain/model/filter_message_option.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; typedef OnFilterEmailAction = void Function(FilterMessageOption, RelativeRect? position); @@ -15,6 +15,9 @@ typedef OnCancelEditThread = void Function(); typedef OnEmailSelectionAction = void Function(EmailActionType actionType, List); class AppBarThreadWidgetBuilder { + final _imagePaths = Get.find(); + final _responsiveUtils = Get.find(); + OnFilterEmailAction? _onFilterEmailAction; OnOpenMailboxMenuActionClick? _onOpenMailboxMenuActionClick; OnEditThreadAction? _onEditThreadAction; @@ -22,8 +25,6 @@ class AppBarThreadWidgetBuilder { OnEmailSelectionAction? _onEmailSelectionAction; final BuildContext _context; - final ImagePaths _imagePaths; - final ResponsiveUtils _responsiveUtils; final PresentationMailbox? _currentMailbox; final List _listSelectionEmail; final SelectMode _selectMode; @@ -31,8 +32,6 @@ class AppBarThreadWidgetBuilder { AppBarThreadWidgetBuilder( this._context, - this._imagePaths, - this._responsiveUtils, this._currentMailbox, this._listSelectionEmail, this._selectMode, @@ -65,8 +64,11 @@ class AppBarThreadWidgetBuilder { alignment: Alignment.topCenter, color: Colors.white, margin: EdgeInsets.zero, - padding: EdgeInsets.only(left: 8, bottom: 8, right: 8, - top: (!BuildUtils.isWeb && _responsiveUtils.isPortraitMobile(_context)) ? 0 : 16), + padding: EdgeInsets.only( + left: 8, + bottom: !BuildUtils.isWeb ? 8 : 16, + right: 8, + top: !BuildUtils.isWeb && _responsiveUtils.isPortraitMobile(_context) ? 0 : 16), child: MediaQuery( data: const MediaQueryData(padding: EdgeInsets.zero), child: kIsWeb @@ -211,7 +213,8 @@ class AppBarThreadWidgetBuilder { color: Colors.transparent, child: Padding( padding: const EdgeInsets.only(left: 16, right: 16), - child: GestureDetector( + child: InkWell( + borderRadius: const BorderRadius.all(Radius.circular(10)), onTap: () { if (_onFilterEmailAction != null && _responsiveUtils.isScreenWithShortestSide(_context)) { _onFilterEmailAction!.call(_filterMessageOption, null); diff --git a/lib/features/thread/presentation/widgets/filter_message_cupertino_action_sheet_action_builder.dart b/lib/features/thread/presentation/widgets/filter_message_cupertino_action_sheet_action_builder.dart index 36d7e62d6..55d4a5021 100644 --- a/lib/features/thread/presentation/widgets/filter_message_cupertino_action_sheet_action_builder.dart +++ b/lib/features/thread/presentation/widgets/filter_message_cupertino_action_sheet_action_builder.dart @@ -3,9 +3,10 @@ import 'package:core/core.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; -import 'package:model/model.dart'; +import 'package:tmail_ui_user/features/thread/domain/model/filter_message_option.dart'; -class FilterMessageCupertinoActionSheetActionBuilder extends CupertinoActionSheetActionBuilder { +class FilterMessageCupertinoActionSheetActionBuilder + extends CupertinoActionSheetActionBuilder { final FilterMessageOption option; final FilterMessageOption? optionCurrent; diff --git a/lib/features/thread/presentation/widgets/suggestion_box_widget.dart b/lib/features/thread/presentation/widgets/suggestion_box_widget.dart deleted file mode 100644 index 6728a0ff4..000000000 --- a/lib/features/thread/presentation/widgets/suggestion_box_widget.dart +++ /dev/null @@ -1,88 +0,0 @@ - -import 'package:core/core.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_svg/flutter_svg.dart'; -import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; - -typedef OnSelectedSuggestion = Function(String); - -class SuggestionBoxWidget { - - OnSelectedSuggestion? _onSelectedSuggestion; - - final BuildContext _context; - final ImagePaths _imagePaths; - - double? _suggestionHeight; - double? _elevation; - final List _listData; - - SuggestionBoxWidget( - this._context, - this._imagePaths, - this._listData, - ); - - void setSuggestionHeight(double suggestionHeight) { - _suggestionHeight = suggestionHeight; - } - - void setElevation(double elevation) { - _elevation = elevation; - } - - void addOnSelectedSuggestion(OnSelectedSuggestion onSelectedSuggestion) { - _onSelectedSuggestion = onSelectedSuggestion; - } - - Widget build() { - return Material( - elevation: _elevation ?? 3, - color: Colors.white, - child: ConstrainedBox( - constraints: BoxConstraints( - maxHeight: _suggestionHeight ?? 60, - ), - child: ListView.builder( - shrinkWrap: true, - padding: EdgeInsets.zero, - itemCount: _listData.length, - itemBuilder: (BuildContext context, int index) => ListTile( - contentPadding: const EdgeInsets.symmetric(horizontal: 10), - onTap: () { - if (_onSelectedSuggestion != null) { - _onSelectedSuggestion!(_listData[index]); - }}, - leading: IconButton( - color: AppColor.baseTextColor, - icon: SvgPicture.asset(_imagePaths.icSearch, color: AppColor.baseTextColor, fit: BoxFit.fill), - onPressed: () { - if (_onSelectedSuggestion != null) { - _onSelectedSuggestion!(_listData[index]); - } - }), - title: Transform( - transform: Matrix4.translationValues(-12.0, 0.0, 0.0), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Text( - '${AppLocalizations.of(_context).prefix_suggestion_search} ', - style: const TextStyle(fontSize: 15, color: AppColor.baseTextColor), - ), - Expanded(child: Text( - '"${_listData[index]}"', - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: const TextStyle(fontSize: 15, color: AppColor.colorNameEmail), - )) - ], - ) - ) - ), - ), - ), - ); - } -} \ No newline at end of file diff --git a/model/lib/email/filter_message_option.dart b/model/lib/email/filter_message_option.dart deleted file mode 100644 index 5f617a7c2..000000000 --- a/model/lib/email/filter_message_option.dart +++ /dev/null @@ -1,40 +0,0 @@ - -import 'package:jmap_dart_client/jmap/mail/email/email.dart'; -import 'package:model/email/presentation_email.dart'; -import 'package:model/extensions/email_extension.dart'; - -enum FilterMessageOption { - all, - unread, - attachments, - starred, -} - -extension FilterMessageOptionExtension on FilterMessageOption { - - bool filterPresentationEmail(PresentationEmail email) { - switch(this) { - case FilterMessageOption.all: - return true; - case FilterMessageOption.unread: - return !email.hasRead; - case FilterMessageOption.attachments: - return email.withAttachments; - case FilterMessageOption.starred: - return email.hasStarred; - } - } - - bool filterEmail(Email email) { - switch(this) { - case FilterMessageOption.all: - return true; - case FilterMessageOption.unread: - return !email.hasRead; - case FilterMessageOption.attachments: - return email.withAttachments; - case FilterMessageOption.starred: - return email.hasStarred; - } - } -} \ No newline at end of file diff --git a/model/lib/model.dart b/model/lib/model.dart index a16f95a76..4ef431021 100644 --- a/model/lib/model.dart +++ b/model/lib/model.dart @@ -27,9 +27,7 @@ export 'email/read_actions.dart'; export 'email/attachment.dart'; export 'email/mark_star_action.dart'; export 'email/email_property.dart'; -export 'email/email_filter.dart'; export 'email/email_content_type.dart'; -export 'email/filter_message_option.dart'; // Extensions export 'extensions/email_address_extension.dart'; diff --git a/test/features/thread/domain/usecases/get_emails_in_mailbox_interactor_test.dart b/test/features/thread/domain/usecases/get_emails_in_mailbox_interactor_test.dart index ab622f773..3e058158f 100644 --- a/test/features/thread/domain/usecases/get_emails_in_mailbox_interactor_test.dart +++ b/test/features/thread/domain/usecases/get_emails_in_mailbox_interactor_test.dart @@ -11,6 +11,7 @@ import 'package:jmap_dart_client/jmap/core/unsigned_int.dart'; import 'package:jmap_dart_client/jmap/mail/email/email_filter_condition.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; +import 'package:tmail_ui_user/features/thread/domain/model/email_filter.dart'; import 'package:tmail_ui_user/features/thread/domain/model/email_response.dart'; import 'package:tmail_ui_user/features/thread/domain/constants/thread_constants.dart'; import 'package:tmail_ui_user/features/thread/domain/repository/thread_repository.dart'; @@ -38,7 +39,7 @@ void main() { when(threadRepository.getAllEmail( AccountFixtures.aliceAccountId, limit: UnsignedInt(20), - sort: Set()..add(EmailComparator(EmailComparatorProperty.sentAt)..setIsAscending(false)), + sort: {}..add(EmailComparator(EmailComparatorProperty.sentAt)..setIsAscending(false)), emailFilter: EmailFilter( filter: EmailFilterCondition(inMailbox: MailboxFixtures.inboxMailbox.id), mailboxId: MailboxFixtures.inboxMailbox.id, @@ -66,7 +67,7 @@ void main() { final streamStates = getEmailsInMailboxInteractor.execute( AccountFixtures.aliceAccountId, limit: UnsignedInt(20), - sort: Set()..add(EmailComparator(EmailComparatorProperty.sentAt)..setIsAscending(false)), + sort: {}..add(EmailComparator(EmailComparatorProperty.sentAt)..setIsAscending(false)), emailFilter: EmailFilter( filter: EmailFilterCondition(inMailbox: MailboxFixtures.inboxMailbox.id), mailboxId: MailboxFixtures.inboxMailbox.id, diff --git a/test/features/thread/domain/usecases/get_emails_in_mailbox_interactor_test.mocks.dart b/test/features/thread/domain/usecases/get_emails_in_mailbox_interactor_test.mocks.dart index 75619bd3e..dc82f464a 100644 --- a/test/features/thread/domain/usecases/get_emails_in_mailbox_interactor_test.mocks.dart +++ b/test/features/thread/domain/usecases/get_emails_in_mailbox_interactor_test.mocks.dart @@ -5,19 +5,22 @@ import 'dart:async' as _i3; import 'package:jmap_dart_client/jmap/account_id.dart' as _i5; -import 'package:jmap_dart_client/jmap/core/filter/filter.dart' as _i14; +import 'package:jmap_dart_client/jmap/core/filter/filter.dart' as _i15; import 'package:jmap_dart_client/jmap/core/properties/properties.dart' as _i9; import 'package:jmap_dart_client/jmap/core/sort/comparator.dart' as _i7; import 'package:jmap_dart_client/jmap/core/state.dart' as _i10; import 'package:jmap_dart_client/jmap/core/unsigned_int.dart' as _i6; -import 'package:jmap_dart_client/jmap/mail/email/email.dart' as _i13; +import 'package:jmap_dart_client/jmap/mail/email/email.dart' as _i14; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart' as _i11; import 'package:mockito/mockito.dart' as _i1; -import 'package:model/model.dart' as _i8; +import 'package:tmail_ui_user/features/thread/domain/model/email_filter.dart' + as _i8; import 'package:tmail_ui_user/features/thread/domain/model/email_response.dart' as _i4; -import 'package:tmail_ui_user/features/thread/domain/model/get_email_request.dart' +import 'package:tmail_ui_user/features/thread/domain/model/filter_message_option.dart' as _i12; +import 'package:tmail_ui_user/features/thread/domain/model/get_email_request.dart' + as _i13; import 'package:tmail_ui_user/features/thread/domain/repository/thread_repository.dart' as _i2; @@ -64,7 +67,7 @@ class MockThreadRepository extends _i1.Mock implements _i2.ThreadRepository { _i9.Properties? propertiesCreated, _i9.Properties? propertiesUpdated, _i11.MailboxId? inMailboxId, - _i8.FilterMessageOption? filterOption}) => + _i12.FilterMessageOption? filterOption}) => (super.noSuchMethod( Invocation.method(#refreshChanges, [ accountId, @@ -80,15 +83,15 @@ class MockThreadRepository extends _i1.Mock implements _i2.ThreadRepository { as _i3.Stream<_i4.EmailsResponse>); @override _i3.Stream<_i4.EmailsResponse> loadMoreEmails( - _i12.GetEmailRequest? emailRequest) => + _i13.GetEmailRequest? emailRequest) => (super.noSuchMethod(Invocation.method(#loadMoreEmails, [emailRequest]), returnValue: Stream<_i4.EmailsResponse>.empty()) as _i3.Stream<_i4.EmailsResponse>); @override - _i3.Future> searchEmails(_i5.AccountId? accountId, + _i3.Future> searchEmails(_i5.AccountId? accountId, {_i6.UnsignedInt? limit, Set<_i7.Comparator>? sort, - _i14.Filter? filter, + _i15.Filter? filter, _i9.Properties? properties}) => (super.noSuchMethod( Invocation.method(#searchEmails, [ @@ -99,8 +102,8 @@ class MockThreadRepository extends _i1.Mock implements _i2.ThreadRepository { #filter: filter, #properties: properties }), - returnValue: Future>.value(<_i13.Email>[])) - as _i3.Future>); + returnValue: Future>.value(<_i14.Email>[])) + as _i3.Future>); @override _i3.Future emptyTrashFolder( _i5.AccountId? accountId, _i11.MailboxId? trashMailboxId) => diff --git a/test/features/thread/domain/usecases/refresh_changes_emails_in_mailbox_interactor_test.mocks.dart b/test/features/thread/domain/usecases/refresh_changes_emails_in_mailbox_interactor_test.mocks.dart index 0134e9d3e..b2a94e9d3 100644 --- a/test/features/thread/domain/usecases/refresh_changes_emails_in_mailbox_interactor_test.mocks.dart +++ b/test/features/thread/domain/usecases/refresh_changes_emails_in_mailbox_interactor_test.mocks.dart @@ -5,19 +5,22 @@ import 'dart:async' as _i3; import 'package:jmap_dart_client/jmap/account_id.dart' as _i5; -import 'package:jmap_dart_client/jmap/core/filter/filter.dart' as _i14; +import 'package:jmap_dart_client/jmap/core/filter/filter.dart' as _i15; import 'package:jmap_dart_client/jmap/core/properties/properties.dart' as _i9; import 'package:jmap_dart_client/jmap/core/sort/comparator.dart' as _i7; import 'package:jmap_dart_client/jmap/core/state.dart' as _i10; import 'package:jmap_dart_client/jmap/core/unsigned_int.dart' as _i6; -import 'package:jmap_dart_client/jmap/mail/email/email.dart' as _i13; +import 'package:jmap_dart_client/jmap/mail/email/email.dart' as _i14; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart' as _i11; import 'package:mockito/mockito.dart' as _i1; -import 'package:model/model.dart' as _i8; +import 'package:tmail_ui_user/features/thread/domain/model/email_filter.dart' + as _i8; import 'package:tmail_ui_user/features/thread/domain/model/email_response.dart' as _i4; -import 'package:tmail_ui_user/features/thread/domain/model/get_email_request.dart' +import 'package:tmail_ui_user/features/thread/domain/model/filter_message_option.dart' as _i12; +import 'package:tmail_ui_user/features/thread/domain/model/get_email_request.dart' + as _i13; import 'package:tmail_ui_user/features/thread/domain/repository/thread_repository.dart' as _i2; @@ -64,7 +67,7 @@ class MockThreadRepository extends _i1.Mock implements _i2.ThreadRepository { _i9.Properties? propertiesCreated, _i9.Properties? propertiesUpdated, _i11.MailboxId? inMailboxId, - _i8.FilterMessageOption? filterOption}) => + _i12.FilterMessageOption? filterOption}) => (super.noSuchMethod( Invocation.method(#refreshChanges, [ accountId, @@ -80,15 +83,15 @@ class MockThreadRepository extends _i1.Mock implements _i2.ThreadRepository { as _i3.Stream<_i4.EmailsResponse>); @override _i3.Stream<_i4.EmailsResponse> loadMoreEmails( - _i12.GetEmailRequest? emailRequest) => + _i13.GetEmailRequest? emailRequest) => (super.noSuchMethod(Invocation.method(#loadMoreEmails, [emailRequest]), returnValue: Stream<_i4.EmailsResponse>.empty()) as _i3.Stream<_i4.EmailsResponse>); @override - _i3.Future> searchEmails(_i5.AccountId? accountId, + _i3.Future> searchEmails(_i5.AccountId? accountId, {_i6.UnsignedInt? limit, Set<_i7.Comparator>? sort, - _i14.Filter? filter, + _i15.Filter? filter, _i9.Properties? properties}) => (super.noSuchMethod( Invocation.method(#searchEmails, [ @@ -99,8 +102,8 @@ class MockThreadRepository extends _i1.Mock implements _i2.ThreadRepository { #filter: filter, #properties: properties }), - returnValue: Future>.value(<_i13.Email>[])) - as _i3.Future>); + returnValue: Future>.value(<_i14.Email>[])) + as _i3.Future>); @override _i3.Future emptyTrashFolder( _i5.AccountId? accountId, _i11.MailboxId? trashMailboxId) =>