Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions features/home/impl/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ dependencies {
implementation(libs.androidx.datastore.preferences)
implementation(libs.haze)
implementation(libs.haze.materials)
implementation(projects.features.preferences.impl)
implementation(projects.features.reportroom.api)
implementation(projects.features.rolesandpermissions.api)
implementation(projects.libraries.previewutils)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ import im.vector.app.features.analytics.plan.Interaction
import io.element.android.features.announcement.api.Announcement
import io.element.android.features.announcement.api.AnnouncementService
import io.element.android.features.home.impl.datasource.RoomListDataSource
import io.element.android.features.home.impl.filters.RoomListFilter.Rooms
import io.element.android.features.home.impl.filters.RoomListFiltersState
import io.element.android.features.home.impl.filters.into
import io.element.android.features.home.impl.search.RoomListSearchEvent
Expand All @@ -42,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.leaveroom.api.LeaveRoomEvent
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.Presenter
import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsState
Expand All @@ -50,12 +50,9 @@ 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.roomlist.RoomList
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.preferences.api.store.AppPreferencesStore
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.notifications.NotificationCleaner
import io.element.android.services.analytics.api.AnalyticsService
import io.element.android.services.analytics.api.watchers.AnalyticsColdStartWatcher
import io.element.android.services.analyticsproviders.api.trackers.captureInteraction
Expand All @@ -79,12 +76,11 @@ class RoomListPresenter(
private val roomListDataSource: RoomListDataSource,
private val filtersPresenter: Presenter<RoomListFiltersState>,
private val searchPresenter: Presenter<RoomListSearchState>,
private val sessionPreferencesStore: SessionPreferencesStore,
private val analyticsService: AnalyticsService,
private val acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState>,
private val fullScreenIntentPermissionsPresenter: Presenter<FullScreenIntentPermissionsState>,
private val batteryOptimizationPresenter: Presenter<BatteryOptimizationState>,
private val notificationCleaner: NotificationCleaner,
private val markRoomAsRead: MarkRoomAsRead,
private val appPreferencesStore: AppPreferencesStore,
private val seenInvitesStore: SeenInvitesStore,
private val announcementService: AnnouncementService,
Expand Down Expand Up @@ -304,19 +300,10 @@ class RoomListPresenter(
}

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()) {
ReceiptType.READ
} else {
ReceiptType.READ_PRIVATE
markRoomAsRead(roomId)
.onSuccess {
analyticsService.captureInteraction(name = Interaction.Name.MobileRoomListRoomContextMenuUnreadToggle)
}
room.markAsRead(receiptType)
.onSuccess {
analyticsService.captureInteraction(name = Interaction.Name.MobileRoomListRoomContextMenuUnreadToggle)
}
}
}

