change (media preview config) : use the new apis

This commit is contained in:
ganfra
2025-06-26 20:52:44 +02:00
parent 682897cdb6
commit 0b748aa8cb
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,
)
}

View File

@@ -22,10 +22,8 @@ interface AppPreferencesStore {
suspend fun setTheme(theme: String)
fun getThemeFlow(): Flow<String?>
suspend fun setHideInviteAvatars(value: Boolean)
fun getHideInviteAvatarsFlow(): Flow<Boolean>
suspend fun setTimelineMediaPreviewValue(value: MediaPreviewValue)
fun getTimelineMediaPreviewValueFlow(): Flow<MediaPreviewValue>
suspend fun setTracingLogLevel(logLevel: LogLevel)

View File

@@ -85,24 +85,12 @@ class DefaultAppPreferencesStore @Inject constructor(
}
}
override suspend fun setHideInviteAvatars(value: Boolean) {
store.edit { prefs ->
prefs[hideInviteAvatarsKey] = value
}
}
override fun getHideInviteAvatarsFlow(): Flow<Boolean> {
return store.data.map { prefs ->
prefs[hideInviteAvatarsKey] == true
}
}
override suspend fun setTimelineMediaPreviewValue(value: MediaPreviewValue) {
store.edit { prefs ->
prefs[timelineMediaPreviewValueKey] = value.name
}
}
override fun getTimelineMediaPreviewValueFlow(): Flow<MediaPreviewValue> {
return store.data.map { prefs ->
prefs[timelineMediaPreviewValueKey]?.let { MediaPreviewValue.valueOf(it) } ?: MediaPreviewValue.On

View File

@@ -55,18 +55,10 @@ class InMemoryAppPreferencesStore(
return theme
}
override suspend fun setHideInviteAvatars(value: Boolean) {
hideInviteAvatars.value = value
}
override fun getHideInviteAvatarsFlow(): Flow<Boolean> {
return hideInviteAvatars
}
override suspend fun setTimelineMediaPreviewValue(value: MediaPreviewValue) {
timelineMediaPreviewValue.value = value
}
override fun getTimelineMediaPreviewValueFlow(): Flow<MediaPreviewValue> {
return timelineMediaPreviewValue
}

View File

@@ -41,7 +41,6 @@ import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageTy
import io.element.android.libraries.matrix.api.timeline.item.event.VideoMessageType
import io.element.android.libraries.matrix.api.timeline.item.event.VoiceMessageType
import io.element.android.libraries.matrix.ui.messages.toPlainText
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
import io.element.android.libraries.push.impl.R
import io.element.android.libraries.push.impl.notifications.model.FallbackNotifiableEvent
import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent
@@ -50,7 +49,6 @@ import io.element.android.libraries.push.impl.notifications.model.ResolvedPushEv
import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.services.toolbox.api.strings.StringProvider
import io.element.android.services.toolbox.api.systemclock.SystemClock
import kotlinx.coroutines.flow.first
import timber.log.Timber
import javax.inject.Inject
@@ -79,7 +77,6 @@ class DefaultNotifiableEventResolver @Inject constructor(
@ApplicationContext private val context: Context,
private val permalinkParser: PermalinkParser,
private val callNotificationEventResolver: CallNotificationEventResolver,
private val appPreferencesStore: AppPreferencesStore,
) : NotifiableEventResolver {
override suspend fun resolveEvents(
sessionId: SessionId,
@@ -117,6 +114,7 @@ class DefaultNotifiableEventResolver @Inject constructor(
): Result<ResolvedPushEvent> = runCatchingExceptions {
when (val content = this.content) {
is NotificationContent.MessageLike.RoomMessage -> {
val showMediaPreview = client.mediaPreviewService().getMediaPreviewValue() == MediaPreviewValue.On
val senderDisambiguatedDisplayName = getDisambiguatedDisplayName(content.senderId)
val messageBody = descriptionFromMessageContent(content, senderDisambiguatedDisplayName)
val notifiableMessageEvent = buildNotifiableMessageEvent(
@@ -129,8 +127,8 @@ class DefaultNotifiableEventResolver @Inject constructor(
timestamp = this.timestamp,
senderDisambiguatedDisplayName = senderDisambiguatedDisplayName,
body = messageBody,
imageUriString = content.fetchImageIfPresent(client)?.toString(),
imageMimeType = content.getImageMimetype(),
imageUriString = if (showMediaPreview) content.fetchImageIfPresent(client)?.toString() else null,
imageMimeType = if (showMediaPreview) content.getImageMimetype() else null,
roomName = roomDisplayName,
roomIsDm = isDm,
roomAvatarPath = roomAvatarUrl,
@@ -323,9 +321,6 @@ class DefaultNotifiableEventResolver @Inject constructor(
}
private suspend fun NotificationContent.MessageLike.RoomMessage.fetchImageIfPresent(client: MatrixClient): Uri? {
if (appPreferencesStore.getTimelineMediaPreviewValueFlow().first() != MediaPreviewValue.On) {
return null
}
val fileResult = when (val messageType = messageType) {
is ImageMessageType -> notificationMediaRepoFactory.create(client)
.getMediaFile(
@@ -349,10 +344,7 @@ class DefaultNotifiableEventResolver @Inject constructor(
.getOrNull()
}
private suspend fun NotificationContent.MessageLike.RoomMessage.getImageMimetype(): String? {
if (appPreferencesStore.getTimelineMediaPreviewValueFlow().first() != MediaPreviewValue.On) {
return null
}
private fun NotificationContent.MessageLike.RoomMessage.getImageMimetype(): String? {
return when (val messageType = messageType) {
is ImageMessageType -> messageType.info?.mimetype
is VideoMessageType -> null // Use the thumbnail here?

View File

@@ -826,7 +826,6 @@ class DefaultNotifiableEventResolverTest {
private fun createDefaultNotifiableEventResolver(
notificationService: FakeNotificationService? = FakeNotificationService(),
notificationResult: Result<Map<EventId, NotificationData>> = Result.success(emptyMap()),
appPreferencesStore: AppPreferencesStore = InMemoryAppPreferencesStore(),
callNotificationEventResolver: FakeCallNotificationEventResolver = FakeCallNotificationEventResolver(),
): DefaultNotifiableEventResolver {
val context = RuntimeEnvironment.getApplication() as Context
@@ -849,7 +848,6 @@ class DefaultNotifiableEventResolverTest {
context = context,
permalinkParser = FakePermalinkParser(),
callNotificationEventResolver = callNotificationEventResolver,
appPreferencesStore = appPreferencesStore,
)
}
}