diff --git a/lib/features/base/base_controller.dart b/lib/features/base/base_controller.dart index af34702e9..97b14ba25 100644 --- a/lib/features/base/base_controller.dart +++ b/lib/features/base/base_controller.dart @@ -269,7 +269,7 @@ abstract class BaseController extends GetxController FcmTokenController.instance.initialBindingInteractor(); await FcmReceiver.instance.onInitialFcmListener(); if (PlatformInfo.isMobile) { - await LocalNotificationManager.instance.setUp(); + await LocalNotificationManager.instance.setUp(groupId: session.username.value); } } else { throw NotSupportFCMException(); diff --git a/lib/features/push_notification/domain/state/get_email_changes_to_remove_notification_state.dart b/lib/features/push_notification/domain/state/get_email_changes_to_remove_notification_state.dart index e4e552cf4..52f2a92d2 100644 --- a/lib/features/push_notification/domain/state/get_email_changes_to_remove_notification_state.dart +++ b/lib/features/push_notification/domain/state/get_email_changes_to_remove_notification_state.dart @@ -1,18 +1,20 @@ import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; class GetEmailChangesToRemoveNotificationLoading extends UIState {} class GetEmailChangesToRemoveNotificationSuccess extends UIState { + final UserName userName; final List emailIds; - GetEmailChangesToRemoveNotificationSuccess(this.emailIds); + GetEmailChangesToRemoveNotificationSuccess(this.userName, this.emailIds); @override - List get props => [emailIds]; + List get props => [userName, emailIds]; } class GetEmailChangesToRemoveNotificationFailure extends FeatureFailure { diff --git a/lib/features/push_notification/domain/state/get_mailboxes_not_put_notifications_state.dart b/lib/features/push_notification/domain/state/get_mailboxes_not_put_notifications_state.dart index 86e1f90eb..5e1ebacba 100644 --- a/lib/features/push_notification/domain/state/get_mailboxes_not_put_notifications_state.dart +++ b/lib/features/push_notification/domain/state/get_mailboxes_not_put_notifications_state.dart @@ -1,6 +1,7 @@ import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; import 'package:model/mailbox/presentation_mailbox.dart'; class GetMailboxesNotPutNotificationsLoading extends UIState {} @@ -8,14 +9,17 @@ class GetMailboxesNotPutNotificationsLoading extends UIState {} class GetMailboxesNotPutNotificationsSuccess extends UIState { final List mailboxes; + final UserName userName; - GetMailboxesNotPutNotificationsSuccess(this.mailboxes); + GetMailboxesNotPutNotificationsSuccess(this.mailboxes, this.userName); @override - List get props => [mailboxes]; + List get props => [mailboxes, userName]; } class GetMailboxesNotPutNotificationsFailure extends FeatureFailure { - GetMailboxesNotPutNotificationsFailure(exception) : super(exception: exception); + final UserName userName; + + GetMailboxesNotPutNotificationsFailure(exception, this.userName) : super(exception: exception); } \ No newline at end of file diff --git a/lib/features/push_notification/domain/usecases/get_email_changes_to_remove_notification_interactor.dart b/lib/features/push_notification/domain/usecases/get_email_changes_to_remove_notification_interactor.dart index 24b017319..6c828c313 100644 --- a/lib/features/push_notification/domain/usecases/get_email_changes_to_remove_notification_interactor.dart +++ b/lib/features/push_notification/domain/usecases/get_email_changes_to_remove_notification_interactor.dart @@ -38,7 +38,7 @@ class GetEmailChangesToRemoveNotificationInteractor { currentState, propertiesCreated: propertiesCreated, propertiesUpdated: propertiesUpdated); - yield Right(GetEmailChangesToRemoveNotificationSuccess(emailIds)); + yield Right(GetEmailChangesToRemoveNotificationSuccess(session.username, emailIds)); } else { yield Left(GetEmailChangesToRemoveNotificationFailure(NotFoundEmailStateException())); } diff --git a/lib/features/push_notification/domain/usecases/get_mailboxes_not_put_notifications_interactor.dart b/lib/features/push_notification/domain/usecases/get_mailboxes_not_put_notifications_interactor.dart index b953a2d11..dc56c81c2 100644 --- a/lib/features/push_notification/domain/usecases/get_mailboxes_not_put_notifications_interactor.dart +++ b/lib/features/push_notification/domain/usecases/get_mailboxes_not_put_notifications_interactor.dart @@ -15,9 +15,9 @@ class GetMailboxesNotPutNotificationsInteractor { try { yield Right(GetMailboxesNotPutNotificationsLoading()); final mailboxes = await _fcmRepository.getMailboxesNotPutNotifications(session, accountId); - yield Right(GetMailboxesNotPutNotificationsSuccess(mailboxes)); + yield Right(GetMailboxesNotPutNotificationsSuccess(mailboxes, session.username)); } catch (e) { - yield Left(GetMailboxesNotPutNotificationsFailure(e)); + yield Left(GetMailboxesNotPutNotificationsFailure(e, session.username)); } } } \ No newline at end of file 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 2db8d5bf4..9c2cf976d 100644 --- a/lib/features/push_notification/presentation/listener/email_change_listener.dart +++ b/lib/features/push_notification/presentation/listener/email_change_listener.dart @@ -173,15 +173,17 @@ class EmailChangeListener extends ChangeListener { } } - void _showLocalNotification(PresentationEmail presentationEmail) { - final notificationPayload = NotificationPayload(emailId: presentationEmail.id); - log('EmailChangeListener::_showLocalNotification():notificationPayload: $notificationPayload'); - LocalNotificationManager.instance.showPushNotification( + Future _showLocalNotification( + UserName userName, + PresentationEmail presentationEmail + ) async { + await LocalNotificationManager.instance.showPushNotification( id: presentationEmail.id?.id.value ?? '', title: presentationEmail.subject ?? '', message: presentationEmail.preview, emailAddress: presentationEmail.firstFromAddress, - payload: notificationPayload.encodeToString, + payload: NotificationPayload(emailId: presentationEmail.id).encodeToString, + groupId: userName.value ); } @@ -193,7 +195,10 @@ class EmailChangeListener extends ChangeListener { _getStoredEmailState(); } else if (failure is GetMailboxesNotPutNotificationsFailure) { final listEmails = _emailsAvailablePushNotification.toEmailsAvailablePushNotification(); - _handleLocalPushNotification(listEmails); + _handleLocalPushNotification( + userName: failure.userName, + emailList: listEmails + ); } } @@ -208,14 +213,23 @@ class EmailChangeListener extends ChangeListener { _storeEmailDeliveryStateAction(success.accountId, success.userName, _newStateEmailDelivery!); if (PlatformInfo.isAndroid) { - _handleListEmailToPushNotification(success.emailList); + _handleListEmailToPushNotification( + userName: success.userName, + emailList: success.emailList + ); } } else if (success is GetMailboxesNotPutNotificationsSuccess) { final listEmails = _emailsAvailablePushNotification.toEmailsAvailablePushNotification( mailboxIdsNotPutNotifications: success.mailboxes.mailboxIds); - _handleLocalPushNotification(listEmails); + _handleLocalPushNotification( + userName: success.userName, + emailList: listEmails + ); } else if (success is GetEmailChangesToRemoveNotificationSuccess) { - _handleRemoveLocalNotification(success.emailIds); + _handleRemoveLocalNotification( + userName: success.userName, + emailIds: success.emailIds + ); } else if (success is GetNewReceiveEmailFromNotificationSuccess) { _getListDetailedEmailByIdAction(success.session, success.accountId, success.emailIds); } else if (success is GetDetailedEmailByIdSuccess) { @@ -226,27 +240,39 @@ class EmailChangeListener extends ChangeListener { } } - void _handleListEmailToPushNotification(List emailList) { + void _handleListEmailToPushNotification({ + required UserName userName, + required List emailList + }) { _emailsAvailablePushNotification = emailList; if (_getMailboxesNotPutNotificationsInteractor != null && _accountId != null && _session != null) { consumeState(_getMailboxesNotPutNotificationsInteractor!.execute(_session!, _accountId!)); } else { final listEmails = _emailsAvailablePushNotification.toEmailsAvailablePushNotification(); - _handleLocalPushNotification(listEmails); + _handleLocalPushNotification( + userName: userName, + emailList: listEmails + ); } } - void _handleLocalPushNotification(List emailList) { - log('EmailChangeListener::_handleLocalPushNotification():emailList: $emailList'); + void _handleLocalPushNotification({ + required UserName userName, + required List emailList + }) async { + log('EmailChangeListener::_handleLocalPushNotification(): EMAIL_LENGTH = ${emailList.length}'); if (emailList.isEmpty) { + _emailsAvailablePushNotification.clear(); return; } for (var presentationEmail in emailList) { - _showLocalNotification(presentationEmail); + await _showLocalNotification(userName, presentationEmail); } - LocalNotificationManager.instance.groupPushNotification(); + if (PlatformInfo.isAndroid) { + await LocalNotificationManager.instance.groupPushNotification(groupId: userName.value); + } _emailsAvailablePushNotification.clear(); } @@ -272,10 +298,13 @@ class EmailChangeListener extends ChangeListener { } } - void _handleRemoveLocalNotification(List emailIds) async { + void _handleRemoveLocalNotification({ + required UserName userName, + required List emailIds + }) async { log('EmailChangeListener::_handleRemoveLocalNotification():emailIds: $emailIds'); await Future.wait(emailIds.map((emailId) => LocalNotificationManager.instance.removeNotification(emailId.id.value))); - LocalNotificationManager.instance.removeGroupPushNotification(); + await LocalNotificationManager.instance.removeGroupPushNotification(userName.value); } void _getNewReceiveEmailFromNotificationAction(Session? session, AccountId accountId, jmap.State newState) { 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 21f0581e2..ecd8ab4f3 100644 --- a/lib/features/push_notification/presentation/notification/local_notification_config.dart +++ b/lib/features/push_notification/presentation/notification/local_notification_config.dart @@ -1,54 +1,36 @@ import 'package:flutter_local_notifications/flutter_local_notifications.dart'; class LocalNotificationConfig { - static const _groupId = 'team_mail_notification_group_id'; - static const _groupName = 'team_mail_notification_group_name'; - static const _groupDescription = 'Twake Mail group notifications'; - static const _channelId = 'team_mail_notification_channel_id'; - static const _channelName = 'Twake Mail notifications'; - static const _channelDescription = 'Twake Mail notifications'; - static const notificationTitle = 'Twake Mail'; - static const notificationMessage = 'You have new messages'; - static const messageHasBeenSentSuccessfully = 'Message has been sent successfully.'; - static const int groupNotificationId = 1995; + static const String NOTIFICATION_CHANNEL = 'New Email'; + static const int MIN_EMAILS_TO_GROUP = 3; static const iosInitializationSettings = DarwinInitializationSettings(); - static const androidInitializationSettings = AndroidInitializationSettings('notification_icon'); - static const androidNotificationChannel = AndroidNotificationChannel( - _channelId, - _channelName, - description: _channelDescription, - groupId: _groupId, - importance: Importance.max, - showBadge: 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}) { + NotificationDetails generateNotificationDetails({ + StyleInformation? styleInformation, + bool setAsGroup = false, + String? groupId, + }) { return NotificationDetails( android: AndroidNotificationDetails( - androidNotificationChannel.id, - androidNotificationChannel.name, - channelDescription: androidNotificationChannel.description, - groupKey: androidNotificationChannel.groupId, + NOTIFICATION_CHANNEL, + NOTIFICATION_CHANNEL, + groupKey: groupId, visibility: NotificationVisibility.public, importance: Importance.max, priority: Priority.high, setAsGroupSummary: setAsGroup, styleInformation: styleInformation, + groupAlertBehavior: setAsGroup + ? GroupAlertBehavior.summary + : GroupAlertBehavior.all, channelShowBadge: true, showWhen: true, largeIcon: const DrawableResourceAndroidBitmap('ic_large_notification') @@ -57,7 +39,7 @@ class LocalNotificationConfig { presentSound: true, presentAlert: true, presentBadge: true, - threadIdentifier: _channelId + threadIdentifier: NOTIFICATION_CHANNEL ), ); } 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 8b9b42ab1..d4187b7b6 100644 --- a/lib/features/push_notification/presentation/notification/local_notification_manager.dart +++ b/lib/features/push_notification/presentation/notification/local_notification_manager.dart @@ -1,4 +1,3 @@ - import 'dart:async'; import 'package:core/presentation/extensions/html_extension.dart'; @@ -31,14 +30,14 @@ class LocalNotificationManager { NotificationAppLaunchDetails? _notificationAppLaunchDetails; - Future setUp() async { + Future setUp({required String groupId}) async { try { final isInitialNotification = await _initLocalNotification(); log('LocalNotificationManager::setUp:isInitialNotification: $isInitialNotification'); await _checkLocalNotificationPermission(); if (PlatformInfo.isAndroid) { - await _createAndroidNotificationChannelGroup(); - await _createAndroidNotificationChannel(); + await _createAndroidNotificationChannelGroup(groupId); + await _createAndroidNotificationChannel(groupId); } } catch (e) { logError('LocalNotificationManager::setUp(): ERROR: ${e.toString()}'); @@ -81,10 +80,12 @@ class LocalNotificationManager { Future _checkLocalNotificationPermission() async { if (PlatformInfo.isAndroid) { final granted = await _isAndroidPermissionGranted(); + log('LocalNotificationManager::requestPermissionAndroid: _isAndroidPermissionGranted = $granted'); if (!granted) { await _requestPermissions(); } } else if (PlatformInfo.isIOS) { + log('LocalNotificationManager::requestPermissionIOS'); await _requestPermissions(); } } @@ -109,25 +110,33 @@ class LocalNotificationManager { } } - Future _createAndroidNotificationChannel() async { + Future _createAndroidNotificationChannel(String groupId) async { return await _localNotificationsPlugin .resolvePlatformSpecificImplementation() - ?.createNotificationChannel(LocalNotificationConfig.androidNotificationChannel); + ?.createNotificationChannel(AndroidNotificationChannel( + LocalNotificationConfig.NOTIFICATION_CHANNEL, + LocalNotificationConfig.NOTIFICATION_CHANNEL, + groupId: groupId, + )); } - Future _createAndroidNotificationChannelGroup() async { + Future _createAndroidNotificationChannelGroup(String groupId) async { await _localNotificationsPlugin - .resolvePlatformSpecificImplementation()! - .createNotificationChannelGroup(LocalNotificationConfig.androidNotificationChannelGroup); + .resolvePlatformSpecificImplementation() + ?.createNotificationChannelGroup(AndroidNotificationChannelGroup( + groupId, + groupId, + )); } - void showPushNotification({ + Future showPushNotification({ required String id, required String title, String? message, EmailAddress? emailAddress, String? payload, - bool isInboxStyle = true + bool isInboxStyle = true, + String? groupId, }) async { if (isInboxStyle) { final inboxStyleInformation = InboxStyleInformation( @@ -143,7 +152,10 @@ class LocalNotificationManager { id.hashCode, title, message, - LocalNotificationConfig.instance.generateNotificationDetails(styleInformation: inboxStyleInformation), + LocalNotificationConfig.instance.generateNotificationDetails( + styleInformation: inboxStyleInformation, + groupId: groupId + ), payload: payload ); } else { @@ -151,7 +163,10 @@ class LocalNotificationManager { id.hashCode, title, message, - LocalNotificationConfig.instance.generateNotificationDetails(styleInformation: const DefaultStyleInformation(true, true)), + LocalNotificationConfig.instance.generateNotificationDetails( + styleInformation: const DefaultStyleInformation(true, true), + groupId: groupId + ), payload: payload ); } @@ -161,44 +176,49 @@ class LocalNotificationManager { return _localNotificationsPlugin.cancel(id.hashCode); } - void groupPushNotification() async { - if (PlatformInfo.isIOS) { - return; - } - + Future groupPushNotification({required String groupId}) async { final activeNotifications = await _localNotificationsPlugin .resolvePlatformSpecificImplementation() - ?.getActiveNotifications(); + ?.getActiveNotifications() ?? []; - if (activeNotifications != null && activeNotifications.isNotEmpty) { + final listActiveNotificationByGroup = activeNotifications + .where((notification) => notification.groupKey == groupId) + .toList(); + log('LocalNotificationManager::groupPushNotification(): groupId = $groupId | activeNotifications = ${activeNotifications.length} | listActiveNotificationByGroup = ${listActiveNotificationByGroup.length}'); + if (listActiveNotificationByGroup.length >= LocalNotificationConfig.MIN_EMAILS_TO_GROUP) { final inboxStyleInformation = InboxStyleInformation( [''], summaryText: currentContext != null - ? AppLocalizations.of(currentContext!).totalNewMessagePushNotification(activeNotifications.length - 1).addBlockTag('b') - : '${activeNotifications.length - 1} new emails'.addBlockTag('b'), + ? AppLocalizations.of(currentContext!).totalNewMessagePushNotification(listActiveNotificationByGroup.length).addBlockTag('b') + : '${listActiveNotificationByGroup.length} new emails'.addBlockTag('b'), htmlFormatSummaryText: true, ); await _localNotificationsPlugin.show( - LocalNotificationConfig.groupNotificationId, + groupId.hashCode, null, null, LocalNotificationConfig.instance.generateNotificationDetails( setAsGroup: true, - styleInformation: inboxStyleInformation + styleInformation: inboxStyleInformation, + groupId: groupId ), ); } } - void removeGroupPushNotification() async { + Future removeGroupPushNotification(String groupId) async { final activeNotifications = await _localNotificationsPlugin .resolvePlatformSpecificImplementation() - ?.getActiveNotifications(); - log('LocalNotificationManager::removeGroupPushNotification():activeNotifications: ${activeNotifications?.length}'); - if (activeNotifications == null || activeNotifications.length <= 1) { + ?.getActiveNotifications() ?? []; + + final listActiveNotificationByGroup = activeNotifications + .where((notification) => notification.groupKey == groupId) + .toList(); + log('LocalNotificationManager::removeGroupPushNotification(): activeNotifications = ${activeNotifications.length} | listActiveNotificationByGroup = ${listActiveNotificationByGroup.length}'); + if (listActiveNotificationByGroup.length <= 1) { log('LocalNotificationManager::groupPushNotification():canceled'); - await _localNotificationsPlugin.cancel(LocalNotificationConfig.groupNotificationId); + await _localNotificationsPlugin.cancel(groupId.hashCode); } }