Add some tests (mainly imported from EA). Also change type from String to SessionId, RoomId, etc.

This commit is contained in:
Benoit Marty
2023-04-04 17:33:22 +02:00
committed by Benoit Marty
parent 31ff2e7e0b
commit 7e7aca4a53
54 changed files with 1569 additions and 151 deletions

View File

@@ -21,6 +21,9 @@ import android.content.Intent
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.ApplicationContext
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.push.impl.intent.IntentProvider
import io.element.android.x.MainActivity
import javax.inject.Inject
@@ -34,7 +37,7 @@ class IntentProviderImpl @Inject constructor(
return Intent(context, MainActivity::class.java)
}
override fun getIntent(sessionId: String, roomId: String?, threadId: String?): Intent {
override fun getIntent(sessionId: SessionId, roomId: RoomId?, threadId: ThreadId?): Intent {
// TODO Handle deeplink or pass parameters
return Intent(context, MainActivity::class.java)
}

View File

@@ -0,0 +1,71 @@
/*
* 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.core.cache
import com.google.common.truth.Truth.assertThat
import org.junit.Test
class CircularCacheTest {
@Test
fun `when putting more than cache size then cache is limited to cache size`() {
val (cache, internalData) = createIntCache(cacheSize = 3)
cache.putInOrder(1, 1, 1, 1, 1, 1)
assertThat(internalData).isEqualTo(arrayOf(1, 1, 1))
}
@Test
fun `when putting more than cache then acts as FIFO`() {
val (cache, internalData) = createIntCache(cacheSize = 3)
cache.putInOrder(1, 2, 3, 4)
assertThat(internalData).isEqualTo(arrayOf(4, 2, 3))
}
@Test
fun `given empty cache when checking if contains key then is false`() {
val (cache, _) = createIntCache(cacheSize = 3)
val result = cache.contains(1)
assertThat(result).isFalse()
}
@Test
fun `given cached key when checking if contains key then is true`() {
val (cache, _) = createIntCache(cacheSize = 3)
cache.put(1)
val result = cache.contains(1)
assertThat(result).isTrue()
}
private fun createIntCache(cacheSize: Int): Pair<CircularCache<Int>, Array<Int?>> {
var internalData: Array<Int?>? = null
val factory: (Int) -> Array<Int?> = {
Array<Int?>(it) { null }.also { array -> internalData = array }
}
return CircularCache(cacheSize, factory) to internalData!!
}
private fun CircularCache<Int>.putInOrder(vararg values: Int) {
values.forEach { put(it) }
}
}

View File

@@ -20,3 +20,5 @@ import java.io.Serializable
@JvmInline
value class EventId(val value: String) : Serializable
fun String.asEventId() = EventId(this)

View File

@@ -20,3 +20,5 @@ import java.io.Serializable
@JvmInline
value class RoomId(val value: String) : Serializable
fun String.asRoomId() = RoomId(this)

View File

@@ -17,3 +17,5 @@
package io.element.android.libraries.matrix.api.core
typealias SessionId = UserId
fun String.asSessionId() = SessionId(this)

View File

@@ -25,3 +25,5 @@ value class SpaceId(val value: String) : Serializable
* Value to use when no space is selected by the user.
*/
val MAIN_SPACE = SpaceId("!mainSpace")
fun String.asSpaceId() = SpaceId(this)

View File

@@ -20,3 +20,5 @@ import java.io.Serializable
@JvmInline
value class ThreadId(val value: String) : Serializable
fun String.asThreadId() = ThreadId(this)

View File

@@ -20,3 +20,5 @@ import java.io.Serializable
@JvmInline
value class UserId(val value: String) : Serializable
fun String.asUserId() = UserId(this)

View File

@@ -16,6 +16,10 @@
package io.element.android.libraries.matrix.api.notification
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
interface NotificationService {
suspend fun getNotification(userId: String, roomId: String, eventId: String): Result<NotificationData?>
suspend fun getNotification(userId: SessionId, roomId: RoomId, eventId: EventId): Result<NotificationData?>
}

View File

@@ -17,6 +17,9 @@
package io.element.android.libraries.matrix.impl.notification
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
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.notification.NotificationData
import io.element.android.libraries.matrix.api.notification.NotificationService
import kotlinx.coroutines.withContext
@@ -28,15 +31,15 @@ class RustNotificationService(
) : NotificationService {
private val notificationMapper: NotificationMapper = NotificationMapper()
override suspend fun getNotification(userId: String, roomId: String, eventId: String): Result<NotificationData?> {
override suspend fun getNotification(userId: SessionId, roomId: RoomId, eventId: EventId): Result<NotificationData?> {
return withContext(dispatchers.io) {
runCatching {
org.matrix.rustcomponents.sdk.NotificationService(
basePath = File(baseDirectory, "sessions").absolutePath,
userId = userId
userId = userId.value
).use {
// TODO Not implemented yet, see https://github.com/matrix-org/matrix-rust-sdk/issues/1628
it.getNotificationItem(roomId, eventId)?.let { notificationItem ->
it.getNotificationItem(roomId.value, eventId.value)?.let { notificationItem ->
notificationMapper.map(notificationItem)
}
}

View File

@@ -31,8 +31,10 @@ val A_USER_ID = UserId("@alice:server.org")
val A_SESSION_ID = SessionId(A_USER_ID.value)
val A_SPACE_ID = SpaceId("!aSpaceId")
val A_ROOM_ID = RoomId("!aRoomId")
val A_ROOM_ID_2 = RoomId("!aRoomId2")
val A_THREAD_ID = ThreadId("\$aThreadId")
val AN_EVENT_ID = EventId("\$anEventId")
val AN_EVENT_ID_2 = EventId("\$anEventId2")
const val A_UNIQUE_ID = "aUniqueId"
const val A_ROOM_NAME = "A room name"

View File

@@ -16,11 +16,14 @@
package io.element.android.libraries.matrix.test.notification
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.notification.NotificationData
import io.element.android.libraries.matrix.api.notification.NotificationService
class FakeNotificationService : NotificationService {
override suspend fun getNotification(userId: String, roomId: String, eventId: String): Result<NotificationData?> {
override suspend fun getNotification(userId: SessionId, roomId: RoomId, eventId: EventId): Result<NotificationData?> {
return Result.success(null)
}
}

View File

@@ -54,7 +54,6 @@ dependencies {
exclude(group = "com.android.support", module = "support-annotations")
}
implementation(platform(libs.google.firebase.bom))
implementation("com.google.firebase:firebase-messaging-ktx")
@@ -62,7 +61,10 @@ dependencies {
api("com.github.UnifiedPush:android-connector:2.1.1")
testImplementation(libs.test.junit)
testImplementation(libs.test.mockk)
testImplementation(libs.test.truth)
testImplementation(libs.test.turbine)
testImplementation(libs.coroutines.test)
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.services.appnavstate.test)
}

View File

@@ -19,6 +19,7 @@ package io.element.android.libraries.push.impl
import io.element.android.libraries.core.log.logger.LoggerTag
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.pusher.SetHttpPusherData
import io.element.android.libraries.push.impl.clientsecret.PushClientSecret
@@ -98,7 +99,7 @@ class PushersManager @Inject constructor(
} else {
// Register the pusher to the server
matrixClient.pushersService().setHttpPusher(
createHttpPusher(pushKey, gateway, matrixClient.sessionId.value)
createHttpPusher(pushKey, gateway, matrixClient.sessionId)
).fold(
{
userDataStore.setCurrentRegisteredPushKey(pushKey)
@@ -113,7 +114,7 @@ class PushersManager @Inject constructor(
private suspend fun createHttpPusher(
pushKey: String,
gateway: String,
userId: String,
userId: SessionId,
): SetHttpPusherData =
SetHttpPusherData(
pushKey = pushKey,
@@ -167,6 +168,6 @@ class PushersManager @Inject constructor(
}
companion object {
const val TEST_EVENT_ID = "\$THIS_IS_A_FAKE_EVENT_ID"
val TEST_EVENT_ID = EventId("\$THIS_IS_A_FAKE_EVENT_ID")
}
}

View File

@@ -16,20 +16,22 @@
package io.element.android.libraries.push.impl.clientsecret
import io.element.android.libraries.matrix.api.core.SessionId
interface PushClientSecret {
/**
* To call when registering a pusher. It will return the existing secret or create a new one.
*/
suspend fun getSecretForUser(userId: String): String
suspend fun getSecretForUser(userId: SessionId): String
/**
* To call when receiving a push containing a client secret.
* Return null if not found.
*/
suspend fun getUserIdFromSecret(clientSecret: String): String?
suspend fun getUserIdFromSecret(clientSecret: String): SessionId?
/**
* To call when the user signs out.
*/
suspend fun resetSecretForUser(userId: String)
suspend fun resetSecretForUser(userId: SessionId)
}

View File

@@ -18,6 +18,7 @@ package io.element.android.libraries.push.impl.clientsecret
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.matrix.api.core.SessionId
import javax.inject.Inject
@ContributesBinding(AppScope::class)
@@ -25,7 +26,7 @@ class PushClientSecretImpl @Inject constructor(
private val pushClientSecretFactory: PushClientSecretFactory,
private val pushClientSecretStore: PushClientSecretStore,
) : PushClientSecret {
override suspend fun getSecretForUser(userId: String): String {
override suspend fun getSecretForUser(userId: SessionId): String {
val existingSecret = pushClientSecretStore.getSecret(userId)
if (existingSecret != null) {
return existingSecret
@@ -35,11 +36,11 @@ class PushClientSecretImpl @Inject constructor(
return newSecret
}
override suspend fun getUserIdFromSecret(clientSecret: String): String? {
override suspend fun getUserIdFromSecret(clientSecret: String): SessionId? {
return pushClientSecretStore.getUserIdFromSecret(clientSecret)
}
override suspend fun resetSecretForUser(userId: String) {
override suspend fun resetSecretForUser(userId: SessionId) {
pushClientSecretStore.resetSecret(userId)
}
}

View File

@@ -16,9 +16,11 @@
package io.element.android.libraries.push.impl.clientsecret
import io.element.android.libraries.matrix.api.core.SessionId
interface PushClientSecretStore {
suspend fun storeSecret(userId: String, clientSecret: String)
suspend fun getSecret(userId: String): String?
suspend fun resetSecret(userId: String)
suspend fun getUserIdFromSecret(clientSecret: String): String?
suspend fun storeSecret(userId: SessionId, clientSecret: String)
suspend fun getSecret(userId: SessionId): String?
suspend fun resetSecret(userId: SessionId)
suspend fun getUserIdFromSecret(clientSecret: String): SessionId?
}

View File

@@ -25,6 +25,8 @@ import androidx.datastore.preferences.preferencesDataStore
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.ApplicationContext
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.core.asSessionId
import kotlinx.coroutines.flow.first
import javax.inject.Inject
@@ -34,29 +36,29 @@ private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(na
class PushClientSecretStoreDataStore @Inject constructor(
@ApplicationContext private val context: Context,
) : PushClientSecretStore {
override suspend fun storeSecret(userId: String, clientSecret: String) {
override suspend fun storeSecret(userId: SessionId, clientSecret: String) {
context.dataStore.edit { settings ->
settings[getPreferenceKeyForUser(userId)] = clientSecret
}
}
override suspend fun getSecret(userId: String): String? {
override suspend fun getSecret(userId: SessionId): String? {
return context.dataStore.data.first()[getPreferenceKeyForUser(userId)]
}
override suspend fun resetSecret(userId: String) {
override suspend fun resetSecret(userId: SessionId) {
context.dataStore.edit { settings ->
settings.remove(getPreferenceKeyForUser(userId))
}
}
override suspend fun getUserIdFromSecret(clientSecret: String): String? {
override suspend fun getUserIdFromSecret(clientSecret: String): SessionId? {
val keyValues = context.dataStore.data.first().asMap()
val matchingKey = keyValues.keys.firstOrNull {
keyValues[it] == clientSecret
}
return matchingKey?.name
return matchingKey?.name?.asSessionId()
}
private fun getPreferenceKeyForUser(userId: String) = stringPreferencesKey(userId)
private fun getPreferenceKeyForUser(userId: SessionId) = stringPreferencesKey(userId.value)
}

View File

@@ -17,6 +17,8 @@
package io.element.android.libraries.push.impl.firebase
import io.element.android.libraries.matrix.api.core.MatrixPatterns
import io.element.android.libraries.matrix.api.core.asEventId
import io.element.android.libraries.matrix.api.core.asRoomId
import io.element.android.libraries.push.impl.push.PushData
/**
@@ -40,8 +42,8 @@ data class PushDataFirebase(
)
fun PushDataFirebase.toPushData() = PushData(
eventId = eventId?.takeIf { MatrixPatterns.isEventId(it) },
roomId = roomId?.takeIf { MatrixPatterns.isRoomId(it) },
eventId = eventId?.takeIf { MatrixPatterns.isEventId(it) }?.asEventId(),
roomId = roomId?.takeIf { MatrixPatterns.isRoomId(it) }?.asRoomId(),
unread = unread,
clientSecret = clientSecret,
)

View File

@@ -17,6 +17,9 @@
package io.element.android.libraries.push.impl.intent
import android.content.Intent
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
interface IntentProvider {
/**
@@ -25,8 +28,8 @@ interface IntentProvider {
fun getMainIntent(): Intent
fun getIntent(
sessionId: String,
roomId: String?,
threadId: String?,
sessionId: SessionId,
roomId: RoomId?,
threadId: ThreadId?,
): Intent
}

View File

@@ -19,6 +19,7 @@ import io.element.android.libraries.core.log.logger.LoggerTag
import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
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
@@ -52,13 +53,13 @@ class NotifiableEventResolver @Inject constructor(
private val buildMeta: BuildMeta,
) {
suspend fun resolveEvent(userId: String, roomId: String, eventId: String): NotifiableEvent? {
suspend fun resolveEvent(sessionId: SessionId, roomId: RoomId, eventId: EventId): NotifiableEvent? {
// Restore session
val session = matrixAuthenticationService.restoreSession(SessionId(userId)).getOrNull() ?: return null
val session = matrixAuthenticationService.restoreSession(sessionId).getOrNull() ?: return null
// TODO EAx, no need for a session?
val notificationData = session.let {// TODO Use make the app crashes
it.notificationService().getNotification(
userId = userId,
userId = sessionId,
roomId = roomId,
eventId = eventId,
)
@@ -72,11 +73,11 @@ class NotifiableEventResolver @Inject constructor(
}
).orDefault(roomId, eventId)
return notificationData.asNotifiableEvent(userId, roomId, eventId)
return notificationData.asNotifiableEvent(sessionId, roomId, eventId)
}
}
private fun NotificationData.asNotifiableEvent(userId: String, roomId: String, eventId: String): NotifiableEvent {
private fun NotificationData.asNotifiableEvent(userId: SessionId, roomId: RoomId, eventId: EventId): NotifiableEvent {
return NotifiableMessageEvent(
sessionId = userId,
roomId = roomId,
@@ -105,12 +106,12 @@ private fun NotificationData.asNotifiableEvent(userId: String, roomId: String, e
/**
* TODO This is a temporary method for EAx
*/
private fun NotificationData?.orDefault(roomId: String, eventId: String): NotificationData {
private fun NotificationData?.orDefault(roomId: RoomId, eventId: EventId): NotificationData {
return this ?: NotificationData(
item = MatrixTimelineItem.Event(
event = EventTimelineItem(
uniqueIdentifier = eventId,
eventId = EventId(eventId),
uniqueIdentifier = eventId.value,
eventId = eventId,
isEditable = false,
isLocal = false,
isOwn = false,
@@ -121,18 +122,18 @@ private fun NotificationData?.orDefault(roomId: String, eventId: String): Notifi
senderProfile = ProfileTimelineDetails.Unavailable,
timestamp = System.currentTimeMillis(),
content = MessageContent(
body = eventId,
body = eventId.value,
inReplyTo = null,
isEdited = false,
type = TextMessageType(
body = eventId,
body = eventId.value,
formatted = null
)
)
),
),
title = roomId,
subtitle = eventId,
title = roomId.value,
subtitle = eventId.value,
isNoisy = false,
avatarUrl = null,
)

View File

@@ -22,6 +22,11 @@ import android.content.Intent
import androidx.core.app.RemoteInput
import io.element.android.libraries.architecture.bindings
import io.element.android.libraries.core.log.logger.LoggerTag
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.asRoomId
import io.element.android.libraries.matrix.api.core.asSessionId
import io.element.android.libraries.matrix.api.core.asThreadId
import io.element.android.libraries.push.impl.log.notificationLoggerTag
import io.element.android.services.analytics.api.AnalyticsTracker
import io.element.android.services.toolbox.api.systemclock.SystemClock
@@ -46,29 +51,29 @@ class NotificationBroadcastReceiver : BroadcastReceiver() {
if (intent == null || context == null) return
context.bindings<NotificationBroadcastReceiverBindings>().inject(this)
Timber.tag(loggerTag.value).v("NotificationBroadcastReceiver received : $intent")
val sessionId = intent.extras?.getString(KEY_SESSION_ID) ?: return
val sessionId = intent.extras?.getString(KEY_SESSION_ID)?.asSessionId() ?: return
when (intent.action) {
actionIds.smartReply ->
handleSmartReply(intent, context)
actionIds.dismissRoom ->
intent.getStringExtra(KEY_ROOM_ID)?.let { roomId ->
intent.getStringExtra(KEY_ROOM_ID)?.asRoomId()?.let { roomId ->
notificationDrawerManager.updateEvents { it.clearMessagesForRoom(sessionId, roomId) }
}
actionIds.dismissSummary ->
notificationDrawerManager.clearAllEvents(sessionId)
actionIds.markRoomRead ->
intent.getStringExtra(KEY_ROOM_ID)?.let { roomId ->
intent.getStringExtra(KEY_ROOM_ID)?.asRoomId()?.let { roomId ->
notificationDrawerManager.updateEvents { it.clearMessagesForRoom(sessionId, roomId) }
handleMarkAsRead(sessionId, roomId)
}
actionIds.join -> {
intent.getStringExtra(KEY_ROOM_ID)?.let { roomId ->
intent.getStringExtra(KEY_ROOM_ID)?.asRoomId()?.let { roomId ->
notificationDrawerManager.updateEvents { it.clearMemberShipNotificationForRoom(sessionId, roomId) }
handleJoinRoom(sessionId, roomId)
}
}
actionIds.reject -> {
intent.getStringExtra(KEY_ROOM_ID)?.let { roomId ->
intent.getStringExtra(KEY_ROOM_ID)?.asRoomId()?.let { roomId ->
notificationDrawerManager.updateEvents { it.clearMemberShipNotificationForRoom(sessionId, roomId) }
handleRejectRoom(sessionId, roomId)
}
@@ -76,7 +81,7 @@ class NotificationBroadcastReceiver : BroadcastReceiver() {
}
}
private fun handleJoinRoom(sessionId: String, roomId: String) {
private fun handleJoinRoom(sessionId: SessionId, roomId: RoomId) {
/*
activeSessionHolder.getSafeActiveSession()?.let { session ->
val room = session.getRoom(roomId)
@@ -93,7 +98,7 @@ class NotificationBroadcastReceiver : BroadcastReceiver() {
*/
}
private fun handleRejectRoom(sessionId: String, roomId: String) {
private fun handleRejectRoom(sessionId: SessionId, roomId: RoomId) {
/*
activeSessionHolder.getSafeActiveSession()?.let { session ->
session.coroutineScope.launch {
@@ -104,7 +109,7 @@ class NotificationBroadcastReceiver : BroadcastReceiver() {
*/
}
private fun handleMarkAsRead(sessionId: String, roomId: String) {
private fun handleMarkAsRead(sessionId: SessionId, roomId: RoomId) {
/*
activeSessionHolder.getActiveSession().let { session ->
val room = session.getRoom(roomId)
@@ -120,11 +125,11 @@ class NotificationBroadcastReceiver : BroadcastReceiver() {
private fun handleSmartReply(intent: Intent, context: Context) {
val message = getReplyMessage(intent)
val sessionId = intent.getStringExtra(KEY_SESSION_ID)
val roomId = intent.getStringExtra(KEY_ROOM_ID)
val threadId = intent.getStringExtra(KEY_THREAD_ID)
val sessionId = intent.getStringExtra(KEY_SESSION_ID)?.asSessionId()
val roomId = intent.getStringExtra(KEY_ROOM_ID)?.asRoomId()
val threadId = intent.getStringExtra(KEY_THREAD_ID)?.asThreadId()
if (message.isNullOrBlank() || roomId.isNullOrBlank()) {
if (message.isNullOrBlank() || roomId == null) {
// ignore this event
// Can this happen? should we update notification?
return

View File

@@ -25,6 +25,9 @@ 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.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.push.api.store.PushDataStore
import io.element.android.libraries.push.impl.R
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
@@ -86,13 +89,13 @@ class NotificationDrawerManager @Inject constructor(
is AppNavigationState.Space -> {}
is AppNavigationState.Room -> {
// Cleanup notification for current room
onEnteringRoom(appNavigationState.parentSpace.parentSession.sessionId.value, appNavigationState.roomId.value)
onEnteringRoom(appNavigationState.parentSpace.parentSession.sessionId, appNavigationState.roomId)
}
is AppNavigationState.Thread -> {
onEnteringThread(
appNavigationState.parentRoom.parentSpace.parentSession.sessionId.value,
appNavigationState.parentRoom.roomId.value,
appNavigationState.threadId.value
appNavigationState.parentRoom.parentSpace.parentSession.sessionId,
appNavigationState.parentRoom.roomId,
appNavigationState.threadId
)
}
}
@@ -136,7 +139,7 @@ class NotificationDrawerManager @Inject constructor(
/**
* Clear all known events and refresh the notification drawer.
*/
fun clearAllEvents(sessionId: String) {
fun clearAllEvents(sessionId: SessionId) {
updateEvents { it.clearMessagesForSession(sessionId) }
}
@@ -144,7 +147,7 @@ class NotificationDrawerManager @Inject constructor(
* Should be called when the application is currently opened and showing timeline for the given roomId.
* Used to ignore events related to that room (no need to display notification) and clean any existing notification on this room.
*/
private fun onEnteringRoom(sessionId: String, roomId: String) {
private fun onEnteringRoom(sessionId: SessionId, roomId: RoomId) {
updateEvents {
it.clearMessagesForRoom(sessionId, roomId)
}
@@ -154,7 +157,7 @@ class NotificationDrawerManager @Inject constructor(
* Should be called when the application is currently opened and showing timeline for the given threadId.
* Used to ignore events related to that thread (no need to display notification) and clean any existing notification on this room.
*/
private fun onEnteringThread(sessionId: String, roomId: String, threadId: String) {
private fun onEnteringThread(sessionId: SessionId, roomId: RoomId, threadId: ThreadId) {
updateEvents {
it.clearMessagesForThread(sessionId, roomId, threadId)
}

View File

@@ -17,6 +17,10 @@
package io.element.android.libraries.push.impl.notifications
import io.element.android.libraries.core.cache.CircularCache
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.ThreadId
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
@@ -30,10 +34,10 @@ data class NotificationEventQueue constructor(
* Acts as a notification debouncer to stop already dismissed push notifications from
* displaying again when the /sync response is delayed.
*/
private val seenEventIds: CircularCache<String>
private val seenEventIds: CircularCache<EventId>
) {
fun markRedacted(eventIds: List<String>) {
fun markRedacted(eventIds: List<EventId>) {
eventIds.forEach { redactedId ->
queue.replace(redactedId) {
when (it) {
@@ -45,7 +49,8 @@ data class NotificationEventQueue constructor(
}
}
fun syncRoomEvents(roomsLeft: Collection<String>, roomsJoined: Collection<String>) {
// TODO EAx call this
fun syncRoomEvents(roomsLeft: Collection<RoomId>, roomsJoined: Collection<RoomId>) {
if (roomsLeft.isNotEmpty() || roomsJoined.isNotEmpty()) {
queue.removeAll {
when (it) {
@@ -125,30 +130,30 @@ data class NotificationEventQueue constructor(
)
}
fun clearMemberShipNotificationForRoom(sessionId: String, roomId: String) {
fun clearMemberShipNotificationForRoom(sessionId: SessionId, roomId: RoomId) {
Timber.d("clearMemberShipOfRoom $sessionId, $roomId")
queue.removeAll { it is InviteNotifiableEvent && it.sessionId == sessionId && it.roomId == roomId }
queue.removeAll { it is InviteNotifiableEvent && it.sessionId == sessionId && it.roomId == roomId }
}
fun clearMessagesForSession(sessionId: String) {
fun clearMessagesForSession(sessionId: SessionId) {
Timber.d("clearMessagesForSession $sessionId")
queue.removeAll { it is NotifiableMessageEvent && it.sessionId == sessionId}
queue.removeAll { it is NotifiableMessageEvent && it.sessionId == sessionId }
}
fun clearMessagesForRoom(sessionId: String, roomId: String) {
fun clearMessagesForRoom(sessionId: SessionId, roomId: RoomId) {
Timber.d("clearMessageEventOfRoom $sessionId, $roomId")
queue.removeAll { it is NotifiableMessageEvent && it.sessionId == sessionId && it.roomId == roomId }
queue.removeAll { it is NotifiableMessageEvent && it.sessionId == sessionId && it.roomId == roomId }
}
fun clearMessagesForThread(sessionId: String, roomId: String, threadId: String) {
fun clearMessagesForThread(sessionId: SessionId, roomId: RoomId, threadId: ThreadId) {
Timber.d("clearMessageEventOfThread $sessionId, $roomId, $threadId")
queue.removeAll { it is NotifiableMessageEvent && it.sessionId == sessionId && it.roomId == roomId && it.threadId == threadId }
queue.removeAll { it is NotifiableMessageEvent && it.sessionId == sessionId && it.roomId == roomId && it.threadId == threadId }
}
fun rawEvents(): List<NotifiableEvent> = queue
}
private fun MutableList<NotifiableEvent>.replace(eventId: String, block: (NotifiableEvent) -> NotifiableEvent) {
private fun MutableList<NotifiableEvent>.replace(eventId: EventId, block: (NotifiableEvent) -> NotifiableEvent) {
val indexToReplace = indexOfFirst { it.eventId == eventId }
if (indexToReplace == -1) {
return

View File

@@ -17,6 +17,8 @@
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.push.impl.notifications.model.InviteNotifiableEvent
import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent
import io.element.android.libraries.push.impl.notifications.model.SimpleNotifiableEvent
@@ -30,8 +32,8 @@ class NotificationFactory @Inject constructor(
private val summaryGroupMessageCreator: SummaryGroupMessageCreator
) {
fun Map<String, ProcessedMessageEvents>.toNotifications(
sessionId: String,
fun Map<RoomId, ProcessedMessageEvents>.toNotifications(
sessionId: SessionId,
myUserDisplayName: String,
myUserAvatarUrl: String?
): List<RoomNotification> {
@@ -62,11 +64,11 @@ class NotificationFactory @Inject constructor(
fun List<ProcessedEvent<InviteNotifiableEvent>>.toNotifications(): List<OneShotNotification> {
return map { (processed, event) ->
when (processed) {
ProcessedEvent.Type.REMOVE -> OneShotNotification.Removed(key = event.roomId)
ProcessedEvent.Type.REMOVE -> OneShotNotification.Removed(key = event.roomId.value)
ProcessedEvent.Type.KEEP -> OneShotNotification.Append(
notificationUtils.buildRoomInvitationNotification(event),
OneShotNotification.Append.Meta(
key = event.roomId,
key = event.roomId.value,
summaryLine = event.description,
isNoisy = event.noisy,
timestamp = event.timestamp
@@ -80,11 +82,11 @@ class NotificationFactory @Inject constructor(
fun List<ProcessedEvent<SimpleNotifiableEvent>>.toNotifications(): List<OneShotNotification> {
return map { (processed, event) ->
when (processed) {
ProcessedEvent.Type.REMOVE -> OneShotNotification.Removed(key = event.eventId)
ProcessedEvent.Type.REMOVE -> OneShotNotification.Removed(key = event.eventId.value)
ProcessedEvent.Type.KEEP -> OneShotNotification.Append(
notificationUtils.buildSimpleEventNotification(event),
OneShotNotification.Append.Meta(
key = event.eventId,
key = event.eventId.value,
summaryLine = event.description,
isNoisy = event.noisy,
timestamp = event.timestamp
@@ -95,7 +97,7 @@ class NotificationFactory @Inject constructor(
}
fun createSummaryNotification(
sessionId: String,
sessionId: SessionId,
roomNotifications: List<RoomNotification>,
invitationNotifications: List<OneShotNotification>,
simpleNotifications: List<OneShotNotification>,
@@ -120,10 +122,10 @@ class NotificationFactory @Inject constructor(
}
sealed interface RoomNotification {
data class Removed(val roomId: String) : RoomNotification
data class Removed(val roomId: RoomId) : RoomNotification
data class Message(val notification: Notification, val meta: Meta) : RoomNotification {
data class Meta(
val roomId: String,
val roomId: RoomId,
val summaryLine: CharSequence,
val messageCount: Int,
val latestTimestamp: Long,

View File

@@ -18,27 +18,28 @@ package io.element.android.libraries.push.impl.notifications
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.SingleIn
import io.element.android.libraries.matrix.api.core.SessionId
import javax.inject.Inject
@SingleIn(AppScope::class)
class NotificationIdProvider @Inject constructor() {
fun getSummaryNotificationId(sessionId: String): Int {
fun getSummaryNotificationId(sessionId: SessionId): Int {
return getOffset(sessionId) + SUMMARY_NOTIFICATION_ID
}
fun getRoomMessagesNotificationId(sessionId: String): Int {
fun getRoomMessagesNotificationId(sessionId: SessionId): Int {
return getOffset(sessionId) + ROOM_MESSAGES_NOTIFICATION_ID
}
fun getRoomEventNotificationId(sessionId: String): Int {
fun getRoomEventNotificationId(sessionId: SessionId): Int {
return getOffset(sessionId) + ROOM_EVENT_NOTIFICATION_ID
}
fun getRoomInvitationNotificationId(sessionId: String): Int {
fun getRoomInvitationNotificationId(sessionId: SessionId): Int {
return getOffset(sessionId) + ROOM_INVITATION_NOTIFICATION_ID
}
private fun getOffset(sessionId: String): Int {
private fun getOffset(sessionId: SessionId): Int {
// TODO EAx multi account: return different value for users and persist data
return 0
}

View File

@@ -16,6 +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.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
@@ -31,7 +33,7 @@ class NotificationRenderer @Inject constructor(
@WorkerThread
fun render(
sessionId: String,
sessionId: SessionId,
myUserDisplayName: String,
myUserAvatarUrl: String?,
useCompleteNotificationFormat: Boolean,
@@ -60,12 +62,12 @@ class NotificationRenderer @Inject constructor(
when (wrapper) {
is RoomNotification.Removed -> {
Timber.d("Removing room messages notification ${wrapper.roomId}")
notificationDisplayer.cancelNotificationMessage(wrapper.roomId, notificationIdProvider.getRoomMessagesNotificationId(sessionId))
notificationDisplayer.cancelNotificationMessage(wrapper.roomId.value, notificationIdProvider.getRoomMessagesNotificationId(sessionId))
}
is RoomNotification.Message -> if (useCompleteNotificationFormat) {
Timber.d("Updating room messages notification ${wrapper.meta.roomId}")
notificationDisplayer.showNotificationMessage(
wrapper.meta.roomId,
wrapper.meta.roomId.value,
notificationIdProvider.getRoomMessagesNotificationId(sessionId),
wrapper.notification
)
@@ -125,7 +127,7 @@ class NotificationRenderer @Inject constructor(
}
private fun List<ProcessedEvent<NotifiableEvent>>.groupByType(): GroupedNotificationEvents {
val roomIdToEventMap: MutableMap<String, MutableList<ProcessedEvent<NotifiableMessageEvent>>> = LinkedHashMap()
val roomIdToEventMap: MutableMap<RoomId, MutableList<ProcessedEvent<NotifiableMessageEvent>>> = LinkedHashMap()
val simpleEvents: MutableList<ProcessedEvent<SimpleNotifiableEvent>> = ArrayList()
val invitationEvents: MutableList<ProcessedEvent<InviteNotifiableEvent>> = ArrayList()
forEach {
@@ -145,7 +147,7 @@ private fun List<ProcessedEvent<NotifiableEvent>>.groupByType(): GroupedNotifica
private fun <T : NotifiableEvent> ProcessedEvent<NotifiableEvent>.castedToEventType(): ProcessedEvent<T> = this as ProcessedEvent<T>
data class GroupedNotificationEvents(
val roomEvents: Map<String, List<ProcessedEvent<NotifiableMessageEvent>>>,
val roomEvents: Map<RoomId, List<ProcessedEvent<NotifiableMessageEvent>>>,
val simpleEvents: List<ProcessedEvent<SimpleNotifiableEvent>>,
val invitationEvents: List<ProcessedEvent<InviteNotifiableEvent>>
)

View File

@@ -44,6 +44,9 @@ 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.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.push.impl.R
import io.element.android.libraries.push.impl.intent.IntentProvider
import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent
@@ -215,7 +218,7 @@ class NotificationUtils @Inject constructor(
fun buildMessagesListNotification(
messageStyle: NotificationCompat.MessagingStyle,
roomInfo: RoomEventGroupInfo,
threadId: String?,
threadId: ThreadId?,
largeIcon: Bitmap?,
lastMessageTimestamp: Long,
senderDisplayNameForReplyCompat: String?,
@@ -244,7 +247,7 @@ class NotificationUtils @Inject constructor(
// that can be displayed in not disturb mode if white listed (the later will need compat28.x)
.setCategory(NotificationCompat.CATEGORY_MESSAGE)
// ID of the corresponding shortcut, for conversation features under API 30+
.setShortcutId(roomInfo.roomId)
.setShortcutId(roomInfo.roomId.value)
// Title for API < 16 devices.
.setContentTitle(roomInfo.roomDisplayName)
// Content for API < 16 devices.
@@ -259,7 +262,7 @@ class NotificationUtils @Inject constructor(
)
// 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
.setGroup(roomInfo.sessionId)
.setGroup(roomInfo.sessionId.value)
// In order to avoid notification making sound twice (due to the summary notification)
.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_ALL)
.setSmallIcon(smallIcon)
@@ -359,7 +362,7 @@ class NotificationUtils @Inject constructor(
.setOnlyAlertOnce(true)
.setContentTitle(inviteNotifiableEvent.roomName ?: buildMeta.applicationName)
.setContentText(inviteNotifiableEvent.description)
.setGroup(inviteNotifiableEvent.sessionId)
.setGroup(inviteNotifiableEvent.sessionId.value)
.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_ALL)
.setSmallIcon(smallIcon)
.setColor(accentColor)
@@ -446,7 +449,7 @@ class NotificationUtils @Inject constructor(
.setOnlyAlertOnce(true)
.setContentTitle(buildMeta.applicationName)
.setContentText(simpleNotifiableEvent.description)
.setGroup(simpleNotifiableEvent.sessionId)
.setGroup(simpleNotifiableEvent.sessionId.value)
.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_ALL)
.setSmallIcon(smallIcon)
.setColor(accentColor)
@@ -477,7 +480,7 @@ class NotificationUtils @Inject constructor(
.build()
}
private fun buildOpenRoomIntent(sessionId: String, roomId: String): PendingIntent? {
private fun buildOpenRoomIntent(sessionId: SessionId, roomId: RoomId): PendingIntent? {
val roomIntent = intentProvider.getIntent(sessionId = sessionId, roomId = roomId, threadId = null)
roomIntent.action = actionIds.tapToView
// pending intent get reused by system, this will mess up the extra params, so put unique info to avoid that
@@ -491,7 +494,7 @@ class NotificationUtils @Inject constructor(
)
}
private fun buildOpenThreadIntent(roomInfo: RoomEventGroupInfo, threadId: String?): PendingIntent? {
private fun buildOpenThreadIntent(roomInfo: RoomEventGroupInfo, threadId: ThreadId?): PendingIntent? {
val sessionId = roomInfo.sessionId
val roomId = roomInfo.roomId
val threadIntentTap = intentProvider.getIntent(sessionId = sessionId, roomId = roomId, threadId = threadId)
@@ -507,7 +510,7 @@ class NotificationUtils @Inject constructor(
)
}
private fun buildOpenHomePendingIntentForSummary(sessionId: String): PendingIntent {
private fun buildOpenHomePendingIntentForSummary(sessionId: SessionId): PendingIntent {
val intent = intentProvider.getIntent(sessionId = sessionId, roomId = null, threadId = null)
intent.data = createIgnoredUri("tapSummary?$sessionId")
return PendingIntent.getActivity(
@@ -526,9 +529,9 @@ class NotificationUtils @Inject constructor(
it will be more appropriate to use an activity. Since you have to provide your own UI.
*/
private fun buildQuickReplyIntent(
sessionId: String,
roomId: String,
threadId: String?,
sessionId: SessionId,
roomId: RoomId,
threadId: ThreadId?,
senderName: String?
): PendingIntent? {
val intent: Intent
@@ -573,7 +576,7 @@ class NotificationUtils @Inject constructor(
* Build the summary notification.
*/
fun buildSummaryListNotification(
sessionId: String,
sessionId: SessionId,
style: NotificationCompat.InboxStyle?,
compatSummary: String,
noisy: Boolean,
@@ -587,12 +590,12 @@ class NotificationUtils @Inject constructor(
// used in compat < N, after summary is built based on child notifications
.setWhen(lastMessageTimestamp)
.setStyle(style)
.setContentTitle(sessionId)
.setContentTitle(sessionId.value)
.setCategory(NotificationCompat.CATEGORY_MESSAGE)
.setSmallIcon(smallIcon)
// set content text to support devices running API level < 24
.setContentText(compatSummary)
.setGroup(sessionId)
.setGroup(sessionId.value)
// set this notification as the summary for the group
.setGroupSummary(true)
.setColor(accentColor)
@@ -616,7 +619,7 @@ class NotificationUtils @Inject constructor(
.build()
}
private fun getDismissSummaryPendingIntent(sessionId: String): PendingIntent {
private fun getDismissSummaryPendingIntent(sessionId: SessionId): PendingIntent {
val intent = Intent(context, NotificationBroadcastReceiver::class.java)
intent.action = actionIds.dismissSummary
intent.data = createIgnoredUri("deleteSummary?$sessionId")

View File

@@ -16,12 +16,15 @@
package io.element.android.libraries.push.impl.notifications
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId
/**
* Data class to hold information about a group of notifications for a room.
*/
data class RoomEventGroupInfo(
val sessionId: String,
val roomId: String,
val sessionId: SessionId,
val roomId: RoomId,
val roomDisplayName: String,
val isDirect: Boolean = false
) {

View File

@@ -19,6 +19,8 @@ package io.element.android.libraries.push.impl.notifications
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.push.impl.R
import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent
import io.element.android.services.toolbox.api.strings.StringProvider
@@ -34,9 +36,9 @@ class RoomGroupMessageCreator @Inject constructor(
) {
fun createRoomMessage(
sessionId: String,
sessionId: SessionId,
events: List<NotifiableMessageEvent>,
roomId: String,
roomId: RoomId,
userDisplayName: String,
userAvatarUrl: String?
): RoomNotification.Message {
@@ -47,7 +49,7 @@ class RoomGroupMessageCreator @Inject constructor(
Person.Builder()
.setName(userDisplayName)
.setIcon(bitmapLoader.getUserIcon(userAvatarUrl))
.setKey(lastKnownRoomEvent.sessionId)
.setKey(lastKnownRoomEvent.sessionId.value)
.build()
).also {
it.conversationTitle = roomName.takeIf { roomIsGroup }

View File

@@ -18,6 +18,7 @@ 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.push.impl.R
import io.element.android.services.toolbox.api.strings.StringProvider
import javax.inject.Inject
@@ -42,7 +43,7 @@ class SummaryGroupMessageCreator @Inject constructor(
) {
fun createSummaryNotification(
sessionId: String,
sessionId: SessionId,
roomNotifications: List<RoomNotification.Message.Meta>,
invitationNotifications: List<OneShotNotification.Append.Meta>,
simpleNotifications: List<OneShotNotification.Append.Meta>,
@@ -92,7 +93,7 @@ class SummaryGroupMessageCreator @Inject constructor(
}
private fun processSimpleGroupSummary(
sessionId: String,
sessionId: SessionId,
summaryIsNoisy: Boolean,
messageEventsCount: Int,
simpleEventsCount: Int,

View File

@@ -15,11 +15,15 @@
*/
package io.element.android.libraries.push.impl.notifications.model
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
data class InviteNotifiableEvent(
override val sessionId: String,
override val roomId: String,
override val eventId: String,
override val editedEventId: String?,
override val sessionId: SessionId,
override val roomId: RoomId,
override val eventId: EventId,
override val editedEventId: EventId?,
override val canBeReplaced: Boolean,
val roomName: String?,
val noisy: Boolean,

View File

@@ -15,16 +15,19 @@
*/
package io.element.android.libraries.push.impl.notifications.model
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 java.io.Serializable
/**
* Parent interface for all events which can be displayed as a Notification.
*/
sealed interface NotifiableEvent : Serializable {
val sessionId: String
val roomId: String
val eventId: String
val editedEventId: String?
val sessionId: SessionId
val roomId: RoomId
val eventId: EventId
val editedEventId: EventId?
// Used to know if event should be replaced with the one coming from eventstream
val canBeReplaced: Boolean

View File

@@ -16,16 +16,20 @@
package io.element.android.libraries.push.impl.notifications.model
import android.net.Uri
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.ThreadId
import io.element.android.services.appnavstate.api.AppNavigationState
import io.element.android.services.appnavstate.api.currentRoomId
import io.element.android.services.appnavstate.api.currentSessionId
import io.element.android.services.appnavstate.api.currentThreadId
data class NotifiableMessageEvent(
override val sessionId: String,
override val roomId: String,
override val eventId: String,
override val editedEventId: String?,
override val sessionId: SessionId,
override val roomId: RoomId,
override val eventId: EventId,
override val editedEventId: EventId?,
override val canBeReplaced: Boolean,
val noisy: Boolean,
val timestamp: Long,
@@ -35,7 +39,7 @@ data class NotifiableMessageEvent(
// We cannot use Uri? type here, as that could trigger a
// NotSerializableException when persisting this to storage
val imageUriString: String?,
val threadId: String?,
val threadId: ThreadId?,
val roomName: String?,
val roomIsDirect: Boolean = false,
val roomAvatarPath: String? = null,
@@ -59,9 +63,9 @@ data class NotifiableMessageEvent(
fun NotifiableMessageEvent.shouldIgnoreMessageEventInRoom(
appNavigationState: AppNavigationState?
): Boolean {
val currentSessionId = appNavigationState?.currentSessionId()?.value ?: return false
return when (val currentRoomId = appNavigationState.currentRoomId()?.value) {
val currentSessionId = appNavigationState?.currentSessionId() ?: return false
return when (val currentRoomId = appNavigationState.currentRoomId()) {
null -> false
else -> sessionId == currentSessionId && roomId == currentRoomId && threadId == appNavigationState.currentThreadId()?.value
else -> sessionId == currentSessionId && roomId == currentRoomId && threadId == appNavigationState.currentThreadId()
}
}

View File

@@ -15,11 +15,15 @@
*/
package io.element.android.libraries.push.impl.notifications.model
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
data class SimpleNotifiableEvent(
override val sessionId: String,
override val roomId: String,
override val eventId: String,
override val editedEventId: String?,
override val sessionId: SessionId,
override val roomId: RoomId,
override val eventId: EventId,
override val editedEventId: EventId?,
val noisy: Boolean,
val title: String,
val description: String,

View File

@@ -1,5 +1,5 @@
/*
* Copyright (c) 2022 New Vector Ltd
* 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.
@@ -16,6 +16,9 @@
package io.element.android.libraries.push.impl.push
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId
/**
* Represent parsed data that the app has received from a Push content.
*
@@ -24,8 +27,8 @@ package io.element.android.libraries.push.impl.push
* @property unread Number of unread message.
*/
data class PushData(
val eventId: String?,
val roomId: String?,
val eventId: EventId?,
val roomId: RoomId?,
val unread: Int?,
val clientSecret: String?,
)

View File

@@ -119,7 +119,7 @@ class PushHandler @Inject constructor(
// Get userId from client secret
pushClientSecret.getUserIdFromSecret(clientSecret)
} ?: run {
matrixAuthenticationService.getLatestSessionId()?.value
matrixAuthenticationService.getLatestSessionId()
}
if (userId == null) {

View File

@@ -15,6 +15,7 @@
*/
package io.element.android.libraries.push.impl.pushgateway
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.network.RetrofitFactory
import io.element.android.libraries.push.api.gateway.PushGatewayFailure
import javax.inject.Inject
@@ -26,7 +27,7 @@ class PushGatewayNotifyRequest @Inject constructor(
val url: String,
val appId: String,
val pushKey: String,
val eventId: String
val eventId: EventId
)
suspend fun execute(params: Params) {
@@ -38,7 +39,7 @@ class PushGatewayNotifyRequest @Inject constructor(
val response = sygnalApi.notify(
PushGatewayNotifyBody(
PushGatewayNotification(
eventId = params.eventId,
eventId = params.eventId.value,
devices = listOf(
PushGatewayDevice(
params.appId,

View File

@@ -17,6 +17,8 @@
package io.element.android.libraries.push.impl.unifiedpush
import io.element.android.libraries.matrix.api.core.MatrixPatterns
import io.element.android.libraries.matrix.api.core.asEventId
import io.element.android.libraries.matrix.api.core.asRoomId
import io.element.android.libraries.push.impl.push.PushData
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@@ -55,8 +57,8 @@ data class PushDataUnifiedPushCounts(
)
fun PushDataUnifiedPush.toPushData() = PushData(
eventId = notification?.eventId?.takeIf { MatrixPatterns.isEventId(it) },
roomId = notification?.roomId?.takeIf { MatrixPatterns.isRoomId(it) },
eventId = notification?.eventId?.takeIf { MatrixPatterns.isEventId(it) }?.asEventId(),
roomId = notification?.roomId?.takeIf { MatrixPatterns.isRoomId(it) }?.asRoomId(),
unread = notification?.counts?.unread,
clientSecret = null // TODO EAx check how client secret will be sent through UnifiedPush
)

View File

@@ -16,24 +16,26 @@
package io.element.android.libraries.push.impl.clientsecret
import io.element.android.libraries.matrix.api.core.SessionId
class InMemoryPushClientSecretStore : PushClientSecretStore {
private val secrets = mutableMapOf<String, String>()
private val secrets = mutableMapOf<SessionId, String>()
fun getSecrets(): Map<String, String> = secrets
fun getSecrets(): Map<SessionId, String> = secrets
override suspend fun storeSecret(userId: String, clientSecret: String) {
override suspend fun storeSecret(userId: SessionId, clientSecret: String) {
secrets[userId] = clientSecret
}
override suspend fun getSecret(userId: String): String? {
override suspend fun getSecret(userId: SessionId): String? {
return secrets[userId]
}
override suspend fun resetSecret(userId: String) {
override suspend fun resetSecret(userId: SessionId) {
secrets.remove(userId)
}
override suspend fun getUserIdFromSecret(clientSecret: String): String? {
override suspend fun getUserIdFromSecret(clientSecret: String): SessionId? {
return secrets.keys.firstOrNull { secrets[it] == clientSecret }
}
}

View File

@@ -19,12 +19,13 @@
package io.element.android.libraries.push.impl.clientsecret
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.core.SessionId
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Test
private const val A_USER_ID_0 = "A_USER_ID_0"
private const val A_USER_ID_1 = "A_USER_ID_1"
private val A_USER_ID_0 = SessionId("A_USER_ID_0")
private val A_USER_ID_1 = SessionId("A_USER_ID_1")
private const val A_UNKNOWN_SECRET = "A_UNKNOWN_SECRET"

View File

@@ -0,0 +1,194 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.push.impl.notifications
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.test.AN_EVENT_ID
import io.element.android.libraries.matrix.test.AN_EVENT_ID_2
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_ROOM_ID_2
import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.matrix.test.A_SPACE_ID
import io.element.android.libraries.matrix.test.A_THREAD_ID
import io.element.android.libraries.push.impl.notifications.fake.FakeOutdatedEventDetector
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 io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
import io.element.android.services.appnavstate.test.anAppNavigationState
import org.junit.Test
private val NOT_VIEWING_A_ROOM = anAppNavigationState()
private val VIEWING_A_ROOM = anAppNavigationState(A_SESSION_ID, A_SPACE_ID, A_ROOM_ID)
private val VIEWING_A_THREAD = anAppNavigationState(A_SESSION_ID, A_SPACE_ID, A_ROOM_ID, A_THREAD_ID)
class NotifiableEventProcessorTest {
private val outdatedDetector = FakeOutdatedEventDetector()
private val eventProcessor = NotifiableEventProcessor(outdatedDetector.instance)
@Test
fun `given simple events when processing then keep simple events`() {
val events = listOf(
aSimpleNotifiableEvent(eventId = AN_EVENT_ID),
aSimpleNotifiableEvent(eventId = AN_EVENT_ID_2)
)
val result = eventProcessor.process(events, appNavigationState = NOT_VIEWING_A_ROOM, renderedEvents = emptyList())
assertThat(result).isEqualTo(
listOfProcessedEvents(
ProcessedEvent.Type.KEEP to events[0],
ProcessedEvent.Type.KEEP to events[1]
)
)
}
@Test
fun `given redacted simple event when processing then remove redaction event`() {
val events = listOf(aSimpleNotifiableEvent(eventId = AN_EVENT_ID, type = "m.room.redaction"))
val result = eventProcessor.process(events, appNavigationState = NOT_VIEWING_A_ROOM, renderedEvents = emptyList())
assertThat(result).isEqualTo(
listOfProcessedEvents(
ProcessedEvent.Type.REMOVE to events[0]
)
)
}
@Test
fun `given invites are not auto accepted when processing then keep invitation events`() {
val events = listOf(
anInviteNotifiableEvent(roomId = A_ROOM_ID),
anInviteNotifiableEvent(roomId = A_ROOM_ID_2)
)
val result = eventProcessor.process(events, appNavigationState = NOT_VIEWING_A_ROOM, renderedEvents = emptyList())
assertThat(result).isEqualTo(
listOfProcessedEvents(
ProcessedEvent.Type.KEEP to events[0],
ProcessedEvent.Type.KEEP to events[1]
)
)
}
@Test
fun `given out of date message event when processing then removes message event`() {
val events = listOf(aNotifiableMessageEvent(eventId = AN_EVENT_ID, roomId = A_ROOM_ID))
outdatedDetector.givenEventIsOutOfDate(events[0])
val result = eventProcessor.process(events, appNavigationState = NOT_VIEWING_A_ROOM, renderedEvents = emptyList())
assertThat(result).isEqualTo(
listOfProcessedEvents(
ProcessedEvent.Type.REMOVE to events[0],
)
)
}
@Test
fun `given in date message event when processing then keep message event`() {
val events = listOf(aNotifiableMessageEvent(eventId = AN_EVENT_ID, roomId = A_ROOM_ID))
outdatedDetector.givenEventIsInDate(events[0])
val result = eventProcessor.process(events, appNavigationState = NOT_VIEWING_A_ROOM, renderedEvents = emptyList())
assertThat(result).isEqualTo(
listOfProcessedEvents(
ProcessedEvent.Type.KEEP to events[0],
)
)
}
@Test
fun `given viewing the same room main timeline when processing main timeline message event then removes message`() {
val events = listOf(aNotifiableMessageEvent(eventId = AN_EVENT_ID, roomId = A_ROOM_ID, threadId = null))
val result = eventProcessor.process(events, VIEWING_A_ROOM, renderedEvents = emptyList())
assertThat(result).isEqualTo(
listOfProcessedEvents(
ProcessedEvent.Type.REMOVE to events[0],
)
)
}
@Test
fun `given viewing the same thread timeline when processing thread message event then removes message`() {
val events = listOf(aNotifiableMessageEvent(eventId = AN_EVENT_ID, roomId = A_ROOM_ID, threadId = A_THREAD_ID))
val result = eventProcessor.process(events, VIEWING_A_THREAD, renderedEvents = emptyList())
assertThat(result).isEqualTo(
listOfProcessedEvents(
ProcessedEvent.Type.REMOVE to events[0],
)
)
}
@Test
fun `given viewing main timeline of the same room when processing thread timeline message event then keep message`() {
val events = listOf(aNotifiableMessageEvent(eventId = AN_EVENT_ID, roomId = A_ROOM_ID, threadId = A_THREAD_ID))
outdatedDetector.givenEventIsInDate(events[0])
val result = eventProcessor.process(events, VIEWING_A_ROOM, renderedEvents = emptyList())
assertThat(result).isEqualTo(
listOfProcessedEvents(
ProcessedEvent.Type.KEEP to events[0],
)
)
}
@Test
fun `given viewing thread timeline of the same room when processing main timeline message event then keep message`() {
val events = listOf(aNotifiableMessageEvent(eventId = AN_EVENT_ID, roomId = A_ROOM_ID))
outdatedDetector.givenEventIsInDate(events[0])
val result = eventProcessor.process(events, VIEWING_A_THREAD, renderedEvents = emptyList())
assertThat(result).isEqualTo(
listOfProcessedEvents(
ProcessedEvent.Type.KEEP to events[0],
)
)
}
@Test
fun `given events are different to rendered events when processing then removes difference`() {
val events = listOf(aSimpleNotifiableEvent(eventId = AN_EVENT_ID))
val renderedEvents = listOf<ProcessedEvent<NotifiableEvent>>(
ProcessedEvent(ProcessedEvent.Type.KEEP, events[0]),
ProcessedEvent(ProcessedEvent.Type.KEEP, anInviteNotifiableEvent(eventId = AN_EVENT_ID_2))
)
val result = eventProcessor.process(events, appNavigationState = NOT_VIEWING_A_ROOM, renderedEvents = renderedEvents)
assertThat(result).isEqualTo(
listOfProcessedEvents(
ProcessedEvent.Type.REMOVE to renderedEvents[1].event,
ProcessedEvent.Type.KEEP to renderedEvents[0].event
)
)
}
private fun listOfProcessedEvents(vararg event: Pair<ProcessedEvent.Type, NotifiableEvent>) = event.map {
ProcessedEvent(it.first, it.second)
}
}

View File

@@ -0,0 +1,231 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.push.impl.notifications
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.core.cache.CircularCache
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.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 io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
import org.junit.Test
class NotificationEventQueueTest {
private val seenIdsCache = CircularCache.create<EventId>(5)
@Test
fun `given events when redacting some then marks matching event ids as redacted`() {
val queue = givenQueue(
listOf(
aSimpleNotifiableEvent(eventId = EventId("redacted-id-1")),
aNotifiableMessageEvent(eventId = EventId("redacted-id-2")),
anInviteNotifiableEvent(eventId = EventId("redacted-id-3")),
aSimpleNotifiableEvent(eventId = EventId("kept-id")),
)
)
queue.markRedacted(listOf(EventId("redacted-id-1"), EventId("redacted-id-2"), EventId("redacted-id-3")))
assertThat(queue.rawEvents()).isEqualTo(
listOf(
aSimpleNotifiableEvent(eventId = EventId("redacted-id-1"), isRedacted = true),
aNotifiableMessageEvent(eventId = EventId("redacted-id-2"), isRedacted = true),
anInviteNotifiableEvent(eventId = EventId("redacted-id-3"), isRedacted = true),
aSimpleNotifiableEvent(eventId = EventId("kept-id"), isRedacted = false),
)
)
}
@Test
fun `given invite event when leaving invited room and syncing then removes event`() {
val queue = givenQueue(listOf(anInviteNotifiableEvent(roomId = A_ROOM_ID)))
val roomsLeft = listOf(A_ROOM_ID)
queue.syncRoomEvents(roomsLeft = roomsLeft, roomsJoined = emptyList())
assertThat(queue.rawEvents()).isEmpty()
}
@Test
fun `given invite event when joining invited room and syncing then removes event`() {
val queue = givenQueue(listOf(anInviteNotifiableEvent(roomId = A_ROOM_ID)))
val joinedRooms = listOf(A_ROOM_ID)
queue.syncRoomEvents(roomsLeft = emptyList(), roomsJoined = joinedRooms)
assertThat(queue.rawEvents()).isEmpty()
}
@Test
fun `given message event when leaving message room and syncing then removes event`() {
val queue = givenQueue(listOf(aNotifiableMessageEvent(roomId = A_ROOM_ID)))
val roomsLeft = listOf(A_ROOM_ID)
queue.syncRoomEvents(roomsLeft = roomsLeft, roomsJoined = emptyList())
assertThat(queue.rawEvents()).isEmpty()
}
@Test
fun `given events when syncing without rooms left or joined ids then does not change the events`() {
val queue = givenQueue(
listOf(
aNotifiableMessageEvent(roomId = A_ROOM_ID),
anInviteNotifiableEvent(roomId = A_ROOM_ID)
)
)
queue.syncRoomEvents(roomsLeft = emptyList(), roomsJoined = emptyList())
assertThat(queue.rawEvents()).isEqualTo(
listOf(
aNotifiableMessageEvent(roomId = A_ROOM_ID),
anInviteNotifiableEvent(roomId = A_ROOM_ID)
)
)
}
@Test
fun `given events then is not empty`() {
val queue = givenQueue(listOf(aSimpleNotifiableEvent()))
assertThat(queue.isEmpty()).isFalse()
}
@Test
fun `given no events then is empty`() {
val queue = givenQueue(emptyList())
assertThat(queue.isEmpty()).isTrue()
}
@Test
fun `given events when clearing and adding then removes previous events and adds only new events`() {
val queue = givenQueue(listOf(aSimpleNotifiableEvent()))
queue.clearAndAdd(listOf(anInviteNotifiableEvent()))
assertThat(queue.rawEvents()).isEqualTo(listOf(anInviteNotifiableEvent()))
}
@Test
fun `when clearing then is empty`() {
val queue = givenQueue(listOf(aSimpleNotifiableEvent()))
queue.clear()
assertThat(queue.rawEvents()).isEmpty()
}
@Test
fun `given no events when adding then adds event`() {
val queue = givenQueue(listOf())
queue.add(aSimpleNotifiableEvent())
assertThat(queue.rawEvents()).isEqualTo(listOf(aSimpleNotifiableEvent()))
}
@Test
fun `given no events when adding already seen event then ignores event`() {
val queue = givenQueue(listOf())
val notifiableEvent = aSimpleNotifiableEvent()
seenIdsCache.put(notifiableEvent.eventId)
queue.add(notifiableEvent)
assertThat(queue.rawEvents()).isEmpty()
}
@Test
fun `given replaceable event when adding event with same id then updates existing event`() {
val replaceableEvent = aSimpleNotifiableEvent(canBeReplaced = true)
val updatedEvent = replaceableEvent.copy(title = "updated title", isUpdated = true)
val queue = givenQueue(listOf(replaceableEvent))
queue.add(updatedEvent)
assertThat(queue.rawEvents()).isEqualTo(listOf(updatedEvent))
}
@Test
fun `given non replaceable event when adding event with same id then ignores event`() {
val nonReplaceableEvent = aSimpleNotifiableEvent(canBeReplaced = false)
val updatedEvent = nonReplaceableEvent.copy(title = "updated title")
val queue = givenQueue(listOf(nonReplaceableEvent))
queue.add(updatedEvent)
assertThat(queue.rawEvents()).isEqualTo(listOf(nonReplaceableEvent))
}
@Test
fun `given event when adding new event with edited event id matching the existing event id then updates existing event`() {
val editedEvent = aSimpleNotifiableEvent(eventId = EventId("id-to-edit"))
val updatedEvent = editedEvent.copy(eventId = EventId("1"), editedEventId = EventId("id-to-edit"), title = "updated title", isUpdated = true)
val queue = givenQueue(listOf(editedEvent))
queue.add(updatedEvent)
assertThat(queue.rawEvents()).isEqualTo(listOf(updatedEvent))
}
@Test
fun `given event when adding new event with edited event id matching the existing event edited id then updates existing event`() {
val editedEvent = aSimpleNotifiableEvent(eventId = EventId("0"), editedEventId = EventId("id-to-edit"))
val updatedEvent = editedEvent.copy(eventId = EventId("1"), editedEventId = EventId("id-to-edit"), title = "updated title", isUpdated = true)
val queue = givenQueue(listOf(editedEvent))
queue.add(updatedEvent)
assertThat(queue.rawEvents()).isEqualTo(listOf(updatedEvent))
}
@Test
fun `when clearing membership notification then removes invite events with matching room id`() {
val queue = givenQueue(
listOf(
anInviteNotifiableEvent(roomId = A_ROOM_ID),
aNotifiableMessageEvent(roomId = A_ROOM_ID)
)
)
queue.clearMemberShipNotificationForRoom(A_SESSION_ID, A_ROOM_ID)
assertThat(queue.rawEvents()).isEqualTo(listOf(aNotifiableMessageEvent(roomId = A_ROOM_ID)))
}
@Test
fun `when clearing messages for room then removes message events with matching room id`() {
val queue = givenQueue(
listOf(
anInviteNotifiableEvent(roomId = A_ROOM_ID),
aNotifiableMessageEvent(roomId = A_ROOM_ID)
)
)
queue.clearMessagesForRoom(A_SESSION_ID, A_ROOM_ID)
assertThat(queue.rawEvents()).isEqualTo(listOf(anInviteNotifiableEvent(roomId = A_ROOM_ID)))
}
private fun givenQueue(events: List<NotifiableEvent>) = NotificationEventQueue(events.toMutableList(), seenEventIds = seenIdsCache)
}

View File

@@ -0,0 +1,194 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package 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.test.AN_EVENT_ID
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.push.impl.notifications.fake.FakeNotificationUtils
import io.element.android.libraries.push.impl.notifications.fake.FakeRoomGroupMessageCreator
import io.element.android.libraries.push.impl.notifications.fake.FakeSummaryGroupMessageCreator
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 org.junit.Test
private val MY_AVATAR_URL: String? = null
private val AN_INVITATION_EVENT = anInviteNotifiableEvent(roomId = A_ROOM_ID)
private val A_SIMPLE_EVENT = aSimpleNotifiableEvent(eventId = AN_EVENT_ID)
private val A_MESSAGE_EVENT = aNotifiableMessageEvent(eventId = AN_EVENT_ID, roomId = A_ROOM_ID)
class NotificationFactoryTest {
private val notificationUtils = FakeNotificationUtils()
private val roomGroupMessageCreator = FakeRoomGroupMessageCreator()
private val summaryGroupMessageCreator = FakeSummaryGroupMessageCreator()
private val notificationFactory = NotificationFactory(
notificationUtils.instance,
roomGroupMessageCreator.instance,
summaryGroupMessageCreator.instance
)
@Test
fun `given a room invitation when mapping to notification then is Append`() = testWith(notificationFactory) {
val expectedNotification = notificationUtils.givenBuildRoomInvitationNotificationFor(AN_INVITATION_EVENT)
val roomInvitation = listOf(ProcessedEvent(ProcessedEvent.Type.KEEP, AN_INVITATION_EVENT))
val result = roomInvitation.toNotifications()
assertThat(result).isEqualTo(
listOf(
OneShotNotification.Append(
notification = expectedNotification,
meta = OneShotNotification.Append.Meta(
key = A_ROOM_ID.value,
summaryLine = AN_INVITATION_EVENT.description,
isNoisy = AN_INVITATION_EVENT.noisy,
timestamp = AN_INVITATION_EVENT.timestamp
)
)
)
)
}
@Test
fun `given a missing event in room invitation when mapping to notification then is Removed`() = testWith(notificationFactory) {
val missingEventRoomInvitation = listOf(ProcessedEvent(ProcessedEvent.Type.REMOVE, AN_INVITATION_EVENT))
val result = missingEventRoomInvitation.toNotifications()
assertThat(result).isEqualTo(
listOf(
OneShotNotification.Removed(
key = A_ROOM_ID.value
)
)
)
}
@Test
fun `given a simple event when mapping to notification then is Append`() = testWith(notificationFactory) {
val expectedNotification = notificationUtils.givenBuildSimpleInvitationNotificationFor(A_SIMPLE_EVENT)
val roomInvitation = listOf(ProcessedEvent(ProcessedEvent.Type.KEEP, A_SIMPLE_EVENT))
val result = roomInvitation.toNotifications()
assertThat(result).isEqualTo(
listOf(
OneShotNotification.Append(
notification = expectedNotification,
meta = OneShotNotification.Append.Meta(
key = AN_EVENT_ID.value,
summaryLine = A_SIMPLE_EVENT.description,
isNoisy = A_SIMPLE_EVENT.noisy,
timestamp = AN_INVITATION_EVENT.timestamp
)
)
)
)
}
@Test
fun `given a missing simple event when mapping to notification then is Removed`() = testWith(notificationFactory) {
val missingEventRoomInvitation = listOf(ProcessedEvent(ProcessedEvent.Type.REMOVE, A_SIMPLE_EVENT))
val result = missingEventRoomInvitation.toNotifications()
assertThat(result).isEqualTo(
listOf(
OneShotNotification.Removed(
key = AN_EVENT_ID.value
)
)
)
}
@Test
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
)
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)
assertThat(result).isEqualTo(listOf(expectedNotification))
}
@Test
fun `given a room with no events to display when mapping to notification then is Empty`() = testWith(notificationFactory) {
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)
assertThat(result).isEqualTo(
listOf(
RoomNotification.Removed(
roomId = A_ROOM_ID
)
)
)
}
@Test
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)
assertThat(result).isEqualTo(
listOf(
RoomNotification.Removed(
roomId = A_ROOM_ID
)
)
)
}
@Test
fun `given a room with redacted and non redacted message events when mapping to notification then redacted events are removed`() = testWith(
notificationFactory
) {
val roomWithRedactedMessage = mapOf(
A_ROOM_ID to listOf(
ProcessedEvent(ProcessedEvent.Type.KEEP, A_MESSAGE_EVENT.copy(isRedacted = true)),
ProcessedEvent(ProcessedEvent.Type.KEEP, A_MESSAGE_EVENT.copy(eventId = EventId("not-redacted")))
)
)
val withRedactedRemoved = listOf(A_MESSAGE_EVENT.copy(eventId = EventId("not-redacted")))
val expectedNotification = roomGroupMessageCreator.givenCreatesRoomMessageFor(
A_SESSION_ID,
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)
assertThat(result).isEqualTo(listOf(expectedNotification))
}
}
fun <T> testWith(receiver: T, block: T.() -> Unit) {
receiver.block()
}

View File

@@ -0,0 +1,227 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.push.impl.notifications
import android.app.Notification
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
import io.element.android.libraries.push.impl.notifications.fake.FakeNotificationDisplayer
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 org.junit.Test
private const val MY_USER_DISPLAY_NAME = "display-name"
private const val MY_USER_AVATAR_URL = "avatar-url"
private const val USE_COMPLETE_NOTIFICATION_FORMAT = true
private val AN_EVENT_LIST = listOf<ProcessedEvent<NotifiableEvent>>()
private val A_PROCESSED_EVENTS = GroupedNotificationEvents(emptyMap(), emptyList(), emptyList())
private val A_SUMMARY_NOTIFICATION = SummaryNotification.Update(mockk())
private val A_REMOVE_SUMMARY_NOTIFICATION = SummaryNotification.Removed
private val A_NOTIFICATION = mockk<Notification>()
private val MESSAGE_META = RoomNotification.Message.Meta(
summaryLine = "ignored", messageCount = 1, latestTimestamp = -1, roomId = A_ROOM_ID, shouldBing = false
)
private val ONE_SHOT_META = OneShotNotification.Append.Meta(key = "ignored", summaryLine = "ignored", isNoisy = false, timestamp = -1)
class NotificationRendererTest {
private val notificationDisplayer = FakeNotificationDisplayer()
private val notificationFactory = FakeNotificationFactory()
private val notificationIdProvider = NotificationIdProvider()
private val notificationRenderer = NotificationRenderer(
notificationIdProvider = notificationIdProvider,
notificationDisplayer = notificationDisplayer.instance,
notificationFactory = notificationFactory.instance,
)
@Test
fun `given no notifications when rendering then cancels summary notification`() {
givenNoNotifications()
renderEventsAsNotifications()
notificationDisplayer.verifySummaryCancelled()
notificationDisplayer.verifyNoOtherInteractions()
}
@Test
fun `given last room message group notification is removed when rendering then remove the summary and then remove message notification`() {
givenNotifications(roomNotifications = listOf(RoomNotification.Removed(A_ROOM_ID)), summaryNotification = A_REMOVE_SUMMARY_NOTIFICATION)
renderEventsAsNotifications()
notificationDisplayer.verifyInOrder {
cancelNotificationMessage(tag = null, notificationIdProvider.getSummaryNotificationId(A_SESSION_ID))
cancelNotificationMessage(tag = A_ROOM_ID.value, notificationIdProvider.getRoomMessagesNotificationId(A_SESSION_ID))
}
}
@Test
fun `given a room message group notification is removed when rendering then remove the message notification and update summary`() {
givenNotifications(roomNotifications = listOf(RoomNotification.Removed(A_ROOM_ID)))
renderEventsAsNotifications()
notificationDisplayer.verifyInOrder {
cancelNotificationMessage(tag = A_ROOM_ID.value, notificationIdProvider.getRoomMessagesNotificationId(A_SESSION_ID))
showNotificationMessage(tag = null, notificationIdProvider.getSummaryNotificationId(A_SESSION_ID), A_SUMMARY_NOTIFICATION.notification)
}
}
@Test
fun `given a room message group notification is added when rendering then show the message notification and update summary`() {
givenNotifications(
roomNotifications = listOf(
RoomNotification.Message(
A_NOTIFICATION,
MESSAGE_META
)
)
)
renderEventsAsNotifications()
notificationDisplayer.verifyInOrder {
showNotificationMessage(tag = A_ROOM_ID.value, notificationIdProvider.getRoomMessagesNotificationId(A_SESSION_ID), A_NOTIFICATION)
showNotificationMessage(tag = null, notificationIdProvider.getSummaryNotificationId(A_SESSION_ID), A_SUMMARY_NOTIFICATION.notification)
}
}
@Test
fun `given last simple notification is removed when rendering then remove the summary and then remove simple notification`() {
givenNotifications(simpleNotifications = listOf(OneShotNotification.Removed(AN_EVENT_ID.value)), summaryNotification = A_REMOVE_SUMMARY_NOTIFICATION)
renderEventsAsNotifications()
notificationDisplayer.verifyInOrder {
cancelNotificationMessage(tag = null, notificationIdProvider.getSummaryNotificationId(A_SESSION_ID))
cancelNotificationMessage(tag = AN_EVENT_ID.value, notificationIdProvider.getRoomEventNotificationId(A_SESSION_ID))
}
}
@Test
fun `given a simple notification is removed when rendering then remove the simple notification and update summary`() {
givenNotifications(simpleNotifications = listOf(OneShotNotification.Removed(AN_EVENT_ID.value)))
renderEventsAsNotifications()
notificationDisplayer.verifyInOrder {
cancelNotificationMessage(tag = AN_EVENT_ID.value, notificationIdProvider.getRoomEventNotificationId(A_SESSION_ID))
showNotificationMessage(tag = null, notificationIdProvider.getSummaryNotificationId(A_SESSION_ID), A_SUMMARY_NOTIFICATION.notification)
}
}
@Test
fun `given a simple notification is added when rendering then show the simple notification and update summary`() {
givenNotifications(
simpleNotifications = listOf(
OneShotNotification.Append(
A_NOTIFICATION,
ONE_SHOT_META.copy(key = AN_EVENT_ID.value)
)
)
)
renderEventsAsNotifications()
notificationDisplayer.verifyInOrder {
showNotificationMessage(tag = AN_EVENT_ID.value, notificationIdProvider.getRoomEventNotificationId(A_SESSION_ID), A_NOTIFICATION)
showNotificationMessage(tag = null, notificationIdProvider.getSummaryNotificationId(A_SESSION_ID), A_SUMMARY_NOTIFICATION.notification)
}
}
@Test
fun `given last invitation notification is removed when rendering then remove the summary and then remove invitation notification`() {
givenNotifications(invitationNotifications = listOf(OneShotNotification.Removed(A_ROOM_ID.value)), summaryNotification = A_REMOVE_SUMMARY_NOTIFICATION)
renderEventsAsNotifications()
notificationDisplayer.verifyInOrder {
cancelNotificationMessage(tag = null, notificationIdProvider.getSummaryNotificationId(A_SESSION_ID))
cancelNotificationMessage(tag = A_ROOM_ID.value, notificationIdProvider.getRoomInvitationNotificationId(A_SESSION_ID))
}
}
@Test
fun `given an invitation notification is removed when rendering then remove the invitation notification and update summary`() {
givenNotifications(invitationNotifications = listOf(OneShotNotification.Removed(A_ROOM_ID.value)))
renderEventsAsNotifications()
notificationDisplayer.verifyInOrder {
cancelNotificationMessage(tag = A_ROOM_ID.value, notificationIdProvider.getRoomInvitationNotificationId(A_SESSION_ID))
showNotificationMessage(tag = null, notificationIdProvider.getSummaryNotificationId(A_SESSION_ID), A_SUMMARY_NOTIFICATION.notification)
}
}
@Test
fun `given an invitation notification is added when rendering then show the invitation notification and update summary`() {
givenNotifications(
simpleNotifications = listOf(
OneShotNotification.Append(
A_NOTIFICATION,
ONE_SHOT_META.copy(key = A_ROOM_ID.value)
)
)
)
renderEventsAsNotifications()
notificationDisplayer.verifyInOrder {
showNotificationMessage(tag = A_ROOM_ID.value, notificationIdProvider.getRoomEventNotificationId(A_SESSION_ID), A_NOTIFICATION)
showNotificationMessage(tag = null, notificationIdProvider.getSummaryNotificationId(A_SESSION_ID), A_SUMMARY_NOTIFICATION.notification)
}
}
private fun renderEventsAsNotifications() {
notificationRenderer.render(
sessionId = A_SESSION_ID,
myUserDisplayName = MY_USER_DISPLAY_NAME,
myUserAvatarUrl = MY_USER_AVATAR_URL,
useCompleteNotificationFormat = USE_COMPLETE_NOTIFICATION_FORMAT,
eventsToProcess = AN_EVENT_LIST
)
}
private fun givenNoNotifications() {
givenNotifications(emptyList(), emptyList(), emptyList(), USE_COMPLETE_NOTIFICATION_FORMAT, A_REMOVE_SUMMARY_NOTIFICATION)
}
private fun givenNotifications(
roomNotifications: List<RoomNotification> = emptyList(),
invitationNotifications: List<OneShotNotification> = emptyList(),
simpleNotifications: List<OneShotNotification> = emptyList(),
useCompleteNotificationFormat: Boolean = USE_COMPLETE_NOTIFICATION_FORMAT,
summaryNotification: SummaryNotification = A_SUMMARY_NOTIFICATION
) {
notificationFactory.givenNotificationsFor(
groupedEvents = A_PROCESSED_EVENTS,
sessionId = A_SESSION_ID,
myUserDisplayName = MY_USER_DISPLAY_NAME,
myUserAvatarUrl = MY_USER_AVATAR_URL,
useCompleteNotificationFormat = useCompleteNotificationFormat,
roomNotifications = roomNotifications,
invitationNotifications = invitationNotifications,
simpleNotifications = simpleNotifications,
summaryNotification = summaryNotification
)
}
}

View File

@@ -0,0 +1,42 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.push.impl.notifications.fake
import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.push.impl.notifications.NotificationDisplayer
import io.element.android.libraries.push.impl.notifications.NotificationIdProvider
import io.mockk.confirmVerified
import io.mockk.mockk
import io.mockk.verify
import io.mockk.verifyOrder
class FakeNotificationDisplayer {
val instance = mockk<NotificationDisplayer>(relaxed = true)
fun verifySummaryCancelled() {
verify { instance.cancelNotificationMessage(tag = null, NotificationIdProvider().getSummaryNotificationId(A_SESSION_ID)) }
}
fun verifyNoOtherInteractions() {
confirmVerified(instance)
}
fun verifyInOrder(verifyBlock: NotificationDisplayer.() -> Unit) {
verifyOrder { verifyBlock(instance) }
verifyNoOtherInteractions()
}
}

View File

@@ -0,0 +1,54 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.push.impl.notifications.fake
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.push.impl.notifications.*
import io.mockk.every
import io.mockk.mockk
class FakeNotificationFactory {
val instance = mockk<NotificationFactory>()
fun givenNotificationsFor(
groupedEvents: GroupedNotificationEvents,
sessionId: SessionId,
myUserDisplayName: String,
myUserAvatarUrl: String?,
useCompleteNotificationFormat: Boolean,
roomNotifications: List<RoomNotification>,
invitationNotifications: List<OneShotNotification>,
simpleNotifications: List<OneShotNotification>,
summaryNotification: SummaryNotification
) {
with(instance) {
every { groupedEvents.roomEvents.toNotifications(sessionId, myUserDisplayName, myUserAvatarUrl) } returns roomNotifications
every { groupedEvents.invitationEvents.toNotifications() } returns invitationNotifications
every { groupedEvents.simpleEvents.toNotifications() } returns simpleNotifications
every {
createSummaryNotification(
sessionId,
roomNotifications,
invitationNotifications,
simpleNotifications,
useCompleteNotificationFormat
)
} returns summaryNotification
}
}
}

View File

@@ -0,0 +1,40 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.push.impl.notifications.fake
import android.app.Notification
import io.element.android.libraries.push.impl.notifications.NotificationUtils
import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent
import io.element.android.libraries.push.impl.notifications.model.SimpleNotifiableEvent
import io.mockk.every
import io.mockk.mockk
class FakeNotificationUtils {
val instance = mockk<NotificationUtils>()
fun givenBuildRoomInvitationNotificationFor(event: InviteNotifiableEvent): Notification {
val mockNotification = mockk<Notification>()
every { instance.buildRoomInvitationNotification(event) } returns mockNotification
return mockNotification
}
fun givenBuildSimpleInvitationNotificationFor(event: SimpleNotifiableEvent): Notification {
val mockNotification = mockk<Notification>()
every { instance.buildSimpleEventNotification(event) } returns mockNotification
return mockNotification
}
}

View File

@@ -0,0 +1,34 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.push.impl.notifications.fake
import io.element.android.libraries.push.impl.notifications.OutdatedEventDetector
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
import io.mockk.every
import io.mockk.mockk
class FakeOutdatedEventDetector {
val instance = mockk<OutdatedEventDetector>()
fun givenEventIsOutOfDate(notifiableEvent: NotifiableEvent) {
every { instance.isMessageOutdated(notifiableEvent) } returns true
}
fun givenEventIsInDate(notifiableEvent: NotifiableEvent) {
every { instance.isMessageOutdated(notifiableEvent) } returns false
}
}

View File

@@ -0,0 +1,42 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package 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.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.mockk
class FakeRoomGroupMessageCreator {
val instance = mockk<RoomGroupMessageCreator>()
fun givenCreatesRoomMessageFor(
sessionId: SessionId,
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
return mockMessage
}
}

View File

@@ -0,0 +1,25 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.push.impl.notifications.fake
import io.element.android.libraries.push.impl.notifications.SummaryGroupMessageCreator
import io.mockk.mockk
class FakeSummaryGroupMessageCreator {
val instance = mockk<SummaryGroupMessageCreator>()
}

View File

@@ -0,0 +1,96 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.push.impl.notifications.fixtures
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.ThreadId
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
import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent
import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent
import io.element.android.libraries.push.impl.notifications.model.SimpleNotifiableEvent
fun aSimpleNotifiableEvent(
sessionId: SessionId = A_SESSION_ID,
roomId: RoomId = A_ROOM_ID,
eventId: EventId = AN_EVENT_ID,
type: String? = null,
isRedacted: Boolean = false,
canBeReplaced: Boolean = false,
editedEventId: EventId? = null
) = SimpleNotifiableEvent(
sessionId = sessionId,
roomId = roomId,
eventId = eventId,
editedEventId = editedEventId,
noisy = false,
title = "title",
description = "description",
type = type,
timestamp = 0,
soundName = null,
canBeReplaced = canBeReplaced,
isRedacted = isRedacted
)
fun anInviteNotifiableEvent(
sessionId: SessionId = A_SESSION_ID,
roomId: RoomId = A_ROOM_ID,
eventId: EventId = AN_EVENT_ID,
isRedacted: Boolean = false
) = InviteNotifiableEvent(
sessionId = sessionId,
eventId = eventId,
roomId = roomId,
roomName = "a room name",
editedEventId = null,
noisy = false,
title = "title",
description = "description",
type = null,
timestamp = 0,
soundName = null,
canBeReplaced = false,
isRedacted = isRedacted
)
fun aNotifiableMessageEvent(
sessionId: SessionId = A_SESSION_ID,
roomId: RoomId = A_ROOM_ID,
eventId: EventId = AN_EVENT_ID,
threadId: ThreadId? = null,
isRedacted: Boolean = false
) = NotifiableMessageEvent(
sessionId = sessionId,
eventId = eventId,
editedEventId = null,
noisy = false,
timestamp = 0,
senderName = "sender-name",
senderId = "sending-id",
body = "message-body",
roomId = roomId,
threadId = threadId,
roomName = "room-name",
roomIsDirect = false,
canBeReplaced = false,
isRedacted = isRedacted,
imageUriString = null
)

View File

@@ -0,0 +1,30 @@
/*
* Copyright (c) 2022 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.
*/
// TODO: Remove once https://youtrack.jetbrains.com/issue/KTIJ-19369 is fixed
@Suppress("DSL_SCOPE_VIOLATION")
plugins {
id("io.element.android-library")
}
android {
namespace = "io.element.android.services.appnavstate.test"
}
dependencies {
api(projects.libraries.matrix.api)
api(projects.services.appnavstate.api)
}

View File

@@ -0,0 +1,44 @@
/*
* 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.services.appnavstate.test
import io.element.android.libraries.matrix.api.core.*
import io.element.android.services.appnavstate.api.AppNavigationState
fun anAppNavigationState(
sessionId: SessionId? = null,
spaceId: SpaceId? = MAIN_SPACE,
roomId: RoomId? = null,
threadId: ThreadId? = null,
): AppNavigationState {
if (sessionId == null) {
return AppNavigationState.Root
}
val session = AppNavigationState.Session(sessionId)
if (spaceId == null) {
return session
}
val space = AppNavigationState.Space(spaceId, session)
if (roomId == null) {
return space
}
val room = AppNavigationState.Room(roomId, space)
if (threadId == null) {
return room
}
return AppNavigationState.Thread(threadId, room)
}