From 712bc26402e7efae800b1b4c03d5acb09b4cadf0 Mon Sep 17 00:00:00 2001 From: dab246 Date: Thu, 3 Apr 2025 14:43:31 +0700 Subject: [PATCH] TF-3507 Fix offline cache prevents updating email query view cache Signed-off-by: dab246 --- contact/pubspec.lock | 8 + core/lib/core.dart | 1 + .../pull_to_refresh_widget.dart | 153 +++++++++++++++ core/pubspec.lock | 8 + core/pubspec.yaml | 2 + .../data/local/state_cache_manager.dart | 5 + .../bindings/mailbox_dashboard_bindings.dart | 9 +- .../data/local/fcm_cache_manager.dart | 17 ++ .../data/datasource/thread_datasource.dart | 2 + .../local_thread_datasource_impl.dart | 33 +++- .../thread_datasource_impl.dart | 5 + .../data/local/email_cache_manager.dart | 4 + .../repository/thread_repository_impl.dart | 9 + .../domain/repository/thread_repository.dart | 2 + .../state/clean_and_get_all_email_state.dart | 9 + ..._and_get_emails_in_mailbox_interactor.dart | 54 ++++++ ..._pull_to_refresh_list_email_extension.dart | 53 ++++++ .../thread/presentation/thread_bindings.dart | 16 +- .../presentation/thread_controller.dart | 45 +++-- .../thread/presentation/thread_view.dart | 177 ++++++++++-------- .../thread_view_loading_bar_widget.dart | 5 +- lib/l10n/intl_messages.arb | 56 +++++- lib/main/localizations/app_localizations.dart | 56 ++++++ model/pubspec.lock | 8 + pubspec.lock | 8 + .../mailbox_dashboard_controller_test.dart | 10 +- .../mailbox_dashboard_view_widget_test.dart | 25 +-- ...fore_time_in_search_email_filter_test.dart | 5 + .../controller/thread_controller_test.dart | 8 +- 29 files changed, 662 insertions(+), 131 deletions(-) create mode 100644 core/lib/presentation/views/pull_to_refresh/pull_to_refresh_widget.dart create mode 100644 lib/features/thread/domain/state/clean_and_get_all_email_state.dart create mode 100644 lib/features/thread/domain/usecases/clean_and_get_emails_in_mailbox_interactor.dart create mode 100644 lib/features/thread/presentation/extensions/handle_pull_to_refresh_list_email_extension.dart diff --git a/contact/pubspec.lock b/contact/pubspec.lock index ae6919613..297b88192 100644 --- a/contact/pubspec.lock +++ b/contact/pubspec.lock @@ -968,6 +968,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.3" + pull_to_refresh: + dependency: transitive + description: + name: pull_to_refresh + sha256: bbadd5a931837b57739cf08736bea63167e284e71fb23b218c8c9a6e042aad12 + url: "https://pub.dev" + source: hosted + version: "2.0.0" qr: dependency: transitive description: diff --git a/core/lib/core.dart b/core/lib/core.dart index 3062bdfc9..79cdfdeb4 100644 --- a/core/lib/core.dart +++ b/core/lib/core.dart @@ -97,6 +97,7 @@ export 'presentation/views/clipper/side_arrow_clipper.dart'; export 'presentation/views/avatar/gradient_circle_avatar_icon.dart'; export 'presentation/views/loading/cupertino_loading_widget.dart'; export 'presentation/views/image/image_loader_mixin.dart'; +export 'presentation/views/pull_to_refresh/pull_to_refresh_widget.dart'; // Resources export 'presentation/resources/assets_paths.dart'; diff --git a/core/lib/presentation/views/pull_to_refresh/pull_to_refresh_widget.dart b/core/lib/presentation/views/pull_to_refresh/pull_to_refresh_widget.dart new file mode 100644 index 000000000..1cf7fe032 --- /dev/null +++ b/core/lib/presentation/views/pull_to_refresh/pull_to_refresh_widget.dart @@ -0,0 +1,153 @@ +import 'dart:ui'; + +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/presentation/utils/theme_utils.dart'; +import 'package:flutter/material.dart'; +import 'package:pull_to_refresh/pull_to_refresh.dart'; + +class PullToRefreshWidget extends StatefulWidget { + final Widget child; + final Future Function() onNormalRefresh; + final Future Function() onDeepRefresh; + final double deepRefreshThreshold; + final String normalRefreshText; + final String deepRefreshText; + final String pullDownToRefreshText; + final String refreshingText; + final String pullingText; + final String releaseToText; + final String pullFurtherForText; + final String pullHarderForText; + final Icon? normalRefreshIcon; + final Icon? deepRefreshIcon; + + const PullToRefreshWidget({ + Key? key, + required this.child, + required this.onNormalRefresh, + required this.onDeepRefresh, + this.deepRefreshThreshold = 200.0, + this.normalRefreshText = 'Refresh', + this.deepRefreshText = 'Deep refresh', + this.pullDownToRefreshText = 'Pull down to refresh', + this.refreshingText = 'Refreshing', + this.pullingText = 'Pulling', + this.releaseToText = 'Release to', + this.pullFurtherForText = 'Pull further for', + this.pullHarderForText = 'Pull harder for', + this.normalRefreshIcon = const Icon(Icons.refresh, size: 20), + this.deepRefreshIcon = const Icon(Icons.autorenew, size: 20), + }) : super(key: key); + + @override + State createState() => _PullToRefreshWidgetState(); +} + +class _PullToRefreshWidgetState extends State { + final RefreshController _refreshController = RefreshController(); + double _scrollOffset = 0.0; + String? _lastAction; + + @override + Widget build(BuildContext context) { + return NotificationListener( + onNotification: (notification) { + if (notification is ScrollUpdateNotification) { + setState(() { + _scrollOffset = notification.metrics.pixels; + }); + } + return false; + }, + child: SmartRefresher( + controller: _refreshController, + enablePullDown: true, + enablePullUp: false, + header: CustomHeader( + builder: (context, mode) { + String statusText; + Icon? leadingIcon; + + if (mode == RefreshStatus.idle) { + statusText = widget.pullDownToRefreshText; + leadingIcon = widget.normalRefreshIcon; + } else if (mode == RefreshStatus.canRefresh) { + if (_scrollOffset.abs() > widget.deepRefreshThreshold) { + statusText = '${widget.releaseToText} ${widget.deepRefreshText}'; + leadingIcon = widget.deepRefreshIcon; + } else { + statusText = '${widget.releaseToText} ${widget.normalRefreshText}'; + leadingIcon = widget.normalRefreshIcon; + } + } else if (mode == RefreshStatus.refreshing || mode == RefreshStatus.completed) { + statusText = '${widget.refreshingText}...'; + leadingIcon = _lastAction == widget.deepRefreshText + ? widget.deepRefreshIcon + : widget.normalRefreshIcon; + } else { + statusText = '${widget.pullingText}...'; + leadingIcon = widget.normalRefreshIcon; + } + + return SizedBox( + height: clampDouble(_scrollOffset.abs(), 60, widget.deepRefreshThreshold), + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (leadingIcon != null) leadingIcon, + Flexible( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Text( + statusText, + style: ThemeUtils.textStyleBodyBody1(color: Colors.black), + ), + ), + ), + ], + ), + if (mode == RefreshStatus.canRefresh) + Padding( + padding: const EdgeInsetsDirectional.only(top: 4, start: 8, end: 8), + child: Text( + _scrollOffset.abs() > widget.deepRefreshThreshold + ? '${widget.pullFurtherForText} ${widget.deepRefreshText.toLowerCase()}' + : '${widget.pullHarderForText} ${widget.deepRefreshText.toLowerCase()}', + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: AppColor.steelGray400, + ), + ), + ), + ], + ), + ), + ); + }, + height: widget.deepRefreshThreshold, + ), + onRefresh: () { + if (_scrollOffset.abs() > widget.deepRefreshThreshold) { + _lastAction = widget.deepRefreshText; + widget.onDeepRefresh(); + } else { + _lastAction = widget.normalRefreshText; + widget.onNormalRefresh(); + } + _refreshController.refreshCompleted(); + }, + child: widget.child, + ), + ); + } + + @override + void dispose() { + _refreshController.dispose(); + super.dispose(); + } +} \ No newline at end of file diff --git a/core/pubspec.lock b/core/pubspec.lock index 81b3cab43..853dfb85a 100644 --- a/core/pubspec.lock +++ b/core/pubspec.lock @@ -929,6 +929,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" + pull_to_refresh: + dependency: "direct main" + description: + name: pull_to_refresh + sha256: bbadd5a931837b57739cf08736bea63167e284e71fb23b218c8c9a6e042aad12 + url: "https://pub.dev" + source: hosted + version: "2.0.0" qr: dependency: transitive description: diff --git a/core/pubspec.yaml b/core/pubspec.yaml index 01c916022..a0fa2c7e6 100644 --- a/core/pubspec.yaml +++ b/core/pubspec.yaml @@ -102,6 +102,8 @@ dependencies: win32: 5.5.1 + pull_to_refresh: 2.0.0 + dev_dependencies: flutter_test: sdk: flutter diff --git a/lib/features/mailbox/data/local/state_cache_manager.dart b/lib/features/mailbox/data/local/state_cache_manager.dart index 31409a174..8d9b3e7f3 100644 --- a/lib/features/mailbox/data/local/state_cache_manager.dart +++ b/lib/features/mailbox/data/local/state_cache_manager.dart @@ -25,5 +25,10 @@ class StateCacheManager { return await _stateCacheClient.insertItem(stateKey, stateCache); } + Future deleteState(AccountId accountId, UserName userName, StateType stateType) async { + final stateKey = TupleKey(stateType.name, accountId.asString, userName.value).encodeKey; + return await _stateCacheClient.deleteItem(stateKey); + } + Future closeStateHiveCacheBox() => _stateCacheClient.closeBox(); } \ No newline at end of file diff --git a/lib/features/mailbox_dashboard/presentation/bindings/mailbox_dashboard_bindings.dart b/lib/features/mailbox_dashboard/presentation/bindings/mailbox_dashboard_bindings.dart index c0ee57225..5d1562fd0 100644 --- a/lib/features/mailbox_dashboard/presentation/bindings/mailbox_dashboard_bindings.dart +++ b/lib/features/mailbox_dashboard/presentation/bindings/mailbox_dashboard_bindings.dart @@ -103,6 +103,7 @@ import 'package:tmail_ui_user/features/offline_mode/manager/new_email_cache_work import 'package:tmail_ui_user/features/offline_mode/manager/opened_email_cache_manager.dart'; import 'package:tmail_ui_user/features/offline_mode/manager/opened_email_cache_worker_queue.dart'; import 'package:tmail_ui_user/features/offline_mode/manager/sending_email_cache_manager.dart'; +import 'package:tmail_ui_user/features/push_notification/data/local/fcm_cache_manager.dart'; import 'package:tmail_ui_user/features/quotas/presentation/quotas_bindings.dart'; import 'package:tmail_ui_user/features/search/email/domain/usecases/refresh_changes_search_email_interactor.dart'; import 'package:tmail_ui_user/features/search/email/presentation/search_email_bindings.dart'; @@ -236,7 +237,13 @@ class MailboxDashBoardBindings extends BaseBindings { Get.find(), Get.find(), Get.find())); - Get.lazyPut(() => LocalThreadDataSourceImpl(Get.find(), Get.find())); + Get.lazyPut(() => LocalThreadDataSourceImpl( + Get.find(), + Get.find(), + Get.find(), + Get.find(), + Get.find(), + )); Get.lazyPut(() => StateDataSourceImpl( Get.find(), Get.find(), diff --git a/lib/features/push_notification/data/local/fcm_cache_manager.dart b/lib/features/push_notification/data/local/fcm_cache_manager.dart index c1c5ab611..7a2848fd6 100644 --- a/lib/features/push_notification/data/local/fcm_cache_manager.dart +++ b/lib/features/push_notification/data/local/fcm_cache_manager.dart @@ -1,5 +1,6 @@ import 'package:fcm/model/type_name.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/core/state.dart' as jmap; import 'package:jmap_dart_client/jmap/core/user_name.dart'; import 'package:model/extensions/account_id_extensions.dart'; @@ -62,4 +63,20 @@ class FCMCacheManager { Future deleteFirebaseRegistration() async { await _firebaseRegistrationCacheClient.deleteItem(FirebaseRegistrationCache.keyCacheValue); } + + Future clearAllEmailState( + AccountId accountId, + Session session, + ) async { + await deleteStateToRefresh( + accountId, + session.username, + TypeName.emailDelivery, + ); + await deleteStateToRefresh( + accountId, + session.username, + TypeName.emailType, + ); + } } \ No newline at end of file diff --git a/lib/features/thread/data/datasource/thread_datasource.dart b/lib/features/thread/data/datasource/thread_datasource.dart index 697ead420..2ce28b889 100644 --- a/lib/features/thread/data/datasource/thread_datasource.dart +++ b/lib/features/thread/data/datasource/thread_datasource.dart @@ -76,4 +76,6 @@ abstract class ThreadDataSource { ); Future getEmailById(Session session, AccountId accountId, EmailId emailId, {Properties? properties}); + + Future clearEmailCacheAndStateCache(AccountId accountId, Session session); } \ No newline at end of file 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 c563fc0e7..0b5944769 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 @@ -2,6 +2,8 @@ import 'dart:async'; import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; +import 'package:core/utils/file_utils.dart'; +import 'package:core/utils/platform_info.dart'; import 'package:dartz/dartz.dart' as dartz; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/filter/filter.dart'; @@ -14,6 +16,10 @@ import 'package:jmap_dart_client/jmap/core/user_name.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; import 'package:model/email/presentation_email.dart'; +import 'package:tmail_ui_user/features/caching/utils/caching_constants.dart'; +import 'package:tmail_ui_user/features/mailbox/data/local/state_cache_manager.dart'; +import 'package:tmail_ui_user/features/mailbox/data/model/state_type.dart'; +import 'package:tmail_ui_user/features/push_notification/data/local/fcm_cache_manager.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'; @@ -25,9 +31,18 @@ import 'package:tmail_ui_user/main/exceptions/exception_thrower.dart'; class LocalThreadDataSourceImpl extends ThreadDataSource { final EmailCacheManager _emailCacheManager; + final FCMCacheManager _fcmCacheManager; + final StateCacheManager _stateCacheManager; + final FileUtils _fileUtils; final ExceptionThrower _exceptionThrower; - LocalThreadDataSourceImpl(this._emailCacheManager, this._exceptionThrower); + LocalThreadDataSourceImpl( + this._emailCacheManager, + this._fcmCacheManager, + this._stateCacheManager, + this._fileUtils, + this._exceptionThrower, + ); @override Future getAllEmail( @@ -125,4 +140,20 @@ class LocalThreadDataSourceImpl extends ThreadDataSource { Future getEmailById(Session session, AccountId accountId, EmailId emailId, {Properties? properties}) { throw UnimplementedError(); } + + @override + Future clearEmailCacheAndStateCache(AccountId accountId, Session session) { + return Future.sync(() async { + return await Future.wait([ + _stateCacheManager.deleteState(accountId, session.username, StateType.email), + _emailCacheManager.clearAll(), + if (PlatformInfo.isMobile) + ...[ + _fileUtils.removeFolder(CachingConstants.newEmailsContentFolderName), + _fileUtils.removeFolder(CachingConstants.openedEmailContentFolderName), + _fcmCacheManager.clearAllEmailState(accountId, session), + ], + ]); + }).catchError(_exceptionThrower.throwException); + } } \ No newline at end of file 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 490b1677e..1f4f155e5 100644 --- a/lib/features/thread/data/datasource_impl/thread_datasource_impl.dart +++ b/lib/features/thread/data/datasource_impl/thread_datasource_impl.dart @@ -140,4 +140,9 @@ class ThreadDataSourceImpl extends ThreadDataSource { return email.toPresentationEmail(); }).catchError(_exceptionThrower.throwException); } + + @override + Future clearEmailCacheAndStateCache(AccountId accountId, Session session) { + throw UnimplementedError(); + } } \ No newline at end of file diff --git a/lib/features/thread/data/local/email_cache_manager.dart b/lib/features/thread/data/local/email_cache_manager.dart index 47bfe07eb..fe5b17683 100644 --- a/lib/features/thread/data/local/email_cache_manager.dart +++ b/lib/features/thread/data/local/email_cache_manager.dart @@ -90,6 +90,10 @@ class EmailCacheManager { await _emailCacheClient.deleteMultipleItem(listEmailIdCacheExpire); } + Future clearAll() async { + return await _emailCacheClient.clearAllData(); + } + Future storeEmail(AccountId accountId, UserName userName, EmailCache emailCache) { final keyCache = TupleKey(emailCache.id, accountId.asString, userName.value).encodeKey; return _emailCacheClient.insertItem(keyCache, emailCache); diff --git a/lib/features/thread/data/repository/thread_repository_impl.dart b/lib/features/thread/data/repository/thread_repository_impl.dart index 72aea4364..f18768e90 100644 --- a/lib/features/thread/data/repository/thread_repository_impl.dart +++ b/lib/features/thread/data/repository/thread_repository_impl.dart @@ -432,4 +432,13 @@ class ThreadRepositoryImpl extends ThreadRepository { return listEmailIdDeleted; } + + @override + Future clearEmailCacheAndStateCache( + AccountId accountId, + Session session, + ) => mapDataSource[DataSourceType.local]!.clearEmailCacheAndStateCache( + accountId, + session, + ); } \ 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 ec0503061..4303abd02 100644 --- a/lib/features/thread/domain/repository/thread_repository.dart +++ b/lib/features/thread/domain/repository/thread_repository.dart @@ -80,4 +80,6 @@ abstract class ThreadRepository { int totalEmails, StreamController> onProgressController ); + + Future clearEmailCacheAndStateCache(AccountId accountId, Session session); } \ No newline at end of file diff --git a/lib/features/thread/domain/state/clean_and_get_all_email_state.dart b/lib/features/thread/domain/state/clean_and_get_all_email_state.dart new file mode 100644 index 000000000..f7412ce2e --- /dev/null +++ b/lib/features/thread/domain/state/clean_and_get_all_email_state.dart @@ -0,0 +1,9 @@ +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; + +class CleanAndGetAllEmailLoading extends LoadingState {} + +class CleanAndGetAllEmailFailure extends FeatureFailure { + + CleanAndGetAllEmailFailure(dynamic exception) : super(exception: exception); +} \ No newline at end of file diff --git a/lib/features/thread/domain/usecases/clean_and_get_emails_in_mailbox_interactor.dart b/lib/features/thread/domain/usecases/clean_and_get_emails_in_mailbox_interactor.dart new file mode 100644 index 000000000..b680d7341 --- /dev/null +++ b/lib/features/thread/domain/usecases/clean_and_get_emails_in_mailbox_interactor.dart @@ -0,0 +1,54 @@ +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:dartz/dartz.dart'; +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/session/session.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/repository/thread_repository.dart'; +import 'package:tmail_ui_user/features/thread/domain/state/clean_and_get_all_email_state.dart'; +import 'package:tmail_ui_user/features/thread/domain/usecases/get_emails_in_mailbox_interactor.dart'; + +class CleanAndGetEmailsInMailboxInteractor { + final GetEmailsInMailboxInteractor _getEmailsInMailboxInteractor; + final ThreadRepository _threadRepository; + + const CleanAndGetEmailsInMailboxInteractor( + this._threadRepository, + this._getEmailsInMailboxInteractor, + ); + + Stream> execute( + Session session, + AccountId accountId, + { + UnsignedInt? limit, + Set? sort, + EmailFilter? emailFilter, + Properties? propertiesCreated, + Properties? propertiesUpdated, + bool getLatestChanges = true, + } + ) async* { + try { + yield Right(CleanAndGetAllEmailLoading()); + + await _threadRepository.clearEmailCacheAndStateCache(accountId, session); + + yield* _getEmailsInMailboxInteractor.execute( + session, + accountId, + limit: limit, + sort: sort, + emailFilter: emailFilter, + propertiesCreated: propertiesCreated, + propertiesUpdated: propertiesUpdated, + getLatestChanges: getLatestChanges, + ); + } catch (e) { + yield Left(CleanAndGetAllEmailFailure(e)); + } + } +} \ No newline at end of file diff --git a/lib/features/thread/presentation/extensions/handle_pull_to_refresh_list_email_extension.dart b/lib/features/thread/presentation/extensions/handle_pull_to_refresh_list_email_extension.dart new file mode 100644 index 000000000..b4fbac0ab --- /dev/null +++ b/lib/features/thread/presentation/extensions/handle_pull_to_refresh_list_email_extension.dart @@ -0,0 +1,53 @@ + +import 'package:dartz/dartz.dart'; +import 'package:rich_text_composer/views/commons/logger.dart'; +import 'package:tmail_ui_user/features/email/presentation/utils/email_utils.dart'; +import 'package:tmail_ui_user/features/home/data/exceptions/session_exceptions.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/search/email_sort_order_type.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/state/clean_and_get_all_email_state.dart'; +import 'package:tmail_ui_user/features/thread/presentation/model/loading_more_status.dart'; +import 'package:tmail_ui_user/features/thread/presentation/thread_controller.dart'; + +extension HandlePullToRefreshListEmailExtension on ThreadController { + + Future onRefresh() async { + log('HandlePullToRefreshListEmailExtension::onRefresh:Started'); + canLoadMore = false; + loadingMoreStatus.value == LoadingMoreStatus.idle; + getAllEmailAction(); + } + + Future onCleanAndRefresh() async { + log('HandlePullToRefreshListEmailExtension::onCleanAndRefresh:Started'); + canLoadMore = false; + loadingMoreStatus.value == LoadingMoreStatus.idle; + cleanAndGetAllEmailAction(); + } + + Future cleanAndGetAllEmailAction() async { + final accountId = mailboxDashBoardController.accountId.value; + final session = mailboxDashBoardController.sessionCurrent; + + if (accountId == null || session == null) { + consumeState(Stream.value(Left(CleanAndGetAllEmailFailure(NotFoundSessionException())))); + return; + } + + consumeState(cleanAndGetEmailsInMailboxInteractor.execute( + session, + accountId, + limit: ThreadConstants.defaultLimit, + sort: EmailSortOrderType.mostRecent.getSortOrder().toNullable(), + emailFilter: EmailFilter( + filter: getFilterCondition(mailboxIdSelected: selectedMailboxId), + filterOption: mailboxDashBoardController.filterMessageOption.value, + mailboxId: selectedMailboxId, + ), + propertiesCreated: EmailUtils.getPropertiesForEmailGetMethod(session, accountId), + propertiesUpdated: ThreadConstants.propertiesUpdatedDefault, + getLatestChanges: true, + )); + } +} \ No newline at end of file diff --git a/lib/features/thread/presentation/thread_bindings.dart b/lib/features/thread/presentation/thread_bindings.dart index a979a6f19..8874aef21 100644 --- a/lib/features/thread/presentation/thread_bindings.dart +++ b/lib/features/thread/presentation/thread_bindings.dart @@ -1,10 +1,12 @@ import 'package:core/data/model/source_type/data_source_type.dart'; +import 'package:core/utils/file_utils.dart'; import 'package:get/get.dart'; import 'package:tmail_ui_user/features/base/base_bindings.dart'; import 'package:tmail_ui_user/features/email/domain/repository/email_repository.dart'; import 'package:tmail_ui_user/features/mailbox/data/datasource/state_datasource.dart'; import 'package:tmail_ui_user/features/mailbox/data/datasource_impl/state_datasource_impl.dart'; import 'package:tmail_ui_user/features/mailbox/data/local/state_cache_manager.dart'; +import 'package:tmail_ui_user/features/push_notification/data/local/fcm_cache_manager.dart'; import 'package:tmail_ui_user/features/thread/data/datasource/thread_datasource.dart'; import 'package:tmail_ui_user/features/thread/data/datasource_impl/local_thread_datasource_impl.dart'; import 'package:tmail_ui_user/features/thread/data/datasource_impl/thread_datasource_impl.dart'; @@ -13,6 +15,7 @@ import 'package:tmail_ui_user/features/thread/data/network/thread_api.dart'; import 'package:tmail_ui_user/features/thread/data/network/thread_isolate_worker.dart'; import 'package:tmail_ui_user/features/thread/data/repository/thread_repository_impl.dart'; import 'package:tmail_ui_user/features/thread/domain/repository/thread_repository.dart'; +import 'package:tmail_ui_user/features/thread/domain/usecases/clean_and_get_emails_in_mailbox_interactor.dart'; import 'package:tmail_ui_user/features/thread/domain/usecases/get_email_by_id_interactor.dart'; import 'package:tmail_ui_user/features/thread/domain/usecases/get_emails_in_mailbox_interactor.dart'; import 'package:tmail_ui_user/features/thread/domain/usecases/load_more_emails_in_mailbox_interactor.dart'; @@ -35,6 +38,7 @@ class ThreadBindings extends BaseBindings { Get.find(), Get.find(), Get.find(), + Get.find(), )); } @@ -50,7 +54,13 @@ class ThreadBindings extends BaseBindings { Get.find(), Get.find(), Get.find())); - Get.lazyPut(() => LocalThreadDataSourceImpl(Get.find(), Get.find())); + Get.lazyPut(() => LocalThreadDataSourceImpl( + Get.find(), + Get.find(), + Get.find(), + Get.find(), + Get.find(), + )); Get.lazyPut(() => StateDataSourceImpl( Get.find(), Get.find(), @@ -66,6 +76,10 @@ class ThreadBindings extends BaseBindings { Get.lazyPut(() => SearchEmailInteractor(Get.find())); Get.lazyPut(() => SearchMoreEmailInteractor(Get.find())); Get.lazyPut(() => GetEmailByIdInteractor(Get.find(), Get.find())); + Get.lazyPut(() => CleanAndGetEmailsInMailboxInteractor( + Get.find(), + Get.find(), + )); } @override diff --git a/lib/features/thread/presentation/thread_controller.dart b/lib/features/thread/presentation/thread_controller.dart index c9bb7a9d8..ed1f07a63 100644 --- a/lib/features/thread/presentation/thread_controller.dart +++ b/lib/features/thread/presentation/thread_controller.dart @@ -48,6 +48,7 @@ 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/clean_and_get_all_email_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/get_all_email_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/get_email_by_id_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/load_more_emails_state.dart'; @@ -56,6 +57,7 @@ import 'package:tmail_ui_user/features/thread/domain/state/refresh_all_email_sta import 'package:tmail_ui_user/features/thread/domain/state/refresh_changes_all_email_state.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/domain/usecases/clean_and_get_emails_in_mailbox_interactor.dart'; import 'package:tmail_ui_user/features/thread/domain/usecases/get_email_by_id_interactor.dart'; import 'package:tmail_ui_user/features/thread/domain/usecases/get_emails_in_mailbox_interactor.dart'; import 'package:tmail_ui_user/features/thread/domain/usecases/load_more_emails_in_mailbox_interactor.dart'; @@ -89,6 +91,7 @@ class ThreadController extends BaseController with EmailActionController { final SearchEmailInteractor _searchEmailInteractor; final SearchMoreEmailInteractor _searchMoreEmailInteractor; final GetEmailByIdInteractor _getEmailByIdInteractor; + final CleanAndGetEmailsInMailboxInteractor cleanAndGetEmailsInMailboxInteractor; CreateNewEmailRuleFilterInteractor? _createNewEmailRuleFilterInteractor; @@ -101,11 +104,11 @@ class ThreadController extends BaseController with EmailActionController { bool canSearchMore = false; MailboxId? _currentMemoryMailboxId; final ScrollController listEmailController = ScrollController(); - final FocusNode focusNodeKeyBoard = FocusNode(); final latestEmailSelectedOrUnselected = Rxn(); @visibleForTesting bool isListEmailScrollViewJumping = false; WebSocketQueueHandler? _webSocketQueueHandler; + FocusNode? focusNodeKeyBoard; StreamSubscription? _resizeBrowserStreamSubscription; @@ -130,12 +133,14 @@ class ThreadController extends BaseController with EmailActionController { this._searchEmailInteractor, this._searchMoreEmailInteractor, this._getEmailByIdInteractor, + this.cleanAndGetEmailsInMailboxInteractor, ); @override void onInit() { _registerObxStreamListener(); if (PlatformInfo.isWeb) { + focusNodeKeyBoard = FocusNode(); _registerBrowserResizeListener(); } _initWebSocketQueueHandler(); @@ -152,7 +157,8 @@ class ThreadController extends BaseController with EmailActionController { void onClose() { _currentMemoryMailboxId = null; listEmailController.dispose(); - focusNodeKeyBoard.dispose(); + focusNodeKeyBoard?.dispose(); + focusNodeKeyBoard = null; if (PlatformInfo.isWeb) { _resizeBrowserStreamSubscription?.cancel(); } @@ -211,7 +217,7 @@ class ThreadController extends BaseController with EmailActionController { } else if (failure is GetEmailByIdFailure) { openingEmail.value = false; popAndPush(AppRoutes.unknownRoutePage); - } else if (failure is GetAllEmailFailure) { + } else if (failure is GetAllEmailFailure || failure is CleanAndGetAllEmailFailure) { mailboxDashBoardController.updateRefreshAllEmailState(Left(RefreshAllEmailFailure())); canLoadMore = true; } @@ -262,7 +268,7 @@ class ThreadController extends BaseController with EmailActionController { _currentMemoryMailboxId = mailbox.id; consumeState(Stream.value(Right(GetAllEmailLoading()))); _resetToOriginalValue(); - _getAllEmailAction( + getAllEmailAction( getLatestChanges: mailboxDashBoardController.isFirstSessionLoad, ); mailboxDashBoardController.setIsFirstSessionLoad(false); @@ -542,7 +548,7 @@ class ThreadController extends BaseController with EmailActionController { } } - void _getAllEmailAction({ + void getAllEmailAction({ bool getLatestChanges = true, }) { log('ThreadController::_getAllEmailAction:getLatestChanges = $getLatestChanges'); @@ -553,7 +559,7 @@ class ThreadController extends BaseController with EmailActionController { limit: ThreadConstants.defaultLimit, sort: EmailSortOrderType.mostRecent.getSortOrder().toNullable(), emailFilter: EmailFilter( - filter: _getFilterCondition(mailboxIdSelected: selectedMailboxId), + filter: getFilterCondition(mailboxIdSelected: selectedMailboxId), filterOption: mailboxDashBoardController.filterMessageOption.value, mailboxId: selectedMailboxId ), @@ -566,16 +572,7 @@ class ThreadController extends BaseController with EmailActionController { } } - @visibleForTesting - EmailFilterCondition getFilterCondition({ - PresentationEmail? oldestEmail, - MailboxId? mailboxIdSelected, - }) => _getFilterCondition( - oldestEmail: oldestEmail, - mailboxIdSelected: mailboxIdSelected, - ); - - EmailFilterCondition _getFilterCondition({PresentationEmail? oldestEmail, MailboxId? mailboxIdSelected}) { + EmailFilterCondition getFilterCondition({PresentationEmail? oldestEmail, MailboxId? mailboxIdSelected}) { switch(mailboxDashBoardController.filterMessageOption.value) { case FilterMessageOption.all: return EmailFilterCondition( @@ -617,7 +614,7 @@ class ThreadController extends BaseController with EmailActionController { if (searchController.isSearchEmailRunning) { _searchEmail(limit: limitEmailFetched); } else { - _getAllEmailAction(); + getAllEmailAction(); } } @@ -689,7 +686,7 @@ class ThreadController extends BaseController with EmailActionController { position: _searchEmailFilter.position, sort: _searchEmailFilter.sortOrderType.getSortOrder().toNullable(), filter: _searchEmailFilter.mappingToEmailFilterCondition( - moreFilterCondition: _getFilterCondition(), + moreFilterCondition: getFilterCondition(), ), properties: EmailUtils.getPropertiesForEmailGetMethod( _session!, @@ -724,7 +721,7 @@ class ThreadController extends BaseController with EmailActionController { ), propertiesUpdated: ThreadConstants.propertiesUpdatedDefault, emailFilter: EmailFilter( - filter: _getFilterCondition(mailboxIdSelected: selectedMailboxId), + filter: getFilterCondition(mailboxIdSelected: selectedMailboxId), filterOption: mailboxDashBoardController.filterMessageOption.value, mailboxId: selectedMailboxId, ), @@ -759,7 +756,7 @@ class ThreadController extends BaseController with EmailActionController { limit: ThreadConstants.defaultLimit, sort: EmailSortOrderType.mostRecent.getSortOrder().toNullable(), filterOption: mailboxDashBoardController.filterMessageOption.value, - filter: _getFilterCondition(oldestEmail: oldestEmail, mailboxIdSelected: selectedMailboxId), + filter: getFilterCondition(oldestEmail: oldestEmail, mailboxIdSelected: selectedMailboxId), properties: EmailUtils.getPropertiesForEmailGetMethod(_session!, _accountId!), lastEmailId: oldestEmail?.id ) @@ -873,8 +870,8 @@ class ThreadController extends BaseController with EmailActionController { latestEmailSelectedOrUnselected.value = emailsInCurrentMailbox .firstWhereOrNull((e) => e.id == presentationEmailSelected.id); - if (!PlatformInfo.isMobile) { - focusNodeKeyBoard.requestFocus(); + if (PlatformInfo.isWeb) { + focusNodeKeyBoard?.requestFocus(); } if (_isUnSelectedAll()) { @@ -986,7 +983,7 @@ class ThreadController extends BaseController with EmailActionController { position: _searchEmailFilter.position, sort: _searchEmailFilter.sortOrderType.getSortOrder().toNullable(), filter: _searchEmailFilter.mappingToEmailFilterCondition( - moreFilterCondition: _getFilterCondition() + moreFilterCondition: getFilterCondition() ), properties: EmailUtils.getPropertiesForEmailGetMethod(_session!, _accountId!), needRefreshSearchState: needRefreshSearchState @@ -1066,7 +1063,7 @@ class ThreadController extends BaseController with EmailActionController { sort: _searchEmailFilter.sortOrderType.getSortOrder().toNullable(), position: _searchEmailFilter.position, filter: _searchEmailFilter.mappingToEmailFilterCondition( - moreFilterCondition: _getFilterCondition() + moreFilterCondition: getFilterCondition() ), properties: EmailUtils.getPropertiesForEmailGetMethod(_session!, _accountId!), lastEmailId: lastEmail?.id diff --git a/lib/features/thread/presentation/thread_view.dart b/lib/features/thread/presentation/thread_view.dart index 1614af220..1fac33572 100644 --- a/lib/features/thread/presentation/thread_view.dart +++ b/lib/features/thread/presentation/thread_view.dart @@ -20,10 +20,12 @@ import 'package:tmail_ui_user/features/manage_account/presentation/vacation/widg import 'package:tmail_ui_user/features/network_connection/presentation/network_connection_banner_widget.dart'; import 'package:tmail_ui_user/features/quotas/presentation/widget/quotas_banner_widget.dart'; import 'package:tmail_ui_user/features/thread/domain/model/filter_message_option.dart'; +import 'package:tmail_ui_user/features/thread/domain/state/clean_and_get_all_email_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/empty_spam_folder_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/empty_trash_folder_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/get_all_email_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/search_email_state.dart'; +import 'package:tmail_ui_user/features/thread/presentation/extensions/handle_pull_to_refresh_list_email_extension.dart'; import 'package:tmail_ui_user/features/thread/presentation/model/delete_action_type.dart'; import 'package:tmail_ui_user/features/thread/presentation/model/loading_more_status.dart'; import 'package:tmail_ui_user/features/thread/presentation/styles/item_email_tile_styles.dart'; @@ -341,90 +343,94 @@ class ThreadView extends GetWidget } Widget _buildResultListEmail(BuildContext context, List listPresentationEmail) { - if (controller.mailboxDashBoardController.currentSelectMode.value == SelectMode.INACTIVE) { - return listPresentationEmail.isNotEmpty - ? RefreshIndicator( - color: AppColor.colorTextButton, - onRefresh: () async => controller.refreshAllEmail(), - child: _buildListEmailBody(context, listPresentationEmail)) - : RefreshIndicator( - color: AppColor.colorTextButton, - onRefresh: () async => controller.refreshAllEmail(), - child: _buildEmptyEmail(context)); - } else { - return listPresentationEmail.isNotEmpty + return listPresentationEmail.isNotEmpty ? _buildListEmailBody(context, listPresentationEmail) : _buildEmptyEmail(context); - } } Widget _buildListEmailBody(BuildContext context, List listPresentationEmail) { + Widget listView = ListView.separated( + key: const PageStorageKey('list_presentation_email_in_threads'), + controller: controller.listEmailController, + physics: const AlwaysScrollableScrollPhysics(), + itemCount: listPresentationEmail.length + 2, + itemBuilder: (context, index) => Obx(() { + if (index == listPresentationEmail.length) { + return _buildLoadMoreButton( + context, + controller.loadingMoreStatus.value, + ); + } + if (index == listPresentationEmail.length + 1) { + return _buildLoadMoreProgressBar(controller.loadingMoreStatus.value); + } + + if (PlatformInfo.isMobile) { + return _buildEmailItemNotDraggable( + context, + listPresentationEmail[index], + ); + } else { + return _buildEmailItem( + context, + listPresentationEmail[index], + ); + } + }), + separatorBuilder: (context, index) { + if (PlatformInfo.isMobile) { + if (index < listPresentationEmail.length - 1) { + return Padding( + padding: ItemEmailTileStyles.getMobilePaddingItemList( + context, + controller.responsiveUtils, + ), + child: const Divider(), + ); + } else { + return const SizedBox.shrink(); + } + } else { + return Padding( + padding: ItemEmailTileStyles.getPaddingDividerWeb( + context, + controller.responsiveUtils, + ), + child: Divider( + color: index < listPresentationEmail.length - 1 && controller.mailboxDashBoardController.currentSelectMode.value == SelectMode.INACTIVE + ? null + : Colors.white, + ), + ); + } + }, + ); + + if (!controller.mailboxDashBoardController.isSelectionEnabled()) { + listView = PullToRefreshWidget( + onNormalRefresh: controller.onRefresh, + onDeepRefresh: controller.onCleanAndRefresh, + pullDownToRefreshText: AppLocalizations.of(context).pullDownToRefresh, + normalRefreshText: AppLocalizations.of(context).refresh, + deepRefreshText: AppLocalizations.of(context).deepRefresh, + refreshingText: AppLocalizations.of(context).refreshing, + pullingText: AppLocalizations.of(context).pulling, + pullFurtherForText: AppLocalizations.of(context).pullFurtherFor, + pullHarderForText: AppLocalizations.of(context).pullHarderFor, + child: listView, + ); + } + return NotificationListener( onNotification: _handleScrollNotificationListener, child: PlatformInfo.isMobile - ? ListView.separated( - key: const PageStorageKey('list_presentation_email_in_threads'), - controller: controller.listEmailController, - physics: const AlwaysScrollableScrollPhysics(), - itemCount: listPresentationEmail.length + 2, - itemBuilder: (context, index) => Obx(() { - if (index == listPresentationEmail.length) { - return _buildLoadMoreButton( - context, - controller.loadingMoreStatus.value); - } - if (index == listPresentationEmail.length + 1) { - return _buildLoadMoreProgressBar(controller.loadingMoreStatus.value); - } - return _buildEmailItemNotDraggable( - context, - listPresentationEmail[index]); - }), - separatorBuilder: (context, index) { - if (index < listPresentationEmail.length - 1) { - return Padding( - padding: ItemEmailTileStyles.getMobilePaddingItemList(context, controller.responsiveUtils), - child: const Divider()); - } else { - return const SizedBox.shrink(); - } - }, - ) + ? listView : Focus( focusNode: controller.focusNodeKeyBoard, autofocus: true, onKeyEvent: controller.handleKeyEvent, - child: ListView.separated( - key: const PageStorageKey('list_presentation_email_in_threads'), - controller: controller.listEmailController, - physics: const AlwaysScrollableScrollPhysics(), - itemCount: listPresentationEmail.length + 2, - itemBuilder: (context, index) => Obx(() { - if (index == listPresentationEmail.length) { - return _buildLoadMoreButton( - context, - controller.loadingMoreStatus.value); - } - if (index == listPresentationEmail.length + 1) { - return _buildLoadMoreProgressBar(controller.loadingMoreStatus.value); - } - return _buildEmailItem( - context, - listPresentationEmail[index]); - }), - separatorBuilder: (context, index) { - return Padding( - padding: ItemEmailTileStyles.getPaddingDividerWeb(context, controller.responsiveUtils), - child: Divider( - color: index < listPresentationEmail.length - 1 && - controller.mailboxDashBoardController.currentSelectMode.value == SelectMode.INACTIVE - ? null - : Colors.white, - ) - ); - }, - ), - ) + child: listView, + ), ); } @@ -677,7 +683,9 @@ class ThreadView extends GetWidget return Obx(() => controller.viewState.value.fold( (failure) => const SizedBox.shrink(), (success) { - if (success is GetAllEmailLoading || success is SearchingState) { + if (success is GetAllEmailLoading || + success is SearchingState || + success is CleanAndGetAllEmailLoading) { return const SizedBox.shrink(); } @@ -685,12 +693,23 @@ class ThreadView extends GetWidget && success.currentMailboxId != controller.selectedMailboxId) { return const SizedBox.shrink(); } else { - return EmptyEmailsWidget( - key: const Key('empty_thread_view'), - isNetworkConnectionAvailable: controller.networkConnectionController.isNetworkConnectionAvailable(), - isSearchActive: controller.isSearchActive, - isFilterMessageActive: controller.mailboxDashBoardController.filterMessageOption.value != FilterMessageOption.all, - onCreateFiltersActionCallback: controller.goToCreateEmailRuleView + return PullToRefreshWidget( + onNormalRefresh: controller.onRefresh, + onDeepRefresh: controller.onCleanAndRefresh, + pullDownToRefreshText: AppLocalizations.of(context).pullDownToRefresh, + normalRefreshText: AppLocalizations.of(context).refresh, + deepRefreshText: AppLocalizations.of(context).deepRefresh, + refreshingText: AppLocalizations.of(context).refreshing, + pullingText: AppLocalizations.of(context).pulling, + pullFurtherForText: AppLocalizations.of(context).pullFurtherFor, + pullHarderForText: AppLocalizations.of(context).pullHarderFor, + child: EmptyEmailsWidget( + key: const Key('empty_thread_view'), + isNetworkConnectionAvailable: controller.networkConnectionController.isNetworkConnectionAvailable(), + isSearchActive: controller.isSearchActive, + isFilterMessageActive: controller.mailboxDashBoardController.filterMessageOption.value != FilterMessageOption.all, + onCreateFiltersActionCallback: controller.goToCreateEmailRuleView + ), ); } } diff --git a/lib/features/thread/presentation/widgets/thread_view_loading_bar_widget.dart b/lib/features/thread/presentation/widgets/thread_view_loading_bar_widget.dart index b5d7972d6..f76eb935d 100644 --- a/lib/features/thread/presentation/widgets/thread_view_loading_bar_widget.dart +++ b/lib/features/thread/presentation/widgets/thread_view_loading_bar_widget.dart @@ -3,6 +3,7 @@ import 'package:core/presentation/state/success.dart'; import 'package:core/presentation/views/loading/cupertino_loading_widget.dart'; import 'package:dartz/dartz.dart'; import 'package:flutter/material.dart'; +import 'package:tmail_ui_user/features/thread/domain/state/clean_and_get_all_email_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/get_all_email_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/search_email_state.dart'; @@ -20,7 +21,9 @@ class ThreadViewLoadingBarWidget extends StatelessWidget { return viewState.fold( (failure) => const SizedBox.shrink(), (success) { - if (success is SearchingState || success is GetAllEmailLoading) { + if (success is SearchingState || + success is GetAllEmailLoading || + success is CleanAndGetAllEmailLoading) { return const Padding( padding: EdgeInsetsDirectional.only(top: 16), child: CupertinoLoadingWidget()); diff --git a/lib/l10n/intl_messages.arb b/lib/l10n/intl_messages.arb index f091d42c0..2c7f0e81f 100644 --- a/lib/l10n/intl_messages.arb +++ b/lib/l10n/intl_messages.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2025-03-31T03:40:41.454034", + "@@last_modified": "2025-04-03T14:19:11.171517", "initializing_data": "Initializing data...", "@initializing_data": { "type": "text", @@ -2968,6 +2968,12 @@ "placeholders_order": [], "placeholders": {} }, + "errorWhileFetchingSubaddress": "Error while fetching the subaddress", + "@errorWhileFetchingSubaddress": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, "connectedToTheInternet": "Connected to the internet", "@connectedToTheInternet": { "type": "text", @@ -4399,5 +4405,53 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "refresh": "Refresh", + "@refresh": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "deepRefresh": "Deep refresh", + "@deepRefresh": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "pullDownToRefresh": "Pull down to refresh", + "@pullDownToRefresh": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "refreshing": "Refreshing", + "@refreshing": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "pulling": "Pulling", + "@pulling": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "releaseTo": "Release to", + "@releaseTo": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "pullFurtherFor": "Pull further for", + "@pullFurtherFor": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "pullHarderFor": "Pull harder for", + "@pullHarderFor": { + "type": "text", + "placeholders_order": [], + "placeholders": {} } } \ No newline at end of file diff --git a/lib/main/localizations/app_localizations.dart b/lib/main/localizations/app_localizations.dart index 9f7ab9626..10dbf1733 100644 --- a/lib/main/localizations/app_localizations.dart +++ b/lib/main/localizations/app_localizations.dart @@ -4625,4 +4625,60 @@ class AppLocalizations { name: 'retry', ); } + + String get refresh { + return Intl.message( + 'Refresh', + name: 'refresh', + ); + } + + String get deepRefresh { + return Intl.message( + 'Deep refresh', + name: 'deepRefresh', + ); + } + + String get pullDownToRefresh { + return Intl.message( + 'Pull down to refresh', + name: 'pullDownToRefresh', + ); + } + + String get refreshing { + return Intl.message( + 'Refreshing', + name: 'refreshing', + ); + } + + String get pulling { + return Intl.message( + 'Pulling', + name: 'pulling', + ); + } + + String get releaseTo { + return Intl.message( + 'Release to', + name: 'releaseTo', + ); + } + + String get pullFurtherFor { + return Intl.message( + 'Pull further for', + name: 'pullFurtherFor', + ); + } + + String get pullHarderFor { + return Intl.message( + 'Pull harder for', + name: 'pullHarderFor', + ); + } } diff --git a/model/pubspec.lock b/model/pubspec.lock index 7a38fb772..feaaeff93 100644 --- a/model/pubspec.lock +++ b/model/pubspec.lock @@ -945,6 +945,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.3" + pull_to_refresh: + dependency: transitive + description: + name: pull_to_refresh + sha256: bbadd5a931837b57739cf08736bea63167e284e71fb23b218c8c9a6e042aad12 + url: "https://pub.dev" + source: hosted + version: "2.0.0" qr: dependency: transitive description: diff --git a/pubspec.lock b/pubspec.lock index cb53b5f9c..18b36c602 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1793,6 +1793,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.3" + pull_to_refresh: + dependency: transitive + description: + name: pull_to_refresh + sha256: bbadd5a931837b57739cf08736bea63167e284e71fb23b218c8c9a6e042aad12 + url: "https://pub.dev" + source: hosted + version: "2.0.0" qr: dependency: transitive description: diff --git a/test/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller_test.dart b/test/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller_test.dart index a09c7c8f7..4a5d5d9f9 100644 --- a/test/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller_test.dart +++ b/test/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller_test.dart @@ -81,6 +81,7 @@ import 'package:tmail_ui_user/features/thread/domain/constants/thread_constants. 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/search_query.dart'; +import 'package:tmail_ui_user/features/thread/domain/usecases/clean_and_get_emails_in_mailbox_interactor.dart'; import 'package:tmail_ui_user/features/thread/domain/usecases/empty_spam_folder_interactor.dart'; import 'package:tmail_ui_user/features/thread/domain/usecases/empty_trash_folder_interactor.dart'; import 'package:tmail_ui_user/features/thread/domain/usecases/get_email_by_id_interactor.dart'; @@ -182,6 +183,7 @@ const fallbackGenerators = { MockSpec(), MockSpec(), MockSpec(fallbackGenerators: fallbackGenerators), + MockSpec(), ]) void main() { // mock mailbox dashboard controller direct dependencies @@ -203,6 +205,7 @@ void main() { final deleteMultipleEmailsPermanentlyInteractor = MockDeleteMultipleEmailsPermanentlyInteractor(); final getEmailByIdInteractor = MockGetEmailByIdInteractor(); + final cleanAndGetEmailsInMailboxInteractor = MockCleanAndGetEmailsInMailboxInteractor(); final sendEmailInteractor = MockSendEmailInteractor(); final storeSendingEmailInteractor = MockStoreSendingEmailInteractor(); final updateSendingEmailInteractor = MockUpdateSendingEmailInteractor(); @@ -391,7 +394,8 @@ void main() { loadMoreEmailsInMailboxInteractor, searchEmailInteractor, searchMoreEmailInteractor, - getEmailByIdInteractor); + getEmailByIdInteractor, + cleanAndGetEmailsInMailboxInteractor); Get.put(threadController); advancedFilterController = AdvancedFilterController(); @@ -622,7 +626,9 @@ void main() { loadMoreEmailsInMailboxInteractor, searchEmailInteractor, searchMoreEmailInteractor, - getEmailByIdInteractor); + getEmailByIdInteractor, + cleanAndGetEmailsInMailboxInteractor, + ); Get.put(threadController); advancedFilterController = AdvancedFilterController(); diff --git a/test/features/mailbox_dashboard/presentation/view/mailbox_dashboard_view_widget_test.dart b/test/features/mailbox_dashboard/presentation/view/mailbox_dashboard_view_widget_test.dart index 67171beb1..24ae61201 100644 --- a/test/features/mailbox_dashboard/presentation/view/mailbox_dashboard_view_widget_test.dart +++ b/test/features/mailbox_dashboard/presentation/view/mailbox_dashboard_view_widget_test.dart @@ -79,6 +79,7 @@ import 'package:tmail_ui_user/features/sending_queue/domain/usecases/update_send import 'package:tmail_ui_user/features/thread/domain/model/filter_message_option.dart'; import 'package:tmail_ui_user/features/thread/domain/state/get_all_email_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/load_more_emails_state.dart'; +import 'package:tmail_ui_user/features/thread/domain/usecases/clean_and_get_emails_in_mailbox_interactor.dart'; import 'package:tmail_ui_user/features/thread/domain/usecases/empty_spam_folder_interactor.dart'; import 'package:tmail_ui_user/features/thread/domain/usecases/empty_trash_folder_interactor.dart'; import 'package:tmail_ui_user/features/thread/domain/usecases/get_email_by_id_interactor.dart'; @@ -184,6 +185,7 @@ const fallbackGenerators = { MockSpec(), MockSpec(), MockSpec(fallbackGenerators: fallbackGenerators), + MockSpec(), ]) void main() { final moveToMailboxInteractor = MockMoveToMailboxInteractor(); @@ -199,6 +201,7 @@ void main() { final emptyTrashFolderInteractor = MockEmptyTrashFolderInteractor(); final deleteMultipleEmailsPermanentlyInteractor = MockDeleteMultipleEmailsPermanentlyInteractor(); final getEmailByIdInteractor = MockGetEmailByIdInteractor(); + final cleanAndGetEmailsInMailboxInteractor = MockCleanAndGetEmailsInMailboxInteractor(); final sendEmailInteractor = MockSendEmailInteractor(); final storeSendingEmailInteractor = MockStoreSendingEmailInteractor(); final updateSendingEmailInteractor = MockUpdateSendingEmailInteractor(); @@ -383,7 +386,8 @@ void main() { loadMoreEmailsInMailboxInteractor, searchEmailInteractor, searchMoreEmailInteractor, - getEmailByIdInteractor + getEmailByIdInteractor, + cleanAndGetEmailsInMailboxInteractor, ); Get.put(threadController); @@ -433,9 +437,6 @@ void main() { final listViewEmailWidgetFinder = find.byKey(const PageStorageKey('list_presentation_email_in_threads')); expect(listViewEmailWidgetFinder, findsOneWidget); - final listViewEmailWidget = tester.widget(listViewEmailWidgetFinder); - expect(listViewEmailWidget.semanticChildCount, equals(listEmailsOfInbox.length + 2)); - final emailTile1Finder = find.byKey(Key('email_tile_builder_${EmailFixtures.email1.toPresentationEmail().id?.asString}'),); expect(emailTile1Finder, findsOneWidget); @@ -461,9 +462,6 @@ void main() { final folder1ListViewEmailWidgetFinder = find.byKey(const PageStorageKey('list_presentation_email_in_threads')); expect(folder1ListViewEmailWidgetFinder, findsOneWidget); - final folder1ListViewEmailWidget = tester.widget(folder1ListViewEmailWidgetFinder); - expect(folder1ListViewEmailWidget.semanticChildCount, equals(listEmailsOfFolder1.length + 2)); - final emailTile4Finder = find.byKey(Key('email_tile_builder_${EmailFixtures.email4.toPresentationEmail().id?.asString}'),); expect(emailTile4Finder, findsOneWidget); @@ -531,9 +529,6 @@ void main() { final folder1ListViewEmailWidgetFinder = find.byKey(const PageStorageKey('list_presentation_email_in_threads')); expect(folder1ListViewEmailWidgetFinder, findsOneWidget); - final folder1ListViewEmailWidget = tester.widget(folder1ListViewEmailWidgetFinder); - expect(folder1ListViewEmailWidget.semanticChildCount, equals(listEmailsOfFolder1.length + 2)); - final emailTile1Finder = find.byKey(Key('email_tile_builder_${EmailFixtures.email1.toPresentationEmail().id?.asString}'),); expect(emailTile1Finder, findsOneWidget); @@ -581,10 +576,6 @@ void main() { final emptyEmailWidgetFinder = find.byKey(const Key('empty_thread_view')); expect(emptyEmailWidgetFinder, findsNothing); - final listViewEmailWidgetFinder = find.byKey(const PageStorageKey('list_presentation_email_in_threads')); - final listViewEmailWidget = tester.widget(listViewEmailWidgetFinder); - expect(listViewEmailWidget.semanticChildCount, equals(listEmailsOfInbox.length + 2)); - // Switch to mailbox Folder 1 final listEmailsOfFolder1 = []; mailboxDashboardController.setSelectedMailbox(MailboxFixtures.folder1.toPresentationMailbox()); @@ -659,9 +650,6 @@ void main() { final listViewEmailWidgetFinder = find.byKey(const PageStorageKey('list_presentation_email_in_threads')); expect(listViewEmailWidgetFinder, findsOneWidget); - final listViewEmailWidget = tester.widget(listViewEmailWidgetFinder); - expect(listViewEmailWidget.semanticChildCount, equals(7)); - expect(find.text('Load more'), findsOneWidget); debugDefaultTargetPlatformOverride = null; @@ -705,9 +693,6 @@ void main() { final listViewEmailWidgetFinder = find.byKey(const PageStorageKey('list_presentation_email_in_threads')); expect(listViewEmailWidgetFinder, findsOneWidget); - final listViewEmailWidget = tester.widget(listViewEmailWidgetFinder); - expect(listViewEmailWidget.semanticChildCount, equals(5)); - expect(find.text('Load more'), findsNothing); debugDefaultTargetPlatformOverride = null; diff --git a/test/features/search/verify_before_time_in_search_email_filter_test.dart b/test/features/search/verify_before_time_in_search_email_filter_test.dart index 1d82e4a3d..b95324c77 100644 --- a/test/features/search/verify_before_time_in_search_email_filter_test.dart +++ b/test/features/search/verify_before_time_in_search_email_filter_test.dart @@ -66,6 +66,7 @@ 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/state/refresh_changes_all_email_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/search_email_state.dart'; +import 'package:tmail_ui_user/features/thread/domain/usecases/clean_and_get_emails_in_mailbox_interactor.dart'; import 'package:tmail_ui_user/features/thread/domain/usecases/empty_spam_folder_interactor.dart'; import 'package:tmail_ui_user/features/thread/domain/usecases/empty_trash_folder_interactor.dart'; import 'package:tmail_ui_user/features/thread/domain/usecases/get_email_by_id_interactor.dart'; @@ -157,6 +158,7 @@ const fallbackGenerators = { MockSpec(), MockSpec(), MockSpec(fallbackGenerators: fallbackGenerators), + MockSpec(), ]) void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -169,6 +171,7 @@ void main() { late MockSearchEmailInteractor mockSearchEmailInteractor; late MockSearchMoreEmailInteractor mockSearchMoreEmailInteractor; late MockGetEmailByIdInteractor mockGetEmailByIdInteractor; + late MockCleanAndGetEmailsInMailboxInteractor mockCleanAndGetEmailsInMailboxInteractor; // Declaration search controller late SearchController searchController; @@ -275,6 +278,7 @@ void main() { mockSearchEmailInteractor = MockSearchEmailInteractor(); mockSearchMoreEmailInteractor = MockSearchMoreEmailInteractor(); mockGetEmailByIdInteractor = MockGetEmailByIdInteractor(); + mockCleanAndGetEmailsInMailboxInteractor = MockCleanAndGetEmailsInMailboxInteractor(); // Mock search controller mockQuickSearchEmailInteractor = MockQuickSearchEmailInteractor(); @@ -366,6 +370,7 @@ void main() { mockSearchEmailInteractor, mockSearchMoreEmailInteractor, mockGetEmailByIdInteractor, + mockCleanAndGetEmailsInMailboxInteractor, ); mailboxDashboardController.sessionCurrent = SessionFixtures.aliceSession; diff --git a/test/features/thread/presentation/controller/thread_controller_test.dart b/test/features/thread/presentation/controller/thread_controller_test.dart index 091aec8d4..27064a240 100644 --- a/test/features/thread/presentation/controller/thread_controller_test.dart +++ b/test/features/thread/presentation/controller/thread_controller_test.dart @@ -37,6 +37,7 @@ import 'package:tmail_ui_user/features/thread/domain/model/filter_message_option import 'package:tmail_ui_user/features/thread/domain/model/search_query.dart'; import 'package:tmail_ui_user/features/thread/domain/state/refresh_changes_all_email_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/search_email_state.dart'; +import 'package:tmail_ui_user/features/thread/domain/usecases/clean_and_get_emails_in_mailbox_interactor.dart'; import 'package:tmail_ui_user/features/thread/domain/usecases/get_email_by_id_interactor.dart'; import 'package:tmail_ui_user/features/thread/domain/usecases/get_emails_in_mailbox_interactor.dart'; import 'package:tmail_ui_user/features/thread/domain/usecases/load_more_emails_in_mailbox_interactor.dart'; @@ -87,6 +88,7 @@ const fallbackGenerators = { MockSpec(), MockSpec(), MockSpec(), + MockSpec(), ]) void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -102,6 +104,7 @@ void main() { late MockSearchEmailInteractor mockSearchEmailInteractor; late MockSearchMoreEmailInteractor mockSearchMoreEmailInteractor; late MockGetEmailByIdInteractor mockGetEmailByIdInteractor; + late MockCleanAndGetEmailsInMailboxInteractor mockCleanAndGetEmailsInMailboxInteractor; // Declaration base controller late MockCachingManager mockCachingManager; @@ -166,6 +169,7 @@ void main() { mockSearchEmailInteractor = MockSearchEmailInteractor(); mockSearchMoreEmailInteractor = MockSearchMoreEmailInteractor(); mockGetEmailByIdInteractor = MockGetEmailByIdInteractor(); + mockCleanAndGetEmailsInMailboxInteractor = MockCleanAndGetEmailsInMailboxInteractor(); Get.put(mockNetworkConnectionController); Get.put(mockSearchController); @@ -177,7 +181,9 @@ void main() { mockLoadMoreEmailsInMailboxInteractor, mockSearchEmailInteractor, mockSearchMoreEmailInteractor, - mockGetEmailByIdInteractor); + mockGetEmailByIdInteractor, + mockCleanAndGetEmailsInMailboxInteractor, + ); }); group('ThreadController::test', () {