From 4734b560f76f9dc9910836739678fead048e6f01 Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 30 Jun 2025 21:31:58 +0200 Subject: [PATCH] change (media preview config) : final refactoring and tests --- .../loggedin/MediaPreviewConfigMigration.kt | 4 +- .../MediaPreviewConfigMigrationTest.kt | 160 ++++++++++++++ .../joinroom/impl/JoinRoomPresenter.kt | 10 +- .../joinroom/impl/di/JoinRoomModule.kt | 3 - .../joinroom/impl/JoinRoomPresenterTest.kt | 4 - .../protection/TimelineProtectionPresenter.kt | 10 +- .../TimelineProtectionPresenterTest.kt | 27 ++- .../advanced/AdvancedSettingsPresenter.kt | 13 +- .../impl/advanced/AdvancedSettingsState.kt | 7 +- .../advanced/AdvancedSettingsStateProvider.kt | 12 +- .../impl/advanced/AdvancedSettingsView.kt | 25 ++- .../advanced/MediaPreviewConfigStateStore.kt | 49 +++-- .../advanced/AdvancedSettingsPresenterTest.kt | 90 +++++--- .../impl/advanced/AdvancedSettingsViewTest.kt | 66 +++++- .../FakeMediaPreviewConfigStateStore.kt | 51 +++++ .../MediaPreviewConfigStateStoreTest.kt | 206 ++++++++++++++++++ .../roomlist/impl/RoomListPresenter.kt | 8 +- .../libraries/matrix/api/MatrixClient.kt | 5 - .../matrix/api/media/MediaPreviewConfig.kt | 12 +- .../matrix/api/media/MediaPreviewService.kt | 17 +- .../libraries/matrix/impl/RustMatrixClient.kt | 6 +- .../impl/media/RustMediaPreviewService.kt | 22 +- .../libraries/matrix/test/FakeMatrixClient.kt | 4 - .../test/media/FakeMediaPreviewService.kt | 23 +- .../api/store/AppPreferencesStore.kt | 4 + .../DefaultNotifiableEventResolver.kt | 1 + .../DefaultNotifiableEventResolverTest.kt | 2 - 27 files changed, 676 insertions(+), 165 deletions(-) create mode 100644 appnav/src/test/kotlin/io/element/android/appnav/loggedin/MediaPreviewConfigMigrationTest.kt create mode 100644 features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/advanced/FakeMediaPreviewConfigStateStore.kt create mode 100644 features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/advanced/MediaPreviewConfigStateStoreTest.kt diff --git a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/MediaPreviewConfigMigration.kt b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/MediaPreviewConfigMigration.kt index ac7a1c6145..54299acc74 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/MediaPreviewConfigMigration.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/MediaPreviewConfigMigration.kt @@ -26,6 +26,7 @@ class MediaPreviewConfigMigration @Inject constructor( @SessionCoroutineScope private val sessionCoroutineScope: CoroutineScope, ) { + @Suppress("DEPRECATION") operator fun invoke() = sessionCoroutineScope.launch { val hideInviteAvatars = appPreferencesStore.getHideInviteAvatarsFlow().first() val mediaPreviewValue = appPreferencesStore.getTimelineMediaPreviewValueFlow().first() @@ -49,7 +50,8 @@ class MediaPreviewConfigMigration @Inject constructor( appPreferencesStore.setTimelineMediaPreviewValue(null) } } - }.onFailure { + } + .onFailure { Timber.d("Couldn't perform migration, failed to fetch media preview config.") } } diff --git a/appnav/src/test/kotlin/io/element/android/appnav/loggedin/MediaPreviewConfigMigrationTest.kt b/appnav/src/test/kotlin/io/element/android/appnav/loggedin/MediaPreviewConfigMigrationTest.kt new file mode 100644 index 0000000000..e6a17b440f --- /dev/null +++ b/appnav/src/test/kotlin/io/element/android/appnav/loggedin/MediaPreviewConfigMigrationTest.kt @@ -0,0 +1,160 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.appnav.loggedin + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.api.media.MediaPreviewConfig +import io.element.android.libraries.matrix.api.media.MediaPreviewValue +import io.element.android.libraries.matrix.test.media.FakeMediaPreviewService +import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class MediaPreviewConfigMigrationTest { + @Test + fun `when no local data exists, migration does nothing`() = runTest { + val appPreferencesStore = InMemoryAppPreferencesStore() + val mediaPreviewService = FakeMediaPreviewService( + fetchMediaPreviewConfigResult = { Result.success(null) } + ) + val migration = createMigration(appPreferencesStore, mediaPreviewService) + + migration().join() + + // Verify no calls were made to set server config + // since there's nothing to migrate + } + + @Test + fun `when local data exists and server has config, clears local data`() = runTest { + val appPreferencesStore = InMemoryAppPreferencesStore().apply { + setHideInviteAvatars(true) + setTimelineMediaPreviewValue(MediaPreviewValue.Private) + } + val serverConfig = MediaPreviewConfig( + hideInviteAvatar = false, + mediaPreviewValue = MediaPreviewValue.On + ) + val mediaPreviewService = FakeMediaPreviewService( + fetchMediaPreviewConfigResult = { Result.success(serverConfig) } + ) + val migration = createMigration(appPreferencesStore, mediaPreviewService) + + migration().join() + + // Verify local data was cleared + assertThat(appPreferencesStore.getHideInviteAvatarsFlow().first()).isNull() + assertThat(appPreferencesStore.getTimelineMediaPreviewValueFlow().first()).isNull() + } + + @Test + fun `when local hideInviteAvatars exists and server has no config, migrates to server`() = runTest { + val appPreferencesStore = InMemoryAppPreferencesStore().apply { + setHideInviteAvatars(true) + } + var setHideInviteAvatarsValue: Boolean? = null + val mediaPreviewService = FakeMediaPreviewService( + fetchMediaPreviewConfigResult = { Result.success(null) }, + setHideInviteAvatarsResult = { value -> + setHideInviteAvatarsValue = value + Result.success(Unit) + } + ) + val migration = createMigration(appPreferencesStore, mediaPreviewService) + + migration().join() + + // Verify server was updated with local value + assertThat(setHideInviteAvatarsValue).isTrue() + // Verify local data was cleared + assertThat(appPreferencesStore.getHideInviteAvatarsFlow().first()).isNull() + } + + @Test + fun `when local mediaPreviewValue exists and server has no config, migrates to server`() = runTest { + val appPreferencesStore = InMemoryAppPreferencesStore().apply { + setTimelineMediaPreviewValue(MediaPreviewValue.Private) + } + var setMediaPreviewValue: MediaPreviewValue? = null + val mediaPreviewService = FakeMediaPreviewService( + fetchMediaPreviewConfigResult = { Result.success(null) }, + setMediaPreviewValueResult = { value -> + setMediaPreviewValue = value + Result.success(Unit) + } + ) + val migration = createMigration(appPreferencesStore, mediaPreviewService) + + migration().join() + + // Verify server was updated with local value + assertThat(setMediaPreviewValue).isEqualTo(MediaPreviewValue.Private) + // Verify local data was cleared + assertThat(appPreferencesStore.getTimelineMediaPreviewValueFlow().first()).isNull() + } + + @Test + fun `when both local values exist and server has no config, migrates both to server`() = runTest { + val appPreferencesStore = InMemoryAppPreferencesStore().apply { + setHideInviteAvatars(true) + setTimelineMediaPreviewValue(MediaPreviewValue.Off) + } + var setHideInviteAvatarsValue: Boolean? = null + var setMediaPreviewValue: MediaPreviewValue? = null + val mediaPreviewService = FakeMediaPreviewService( + fetchMediaPreviewConfigResult = { Result.success(null) }, + setHideInviteAvatarsResult = { value -> + setHideInviteAvatarsValue = value + Result.success(Unit) + }, + setMediaPreviewValueResult = { value -> + setMediaPreviewValue = value + Result.success(Unit) + } + ) + val migration = createMigration(appPreferencesStore, mediaPreviewService) + + migration().join() + + // Verify server was updated with both local values + assertThat(setHideInviteAvatarsValue).isTrue() + assertThat(setMediaPreviewValue).isEqualTo(MediaPreviewValue.Off) + // Verify local data was cleared + assertThat(appPreferencesStore.getHideInviteAvatarsFlow().first()).isNull() + assertThat(appPreferencesStore.getTimelineMediaPreviewValueFlow().first()).isNull() + } + + @Test + fun `when fetch config fails, migration does nothing`() = runTest { + val appPreferencesStore = InMemoryAppPreferencesStore().apply { + setHideInviteAvatars(true) + setTimelineMediaPreviewValue(MediaPreviewValue.Private) + } + val mediaPreviewService = FakeMediaPreviewService( + fetchMediaPreviewConfigResult = { Result.failure(Exception("Network error")) } + ) + val migration = createMigration(appPreferencesStore, mediaPreviewService) + + migration().join() + + // Verify local data was not cleared since migration failed + assertThat(appPreferencesStore.getHideInviteAvatarsFlow().first()).isTrue() + assertThat(appPreferencesStore.getTimelineMediaPreviewValueFlow().first()).isEqualTo(MediaPreviewValue.Private) + } + + private fun TestScope.createMigration( + appPreferencesStore: InMemoryAppPreferencesStore, + mediaPreviewService: FakeMediaPreviewService + ) = MediaPreviewConfigMigration( + mediaPreviewService = mediaPreviewService, + appPreferencesStore = appPreferencesStore, + sessionCoroutineScope = this + ) +} diff --git a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenter.kt b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenter.kt index e8514ac4a5..6206c2426a 100644 --- a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenter.kt +++ b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenter.kt @@ -34,6 +34,7 @@ import io.element.android.features.roomdirectory.api.RoomDescription import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.runUpdatingState +import io.element.android.libraries.core.coroutine.mapState import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomId @@ -49,7 +50,6 @@ import io.element.android.libraries.matrix.api.room.join.JoinRoom import io.element.android.libraries.matrix.api.room.join.JoinRule import io.element.android.libraries.matrix.api.room.preview.RoomPreviewInfo import io.element.android.libraries.matrix.ui.model.toInviteSender -import io.element.android.libraries.preferences.api.store.AppPreferencesStore import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import java.util.Optional @@ -67,7 +67,6 @@ class JoinRoomPresenter @AssistedInject constructor( private val forgetRoom: ForgetRoom, private val acceptDeclineInvitePresenter: Presenter, private val buildMeta: BuildMeta, - private val appPreferencesStore: AppPreferencesStore, private val seenInvitesStore: SeenInvitesStore, ) : Presenter { interface Factory { @@ -94,8 +93,11 @@ class JoinRoomPresenter @AssistedInject constructor( var knockMessage by rememberSaveable { mutableStateOf("") } var isDismissingContent by remember { mutableStateOf(false) } val hideInviteAvatars by remember { - appPreferencesStore.getHideInviteAvatarsFlow() - }.collectAsState(initial = false) + matrixClient + .mediaPreviewService() + .mediaPreviewConfigFlow + .mapState { config -> config.hideInviteAvatar } + }.collectAsState() val canReportRoom by produceState(false) { value = matrixClient.canReportRoom() } val contentState by produceState( diff --git a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/di/JoinRoomModule.kt b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/di/JoinRoomModule.kt index 7142133eb1..5e85c0abbc 100644 --- a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/di/JoinRoomModule.kt +++ b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/di/JoinRoomModule.kt @@ -22,7 +22,6 @@ import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomIdOrAlias import io.element.android.libraries.matrix.api.room.join.JoinRoom -import io.element.android.libraries.preferences.api.store.AppPreferencesStore import java.util.Optional @Module @@ -37,7 +36,6 @@ object JoinRoomModule { forgetRoom: ForgetRoom, acceptDeclineInvitePresenter: Presenter, buildMeta: BuildMeta, - appPreferencesStore: AppPreferencesStore, seenInvitesStore: SeenInvitesStore, ): JoinRoomPresenter.Factory { return object : JoinRoomPresenter.Factory { @@ -61,7 +59,6 @@ object JoinRoomModule { cancelKnockRoom = cancelKnockRoom, acceptDeclineInvitePresenter = acceptDeclineInvitePresenter, buildMeta = buildMeta, - appPreferencesStore = appPreferencesStore, seenInvitesStore = seenInvitesStore, ) } diff --git a/features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenterTest.kt b/features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenterTest.kt index ac716a4203..8a475668ae 100644 --- a/features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenterTest.kt +++ b/features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenterTest.kt @@ -52,8 +52,6 @@ import io.element.android.libraries.matrix.test.room.aRoomPreviewInfo import io.element.android.libraries.matrix.test.room.join.FakeJoinRoom import io.element.android.libraries.matrix.ui.model.InviteSender import io.element.android.libraries.matrix.ui.model.toInviteSender -import io.element.android.libraries.preferences.api.store.AppPreferencesStore -import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.lambda.any import io.element.android.tests.testutils.lambda.assert @@ -1057,7 +1055,6 @@ class JoinRoomPresenterTest { forgetRoom: ForgetRoom = FakeForgetRoom(), buildMeta: BuildMeta = aBuildMeta(applicationName = "AppName"), acceptDeclineInvitePresenter: Presenter = Presenter { anAcceptDeclineInviteState() }, - appPreferencesStore: AppPreferencesStore = InMemoryAppPreferencesStore(), seenInvitesStore: SeenInvitesStore = InMemorySeenInvitesStore(), ): JoinRoomPresenter { return JoinRoomPresenter( @@ -1073,7 +1070,6 @@ class JoinRoomPresenterTest { forgetRoom = forgetRoom, buildMeta = buildMeta, acceptDeclineInvitePresenter = acceptDeclineInvitePresenter, - appPreferencesStore = appPreferencesStore, seenInvitesStore = seenInvitesStore, ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/protection/TimelineProtectionPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/protection/TimelineProtectionPresenter.kt index 00cde0f880..b4c2576a65 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/protection/TimelineProtectionPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/protection/TimelineProtectionPresenter.kt @@ -14,16 +14,16 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.core.coroutine.mapState import io.element.android.libraries.matrix.api.core.EventId -import io.element.android.libraries.matrix.api.media.MediaPreviewValue +import io.element.android.libraries.matrix.api.media.MediaPreviewService import io.element.android.libraries.matrix.api.media.isPreviewEnabled import io.element.android.libraries.matrix.api.room.BaseRoom -import io.element.android.libraries.preferences.api.store.AppPreferencesStore import kotlinx.collections.immutable.toImmutableSet import javax.inject.Inject class TimelineProtectionPresenter @Inject constructor( - private val appPreferencesStore: AppPreferencesStore, + private val mediaPreviewService: MediaPreviewService, private val room: BaseRoom, ) : Presenter { private val allowedEvents = mutableStateOf>(setOf()) @@ -31,8 +31,8 @@ class TimelineProtectionPresenter @Inject constructor( @Composable override fun present(): TimelineProtectionState { val mediaPreviewValue = remember { - appPreferencesStore.getTimelineMediaPreviewValueFlow() - }.collectAsState(initial = MediaPreviewValue.On) + mediaPreviewService.mediaPreviewConfigFlow.mapState { config -> config.mediaPreviewValue } + }.collectAsState() val roomInfo = room.roomInfoFlow.collectAsState() val protectionState by remember { derivedStateOf { diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/protection/TimelineProtectionPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/protection/TimelineProtectionPresenterTest.kt index ac8e2a5290..49f30716a8 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/protection/TimelineProtectionPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/protection/TimelineProtectionPresenterTest.kt @@ -8,17 +8,19 @@ package io.element.android.features.messages.impl.timeline.protection import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.api.media.MediaPreviewConfig +import io.element.android.libraries.matrix.api.media.MediaPreviewService import io.element.android.libraries.matrix.api.media.MediaPreviewValue import io.element.android.libraries.matrix.api.room.BaseRoom import io.element.android.libraries.matrix.api.room.join.JoinRule import io.element.android.libraries.matrix.test.AN_EVENT_ID +import io.element.android.libraries.matrix.test.media.FakeMediaPreviewService import io.element.android.libraries.matrix.test.room.FakeBaseRoom import io.element.android.libraries.matrix.test.room.aRoomInfo -import io.element.android.libraries.preferences.api.store.AppPreferencesStore -import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.test import kotlinx.collections.immutable.persistentSetOf +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.runTest import org.junit.Rule import org.junit.Test @@ -38,10 +40,10 @@ class TimelineProtectionPresenterTest { @Test fun `present - media preview value off`() = runTest { - val appPreferencesStore = InMemoryAppPreferencesStore(timelineMediaPreviewValue = MediaPreviewValue.Off) - val presenter = createPresenter(appPreferencesStore) + val mediaPreviewConfig = MediaPreviewConfig(mediaPreviewValue = MediaPreviewValue.Off, hideInviteAvatar = false) + val mediaPreviewService = FakeMediaPreviewService(mediaPreviewConfigFlow = MutableStateFlow(mediaPreviewConfig)) + val presenter = createPresenter(mediaPreviewService = mediaPreviewService) presenter.test { - skipItems(1) val initialState = awaitItem() assertThat(initialState.protectionState).isEqualTo(ProtectionState.RenderOnly(persistentSetOf())) // ShowContent with null should have no effect. @@ -54,11 +56,11 @@ class TimelineProtectionPresenterTest { @Test fun `present - media preview value private in public room`() = runTest { - val appPreferencesStore = InMemoryAppPreferencesStore(timelineMediaPreviewValue = MediaPreviewValue.Private) + val mediaPreviewConfig = MediaPreviewConfig(mediaPreviewValue = MediaPreviewValue.Private, hideInviteAvatar = false) + val mediaPreviewService = FakeMediaPreviewService(mediaPreviewConfigFlow = MutableStateFlow(mediaPreviewConfig)) val room = FakeBaseRoom(initialRoomInfo = aRoomInfo(joinRule = JoinRule.Public)) - val presenter = createPresenter(appPreferencesStore, room) + val presenter = createPresenter(mediaPreviewService = mediaPreviewService, room = room) presenter.test { - skipItems(1) val initialState = awaitItem() assertThat(initialState.protectionState).isEqualTo(ProtectionState.RenderOnly(persistentSetOf())) // ShowContent with null should have no effect. @@ -71,9 +73,10 @@ class TimelineProtectionPresenterTest { @Test fun `present - media preview value private in non public room`() = runTest { - val appPreferencesStore = InMemoryAppPreferencesStore(timelineMediaPreviewValue = MediaPreviewValue.Private) + val mediaPreviewConfig = MediaPreviewConfig(mediaPreviewValue = MediaPreviewValue.Private, hideInviteAvatar = false) + val mediaPreviewService = FakeMediaPreviewService(mediaPreviewConfigFlow = MutableStateFlow(mediaPreviewConfig)) val room = FakeBaseRoom(initialRoomInfo = aRoomInfo(joinRule = JoinRule.Invite)) - val presenter = createPresenter(appPreferencesStore, room) + val presenter = createPresenter(mediaPreviewService = mediaPreviewService, room = room) presenter.test { val initialState = awaitItem() assertThat(initialState.protectionState).isEqualTo(ProtectionState.RenderAll) @@ -84,10 +87,10 @@ class TimelineProtectionPresenterTest { } private fun createPresenter( - appPreferencesStore: AppPreferencesStore = InMemoryAppPreferencesStore(), room: BaseRoom = FakeBaseRoom(), + mediaPreviewService: MediaPreviewService = FakeMediaPreviewService(), ) = TimelineProtectionPresenter( - appPreferencesStore = appPreferencesStore, + mediaPreviewService = mediaPreviewService, room = room, ) } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenter.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenter.kt index a47ea25e4e..be5f11116c 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenter.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenter.kt @@ -8,21 +8,15 @@ package io.element.android.features.preferences.impl.advanced import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import io.element.android.compound.theme.Theme import io.element.android.compound.theme.mapToTheme -import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.Presenter -import io.element.android.libraries.architecture.runUpdatingState import io.element.android.libraries.di.annotations.SessionCoroutineScope -import io.element.android.libraries.matrix.api.MatrixClient -import io.element.android.libraries.matrix.api.media.MediaPreviewValue import io.element.android.libraries.preferences.api.store.AppPreferencesStore import io.element.android.libraries.preferences.api.store.SessionPreferencesStore import kotlinx.coroutines.CoroutineScope @@ -51,6 +45,8 @@ class AdvancedSettingsPresenter @Inject constructor( appPreferencesStore.getThemeFlow().mapToTheme() }.collectAsState(initial = Theme.System) + val mediaPreviewConfigState = mediaPreviewConfigStateStore.state() + val themeOption by remember { derivedStateOf { when (theme.value) { @@ -89,10 +85,7 @@ class AdvancedSettingsPresenter @Inject constructor( isSharePresenceEnabled = isSharePresenceEnabled, doesCompressMedia = doesCompressMedia, theme = themeOption, - hideInviteAvatars = mediaPreviewConfigStateStore.hideInviteAvatars.value, - timelineMediaPreviewValue = mediaPreviewConfigStateStore.timelineMediaPreviewValue.value, - setHideInviteAvatarsAction = mediaPreviewConfigStateStore.setHideInviteAvatarsAction.value, - setTimelineMediaPreviewAction = mediaPreviewConfigStateStore.setTimelineMediaPreviewAction.value, + mediaPreviewConfigState = mediaPreviewConfigState, eventSink = ::handleEvents, ) } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsState.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsState.kt index 6386984146..ef41ab26b3 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsState.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsState.kt @@ -10,9 +10,7 @@ package io.element.android.features.preferences.impl.advanced import androidx.compose.runtime.Composable import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.ui.res.stringResource -import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.designsystem.components.preferences.DropdownOption -import io.element.android.libraries.matrix.api.media.MediaPreviewValue import io.element.android.libraries.ui.strings.CommonStrings data class AdvancedSettingsState( @@ -20,10 +18,7 @@ data class AdvancedSettingsState( val isSharePresenceEnabled: Boolean, val doesCompressMedia: Boolean, val theme: ThemeOption, - val hideInviteAvatars: Boolean, - val timelineMediaPreviewValue: MediaPreviewValue, - val setHideInviteAvatarsAction: AsyncAction, - val setTimelineMediaPreviewAction: AsyncAction, + val mediaPreviewConfigState: MediaPreviewConfigState, val eventSink: (AdvancedSettingsEvents) -> Unit ) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsStateProvider.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsStateProvider.kt index 09b3b6e1a5..42e63134d1 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsStateProvider.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsStateProvider.kt @@ -29,8 +29,8 @@ fun aAdvancedSettingsState( isDeveloperModeEnabled: Boolean = false, isSharePresenceEnabled: Boolean = false, doesCompressMedia: Boolean = false, - hideInviteAvatars: Boolean = false, theme: ThemeOption = ThemeOption.System, + hideInviteAvatars: Boolean = false, timelineMediaPreviewValue: MediaPreviewValue = MediaPreviewValue.On, setTimelineMediaPreviewAction: AsyncAction = AsyncAction.Uninitialized, setHideInviteAvatarsAction: AsyncAction = AsyncAction.Uninitialized, @@ -40,9 +40,11 @@ fun aAdvancedSettingsState( isSharePresenceEnabled = isSharePresenceEnabled, doesCompressMedia = doesCompressMedia, theme = theme, - hideInviteAvatars = hideInviteAvatars, - timelineMediaPreviewValue = timelineMediaPreviewValue, - setTimelineMediaPreviewAction = setTimelineMediaPreviewAction, - setHideInviteAvatarsAction = setHideInviteAvatarsAction, + mediaPreviewConfigState = MediaPreviewConfigState( + hideInviteAvatars = hideInviteAvatars, + timelineMediaPreviewValue = timelineMediaPreviewValue, + setTimelineMediaPreviewAction = setTimelineMediaPreviewAction, + setHideInviteAvatarsAction = setHideInviteAvatarsAction + ), eventSink = eventSink ) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsView.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsView.kt index 924f178b32..c92f50bd2e 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsView.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsView.kt @@ -133,11 +133,11 @@ private fun ModerationAndSafety( ) { PreferenceSwitch( title = stringResource(R.string.screen_advanced_settings_hide_invite_avatars_toggle_title), - isChecked = state.hideInviteAvatars, + isChecked = state.mediaPreviewConfigState.hideInviteAvatars, onCheckedChange = { state.eventSink(AdvancedSettingsEvents.SetHideInviteAvatars(it)) }, - enabled = !state.setHideInviteAvatarsAction.isLoading() + enabled = !state.mediaPreviewConfigState.setHideInviteAvatarsAction.isLoading() ) ListSectionHeader( title = stringResource(R.string.screen_advanced_settings_show_media_timeline_title), @@ -153,27 +153,36 @@ private fun ModerationAndSafety( ) ListItem( headlineContent = { Text(text = stringResource(R.string.screen_advanced_settings_show_media_timeline_always_hide)) }, - leadingContent = ListItemContent.RadioButton(selected = state.timelineMediaPreviewValue == MediaPreviewValue.Off, compact = true), + leadingContent = ListItemContent.RadioButton( + selected = state.mediaPreviewConfigState.timelineMediaPreviewValue == MediaPreviewValue.Off, + compact = true + ), onClick = { state.eventSink(AdvancedSettingsEvents.SetTimelineMediaPreviewValue(MediaPreviewValue.Off)) }, - enabled = !state.setTimelineMediaPreviewAction.isLoading() + enabled = !state.mediaPreviewConfigState.setTimelineMediaPreviewAction.isLoading() ) ListItem( headlineContent = { Text(text = stringResource(R.string.screen_advanced_settings_show_media_timeline_private_rooms)) }, - leadingContent = ListItemContent.RadioButton(selected = state.timelineMediaPreviewValue == MediaPreviewValue.Private, compact = true), + leadingContent = ListItemContent.RadioButton( + selected = state.mediaPreviewConfigState.timelineMediaPreviewValue == MediaPreviewValue.Private, + compact = true + ), onClick = { state.eventSink(AdvancedSettingsEvents.SetTimelineMediaPreviewValue(MediaPreviewValue.Private)) }, - enabled = !state.setTimelineMediaPreviewAction.isLoading() + enabled = !state.mediaPreviewConfigState.setTimelineMediaPreviewAction.isLoading() ) ListItem( headlineContent = { Text(text = stringResource(R.string.screen_advanced_settings_show_media_timeline_always_show)) }, - leadingContent = ListItemContent.RadioButton(selected = state.timelineMediaPreviewValue == MediaPreviewValue.On, compact = true), + leadingContent = ListItemContent.RadioButton( + selected = state.mediaPreviewConfigState.timelineMediaPreviewValue == MediaPreviewValue.On, + compact = true + ), onClick = { state.eventSink(AdvancedSettingsEvents.SetTimelineMediaPreviewValue(MediaPreviewValue.On)) }, - enabled = !state.setTimelineMediaPreviewAction.isLoading() + enabled = !state.mediaPreviewConfigState.setTimelineMediaPreviewAction.isLoading() ) } } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/MediaPreviewConfigStateStore.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/MediaPreviewConfigStateStore.kt index c503ba49a2..715938b93b 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/MediaPreviewConfigStateStore.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/MediaPreviewConfigStateStore.kt @@ -7,7 +7,7 @@ package io.element.android.features.preferences.impl.advanced -import androidx.compose.runtime.State +import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf import com.squareup.anvil.annotations.ContributesBinding import io.element.android.libraries.architecture.AsyncAction @@ -21,27 +21,29 @@ import io.element.android.libraries.matrix.api.media.MediaPreviewService import io.element.android.libraries.matrix.api.media.MediaPreviewValue import io.element.android.libraries.ui.strings.CommonStrings import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject -interface MediaPreviewConfigStateStore { - val hideInviteAvatars: State - val timelineMediaPreviewValue: State - val setHideInviteAvatarsAction: State> - val setTimelineMediaPreviewAction: State> +data class MediaPreviewConfigState( + val hideInviteAvatars: Boolean, + val timelineMediaPreviewValue: MediaPreviewValue, + val setHideInviteAvatarsAction: AsyncAction, + val setTimelineMediaPreviewAction: AsyncAction, +) +interface MediaPreviewConfigStateStore { + @Composable + fun state(): MediaPreviewConfigState fun setHideInviteAvatars(hide: Boolean) fun setTimelineMediaPreviewValue(value: MediaPreviewValue) } -@ContributesBinding(SessionScope::class, boundType = MediaPreviewConfigStateStore::class) +@ContributesBinding(SessionScope::class) @SingleIn(SessionScope::class) class DefaultMediaPreviewConfigStateStore @Inject constructor( @SessionCoroutineScope @@ -49,19 +51,19 @@ class DefaultMediaPreviewConfigStateStore @Inject constructor( private val mediaPreviewService: MediaPreviewService, private val snackbarDispatcher: SnackbarDispatcher, ) : MediaPreviewConfigStateStore { - override val hideInviteAvatars = mutableStateOf(false) - override val timelineMediaPreviewValue = mutableStateOf(MediaPreviewValue.On) - override val setHideInviteAvatarsAction = mutableStateOf>(AsyncAction.Uninitialized) - override val setTimelineMediaPreviewAction = mutableStateOf>(AsyncAction.Uninitialized) + private val hideInviteAvatars = mutableStateOf(false) + private val timelineMediaPreviewValue = mutableStateOf(MediaPreviewValue.On) + private val setHideInviteAvatarsAction = mutableStateOf>(AsyncAction.Uninitialized) + private val setTimelineMediaPreviewAction = mutableStateOf>(AsyncAction.Uninitialized) init { - val configFlow = mediaPreviewService.getMediaPreviewConfigFlow().shareIn(sessionCoroutineScope, SharingStarted.Eagerly) - val hideInviteAvatarsFlow = configFlow.mapNotNull { it?.hideInviteAvatar }.distinctUntilChanged() - val timelineMediaPreviewFlow = configFlow.mapNotNull { it?.mediaPreviewValue }.distinctUntilChanged() + val configFlow = mediaPreviewService.mediaPreviewConfigFlow + val hideInviteAvatarsFlow = configFlow.map { it.hideInviteAvatar }.distinctUntilChanged() + val timelineMediaPreviewFlow = configFlow.map { it.mediaPreviewValue }.distinctUntilChanged() hideInviteAvatarsFlow .onEach { - Timber.d("Hide invi@te avatars changed to $it") + Timber.d("Hide invite avatars changed to $it") hideInviteAvatars.value = it } .launchIn(sessionCoroutineScope) @@ -74,6 +76,16 @@ class DefaultMediaPreviewConfigStateStore @Inject constructor( .launchIn(sessionCoroutineScope) } + @Composable + override fun state(): MediaPreviewConfigState { + return MediaPreviewConfigState( + hideInviteAvatars = hideInviteAvatars.value, + timelineMediaPreviewValue = timelineMediaPreviewValue.value, + setHideInviteAvatarsAction = setHideInviteAvatarsAction.value, + setTimelineMediaPreviewAction = setTimelineMediaPreviewAction.value, + ) + } + override fun setHideInviteAvatars(hide: Boolean) { sessionCoroutineScope.launch { Timber.d("Setting hide invite avatars to $hide") @@ -106,4 +118,3 @@ class DefaultMediaPreviewConfigStateStore @Inject constructor( } } } - diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenterTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenterTest.kt index de841f3f6d..de84e887d6 100644 --- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenterTest.kt +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenterTest.kt @@ -11,14 +11,12 @@ import app.cash.molecule.RecompositionMode import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth.assertThat -import io.element.android.libraries.matrix.api.MatrixClient -import io.element.android.libraries.matrix.api.media.MediaPreviewConfig +import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.matrix.api.media.MediaPreviewValue -import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore import io.element.android.libraries.preferences.test.InMemorySessionPreferencesStore import io.element.android.tests.testutils.WarmUpRule -import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.test.runTest import org.junit.Rule import org.junit.Test @@ -38,6 +36,10 @@ class AdvancedSettingsPresenterTest { assertThat(isSharePresenceEnabled).isTrue() assertThat(doesCompressMedia).isTrue() assertThat(theme).isEqualTo(ThemeOption.System) + assertThat(mediaPreviewConfigState.hideInviteAvatars).isFalse() + assertThat(mediaPreviewConfigState.timelineMediaPreviewValue).isEqualTo(MediaPreviewValue.On) + assertThat(mediaPreviewConfigState.setHideInviteAvatarsAction).isEqualTo(AsyncAction.Uninitialized) + assertThat(mediaPreviewConfigState.setTimelineMediaPreviewAction).isEqualTo(AsyncAction.Uninitialized) } } } @@ -128,56 +130,92 @@ class AdvancedSettingsPresenterTest { @Test fun `present - hide invite avatars`() = runTest { - val presenter = createAdvancedSettingsPresenter() + val mediaPreviewStore = FakeMediaPreviewConfigStateStore() + val presenter = createAdvancedSettingsPresenter(mediaPreviewConfigStateStore = mediaPreviewStore) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { with(awaitItem()) { - assertThat(hideInviteAvatars).isFalse() + assertThat(mediaPreviewConfigState.hideInviteAvatars).isFalse() eventSink(AdvancedSettingsEvents.SetHideInviteAvatars(true)) } with(awaitItem()) { - assertThat(hideInviteAvatars).isTrue() + assertThat(mediaPreviewConfigState.hideInviteAvatars).isTrue() eventSink(AdvancedSettingsEvents.SetHideInviteAvatars(false)) } with(awaitItem()) { - assertThat(hideInviteAvatars).isFalse() + assertThat(mediaPreviewConfigState.hideInviteAvatars).isFalse() + } + } + assertThat(mediaPreviewStore.getSetHideInviteAvatarsEvents()).isEqualTo(listOf(true, false)) + } + + @Test + fun `present - timeline media preview value`() = runTest { + val mediaPreviewStore = FakeMediaPreviewConfigStateStore() + val presenter = createAdvancedSettingsPresenter(mediaPreviewConfigStateStore = mediaPreviewStore) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + with(awaitItem()) { + assertThat(mediaPreviewConfigState.timelineMediaPreviewValue).isEqualTo(MediaPreviewValue.On) + eventSink(AdvancedSettingsEvents.SetTimelineMediaPreviewValue(MediaPreviewValue.Off)) + } + with(awaitItem()) { + assertThat(mediaPreviewConfigState.timelineMediaPreviewValue).isEqualTo(MediaPreviewValue.Off) + eventSink(AdvancedSettingsEvents.SetTimelineMediaPreviewValue(MediaPreviewValue.Private)) + } + with(awaitItem()) { + assertThat(mediaPreviewConfigState.timelineMediaPreviewValue).isEqualTo(MediaPreviewValue.Private) + } + } + assertThat(mediaPreviewStore.getSetTimelineMediaPreviewValueEvents()).isEqualTo( + listOf(MediaPreviewValue.Off, MediaPreviewValue.Private) + ) + } + + @Test + fun `present - media preview state with custom initial values`() = runTest { + val mediaPreviewStore = FakeMediaPreviewConfigStateStore( + hideInviteAvatarsValue = true, + timelineMediaPreviewValue = MediaPreviewValue.Private + ) + val presenter = createAdvancedSettingsPresenter(mediaPreviewConfigStateStore = mediaPreviewStore) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + with(awaitItem()) { + assertThat(mediaPreviewConfigState.hideInviteAvatars).isTrue() + assertThat(mediaPreviewConfigState.timelineMediaPreviewValue).isEqualTo(MediaPreviewValue.Private) } } } @Test - fun `present - timeline media preview value`() = runTest { - val mediaPreviewConfigFlow = MutableStateFlow(null) - val presenter = createAdvancedSettingsPresenter( - matrixClient = FakeMatrixClient( - mediaPreviewConfigFlow = mediaPreviewConfigFlow - ) + fun `present - async actions state`() = runTest { + val mediaPreviewStore = FakeMediaPreviewConfigStateStore( + setHideInviteAvatarsActionValue = AsyncAction.Loading, + setTimelineMediaPreviewActionValue = AsyncAction.Success(Unit) ) + val presenter = createAdvancedSettingsPresenter(mediaPreviewConfigStateStore = mediaPreviewStore) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { with(awaitItem()) { - assertThat(timelineMediaPreviewValue).isEqualTo(MediaPreviewValue.On) - eventSink(AdvancedSettingsEvents.SetTimelineMediaPreviewValue(MediaPreviewValue.Off)) - } - with(awaitItem()) { - assertThat(timelineMediaPreviewValue).isEqualTo(MediaPreviewValue.Off) - eventSink(AdvancedSettingsEvents.SetTimelineMediaPreviewValue(MediaPreviewValue.Private)) - } - with(awaitItem()) { - assertThat(timelineMediaPreviewValue).isEqualTo(MediaPreviewValue.Private) + assertThat(mediaPreviewConfigState.setHideInviteAvatarsAction).isEqualTo(AsyncAction.Loading) + assertThat(mediaPreviewConfigState.setTimelineMediaPreviewAction).isEqualTo(AsyncAction.Success(Unit)) } } } - private fun createAdvancedSettingsPresenter( + private fun CoroutineScope.createAdvancedSettingsPresenter( appPreferencesStore: InMemoryAppPreferencesStore = InMemoryAppPreferencesStore(), sessionPreferencesStore: InMemorySessionPreferencesStore = InMemorySessionPreferencesStore(), - matrixClient: MatrixClient = FakeMatrixClient(), + mediaPreviewConfigStateStore: MediaPreviewConfigStateStore = FakeMediaPreviewConfigStateStore(), ) = AdvancedSettingsPresenter( appPreferencesStore = appPreferencesStore, sessionPreferencesStore = sessionPreferencesStore, - matrixClient = matrixClient, + mediaPreviewConfigStateStore = mediaPreviewConfigStateStore, + sessionCoroutineScope = this, ) } diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsViewTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsViewTest.kt index baf55d2999..d5e90047c7 100644 --- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsViewTest.kt +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsViewTest.kt @@ -15,6 +15,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat import im.vector.app.features.analytics.plan.Interaction import io.element.android.features.preferences.impl.R +import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.matrix.api.media.MediaPreviewValue import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.services.analytics.api.AnalyticsService @@ -131,7 +132,7 @@ class AdvancedSettingsViewTest { } @Test - @Config(qualifiers = "h640dp") + @Config(qualifiers = "h1080dp") fun `clicking on hide invite avatars emits the expected event`() { val eventsRecorder = EventsRecorder() rule.setAdvancedSettingsView( @@ -145,8 +146,8 @@ class AdvancedSettingsViewTest { } @Test - @Config(qualifiers = "h640dp") - fun `clicking on timeline media preview emits the expected event`() { + @Config(qualifiers = "h1080dp") + fun `clicking on timeline media preview always hide emits the expected event`() { val eventsRecorder = EventsRecorder() rule.setAdvancedSettingsView( state = aAdvancedSettingsState( @@ -157,6 +158,65 @@ class AdvancedSettingsViewTest { rule.clickOn(R.string.screen_advanced_settings_show_media_timeline_always_hide) eventsRecorder.assertSingle(AdvancedSettingsEvents.SetTimelineMediaPreviewValue(MediaPreviewValue.Off)) } + + @Test + @Config(qualifiers = "h1080dp") + fun `clicking on timeline media preview private rooms emits the expected event`() { + val eventsRecorder = EventsRecorder() + rule.setAdvancedSettingsView( + state = aAdvancedSettingsState( + eventSink = eventsRecorder, + timelineMediaPreviewValue = MediaPreviewValue.On + ), + ) + rule.clickOn(R.string.screen_advanced_settings_show_media_timeline_private_rooms) + eventsRecorder.assertSingle(AdvancedSettingsEvents.SetTimelineMediaPreviewValue(MediaPreviewValue.Private)) + } + + @Test + @Config(qualifiers = "h1080dp") + fun `clicking on timeline media preview always show emits the expected event`() { + val eventsRecorder = EventsRecorder() + rule.setAdvancedSettingsView( + state = aAdvancedSettingsState( + eventSink = eventsRecorder, + timelineMediaPreviewValue = MediaPreviewValue.Off + ), + ) + rule.clickOn(R.string.screen_advanced_settings_show_media_timeline_always_show) + eventsRecorder.assertSingle(AdvancedSettingsEvents.SetTimelineMediaPreviewValue(MediaPreviewValue.On)) + } + + @Test + @Config(qualifiers = "h1080dp") + fun `hide invite avatars toggle is disabled when action is loading`() { + val eventsRecorder = EventsRecorder(expectEvents = false) + rule.setAdvancedSettingsView( + state = aAdvancedSettingsState( + eventSink = eventsRecorder, + hideInviteAvatars = false, + setHideInviteAvatarsAction = AsyncAction.Loading + ), + ) + // The toggle should be disabled, so clicking should not emit any events + rule.clickOn(R.string.screen_advanced_settings_hide_invite_avatars_toggle_title) + } + + @Test + @Config(qualifiers = "h1080dp") + fun `timeline media preview options are disabled when action is loading`() { + val eventsRecorder = EventsRecorder(expectEvents = false) + rule.setAdvancedSettingsView( + state = aAdvancedSettingsState( + eventSink = eventsRecorder, + timelineMediaPreviewValue = MediaPreviewValue.On, + setTimelineMediaPreviewAction = AsyncAction.Loading + ), + ) + // The options should be disabled, so clicking should not emit any events + rule.clickOn(R.string.screen_advanced_settings_show_media_timeline_always_hide) + rule.clickOn(R.string.screen_advanced_settings_show_media_timeline_private_rooms) + } } private fun AndroidComposeTestRule.setAdvancedSettingsView( diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/advanced/FakeMediaPreviewConfigStateStore.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/advanced/FakeMediaPreviewConfigStateStore.kt new file mode 100644 index 0000000000..18c6283965 --- /dev/null +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/advanced/FakeMediaPreviewConfigStateStore.kt @@ -0,0 +1,51 @@ +/* + * Copyright 2025 New Vector 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.advanced + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.matrix.api.media.MediaPreviewValue + +class FakeMediaPreviewConfigStateStore( + hideInviteAvatarsValue: Boolean = false, + timelineMediaPreviewValue: MediaPreviewValue = MediaPreviewValue.On, + setHideInviteAvatarsActionValue: AsyncAction = AsyncAction.Uninitialized, + setTimelineMediaPreviewActionValue: AsyncAction = AsyncAction.Uninitialized, +) : MediaPreviewConfigStateStore { + private val hideInviteAvatars = mutableStateOf(hideInviteAvatarsValue) + private val timelineMediaPreviewValue = mutableStateOf(timelineMediaPreviewValue) + private val setHideInviteAvatarsAction = mutableStateOf(setHideInviteAvatarsActionValue) + private val setTimelineMediaPreviewAction = mutableStateOf(setTimelineMediaPreviewActionValue) + + private val setHideInviteAvatarsEvents = mutableListOf() + private val setTimelineMediaPreviewValueEvents = mutableListOf() + + @Composable + override fun state(): MediaPreviewConfigState { + return MediaPreviewConfigState( + hideInviteAvatars = hideInviteAvatars.value, + timelineMediaPreviewValue = timelineMediaPreviewValue.value, + setHideInviteAvatarsAction = setHideInviteAvatarsAction.value, + setTimelineMediaPreviewAction = setTimelineMediaPreviewAction.value, + ) + } + + override fun setHideInviteAvatars(hide: Boolean) { + setHideInviteAvatarsEvents.add(hide) + hideInviteAvatars.value = hide + } + + override fun setTimelineMediaPreviewValue(value: MediaPreviewValue) { + setTimelineMediaPreviewValueEvents.add(value) + timelineMediaPreviewValue.value = value + } + + fun getSetHideInviteAvatarsEvents(): List = setHideInviteAvatarsEvents.toList() + fun getSetTimelineMediaPreviewValueEvents(): List = setTimelineMediaPreviewValueEvents.toList() +} diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/advanced/MediaPreviewConfigStateStoreTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/advanced/MediaPreviewConfigStateStoreTest.kt new file mode 100644 index 0000000000..01ede60e92 --- /dev/null +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/advanced/MediaPreviewConfigStateStoreTest.kt @@ -0,0 +1,206 @@ +/* + * Copyright 2025 New Vector 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.advanced + +import app.cash.molecule.RecompositionMode +import app.cash.molecule.moleculeFlow +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher +import io.element.android.libraries.matrix.api.media.MediaPreviewConfig +import io.element.android.libraries.matrix.api.media.MediaPreviewValue +import io.element.android.libraries.matrix.test.media.FakeMediaPreviewService +import io.element.android.tests.testutils.WarmUpRule +import io.element.android.tests.testutils.lambda.assert +import io.element.android.tests.testutils.lambda.lambdaRecorder +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest +import org.junit.Rule +import org.junit.Test + +class MediaPreviewConfigStateStoreTest { + @get:Rule + val warmUpRule = WarmUpRule() + + @Test + fun `initial state is correct with default values`() = runTest { + val store = createMediaPreviewConfigStateStore() + + moleculeFlow(RecompositionMode.Immediate) { + store.state() + }.test { + val initialState = awaitItem() + assertThat(initialState.hideInviteAvatars).isFalse() + assertThat(initialState.timelineMediaPreviewValue).isEqualTo(MediaPreviewValue.On) + assertThat(initialState.setHideInviteAvatarsAction).isInstanceOf(AsyncAction.Uninitialized::class.java) + assertThat(initialState.setTimelineMediaPreviewAction).isInstanceOf(AsyncAction.Uninitialized::class.java) + } + } + + @Test + fun `state updates when config flow emits new values`() = runTest { + val configFlow = MutableStateFlow(MediaPreviewConfig.DEFAULT) + val mediaPreviewService = FakeMediaPreviewService(configFlow) + val store = createMediaPreviewConfigStateStore(mediaPreviewService = mediaPreviewService) + + moleculeFlow(RecompositionMode.Immediate) { + store.state() + }.test { + // Initial state + val initialState = awaitItem() + assertThat(initialState.hideInviteAvatars).isFalse() + assertThat(initialState.timelineMediaPreviewValue).isEqualTo(MediaPreviewValue.On) + + // Update config + configFlow.value = MediaPreviewConfig(hideInviteAvatar = true, mediaPreviewValue = MediaPreviewValue.Private) + + skipItems(1) + // Updated state + val updatedState = awaitItem() + assertThat(updatedState.hideInviteAvatars).isTrue() + assertThat(updatedState.timelineMediaPreviewValue).isEqualTo(MediaPreviewValue.Private) + } + } + + @Test + fun `setHideInviteAvatars updates state and calls service on success`() = runTest { + val setHideInviteAvatarsValueLambda = lambdaRecorder> { Result.success(Unit) } + val mediaPreviewService = FakeMediaPreviewService( + setHideInviteAvatarsResult = setHideInviteAvatarsValueLambda + ) + val store = createMediaPreviewConfigStateStore(mediaPreviewService = mediaPreviewService) + moleculeFlow(RecompositionMode.Immediate) { + store.state() + }.test { + awaitItem().also { state -> + assertThat(state.hideInviteAvatars).isFalse() + } + store.setHideInviteAvatars(true) + + awaitItem().also { state -> + assertThat(state.hideInviteAvatars).isTrue() + } + awaitItem().also { state -> + assertThat(state.hideInviteAvatars).isTrue() + assertThat(state.setHideInviteAvatarsAction).isInstanceOf(AsyncAction.Loading::class.java) + } + awaitItem().also { state -> + assertThat(state.hideInviteAvatars).isTrue() + assertThat(state.setHideInviteAvatarsAction).isInstanceOf(AsyncAction.Success::class.java) + } + assert(setHideInviteAvatarsValueLambda).isCalledOnce() + } + } + + @Test + fun `setHideInviteAvatars reverts state on failure`() = runTest { + val setHideInviteAvatarsValueLambda = lambdaRecorder> { + Result.failure(Exception()) + } + val mediaPreviewService = FakeMediaPreviewService( + setHideInviteAvatarsResult = setHideInviteAvatarsValueLambda + ) + val store = createMediaPreviewConfigStateStore(mediaPreviewService = mediaPreviewService) + moleculeFlow(RecompositionMode.Immediate) { + store.state() + }.test { + awaitItem().also { state -> + assertThat(state.hideInviteAvatars).isFalse() + } + store.setHideInviteAvatars(true) + + awaitItem().also { state -> + assertThat(state.hideInviteAvatars).isTrue() + } + awaitItem().also { state -> + assertThat(state.hideInviteAvatars).isTrue() + assertThat(state.setHideInviteAvatarsAction).isInstanceOf(AsyncAction.Loading::class.java) + } + skipItems(1) + awaitItem().also { state -> + assertThat(state.hideInviteAvatars).isFalse() + assertThat(state.setHideInviteAvatarsAction).isInstanceOf(AsyncAction.Failure::class.java) + } + assert(setHideInviteAvatarsValueLambda).isCalledOnce() + } + } + + @Test + fun `setTimelineMediaPreviewValue updates state and calls service on success`() = runTest { + val setMediaPreviewValueLambda = lambdaRecorder> { Result.success(Unit) } + val mediaPreviewService = FakeMediaPreviewService( + setMediaPreviewValueResult = setMediaPreviewValueLambda + ) + val store = createMediaPreviewConfigStateStore(mediaPreviewService = mediaPreviewService) + moleculeFlow(RecompositionMode.Immediate) { + store.state() + }.test { + awaitItem().also { state -> + assertThat(state.timelineMediaPreviewValue).isEqualTo(MediaPreviewValue.On) + } + store.setTimelineMediaPreviewValue(MediaPreviewValue.Off) + + awaitItem().also { state -> + assertThat(state.timelineMediaPreviewValue).isEqualTo(MediaPreviewValue.Off) + } + awaitItem().also { state -> + assertThat(state.timelineMediaPreviewValue).isEqualTo(MediaPreviewValue.Off) + assertThat(state.setTimelineMediaPreviewAction).isInstanceOf(AsyncAction.Loading::class.java) + } + awaitItem().also { state -> + assertThat(state.timelineMediaPreviewValue).isEqualTo(MediaPreviewValue.Off) + assertThat(state.setTimelineMediaPreviewAction).isInstanceOf(AsyncAction.Success::class.java) + } + assert(setMediaPreviewValueLambda).isCalledOnce() + } + } + + @Test + fun `setTimelineMediaPreviewValue reverts state on failure`() = runTest { + val setMediaPreviewValueLambda = lambdaRecorder> { + Result.failure(Exception()) + } + val mediaPreviewService = FakeMediaPreviewService( + setMediaPreviewValueResult = setMediaPreviewValueLambda + ) + val store = createMediaPreviewConfigStateStore(mediaPreviewService = mediaPreviewService) + moleculeFlow(RecompositionMode.Immediate) { + store.state() + }.test { + awaitItem().also { state -> + assertThat(state.timelineMediaPreviewValue).isEqualTo(MediaPreviewValue.On) + } + store.setTimelineMediaPreviewValue(MediaPreviewValue.Off) + + awaitItem().also { state -> + assertThat(state.timelineMediaPreviewValue).isEqualTo(MediaPreviewValue.Off) + } + awaitItem().also { state -> + assertThat(state.timelineMediaPreviewValue).isEqualTo(MediaPreviewValue.Off) + assertThat(state.setTimelineMediaPreviewAction).isInstanceOf(AsyncAction.Loading::class.java) + } + skipItems(1) + awaitItem().also { state -> + assertThat(state.timelineMediaPreviewValue).isEqualTo(MediaPreviewValue.On) + assertThat(state.setTimelineMediaPreviewAction).isInstanceOf(AsyncAction.Failure::class.java) + } + assert(setMediaPreviewValueLambda).isCalledOnce() + } + } + + private fun TestScope.createMediaPreviewConfigStateStore( + mediaPreviewService: FakeMediaPreviewService = FakeMediaPreviewService(), + snackbarDispatcher: SnackbarDispatcher = SnackbarDispatcher() + ): MediaPreviewConfigStateStore = DefaultMediaPreviewConfigStateStore( + sessionCoroutineScope = backgroundScope, + mediaPreviewService = mediaPreviewService, + snackbarDispatcher = snackbarDispatcher + ) +} diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt index 9e8199cd80..59ff6bc379 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt @@ -37,6 +37,7 @@ import io.element.android.features.roomlist.impl.search.RoomListSearchEvents import io.element.android.features.roomlist.impl.search.RoomListSearchState import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.core.coroutine.mapState import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher import io.element.android.libraries.designsystem.utils.snackbar.collectSnackbarMessageAsState import io.element.android.libraries.featureflag.api.FeatureFlagService @@ -120,8 +121,11 @@ class RoomListPresenter @Inject constructor( // Avatar indicator val showAvatarIndicator by indicatorService.showRoomListTopBarIndicator() val hideInvitesAvatar by remember { - appPreferencesStore.getHideInviteAvatarsFlow() - }.collectAsState(initial = false) + client + .mediaPreviewService() + .mediaPreviewConfigFlow + .mapState { config -> config.hideInviteAvatar } + }.collectAsState() val contextMenu = remember { mutableStateOf(RoomListState.ContextMenu.Hidden) } val declineInviteMenu = remember { mutableStateOf(RoomListState.DeclineInviteMenu.Hidden) } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt index 81aa28be6b..d968ae63de 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt @@ -8,7 +8,6 @@ package io.element.android.libraries.matrix.api import io.element.android.libraries.core.data.tryOrNull -import io.element.android.libraries.core.extensions.runCatchingExceptions import io.element.android.libraries.matrix.api.core.DeviceId import io.element.android.libraries.matrix.api.core.MatrixPatterns import io.element.android.libraries.matrix.api.core.ProgressCallback @@ -20,9 +19,7 @@ import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.createroom.CreateRoomParameters import io.element.android.libraries.matrix.api.encryption.EncryptionService import io.element.android.libraries.matrix.api.media.MatrixMediaLoader -import io.element.android.libraries.matrix.api.media.MediaPreviewConfig import io.element.android.libraries.matrix.api.media.MediaPreviewService -import io.element.android.libraries.matrix.api.media.MediaPreviewValue import io.element.android.libraries.matrix.api.notification.NotificationService import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService import io.element.android.libraries.matrix.api.oidc.AccountManagementAction @@ -44,7 +41,6 @@ import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.withContext import java.util.Optional interface MatrixClient { @@ -175,7 +171,6 @@ interface MatrixClient { * Return true if Livekit Rtc is supported, i.e. if Element Call is available. */ suspend fun isLivekitRtcSupported(): Boolean - } /** diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/MediaPreviewConfig.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/MediaPreviewConfig.kt index 5c964ca15e..66a53b3ad3 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/MediaPreviewConfig.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/MediaPreviewConfig.kt @@ -13,4 +13,14 @@ package io.element.android.libraries.matrix.api.media data class MediaPreviewConfig( val mediaPreviewValue: MediaPreviewValue, val hideInviteAvatar: Boolean, -) +) { + companion object { + /** + * The default config if unknown (no local nor server config). + */ + val DEFAULT = MediaPreviewConfig( + mediaPreviewValue = MediaPreviewValue.On, + hideInviteAvatar = false + ) + } +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/MediaPreviewService.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/MediaPreviewService.kt index 19cbe9cf2a..dffa2d25e4 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/MediaPreviewService.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/MediaPreviewService.kt @@ -7,7 +7,7 @@ package io.element.android.libraries.matrix.api.media -import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow interface MediaPreviewService { /** @@ -19,24 +19,17 @@ interface MediaPreviewService { * Will emit the media preview config known by the client. * This will emit a new value when received from sync. */ - fun getMediaPreviewConfigFlow(): Flow - - /** - * Get the media preview display policy from the cache. This value is updated through sync. - */ - suspend fun getMediaPreviewValue(): MediaPreviewValue? - - /** - * Get the invite avatars display policy from the cache. This value is updated through sync. - */ - suspend fun getHideInviteAvatars(): Boolean + val mediaPreviewConfigFlow: StateFlow /** * Set the media preview display policy. This will update the value on the server and update the local value when successful. */ suspend fun setMediaPreviewValue(mediaPreviewValue: MediaPreviewValue): Result + /** * Set the invite avatars display policy. This will update the value on the server and update the local value when successful. */ suspend fun setHideInviteAvatars(hide: Boolean): Result } + +fun MediaPreviewService.getMediaPreviewValue() = mediaPreviewConfigFlow.value.mediaPreviewValue diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt index 1e9d703b28..d71db05a02 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt @@ -26,9 +26,7 @@ import io.element.android.libraries.matrix.api.createroom.CreateRoomParameters import io.element.android.libraries.matrix.api.createroom.RoomPreset import io.element.android.libraries.matrix.api.encryption.EncryptionService import io.element.android.libraries.matrix.api.media.MatrixMediaLoader -import io.element.android.libraries.matrix.api.media.MediaPreviewConfig import io.element.android.libraries.matrix.api.media.MediaPreviewService -import io.element.android.libraries.matrix.api.media.MediaPreviewValue import io.element.android.libraries.matrix.api.notification.NotificationService import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService import io.element.android.libraries.matrix.api.oidc.AccountManagementAction @@ -111,7 +109,6 @@ import org.matrix.rustcomponents.sdk.AuthDataPasswordDetails import org.matrix.rustcomponents.sdk.Client import org.matrix.rustcomponents.sdk.ClientException import org.matrix.rustcomponents.sdk.IgnoredUsersListener -import org.matrix.rustcomponents.sdk.InviteAvatars import org.matrix.rustcomponents.sdk.NotificationProcessSetup import org.matrix.rustcomponents.sdk.PowerLevels import org.matrix.rustcomponents.sdk.RoomInfoListener @@ -220,6 +217,7 @@ class RustMatrixClient( ) private val mediaPreviewService = RustMediaPreviewService( + sessionCoroutineScope = sessionCoroutineScope, innerClient = innerClient, sessionDispatcher = sessionDispatcher, ) @@ -694,8 +692,6 @@ class RustMatrixClient( innerClient.isLivekitRtcSupported() } - - private suspend fun File.getCacheSize( includeCryptoDb: Boolean = false, ): Long = withContext(sessionDispatcher) { diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/RustMediaPreviewService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/RustMediaPreviewService.kt index a2ab6c8bb9..c18f85712f 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/RustMediaPreviewService.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/RustMediaPreviewService.kt @@ -13,7 +13,10 @@ import io.element.android.libraries.matrix.api.media.MediaPreviewService import io.element.android.libraries.matrix.api.media.MediaPreviewValue import io.element.android.libraries.matrix.impl.util.mxCallbackFlow import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.withContext import org.matrix.rustcomponents.sdk.Client import org.matrix.rustcomponents.sdk.InviteAvatars @@ -22,27 +25,27 @@ import org.matrix.rustcomponents.sdk.MediaPreviews import org.matrix.rustcomponents.sdk.MediaPreviewConfig as RustMediaPreviewConfig class RustMediaPreviewService( + sessionCoroutineScope: CoroutineScope, private val sessionDispatcher: CoroutineDispatcher, private val innerClient: Client, ) : MediaPreviewService { + override val mediaPreviewConfigFlow: StateFlow = + innerClient + .getMediaPreviewConfigFlow() + .stateIn(sessionCoroutineScope, started = SharingStarted.Lazily, initialValue = MediaPreviewConfig.DEFAULT) + override suspend fun fetchMediaPreviewConfig(): Result = withContext(sessionDispatcher) { runCatchingExceptions { innerClient.fetchMediaPreviewConfig()?.into() } } - override fun getMediaPreviewConfigFlow(): Flow = innerClient.getMediaPreviewConfigFlow() - - override suspend fun getMediaPreviewValue(): MediaPreviewValue? = innerClient.getMediaPreviewDisplayPolicy()?.into() - override suspend fun setMediaPreviewValue(mediaPreviewValue: MediaPreviewValue): Result = withContext(sessionDispatcher) { runCatchingExceptions { innerClient.setMediaPreviewDisplayPolicy(mediaPreviewValue.into()) } } - override suspend fun getHideInviteAvatars(): Boolean = innerClient.getInviteAvatarsDisplayPolicy() == InviteAvatars.OFF - override suspend fun setHideInviteAvatars(hide: Boolean): Result = withContext(sessionDispatcher) { runCatchingExceptions { val inviteAvatars = if (hide) InviteAvatars.OFF else InviteAvatars.ON @@ -61,7 +64,9 @@ private fun RustMediaPreviewConfig.into(): MediaPreviewConfig { private fun Client.getMediaPreviewConfigFlow() = mxCallbackFlow { subscribeToMediaPreviewConfig(object : MediaPreviewConfigListener { override fun onChange(mediaPreviewConfig: RustMediaPreviewConfig?) { - trySend(mediaPreviewConfig?.into()) + if (mediaPreviewConfig != null) { + trySend(mediaPreviewConfig.into()) + } } }) } @@ -81,4 +86,3 @@ private fun MediaPreviews.into(): MediaPreviewValue { MediaPreviews.PRIVATE -> MediaPreviewValue.Private } } - diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt index 11b95a647e..9b609b39c7 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt @@ -17,8 +17,6 @@ import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.createroom.CreateRoomParameters import io.element.android.libraries.matrix.api.encryption.EncryptionService -import io.element.android.libraries.matrix.api.media.MediaPreviewConfig -import io.element.android.libraries.matrix.api.media.MediaPreviewValue import io.element.android.libraries.matrix.api.media.MatrixMediaLoader import io.element.android.libraries.matrix.api.media.MediaPreviewService import io.element.android.libraries.matrix.api.notification.NotificationService @@ -96,7 +94,6 @@ class FakeMatrixClient( private val canReportRoomLambda: () -> Boolean = { false }, private val isLivekitRtcSupportedLambda: () -> Boolean = { false }, override val ignoredUsersFlow: StateFlow> = MutableStateFlow(persistentListOf()), - ) : MatrixClient { var setDisplayNameCalled: Boolean = false private set @@ -246,7 +243,6 @@ class FakeMatrixClient( return RoomMembershipObserver() } - // Mocks fun givenCreateRoomResult(result: Result) { diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/media/FakeMediaPreviewService.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/media/FakeMediaPreviewService.kt index 7e41b03e41..d8e9114817 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/media/FakeMediaPreviewService.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/media/FakeMediaPreviewService.kt @@ -12,34 +12,19 @@ import io.element.android.libraries.matrix.api.media.MediaPreviewService import io.element.android.libraries.matrix.api.media.MediaPreviewValue import io.element.android.tests.testutils.lambda.lambdaError import io.element.android.tests.testutils.simulateLongTask -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow class FakeMediaPreviewService( + override val mediaPreviewConfigFlow: StateFlow = MutableStateFlow(MediaPreviewConfig.DEFAULT), private val fetchMediaPreviewConfigResult: () -> Result = { lambdaError() }, - private val mediaPreviewConfigFlow: Flow = flowOf(null), - private val getMediaPreviewValue: ()-> MediaPreviewValue? = { null }, - private val getHideInviteAvatars: () -> Boolean = { false }, private val setMediaPreviewValueResult: (MediaPreviewValue) -> Result = { lambdaError() }, private val setHideInviteAvatarsResult: (Boolean) -> Result = { lambdaError() }, -): MediaPreviewService { - +) : MediaPreviewService { override suspend fun fetchMediaPreviewConfig(): Result = simulateLongTask { fetchMediaPreviewConfigResult() } - override fun getMediaPreviewConfigFlow(): Flow { - return mediaPreviewConfigFlow - } - - override suspend fun getMediaPreviewValue(): MediaPreviewValue? = simulateLongTask { - getMediaPreviewValue.invoke() - } - - override suspend fun getHideInviteAvatars(): Boolean = simulateLongTask { - getHideInviteAvatars.invoke() - } - override suspend fun setMediaPreviewValue(mediaPreviewValue: MediaPreviewValue): Result = simulateLongTask { setMediaPreviewValueResult(mediaPreviewValue) } diff --git a/libraries/preferences/api/src/main/kotlin/io/element/android/libraries/preferences/api/store/AppPreferencesStore.kt b/libraries/preferences/api/src/main/kotlin/io/element/android/libraries/preferences/api/store/AppPreferencesStore.kt index 59a074c99b..7e47785088 100644 --- a/libraries/preferences/api/src/main/kotlin/io/element/android/libraries/preferences/api/store/AppPreferencesStore.kt +++ b/libraries/preferences/api/src/main/kotlin/io/element/android/libraries/preferences/api/store/AppPreferencesStore.kt @@ -22,9 +22,13 @@ interface AppPreferencesStore { suspend fun setTheme(theme: String) fun getThemeFlow(): Flow + @Deprecated("Use MediaPreviewService instead. Kept only for migration.") suspend fun setHideInviteAvatars(hide: Boolean?) + @Deprecated("Use MediaPreviewService instead. Kept only for migration.") fun getHideInviteAvatarsFlow(): Flow + @Deprecated("Use MediaPreviewService instead. Kept only for migration.") suspend fun setTimelineMediaPreviewValue(mediaPreviewValue: MediaPreviewValue?) + @Deprecated("Use MediaPreviewService instead. Kept only for migration.") fun getTimelineMediaPreviewValueFlow(): Flow suspend fun setTracingLogLevel(logLevel: LogLevel) diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotifiableEventResolver.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotifiableEventResolver.kt index dd2f7eb760..7e7dbe3fb4 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotifiableEventResolver.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotifiableEventResolver.kt @@ -25,6 +25,7 @@ import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.ThreadId import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.media.MediaPreviewValue +import io.element.android.libraries.matrix.api.media.getMediaPreviewValue import io.element.android.libraries.matrix.api.notification.NotificationContent import io.element.android.libraries.matrix.api.notification.NotificationData import io.element.android.libraries.matrix.api.permalink.PermalinkParser diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotifiableEventResolverTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotifiableEventResolverTest.kt index cb3c0a78f0..3c609f184e 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotifiableEventResolverTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotifiableEventResolverTest.kt @@ -43,8 +43,6 @@ import io.element.android.libraries.matrix.test.FakeMatrixClientProvider import io.element.android.libraries.matrix.test.notification.FakeNotificationService import io.element.android.libraries.matrix.test.notification.aNotificationData import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser -import io.element.android.libraries.preferences.api.store.AppPreferencesStore -import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore import io.element.android.libraries.push.impl.notifications.fake.FakeNotificationMediaRepo import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableMessageEvent import io.element.android.libraries.push.impl.notifications.model.FallbackNotifiableEvent