mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-11-02 21:32:34 +00:00
Add read all messages feature (#7049)
* Add read all messages feature: read all messages using Client.markAllRoomsAsRead() method * Move the 'mark all rooms as read' footer as part of the list item * Replace `showMarkAllRoomsAsReadConfirmation` and its associated event with `MarkAllRoomsAsRead(val needsConfirmation: Boolean)` and a `AsyncAction.Confirming` state --------- Co-authored-by: Jorge Martín <jorgem@element.io>
This commit is contained in:
committed by
GitHub
parent
c4f32db4be
commit
b8752152cf
@@ -57,6 +57,7 @@ dependencies {
|
|||||||
implementation(libs.androidx.datastore.preferences)
|
implementation(libs.androidx.datastore.preferences)
|
||||||
implementation(libs.haze)
|
implementation(libs.haze)
|
||||||
implementation(libs.haze.materials)
|
implementation(libs.haze.materials)
|
||||||
|
implementation(projects.features.preferences.impl)
|
||||||
implementation(projects.features.reportroom.api)
|
implementation(projects.features.reportroom.api)
|
||||||
implementation(projects.features.rolesandpermissions.api)
|
implementation(projects.features.rolesandpermissions.api)
|
||||||
implementation(projects.libraries.previewutils)
|
implementation(projects.libraries.previewutils)
|
||||||
|
|||||||
+5
-18
@@ -41,6 +41,7 @@ import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteE
|
|||||||
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState
|
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState
|
||||||
import io.element.android.features.leaveroom.api.LeaveRoomEvent
|
import io.element.android.features.leaveroom.api.LeaveRoomEvent
|
||||||
import io.element.android.features.leaveroom.api.LeaveRoomState
|
import io.element.android.features.leaveroom.api.LeaveRoomState
|
||||||
|
import io.element.android.features.preferences.impl.tasks.MarkRoomAsRead
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||||
@@ -51,11 +52,8 @@ import io.element.android.libraries.matrix.api.core.RoomId
|
|||||||
import io.element.android.libraries.matrix.api.encryption.RecoveryState
|
import io.element.android.libraries.matrix.api.encryption.RecoveryState
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomList
|
import io.element.android.libraries.matrix.api.roomlist.RoomList
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomListFilter
|
import io.element.android.libraries.matrix.api.roomlist.RoomListFilter
|
||||||
import io.element.android.libraries.matrix.api.timeline.ReceiptType
|
|
||||||
import io.element.android.libraries.matrix.ui.safety.rememberHideInvitesAvatar
|
import io.element.android.libraries.matrix.ui.safety.rememberHideInvitesAvatar
|
||||||
import io.element.android.libraries.preferences.api.store.SessionPreferencesStore
|
|
||||||
import io.element.android.libraries.push.api.battery.BatteryOptimizationState
|
import io.element.android.libraries.push.api.battery.BatteryOptimizationState
|
||||||
import io.element.android.libraries.push.api.notifications.NotificationCleaner
|
|
||||||
import io.element.android.services.analytics.api.AnalyticsService
|
import io.element.android.services.analytics.api.AnalyticsService
|
||||||
import io.element.android.services.analytics.api.watchers.AnalyticsColdStartWatcher
|
import io.element.android.services.analytics.api.watchers.AnalyticsColdStartWatcher
|
||||||
import io.element.android.services.analyticsproviders.api.trackers.captureInteraction
|
import io.element.android.services.analyticsproviders.api.trackers.captureInteraction
|
||||||
@@ -65,7 +63,6 @@ import kotlinx.coroutines.CoroutineScope
|
|||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.flow.collect
|
import kotlinx.coroutines.flow.collect
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.first
|
|
||||||
import kotlinx.coroutines.flow.flatMapLatest
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
@@ -79,12 +76,11 @@ class RoomListPresenter(
|
|||||||
private val roomListDataSource: RoomListDataSource,
|
private val roomListDataSource: RoomListDataSource,
|
||||||
private val filtersPresenter: Presenter<RoomListFiltersState>,
|
private val filtersPresenter: Presenter<RoomListFiltersState>,
|
||||||
private val searchPresenter: Presenter<RoomListSearchState>,
|
private val searchPresenter: Presenter<RoomListSearchState>,
|
||||||
private val sessionPreferencesStore: SessionPreferencesStore,
|
|
||||||
private val analyticsService: AnalyticsService,
|
private val analyticsService: AnalyticsService,
|
||||||
private val acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState>,
|
private val acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState>,
|
||||||
private val fullScreenIntentPermissionsPresenter: Presenter<FullScreenIntentPermissionsState>,
|
private val fullScreenIntentPermissionsPresenter: Presenter<FullScreenIntentPermissionsState>,
|
||||||
private val batteryOptimizationPresenter: Presenter<BatteryOptimizationState>,
|
private val batteryOptimizationPresenter: Presenter<BatteryOptimizationState>,
|
||||||
private val notificationCleaner: NotificationCleaner,
|
private val markRoomAsRead: MarkRoomAsRead,
|
||||||
private val seenInvitesStore: SeenInvitesStore,
|
private val seenInvitesStore: SeenInvitesStore,
|
||||||
private val announcementService: AnnouncementService,
|
private val announcementService: AnnouncementService,
|
||||||
private val coldStartWatcher: AnalyticsColdStartWatcher,
|
private val coldStartWatcher: AnalyticsColdStartWatcher,
|
||||||
@@ -308,19 +304,10 @@ class RoomListPresenter(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun CoroutineScope.markAsRead(roomId: RoomId) = launch {
|
private fun CoroutineScope.markAsRead(roomId: RoomId) = launch {
|
||||||
notificationCleaner.clearMessagesForRoom(client.sessionId, roomId)
|
markRoomAsRead(roomId)
|
||||||
client.getRoom(roomId)?.use { room ->
|
.onSuccess {
|
||||||
room.setUnreadFlag(isUnread = false)
|
analyticsService.captureInteraction(name = Interaction.Name.MobileRoomListRoomContextMenuUnreadToggle)
|
||||||
val receiptType = if (sessionPreferencesStore.isSendPublicReadReceiptsEnabled().first()) {
|
|
||||||
ReceiptType.READ
|
|
||||||
} else {
|
|
||||||
ReceiptType.READ_PRIVATE
|
|
||||||
}
|
}
|
||||||
room.markAsRead(receiptType)
|
|
||||||
.onSuccess {
|
|
||||||
analyticsService.captureInteraction(name = Interaction.Name.MobileRoomListRoomContextMenuUnreadToggle)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun CoroutineScope.markAsUnread(roomId: RoomId) = launch {
|
private fun CoroutineScope.markAsUnread(roomId: RoomId) = launch {
|
||||||
|
|||||||
+22
@@ -0,0 +1,22 @@
|
|||||||
|
/*
|
||||||
|
* 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.features.home.impl.roomlist
|
||||||
|
|
||||||
|
import io.element.android.features.preferences.impl.tasks.MarkRoomAsRead
|
||||||
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
|
||||||
|
class FakeMarkRoomAsRead(
|
||||||
|
private val invokeLambda: suspend (RoomId) -> Result<Unit> = { Result.success(Unit) },
|
||||||
|
) : MarkRoomAsRead {
|
||||||
|
val invokedRoomIds = mutableListOf<RoomId>()
|
||||||
|
|
||||||
|
override suspend fun invoke(roomId: RoomId): Result<Unit> {
|
||||||
|
invokedRoomIds.add(roomId)
|
||||||
|
return invokeLambda(roomId)
|
||||||
|
}
|
||||||
|
}
|
||||||
+31
-2
@@ -30,6 +30,7 @@ import io.element.android.features.invite.api.acceptdecline.anAcceptDeclineInvit
|
|||||||
import io.element.android.features.invite.test.InMemorySeenInvitesStore
|
import io.element.android.features.invite.test.InMemorySeenInvitesStore
|
||||||
import io.element.android.features.leaveroom.api.LeaveRoomEvent
|
import io.element.android.features.leaveroom.api.LeaveRoomEvent
|
||||||
import io.element.android.features.leaveroom.api.LeaveRoomState
|
import io.element.android.features.leaveroom.api.LeaveRoomState
|
||||||
|
import io.element.android.features.preferences.impl.tasks.MarkRoomAsRead
|
||||||
import io.element.android.features.rageshake.test.logs.FakeAnnouncementService
|
import io.element.android.features.rageshake.test.logs.FakeAnnouncementService
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.dateformatter.api.DateFormatter
|
import io.element.android.libraries.dateformatter.api.DateFormatter
|
||||||
@@ -427,11 +428,17 @@ class RoomListPresenterTest {
|
|||||||
val notificationCleaner = FakeNotificationCleaner(
|
val notificationCleaner = FakeNotificationCleaner(
|
||||||
clearMessagesForRoomLambda = clearMessagesForRoomLambda,
|
clearMessagesForRoomLambda = clearMessagesForRoomLambda,
|
||||||
)
|
)
|
||||||
|
val markRoomAsRead = createTestMarkRoomAsRead(
|
||||||
|
client = matrixClient,
|
||||||
|
notificationCleaner = notificationCleaner,
|
||||||
|
sessionPreferencesStore = sessionPreferencesStore,
|
||||||
|
)
|
||||||
val presenter = createRoomListPresenter(
|
val presenter = createRoomListPresenter(
|
||||||
client = matrixClient,
|
client = matrixClient,
|
||||||
sessionPreferencesStore = sessionPreferencesStore,
|
sessionPreferencesStore = sessionPreferencesStore,
|
||||||
analyticsService = analyticsService,
|
analyticsService = analyticsService,
|
||||||
notificationCleaner = notificationCleaner,
|
notificationCleaner = notificationCleaner,
|
||||||
|
markRoomAsRead = markRoomAsRead,
|
||||||
)
|
)
|
||||||
presenter.test {
|
presenter.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
@@ -623,6 +630,24 @@ class RoomListPresenterTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun createTestMarkRoomAsRead(
|
||||||
|
client: MatrixClient,
|
||||||
|
notificationCleaner: NotificationCleaner,
|
||||||
|
sessionPreferencesStore: SessionPreferencesStore,
|
||||||
|
): MarkRoomAsRead = FakeMarkRoomAsRead { roomId ->
|
||||||
|
notificationCleaner.clearMessagesForRoom(client.sessionId, roomId)
|
||||||
|
val room = client.getRoom(roomId) ?: return@FakeMarkRoomAsRead Result.failure(IllegalStateException("Room not found"))
|
||||||
|
room.use {
|
||||||
|
it.setUnreadFlag(isUnread = false)
|
||||||
|
val receiptType = if (sessionPreferencesStore.isSendPublicReadReceiptsEnabled().first()) {
|
||||||
|
ReceiptType.READ
|
||||||
|
} else {
|
||||||
|
ReceiptType.READ_PRIVATE
|
||||||
|
}
|
||||||
|
it.markAsRead(receiptType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun TestScope.createRoomListPresenter(
|
private fun TestScope.createRoomListPresenter(
|
||||||
client: MatrixClient = FakeMatrixClient(),
|
client: MatrixClient = FakeMatrixClient(),
|
||||||
leaveRoomState: LeaveRoomState = aLeaveRoomState(),
|
leaveRoomState: LeaveRoomState = aLeaveRoomState(),
|
||||||
@@ -638,6 +663,7 @@ class RoomListPresenterTest {
|
|||||||
seenInvitesStore: SeenInvitesStore = InMemorySeenInvitesStore(),
|
seenInvitesStore: SeenInvitesStore = InMemorySeenInvitesStore(),
|
||||||
announcementService: AnnouncementService = FakeAnnouncementService(),
|
announcementService: AnnouncementService = FakeAnnouncementService(),
|
||||||
featureFlagService: FeatureFlagService = FakeFeatureFlagService(),
|
featureFlagService: FeatureFlagService = FakeFeatureFlagService(),
|
||||||
|
markRoomAsRead: MarkRoomAsRead? = null,
|
||||||
) = RoomListPresenter(
|
) = RoomListPresenter(
|
||||||
client = client,
|
client = client,
|
||||||
leaveRoomPresenter = { leaveRoomState },
|
leaveRoomPresenter = { leaveRoomState },
|
||||||
@@ -654,14 +680,17 @@ class RoomListPresenterTest {
|
|||||||
analyticsService = FakeAnalyticsService(),
|
analyticsService = FakeAnalyticsService(),
|
||||||
),
|
),
|
||||||
searchPresenter = searchPresenter,
|
searchPresenter = searchPresenter,
|
||||||
sessionPreferencesStore = sessionPreferencesStore,
|
|
||||||
filtersPresenter = filtersPresenter,
|
filtersPresenter = filtersPresenter,
|
||||||
spaceFiltersPresenter = spaceFiltersPresenter,
|
spaceFiltersPresenter = spaceFiltersPresenter,
|
||||||
analyticsService = analyticsService,
|
analyticsService = analyticsService,
|
||||||
acceptDeclineInvitePresenter = acceptDeclineInvitePresenter,
|
acceptDeclineInvitePresenter = acceptDeclineInvitePresenter,
|
||||||
fullScreenIntentPermissionsPresenter = { aFullScreenIntentPermissionsState() },
|
fullScreenIntentPermissionsPresenter = { aFullScreenIntentPermissionsState() },
|
||||||
batteryOptimizationPresenter = { aBatteryOptimizationState() },
|
batteryOptimizationPresenter = { aBatteryOptimizationState() },
|
||||||
notificationCleaner = notificationCleaner,
|
markRoomAsRead = markRoomAsRead ?: createTestMarkRoomAsRead(
|
||||||
|
client = client,
|
||||||
|
notificationCleaner = notificationCleaner,
|
||||||
|
sessionPreferencesStore = sessionPreferencesStore,
|
||||||
|
),
|
||||||
seenInvitesStore = seenInvitesStore,
|
seenInvitesStore = seenInvitesStore,
|
||||||
announcementService = announcementService,
|
announcementService = announcementService,
|
||||||
coldStartWatcher = FakeAnalyticsColdStartWatcher(),
|
coldStartWatcher = FakeAnalyticsColdStartWatcher(),
|
||||||
|
|||||||
+2
@@ -15,4 +15,6 @@ sealed interface DeveloperSettingsEvents {
|
|||||||
data class ChangeBrandColor(val color: Color?) : DeveloperSettingsEvents
|
data class ChangeBrandColor(val color: Color?) : DeveloperSettingsEvents
|
||||||
data object ClearCache : DeveloperSettingsEvents
|
data object ClearCache : DeveloperSettingsEvents
|
||||||
data object VacuumStores : DeveloperSettingsEvents
|
data object VacuumStores : DeveloperSettingsEvents
|
||||||
|
data class MarkAllRoomsAsRead(val needsConfirmation: Boolean) : DeveloperSettingsEvents
|
||||||
|
data object DismissMarkAllRoomsAsReadConfirmation : DeveloperSettingsEvents
|
||||||
}
|
}
|
||||||
|
|||||||
+26
-2
@@ -22,6 +22,7 @@ import io.element.android.features.enterprise.api.EnterpriseService
|
|||||||
import io.element.android.features.preferences.impl.developer.appsettings.AppDeveloperSettingsState
|
import io.element.android.features.preferences.impl.developer.appsettings.AppDeveloperSettingsState
|
||||||
import io.element.android.features.preferences.impl.tasks.ClearCacheUseCase
|
import io.element.android.features.preferences.impl.tasks.ClearCacheUseCase
|
||||||
import io.element.android.features.preferences.impl.tasks.ComputeCacheSizeUseCase
|
import io.element.android.features.preferences.impl.tasks.ComputeCacheSizeUseCase
|
||||||
|
import io.element.android.features.preferences.impl.tasks.MarkAllRoomsAsRead
|
||||||
import io.element.android.features.preferences.impl.tasks.VacuumStoresUseCase
|
import io.element.android.features.preferences.impl.tasks.VacuumStoresUseCase
|
||||||
import io.element.android.libraries.androidutils.filesize.FileSizeFormatter
|
import io.element.android.libraries.androidutils.filesize.FileSizeFormatter
|
||||||
import io.element.android.libraries.architecture.AsyncAction
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
@@ -46,6 +47,7 @@ class DeveloperSettingsPresenter(
|
|||||||
private val vacuumStoresUseCase: VacuumStoresUseCase,
|
private val vacuumStoresUseCase: VacuumStoresUseCase,
|
||||||
private val databaseSizesUseCase: GetDatabaseSizesUseCase,
|
private val databaseSizesUseCase: GetDatabaseSizesUseCase,
|
||||||
private val fileSizeFormatter: FileSizeFormatter,
|
private val fileSizeFormatter: FileSizeFormatter,
|
||||||
|
private val markAllRoomsAsRead: MarkAllRoomsAsRead,
|
||||||
) : Presenter<DeveloperSettingsState> {
|
) : Presenter<DeveloperSettingsState> {
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): DeveloperSettingsState {
|
override fun present(): DeveloperSettingsState {
|
||||||
@@ -58,6 +60,9 @@ class DeveloperSettingsPresenter(
|
|||||||
val clearCacheAction = remember {
|
val clearCacheAction = remember {
|
||||||
mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized)
|
mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized)
|
||||||
}
|
}
|
||||||
|
val markAllRoomsAsReadAction = remember {
|
||||||
|
mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized)
|
||||||
|
}
|
||||||
var showColorPicker by remember {
|
var showColorPicker by remember {
|
||||||
mutableStateOf(false)
|
mutableStateOf(false)
|
||||||
}
|
}
|
||||||
@@ -88,6 +93,18 @@ class DeveloperSettingsPresenter(
|
|||||||
DeveloperSettingsEvents.VacuumStores -> coroutineScope.launch {
|
DeveloperSettingsEvents.VacuumStores -> coroutineScope.launch {
|
||||||
vacuumStoresUseCase()
|
vacuumStoresUseCase()
|
||||||
}
|
}
|
||||||
|
is DeveloperSettingsEvents.MarkAllRoomsAsRead -> {
|
||||||
|
if (event.needsConfirmation) {
|
||||||
|
markAllRoomsAsReadAction.value = AsyncAction.ConfirmingNoParams
|
||||||
|
} else {
|
||||||
|
coroutineScope.markAllRoomsAsRead(
|
||||||
|
markAllRoomsAsReadAction = markAllRoomsAsReadAction,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DeveloperSettingsEvents.DismissMarkAllRoomsAsReadConfirmation -> {
|
||||||
|
markAllRoomsAsReadAction.value = AsyncAction.Uninitialized
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,6 +114,7 @@ class DeveloperSettingsPresenter(
|
|||||||
cacheSize = cacheSize.value,
|
cacheSize = cacheSize.value,
|
||||||
databaseSizes = databaseSizes.value,
|
databaseSizes = databaseSizes.value,
|
||||||
clearCacheAction = clearCacheAction.value,
|
clearCacheAction = clearCacheAction.value,
|
||||||
|
markAllRoomsAsReadAction = markAllRoomsAsReadAction.value,
|
||||||
isEnterpriseBuild = enterpriseService.isEnterpriseBuild,
|
isEnterpriseBuild = enterpriseService.isEnterpriseBuild,
|
||||||
showColorPicker = showColorPicker,
|
showColorPicker = showColorPicker,
|
||||||
eventSink = ::handleEvent,
|
eventSink = ::handleEvent,
|
||||||
@@ -131,8 +149,14 @@ class DeveloperSettingsPresenter(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun CoroutineScope.clearCache(clearCacheAction: MutableState<AsyncAction<Unit>>) = launch {
|
private fun CoroutineScope.clearCache(clearCacheAction: MutableState<AsyncAction<Unit>>) = launch {
|
||||||
|
suspend { clearCacheUseCase() }.runCatchingUpdatingState(state = clearCacheAction)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun CoroutineScope.markAllRoomsAsRead(
|
||||||
|
markAllRoomsAsReadAction: MutableState<AsyncAction<Unit>>,
|
||||||
|
) = launch {
|
||||||
suspend {
|
suspend {
|
||||||
clearCacheUseCase()
|
markAllRoomsAsRead().getOrThrow()
|
||||||
}.runCatchingUpdatingState(clearCacheAction)
|
}.runCatchingUpdatingState(state = markAllRoomsAsReadAction)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -18,9 +18,10 @@ data class DeveloperSettingsState(
|
|||||||
val cacheSize: AsyncData<String>,
|
val cacheSize: AsyncData<String>,
|
||||||
val databaseSizes: AsyncData<ImmutableMap<String, String>>,
|
val databaseSizes: AsyncData<ImmutableMap<String, String>>,
|
||||||
val clearCacheAction: AsyncAction<Unit>,
|
val clearCacheAction: AsyncAction<Unit>,
|
||||||
|
val markAllRoomsAsReadAction: AsyncAction<Unit>,
|
||||||
val isEnterpriseBuild: Boolean,
|
val isEnterpriseBuild: Boolean,
|
||||||
val showColorPicker: Boolean,
|
val showColorPicker: Boolean,
|
||||||
val eventSink: (DeveloperSettingsEvents) -> Unit
|
val eventSink: (DeveloperSettingsEvents) -> Unit
|
||||||
) {
|
) {
|
||||||
val showLoader = clearCacheAction is AsyncAction.Loading
|
val showLoader = clearCacheAction is AsyncAction.Loading || markAllRoomsAsReadAction is AsyncAction.Loading
|
||||||
}
|
}
|
||||||
|
|||||||
+2
@@ -37,6 +37,7 @@ open class DeveloperSettingsStateProvider : PreviewParameterProvider<DeveloperSe
|
|||||||
fun aDeveloperSettingsState(
|
fun aDeveloperSettingsState(
|
||||||
appDeveloperSettingsState: AppDeveloperSettingsState = anAppDeveloperSettingsState(),
|
appDeveloperSettingsState: AppDeveloperSettingsState = anAppDeveloperSettingsState(),
|
||||||
clearCacheAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
clearCacheAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||||
|
markAllRoomsAsReadAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||||
isEnterpriseBuild: Boolean = false,
|
isEnterpriseBuild: Boolean = false,
|
||||||
showColorPicker: Boolean = false,
|
showColorPicker: Boolean = false,
|
||||||
eventSink: (DeveloperSettingsEvents) -> Unit = {},
|
eventSink: (DeveloperSettingsEvents) -> Unit = {},
|
||||||
@@ -45,6 +46,7 @@ fun aDeveloperSettingsState(
|
|||||||
cacheSize = AsyncData.Success("1.2 MB"),
|
cacheSize = AsyncData.Success("1.2 MB"),
|
||||||
databaseSizes = AsyncData.Success(persistentMapOf("state_store" to "1.2MB")),
|
databaseSizes = AsyncData.Success(persistentMapOf("state_store" to "1.2MB")),
|
||||||
clearCacheAction = clearCacheAction,
|
clearCacheAction = clearCacheAction,
|
||||||
|
markAllRoomsAsReadAction = markAllRoomsAsReadAction,
|
||||||
isEnterpriseBuild = isEnterpriseBuild,
|
isEnterpriseBuild = isEnterpriseBuild,
|
||||||
showColorPicker = showColorPicker,
|
showColorPicker = showColorPicker,
|
||||||
eventSink = eventSink,
|
eventSink = eventSink,
|
||||||
|
|||||||
+38
@@ -18,9 +18,11 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import io.element.android.compound.theme.ElementTheme
|
||||||
import io.element.android.features.preferences.impl.R
|
import io.element.android.features.preferences.impl.R
|
||||||
import io.element.android.features.preferences.impl.developer.appsettings.AppDeveloperSettingsView
|
import io.element.android.features.preferences.impl.developer.appsettings.AppDeveloperSettingsView
|
||||||
import io.element.android.libraries.designsystem.components.ProgressDialog
|
import io.element.android.libraries.designsystem.components.ProgressDialog
|
||||||
|
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
|
||||||
import io.element.android.libraries.designsystem.components.list.ListItemContent
|
import io.element.android.libraries.designsystem.components.list.ListItemContent
|
||||||
import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory
|
import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory
|
||||||
import io.element.android.libraries.designsystem.components.preferences.PreferencePage
|
import io.element.android.libraries.designsystem.components.preferences.PreferencePage
|
||||||
@@ -45,6 +47,15 @@ fun DeveloperSettingsView(
|
|||||||
if (state.showLoader) {
|
if (state.showLoader) {
|
||||||
ProgressDialog()
|
ProgressDialog()
|
||||||
}
|
}
|
||||||
|
if (state.markAllRoomsAsReadAction.isConfirming()) {
|
||||||
|
ConfirmationDialog(
|
||||||
|
title = "Are you sure you want to mark all the rooms as read?",
|
||||||
|
content = "",
|
||||||
|
submitText = stringResource(CommonStrings.action_yes),
|
||||||
|
onSubmitClick = { state.eventSink(DeveloperSettingsEvents.MarkAllRoomsAsRead(needsConfirmation = false)) },
|
||||||
|
onDismiss = { state.eventSink(DeveloperSettingsEvents.DismissMarkAllRoomsAsReadConfirmation) },
|
||||||
|
)
|
||||||
|
}
|
||||||
BackHandler(
|
BackHandler(
|
||||||
enabled = !state.showLoader,
|
enabled = !state.showLoader,
|
||||||
onBack = onBackClick,
|
onBack = onBackClick,
|
||||||
@@ -64,6 +75,7 @@ fun DeveloperSettingsView(
|
|||||||
onOpenShowkase = onOpenShowkase,
|
onOpenShowkase = onOpenShowkase,
|
||||||
)
|
)
|
||||||
NotificationCategory(onPushHistoryClick)
|
NotificationCategory(onPushHistoryClick)
|
||||||
|
MarkAllRoomsAsReadCategory(state)
|
||||||
|
|
||||||
if (state.isEnterpriseBuild) {
|
if (state.isEnterpriseBuild) {
|
||||||
PreferenceCategory(title = "Theme") {
|
PreferenceCategory(title = "Theme") {
|
||||||
@@ -152,6 +164,32 @@ fun DeveloperSettingsView(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun MarkAllRoomsAsReadCategory(state: DeveloperSettingsState) {
|
||||||
|
PreferenceCategory(title = "Room list") {
|
||||||
|
ListItem(
|
||||||
|
headlineContent = {
|
||||||
|
Text("Mark all rooms as read")
|
||||||
|
},
|
||||||
|
supportingContent = {
|
||||||
|
Text(
|
||||||
|
text = """
|
||||||
|
This will send a private read receipt and a read marker in every room you are part of.
|
||||||
|
It's a long running operation that might get rate limited.
|
||||||
|
It will run in the background but the app must be alive for it to finish.
|
||||||
|
""".trimIndent(),
|
||||||
|
style = ElementTheme.typography.fontBodySmRegular,
|
||||||
|
color = ElementTheme.colors.textSecondary,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
enabled = !state.showLoader,
|
||||||
|
onClick = {
|
||||||
|
state.eventSink(DeveloperSettingsEvents.MarkAllRoomsAsRead(needsConfirmation = true))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun NotificationCategory(onPushHistoryClick: () -> Unit) {
|
private fun NotificationCategory(onPushHistoryClick: () -> Unit) {
|
||||||
PreferenceCategory(title = stringResource(id = R.string.screen_notification_settings_title)) {
|
PreferenceCategory(title = stringResource(id = R.string.screen_notification_settings_title)) {
|
||||||
|
|||||||
+33
@@ -0,0 +1,33 @@
|
|||||||
|
/*
|
||||||
|
* 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.features.preferences.impl.tasks
|
||||||
|
|
||||||
|
import dev.zacsweers.metro.ContributesBinding
|
||||||
|
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||||
|
import io.element.android.libraries.di.SessionScope
|
||||||
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
|
import io.element.android.libraries.push.api.notifications.NotificationCleaner
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
|
interface MarkAllRoomsAsRead {
|
||||||
|
suspend operator fun invoke(): Result<Unit>
|
||||||
|
}
|
||||||
|
|
||||||
|
@ContributesBinding(SessionScope::class)
|
||||||
|
class DefaultMarkAllRoomsAsRead(
|
||||||
|
private val client: MatrixClient,
|
||||||
|
private val notificationCleaner: NotificationCleaner,
|
||||||
|
private val coroutineDispatchers: CoroutineDispatchers,
|
||||||
|
) : MarkAllRoomsAsRead {
|
||||||
|
override suspend fun invoke(): Result<Unit> = withContext(coroutineDispatchers.io) {
|
||||||
|
client.markAllRoomsAsRead()
|
||||||
|
.onSuccess {
|
||||||
|
notificationCleaner.clearAllMessagesEvents(client.sessionId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+45
@@ -0,0 +1,45 @@
|
|||||||
|
/*
|
||||||
|
* 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.features.preferences.impl.tasks
|
||||||
|
|
||||||
|
import dev.zacsweers.metro.ContributesBinding
|
||||||
|
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||||
|
import io.element.android.libraries.di.SessionScope
|
||||||
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
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 kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
|
interface MarkRoomAsRead {
|
||||||
|
suspend operator fun invoke(roomId: RoomId): Result<Unit>
|
||||||
|
}
|
||||||
|
|
||||||
|
@ContributesBinding(SessionScope::class)
|
||||||
|
class DefaultMarkRoomAsRead(
|
||||||
|
private val client: MatrixClient,
|
||||||
|
private val notificationCleaner: NotificationCleaner,
|
||||||
|
private val sessionPreferencesStore: SessionPreferencesStore,
|
||||||
|
private val coroutineDispatchers: CoroutineDispatchers,
|
||||||
|
) : MarkRoomAsRead {
|
||||||
|
override suspend fun invoke(roomId: RoomId): Result<Unit> = withContext(coroutineDispatchers.io) {
|
||||||
|
notificationCleaner.clearMessagesForRoom(client.sessionId, roomId)
|
||||||
|
val room = client.getRoom(roomId) ?: return@withContext Result.failure(IllegalStateException("Room not found"))
|
||||||
|
room.use {
|
||||||
|
it.setUnreadFlag(isUnread = false)
|
||||||
|
val receiptType = if (sessionPreferencesStore.isSendPublicReadReceiptsEnabled().first()) {
|
||||||
|
ReceiptType.READ
|
||||||
|
} else {
|
||||||
|
ReceiptType.READ_PRIVATE
|
||||||
|
}
|
||||||
|
it.markAsRead(receiptType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+25
@@ -17,6 +17,7 @@ import io.element.android.features.enterprise.test.FakeEnterpriseService
|
|||||||
import io.element.android.features.preferences.impl.developer.appsettings.anAppDeveloperSettingsState
|
import io.element.android.features.preferences.impl.developer.appsettings.anAppDeveloperSettingsState
|
||||||
import io.element.android.features.preferences.impl.tasks.FakeClearCacheUseCase
|
import io.element.android.features.preferences.impl.tasks.FakeClearCacheUseCase
|
||||||
import io.element.android.features.preferences.impl.tasks.FakeComputeCacheSizeUseCase
|
import io.element.android.features.preferences.impl.tasks.FakeComputeCacheSizeUseCase
|
||||||
|
import io.element.android.features.preferences.impl.tasks.FakeMarkAllRoomsAsRead
|
||||||
import io.element.android.features.preferences.impl.tasks.VacuumStoresUseCase
|
import io.element.android.features.preferences.impl.tasks.VacuumStoresUseCase
|
||||||
import io.element.android.libraries.androidutils.filesize.FakeFileSizeFormatter
|
import io.element.android.libraries.androidutils.filesize.FakeFileSizeFormatter
|
||||||
import io.element.android.libraries.architecture.AsyncAction
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
@@ -127,6 +128,28 @@ class DeveloperSettingsPresenterTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - confirm mark all rooms as read`() = runTest {
|
||||||
|
val markAllRoomsAsRead = FakeMarkAllRoomsAsRead()
|
||||||
|
val presenter = createDeveloperSettingsPresenter(markAllRoomsAsRead = markAllRoomsAsRead)
|
||||||
|
presenter.test {
|
||||||
|
skipItems(2)
|
||||||
|
val initialState = awaitItem()
|
||||||
|
initialState.eventSink(DeveloperSettingsEvents.MarkAllRoomsAsRead(needsConfirmation = true))
|
||||||
|
val stateWithConfirmation = awaitItem()
|
||||||
|
assertThat(stateWithConfirmation.markAllRoomsAsReadAction.isConfirming()).isTrue()
|
||||||
|
stateWithConfirmation.eventSink(DeveloperSettingsEvents.MarkAllRoomsAsRead(needsConfirmation = false))
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.markAllRoomsAsReadAction.isConfirming()).isFalse()
|
||||||
|
assertThat(state.markAllRoomsAsReadAction).isInstanceOf(AsyncAction.Loading::class.java)
|
||||||
|
}
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.markAllRoomsAsReadAction).isInstanceOf(AsyncAction.Success::class.java)
|
||||||
|
assertThat(markAllRoomsAsRead.invokeCallCount).isEqualTo(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - VacuumStores action invokes the VacuumStoresUseCase`() = runTest {
|
fun `present - VacuumStores action invokes the VacuumStoresUseCase`() = runTest {
|
||||||
var vacuumCalled = false
|
var vacuumCalled = false
|
||||||
@@ -151,6 +174,7 @@ class DeveloperSettingsPresenterTest {
|
|||||||
enterpriseService: EnterpriseService = FakeEnterpriseService(),
|
enterpriseService: EnterpriseService = FakeEnterpriseService(),
|
||||||
vacuumStoresUseCase: VacuumStoresUseCase = VacuumStoresUseCase {},
|
vacuumStoresUseCase: VacuumStoresUseCase = VacuumStoresUseCase {},
|
||||||
databaseSizesUseCase: GetDatabaseSizesUseCase = GetDatabaseSizesUseCase { Result.success(SdkStoreSizes(null, null, null, null)) },
|
databaseSizesUseCase: GetDatabaseSizesUseCase = GetDatabaseSizesUseCase { Result.success(SdkStoreSizes(null, null, null, null)) },
|
||||||
|
markAllRoomsAsRead: FakeMarkAllRoomsAsRead = FakeMarkAllRoomsAsRead(),
|
||||||
): DeveloperSettingsPresenter {
|
): DeveloperSettingsPresenter {
|
||||||
return DeveloperSettingsPresenter(
|
return DeveloperSettingsPresenter(
|
||||||
appDeveloperSettingsPresenter = { anAppDeveloperSettingsState() },
|
appDeveloperSettingsPresenter = { anAppDeveloperSettingsState() },
|
||||||
@@ -161,6 +185,7 @@ class DeveloperSettingsPresenterTest {
|
|||||||
vacuumStoresUseCase = vacuumStoresUseCase,
|
vacuumStoresUseCase = vacuumStoresUseCase,
|
||||||
databaseSizesUseCase = databaseSizesUseCase,
|
databaseSizesUseCase = databaseSizesUseCase,
|
||||||
fileSizeFormatter = FakeFileSizeFormatter(),
|
fileSizeFormatter = FakeFileSizeFormatter(),
|
||||||
|
markAllRoomsAsRead = markAllRoomsAsRead,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -71,7 +71,7 @@ class DeveloperSettingsViewTest : RobolectricTest() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Config(qualifiers = "h2200dp")
|
@Config(qualifiers = "h2400dp")
|
||||||
@Test
|
@Test
|
||||||
fun `clicking on clear cache emits the expected event`() = runAndroidComposeUiTest {
|
fun `clicking on clear cache emits the expected event`() = runAndroidComposeUiTest {
|
||||||
val eventsRecorder = EventsRecorder<DeveloperSettingsEvents>()
|
val eventsRecorder = EventsRecorder<DeveloperSettingsEvents>()
|
||||||
|
|||||||
+22
@@ -0,0 +1,22 @@
|
|||||||
|
/*
|
||||||
|
* 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.features.preferences.impl.tasks
|
||||||
|
|
||||||
|
import io.element.android.tests.testutils.simulateLongTask
|
||||||
|
|
||||||
|
class FakeMarkAllRoomsAsRead(
|
||||||
|
private val invokeLambda: suspend () -> Result<Unit> = { Result.success(Unit) },
|
||||||
|
) : MarkAllRoomsAsRead {
|
||||||
|
var invokeCallCount = 0
|
||||||
|
private set
|
||||||
|
|
||||||
|
override suspend fun invoke(): Result<Unit> = simulateLongTask {
|
||||||
|
invokeCallCount++
|
||||||
|
invokeLambda()
|
||||||
|
}
|
||||||
|
}
|
||||||
+21
@@ -0,0 +1,21 @@
|
|||||||
|
/*
|
||||||
|
* 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.features.preferences.impl.tasks
|
||||||
|
|
||||||
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
|
||||||
|
class FakeMarkRoomAsRead(
|
||||||
|
private val invokeLambda: suspend (RoomId) -> Result<Unit> = { Result.success(Unit) },
|
||||||
|
) : MarkRoomAsRead {
|
||||||
|
val invokedRoomIds = mutableListOf<RoomId>()
|
||||||
|
|
||||||
|
override suspend fun invoke(roomId: RoomId): Result<Unit> {
|
||||||
|
invokedRoomIds.add(roomId)
|
||||||
|
return invokeLambda(roomId)
|
||||||
|
}
|
||||||
|
}
|
||||||
+61
@@ -0,0 +1,61 @@
|
|||||||
|
/*
|
||||||
|
* 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.features.preferences.impl.tasks
|
||||||
|
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
|
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||||
|
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
|
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.test.runTest
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class MarkAllRoomsAsReadTest {
|
||||||
|
@Test
|
||||||
|
fun `invoke - delegates to client and clears all notifications`() = runTest {
|
||||||
|
val markAllRoomsAsReadLambda = lambdaRecorder<Result<Unit>> { Result.success(Unit) }
|
||||||
|
val clearAllMessagesEventsLambda = lambdaRecorder<SessionId, Unit> { }
|
||||||
|
val markAllRoomsAsRead = DefaultMarkAllRoomsAsRead(
|
||||||
|
client = FakeMatrixClient(
|
||||||
|
markAllRoomsAsReadResult = markAllRoomsAsReadLambda,
|
||||||
|
),
|
||||||
|
notificationCleaner = FakeNotificationCleaner(
|
||||||
|
clearAllMessagesEventsLambda = clearAllMessagesEventsLambda,
|
||||||
|
),
|
||||||
|
coroutineDispatchers = testCoroutineDispatchers(),
|
||||||
|
)
|
||||||
|
|
||||||
|
val result = markAllRoomsAsRead()
|
||||||
|
|
||||||
|
assertThat(result.isSuccess).isTrue()
|
||||||
|
markAllRoomsAsReadLambda.assertions().isCalledOnce()
|
||||||
|
clearAllMessagesEventsLambda.assertions().isCalledOnce().with(value(A_SESSION_ID))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `invoke - does not clear notifications when client fails`() = runTest {
|
||||||
|
val clearAllMessagesEventsLambda = lambdaRecorder<SessionId, Unit> { }
|
||||||
|
val markAllRoomsAsRead = DefaultMarkAllRoomsAsRead(
|
||||||
|
client = FakeMatrixClient(
|
||||||
|
markAllRoomsAsReadResult = { Result.failure(IllegalStateException("Failed")) },
|
||||||
|
),
|
||||||
|
notificationCleaner = FakeNotificationCleaner(
|
||||||
|
clearAllMessagesEventsLambda = clearAllMessagesEventsLambda,
|
||||||
|
),
|
||||||
|
coroutineDispatchers = testCoroutineDispatchers(),
|
||||||
|
)
|
||||||
|
|
||||||
|
val result = markAllRoomsAsRead()
|
||||||
|
|
||||||
|
assertThat(result.isFailure).isTrue()
|
||||||
|
clearAllMessagesEventsLambda.assertions().isNeverCalled()
|
||||||
|
}
|
||||||
|
}
|
||||||
+79
@@ -0,0 +1,79 @@
|
|||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
@file:OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
|
||||||
|
package io.element.android.features.preferences.impl.tasks
|
||||||
|
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
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.timeline.ReceiptType
|
||||||
|
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.FakeMatrixClient
|
||||||
|
import io.element.android.libraries.matrix.test.room.FakeBaseRoom
|
||||||
|
import io.element.android.libraries.preferences.test.InMemorySessionPreferencesStore
|
||||||
|
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.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.test.runCurrent
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class MarkRoomAsReadTest {
|
||||||
|
@Test
|
||||||
|
fun `invoke - clears notifications, clears unread flag and sends public read receipt`() = runTest {
|
||||||
|
val markAsReadResult = lambdaRecorder<ReceiptType, Result<Unit>> { Result.success(Unit) }
|
||||||
|
val room = FakeBaseRoom(markAsReadResult = markAsReadResult)
|
||||||
|
val matrixClient = FakeMatrixClient().apply {
|
||||||
|
givenGetRoomResult(A_ROOM_ID, room)
|
||||||
|
}
|
||||||
|
val clearMessagesForRoomLambda = lambdaRecorder { _: SessionId, _: RoomId -> }
|
||||||
|
val notificationCleaner = FakeNotificationCleaner(clearMessagesForRoomLambda = clearMessagesForRoomLambda)
|
||||||
|
val sessionPreferencesStore = InMemorySessionPreferencesStore()
|
||||||
|
val markRoomAsRead = DefaultMarkRoomAsRead(
|
||||||
|
client = matrixClient,
|
||||||
|
notificationCleaner = notificationCleaner,
|
||||||
|
sessionPreferencesStore = sessionPreferencesStore,
|
||||||
|
coroutineDispatchers = testCoroutineDispatchers(),
|
||||||
|
)
|
||||||
|
|
||||||
|
val result = markRoomAsRead(A_ROOM_ID)
|
||||||
|
runCurrent()
|
||||||
|
|
||||||
|
assertThat(result.isSuccess).isTrue()
|
||||||
|
clearMessagesForRoomLambda.assertions().isCalledOnce()
|
||||||
|
.with(value(A_SESSION_ID), value(A_ROOM_ID))
|
||||||
|
assertThat(room.setUnreadFlagCalls).isEqualTo(listOf(false))
|
||||||
|
markAsReadResult.assertions().isCalledOnce().with(value(ReceiptType.READ))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `invoke - sends private read receipt when public receipts are disabled`() = runTest {
|
||||||
|
val markAsReadResult = lambdaRecorder<ReceiptType, Result<Unit>> { Result.success(Unit) }
|
||||||
|
val room = FakeBaseRoom(markAsReadResult = markAsReadResult)
|
||||||
|
val matrixClient = FakeMatrixClient().apply {
|
||||||
|
givenGetRoomResult(A_ROOM_ID, room)
|
||||||
|
}
|
||||||
|
val sessionPreferencesStore = InMemorySessionPreferencesStore(isSendPublicReadReceiptsEnabled = false)
|
||||||
|
val markRoomAsRead = DefaultMarkRoomAsRead(
|
||||||
|
client = matrixClient,
|
||||||
|
notificationCleaner = FakeNotificationCleaner(clearMessagesForRoomLambda = { _, _ -> }),
|
||||||
|
sessionPreferencesStore = sessionPreferencesStore,
|
||||||
|
coroutineDispatchers = testCoroutineDispatchers(),
|
||||||
|
)
|
||||||
|
|
||||||
|
val result = markRoomAsRead(A_ROOM_ID)
|
||||||
|
runCurrent()
|
||||||
|
|
||||||
|
assertThat(result.isSuccess).isTrue()
|
||||||
|
markAsReadResult.assertions().isCalledOnce().with(value(ReceiptType.READ_PRIVATE))
|
||||||
|
}
|
||||||
|
}
|
||||||
+6
@@ -203,6 +203,12 @@ interface MatrixClient {
|
|||||||
*/
|
*/
|
||||||
suspend fun markRoomAsFullyRead(roomId: RoomId, eventId: EventId): Result<Unit>
|
suspend fun markRoomAsFullyRead(roomId: RoomId, eventId: EventId): Result<Unit>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark all joined rooms as read by sending public, private and fully-read receipts
|
||||||
|
* on each room's latest event. Per-room errors are logged and skipped by the SDK.
|
||||||
|
*/
|
||||||
|
suspend fun markAllRoomsAsRead(): Result<Unit>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if linking a new device using QrCode is supported by the server.
|
* Check if linking a new device using QrCode is supported by the server.
|
||||||
*/
|
*/
|
||||||
|
|||||||
+6
@@ -800,6 +800,12 @@ class RustMatrixClient(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun markAllRoomsAsRead(): Result<Unit> = withContext(sessionDispatcher) {
|
||||||
|
runCatchingExceptions {
|
||||||
|
innerClient.markAllRoomsAsRead()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun performDatabaseVacuum(): Result<Unit> = withContext(sessionDispatcher) {
|
override suspend fun performDatabaseVacuum(): Result<Unit> = withContext(sessionDispatcher) {
|
||||||
runCatchingExceptions {
|
runCatchingExceptions {
|
||||||
Timber.d("Performing database vacuuming for session $sessionId...")
|
Timber.d("Performing database vacuuming for session $sessionId...")
|
||||||
|
|||||||
+5
@@ -117,6 +117,7 @@ class FakeMatrixClient(
|
|||||||
private val getRecentEmojisLambda: () -> Result<List<String>> = { Result.success(emptyList()) },
|
private val getRecentEmojisLambda: () -> Result<List<String>> = { Result.success(emptyList()) },
|
||||||
private val addRecentEmojiLambda: (String) -> Result<Unit> = { Result.success(Unit) },
|
private val addRecentEmojiLambda: (String) -> Result<Unit> = { Result.success(Unit) },
|
||||||
private val markRoomAsFullyReadResult: (RoomId, EventId) -> Result<Unit> = { _, _ -> lambdaError() },
|
private val markRoomAsFullyReadResult: (RoomId, EventId) -> Result<Unit> = { _, _ -> lambdaError() },
|
||||||
|
private val markAllRoomsAsReadResult: () -> Result<Unit> = { Result.success(Unit) },
|
||||||
private val performDatabaseVacuumLambda: () -> Result<Unit> = { lambdaError() },
|
private val performDatabaseVacuumLambda: () -> Result<Unit> = { lambdaError() },
|
||||||
private val getMapStyleUrlResult: () -> Result<String?> = { lambdaError() },
|
private val getMapStyleUrlResult: () -> Result<String?> = { lambdaError() },
|
||||||
private val getDatabaseSizesLambda: () -> Result<SdkStoreSizes> = { lambdaError() },
|
private val getDatabaseSizesLambda: () -> Result<SdkStoreSizes> = { lambdaError() },
|
||||||
@@ -373,6 +374,10 @@ class FakeMatrixClient(
|
|||||||
return markRoomAsFullyReadResult(roomId, eventId)
|
return markRoomAsFullyReadResult(roomId, eventId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun markAllRoomsAsRead(): Result<Unit> {
|
||||||
|
return markAllRoomsAsReadResult()
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun performDatabaseVacuum(): Result<Unit> {
|
override suspend fun performDatabaseVacuum(): Result<Unit> {
|
||||||
return performDatabaseVacuumLambda()
|
return performDatabaseVacuumLambda()
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user