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:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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) }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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 = { }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 won’t 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>
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 = {},
|
||||
|
||||
@@ -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 = {},
|
||||
|
||||
@@ -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 = {},
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user