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:
commit
ce0bac55c5
67 changed files with 382 additions and 147 deletions
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue