mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-10-31 16:22:49 +00:00
Dismiss notifications for rooms fully read in other devices (#7846)
* Dismiss notifications for rooms fully read in other devices
This commit is contained in:
committed by
GitHub
parent
774b4087f5
commit
b76548f9a2
@@ -8,28 +8,51 @@
|
|||||||
|
|
||||||
package io.element.android.appnav
|
package io.element.android.appnav
|
||||||
|
|
||||||
import dev.zacsweers.metro.Inject
|
import androidx.annotation.VisibleForTesting
|
||||||
|
import dev.zacsweers.metro.Assisted
|
||||||
|
import dev.zacsweers.metro.AssistedFactory
|
||||||
|
import dev.zacsweers.metro.AssistedInject
|
||||||
|
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||||
|
import io.element.android.libraries.core.coroutine.childScope
|
||||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
||||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
|
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
|
||||||
|
import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
|
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
|
||||||
|
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.MembershipChange
|
import io.element.android.libraries.matrix.api.timeline.item.event.MembershipChange
|
||||||
|
import io.element.android.libraries.push.api.notifications.NotificationCleaner
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.filter
|
import kotlinx.coroutines.flow.filter
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
|
||||||
@Inject
|
@AssistedInject
|
||||||
class LoggedInEventProcessor(
|
class LoggedInEventProcessor(
|
||||||
private val snackbarDispatcher: SnackbarDispatcher,
|
@Assisted private val snackbarDispatcher: SnackbarDispatcher,
|
||||||
private val roomMembershipObserver: RoomMembershipObserver,
|
private val roomMembershipObserver: RoomMembershipObserver,
|
||||||
|
private val sessionId: SessionId,
|
||||||
|
private val roomListService: RoomListService,
|
||||||
|
private val notificationCleaner: NotificationCleaner,
|
||||||
|
private val dispatchers: CoroutineDispatchers
|
||||||
) {
|
) {
|
||||||
private var observingJob: Job? = null
|
@AssistedFactory
|
||||||
|
interface Factory {
|
||||||
|
fun create(snackbarDispatcher: SnackbarDispatcher): LoggedInEventProcessor
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||||
|
internal var currentChildScope: CoroutineScope? = null
|
||||||
|
|
||||||
fun observeEvents(coroutineScope: CoroutineScope) {
|
fun observeEvents(coroutineScope: CoroutineScope) {
|
||||||
observingJob = roomMembershipObserver.updates
|
if (currentChildScope != null) return
|
||||||
|
|
||||||
|
val childScope = coroutineScope.childScope(dispatchers.computation, "LoggedInEventProcessor")
|
||||||
|
.also { currentChildScope = it }
|
||||||
|
|
||||||
|
roomMembershipObserver.updates
|
||||||
.filter { !it.isUserInRoom }
|
.filter { !it.isUserInRoom }
|
||||||
.distinctUntilChanged()
|
.distinctUntilChanged()
|
||||||
.onEach { roomMemberShipUpdate ->
|
.onEach { roomMemberShipUpdate ->
|
||||||
@@ -48,12 +71,20 @@ class LoggedInEventProcessor(
|
|||||||
else -> Unit
|
else -> Unit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.launchIn(coroutineScope)
|
.launchIn(childScope)
|
||||||
|
|
||||||
|
// Use the room list service state as a 'heartbeat' update to check existing notifications
|
||||||
|
roomListService.state
|
||||||
|
.filter { it == RoomListService.State.Running }
|
||||||
|
.onEach {
|
||||||
|
notificationCleaner.clearReadRoomsNotifications(sessionId)
|
||||||
|
}
|
||||||
|
.launchIn(childScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun stopObserving() {
|
fun stopObserving() {
|
||||||
observingJob?.cancel()
|
currentChildScope?.cancel()
|
||||||
observingJob = null
|
currentChildScope = null
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun displayMessage(message: Int) {
|
private fun displayMessage(message: Int) {
|
||||||
|
|||||||
@@ -157,6 +157,7 @@ class LoggedInFlowNode(
|
|||||||
private val createRoomEntryPoint: CreateRoomEntryPoint,
|
private val createRoomEntryPoint: CreateRoomEntryPoint,
|
||||||
private val activeLiveLocationShareManager: ActiveLiveLocationShareManager,
|
private val activeLiveLocationShareManager: ActiveLiveLocationShareManager,
|
||||||
private val customMapTilerConfigProvider: CustomMapTilerConfigProvider,
|
private val customMapTilerConfigProvider: CustomMapTilerConfigProvider,
|
||||||
|
private val loggedInEventProcessorFactory: LoggedInEventProcessor.Factory,
|
||||||
) : BaseFlowNode<LoggedInFlowNode.NavTarget>(
|
) : BaseFlowNode<LoggedInFlowNode.NavTarget>(
|
||||||
backstack = BackStack(
|
backstack = BackStack(
|
||||||
initialElement = NavTarget.Placeholder,
|
initialElement = NavTarget.Placeholder,
|
||||||
@@ -175,10 +176,7 @@ class LoggedInFlowNode(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private val callback: Callback = callback()
|
private val callback: Callback = callback()
|
||||||
private val loggedInFlowProcessor = LoggedInEventProcessor(
|
private val loggedInFlowProcessor = loggedInEventProcessorFactory.create(snackbarDispatcher)
|
||||||
snackbarDispatcher = snackbarDispatcher,
|
|
||||||
roomMembershipObserver = matrixClient.roomMembershipObserver,
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Display the incoming verification request, the app is expected to be in foreground.
|
* Display the incoming verification request, the app is expected to be in foreground.
|
||||||
|
|||||||
@@ -0,0 +1,197 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2026 Element Creations Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.appnav
|
||||||
|
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
||||||
|
import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
|
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
||||||
|
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
|
||||||
|
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
||||||
|
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||||
|
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||||
|
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
|
||||||
|
import io.element.android.libraries.push.test.notifications.FakeNotificationCleaner
|
||||||
|
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||||
|
import io.element.android.tests.testutils.lambda.value
|
||||||
|
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.isActive
|
||||||
|
import kotlinx.coroutines.test.TestScope
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class LoggedInEventProcessorTest {
|
||||||
|
@Test
|
||||||
|
fun `test observeEvents with left membership change`() = runTest {
|
||||||
|
val snackbarDispatcher = SnackbarDispatcher()
|
||||||
|
val roomMembershipObserver = RoomMembershipObserver()
|
||||||
|
|
||||||
|
val loggedInEventProcessor = createLoggedInEventProcessor(
|
||||||
|
snackbarDispatcher = snackbarDispatcher,
|
||||||
|
roomMembershipObserver = roomMembershipObserver,
|
||||||
|
)
|
||||||
|
|
||||||
|
loggedInEventProcessor.observeEvents(backgroundScope)
|
||||||
|
|
||||||
|
testScheduler.runCurrent()
|
||||||
|
|
||||||
|
roomMembershipObserver.notifyUserLeftRoom(
|
||||||
|
roomId = A_ROOM_ID,
|
||||||
|
isSpace = false,
|
||||||
|
membershipBeforeLeft = CurrentUserMembership.JOINED,
|
||||||
|
)
|
||||||
|
|
||||||
|
testScheduler.runCurrent()
|
||||||
|
|
||||||
|
// Verify that the snackbar message was displayed
|
||||||
|
assertThat(snackbarDispatcher.snackbarMessage.first()).isNotNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `test observeEvents with left membership change for space`() = runTest {
|
||||||
|
val snackbarDispatcher = SnackbarDispatcher()
|
||||||
|
val roomMembershipObserver = RoomMembershipObserver()
|
||||||
|
|
||||||
|
val loggedInEventProcessor = createLoggedInEventProcessor(
|
||||||
|
snackbarDispatcher = snackbarDispatcher,
|
||||||
|
roomMembershipObserver = roomMembershipObserver,
|
||||||
|
)
|
||||||
|
|
||||||
|
loggedInEventProcessor.observeEvents(backgroundScope)
|
||||||
|
|
||||||
|
testScheduler.runCurrent()
|
||||||
|
|
||||||
|
roomMembershipObserver.notifyUserLeftRoom(
|
||||||
|
roomId = A_ROOM_ID,
|
||||||
|
isSpace = true,
|
||||||
|
membershipBeforeLeft = CurrentUserMembership.JOINED,
|
||||||
|
)
|
||||||
|
|
||||||
|
testScheduler.runCurrent()
|
||||||
|
|
||||||
|
// Verify that the snackbar message was displayed
|
||||||
|
assertThat(snackbarDispatcher.snackbarMessage.first()).isNotNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `test observeEvents with invitation rejected membership change`() = runTest {
|
||||||
|
val snackbarDispatcher = SnackbarDispatcher()
|
||||||
|
val roomMembershipObserver = RoomMembershipObserver()
|
||||||
|
|
||||||
|
val loggedInEventProcessor = createLoggedInEventProcessor(
|
||||||
|
snackbarDispatcher = snackbarDispatcher,
|
||||||
|
roomMembershipObserver = roomMembershipObserver,
|
||||||
|
)
|
||||||
|
|
||||||
|
loggedInEventProcessor.observeEvents(backgroundScope)
|
||||||
|
|
||||||
|
testScheduler.runCurrent()
|
||||||
|
|
||||||
|
roomMembershipObserver.notifyUserLeftRoom(
|
||||||
|
roomId = A_ROOM_ID,
|
||||||
|
isSpace = false,
|
||||||
|
membershipBeforeLeft = CurrentUserMembership.INVITED,
|
||||||
|
)
|
||||||
|
|
||||||
|
testScheduler.runCurrent()
|
||||||
|
|
||||||
|
// Verify that the snackbar message was displayed
|
||||||
|
assertThat(snackbarDispatcher.snackbarMessage.first()).isNotNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `test observeEvents with knock retracted membership change`() = runTest {
|
||||||
|
val snackbarDispatcher = SnackbarDispatcher()
|
||||||
|
val roomMembershipObserver = RoomMembershipObserver()
|
||||||
|
|
||||||
|
val loggedInEventProcessor = createLoggedInEventProcessor(
|
||||||
|
snackbarDispatcher = snackbarDispatcher,
|
||||||
|
roomMembershipObserver = roomMembershipObserver,
|
||||||
|
)
|
||||||
|
|
||||||
|
loggedInEventProcessor.observeEvents(backgroundScope)
|
||||||
|
|
||||||
|
testScheduler.runCurrent()
|
||||||
|
|
||||||
|
roomMembershipObserver.notifyUserLeftRoom(
|
||||||
|
roomId = A_ROOM_ID,
|
||||||
|
isSpace = false,
|
||||||
|
membershipBeforeLeft = CurrentUserMembership.KNOCKED,
|
||||||
|
)
|
||||||
|
|
||||||
|
testScheduler.runCurrent()
|
||||||
|
|
||||||
|
// Verify that the snackbar message was displayed
|
||||||
|
assertThat(snackbarDispatcher.snackbarMessage.first()).isNotNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `test stopObserving cancels the child scope`() = runTest {
|
||||||
|
val snackbarDispatcher = SnackbarDispatcher()
|
||||||
|
val roomMembershipObserver = RoomMembershipObserver()
|
||||||
|
|
||||||
|
val loggedInEventProcessor = createLoggedInEventProcessor(
|
||||||
|
snackbarDispatcher = snackbarDispatcher,
|
||||||
|
roomMembershipObserver = roomMembershipObserver,
|
||||||
|
)
|
||||||
|
|
||||||
|
loggedInEventProcessor.observeEvents(backgroundScope)
|
||||||
|
|
||||||
|
assertThat(loggedInEventProcessor.currentChildScope?.isActive).isTrue()
|
||||||
|
|
||||||
|
testScheduler.runCurrent()
|
||||||
|
|
||||||
|
// Stop observing events
|
||||||
|
loggedInEventProcessor.stopObserving()
|
||||||
|
|
||||||
|
// Verify that the child scope is cancelled (the scope is null)
|
||||||
|
assertThat(loggedInEventProcessor.currentChildScope).isNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `test notifications are cleared when room list service state is running`() = runTest {
|
||||||
|
val clearReadNotificationsLambda = lambdaRecorder { _: SessionId -> }
|
||||||
|
val snackbarDispatcher = SnackbarDispatcher()
|
||||||
|
val notificationCleaner = FakeNotificationCleaner(clearReadRoomsNotificationsLambda = clearReadNotificationsLambda)
|
||||||
|
val roomListService = FakeRoomListService()
|
||||||
|
|
||||||
|
val loggedInEventProcessor = createLoggedInEventProcessor(
|
||||||
|
snackbarDispatcher = snackbarDispatcher,
|
||||||
|
notificationCleaner = notificationCleaner,
|
||||||
|
roomListService = roomListService,
|
||||||
|
)
|
||||||
|
|
||||||
|
loggedInEventProcessor.observeEvents(backgroundScope)
|
||||||
|
|
||||||
|
testScheduler.runCurrent()
|
||||||
|
|
||||||
|
// Simulate the room list service state changing to Running
|
||||||
|
roomListService.postState(RoomListService.State.Running)
|
||||||
|
|
||||||
|
testScheduler.runCurrent()
|
||||||
|
|
||||||
|
// Verify that the notifications were cleared
|
||||||
|
clearReadNotificationsLambda.assertions().isCalledOnce().with(value(A_SESSION_ID))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun TestScope.createLoggedInEventProcessor(
|
||||||
|
snackbarDispatcher: SnackbarDispatcher,
|
||||||
|
roomMembershipObserver: RoomMembershipObserver = RoomMembershipObserver(),
|
||||||
|
sessionId: SessionId = A_SESSION_ID,
|
||||||
|
roomListService: FakeRoomListService = FakeRoomListService(),
|
||||||
|
notificationCleaner: FakeNotificationCleaner = FakeNotificationCleaner(),
|
||||||
|
) = LoggedInEventProcessor(
|
||||||
|
snackbarDispatcher = snackbarDispatcher,
|
||||||
|
roomMembershipObserver = roomMembershipObserver,
|
||||||
|
sessionId = sessionId,
|
||||||
|
roomListService = roomListService,
|
||||||
|
notificationCleaner = notificationCleaner,
|
||||||
|
dispatchers = testCoroutineDispatchers(),
|
||||||
|
)
|
||||||
|
}
|
||||||
+7
@@ -65,4 +65,11 @@ interface NotificationCleaner {
|
|||||||
* @param roomId the room whose notifications are dismissed.
|
* @param roomId the room whose notifications are dismissed.
|
||||||
*/
|
*/
|
||||||
fun clearMembershipNotificationForRoom(sessionId: SessionId, roomId: RoomId)
|
fun clearMembershipNotificationForRoom(sessionId: SessionId, roomId: RoomId)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dismisses the notifications of every read room.
|
||||||
|
*
|
||||||
|
* @param sessionId the session whose notifications should be dismissed.
|
||||||
|
*/
|
||||||
|
fun clearReadRoomsNotifications(sessionId: SessionId)
|
||||||
}
|
}
|
||||||
|
|||||||
+27
-1
@@ -12,6 +12,7 @@ import dev.zacsweers.metro.AppScope
|
|||||||
import dev.zacsweers.metro.ContributesBinding
|
import dev.zacsweers.metro.ContributesBinding
|
||||||
import dev.zacsweers.metro.SingleIn
|
import dev.zacsweers.metro.SingleIn
|
||||||
import io.element.android.features.lockscreen.api.LockScreenService
|
import io.element.android.features.lockscreen.api.LockScreenService
|
||||||
|
import io.element.android.libraries.core.data.tryOrNull
|
||||||
import io.element.android.libraries.di.annotations.AppCoroutineScope
|
import io.element.android.libraries.di.annotations.AppCoroutineScope
|
||||||
import io.element.android.libraries.matrix.api.MatrixClientProvider
|
import io.element.android.libraries.matrix.api.MatrixClientProvider
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
@@ -53,7 +54,7 @@ class DefaultNotificationDrawerManager(
|
|||||||
private val notificationRenderer: NotificationRenderer,
|
private val notificationRenderer: NotificationRenderer,
|
||||||
private val appNavigationStateService: AppNavigationStateService,
|
private val appNavigationStateService: AppNavigationStateService,
|
||||||
@AppCoroutineScope
|
@AppCoroutineScope
|
||||||
coroutineScope: CoroutineScope,
|
private val coroutineScope: CoroutineScope,
|
||||||
private val matrixClientProvider: MatrixClientProvider,
|
private val matrixClientProvider: MatrixClientProvider,
|
||||||
private val imageLoaderHolder: ImageLoaderHolder,
|
private val imageLoaderHolder: ImageLoaderHolder,
|
||||||
private val activeNotificationsProvider: ActiveNotificationsProvider,
|
private val activeNotificationsProvider: ActiveNotificationsProvider,
|
||||||
@@ -187,6 +188,31 @@ class DefaultNotificationDrawerManager(
|
|||||||
clearSummaryNotificationIfNeeded(sessionId)
|
clearSummaryNotificationIfNeeded(sessionId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun clearReadRoomsNotifications(sessionId: SessionId) {
|
||||||
|
val notifications = activeNotificationsProvider.getNotificationsForSession(sessionId)
|
||||||
|
val notificationsGroupedByRoomId = notifications.mapNotNull {
|
||||||
|
tryOrNull { RoomId(it.tag) }?.let { roomId -> roomId to it }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notificationsGroupedByRoomId.isEmpty()) return
|
||||||
|
|
||||||
|
coroutineScope.launch {
|
||||||
|
val client = matrixClientProvider.getOrRestore(sessionId).getOrElse { return@launch }
|
||||||
|
|
||||||
|
notificationsGroupedByRoomId.forEach { (roomId, notification) ->
|
||||||
|
val roomInfo = client.getRoomInfo(roomId).getOrNull() ?: return@forEach
|
||||||
|
|
||||||
|
if (roomInfo.numUnreadNotifications > 0L || roomInfo.numUnreadMessages > 0 || roomInfo.numUnreadMentions > 0) {
|
||||||
|
return@forEach
|
||||||
|
}
|
||||||
|
|
||||||
|
notificationDisplayer.cancelNotification(notification.tag, notification.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
clearSummaryNotificationIfNeeded(sessionId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun clearSummaryNotificationIfNeeded(sessionId: SessionId) {
|
private fun clearSummaryNotificationIfNeeded(sessionId: SessionId) {
|
||||||
val summaryNotification = activeNotificationsProvider.getSummaryNotification(sessionId)
|
val summaryNotification = activeNotificationsProvider.getSummaryNotification(sessionId)
|
||||||
if (summaryNotification != null && activeNotificationsProvider.count(sessionId) == 1) {
|
if (summaryNotification != null && activeNotificationsProvider.count(sessionId) == 1) {
|
||||||
|
|||||||
+125
@@ -9,6 +9,7 @@
|
|||||||
package io.element.android.libraries.push.impl.notifications
|
package io.element.android.libraries.push.impl.notifications
|
||||||
|
|
||||||
import android.app.Notification
|
import android.app.Notification
|
||||||
|
import android.service.notification.StatusBarNotification
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.features.enterprise.api.EnterpriseService
|
import io.element.android.features.enterprise.api.EnterpriseService
|
||||||
@@ -16,6 +17,8 @@ import io.element.android.features.enterprise.test.FakeEnterpriseService
|
|||||||
import io.element.android.features.lockscreen.api.LockScreenService
|
import io.element.android.features.lockscreen.api.LockScreenService
|
||||||
import io.element.android.features.lockscreen.test.FakeLockScreenService
|
import io.element.android.features.lockscreen.test.FakeLockScreenService
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
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.room.RoomInfo
|
||||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID_2
|
import io.element.android.libraries.matrix.test.AN_EVENT_ID_2
|
||||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||||
@@ -27,6 +30,7 @@ import io.element.android.libraries.matrix.test.A_THREAD_ID_2
|
|||||||
import io.element.android.libraries.matrix.test.A_TIMESTAMP
|
import io.element.android.libraries.matrix.test.A_TIMESTAMP
|
||||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
import io.element.android.libraries.matrix.test.FakeMatrixClientProvider
|
import io.element.android.libraries.matrix.test.FakeMatrixClientProvider
|
||||||
|
import io.element.android.libraries.matrix.test.room.aRoomInfo
|
||||||
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
||||||
import io.element.android.libraries.matrix.ui.media.test.FakeImageLoaderHolder
|
import io.element.android.libraries.matrix.ui.media.test.FakeImageLoaderHolder
|
||||||
import io.element.android.libraries.push.api.notifications.NotificationIdProvider
|
import io.element.android.libraries.push.api.notifications.NotificationIdProvider
|
||||||
@@ -57,6 +61,7 @@ import io.element.android.services.appnavstate.test.aNavigationState
|
|||||||
import io.element.android.services.appnavstate.test.anAppNavigationState
|
import io.element.android.services.appnavstate.test.anAppNavigationState
|
||||||
import io.element.android.services.toolbox.test.systemclock.A_FAKE_TIMESTAMP
|
import io.element.android.services.toolbox.test.systemclock.A_FAKE_TIMESTAMP
|
||||||
import io.element.android.tests.testutils.lambda.LambdaThreeParamsRecorder
|
import io.element.android.tests.testutils.lambda.LambdaThreeParamsRecorder
|
||||||
|
import io.element.android.tests.testutils.lambda.LambdaTwoParamsRecorder
|
||||||
import io.element.android.tests.testutils.lambda.any
|
import io.element.android.tests.testutils.lambda.any
|
||||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||||
import io.element.android.tests.testutils.lambda.value
|
import io.element.android.tests.testutils.lambda.value
|
||||||
@@ -569,6 +574,126 @@ class DefaultNotificationDrawerManagerTest {
|
|||||||
createFallbackNotificationResult.assertions().isNeverCalled()
|
createFallbackNotificationResult.assertions().isNeverCalled()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `clearReadRoomsNotifications cancels notifications of rooms without unread counts`() = runTest {
|
||||||
|
val cancelNotificationResult = lambdaRecorder<String?, Int, Unit> { _, _ -> }
|
||||||
|
val sut = createClearReadRoomsManager(
|
||||||
|
cancelNotificationResult = cancelNotificationResult,
|
||||||
|
notifications = listOf(aRoomStatusBarNotification(A_ROOM_ID, 1)),
|
||||||
|
roomInfo = aRoomInfo(id = A_ROOM_ID),
|
||||||
|
)
|
||||||
|
sut.clearReadRoomsNotifications(A_SESSION_ID)
|
||||||
|
runCurrent()
|
||||||
|
cancelNotificationResult.assertions().isCalledOnce().with(value(A_ROOM_ID.value), value(1))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `clearReadRoomsNotifications keeps notifications of rooms with unread messages`() = testKeepsNotification(
|
||||||
|
aRoomInfo(id = A_ROOM_ID, numUnreadMessages = 1)
|
||||||
|
)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `clearReadRoomsNotifications keeps notifications of rooms with unread notifications`() =
|
||||||
|
testKeepsNotification(aRoomInfo(id = A_ROOM_ID, numUnreadNotifications = 1))
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `clearReadRoomsNotifications keeps notifications of rooms with unread mentions`() = testKeepsNotification(
|
||||||
|
aRoomInfo(id = A_ROOM_ID, numUnreadMentions = 1)
|
||||||
|
)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `clearReadRoomsNotifications keeps notifications when the room info is not found`() = testKeepsNotification(null)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `clearReadRoomsNotifications does nothing when the client cannot be restored`() = runTest {
|
||||||
|
val cancelNotificationResult = lambdaRecorder<String?, Int, Unit> { _, _ -> }
|
||||||
|
val sut = createClearReadRoomsManager(
|
||||||
|
cancelNotificationResult = cancelNotificationResult,
|
||||||
|
notifications = listOf(aRoomStatusBarNotification(A_ROOM_ID, 1)),
|
||||||
|
roomInfo = aRoomInfo(id = A_ROOM_ID),
|
||||||
|
clientResult = Result.failure(IllegalStateException("No client")),
|
||||||
|
)
|
||||||
|
sut.clearReadRoomsNotifications(A_SESSION_ID)
|
||||||
|
runCurrent()
|
||||||
|
cancelNotificationResult.assertions().isNeverCalled()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `clearReadRoomsNotifications ignores notifications not linked to a room`() = runTest {
|
||||||
|
val cancelNotificationResult = lambdaRecorder<String?, Int, Unit> { _, _ -> }
|
||||||
|
val sut = createClearReadRoomsManager(
|
||||||
|
cancelNotificationResult = cancelNotificationResult,
|
||||||
|
notifications = listOf(
|
||||||
|
mockk<StatusBarNotification> {
|
||||||
|
every { id } returns 1
|
||||||
|
every { tag } returns null
|
||||||
|
},
|
||||||
|
),
|
||||||
|
roomInfo = aRoomInfo(id = A_ROOM_ID),
|
||||||
|
)
|
||||||
|
sut.clearReadRoomsNotifications(A_SESSION_ID)
|
||||||
|
runCurrent()
|
||||||
|
cancelNotificationResult.assertions().isNeverCalled()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `clearReadRoomsNotifications clears the summary when it is the only notification left`() = runTest {
|
||||||
|
val cancelNotificationResult = lambdaRecorder<String?, Int, Unit> { _, _ -> }
|
||||||
|
val summaryId = NotificationIdProvider.getSummaryNotificationId(A_SESSION_ID)
|
||||||
|
val sut = createClearReadRoomsManager(
|
||||||
|
cancelNotificationResult = cancelNotificationResult,
|
||||||
|
notifications = listOf(aRoomStatusBarNotification(A_ROOM_ID, 1)),
|
||||||
|
roomInfo = aRoomInfo(id = A_ROOM_ID),
|
||||||
|
summaryNotification = mockk { every { id } returns summaryId },
|
||||||
|
count = 1,
|
||||||
|
)
|
||||||
|
sut.clearReadRoomsNotifications(A_SESSION_ID)
|
||||||
|
runCurrent()
|
||||||
|
cancelNotificationResult.assertions().isCalledExactly(2).withSequence(
|
||||||
|
listOf(value(A_ROOM_ID.value), value(1)),
|
||||||
|
listOf(value(null), value(summaryId)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun testKeepsNotification(roomInfo: RoomInfo?) = runTest {
|
||||||
|
val cancelNotificationResult = lambdaRecorder<String?, Int, Unit> { _, _ -> }
|
||||||
|
val sut = createClearReadRoomsManager(
|
||||||
|
cancelNotificationResult = cancelNotificationResult,
|
||||||
|
notifications = listOf(aRoomStatusBarNotification(A_ROOM_ID, 1)),
|
||||||
|
roomInfo = roomInfo,
|
||||||
|
)
|
||||||
|
sut.clearReadRoomsNotifications(A_SESSION_ID)
|
||||||
|
runCurrent()
|
||||||
|
cancelNotificationResult.assertions().isNeverCalled()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun aRoomStatusBarNotification(roomId: RoomId, notificationId: Int): StatusBarNotification = mockk {
|
||||||
|
every { id } returns notificationId
|
||||||
|
every { tag } returns roomId.value
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun TestScope.createClearReadRoomsManager(
|
||||||
|
cancelNotificationResult: LambdaTwoParamsRecorder<String?, Int, Unit>,
|
||||||
|
notifications: List<StatusBarNotification>,
|
||||||
|
roomInfo: RoomInfo?,
|
||||||
|
clientResult: Result<FakeMatrixClient>? = null,
|
||||||
|
summaryNotification: StatusBarNotification? = null,
|
||||||
|
count: Int = notifications.size,
|
||||||
|
): DefaultNotificationDrawerManager {
|
||||||
|
val matrixClient = FakeMatrixClient().apply {
|
||||||
|
getRoomInfoLambda = { Result.success(roomInfo) }
|
||||||
|
}
|
||||||
|
return createDefaultNotificationDrawerManager(
|
||||||
|
notificationDisplayer = FakeNotificationDisplayer(cancelNotificationResult = cancelNotificationResult),
|
||||||
|
activeNotificationsProvider = FakeActiveNotificationsProvider(
|
||||||
|
getNotificationsForSessionResult = { notifications },
|
||||||
|
getSummaryNotificationResult = { summaryNotification },
|
||||||
|
countResult = { count },
|
||||||
|
),
|
||||||
|
matrixClientProvider = FakeMatrixClientProvider(getClient = { clientResult ?: Result.success(matrixClient) }),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The content of the original event must be stripped, only the noisy flag, the ids and the timestamp are kept.
|
* The content of the original event must be stripped, only the noisy flag, the ids and the timestamp are kept.
|
||||||
*/
|
*/
|
||||||
|
|||||||
+6
-1
@@ -21,7 +21,8 @@ class FakeNotificationCleaner(
|
|||||||
private val clearMessagesForThreadLambda: (SessionId, RoomId, ThreadId) -> Unit = { _, _, _ -> lambdaError() },
|
private val clearMessagesForThreadLambda: (SessionId, RoomId, ThreadId) -> Unit = { _, _, _ -> lambdaError() },
|
||||||
private val clearEventLambda: (SessionId, EventId) -> Unit = { _, _ -> lambdaError() },
|
private val clearEventLambda: (SessionId, EventId) -> Unit = { _, _ -> lambdaError() },
|
||||||
private val clearMembershipNotificationForSessionLambda: (SessionId) -> Unit = { lambdaError() },
|
private val clearMembershipNotificationForSessionLambda: (SessionId) -> Unit = { lambdaError() },
|
||||||
private val clearMembershipNotificationForRoomLambda: (SessionId, RoomId) -> Unit = { _, _ -> lambdaError() }
|
private val clearMembershipNotificationForRoomLambda: (SessionId, RoomId) -> Unit = { _, _ -> lambdaError() },
|
||||||
|
private val clearReadRoomsNotificationsLambda: (SessionId) -> Unit = { lambdaError() },
|
||||||
) : NotificationCleaner {
|
) : NotificationCleaner {
|
||||||
override fun clearAllMessagesEvents(sessionId: SessionId) {
|
override fun clearAllMessagesEvents(sessionId: SessionId) {
|
||||||
clearAllMessagesEventsLambda(sessionId)
|
clearAllMessagesEventsLambda(sessionId)
|
||||||
@@ -46,4 +47,8 @@ class FakeNotificationCleaner(
|
|||||||
override fun clearMembershipNotificationForRoom(sessionId: SessionId, roomId: RoomId) {
|
override fun clearMembershipNotificationForRoom(sessionId: SessionId, roomId: RoomId) {
|
||||||
clearMembershipNotificationForRoomLambda(sessionId, roomId)
|
clearMembershipNotificationForRoomLambda(sessionId, roomId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun clearReadRoomsNotifications(sessionId: SessionId) {
|
||||||
|
clearReadRoomsNotificationsLambda(sessionId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user