import 'dart:async'; import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; import 'package:core/presentation/utils/theme_utils.dart'; import 'package:core/utils/app_logger.dart'; import 'package:core/utils/platform_info.dart'; import 'package:dartz/dartz.dart'; import 'package:debounce_throttle/debounce_throttle.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; import 'package:model/autocomplete/auto_complete_pattern.dart'; import 'package:model/extensions/email_address_extension.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:tmail_ui_user/features/base/base_controller.dart'; import 'package:tmail_ui_user/features/base/mixin/auto_complete_result_mixin.dart'; import 'package:tmail_ui_user/features/composer/domain/model/contact_suggestion_source.dart'; import 'package:tmail_ui_user/features/composer/domain/usecases/get_all_autocomplete_interactor.dart'; import 'package:tmail_ui_user/features/composer/domain/usecases/get_autocomplete_interactor.dart'; import 'package:tmail_ui_user/features/composer/domain/usecases/get_device_contact_suggestions_interactor.dart'; import 'package:tmail_ui_user/features/contact/presentation/model/contact_arguments.dart'; import 'package:tmail_ui_user/features/contact/presentation/widgets/contact_suggestion_box_item.dart'; import 'package:tmail_ui_user/features/thread/domain/model/search_query.dart'; import 'package:tmail_ui_user/features/thread/domain/state/search_email_state.dart'; import 'package:tmail_ui_user/features/thread/presentation/model/search_status.dart'; import 'package:tmail_ui_user/main/routes/route_navigation.dart'; import 'package:tmail_ui_user/main/utils/app_config.dart'; class ContactController extends BaseController with AutoCompleteResultMixin { final TextEditingController textInputSearchController = TextEditingController(); final FocusNode textInputSearchFocus = FocusNode(); ContactSuggestionSource _contactSuggestionSource = ContactSuggestionSource.tMailContact; final searchQuery = SearchQuery.initial().obs; final searchedContactList = RxList(); final selectedContactList = RxList(); final contactArguments = Rxn(); final searchStatus = SearchStatus.INACTIVE.obs; final searchViewState = Rx>(Right(UIState.idle)); GetAllAutoCompleteInteractor? _getAllAutoCompleteInteractor; GetAutoCompleteInteractor? _getAutoCompleteInteractor; GetDeviceContactSuggestionsInteractor? _getDeviceContactSuggestionsInteractor; final Debouncer _deBouncerTime = Debouncer( const Duration(milliseconds: 300), initialValue: '' ); AccountId? _accountId; SelectedContactCallbackAction? onSelectedContactCallback; VoidCallback? onDismissContactView; StreamSubscription? _deBouncerTimeStreamSubscription; late int _minInputLengthAutocomplete; @override void onInit() { super.onInit(); ThemeUtils.setStatusBarTransparentColor(); log('ContactController::onInit():arguments: ${Get.arguments}'); contactArguments.value = Get.arguments; _deBouncerTimeStreamSubscription = _deBouncerTime.values.listen(_handleDeBounceTimeSearchContact); textInputSearchFocus.addListener(_onSearchInputTextFocusListener); } @override void onReady() { super.onReady(); if (contactArguments.value != null) { _accountId = contactArguments.value!.accountId; selectedContactList.value = contactArguments.value!.selectedContactList .map((mailAddress) => EmailAddress(null, mailAddress)) .toList(); injectAutoCompleteBindings(contactArguments.value!.session, _accountId); if (selectedContactList.isEmpty) { textInputSearchFocus.requestFocus(); } } _minInputLengthAutocomplete = _fetchMinInputLengthAutocomplete(); if (PlatformInfo.isMobile) { Future.delayed( const Duration(milliseconds: 500), () => _checkContactPermission()); } } @override void onClose() { log('ContactController::onClose():'); searchViewState.value = Right(UIState.idle); textInputSearchFocus.removeListener(_onSearchInputTextFocusListener); textInputSearchFocus.dispose(); textInputSearchController.dispose(); _deBouncerTimeStreamSubscription?.cancel(); _deBouncerTime.cancel(); _accountId = null; super.onClose(); } void _onSearchInputTextFocusListener() { if (textInputSearchFocus.hasFocus && textInputSearchController.text.trim().isEmpty) { searchStatus.value = SearchStatus.INACTIVE; } } void onTextSearchChange(String text) { _deBouncerTime.value = text; } void onSearchTextAction(String text) { _deBouncerTime.value = text; } Future _handleDeBounceTimeSearchContact(String value) async { final queryStringTrimmed = value.trim(); if (queryStringTrimmed.length < _minInputLengthAutocomplete) { searchStatus.value = SearchStatus.INACTIVE; return; } searchStatus.value = SearchStatus.ACTIVE; searchViewState.value = Right(SearchingState()); searchQuery.value = SearchQuery(queryStringTrimmed); await _searchContactByNameOrEmail(searchQuery.value.value); searchViewState.value = Right(UIState.idle); } void clearAllTextInputSearchForm() { textInputSearchController.clear(); searchQuery.value = SearchQuery.initial(); searchedContactList.clear(); textInputSearchFocus.requestFocus(); } void _checkContactPermission() async { final permissionStatus = await Permission.contacts.status; if (permissionStatus.isGranted) { _contactSuggestionSource = ContactSuggestionSource.all; } else if (!permissionStatus.isPermanentlyDenied) { final requestedPermission = await Permission.contacts.request(); _contactSuggestionSource = requestedPermission == PermissionStatus.granted ? ContactSuggestionSource.all : _contactSuggestionSource; } } Future _searchContactByNameOrEmail(String query) async { log('ContactController::_searchContactByNameOrEmail(): query: $query'); final listContact = await _getAutoCompleteSuggestion(query); if (query.isEmail && !listContact.any((emailAddress) => emailAddress.email == query)) { listContact.add(EmailAddress(null, query)); } searchedContactList.value = listContact; } Future> _getAutoCompleteSuggestion(String queryString) async { _getAllAutoCompleteInteractor = getBinding(); _getAutoCompleteInteractor = getBinding(); _getDeviceContactSuggestionsInteractor = getBinding(); final autoCompletePattern = AutoCompletePattern( word: queryString, accountId: _accountId); if (_contactSuggestionSource == ContactSuggestionSource.all) { if (_getAllAutoCompleteInteractor != null) { return await _getAllAutoCompleteInteractor! .execute(autoCompletePattern) .then((value) => handleAutoCompleteResultState( resultState: value, queryString: queryString, onFailureCallback: (failure) => consumeState(Stream.value(Left(failure))), ) ); } else if (_getDeviceContactSuggestionsInteractor != null) { return await _getDeviceContactSuggestionsInteractor! .execute(autoCompletePattern) .then((value) => handleAutoCompleteResultState( resultState: value, queryString: queryString, ) ); } else { return []; } } else { return await _getAutoCompleteInteractor ?.execute(autoCompletePattern) .then((value) => handleAutoCompleteResultState( resultState: value, queryString: queryString, onFailureCallback: (failure) => consumeState(Stream.value(Left(failure))), ) ) ?? []; } } void handleOnSelectContactAction(EmailAddress emailAddress) { log('ContactController::selectContact:emailAddress = $emailAddress'); final isEmailAddressExist = selectedContactList .any((contact) => contact.emailAddress == emailAddress.emailAddress); log('ContactController::selectContact:isEmailAddressExist = $isEmailAddressExist'); if (!isEmailAddressExist) { selectedContactList.add(emailAddress); } } void handleOnDeleteContactAction(EmailAddress emailAddress) { log('ContactController::handleOnDeleteContactAction:emailAddress = $emailAddress'); selectedContactList.removeWhere((contact) => contact.emailAddress == emailAddress.emailAddress); } void closeContactView() { textInputSearchFocus.unfocus(); FocusManager.instance.primaryFocus?.unfocus(); popBack(); } void handleOnClearFilterAction() { selectedContactList.clear(); textInputSearchFocus.unfocus(); FocusManager.instance.primaryFocus?.unfocus(); popBack(result: selectedContactList); } void handleOnDoneAction() { textInputSearchFocus.unfocus(); FocusManager.instance.primaryFocus?.unfocus(); popBack(result: selectedContactList); } void handleOnSearchBackAction() { textInputSearchController.clear(); searchQuery.value = SearchQuery.initial(); searchedContactList.clear(); textInputSearchFocus.unfocus(); FocusManager.instance.primaryFocus?.unfocus(); searchStatus.value = SearchStatus.INACTIVE; } int _fetchMinInputLengthAutocomplete() { if (contactArguments.value?.session == null || _accountId == null) { return AppConfig.defaultMinInputLengthAutocomplete; } return getMinInputLengthAutocomplete( session: contactArguments.value!.session, accountId: _accountId!); } }