Merge pull request #458 from vector-im/feature/bma/notificationContent
Notification content
This commit is contained in:
@@ -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?
|
||||
)
|
||||
|
||||
@@ -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) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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) }
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user