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();
await FcmReceiver.instance.onInitialFcmListener();
if (PlatformInfo.isMobile) {
await LocalNotificationManager.instance.setUp();
await LocalNotificationManager.instance.setUp(groupId: session.username.value);
}
} else {
throw NotSupportFCMException();
@@ -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<EmailId> emailIds;
GetEmailChangesToRemoveNotificationSuccess(this.emailIds);
GetEmailChangesToRemoveNotificationSuccess(this.userName, this.emailIds);
@override
List<Object> get props => [emailIds];
List<Object> get props => [userName, emailIds];
}
class GetEmailChangesToRemoveNotificationFailure extends FeatureFailure {
@@ -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<PresentationMailbox> mailboxes;
final UserName userName;
GetMailboxesNotPutNotificationsSuccess(this.mailboxes);
GetMailboxesNotPutNotificationsSuccess(this.mailboxes, this.userName);
@override
List<Object> get props => [mailboxes];
List<Object> get props => [mailboxes, userName];
}
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,
propertiesCreated: propertiesCreated,
propertiesUpdated: propertiesUpdated);
yield Right<Failure, Success>(GetEmailChangesToRemoveNotificationSuccess(emailIds));
yield Right<Failure, Success>(GetEmailChangesToRemoveNotificationSuccess(session.username, emailIds));
} else {
yield Left<Failure, Success>(GetEmailChangesToRemoveNotificationFailure(NotFoundEmailStateException()));
}
@@ -15,9 +15,9 @@ class GetMailboxesNotPutNotificationsInteractor {
try {
yield Right<Failure, Success>(GetMailboxesNotPutNotificationsLoading());
final mailboxes = await _fcmRepository.getMailboxesNotPutNotifications(session, accountId);
yield Right<Failure, Success>(GetMailboxesNotPutNotificationsSuccess(mailboxes));
yield Right<Failure, Success>(GetMailboxesNotPutNotificationsSuccess(mailboxes, session.username));
} 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) {
final notificationPayload = NotificationPayload(emailId: presentationEmail.id);
log('EmailChangeListener::_showLocalNotification():notificationPayload: $notificationPayload');
LocalNotificationManager.instance.showPushNotification(
Future<void> _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<PresentationEmail> emailList) {
void _handleListEmailToPushNotification({
required UserName userName,
required List<PresentationEmail> 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<PresentationEmail> emailList) {
log('EmailChangeListener::_handleLocalPushNotification():emailList: $emailList');
void _handleLocalPushNotification({
required UserName userName,
required List<PresentationEmail> 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<EmailId> emailIds) async {
void _handleRemoveLocalNotification({
required UserName userName,
required List<EmailId> 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) {
@@ -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
),
);
}
@@ -1,4 +1,3 @@
import 'dart:async';
import 'package:core/presentation/extensions/html_extension.dart';
@@ -31,14 +30,14 @@ class LocalNotificationManager {
NotificationAppLaunchDetails? _notificationAppLaunchDetails;
Future<void> setUp() async {
Future<void> 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<void> _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<void> _createAndroidNotificationChannel() async {
Future<void> _createAndroidNotificationChannel(String groupId) async {
return await _localNotificationsPlugin
.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
.resolvePlatformSpecificImplementation<AndroidFlutterLocalNotificationsPlugin>()!
.createNotificationChannelGroup(LocalNotificationConfig.androidNotificationChannelGroup);
.resolvePlatformSpecificImplementation<AndroidFlutterLocalNotificationsPlugin>()
?.createNotificationChannelGroup(AndroidNotificationChannelGroup(
groupId,
groupId,
));
}
void showPushNotification({
Future<void> 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<void> groupPushNotification({required String groupId}) async {
final activeNotifications = await _localNotificationsPlugin
.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(
[''],
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<void> removeGroupPushNotification(String groupId) async {
final activeNotifications = await _localNotificationsPlugin
.resolvePlatformSpecificImplementation<AndroidFlutterLocalNotificationsPlugin>()
?.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);
}
}