Merge branch 'develop' into feature/fga/set_user_status

This commit is contained in:
ganfra
2026-07-08 17:59:30 +02:00
834 changed files with 11059 additions and 3245 deletions
@@ -15,4 +15,6 @@ sealed interface DeveloperSettingsEvents {
data class ChangeBrandColor(val color: Color?) : DeveloperSettingsEvents
data object ClearCache : DeveloperSettingsEvents
data object VacuumStores : DeveloperSettingsEvents
data class MarkAllRoomsAsRead(val needsConfirmation: Boolean) : DeveloperSettingsEvents
data object DismissMarkAllRoomsAsReadConfirmation : DeveloperSettingsEvents
}
@@ -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
@@ -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 {
@@ -58,6 +60,9 @@ class DeveloperSettingsPresenter(
val clearCacheAction = remember {
mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized)
}
val markAllRoomsAsReadAction = remember {
mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized)
}
var showColorPicker by remember {
mutableStateOf(false)
}
@@ -88,6 +93,18 @@ class DeveloperSettingsPresenter(
DeveloperSettingsEvents.VacuumStores -> coroutineScope.launch {
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,
databaseSizes = databaseSizes.value,
clearCacheAction = clearCacheAction.value,
markAllRoomsAsReadAction = markAllRoomsAsReadAction.value,
isEnterpriseBuild = enterpriseService.isEnterpriseBuild,
showColorPicker = showColorPicker,
eventSink = ::handleEvent,
@@ -131,8 +149,14 @@ class DeveloperSettingsPresenter(
}
private fun CoroutineScope.clearCache(clearCacheAction: MutableState<AsyncAction<Unit>>) = launch {
suspend { clearCacheUseCase() }.runCatchingUpdatingState(state = clearCacheAction)
}
private fun CoroutineScope.markAllRoomsAsRead(
markAllRoomsAsReadAction: MutableState<AsyncAction<Unit>>,
) = launch {
suspend {
clearCacheUseCase()
}.runCatchingUpdatingState(clearCacheAction)
markAllRoomsAsRead().getOrThrow()
}.runCatchingUpdatingState(state = markAllRoomsAsReadAction)
}
}
@@ -18,9 +18,10 @@ data class DeveloperSettingsState(
val cacheSize: AsyncData<String>,
val databaseSizes: AsyncData<ImmutableMap<String, String>>,
val clearCacheAction: AsyncAction<Unit>,
val markAllRoomsAsReadAction: AsyncAction<Unit>,
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
}
@@ -37,6 +37,7 @@ open class DeveloperSettingsStateProvider : PreviewParameterProvider<DeveloperSe
fun aDeveloperSettingsState(
appDeveloperSettingsState: AppDeveloperSettingsState = anAppDeveloperSettingsState(),
clearCacheAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
markAllRoomsAsReadAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
isEnterpriseBuild: Boolean = false,
showColorPicker: Boolean = false,
eventSink: (DeveloperSettingsEvents) -> Unit = {},
@@ -45,6 +46,7 @@ fun aDeveloperSettingsState(
cacheSize = AsyncData.Success("1.2 MB"),
databaseSizes = AsyncData.Success(persistentMapOf("state_store" to "1.2MB")),
clearCacheAction = clearCacheAction,
markAllRoomsAsReadAction = markAllRoomsAsReadAction,
isEnterpriseBuild = isEnterpriseBuild,
showColorPicker = showColorPicker,
eventSink = eventSink,
@@ -18,9 +18,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
@@ -45,6 +47,15 @@ fun DeveloperSettingsView(
if (state.showLoader) {
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(
enabled = !state.showLoader,
onBack = onBackClick,
@@ -64,6 +75,7 @@ fun DeveloperSettingsView(
onOpenShowkase = onOpenShowkase,
)
NotificationCategory(onPushHistoryClick)
MarkAllRoomsAsReadCategory(state)
if (state.isEnterpriseBuild) {
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
private fun NotificationCategory(onPushHistoryClick: () -> Unit) {
PreferenceCategory(title = stringResource(id = R.string.screen_notification_settings_title)) {
@@ -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)
}
}
}
@@ -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)
}
}
}
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="full_screen_intent_banner_message">"Щоб ніколи не пропустити важливий виклик, змініть налаштування, щоб увімкнути повноекранні сповіщення, коли телефон заблоковано."</string>
<string name="full_screen_intent_banner_title">"Покращуйте досвід дзвінків"</string>
<string name="full_screen_intent_banner_title">"Покращуйте якість викликів"</string>
<string name="screen_advanced_settings_choose_distributor_dialog_title_android">"Виберіть спосіб отримання сповіщень"</string>
<string name="screen_advanced_settings_developer_mode">"Режим розробника"</string>
<string name="screen_advanced_settings_developer_mode_description">"Увімкніть доступ до функцій і можливостей для розробників."</string>
@@ -10,18 +10,18 @@
<string name="screen_advanced_settings_element_call_base_url_validation_error">"Неправильна URL-адреса. Переконайтеся, що ви вказали протокол (http/https) та правильну адресу."</string>
<string name="screen_advanced_settings_hide_invite_avatars_toggle_title">"Сховати аватари у запитах на запрошення до кімнат"</string>
<string name="screen_advanced_settings_hide_timeline_media_toggle_title">"Сховати попередній перегляд медіа у стрічці"</string>
<string name="screen_advanced_settings_labs">"Лабораторії"</string>
<string name="screen_advanced_settings_labs">"Лабораторія"</string>
<string name="screen_advanced_settings_live_location_section_description">"Відстань, яку потрібно пройти, щоб ініціювати оновлення."</string>
<string name="screen_advanced_settings_live_location_section_footer">"Переконайтеся, що для цього додатка увімкнено функцію «Точна геолокація». Щоб змінити дозвіл, перейдіть на сторінку %1$s."</string>
<string name="screen_advanced_settings_live_location_section_footer_link">"Налаштування додатка"</string>
<string name="screen_advanced_settings_live_location_section_footer">"Переконайтеся, що для цього застосунку ввімкнено функцію «Точна геолокація». Щоб змінити дозвіл, перейдіть на сторінку %1$s."</string>
<string name="screen_advanced_settings_live_location_section_footer_link">"Налаштування застосунку"</string>
<string name="screen_advanced_settings_live_location_section_title">"Оновлення місцезнаходження в реальному часі"</string>
<plurals name="screen_advanced_settings_live_location_update_distance">
<item quantity="one">"Кожен %1$d метр"</item>
<item quantity="few">"Кожні %1$d метри"</item>
<item quantity="many">"Кожні %1$d метрів"</item>
</plurals>
<string name="screen_advanced_settings_media_compression_description">"Швидше завантажуйте фотографії та відео та зменшуйте використання даних"</string>
<string name="screen_advanced_settings_media_compression_title">"Оптимізуйте медіаякість"</string>
<string name="screen_advanced_settings_media_compression_description">"Швидше завантаження світлин і відео та зменшення обсягу трафіку"</string>
<string name="screen_advanced_settings_media_compression_title">"Оптимізація якості медіа"</string>
<string name="screen_advanced_settings_moderation_and_safety_section_title">"Модерування й безпека"</string>
<string name="screen_advanced_settings_optimise_image_upload_quality_description">"Автоматична оптимізація зображень для швидшого вивантаження та зменшення розміру файлів."</string>
<string name="screen_advanced_settings_optimise_image_upload_quality_title">"Оптимізація якості вивантажуваних зображень"</string>
@@ -34,14 +34,14 @@
<string name="screen_advanced_settings_rich_text_editor_description">"Вимкніть редактор розширеного тексту, щоб вводити Markdown вручну."</string>
<string name="screen_advanced_settings_send_read_receipts">"Читати журнали"</string>
<string name="screen_advanced_settings_send_read_receipts_description">"Якщо вимкнено, ваші сповіщення про прочитання нікому не надсилатимуться. Ви все одно отримуватимете сповіщення про прочитання від інших користувачів."</string>
<string name="screen_advanced_settings_share_presence">"Поділіться присутністю"</string>
<string name="screen_advanced_settings_share_presence_description">"Якщо цей параметр вимкнено, ви не зможете надсилати й отримувати звіти про прочитання чи сповіщення про введення тексту."</string>
<string name="screen_advanced_settings_share_presence">"Ділитися присутністю"</string>
<string name="screen_advanced_settings_share_presence_description">"Якщо вимкнути цю функцію, ви не зможете надсилати чи отримувати підтвердження прочитання або сповіщення про набір тексту."</string>
<string name="screen_advanced_settings_show_media_timeline_always_hide">"Завжди ховати"</string>
<string name="screen_advanced_settings_show_media_timeline_always_show">"Завжди показувати"</string>
<string name="screen_advanced_settings_show_media_timeline_private_rooms">"У приватних кімнатах"</string>
<string name="screen_advanced_settings_show_media_timeline_subtitle">"Сховані медіа завжди можна переглянути, натиснувши на нього"</string>
<string name="screen_advanced_settings_show_media_timeline_title">"Показувати медіа у стрічці"</string>
<string name="screen_advanced_settings_view_source_description">"Увімкнути опцію для перегляду коду повідомлення в стрічці"</string>
<string name="screen_advanced_settings_view_source_description">"Увімкнення цього параметра уможливить перегляд джерела повідомлень зі стрічки."</string>
<string name="screen_blocked_users_empty">"У вас немає заблокованих користувачів."</string>
<string name="screen_blocked_users_unblock_alert_action">"Розблокувати"</string>
<string name="screen_blocked_users_unblock_alert_description">"Ви знову зможете бачити всі повідомлення від них."</string>
@@ -55,11 +55,12 @@
<string name="screen_edit_profile_updating_details">"Оновлення профілю…"</string>
<string name="screen_labs_enable_threads">"Увімкнути відповіді в гілках"</string>
<string name="screen_labs_enable_threads_description">"Застосунок перезапуститься, щоб застосувати цю зміну."</string>
<string name="screen_labs_header_description">"Випробуйте наші останні ідеї, що перебувають на стадії розробки. Ці функції ще не остаточні; вони можуть бути нестабільними та змінюватися."</string>
<string name="screen_labs_header_description">"Випробуйте найновіші ідеї наших розробників. Ці функції ще не завершені, вони можуть бути нестабільними та змінюватись."</string>
<string name="screen_labs_header_title">"Хочете поекспериментувати?"</string>
<string name="screen_labs_title">"Лабораторії"</string>
<string name="screen_labs_title">"Лабораторія"</string>
<string name="screen_notification_settings_additional_settings_section_title">"Додаткові налаштування"</string>
<string name="screen_notification_settings_calls_label">"Аудіо та відеодзвінки"</string>
<string name="screen_notification_settings_call_ringtone_label">"Мелодія виклику"</string>
<string name="screen_notification_settings_calls_label">"Аудіо та відеовиклики"</string>
<string name="screen_notification_settings_configuration_mismatch">"Невідповідність конфігурації"</string>
<string name="screen_notification_settings_configuration_mismatch_description">"Ми спростили налаштування сповіщень, щоб полегшити пошук параметрів. Деякі користувацькі налаштування, які ви вибрали раніше, тут не відображаються, але вони все ще активні.
@@ -77,18 +78,25 @@
<string name="screen_notification_settings_invite_for_me_label">"Запрошення"</string>
<string name="screen_notification_settings_mentions_only_disclaimer">"Ваш домашній сервер не підтримує цю опцію в зашифрованих кімнатах, ви можете не отримати сповіщення в деяких кімнатах."</string>
<string name="screen_notification_settings_mentions_section_title">"Згадки"</string>
<string name="screen_notification_settings_message_sound_dialog_choose_other">"Обрати інший звук…"</string>
<string name="screen_notification_settings_message_sound_dialog_current_subtitle">"Наразі використовується %1$s"</string>
<string name="screen_notification_settings_message_sound_dialog_title">"Звук повідомлення"</string>
<string name="screen_notification_settings_message_sound_label">"Звук повідомлення"</string>
<string name="screen_notification_settings_mode_all">"Усі"</string>
<string name="screen_notification_settings_mode_mentions">"Згадки"</string>
<string name="screen_notification_settings_notification_section_title">"Сповіщати мене про"</string>
<string name="screen_notification_settings_room_mention_label">"Сповіщати про @room"</string>
<string name="screen_notification_settings_sound_custom_fallback">"Власний"</string>
<string name="screen_notification_settings_sound_custom_sound_button_title">"Користувацький звук…"</string>
<string name="screen_notification_settings_sound_delete_sound_error_title">"Помилка видалення файлу"</string>
<string name="screen_notification_settings_sound_element_default">"За замовчуванням Element"</string>
<string name="screen_notification_settings_sound_element_default">"Типово Element"</string>
<string name="screen_notification_settings_sound_element_fade">"Згасання Element"</string>
<string name="screen_notification_settings_sound_import_sound_error_title">"Помилка імпорту файлу"</string>
<string name="screen_notification_settings_sound_preview_sound_error_title">"Проблема з попереднім прослуховуванням звуку сповіщення"</string>
<string name="screen_notification_settings_sound_section_title">"Звук"</string>
<string name="screen_notification_settings_sound_set_sound_error_title">"Проблема налаштовування звуку сповіщення"</string>
<string name="screen_notification_settings_sound_silent">"Без звуку"</string>
<string name="screen_notification_settings_sound_system_default">"Як у системі"</string>
<string name="screen_notification_settings_system_notifications_action_required">"Щоб отримувати сповіщення змініть свої %1$s."</string>
<string name="screen_notification_settings_system_notifications_action_required_content_link">"системні налаштування"</string>
<string name="screen_notification_settings_system_notifications_turned_off">"Системні сповіщення вимкнені"</string>
@@ -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.tasks.FakeClearCacheUseCase
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.libraries.androidutils.filesize.FakeFileSizeFormatter
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
fun `present - VacuumStores action invokes the VacuumStoresUseCase`() = runTest {
var vacuumCalled = false
@@ -151,6 +174,7 @@ class DeveloperSettingsPresenterTest {
enterpriseService: EnterpriseService = FakeEnterpriseService(),
vacuumStoresUseCase: VacuumStoresUseCase = VacuumStoresUseCase {},
databaseSizesUseCase: GetDatabaseSizesUseCase = GetDatabaseSizesUseCase { Result.success(SdkStoreSizes(null, null, null, null)) },
markAllRoomsAsRead: FakeMarkAllRoomsAsRead = FakeMarkAllRoomsAsRead(),
): DeveloperSettingsPresenter {
return DeveloperSettingsPresenter(
appDeveloperSettingsPresenter = { anAppDeveloperSettingsState() },
@@ -161,6 +185,7 @@ class DeveloperSettingsPresenterTest {
vacuumStoresUseCase = vacuumStoresUseCase,
databaseSizesUseCase = databaseSizesUseCase,
fileSizeFormatter = FakeFileSizeFormatter(),
markAllRoomsAsRead = markAllRoomsAsRead,
)
}
}
@@ -71,7 +71,7 @@ class DeveloperSettingsViewTest : RobolectricTest() {
}
}
@Config(qualifiers = "h2200dp")
@Config(qualifiers = "h2400dp")
@Test
fun `clicking on clear cache emits the expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<DeveloperSettingsEvents>()
@@ -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()
}
}
@@ -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)
}
}
@@ -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()
}
}
@@ -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))
}
}