diff --git a/lib/features/push_notification/presentation/action/fcm_action.dart b/lib/features/push_notification/presentation/action/fcm_action.dart index 5a61a9bfb..d26d93a3b 100644 --- a/lib/features/push_notification/presentation/action/fcm_action.dart +++ b/lib/features/push_notification/presentation/action/fcm_action.dart @@ -32,11 +32,11 @@ class PushNotificationAction extends FcmStateChangeAction { List get props => [typeName, newState, accountId]; } -class StoreEmailStateChangeToRefreshAction extends FcmStateChangeAction { +class StoreEmailStateToRefreshAction extends FcmStateChangeAction { final AccountId accountId; - StoreEmailStateChangeToRefreshAction( + StoreEmailStateToRefreshAction( TypeName typeName, jmap.State newState, this.accountId diff --git a/lib/features/push_notification/presentation/bindings/fcm_interactor_bindings.dart b/lib/features/push_notification/presentation/bindings/fcm_interactor_bindings.dart index 2257434d7..454b2b835 100644 --- a/lib/features/push_notification/presentation/bindings/fcm_interactor_bindings.dart +++ b/lib/features/push_notification/presentation/bindings/fcm_interactor_bindings.dart @@ -5,40 +5,66 @@ import 'package:tmail_ui_user/features/push_notification/data/datasource_impl/hi import 'package:tmail_ui_user/features/push_notification/data/local/fcm_cache_manager.dart'; import 'package:tmail_ui_user/features/push_notification/data/repository/fcm_repository_impl.dart'; import 'package:tmail_ui_user/features/push_notification/domain/repository/fcm_repository.dart'; +import 'package:tmail_ui_user/features/push_notification/domain/usecases/delete_email_state_to_refresh_interactor.dart'; import 'package:tmail_ui_user/features/push_notification/domain/usecases/delete_fcm_token_cache_interactor.dart'; +import 'package:tmail_ui_user/features/push_notification/domain/usecases/get_email_changes_to_push_notification_interactor.dart'; +import 'package:tmail_ui_user/features/push_notification/domain/usecases/get_email_state_to_refresh_interactor.dart'; import 'package:tmail_ui_user/features/push_notification/domain/usecases/get_fcm_token_cache_interactor.dart'; +import 'package:tmail_ui_user/features/push_notification/domain/usecases/get_stored_email_delivery_state_interactor.dart'; import 'package:tmail_ui_user/features/push_notification/domain/usecases/save_fcm_token_cache_interactor.dart'; +import 'package:tmail_ui_user/features/push_notification/domain/usecases/store_email_delivery_state_interactor.dart'; +import 'package:tmail_ui_user/features/push_notification/domain/usecases/store_email_state_to_refresh_interactor.dart'; +import 'package:tmail_ui_user/features/thread/data/datasource/thread_datasource.dart'; +import 'package:tmail_ui_user/features/thread/data/datasource_impl/thread_datasource_impl.dart'; +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/main/exceptions/cache_exception_thrower.dart'; +import 'package:tmail_ui_user/main/exceptions/remote_exception_thrower.dart'; class FcmInteractorBindings extends InteractorsBindings { @override void bindingsDataSource() { - Get.put(Get.find()); + Get.lazyPut(() => Get.find()); + Get.lazyPut(() => Get.find()); } @override void bindingsDataSourceImpl() { - Get.put(HiveFCMDatasourceImpl( + Get.lazyPut(() => HiveFCMDatasourceImpl( Get.find(), Get.find(), )); + Get.lazyPut(() => ThreadDataSourceImpl( + Get.find(), + Get.find(), + Get.find() + )); } @override void bindingsInteractor() { - Get.put(DeleteFCMTokenCacheInteractor(Get.find())); - Get.put(SaveFCMTokenCacheInteractor(Get.find())); - Get.put(GetFCMTokenCacheInteractor(Get.find())); + Get.lazyPut(() => DeleteFCMTokenCacheInteractor(Get.find())); + Get.lazyPut(() => SaveFCMTokenCacheInteractor(Get.find())); + Get.lazyPut(() => GetFCMTokenCacheInteractor(Get.find())); + Get.lazyPut(() => GetStoredEmailDeliveryStateInteractor(Get.find())); + Get.lazyPut(() => StoreEmailDeliveryStateInteractor(Get.find())); + Get.lazyPut(() => GetEmailChangesToPushNotificationInteractor(Get.find())); + Get.lazyPut(() => StoreEmailStateToRefreshInteractor(Get.find())); + Get.lazyPut(() => GetEmailStateToRefreshInteractor(Get.find())); + Get.lazyPut(() => DeleteEmailStateToRefreshInteractor(Get.find())); } @override void bindingsRepository() { - Get.put(Get.find()); + Get.lazyPut(() => Get.find()); } @override void bindingsRepositoryImpl() { - Get.put(FCMRepositoryImpl(Get.find())); + Get.lazyPut(() => FCMRepositoryImpl( + Get.find(), + Get.find() + )); } } diff --git a/lib/features/push_notification/presentation/controller/fcm_controller.dart b/lib/features/push_notification/presentation/controller/fcm_controller.dart index f67fe3a3c..2fbe8baff 100644 --- a/lib/features/push_notification/presentation/controller/fcm_controller.dart +++ b/lib/features/push_notification/presentation/controller/fcm_controller.dart @@ -25,6 +25,7 @@ import 'package:tmail_ui_user/features/login/domain/state/get_stored_token_oidc_ import 'package:tmail_ui_user/features/login/domain/usecases/get_authenticated_account_interactor.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/bindings/mailbox_dashboard_bindings.dart'; import 'package:tmail_ui_user/features/push_notification/presentation/action/fcm_action.dart'; +import 'package:tmail_ui_user/features/push_notification/presentation/bindings/fcm_interactor_bindings.dart'; import 'package:tmail_ui_user/features/push_notification/presentation/extensions/state_change_extension.dart'; import 'package:tmail_ui_user/features/push_notification/presentation/listener/email_change_listener.dart'; import 'package:tmail_ui_user/features/push_notification/presentation/services/fcm_service.dart'; @@ -119,7 +120,7 @@ class FcmController extends BaseController { if (isForeground) { return SynchronizeEmailOnForegroundAction(typeName, newState, accountId); } else { - return StoreEmailStateChangeToRefreshAction(typeName, newState, accountId); + return StoreEmailStateToRefreshAction(typeName, newState, accountId); } } else if (typeName == TypeName.emailDelivery) { if (!isForeground) { @@ -138,6 +139,7 @@ class FcmController extends BaseController { await Future.sync(() { HomeBindings().dependencies(); MailboxDashBoardBindings().dependencies(); + FcmInteractorBindings().dependencies(); }); _getInteractorBindings(); diff --git a/lib/features/push_notification/presentation/listener/email_change_listener.dart b/lib/features/push_notification/presentation/listener/email_change_listener.dart index d56363cb3..ac713041c 100644 --- a/lib/features/push_notification/presentation/listener/email_change_listener.dart +++ b/lib/features/push_notification/presentation/listener/email_change_listener.dart @@ -1,21 +1,50 @@ +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; import 'package:core/utils/app_logger.dart'; +import 'package:dartz/dartz.dart'; +import 'package:fcm/model/type_name.dart'; import 'package:get/get.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/state.dart' as jmap; +import 'package:model/email/presentation_email.dart'; import 'package:tmail_ui_user/features/base/action/ui_action.dart'; +import 'package:tmail_ui_user/features/email/domain/state/get_stored_state_email_state.dart'; +import 'package:tmail_ui_user/features/email/domain/usecases/get_stored_email_state_interactor.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/action/dashboard_action.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart'; +import 'package:tmail_ui_user/features/push_notification/domain/exceptions/fcm_exception.dart'; +import 'package:tmail_ui_user/features/push_notification/domain/state/get_email_changes_state.dart'; +import 'package:tmail_ui_user/features/push_notification/domain/state/get_stored_email_delivery_state.dart'; +import 'package:tmail_ui_user/features/push_notification/domain/usecases/get_email_changes_to_push_notification_interactor.dart'; +import 'package:tmail_ui_user/features/push_notification/domain/usecases/get_stored_email_delivery_state_interactor.dart'; +import 'package:tmail_ui_user/features/push_notification/domain/usecases/store_email_delivery_state_interactor.dart'; +import 'package:tmail_ui_user/features/push_notification/domain/usecases/store_email_state_to_refresh_interactor.dart'; import 'package:tmail_ui_user/features/push_notification/presentation/action/fcm_action.dart'; import 'package:tmail_ui_user/features/push_notification/presentation/listener/change_listener.dart'; +import 'package:tmail_ui_user/features/push_notification/presentation/notification/local_notification_manager.dart'; +import 'package:tmail_ui_user/features/thread/domain/constants/thread_constants.dart'; class EmailChangeListener extends ChangeListener { MailboxDashBoardController? _dashBoardController; + GetStoredEmailDeliveryStateInteractor? _getStoredEmailDeliveryStateInteractor; + StoreEmailDeliveryStateInteractor? _storeEmailDeliveryStateInteractor; + GetEmailChangesToPushNotificationInteractor? _getEmailChangesToPushNotificationInteractor; + GetStoredEmailStateInteractor? _getStoredEmailStateInteractor; + StoreEmailStateToRefreshInteractor? _storeEmailStateToRefreshInteractor; + + jmap.State? _newState; + AccountId? _accountId; EmailChangeListener._internal() { try { _dashBoardController = Get.find(); + _getStoredEmailStateInteractor = Get.find(); + _getStoredEmailDeliveryStateInteractor = Get.find(); + _storeEmailDeliveryStateInteractor = Get.find(); + _getEmailChangesToPushNotificationInteractor = Get.find(); + _storeEmailStateToRefreshInteractor = Get.find(); } catch (e) { logError('EmailChangeListener::_internal(): IS NOT REGISTERED: ${e.toString()}'); } @@ -33,8 +62,8 @@ class EmailChangeListener extends ChangeListener { _synchronizeEmailOnForegroundAction(action.newState); } else if (action is PushNotificationAction) { _pushNotificationAction(action.newState, action.accountId); - } else if (action is StoreEmailStateChangeToRefreshAction) { - _handleStoreEmailStateChangeToRefreshAction(action.newState, action.accountId); + } else if (action is StoreEmailStateToRefreshAction) { + _handleStoreEmailStateToRefreshAction(action.newState); } } } @@ -42,15 +71,107 @@ class EmailChangeListener extends ChangeListener { void _synchronizeEmailOnForegroundAction(jmap.State newState) { log('EmailChangeListener::_synchronizeEmailAction():newState: $newState'); if (_dashBoardController != null) { - _dashBoardController!.dispatchAction(RefreshChangedEmailAction(newState)); + _dashBoardController!.dispatchAction(RefreshChangeEmailAction(newState)); } } void _pushNotificationAction(jmap.State newState, AccountId accountId) { + _newState = newState; + _accountId = accountId; log('EmailChangeListener::_pushNotificationAction():newState: $newState'); + _getStoredEmailDeliveryState(); } - void _handleStoreEmailStateChangeToRefreshAction(jmap.State newState, AccountId accountId) { - log('EmailChangeListener::_handleStoreEmailStateChangeToRefreshAction():newState: $newState'); + void _getStoredEmailDeliveryState() { + if (_getStoredEmailDeliveryStateInteractor != null) { + consumeState(_getStoredEmailDeliveryStateInteractor!.execute()); + } + } + + void _getStoredEmailState() { + if (_getStoredEmailStateInteractor != null && _accountId != null) { + consumeState(_getStoredEmailStateInteractor!.execute(_accountId!)); + } else { + logError('EmailChangeListener::_getStoredEmailState(): _getStoredEmailStateInteractor is null'); + } + } + + void _getEmailChangesAction(jmap.State state) { + if (_getEmailChangesToPushNotificationInteractor != null && _accountId != null) { + consumeState(_getEmailChangesToPushNotificationInteractor!.execute( + _accountId!, + state, + propertiesCreated: ThreadConstants.propertiesDefault, + propertiesUpdated: ThreadConstants.propertiesUpdatedDefault, + )); + } + } + + void _storeEmailDeliveryStateAction(jmap.State state) { + if (_storeEmailDeliveryStateInteractor != null) { + consumeState(_storeEmailDeliveryStateInteractor!.execute(state)); + } + } + + void _showLocalNotification(PresentationEmail presentationEmail, jmap.State newState) { + LocalNotificationManager.instance.showPushNotification( + id: presentationEmail.id.id.value, + title: presentationEmail.subject ?? '', + message: presentationEmail.preview, + emailAddress: presentationEmail.from?.first, + payload: newState.value + ); + } + + void consumeState(Stream> newStateStream) async { + newStateStream.listen( + _handleStateStream, + onError: (error, stackTrace) { + logError('EmailChangeListener::consumeState():onError:error: $error'); + logError('EmailChangeListener::consumeState():onError:stackTrace: $stackTrace'); + } + ); + } + + void _handleStateStream(Either newState) { + newState.fold(_handleFailureViewState, _handleSuccessViewState); + } + + void _handleFailureViewState(Failure failure) { + log('EmailChangeListener::_handleFailureViewState(): $failure'); + if (failure is GetStoredEmailDeliveryStateFailure && + failure.exception is NotFoundEmailDeliveryStateException) { + _getStoredEmailState(); + } + } + + void _handleSuccessViewState(Success success) { + log('EmailChangeListener::_handleSuccessViewState(): $success'); + if (success is GetStoredEmailDeliveryStateSuccess) { + if (_newState != success.state) { + _getEmailChangesAction(success.state); + } + } else if (success is GetStoredEmailStateSuccess) { + _getEmailChangesAction(success.state); + } else if (success is GetEmailChangesToPushNotificationSuccess) { + if (_newState != null) { + _storeEmailDeliveryStateAction(_newState!); + + for (var presentationEmail in success.emailList) { + _showLocalNotification(presentationEmail, _newState!); + } + } + + LocalNotificationManager.instance.groupPushNotification(); + } + } + + void _handleStoreEmailStateToRefreshAction(jmap.State newState) { + log('EmailChangeListener::_handleStoreEmailStateToRefreshAction():newState: $newState'); + if (_storeEmailStateToRefreshInteractor != null) { + consumeState(_storeEmailStateToRefreshInteractor!.execute(TypeName.emailType, newState)); + } else { + logError('EmailChangeListener::_handleStoreEmailStateToRefreshAction():_storeEmailStateToRefreshInteractor is null'); + } } } \ No newline at end of file diff --git a/lib/features/push_notification/presentation/notification/local_notification_config.dart b/lib/features/push_notification/presentation/notification/local_notification_config.dart index d8feb30eb..dab319ae1 100644 --- a/lib/features/push_notification/presentation/notification/local_notification_config.dart +++ b/lib/features/push_notification/presentation/notification/local_notification_config.dart @@ -1,38 +1,56 @@ import 'package:flutter_local_notifications/flutter_local_notifications.dart'; class LocalNotificationConfig { - static const channelId = 'team_mail_channel_id'; - static const channelKey = 'team_mail_channel_key'; - static const channelName = 'Team Mail notifications'; - static const channelDescription = 'Team Mail notifications'; + static const _groupId = 'team_mail_notification_group_id'; + static const _groupName = 'team_mail_notification_group_name'; + static const _groupDescription = 'Team Mail group notifications'; + static const _channelId = 'team_mail_notification_channel_id'; + static const _channelName = 'Team Mail notifications'; + static const _channelDescription = 'Team Mail notifications'; static const iosInitializationSettings = DarwinInitializationSettings(); - static const androidInitializationSettings = AndroidInitializationSettings('background'); + static const androidInitializationSettings = AndroidInitializationSettings('ic_notification'); static const androidNotificationChannel = AndroidNotificationChannel( - channelId, - channelName, - description: channelDescription, + _channelId, + _channelName, + description: _channelDescription, + groupId: _groupId, importance: Importance.max, - enableLights: true, + showBadge: true ); - static const pushNotificationDetails = NotificationDetails( - android: AndroidNotificationDetails( - channelId, - channelName, - channelDescription: channelDescription, - visibility: NotificationVisibility.public, - importance: Importance.max, - ledOnMs: 100, - ledOffMs: 1000, - category: AndroidNotificationCategory.message, - ), - iOS: DarwinNotificationDetails( - presentSound: true, - presentAlert: true, - presentBadge: true, - ), + static const androidNotificationChannelGroup = AndroidNotificationChannelGroup( + _groupId, + _groupName, + description: _groupDescription ); + + LocalNotificationConfig._internal(); + + static final LocalNotificationConfig _instance = LocalNotificationConfig._internal(); + + static LocalNotificationConfig get instance => _instance; + + NotificationDetails generateNotificationDetails({StyleInformation? styleInformation, bool setAsGroup = false}) { + return NotificationDetails( + android: AndroidNotificationDetails( + androidNotificationChannel.id, + androidNotificationChannel.name, + channelDescription: androidNotificationChannel.description, + groupKey: androidNotificationChannel.groupId, + visibility: NotificationVisibility.public, + importance: Importance.max, + priority: Priority.high, + setAsGroupSummary: setAsGroup, + styleInformation: styleInformation + ), + iOS: const DarwinNotificationDetails( + presentSound: true, + presentAlert: true, + presentBadge: true, + ), + ); + } } \ No newline at end of file diff --git a/lib/features/push_notification/presentation/notification/local_notification_manager.dart b/lib/features/push_notification/presentation/notification/local_notification_manager.dart index ce8a7ecf6..be328289e 100644 --- a/lib/features/push_notification/presentation/notification/local_notification_manager.dart +++ b/lib/features/push_notification/presentation/notification/local_notification_manager.dart @@ -1,15 +1,13 @@ import 'dart:io'; +import 'package:core/presentation/extensions/html_extension.dart'; import 'package:core/utils/app_logger.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; +import 'package:model/model.dart'; import 'package:tmail_ui_user/features/push_notification/presentation/notification/local_notification_config.dart'; -@pragma('vm:entry-point') -void _handleReceiveBackgroundNotificationResponse(NotificationResponse notificationResponse) { - log('LocalNotificationManager::_handleReceiveBackgroundNotificationResponse():notificationResponse: $notificationResponse'); -} - class LocalNotificationManager { LocalNotificationManager._internal(); @@ -26,7 +24,8 @@ class LocalNotificationManager { await _initLocalNotification(); _checkLocalNotificationPermission(); if (Platform.isAndroid) { - await _createAndroidNotificationChannels(); + await _createAndroidNotificationChannelGroup(); + await _createAndroidNotificationChannel(); } } catch (e) { logError('LocalNotificationManager::setUp(): ERROR: ${e.toString()}'); @@ -39,8 +38,7 @@ class LocalNotificationManager { android: LocalNotificationConfig.androidInitializationSettings, iOS: LocalNotificationConfig.iosInitializationSettings ), - onDidReceiveNotificationResponse: _handleReceiveNotificationResponse, - onDidReceiveBackgroundNotificationResponse: _handleReceiveBackgroundNotificationResponse + onDidReceiveNotificationResponse: _handleReceiveNotificationResponse ); } @@ -81,21 +79,56 @@ class LocalNotificationManager { } } - Future _createAndroidNotificationChannels() async { + Future _createAndroidNotificationChannel() async { return await _localNotificationsPlugin .resolvePlatformSpecificImplementation() ?.createNotificationChannel(LocalNotificationConfig.androidNotificationChannel); } - void showPushNotification({String? title, String? message, String? payload}) async { - final generateNotificationId = DateTime.now().millisecondsSinceEpoch.remainder(100000); + Future _createAndroidNotificationChannelGroup() async { + await _localNotificationsPlugin + .resolvePlatformSpecificImplementation()! + .createNotificationChannelGroup(LocalNotificationConfig.androidNotificationChannelGroup); + } + + void showPushNotification({ + required String id, + required String title, + String? message, + EmailAddress? emailAddress, + String? payload + }) async { + final inboxStyleInformation = InboxStyleInformation( + [title, message ?? ''], + contentTitle: (emailAddress?.asString() ?? '').addBlockTag('b'), + summaryText: (emailAddress?.asString() ?? '').addBlockTag('b'), + htmlFormatTitle: true, + htmlFormatContent: true, + htmlFormatContentTitle: true, + htmlFormatSummaryText: true, + ); await _localNotificationsPlugin.show( - generateNotificationId, - title ?? LocalNotificationConfig.channelName, - message ?? LocalNotificationConfig.channelDescription, - LocalNotificationConfig.pushNotificationDetails, + id.hashCode, + title.addBlockTag('b'), + message ?? '', + LocalNotificationConfig.instance.generateNotificationDetails(styleInformation: inboxStyleInformation), payload: payload ); } + + void groupPushNotification() async { + final activeNotifications = await _localNotificationsPlugin + .resolvePlatformSpecificImplementation() + ?.getActiveNotifications(); + + if (activeNotifications != null && activeNotifications.isNotEmpty) { + await _localNotificationsPlugin.show( + 1995, + '', + '', + LocalNotificationConfig.instance.generateNotificationDetails(setAsGroup: true) + ); + } + } } \ No newline at end of file diff --git a/lib/features/push_notification/presentation/services/fcm_receiver.dart b/lib/features/push_notification/presentation/services/fcm_receiver.dart index bddbb8a42..71a4d9fb5 100644 --- a/lib/features/push_notification/presentation/services/fcm_receiver.dart +++ b/lib/features/push_notification/presentation/services/fcm_receiver.dart @@ -3,16 +3,9 @@ import 'package:core/utils/app_logger.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:tmail_ui_user/features/push_notification/presentation/services/fcm_service.dart'; -int semaphoreBackground = 0; - @pragma('vm:entry-point') Future handleFirebaseBackgroundMessage(RemoteMessage message) async { - if (semaphoreBackground != 0) { - return; - } - semaphoreBackground = 1; - Future.delayed(const Duration(milliseconds: FcmService.durationMessageComing)).then((_) => semaphoreBackground = 0); - + log('FcmReceiver::handleFirebaseBackgroundMessage():messageId: ${message.messageId}'); log('FcmReceiver::handleFirebaseBackgroundMessage(): ${message.data}'); FcmService.instance.handleFirebaseBackgroundMessage(message); } diff --git a/lib/features/thread/presentation/thread_controller.dart b/lib/features/thread/presentation/thread_controller.dart index 595b05816..fc75de779 100644 --- a/lib/features/thread/presentation/thread_controller.dart +++ b/lib/features/thread/presentation/thread_controller.dart @@ -267,7 +267,7 @@ class ThreadController extends BaseController with EmailActionController { _navigationRouter = action.navigationRouter; _activateSearchFromRouter(); mailboxDashBoardController.clearDashBoardAction(); - } else if (action is RefreshChangedEmailAction) { + } else if (action is RefreshChangeEmailAction) { if (action.newState != _currentEmailState) { _refreshEmailChanges(); } diff --git a/pubspec.lock b/pubspec.lock index 76c97fe42..ea5eae131 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -715,6 +715,15 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "8.0.8" + focus_detector: + dependency: "direct main" + description: + path: "." + ref: HEAD + resolved-ref: "4b3107b16a93f7c91eed93f0aac1eb1b205f4a45" + url: "https://github.com/ifnyas/focus_detector.git" + source: git + version: "2.0.1" forward: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 3758d8fca..4d84fa9c2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -196,10 +196,18 @@ dependencies: ref: master url_launcher: 6.1.5 + firebase_core: 2.2.0 + firebase_messaging: 14.0.4 + flutter_local_notifications: 12.0.3 + # https://pub.dev/packages/focus_detector + # Use for flutter 3.0.5 https://github.com/EdsonBueno/focus_detector/pull/14 + focus_detector: + git: https://github.com/ifnyas/focus_detector.git + dev_dependencies: flutter_test: sdk: flutter