From 197e72ca9353ede679c8c1adc59012ccdbf3bd66 Mon Sep 17 00:00:00 2001 From: Boehrsi Date: Fri, 24 Apr 2020 10:25:49 +0200 Subject: [PATCH] Duplicated notifications #447 OT-715 --- .../background_refresh_manager.dart | 2 +- lib/src/data/repository_manager.dart | 2 +- lib/src/l10n/l.dart | 2 +- .../local_notification_manager.dart | 146 +++++++++++------- lib/src/platform/preferences.dart | 2 + 5 files changed, 92 insertions(+), 62 deletions(-) diff --git a/lib/src/background_refresh/background_refresh_manager.dart b/lib/src/background_refresh/background_refresh_manager.dart index 619d0b05..196b2fd0 100644 --- a/lib/src/background_refresh/background_refresh_manager.dart +++ b/lib/src/background_refresh/background_refresh_manager.dart @@ -62,7 +62,7 @@ Future getMessages() async { await context.interruptIdleForIncomingMessages(); var localNotificationManager = LocalNotificationManager(); localNotificationManager.setup(); - await localNotificationManager.triggerNotification(); + await localNotificationManager.triggerNotificationAsync(); } class BackgroundRefreshManager { diff --git a/lib/src/data/repository_manager.dart b/lib/src/data/repository_manager.dart index 9f397e4e..7f11a9f5 100644 --- a/lib/src/data/repository_manager.dart +++ b/lib/src/data/repository_manager.dart @@ -57,7 +57,7 @@ class RepositoryManager { static Map _repositories = Map(); - static Repository get(RepositoryType type, [int id]) { + static Repository get(RepositoryType type, [int id]) { String identifier = getIdentifier(type, id); if (_repositories.containsKey(identifier)) { return _repositories[identifier]; diff --git a/lib/src/l10n/l.dart b/lib/src/l10n/l.dart index 1e3dbafc..866df7fe 100644 --- a/lib/src/l10n/l.dart +++ b/lib/src/l10n/l.dart @@ -114,7 +114,7 @@ class L { static final readReceiptText = _translationKey( "This is a read receipt.\n\nIt means the message was displayed on the recipient's device, not necessarily that the content was read."); static final voiceMessage = _translationKey("Voice message"); - static final moreMessages = _translationKey("more messages"); + static final moreMessagesX = _translationKey("%d more messages"); static final privacyDeclaration = _translationKey("privacy declaration"); static final termsConditions = _translationKey("terms & conditions"); static final code = _translationKey("Code"); diff --git a/lib/src/notifications/local_notification_manager.dart b/lib/src/notifications/local_notification_manager.dart index 42daa1aa..2c49cc13 100644 --- a/lib/src/notifications/local_notification_manager.dart +++ b/lib/src/notifications/local_notification_manager.dart @@ -40,26 +40,30 @@ * for more details. */ +import 'dart:collection'; +import 'dart:convert'; + import 'package:delta_chat_core/delta_chat_core.dart'; import 'package:logging/logging.dart'; import 'package:ox_coi/src/data/chat_message_repository.dart'; -import 'package:ox_coi/src/data/repository.dart'; import 'package:ox_coi/src/data/repository_manager.dart'; import 'package:ox_coi/src/l10n/l.dart'; import 'package:ox_coi/src/l10n/l10n.dart'; import 'package:ox_coi/src/notifications/notification_manager.dart'; +import 'package:ox_coi/src/platform/preferences.dart'; import 'package:rxdart/rxdart.dart'; class LocalNotificationManager { static LocalNotificationManager _instance; + final Logger _logger = Logger("local_notification_manager"); + final _messageSubject = PublishSubject(); + final _chatRepository = RepositoryManager.get(RepositoryType.chat); + final _contactRepository = RepositoryManager.get(RepositoryType.contact); + final _temporaryMessageRepository = ChatMessageRepository(ChatMsg.getCreator()); + final _core = DeltaChatCore(); + final _context = Context(); - Repository _chatRepository = RepositoryManager.get(RepositoryType.chat); - Repository _temporaryMessageRepository = ChatMessageRepository(ChatMsg.getCreator()); - Repository _inviteMessageListRepository = RepositoryManager.get(RepositoryType.chatMessage, Chat.typeInvite); - PublishSubject _messageSubject = new PublishSubject(); - DeltaChatCore _core = DeltaChatCore(); - Context _context = Context(); NotificationManager _notificationManager; bool _listenersRegistered = false; @@ -68,96 +72,120 @@ class LocalNotificationManager { LocalNotificationManager._internal(); void setup() { + _registerListeners(); + } + + void tearDown() { + _unregisterListeners(); + } + + void _registerListeners() { if (!_listenersRegistered) { _listenersRegistered = true; _notificationManager = NotificationManager(); - _messageSubject.listen(_successCallback); + _messageSubject.listen(_messagesUpdated); _core.addListener(eventIdList: [Event.incomingMsg, Event.msgsChanged], streamController: _messageSubject); } } - Future tearDown() async { + void _unregisterListeners() { if (_listenersRegistered) { _core.removeListener(_messageSubject); _listenersRegistered = false; } } - void _successCallback(Event event) { + void _messagesUpdated(Event event) { _logger.info("Callback event for local notification received"); - triggerNotification(); + triggerNotificationAsync(); } - Future triggerNotification() async { + Future triggerNotificationAsync() async { _logger.info("Local notification triggered"); - await createFreshMessagesNotifications(); - await createInviteNotifications(); + await createChatNotificationsAsync(); + await createInviteNotificationsAsync(); } - Future createFreshMessagesNotifications() async { - var createdNotifications = List(); - List notNotifiedMessages = await getNotNotifiedMessages(); - _temporaryMessageRepository.putIfAbsent(ids: notNotifiedMessages); - Future.forEach(notNotifiedMessages, (int messageId) async { - ChatMsg message = _temporaryMessageRepository.get(messageId); - int chatId = await message.getChatId(); - if (!createdNotifications.contains(chatId) && chatId > Chat.typeLastSpecial) { - Chat chat = _chatRepository.get(chatId); - if (chat == null) { - _chatRepository.putIfAbsent(id: chatId); - chat = _chatRepository.get(chatId); - } - createdNotifications.add(chatId); + Future createChatNotificationsAsync() async { + final HashMap notificationHistory = await _getNotificationHistoryAsync(); + final List freshMessages = await _context.getFreshMessages(); + _temporaryMessageRepository.putIfAbsent(ids: freshMessages); + _logger.info("Handling ${freshMessages.length} fresh messages"); + + await Future.forEach(freshMessages, (int messageId) async { + final message = _temporaryMessageRepository.get(messageId); + final chatId = await message.getChatId(); + if (isMessageNew(notificationHistory, chatId, messageId)) { + notificationHistory.update(chatId.toString(), (value) => messageId, ifAbsent: () => messageId); + _chatRepository.putIfAbsent(id: chatId); + final chat = _chatRepository.get(chatId); String title = await chat.getName(); - int count = (await _context.getFreshMessageCount(chatId)) - 1; + final count = (await _context.getFreshMessageCount(chatId)) - 1; if (count > 1) { - title = "$title (+ $count ${L10n.get(L.moreMessages)})"; + title = "$title (+ ${L10n.getFormatted(L.moreMessagesX, [count])})"; } - String teaser = await message.getSummaryText(200); - var payload = chatId?.toString(); + final teaser = await message.getSummaryText(200); + final payload = chatId?.toString(); + _logger.info("Creating chat notification for chat id $chatId with message id $messageId"); _notificationManager.showNotificationFromLocal(chatId, title, teaser, payload: payload); } }); + + await _setNotificationHistoryAsync(notificationHistory); } - Future> getNotNotifiedMessages() async { - var freshMessages = List.from(await _context.getFreshMessages()); - freshMessages.removeWhere((int messageId) => _temporaryMessageRepository.contains(messageId)); - _logger.info("Temporary notification repository (messages) contains ${_temporaryMessageRepository.length()} messages"); - return freshMessages; + bool isMessageNew(HashMap notificationHistory, int keyId, int messageId, {bool isInvite = false}) { + final isNormalMessage = isInvite ? keyId > Contact.idLastSpecial : keyId > Chat.typeLastSpecial; + if (isNormalMessage) { + final chatIdString = keyId.toString(); + if (notificationHistory.keys.contains(chatIdString)) { + return notificationHistory[chatIdString] < messageId; + } + return true; + } + return false; + } + + Future> _getNotificationHistoryAsync({bool isInvite = false}) async { + final preferenceTarget = isInvite ? preferenceNotificationInviteHistory : preferenceNotificationHistory; + final notificationHistoryString = await getPreference(preferenceTarget); + return notificationHistoryString != null ? HashMap.from(json.decode(notificationHistoryString)) : HashMap(); } - Future createInviteNotifications() async { - var createdNotifications = List(); - List notNotifiedInvites = await getNotNotifiedInvites(); - _inviteMessageListRepository.putIfAbsent(ids: notNotifiedInvites); - Repository contactRepository = RepositoryManager.get(RepositoryType.contact); - Future.forEach(notNotifiedInvites.reversed, (int messageId) async { - ChatMsg invite = _inviteMessageListRepository.get(messageId); - int senderId = await invite.getFromId(); - if (!createdNotifications.contains(senderId)) { - createdNotifications.add(senderId); - contactRepository.putIfAbsent(id: senderId); - Contact contact = contactRepository.get(senderId); - var contactName = await contact.getName(); - var contactMail = await contact.getAddress(); + Future _setNotificationHistoryAsync(HashMap notificationHistory, {bool isInvite = false}) async { + final preferenceTarget = isInvite ? preferenceNotificationInviteHistory : preferenceNotificationHistory; + final notificationHistoryString = json.encode(notificationHistory); + await setPreference(preferenceTarget, notificationHistoryString); + } + + Future createInviteNotificationsAsync() async { + final HashMap notificationInviteHistory = await _getNotificationHistoryAsync(isInvite: true); + final List openInvites = await _context.getChatMessages(Chat.typeInvite); + _temporaryMessageRepository.putIfAbsent(ids: openInvites); + _logger.info("Handling ${openInvites.length} open invites"); + + await Future.forEach(openInvites.reversed, (int messageId) async { + final message = _temporaryMessageRepository.get(messageId); + final senderId = await message.getFromId(); + if (isMessageNew(notificationInviteHistory, senderId, messageId)) { + notificationInviteHistory.update(senderId.toString(), (value) => messageId, ifAbsent: () => messageId); + _contactRepository.putIfAbsent(id: senderId); + final contact = _contactRepository.get(senderId); + final contactName = await contact.getName(); + final contactMail = await contact.getAddress(); String title; if (contactName.isNotEmpty) { title = L10n.getFormatted(L.chatListInviteDialogXY, [contactName, contactMail]); } else { title = L10n.getFormatted(L.chatListInviteDialogX, [contactMail]); } - String teaser = await invite.getSummaryText(200); - String payload = "${Chat.typeInvite.toString()}_$messageId"; + final teaser = await message.getSummaryText(200); + final payload = "${Chat.typeInvite.toString()}_$messageId"; + _logger.info("Creating invite notification for sender id $senderId with message id $messageId"); _notificationManager.showNotificationFromLocal(Chat.typeInvite, title, teaser, payload: payload); } }); - } - Future> getNotNotifiedInvites() async { - var invites = List.from(await _context.getChatMessages(Chat.typeInvite)); - invites.removeWhere((int messageId) => _inviteMessageListRepository.contains(messageId)); - _logger.info("Temporary notification repository (invites) contains ${_inviteMessageListRepository.length()} messages"); - return invites; + await _setNotificationHistoryAsync(notificationInviteHistory, isInvite: true); } } diff --git a/lib/src/platform/preferences.dart b/lib/src/platform/preferences.dart index 6362575a..54b11ab6 100644 --- a/lib/src/platform/preferences.dart +++ b/lib/src/platform/preferences.dart @@ -55,6 +55,8 @@ const preferenceAppState = "preferenceAppState"; const preferenceInviteServiceUrl = "preferenceInviteServiceUrl"; const preferenceHasAuthenticationError = "preferenceHasAuthenticationError"; const preferenceApplicationTheme = "preferenceApplicationTheme"; +const preferenceNotificationHistory = "preferenceNotificationHistory"; +const preferenceNotificationInviteHistory = "preferenceNotificationInviteHistory"; const preferenceNotificationsAuth = "preferenceNotificationsAuth"; // Unused const preferenceNotificationsP256dhPublic = "preferenceNotificationsP256dhPublic"; // Unused