mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-11-02 12:07:28 +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
|
||||
|
||||
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.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.roomlist.RoomListService
|
||||
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 kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
|
||||
@Inject
|
||||
@AssistedInject
|
||||
class LoggedInEventProcessor(
|
||||
private val snackbarDispatcher: SnackbarDispatcher,
|
||||
@Assisted private val snackbarDispatcher: SnackbarDispatcher,
|
||||
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) {
|
||||
observingJob = roomMembershipObserver.updates
|
||||
if (currentChildScope != null) return
|
||||
|
||||
val childScope = coroutineScope.childScope(dispatchers.computation, "LoggedInEventProcessor")
|
||||
.also { currentChildScope = it }
|
||||
|
||||
roomMembershipObserver.updates
|
||||
.filter { !it.isUserInRoom }
|
||||
.distinctUntilChanged()
|
||||
.onEach { roomMemberShipUpdate ->
|
||||
@@ -48,12 +71,20 @@ class LoggedInEventProcessor(
|
||||
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() {
|
||||
observingJob?.cancel()
|
||||
observingJob = null
|
||||
currentChildScope?.cancel()
|
||||
currentChildScope = null
|
||||
}
|
||||
|
||||
private fun displayMessage(message: Int) {
|
||||
|
||||
@@ -157,6 +157,7 @@ class LoggedInFlowNode(
|
||||
private val createRoomEntryPoint: CreateRoomEntryPoint,
|
||||
private val activeLiveLocationShareManager: ActiveLiveLocationShareManager,
|
||||
private val customMapTilerConfigProvider: CustomMapTilerConfigProvider,
|
||||
private val loggedInEventProcessorFactory: LoggedInEventProcessor.Factory,
|
||||
) : BaseFlowNode<LoggedInFlowNode.NavTarget>(
|
||||
backstack = BackStack(
|
||||
initialElement = NavTarget.Placeholder,
|
||||
@@ -175,10 +176,7 @@ class LoggedInFlowNode(
|
||||
}
|
||||
|
||||
private val callback: Callback = callback()
|
||||
private val loggedInFlowProcessor = LoggedInEventProcessor(
|
||||
snackbarDispatcher = snackbarDispatcher,
|
||||
roomMembershipObserver = matrixClient.roomMembershipObserver,
|
||||
)
|
||||
private val loggedInFlowProcessor = loggedInEventProcessorFactory.create(snackbarDispatcher)
|
||||
|
||||
/**
|
||||
* 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(),
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user