TF-2599 Fix notification flood

Signed-off-by: dab246 <tdvu@linagora.com>
This commit is contained in:
dab246
2024-02-22 15:49:01 +07:00
committed by Dat H. Pham
parent 07042bfb45
commit a90e36c6b3
8 changed files with 124 additions and 87 deletions
+1 -1
View File
@@ -269,7 +269,7 @@ abstract class BaseController extends GetxController
FcmTokenController.instance.initialBindingInteractor(); FcmTokenController.instance.initialBindingInteractor();
await FcmReceiver.instance.onInitialFcmListener(); await FcmReceiver.instance.onInitialFcmListener();
if (PlatformInfo.isMobile) { if (PlatformInfo.isMobile) {
await LocalNotificationManager.instance.setUp(); await LocalNotificationManager.instance.setUp(groupId: session.username.value);
} }
} else { } else {
throw NotSupportFCMException(); throw NotSupportFCMException();
@@ -1,18 +1,20 @@
import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/failure.dart';
import 'package:core/presentation/state/success.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'; import 'package:jmap_dart_client/jmap/mail/email/email.dart';
class GetEmailChangesToRemoveNotificationLoading extends UIState {} class GetEmailChangesToRemoveNotificationLoading extends UIState {}
class GetEmailChangesToRemoveNotificationSuccess extends UIState { class GetEmailChangesToRemoveNotificationSuccess extends UIState {
final UserName userName;
final List<EmailId> emailIds; final List<EmailId> emailIds;
GetEmailChangesToRemoveNotificationSuccess(this.emailIds); GetEmailChangesToRemoveNotificationSuccess(this.userName, this.emailIds);
@override @override
List<Object> get props => [emailIds]; List<Object> get props => [userName, emailIds];
} }
class GetEmailChangesToRemoveNotificationFailure extends FeatureFailure { class GetEmailChangesToRemoveNotificationFailure extends FeatureFailure {
@@ -1,6 +1,7 @@
import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/failure.dart';
import 'package:core/presentation/state/success.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'; import 'package:model/mailbox/presentation_mailbox.dart';
class GetMailboxesNotPutNotificationsLoading extends UIState {} class GetMailboxesNotPutNotificationsLoading extends UIState {}
@@ -8,14 +9,17 @@ class GetMailboxesNotPutNotificationsLoading extends UIState {}
class GetMailboxesNotPutNotificationsSuccess extends UIState { class GetMailboxesNotPutNotificationsSuccess extends UIState {
final List<PresentationMailbox> mailboxes; final List<PresentationMailbox> mailboxes;
final UserName userName;
GetMailboxesNotPutNotificationsSuccess(this.mailboxes); GetMailboxesNotPutNotificationsSuccess(this.mailboxes, this.userName);
@override @override
List<Object> get props => [mailboxes]; List<Object> get props => [mailboxes, userName];
} }
class GetMailboxesNotPutNotificationsFailure extends FeatureFailure { class GetMailboxesNotPutNotificationsFailure extends FeatureFailure {
GetMailboxesNotPutNotificationsFailure(exception) : super(exception: exception); final UserName userName;
GetMailboxesNotPutNotificationsFailure(exception, this.userName) : super(exception: exception);
} }
@@ -38,7 +38,7 @@ class GetEmailChangesToRemoveNotificationInteractor {
currentState, currentState,
propertiesCreated: propertiesCreated, propertiesCreated: propertiesCreated,
propertiesUpdated: propertiesUpdated); propertiesUpdated: propertiesUpdated);
yield Right<Failure, Success>(GetEmailChangesToRemoveNotificationSuccess(emailIds)); yield Right<Failure, Success>(GetEmailChangesToRemoveNotificationSuccess(session.username, emailIds));
} else { } else {
yield Left<Failure, Success>(GetEmailChangesToRemoveNotificationFailure(NotFoundEmailStateException())); yield Left<Failure, Success>(GetEmailChangesToRemoveNotificationFailure(NotFoundEmailStateException()));
} }
@@ -15,9 +15,9 @@ class GetMailboxesNotPutNotificationsInteractor {
try { try {
yield Right<Failure, Success>(GetMailboxesNotPutNotificationsLoading()); yield Right<Failure, Success>(GetMailboxesNotPutNotificationsLoading());
final mailboxes = await _fcmRepository.getMailboxesNotPutNotifications(session, accountId); final mailboxes = await _fcmRepository.getMailboxesNotPutNotifications(session, accountId);
yield Right<Failure, Success>(GetMailboxesNotPutNotificationsSuccess(mailboxes)); yield Right<Failure, Success>(GetMailboxesNotPutNotificationsSuccess(mailboxes, session.username));
} catch (e) { } catch (e) {
yield Left<Failure, Success>(GetMailboxesNotPutNotificationsFailure(e)); yield Left<Failure, Success>(GetMailboxesNotPutNotificationsFailure(e, session.username));
} }
} }
} }
@@ -173,15 +173,17 @@ class EmailChangeListener extends ChangeListener {
} }
} }
void _showLocalNotification(PresentationEmail presentationEmail) { Future<void> _showLocalNotification(
final notificationPayload = NotificationPayload(emailId: presentationEmail.id); UserName userName,
log('EmailChangeListener::_showLocalNotification():notificationPayload: $notificationPayload'); PresentationEmail presentationEmail
LocalNotificationManager.instance.showPushNotification( ) async {
await LocalNotificationManager.instance.showPushNotification(
id: presentationEmail.id?.id.value ?? '', id: presentationEmail.id?.id.value ?? '',
title: presentationEmail.subject ?? '', title: presentationEmail.subject ?? '',
message: presentationEmail.preview, message: presentationEmail.preview,
emailAddress: presentationEmail.firstFromAddress, emailAddress: presentationEmail.firstFromAddress,
payload: notificationPayload.encodeToString, payload: NotificationPayload(emailId: presentationEmail.id).encodeToString,
groupId: userName.value
); );
} }
@@ -193,7 +195,10 @@ class EmailChangeListener extends ChangeListener {
_getStoredEmailState(); _getStoredEmailState();
} else if (failure is GetMailboxesNotPutNotificationsFailure) { } else if (failure is GetMailboxesNotPutNotificationsFailure) {
final listEmails = _emailsAvailablePushNotification.toEmailsAvailablePushNotification(); 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!); _storeEmailDeliveryStateAction(success.accountId, success.userName, _newStateEmailDelivery!);
if (PlatformInfo.isAndroid) { if (PlatformInfo.isAndroid) {
_handleListEmailToPushNotification(success.emailList); _handleListEmailToPushNotification(
userName: success.userName,
emailList: success.emailList
);
} }
} else if (success is GetMailboxesNotPutNotificationsSuccess) { } else if (success is GetMailboxesNotPutNotificationsSuccess) {
final listEmails = _emailsAvailablePushNotification.toEmailsAvailablePushNotification( final listEmails = _emailsAvailablePushNotification.toEmailsAvailablePushNotification(
mailboxIdsNotPutNotifications: success.mailboxes.mailboxIds); mailboxIdsNotPutNotifications: success.mailboxes.mailboxIds);
_handleLocalPushNotification(listEmails); _handleLocalPushNotification(
userName: success.userName,
emailList: listEmails
);
} else if (success is GetEmailChangesToRemoveNotificationSuccess) { } else if (success is GetEmailChangesToRemoveNotificationSuccess) {
_handleRemoveLocalNotification(success.emailIds); _handleRemoveLocalNotification(
userName: success.userName,
emailIds: success.emailIds
);
} else if (success is GetNewReceiveEmailFromNotificationSuccess) { } else if (success is GetNewReceiveEmailFromNotificationSuccess) {
_getListDetailedEmailByIdAction(success.session, success.accountId, success.emailIds); _getListDetailedEmailByIdAction(success.session, success.accountId, success.emailIds);
} else if (success is GetDetailedEmailByIdSuccess) { } else if (success is GetDetailedEmailByIdSuccess) {
@@ -226,27 +240,39 @@ class EmailChangeListener extends ChangeListener {
} }
} }
void _handleListEmailToPushNotification(List<PresentationEmail> emailList) { void _handleListEmailToPushNotification({
required UserName userName,
required List<PresentationEmail> emailList
}) {
_emailsAvailablePushNotification = emailList; _emailsAvailablePushNotification = emailList;
if (_getMailboxesNotPutNotificationsInteractor != null && _accountId != null && _session != null) { if (_getMailboxesNotPutNotificationsInteractor != null && _accountId != null && _session != null) {
consumeState(_getMailboxesNotPutNotificationsInteractor!.execute(_session!, _accountId!)); consumeState(_getMailboxesNotPutNotificationsInteractor!.execute(_session!, _accountId!));
} else { } else {
final listEmails = _emailsAvailablePushNotification.toEmailsAvailablePushNotification(); final listEmails = _emailsAvailablePushNotification.toEmailsAvailablePushNotification();
_handleLocalPushNotification(listEmails); _handleLocalPushNotification(
userName: userName,
emailList: listEmails
);
} }
} }
void _handleLocalPushNotification(List<PresentationEmail> emailList) { void _handleLocalPushNotification({
log('EmailChangeListener::_handleLocalPushNotification():emailList: $emailList'); required UserName userName,
required List<PresentationEmail> emailList
}) async {
log('EmailChangeListener::_handleLocalPushNotification(): EMAIL_LENGTH = ${emailList.length}');
if (emailList.isEmpty) { if (emailList.isEmpty) {
_emailsAvailablePushNotification.clear();
return; return;
} }
for (var presentationEmail in emailList) { 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(); _emailsAvailablePushNotification.clear();
} }
@@ -272,10 +298,13 @@ class EmailChangeListener extends ChangeListener {
} }
} }
void _handleRemoveLocalNotification(List<EmailId> emailIds) async { void _handleRemoveLocalNotification({
required UserName userName,
required List<EmailId> emailIds
}) async {
log('EmailChangeListener::_handleRemoveLocalNotification():emailIds: $emailIds'); log('EmailChangeListener::_handleRemoveLocalNotification():emailIds: $emailIds');
await Future.wait(emailIds.map((emailId) => LocalNotificationManager.instance.removeNotification(emailId.id.value))); 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) { void _getNewReceiveEmailFromNotificationAction(Session? session, AccountId accountId, jmap.State newState) {
@@ -1,54 +1,36 @@
import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart';
class LocalNotificationConfig { class LocalNotificationConfig {
static const _groupId = 'team_mail_notification_group_id'; static const String NOTIFICATION_CHANNEL = 'New Email';
static const _groupName = 'team_mail_notification_group_name'; static const int MIN_EMAILS_TO_GROUP = 3;
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 iosInitializationSettings = DarwinInitializationSettings(); static const iosInitializationSettings = DarwinInitializationSettings();
static const androidInitializationSettings = AndroidInitializationSettings('notification_icon'); 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(); LocalNotificationConfig._internal();
static final LocalNotificationConfig _instance = LocalNotificationConfig._internal(); static final LocalNotificationConfig _instance = LocalNotificationConfig._internal();
static LocalNotificationConfig get instance => _instance; static LocalNotificationConfig get instance => _instance;
NotificationDetails generateNotificationDetails({StyleInformation? styleInformation, bool setAsGroup = false}) { NotificationDetails generateNotificationDetails({
StyleInformation? styleInformation,
bool setAsGroup = false,
String? groupId,
}) {
return NotificationDetails( return NotificationDetails(
android: AndroidNotificationDetails( android: AndroidNotificationDetails(
androidNotificationChannel.id, NOTIFICATION_CHANNEL,
androidNotificationChannel.name, NOTIFICATION_CHANNEL,
channelDescription: androidNotificationChannel.description, groupKey: groupId,
groupKey: androidNotificationChannel.groupId,
visibility: NotificationVisibility.public, visibility: NotificationVisibility.public,
importance: Importance.max, importance: Importance.max,
priority: Priority.high, priority: Priority.high,
setAsGroupSummary: setAsGroup, setAsGroupSummary: setAsGroup,
styleInformation: styleInformation, styleInformation: styleInformation,
groupAlertBehavior: setAsGroup
? GroupAlertBehavior.summary
: GroupAlertBehavior.all,
channelShowBadge: true, channelShowBadge: true,
showWhen: true, showWhen: true,
largeIcon: const DrawableResourceAndroidBitmap('ic_large_notification') largeIcon: const DrawableResourceAndroidBitmap('ic_large_notification')
@@ -57,7 +39,7 @@ class LocalNotificationConfig {
presentSound: true, presentSound: true,
presentAlert: true, presentAlert: true,
presentBadge: true, presentBadge: true,
threadIdentifier: _channelId threadIdentifier: NOTIFICATION_CHANNEL
), ),
); );
} }
@@ -1,4 +1,3 @@
import 'dart:async'; import 'dart:async';
import 'package:core/presentation/extensions/html_extension.dart'; import 'package:core/presentation/extensions/html_extension.dart';
@@ -31,14 +30,14 @@ class LocalNotificationManager {
NotificationAppLaunchDetails? _notificationAppLaunchDetails; NotificationAppLaunchDetails? _notificationAppLaunchDetails;
Future<void> setUp() async { Future<void> setUp({required String groupId}) async {
try { try {
final isInitialNotification = await _initLocalNotification(); final isInitialNotification = await _initLocalNotification();
log('LocalNotificationManager::setUp:isInitialNotification: $isInitialNotification'); log('LocalNotificationManager::setUp:isInitialNotification: $isInitialNotification');
await _checkLocalNotificationPermission(); await _checkLocalNotificationPermission();
if (PlatformInfo.isAndroid) { if (PlatformInfo.isAndroid) {
await _createAndroidNotificationChannelGroup(); await _createAndroidNotificationChannelGroup(groupId);
await _createAndroidNotificationChannel(); await _createAndroidNotificationChannel(groupId);
} }
} catch (e) { } catch (e) {
logError('LocalNotificationManager::setUp(): ERROR: ${e.toString()}'); logError('LocalNotificationManager::setUp(): ERROR: ${e.toString()}');
@@ -81,10 +80,12 @@ class LocalNotificationManager {
Future<void> _checkLocalNotificationPermission() async { Future<void> _checkLocalNotificationPermission() async {
if (PlatformInfo.isAndroid) { if (PlatformInfo.isAndroid) {
final granted = await _isAndroidPermissionGranted(); final granted = await _isAndroidPermissionGranted();
log('LocalNotificationManager::requestPermissionAndroid: _isAndroidPermissionGranted = $granted');
if (!granted) { if (!granted) {
await _requestPermissions(); await _requestPermissions();
} }
} else if (PlatformInfo.isIOS) { } else if (PlatformInfo.isIOS) {
log('LocalNotificationManager::requestPermissionIOS');
await _requestPermissions(); await _requestPermissions();
} }
} }
@@ -109,25 +110,33 @@ class LocalNotificationManager {
} }
} }
Future<void> _createAndroidNotificationChannel() async { Future<void> _createAndroidNotificationChannel(String groupId) async {
return await _localNotificationsPlugin return await _localNotificationsPlugin
.resolvePlatformSpecificImplementation<AndroidFlutterLocalNotificationsPlugin>() .resolvePlatformSpecificImplementation<AndroidFlutterLocalNotificationsPlugin>()
?.createNotificationChannel(LocalNotificationConfig.androidNotificationChannel); ?.createNotificationChannel(AndroidNotificationChannel(
LocalNotificationConfig.NOTIFICATION_CHANNEL,
LocalNotificationConfig.NOTIFICATION_CHANNEL,
groupId: groupId,
));
} }
Future<void> _createAndroidNotificationChannelGroup() async { Future<void> _createAndroidNotificationChannelGroup(String groupId) async {
await _localNotificationsPlugin await _localNotificationsPlugin
.resolvePlatformSpecificImplementation<AndroidFlutterLocalNotificationsPlugin>()! .resolvePlatformSpecificImplementation<AndroidFlutterLocalNotificationsPlugin>()
.createNotificationChannelGroup(LocalNotificationConfig.androidNotificationChannelGroup); ?.createNotificationChannelGroup(AndroidNotificationChannelGroup(
groupId,
groupId,
));
} }
void showPushNotification({ Future<void> showPushNotification({
required String id, required String id,
required String title, required String title,
String? message, String? message,
EmailAddress? emailAddress, EmailAddress? emailAddress,
String? payload, String? payload,
bool isInboxStyle = true bool isInboxStyle = true,
String? groupId,
}) async { }) async {
if (isInboxStyle) { if (isInboxStyle) {
final inboxStyleInformation = InboxStyleInformation( final inboxStyleInformation = InboxStyleInformation(
@@ -143,7 +152,10 @@ class LocalNotificationManager {
id.hashCode, id.hashCode,
title, title,
message, message,
LocalNotificationConfig.instance.generateNotificationDetails(styleInformation: inboxStyleInformation), LocalNotificationConfig.instance.generateNotificationDetails(
styleInformation: inboxStyleInformation,
groupId: groupId
),
payload: payload payload: payload
); );
} else { } else {
@@ -151,7 +163,10 @@ class LocalNotificationManager {
id.hashCode, id.hashCode,
title, title,
message, message,
LocalNotificationConfig.instance.generateNotificationDetails(styleInformation: const DefaultStyleInformation(true, true)), LocalNotificationConfig.instance.generateNotificationDetails(
styleInformation: const DefaultStyleInformation(true, true),
groupId: groupId
),
payload: payload payload: payload
); );
} }
@@ -161,44 +176,49 @@ class LocalNotificationManager {
return _localNotificationsPlugin.cancel(id.hashCode); return _localNotificationsPlugin.cancel(id.hashCode);
} }
void groupPushNotification() async { Future<void> groupPushNotification({required String groupId}) async {
if (PlatformInfo.isIOS) {
return;
}
final activeNotifications = await _localNotificationsPlugin final activeNotifications = await _localNotificationsPlugin
.resolvePlatformSpecificImplementation<AndroidFlutterLocalNotificationsPlugin>() .resolvePlatformSpecificImplementation<AndroidFlutterLocalNotificationsPlugin>()
?.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( final inboxStyleInformation = InboxStyleInformation(
[''], [''],
summaryText: currentContext != null summaryText: currentContext != null
? AppLocalizations.of(currentContext!).totalNewMessagePushNotification(activeNotifications.length - 1).addBlockTag('b') ? AppLocalizations.of(currentContext!).totalNewMessagePushNotification(listActiveNotificationByGroup.length).addBlockTag('b')
: '${activeNotifications.length - 1} new emails'.addBlockTag('b'), : '${listActiveNotificationByGroup.length} new emails'.addBlockTag('b'),
htmlFormatSummaryText: true, htmlFormatSummaryText: true,
); );
await _localNotificationsPlugin.show( await _localNotificationsPlugin.show(
LocalNotificationConfig.groupNotificationId, groupId.hashCode,
null, null,
null, null,
LocalNotificationConfig.instance.generateNotificationDetails( LocalNotificationConfig.instance.generateNotificationDetails(
setAsGroup: true, setAsGroup: true,
styleInformation: inboxStyleInformation styleInformation: inboxStyleInformation,
groupId: groupId
), ),
); );
} }
} }
void removeGroupPushNotification() async { Future<void> removeGroupPushNotification(String groupId) async {
final activeNotifications = await _localNotificationsPlugin final activeNotifications = await _localNotificationsPlugin
.resolvePlatformSpecificImplementation<AndroidFlutterLocalNotificationsPlugin>() .resolvePlatformSpecificImplementation<AndroidFlutterLocalNotificationsPlugin>()
?.getActiveNotifications(); ?.getActiveNotifications() ?? [];
log('LocalNotificationManager::removeGroupPushNotification():activeNotifications: ${activeNotifications?.length}');
if (activeNotifications == null || activeNotifications.length <= 1) { 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'); log('LocalNotificationManager::groupPushNotification():canceled');
await _localNotificationsPlugin.cancel(LocalNotificationConfig.groupNotificationId); await _localNotificationsPlugin.cancel(groupId.hashCode);
} }
} }