Skip to content

Commit

Permalink
Merge pull request #3203 from element-hq/feature/bma/clearNotificatio…
Browse files Browse the repository at this point in the history
…nWhenMarkAsRead

Clear existing notification when a room is marked as read
  • Loading branch information
bmarty authored Jul 16, 2024
2 parents fa90191 + 85ceb02 commit d9599a2
Show file tree
Hide file tree
Showing 13 changed files with 92 additions and 89 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import io.element.android.libraries.architecture.runUpdatingState
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.join.JoinRoom
import io.element.android.libraries.push.api.notifications.NotificationDrawerManager
import io.element.android.libraries.push.api.notifications.NotificationCleaner
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import java.util.Optional
Expand All @@ -44,7 +44,7 @@ import kotlin.jvm.optionals.getOrNull
class AcceptDeclineInvitePresenter @Inject constructor(
private val client: MatrixClient,
private val joinRoom: JoinRoom,
private val notificationDrawerManager: NotificationDrawerManager,
private val notificationCleaner: NotificationCleaner,
) : Presenter<AcceptDeclineInviteState> {
@Composable
override fun present(): AcceptDeclineInviteState {
Expand Down Expand Up @@ -112,7 +112,7 @@ class AcceptDeclineInvitePresenter @Inject constructor(
trigger = JoinedRoom.Trigger.Invite,
)
.onSuccess {
notificationDrawerManager.clearMembershipNotificationForRoom(client.sessionId, roomId)
notificationCleaner.clearMembershipNotificationForRoom(client.sessionId, roomId)
}
.map { roomId }
}
Expand All @@ -122,7 +122,7 @@ class AcceptDeclineInvitePresenter @Inject constructor(
suspend {
client.getRoom(roomId)?.use {
it.leave().getOrThrow()
notificationDrawerManager.clearMembershipNotificationForRoom(client.sessionId, roomId)
notificationCleaner.clearMembershipNotificationForRoom(client.sessionId, roomId)
}
roomId
}.runCatchingUpdatingState(declinedAction)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ import io.element.android.libraries.matrix.test.A_SESSION_ID
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.join.FakeJoinRoom
import io.element.android.libraries.push.api.notifications.NotificationDrawerManager
import io.element.android.libraries.push.test.notifications.FakeNotificationDrawerManager
import io.element.android.libraries.push.api.notifications.NotificationCleaner
import io.element.android.libraries.push.test.notifications.FakeNotificationCleaner
import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.lambda.assert
import io.element.android.tests.testutils.lambda.lambdaRecorder
Expand Down Expand Up @@ -133,7 +133,7 @@ class AcceptDeclineInvitePresenterTest {
val clearMembershipNotificationForRoomLambda = lambdaRecorder<SessionId, RoomId, Unit> { _, _ ->
Result.success(Unit)
}
val notificationDrawerManager = FakeNotificationDrawerManager(
val fakeNotificationCleaner = FakeNotificationCleaner(
clearMembershipNotificationForRoomLambda = clearMembershipNotificationForRoomLambda
)
val declineInviteSuccess = lambdaRecorder { ->
Expand All @@ -149,7 +149,7 @@ class AcceptDeclineInvitePresenterTest {
}
val presenter = createAcceptDeclineInvitePresenter(
client = client,
notificationDrawerManager = notificationDrawerManager,
notificationCleaner = fakeNotificationCleaner,
)
presenter.test {
val inviteData = anInviteData()
Expand Down Expand Up @@ -219,15 +219,15 @@ class AcceptDeclineInvitePresenterTest {
val clearMembershipNotificationForRoomLambda = lambdaRecorder<SessionId, RoomId, Unit> { _, _ ->
Result.success(Unit)
}
val notificationDrawerManager = FakeNotificationDrawerManager(
val fakeNotificationCleaner = FakeNotificationCleaner(
clearMembershipNotificationForRoomLambda = clearMembershipNotificationForRoomLambda
)
val joinRoomSuccess = lambdaRecorder { _: RoomId, _: List<String>, _: JoinedRoom.Trigger ->
Result.success(Unit)
}
val presenter = createAcceptDeclineInvitePresenter(
joinRoomLambda = joinRoomSuccess,
notificationDrawerManager = notificationDrawerManager,
notificationCleaner = fakeNotificationCleaner,
)
presenter.test {
val inviteData = anInviteData()
Expand Down Expand Up @@ -274,12 +274,12 @@ class AcceptDeclineInvitePresenterTest {
joinRoomLambda: (RoomId, List<String>, JoinedRoom.Trigger) -> Result<Unit> = { _, _, _ ->
Result.success(Unit)
},
notificationDrawerManager: NotificationDrawerManager = FakeNotificationDrawerManager(),
notificationCleaner: NotificationCleaner = FakeNotificationCleaner(),
): AcceptDeclineInvitePresenter {
return AcceptDeclineInvitePresenter(
client = client,
joinRoom = FakeJoinRoom(joinRoomLambda),
notificationDrawerManager = notificationDrawerManager,
notificationCleaner = notificationCleaner,
)
}
}
2 changes: 2 additions & 0 deletions features/roomlist/impl/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ dependencies {
implementation(projects.libraries.permissions.api)
implementation(projects.libraries.permissions.noop)
implementation(projects.libraries.preferences.api)
implementation(projects.libraries.push.api)
implementation(projects.features.invite.api)
implementation(projects.features.networkmonitor.api)
implementation(projects.features.leaveroom.api)
Expand All @@ -79,6 +80,7 @@ dependencies {
testImplementation(projects.libraries.permissions.noop)
testImplementation(projects.libraries.permissions.test)
testImplementation(projects.libraries.preferences.test)
testImplementation(projects.libraries.push.test)
testImplementation(projects.services.analytics.test)
testImplementation(projects.services.toolbox.test)
testImplementation(projects.features.networkmonitor.test)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ import io.element.android.libraries.matrix.api.sync.SyncService
import io.element.android.libraries.matrix.api.sync.SyncState
import io.element.android.libraries.matrix.api.timeline.ReceiptType
import io.element.android.libraries.preferences.api.store.SessionPreferencesStore
import io.element.android.libraries.push.api.notifications.NotificationCleaner
import io.element.android.services.analytics.api.AnalyticsService
import io.element.android.services.analyticsproviders.api.trackers.captureInteraction
import kotlinx.collections.immutable.toPersistentList
Expand Down Expand Up @@ -97,6 +98,7 @@ class RoomListPresenter @Inject constructor(
private val analyticsService: AnalyticsService,
private val acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState>,
private val fullScreenIntentPermissionsPresenter: FullScreenIntentPermissionsPresenter,
private val notificationCleaner: NotificationCleaner,
) : Presenter<RoomListState> {
private val encryptionService: EncryptionService = client.encryptionService()
private val syncService: SyncService = client.syncService()
Expand Down Expand Up @@ -268,6 +270,7 @@ class RoomListPresenter @Inject constructor(
}

private fun CoroutineScope.markAsRead(roomId: RoomId) = launch {
notificationCleaner.clearMessagesForRoom(client.sessionId, roomId)
client.getRoom(roomId)?.use { room ->
room.setUnreadFlag(isUnread = false)
val receiptType = if (sessionPreferencesStore.isSendPublicReadReceiptsEnabled().first()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import io.element.android.libraries.fullscreenintent.test.FakeFullScreenIntentPe
import io.element.android.libraries.indicator.impl.DefaultIndicatorService
import io.element.android.libraries.matrix.api.MatrixClient
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.encryption.BackupState
import io.element.android.libraries.matrix.api.encryption.RecoveryState
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
Expand All @@ -62,6 +63,9 @@ import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.test.AN_AVATAR_URL
import io.element.android.libraries.matrix.test.AN_EXCEPTION
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_ROOM_ID_2
import io.element.android.libraries.matrix.test.A_ROOM_ID_3
import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.matrix.test.A_USER_ID
import io.element.android.libraries.matrix.test.A_USER_NAME
import io.element.android.libraries.matrix.test.FakeMatrixClient
Expand All @@ -75,6 +79,8 @@ import io.element.android.libraries.matrix.test.sync.FakeSyncService
import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService
import io.element.android.libraries.preferences.api.store.SessionPreferencesStore
import io.element.android.libraries.preferences.test.InMemorySessionPreferencesStore
import io.element.android.libraries.push.api.notifications.NotificationCleaner
import io.element.android.libraries.push.test.notifications.FakeNotificationCleaner
import io.element.android.services.analytics.api.AnalyticsService
import io.element.android.services.analytics.test.FakeAnalyticsService
import io.element.android.tests.testutils.EventsRecorder
Expand Down Expand Up @@ -503,35 +509,54 @@ class RoomListPresenterTest {
@Test
fun `present - check that the room is marked as read with correct RR and as unread`() = runTest {
val room = FakeMatrixRoom()
val room2 = FakeMatrixRoom(roomId = A_ROOM_ID_2)
val room3 = FakeMatrixRoom(roomId = A_ROOM_ID_3)
val allRooms = setOf(room, room2, room3)
val sessionPreferencesStore = InMemorySessionPreferencesStore()
val matrixClient = FakeMatrixClient().apply {
givenGetRoomResult(A_ROOM_ID, room)
givenGetRoomResult(A_ROOM_ID_2, room2)
givenGetRoomResult(A_ROOM_ID_3, room3)
}
val analyticsService = FakeAnalyticsService()
val scope = CoroutineScope(coroutineContext + SupervisorJob())
val clearMessagesForRoomLambda = lambdaRecorder<SessionId, RoomId, Unit> { _, _ -> }
val notificationCleaner = FakeNotificationCleaner(
clearMessagesForRoomLambda = clearMessagesForRoomLambda,
)
val presenter = createRoomListPresenter(
client = matrixClient,
coroutineScope = scope,
sessionPreferencesStore = sessionPreferencesStore,
analyticsService = analyticsService,
notificationCleaner = notificationCleaner,
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
assertThat(room.markAsReadCalls).isEmpty()
assertThat(room.setUnreadFlagCalls).isEmpty()
allRooms.forEach {
assertThat(it.markAsReadCalls).isEmpty()
assertThat(it.setUnreadFlagCalls).isEmpty()
}
initialState.eventSink.invoke(RoomListEvents.MarkAsRead(A_ROOM_ID))
assertThat(room.markAsReadCalls).isEqualTo(listOf(ReceiptType.READ))
assertThat(room.setUnreadFlagCalls).isEqualTo(listOf(false))
initialState.eventSink.invoke(RoomListEvents.MarkAsUnread(A_ROOM_ID))
assertThat(room.markAsReadCalls).isEqualTo(listOf(ReceiptType.READ))
assertThat(room.setUnreadFlagCalls).isEqualTo(listOf(false, true))
clearMessagesForRoomLambda.assertions().isCalledOnce()
.with(value(A_SESSION_ID), value(A_ROOM_ID))
initialState.eventSink.invoke(RoomListEvents.MarkAsUnread(A_ROOM_ID_2))
assertThat(room2.markAsReadCalls).isEqualTo(emptyList<ReceiptType>())
assertThat(room2.setUnreadFlagCalls).isEqualTo(listOf(true))
// Test again with private read receipts
sessionPreferencesStore.setSendPublicReadReceipts(false)
initialState.eventSink.invoke(RoomListEvents.MarkAsRead(A_ROOM_ID))
assertThat(room.markAsReadCalls).isEqualTo(listOf(ReceiptType.READ, ReceiptType.READ_PRIVATE))
assertThat(room.setUnreadFlagCalls).isEqualTo(listOf(false, true, false))
initialState.eventSink.invoke(RoomListEvents.MarkAsRead(A_ROOM_ID_3))
assertThat(room3.markAsReadCalls).isEqualTo(listOf(ReceiptType.READ_PRIVATE))
assertThat(room3.setUnreadFlagCalls).isEqualTo(listOf(false))
clearMessagesForRoomLambda.assertions().isCalledExactly(2)
.withSequence(
listOf(value(A_SESSION_ID), value(A_ROOM_ID)),
listOf(value(A_SESSION_ID), value(A_ROOM_ID_3)),
)
assertThat(analyticsService.capturedEvents).containsExactly(
Interaction(name = Interaction.Name.MobileRoomListRoomContextMenuUnreadToggle),
Interaction(name = Interaction.Name.MobileRoomListRoomContextMenuUnreadToggle),
Expand Down Expand Up @@ -633,6 +658,7 @@ class RoomListPresenterTest {
filtersPresenter: Presenter<RoomListFiltersState> = Presenter { aRoomListFiltersState() },
searchPresenter: Presenter<RoomListSearchState> = Presenter { aRoomListSearchState() },
acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState> = Presenter { anAcceptDeclineInviteState() },
notificationCleaner: NotificationCleaner = FakeNotificationCleaner(),
) = RoomListPresenter(
client = client,
networkMonitor = networkMonitor,
Expand Down Expand Up @@ -660,5 +686,6 @@ class RoomListPresenterTest {
analyticsService = analyticsService,
acceptDeclineInvitePresenter = acceptDeclineInvitePresenter,
fullScreenIntentPermissionsPresenter = FakeFullScreenIntentPermissionsPresenter(),
notificationCleaner = notificationCleaner,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ val A_SPACE_ID = SpaceId("!aSpaceId:domain")
val A_SPACE_ID_2 = SpaceId("!aSpaceId2:domain")
val A_ROOM_ID = RoomId("!aRoomId:domain")
val A_ROOM_ID_2 = RoomId("!aRoomId2:domain")
val A_ROOM_ID_3 = RoomId("!aRoomId3:domain")
val A_THREAD_ID = ThreadId("\$aThreadId")
val A_THREAD_ID_2 = ThreadId("\$aThreadId2")
val AN_EVENT_ID = EventId("\$anEventId")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,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

interface NotificationDrawerManager {
interface NotificationCleaner {
fun clearAllMessagesEvents(sessionId: SessionId)
fun clearMessagesForRoom(sessionId: SessionId, roomId: RoomId)
fun clearEvent(sessionId: SessionId, eventId: EventId)
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package io.element.android.libraries.push.impl.notifications

import androidx.annotation.VisibleForTesting
import androidx.core.app.NotificationManagerCompat
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.core.data.tryOrNull
import io.element.android.libraries.core.log.logger.LoggerTag
import io.element.android.libraries.di.AppScope
Expand All @@ -30,7 +31,7 @@ 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.matrix.ui.media.ImageLoaderHolder
import io.element.android.libraries.push.api.notifications.NotificationDrawerManager
import io.element.android.libraries.push.api.notifications.NotificationCleaner
import io.element.android.libraries.push.api.notifications.NotificationIdProvider
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
import io.element.android.libraries.push.impl.notifications.model.shouldIgnoreEventInRoom
Expand All @@ -46,11 +47,12 @@ import javax.inject.Inject
private val loggerTag = LoggerTag("DefaultNotificationDrawerManager", LoggerTag.NotificationLoggerTag)

/**
* The NotificationDrawerManager receives notification events as they arrive (from event stream or fcm) and
* This class receives notification events as they arrive from the PushHandler calling [onNotifiableEventReceived] and
* organise them in order to display them in the notification drawer.
* Events can be grouped into the same notification, old (already read) events can be removed to do some cleaning.
*/
@SingleIn(AppScope::class)
@ContributesBinding(AppScope::class)
class DefaultNotificationDrawerManager @Inject constructor(
private val notificationManager: NotificationManagerCompat,
private val notificationRenderer: NotificationRenderer,
Expand All @@ -59,7 +61,7 @@ class DefaultNotificationDrawerManager @Inject constructor(
private val matrixClientProvider: MatrixClientProvider,
private val imageLoaderHolder: ImageLoaderHolder,
private val activeNotificationsProvider: ActiveNotificationsProvider,
) : NotificationDrawerManager {
) : NotificationCleaner {
private var appNavigationStateObserver: Job? = null

// TODO EAx add a setting per user for this
Expand Down
Loading

0 comments on commit d9599a2

Please sign in to comment.