From a0c1f2c18a5cfdce2408b4af6782271b38c9d6d4 Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Mon, 10 Jul 2023 14:34:58 +0200 Subject: [PATCH] Display room invitation notification (#735) * Notifications: Add some extra mappings so we keep the original contents and can pass it later to an UI layer * Fix notifications not appearing for a room if the app was on that room when it went to background. * Modernize how we create spannable strings for notifications, remove unneeded dependency * Remove actions from invite notifications temporarily * Add `NotificationDrawerManager` interface to be able to clear membership notifications when accepting or rejecting a room invite * Fix tests * Add comment to clarify some weird behaviours * Address review comments * Set circle shape for `largeBitmap` in message notifications * Fix no avatar in DM rooms * Fix rebase issues * Add invite list pending intent: - Refactor pending intents. - Make `DeepLinkData` a sealed interface. - Fix and add tests. * Rename `navigate__` functions to `attach__` * Add an extra test case for the `InviteList` deep link * Address most review comments. * Fix rebase issue * Add fallback notification type, allow dismissing invite notifications. Fallback notifications have a different underlying type and can be dismissed at will. * Fix tests --- app/src/main/AndroidManifest.xml | 6 +- .../element/android/x/ElementXApplication.kt | 2 +- .../android/x/intent/IntentProviderImpl.kt | 11 +- .../android/appnav/LoggedInFlowNode.kt | 15 +- .../io/element/android/appnav/RootFlowNode.kt | 11 +- .../android/appnav/intent/IntentResolver.kt | 2 + features/invitelist/impl/build.gradle.kts | 2 + .../invitelist/impl/InviteListPresenter.kt | 7 +- .../impl/InviteListPresenterTests.kt | 76 +++--- gradle/libs.versions.toml | 1 - .../android/libraries/deeplink/Constants.kt | 4 + .../libraries/deeplink/DeepLinkCreator.kt | 11 +- .../libraries/deeplink/DeeplinkData.kt | 18 +- .../libraries/deeplink/DeeplinkParser.kt | 24 +- .../libraries/deeplink/DeepLinkCreatorTest.kt | 15 +- .../libraries/deeplink/DeeplinkParserTest.kt | 10 +- .../api/notification/NotificationData.kt | 57 ++++- .../impl/notification/NotificationMapper.kt | 24 +- .../notification/RustNotificationService.kt | 3 +- .../impl/notification/TimelineEventMapper.kt | 101 ++++---- .../timeline/item/event/EventMessageMapper.kt | 63 +++-- .../NotificationDrawerManager.kt | 25 ++ libraries/push/impl/build.gradle.kts | 5 +- .../libraries/push/impl/DefaultPushService.kt | 7 +- .../libraries/push/impl/di/PushBindsModule.kt | 33 +++ .../push/impl/intent/IntentProvider.kt | 9 +- ...kt => DefaultNotificationDrawerManager.kt} | 31 ++- .../notifications/NotifiableEventProcessor.kt | 12 +- .../notifications/NotifiableEventResolver.kt | 228 +++++++++++++----- .../notifications/NotificationActionIds.kt | 2 + .../notifications/NotificationBitmapLoader.kt | 1 + .../NotificationBroadcastReceiver.kt | 21 +- .../notifications/NotificationEventQueue.kt | 17 +- .../impl/notifications/NotificationFactory.kt | 21 ++ .../notifications/NotificationIdProvider.kt | 5 + .../notifications/NotificationRenderer.kt | 40 ++- .../impl/notifications/NotificationState.kt | 4 +- .../notifications/RoomGroupMessageCreator.kt | 33 +-- .../SummaryGroupMessageCreator.kt | 2 + .../factories/NotificationFactory.kt | 62 +++-- .../factories/PendingIntentFactory.kt | 46 +++- .../model/FallbackNotifiableEvent.kt | 37 +++ .../model/InviteNotifiableEvent.kt | 4 +- .../notifications/model/NotifiableEvent.kt | 1 + .../model/NotifiableMessageEvent.kt | 17 +- .../model/SimpleNotifiableEvent.kt | 2 +- .../push/impl/push/DefaultPushHandler.kt | 10 +- .../impl/src/main/res/values/localazy.xml | 2 +- .../NotifiableEventProcessorTest.kt | 2 + .../NotificationEventQueueTest.kt | 2 +- .../notifications/NotificationRendererTest.kt | 6 +- .../fake/FakeNotificationFactory.kt | 3 + libraries/push/test/build.gradle.kts | 29 +++ .../FakeNotificationDrawerManager.kt | 48 ++++ settings.gradle.kts | 2 - 55 files changed, 905 insertions(+), 327 deletions(-) create mode 100644 libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/notifications/NotificationDrawerManager.kt create mode 100644 libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/di/PushBindsModule.kt rename libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/{NotificationDrawerManager.kt => DefaultNotificationDrawerManager.kt} (91%) create mode 100644 libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/FallbackNotifiableEvent.kt create mode 100644 libraries/push/test/build.gradle.kts create mode 100644 libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/notifications/FakeNotificationDrawerManager.kt 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") }