change (media preview config) : use the new apis

This commit is contained in:
ganfra 2025-06-26 20:52:44 +02:00
parent 416f4f2215
commit 5332246bde
10 changed files with 158 additions and 60 deletions

View file

@ -8,27 +8,36 @@
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.rememberCoroutineScope
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
import kotlinx.coroutines.launch
import javax.inject.Inject
class AdvancedSettingsPresenter @Inject constructor(
private val appPreferencesStore: AppPreferencesStore,
private val sessionPreferencesStore: SessionPreferencesStore,
private val mediaPreviewConfigStateStore: MediaPreviewConfigStateStore,
@SessionCoroutineScope
private val sessionCoroutineScope: CoroutineScope,
) : Presenter<AdvancedSettingsState> {
@Composable
override fun present(): AdvancedSettingsState {
val localCoroutineScope = rememberCoroutineScope()
val isDeveloperModeEnabled by remember {
appPreferencesStore.isDeveloperModeEnabledFlow()
}.collectAsState(initial = false)
@ -41,13 +50,6 @@ class AdvancedSettingsPresenter @Inject constructor(
val theme = remember {
appPreferencesStore.getThemeFlow().mapToTheme()
}.collectAsState(initial = Theme.System)
val hideInviteAvatars by remember {
appPreferencesStore.getHideInviteAvatarsFlow()
}.collectAsState(false)
val timelineMediaPreviewValue by remember {
appPreferencesStore.getTimelineMediaPreviewValueFlow()
}.collectAsState(initial = MediaPreviewValue.On)
val themeOption by remember {
derivedStateOf {
@ -61,28 +63,24 @@ class AdvancedSettingsPresenter @Inject constructor(
fun handleEvents(event: AdvancedSettingsEvents) {
when (event) {
is AdvancedSettingsEvents.SetDeveloperModeEnabled -> localCoroutineScope.launch {
is AdvancedSettingsEvents.SetDeveloperModeEnabled -> sessionCoroutineScope.launch {
appPreferencesStore.setDeveloperModeEnabled(event.enabled)
}
is AdvancedSettingsEvents.SetSharePresenceEnabled -> localCoroutineScope.launch {
is AdvancedSettingsEvents.SetSharePresenceEnabled -> sessionCoroutineScope.launch {
sessionPreferencesStore.setSharePresence(event.enabled)
}
is AdvancedSettingsEvents.SetCompressMedia -> localCoroutineScope.launch {
is AdvancedSettingsEvents.SetCompressMedia -> sessionCoroutineScope.launch {
sessionPreferencesStore.setCompressMedia(event.compress)
}
is AdvancedSettingsEvents.SetTheme -> localCoroutineScope.launch {
is AdvancedSettingsEvents.SetTheme -> sessionCoroutineScope.launch {
when (event.theme) {
ThemeOption.System -> appPreferencesStore.setTheme(Theme.System.name)
ThemeOption.Dark -> appPreferencesStore.setTheme(Theme.Dark.name)
ThemeOption.Light -> appPreferencesStore.setTheme(Theme.Light.name)
}
}
is AdvancedSettingsEvents.SetHideInviteAvatars -> localCoroutineScope.launch {
appPreferencesStore.setHideInviteAvatars(event.value)
}
is AdvancedSettingsEvents.SetTimelineMediaPreviewValue -> localCoroutineScope.launch {
appPreferencesStore.setTimelineMediaPreviewValue(event.value)
}
is AdvancedSettingsEvents.SetHideInviteAvatars -> mediaPreviewConfigStateStore.setHideInviteAvatars(event.value)
is AdvancedSettingsEvents.SetTimelineMediaPreviewValue -> mediaPreviewConfigStateStore.setTimelineMediaPreviewValue(event.value)
}
}
@ -91,9 +89,11 @@ class AdvancedSettingsPresenter @Inject constructor(
isSharePresenceEnabled = isSharePresenceEnabled,
doesCompressMedia = doesCompressMedia,
theme = themeOption,
hideInviteAvatars = hideInviteAvatars,
timelineMediaPreviewValue = timelineMediaPreviewValue,
eventSink = { handleEvents(it) }
hideInviteAvatars = mediaPreviewConfigStateStore.hideInviteAvatars.value,
timelineMediaPreviewValue = mediaPreviewConfigStateStore.timelineMediaPreviewValue.value,
setHideInviteAvatarsAction = mediaPreviewConfigStateStore.setHideInviteAvatarsAction.value,
setTimelineMediaPreviewAction = mediaPreviewConfigStateStore.setTimelineMediaPreviewAction.value,
eventSink = ::handleEvents,
)
}
}

View file

@ -10,6 +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
@ -21,6 +22,8 @@ data class AdvancedSettingsState(
val theme: ThemeOption,
val hideInviteAvatars: Boolean,
val timelineMediaPreviewValue: MediaPreviewValue,
val setHideInviteAvatarsAction: AsyncAction<Unit>,
val setTimelineMediaPreviewAction: AsyncAction<Unit>,
val eventSink: (AdvancedSettingsEvents) -> Unit
)

View file

@ -8,6 +8,7 @@
package io.element.android.features.preferences.impl.advanced
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.api.media.MediaPreviewValue
open class AdvancedSettingsStateProvider : PreviewParameterProvider<AdvancedSettingsState> {
@ -18,7 +19,9 @@ open class AdvancedSettingsStateProvider : PreviewParameterProvider<AdvancedSett
aAdvancedSettingsState(isSharePresenceEnabled = true),
aAdvancedSettingsState(doesCompressMedia = true),
aAdvancedSettingsState(hideInviteAvatars = true),
aAdvancedSettingsState(timelineMediaPreviewValue = MediaPreviewValue.Off)
aAdvancedSettingsState(timelineMediaPreviewValue = MediaPreviewValue.Off),
aAdvancedSettingsState(setHideInviteAvatarsAction = AsyncAction.Loading),
aAdvancedSettingsState(setTimelineMediaPreviewAction = AsyncAction.Loading),
)
}
@ -29,6 +32,8 @@ fun aAdvancedSettingsState(
hideInviteAvatars: Boolean = false,
theme: ThemeOption = ThemeOption.System,
timelineMediaPreviewValue: MediaPreviewValue = MediaPreviewValue.On,
setTimelineMediaPreviewAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
setHideInviteAvatarsAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
eventSink: (AdvancedSettingsEvents) -> Unit = {},
) = AdvancedSettingsState(
isDeveloperModeEnabled = isDeveloperModeEnabled,
@ -37,5 +42,7 @@ fun aAdvancedSettingsState(
theme = theme,
hideInviteAvatars = hideInviteAvatars,
timelineMediaPreviewValue = timelineMediaPreviewValue,
setTimelineMediaPreviewAction = setTimelineMediaPreviewAction,
setHideInviteAvatarsAction = setHideInviteAvatarsAction,
eventSink = eventSink
)

View file

@ -0,0 +1,109 @@
/*
* 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.State
import androidx.compose.runtime.mutableStateOf
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.runUpdatingState
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.di.SingleIn
import io.element.android.libraries.di.annotations.SessionCoroutineScope
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.onEach
import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
interface MediaPreviewConfigStateStore {
val hideInviteAvatars: State<Boolean>
val timelineMediaPreviewValue: State<MediaPreviewValue>
val setHideInviteAvatarsAction: State<AsyncAction<Unit>>
val setTimelineMediaPreviewAction: State<AsyncAction<Unit>>
fun setHideInviteAvatars(hide: Boolean)
fun setTimelineMediaPreviewValue(value: MediaPreviewValue)
}
@ContributesBinding(SessionScope::class, boundType = MediaPreviewConfigStateStore::class)
@SingleIn(SessionScope::class)
class DefaultMediaPreviewConfigStateStore @Inject constructor(
@SessionCoroutineScope
private val sessionCoroutineScope: CoroutineScope,
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<Unit>>(AsyncAction.Uninitialized)
override val setTimelineMediaPreviewAction = mutableStateOf<AsyncAction<Unit>>(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()
hideInviteAvatarsFlow
.onEach {
Timber.d("Hide invi@te avatars changed to $it")
hideInviteAvatars.value = it
}
.launchIn(sessionCoroutineScope)
timelineMediaPreviewFlow
.onEach {
Timber.d("Timeline media preview value changed to $it")
timelineMediaPreviewValue.value = it
}
.launchIn(sessionCoroutineScope)
}
override fun setHideInviteAvatars(hide: Boolean) {
sessionCoroutineScope.launch {
Timber.d("Setting hide invite avatars to $hide")
val prevHideInviteAvatars = hideInviteAvatars.value
hideInviteAvatars.value = hide
runUpdatingState(setHideInviteAvatarsAction) {
mediaPreviewService
.setHideInviteAvatars(hide)
.onFailure {
hideInviteAvatars.value = prevHideInviteAvatars
snackbarDispatcher.post(SnackbarMessage(CommonStrings.common_something_went_wrong_message))
}
}
}
}
override fun setTimelineMediaPreviewValue(value: MediaPreviewValue) {
sessionCoroutineScope.launch {
Timber.d("Setting timeline media preview value to $value")
val prevTimelineMediaPreviewValue = timelineMediaPreviewValue.value
timelineMediaPreviewValue.value = value
runUpdatingState(setTimelineMediaPreviewAction) {
mediaPreviewService
.setMediaPreviewValue(value)
.onFailure {
timelineMediaPreviewValue.value = prevTimelineMediaPreviewValue
snackbarDispatcher.post(SnackbarMessage(CommonStrings.common_something_went_wrong_message))
}
}
}
}
}

View file

@ -11,10 +11,14 @@ 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.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.test.runTest
import org.junit.Rule
import org.junit.Test
@ -144,7 +148,12 @@ class AdvancedSettingsPresenterTest {
@Test
fun `present - timeline media preview value`() = runTest {
val presenter = createAdvancedSettingsPresenter()
val mediaPreviewConfigFlow = MutableStateFlow<MediaPreviewConfig?>(null)
val presenter = createAdvancedSettingsPresenter(
matrixClient = FakeMatrixClient(
mediaPreviewConfigFlow = mediaPreviewConfigFlow
)
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
@ -165,8 +174,10 @@ class AdvancedSettingsPresenterTest {
private fun createAdvancedSettingsPresenter(
appPreferencesStore: InMemoryAppPreferencesStore = InMemoryAppPreferencesStore(),
sessionPreferencesStore: InMemorySessionPreferencesStore = InMemorySessionPreferencesStore(),
matrixClient: MatrixClient = FakeMatrixClient(),
) = AdvancedSettingsPresenter(
appPreferencesStore = appPreferencesStore,
sessionPreferencesStore = sessionPreferencesStore,
matrixClient = matrixClient,
)
}