Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TF-2198 Add autocompleting people in quick search #2312

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ final supportedPlatform = (kIsWeb || Platform.isAndroid || Platform.isIOS);
///
/// * [TypeAheadFieldQuickSearch], A [TextField](https://docs.flutter.io/flutter/material/TextField-class.html)
/// that displays a list of suggestions as the user types
class QuickSearchInputForm<T, R> extends FormField<String> {
class QuickSearchInputForm<T, P, R> extends FormField<String> {
/// The configuration of the [TextField](https://docs.flutter.io/flutter/material/TextField-class.html)
/// that the TypeAhead widget displays
final QuickSearchTextFieldConfiguration textFieldConfiguration;
Expand Down Expand Up @@ -87,6 +87,9 @@ class QuickSearchInputForm<T, R> extends FormField<String> {
BoxDecoration? decoration,
double? maxHeight,
bool isDirectionRTL = false,
ItemBuilder<P>? contactItemBuilder,
SuggestionsCallback<P>? contactSuggestionsCallback,
SuggestionSelectionCallback<P>? onContactSuggestionSelected,
}) : assert(
initialValue == null || textFieldConfiguration.controller == null),
assert(minCharsForSuggestions >= 0),
Expand All @@ -101,7 +104,7 @@ class QuickSearchInputForm<T, R> extends FormField<String> {
autovalidateMode: autovalidateMode,
builder: (FormFieldState<String> field) {
final _TypeAheadFormFieldState state =
field as _TypeAheadFormFieldState<dynamic, dynamic>;
field as _TypeAheadFormFieldState<dynamic, dynamic, dynamic>;

return TypeAheadFieldQuickSearch(
getImmediateSuggestions: getImmediateSuggestions,
Expand Down Expand Up @@ -151,21 +154,24 @@ class QuickSearchInputForm<T, R> extends FormField<String> {
decoration: decoration,
maxHeight: maxHeight,
isDirectionRTL: isDirectionRTL,
contactItemBuilder: contactItemBuilder,
contactSuggestionsCallback: contactSuggestionsCallback,
onContactSuggestionSelected: onContactSuggestionSelected,
);
});

@override
FormFieldState<String> createState() => _TypeAheadFormFieldState<T, R>();
FormFieldState<String> createState() => _TypeAheadFormFieldState<T, P, R>();
}

class _TypeAheadFormFieldState<T, R> extends FormFieldState<String> {
class _TypeAheadFormFieldState<T, P, R> extends FormFieldState<String> {
TextEditingController? _controller;

TextEditingController? get _effectiveController =>
widget.textFieldConfiguration.controller ?? _controller;

@override
QuickSearchInputForm get widget => super.widget as QuickSearchInputForm<dynamic, dynamic>;
QuickSearchInputForm get widget => super.widget as QuickSearchInputForm<dynamic, dynamic, dynamic>;

@override
void initState() {
Expand Down Expand Up @@ -239,7 +245,7 @@ class _TypeAheadFormFieldState<T, R> extends FormFieldState<String> {
/// * [QuickSearchInputForm], 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<T, R> extends StatefulWidget {
class TypeAheadFieldQuickSearch<T, P, R> 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
Expand Down Expand Up @@ -517,8 +523,14 @@ class TypeAheadFieldQuickSearch<T, R> extends StatefulWidget {
final BoxDecoration? decoration;
/// Max height search input
final double? maxHeight;

/// Check direction text input
final bool isDirectionRTL;
/// Widget contact item
final ItemBuilder<P>? contactItemBuilder;
/// Get all contact callback
final SuggestionsCallback<P>? contactSuggestionsCallback;
/// On listen select contact
final SuggestionSelectionCallback<P>? onContactSuggestionSelected;

/// Creates a [TypeAheadFieldQuickSearch]
const TypeAheadFieldQuickSearch(
Expand Down Expand Up @@ -562,17 +574,20 @@ class TypeAheadFieldQuickSearch<T, R> extends StatefulWidget {
this.decoration,
this.maxHeight,
this.isDirectionRTL = false,
this.contactItemBuilder,
this.contactSuggestionsCallback,
this.onContactSuggestionSelected,
}) : assert(animationStart >= 0.0 && animationStart <= 1.0),
assert(
direction == AxisDirection.down || direction == AxisDirection.up),
assert(minCharsForSuggestions >= 0),
super(key: key);

@override
State<TypeAheadFieldQuickSearch<T, R>> createState() => _TypeAheadFieldQuickSearchState<T, R>();
State<TypeAheadFieldQuickSearch<T, P, R>> createState() => _TypeAheadFieldQuickSearchState<T, P, R>();
}

class _TypeAheadFieldQuickSearchState<T, R> extends State<TypeAheadFieldQuickSearch<T, R>>
class _TypeAheadFieldQuickSearchState<T, P, R> extends State<TypeAheadFieldQuickSearch<T, P, R>>
with WidgetsBindingObserver {
FocusNode? _focusNode;
TextEditingController? _textEditingController;
Expand Down Expand Up @@ -707,7 +722,7 @@ class _TypeAheadFieldQuickSearchState<T, R> extends State<TypeAheadFieldQuickSea

void _initOverlayEntry() {
_suggestionsBox!._overlayEntry = OverlayEntry(builder: (context) {
final suggestionsList = _SuggestionsList<T, R>(
final suggestionsList = _SuggestionsList<T, P, R>(
suggestionsBox: _suggestionsBox,
decoration: widget.suggestionsBoxDecoration,
debounceDuration: widget.debounceDuration,
Expand Down Expand Up @@ -754,6 +769,15 @@ class _TypeAheadFieldQuickSearchState<T, R> extends State<TypeAheadFieldQuickSea
listActionPadding: widget.listActionPadding,
hideSuggestionsBox: widget.hideSuggestionsBox,
isDirectionRTL: widget.isDirectionRTL,
contactSuggestionBuilder: widget.contactItemBuilder,
contactSuggestionCallback: widget.contactSuggestionsCallback,
onContactSuggestionSelected: (P selection) {
if (!widget.keepSuggestionsOnSuggestionSelected) {
_effectiveFocusNode!.unfocus();
_suggestionsBox!.close();
}
widget.onContactSuggestionSelected?.call(selection);
},
);

double w = _suggestionsBox!.textBoxWidth;
Expand Down Expand Up @@ -882,7 +906,7 @@ class _TypeAheadFieldQuickSearchState<T, R> extends State<TypeAheadFieldQuickSea
}
}

class _SuggestionsList<T, R> extends StatefulWidget {
class _SuggestionsList<T, P, R> extends StatefulWidget {
final _SuggestionsBox? suggestionsBox;
final TextEditingController? controller;
final bool getImmediateSuggestions;
Expand Down Expand Up @@ -915,6 +939,9 @@ class _SuggestionsList<T, R> extends StatefulWidget {
final EdgeInsets? listActionPadding;
final bool hideSuggestionsBox;
final bool isDirectionRTL;
final ItemBuilder<P>? contactSuggestionBuilder;
final SuggestionsCallback<P>? contactSuggestionCallback;
final SuggestionSelectionCallback<P>? onContactSuggestionSelected;

const _SuggestionsList({
required this.suggestionsBox,
Expand Down Expand Up @@ -949,16 +976,20 @@ class _SuggestionsList<T, R> extends StatefulWidget {
this.listActionPadding,
this.hideSuggestionsBox = false,
this.isDirectionRTL = false,
this.contactSuggestionBuilder,
this.contactSuggestionCallback,
this.onContactSuggestionSelected,
});

@override
_SuggestionsListState<T, R> createState() => _SuggestionsListState<T, R>();
_SuggestionsListState<T, P, R> createState() => _SuggestionsListState<T, P, R>();
}

class _SuggestionsListState<T, R> extends State<_SuggestionsList<T, R>>
class _SuggestionsListState<T, P, R> extends State<_SuggestionsList<T, P, R>>
with SingleTickerProviderStateMixin {
Iterable<T>? _suggestions;
Iterable<R>? _recentItems;
Iterable<P>? _contacts;
late bool _suggestionsValid;
late VoidCallback _controllerListener;
Timer? _debounceTimer;
Expand Down Expand Up @@ -991,6 +1022,7 @@ class _SuggestionsListState<T, R> extends State<_SuggestionsList<T, R>>
setState(() {
_isLoading = false;
_suggestions = null;
_contacts = null;
_recentItems = recentItems;
_suggestionsValid = true;
});
Expand All @@ -1015,7 +1047,7 @@ class _SuggestionsListState<T, R> extends State<_SuggestionsList<T, R>>
}

@override
void didUpdateWidget(_SuggestionsList<T, R> oldWidget) {
void didUpdateWidget(_SuggestionsList<T, P, R> oldWidget) {
super.didUpdateWidget(oldWidget);
widget.controller!.addListener(_controllerListener);
_getSuggestions();
Expand Down Expand Up @@ -1067,13 +1099,17 @@ class _SuggestionsListState<T, R> extends State<_SuggestionsList<T, R>>

Iterable<T>? suggestions;
Iterable<R>? recentItems;
Iterable<P>? contacts;
Object? error;

try {
suggestions = await widget.suggestionsCallback!(widget.controller!.text);
if (widget.fetchRecentActionCallback != null) {
recentItems = await widget.fetchRecentActionCallback!(widget.controller!.text);
}
if (widget.contactSuggestionCallback != null) {
contacts = await widget.contactSuggestionCallback!(widget.controller!.text);
}
} catch (e) {
error = e;
}
Expand All @@ -1091,6 +1127,7 @@ class _SuggestionsListState<T, R> extends State<_SuggestionsList<T, R>>
_isLoading = false;
_suggestions = suggestions;
_recentItems = recentItems;
_contacts = contacts;
});
}
}
Expand All @@ -1111,7 +1148,7 @@ class _SuggestionsListState<T, R> extends State<_SuggestionsList<T, R>>

Widget child;

if (_suggestions?.isNotEmpty == true && widget.controller?.text.isNotEmpty == true) {
if ((_suggestions?.isNotEmpty == true || _contacts?.isNotEmpty == true) && widget.controller?.text.isNotEmpty == true) {
child = createSuggestionsWidget();
} else {
child = createRecentWidget();
Expand Down Expand Up @@ -1165,12 +1202,10 @@ class _SuggestionsListState<T, R> extends State<_SuggestionsList<T, R>>

Widget createSuggestionsWidget() {
final listItemSuggestionWidget = _suggestions?.map((T suggestion) {
if ( widget.itemBuilder != null) {
if (widget.itemBuilder != null) {
return InkWell(
child: widget.itemBuilder!(context, suggestion),
onTap: () {
widget.onSuggestionSelected!(suggestion);
},
onTap: () => widget.onSuggestionSelected?.call(suggestion),
);
} else {
return const SizedBox.shrink();
Expand Down Expand Up @@ -1212,6 +1247,20 @@ class _SuggestionsListState<T, R> extends State<_SuggestionsList<T, R>>
}
}).toList());

final listItemContactWidget = _contacts?.map((P contact) {
if (widget.contactSuggestionBuilder != null) {
return Material(
color: Colors.transparent,
child: InkWell(
child: widget.contactSuggestionBuilder!(context, contact),
onTap: () => widget.onContactSuggestionSelected?.call(contact),
),
);
} else {
return const SizedBox.shrink();
}
}).toList() ?? [];

Widget child = ListView(
padding: EdgeInsets.zero,
primary: false,
Expand All @@ -1226,6 +1275,8 @@ class _SuggestionsListState<T, R> extends State<_SuggestionsList<T, R>>
),
if (_isLoading == true && widget.hideOnLoading == false && widget.keepSuggestionsOnLoading == false)
loadingWidget,
if (listItemContactWidget.isNotEmpty)
... listItemContactWidget,
if (widget.buttonShowAllResult != null && widget.controller?.text.isNotEmpty == true)
widget.buttonShowAllResult!(context, widget.controller?.text),
if (listItemSuggestionWidget.isNotEmpty)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@

import 'package:flutter/cupertino.dart';
import 'package:jmap_dart_client/jmap/mail/email/email_address.dart';
import 'package:model/email/email_action_type.dart';
import 'package:model/email/presentation_email.dart';
import 'package:tmail_ui_user/features/base/action/ui_action.dart';
Expand Down Expand Up @@ -119,4 +120,14 @@ class ClearDateRangeToAdvancedSearch extends DashBoardAction {

@override
List<Object?> get props => [receiveTime];
}
}

class SearchEmailByFromFieldsAction extends DashBoardAction {

final EmailAddress emailAddress;

SearchEmailByFromFieldsAction(this.emailAddress);

@override
List<Object?> get props => [emailAddress];
}
Original file line number Diff line number Diff line change
Expand Up @@ -447,10 +447,21 @@ class AdvancedFilterController extends BaseController {
);
} else if (action is ClearDateRangeToAdvancedSearch) {
_updateDateRangeTime(action.receiveTime);
} else if (action is StartSearchEmailAction) {
if (action.filter == QuickSearchFilter.fromMe) {
_updateFromField();
}
} else if (action is StartSearchEmailAction && action.filter == QuickSearchFilter.fromMe) {
_updateFromField();
} else if (action is SearchEmailByFromFieldsAction) {
searchController.clearSearchFilter();
_resetAllToOriginalValue();
_clearAllTextFieldInput();
searchController.searchInputController.clear();
searchController.deactivateAdvancedSearch();
searchController.isAdvancedSearchViewOpen.value = false;

listFromEmailAddress = List.from({action.emailAddress});
final listAddress = listFromEmailAddress.map((emailAddress) => emailAddress.emailAddress).toSet();
searchController.updateFilterEmail(fromOption: Some(listAddress));

_mailboxDashBoardController.dispatchAction(StartSearchEmailAction());
}
}
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import 'package:jmap_dart_client/jmap/core/session/session.dart';
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/email/email_address.dart';
import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart';
import 'package:jmap_dart_client/jmap/mail/vacation/vacation_response.dart';
import 'package:model/model.dart';
Expand All @@ -26,9 +27,11 @@ import 'package:tmail_ui_user/features/base/reloadable/reloadable_controller.dar
import 'package:tmail_ui_user/features/composer/domain/exceptions/set_email_method_exception.dart';
import 'package:tmail_ui_user/features/composer/domain/extensions/email_request_extension.dart';
import 'package:tmail_ui_user/features/composer/domain/model/email_request.dart';
import 'package:tmail_ui_user/features/composer/domain/state/get_autocomplete_state.dart';
import 'package:tmail_ui_user/features/composer/domain/state/save_email_as_drafts_state.dart';
import 'package:tmail_ui_user/features/composer/domain/state/send_email_state.dart';
import 'package:tmail_ui_user/features/composer/domain/state/update_email_drafts_state.dart';
import 'package:tmail_ui_user/features/composer/domain/usecases/get_autocomplete_interactor.dart';
import 'package:tmail_ui_user/features/composer/domain/usecases/save_email_as_drafts_interactor.dart';
import 'package:tmail_ui_user/features/composer/domain/usecases/send_email_interactor.dart';
import 'package:tmail_ui_user/features/composer/domain/usecases/update_email_drafts_interactor.dart';
Expand Down Expand Up @@ -184,6 +187,7 @@ class MailboxDashBoardController extends ReloadableController {
DeleteEmailStateToRefreshInteractor? _deleteEmailStateToRefreshInteractor;
GetMailboxStateToRefreshInteractor? _getMailboxStateToRefreshInteractor;
DeleteMailboxStateToRefreshInteractor? _deleteMailboxStateToRefreshInteractor;
GetAutoCompleteInteractor? _getAutoCompleteInteractor;

final scaffoldKey = GlobalKey<ScaffoldState>();
final selectedMailbox = Rxn<PresentationMailbox>();
Expand Down Expand Up @@ -2174,6 +2178,34 @@ class MailboxDashBoardController extends ReloadableController {
}
}

Future<List<EmailAddress>> getContactSuggestion(String query) async {
_getAutoCompleteInteractor = getBinding<GetAutoCompleteInteractor>();

if (_getAutoCompleteInteractor == null || accountId.value == null) {
return <EmailAddress>[];
}

final listEmailAddress = await _getAutoCompleteInteractor!
.execute(AutoCompletePattern(word: query, accountId: accountId.value!, limit: 2))
.then((value) => value.fold(
(failure) => <EmailAddress>[],
(success) => success is GetAutoCompleteSuccess ? success.listEmailAddress : <EmailAddress>[]
));
log('MailboxDashBoardController::getAutoCompleteSuggestion:listEmailAddress: $listEmailAddress');
return listEmailAddress;
}

void searchEmailByFromFields(BuildContext context, EmailAddress emailAddress) {
KeyboardUtils.hideKeyboard(context);
clearFilterMessageOption();
searchController.clearFilterSuggestion();
if (_searchInsideEmailDetailedViewIsActive(context)) {
_closeEmailDetailedView();
}
_unSelectedMailbox();
dispatchAction(SearchEmailByFromFieldsAction(emailAddress));
}

@override
void onClose() {
_emailReceiveManager.closeEmailReceiveManagerStream();
Expand Down
Loading
Loading