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 ce2ef75321..ad486bd2c5 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 @@ -16,6 +16,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.media.MediaPreviewValue import io.element.android.libraries.preferences.api.store.AppPreferencesStore import kotlinx.collections.immutable.toImmutableSet import javax.inject.Inject @@ -25,14 +26,14 @@ class TimelineProtectionPresenter @Inject constructor( ) : Presenter { @Composable override fun present(): TimelineProtectionState { - val hideMediaContent by appPreferencesStore.doesHideImagesAndVideosFlow().collectAsState(initial = false) + val mediaPreviewValue = appPreferencesStore.getTimelineMediaPreviewValueFlow().collectAsState(initial = MediaPreviewValue.Off) var allowedEvents by remember { mutableStateOf>(setOf()) } - val protectionState by remember(hideMediaContent) { + val protectionState by remember { derivedStateOf { - if (hideMediaContent) { - ProtectionState.RenderOnly(eventIds = allowedEvents.toImmutableSet()) - } else { + if (mediaPreviewValue.value == MediaPreviewValue.On) { ProtectionState.RenderAll + } else { + ProtectionState.RenderOnly(eventIds = allowedEvents.toImmutableSet()) } } } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsEvents.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsEvents.kt index 45d7ae615b..fabaf7afc5 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsEvents.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsEvents.kt @@ -8,6 +8,7 @@ package io.element.android.features.preferences.impl.advanced import io.element.android.compound.theme.Theme +import io.element.android.libraries.matrix.api.media.MediaPreviewValue sealed interface AdvancedSettingsEvents { data class SetDeveloperModeEnabled(val enabled: Boolean) : AdvancedSettingsEvents @@ -16,4 +17,6 @@ sealed interface AdvancedSettingsEvents { data object ChangeTheme : AdvancedSettingsEvents data object CancelChangeTheme : AdvancedSettingsEvents data class SetTheme(val theme: Theme) : AdvancedSettingsEvents + data class SetTimelineMediaPreviewValue(val value: MediaPreviewValue) : AdvancedSettingsEvents + data class SetHideInviteAvatars(val value: Boolean) : AdvancedSettingsEvents } 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 b256997d1c..5cefc1ba72 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 @@ -17,6 +17,7 @@ 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.Presenter +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.launch @@ -44,6 +45,14 @@ class AdvancedSettingsPresenter @Inject constructor( .collectAsState(initial = Theme.System) var showChangeThemeDialog by remember { mutableStateOf(false) } + val hideInviteAvatars by remember { + appPreferencesStore.getHideInviteAvatarsFlow() + }.collectAsState(false) + + val timelineMediaPreviewValue by remember { + appPreferencesStore.getTimelineMediaPreviewValueFlow() + }.collectAsState(initial = MediaPreviewValue.On) + fun handleEvents(event: AdvancedSettingsEvents) { when (event) { is AdvancedSettingsEvents.SetDeveloperModeEnabled -> localCoroutineScope.launch { @@ -61,6 +70,12 @@ class AdvancedSettingsPresenter @Inject constructor( appPreferencesStore.setTheme(event.theme.name) showChangeThemeDialog = false } + is AdvancedSettingsEvents.SetHideInviteAvatars -> localCoroutineScope.launch { + appPreferencesStore.setHideInviteAvatars(event.value) + } + is AdvancedSettingsEvents.SetTimelineMediaPreviewValue -> localCoroutineScope.launch { + appPreferencesStore.setTimelineMediaPreviewValue(event.value) + } } } @@ -70,6 +85,8 @@ class AdvancedSettingsPresenter @Inject constructor( doesCompressMedia = doesCompressMedia, theme = theme, showChangeThemeDialog = showChangeThemeDialog, + hideInviteAvatars = hideInviteAvatars, + timelineMediaPreviewValue = timelineMediaPreviewValue, eventSink = { handleEvents(it) } ) } 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 a202ccc95f..9f55036154 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 @@ -8,6 +8,7 @@ package io.element.android.features.preferences.impl.advanced import io.element.android.compound.theme.Theme +import io.element.android.libraries.matrix.api.media.MediaPreviewValue data class AdvancedSettingsState( val isDeveloperModeEnabled: Boolean, @@ -15,5 +16,7 @@ data class AdvancedSettingsState( val doesCompressMedia: Boolean, val theme: Theme, val showChangeThemeDialog: Boolean, + val hideInviteAvatars: Boolean, + val timelineMediaPreviewValue: MediaPreviewValue, 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 d8e0730bf0..19e4b8b26a 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 @@ -9,6 +9,7 @@ package io.element.android.features.preferences.impl.advanced import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.compound.theme.Theme +import io.element.android.libraries.matrix.api.media.MediaPreviewValue open class AdvancedSettingsStateProvider : PreviewParameterProvider { override val values: Sequence @@ -18,6 +19,8 @@ open class AdvancedSettingsStateProvider : PreviewParameterProvider Unit = {}, ) = AdvancedSettingsState( isDeveloperModeEnabled = isDeveloperModeEnabled, @@ -33,5 +38,7 @@ fun aAdvancedSettingsState( doesCompressMedia = doesCompressMedia, theme = Theme.System, showChangeThemeDialog = showChangeThemeDialog, + hideInviteAvatars = hideInviteAvatars, + timelineMediaPreviewValue = timelineMediaPreviewValue, 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 0332e98a95..d6c856b169 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 @@ -7,6 +7,7 @@ package io.element.android.features.preferences.impl.advanced +import android.preference.PreferenceCategory import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource @@ -18,11 +19,18 @@ import io.element.android.features.preferences.impl.R import io.element.android.libraries.designsystem.components.dialogs.ListOption import io.element.android.libraries.designsystem.components.dialogs.SingleSelectionDialog import io.element.android.libraries.designsystem.components.list.ListItemContent +import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory +import io.element.android.libraries.designsystem.components.preferences.PreferenceDivider import io.element.android.libraries.designsystem.components.preferences.PreferencePage +import io.element.android.libraries.designsystem.components.preferences.PreferenceSwitch import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.components.ListItem +import io.element.android.libraries.designsystem.theme.components.ListSectionHeader +import io.element.android.libraries.designsystem.theme.components.ListSupportingText +import io.element.android.libraries.designsystem.theme.components.ListSupportingTextDefaults import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.matrix.api.media.MediaPreviewValue import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.services.analytics.compose.LocalAnalyticsService import io.element.android.services.analyticsproviders.api.trackers.captureInteraction @@ -98,6 +106,7 @@ fun AdvancedSettingsView( state.eventSink(AdvancedSettingsEvents.SetCompressMedia(newValue)) } ) + ModerationAndSafety(state) } if (state.showChangeThemeDialog) { @@ -116,6 +125,57 @@ fun AdvancedSettingsView( } } +@Composable +private fun ModerationAndSafety( + state: AdvancedSettingsState, + modifier: Modifier = Modifier, +) { + PreferenceCategory( + modifier = modifier, + title = stringResource(R.string.screen_advanced_settings_moderation_and_safety_section_title), + showTopDivider = true + ) { + PreferenceSwitch( + title = stringResource(R.string.screen_advanced_settings_hide_invite_avatars_toggle_title), + isChecked = state.hideInviteAvatars, + onCheckedChange = { + state.eventSink(AdvancedSettingsEvents.SetHideInviteAvatars(it)) + }, + ) + ListSectionHeader( + title = stringResource(R.string.screen_advanced_settings_show_media_timeline_title), + hasDivider = false, + description = { + ListSupportingText( + text = stringResource(R.string.screen_advanced_settings_show_media_timeline_subtitle), + contentPadding = ListSupportingTextDefaults.Padding.None, + ) + } + ) + 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), + onClick = { + state.eventSink(AdvancedSettingsEvents.SetTimelineMediaPreviewValue(MediaPreviewValue.Off)) + }, + ) + 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), + onClick = { + state.eventSink(AdvancedSettingsEvents.SetTimelineMediaPreviewValue(MediaPreviewValue.Private)) + }, + ) + 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), + onClick = { + state.eventSink(AdvancedSettingsEvents.SetTimelineMediaPreviewValue(MediaPreviewValue.On)) + }, + ) + } +} + @Composable private fun getOptions(): ImmutableList { return themes.map { diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsEvents.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsEvents.kt index 74c673bc8a..ced7b8d2b4 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsEvents.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsEvents.kt @@ -14,7 +14,6 @@ import io.element.android.libraries.matrix.api.tracing.TraceLogPack sealed interface DeveloperSettingsEvents { data class UpdateEnabledFeature(val feature: FeatureUiModel, val isEnabled: Boolean) : DeveloperSettingsEvents data class SetCustomElementCallBaseUrl(val baseUrl: String?) : DeveloperSettingsEvents - data class SetHideImagesAndVideos(val value: Boolean) : DeveloperSettingsEvents data class SetTracingLogLevel(val logLevel: LogLevelItem) : DeveloperSettingsEvents data class ToggleTracingLogPack(val logPack: TraceLogPack, val enabled: Boolean) : DeveloperSettingsEvents data object ClearCache : DeveloperSettingsEvents diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenter.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenter.kt index aa2c5a1c50..d938f064c1 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenter.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenter.kt @@ -74,9 +74,6 @@ class DeveloperSettingsPresenter @Inject constructor( val customElementCallBaseUrl by appPreferencesStore .getCustomElementCallBaseUrlFlow() .collectAsState(initial = null) - val hideImagesAndVideos by appPreferencesStore - .doesHideImagesAndVideosFlow() - .collectAsState(initial = false) val tracingLogLevelFlow = remember { appPreferencesStore.getTracingLogLevelFlow().map { AsyncData.Success(it.toLogLevelItem()) } @@ -126,9 +123,6 @@ class DeveloperSettingsPresenter @Inject constructor( appPreferencesStore.setCustomElementCallBaseUrl(urlToSave) } DeveloperSettingsEvents.ClearCache -> coroutineScope.clearCache(clearCacheAction) - is DeveloperSettingsEvents.SetHideImagesAndVideos -> coroutineScope.launch { - appPreferencesStore.setHideImagesAndVideos(event.value) - } is DeveloperSettingsEvents.SetTracingLogLevel -> coroutineScope.launch { appPreferencesStore.setTracingLogLevel(event.logLevel.toLogLevel()) } @@ -153,7 +147,6 @@ class DeveloperSettingsPresenter @Inject constructor( baseUrl = customElementCallBaseUrl, validator = ::customElementCallUrlValidator, ), - hideImagesAndVideos = hideImagesAndVideos, tracingLogLevel = tracingLogLevel, tracingLogPacks = tracingLogPacks, eventSink = ::handleEvents diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsState.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsState.kt index efcfcd01d4..93e7b9ae7b 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsState.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsState.kt @@ -21,7 +21,6 @@ data class DeveloperSettingsState( val rageshakeState: RageshakePreferencesState, val clearCacheAction: AsyncAction, val customElementCallBaseUrlState: CustomElementCallBaseUrlState, - val hideImagesAndVideos: Boolean, val tracingLogLevel: AsyncData, val tracingLogPacks: ImmutableList, val eventSink: (DeveloperSettingsEvents) -> Unit diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsStateProvider.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsStateProvider.kt index 18151d32c7..1585a3b8dd 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsStateProvider.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsStateProvider.kt @@ -34,7 +34,6 @@ open class DeveloperSettingsStateProvider : PreviewParameterProvider = AsyncAction.Uninitialized, customElementCallBaseUrlState: CustomElementCallBaseUrlState = aCustomElementCallBaseUrlState(), - hideImagesAndVideos: Boolean = false, traceLogPacks: List = emptyList(), eventSink: (DeveloperSettingsEvents) -> Unit = {}, ) = DeveloperSettingsState( @@ -43,7 +42,6 @@ fun aDeveloperSettingsState( cacheSize = AsyncData.Success("1.2 MB"), clearCacheAction = clearCacheAction, customElementCallBaseUrlState = customElementCallBaseUrlState, - hideImagesAndVideos = hideImagesAndVideos, tracingLogLevel = AsyncData.Success(LogLevelItem.INFO), tracingLogPacks = traceLogPacks.toPersistentList(), eventSink = eventSink, diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsView.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsView.kt index e335c0a6cd..9a9cc61d68 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsView.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsView.kt @@ -51,7 +51,6 @@ fun DeveloperSettingsView( title = stringResource(id = CommonStrings.common_developer_options) ) { // Note: this is OK to hardcode strings in this debug screen. - SettingsCategory(state) PreferenceCategory( title = "Feature flags", showTopDivider = true, @@ -134,22 +133,6 @@ fun DeveloperSettingsView( } } -@Composable -private fun SettingsCategory( - state: DeveloperSettingsState, -) { - PreferenceCategory(title = "Preferences", showTopDivider = false) { - PreferenceSwitch( - title = "Hide image & video previews", - subtitle = "When toggled image & video will not render in the timeline by default.", - isChecked = state.hideImagesAndVideos, - onCheckedChange = { - state.eventSink(DeveloperSettingsEvents.SetHideImagesAndVideos(it)) - } - ) - } -} - @Composable private fun ElementCallCategory( state: DeveloperSettingsState, diff --git a/features/preferences/impl/src/main/res/values/localazy.xml b/features/preferences/impl/src/main/res/values/localazy.xml index 989910384c..e27ebd661a 100644 --- a/features/preferences/impl/src/main/res/values/localazy.xml +++ b/features/preferences/impl/src/main/res/values/localazy.xml @@ -19,6 +19,11 @@ "If turned off, your read receipts won\'t be sent to anyone. You will still receive read receipts from other users." "Share presence" "If turned off, you won’t be able to send or receive read receipts or typing notifications." + "Always hide" + "Always show" + "In private rooms" + "A hidden media can always be shown by tapping on it" + "Show media in timeline" "Enable option to view message source in the timeline." "You have no blocked users" "Unblock" diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenterTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenterTest.kt index c2b4672878..4960add4ac 100644 --- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenterTest.kt +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenterTest.kt @@ -147,28 +147,6 @@ class DeveloperSettingsPresenterTest { } } - @Test - fun `present - toggling hide image and video`() = runTest { - val preferences = InMemoryAppPreferencesStore() - val presenter = createDeveloperSettingsPresenter(preferencesStore = preferences) - presenter.test { - skipItems(2) - awaitItem().also { state -> - assertThat(state.hideImagesAndVideos).isFalse() - state.eventSink(DeveloperSettingsEvents.SetHideImagesAndVideos(true)) - } - awaitItem().also { state -> - assertThat(state.hideImagesAndVideos).isTrue() - assertThat(preferences.doesHideImagesAndVideosFlow().first()).isTrue() - state.eventSink(DeveloperSettingsEvents.SetHideImagesAndVideos(false)) - } - awaitItem().also { state -> - assertThat(state.hideImagesAndVideos).isFalse() - assertThat(preferences.doesHideImagesAndVideosFlow().first()).isFalse() - } - } - } - @Test fun `present - changing tracing log level`() = runTest { val preferences = InMemoryAppPreferencesStore() diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsViewTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsViewTest.kt index 255c6442d9..5d0030af8e 100644 --- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsViewTest.kt +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsViewTest.kt @@ -109,18 +109,6 @@ class DeveloperSettingsViewTest { rule.onNodeWithText("Clear cache").performClick() eventsRecorder.assertSingle(DeveloperSettingsEvents.ClearCache) } - - @Test - fun `clicking on the hide images and videos switch emits the expected event`() { - val eventsRecorder = EventsRecorder() - rule.setDeveloperSettingsView( - state = aDeveloperSettingsState( - eventSink = eventsRecorder - ), - ) - rule.onNodeWithText("Hide image & video previews").performClick() - eventsRecorder.assertSingle(DeveloperSettingsEvents.SetHideImagesAndVideos(true)) - } } private fun AndroidComposeTestRule.setDeveloperSettingsView( diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/MediaPreviewValue.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/MediaPreviewValue.kt new file mode 100644 index 0000000000..06ebc237e0 --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/MediaPreviewValue.kt @@ -0,0 +1,20 @@ +/* + * 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.libraries.matrix.api.media + +/** + * Represents the values for media preview settings. + * - [On] means that media preview are enabled + * - [Off] means that media preview are disabled + * - [Private] means that media preview are enabled only for private chats. + */ +enum class MediaPreviewValue { + On, + Off, + Private +} 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 c072229626..dfc0e38b89 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 @@ -7,6 +7,7 @@ package io.element.android.libraries.preferences.api.store +import io.element.android.libraries.matrix.api.media.MediaPreviewValue import io.element.android.libraries.matrix.api.tracing.LogLevel import io.element.android.libraries.matrix.api.tracing.TraceLogPack import kotlinx.coroutines.flow.Flow @@ -21,8 +22,11 @@ interface AppPreferencesStore { suspend fun setTheme(theme: String) fun getThemeFlow(): Flow - suspend fun setHideImagesAndVideos(value: Boolean) - fun doesHideImagesAndVideosFlow(): Flow + suspend fun setHideInviteAvatars(value: Boolean) + fun getHideInviteAvatarsFlow(): Flow + + suspend fun setTimelineMediaPreviewValue(value: MediaPreviewValue) + fun getTimelineMediaPreviewValueFlow(): Flow suspend fun setTracingLogLevel(logLevel: LogLevel) fun getTracingLogLevelFlow(): Flow diff --git a/libraries/preferences/impl/src/main/kotlin/io/element/android/libraries/preferences/impl/store/DefaultAppPreferencesStore.kt b/libraries/preferences/impl/src/main/kotlin/io/element/android/libraries/preferences/impl/store/DefaultAppPreferencesStore.kt index a05e9c48da..599c73fc9d 100644 --- a/libraries/preferences/impl/src/main/kotlin/io/element/android/libraries/preferences/impl/store/DefaultAppPreferencesStore.kt +++ b/libraries/preferences/impl/src/main/kotlin/io/element/android/libraries/preferences/impl/store/DefaultAppPreferencesStore.kt @@ -19,6 +19,7 @@ import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.core.meta.BuildType import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.ApplicationContext +import io.element.android.libraries.matrix.api.media.MediaPreviewValue import io.element.android.libraries.matrix.api.tracing.LogLevel import io.element.android.libraries.matrix.api.tracing.TraceLogPack import io.element.android.libraries.preferences.api.store.AppPreferencesStore @@ -31,7 +32,8 @@ private val Context.dataStore: DataStore by preferencesDataStore(na private val developerModeKey = booleanPreferencesKey("developerMode") private val customElementCallBaseUrlKey = stringPreferencesKey("elementCallBaseUrl") private val themeKey = stringPreferencesKey("theme") -private val hideImagesAndVideosKey = booleanPreferencesKey("hideImagesAndVideos") +private val hideInviteAvatarsKey = booleanPreferencesKey("hideInviteAvatars") +private val timelineMediaPreviewValueKey = stringPreferencesKey("timelineMediaPreviewValue") private val logLevelKey = stringPreferencesKey("logLevel") private val traceLogPacksKey = stringPreferencesKey("traceLogPacks") @@ -83,15 +85,27 @@ class DefaultAppPreferencesStore @Inject constructor( } } - override suspend fun setHideImagesAndVideos(value: Boolean) { + override suspend fun setHideInviteAvatars(value: Boolean) { store.edit { prefs -> - prefs[hideImagesAndVideosKey] = value + prefs[hideInviteAvatarsKey] = value } } - override fun doesHideImagesAndVideosFlow(): Flow { + override fun getHideInviteAvatarsFlow(): Flow { return store.data.map { prefs -> - prefs[hideImagesAndVideosKey] ?: false + prefs[hideInviteAvatarsKey] == true + } + } + + override suspend fun setTimelineMediaPreviewValue(value: MediaPreviewValue) { + store.edit { prefs -> + prefs[timelineMediaPreviewValueKey] = value.name + } + } + + override fun getTimelineMediaPreviewValueFlow(): Flow { + return store.data.map { prefs -> + prefs[timelineMediaPreviewValueKey]?.let { MediaPreviewValue.valueOf(it) } ?: MediaPreviewValue.On } } diff --git a/libraries/preferences/test/src/main/kotlin/io/element/android/libraries/preferences/test/InMemoryAppPreferencesStore.kt b/libraries/preferences/test/src/main/kotlin/io/element/android/libraries/preferences/test/InMemoryAppPreferencesStore.kt index ab3913cd08..c5440a6be9 100644 --- a/libraries/preferences/test/src/main/kotlin/io/element/android/libraries/preferences/test/InMemoryAppPreferencesStore.kt +++ b/libraries/preferences/test/src/main/kotlin/io/element/android/libraries/preferences/test/InMemoryAppPreferencesStore.kt @@ -7,6 +7,7 @@ package io.element.android.libraries.preferences.test +import io.element.android.libraries.matrix.api.media.MediaPreviewValue import io.element.android.libraries.matrix.api.tracing.LogLevel import io.element.android.libraries.matrix.api.tracing.TraceLogPack import io.element.android.libraries.preferences.api.store.AppPreferencesStore @@ -15,18 +16,20 @@ import kotlinx.coroutines.flow.MutableStateFlow class InMemoryAppPreferencesStore( isDeveloperModeEnabled: Boolean = false, - hideImagesAndVideos: Boolean = false, customElementCallBaseUrl: String? = null, + hideInviteAvatars: Boolean = false, + timelineMediaPreviewValue: MediaPreviewValue = MediaPreviewValue.On, theme: String? = null, logLevel: LogLevel = LogLevel.INFO, traceLockPacks: Set = emptySet(), ) : AppPreferencesStore { private val isDeveloperModeEnabled = MutableStateFlow(isDeveloperModeEnabled) - private val hideImagesAndVideos = MutableStateFlow(hideImagesAndVideos) private val customElementCallBaseUrl = MutableStateFlow(customElementCallBaseUrl) private val theme = MutableStateFlow(theme) private val logLevel = MutableStateFlow(logLevel) private val tracingLogPacks = MutableStateFlow(traceLockPacks) + private val hideInviteAvatars = MutableStateFlow(hideInviteAvatars) + private val timelineMediaPreviewValue = MutableStateFlow(timelineMediaPreviewValue) override suspend fun setDeveloperModeEnabled(enabled: Boolean) { isDeveloperModeEnabled.value = enabled @@ -52,12 +55,20 @@ class InMemoryAppPreferencesStore( return theme } - override suspend fun setHideImagesAndVideos(value: Boolean) { - hideImagesAndVideos.value = value + override suspend fun setHideInviteAvatars(value: Boolean) { + hideInviteAvatars.value = value } - override fun doesHideImagesAndVideosFlow(): Flow { - return hideImagesAndVideos + override fun getHideInviteAvatarsFlow(): Flow { + return hideInviteAvatars + } + + override suspend fun setTimelineMediaPreviewValue(value: MediaPreviewValue) { + timelineMediaPreviewValue.value = value + } + + override fun getTimelineMediaPreviewValueFlow(): Flow { + return timelineMediaPreviewValue } override 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 c8e8ba4431..5116e945c5 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 @@ -21,6 +21,7 @@ import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.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.notification.NotificationContent import io.element.android.libraries.matrix.api.notification.NotificationData import io.element.android.libraries.matrix.api.permalink.PermalinkParser @@ -292,7 +293,7 @@ class DefaultNotifiableEventResolver @Inject constructor( } private suspend fun NotificationContent.MessageLike.RoomMessage.fetchImageIfPresent(client: MatrixClient): Uri? { - if (appPreferencesStore.doesHideImagesAndVideosFlow().first()) { + if (appPreferencesStore.getTimelineMediaPreviewValueFlow().first() != MediaPreviewValue.On) { return null } val fileResult = when (val messageType = messageType) { @@ -319,7 +320,7 @@ class DefaultNotifiableEventResolver @Inject constructor( } private suspend fun NotificationContent.MessageLike.RoomMessage.getImageMimetype(): String? { - if (appPreferencesStore.doesHideImagesAndVideosFlow().first()) { + if (appPreferencesStore.getTimelineMediaPreviewValueFlow().first() != MediaPreviewValue.On) { return null } return when (val messageType = messageType) {