diff --git a/changelog.d/4401.removal b/changelog.d/4401.removal new file mode 100644 index 0000000000..ce58372a18 --- /dev/null +++ b/changelog.d/4401.removal @@ -0,0 +1 @@ +Breaking SDK API change to PushRuleListener, the separated callbacks have been merged into one with a data class which includes all the previously separated push information diff --git a/changelog.d/4402.feature b/changelog.d/4402.feature new file mode 100644 index 0000000000..29b9f9a337 --- /dev/null +++ b/changelog.d/4402.feature @@ -0,0 +1 @@ +Adds support for images inside message notifications \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/PushEvents.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/PushEvents.kt new file mode 100644 index 0000000000..466e345cad --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/PushEvents.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.matrix.android.sdk.api.pushrules + +import org.matrix.android.sdk.api.pushrules.rest.PushRule +import org.matrix.android.sdk.api.session.events.model.Event + +data class PushEvents( + val matchedEvents: List>, + val roomsJoined: Collection, + val roomsLeft: Collection, + val redactedEventIds: List +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/PushRuleService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/PushRuleService.kt index 1d0acf38fa..88268f0f86 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/PushRuleService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/PushRuleService.kt @@ -51,11 +51,7 @@ interface PushRuleService { // fun fulfilledBingRule(event: Event, rules: List): PushRule? interface PushRuleListener { - fun onMatchRule(event: Event, actions: List) - fun onRoomJoined(roomId: String) - fun onRoomLeft(roomId: String) - fun onEventRedacted(redactedEventId: String) - fun batchFinish() + fun onEvents(pushEvents: PushEvents) } fun getKeywords(): LiveData> diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/DefaultPushRuleService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/DefaultPushRuleService.kt index 65974151c8..3e821b8956 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/DefaultPushRuleService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/DefaultPushRuleService.kt @@ -19,6 +19,7 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.Transformations import com.zhuinden.monarchy.Monarchy import org.matrix.android.sdk.api.pushrules.Action +import org.matrix.android.sdk.api.pushrules.PushEvents import org.matrix.android.sdk.api.pushrules.PushRuleService import org.matrix.android.sdk.api.pushrules.RuleKind import org.matrix.android.sdk.api.pushrules.RuleScope @@ -142,79 +143,6 @@ internal class DefaultPushRuleService @Inject constructor( return pushRuleFinder.fulfilledBingRule(event, rules)?.getActions().orEmpty() } -// fun processEvents(events: List) { -// var hasDoneSomething = false -// events.forEach { event -> -// fulfilledBingRule(event)?.let { -// hasDoneSomething = true -// dispatchBing(event, it) -// } -// } -// if (hasDoneSomething) -// dispatchFinish() -// } - - fun dispatchBing(event: Event, rule: PushRule) { - synchronized(listeners) { - val actionsList = rule.getActions() - listeners.forEach { - try { - it.onMatchRule(event, actionsList) - } catch (e: Throwable) { - Timber.e(e, "Error while dispatching bing") - } - } - } - } - - fun dispatchRoomJoined(roomId: String) { - synchronized(listeners) { - listeners.forEach { - try { - it.onRoomJoined(roomId) - } catch (e: Throwable) { - Timber.e(e, "Error while dispatching room joined") - } - } - } - } - - fun dispatchRoomLeft(roomId: String) { - synchronized(listeners) { - listeners.forEach { - try { - it.onRoomLeft(roomId) - } catch (e: Throwable) { - Timber.e(e, "Error while dispatching room left") - } - } - } - } - - fun dispatchRedactedEventId(redactedEventId: String) { - synchronized(listeners) { - listeners.forEach { - try { - it.onEventRedacted(redactedEventId) - } catch (e: Throwable) { - Timber.e(e, "Error while dispatching redacted event") - } - } - } - } - - fun dispatchFinish() { - synchronized(listeners) { - listeners.forEach { - try { - it.batchFinish() - } catch (e: Throwable) { - Timber.e(e, "Error while dispatching finish") - } - } - } - } - override fun getKeywords(): LiveData> { // Keywords are all content rules that don't start with '.' val liveData = monarchy.findAllMappedWithChanges( @@ -229,4 +157,16 @@ internal class DefaultPushRuleService @Inject constructor( results.firstOrNull().orEmpty().toSet() } } + + fun dispatchEvents(pushEvents: PushEvents) { + synchronized(listeners) { + listeners.forEach { + try { + it.onEvents(pushEvents) + } catch (e: Throwable) { + Timber.e(e, "Error while dispatching push events") + } + } + } + } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/ProcessEventForPushTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/ProcessEventForPushTask.kt index 3c74888eda..0ac21b555e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/ProcessEventForPushTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/ProcessEventForPushTask.kt @@ -16,6 +16,7 @@ package org.matrix.android.sdk.internal.session.notification +import org.matrix.android.sdk.api.pushrules.PushEvents import org.matrix.android.sdk.api.pushrules.rest.PushRule import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.isInvitation @@ -39,14 +40,6 @@ internal class DefaultProcessEventForPushTask @Inject constructor( ) : ProcessEventForPushTask { override suspend fun execute(params: ProcessEventForPushTask.Params) { - // Handle left rooms - params.syncResponse.leave.keys.forEach { - defaultPushRuleService.dispatchRoomLeft(it) - } - // Handle joined rooms - params.syncResponse.join.keys.forEach { - defaultPushRuleService.dispatchRoomJoined(it) - } val newJoinEvents = params.syncResponse.join .mapNotNull { (key, value) -> value.timeline?.events?.mapNotNull { @@ -74,10 +67,10 @@ internal class DefaultProcessEventForPushTask @Inject constructor( } Timber.v("[PushRules] Found ${allEvents.size} out of ${(newJoinEvents + inviteEvents).size}" + " to check for push rules with ${params.rules.size} rules") - allEvents.forEach { event -> + val matchedEvents = allEvents.mapNotNull { event -> pushRuleFinder.fulfilledBingRule(event, params.rules)?.let { Timber.v("[PushRules] Rule $it match for event ${event.eventId}") - defaultPushRuleService.dispatchBing(event, it) + event to it } } @@ -91,10 +84,13 @@ internal class DefaultProcessEventForPushTask @Inject constructor( Timber.v("[PushRules] Found ${allRedactedEvents.size} redacted events") - allRedactedEvents.forEach { redactedEventId -> - defaultPushRuleService.dispatchRedactedEventId(redactedEventId) - } - - defaultPushRuleService.dispatchFinish() + defaultPushRuleService.dispatchEvents( + PushEvents( + matchedEvents = matchedEvents, + roomsJoined = params.syncResponse.join.keys, + roomsLeft = params.syncResponse.leave.keys, + redactedEventIds = allRedactedEvents + ) + ) } } diff --git a/vector/src/gplay/java/im/vector/app/gplay/push/fcm/VectorFirebaseMessagingService.kt b/vector/src/gplay/java/im/vector/app/gplay/push/fcm/VectorFirebaseMessagingService.kt index 63d50d4f97..e323506e9f 100755 --- a/vector/src/gplay/java/im/vector/app/gplay/push/fcm/VectorFirebaseMessagingService.kt +++ b/vector/src/gplay/java/im/vector/app/gplay/push/fcm/VectorFirebaseMessagingService.kt @@ -201,8 +201,7 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() { resolvedEvent ?.also { Timber.tag(loggerTag.value).d("Fast lane: notify drawer") } ?.let { - notificationDrawerManager.onNotifiableEventReceived(it) - notificationDrawerManager.refreshNotificationDrawer() + notificationDrawerManager.updateEvents { it.onNotifiableEventReceived(resolvedEvent) } } } } diff --git a/vector/src/main/java/im/vector/app/core/extensions/BasicExtensions.kt b/vector/src/main/java/im/vector/app/core/extensions/BasicExtensions.kt index ee3d79d846..dbe90dfdc1 100644 --- a/vector/src/main/java/im/vector/app/core/extensions/BasicExtensions.kt +++ b/vector/src/main/java/im/vector/app/core/extensions/BasicExtensions.kt @@ -66,3 +66,7 @@ fun String?.insertBeforeLast(insert: String, delimiter: String = "."): String { replaceRange(idx, idx, insert) } } + +inline fun Any?.takeAs(): R? { + return takeIf { it is R } as R? +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt index d3af9c4196..dbd9c4466b 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt @@ -2102,12 +2102,12 @@ class RoomDetailFragment @Inject constructor( // VectorInviteView.Callback override fun onAcceptInvite() { - notificationDrawerManager.clearMemberShipNotificationForRoom(roomDetailArgs.roomId) + notificationDrawerManager.updateEvents { it.clearMemberShipNotificationForRoom(roomDetailArgs.roomId) } roomDetailViewModel.handle(RoomDetailAction.AcceptInvite) } override fun onRejectInvite() { - notificationDrawerManager.clearMemberShipNotificationForRoom(roomDetailArgs.roomId) + notificationDrawerManager.updateEvents { it.clearMemberShipNotificationForRoom(roomDetailArgs.roomId) } roomDetailViewModel.handle(RoomDetailAction.RejectInvite) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListFragment.kt index 0e049e22b1..6543cc8795 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListFragment.kt @@ -482,7 +482,7 @@ class RoomListFragment @Inject constructor( } override fun onAcceptRoomInvitation(room: RoomSummary) { - notificationDrawerManager.clearMemberShipNotificationForRoom(room.roomId) + notificationDrawerManager.updateEvents { it.clearMemberShipNotificationForRoom(room.roomId) } roomListViewModel.handle(RoomListAction.AcceptInvitation(room)) } @@ -495,7 +495,7 @@ class RoomListFragment @Inject constructor( } override fun onRejectRoomInvitation(room: RoomSummary) { - notificationDrawerManager.clearMemberShipNotificationForRoom(room.roomId) + notificationDrawerManager.updateEvents { it.clearMemberShipNotificationForRoom(room.roomId) } roomListViewModel.handle(RoomListAction.RejectInvitation(room)) } } diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt b/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt index d2db73af3d..87b31fa92a 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt @@ -15,8 +15,10 @@ */ package im.vector.app.features.notifications +import android.net.Uri import im.vector.app.BuildConfig import im.vector.app.R +import im.vector.app.core.extensions.takeAs import im.vector.app.core.resources.StringProvider import im.vector.app.features.displayname.getBestName import im.vector.app.features.home.room.detail.timeline.format.DisplayableEventFormatter @@ -28,12 +30,15 @@ import org.matrix.android.sdk.api.session.crypto.MXCryptoError import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.isEdition +import org.matrix.android.sdk.api.session.events.model.isImageMessage import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.RoomMemberContent +import org.matrix.android.sdk.api.session.room.model.message.MessageWithAttachmentContent import org.matrix.android.sdk.api.session.room.sender.SenderInfo import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.session.room.timeline.getEditedEventId +import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent import org.matrix.android.sdk.api.util.toMatrixItem import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult import timber.log.Timber @@ -49,11 +54,12 @@ import javax.inject.Inject class NotifiableEventResolver @Inject constructor( private val stringProvider: StringProvider, private val noticeEventFormatter: NoticeEventFormatter, - private val displayableEventFormatter: DisplayableEventFormatter) { + private val displayableEventFormatter: DisplayableEventFormatter +) { // private val eventDisplay = RiotEventDisplay(context) - fun resolveEvent(event: Event/*, roomState: RoomState?, bingRule: PushRule?*/, session: Session, isNoisy: Boolean): NotifiableEvent? { + suspend fun resolveEvent(event: Event/*, roomState: RoomState?, bingRule: PushRule?*/, session: Session, isNoisy: Boolean): NotifiableEvent? { val roomID = event.roomId ?: return null val eventId = event.eventId ?: return null if (event.getClearType() == EventType.STATE_ROOM_MEMBER) { @@ -89,7 +95,7 @@ class NotifiableEventResolver @Inject constructor( } } - fun resolveInMemoryEvent(session: Session, event: Event, canBeReplaced: Boolean): NotifiableEvent? { + suspend fun resolveInMemoryEvent(session: Session, event: Event, canBeReplaced: Boolean): NotifiableEvent? { if (event.getClearType() != EventType.MESSAGE) return null // Ignore message edition @@ -120,7 +126,7 @@ class NotifiableEventResolver @Inject constructor( } } - private fun resolveMessageEvent(event: TimelineEvent, session: Session, canBeReplaced: Boolean, isNoisy: Boolean): NotifiableEvent { + private suspend fun resolveMessageEvent(event: TimelineEvent, session: Session, canBeReplaced: Boolean, isNoisy: Boolean): NotifiableEvent { // The event only contains an eventId, and roomId (type is m.room.*) , we need to get the displayable content (names, avatar, text, etc...) val room = session.getRoom(event.root.roomId!! /*roomID cannot be null*/) @@ -140,6 +146,7 @@ class NotifiableEventResolver @Inject constructor( senderName = senderDisplayName, senderId = event.root.senderId, body = body.toString(), + imageUri = event.fetchImageIfPresent(session), roomId = event.root.roomId!!, roomName = roomName, matrixID = session.myUserId @@ -173,6 +180,7 @@ class NotifiableEventResolver @Inject constructor( senderName = senderDisplayName, senderId = event.root.senderId, body = body, + imageUri = event.fetchImageIfPresent(session), roomId = event.root.roomId!!, roomName = roomName, roomIsDirect = room.roomSummary()?.isDirect ?: false, @@ -192,6 +200,26 @@ class NotifiableEventResolver @Inject constructor( } } + private suspend fun TimelineEvent.fetchImageIfPresent(session: Session): Uri? { + return when { + root.isEncrypted() && root.mxDecryptionResult == null -> null + root.isImageMessage() -> downloadAndExportImage(session) + else -> null + } + } + + private suspend fun TimelineEvent.downloadAndExportImage(session: Session): Uri? { + return kotlin.runCatching { + getLastMessageContent()?.takeAs()?.let { imageMessage -> + val fileService = session.fileService() + fileService.downloadFile(imageMessage) + fileService.getTemporarySharableURI(imageMessage) + } + }.onFailure { + Timber.e(it, "Failed to download and export image for notification") + }.getOrNull() + } + private fun resolveStateRoomEvent(event: Event, session: Session, canBeReplaced: Boolean, isNoisy: Boolean): NotifiableEvent? { val content = event.content?.toModel() ?: return null val roomId = event.roomId ?: return null diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotifiableMessageEvent.kt b/vector/src/main/java/im/vector/app/features/notifications/NotifiableMessageEvent.kt index 161c9f74a6..35718666b0 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotifiableMessageEvent.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotifiableMessageEvent.kt @@ -15,6 +15,7 @@ */ package im.vector.app.features.notifications +import android.net.Uri import org.matrix.android.sdk.api.session.events.model.EventType data class NotifiableMessageEvent( @@ -26,6 +27,7 @@ data class NotifiableMessageEvent( val senderName: String?, val senderId: String?, val body: String?, + val imageUri: Uri?, val roomId: String, val roomName: String?, val roomIsDirect: Boolean = false, diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationBroadcastReceiver.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationBroadcastReceiver.kt index 33e43cd7e4..b1905059a1 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotificationBroadcastReceiver.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationBroadcastReceiver.kt @@ -49,26 +49,26 @@ class NotificationBroadcastReceiver : BroadcastReceiver() { NotificationUtils.SMART_REPLY_ACTION -> handleSmartReply(intent, context) NotificationUtils.DISMISS_ROOM_NOTIF_ACTION -> - intent.getStringExtra(KEY_ROOM_ID)?.let { - notificationDrawerManager.clearMessageEventOfRoom(it) + intent.getStringExtra(KEY_ROOM_ID)?.let { roomId -> + notificationDrawerManager.updateEvents { it.clearMessagesForRoom(roomId) } } NotificationUtils.DISMISS_SUMMARY_ACTION -> notificationDrawerManager.clearAllEvents() NotificationUtils.MARK_ROOM_READ_ACTION -> - intent.getStringExtra(KEY_ROOM_ID)?.let { - notificationDrawerManager.clearMessageEventOfRoom(it) - handleMarkAsRead(it) + intent.getStringExtra(KEY_ROOM_ID)?.let { roomId -> + notificationDrawerManager.updateEvents { it.clearMessagesForRoom(roomId) } + handleMarkAsRead(roomId) } NotificationUtils.JOIN_ACTION -> { - intent.getStringExtra(KEY_ROOM_ID)?.let { - notificationDrawerManager.clearMemberShipNotificationForRoom(it) - handleJoinRoom(it) + intent.getStringExtra(KEY_ROOM_ID)?.let { roomId -> + notificationDrawerManager.updateEvents { it.clearMemberShipNotificationForRoom(roomId) } + handleJoinRoom(roomId) } } NotificationUtils.REJECT_ACTION -> { - intent.getStringExtra(KEY_ROOM_ID)?.let { - notificationDrawerManager.clearMemberShipNotificationForRoom(it) - handleRejectRoom(it) + intent.getStringExtra(KEY_ROOM_ID)?.let { roomId -> + notificationDrawerManager.updateEvents { it.clearMemberShipNotificationForRoom(roomId) } + handleRejectRoom(roomId) } } } @@ -138,6 +138,7 @@ class NotificationBroadcastReceiver : BroadcastReceiver() { ?: context?.getString(R.string.notification_sender_me), senderId = session.myUserId, body = message, + imageUri = null, roomId = room.roomId, roomName = room.roomSummary()?.displayName ?: room.roomId, roomIsDirect = room.roomSummary()?.isDirect == true, @@ -145,8 +146,7 @@ class NotificationBroadcastReceiver : BroadcastReceiver() { canBeReplaced = false ) - notificationDrawerManager.onNotifiableEventReceived(notifiableMessageEvent) - notificationDrawerManager.refreshNotificationDrawer() + notificationDrawerManager.updateEvents { it.onNotifiableEventReceived(notifiableMessageEvent) } /* // TODO Error cannot be managed the same way than in Riot diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationDrawerManager.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationDrawerManager.kt index c052de650e..e86a65c3fe 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotificationDrawerManager.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationDrawerManager.kt @@ -93,7 +93,7 @@ class NotificationDrawerManager @Inject constructor(private val context: Context #refreshNotificationDrawer() is called. Events might be grouped and there might not be one notification per event! */ - fun onNotifiableEventReceived(notifiableEvent: NotifiableEvent) { + fun NotificationEventQueue.onNotifiableEventReceived(notifiableEvent: NotifiableEvent) { if (!vectorPreferences.areNotificationEnabledForDevice()) { Timber.i("Notification are disabled for this device") return @@ -105,87 +105,15 @@ class NotificationDrawerManager @Inject constructor(private val context: Context } else { Timber.d("onNotifiableEventReceived(): is push: ${notifiableEvent.canBeReplaced}") } - synchronized(queuedEvents) { - val existing = queuedEvents.firstOrNull { it.eventId == notifiableEvent.eventId } - if (existing != null) { - if (existing.canBeReplaced) { - // Use the event coming from the event stream as it may contains more info than - // the fcm one (like type/content/clear text) (e.g when an encrypted message from - // FCM should be update with clear text after a sync) - // In this case the message has already been notified, and might have done some noise - // So we want the notification to be updated even if it has already been displayed - // Use setOnlyAlertOnce to ensure update notification does not interfere with sound - // from first notify invocation as outlined in: - // https://developer.android.com/training/notify-user/build-notification#Updating - queuedEvents.remove(existing) - queuedEvents.add(notifiableEvent) - } else { - // keep the existing one, do not replace - } - } else { - // Check if this is an edit - if (notifiableEvent.editedEventId != null) { - // This is an edition - val eventBeforeEdition = queuedEvents.firstOrNull { - // Edition of an event - it.eventId == notifiableEvent.editedEventId || - // or edition of an edition - it.editedEventId == notifiableEvent.editedEventId - } - if (eventBeforeEdition != null) { - // Replace the existing notification with the new content - queuedEvents.remove(eventBeforeEdition) - - queuedEvents.add(notifiableEvent) - } else { - // Ignore an edit of a not displayed event in the notification drawer - } - } else { - // Not an edit - if (seenEventIds.contains(notifiableEvent.eventId)) { - // we've already seen the event, lets skip - Timber.d("onNotifiableEventReceived(): skipping event, already seen") - } else { - seenEventIds.put(notifiableEvent.eventId) - queuedEvents.add(notifiableEvent) - } - } - } - } - } - - fun onEventRedacted(eventId: String) { - synchronized(queuedEvents) { - queuedEvents.replace(eventId) { - when (it) { - is InviteNotifiableEvent -> it.copy(isRedacted = true) - is NotifiableMessageEvent -> it.copy(isRedacted = true) - is SimpleNotifiableEvent -> it.copy(isRedacted = true) - } - } - } + add(notifiableEvent, seenEventIds) } /** * Clear all known events and refresh the notification drawer */ fun clearAllEvents() { - synchronized(queuedEvents) { - queuedEvents.clear() - } - refreshNotificationDrawer() - } - - /** Clear all known message events for this room */ - fun clearMessageEventOfRoom(roomId: String?) { - Timber.v("clearMessageEventOfRoom $roomId") - if (roomId != null) { - val shouldUpdate = removeAll { it is NotifiableMessageEvent && it.roomId == roomId } - if (shouldUpdate) { - refreshNotificationDrawer() - } - } + updateEvents { it.clear() } } /** @@ -193,32 +121,36 @@ class NotificationDrawerManager @Inject constructor(private val context: Context * Used to ignore events related to that room (no need to display notification) and clean any existing notification on this room. */ fun setCurrentRoom(roomId: String?) { - var hasChanged: Boolean - synchronized(queuedEvents) { - hasChanged = roomId != currentRoomId + updateEvents { + val hasChanged = roomId != currentRoomId currentRoomId = roomId - } - if (hasChanged) { - clearMessageEventOfRoom(roomId) + if (hasChanged && roomId != null) { + it.clearMessagesForRoom(roomId) + } } } - fun clearMemberShipNotificationForRoom(roomId: String) { - val shouldUpdate = removeAll { it is InviteNotifiableEvent && it.roomId == roomId } - if (shouldUpdate) { - refreshNotificationDrawerBg() + fun notificationStyleChanged() { + updateEvents { + val newSettings = vectorPreferences.useCompleteNotificationFormat() + if (newSettings != useCompleteNotificationFormat) { + // Settings has changed, remove all current notifications + notificationDisplayer.cancelAllNotifications() + useCompleteNotificationFormat = newSettings + } } } - private fun removeAll(predicate: (NotifiableEvent) -> Boolean): Boolean { - return synchronized(queuedEvents) { - queuedEvents.removeAll(predicate) + fun updateEvents(action: NotificationDrawerManager.(NotificationEventQueue) -> Unit) { + synchronized(queuedEvents) { + action(this, queuedEvents) } + refreshNotificationDrawer() } private var firstThrottler = FirstThrottler(200) - fun refreshNotificationDrawer() { + private fun refreshNotificationDrawer() { // Implement last throttler val canHandle = firstThrottler.canHandle() Timber.v("refreshNotificationDrawer(), delay: ${canHandle.waitMillis()} ms") @@ -239,18 +171,9 @@ class NotificationDrawerManager @Inject constructor(private val context: Context @WorkerThread private fun refreshNotificationDrawerBg() { Timber.v("refreshNotificationDrawerBg()") - - val newSettings = vectorPreferences.useCompleteNotificationFormat() - if (newSettings != useCompleteNotificationFormat) { - // Settings has changed, remove all current notifications - notificationDisplayer.cancelAllNotifications() - useCompleteNotificationFormat = newSettings - } - val eventsToRender = synchronized(queuedEvents) { - notifiableEventProcessor.process(queuedEvents, currentRoomId, renderedEvents).also { - queuedEvents.clear() - queuedEvents.addAll(it.onlyKeptEvents()) + notifiableEventProcessor.process(queuedEvents.rawEvents(), currentRoomId, renderedEvents).also { + queuedEvents.clearAndAdd(it.onlyKeptEvents()) } } @@ -286,7 +209,7 @@ class NotificationDrawerManager @Inject constructor(private val context: Context val file = File(context.applicationContext.cacheDir, ROOMS_NOTIFICATIONS_FILE_NAME) if (!file.exists()) file.createNewFile() FileOutputStream(file).use { - currentSession?.securelyStoreObject(queuedEvents, KEY_ALIAS_SECRET_STORAGE, it) + currentSession?.securelyStoreObject(queuedEvents.rawEvents(), KEY_ALIAS_SECRET_STORAGE, it) } } catch (e: Throwable) { Timber.e(e, "## Failed to save cached notification info") @@ -294,21 +217,21 @@ class NotificationDrawerManager @Inject constructor(private val context: Context } } - private fun loadEventInfo(): MutableList { + private fun loadEventInfo(): NotificationEventQueue { try { val file = File(context.applicationContext.cacheDir, ROOMS_NOTIFICATIONS_FILE_NAME) if (file.exists()) { file.inputStream().use { val events: ArrayList? = currentSession?.loadSecureSecret(it, KEY_ALIAS_SECRET_STORAGE) if (events != null) { - return events.toMutableList() + return NotificationEventQueue(events.toMutableList()) } } } } catch (e: Throwable) { Timber.e(e, "## Failed to load cached notification info") } - return ArrayList() + return NotificationEventQueue() } private fun deleteCachedRoomNotifications() { @@ -330,11 +253,3 @@ class NotificationDrawerManager @Inject constructor(private val context: Context private const val KEY_ALIAS_SECRET_STORAGE = "notificationMgr" } } - -private fun MutableList.replace(eventId: String, block: (NotifiableEvent) -> NotifiableEvent) { - val indexToReplace = indexOfFirst { it.eventId == eventId } - if (indexToReplace == -1) { - return - } - set(indexToReplace, block(get(indexToReplace))) -} diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationEventQueue.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationEventQueue.kt new file mode 100644 index 0000000000..83beb4fa02 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationEventQueue.kt @@ -0,0 +1,130 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.notifications + +import timber.log.Timber + +class NotificationEventQueue( + private val queue: MutableList = mutableListOf() +) { + + fun markRedacted(eventIds: List) { + eventIds.forEach { redactedId -> + queue.replace(redactedId) { + when (it) { + is InviteNotifiableEvent -> it.copy(isRedacted = true) + is NotifiableMessageEvent -> it.copy(isRedacted = true) + is SimpleNotifiableEvent -> it.copy(isRedacted = true) + } + } + } + } + + fun syncRoomEvents(roomsLeft: Collection, roomsJoined: Collection) { + if (roomsLeft.isNotEmpty() || roomsJoined.isNotEmpty()) { + queue.removeAll { + when (it) { + is NotifiableMessageEvent -> roomsLeft.contains(it.roomId) + is InviteNotifiableEvent -> roomsLeft.contains(it.roomId) || roomsJoined.contains(it.roomId) + else -> false + } + } + } + } + + fun isEmpty() = queue.isEmpty() + + fun clearAndAdd(events: List) { + queue.clear() + queue.addAll(events) + } + + fun clear() { + queue.clear() + } + + fun add(notifiableEvent: NotifiableEvent, seenEventIds: CircularCache) { + val existing = findExistingById(notifiableEvent) + val edited = findEdited(notifiableEvent) + when { + existing != null -> { + if (existing.canBeReplaced) { + // Use the event coming from the event stream as it may contains more info than + // the fcm one (like type/content/clear text) (e.g when an encrypted message from + // FCM should be update with clear text after a sync) + // In this case the message has already been notified, and might have done some noise + // So we want the notification to be updated even if it has already been displayed + // Use setOnlyAlertOnce to ensure update notification does not interfere with sound + // from first notify invocation as outlined in: + // https://developer.android.com/training/notify-user/build-notification#Updating + replace(replace = existing, with = notifiableEvent) + } else { + // keep the existing one, do not replace + } + } + edited != null -> { + // Replace the existing notification with the new content + replace(replace = edited, with = notifiableEvent) + } + seenEventIds.contains(notifiableEvent.eventId) -> { + // we've already seen the event, lets skip + Timber.d("onNotifiableEventReceived(): skipping event, already seen") + } + else -> { + seenEventIds.put(notifiableEvent.eventId) + queue.add(notifiableEvent) + } + } + } + + private fun findExistingById(notifiableEvent: NotifiableEvent): NotifiableEvent? { + return queue.firstOrNull { it.eventId == notifiableEvent.eventId } + } + + private fun findEdited(notifiableEvent: NotifiableEvent): NotifiableEvent? { + return notifiableEvent.editedEventId?.let { editedId -> + queue.firstOrNull { + it.eventId == editedId || it.editedEventId == editedId + } + } + } + + private fun replace(replace: NotifiableEvent, with: NotifiableEvent) { + queue.remove(replace) + queue.add(with) + } + + fun clearMemberShipNotificationForRoom(roomId: String) { + Timber.v("clearMemberShipOfRoom $roomId") + queue.removeAll { it is InviteNotifiableEvent && it.roomId == roomId } + } + + fun clearMessagesForRoom(roomId: String) { + Timber.v("clearMessageEventOfRoom $roomId") + queue.removeAll { it is NotifiableMessageEvent && it.roomId == roomId } + } + + fun rawEvents(): List = queue +} + +private fun MutableList.replace(eventId: String, block: (NotifiableEvent) -> NotifiableEvent) { + val indexToReplace = indexOfFirst { it.eventId == eventId } + if (indexToReplace == -1) { + return + } + set(indexToReplace, block(get(indexToReplace))) +} diff --git a/vector/src/main/java/im/vector/app/features/notifications/PushRuleTriggerListener.kt b/vector/src/main/java/im/vector/app/features/notifications/PushRuleTriggerListener.kt index abbbd47f95..ff817520db 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/PushRuleTriggerListener.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/PushRuleTriggerListener.kt @@ -16,10 +16,15 @@ package im.vector.app.features.notifications -import org.matrix.android.sdk.api.pushrules.Action +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancelChildren +import kotlinx.coroutines.launch +import org.matrix.android.sdk.api.pushrules.PushEvents import org.matrix.android.sdk.api.pushrules.PushRuleService +import org.matrix.android.sdk.api.pushrules.getActions import org.matrix.android.sdk.api.session.Session -import org.matrix.android.sdk.api.session.events.model.Event import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton @@ -31,45 +36,36 @@ class PushRuleTriggerListener @Inject constructor( ) : PushRuleService.PushRuleListener { private var session: Session? = null + private val scope: CoroutineScope = CoroutineScope(SupervisorJob()) - override fun onMatchRule(event: Event, actions: List) { - Timber.v("Push rule match for event ${event.eventId}") - val safeSession = session ?: return Unit.also { - Timber.e("Called without active session") + override fun onEvents(pushEvents: PushEvents) { + scope.launch { + session?.let { session -> + val notifiableEvents = createNotifiableEvents(pushEvents, session) + notificationDrawerManager.updateEvents { queuedEvents -> + notifiableEvents.forEach { notifiableEvent -> + queuedEvents.onNotifiableEventReceived(notifiableEvent) + } + queuedEvents.syncRoomEvents(roomsLeft = pushEvents.roomsLeft, roomsJoined = pushEvents.roomsJoined) + queuedEvents.markRedacted(pushEvents.redactedEventIds) + } + } ?: Timber.e("Called without active session") } + } - val notificationAction = actions.toNotificationAction() - if (notificationAction.shouldNotify) { - val notifiableEvent = resolver.resolveEvent(event, safeSession, isNoisy = !notificationAction.soundName.isNullOrBlank()) - if (notifiableEvent == null) { - Timber.v("## Failed to resolve event") - // TODO + private suspend fun createNotifiableEvents(pushEvents: PushEvents, session: Session): List { + return pushEvents.matchedEvents.mapNotNull { (event, pushRule) -> + Timber.v("Push rule match for event ${event.eventId}") + val action = pushRule.getActions().toNotificationAction() + if (action.shouldNotify) { + resolver.resolveEvent(event, session, isNoisy = !action.soundName.isNullOrBlank()) } else { - Timber.v("New event to notify") - notificationDrawerManager.onNotifiableEventReceived(notifiableEvent) + Timber.v("Matched push rule is set to not notify") + null } - } else { - Timber.v("Matched push rule is set to not notify") } } - override fun onRoomLeft(roomId: String) { - notificationDrawerManager.clearMessageEventOfRoom(roomId) - notificationDrawerManager.clearMemberShipNotificationForRoom(roomId) - } - - override fun onRoomJoined(roomId: String) { - notificationDrawerManager.clearMemberShipNotificationForRoom(roomId) - } - - override fun onEventRedacted(redactedEventId: String) { - notificationDrawerManager.onEventRedacted(redactedEventId) - } - - override fun batchFinish() { - notificationDrawerManager.refreshNotificationDrawer() - } - fun startWithSession(session: Session) { if (this.session != null) { stop() @@ -79,6 +75,7 @@ class PushRuleTriggerListener @Inject constructor( } fun stop() { + scope.coroutineContext.cancelChildren(CancellationException("PushRuleTriggerListener stopping")) session?.removePushRuleListener(this) session = null notificationDrawerManager.clearAllEvents() diff --git a/vector/src/main/java/im/vector/app/features/notifications/RoomGroupMessageCreator.kt b/vector/src/main/java/im/vector/app/features/notifications/RoomGroupMessageCreator.kt index d50ccded64..b57c81f686 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/RoomGroupMessageCreator.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/RoomGroupMessageCreator.kt @@ -98,7 +98,14 @@ class RoomGroupMessageCreator @Inject constructor( } when { event.isSmartReplyError() -> addMessage(stringProvider.getString(R.string.notification_inline_reply_failed), event.timestamp, senderPerson) - else -> addMessage(event.body, event.timestamp, senderPerson) + else -> { + val message = NotificationCompat.MessagingStyle.Message(event.body, event.timestamp, senderPerson).also { message -> + event.imageUri?.let { + message.setData("image/", it) + } + } + addMessage(message) + } } } } diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsPinFragment.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsPinFragment.kt index 1a04dab950..254c82e285 100644 --- a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsPinFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsPinFragment.kt @@ -55,7 +55,7 @@ class VectorSettingsPinFragment @Inject constructor( useCompleteNotificationPref.setOnPreferenceChangeListener { _, _ -> // Refresh the drawer for an immediate effect of this change - notificationDrawerManager.refreshNotificationDrawer() + notificationDrawerManager.notificationStyleChanged() true } } diff --git a/vector/src/test/java/im/vector/app/features/notifications/NotifiableEventProcessorTest.kt b/vector/src/test/java/im/vector/app/features/notifications/NotifiableEventProcessorTest.kt index 229ab39d1d..131a423316 100644 --- a/vector/src/test/java/im/vector/app/features/notifications/NotifiableEventProcessorTest.kt +++ b/vector/src/test/java/im/vector/app/features/notifications/NotifiableEventProcessorTest.kt @@ -19,6 +19,9 @@ package im.vector.app.features.notifications import im.vector.app.features.notifications.ProcessedEvent.Type import im.vector.app.test.fakes.FakeAutoAcceptInvites import im.vector.app.test.fakes.FakeOutdatedEventDetector +import im.vector.app.test.fixtures.aNotifiableMessageEvent +import im.vector.app.test.fixtures.aSimpleNotifiableEvent +import im.vector.app.test.fixtures.anInviteNotifiableEvent import org.amshove.kluent.shouldBeEqualTo import org.junit.Test import org.matrix.android.sdk.api.session.events.model.EventType @@ -145,48 +148,3 @@ class NotifiableEventProcessorTest { ProcessedEvent(it.first, it.second) } } - -fun aSimpleNotifiableEvent(eventId: String, type: String? = null) = SimpleNotifiableEvent( - matrixID = null, - eventId = eventId, - editedEventId = null, - noisy = false, - title = "title", - description = "description", - type = type, - timestamp = 0, - soundName = null, - canBeReplaced = false, - isRedacted = false -) - -fun anInviteNotifiableEvent(roomId: String) = InviteNotifiableEvent( - matrixID = null, - eventId = "event-id", - roomId = roomId, - roomName = "a room name", - editedEventId = null, - noisy = false, - title = "title", - description = "description", - type = null, - timestamp = 0, - soundName = null, - canBeReplaced = false, - isRedacted = false -) - -fun aNotifiableMessageEvent(eventId: String, roomId: String) = NotifiableMessageEvent( - eventId = eventId, - editedEventId = null, - noisy = false, - timestamp = 0, - senderName = "sender-name", - senderId = "sending-id", - body = "message-body", - roomId = roomId, - roomName = "room-name", - roomIsDirect = false, - canBeReplaced = false, - isRedacted = false -) diff --git a/vector/src/test/java/im/vector/app/features/notifications/NotificationEventQueueTest.kt b/vector/src/test/java/im/vector/app/features/notifications/NotificationEventQueueTest.kt new file mode 100644 index 0000000000..26c0030d26 --- /dev/null +++ b/vector/src/test/java/im/vector/app/features/notifications/NotificationEventQueueTest.kt @@ -0,0 +1,216 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.notifications + +import im.vector.app.test.fixtures.aNotifiableMessageEvent +import im.vector.app.test.fixtures.aSimpleNotifiableEvent +import im.vector.app.test.fixtures.anInviteNotifiableEvent +import org.amshove.kluent.shouldBeEqualTo +import org.junit.Test + +class NotificationEventQueueTest { + + private val seenIdsCache = CircularCache.create(5) + + @Test + fun `given events when redacting some then marks matching event ids as redacted`() { + val queue = givenQueue(listOf( + aSimpleNotifiableEvent(eventId = "redacted-id-1"), + aNotifiableMessageEvent(eventId = "redacted-id-2"), + anInviteNotifiableEvent(eventId = "redacted-id-3"), + aSimpleNotifiableEvent(eventId = "kept-id"), + )) + + queue.markRedacted(listOf("redacted-id-1", "redacted-id-2", "redacted-id-3")) + + queue.rawEvents() shouldBeEqualTo listOf( + aSimpleNotifiableEvent(eventId = "redacted-id-1", isRedacted = true), + aNotifiableMessageEvent(eventId = "redacted-id-2", isRedacted = true), + anInviteNotifiableEvent(eventId = "redacted-id-3", isRedacted = true), + aSimpleNotifiableEvent(eventId = "kept-id", isRedacted = false), + ) + } + + @Test + fun `given invite event when leaving invited room and syncing then removes event`() { + val queue = givenQueue(listOf(anInviteNotifiableEvent(roomId = "a-room-id"))) + val roomsLeft = listOf("a-room-id") + + queue.syncRoomEvents(roomsLeft = roomsLeft, roomsJoined = emptyList()) + + queue.rawEvents() shouldBeEqualTo emptyList() + } + + @Test + fun `given invite event when joining invited room and syncing then removes event`() { + val queue = givenQueue(listOf(anInviteNotifiableEvent(roomId = "a-room-id"))) + val joinedRooms = listOf("a-room-id") + + queue.syncRoomEvents(roomsLeft = emptyList(), roomsJoined = joinedRooms) + + queue.rawEvents() shouldBeEqualTo emptyList() + } + + @Test + fun `given message event when leaving message room and syncing then removes event`() { + val queue = givenQueue(listOf(aNotifiableMessageEvent(roomId = "a-room-id"))) + val roomsLeft = listOf("a-room-id") + + queue.syncRoomEvents(roomsLeft = roomsLeft, roomsJoined = emptyList()) + + queue.rawEvents() shouldBeEqualTo emptyList() + } + + @Test + fun `given events when syncing without rooms left or joined ids then does not change the events`() { + val queue = givenQueue(listOf( + aNotifiableMessageEvent(roomId = "a-room-id"), + anInviteNotifiableEvent(roomId = "a-room-id") + )) + + queue.syncRoomEvents(roomsLeft = emptyList(), roomsJoined = emptyList()) + + queue.rawEvents() shouldBeEqualTo listOf( + aNotifiableMessageEvent(roomId = "a-room-id"), + anInviteNotifiableEvent(roomId = "a-room-id") + ) + } + + @Test + fun `given events then is not empty`() { + val queue = givenQueue(listOf(aSimpleNotifiableEvent())) + + queue.isEmpty() shouldBeEqualTo false + } + + @Test + fun `given no events then is empty`() { + val queue = givenQueue(emptyList()) + + queue.isEmpty() shouldBeEqualTo true + } + + @Test + fun `given events when clearing and adding then removes previous events and adds only new events`() { + val queue = givenQueue(listOf(aSimpleNotifiableEvent())) + + queue.clearAndAdd(listOf(anInviteNotifiableEvent())) + + queue.rawEvents() shouldBeEqualTo listOf(anInviteNotifiableEvent()) + } + + @Test + fun `when clearing then is empty`() { + val queue = givenQueue(listOf(aSimpleNotifiableEvent())) + + queue.clear() + + queue.rawEvents() shouldBeEqualTo emptyList() + } + + @Test + fun `given no events when adding then adds event`() { + val queue = givenQueue(listOf()) + + queue.add(aSimpleNotifiableEvent(), seenEventIds = seenIdsCache) + + queue.rawEvents() shouldBeEqualTo listOf(aSimpleNotifiableEvent()) + } + + @Test + fun `given no events when adding already seen event then ignores event`() { + val queue = givenQueue(listOf()) + val notifiableEvent = aSimpleNotifiableEvent() + seenIdsCache.put(notifiableEvent.eventId) + + queue.add(notifiableEvent, seenEventIds = seenIdsCache) + + queue.rawEvents() shouldBeEqualTo emptyList() + } + + @Test + fun `given replaceable event when adding event with same id then updates existing event`() { + val replaceableEvent = aSimpleNotifiableEvent(canBeReplaced = true) + val updatedEvent = replaceableEvent.copy(title = "updated title") + val queue = givenQueue(listOf(replaceableEvent)) + + queue.add(updatedEvent, seenEventIds = seenIdsCache) + + queue.rawEvents() shouldBeEqualTo listOf(updatedEvent) + } + + @Test + fun `given non replaceable event when adding event with same id then ignores event`() { + val nonReplaceableEvent = aSimpleNotifiableEvent(canBeReplaced = false) + val updatedEvent = nonReplaceableEvent.copy(title = "updated title") + val queue = givenQueue(listOf(nonReplaceableEvent)) + + queue.add(updatedEvent, seenEventIds = seenIdsCache) + + queue.rawEvents() shouldBeEqualTo listOf(nonReplaceableEvent) + } + + @Test + fun `given event when adding new event with edited event id matching the existing event id then updates existing event`() { + val editedEvent = aSimpleNotifiableEvent(eventId = "id-to-edit") + val updatedEvent = editedEvent.copy(eventId = "1", editedEventId = "id-to-edit", title = "updated title") + val queue = givenQueue(listOf(editedEvent)) + + queue.add(updatedEvent, seenEventIds = seenIdsCache) + + queue.rawEvents() shouldBeEqualTo listOf(updatedEvent) + } + + @Test + fun `given event when adding new event with edited event id matching the existing event edited id then updates existing event`() { + val editedEvent = aSimpleNotifiableEvent(eventId = "0", editedEventId = "id-to-edit") + val updatedEvent = editedEvent.copy(eventId = "1", editedEventId = "id-to-edit", title = "updated title") + val queue = givenQueue(listOf(editedEvent)) + + queue.add(updatedEvent, seenEventIds = seenIdsCache) + + queue.rawEvents() shouldBeEqualTo listOf(updatedEvent) + } + + @Test + fun `when clearing membership notification then removes invite events with matching room id`() { + val roomId = "a-room-id" + val queue = givenQueue(listOf( + anInviteNotifiableEvent(roomId = roomId), + aNotifiableMessageEvent(roomId = roomId) + )) + + queue.clearMemberShipNotificationForRoom(roomId) + + queue.rawEvents() shouldBeEqualTo listOf(aNotifiableMessageEvent(roomId = roomId)) + } + + @Test + fun `when clearing messages for room then removes message events with matching room id`() { + val roomId = "a-room-id" + val queue = givenQueue(listOf( + anInviteNotifiableEvent(roomId = roomId), + aNotifiableMessageEvent(roomId = roomId) + )) + + queue.clearMessagesForRoom(roomId) + + queue.rawEvents() shouldBeEqualTo listOf(anInviteNotifiableEvent(roomId = roomId)) + } + + private fun givenQueue(events: List) = NotificationEventQueue(events.toMutableList()) +} diff --git a/vector/src/test/java/im/vector/app/features/notifications/NotificationFactoryTest.kt b/vector/src/test/java/im/vector/app/features/notifications/NotificationFactoryTest.kt index d720881bac..f0f9a4dbc7 100644 --- a/vector/src/test/java/im/vector/app/features/notifications/NotificationFactoryTest.kt +++ b/vector/src/test/java/im/vector/app/features/notifications/NotificationFactoryTest.kt @@ -20,6 +20,9 @@ import im.vector.app.features.notifications.ProcessedEvent.Type import im.vector.app.test.fakes.FakeNotificationUtils import im.vector.app.test.fakes.FakeRoomGroupMessageCreator import im.vector.app.test.fakes.FakeSummaryGroupMessageCreator +import im.vector.app.test.fixtures.aNotifiableMessageEvent +import im.vector.app.test.fixtures.aSimpleNotifiableEvent +import im.vector.app.test.fixtures.anInviteNotifiableEvent import org.amshove.kluent.shouldBeEqualTo import org.junit.Test diff --git a/vector/src/test/java/im/vector/app/test/fixtures/NotifiableEventFixture.kt b/vector/src/test/java/im/vector/app/test/fixtures/NotifiableEventFixture.kt new file mode 100644 index 0000000000..53d38aa228 --- /dev/null +++ b/vector/src/test/java/im/vector/app/test/fixtures/NotifiableEventFixture.kt @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.test.fixtures + +import im.vector.app.features.notifications.InviteNotifiableEvent +import im.vector.app.features.notifications.NotifiableMessageEvent +import im.vector.app.features.notifications.SimpleNotifiableEvent + +fun aSimpleNotifiableEvent( + eventId: String = "simple-event-id", + type: String? = null, + isRedacted: Boolean = false, + canBeReplaced: Boolean = false, + editedEventId: String? = null +) = SimpleNotifiableEvent( + matrixID = null, + eventId = eventId, + editedEventId = editedEventId, + noisy = false, + title = "title", + description = "description", + type = type, + timestamp = 0, + soundName = null, + canBeReplaced = canBeReplaced, + isRedacted = isRedacted +) + +fun anInviteNotifiableEvent( + roomId: String = "an-invite-room-id", + eventId: String = "invite-event-id", + isRedacted: Boolean = false +) = InviteNotifiableEvent( + matrixID = null, + eventId = eventId, + roomId = roomId, + roomName = "a room name", + editedEventId = null, + noisy = false, + title = "title", + description = "description", + type = null, + timestamp = 0, + soundName = null, + canBeReplaced = false, + isRedacted = isRedacted +) + +fun aNotifiableMessageEvent( + eventId: String = "a-message-event-id", + roomId: String = "a-message-room-id", + isRedacted: Boolean = false +) = NotifiableMessageEvent( + eventId = eventId, + editedEventId = null, + noisy = false, + timestamp = 0, + senderName = "sender-name", + senderId = "sending-id", + body = "message-body", + roomId = roomId, + roomName = "room-name", + roomIsDirect = false, + canBeReplaced = false, + isRedacted = isRedacted, + imageUri = null +)