Merge pull request #4574 from element-hq/feature/fga/advanced_settings_moderation_and_safety

change (preferences) : new moderation and safety settings
This commit is contained in:
ganfra
2025-04-11 14:38:46 +02:00
committed by GitHub
67 changed files with 382 additions and 147 deletions

View File

@@ -35,6 +35,7 @@ dependencies {
implementation(projects.features.invite.api)
implementation(projects.features.roomdirectory.api)
implementation(projects.services.analytics.api)
implementation(projects.libraries.preferences.api)
testImplementation(libs.test.junit)
testImplementation(libs.coroutines.test)
@@ -46,5 +47,6 @@ dependencies {
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.tests.testutils)
testImplementation(libs.androidx.compose.ui.test.junit)
testImplementation(projects.libraries.preferences.test)
testReleaseImplementation(libs.androidx.compose.ui.test.manifest)
}

View File

@@ -52,6 +52,7 @@ 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
@@ -69,6 +70,7 @@ class JoinRoomPresenter @AssistedInject constructor(
private val forgetRoom: ForgetRoom,
private val acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState>,
private val buildMeta: BuildMeta,
private val appPreferencesStore: AppPreferencesStore,
private val seenInvitesStore: SeenInvitesStore,
) : Presenter<JoinRoomState> {
interface Factory {
@@ -94,6 +96,9 @@ class JoinRoomPresenter @AssistedInject constructor(
val forgetRoomAction: MutableState<AsyncAction<Unit>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
var knockMessage by rememberSaveable { mutableStateOf("") }
var isDismissingContent by remember { mutableStateOf(false) }
val hideInviteAvatars by remember {
appPreferencesStore.getHideInviteAvatarsFlow()
}.collectAsState(initial = false)
val contentState by produceState<ContentState>(
initialValue = ContentState.Loading,
key1 = roomInfo,
@@ -202,6 +207,7 @@ class JoinRoomPresenter @AssistedInject constructor(
cancelKnockAction = cancelKnockAction.value,
applicationName = buildMeta.applicationName,
knockMessage = knockMessage,
hideInviteAvatars = hideInviteAvatars,
eventSink = ::handleEvents
)
}

View File

@@ -31,6 +31,7 @@ data class JoinRoomState(
val cancelKnockAction: AsyncAction<Unit>,
private val applicationName: String,
val knockMessage: String,
val hideInviteAvatars: Boolean,
val eventSink: (JoinRoomEvents) -> Unit
) {
val isJoinActionUnauthorized = joinAction is AsyncAction.Failure && joinAction.error is JoinRoomFailures.UnauthorizedJoin
@@ -57,6 +58,8 @@ data class JoinRoomState(
}
else -> JoinAuthorisationStatus.None
}
val hideAvatarsImages = hideInviteAvatars && joinAuthorisationStatus is JoinAuthorisationStatus.IsInvited
}
@Immutable

View File

@@ -171,6 +171,7 @@ fun aJoinRoomState(
forgetAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
cancelKnockAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
knockMessage: String = "",
hideInviteAvatars: Boolean = false,
eventSink: (JoinRoomEvents) -> Unit = {}
) = JoinRoomState(
roomIdOrAlias = roomIdOrAlias,
@@ -182,6 +183,7 @@ fun aJoinRoomState(
forgetAction = forgetAction,
applicationName = "AppName",
knockMessage = knockMessage,
hideInviteAvatars = hideInviteAvatars,
eventSink = eventSink
)

View File

@@ -97,6 +97,7 @@ fun JoinRoomView(
roomIdOrAlias = state.roomIdOrAlias,
contentState = state.contentState,
knockMessage = state.knockMessage,
hideAvatarsImages = state.hideAvatarsImages,
onKnockMessageUpdate = { state.eventSink(JoinRoomEvents.UpdateKnockMessage(it)) },
)
},
@@ -371,6 +372,7 @@ private fun JoinRoomContent(
roomIdOrAlias: RoomIdOrAlias,
contentState: ContentState,
knockMessage: String,
hideAvatarsImages: Boolean,
onKnockMessageUpdate: (String) -> Unit,
modifier: Modifier = Modifier,
) {
@@ -385,13 +387,14 @@ private fun JoinRoomContent(
Column(horizontalAlignment = Alignment.CenterHorizontally) {
val inviteSender = (contentState.joinAuthorisationStatus as? JoinAuthorisationStatus.IsInvited)?.inviteSender
if (inviteSender != null) {
InviteSenderView(inviteSender = inviteSender)
InviteSenderView(inviteSender = inviteSender, hideAvatarImage = hideAvatarsImages)
Spacer(modifier = Modifier.height(32.dp))
}
DefaultLoadedContent(
modifier = Modifier.verticalScroll(rememberScrollState()),
contentState = contentState,
knockMessage = knockMessage,
hideAvatarImage = hideAvatarsImages,
onKnockMessageUpdate = onKnockMessageUpdate
)
}
@@ -474,13 +477,14 @@ private fun IsKnockedLoadedContent(modifier: Modifier = Modifier) {
private fun DefaultLoadedContent(
contentState: ContentState.Loaded,
knockMessage: String,
hideAvatarImage: Boolean,
onKnockMessageUpdate: (String) -> Unit,
modifier: Modifier = Modifier,
) {
RoomPreviewOrganism(
modifier = modifier,
avatar = {
Avatar(contentState.avatarData(AvatarSize.RoomHeader))
Avatar(contentState.avatarData(AvatarSize.RoomHeader), hideImage = hideAvatarImage)
},
title = {
if (contentState.name != null) {

View File

@@ -22,6 +22,7 @@ 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
@@ -36,6 +37,7 @@ object JoinRoomModule {
forgetRoom: ForgetRoom,
acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState>,
buildMeta: BuildMeta,
appPreferencesStore: AppPreferencesStore,
seenInvitesStore: SeenInvitesStore,
): JoinRoomPresenter.Factory {
return object : JoinRoomPresenter.Factory {
@@ -59,6 +61,7 @@ object JoinRoomModule {
cancelKnockRoom = cancelKnockRoom,
acceptDeclineInvitePresenter = acceptDeclineInvitePresenter,
buildMeta = buildMeta,
appPreferencesStore = appPreferencesStore,
seenInvitesStore = seenInvitesStore,
)
}

View File

@@ -47,6 +47,8 @@ import io.element.android.libraries.matrix.test.room.aRoomPreviewInfo
import io.element.android.libraries.matrix.test.room.aRoomSummary
import io.element.android.libraries.matrix.test.room.join.FakeJoinRoom
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
@@ -768,6 +770,7 @@ class JoinRoomPresenterTest {
forgetRoom: ForgetRoom = FakeForgetRoom(),
buildMeta: BuildMeta = aBuildMeta(applicationName = "AppName"),
acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState> = Presenter { anAcceptDeclineInviteState() },
appPreferencesStore: AppPreferencesStore = InMemoryAppPreferencesStore(),
seenInvitesStore: SeenInvitesStore = InMemorySeenInvitesStore(),
): JoinRoomPresenter {
return JoinRoomPresenter(
@@ -783,6 +786,7 @@ class JoinRoomPresenterTest {
forgetRoom = forgetRoom,
buildMeta = buildMeta,
acceptDeclineInvitePresenter = acceptDeclineInvitePresenter,
appPreferencesStore = appPreferencesStore,
seenInvitesStore = seenInvitesStore,
)
}

View File

@@ -16,25 +16,32 @@ 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.matrix.api.media.isPreviewEnabled
import io.element.android.libraries.matrix.api.room.MatrixRoom
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 room: MatrixRoom,
) : Presenter<TimelineProtectionState> {
private val allowedEvents = mutableStateOf<Set<EventId>>(setOf())
@Composable
override fun present(): TimelineProtectionState {
val hideMediaContent by remember {
appPreferencesStore.doesHideImagesAndVideosFlow()
}.collectAsState(initial = false)
var allowedEvents by remember { mutableStateOf<Set<EventId>>(setOf()) }
val protectionState by remember(hideMediaContent) {
val mediaPreviewValue = remember {
appPreferencesStore.getTimelineMediaPreviewValueFlow()
}.collectAsState(initial = MediaPreviewValue.On)
val roomInfo = room.roomInfoFlow.collectAsState()
val protectionState by remember {
derivedStateOf {
if (hideMediaContent) {
ProtectionState.RenderOnly(eventIds = allowedEvents.toImmutableSet())
} else {
val isPreviewEnabled = mediaPreviewValue.value.isPreviewEnabled(roomInfo.value.joinRule)
if (isPreviewEnabled) {
ProtectionState.RenderAll
} else {
ProtectionState.RenderOnly(eventIds = allowedEvents.value.toImmutableSet())
}
}
}
@@ -42,7 +49,7 @@ class TimelineProtectionPresenter @Inject constructor(
fun handleEvent(event: TimelineProtectionEvent) {
when (event) {
is TimelineProtectionEvent.ShowContent -> {
allowedEvents = allowedEvents + setOfNotNull(event.eventId)
allowedEvents.value = allowedEvents.value + setOfNotNull(event.eventId)
}
}
}

View File

@@ -8,7 +8,12 @@
package io.element.android.features.messages.impl.timeline.protection
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.media.MediaPreviewValue
import io.element.android.libraries.matrix.api.room.MatrixRoom
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.room.FakeMatrixRoom
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
@@ -32,8 +37,8 @@ class TimelineProtectionPresenterTest {
}
@Test
fun `present - protected`() = runTest {
val appPreferencesStore = InMemoryAppPreferencesStore(hideImagesAndVideos = true)
fun `present - media preview value off`() = runTest {
val appPreferencesStore = InMemoryAppPreferencesStore(timelineMediaPreviewValue = MediaPreviewValue.Off)
val presenter = createPresenter(appPreferencesStore)
presenter.test {
skipItems(1)
@@ -47,9 +52,42 @@ class TimelineProtectionPresenterTest {
}
}
@Test
fun `present - media preview value private in public room`() = runTest {
val appPreferencesStore = InMemoryAppPreferencesStore(timelineMediaPreviewValue = MediaPreviewValue.Private)
val room = FakeMatrixRoom(initialRoomInfo = aRoomInfo(joinRule = JoinRule.Public))
val presenter = createPresenter(appPreferencesStore, room)
presenter.test {
skipItems(1)
val initialState = awaitItem()
assertThat(initialState.protectionState).isEqualTo(ProtectionState.RenderOnly(persistentSetOf()))
// ShowContent with null should have no effect.
initialState.eventSink(TimelineProtectionEvent.ShowContent(eventId = null))
initialState.eventSink(TimelineProtectionEvent.ShowContent(eventId = AN_EVENT_ID))
val finalState = awaitItem()
assertThat(finalState.protectionState).isEqualTo(ProtectionState.RenderOnly(persistentSetOf(AN_EVENT_ID)))
}
}
@Test
fun `present - media preview value private in non public room`() = runTest {
val appPreferencesStore = InMemoryAppPreferencesStore(timelineMediaPreviewValue = MediaPreviewValue.Private)
val room = FakeMatrixRoom(initialRoomInfo = aRoomInfo(joinRule = JoinRule.Invite))
val presenter = createPresenter(appPreferencesStore, room)
presenter.test {
val initialState = awaitItem()
assertThat(initialState.protectionState).isEqualTo(ProtectionState.RenderAll)
// ShowContent with null should have no effect.
initialState.eventSink(TimelineProtectionEvent.ShowContent(eventId = null))
initialState.eventSink(TimelineProtectionEvent.ShowContent(eventId = AN_EVENT_ID))
}
}
private fun createPresenter(
appPreferencesStore: AppPreferencesStore = InMemoryAppPreferencesStore(),
room: MatrixRoom = FakeMatrixRoom(),
) = TimelineProtectionPresenter(
appPreferencesStore = appPreferencesStore,
room = room,
)
}

View File

@@ -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
}

View File

@@ -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
@@ -43,6 +44,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 {
@@ -60,6 +69,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)
}
}
}
@@ -69,6 +84,8 @@ class AdvancedSettingsPresenter @Inject constructor(
doesCompressMedia = doesCompressMedia,
theme = theme,
showChangeThemeDialog = showChangeThemeDialog,
hideInviteAvatars = hideInviteAvatars,
timelineMediaPreviewValue = timelineMediaPreviewValue,
eventSink = { handleEvents(it) }
)
}

View File

@@ -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
)

View File

@@ -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<AdvancedSettingsState> {
override val values: Sequence<AdvancedSettingsState>
@@ -18,6 +19,8 @@ open class AdvancedSettingsStateProvider : PreviewParameterProvider<AdvancedSett
aAdvancedSettingsState(showChangeThemeDialog = true),
aAdvancedSettingsState(isSharePresenceEnabled = true),
aAdvancedSettingsState(doesCompressMedia = true),
aAdvancedSettingsState(hideInviteAvatars = true),
aAdvancedSettingsState(timelineMediaPreviewValue = MediaPreviewValue.Off)
)
}
@@ -26,6 +29,8 @@ fun aAdvancedSettingsState(
isSharePresenceEnabled: Boolean = false,
doesCompressMedia: Boolean = false,
showChangeThemeDialog: Boolean = false,
hideInviteAvatars: Boolean = false,
timelineMediaPreviewValue: MediaPreviewValue = MediaPreviewValue.On,
eventSink: (AdvancedSettingsEvents) -> Unit = {},
) = AdvancedSettingsState(
isDeveloperModeEnabled = isDeveloperModeEnabled,
@@ -33,5 +38,7 @@ fun aAdvancedSettingsState(
doesCompressMedia = doesCompressMedia,
theme = Theme.System,
showChangeThemeDialog = showChangeThemeDialog,
hideInviteAvatars = hideInviteAvatars,
timelineMediaPreviewValue = timelineMediaPreviewValue,
eventSink = eventSink
)

View File

@@ -15,14 +15,22 @@ import im.vector.app.features.analytics.plan.Interaction
import io.element.android.compound.theme.Theme
import io.element.android.compound.theme.themes
import io.element.android.features.preferences.impl.R
import io.element.android.libraries.architecture.coverage.ExcludeFromCoverage
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.PreferencePage
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.components.preferences.PreferenceSwitch
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.preview.PreviewWithLargeHeight
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<ListOption> {
return themes.map {
@@ -134,9 +194,21 @@ private fun Theme.toHumanReadable(): String {
)
}
@PreviewsDayNight
@PreviewWithLargeHeight
@Composable
internal fun AdvancedSettingsViewPreview(@PreviewParameter(AdvancedSettingsStateProvider::class) state: AdvancedSettingsState) =
ElementPreview {
AdvancedSettingsView(state = state, onBackClick = { })
}
internal fun AdvancedSettingsViewLightPreview(@PreviewParameter(AdvancedSettingsStateProvider::class) state: AdvancedSettingsState) =
ElementPreviewLight { ContentToPreview(state) }
@PreviewWithLargeHeight
@Composable
internal fun AdvancedSettingsViewDarkPreview(@PreviewParameter(AdvancedSettingsStateProvider::class) state: AdvancedSettingsState) =
ElementPreviewDark { ContentToPreview(state) }
@ExcludeFromCoverage
@Composable
private fun ContentToPreview(state: AdvancedSettingsState) {
AdvancedSettingsView(
state = state,
onBackClick = { }
)
}

View File

@@ -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

View File

@@ -75,10 +75,6 @@ class DeveloperSettingsPresenter @Inject constructor(
appPreferencesStore
.getCustomElementCallBaseUrlFlow()
}.collectAsState(initial = null)
val hideImagesAndVideos by remember {
appPreferencesStore
.doesHideImagesAndVideosFlow()
}.collectAsState(initial = false)
val tracingLogLevelFlow = remember {
appPreferencesStore.getTracingLogLevelFlow().map { AsyncData.Success(it.toLogLevelItem()) }
@@ -128,9 +124,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())
}
@@ -155,7 +148,6 @@ class DeveloperSettingsPresenter @Inject constructor(
baseUrl = customElementCallBaseUrl,
validator = ::customElementCallUrlValidator,
),
hideImagesAndVideos = hideImagesAndVideos,
tracingLogLevel = tracingLogLevel,
tracingLogPacks = tracingLogPacks,
eventSink = ::handleEvents

View File

@@ -21,7 +21,6 @@ data class DeveloperSettingsState(
val rageshakeState: RageshakePreferencesState,
val clearCacheAction: AsyncAction<Unit>,
val customElementCallBaseUrlState: CustomElementCallBaseUrlState,
val hideImagesAndVideos: Boolean,
val tracingLogLevel: AsyncData<LogLevelItem>,
val tracingLogPacks: ImmutableList<TraceLogPack>,
val eventSink: (DeveloperSettingsEvents) -> Unit

View File

@@ -34,7 +34,6 @@ open class DeveloperSettingsStateProvider : PreviewParameterProvider<DeveloperSe
fun aDeveloperSettingsState(
clearCacheAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
customElementCallBaseUrlState: CustomElementCallBaseUrlState = aCustomElementCallBaseUrlState(),
hideImagesAndVideos: Boolean = false,
traceLogPacks: List<TraceLogPack> = 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,

View File

@@ -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,

View File

@@ -19,6 +19,11 @@
<string name="screen_advanced_settings_send_read_receipts_description">"If turned off, your read receipts won\'t be sent to anyone. You will still receive read receipts from other users."</string>
<string name="screen_advanced_settings_share_presence">"Share presence"</string>
<string name="screen_advanced_settings_share_presence_description">"If turned off, you wont be able to send or receive read receipts or typing notifications."</string>
<string name="screen_advanced_settings_show_media_timeline_always_hide">"Always hide"</string>
<string name="screen_advanced_settings_show_media_timeline_always_show">"Always show"</string>
<string name="screen_advanced_settings_show_media_timeline_private_rooms">"In private rooms"</string>
<string name="screen_advanced_settings_show_media_timeline_subtitle">"A hidden media can always be shown by tapping on it"</string>
<string name="screen_advanced_settings_show_media_timeline_title">"Show media in timeline"</string>
<string name="screen_advanced_settings_view_source_description">"Enable option to view message source in the timeline."</string>
<string name="screen_blocked_users_empty">"You have no blocked users"</string>
<string name="screen_blocked_users_unblock_alert_action">"Unblock"</string>

View File

@@ -44,7 +44,6 @@ class DeveloperSettingsPresenterTest {
assertThat(state.cacheSize).isEqualTo(AsyncData.Uninitialized)
assertThat(state.customElementCallBaseUrlState).isNotNull()
assertThat(state.customElementCallBaseUrlState.baseUrl).isNull()
assertThat(state.hideImagesAndVideos).isFalse()
assertThat(state.rageshakeState.isEnabled).isFalse()
assertThat(state.rageshakeState.isSupported).isTrue()
assertThat(state.rageshakeState.sensitivity).isEqualTo(0.3f)
@@ -147,28 +146,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()

View File

@@ -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<DeveloperSettingsEvents>()
rule.setDeveloperSettingsView(
state = aDeveloperSettingsState(
eventSink = eventsRecorder
),
)
rule.onNodeWithText("Hide image & video previews").performClick()
eventsRecorder.assertSingle(DeveloperSettingsEvents.SetHideImagesAndVideos(true))
}
}
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setDeveloperSettingsView(

View File

@@ -119,6 +119,9 @@ class RoomListPresenter @Inject constructor(
// Avatar indicator
val showAvatarIndicator by indicatorService.showRoomListTopBarIndicator()
val hideInvitesAvatar by remember {
appPreferencesStore.getHideInviteAvatarsFlow()
}.collectAsState(initial = false)
val contextMenu = remember { mutableStateOf<RoomListState.ContextMenu>(RoomListState.ContextMenu.Hidden) }
@@ -173,6 +176,7 @@ class RoomListPresenter @Inject constructor(
contentState = contentState,
acceptDeclineInviteState = acceptDeclineInviteState,
directLogoutState = directLogoutState,
hideInvitesAvatars = hideInvitesAvatar,
eventSink = ::handleEvents,
)
}

View File

@@ -35,6 +35,7 @@ data class RoomListState(
val contentState: RoomListContentState,
val acceptDeclineInviteState: AcceptDeclineInviteState,
val directLogoutState: DirectLogoutState,
val hideInvitesAvatars: Boolean,
val eventSink: (RoomListEvents) -> Unit,
) {
val displayFilters = contentState is RoomListContentState.Rooms

View File

@@ -61,6 +61,7 @@ internal fun aRoomListState(
contentState: RoomListContentState = aRoomsContentState(),
acceptDeclineInviteState: AcceptDeclineInviteState = anAcceptDeclineInviteState(),
directLogoutState: DirectLogoutState = aDirectLogoutState(),
hideInvitesAvatars: Boolean = false,
eventSink: (RoomListEvents) -> Unit = {}
) = RoomListState(
matrixUser = matrixUser,
@@ -75,6 +76,7 @@ internal fun aRoomListState(
contentState = contentState,
acceptDeclineInviteState = acceptDeclineInviteState,
directLogoutState = directLogoutState,
hideInvitesAvatars = hideInvitesAvatars,
eventSink = eventSink,
)

View File

@@ -81,6 +81,7 @@ fun RoomListView(
RoomListSearchView(
state = state.searchState,
eventSink = state.eventSink,
hideInvitesAvatars = state.hideInvitesAvatars,
onRoomClick = onRoomClick,
modifier = Modifier
.statusBarsPadding()
@@ -134,6 +135,7 @@ private fun RoomListScaffold(
RoomListContentView(
contentState = state.contentState,
filtersState = state.filtersState,
hideInvitesAvatars = state.hideInvitesAvatars,
eventSink = state.eventSink,
onSetUpRecoveryClick = onSetUpRecoveryClick,
onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick,

View File

@@ -60,6 +60,7 @@ import kotlinx.collections.immutable.ImmutableList
fun RoomListContentView(
contentState: RoomListContentState,
filtersState: RoomListFiltersState,
hideInvitesAvatars: Boolean,
eventSink: (RoomListEvents) -> Unit,
onSetUpRecoveryClick: () -> Unit,
onConfirmRecoveryKeyClick: () -> Unit,
@@ -86,6 +87,7 @@ fun RoomListContentView(
is RoomListContentState.Rooms -> {
RoomsView(
state = contentState,
hideInvitesAvatars = hideInvitesAvatars,
filtersState = filtersState,
eventSink = eventSink,
onSetUpRecoveryClick = onSetUpRecoveryClick,
@@ -156,6 +158,7 @@ private fun EmptyView(
@Composable
private fun RoomsView(
state: RoomListContentState.Rooms,
hideInvitesAvatars: Boolean,
filtersState: RoomListFiltersState,
eventSink: (RoomListEvents) -> Unit,
onSetUpRecoveryClick: () -> Unit,
@@ -171,6 +174,7 @@ private fun RoomsView(
} else {
RoomsViewList(
state = state,
hideInvitesAvatars = hideInvitesAvatars,
eventSink = eventSink,
onSetUpRecoveryClick = onSetUpRecoveryClick,
onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick,
@@ -183,6 +187,7 @@ private fun RoomsView(
@Composable
private fun RoomsViewList(
state: RoomListContentState.Rooms,
hideInvitesAvatars: Boolean,
eventSink: (RoomListEvents) -> Unit,
onSetUpRecoveryClick: () -> Unit,
onConfirmRecoveryKeyClick: () -> Unit,
@@ -240,6 +245,7 @@ private fun RoomsViewList(
) { index, room ->
RoomSummaryRow(
room = room,
hideInviteAvatars = hideInvitesAvatars,
isInviteSeen = room.displayType == RoomSummaryDisplayType.INVITE &&
state.seenRoomInvites.contains(room.roomId),
onClick = onRoomClick,
@@ -303,6 +309,7 @@ internal fun RoomListContentViewPreview(@PreviewParameter(RoomListContentStatePr
filtersState = aRoomListFiltersState(
filterSelectionStates = RoomListFilter.entries.map { FilterSelectionState(it, isSelected = true) }
),
hideInvitesAvatars = false,
eventSink = {},
onSetUpRecoveryClick = {},
onConfirmRecoveryKeyClick = {},

View File

@@ -68,6 +68,7 @@ internal val minHeight = 84.dp
@Composable
internal fun RoomSummaryRow(
room: RoomListRoomSummary,
hideInviteAvatars: Boolean,
isInviteSeen: Boolean,
onClick: (RoomListRoomSummary) -> Unit,
eventSink: (RoomListEvents) -> Unit,
@@ -81,6 +82,7 @@ internal fun RoomSummaryRow(
RoomSummaryDisplayType.INVITE -> {
RoomSummaryScaffoldRow(
room = room,
hideAvatarImage = hideInviteAvatars,
onClick = onClick,
onLongClick = {
Timber.d("Long click on invite room")
@@ -93,6 +95,7 @@ internal fun RoomSummaryRow(
InviteSenderView(
modifier = Modifier.fillMaxWidth(),
inviteSender = room.inviteSender,
hideAvatarImage = hideInviteAvatars
)
}
Spacer(modifier = Modifier.height(12.dp))
@@ -165,6 +168,7 @@ private fun RoomSummaryScaffoldRow(
onClick: (RoomListRoomSummary) -> Unit,
onLongClick: (RoomListRoomSummary) -> Unit,
modifier: Modifier = Modifier,
hideAvatarImage: Boolean = false,
content: @Composable ColumnScope.() -> Unit
) {
val clickModifier = Modifier.combinedClickable(
@@ -185,6 +189,7 @@ private fun RoomSummaryScaffoldRow(
CompositeAvatar(
avatarData = room.avatarData,
heroes = room.heroes,
hideAvatarImages = hideAvatarImage,
)
Spacer(modifier = Modifier.width(16.dp))
Column(
@@ -388,6 +393,7 @@ private fun MentionIndicatorAtom() {
internal fun RoomSummaryRowPreview(@PreviewParameter(RoomListRoomSummaryProvider::class) data: RoomListRoomSummary) = ElementPreview {
RoomSummaryRow(
room = data,
hideInviteAvatars = false,
// Set isInviteSeen to true for the preview when the room has name "Bob"
isInviteSeen = data.name == "Bob",
onClick = {},

View File

@@ -54,6 +54,7 @@ import io.element.android.libraries.ui.strings.CommonStrings
@Composable
internal fun RoomListSearchView(
state: RoomListSearchState,
hideInvitesAvatars: Boolean,
eventSink: (RoomListEvents) -> Unit,
onRoomClick: (RoomId) -> Unit,
modifier: Modifier = Modifier,
@@ -80,6 +81,7 @@ internal fun RoomListSearchView(
if (state.isSearchActive) {
RoomListSearchContent(
state = state,
hideInvitesAvatars = hideInvitesAvatars,
onRoomClick = onRoomClick,
eventSink = eventSink,
)
@@ -92,6 +94,7 @@ internal fun RoomListSearchView(
@Composable
private fun RoomListSearchContent(
state: RoomListSearchState,
hideInvitesAvatars: Boolean,
eventSink: (RoomListEvents) -> Unit,
onRoomClick: (RoomId) -> Unit,
) {
@@ -173,6 +176,7 @@ private fun RoomListSearchContent(
) { room ->
RoomSummaryRow(
room = room,
hideInviteAvatars = hideInvitesAvatars,
// TODO
isInviteSeen = false,
onClick = ::onRoomClick,
@@ -189,6 +193,7 @@ private fun RoomListSearchContent(
internal fun RoomListSearchContentPreview(@PreviewParameter(RoomListSearchStateProvider::class) state: RoomListSearchState) = ElementPreview {
RoomListSearchContent(
state = state,
hideInvitesAvatars = false,
onRoomClick = {},
eventSink = {},
)

View File

@@ -48,11 +48,13 @@ fun Avatar(
contentDescription: String? = null,
// If not null, will be used instead of the size from avatarData
forcedAvatarSize: Dp? = null,
// If true, will show initials even if avatarData.url is not null
hideImage: Boolean = false,
) {
val commonModifier = modifier
.size(forcedAvatarSize ?: avatarData.size.dp)
.clip(CircleShape)
if (avatarData.url.isNullOrBlank()) {
if (avatarData.url.isNullOrBlank() || hideImage) {
InitialsAvatar(
avatarData = avatarData,
forcedAvatarSize = forcedAvatarSize,

View File

@@ -33,10 +33,16 @@ fun CompositeAvatar(
avatarData: AvatarData,
heroes: ImmutableList<AvatarData>,
modifier: Modifier = Modifier,
hideAvatarImages: Boolean = false,
contentDescription: String? = null,
) {
if (avatarData.url != null || heroes.isEmpty()) {
Avatar(avatarData, modifier, contentDescription)
Avatar(
avatarData = avatarData,
modifier = modifier,
contentDescription = contentDescription,
hideImage = hideAvatarImages
)
} else {
val limitedHeroes = heroes.take(4)
val numberOfHeroes = limitedHeroes.size
@@ -49,7 +55,12 @@ fun CompositeAvatar(
error("Unsupported number of heroes: 0")
}
1 -> {
Avatar(heroes[0], modifier, contentDescription)
Avatar(
avatarData = heroes[0],
modifier = modifier,
contentDescription = contentDescription,
hideImage = hideAvatarImages
)
}
else -> {
val angle = 2 * Math.PI / numberOfHeroes
@@ -91,8 +102,9 @@ fun CompositeAvatar(
)
) {
Avatar(
heroAvatar,
avatarData = heroAvatar,
forcedAvatarSize = heroAvatarSize,
hideImage = hideAvatarImages,
)
}
}

View File

@@ -0,0 +1,39 @@
/*
* 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
import io.element.android.libraries.matrix.api.media.MediaPreviewValue.Off
import io.element.android.libraries.matrix.api.media.MediaPreviewValue.On
import io.element.android.libraries.matrix.api.media.MediaPreviewValue.Private
import io.element.android.libraries.matrix.api.room.join.JoinRule
/**
* 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
}
fun MediaPreviewValue.isPreviewEnabled(joinRule: JoinRule?): Boolean {
return when (this) {
On -> true
Off -> false
Private -> when (joinRule) {
is JoinRule.Knock,
is JoinRule.Invite,
is JoinRule.Restricted,
is JoinRule.KnockRestricted -> true
else -> false
}
}
}

View File

@@ -27,14 +27,15 @@ import io.element.android.libraries.matrix.ui.model.InviteSender
@Composable
fun InviteSenderView(
inviteSender: InviteSender,
modifier: Modifier = Modifier
modifier: Modifier = Modifier,
hideAvatarImage: Boolean = false,
) {
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
modifier = modifier,
) {
Box(modifier = Modifier.padding(vertical = 2.dp)) {
Avatar(avatarData = inviteSender.avatarData)
Avatar(avatarData = inviteSender.avatarData, hideImage = hideAvatarImage)
}
Text(
text = inviteSender.annotatedString(),

View File

@@ -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<String?>
suspend fun setHideImagesAndVideos(value: Boolean)
fun doesHideImagesAndVideosFlow(): Flow<Boolean>
suspend fun setHideInviteAvatars(value: Boolean)
fun getHideInviteAvatarsFlow(): Flow<Boolean>
suspend fun setTimelineMediaPreviewValue(value: MediaPreviewValue)
fun getTimelineMediaPreviewValueFlow(): Flow<MediaPreviewValue>
suspend fun setTracingLogLevel(logLevel: LogLevel)
fun getTracingLogLevelFlow(): Flow<LogLevel>

View File

@@ -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<Preferences> 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<Boolean> {
override fun getHideInviteAvatarsFlow(): Flow<Boolean> {
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<MediaPreviewValue> {
return store.data.map { prefs ->
prefs[timelineMediaPreviewValueKey]?.let { MediaPreviewValue.valueOf(it) } ?: MediaPreviewValue.On
}
}

View File

@@ -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<TraceLogPack> = 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<Boolean> {
return hideImagesAndVideos
override fun getHideInviteAvatarsFlow(): Flow<Boolean> {
return hideInviteAvatars
}
override suspend fun setTimelineMediaPreviewValue(value: MediaPreviewValue) {
timelineMediaPreviewValue.value = value
}
override fun getTimelineMediaPreviewValueFlow(): Flow<MediaPreviewValue> {
return timelineMediaPreviewValue
}
override suspend fun setTracingLogLevel(logLevel: LogLevel) {

View File

@@ -22,6 +22,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
@@ -305,7 +306,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) {
@@ -332,7 +333,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) {