Skip to content

Commit 62a2c2f

Browse files
authored
Merge pull request #4574 from element-hq/feature/fga/advanced_settings_moderation_and_safety
change (preferences) : new moderation and safety settings
2 parents c7f0566 + 0964b7b commit 62a2c2f

File tree

67 files changed

+382
-147
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

67 files changed

+382
-147
lines changed

features/joinroom/impl/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ dependencies {
3535
implementation(projects.features.invite.api)
3636
implementation(projects.features.roomdirectory.api)
3737
implementation(projects.services.analytics.api)
38+
implementation(projects.libraries.preferences.api)
3839

3940
testImplementation(libs.test.junit)
4041
testImplementation(libs.coroutines.test)
@@ -46,5 +47,6 @@ dependencies {
4647
testImplementation(projects.libraries.matrix.test)
4748
testImplementation(projects.tests.testutils)
4849
testImplementation(libs.androidx.compose.ui.test.junit)
50+
testImplementation(projects.libraries.preferences.test)
4951
testReleaseImplementation(libs.androidx.compose.ui.test.manifest)
5052
}

features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenter.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ import io.element.android.libraries.matrix.api.room.join.JoinRoom
5252
import io.element.android.libraries.matrix.api.room.join.JoinRule
5353
import io.element.android.libraries.matrix.api.room.preview.RoomPreviewInfo
5454
import io.element.android.libraries.matrix.ui.model.toInviteSender
55+
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
5556
import kotlinx.coroutines.CoroutineScope
5657
import kotlinx.coroutines.launch
5758
import java.util.Optional
@@ -69,6 +70,7 @@ class JoinRoomPresenter @AssistedInject constructor(
6970
private val forgetRoom: ForgetRoom,
7071
private val acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState>,
7172
private val buildMeta: BuildMeta,
73+
private val appPreferencesStore: AppPreferencesStore,
7274
private val seenInvitesStore: SeenInvitesStore,
7375
) : Presenter<JoinRoomState> {
7476
interface Factory {
@@ -94,6 +96,9 @@ class JoinRoomPresenter @AssistedInject constructor(
9496
val forgetRoomAction: MutableState<AsyncAction<Unit>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
9597
var knockMessage by rememberSaveable { mutableStateOf("") }
9698
var isDismissingContent by remember { mutableStateOf(false) }
99+
val hideInviteAvatars by remember {
100+
appPreferencesStore.getHideInviteAvatarsFlow()
101+
}.collectAsState(initial = false)
97102
val contentState by produceState<ContentState>(
98103
initialValue = ContentState.Loading,
99104
key1 = roomInfo,
@@ -202,6 +207,7 @@ class JoinRoomPresenter @AssistedInject constructor(
202207
cancelKnockAction = cancelKnockAction.value,
203208
applicationName = buildMeta.applicationName,
204209
knockMessage = knockMessage,
210+
hideInviteAvatars = hideInviteAvatars,
205211
eventSink = ::handleEvents
206212
)
207213
}

features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomState.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ data class JoinRoomState(
3131
val cancelKnockAction: AsyncAction<Unit>,
3232
private val applicationName: String,
3333
val knockMessage: String,
34+
val hideInviteAvatars: Boolean,
3435
val eventSink: (JoinRoomEvents) -> Unit
3536
) {
3637
val isJoinActionUnauthorized = joinAction is AsyncAction.Failure && joinAction.error is JoinRoomFailures.UnauthorizedJoin
@@ -57,6 +58,8 @@ data class JoinRoomState(
5758
}
5859
else -> JoinAuthorisationStatus.None
5960
}
61+
62+
val hideAvatarsImages = hideInviteAvatars && joinAuthorisationStatus is JoinAuthorisationStatus.IsInvited
6063
}
6164

6265
@Immutable

features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomStateProvider.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,7 @@ fun aJoinRoomState(
171171
forgetAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
172172
cancelKnockAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
173173
knockMessage: String = "",
174+
hideInviteAvatars: Boolean = false,
174175
eventSink: (JoinRoomEvents) -> Unit = {}
175176
) = JoinRoomState(
176177
roomIdOrAlias = roomIdOrAlias,
@@ -182,6 +183,7 @@ fun aJoinRoomState(
182183
forgetAction = forgetAction,
183184
applicationName = "AppName",
184185
knockMessage = knockMessage,
186+
hideInviteAvatars = hideInviteAvatars,
185187
eventSink = eventSink
186188
)
187189

features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomView.kt

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ fun JoinRoomView(
9797
roomIdOrAlias = state.roomIdOrAlias,
9898
contentState = state.contentState,
9999
knockMessage = state.knockMessage,
100+
hideAvatarsImages = state.hideAvatarsImages,
100101
onKnockMessageUpdate = { state.eventSink(JoinRoomEvents.UpdateKnockMessage(it)) },
101102
)
102103
},
@@ -371,6 +372,7 @@ private fun JoinRoomContent(
371372
roomIdOrAlias: RoomIdOrAlias,
372373
contentState: ContentState,
373374
knockMessage: String,
375+
hideAvatarsImages: Boolean,
374376
onKnockMessageUpdate: (String) -> Unit,
375377
modifier: Modifier = Modifier,
376378
) {
@@ -385,13 +387,14 @@ private fun JoinRoomContent(
385387
Column(horizontalAlignment = Alignment.CenterHorizontally) {
386388
val inviteSender = (contentState.joinAuthorisationStatus as? JoinAuthorisationStatus.IsInvited)?.inviteSender
387389
if (inviteSender != null) {
388-
InviteSenderView(inviteSender = inviteSender)
390+
InviteSenderView(inviteSender = inviteSender, hideAvatarImage = hideAvatarsImages)
389391
Spacer(modifier = Modifier.height(32.dp))
390392
}
391393
DefaultLoadedContent(
392394
modifier = Modifier.verticalScroll(rememberScrollState()),
393395
contentState = contentState,
394396
knockMessage = knockMessage,
397+
hideAvatarImage = hideAvatarsImages,
395398
onKnockMessageUpdate = onKnockMessageUpdate
396399
)
397400
}
@@ -474,13 +477,14 @@ private fun IsKnockedLoadedContent(modifier: Modifier = Modifier) {
474477
private fun DefaultLoadedContent(
475478
contentState: ContentState.Loaded,
476479
knockMessage: String,
480+
hideAvatarImage: Boolean,
477481
onKnockMessageUpdate: (String) -> Unit,
478482
modifier: Modifier = Modifier,
479483
) {
480484
RoomPreviewOrganism(
481485
modifier = modifier,
482486
avatar = {
483-
Avatar(contentState.avatarData(AvatarSize.RoomHeader))
487+
Avatar(contentState.avatarData(AvatarSize.RoomHeader), hideImage = hideAvatarImage)
484488
},
485489
title = {
486490
if (contentState.name != null) {

features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/di/JoinRoomModule.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import io.element.android.libraries.matrix.api.MatrixClient
2222
import io.element.android.libraries.matrix.api.core.RoomId
2323
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
2424
import io.element.android.libraries.matrix.api.room.join.JoinRoom
25+
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
2526
import java.util.Optional
2627

2728
@Module
@@ -36,6 +37,7 @@ object JoinRoomModule {
3637
forgetRoom: ForgetRoom,
3738
acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState>,
3839
buildMeta: BuildMeta,
40+
appPreferencesStore: AppPreferencesStore,
3941
seenInvitesStore: SeenInvitesStore,
4042
): JoinRoomPresenter.Factory {
4143
return object : JoinRoomPresenter.Factory {
@@ -59,6 +61,7 @@ object JoinRoomModule {
5961
cancelKnockRoom = cancelKnockRoom,
6062
acceptDeclineInvitePresenter = acceptDeclineInvitePresenter,
6163
buildMeta = buildMeta,
64+
appPreferencesStore = appPreferencesStore,
6265
seenInvitesStore = seenInvitesStore,
6366
)
6467
}

features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenterTest.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ import io.element.android.libraries.matrix.test.room.aRoomPreviewInfo
4747
import io.element.android.libraries.matrix.test.room.aRoomSummary
4848
import io.element.android.libraries.matrix.test.room.join.FakeJoinRoom
4949
import io.element.android.libraries.matrix.ui.model.toInviteSender
50+
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
51+
import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore
5052
import io.element.android.tests.testutils.WarmUpRule
5153
import io.element.android.tests.testutils.lambda.any
5254
import io.element.android.tests.testutils.lambda.assert
@@ -768,6 +770,7 @@ class JoinRoomPresenterTest {
768770
forgetRoom: ForgetRoom = FakeForgetRoom(),
769771
buildMeta: BuildMeta = aBuildMeta(applicationName = "AppName"),
770772
acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState> = Presenter { anAcceptDeclineInviteState() },
773+
appPreferencesStore: AppPreferencesStore = InMemoryAppPreferencesStore(),
771774
seenInvitesStore: SeenInvitesStore = InMemorySeenInvitesStore(),
772775
): JoinRoomPresenter {
773776
return JoinRoomPresenter(
@@ -783,6 +786,7 @@ class JoinRoomPresenterTest {
783786
forgetRoom = forgetRoom,
784787
buildMeta = buildMeta,
785788
acceptDeclineInvitePresenter = acceptDeclineInvitePresenter,
789+
appPreferencesStore = appPreferencesStore,
786790
seenInvitesStore = seenInvitesStore,
787791
)
788792
}

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/protection/TimelineProtectionPresenter.kt

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,33 +16,40 @@ import androidx.compose.runtime.remember
1616
import androidx.compose.runtime.setValue
1717
import io.element.android.libraries.architecture.Presenter
1818
import io.element.android.libraries.matrix.api.core.EventId
19+
import io.element.android.libraries.matrix.api.media.MediaPreviewValue
20+
import io.element.android.libraries.matrix.api.media.isPreviewEnabled
21+
import io.element.android.libraries.matrix.api.room.MatrixRoom
1922
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
2023
import kotlinx.collections.immutable.toImmutableSet
2124
import javax.inject.Inject
2225

2326
class TimelineProtectionPresenter @Inject constructor(
2427
private val appPreferencesStore: AppPreferencesStore,
28+
private val room: MatrixRoom,
2529
) : Presenter<TimelineProtectionState> {
30+
private val allowedEvents = mutableStateOf<Set<EventId>>(setOf())
31+
2632
@Composable
2733
override fun present(): TimelineProtectionState {
28-
val hideMediaContent by remember {
29-
appPreferencesStore.doesHideImagesAndVideosFlow()
30-
}.collectAsState(initial = false)
31-
var allowedEvents by remember { mutableStateOf<Set<EventId>>(setOf()) }
32-
val protectionState by remember(hideMediaContent) {
34+
val mediaPreviewValue = remember {
35+
appPreferencesStore.getTimelineMediaPreviewValueFlow()
36+
}.collectAsState(initial = MediaPreviewValue.On)
37+
val roomInfo = room.roomInfoFlow.collectAsState()
38+
val protectionState by remember {
3339
derivedStateOf {
34-
if (hideMediaContent) {
35-
ProtectionState.RenderOnly(eventIds = allowedEvents.toImmutableSet())
36-
} else {
40+
val isPreviewEnabled = mediaPreviewValue.value.isPreviewEnabled(roomInfo.value.joinRule)
41+
if (isPreviewEnabled) {
3742
ProtectionState.RenderAll
43+
} else {
44+
ProtectionState.RenderOnly(eventIds = allowedEvents.value.toImmutableSet())
3845
}
3946
}
4047
}
4148

4249
fun handleEvent(event: TimelineProtectionEvent) {
4350
when (event) {
4451
is TimelineProtectionEvent.ShowContent -> {
45-
allowedEvents = allowedEvents + setOfNotNull(event.eventId)
52+
allowedEvents.value = allowedEvents.value + setOfNotNull(event.eventId)
4653
}
4754
}
4855
}

features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/protection/TimelineProtectionPresenterTest.kt

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,12 @@
88
package io.element.android.features.messages.impl.timeline.protection
99

1010
import com.google.common.truth.Truth.assertThat
11+
import io.element.android.libraries.matrix.api.media.MediaPreviewValue
12+
import io.element.android.libraries.matrix.api.room.MatrixRoom
13+
import io.element.android.libraries.matrix.api.room.join.JoinRule
1114
import io.element.android.libraries.matrix.test.AN_EVENT_ID
15+
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
16+
import io.element.android.libraries.matrix.test.room.aRoomInfo
1217
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
1318
import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore
1419
import io.element.android.tests.testutils.WarmUpRule
@@ -32,8 +37,8 @@ class TimelineProtectionPresenterTest {
3237
}
3338

3439
@Test
35-
fun `present - protected`() = runTest {
36-
val appPreferencesStore = InMemoryAppPreferencesStore(hideImagesAndVideos = true)
40+
fun `present - media preview value off`() = runTest {
41+
val appPreferencesStore = InMemoryAppPreferencesStore(timelineMediaPreviewValue = MediaPreviewValue.Off)
3742
val presenter = createPresenter(appPreferencesStore)
3843
presenter.test {
3944
skipItems(1)
@@ -47,9 +52,42 @@ class TimelineProtectionPresenterTest {
4752
}
4853
}
4954

55+
@Test
56+
fun `present - media preview value private in public room`() = runTest {
57+
val appPreferencesStore = InMemoryAppPreferencesStore(timelineMediaPreviewValue = MediaPreviewValue.Private)
58+
val room = FakeMatrixRoom(initialRoomInfo = aRoomInfo(joinRule = JoinRule.Public))
59+
val presenter = createPresenter(appPreferencesStore, room)
60+
presenter.test {
61+
skipItems(1)
62+
val initialState = awaitItem()
63+
assertThat(initialState.protectionState).isEqualTo(ProtectionState.RenderOnly(persistentSetOf()))
64+
// ShowContent with null should have no effect.
65+
initialState.eventSink(TimelineProtectionEvent.ShowContent(eventId = null))
66+
initialState.eventSink(TimelineProtectionEvent.ShowContent(eventId = AN_EVENT_ID))
67+
val finalState = awaitItem()
68+
assertThat(finalState.protectionState).isEqualTo(ProtectionState.RenderOnly(persistentSetOf(AN_EVENT_ID)))
69+
}
70+
}
71+
72+
@Test
73+
fun `present - media preview value private in non public room`() = runTest {
74+
val appPreferencesStore = InMemoryAppPreferencesStore(timelineMediaPreviewValue = MediaPreviewValue.Private)
75+
val room = FakeMatrixRoom(initialRoomInfo = aRoomInfo(joinRule = JoinRule.Invite))
76+
val presenter = createPresenter(appPreferencesStore, room)
77+
presenter.test {
78+
val initialState = awaitItem()
79+
assertThat(initialState.protectionState).isEqualTo(ProtectionState.RenderAll)
80+
// ShowContent with null should have no effect.
81+
initialState.eventSink(TimelineProtectionEvent.ShowContent(eventId = null))
82+
initialState.eventSink(TimelineProtectionEvent.ShowContent(eventId = AN_EVENT_ID))
83+
}
84+
}
85+
5086
private fun createPresenter(
5187
appPreferencesStore: AppPreferencesStore = InMemoryAppPreferencesStore(),
88+
room: MatrixRoom = FakeMatrixRoom(),
5289
) = TimelineProtectionPresenter(
5390
appPreferencesStore = appPreferencesStore,
91+
room = room,
5492
)
5593
}

features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsEvents.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
package io.element.android.features.preferences.impl.advanced
99

1010
import io.element.android.compound.theme.Theme
11+
import io.element.android.libraries.matrix.api.media.MediaPreviewValue
1112

1213
sealed interface AdvancedSettingsEvents {
1314
data class SetDeveloperModeEnabled(val enabled: Boolean) : AdvancedSettingsEvents
@@ -16,4 +17,6 @@ sealed interface AdvancedSettingsEvents {
1617
data object ChangeTheme : AdvancedSettingsEvents
1718
data object CancelChangeTheme : AdvancedSettingsEvents
1819
data class SetTheme(val theme: Theme) : AdvancedSettingsEvents
20+
data class SetTimelineMediaPreviewValue(val value: MediaPreviewValue) : AdvancedSettingsEvents
21+
data class SetHideInviteAvatars(val value: Boolean) : AdvancedSettingsEvents
1922
}

0 commit comments

Comments
 (0)