diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index d2d648f145..c77fba93e1 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -44,14 +44,14 @@
-
-
+
-
+
diff --git a/app/src/main/kotlin/io/element/android/x/ElementXApplication.kt b/app/src/main/kotlin/io/element/android/x/ElementXApplication.kt
index ba8fe290e2..ec3259fb7c 100644
--- a/app/src/main/kotlin/io/element/android/x/ElementXApplication.kt
+++ b/app/src/main/kotlin/io/element/android/x/ElementXApplication.kt
@@ -18,8 +18,8 @@ package io.element.android.x
import android.app.Application
import androidx.startup.AppInitializer
-import io.element.android.x.di.AppComponent
import io.element.android.libraries.di.DaggerComponentOwner
+import io.element.android.x.di.AppComponent
import io.element.android.x.di.DaggerAppComponent
import io.element.android.x.info.logApplicationInfo
import io.element.android.x.initializer.CrashInitializer
diff --git a/app/src/main/kotlin/io/element/android/x/intent/IntentProviderImpl.kt b/app/src/main/kotlin/io/element/android/x/intent/IntentProviderImpl.kt
index e777b08906..88a86b9467 100644
--- a/app/src/main/kotlin/io/element/android/x/intent/IntentProviderImpl.kt
+++ b/app/src/main/kotlin/io/element/android/x/intent/IntentProviderImpl.kt
@@ -35,14 +35,21 @@ class IntentProviderImpl @Inject constructor(
@ApplicationContext private val context: Context,
private val deepLinkCreator: DeepLinkCreator,
) : IntentProvider {
- override fun getViewIntent(
+ override fun getViewRoomIntent(
sessionId: SessionId,
roomId: RoomId?,
threadId: ThreadId?,
): Intent {
return Intent(context, MainActivity::class.java).apply {
action = Intent.ACTION_VIEW
- data = deepLinkCreator.create(sessionId, roomId, threadId).toUri()
+ data = deepLinkCreator.room(sessionId, roomId, threadId).toUri()
+ }
+ }
+
+ override fun getInviteListIntent(sessionId: SessionId): Intent {
+ return Intent(context, MainActivity::class.java).apply {
+ action = Intent.ACTION_VIEW
+ data = deepLinkCreator.inviteList(sessionId).toUri()
}
}
}
diff --git a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt
index f10d3e0f31..bc73efde71 100644
--- a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt
+++ b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt
@@ -18,9 +18,7 @@ package io.element.android.appnav
import android.os.Parcelable
import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
-import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
@@ -30,7 +28,6 @@ import com.bumble.appyx.core.composable.Children
import com.bumble.appyx.core.lifecycle.subscribe
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
-import com.bumble.appyx.core.node.node
import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import com.bumble.appyx.navmodel.backstack.BackStack
@@ -58,6 +55,7 @@ import io.element.android.libraries.architecture.animation.rememberDefaultTransi
import io.element.android.libraries.architecture.bindings
import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.architecture.inputs
+import io.element.android.libraries.deeplink.DeeplinkData
import io.element.android.libraries.designsystem.utils.SnackbarDispatcher
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.matrix.api.MatrixClient
@@ -65,6 +63,7 @@ import io.element.android.libraries.matrix.api.core.MAIN_SPACE
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.sync.SyncState
import io.element.android.libraries.matrix.ui.di.MatrixUIBindings
+import io.element.android.libraries.push.api.notifications.NotificationDrawerManager
import io.element.android.services.analytics.api.AnalyticsService
import io.element.android.services.appnavstate.api.AppNavigationStateService
import kotlinx.coroutines.CoroutineScope
@@ -89,6 +88,7 @@ class LoggedInFlowNode @AssistedInject constructor(
private val analyticsService: AnalyticsService,
private val coroutineScope: CoroutineScope,
private val networkMonitor: NetworkMonitor,
+ private val notificationDrawerManager: NotificationDrawerManager,
snackbarDispatcher: SnackbarDispatcher,
) : BackstackNode(
backstack = BackStack(
@@ -340,4 +340,13 @@ class LoggedInFlowNode @AssistedInject constructor(
PermanentChild(navTarget = NavTarget.Permanent)
}
}
+
+ internal suspend fun attachRoom(deeplinkData: DeeplinkData.Room) {
+ backstack.push(NavTarget.Room(deeplinkData.roomId))
+ }
+
+ internal suspend fun attachInviteList(deeplinkData: DeeplinkData.InviteList) {
+ notificationDrawerManager.clearMembershipNotificationForSession(deeplinkData.sessionId)
+ backstack.push(NavTarget.InviteList)
+ }
}
diff --git a/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt
index 12e54b2ea5..8803d574d9 100644
--- a/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt
+++ b/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt
@@ -253,13 +253,10 @@ class RootFlowNode @AssistedInject constructor(
Timber.d("Navigating to $deeplinkData")
attachSession(deeplinkData.sessionId)
.apply {
- val roomId = deeplinkData.roomId
- if (roomId == null) {
- // In case room is not provided, ensure the app navigate back to the room list
- attachRoot()
- } else {
- attachRoom(roomId)
- // TODO .attachThread(deeplinkData.threadId)
+ when (deeplinkData) {
+ is DeeplinkData.Root -> attachRoot()
+ is DeeplinkData.Room -> attachRoom(deeplinkData)
+ is DeeplinkData.InviteList -> attachInviteList(deeplinkData)
}
}
}
diff --git a/appnav/src/main/kotlin/io/element/android/appnav/intent/IntentResolver.kt b/appnav/src/main/kotlin/io/element/android/appnav/intent/IntentResolver.kt
index b567395c1e..6a3d8ff9dd 100644
--- a/appnav/src/main/kotlin/io/element/android/appnav/intent/IntentResolver.kt
+++ b/appnav/src/main/kotlin/io/element/android/appnav/intent/IntentResolver.kt
@@ -21,6 +21,8 @@ import io.element.android.features.login.api.oidc.OidcAction
import io.element.android.features.login.api.oidc.OidcIntentResolver
import io.element.android.libraries.deeplink.DeeplinkData
import io.element.android.libraries.deeplink.DeeplinkParser
+import io.element.android.libraries.matrix.api.core.RoomId
+import io.element.android.libraries.matrix.api.core.SessionId
import timber.log.Timber
import javax.inject.Inject
diff --git a/features/invitelist/impl/build.gradle.kts b/features/invitelist/impl/build.gradle.kts
index 6ff90d676d..3f8f1a44ed 100644
--- a/features/invitelist/impl/build.gradle.kts
+++ b/features/invitelist/impl/build.gradle.kts
@@ -43,6 +43,7 @@ dependencies {
implementation(projects.libraries.designsystem)
implementation(projects.libraries.uiStrings)
implementation(projects.services.analytics.api)
+ implementation(projects.libraries.push.api)
testImplementation(libs.test.junit)
testImplementation(libs.coroutines.test)
@@ -50,6 +51,7 @@ dependencies {
testImplementation(libs.test.truth)
testImplementation(libs.test.turbine)
testImplementation(projects.libraries.matrix.test)
+ testImplementation(projects.libraries.push.test)
testImplementation(projects.features.invitelist.test)
testImplementation(projects.features.analytics.test)
diff --git a/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/InviteListPresenter.kt b/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/InviteListPresenter.kt
index a87b2b0ebd..735b4a79a2 100644
--- a/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/InviteListPresenter.kt
+++ b/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/InviteListPresenter.kt
@@ -37,6 +37,7 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.RoomSummary
+import io.element.android.libraries.push.api.notifications.NotificationDrawerManager
import io.element.android.services.analytics.api.AnalyticsService
import io.element.android.services.analytics.api.extensions.toAnalyticsJoinedRoom
import kotlinx.collections.immutable.toPersistentList
@@ -49,6 +50,7 @@ class InviteListPresenter @Inject constructor(
private val client: MatrixClient,
private val store: SeenInvitesStore,
private val analyticsService: AnalyticsService,
+ private val notificationDrawerManager: NotificationDrawerManager,
) : Presenter {
@Composable
@@ -138,6 +140,7 @@ class InviteListPresenter @Inject constructor(
suspend {
client.getRoom(roomId)?.use {
it.acceptInvitation().getOrThrow()
+ notificationDrawerManager.clearMembershipNotificationForRoom(client.sessionId, roomId)
analyticsService.capture(it.toAnalyticsJoinedRoom(JoinedRoom.Trigger.Invite))
}
roomId
@@ -148,7 +151,9 @@ class InviteListPresenter @Inject constructor(
suspend {
client.getRoom(roomId)?.use {
it.rejectInvitation().getOrThrow()
- } ?: Unit
+ notificationDrawerManager.clearMembershipNotificationForRoom(client.sessionId, roomId)
+ }
+ Unit
}.runCatchingUpdatingState(declinedAction)
}
diff --git a/features/invitelist/impl/src/test/kotlin/io/element/android/features/invitelist/impl/InviteListPresenterTests.kt b/features/invitelist/impl/src/test/kotlin/io/element/android/features/invitelist/impl/InviteListPresenterTests.kt
index e179ed3878..503e7ad0d7 100644
--- a/features/invitelist/impl/src/test/kotlin/io/element/android/features/invitelist/impl/InviteListPresenterTests.kt
+++ b/features/invitelist/impl/src/test/kotlin/io/element/android/features/invitelist/impl/InviteListPresenterTests.kt
@@ -21,10 +21,12 @@ import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
import com.google.common.truth.Truth
import io.element.android.features.analytics.test.FakeAnalyticsService
+import io.element.android.features.invitelist.api.SeenInvitesStore
import io.element.android.features.invitelist.test.FakeSeenInvitesStore
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
+import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.RoomMembershipState
@@ -39,6 +41,9 @@ import io.element.android.libraries.matrix.test.A_USER_NAME
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import io.element.android.libraries.matrix.test.room.FakeRoomSummaryDataSource
+import io.element.android.libraries.push.api.notifications.NotificationDrawerManager
+import io.element.android.libraries.push.test.notifications.FakeNotificationDrawerManager
+import io.element.android.services.analytics.api.AnalyticsService
import kotlinx.coroutines.test.runTest
import org.junit.Test
@@ -47,12 +52,8 @@ class InviteListPresenterTests {
@Test
fun `present - starts empty, adds invites when received`() = runTest {
val roomSummaryDataSource = FakeRoomSummaryDataSource()
- val presenter = InviteListPresenter(
- FakeMatrixClient(
- roomSummaryDataSource = roomSummaryDataSource,
- ),
- FakeSeenInvitesStore(),
- FakeAnalyticsService(),
+ val presenter = createPresenter(
+ FakeMatrixClient(roomSummaryDataSource = roomSummaryDataSource)
)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
@@ -72,12 +73,8 @@ class InviteListPresenterTests {
@Test
fun `present - uses user ID and avatar for direct invites`() = runTest {
val roomSummaryDataSource = FakeRoomSummaryDataSource().withDirectChatInvitation()
- val presenter = InviteListPresenter(
- FakeMatrixClient(
- roomSummaryDataSource = roomSummaryDataSource,
- ),
- FakeSeenInvitesStore(),
- FakeAnalyticsService(),
+ val presenter = createPresenter(
+ FakeMatrixClient(roomSummaryDataSource = roomSummaryDataSource)
)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
@@ -102,12 +99,8 @@ class InviteListPresenterTests {
@Test
fun `present - includes sender details for room invites`() = runTest {
val roomSummaryDataSource = FakeRoomSummaryDataSource().withRoomInvitation()
- val presenter = InviteListPresenter(
- FakeMatrixClient(
- roomSummaryDataSource = roomSummaryDataSource,
- ),
- FakeSeenInvitesStore(),
- FakeAnalyticsService(),
+ val presenter = createPresenter(
+ FakeMatrixClient(roomSummaryDataSource = roomSummaryDataSource)
)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
@@ -136,6 +129,7 @@ class InviteListPresenterTests {
),
FakeSeenInvitesStore(),
FakeAnalyticsService(),
+ FakeNotificationDrawerManager()
)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
@@ -155,12 +149,8 @@ class InviteListPresenterTests {
@Test
fun `present - shows confirm dialog for declining room invites`() = runTest {
val roomSummaryDataSource = FakeRoomSummaryDataSource().withRoomInvitation()
- val presenter = InviteListPresenter(
- FakeMatrixClient(
- roomSummaryDataSource = roomSummaryDataSource,
- ),
- FakeSeenInvitesStore(),
- FakeAnalyticsService(),
+ val presenter = createPresenter(
+ FakeMatrixClient(roomSummaryDataSource = roomSummaryDataSource)
)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
@@ -180,12 +170,8 @@ class InviteListPresenterTests {
@Test
fun `present - hides confirm dialog when cancelling`() = runTest {
val roomSummaryDataSource = FakeRoomSummaryDataSource().withRoomInvitation()
- val presenter = InviteListPresenter(
- FakeMatrixClient(
- roomSummaryDataSource = roomSummaryDataSource,
- ),
- FakeSeenInvitesStore(),
- FakeAnalyticsService(),
+ val presenter = createPresenter(
+ FakeMatrixClient(roomSummaryDataSource = roomSummaryDataSource)
)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
@@ -205,11 +191,12 @@ class InviteListPresenterTests {
@Test
fun `present - declines invite after confirming`() = runTest {
val roomSummaryDataSource = FakeRoomSummaryDataSource().withRoomInvitation()
+ val fakeNotificationDrawerManager = FakeNotificationDrawerManager()
val client = FakeMatrixClient(
roomSummaryDataSource = roomSummaryDataSource,
)
val room = FakeMatrixRoom()
- val presenter = InviteListPresenter(client, FakeSeenInvitesStore(), FakeAnalyticsService())
+ val presenter = createPresenter(client = client, notificationDrawerManager = fakeNotificationDrawerManager)
client.givenGetRoomResult(A_ROOM_ID, room)
moleculeFlow(RecompositionClock.Immediate) {
@@ -225,6 +212,7 @@ class InviteListPresenterTests {
skipItems(2)
Truth.assertThat(room.isInviteRejected).isTrue()
+ Truth.assertThat(fakeNotificationDrawerManager.getClearMembershipNotificationForRoomCount(client.sessionId, A_ROOM_ID)).isEqualTo(1)
}
}
@@ -235,7 +223,7 @@ class InviteListPresenterTests {
roomSummaryDataSource = roomSummaryDataSource,
)
val room = FakeMatrixRoom()
- val presenter = InviteListPresenter(client, FakeSeenInvitesStore(), FakeAnalyticsService())
+ val presenter = createPresenter(client)
val ex = Throwable("Ruh roh!")
room.givenRejectInviteResult(Result.failure(ex))
client.givenGetRoomResult(A_ROOM_ID, room)
@@ -266,7 +254,7 @@ class InviteListPresenterTests {
roomSummaryDataSource = roomSummaryDataSource,
)
val room = FakeMatrixRoom()
- val presenter = InviteListPresenter(client, FakeSeenInvitesStore(), FakeAnalyticsService())
+ val presenter = createPresenter(client)
val ex = Throwable("Ruh roh!")
room.givenRejectInviteResult(Result.failure(ex))
client.givenGetRoomResult(A_ROOM_ID, room)
@@ -294,11 +282,12 @@ class InviteListPresenterTests {
@Test
fun `present - accepts invites and sets state on success`() = runTest {
val roomSummaryDataSource = FakeRoomSummaryDataSource().withRoomInvitation()
+ val fakeNotificationDrawerManager = FakeNotificationDrawerManager()
val client = FakeMatrixClient(
roomSummaryDataSource = roomSummaryDataSource,
)
val room = FakeMatrixRoom()
- val presenter = InviteListPresenter(client, FakeSeenInvitesStore(), FakeAnalyticsService())
+ val presenter = createPresenter(client = client, notificationDrawerManager = fakeNotificationDrawerManager)
client.givenGetRoomResult(A_ROOM_ID, room)
moleculeFlow(RecompositionClock.Immediate) {
@@ -311,6 +300,7 @@ class InviteListPresenterTests {
Truth.assertThat(room.isInviteAccepted).isTrue()
Truth.assertThat(newState.acceptedAction).isEqualTo(Async.Success(A_ROOM_ID))
+ Truth.assertThat(fakeNotificationDrawerManager.getClearMembershipNotificationForRoomCount(client.sessionId, A_ROOM_ID)).isEqualTo(1)
}
}
@@ -321,7 +311,7 @@ class InviteListPresenterTests {
roomSummaryDataSource = roomSummaryDataSource,
)
val room = FakeMatrixRoom()
- val presenter = InviteListPresenter(client, FakeSeenInvitesStore(), FakeAnalyticsService())
+ val presenter = createPresenter(client)
val ex = Throwable("Ruh roh!")
room.givenAcceptInviteResult(Result.failure(ex))
client.givenGetRoomResult(A_ROOM_ID, room)
@@ -346,7 +336,7 @@ class InviteListPresenterTests {
roomSummaryDataSource = roomSummaryDataSource,
)
val room = FakeMatrixRoom()
- val presenter = InviteListPresenter(client, FakeSeenInvitesStore(), FakeAnalyticsService())
+ val presenter = createPresenter(client)
val ex = Throwable("Ruh roh!")
room.givenAcceptInviteResult(Result.failure(ex))
client.givenGetRoomResult(A_ROOM_ID, room)
@@ -376,6 +366,7 @@ class InviteListPresenterTests {
),
store,
FakeAnalyticsService(),
+ FakeNotificationDrawerManager()
)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
@@ -413,6 +404,7 @@ class InviteListPresenterTests {
),
store,
FakeAnalyticsService(),
+ FakeNotificationDrawerManager()
)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
@@ -500,4 +492,16 @@ class InviteListPresenterTests {
unreadNotificationCount = 0,
)
)
+
+ private fun createPresenter(
+ client: MatrixClient,
+ seenInvitesStore: SeenInvitesStore = FakeSeenInvitesStore(),
+ fakeAnalyticsService: AnalyticsService = FakeAnalyticsService(),
+ notificationDrawerManager: NotificationDrawerManager = FakeNotificationDrawerManager()
+ ) = InviteListPresenter(
+ client,
+ seenInvitesStore,
+ fakeAnalyticsService,
+ notificationDrawerManager
+ )
}
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 9038c7a72a..a5c98b7f3c 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -152,7 +152,6 @@ sqldelight-coroutines = { module = "com.squareup.sqldelight:coroutines-extension
sqlcipher = "net.zetetic:android-database-sqlcipher:4.5.4"
sqlite = "androidx.sqlite:sqlite:2.3.1"
unifiedpush = "com.github.UnifiedPush:android-connector:2.1.1"
-gujun_span = "me.gujun.android:span:1.7"
otaliastudios_transcoder = "com.otaliastudios:transcoder:0.10.5"
vanniktech_blurhash = "com.vanniktech:blurhash:0.1.0"
vanniktech_emoji = "com.vanniktech:emoji-google:0.16.0"
diff --git a/libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/Constants.kt b/libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/Constants.kt
index df26ef2fa0..d16d31fb82 100644
--- a/libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/Constants.kt
+++ b/libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/Constants.kt
@@ -18,3 +18,7 @@ package io.element.android.libraries.deeplink
internal const val SCHEME = "elementx"
internal const val HOST = "open"
+
+object DeepLinkPaths {
+ const val INVITE_LIST = "invites"
+}
diff --git a/libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/DeepLinkCreator.kt b/libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/DeepLinkCreator.kt
index 71aa7ebddd..0cf2a7fca8 100644
--- a/libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/DeepLinkCreator.kt
+++ b/libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/DeepLinkCreator.kt
@@ -22,7 +22,7 @@ import io.element.android.libraries.matrix.api.core.ThreadId
import javax.inject.Inject
class DeepLinkCreator @Inject constructor() {
- fun create(sessionId: SessionId, roomId: RoomId?, threadId: ThreadId?): String {
+ fun room(sessionId: SessionId, roomId: RoomId?, threadId: ThreadId?): String {
return buildString {
append("$SCHEME://$HOST/")
append(sessionId.value)
@@ -36,4 +36,13 @@ class DeepLinkCreator @Inject constructor() {
}
}
}
+
+ fun inviteList(sessionId: SessionId): String {
+ return buildString {
+ append("$SCHEME://$HOST/")
+ append(sessionId.value)
+ append("/")
+ append(DeepLinkPaths.INVITE_LIST)
+ }
+ }
}
diff --git a/libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/DeeplinkData.kt b/libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/DeeplinkData.kt
index d393a37c16..aa373411c7 100644
--- a/libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/DeeplinkData.kt
+++ b/libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/DeeplinkData.kt
@@ -20,8 +20,16 @@ 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
-data class DeeplinkData(
- val sessionId: SessionId,
- val roomId: RoomId? = null,
- val threadId: ThreadId? = null,
-)
+sealed interface DeeplinkData {
+ /** Session id is common for all deep links. */
+ val sessionId: SessionId
+
+ /** The target is the root of the app, with the given [sessionId]. */
+ data class Root(override val sessionId: SessionId) : DeeplinkData
+
+ /** The target is a room, with the given [sessionId], [roomId] and optionally a [threadId]. */
+ data class Room(override val sessionId: SessionId, val roomId: RoomId, val threadId: ThreadId?) : DeeplinkData
+
+ /** The target is the invites list, with the given [sessionId]. */
+ data class InviteList(override val sessionId: SessionId) : DeeplinkData
+}
diff --git a/libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/DeeplinkParser.kt b/libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/DeeplinkParser.kt
index 616cd58245..7d2c8af135 100644
--- a/libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/DeeplinkParser.kt
+++ b/libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/DeeplinkParser.kt
@@ -18,6 +18,7 @@ package io.element.android.libraries.deeplink
import android.content.Intent
import android.net.Uri
+import io.element.android.libraries.core.data.tryOrNull
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
@@ -36,12 +37,21 @@ class DeeplinkParser @Inject constructor() {
if (host != HOST) return null
val pathBits = path.orEmpty().split("/").drop(1)
val sessionId = pathBits.elementAtOrNull(0)?.let(::SessionId) ?: return null
- val roomId = pathBits.elementAtOrNull(1)?.let(::RoomId)
- val threadId = pathBits.elementAtOrNull(2)?.let(::ThreadId)
- return DeeplinkData(
- sessionId = sessionId,
- roomId = roomId,
- threadId = threadId,
- )
+ val screenPathComponent = pathBits.elementAtOrNull(1)
+ val roomId = tryOrNull { screenPathComponent?.let(::RoomId) }
+
+ return when {
+ roomId != null -> {
+ val threadId = pathBits.elementAtOrNull(2)?.let(::ThreadId)
+ DeeplinkData.Room(sessionId, roomId, threadId)
+ }
+ screenPathComponent == DeepLinkPaths.INVITE_LIST -> {
+ DeeplinkData.InviteList(sessionId)
+ }
+ screenPathComponent == null -> {
+ DeeplinkData.Root(sessionId)
+ }
+ else -> null
+ }
}
}
diff --git a/libraries/deeplink/src/test/kotlin/io/element/android/libraries/deeplink/DeepLinkCreatorTest.kt b/libraries/deeplink/src/test/kotlin/io/element/android/libraries/deeplink/DeepLinkCreatorTest.kt
index 730bdde248..70c047f8ed 100644
--- a/libraries/deeplink/src/test/kotlin/io/element/android/libraries/deeplink/DeepLinkCreatorTest.kt
+++ b/libraries/deeplink/src/test/kotlin/io/element/android/libraries/deeplink/DeepLinkCreatorTest.kt
@@ -25,13 +25,20 @@ import org.junit.Test
class DeepLinkCreatorTest {
@Test
- fun create() {
+ fun room() {
val sut = DeepLinkCreator()
- assertThat(sut.create(A_SESSION_ID, null, null))
+ assertThat(sut.room(A_SESSION_ID, null, null))
.isEqualTo("elementx://open/@alice:server.org")
- assertThat(sut.create(A_SESSION_ID, A_ROOM_ID, null))
+ assertThat(sut.room(A_SESSION_ID, A_ROOM_ID, null))
.isEqualTo("elementx://open/@alice:server.org/!aRoomId:domain")
- assertThat(sut.create(A_SESSION_ID, A_ROOM_ID, A_THREAD_ID))
+ assertThat(sut.room(A_SESSION_ID, A_ROOM_ID, A_THREAD_ID))
.isEqualTo("elementx://open/@alice:server.org/!aRoomId:domain/\$aThreadId")
}
+
+ @Test
+ fun inviteList() {
+ val sut = DeepLinkCreator()
+ assertThat(sut.inviteList(A_SESSION_ID))
+ .isEqualTo("elementx://open/@alice:server.org/invites")
+ }
}
diff --git a/libraries/deeplink/src/test/kotlin/io/element/android/libraries/deeplink/DeeplinkParserTest.kt b/libraries/deeplink/src/test/kotlin/io/element/android/libraries/deeplink/DeeplinkParserTest.kt
index 4ebb079189..553850a4d6 100644
--- a/libraries/deeplink/src/test/kotlin/io/element/android/libraries/deeplink/DeeplinkParserTest.kt
+++ b/libraries/deeplink/src/test/kotlin/io/element/android/libraries/deeplink/DeeplinkParserTest.kt
@@ -36,6 +36,8 @@ class DeeplinkParserTest {
"elementx://open/@alice:server.org/!aRoomId:domain"
const val A_URI_WITH_ROOM_WITH_THREAD =
"elementx://open/@alice:server.org/!aRoomId:domain/\$aThreadId"
+ const val A_URI_FOR_INVITE_LIST =
+ "elementx://open/@alice:server.org/invites"
}
private val sut = DeeplinkParser()
@@ -43,11 +45,13 @@ class DeeplinkParserTest {
@Test
fun `nominal cases`() {
assertThat(sut.getFromIntent(createIntent(A_URI)))
- .isEqualTo(DeeplinkData(A_SESSION_ID, null, null))
+ .isEqualTo(DeeplinkData.Root(A_SESSION_ID))
assertThat(sut.getFromIntent(createIntent(A_URI_WITH_ROOM)))
- .isEqualTo(DeeplinkData(A_SESSION_ID, A_ROOM_ID, null))
+ .isEqualTo(DeeplinkData.Room(A_SESSION_ID, A_ROOM_ID, null))
assertThat(sut.getFromIntent(createIntent(A_URI_WITH_ROOM_WITH_THREAD)))
- .isEqualTo(DeeplinkData(A_SESSION_ID, A_ROOM_ID, A_THREAD_ID))
+ .isEqualTo(DeeplinkData.Room(A_SESSION_ID, A_ROOM_ID, A_THREAD_ID))
+ assertThat(sut.getFromIntent(createIntent(A_URI_FOR_INVITE_LIST)))
+ .isEqualTo(DeeplinkData.InviteList(A_SESSION_ID))
}
@Test
diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notification/NotificationData.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notification/NotificationData.kt
index eb6e9998ac..4ded947d56 100644
--- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notification/NotificationData.kt
+++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notification/NotificationData.kt
@@ -19,6 +19,8 @@ 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.UserId
+import io.element.android.libraries.matrix.api.room.RoomMembershipState
+import io.element.android.libraries.matrix.api.timeline.item.event.MessageType
data class NotificationData(
val senderId: UserId,
@@ -36,7 +38,60 @@ data class NotificationData(
data class NotificationEvent(
val timestamp: Long,
- val content: String,
+ val content: NotificationContent,
// For images for instance
val contentUrl: String?
)
+
+sealed interface NotificationContent {
+ sealed interface MessageLike : NotificationContent {
+ object CallAnswer : MessageLike
+ object CallInvite : MessageLike
+ object CallHangup : MessageLike
+ object CallCandidates : MessageLike
+ object KeyVerificationReady : MessageLike
+ object KeyVerificationStart : MessageLike
+ object KeyVerificationCancel : MessageLike
+ object KeyVerificationAccept : MessageLike
+ object KeyVerificationKey : MessageLike
+ object KeyVerificationMac : MessageLike
+ object KeyVerificationDone : MessageLike
+ data class ReactionContent(
+ val relatedEventId: String
+ ) : MessageLike
+ object RoomEncrypted : MessageLike
+ data class RoomMessage(
+ val messageType: MessageType
+ ) : MessageLike
+ object RoomRedaction : MessageLike
+ object Sticker : MessageLike
+ }
+
+ sealed interface StateEvent : NotificationContent {
+ object PolicyRuleRoom : StateEvent
+ object PolicyRuleServer : StateEvent
+ object PolicyRuleUser : StateEvent
+ object RoomAliases : StateEvent
+ object RoomAvatar : StateEvent
+ object RoomCanonicalAlias : StateEvent
+ object RoomCreate : StateEvent
+ object RoomEncryption : StateEvent
+ object RoomGuestAccess : StateEvent
+ object RoomHistoryVisibility : StateEvent
+ object RoomJoinRules : StateEvent
+ data class RoomMemberContent(
+ val userId: String,
+ val membershipState: RoomMembershipState
+ ) : StateEvent
+ object RoomName : StateEvent
+ object RoomPinnedEvents : StateEvent
+ object RoomPowerLevels : StateEvent
+ object RoomServerAcl : StateEvent
+ object RoomThirdPartyInvite : StateEvent
+ object RoomTombstone : StateEvent
+ object RoomTopic : StateEvent
+ object SpaceChild : StateEvent
+ object SpaceParent : StateEvent
+ }
+
+}
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/NotificationMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/NotificationMapper.kt
index b62063ddc5..957f2ff7b9 100644
--- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/NotificationMapper.kt
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/NotificationMapper.kt
@@ -28,19 +28,19 @@ class NotificationMapper {
private val timelineEventMapper = TimelineEventMapper()
fun map(notificationItem: NotificationItem): NotificationData {
- return notificationItem.use {
+ return notificationItem.use { item ->
NotificationData(
- senderId = UserId(it.event.senderId()),
- eventId = EventId(it.event.eventId()),
- roomId = RoomId(it.roomInfo.id),
- senderAvatarUrl = it.senderInfo.avatarUrl,
- senderDisplayName = it.senderInfo.displayName,
- roomAvatarUrl = it.roomInfo.avatarUrl,
- roomDisplayName = it.roomInfo.displayName,
- isDirect = it.roomInfo.isDirect,
- isEncrypted = it.roomInfo.isEncrypted.orFalse(),
- isNoisy = it.isNoisy,
- event = it.event.use { event -> timelineEventMapper.map(event) }
+ senderId = UserId(item.event.senderId()),
+ eventId = EventId(item.event.eventId()),
+ roomId = RoomId(item.roomInfo.id),
+ senderAvatarUrl = item.senderInfo.avatarUrl,
+ senderDisplayName = item.senderInfo.displayName,
+ roomAvatarUrl = item.roomInfo.avatarUrl ?: item.senderInfo.avatarUrl.takeIf { item.roomInfo.isDirect },
+ roomDisplayName = item.roomInfo.displayName,
+ isDirect = item.roomInfo.isDirect,
+ isEncrypted = item.roomInfo.isEncrypted.orFalse(),
+ isNoisy = item.isNoisy,
+ event = item.event.use { event -> timelineEventMapper.map(event) }
)
}
}
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/RustNotificationService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/RustNotificationService.kt
index 99e5991719..7d523abdc7 100644
--- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/RustNotificationService.kt
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/RustNotificationService.kt
@@ -36,7 +36,8 @@ class RustNotificationService(
filterByPushRules: Boolean,
): Result {
return runCatching {
- client.getNotificationItem(roomId.value, eventId.value, filterByPushRules)?.use(notificationMapper::map)
+ val item = client.getNotificationItem(roomId.value, eventId.value, filterByPushRules)
+ item?.use(notificationMapper::map)
}
}
}
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/TimelineEventMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/TimelineEventMapper.kt
index 18d6d389e2..f7d4a00188 100644
--- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/TimelineEventMapper.kt
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/TimelineEventMapper.kt
@@ -16,9 +16,11 @@
package io.element.android.libraries.matrix.impl.notification
+import io.element.android.libraries.matrix.api.notification.NotificationContent
import io.element.android.libraries.matrix.api.notification.NotificationEvent
+import io.element.android.libraries.matrix.impl.room.RoomMemberMapper
+import io.element.android.libraries.matrix.impl.timeline.item.event.EventMessageMapper
import org.matrix.rustcomponents.sdk.MessageLikeEventContent
-import org.matrix.rustcomponents.sdk.MessageType
import org.matrix.rustcomponents.sdk.StateEventContent
import org.matrix.rustcomponents.sdk.TimelineEvent
import org.matrix.rustcomponents.sdk.TimelineEventType
@@ -38,71 +40,62 @@ class TimelineEventMapper @Inject constructor() {
}
}
-private fun TimelineEventType.toContent(): String {
+private fun TimelineEventType.toContent(): NotificationContent {
return when (this) {
is TimelineEventType.MessageLike -> content.toContent()
is TimelineEventType.State -> content.toContent()
}
}
-private fun StateEventContent.toContent(): String {
+private fun StateEventContent.toContent(): NotificationContent.StateEvent {
return when (this) {
- StateEventContent.PolicyRuleRoom -> "PolicyRuleRoom"
- StateEventContent.PolicyRuleServer -> "PolicyRuleServer"
- StateEventContent.PolicyRuleUser -> "PolicyRuleUser"
- StateEventContent.RoomAliases -> "RoomAliases"
- StateEventContent.RoomAvatar -> "RoomAvatar"
- StateEventContent.RoomCanonicalAlias -> "RoomCanonicalAlias"
- StateEventContent.RoomCreate -> "RoomCreate"
- StateEventContent.RoomEncryption -> "RoomEncryption"
- StateEventContent.RoomGuestAccess -> "RoomGuestAccess"
- StateEventContent.RoomHistoryVisibility -> "RoomHistoryVisibility"
- StateEventContent.RoomJoinRules -> "RoomJoinRules"
- is StateEventContent.RoomMemberContent -> "$userId is now $membershipState"
- StateEventContent.RoomName -> "RoomName"
- StateEventContent.RoomPinnedEvents -> "RoomPinnedEvents"
- StateEventContent.RoomPowerLevels -> "RoomPowerLevels"
- StateEventContent.RoomServerAcl -> "RoomServerAcl"
- StateEventContent.RoomThirdPartyInvite -> "RoomThirdPartyInvite"
- StateEventContent.RoomTombstone -> "RoomTombstone"
- StateEventContent.RoomTopic -> "RoomTopic"
- StateEventContent.SpaceChild -> "SpaceChild"
- StateEventContent.SpaceParent -> "SpaceParent"
+ StateEventContent.PolicyRuleRoom -> NotificationContent.StateEvent.PolicyRuleRoom
+ StateEventContent.PolicyRuleServer -> NotificationContent.StateEvent.PolicyRuleServer
+ StateEventContent.PolicyRuleUser -> NotificationContent.StateEvent.PolicyRuleUser
+ StateEventContent.RoomAliases -> NotificationContent.StateEvent.RoomAliases
+ StateEventContent.RoomAvatar -> NotificationContent.StateEvent.RoomAvatar
+ StateEventContent.RoomCanonicalAlias -> NotificationContent.StateEvent.RoomCanonicalAlias
+ StateEventContent.RoomCreate -> NotificationContent.StateEvent.RoomCreate
+ StateEventContent.RoomEncryption -> NotificationContent.StateEvent.RoomEncryption
+ StateEventContent.RoomGuestAccess -> NotificationContent.StateEvent.RoomGuestAccess
+ StateEventContent.RoomHistoryVisibility -> NotificationContent.StateEvent.RoomHistoryVisibility
+ StateEventContent.RoomJoinRules -> NotificationContent.StateEvent.RoomJoinRules
+ is StateEventContent.RoomMemberContent -> {
+ NotificationContent.StateEvent.RoomMemberContent(userId, RoomMemberMapper.mapMembership(membershipState))
+ }
+ StateEventContent.RoomName -> NotificationContent.StateEvent.RoomName
+ StateEventContent.RoomPinnedEvents -> NotificationContent.StateEvent.RoomPinnedEvents
+ StateEventContent.RoomPowerLevels -> NotificationContent.StateEvent.RoomPowerLevels
+ StateEventContent.RoomServerAcl -> NotificationContent.StateEvent.RoomServerAcl
+ StateEventContent.RoomThirdPartyInvite -> NotificationContent.StateEvent.RoomThirdPartyInvite
+ StateEventContent.RoomTombstone -> NotificationContent.StateEvent.RoomTombstone
+ StateEventContent.RoomTopic -> NotificationContent.StateEvent.RoomTopic
+ StateEventContent.SpaceChild -> NotificationContent.StateEvent.SpaceChild
+ StateEventContent.SpaceParent -> NotificationContent.StateEvent.SpaceParent
}
}
-private fun MessageLikeEventContent.toContent(): String {
+private fun MessageLikeEventContent.toContent(): NotificationContent.MessageLike {
return use {
when (it) {
- MessageLikeEventContent.CallAnswer -> "CallAnswer"
- MessageLikeEventContent.CallCandidates -> "CallCandidates"
- MessageLikeEventContent.CallHangup -> "CallHangup"
- MessageLikeEventContent.CallInvite -> "CallInvite"
- MessageLikeEventContent.KeyVerificationAccept -> "KeyVerificationAccept"
- MessageLikeEventContent.KeyVerificationCancel -> "KeyVerificationCancel"
- MessageLikeEventContent.KeyVerificationDone -> "KeyVerificationDone"
- MessageLikeEventContent.KeyVerificationKey -> "KeyVerificationKey"
- MessageLikeEventContent.KeyVerificationMac -> "KeyVerificationMac"
- MessageLikeEventContent.KeyVerificationReady -> "KeyVerificationReady"
- MessageLikeEventContent.KeyVerificationStart -> "KeyVerificationStart"
- is MessageLikeEventContent.ReactionContent -> "Reacted to ${it.relatedEventId.take(8)}…"
- MessageLikeEventContent.RoomEncrypted -> "RoomEncrypted"
- is MessageLikeEventContent.RoomMessage -> it.messageType.toContent()
- MessageLikeEventContent.RoomRedaction -> "RoomRedaction"
- MessageLikeEventContent.Sticker -> "Sticker"
+ MessageLikeEventContent.CallAnswer -> NotificationContent.MessageLike.CallAnswer
+ MessageLikeEventContent.CallCandidates -> NotificationContent.MessageLike.CallCandidates
+ MessageLikeEventContent.CallHangup -> NotificationContent.MessageLike.CallHangup
+ MessageLikeEventContent.CallInvite -> NotificationContent.MessageLike.CallInvite
+ MessageLikeEventContent.KeyVerificationAccept -> NotificationContent.MessageLike.KeyVerificationAccept
+ MessageLikeEventContent.KeyVerificationCancel -> NotificationContent.MessageLike.KeyVerificationCancel
+ MessageLikeEventContent.KeyVerificationDone -> NotificationContent.MessageLike.KeyVerificationDone
+ MessageLikeEventContent.KeyVerificationKey -> NotificationContent.MessageLike.KeyVerificationKey
+ MessageLikeEventContent.KeyVerificationMac -> NotificationContent.MessageLike.KeyVerificationMac
+ MessageLikeEventContent.KeyVerificationReady -> NotificationContent.MessageLike.KeyVerificationReady
+ MessageLikeEventContent.KeyVerificationStart -> NotificationContent.MessageLike.KeyVerificationStart
+ is MessageLikeEventContent.ReactionContent -> NotificationContent.MessageLike.ReactionContent(it.relatedEventId)
+ MessageLikeEventContent.RoomEncrypted -> NotificationContent.MessageLike.RoomEncrypted
+ is MessageLikeEventContent.RoomMessage -> {
+ NotificationContent.MessageLike.RoomMessage(EventMessageMapper().mapMessageType(it.messageType))
+ }
+ MessageLikeEventContent.RoomRedaction -> NotificationContent.MessageLike.RoomRedaction
+ MessageLikeEventContent.Sticker -> NotificationContent.MessageLike.Sticker
}
}
}
-
-private fun MessageType.toContent(): String {
- return when (this) {
- is MessageType.Audio -> content.use { it.body }
- is MessageType.Emote -> content.body
- is MessageType.File -> content.use { it.body }
- is MessageType.Image -> content.use { it.body }
- is MessageType.Location -> content.body
- is MessageType.Notice -> content.body
- is MessageType.Text -> content.body
- is MessageType.Video -> content.use { it.body }
- }
-}
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventMessageMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventMessageMapper.kt
index c4148de745..2ecad95e4f 100644
--- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventMessageMapper.kt
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventMessageMapper.kt
@@ -33,48 +33,17 @@ import io.element.android.libraries.matrix.api.timeline.item.event.UnknownMessag
import io.element.android.libraries.matrix.api.timeline.item.event.VideoMessageType
import io.element.android.libraries.matrix.impl.media.map
import org.matrix.rustcomponents.sdk.Message
-import org.matrix.rustcomponents.sdk.MessageType
import org.matrix.rustcomponents.sdk.ProfileDetails
import org.matrix.rustcomponents.sdk.RepliedToEventDetails
import org.matrix.rustcomponents.sdk.use
import org.matrix.rustcomponents.sdk.FormattedBody as RustFormattedBody
import org.matrix.rustcomponents.sdk.MessageFormat as RustMessageFormat
+import org.matrix.rustcomponents.sdk.MessageType as RustMessageType
class EventMessageMapper {
fun map(message: Message): MessageContent = message.use {
- val type = it.msgtype().use { type ->
- when (type) {
- is MessageType.Audio -> {
- AudioMessageType(type.content.body, type.content.source.map(), type.content.info?.map())
- }
- is MessageType.File -> {
- FileMessageType(type.content.body, type.content.source.map(), type.content.info?.map())
- }
- is MessageType.Image -> {
- ImageMessageType(type.content.body, type.content.source.map(), type.content.info?.map())
- }
- is MessageType.Location -> {
- LocationMessageType(type.content.body, type.content.geoUri, type.content.description)
- }
- is MessageType.Notice -> {
- NoticeMessageType(type.content.body, type.content.formatted?.map())
- }
- is MessageType.Text -> {
- TextMessageType(type.content.body, type.content.formatted?.map())
- }
- is MessageType.Emote -> {
- EmoteMessageType(type.content.body, type.content.formatted?.map())
- }
- is MessageType.Video -> {
- VideoMessageType(type.content.body, type.content.source.map(), type.content.info?.map())
- }
- null -> {
- UnknownMessageType
- }
-
- }
- }
+ val type = it.msgtype().use(this::mapMessageType)
val inReplyToId = it.inReplyTo()?.eventId?.let(::EventId)
val inReplyToEvent: InReplyTo? = (it.inReplyTo()?.event)?.use { details ->
when (details) {
@@ -99,6 +68,34 @@ class EventMessageMapper {
type = type
)
}
+
+ fun mapMessageType(type: RustMessageType?) = when (type) {
+ is RustMessageType.Audio -> {
+ AudioMessageType(type.content.body, type.content.source.map(), type.content.info?.map())
+ }
+ is RustMessageType.File -> {
+ FileMessageType(type.content.body, type.content.source.map(), type.content.info?.map())
+ }
+ is RustMessageType.Image -> {
+ ImageMessageType(type.content.body, type.content.source.map(), type.content.info?.map())
+ }
+ is RustMessageType.Notice -> {
+ NoticeMessageType(type.content.body, type.content.formatted?.map())
+ }
+ is RustMessageType.Text -> {
+ TextMessageType(type.content.body, type.content.formatted?.map())
+ }
+ is RustMessageType.Emote -> {
+ EmoteMessageType(type.content.body, type.content.formatted?.map())
+ }
+ is RustMessageType.Video -> {
+ VideoMessageType(type.content.body, type.content.source.map(), type.content.info?.map())
+ }
+ is RustMessageType.Location -> {
+ LocationMessageType(type.content.body, type.content.geoUri, type.content.description)
+ }
+ null -> UnknownMessageType
+ }
}
private fun RustFormattedBody.map(): FormattedBody = FormattedBody(
diff --git a/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/notifications/NotificationDrawerManager.kt b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/notifications/NotificationDrawerManager.kt
new file mode 100644
index 0000000000..9a778195fa
--- /dev/null
+++ b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/notifications/NotificationDrawerManager.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.libraries.push.api.notifications
+
+import io.element.android.libraries.matrix.api.core.RoomId
+import io.element.android.libraries.matrix.api.core.SessionId
+
+interface NotificationDrawerManager {
+ fun clearMembershipNotificationForSession(sessionId: SessionId)
+ fun clearMembershipNotificationForRoom(sessionId: SessionId, roomId: RoomId)
+}
diff --git a/libraries/push/impl/build.gradle.kts b/libraries/push/impl/build.gradle.kts
index be302eb57d..ebd4c8f105 100644
--- a/libraries/push/impl/build.gradle.kts
+++ b/libraries/push/impl/build.gradle.kts
@@ -44,6 +44,7 @@ dependencies {
implementation(projects.libraries.network)
implementation(projects.libraries.matrix.api)
implementation(projects.libraries.matrixui)
+ implementation(projects.libraries.uiStrings)
api(projects.libraries.pushproviders.api)
api(projects.libraries.pushstore.api)
api(projects.libraries.push.api)
@@ -52,10 +53,6 @@ dependencies {
implementation(projects.services.appnavstate.api)
implementation(projects.services.toolbox.api)
- api(libs.gujun.span) {
- exclude(group = "com.android.support", module = "support-annotations")
- }
-
// TODO Temporary use the deprecated LocalBroadcastManager, to be changed later.
implementation("androidx.localbroadcastmanager:localbroadcastmanager:1.1.0")
diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/DefaultPushService.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/DefaultPushService.kt
index f57651c3a7..b16908269d 100644
--- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/DefaultPushService.kt
+++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/DefaultPushService.kt
@@ -20,8 +20,7 @@ import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.push.api.PushService
-import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecret
-import io.element.android.libraries.push.impl.notifications.NotificationDrawerManager
+import io.element.android.libraries.push.impl.notifications.DefaultNotificationDrawerManager
import io.element.android.libraries.pushproviders.api.Distributor
import io.element.android.libraries.pushproviders.api.PushProvider
import io.element.android.libraries.pushstore.api.UserPushStoreFactory
@@ -29,13 +28,13 @@ import javax.inject.Inject
@ContributesBinding(AppScope::class)
class DefaultPushService @Inject constructor(
- private val notificationDrawerManager: NotificationDrawerManager,
+ private val defaultNotificationDrawerManager: DefaultNotificationDrawerManager,
private val pushersManager: PushersManager,
private val userPushStoreFactory: UserPushStoreFactory,
private val pushProviders: Set<@JvmSuppressWildcards PushProvider>,
) : PushService {
override fun notificationStyleChanged() {
- notificationDrawerManager.notificationStyleChanged()
+ defaultNotificationDrawerManager.notificationStyleChanged()
}
override fun getAvailablePushProviders(): List {
diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/di/PushBindsModule.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/di/PushBindsModule.kt
new file mode 100644
index 0000000000..63c5198514
--- /dev/null
+++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/di/PushBindsModule.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.libraries.push.impl.di
+
+import com.squareup.anvil.annotations.ContributesTo
+import dagger.Binds
+import dagger.Module
+import io.element.android.libraries.di.AppScope
+import io.element.android.libraries.push.api.notifications.NotificationDrawerManager
+import io.element.android.libraries.push.impl.notifications.DefaultNotificationDrawerManager
+
+@Module
+@ContributesTo(AppScope::class)
+abstract class PushBindsModule {
+ @Binds
+ abstract fun bindNotificationDrawerManager(
+ defaultNotificationDrawerManager: DefaultNotificationDrawerManager
+ ): NotificationDrawerManager
+}
diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/intent/IntentProvider.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/intent/IntentProvider.kt
index ce2b1d3fce..8e0dd3e6f2 100644
--- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/intent/IntentProvider.kt
+++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/intent/IntentProvider.kt
@@ -23,11 +23,16 @@ import io.element.android.libraries.matrix.api.core.ThreadId
interface IntentProvider {
/**
- * Provide an intent to start the application.
+ * Provide an intent to start the application on a room or thread.
*/
- fun getViewIntent(
+ fun getViewRoomIntent(
sessionId: SessionId,
roomId: RoomId?,
threadId: ThreadId?,
): Intent
+
+ /**
+ * Provide an intent to start the application on the invite list.
+ */
+ fun getInviteListIntent(sessionId: SessionId): Intent
}
diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDrawerManager.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManager.kt
similarity index 91%
rename from libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDrawerManager.kt
rename to libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManager.kt
index 87d37e7e33..cd0274016b 100644
--- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDrawerManager.kt
+++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManager.kt
@@ -24,14 +24,16 @@ import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.SingleIn
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.ThreadId
import io.element.android.libraries.matrix.api.user.MatrixUser
+import io.element.android.libraries.push.api.notifications.NotificationDrawerManager
import io.element.android.libraries.push.api.store.PushDataStore
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent
-import io.element.android.libraries.push.impl.notifications.model.shouldIgnoreMessageEventInRoom
+import io.element.android.libraries.push.impl.notifications.model.shouldIgnoreEventInRoom
import io.element.android.services.appnavstate.api.AppNavigationState
import io.element.android.services.appnavstate.api.AppNavigationStateService
import kotlinx.coroutines.CoroutineScope
@@ -47,7 +49,7 @@ import javax.inject.Inject
* Events can be grouped into the same notification, old (already read) events can be removed to do some cleaning.
*/
@SingleIn(AppScope::class)
-class NotificationDrawerManager @Inject constructor(
+class DefaultNotificationDrawerManager @Inject constructor(
private val pushDataStore: PushDataStore,
private val notifiableEventProcessor: NotifiableEventProcessor,
private val notificationRenderer: NotificationRenderer,
@@ -58,7 +60,7 @@ class NotificationDrawerManager @Inject constructor(
private val dispatchers: CoroutineDispatchers,
private val buildMeta: BuildMeta,
private val matrixAuthenticationService: MatrixAuthenticationService,
-) {
+) : NotificationDrawerManager {
/**
* Lazily initializes the NotificationState as we rely on having a current session in order to fetch the persisted queue of events.
*/
@@ -152,12 +154,27 @@ class NotificationDrawerManager @Inject constructor(
}
}
+ override fun clearMembershipNotificationForSession(sessionId: SessionId) {
+ updateEvents {
+ it.clearMembershipNotificationForSession(sessionId)
+ }
+ }
+
/**
* Clear invitation notification for the provided room.
*/
- fun clearMemberShipNotificationForRoom(sessionId: SessionId, roomId: RoomId) {
+ override fun clearMembershipNotificationForRoom(sessionId: SessionId, roomId: RoomId) {
+ updateEvents {
+ it.clearMembershipNotificationForRoom(sessionId, roomId)
+ }
+ }
+
+ /**
+ * Clear the notifications for a single event.
+ */
+ fun clearEvent(eventId: EventId) {
updateEvents {
- it.clearMemberShipNotificationForRoom(sessionId, roomId)
+ it.clearEvent(eventId)
}
}
@@ -183,7 +200,7 @@ class NotificationDrawerManager @Inject constructor(
}
}
- private fun updateEvents(action: NotificationDrawerManager.(NotificationEventQueue) -> Unit) {
+ private fun updateEvents(action: DefaultNotificationDrawerManager.(NotificationEventQueue) -> Unit) {
notificationState.updateQueuedEvents(this) { queuedEvents, _ ->
action(queuedEvents)
}
@@ -260,6 +277,6 @@ class NotificationDrawerManager @Inject constructor(
}
fun shouldIgnoreMessageEventInRoom(resolvedEvent: NotifiableMessageEvent): Boolean {
- return resolvedEvent.shouldIgnoreMessageEventInRoom(currentAppNavigationState)
+ return resolvedEvent.shouldIgnoreEventInRoom(currentAppNavigationState)
}
}
diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventProcessor.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventProcessor.kt
index 07c3a99708..4202ef78d4 100644
--- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventProcessor.kt
+++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventProcessor.kt
@@ -17,11 +17,12 @@
package io.element.android.libraries.push.impl.notifications
import io.element.android.libraries.matrix.api.timeline.item.event.EventType
+import io.element.android.libraries.push.impl.notifications.model.FallbackNotifiableEvent
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
import io.element.android.libraries.push.impl.notifications.model.SimpleNotifiableEvent
-import io.element.android.libraries.push.impl.notifications.model.shouldIgnoreMessageEventInRoom
+import io.element.android.libraries.push.impl.notifications.model.shouldIgnoreEventInRoom
import io.element.android.services.appnavstate.api.AppNavigationState
import timber.log.Timber
import javax.inject.Inject
@@ -41,7 +42,7 @@ class NotifiableEventProcessor @Inject constructor(
val type = when (it) {
is InviteNotifiableEvent -> ProcessedEvent.Type.KEEP
is NotifiableMessageEvent -> when {
- it.shouldIgnoreMessageEventInRoom(appNavigationState) -> {
+ it.shouldIgnoreEventInRoom(appNavigationState) -> {
ProcessedEvent.Type.REMOVE
.also { Timber.d("notification message removed due to currently viewing the same room or thread") }
}
@@ -53,6 +54,13 @@ class NotifiableEventProcessor @Inject constructor(
EventType.REDACTION -> ProcessedEvent.Type.REMOVE
else -> ProcessedEvent.Type.KEEP
}
+ is FallbackNotifiableEvent -> when {
+ it.shouldIgnoreEventInRoom(appNavigationState) -> {
+ ProcessedEvent.Type.REMOVE
+ .also { Timber.d("notification fallback removed due to currently viewing the same room or thread") }
+ }
+ else -> ProcessedEvent.Type.KEEP
+ }
}
ProcessedEvent(type, it)
}
diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolver.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolver.kt
index faa8eb86d8..f0b70d8fac 100644
--- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolver.kt
+++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolver.kt
@@ -22,12 +22,26 @@ 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.core.ThreadId
+import io.element.android.libraries.matrix.api.notification.NotificationContent
import io.element.android.libraries.matrix.api.notification.NotificationData
-import io.element.android.libraries.matrix.api.notification.NotificationEvent
+import io.element.android.libraries.matrix.api.room.RoomMembershipState
+import io.element.android.libraries.matrix.api.timeline.item.event.AudioMessageType
+import io.element.android.libraries.matrix.api.timeline.item.event.EmoteMessageType
+import io.element.android.libraries.matrix.api.timeline.item.event.FileMessageType
+import io.element.android.libraries.matrix.api.timeline.item.event.ImageMessageType
+import io.element.android.libraries.matrix.api.timeline.item.event.LocationMessageType
+import io.element.android.libraries.matrix.api.timeline.item.event.NoticeMessageType
+import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType
+import io.element.android.libraries.matrix.api.timeline.item.event.UnknownMessageType
+import io.element.android.libraries.matrix.api.timeline.item.event.VideoMessageType
+import io.element.android.libraries.push.impl.R
import io.element.android.libraries.push.impl.log.pushLoggerTag
+import io.element.android.libraries.push.impl.notifications.model.FallbackNotifiableEvent
+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
+import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.services.toolbox.api.strings.StringProvider
import io.element.android.services.toolbox.api.systemclock.SystemClock
import timber.log.Timber
@@ -53,73 +67,163 @@ class NotifiableEventResolver @Inject constructor(
suspend fun resolveEvent(sessionId: SessionId, roomId: RoomId, eventId: EventId): NotifiableEvent? {
// Restore session
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(
+ val notificationService = session.notificationService()
+ val notificationData = notificationService.getNotification(
userId = sessionId,
roomId = roomId,
eventId = eventId,
- filterByPushRules = true,
- )
- }.fold(
- {
- it
- },
- {
- Timber.tag(loggerTag.value).e(it, "Unable to resolve event.")
- null
- }
- ).orDefault(roomId, eventId)
+ // FIXME should be true in the future, but right now it's broken
+ // (https://github.com/vector-im/element-x-android/issues/640#issuecomment-1612913658)
+ filterByPushRules = false,
+ ).onFailure {
+ Timber.tag(loggerTag.value).e(it, "Unable to resolve event: $eventId.")
+ }.getOrNull()
+
+ // TODO this notificationData is not always valid at the moment, sometimes the Rust SDK can't fetch the matching event
+ return notificationData?.asNotifiableEvent(sessionId)
+ ?: fallbackNotifiableEvent(sessionId, roomId, eventId)
+ }
- return notificationData.asNotifiableEvent(sessionId)
+ private fun NotificationData.asNotifiableEvent(userId: SessionId): NotifiableEvent? {
+ return when (val content = this.event.content) {
+ is NotificationContent.MessageLike.RoomMessage -> {
+ buildNotifiableMessageEvent(
+ sessionId = userId,
+ roomId = roomId,
+ eventId = eventId,
+ noisy = isNoisy,
+ timestamp = event.timestamp,
+ senderName = senderDisplayName,
+ senderId = senderId.value,
+ body = descriptionFromMessageContent(content),
+ imageUriString = event.contentUrl,
+ roomName = roomDisplayName,
+ roomIsDirect = isDirect,
+ roomAvatarPath = roomAvatarUrl,
+ senderAvatarPath = senderAvatarUrl,
+ )
+ }
+ is NotificationContent.StateEvent.RoomMemberContent -> {
+ if (content.membershipState == RoomMembershipState.INVITE) {
+ InviteNotifiableEvent(
+ sessionId = userId,
+ roomId = roomId,
+ eventId = eventId,
+ editedEventId = null,
+ canBeReplaced = true,
+ roomName = roomDisplayName,
+ noisy = isNoisy,
+ timestamp = event.timestamp,
+ soundName = null,
+ isRedacted = false,
+ isUpdated = false,
+ description = descriptionFromRoomMembershipContent(content, isDirect) ?: return null,
+ type = null, // TODO check if type is needed anymore
+ title = null, // TODO check if title is needed anymore
+ )
+ } else {
+ null
+ }
+ }
+ else -> null
+ }
}
- private fun NotificationData.asNotifiableEvent(userId: SessionId): NotifiableEvent {
- return NotifiableMessageEvent(
- sessionId = userId,
- roomId = roomId,
- eventId = eventId,
- editedEventId = null,
- canBeReplaced = true,
- noisy = isNoisy,
- timestamp = event.timestamp,
- senderName = senderDisplayName,
- senderId = senderId.value,
- body = event.content,
- imageUriString = event.contentUrl,
- threadId = null,
- roomName = roomDisplayName,
- roomIsDirect = isDirect,
- roomAvatarPath = roomAvatarUrl,
- senderAvatarPath = senderAvatarUrl,
- soundName = null,
- outGoingMessage = false,
- outGoingMessageFailed = false,
- isRedacted = false,
- isUpdated = false
- )
+ private fun fallbackNotifiableEvent(
+ userId: SessionId,
+ roomId: RoomId,
+ eventId: EventId
+ ) = FallbackNotifiableEvent(
+ sessionId = userId,
+ roomId = roomId,
+ eventId = eventId,
+ editedEventId = null,
+ canBeReplaced = true,
+ isRedacted = false,
+ isUpdated = false,
+ timestamp = clock.epochMillis(),
+ description = stringProvider.getString(R.string.notification_fallback_content),
+ )
+
+ private fun descriptionFromMessageContent(
+ content: NotificationContent.MessageLike.RoomMessage,
+ ): String {
+ return when (val messageType = content.messageType) {
+ is AudioMessageType -> messageType.body
+ is EmoteMessageType -> messageType.body
+ is FileMessageType -> messageType.body
+ is ImageMessageType -> messageType.body
+ is NoticeMessageType -> messageType.body
+ is TextMessageType -> messageType.body
+ is VideoMessageType -> messageType.body
+ is LocationMessageType -> messageType.body
+ is UnknownMessageType -> stringProvider.getString(CommonStrings.common_unsupported_event)
+ }
}
- /**
- * TODO This is a temporary method for EAx.
- */
- private fun NotificationData?.orDefault(roomId: RoomId, eventId: EventId): NotificationData {
- return this ?: NotificationData(
- eventId = eventId,
- senderId = UserId("@user:domain"),
- roomId = roomId,
- senderAvatarUrl = null,
- senderDisplayName = null,
- roomAvatarUrl = null,
- roomDisplayName = null,
- isNoisy = false,
- isEncrypted = false,
- isDirect = false,
- event = NotificationEvent(
- timestamp = clock.epochMillis(),
- content = "Message ${eventId.value.take(8)}… in room ${roomId.value.take(8)}…",
- contentUrl = null
- )
- )
+ private fun descriptionFromRoomMembershipContent(
+ content: NotificationContent.StateEvent.RoomMemberContent,
+ isDirectRoom: Boolean
+ ): String? {
+ return when (content.membershipState) {
+ RoomMembershipState.INVITE -> {
+ if (isDirectRoom) {
+ stringProvider.getString(R.string.notification_invite_body)
+ } else {
+ stringProvider.getString(R.string.notification_room_invite_body)
+ }
+ }
+ else -> null
+ }
}
}
+
+@Suppress("LongParameterList")
+private fun buildNotifiableMessageEvent(
+ sessionId: SessionId,
+ roomId: RoomId,
+ eventId: EventId,
+ editedEventId: EventId? = null,
+ canBeReplaced: Boolean = false,
+ noisy: Boolean,
+ timestamp: Long,
+ senderName: String?,
+ senderId: String?,
+ body: String?,
+ // We cannot use Uri? type here, as that could trigger a
+ // NotSerializableException when persisting this to storage
+ imageUriString: String? = null,
+ threadId: ThreadId? = null,
+ roomName: String? = null,
+ roomIsDirect: Boolean = false,
+ roomAvatarPath: String? = null,
+ senderAvatarPath: String? = null,
+ soundName: String? = null,
+ // This is used for >N notification, as the result of a smart reply
+ outGoingMessage: Boolean = false,
+ outGoingMessageFailed: Boolean = false,
+ isRedacted: Boolean = false,
+ isUpdated: Boolean = false
+) = NotifiableMessageEvent(
+ sessionId = sessionId,
+ roomId = roomId,
+ eventId = eventId,
+ editedEventId = editedEventId,
+ canBeReplaced = canBeReplaced,
+ noisy = noisy,
+ timestamp = timestamp,
+ senderName = senderName,
+ senderId = senderId,
+ body = body,
+ imageUriString = imageUriString,
+ threadId = threadId,
+ roomName = roomName,
+ roomIsDirect = roomIsDirect,
+ roomAvatarPath = roomAvatarPath,
+ senderAvatarPath = senderAvatarPath,
+ soundName = soundName,
+ outGoingMessage = outGoingMessage,
+ outGoingMessageFailed = outGoingMessageFailed,
+ isRedacted = isRedacted,
+ isUpdated = isUpdated
+)
diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationActionIds.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationActionIds.kt
index bc20d49917..6141079130 100644
--- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationActionIds.kt
+++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationActionIds.kt
@@ -34,6 +34,8 @@ data class NotificationActionIds @Inject constructor(
val smartReply = "${buildMeta.applicationId}.NotificationActions.SMART_REPLY_ACTION"
val dismissSummary = "${buildMeta.applicationId}.NotificationActions.DISMISS_SUMMARY_ACTION"
val dismissRoom = "${buildMeta.applicationId}.NotificationActions.DISMISS_ROOM_NOTIF_ACTION"
+ val dismissInvite = "${buildMeta.applicationId}.NotificationActions.DISMISS_INVITE_NOTIF_ACTION"
+ val dismissEvent = "${buildMeta.applicationId}.NotificationActions.DISMISS_EVENT_NOTIF_ACTION"
val diagnostic = "${buildMeta.applicationId}.NotificationActions.DIAGNOSTIC"
val push = "${buildMeta.applicationId}.PUSH"
}
diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBitmapLoader.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBitmapLoader.kt
index c2cdfc5677..1ad38bf787 100644
--- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBitmapLoader.kt
+++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBitmapLoader.kt
@@ -49,6 +49,7 @@ class NotificationBitmapLoader @Inject constructor(
return try {
val imageRequest = ImageRequest.Builder(context)
.data(MediaRequestData(MediaSource(path), MediaRequestData.Kind.Thumbnail(1024)))
+ .transformations(CircleCropTransformation())
.build()
val result = context.imageLoader.execute(imageRequest)
result.drawable?.toBitmap()
diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiver.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiver.kt
index de97986026..d5df1001ca 100644
--- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiver.kt
+++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiver.kt
@@ -22,6 +22,7 @@ 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.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
@@ -37,7 +38,7 @@ private val loggerTag = LoggerTag("NotificationBroadcastReceiver", notificationL
*/
class NotificationBroadcastReceiver : BroadcastReceiver() {
- @Inject lateinit var notificationDrawerManager: NotificationDrawerManager
+ @Inject lateinit var defaultNotificationDrawerManager: DefaultNotificationDrawerManager
//@Inject lateinit var activeSessionHolder: ActiveSessionHolder
//@Inject lateinit var analyticsTracker: AnalyticsTracker
@@ -50,24 +51,31 @@ class NotificationBroadcastReceiver : BroadcastReceiver() {
Timber.tag(loggerTag.value).v("NotificationBroadcastReceiver received : $intent")
val sessionId = intent.extras?.getString(KEY_SESSION_ID)?.let(::SessionId) ?: return
val roomId = intent.getStringExtra(KEY_ROOM_ID)?.let(::RoomId)
+ val eventId = intent.getStringExtra(KEY_EVENT_ID)?.let(::EventId)
when (intent.action) {
actionIds.smartReply ->
handleSmartReply(intent, context)
actionIds.dismissRoom -> if (roomId != null) {
- notificationDrawerManager.clearMessagesForRoom(sessionId, roomId)
+ defaultNotificationDrawerManager.clearMessagesForRoom(sessionId, roomId)
}
actionIds.dismissSummary ->
- notificationDrawerManager.clearAllEvents(sessionId)
+ defaultNotificationDrawerManager.clearAllEvents(sessionId)
+ actionIds.dismissInvite -> if (roomId != null) {
+ defaultNotificationDrawerManager.clearMembershipNotificationForRoom(sessionId, roomId)
+ }
+ actionIds.dismissEvent -> if (eventId != null) {
+ defaultNotificationDrawerManager.clearEvent(eventId)
+ }
actionIds.markRoomRead -> if (roomId != null) {
- notificationDrawerManager.clearMessagesForRoom(sessionId, roomId)
+ defaultNotificationDrawerManager.clearMessagesForRoom(sessionId, roomId)
handleMarkAsRead(sessionId, roomId)
}
actionIds.join -> if (roomId != null) {
- notificationDrawerManager.clearMemberShipNotificationForRoom(sessionId, roomId)
+ defaultNotificationDrawerManager.clearMembershipNotificationForRoom(sessionId, roomId)
handleJoinRoom(sessionId, roomId)
}
actionIds.reject -> if (roomId != null) {
- notificationDrawerManager.clearMemberShipNotificationForRoom(sessionId, roomId)
+ defaultNotificationDrawerManager.clearMembershipNotificationForRoom(sessionId, roomId)
handleRejectRoom(sessionId, roomId)
}
}
@@ -240,6 +248,7 @@ class NotificationBroadcastReceiver : BroadcastReceiver() {
const val KEY_SESSION_ID = "sessionID"
const val KEY_ROOM_ID = "roomID"
const val KEY_THREAD_ID = "threadID"
+ const val KEY_EVENT_ID = "eventID"
const val KEY_TEXT_REPLY = "key_text_reply"
}
}
diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationEventQueue.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationEventQueue.kt
index 862b4784ac..97b90476b0 100644
--- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationEventQueue.kt
+++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationEventQueue.kt
@@ -21,6 +21,7 @@ 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.FallbackNotifiableEvent
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
@@ -45,6 +46,7 @@ data class NotificationEventQueue constructor(
is InviteNotifiableEvent -> it.copy(isRedacted = true)
is NotifiableMessageEvent -> it.copy(isRedacted = true)
is SimpleNotifiableEvent -> it.copy(isRedacted = true)
+ is FallbackNotifiableEvent -> it.copy(isRedacted = true)
}
}
}
@@ -57,7 +59,8 @@ data class NotificationEventQueue constructor(
when (it) {
is NotifiableMessageEvent -> roomsLeft.contains(it.roomId)
is InviteNotifiableEvent -> roomsLeft.contains(it.roomId) || roomsJoined.contains(it.roomId)
- else -> false
+ is SimpleNotifiableEvent -> false
+ is FallbackNotifiableEvent -> roomsLeft.contains(it.roomId)
}
}
}
@@ -127,11 +130,21 @@ data class NotificationEventQueue constructor(
is InviteNotifiableEvent -> with.copy(isUpdated = true)
is NotifiableMessageEvent -> with.copy(isUpdated = true)
is SimpleNotifiableEvent -> with.copy(isUpdated = true)
+ is FallbackNotifiableEvent -> with.copy(isUpdated = true)
}
)
}
- fun clearMemberShipNotificationForRoom(sessionId: SessionId, roomId: RoomId) {
+ fun clearEvent(eventId: EventId) {
+ queue.removeAll { it.eventId == eventId }
+ }
+
+ fun clearMembershipNotificationForSession(sessionId: SessionId) {
+ Timber.d("clearMemberShipOfSession $sessionId")
+ queue.removeAll { it is InviteNotifiableEvent && it.sessionId == sessionId }
+ }
+
+ fun clearMembershipNotificationForRoom(sessionId: SessionId, roomId: RoomId) {
Timber.d("clearMemberShipOfRoom $sessionId, $roomId")
queue.removeAll { it is InviteNotifiableEvent && it.sessionId == sessionId && it.roomId == roomId }
}
diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationFactory.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationFactory.kt
index 79173611dc..0addc1276a 100644
--- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationFactory.kt
+++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationFactory.kt
@@ -20,6 +20,7 @@ import android.app.Notification
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.push.impl.notifications.factories.NotificationFactory
+import io.element.android.libraries.push.impl.notifications.model.FallbackNotifiableEvent
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
@@ -94,16 +95,35 @@ class NotificationFactory @Inject constructor(
}
}
+ fun List>.toNotifications(): List {
+ return map { (processed, event) ->
+ when (processed) {
+ ProcessedEvent.Type.REMOVE -> OneShotNotification.Removed(key = event.eventId.value)
+ ProcessedEvent.Type.KEEP -> OneShotNotification.Append(
+ notificationFactory.createFallbackNotification(event),
+ OneShotNotification.Append.Meta(
+ key = event.eventId.value,
+ summaryLine = event.description.orEmpty(),
+ isNoisy = false,
+ timestamp = event.timestamp
+ )
+ )
+ }
+ }
+ }
+
fun createSummaryNotification(
currentUser: MatrixUser,
roomNotifications: List,
invitationNotifications: List,
simpleNotifications: List,
+ fallbackNotifications: List,
useCompleteNotificationFormat: Boolean
): SummaryNotification {
val roomMeta = roomNotifications.filterIsInstance().map { it.meta }
val invitationMeta = invitationNotifications.filterIsInstance().map { it.meta }
val simpleMeta = simpleNotifications.filterIsInstance().map { it.meta }
+ val fallbackMeta = simpleNotifications.filterIsInstance().map { it.meta }
return when {
roomMeta.isEmpty() && invitationMeta.isEmpty() && simpleMeta.isEmpty() -> SummaryNotification.Removed
else -> SummaryNotification.Update(
@@ -112,6 +132,7 @@ class NotificationFactory @Inject constructor(
roomNotifications = roomMeta,
invitationNotifications = invitationMeta,
simpleNotifications = simpleMeta,
+ fallbackNotifications = fallbackMeta,
useCompleteNotificationFormat = useCompleteNotificationFormat
)
)
diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationIdProvider.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationIdProvider.kt
index 3ce941de2f..050edfcc11 100644
--- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationIdProvider.kt
+++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationIdProvider.kt
@@ -37,12 +37,17 @@ class NotificationIdProvider @Inject constructor() {
return getOffset(sessionId) + ROOM_INVITATION_NOTIFICATION_ID
}
+ fun getFallbackNotificationId(sessionId: SessionId): Int {
+ return getOffset(sessionId) + FALLBACK_NOTIFICATION_ID
+ }
+
private fun getOffset(sessionId: SessionId): Int {
// Compute a int from a string with a low risk of collision.
return abs(sessionId.value.hashCode() % 100_000) * 10
}
companion object {
+ private const val FALLBACK_NOTIFICATION_ID = -1
private const val SUMMARY_NOTIFICATION_ID = 0
private const val ROOM_MESSAGES_NOTIFICATION_ID = 1
private const val ROOM_EVENT_NOTIFICATION_ID = 2
diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRenderer.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRenderer.kt
index 428420211b..a6179b3ec8 100644
--- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRenderer.kt
+++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRenderer.kt
@@ -18,6 +18,7 @@ package io.element.android.libraries.push.impl.notifications
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.user.MatrixUser
+import io.element.android.libraries.push.impl.notifications.model.FallbackNotifiableEvent
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
@@ -36,16 +37,18 @@ class NotificationRenderer @Inject constructor(
useCompleteNotificationFormat: Boolean,
eventsToProcess: List>
) {
- val (roomEvents, simpleEvents, invitationEvents) = eventsToProcess.groupByType()
+ val groupedEvents = eventsToProcess.groupByType()
with(notificationFactory) {
- val roomNotifications = roomEvents.toNotifications(currentUser)
- val invitationNotifications = invitationEvents.toNotifications()
- val simpleNotifications = simpleEvents.toNotifications()
+ val roomNotifications = groupedEvents.roomEvents.toNotifications(currentUser)
+ val invitationNotifications = groupedEvents.invitationEvents.toNotifications()
+ val simpleNotifications = groupedEvents.simpleEvents.toNotifications()
+ val fallbackNotifications = groupedEvents.fallbackEvents.toNotifications()
val summaryNotification = createSummaryNotification(
currentUser = currentUser,
roomNotifications = roomNotifications,
invitationNotifications = invitationNotifications,
simpleNotifications = simpleNotifications,
+ fallbackNotifications = fallbackNotifications,
useCompleteNotificationFormat = useCompleteNotificationFormat
)
@@ -118,6 +121,26 @@ class NotificationRenderer @Inject constructor(
}
}
+ fallbackNotifications.forEach { wrapper ->
+ when (wrapper) {
+ is OneShotNotification.Removed -> {
+ Timber.d("Removing fallback notification ${wrapper.key}")
+ notificationDisplayer.cancelNotificationMessage(
+ tag = wrapper.key,
+ id = notificationIdProvider.getFallbackNotificationId(currentUser.userId)
+ )
+ }
+ is OneShotNotification.Append -> if (useCompleteNotificationFormat) {
+ Timber.d("Updating fallback notification ${wrapper.meta.key}")
+ notificationDisplayer.showNotificationMessage(
+ tag = wrapper.meta.key,
+ id = notificationIdProvider.getFallbackNotificationId(currentUser.userId),
+ notification = wrapper.notification
+ )
+ }
+ }
+ }
+
// Update summary last to avoid briefly displaying it before other notifications
if (summaryNotification is SummaryNotification.Update) {
Timber.d("Updating summary notification")
@@ -139,6 +162,7 @@ private fun List>.groupByType(): GroupedNotifica
val roomIdToEventMap: MutableMap>> = LinkedHashMap()
val simpleEvents: MutableList> = ArrayList()
val invitationEvents: MutableList> = ArrayList()
+ val fallbackEvents: MutableList> = ArrayList()
forEach {
when (val event = it.event) {
is InviteNotifiableEvent -> invitationEvents.add(it.castedToEventType())
@@ -147,9 +171,12 @@ private fun List>.groupByType(): GroupedNotifica
roomEvents.add(it.castedToEventType())
}
is SimpleNotifiableEvent -> simpleEvents.add(it.castedToEventType())
+ is FallbackNotifiableEvent -> {
+ fallbackEvents.add(it.castedToEventType())
+ }
}
}
- return GroupedNotificationEvents(roomIdToEventMap, simpleEvents, invitationEvents)
+ return GroupedNotificationEvents(roomIdToEventMap, simpleEvents, invitationEvents, fallbackEvents)
}
@Suppress("UNCHECKED_CAST")
@@ -158,5 +185,6 @@ private fun ProcessedEvent.castedToEventT
data class GroupedNotificationEvents(
val roomEvents: Map>>,
val simpleEvents: List>,
- val invitationEvents: List>
+ val invitationEvents: List>,
+ val fallbackEvents: List>,
)
diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationState.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationState.kt
index 808bf4114b..4737e891aa 100644
--- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationState.kt
+++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationState.kt
@@ -39,8 +39,8 @@ class NotificationState(
) {
fun updateQueuedEvents(
- drawerManager: NotificationDrawerManager,
- action: NotificationDrawerManager.(NotificationEventQueue, List>) -> T
+ drawerManager: DefaultNotificationDrawerManager,
+ action: DefaultNotificationDrawerManager.(NotificationEventQueue, List>) -> T
): T {
return synchronized(queuedEvents) {
action(drawerManager, queuedEvents, renderedEvents)
diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/RoomGroupMessageCreator.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/RoomGroupMessageCreator.kt
index 5656b81dd9..5f2f6db263 100644
--- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/RoomGroupMessageCreator.kt
+++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/RoomGroupMessageCreator.kt
@@ -17,8 +17,12 @@
package io.element.android.libraries.push.impl.notifications
import android.graphics.Bitmap
+import android.graphics.Typeface
+import android.text.style.StyleSpan
import androidx.core.app.NotificationCompat
import androidx.core.app.Person
+import androidx.core.text.buildSpannedString
+import androidx.core.text.inSpans
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.push.impl.R
@@ -26,8 +30,6 @@ import io.element.android.libraries.push.impl.notifications.debug.annotateForDeb
import io.element.android.libraries.push.impl.notifications.factories.NotificationFactory
import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent
import io.element.android.services.toolbox.api.strings.StringProvider
-import me.gujun.android.span.Span
-import me.gujun.android.span.span
import timber.log.Timber
import javax.inject.Inject
@@ -151,30 +153,31 @@ class RoomGroupMessageCreator @Inject constructor(
}
}
- private fun createFirstMessageSummaryLine(event: NotifiableMessageEvent, roomName: String, roomIsDirect: Boolean): Span {
+ private fun createFirstMessageSummaryLine(event: NotifiableMessageEvent, roomName: String, roomIsDirect: Boolean): CharSequence {
return if (roomIsDirect) {
- span {
- span {
- textStyle = "bold"
- +String.format("%s: ", event.senderName)
+ buildSpannedString {
+ inSpans(StyleSpan(Typeface.BOLD)) {
+ append(event.senderName)
+ append(": ")
}
- +(event.description)
+ append(event.description)
}
} else {
- span {
- span {
- textStyle = "bold"
- +String.format("%s: %s ", roomName, event.senderName)
+ buildSpannedString {
+ inSpans(StyleSpan(Typeface.BOLD)) {
+ append(roomName)
+ append(": ")
+ event.senderName
+ append(" ")
}
- +(event.description)
+ append(event.description)
}
}
}
private suspend fun getRoomBitmap(events: List): Bitmap? {
// Use the last event (most recent?)
- return events.lastOrNull()
- ?.roomAvatarPath
+ return events.reversed().firstNotNullOfOrNull { it.roomAvatarPath }
?.let { bitmapLoader.getRoomBitmap(it) }
}
}
diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/SummaryGroupMessageCreator.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/SummaryGroupMessageCreator.kt
index 5a7f3d36e8..f999456107 100644
--- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/SummaryGroupMessageCreator.kt
+++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/SummaryGroupMessageCreator.kt
@@ -49,12 +49,14 @@ class SummaryGroupMessageCreator @Inject constructor(
roomNotifications: List,
invitationNotifications: List,
simpleNotifications: List,
+ fallbackNotifications: List,
useCompleteNotificationFormat: Boolean
): Notification {
val summaryInboxStyle = NotificationCompat.InboxStyle().also { style ->
roomNotifications.forEach { style.addLine(it.summaryLine.annotateForDebug(40)) }
invitationNotifications.forEach { style.addLine(it.summaryLine.annotateForDebug(41)) }
simpleNotifications.forEach { style.addLine(it.summaryLine.annotateForDebug(42)) }
+ fallbackNotifications.forEach { style.addLine(it.summaryLine) }
}
val summaryIsNoisy = roomNotifications.any { it.shouldBing } ||
diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationFactory.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationFactory.kt
index 9da47a6569..7b7b395862 100755
--- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationFactory.kt
+++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationFactory.kt
@@ -32,10 +32,9 @@ import io.element.android.libraries.push.impl.R
import io.element.android.libraries.push.impl.notifications.RoomEventGroupInfo
import io.element.android.libraries.push.impl.notifications.channels.NotificationChannels
import io.element.android.libraries.push.impl.notifications.debug.annotateForDebug
-import io.element.android.libraries.push.impl.notifications.factories.action.AcceptInvitationActionFactory
import io.element.android.libraries.push.impl.notifications.factories.action.MarkAsReadActionFactory
import io.element.android.libraries.push.impl.notifications.factories.action.QuickReplyActionFactory
-import io.element.android.libraries.push.impl.notifications.factories.action.RejectInvitationActionFactory
+import io.element.android.libraries.push.impl.notifications.model.FallbackNotifiableEvent
import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent
import io.element.android.libraries.push.impl.notifications.model.SimpleNotifiableEvent
import io.element.android.services.toolbox.api.strings.StringProvider
@@ -49,8 +48,6 @@ class NotificationFactory @Inject constructor(
private val pendingIntentFactory: PendingIntentFactory,
private val markAsReadActionFactory: MarkAsReadActionFactory,
private val quickReplyActionFactory: QuickReplyActionFactory,
- private val rejectInvitationActionFactory: RejectInvitationActionFactory,
- private val acceptInvitationActionFactory: AcceptInvitationActionFactory,
) {
/**
* Create a notification for a Room.
@@ -154,22 +151,12 @@ class NotificationFactory @Inject constructor(
.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_ALL)
.setSmallIcon(smallIcon)
.setColor(accentColor)
- .addAction(rejectInvitationActionFactory.create(inviteNotifiableEvent))
- .addAction(acceptInvitationActionFactory.create(inviteNotifiableEvent))
+ // TODO removed for now, will be added back later
+// .addAction(rejectInvitationActionFactory.create(inviteNotifiableEvent))
+// .addAction(acceptInvitationActionFactory.create(inviteNotifiableEvent))
.apply {
- /*
// Build the pending intent for when the notification is clicked
- val contentIntent = HomeActivity.newIntent(
- context,
- firstStartMainActivity = true,
- inviteNotificationRoomId = inviteNotifiableEvent.roomId
- )
- contentIntent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
- // pending intent get reused by system, this will mess up the extra params, so put unique info to avoid that
- contentIntent.data = createIgnoredUri(inviteNotifiableEvent.eventId)
- setContentIntent(PendingIntent.getActivity(context, 0, contentIntent, PendingIntentCompat.FLAG_IMMUTABLE))
-
- */
+ setContentIntent(pendingIntentFactory.createInviteListPendingIntent(inviteNotifiableEvent.sessionId))
if (inviteNotifiableEvent.noisy) {
// Compat
@@ -183,6 +170,12 @@ class NotificationFactory @Inject constructor(
} else {
priority = NotificationCompat.PRIORITY_LOW
}
+ setDeleteIntent(
+ pendingIntentFactory.createDismissInvitePendingIntent(
+ inviteNotifiableEvent.sessionId,
+ inviteNotifiableEvent.roomId,
+ )
+ )
setAutoCancel(true)
}
.build()
@@ -223,6 +216,39 @@ class NotificationFactory @Inject constructor(
.build()
}
+ fun createFallbackNotification(
+ fallbackNotifiableEvent: FallbackNotifiableEvent,
+ ): Notification {
+ val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color)
+ val smallIcon = R.drawable.ic_notification
+
+ val channelId = notificationChannels.getChannelIdForMessage(false)
+ return NotificationCompat.Builder(context, channelId)
+ .setOnlyAlertOnce(true)
+ .setContentTitle(buildMeta.applicationName.annotateForDebug(7))
+ .setContentText(fallbackNotifiableEvent.description.orEmpty().annotateForDebug(8))
+ .setGroup(fallbackNotifiableEvent.sessionId.value)
+ .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_ALL)
+ .setSmallIcon(smallIcon)
+ .setColor(accentColor)
+ .setAutoCancel(true)
+ // Ideally we'd use `createOpenRoomPendingIntent` here, but the broken notification might apply to an invite
+ // and the user won't have access to the room yet, resulting in an error screen.
+ .setContentIntent(pendingIntentFactory.createOpenSessionPendingIntent(fallbackNotifiableEvent.sessionId))
+ .setDeleteIntent(
+ pendingIntentFactory.createDismissEventPendingIntent(
+ fallbackNotifiableEvent.sessionId,
+ fallbackNotifiableEvent.roomId,
+ fallbackNotifiableEvent.eventId
+ )
+ )
+ .apply {
+ priority = NotificationCompat.PRIORITY_LOW
+ setAutoCancel(true)
+ }
+ .build()
+ }
+
/**
* Create the summary notification.
*/
diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/PendingIntentFactory.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/PendingIntentFactory.kt
index d04bae7e18..fc7586cbd7 100644
--- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/PendingIntentFactory.kt
+++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/PendingIntentFactory.kt
@@ -19,8 +19,10 @@ package io.element.android.libraries.push.impl.notifications.factories
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
+import androidx.core.app.PendingIntentCompat
import io.element.android.libraries.androidutils.uri.createIgnoredUri
import io.element.android.libraries.di.ApplicationContext
+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
@@ -39,19 +41,19 @@ class PendingIntentFactory @Inject constructor(
private val actionIds: NotificationActionIds,
) {
fun createOpenSessionPendingIntent(sessionId: SessionId): PendingIntent? {
- return createPendingIntent(sessionId = sessionId, roomId = null, threadId = null)
+ return createRoomPendingIntent(sessionId = sessionId, roomId = null, threadId = null)
}
fun createOpenRoomPendingIntent(sessionId: SessionId, roomId: RoomId): PendingIntent? {
- return createPendingIntent(sessionId = sessionId, roomId = roomId, threadId = null)
+ return createRoomPendingIntent(sessionId = sessionId, roomId = roomId, threadId = null)
}
fun createOpenThreadPendingIntent(roomInfo: RoomEventGroupInfo, threadId: ThreadId?): PendingIntent? {
- return createPendingIntent(sessionId = roomInfo.sessionId, roomId = roomInfo.roomId, threadId = threadId)
+ return createRoomPendingIntent(sessionId = roomInfo.sessionId, roomId = roomInfo.roomId, threadId = threadId)
}
- private fun createPendingIntent(sessionId: SessionId, roomId: RoomId?, threadId: ThreadId?): PendingIntent? {
- val intent = intentProvider.getViewIntent(sessionId = sessionId, roomId = roomId, threadId = threadId)
+ private fun createRoomPendingIntent(sessionId: SessionId, roomId: RoomId?, threadId: ThreadId?): PendingIntent? {
+ val intent = intentProvider.getViewRoomIntent(sessionId = sessionId, roomId = roomId, threadId = threadId)
return PendingIntent.getActivity(
context,
clock.epochMillis().toInt(),
@@ -87,6 +89,35 @@ class PendingIntentFactory @Inject constructor(
)
}
+ fun createDismissInvitePendingIntent(sessionId: SessionId, roomId: RoomId): PendingIntent {
+ val intent = Intent(context, NotificationBroadcastReceiver::class.java)
+ intent.action = actionIds.dismissInvite
+ intent.data = createIgnoredUri("deleteInvite/$sessionId/$roomId")
+ intent.putExtra(NotificationBroadcastReceiver.KEY_SESSION_ID, sessionId.value)
+ intent.putExtra(NotificationBroadcastReceiver.KEY_ROOM_ID, roomId.value)
+ return PendingIntent.getBroadcast(
+ context,
+ clock.epochMillis().toInt(),
+ intent,
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
+ )
+ }
+
+ fun createDismissEventPendingIntent(sessionId: SessionId, roomId: RoomId, eventId: EventId): PendingIntent {
+ val intent = Intent(context, NotificationBroadcastReceiver::class.java)
+ intent.action = actionIds.dismissEvent
+ intent.data = createIgnoredUri("deleteEvent/$sessionId/$roomId")
+ intent.putExtra(NotificationBroadcastReceiver.KEY_SESSION_ID, sessionId.value)
+ intent.putExtra(NotificationBroadcastReceiver.KEY_ROOM_ID, roomId.value)
+ intent.putExtra(NotificationBroadcastReceiver.KEY_EVENT_ID, eventId.value)
+ return PendingIntent.getBroadcast(
+ context,
+ clock.epochMillis().toInt(),
+ intent,
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
+ )
+ }
+
fun createTestPendingIntent(): PendingIntent? {
val testActionIntent = Intent(context, TestNotificationReceiver::class.java)
testActionIntent.action = actionIds.diagnostic
@@ -97,4 +128,9 @@ class PendingIntentFactory @Inject constructor(
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
}
+
+ fun createInviteListPendingIntent(sessionId: SessionId): PendingIntent {
+ val intent = intentProvider.getInviteListIntent(sessionId)
+ return PendingIntentCompat.getActivity(context, 0, intent, 0, false)
+ }
}
diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/FallbackNotifiableEvent.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/FallbackNotifiableEvent.kt
new file mode 100644
index 0000000000..fe6cc537d0
--- /dev/null
+++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/FallbackNotifiableEvent.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.libraries.push.impl.notifications.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
+
+/**
+ * Used for notifications with events that couldn't be retrieved or decrypted, so we don't know their contents.
+ * These are created separately from message notifications, so they can be displayed differently.
+ */
+data class FallbackNotifiableEvent(
+ override val sessionId: SessionId,
+ override val roomId: RoomId,
+ override val eventId: EventId,
+ override val editedEventId: EventId?,
+ override val description: String?,
+ override val canBeReplaced: Boolean,
+ override val isRedacted: Boolean,
+ override val isUpdated: Boolean,
+ val timestamp: Long,
+) : NotifiableEvent
diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/InviteNotifiableEvent.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/InviteNotifiableEvent.kt
index 4524d27ac2..6b562a434e 100644
--- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/InviteNotifiableEvent.kt
+++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/InviteNotifiableEvent.kt
@@ -27,8 +27,8 @@ data class InviteNotifiableEvent(
override val canBeReplaced: Boolean,
val roomName: String?,
val noisy: Boolean,
- val title: String,
- val description: String,
+ val title: String?,
+ override val description: String,
val type: String?,
val timestamp: Long,
val soundName: String?,
diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/NotifiableEvent.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/NotifiableEvent.kt
index b1bb7cd032..ddfbbf8b07 100644
--- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/NotifiableEvent.kt
+++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/NotifiableEvent.kt
@@ -29,6 +29,7 @@ sealed interface NotifiableEvent : Serializable {
val roomId: RoomId
val eventId: EventId
val editedEventId: EventId?
+ val description: String?
// Used to know if event should be replaced with the one coming from eventstream
val canBeReplaced: Boolean
diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/NotifiableMessageEvent.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/NotifiableMessageEvent.kt
index 1216e0fe12..7730066d31 100644
--- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/NotifiableMessageEvent.kt
+++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/NotifiableMessageEvent.kt
@@ -16,6 +16,8 @@
package io.element.android.libraries.push.impl.notifications.model
import android.net.Uri
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.ProcessLifecycleOwner
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
@@ -54,7 +56,7 @@ data class NotifiableMessageEvent(
) : NotifiableEvent {
val type: String = EventType.MESSAGE
- val description: String = body ?: ""
+ override val description: String = body ?: ""
val title: String = senderName ?: ""
// TODO EAx The image has to be downloaded and expose using the file provider.
@@ -64,12 +66,21 @@ data class NotifiableMessageEvent(
get() = imageUriString?.let { Uri.parse(it) }
}
-fun NotifiableMessageEvent.shouldIgnoreMessageEventInRoom(
+/**
+ * Used to check if a notification should be ignored based on the current app and navigation state.
+ */
+fun NotifiableEvent.shouldIgnoreEventInRoom(
appNavigationState: AppNavigationState?
): Boolean {
val currentSessionId = appNavigationState?.currentSessionId() ?: return false
return when (val currentRoomId = appNavigationState.currentRoomId()) {
null -> false
- else -> sessionId == currentSessionId && roomId == currentRoomId && threadId == appNavigationState.currentThreadId()
+ else -> isAppInForeground
+ && sessionId == currentSessionId
+ && roomId == currentRoomId
+ && (this as? NotifiableMessageEvent)?.threadId == appNavigationState.currentThreadId()
}
}
+
+private val isAppInForeground: Boolean
+ get() = ProcessLifecycleOwner.get().lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)
diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/SimpleNotifiableEvent.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/SimpleNotifiableEvent.kt
index 5cfd04474a..f252765530 100644
--- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/SimpleNotifiableEvent.kt
+++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/SimpleNotifiableEvent.kt
@@ -26,7 +26,7 @@ data class SimpleNotifiableEvent(
override val editedEventId: EventId?,
val noisy: Boolean,
val title: String,
- val description: String,
+ override val description: String,
val type: String?,
val timestamp: Long,
val soundName: String?,
diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandler.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandler.kt
index 1bc0ceae93..3ad848aeb4 100644
--- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandler.kt
+++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandler.kt
@@ -31,7 +31,7 @@ import io.element.android.libraries.push.impl.PushersManager
import io.element.android.libraries.push.impl.log.pushLoggerTag
import io.element.android.libraries.push.impl.notifications.NotifiableEventResolver
import io.element.android.libraries.push.impl.notifications.NotificationActionIds
-import io.element.android.libraries.push.impl.notifications.NotificationDrawerManager
+import io.element.android.libraries.push.impl.notifications.DefaultNotificationDrawerManager
import io.element.android.libraries.push.impl.store.DefaultPushDataStore
import io.element.android.libraries.pushproviders.api.PushData
import io.element.android.libraries.pushproviders.api.PushHandler
@@ -48,7 +48,7 @@ private val loggerTag = LoggerTag("PushHandler", pushLoggerTag)
@ContributesBinding(AppScope::class)
class DefaultPushHandler @Inject constructor(
- private val notificationDrawerManager: NotificationDrawerManager,
+ private val defaultNotificationDrawerManager: DefaultNotificationDrawerManager,
private val notifiableEventResolver: NotifiableEventResolver,
private val defaultPushDataStore: DefaultPushDataStore,
private val userPushStoreFactory: UserPushStoreFactory,
@@ -121,9 +121,9 @@ class DefaultPushHandler @Inject constructor(
return
}
- val notificationData = notifiableEventResolver.resolveEvent(userId, pushData.roomId, pushData.eventId)
+ val notifiableEvent = notifiableEventResolver.resolveEvent(userId, pushData.roomId, pushData.eventId)
- if (notificationData == null) {
+ if (notifiableEvent == null) {
Timber.w("Unable to get a notification data")
return
}
@@ -135,7 +135,7 @@ class DefaultPushHandler @Inject constructor(
return
}
- notificationDrawerManager.onNotifiableEventReceived(notificationData)
+ defaultNotificationDrawerManager.onNotifiableEventReceived(notifiableEvent)
} catch (e: Exception) {
Timber.tag(loggerTag.value).e(e, "## handleInternal() failed")
}
diff --git a/libraries/push/impl/src/main/res/values/localazy.xml b/libraries/push/impl/src/main/res/values/localazy.xml
index 987728304a..0d66ac1336 100644
--- a/libraries/push/impl/src/main/res/values/localazy.xml
+++ b/libraries/push/impl/src/main/res/values/localazy.xml
@@ -4,6 +4,7 @@
"Listening for events"
"Noisy notifications"
"Silent notifications"
+ "Notification"
"** Failed to send - please open room"
"Join"
"Reject"
@@ -47,6 +48,5 @@
"Background synchronization"
"Google Services"
"No valid Google Play Services found. Notifications may not work properly."
- "Notification"
"Quick reply"
diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventProcessorTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventProcessorTest.kt
index 38f2edd476..a1398ef429 100644
--- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventProcessorTest.kt
+++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventProcessorTest.kt
@@ -120,6 +120,7 @@ class NotifiableEventProcessorTest {
@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))
+ events.forEach { outdatedDetector.givenEventIsOutOfDate(it) }
val result = eventProcessor.process(events, VIEWING_A_ROOM, renderedEvents = emptyList())
@@ -133,6 +134,7 @@ class NotifiableEventProcessorTest {
@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))
+ events.forEach { outdatedDetector.givenEventIsOutOfDate(it) }
val result = eventProcessor.process(events, VIEWING_A_THREAD, renderedEvents = emptyList())
diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationEventQueueTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationEventQueueTest.kt
index eebf420591..f9b63eb9dc 100644
--- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationEventQueueTest.kt
+++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationEventQueueTest.kt
@@ -208,7 +208,7 @@ class NotificationEventQueueTest {
)
)
- queue.clearMemberShipNotificationForRoom(A_SESSION_ID, A_ROOM_ID)
+ queue.clearMembershipNotificationForRoom(A_SESSION_ID, A_ROOM_ID)
assertThat(queue.rawEvents()).isEqualTo(listOf(aNotifiableMessageEvent(roomId = A_ROOM_ID)))
}
diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRendererTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRendererTest.kt
index c109edb40a..80875406c7 100644
--- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRendererTest.kt
+++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRendererTest.kt
@@ -33,7 +33,7 @@ private const val MY_USER_AVATAR_URL = "avatar-url"
private const val USE_COMPLETE_NOTIFICATION_FORMAT = true
private val AN_EVENT_LIST = listOf>()
-private val A_PROCESSED_EVENTS = GroupedNotificationEvents(emptyMap(), emptyList(), emptyList())
+private val A_PROCESSED_EVENTS = GroupedNotificationEvents(emptyMap(), emptyList(), emptyList(), emptyList())
private val A_SUMMARY_NOTIFICATION = SummaryNotification.Update(mockk())
private val A_REMOVE_SUMMARY_NOTIFICATION = SummaryNotification.Removed
private val A_NOTIFICATION = mockk()
@@ -202,13 +202,14 @@ class NotificationRendererTest {
}
private fun givenNoNotifications() {
- givenNotifications(emptyList(), emptyList(), emptyList(), USE_COMPLETE_NOTIFICATION_FORMAT, A_REMOVE_SUMMARY_NOTIFICATION)
+ givenNotifications(emptyList(), emptyList(), emptyList(), emptyList(), USE_COMPLETE_NOTIFICATION_FORMAT, A_REMOVE_SUMMARY_NOTIFICATION)
}
private fun givenNotifications(
roomNotifications: List = emptyList(),
invitationNotifications: List = emptyList(),
simpleNotifications: List = emptyList(),
+ fallbackNotifications: List = emptyList(),
useCompleteNotificationFormat: Boolean = USE_COMPLETE_NOTIFICATION_FORMAT,
summaryNotification: SummaryNotification = A_SUMMARY_NOTIFICATION
) {
@@ -219,6 +220,7 @@ class NotificationRendererTest {
roomNotifications = roomNotifications,
invitationNotifications = invitationNotifications,
simpleNotifications = simpleNotifications,
+ fallbackNotifications = fallbackNotifications,
summaryNotification = summaryNotification
)
}
diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeNotificationFactory.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeNotificationFactory.kt
index 09957e2cf2..60b9e10c3d 100644
--- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeNotificationFactory.kt
+++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeNotificationFactory.kt
@@ -36,12 +36,14 @@ class FakeNotificationFactory {
roomNotifications: List,
invitationNotifications: List,
simpleNotifications: List,
+ fallbackNotifications: List,
summaryNotification: SummaryNotification
) {
with(instance) {
coEvery { groupedEvents.roomEvents.toNotifications(matrixUser) } returns roomNotifications
every { groupedEvents.invitationEvents.toNotifications() } returns invitationNotifications
every { groupedEvents.simpleEvents.toNotifications() } returns simpleNotifications
+ every { groupedEvents.fallbackEvents.toNotifications() } returns fallbackNotifications
every {
createSummaryNotification(
@@ -49,6 +51,7 @@ class FakeNotificationFactory {
roomNotifications,
invitationNotifications,
simpleNotifications,
+ fallbackNotifications,
useCompleteNotificationFormat
)
} returns summaryNotification
diff --git a/libraries/push/test/build.gradle.kts b/libraries/push/test/build.gradle.kts
new file mode 100644
index 0000000000..9fccadb9be
--- /dev/null
+++ b/libraries/push/test/build.gradle.kts
@@ -0,0 +1,29 @@
+/*
+ * 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.
+ */
+
+plugins {
+ id("io.element.android-library")
+}
+
+android {
+ namespace = "io.element.android.libraries.push.test"
+}
+
+dependencies {
+ api(projects.libraries.push.api)
+ implementation(projects.libraries.matrix.api)
+ implementation(projects.tests.testutils)
+}
diff --git a/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/notifications/FakeNotificationDrawerManager.kt b/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/notifications/FakeNotificationDrawerManager.kt
new file mode 100644
index 0000000000..1531d2df48
--- /dev/null
+++ b/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/notifications/FakeNotificationDrawerManager.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.libraries.push.test.notifications
+
+import io.element.android.libraries.matrix.api.core.RoomId
+import io.element.android.libraries.matrix.api.core.SessionId
+import io.element.android.libraries.push.api.notifications.NotificationDrawerManager
+
+class FakeNotificationDrawerManager : NotificationDrawerManager {
+ private val clearMemberShipNotificationForSessionCallsCount = mutableMapOf()
+ private val clearMemberShipNotificationForRoomCallsCount = mutableMapOf()
+
+ override fun clearMembershipNotificationForSession(sessionId: SessionId) {
+ clearMemberShipNotificationForSessionCallsCount.merge(sessionId.value, 1) { oldValue, value -> oldValue + value }
+ }
+
+ override fun clearMembershipNotificationForRoom(sessionId: SessionId, roomId: RoomId) {
+ val key = getMembershipNotificationKey(sessionId, roomId)
+ clearMemberShipNotificationForRoomCallsCount.merge(key, 1) { oldValue, value -> oldValue + value }
+ }
+
+ fun getClearMembershipNotificationForSessionCount(sessionId: SessionId): Int {
+ return clearMemberShipNotificationForRoomCallsCount[sessionId.value] ?: 0
+ }
+
+ fun getClearMembershipNotificationForRoomCount(sessionId: SessionId, roomId: RoomId): Int {
+ val key = getMembershipNotificationKey(sessionId, roomId)
+ return clearMemberShipNotificationForRoomCallsCount[key] ?: 0
+ }
+
+ private fun getMembershipNotificationKey(sessionId: SessionId, roomId: RoomId): String {
+ return "$sessionId-$roomId"
+ }
+}
diff --git a/settings.gradle.kts b/settings.gradle.kts
index da6c0affd5..408c9e2934 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -36,8 +36,6 @@ dependencyResolutionManagement {
includeModule("com.github.matrix-org", "matrix-analytics-events")
}
}
- //noinspection JcenterRepositoryObsolete
- jcenter()
flatDir {
dirs("libraries/matrix/libs")
}