Merge pull request #458 from vector-im/feature/bma/notificationContent

Notification content
This commit is contained in:
Benoit Marty 2023-06-01 18:01:57 +02:00 committed by GitHub
commit be45feac43
19 changed files with 387 additions and 224 deletions

View file

@ -20,15 +20,23 @@ import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
//TODO add content
data class NotificationData( data class NotificationData(
val senderId: UserId, val senderId: UserId,
val eventId: EventId, val eventId: EventId,
val roomId: RoomId, val roomId: RoomId,
val senderAvatarUrl: String? = null, val senderAvatarUrl: String?,
val senderDisplayName: String? = null, val senderDisplayName: String?,
val roomAvatarUrl: String? = null, val roomAvatarUrl: String?,
val roomDisplayName: String?,
val isDirect: Boolean, val isDirect: Boolean,
val isEncrypted: Boolean, val isEncrypted: Boolean,
val isNoisy: Boolean, val isNoisy: Boolean,
val event: NotificationEvent,
)
data class NotificationEvent(
val timestamp: Long,
val content: String,
// For images for instance
val contentUrl: String?
) )

View file

@ -23,9 +23,9 @@ import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.notification.NotificationData import io.element.android.libraries.matrix.api.notification.NotificationData
import org.matrix.rustcomponents.sdk.NotificationItem import org.matrix.rustcomponents.sdk.NotificationItem
import org.matrix.rustcomponents.sdk.use import org.matrix.rustcomponents.sdk.use
import javax.inject.Inject
class NotificationMapper @Inject constructor() { class NotificationMapper {
private val timelineEventMapper = TimelineEventMapper()
fun map(notificationItem: NotificationItem): NotificationData { fun map(notificationItem: NotificationItem): NotificationData {
return notificationItem.use { return notificationItem.use {
@ -36,9 +36,11 @@ class NotificationMapper @Inject constructor() {
senderAvatarUrl = it.senderAvatarUrl, senderAvatarUrl = it.senderAvatarUrl,
senderDisplayName = it.senderDisplayName, senderDisplayName = it.senderDisplayName,
roomAvatarUrl = it.roomAvatarUrl, roomAvatarUrl = it.roomAvatarUrl,
roomDisplayName = it.roomDisplayName,
isDirect = it.isDirect, isDirect = it.isDirect,
isEncrypted = it.isEncrypted.orFalse(), isEncrypted = it.isEncrypted.orFalse(),
isNoisy = it.isNoisy isNoisy = it.isNoisy,
event = it.event.use { event -> timelineEventMapper.map(event) }
) )
} }
} }

View file

@ -16,18 +16,13 @@
package io.element.android.libraries.matrix.impl.notification package io.element.android.libraries.matrix.impl.notification
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.notification.NotificationData import io.element.android.libraries.matrix.api.notification.NotificationData
import io.element.android.libraries.matrix.api.notification.NotificationService import io.element.android.libraries.matrix.api.notification.NotificationService
import kotlinx.coroutines.withContext
import org.matrix.rustcomponents.sdk.Client import org.matrix.rustcomponents.sdk.Client
import org.matrix.rustcomponents.sdk.use import org.matrix.rustcomponents.sdk.use
import java.io.File
class RustNotificationService( class RustNotificationService(
private val client: Client, private val client: Client,

View file

@ -0,0 +1,109 @@
/*
* Copyright (c) 2023 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 io.element.android.libraries.matrix.impl.notification
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.notification.NotificationEvent
import org.matrix.rustcomponents.sdk.MessageLikeEventContent
import org.matrix.rustcomponents.sdk.MessageType
import org.matrix.rustcomponents.sdk.StateEventContent
import org.matrix.rustcomponents.sdk.TimelineEvent
import org.matrix.rustcomponents.sdk.TimelineEventType
import org.matrix.rustcomponents.sdk.use
import javax.inject.Inject
class TimelineEventMapper @Inject constructor() {
fun map(timelineEvent: TimelineEvent): NotificationEvent {
return timelineEvent.use {
NotificationEvent(
timestamp = it.timestamp().toLong(),
content = it.eventType().toContent(),
contentUrl = null // TODO it.eventType().toContentUrl(),
)
}
}
}
private fun TimelineEventType.toContent(): String {
return when (this) {
is TimelineEventType.MessageLike -> content.toContent()
is TimelineEventType.State -> content.toContent()
}
}
private fun StateEventContent.toContent(): String {
return when (this) {
StateEventContent.PolicyRuleRoom -> "PolicyRuleRoom"
StateEventContent.PolicyRuleServer -> "PolicyRuleServer"
StateEventContent.PolicyRuleUser -> "PolicyRuleUser"
StateEventContent.RoomAliases -> "RoomAliases"
StateEventContent.RoomAvatar -> "RoomAvatar"
StateEventContent.RoomCanonicalAlias -> "RoomCanonicalAlias"
StateEventContent.RoomCreate -> "RoomCreate"
StateEventContent.RoomEncryption -> "RoomEncryption"
StateEventContent.RoomGuestAccess -> "RoomGuestAccess"
StateEventContent.RoomHistoryVisibility -> "RoomHistoryVisibility"
StateEventContent.RoomJoinRules -> "RoomJoinRules"
is StateEventContent.RoomMemberContent -> "$userId is now $membershipState"
StateEventContent.RoomName -> "RoomName"
StateEventContent.RoomPinnedEvents -> "RoomPinnedEvents"
StateEventContent.RoomPowerLevels -> "RoomPowerLevels"
StateEventContent.RoomServerAcl -> "RoomServerAcl"
StateEventContent.RoomThirdPartyInvite -> "RoomThirdPartyInvite"
StateEventContent.RoomTombstone -> "RoomTombstone"
StateEventContent.RoomTopic -> "RoomTopic"
StateEventContent.SpaceChild -> "SpaceChild"
StateEventContent.SpaceParent -> "SpaceParent"
}
}
private fun MessageLikeEventContent.toContent(): String {
return use {
when (it) {
MessageLikeEventContent.CallAnswer -> "CallAnswer"
MessageLikeEventContent.CallCandidates -> "CallCandidates"
MessageLikeEventContent.CallHangup -> "CallHangup"
MessageLikeEventContent.CallInvite -> "CallInvite"
MessageLikeEventContent.KeyVerificationAccept -> "KeyVerificationAccept"
MessageLikeEventContent.KeyVerificationCancel -> "KeyVerificationCancel"
MessageLikeEventContent.KeyVerificationDone -> "KeyVerificationDone"
MessageLikeEventContent.KeyVerificationKey -> "KeyVerificationKey"
MessageLikeEventContent.KeyVerificationMac -> "KeyVerificationMac"
MessageLikeEventContent.KeyVerificationReady -> "KeyVerificationReady"
MessageLikeEventContent.KeyVerificationStart -> "KeyVerificationStart"
is MessageLikeEventContent.ReactionContent -> "Reacted to ${it.relatedEventId.take(8)}"
MessageLikeEventContent.RoomEncrypted -> "RoomEncrypted"
is MessageLikeEventContent.RoomMessage -> it.messageType.toContent()
MessageLikeEventContent.RoomRedaction -> "RoomRedaction"
MessageLikeEventContent.Sticker -> "Sticker"
}
}
}
private fun MessageType.toContent(): String {
return when (this) {
is MessageType.Audio -> content.use { it.body }
is MessageType.Emote -> content.body
is MessageType.File -> content.use { it.body }
is MessageType.Image -> content.use { it.body }
is MessageType.Notice -> content.body
is MessageType.Text -> content.body
is MessageType.Video -> content.use { it.body }
}
}

View file

@ -35,6 +35,7 @@ dependencies {
implementation(libs.androidx.security.crypto) implementation(libs.androidx.security.crypto)
implementation(libs.network.retrofit) implementation(libs.network.retrofit)
implementation(libs.serialization.json) implementation(libs.serialization.json)
implementation(libs.coil)
implementation(projects.libraries.architecture) implementation(projects.libraries.architecture)
implementation(projects.libraries.core) implementation(projects.libraries.core)
@ -42,6 +43,7 @@ dependencies {
implementation(projects.libraries.androidutils) implementation(projects.libraries.androidutils)
implementation(projects.libraries.network) implementation(projects.libraries.network)
implementation(projects.libraries.matrix.api) implementation(projects.libraries.matrix.api)
implementation(projects.libraries.matrixui)
api(projects.libraries.pushproviders.api) api(projects.libraries.pushproviders.api)
api(projects.libraries.pushstore.api) api(projects.libraries.pushstore.api)
api(projects.libraries.push.api) api(projects.libraries.push.api)

View file

@ -24,6 +24,7 @@ import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.notification.NotificationData import io.element.android.libraries.matrix.api.notification.NotificationData
import io.element.android.libraries.matrix.api.notification.NotificationEvent
import io.element.android.libraries.push.impl.log.pushLoggerTag import io.element.android.libraries.push.impl.log.pushLoggerTag
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent
@ -44,9 +45,9 @@ class NotifiableEventResolver @Inject constructor(
private val stringProvider: StringProvider, private val stringProvider: StringProvider,
// private val noticeEventFormatter: NoticeEventFormatter, // private val noticeEventFormatter: NoticeEventFormatter,
// private val displayableEventFormatter: DisplayableEventFormatter, // private val displayableEventFormatter: DisplayableEventFormatter,
private val clock: SystemClock,
private val matrixAuthenticationService: MatrixAuthenticationService, private val matrixAuthenticationService: MatrixAuthenticationService,
private val buildMeta: BuildMeta, private val buildMeta: BuildMeta,
private val clock: SystemClock,
) { ) {
suspend fun resolveEvent(sessionId: SessionId, roomId: RoomId, eventId: EventId): NotifiableEvent? { suspend fun resolveEvent(sessionId: SessionId, roomId: RoomId, eventId: EventId): NotifiableEvent? {
@ -80,14 +81,14 @@ class NotifiableEventResolver @Inject constructor(
editedEventId = null, editedEventId = null,
canBeReplaced = true, canBeReplaced = true,
noisy = isNoisy, noisy = isNoisy,
timestamp = clock.epochMillis(), timestamp = event.timestamp,
senderName = senderDisplayName, senderName = senderDisplayName,
senderId = senderId.value, senderId = senderId.value,
body = "Message ${eventId.value.take(8)}… in room ${roomId.value.take(8)}", body = event.content,
imageUriString = null, imageUriString = event.contentUrl,
threadId = null, threadId = null,
roomName = null, roomName = roomDisplayName,
roomIsDirect = false, roomIsDirect = isDirect,
roomAvatarPath = roomAvatarUrl, roomAvatarPath = roomAvatarUrl,
senderAvatarPath = senderAvatarUrl, senderAvatarPath = senderAvatarUrl,
soundName = null, soundName = null,
@ -97,18 +98,27 @@ class NotifiableEventResolver @Inject constructor(
isUpdated = false isUpdated = false
) )
} }
}
/** /**
* TODO This is a temporary method for EAx. * TODO This is a temporary method for EAx.
*/ */
private fun NotificationData?.orDefault(roomId: RoomId, eventId: EventId): NotificationData { private fun NotificationData?.orDefault(roomId: RoomId, eventId: EventId): NotificationData {
return this ?: NotificationData( return this ?: NotificationData(
eventId = eventId, eventId = eventId,
senderId = UserId("@user:domain"), senderId = UserId("@user:domain"),
roomId = roomId, roomId = roomId,
isNoisy = false, senderAvatarUrl = null,
isEncrypted = false, senderDisplayName = null,
isDirect = false roomAvatarUrl = null,
) roomDisplayName = null,
isNoisy = false,
isEncrypted = false,
isDirect = false,
event = NotificationEvent(
timestamp = clock.epochMillis(),
content = "Message ${eventId.value.take(8)}… in room ${roomId.value.take(8)}",
contentUrl = null
)
)
}
} }

View file

@ -19,9 +19,14 @@ package io.element.android.libraries.push.impl.notifications
import android.content.Context import android.content.Context
import android.graphics.Bitmap import android.graphics.Bitmap
import android.os.Build import android.os.Build
import androidx.annotation.WorkerThread
import androidx.core.graphics.drawable.IconCompat import androidx.core.graphics.drawable.IconCompat
import androidx.core.graphics.drawable.toBitmap
import coil.imageLoader
import coil.request.ImageRequest
import coil.transform.CircleCropTransformation
import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.di.ApplicationContext
import io.element.android.libraries.matrix.api.media.MediaSource
import io.element.android.libraries.matrix.ui.media.MediaRequestData
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@ -31,30 +36,24 @@ class NotificationBitmapLoader @Inject constructor(
/** /**
* Get icon of a room. * Get icon of a room.
* @param path mxc url
*/ */
@WorkerThread suspend fun getRoomBitmap(path: String?): Bitmap? {
fun getRoomBitmap(path: String?): Bitmap? {
if (path == null) { if (path == null) {
return null return null
} }
return loadRoomBitmap(path) return loadRoomBitmap(path)
} }
@WorkerThread private suspend fun loadRoomBitmap(path: String): Bitmap? {
private fun loadRoomBitmap(path: String): Bitmap? {
return try { return try {
null val imageRequest = ImageRequest.Builder(context)
/* TODO Notification .data(MediaRequestData(MediaSource(path), MediaRequestData.Kind.Thumbnail(1024)))
Glide.with(context) .build()
.asBitmap() val result = context.imageLoader.execute(imageRequest)
.load(path) result.drawable?.toBitmap()
.format(DecodeFormat.PREFER_ARGB_8888) } catch (e: Throwable) {
.signature(ObjectKey("room-icon-notification")) Timber.e(e, "Unable to load room bitmap")
.submit()
.get()
*/
} catch (e: Exception) {
Timber.e(e, "decodeFile failed")
null null
} }
} }
@ -62,9 +61,9 @@ class NotificationBitmapLoader @Inject constructor(
/** /**
* Get icon of a user. * Get icon of a user.
* Before Android P, this does nothing because the icon won't be used * Before Android P, this does nothing because the icon won't be used
* @param path mxc url
*/ */
@WorkerThread suspend fun getUserIcon(path: String?): IconCompat? {
fun getUserIcon(path: String?): IconCompat? {
if (path == null || Build.VERSION.SDK_INT < Build.VERSION_CODES.P) { if (path == null || Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
return null return null
} }
@ -72,23 +71,17 @@ class NotificationBitmapLoader @Inject constructor(
return loadUserIcon(path) return loadUserIcon(path)
} }
@WorkerThread private suspend fun loadUserIcon(path: String): IconCompat? {
private fun loadUserIcon(path: String): IconCompat? {
return try { return try {
null val imageRequest = ImageRequest.Builder(context)
/* TODO Notification .data(MediaRequestData(MediaSource(path), MediaRequestData.Kind.Thumbnail(1024)))
val bitmap = Glide.with(context) .transformations(CircleCropTransformation())
.asBitmap() .build()
.load(path) val result = context.imageLoader.execute(imageRequest)
.transform(CircleCrop()) val bitmap = result.drawable?.toBitmap()
.format(DecodeFormat.PREFER_ARGB_8888) return bitmap?.let { IconCompat.createWithBitmap(it) }
.signature(ObjectKey("user-icon-notification")) } catch (e: Throwable) {
.submit() Timber.e(e, "Unable to load user bitmap")
.get()
IconCompat.createWithBitmap(bitmap)
*/
} catch (e: Exception) {
Timber.e(e, "decodeFile failed")
null null
} }
} }

View file

@ -16,28 +16,28 @@
package io.element.android.libraries.push.impl.notifications package io.element.android.libraries.push.impl.notifications
import android.content.Context
import android.os.Handler
import android.os.HandlerThread
import androidx.annotation.WorkerThread
import io.element.android.libraries.androidutils.throttler.FirstThrottler import io.element.android.libraries.androidutils.throttler.FirstThrottler
import io.element.android.libraries.core.cache.CircularCache import io.element.android.libraries.core.cache.CircularCache
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.core.data.tryOrNull
import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.ApplicationContext
import io.element.android.libraries.di.SingleIn import io.element.android.libraries.di.SingleIn
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.core.ThreadId import io.element.android.libraries.matrix.api.core.ThreadId
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.push.api.store.PushDataStore import io.element.android.libraries.push.api.store.PushDataStore
import io.element.android.libraries.push.impl.R
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent
import io.element.android.libraries.push.impl.notifications.model.shouldIgnoreMessageEventInRoom import io.element.android.libraries.push.impl.notifications.model.shouldIgnoreMessageEventInRoom
import io.element.android.services.appnavstate.api.AppNavigationState import io.element.android.services.appnavstate.api.AppNavigationState
import io.element.android.services.appnavstate.api.AppNavigationStateService import io.element.android.services.appnavstate.api.AppNavigationStateService
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@ -48,7 +48,6 @@ import javax.inject.Inject
*/ */
@SingleIn(AppScope::class) @SingleIn(AppScope::class)
class NotificationDrawerManager @Inject constructor( class NotificationDrawerManager @Inject constructor(
@ApplicationContext context: Context,
private val pushDataStore: PushDataStore, private val pushDataStore: PushDataStore,
private val notifiableEventProcessor: NotifiableEventProcessor, private val notifiableEventProcessor: NotifiableEventProcessor,
private val notificationRenderer: NotificationRenderer, private val notificationRenderer: NotificationRenderer,
@ -56,17 +55,14 @@ class NotificationDrawerManager @Inject constructor(
private val filteredEventDetector: FilteredEventDetector, private val filteredEventDetector: FilteredEventDetector,
private val appNavigationStateService: AppNavigationStateService, private val appNavigationStateService: AppNavigationStateService,
private val coroutineScope: CoroutineScope, private val coroutineScope: CoroutineScope,
private val dispatchers: CoroutineDispatchers,
private val buildMeta: BuildMeta, private val buildMeta: BuildMeta,
private val matrixAuthenticationService: MatrixAuthenticationService,
) { ) {
private val handlerThread: HandlerThread = HandlerThread("NotificationDrawerManager", Thread.MIN_PRIORITY)
private var backgroundHandler: Handler
/** /**
* Lazily initializes the NotificationState as we rely on having a current session in order to fetch the persisted queue of events. * Lazily initializes the NotificationState as we rely on having a current session in order to fetch the persisted queue of events.
*/ */
private val notificationState by lazy { createInitialNotificationState() } private val notificationState by lazy { createInitialNotificationState() }
private val avatarSize = context.resources.getDimensionPixelSize(R.dimen.profile_avatar_size)
private var currentAppNavigationState: AppNavigationState? = null private var currentAppNavigationState: AppNavigationState? = null
private val firstThrottler = FirstThrottler(200) private val firstThrottler = FirstThrottler(200)
@ -74,8 +70,6 @@ class NotificationDrawerManager @Inject constructor(
private var useCompleteNotificationFormat = true private var useCompleteNotificationFormat = true
init { init {
handlerThread.start()
backgroundHandler = Handler(handlerThread.looper)
// Observe application state // Observe application state
coroutineScope.launch { coroutineScope.launch {
appNavigationStateService.appNavigationStateFlow appNavigationStateService.appNavigationStateFlow
@ -193,30 +187,25 @@ class NotificationDrawerManager @Inject constructor(
notificationState.updateQueuedEvents(this) { queuedEvents, _ -> notificationState.updateQueuedEvents(this) { queuedEvents, _ ->
action(queuedEvents) action(queuedEvents)
} }
refreshNotificationDrawer() coroutineScope.refreshNotificationDrawer()
} }
private fun refreshNotificationDrawer() { private fun CoroutineScope.refreshNotificationDrawer() = launch {
// Implement last throttler // Implement last throttler
val canHandle = firstThrottler.canHandle() val canHandle = firstThrottler.canHandle()
Timber.v("refreshNotificationDrawer(), delay: ${canHandle.waitMillis()} ms") Timber.v("refreshNotificationDrawer(), delay: ${canHandle.waitMillis()} ms")
backgroundHandler.removeCallbacksAndMessages(null) withContext(dispatchers.io) {
delay(canHandle.waitMillis())
backgroundHandler.postDelayed( try {
{ refreshNotificationDrawerBg()
try { } catch (throwable: Throwable) {
refreshNotificationDrawerBg() // It can happen if for instance session has been destroyed. It's a bit ugly to try catch like this, but it's safer
} catch (throwable: Throwable) { Timber.w(throwable, "refreshNotificationDrawerBg failure")
// It can happen if for instance session has been destroyed. It's a bit ugly to try catch like this, but it's safer }
Timber.w(throwable, "refreshNotificationDrawerBg failure") }
}
},
canHandle.waitMillis()
)
} }
@WorkerThread private suspend fun refreshNotificationDrawerBg() {
private fun refreshNotificationDrawerBg() {
Timber.v("refreshNotificationDrawerBg()") Timber.v("refreshNotificationDrawerBg()")
val eventsToRender = notificationState.updateQueuedEvents(this) { queuedEvents, renderedEvents -> val eventsToRender = notificationState.updateQueuedEvents(this) { queuedEvents, renderedEvents ->
notifiableEventProcessor.process(queuedEvents.rawEvents(), currentAppNavigationState, renderedEvents).also { notifiableEventProcessor.process(queuedEvents.rawEvents(), currentAppNavigationState, renderedEvents).also {
@ -239,24 +228,34 @@ class NotificationDrawerManager @Inject constructor(
} }
} }
private fun renderEvents(eventsToRender: List<ProcessedEvent<NotifiableEvent>>) { private suspend fun renderEvents(eventsToRender: List<ProcessedEvent<NotifiableEvent>>) {
// Group by sessionId // Group by sessionId
val eventsForSessions = eventsToRender.groupBy { val eventsForSessions = eventsToRender.groupBy {
it.event.sessionId it.event.sessionId
} }
eventsForSessions.forEach { (sessionId, notifiableEvents) -> eventsForSessions.forEach { (sessionId, notifiableEvents) ->
// TODO EAx val user = session.getUserOrDefault(session.myUserId) val currentUser = tryOrNull(
// myUserDisplayName cannot be empty else NotificationCompat.MessagingStyle() will crash onError = { Timber.e(it, "Unable to retrieve info for user ${sessionId.value}") },
val myUserDisplayName = "Todo display name" // user.toMatrixItem().getBestName() operation = {
// TODO EAx avatar URL val client = matrixAuthenticationService.restoreSession(sessionId).getOrNull()
val myUserAvatarUrl = null // session.contentUrlResolver().resolveThumbnail(
// contentUrl = user.avatarUrl, // myUserDisplayName cannot be empty else NotificationCompat.MessagingStyle() will crash
// width = avatarSize, val myUserDisplayName = client?.loadUserDisplayName()?.getOrNull() ?: sessionId.value
// height = avatarSize, val userAvatarUrl = client?.loadUserAvatarURLString()?.getOrNull()
// method = ContentUrlResolver.ThumbnailMethod.SCALE MatrixUser(
//) userId = sessionId,
notificationRenderer.render(sessionId, myUserDisplayName, myUserAvatarUrl, useCompleteNotificationFormat, notifiableEvents) displayName = myUserDisplayName,
avatarUrl = userAvatarUrl
)
}
) ?: MatrixUser(
userId = sessionId,
displayName = sessionId.value,
avatarUrl = null
)
notificationRenderer.render(currentUser, useCompleteNotificationFormat, notifiableEvents)
} }
} }

View file

@ -18,7 +18,7 @@ package io.element.android.libraries.push.impl.notifications
import android.app.Notification import android.app.Notification
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.push.impl.notifications.factories.NotificationFactory import io.element.android.libraries.push.impl.notifications.factories.NotificationFactory
import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent
import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent
@ -34,10 +34,8 @@ class NotificationFactory @Inject constructor(
private val summaryGroupMessageCreator: SummaryGroupMessageCreator private val summaryGroupMessageCreator: SummaryGroupMessageCreator
) { ) {
fun Map<RoomId, ProcessedMessageEvents>.toNotifications( suspend fun Map<RoomId, ProcessedMessageEvents>.toNotifications(
sessionId: SessionId, currentUser: MatrixUser,
myUserDisplayName: String,
myUserAvatarUrl: String?
): List<RoomNotification> { ): List<RoomNotification> {
return map { (roomId, events) -> return map { (roomId, events) ->
when { when {
@ -45,11 +43,9 @@ class NotificationFactory @Inject constructor(
else -> { else -> {
val messageEvents = events.onlyKeptEvents().filterNot { it.isRedacted } val messageEvents = events.onlyKeptEvents().filterNot { it.isRedacted }
roomGroupMessageCreator.createRoomMessage( roomGroupMessageCreator.createRoomMessage(
sessionId = sessionId, currentUser = currentUser,
events = messageEvents, events = messageEvents,
roomId = roomId, roomId = roomId,
userDisplayName = myUserDisplayName,
userAvatarUrl = myUserAvatarUrl
) )
} }
} }
@ -99,7 +95,7 @@ class NotificationFactory @Inject constructor(
} }
fun createSummaryNotification( fun createSummaryNotification(
sessionId: SessionId, currentUser: MatrixUser,
roomNotifications: List<RoomNotification>, roomNotifications: List<RoomNotification>,
invitationNotifications: List<OneShotNotification>, invitationNotifications: List<OneShotNotification>,
simpleNotifications: List<OneShotNotification>, simpleNotifications: List<OneShotNotification>,
@ -112,7 +108,7 @@ class NotificationFactory @Inject constructor(
roomMeta.isEmpty() && invitationMeta.isEmpty() && simpleMeta.isEmpty() -> SummaryNotification.Removed roomMeta.isEmpty() && invitationMeta.isEmpty() && simpleMeta.isEmpty() -> SummaryNotification.Removed
else -> SummaryNotification.Update( else -> SummaryNotification.Update(
summaryGroupMessageCreator.createSummaryNotification( summaryGroupMessageCreator.createSummaryNotification(
sessionId = sessionId, currentUser = currentUser,
roomNotifications = roomMeta, roomNotifications = roomMeta,
invitationNotifications = invitationMeta, invitationNotifications = invitationMeta,
simpleNotifications = simpleMeta, simpleNotifications = simpleMeta,

View file

@ -16,9 +16,8 @@
package io.element.android.libraries.push.impl.notifications package io.element.android.libraries.push.impl.notifications
import androidx.annotation.WorkerThread
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent
@ -32,21 +31,18 @@ class NotificationRenderer @Inject constructor(
private val notificationFactory: NotificationFactory, private val notificationFactory: NotificationFactory,
) { ) {
@WorkerThread suspend fun render(
fun render( currentUser: MatrixUser,
sessionId: SessionId,
myUserDisplayName: String,
myUserAvatarUrl: String?,
useCompleteNotificationFormat: Boolean, useCompleteNotificationFormat: Boolean,
eventsToProcess: List<ProcessedEvent<NotifiableEvent>> eventsToProcess: List<ProcessedEvent<NotifiableEvent>>
) { ) {
val (roomEvents, simpleEvents, invitationEvents) = eventsToProcess.groupByType() val (roomEvents, simpleEvents, invitationEvents) = eventsToProcess.groupByType()
with(notificationFactory) { with(notificationFactory) {
val roomNotifications = roomEvents.toNotifications(sessionId, myUserDisplayName, myUserAvatarUrl) val roomNotifications = roomEvents.toNotifications(currentUser)
val invitationNotifications = invitationEvents.toNotifications() val invitationNotifications = invitationEvents.toNotifications()
val simpleNotifications = simpleEvents.toNotifications() val simpleNotifications = simpleEvents.toNotifications()
val summaryNotification = createSummaryNotification( val summaryNotification = createSummaryNotification(
sessionId = sessionId, currentUser = currentUser,
roomNotifications = roomNotifications, roomNotifications = roomNotifications,
invitationNotifications = invitationNotifications, invitationNotifications = invitationNotifications,
simpleNotifications = simpleNotifications, simpleNotifications = simpleNotifications,
@ -56,21 +52,27 @@ class NotificationRenderer @Inject constructor(
// Remove summary first to avoid briefly displaying it after dismissing the last notification // Remove summary first to avoid briefly displaying it after dismissing the last notification
if (summaryNotification == SummaryNotification.Removed) { if (summaryNotification == SummaryNotification.Removed) {
Timber.d("Removing summary notification") Timber.d("Removing summary notification")
notificationDisplayer.cancelNotificationMessage(null, notificationIdProvider.getSummaryNotificationId(sessionId)) notificationDisplayer.cancelNotificationMessage(
tag = null,
id = notificationIdProvider.getSummaryNotificationId(currentUser.userId)
)
} }
roomNotifications.forEach { wrapper -> roomNotifications.forEach { wrapper ->
when (wrapper) { when (wrapper) {
is RoomNotification.Removed -> { is RoomNotification.Removed -> {
Timber.d("Removing room messages notification ${wrapper.roomId}") Timber.d("Removing room messages notification ${wrapper.roomId}")
notificationDisplayer.cancelNotificationMessage(wrapper.roomId.value, notificationIdProvider.getRoomMessagesNotificationId(sessionId)) notificationDisplayer.cancelNotificationMessage(
tag = wrapper.roomId.value,
id = notificationIdProvider.getRoomMessagesNotificationId(currentUser.userId)
)
} }
is RoomNotification.Message -> if (useCompleteNotificationFormat) { is RoomNotification.Message -> if (useCompleteNotificationFormat) {
Timber.d("Updating room messages notification ${wrapper.meta.roomId}") Timber.d("Updating room messages notification ${wrapper.meta.roomId}")
notificationDisplayer.showNotificationMessage( notificationDisplayer.showNotificationMessage(
wrapper.meta.roomId.value, tag = wrapper.meta.roomId.value,
notificationIdProvider.getRoomMessagesNotificationId(sessionId), id = notificationIdProvider.getRoomMessagesNotificationId(currentUser.userId),
wrapper.notification notification = wrapper.notification
) )
} }
} }
@ -80,14 +82,17 @@ class NotificationRenderer @Inject constructor(
when (wrapper) { when (wrapper) {
is OneShotNotification.Removed -> { is OneShotNotification.Removed -> {
Timber.d("Removing invitation notification ${wrapper.key}") Timber.d("Removing invitation notification ${wrapper.key}")
notificationDisplayer.cancelNotificationMessage(wrapper.key, notificationIdProvider.getRoomInvitationNotificationId(sessionId)) notificationDisplayer.cancelNotificationMessage(
tag = wrapper.key,
id = notificationIdProvider.getRoomInvitationNotificationId(currentUser.userId)
)
} }
is OneShotNotification.Append -> if (useCompleteNotificationFormat) { is OneShotNotification.Append -> if (useCompleteNotificationFormat) {
Timber.d("Updating invitation notification ${wrapper.meta.key}") Timber.d("Updating invitation notification ${wrapper.meta.key}")
notificationDisplayer.showNotificationMessage( notificationDisplayer.showNotificationMessage(
wrapper.meta.key, tag = wrapper.meta.key,
notificationIdProvider.getRoomInvitationNotificationId(sessionId), id = notificationIdProvider.getRoomInvitationNotificationId(currentUser.userId),
wrapper.notification notification = wrapper.notification
) )
} }
} }
@ -97,14 +102,17 @@ class NotificationRenderer @Inject constructor(
when (wrapper) { when (wrapper) {
is OneShotNotification.Removed -> { is OneShotNotification.Removed -> {
Timber.d("Removing simple notification ${wrapper.key}") Timber.d("Removing simple notification ${wrapper.key}")
notificationDisplayer.cancelNotificationMessage(wrapper.key, notificationIdProvider.getRoomEventNotificationId(sessionId)) notificationDisplayer.cancelNotificationMessage(
tag = wrapper.key,
id = notificationIdProvider.getRoomEventNotificationId(currentUser.userId)
)
} }
is OneShotNotification.Append -> if (useCompleteNotificationFormat) { is OneShotNotification.Append -> if (useCompleteNotificationFormat) {
Timber.d("Updating simple notification ${wrapper.meta.key}") Timber.d("Updating simple notification ${wrapper.meta.key}")
notificationDisplayer.showNotificationMessage( notificationDisplayer.showNotificationMessage(
wrapper.meta.key, tag = wrapper.meta.key,
notificationIdProvider.getRoomEventNotificationId(sessionId), id = notificationIdProvider.getRoomEventNotificationId(currentUser.userId),
wrapper.notification notification = wrapper.notification
) )
} }
} }
@ -114,9 +122,9 @@ class NotificationRenderer @Inject constructor(
if (summaryNotification is SummaryNotification.Update) { if (summaryNotification is SummaryNotification.Update) {
Timber.d("Updating summary notification") Timber.d("Updating summary notification")
notificationDisplayer.showNotificationMessage( notificationDisplayer.showNotificationMessage(
null, tag = null,
notificationIdProvider.getSummaryNotificationId(sessionId), id = notificationIdProvider.getSummaryNotificationId(currentUser.userId),
summaryNotification.notification notification = summaryNotification.notification
) )
} }
} }

View file

@ -20,8 +20,9 @@ import android.graphics.Bitmap
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.Person import androidx.core.app.Person
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.push.impl.R import io.element.android.libraries.push.impl.R
import io.element.android.libraries.push.impl.notifications.debug.annotateForDebug
import io.element.android.libraries.push.impl.notifications.factories.NotificationFactory import io.element.android.libraries.push.impl.notifications.factories.NotificationFactory
import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent
import io.element.android.services.toolbox.api.strings.StringProvider import io.element.android.services.toolbox.api.strings.StringProvider
@ -36,24 +37,22 @@ class RoomGroupMessageCreator @Inject constructor(
private val notificationFactory: NotificationFactory private val notificationFactory: NotificationFactory
) { ) {
fun createRoomMessage( suspend fun createRoomMessage(
sessionId: SessionId, currentUser: MatrixUser,
events: List<NotifiableMessageEvent>, events: List<NotifiableMessageEvent>,
roomId: RoomId, roomId: RoomId,
userDisplayName: String,
userAvatarUrl: String?
): RoomNotification.Message { ): RoomNotification.Message {
val lastKnownRoomEvent = events.last() val lastKnownRoomEvent = events.last()
val roomName = lastKnownRoomEvent.roomName ?: lastKnownRoomEvent.senderName ?: "Room name (${roomId.value.take(8)}…)" val roomName = lastKnownRoomEvent.roomName ?: lastKnownRoomEvent.senderName ?: "Room name (${roomId.value.take(8)}…)"
val roomIsGroup = !lastKnownRoomEvent.roomIsDirect val roomIsGroup = !lastKnownRoomEvent.roomIsDirect
val style = NotificationCompat.MessagingStyle( val style = NotificationCompat.MessagingStyle(
Person.Builder() Person.Builder()
.setName(userDisplayName) .setName(currentUser.displayName?.annotateForDebug(50))
.setIcon(bitmapLoader.getUserIcon(userAvatarUrl)) .setIcon(bitmapLoader.getUserIcon(currentUser.avatarUrl))
.setKey(lastKnownRoomEvent.sessionId.value) .setKey(lastKnownRoomEvent.sessionId.value)
.build() .build()
).also { ).also {
it.conversationTitle = roomName.takeIf { roomIsGroup } it.conversationTitle = roomName.takeIf { roomIsGroup }?.annotateForDebug(51)
it.isGroupConversation = roomIsGroup it.isGroupConversation = roomIsGroup
it.addMessagesFromEvents(events) it.addMessagesFromEvents(events)
} }
@ -80,7 +79,7 @@ class RoomGroupMessageCreator @Inject constructor(
notificationFactory.createMessagesListNotification( notificationFactory.createMessagesListNotification(
style, style,
RoomEventGroupInfo( RoomEventGroupInfo(
sessionId = sessionId, sessionId = currentUser.userId,
roomId = roomId, roomId = roomId,
roomDisplayName = roomName, roomDisplayName = roomName,
isDirect = !roomIsGroup, isDirect = !roomIsGroup,
@ -99,13 +98,13 @@ class RoomGroupMessageCreator @Inject constructor(
) )
} }
private fun NotificationCompat.MessagingStyle.addMessagesFromEvents(events: List<NotifiableMessageEvent>) { private suspend fun NotificationCompat.MessagingStyle.addMessagesFromEvents(events: List<NotifiableMessageEvent>) {
events.forEach { event -> events.forEach { event ->
val senderPerson = if (event.outGoingMessage) { val senderPerson = if (event.outGoingMessage) {
null null
} else { } else {
Person.Builder() Person.Builder()
.setName(event.senderName) .setName(event.senderName?.annotateForDebug(70))
.setIcon(bitmapLoader.getUserIcon(event.senderAvatarPath)) .setIcon(bitmapLoader.getUserIcon(event.senderAvatarPath))
.setKey(event.senderId) .setKey(event.senderId)
.build() .build()
@ -117,7 +116,11 @@ class RoomGroupMessageCreator @Inject constructor(
senderPerson senderPerson
) )
else -> { else -> {
val message = NotificationCompat.MessagingStyle.Message(event.body, event.timestamp, senderPerson).also { message -> val message = NotificationCompat.MessagingStyle.Message(
event.body?.annotateForDebug(71),
event.timestamp,
senderPerson
).also { message ->
event.imageUri?.let { event.imageUri?.let {
message.setData("image/", it) message.setData("image/", it)
} }
@ -168,7 +171,7 @@ class RoomGroupMessageCreator @Inject constructor(
} }
} }
private fun getRoomBitmap(events: List<NotifiableMessageEvent>): Bitmap? { private suspend fun getRoomBitmap(events: List<NotifiableMessageEvent>): Bitmap? {
// Use the last event (most recent?) // Use the last event (most recent?)
return events.lastOrNull() return events.lastOrNull()
?.roomAvatarPath ?.roomAvatarPath

View file

@ -18,8 +18,9 @@ package io.element.android.libraries.push.impl.notifications
import android.app.Notification import android.app.Notification
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.push.impl.R import io.element.android.libraries.push.impl.R
import io.element.android.libraries.push.impl.notifications.debug.annotateForDebug
import io.element.android.libraries.push.impl.notifications.factories.NotificationFactory import io.element.android.libraries.push.impl.notifications.factories.NotificationFactory
import io.element.android.services.toolbox.api.strings.StringProvider import io.element.android.services.toolbox.api.strings.StringProvider
import javax.inject.Inject import javax.inject.Inject
@ -40,20 +41,20 @@ import javax.inject.Inject
*/ */
class SummaryGroupMessageCreator @Inject constructor( class SummaryGroupMessageCreator @Inject constructor(
private val stringProvider: StringProvider, private val stringProvider: StringProvider,
private val notificationFactory: NotificationFactory private val notificationFactory: NotificationFactory,
) { ) {
fun createSummaryNotification( fun createSummaryNotification(
sessionId: SessionId, currentUser: MatrixUser,
roomNotifications: List<RoomNotification.Message.Meta>, roomNotifications: List<RoomNotification.Message.Meta>,
invitationNotifications: List<OneShotNotification.Append.Meta>, invitationNotifications: List<OneShotNotification.Append.Meta>,
simpleNotifications: List<OneShotNotification.Append.Meta>, simpleNotifications: List<OneShotNotification.Append.Meta>,
useCompleteNotificationFormat: Boolean useCompleteNotificationFormat: Boolean
): Notification { ): Notification {
val summaryInboxStyle = NotificationCompat.InboxStyle().also { style -> val summaryInboxStyle = NotificationCompat.InboxStyle().also { style ->
roomNotifications.forEach { style.addLine(it.summaryLine) } roomNotifications.forEach { style.addLine(it.summaryLine.annotateForDebug(40)) }
invitationNotifications.forEach { style.addLine(it.summaryLine) } invitationNotifications.forEach { style.addLine(it.summaryLine.annotateForDebug(41)) }
simpleNotifications.forEach { style.addLine(it.summaryLine) } simpleNotifications.forEach { style.addLine(it.summaryLine.annotateForDebug(42)) }
} }
val summaryIsNoisy = roomNotifications.any { it.shouldBing } || val summaryIsNoisy = roomNotifications.any { it.shouldBing } ||
@ -69,12 +70,13 @@ class SummaryGroupMessageCreator @Inject constructor(
// FIXME roomIdToEventMap.size is not correct, this is the number of rooms // FIXME roomIdToEventMap.size is not correct, this is the number of rooms
val nbEvents = roomNotifications.size + simpleNotifications.size val nbEvents = roomNotifications.size + simpleNotifications.size
val sumTitle = stringProvider.getQuantityString(R.plurals.notification_compat_summary_title, nbEvents, nbEvents) val sumTitle = stringProvider.getQuantityString(R.plurals.notification_compat_summary_title, nbEvents, nbEvents)
summaryInboxStyle.setBigContentTitle(sumTitle) summaryInboxStyle.setBigContentTitle(sumTitle.annotateForDebug(43))
// TODO get latest event? //.setSummaryText(stringProvider.getQuantityString(R.plurals.notification_unread_notified_messages, nbEvents, nbEvents).annotateForDebug(44))
.setSummaryText(stringProvider.getQuantityString(R.plurals.notification_unread_notified_messages, nbEvents, nbEvents)) // Use account name now, for multi-session
.setSummaryText(currentUser.userId.value.annotateForDebug(44))
return if (useCompleteNotificationFormat) { return if (useCompleteNotificationFormat) {
notificationFactory.createSummaryListNotification( notificationFactory.createSummaryListNotification(
sessionId, currentUser,
summaryInboxStyle, summaryInboxStyle,
sumTitle, sumTitle,
noisy = summaryIsNoisy, noisy = summaryIsNoisy,
@ -82,7 +84,7 @@ class SummaryGroupMessageCreator @Inject constructor(
) )
} else { } else {
processSimpleGroupSummary( processSimpleGroupSummary(
sessionId, currentUser,
summaryIsNoisy, summaryIsNoisy,
messageCount, messageCount,
simpleNotifications.size, simpleNotifications.size,
@ -94,7 +96,7 @@ class SummaryGroupMessageCreator @Inject constructor(
} }
private fun processSimpleGroupSummary( private fun processSimpleGroupSummary(
sessionId: SessionId, currentUser: MatrixUser,
summaryIsNoisy: Boolean, summaryIsNoisy: Boolean,
messageEventsCount: Int, messageEventsCount: Int,
simpleEventsCount: Int, simpleEventsCount: Int,
@ -167,7 +169,7 @@ class SummaryGroupMessageCreator @Inject constructor(
} }
} }
return notificationFactory.createSummaryListNotification( return notificationFactory.createSummaryListNotification(
sessionId = sessionId, currentUser = currentUser,
style = null, style = null,
compatSummary = privacyTitle, compatSummary = privacyTitle,
noisy = summaryIsNoisy, noisy = summaryIsNoisy,

View file

@ -0,0 +1,21 @@
/*
* Copyright (c) 2023 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 io.element.android.libraries.push.impl.notifications.debug
fun CharSequence.annotateForDebug(@Suppress("UNUSED_PARAMETER") prefix: Int): CharSequence {
return this // "$prefix-$this"
}

View file

@ -26,11 +26,12 @@ import androidx.core.content.ContextCompat
import androidx.core.content.res.ResourcesCompat import androidx.core.content.res.ResourcesCompat
import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.di.ApplicationContext
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.core.ThreadId import io.element.android.libraries.matrix.api.core.ThreadId
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.push.impl.R import io.element.android.libraries.push.impl.R
import io.element.android.libraries.push.impl.notifications.RoomEventGroupInfo import io.element.android.libraries.push.impl.notifications.RoomEventGroupInfo
import io.element.android.libraries.push.impl.notifications.channels.NotificationChannels import io.element.android.libraries.push.impl.notifications.channels.NotificationChannels
import io.element.android.libraries.push.impl.notifications.debug.annotateForDebug
import io.element.android.libraries.push.impl.notifications.factories.action.AcceptInvitationActionFactory import io.element.android.libraries.push.impl.notifications.factories.action.AcceptInvitationActionFactory
import io.element.android.libraries.push.impl.notifications.factories.action.MarkAsReadActionFactory import io.element.android.libraries.push.impl.notifications.factories.action.MarkAsReadActionFactory
import io.element.android.libraries.push.impl.notifications.factories.action.QuickReplyActionFactory import io.element.android.libraries.push.impl.notifications.factories.action.QuickReplyActionFactory
@ -84,16 +85,16 @@ class NotificationFactory @Inject constructor(
// ID of the corresponding shortcut, for conversation features under API 30+ // ID of the corresponding shortcut, for conversation features under API 30+
.setShortcutId(roomInfo.roomId.value) .setShortcutId(roomInfo.roomId.value)
// Title for API < 16 devices. // Title for API < 16 devices.
.setContentTitle(roomInfo.roomDisplayName) .setContentTitle(roomInfo.roomDisplayName.annotateForDebug(1))
// Content for API < 16 devices. // Content for API < 16 devices.
.setContentText(stringProvider.getString(R.string.notification_new_messages)) .setContentText(stringProvider.getString(R.string.notification_new_messages).annotateForDebug(2))
// Number of new notifications for API <24 (M and below) devices. // Number of new notifications for API <24 (M and below) devices.
.setSubText( .setSubText(
stringProvider.getQuantityString( stringProvider.getQuantityString(
R.plurals.notification_new_messages_for_room, R.plurals.notification_new_messages_for_room,
messageStyle.messages.size, messageStyle.messages.size,
messageStyle.messages.size messageStyle.messages.size
) ).annotateForDebug(3)
) )
// Auto-bundling is enabled for 4 or more notifications on API 24+ (N+) // Auto-bundling is enabled for 4 or more notifications on API 24+ (N+)
// devices and all Wear devices. But we want a custom grouping, so we specify the groupID // devices and all Wear devices. But we want a custom grouping, so we specify the groupID
@ -135,7 +136,7 @@ class NotificationFactory @Inject constructor(
} }
setDeleteIntent(pendingIntentFactory.createDismissRoomPendingIntent(roomInfo.sessionId, roomInfo.roomId)) setDeleteIntent(pendingIntentFactory.createDismissRoomPendingIntent(roomInfo.sessionId, roomInfo.roomId))
} }
.setTicker(tickerText) .setTicker(tickerText.annotateForDebug(4))
.build() .build()
} }
@ -147,8 +148,8 @@ class NotificationFactory @Inject constructor(
val channelId = notificationChannels.getChannelIdForMessage(inviteNotifiableEvent.noisy) val channelId = notificationChannels.getChannelIdForMessage(inviteNotifiableEvent.noisy)
return NotificationCompat.Builder(context, channelId) return NotificationCompat.Builder(context, channelId)
.setOnlyAlertOnce(true) .setOnlyAlertOnce(true)
.setContentTitle(inviteNotifiableEvent.roomName ?: buildMeta.applicationName) .setContentTitle((inviteNotifiableEvent.roomName ?: buildMeta.applicationName).annotateForDebug(5))
.setContentText(inviteNotifiableEvent.description) .setContentText(inviteNotifiableEvent.description.annotateForDebug(6))
.setGroup(inviteNotifiableEvent.sessionId.value) .setGroup(inviteNotifiableEvent.sessionId.value)
.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_ALL) .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_ALL)
.setSmallIcon(smallIcon) .setSmallIcon(smallIcon)
@ -196,8 +197,8 @@ class NotificationFactory @Inject constructor(
val channelId = notificationChannels.getChannelIdForMessage(simpleNotifiableEvent.noisy) val channelId = notificationChannels.getChannelIdForMessage(simpleNotifiableEvent.noisy)
return NotificationCompat.Builder(context, channelId) return NotificationCompat.Builder(context, channelId)
.setOnlyAlertOnce(true) .setOnlyAlertOnce(true)
.setContentTitle(buildMeta.applicationName) .setContentTitle(buildMeta.applicationName.annotateForDebug(7))
.setContentText(simpleNotifiableEvent.description) .setContentText(simpleNotifiableEvent.description.annotateForDebug(8))
.setGroup(simpleNotifiableEvent.sessionId.value) .setGroup(simpleNotifiableEvent.sessionId.value)
.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_ALL) .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_ALL)
.setSmallIcon(smallIcon) .setSmallIcon(smallIcon)
@ -226,7 +227,7 @@ class NotificationFactory @Inject constructor(
* Create the summary notification. * Create the summary notification.
*/ */
fun createSummaryListNotification( fun createSummaryListNotification(
sessionId: SessionId, currentUser: MatrixUser,
style: NotificationCompat.InboxStyle?, style: NotificationCompat.InboxStyle?,
compatSummary: String, compatSummary: String,
noisy: Boolean, noisy: Boolean,
@ -240,12 +241,12 @@ class NotificationFactory @Inject constructor(
// used in compat < N, after summary is built based on child notifications // used in compat < N, after summary is built based on child notifications
.setWhen(lastMessageTimestamp) .setWhen(lastMessageTimestamp)
.setStyle(style) .setStyle(style)
.setContentTitle(sessionId.value) .setContentTitle(currentUser.userId.value.annotateForDebug(9))
.setCategory(NotificationCompat.CATEGORY_MESSAGE) .setCategory(NotificationCompat.CATEGORY_MESSAGE)
.setSmallIcon(smallIcon) .setSmallIcon(smallIcon)
// set content text to support devices running API level < 24 // set content text to support devices running API level < 24
.setContentText(compatSummary) .setContentText(compatSummary.annotateForDebug(10))
.setGroup(sessionId.value) .setGroup(currentUser.userId.value)
// set this notification as the summary for the group // set this notification as the summary for the group
.setGroupSummary(true) .setGroupSummary(true)
.setColor(accentColor) .setColor(accentColor)
@ -264,8 +265,8 @@ class NotificationFactory @Inject constructor(
priority = NotificationCompat.PRIORITY_LOW priority = NotificationCompat.PRIORITY_LOW
} }
} }
.setContentIntent(pendingIntentFactory.createOpenSessionPendingIntent(sessionId)) .setContentIntent(pendingIntentFactory.createOpenSessionPendingIntent(currentUser.userId))
.setDeleteIntent(pendingIntentFactory.createDismissSummaryPendingIntent(sessionId)) .setDeleteIntent(pendingIntentFactory.createDismissSummaryPendingIntent(currentUser.userId))
.build() .build()
} }

View file

@ -57,6 +57,9 @@ data class NotifiableMessageEvent(
val description: String = body ?: "" val description: String = body ?: ""
val title: String = senderName ?: "" val title: String = senderName ?: ""
// TODO EAx The image has to be downloaded and expose using the file provider.
// Example of value from Element Android:
// content://im.vector.app.debug.mx-sdk.fileprovider/downloads/downloads/816abf76d806c768760568952b1862c8/F/72c33edd23dee3b95f4d5a18aa25fa54/image.png
val imageUri: Uri? val imageUri: Uri?
get() = imageUriString?.let { Uri.parse(it) } get() = imageUriString?.let { Uri.parse(it) }
} }

View file

@ -18,6 +18,7 @@ package io.element.android.libraries.push.impl.notifications
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.AN_EVENT_ID
import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.A_SESSION_ID
@ -27,6 +28,7 @@ import io.element.android.libraries.push.impl.notifications.fake.FakeSummaryGrou
import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableMessageEvent import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableMessageEvent
import io.element.android.libraries.push.impl.notifications.fixtures.aSimpleNotifiableEvent import io.element.android.libraries.push.impl.notifications.fixtures.aSimpleNotifiableEvent
import io.element.android.libraries.push.impl.notifications.fixtures.anInviteNotifiableEvent import io.element.android.libraries.push.impl.notifications.fixtures.anInviteNotifiableEvent
import kotlinx.coroutines.test.runTest
import org.junit.Test import org.junit.Test
private val MY_AVATAR_URL: String? = null private val MY_AVATAR_URL: String? = null
@ -124,11 +126,13 @@ class NotificationFactoryTest {
fun `given room with message when mapping to notification then delegates to room group message creator`() = testWith(notificationFactory) { fun `given room with message when mapping to notification then delegates to room group message creator`() = testWith(notificationFactory) {
val events = listOf(A_MESSAGE_EVENT) val events = listOf(A_MESSAGE_EVENT)
val expectedNotification = roomGroupMessageCreator.givenCreatesRoomMessageFor( val expectedNotification = roomGroupMessageCreator.givenCreatesRoomMessageFor(
A_SESSION_ID, events, A_ROOM_ID, A_SESSION_ID.value, MY_AVATAR_URL MatrixUser(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL), events, A_ROOM_ID
) )
val roomWithMessage = mapOf(A_ROOM_ID to listOf(ProcessedEvent(ProcessedEvent.Type.KEEP, A_MESSAGE_EVENT))) val roomWithMessage = mapOf(A_ROOM_ID to listOf(ProcessedEvent(ProcessedEvent.Type.KEEP, A_MESSAGE_EVENT)))
val result = roomWithMessage.toNotifications(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL) val result = roomWithMessage.toNotifications(
MatrixUser(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL)
)
assertThat(result).isEqualTo(listOf(expectedNotification)) assertThat(result).isEqualTo(listOf(expectedNotification))
} }
@ -138,7 +142,9 @@ class NotificationFactoryTest {
val events = listOf(ProcessedEvent(ProcessedEvent.Type.REMOVE, A_MESSAGE_EVENT)) val events = listOf(ProcessedEvent(ProcessedEvent.Type.REMOVE, A_MESSAGE_EVENT))
val emptyRoom = mapOf(A_ROOM_ID to events) val emptyRoom = mapOf(A_ROOM_ID to events)
val result = emptyRoom.toNotifications(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL) val result = emptyRoom.toNotifications(
MatrixUser(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL)
)
assertThat(result).isEqualTo( assertThat(result).isEqualTo(
listOf( listOf(
@ -153,7 +159,9 @@ class NotificationFactoryTest {
fun `given a room with only redacted events when mapping to notification then is Empty`() = testWith(notificationFactory) { fun `given a room with only redacted events when mapping to notification then is Empty`() = testWith(notificationFactory) {
val redactedRoom = mapOf(A_ROOM_ID to listOf(ProcessedEvent(ProcessedEvent.Type.KEEP, A_MESSAGE_EVENT.copy(isRedacted = true)))) val redactedRoom = mapOf(A_ROOM_ID to listOf(ProcessedEvent(ProcessedEvent.Type.KEEP, A_MESSAGE_EVENT.copy(isRedacted = true))))
val result = redactedRoom.toNotifications(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL) val result = redactedRoom.toNotifications(
MatrixUser(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL)
)
assertThat(result).isEqualTo( assertThat(result).isEqualTo(
listOf( listOf(
@ -176,19 +184,21 @@ class NotificationFactoryTest {
) )
val withRedactedRemoved = listOf(A_MESSAGE_EVENT.copy(eventId = EventId("\$not-redacted"))) val withRedactedRemoved = listOf(A_MESSAGE_EVENT.copy(eventId = EventId("\$not-redacted")))
val expectedNotification = roomGroupMessageCreator.givenCreatesRoomMessageFor( val expectedNotification = roomGroupMessageCreator.givenCreatesRoomMessageFor(
A_SESSION_ID, MatrixUser(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL),
withRedactedRemoved, withRedactedRemoved,
A_ROOM_ID, A_ROOM_ID,
A_SESSION_ID.value,
MY_AVATAR_URL
) )
val result = roomWithRedactedMessage.toNotifications(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL) val result = roomWithRedactedMessage.toNotifications(
MatrixUser(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL)
)
assertThat(result).isEqualTo(listOf(expectedNotification)) assertThat(result).isEqualTo(listOf(expectedNotification))
} }
} }
fun <T> testWith(receiver: T, block: T.() -> Unit) { fun <T> testWith(receiver: T, block: suspend T.() -> Unit) {
receiver.block() runTest {
receiver.block()
}
} }

View file

@ -17,6 +17,7 @@
package io.element.android.libraries.push.impl.notifications package io.element.android.libraries.push.impl.notifications
import android.app.Notification import android.app.Notification
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.AN_EVENT_ID
import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.A_SESSION_ID
@ -24,6 +25,7 @@ import io.element.android.libraries.push.impl.notifications.fake.FakeNotificatio
import io.element.android.libraries.push.impl.notifications.fake.FakeNotificationFactory import io.element.android.libraries.push.impl.notifications.fake.FakeNotificationFactory
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
import io.mockk.mockk import io.mockk.mockk
import kotlinx.coroutines.test.runTest
import org.junit.Test import org.junit.Test
private const val MY_USER_DISPLAY_NAME = "display-name" private const val MY_USER_DISPLAY_NAME = "display-name"
@ -53,7 +55,7 @@ class NotificationRendererTest {
) )
@Test @Test
fun `given no notifications when rendering then cancels summary notification`() { fun `given no notifications when rendering then cancels summary notification`() = runTest {
givenNoNotifications() givenNoNotifications()
renderEventsAsNotifications() renderEventsAsNotifications()
@ -63,7 +65,7 @@ class NotificationRendererTest {
} }
@Test @Test
fun `given last room message group notification is removed when rendering then remove the summary and then remove message notification`() { fun `given last room message group notification is removed when rendering then remove the summary and then remove message notification`() = runTest {
givenNotifications(roomNotifications = listOf(RoomNotification.Removed(A_ROOM_ID)), summaryNotification = A_REMOVE_SUMMARY_NOTIFICATION) givenNotifications(roomNotifications = listOf(RoomNotification.Removed(A_ROOM_ID)), summaryNotification = A_REMOVE_SUMMARY_NOTIFICATION)
renderEventsAsNotifications() renderEventsAsNotifications()
@ -75,7 +77,7 @@ class NotificationRendererTest {
} }
@Test @Test
fun `given a room message group notification is removed when rendering then remove the message notification and update summary`() { fun `given a room message group notification is removed when rendering then remove the message notification and update summary`() = runTest {
givenNotifications(roomNotifications = listOf(RoomNotification.Removed(A_ROOM_ID))) givenNotifications(roomNotifications = listOf(RoomNotification.Removed(A_ROOM_ID)))
renderEventsAsNotifications() renderEventsAsNotifications()
@ -87,7 +89,7 @@ class NotificationRendererTest {
} }
@Test @Test
fun `given a room message group notification is added when rendering then show the message notification and update summary`() { fun `given a room message group notification is added when rendering then show the message notification and update summary`() = runTest {
givenNotifications( givenNotifications(
roomNotifications = listOf( roomNotifications = listOf(
RoomNotification.Message( RoomNotification.Message(
@ -106,7 +108,7 @@ class NotificationRendererTest {
} }
@Test @Test
fun `given last simple notification is removed when rendering then remove the summary and then remove simple notification`() { fun `given last simple notification is removed when rendering then remove the summary and then remove simple notification`() = runTest {
givenNotifications(simpleNotifications = listOf(OneShotNotification.Removed(AN_EVENT_ID.value)), summaryNotification = A_REMOVE_SUMMARY_NOTIFICATION) givenNotifications(simpleNotifications = listOf(OneShotNotification.Removed(AN_EVENT_ID.value)), summaryNotification = A_REMOVE_SUMMARY_NOTIFICATION)
renderEventsAsNotifications() renderEventsAsNotifications()
@ -118,7 +120,7 @@ class NotificationRendererTest {
} }
@Test @Test
fun `given a simple notification is removed when rendering then remove the simple notification and update summary`() { fun `given a simple notification is removed when rendering then remove the simple notification and update summary`() = runTest {
givenNotifications(simpleNotifications = listOf(OneShotNotification.Removed(AN_EVENT_ID.value))) givenNotifications(simpleNotifications = listOf(OneShotNotification.Removed(AN_EVENT_ID.value)))
renderEventsAsNotifications() renderEventsAsNotifications()
@ -130,7 +132,7 @@ class NotificationRendererTest {
} }
@Test @Test
fun `given a simple notification is added when rendering then show the simple notification and update summary`() { fun `given a simple notification is added when rendering then show the simple notification and update summary`() = runTest {
givenNotifications( givenNotifications(
simpleNotifications = listOf( simpleNotifications = listOf(
OneShotNotification.Append( OneShotNotification.Append(
@ -149,7 +151,7 @@ class NotificationRendererTest {
} }
@Test @Test
fun `given last invitation notification is removed when rendering then remove the summary and then remove invitation notification`() { fun `given last invitation notification is removed when rendering then remove the summary and then remove invitation notification`() = runTest {
givenNotifications(invitationNotifications = listOf(OneShotNotification.Removed(A_ROOM_ID.value)), summaryNotification = A_REMOVE_SUMMARY_NOTIFICATION) givenNotifications(invitationNotifications = listOf(OneShotNotification.Removed(A_ROOM_ID.value)), summaryNotification = A_REMOVE_SUMMARY_NOTIFICATION)
renderEventsAsNotifications() renderEventsAsNotifications()
@ -161,7 +163,7 @@ class NotificationRendererTest {
} }
@Test @Test
fun `given an invitation notification is removed when rendering then remove the invitation notification and update summary`() { fun `given an invitation notification is removed when rendering then remove the invitation notification and update summary`() = runTest {
givenNotifications(invitationNotifications = listOf(OneShotNotification.Removed(A_ROOM_ID.value))) givenNotifications(invitationNotifications = listOf(OneShotNotification.Removed(A_ROOM_ID.value)))
renderEventsAsNotifications() renderEventsAsNotifications()
@ -173,7 +175,7 @@ class NotificationRendererTest {
} }
@Test @Test
fun `given an invitation notification is added when rendering then show the invitation notification and update summary`() { fun `given an invitation notification is added when rendering then show the invitation notification and update summary`() = runTest {
givenNotifications( givenNotifications(
simpleNotifications = listOf( simpleNotifications = listOf(
OneShotNotification.Append( OneShotNotification.Append(
@ -191,11 +193,9 @@ class NotificationRendererTest {
} }
} }
private fun renderEventsAsNotifications() { private suspend fun renderEventsAsNotifications() {
notificationRenderer.render( notificationRenderer.render(
sessionId = A_SESSION_ID, MatrixUser(A_SESSION_ID, MY_USER_DISPLAY_NAME, MY_USER_AVATAR_URL),
myUserDisplayName = MY_USER_DISPLAY_NAME,
myUserAvatarUrl = MY_USER_AVATAR_URL,
useCompleteNotificationFormat = USE_COMPLETE_NOTIFICATION_FORMAT, useCompleteNotificationFormat = USE_COMPLETE_NOTIFICATION_FORMAT,
eventsToProcess = AN_EVENT_LIST eventsToProcess = AN_EVENT_LIST
) )
@ -214,9 +214,7 @@ class NotificationRendererTest {
) { ) {
notificationFactory.givenNotificationsFor( notificationFactory.givenNotificationsFor(
groupedEvents = A_PROCESSED_EVENTS, groupedEvents = A_PROCESSED_EVENTS,
sessionId = A_SESSION_ID, matrixUser = MatrixUser(A_SESSION_ID, MY_USER_DISPLAY_NAME, MY_USER_AVATAR_URL),
myUserDisplayName = MY_USER_DISPLAY_NAME,
myUserAvatarUrl = MY_USER_AVATAR_URL,
useCompleteNotificationFormat = useCompleteNotificationFormat, useCompleteNotificationFormat = useCompleteNotificationFormat,
roomNotifications = roomNotifications, roomNotifications = roomNotifications,
invitationNotifications = invitationNotifications, invitationNotifications = invitationNotifications,

View file

@ -16,12 +16,13 @@
package io.element.android.libraries.push.impl.notifications.fake package io.element.android.libraries.push.impl.notifications.fake
import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.push.impl.notifications.GroupedNotificationEvents import io.element.android.libraries.push.impl.notifications.GroupedNotificationEvents
import io.element.android.libraries.push.impl.notifications.NotificationFactory import io.element.android.libraries.push.impl.notifications.NotificationFactory
import io.element.android.libraries.push.impl.notifications.OneShotNotification import io.element.android.libraries.push.impl.notifications.OneShotNotification
import io.element.android.libraries.push.impl.notifications.RoomNotification import io.element.android.libraries.push.impl.notifications.RoomNotification
import io.element.android.libraries.push.impl.notifications.SummaryNotification import io.element.android.libraries.push.impl.notifications.SummaryNotification
import io.mockk.coEvery
import io.mockk.every import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
@ -30,9 +31,7 @@ class FakeNotificationFactory {
fun givenNotificationsFor( fun givenNotificationsFor(
groupedEvents: GroupedNotificationEvents, groupedEvents: GroupedNotificationEvents,
sessionId: SessionId, matrixUser: MatrixUser,
myUserDisplayName: String,
myUserAvatarUrl: String?,
useCompleteNotificationFormat: Boolean, useCompleteNotificationFormat: Boolean,
roomNotifications: List<RoomNotification>, roomNotifications: List<RoomNotification>,
invitationNotifications: List<OneShotNotification>, invitationNotifications: List<OneShotNotification>,
@ -40,13 +39,13 @@ class FakeNotificationFactory {
summaryNotification: SummaryNotification summaryNotification: SummaryNotification
) { ) {
with(instance) { with(instance) {
every { groupedEvents.roomEvents.toNotifications(sessionId, myUserDisplayName, myUserAvatarUrl) } returns roomNotifications coEvery { groupedEvents.roomEvents.toNotifications(matrixUser) } returns roomNotifications
every { groupedEvents.invitationEvents.toNotifications() } returns invitationNotifications every { groupedEvents.invitationEvents.toNotifications() } returns invitationNotifications
every { groupedEvents.simpleEvents.toNotifications() } returns simpleNotifications every { groupedEvents.simpleEvents.toNotifications() } returns simpleNotifications
every { every {
createSummaryNotification( createSummaryNotification(
sessionId, matrixUser,
roomNotifications, roomNotifications,
invitationNotifications, invitationNotifications,
simpleNotifications, simpleNotifications,

View file

@ -17,11 +17,11 @@
package io.element.android.libraries.push.impl.notifications.fake package io.element.android.libraries.push.impl.notifications.fake
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.push.impl.notifications.RoomGroupMessageCreator import io.element.android.libraries.push.impl.notifications.RoomGroupMessageCreator
import io.element.android.libraries.push.impl.notifications.RoomNotification import io.element.android.libraries.push.impl.notifications.RoomNotification
import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent
import io.mockk.every import io.mockk.coEvery
import io.mockk.mockk import io.mockk.mockk
class FakeRoomGroupMessageCreator { class FakeRoomGroupMessageCreator {
@ -29,14 +29,18 @@ class FakeRoomGroupMessageCreator {
val instance = mockk<RoomGroupMessageCreator>() val instance = mockk<RoomGroupMessageCreator>()
fun givenCreatesRoomMessageFor( fun givenCreatesRoomMessageFor(
sessionId: SessionId, matrixUser: MatrixUser,
events: List<NotifiableMessageEvent>, events: List<NotifiableMessageEvent>,
roomId: RoomId, roomId: RoomId,
userDisplayName: String,
userAvatarUrl: String?
): RoomNotification.Message { ): RoomNotification.Message {
val mockMessage = mockk<RoomNotification.Message>() val mockMessage = mockk<RoomNotification.Message>()
every { instance.createRoomMessage(sessionId, events, roomId, userDisplayName, userAvatarUrl) } returns mockMessage coEvery {
instance.createRoomMessage(
currentUser = matrixUser,
events = events,
roomId = roomId,
)
} returns mockMessage
return mockMessage return mockMessage
} }
} }