private fun CoroutineScope.markAsUnread(roomId: RoomId) = launch {
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.leaveroom.api.LeaveRoomEvent
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.libraries.architecture.Presenter
import io.element.android.libraries.dateformatter.api.DateFormatter
Expand Down Expand Up @@ -457,11 +458,17 @@ class RoomListPresenterTest {
val notificationCleaner = FakeNotificationCleaner(
clearMessagesForRoomLambda = clearMessagesForRoomLambda,
)
val markRoomAsRead = createTestMarkRoomAsRead(
client = matrixClient,
notificationCleaner = notificationCleaner,
sessionPreferencesStore = sessionPreferencesStore,
)
val presenter = createRoomListPresenter(
client = matrixClient,
sessionPreferencesStore = sessionPreferencesStore,
analyticsService = analyticsService,
notificationCleaner = notificationCleaner,
markRoomAsRead = markRoomAsRead,
)
presenter.test {
val initialState = awaitItem()
Expand Down Expand Up @@ -653,6 +660,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(
client: MatrixClient = FakeMatrixClient(),
leaveRoomState: LeaveRoomState = aLeaveRoomState(),
Expand All @@ -668,6 +693,7 @@ class RoomListPresenterTest {
appPreferencesStore: AppPreferencesStore = InMemoryAppPreferencesStore(),
seenInvitesStore: SeenInvitesStore = InMemorySeenInvitesStore(),
announcementService: AnnouncementService = FakeAnnouncementService(),
markRoomAsRead: MarkRoomAsRead? = null,
) = RoomListPresenter(
client = client,
leaveRoomPresenter = { leaveRoomState },
Expand All @@ -684,14 +710,17 @@ class RoomListPresenterTest {
analyticsService = FakeAnalyticsService(),
),
searchPresenter = searchPresenter,
sessionPreferencesStore = sessionPreferencesStore,
filtersPresenter = filtersPresenter,
spaceFiltersPresenter = spaceFiltersPresenter,
analyticsService = analyticsService,
acceptDeclineInvitePresenter = acceptDeclineInvitePresenter,
fullScreenIntentPermissionsPresenter = { aFullScreenIntentPermissionsState() },
batteryOptimizationPresenter = { aBatteryOptimizationState() },
notificationCleaner = notificationCleaner,
markRoomAsRead = markRoomAsRead ?: createTestMarkRoomAsRead(
client = client,
notificationCleaner = notificationCleaner,
sessionPreferencesStore = sessionPreferencesStore,
),
appPreferencesStore = appPreferencesStore,
seenInvitesStore = seenInvitesStore,
announcementService = announcementService,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,7 @@ sealed interface DeveloperSettingsEvents {
data class ChangeBrandColor(val color: Color?) : DeveloperSettingsEvents
data object ClearCache : DeveloperSettingsEvents
data object VacuumStores : DeveloperSettingsEvents
data object ShowMarkAllRoomsAsReadConfirmation : DeveloperSettingsEvents
data object ConfirmMarkAllRoomsAsRead : DeveloperSettingsEvents
data object DismissMarkAllRoomsAsReadConfirmation : DeveloperSettingsEvents
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.tasks.ClearCacheUseCase
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.libraries.androidutils.filesize.FileSizeFormatter
import io.element.android.libraries.architecture.AsyncAction
Expand All @@ -46,6 +47,7 @@ class DeveloperSettingsPresenter(
private val vacuumStoresUseCase: VacuumStoresUseCase,
private val databaseSizesUseCase: GetDatabaseSizesUseCase,
private val fileSizeFormatter: FileSizeFormatter,
private val markAllRoomsAsRead: MarkAllRoomsAsRead,
) : Presenter<DeveloperSettingsState> {
@Composable
override fun present(): DeveloperSettingsState {
Expand All @@ -58,6 +60,12 @@ class DeveloperSettingsPresenter(
val clearCacheAction = remember {
mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized)
}
val markAllRoomsAsReadAction = remember {
mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized)
}
var showMarkAllRoomsAsReadConfirmation by remember {
mutableStateOf(false)
}
var showColorPicker by remember {
mutableStateOf(false)
}
Expand Down Expand Up @@ -88,6 +96,16 @@ class DeveloperSettingsPresenter(
DeveloperSettingsEvents.VacuumStores -> coroutineScope.launch {
vacuumStoresUseCase()
}
DeveloperSettingsEvents.ShowMarkAllRoomsAsReadConfirmation -> {
showMarkAllRoomsAsReadConfirmation = true
}
DeveloperSettingsEvents.DismissMarkAllRoomsAsReadConfirmation -> {
showMarkAllRoomsAsReadConfirmation = false
}
DeveloperSettingsEvents.ConfirmMarkAllRoomsAsRead -> coroutineScope.markAllRoomsAsRead(
markAllRoomsAsReadAction = markAllRoomsAsReadAction,
dismissConfirmation = { showMarkAllRoomsAsReadConfirmation = false },
)
}
}

Expand All @@ -97,6 +115,8 @@ class DeveloperSettingsPresenter(
cacheSize = cacheSize.value,
databaseSizes = databaseSizes.value,
clearCacheAction = clearCacheAction.value,
markAllRoomsAsReadAction = markAllRoomsAsReadAction.value,
showMarkAllRoomsAsReadConfirmation = showMarkAllRoomsAsReadConfirmation,
isEnterpriseBuild = enterpriseService.isEnterpriseBuild,
showColorPicker = showColorPicker,
eventSink = ::handleEvent,
Expand Down Expand Up @@ -135,4 +155,15 @@ class DeveloperSettingsPresenter(
clearCacheUseCase()
}.runCatchingUpdatingState(clearCacheAction)
}

private fun CoroutineScope.markAllRoomsAsRead(
markAllRoomsAsReadAction: MutableState<AsyncAction<Unit>>,
dismissConfirmation: () -> Unit,
) = launch {
dismissConfirmation()
suspend {
markAllRoomsAsRead().getOrThrow()
Unit
}.runCatchingUpdatingState(markAllRoomsAsReadAction)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@ data class DeveloperSettingsState(
val cacheSize: AsyncData<String>,
val databaseSizes: AsyncData<ImmutableMap<String, String>>,
val clearCacheAction: AsyncAction<Unit>,
val markAllRoomsAsReadAction: AsyncAction<Unit>,
val showMarkAllRoomsAsReadConfirmation: Boolean,
val isEnterpriseBuild: Boolean,
val showColorPicker: Boolean,
val eventSink: (DeveloperSettingsEvents) -> Unit
) {
val showLoader = clearCacheAction is AsyncAction.Loading
val showLoader = clearCacheAction is AsyncAction.Loading || markAllRoomsAsReadAction is AsyncAction.Loading
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ open class DeveloperSettingsStateProvider : PreviewParameterProvider<DeveloperSe
fun aDeveloperSettingsState(
appDeveloperSettingsState: AppDeveloperSettingsState = anAppDeveloperSettingsState(),
clearCacheAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
markAllRoomsAsReadAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
showMarkAllRoomsAsReadConfirmation: Boolean = false,
isEnterpriseBuild: Boolean = false,
showColorPicker: Boolean = false,
eventSink: (DeveloperSettingsEvents) -> Unit = {},
Expand All @@ -45,6 +47,8 @@ fun aDeveloperSettingsState(
cacheSize = AsyncData.Success("1.2 MB"),
databaseSizes = AsyncData.Success(persistentMapOf("state_store" to "1.2MB")),
clearCacheAction = clearCacheAction,
markAllRoomsAsReadAction = markAllRoomsAsReadAction,
showMarkAllRoomsAsReadConfirmation = showMarkAllRoomsAsReadConfirmation,
isEnterpriseBuild = isEnterpriseBuild,
showColorPicker = showColorPicker,
eventSink = eventSink,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ package io.element.android.features.preferences.impl.developer

import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.progressSemantics
import androidx.compose.runtime.Composable
Expand All @@ -18,9 +19,11 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewParameter
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.developer.appsettings.AppDeveloperSettingsView
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.preferences.PreferenceCategory
import io.element.android.libraries.designsystem.components.preferences.PreferencePage
Expand All @@ -45,6 +48,15 @@ fun DeveloperSettingsView(
if (state.showLoader) {
ProgressDialog()
}
if (state.showMarkAllRoomsAsReadConfirmation) {
ConfirmationDialog(
title = stringResource(R.string.screen_developer_settings_mark_all_rooms_as_read_alert_title),
content = "",
submitText = stringResource(CommonStrings.action_yes),
onSubmitClick = { state.eventSink(DeveloperSettingsEvents.ConfirmMarkAllRoomsAsRead) },
onDismiss = { state.eventSink(DeveloperSettingsEvents.DismissMarkAllRoomsAsReadConfirmation) },
)
}
BackHandler(
enabled = !state.showLoader,
onBack = onBackClick,
Expand All @@ -64,6 +76,7 @@ fun DeveloperSettingsView(
onOpenShowkase = onOpenShowkase,
)
NotificationCategory(onPushHistoryClick)
MarkAllRoomsAsReadCategory(state)

if (state.isEnterpriseBuild) {
PreferenceCategory(title = "Theme") {
Expand Down Expand Up @@ -152,6 +165,27 @@ fun DeveloperSettingsView(
)
}

@Composable
private fun MarkAllRoomsAsReadCategory(state: DeveloperSettingsState) {
PreferenceCategory(title = "Room list") {
ListItem(
headlineContent = {
Text(stringResource(R.string.screen_developer_settings_mark_all_rooms_as_read))
},
enabled = !state.showLoader,
onClick = {
state.eventSink(DeveloperSettingsEvents.ShowMarkAllRoomsAsReadConfirmation)
},
)
Text(
text = stringResource(R.string.screen_developer_settings_mark_all_rooms_as_read_footer),
style = ElementTheme.typography.fontBodySmRegular,
color = ElementTheme.colors.textSecondary,
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
)
}
}

@Composable
private fun NotificationCategory(onPushHistoryClick: () -> Unit) {
PreferenceCategory(title = stringResource(id = R.string.screen_notification_settings_title)) {
Expand Down
Loading