Merge pull request #4430 from vector-im/feature/adm/feature-notification-images
Notification images
This commit is contained in:
commit
df60b0c2b7
|
@ -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
|
|
@ -0,0 +1 @@
|
|||
Adds support for images inside message notifications
|
|
@ -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<Pair<Event, PushRule>>,
|
||||
val roomsJoined: Collection<String>,
|
||||
val roomsLeft: Collection<String>,
|
||||
val redactedEventIds: List<String>
|
||||
)
|
|
@ -51,11 +51,7 @@ interface PushRuleService {
|
|||
// fun fulfilledBingRule(event: Event, rules: List<PushRule>): PushRule?
|
||||
|
||||
interface PushRuleListener {
|
||||
fun onMatchRule(event: Event, actions: List<Action>)
|
||||
fun onRoomJoined(roomId: String)
|
||||
fun onRoomLeft(roomId: String)
|
||||
fun onEventRedacted(redactedEventId: String)
|
||||
fun batchFinish()
|
||||
fun onEvents(pushEvents: PushEvents)
|
||||
}
|
||||
|
||||
fun getKeywords(): LiveData<Set<String>>
|
||||
|
|
|
@ -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<Event>) {
|
||||
// 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<Set<String>> {
|
||||
// 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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -66,3 +66,7 @@ fun String?.insertBeforeLast(insert: String, delimiter: String = "."): String {
|
|||
replaceRange(idx, idx, insert)
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <reified R> Any?.takeAs(): R? {
|
||||
return takeIf { it is R } as R?
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<MessageWithAttachmentContent>()?.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<RoomMemberContent>() ?: return null
|
||||
val roomId = event.roomId ?: return null
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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<NotifiableEvent> {
|
||||
private fun loadEventInfo(): NotificationEventQueue {
|
||||
try {
|
||||
val file = File(context.applicationContext.cacheDir, ROOMS_NOTIFICATIONS_FILE_NAME)
|
||||
if (file.exists()) {
|
||||
file.inputStream().use {
|
||||
val events: ArrayList<NotifiableEvent>? = 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<NotifiableEvent>.replace(eventId: String, block: (NotifiableEvent) -> NotifiableEvent) {
|
||||
val indexToReplace = indexOfFirst { it.eventId == eventId }
|
||||
if (indexToReplace == -1) {
|
||||
return
|
||||
}
|
||||
set(indexToReplace, block(get(indexToReplace)))
|
||||
}
|
||||
|
|
|
@ -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<NotifiableEvent> = mutableListOf()
|
||||
) {
|
||||
|
||||
fun markRedacted(eventIds: List<String>) {
|
||||
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<String>, roomsJoined: Collection<String>) {
|
||||
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<NotifiableEvent>) {
|
||||
queue.clear()
|
||||
queue.addAll(events)
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
queue.clear()
|
||||
}
|
||||
|
||||
fun add(notifiableEvent: NotifiableEvent, seenEventIds: CircularCache<String>) {
|
||||
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<NotifiableEvent> = queue
|
||||
}
|
||||
|
||||
private fun MutableList<NotifiableEvent>.replace(eventId: String, block: (NotifiableEvent) -> NotifiableEvent) {
|
||||
val indexToReplace = indexOfFirst { it.eventId == eventId }
|
||||
if (indexToReplace == -1) {
|
||||
return
|
||||
}
|
||||
set(indexToReplace, block(get(indexToReplace)))
|
||||
}
|
|
@ -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<Action>) {
|
||||
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<NotifiableEvent> {
|
||||
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()
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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<String>(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<NotifiableEvent>) = NotificationEventQueue(events.toMutableList())
|
||||
}
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
)
|
Loading…
Reference in New Issue