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
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.UserId
//TODO add content
data class NotificationData(
val senderId: UserId,
val eventId: EventId,
val roomId: RoomId,
val senderAvatarUrl: String? = null,
val senderDisplayName: String? = null,
val roomAvatarUrl: String? = null,
val senderAvatarUrl: String?,
val senderDisplayName: String?,
val roomAvatarUrl: String?,
val roomDisplayName: String?,
val isDirect: Boolean,
val isEncrypted: 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 org.matrix.rustcomponents.sdk.NotificationItem
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 {
return notificationItem.use {
@@ -36,9 +36,11 @@ class NotificationMapper @Inject constructor() {
senderAvatarUrl = it.senderAvatarUrl,
senderDisplayName = it.senderDisplayName,
roomAvatarUrl = it.roomAvatarUrl,
roomDisplayName = it.roomDisplayName,
isDirect = it.isDirect,
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
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.RoomId
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.NotificationService
import kotlinx.coroutines.withContext
import org.matrix.rustcomponents.sdk.Client
import org.matrix.rustcomponents.sdk.use
import java.io.File
class RustNotificationService(
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.network.retrofit)
implementation(libs.serialization.json)
implementation(libs.coil)
implementation(projects.libraries.architecture)
implementation(projects.libraries.core)
@@ -42,6 +43,7 @@ dependencies {
implementation(projects.libraries.androidutils)
implementation(projects.libraries.network)
implementation(projects.libraries.matrix.api)
implementation(projects.libraries.matrixui)
api(projects.libraries.pushproviders.api)
api(projects.libraries.pushstore.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.UserId
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.notifications.model.NotifiableEvent
import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent
@@ -44,9 +45,9 @@ class NotifiableEventResolver @Inject constructor(
private val stringProvider: StringProvider,
// private val noticeEventFormatter: NoticeEventFormatter,
// private val displayableEventFormatter: DisplayableEventFormatter,
private val clock: SystemClock,
private val matrixAuthenticationService: MatrixAuthenticationService,
private val buildMeta: BuildMeta,
private val clock: SystemClock,
) {
suspend fun resolveEvent(sessionId: SessionId, roomId: RoomId, eventId: EventId): NotifiableEvent? {
@@ -80,14 +81,14 @@ class NotifiableEventResolver @Inject constructor(
editedEventId = null,
canBeReplaced = true,
noisy = isNoisy,
timestamp = clock.epochMillis(),
timestamp = event.timestamp,
senderName = senderDisplayName,
senderId = senderId.value,
body = "Message ${eventId.value.take(8)}… in room ${roomId.value.take(8)}",
imageUriString = null,
body = event.content,
imageUriString = event.contentUrl,
threadId = null,
roomName = null,
roomIsDirect = false,
roomName = roomDisplayName,
roomIsDirect = isDirect,
roomAvatarPath = roomAvatarUrl,
senderAvatarPath = senderAvatarUrl,
soundName = null,
@@ -97,18 +98,27 @@ class NotifiableEventResolver @Inject constructor(
isUpdated = false
)
}
}
/**
* TODO This is a temporary method for EAx.
*/
private fun NotificationData?.orDefault(roomId: RoomId, eventId: EventId): NotificationData {
return this ?: NotificationData(
eventId = eventId,
senderId = UserId("@user:domain"),
roomId = roomId,
isNoisy = false,
isEncrypted = false,
isDirect = false
)
/**
* TODO This is a temporary method for EAx.
*/
private fun NotificationData?.orDefault(roomId: RoomId, eventId: EventId): NotificationData {
return this ?: NotificationData(
eventId = eventId,
senderId = UserId("@user:domain"),
roomId = roomId,
senderAvatarUrl = null,
senderDisplayName = null,
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.graphics.Bitmap
import android.os.Build
import androidx.annotation.WorkerThread
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.matrix.api.media.MediaSource
import io.element.android.libraries.matrix.ui.media.MediaRequestData
import timber.log.Timber
import javax.inject.Inject
@@ -31,30 +36,24 @@ class NotificationBitmapLoader @Inject constructor(
/**
* Get icon of a room.
* @param path mxc url
*/
@WorkerThread
fun getRoomBitmap(path: String?): Bitmap? {
suspend fun getRoomBitmap(path: String?): Bitmap? {
if (path == null) {
return null
}
return loadRoomBitmap(path)
}
@WorkerThread
private fun loadRoomBitmap(path: String): Bitmap? {
private suspend fun loadRoomBitmap(path: String): Bitmap? {
return try {
null
/* TODO Notification
Glide.with(context)
.asBitmap()
.load(path)
.format(DecodeFormat.PREFER_ARGB_8888)
.signature(ObjectKey("room-icon-notification"))
.submit()
.get()
*/
} catch (e: Exception) {
Timber.e(e, "decodeFile failed")
val imageRequest = ImageRequest.Builder(context)
.data(MediaRequestData(MediaSource(path), MediaRequestData.Kind.Thumbnail(1024)))
.build()
val result = context.imageLoader.execute(imageRequest)
result.drawable?.toBitmap()
} catch (e: Throwable) {
Timber.e(e, "Unable to load room bitmap")
null
}
}
@@ -62,9 +61,9 @@ class NotificationBitmapLoader @Inject constructor(
/**
* Get icon of a user.
* Before Android P, this does nothing because the icon won't be used
* @param path mxc url
*/
@WorkerThread
fun getUserIcon(path: String?): IconCompat? {
suspend fun getUserIcon(path: String?): IconCompat? {
if (path == null || Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
return null
}
@@ -72,23 +71,17 @@ class NotificationBitmapLoader @Inject constructor(
return loadUserIcon(path)
}
@WorkerThread
private fun loadUserIcon(path: String): IconCompat? {
private suspend fun loadUserIcon(path: String): IconCompat? {
return try {
null
/* TODO Notification
val bitmap = Glide.with(context)
.asBitmap()
.load(path)
.transform(CircleCrop())
.format(DecodeFormat.PREFER_ARGB_8888)
.signature(ObjectKey("user-icon-notification"))
.submit()
.get()
IconCompat.createWithBitmap(bitmap)
*/
} catch (e: Exception) {
Timber.e(e, "decodeFile failed")
val imageRequest = ImageRequest.Builder(context)
.data(MediaRequestData(MediaSource(path), MediaRequestData.Kind.Thumbnail(1024)))
.transformations(CircleCropTransformation())
.build()
val result = context.imageLoader.execute(imageRequest)
val bitmap = result.drawable?.toBitmap()
return bitmap?.let { IconCompat.createWithBitmap(it) }
} catch (e: Throwable) {
Timber.e(e, "Unable to load user bitmap")
null
}
}

View File

@@ -16,28 +16,28 @@
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.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.di.AppScope
import io.element.android.libraries.di.ApplicationContext
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.SessionId
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.impl.R
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.shouldIgnoreMessageEventInRoom
import io.element.android.services.appnavstate.api.AppNavigationState
import io.element.android.services.appnavstate.api.AppNavigationStateService
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
import javax.inject.Inject
@@ -48,7 +48,6 @@ import javax.inject.Inject
*/
@SingleIn(AppScope::class)
class NotificationDrawerManager @Inject constructor(
@ApplicationContext context: Context,
private val pushDataStore: PushDataStore,
private val notifiableEventProcessor: NotifiableEventProcessor,
private val notificationRenderer: NotificationRenderer,
@@ -56,17 +55,14 @@ class NotificationDrawerManager @Inject constructor(
private val filteredEventDetector: FilteredEventDetector,
private val appNavigationStateService: AppNavigationStateService,
private val coroutineScope: CoroutineScope,
private val dispatchers: CoroutineDispatchers,
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.
*/
private val notificationState by lazy { createInitialNotificationState() }
private val avatarSize = context.resources.getDimensionPixelSize(R.dimen.profile_avatar_size)
private var currentAppNavigationState: AppNavigationState? = null
private val firstThrottler = FirstThrottler(200)
@@ -74,8 +70,6 @@ class NotificationDrawerManager @Inject constructor(
private var useCompleteNotificationFormat = true
init {
handlerThread.start()
backgroundHandler = Handler(handlerThread.looper)
// Observe application state
coroutineScope.launch {
appNavigationStateService.appNavigationStateFlow
@@ -193,30 +187,25 @@ class NotificationDrawerManager @Inject constructor(
notificationState.updateQueuedEvents(this) { queuedEvents, _ ->
action(queuedEvents)
}
refreshNotificationDrawer()
coroutineScope.refreshNotificationDrawer()
}
private fun refreshNotificationDrawer() {
private fun CoroutineScope.refreshNotificationDrawer() = launch {
// Implement last throttler
val canHandle = firstThrottler.canHandle()
Timber.v("refreshNotificationDrawer(), delay: ${canHandle.waitMillis()} ms")
backgroundHandler.removeCallbacksAndMessages(null)
backgroundHandler.postDelayed(
{
try {
refreshNotificationDrawerBg()
} catch (throwable: Throwable) {
// 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()
)
withContext(dispatchers.io) {
delay(canHandle.waitMillis())
try {
refreshNotificationDrawerBg()
} catch (throwable: Throwable) {
// 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")
}
}
}
@WorkerThread
private fun refreshNotificationDrawerBg() {
private suspend fun refreshNotificationDrawerBg() {
Timber.v("refreshNotificationDrawerBg()")
val eventsToRender = notificationState.updateQueuedEvents(this) { queuedEvents, renderedEvents ->
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
val eventsForSessions = eventsToRender.groupBy {
it.event.sessionId
}
eventsForSessions.forEach { (sessionId, notifiableEvents) ->
// TODO EAx val user = session.getUserOrDefault(session.myUserId)
// myUserDisplayName cannot be empty else NotificationCompat.MessagingStyle() will crash
val myUserDisplayName = "Todo display name" // user.toMatrixItem().getBestName()
// TODO EAx avatar URL
val myUserAvatarUrl = null // session.contentUrlResolver().resolveThumbnail(
// contentUrl = user.avatarUrl,
// width = avatarSize,
// height = avatarSize,
// method = ContentUrlResolver.ThumbnailMethod.SCALE
//)
notificationRenderer.render(sessionId, myUserDisplayName, myUserAvatarUrl, useCompleteNotificationFormat, notifiableEvents)
val currentUser = tryOrNull(
onError = { Timber.e(it, "Unable to retrieve info for user ${sessionId.value}") },
operation = {
val client = matrixAuthenticationService.restoreSession(sessionId).getOrNull()
// myUserDisplayName cannot be empty else NotificationCompat.MessagingStyle() will crash
val myUserDisplayName = client?.loadUserDisplayName()?.getOrNull() ?: sessionId.value
val userAvatarUrl = client?.loadUserAvatarURLString()?.getOrNull()
MatrixUser(
userId = sessionId,
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 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.model.InviteNotifiableEvent
import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent
@@ -34,10 +34,8 @@ class NotificationFactory @Inject constructor(
private val summaryGroupMessageCreator: SummaryGroupMessageCreator
) {
fun Map<RoomId, ProcessedMessageEvents>.toNotifications(
sessionId: SessionId,
myUserDisplayName: String,
myUserAvatarUrl: String?
suspend fun Map<RoomId, ProcessedMessageEvents>.toNotifications(
currentUser: MatrixUser,
): List<RoomNotification> {
return map { (roomId, events) ->
when {
@@ -45,11 +43,9 @@ class NotificationFactory @Inject constructor(
else -> {
val messageEvents = events.onlyKeptEvents().filterNot { it.isRedacted }
roomGroupMessageCreator.createRoomMessage(
sessionId = sessionId,
currentUser = currentUser,
events = messageEvents,
roomId = roomId,
userDisplayName = myUserDisplayName,
userAvatarUrl = myUserAvatarUrl
)
}
}
@@ -99,7 +95,7 @@ class NotificationFactory @Inject constructor(
}
fun createSummaryNotification(
sessionId: SessionId,
currentUser: MatrixUser,
roomNotifications: List<RoomNotification>,
invitationNotifications: List<OneShotNotification>,
simpleNotifications: List<OneShotNotification>,
@@ -112,7 +108,7 @@ class NotificationFactory @Inject constructor(
roomMeta.isEmpty() && invitationMeta.isEmpty() && simpleMeta.isEmpty() -> SummaryNotification.Removed
else -> SummaryNotification.Update(
summaryGroupMessageCreator.createSummaryNotification(
sessionId = sessionId,
currentUser = currentUser,
roomNotifications = roomMeta,
invitationNotifications = invitationMeta,
simpleNotifications = simpleMeta,

View File

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

View File

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

View File

@@ -18,8 +18,9 @@ package io.element.android.libraries.push.impl.notifications
import android.app.Notification
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.notifications.debug.annotateForDebug
import io.element.android.libraries.push.impl.notifications.factories.NotificationFactory
import io.element.android.services.toolbox.api.strings.StringProvider
import javax.inject.Inject
@@ -40,20 +41,20 @@ import javax.inject.Inject
*/
class SummaryGroupMessageCreator @Inject constructor(
private val stringProvider: StringProvider,
private val notificationFactory: NotificationFactory
private val notificationFactory: NotificationFactory,
) {
fun createSummaryNotification(
sessionId: SessionId,
currentUser: MatrixUser,
roomNotifications: List<RoomNotification.Message.Meta>,
invitationNotifications: List<OneShotNotification.Append.Meta>,
simpleNotifications: List<OneShotNotification.Append.Meta>,
useCompleteNotificationFormat: Boolean
): Notification {
val summaryInboxStyle = NotificationCompat.InboxStyle().also { style ->
roomNotifications.forEach { style.addLine(it.summaryLine) }
invitationNotifications.forEach { style.addLine(it.summaryLine) }
simpleNotifications.forEach { style.addLine(it.summaryLine) }
roomNotifications.forEach { style.addLine(it.summaryLine.annotateForDebug(40)) }
invitationNotifications.forEach { style.addLine(it.summaryLine.annotateForDebug(41)) }
simpleNotifications.forEach { style.addLine(it.summaryLine.annotateForDebug(42)) }
}
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
val nbEvents = roomNotifications.size + simpleNotifications.size
val sumTitle = stringProvider.getQuantityString(R.plurals.notification_compat_summary_title, nbEvents, nbEvents)
summaryInboxStyle.setBigContentTitle(sumTitle)
// TODO get latest event?
.setSummaryText(stringProvider.getQuantityString(R.plurals.notification_unread_notified_messages, nbEvents, nbEvents))
summaryInboxStyle.setBigContentTitle(sumTitle.annotateForDebug(43))
//.setSummaryText(stringProvider.getQuantityString(R.plurals.notification_unread_notified_messages, nbEvents, nbEvents).annotateForDebug(44))
// Use account name now, for multi-session
.setSummaryText(currentUser.userId.value.annotateForDebug(44))
return if (useCompleteNotificationFormat) {
notificationFactory.createSummaryListNotification(
sessionId,
currentUser,
summaryInboxStyle,
sumTitle,
noisy = summaryIsNoisy,
@@ -82,7 +84,7 @@ class SummaryGroupMessageCreator @Inject constructor(
)
} else {
processSimpleGroupSummary(
sessionId,
currentUser,
summaryIsNoisy,
messageCount,
simpleNotifications.size,
@@ -94,7 +96,7 @@ class SummaryGroupMessageCreator @Inject constructor(
}
private fun processSimpleGroupSummary(
sessionId: SessionId,
currentUser: MatrixUser,
summaryIsNoisy: Boolean,
messageEventsCount: Int,
simpleEventsCount: Int,
@@ -167,7 +169,7 @@ class SummaryGroupMessageCreator @Inject constructor(
}
}
return notificationFactory.createSummaryListNotification(
sessionId = sessionId,
currentUser = currentUser,
style = null,
compatSummary = privacyTitle,
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 io.element.android.libraries.core.meta.BuildMeta
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.user.MatrixUser
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.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.MarkAsReadActionFactory
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+
.setShortcutId(roomInfo.roomId.value)
// Title for API < 16 devices.
.setContentTitle(roomInfo.roomDisplayName)
.setContentTitle(roomInfo.roomDisplayName.annotateForDebug(1))
// 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.
.setSubText(
stringProvider.getQuantityString(
R.plurals.notification_new_messages_for_room,
messageStyle.messages.size,
messageStyle.messages.size
)
).annotateForDebug(3)
)
// 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
@@ -135,7 +136,7 @@ class NotificationFactory @Inject constructor(
}
setDeleteIntent(pendingIntentFactory.createDismissRoomPendingIntent(roomInfo.sessionId, roomInfo.roomId))
}
.setTicker(tickerText)
.setTicker(tickerText.annotateForDebug(4))
.build()
}
@@ -147,8 +148,8 @@ class NotificationFactory @Inject constructor(
val channelId = notificationChannels.getChannelIdForMessage(inviteNotifiableEvent.noisy)
return NotificationCompat.Builder(context, channelId)
.setOnlyAlertOnce(true)
.setContentTitle(inviteNotifiableEvent.roomName ?: buildMeta.applicationName)
.setContentText(inviteNotifiableEvent.description)
.setContentTitle((inviteNotifiableEvent.roomName ?: buildMeta.applicationName).annotateForDebug(5))
.setContentText(inviteNotifiableEvent.description.annotateForDebug(6))
.setGroup(inviteNotifiableEvent.sessionId.value)
.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_ALL)
.setSmallIcon(smallIcon)
@@ -196,8 +197,8 @@ class NotificationFactory @Inject constructor(
val channelId = notificationChannels.getChannelIdForMessage(simpleNotifiableEvent.noisy)
return NotificationCompat.Builder(context, channelId)
.setOnlyAlertOnce(true)
.setContentTitle(buildMeta.applicationName)
.setContentText(simpleNotifiableEvent.description)
.setContentTitle(buildMeta.applicationName.annotateForDebug(7))
.setContentText(simpleNotifiableEvent.description.annotateForDebug(8))
.setGroup(simpleNotifiableEvent.sessionId.value)
.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_ALL)
.setSmallIcon(smallIcon)
@@ -226,7 +227,7 @@ class NotificationFactory @Inject constructor(
* Create the summary notification.
*/
fun createSummaryListNotification(
sessionId: SessionId,
currentUser: MatrixUser,
style: NotificationCompat.InboxStyle?,
compatSummary: String,
noisy: Boolean,
@@ -240,12 +241,12 @@ class NotificationFactory @Inject constructor(
// used in compat < N, after summary is built based on child notifications
.setWhen(lastMessageTimestamp)
.setStyle(style)
.setContentTitle(sessionId.value)
.setContentTitle(currentUser.userId.value.annotateForDebug(9))
.setCategory(NotificationCompat.CATEGORY_MESSAGE)
.setSmallIcon(smallIcon)
// set content text to support devices running API level < 24
.setContentText(compatSummary)
.setGroup(sessionId.value)
.setContentText(compatSummary.annotateForDebug(10))
.setGroup(currentUser.userId.value)
// set this notification as the summary for the group
.setGroupSummary(true)
.setColor(accentColor)
@@ -264,8 +265,8 @@ class NotificationFactory @Inject constructor(
priority = NotificationCompat.PRIORITY_LOW
}
}
.setContentIntent(pendingIntentFactory.createOpenSessionPendingIntent(sessionId))
.setDeleteIntent(pendingIntentFactory.createDismissSummaryPendingIntent(sessionId))
.setContentIntent(pendingIntentFactory.createOpenSessionPendingIntent(currentUser.userId))
.setDeleteIntent(pendingIntentFactory.createDismissSummaryPendingIntent(currentUser.userId))
.build()
}

View File

@@ -57,6 +57,9 @@ data class NotifiableMessageEvent(
val description: String = body ?: ""
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?
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 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.A_ROOM_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.aSimpleNotifiableEvent
import io.element.android.libraries.push.impl.notifications.fixtures.anInviteNotifiableEvent
import kotlinx.coroutines.test.runTest
import org.junit.Test
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) {
val events = listOf(A_MESSAGE_EVENT)
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 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))
}
@@ -138,7 +142,9 @@ class NotificationFactoryTest {
val events = listOf(ProcessedEvent(ProcessedEvent.Type.REMOVE, A_MESSAGE_EVENT))
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(
listOf(
@@ -153,7 +159,9 @@ class NotificationFactoryTest {
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 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(
listOf(
@@ -176,19 +184,21 @@ class NotificationFactoryTest {
)
val withRedactedRemoved = listOf(A_MESSAGE_EVENT.copy(eventId = EventId("\$not-redacted")))
val expectedNotification = roomGroupMessageCreator.givenCreatesRoomMessageFor(
A_SESSION_ID,
MatrixUser(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL),
withRedactedRemoved,
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))
}
}
fun <T> testWith(receiver: T, block: T.() -> Unit) {
receiver.block()
fun <T> testWith(receiver: T, block: suspend T.() -> Unit) {
runTest {
receiver.block()
}
}

View File

@@ -17,6 +17,7 @@
package io.element.android.libraries.push.impl.notifications
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.A_ROOM_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.model.NotifiableEvent
import io.mockk.mockk
import kotlinx.coroutines.test.runTest
import org.junit.Test
private const val MY_USER_DISPLAY_NAME = "display-name"
@@ -53,7 +55,7 @@ class NotificationRendererTest {
)
@Test
fun `given no notifications when rendering then cancels summary notification`() {
fun `given no notifications when rendering then cancels summary notification`() = runTest {
givenNoNotifications()
renderEventsAsNotifications()
@@ -63,7 +65,7 @@ class NotificationRendererTest {
}
@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)
renderEventsAsNotifications()
@@ -75,7 +77,7 @@ class NotificationRendererTest {
}
@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)))
renderEventsAsNotifications()
@@ -87,7 +89,7 @@ class NotificationRendererTest {
}
@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(
roomNotifications = listOf(
RoomNotification.Message(
@@ -106,7 +108,7 @@ class NotificationRendererTest {
}
@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)
renderEventsAsNotifications()
@@ -118,7 +120,7 @@ class NotificationRendererTest {
}
@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)))
renderEventsAsNotifications()
@@ -130,7 +132,7 @@ class NotificationRendererTest {
}
@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(
simpleNotifications = listOf(
OneShotNotification.Append(
@@ -149,7 +151,7 @@ class NotificationRendererTest {
}
@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)
renderEventsAsNotifications()
@@ -161,7 +163,7 @@ class NotificationRendererTest {
}
@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)))
renderEventsAsNotifications()
@@ -173,7 +175,7 @@ class NotificationRendererTest {
}
@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(
simpleNotifications = listOf(
OneShotNotification.Append(
@@ -191,11 +193,9 @@ class NotificationRendererTest {
}
}
private fun renderEventsAsNotifications() {
private suspend fun renderEventsAsNotifications() {
notificationRenderer.render(
sessionId = A_SESSION_ID,
myUserDisplayName = MY_USER_DISPLAY_NAME,
myUserAvatarUrl = MY_USER_AVATAR_URL,
MatrixUser(A_SESSION_ID, MY_USER_DISPLAY_NAME, MY_USER_AVATAR_URL),
useCompleteNotificationFormat = USE_COMPLETE_NOTIFICATION_FORMAT,
eventsToProcess = AN_EVENT_LIST
)
@@ -214,9 +214,7 @@ class NotificationRendererTest {
) {
notificationFactory.givenNotificationsFor(
groupedEvents = A_PROCESSED_EVENTS,
sessionId = A_SESSION_ID,
myUserDisplayName = MY_USER_DISPLAY_NAME,
myUserAvatarUrl = MY_USER_AVATAR_URL,
matrixUser = MatrixUser(A_SESSION_ID, MY_USER_DISPLAY_NAME, MY_USER_AVATAR_URL),
useCompleteNotificationFormat = useCompleteNotificationFormat,
roomNotifications = roomNotifications,
invitationNotifications = invitationNotifications,

View File

@@ -16,12 +16,13 @@
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.NotificationFactory
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.SummaryNotification
import io.mockk.coEvery
import io.mockk.every
import io.mockk.mockk
@@ -30,9 +31,7 @@ class FakeNotificationFactory {
fun givenNotificationsFor(
groupedEvents: GroupedNotificationEvents,
sessionId: SessionId,
myUserDisplayName: String,
myUserAvatarUrl: String?,
matrixUser: MatrixUser,
useCompleteNotificationFormat: Boolean,
roomNotifications: List<RoomNotification>,
invitationNotifications: List<OneShotNotification>,
@@ -40,13 +39,13 @@ class FakeNotificationFactory {
summaryNotification: SummaryNotification
) {
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.simpleEvents.toNotifications() } returns simpleNotifications
every {
createSummaryNotification(
sessionId,
matrixUser,
roomNotifications,
invitationNotifications,
simpleNotifications,

View File

@@ -17,11 +17,11 @@
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.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.RoomNotification
import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent
import io.mockk.every
import io.mockk.coEvery
import io.mockk.mockk
class FakeRoomGroupMessageCreator {
@@ -29,14 +29,18 @@ class FakeRoomGroupMessageCreator {
val instance = mockk<RoomGroupMessageCreator>()
fun givenCreatesRoomMessageFor(
sessionId: SessionId,
matrixUser: MatrixUser,
events: List<NotifiableMessageEvent>,
roomId: RoomId,
userDisplayName: String,
userAvatarUrl: String?
): 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
}
}