Merge pull request #2930 from element-hq/feature/bma/blockedUserData
Render blocked user data (behind a disabled feature flag)
This commit is contained in:
1
changelog.d/2930.misc
Normal file
1
changelog.d/2930.misc
Normal file
@@ -0,0 +1 @@
|
||||
Add a feature flag ShowBlockedUsersDetails, disabled by default to render display name and avatar of blocked users in the blocked users list.
|
||||
@@ -21,20 +21,26 @@ import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.architecture.runUpdatingState
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
class BlockedUsersPresenter @Inject constructor(
|
||||
private val matrixClient: MatrixClient,
|
||||
private val featureFlagService: FeatureFlagService,
|
||||
) : Presenter<BlockedUsersState> {
|
||||
@Composable
|
||||
override fun present(): BlockedUsersState {
|
||||
@@ -47,7 +53,24 @@ class BlockedUsersPresenter @Inject constructor(
|
||||
mutableStateOf(AsyncAction.Uninitialized)
|
||||
}
|
||||
|
||||
val renderBlockedUsersDetail = featureFlagService
|
||||
.isFeatureEnabledFlow(FeatureFlags.ShowBlockedUsersDetails)
|
||||
.collectAsState(initial = false)
|
||||
val ignoredUserIds by matrixClient.ignoredUsersFlow.collectAsState()
|
||||
val ignoredMatrixUser by produceState(
|
||||
initialValue = ignoredUserIds.map { MatrixUser(userId = it) },
|
||||
key1 = renderBlockedUsersDetail.value,
|
||||
key2 = ignoredUserIds
|
||||
) {
|
||||
value = ignoredUserIds.map {
|
||||
if (renderBlockedUsersDetail.value) {
|
||||
matrixClient.getProfile(it).getOrNull()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
?: MatrixUser(userId = it)
|
||||
}
|
||||
}
|
||||
|
||||
fun handleEvents(event: BlockedUsersEvents) {
|
||||
when (event) {
|
||||
@@ -68,7 +91,7 @@ class BlockedUsersPresenter @Inject constructor(
|
||||
}
|
||||
}
|
||||
return BlockedUsersState(
|
||||
blockedUsers = ignoredUserIds,
|
||||
blockedUsers = ignoredMatrixUser.toPersistentList(),
|
||||
unblockUserAction = unblockUserAction.value,
|
||||
eventSink = ::handleEvents
|
||||
)
|
||||
|
||||
@@ -17,11 +17,11 @@
|
||||
package io.element.android.features.preferences.impl.blockedusers
|
||||
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
data class BlockedUsersState(
|
||||
val blockedUsers: ImmutableList<UserId>,
|
||||
val blockedUsers: ImmutableList<MatrixUser>,
|
||||
val unblockUserAction: AsyncAction<Unit>,
|
||||
val eventSink: (BlockedUsersEvents) -> Unit,
|
||||
)
|
||||
|
||||
@@ -18,7 +18,7 @@ package io.element.android.features.preferences.impl.blockedusers
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
|
||||
@@ -26,10 +26,9 @@ class BlockedUsersStatePreviewProvider : PreviewParameterProvider<BlockedUsersSt
|
||||
override val values: Sequence<BlockedUsersState>
|
||||
get() = sequenceOf(
|
||||
aBlockedUsersState(),
|
||||
aBlockedUsersState(blockedUsers = aMatrixUserList().map { it.copy(displayName = null, avatarUrl = null) }),
|
||||
aBlockedUsersState(blockedUsers = emptyList()),
|
||||
aBlockedUsersState(unblockUserAction = AsyncAction.Confirming),
|
||||
// Sadly there's no good way to preview Loading or Failure states since they're presented with an animation
|
||||
// All these 3 screen states will be displayed as the Uninitialized one
|
||||
aBlockedUsersState(unblockUserAction = AsyncAction.Loading),
|
||||
aBlockedUsersState(unblockUserAction = AsyncAction.Failure(Throwable("Failed to unblock user"))),
|
||||
aBlockedUsersState(unblockUserAction = AsyncAction.Success(Unit)),
|
||||
@@ -37,12 +36,13 @@ class BlockedUsersStatePreviewProvider : PreviewParameterProvider<BlockedUsersSt
|
||||
}
|
||||
|
||||
internal fun aBlockedUsersState(
|
||||
blockedUsers: List<UserId> = aMatrixUserList().map { it.userId },
|
||||
blockedUsers: List<MatrixUser> = aMatrixUserList(),
|
||||
unblockUserAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||
eventSink: (BlockedUsersEvents) -> Unit = {},
|
||||
): BlockedUsersState {
|
||||
return BlockedUsersState(
|
||||
blockedUsers = blockedUsers.toPersistentList(),
|
||||
unblockUserAction = unblockUserAction,
|
||||
eventSink = {},
|
||||
eventSink = eventSink,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -73,9 +73,9 @@ fun BlockedUsersView(
|
||||
LazyColumn(
|
||||
modifier = Modifier.padding(padding)
|
||||
) {
|
||||
items(state.blockedUsers) { userId ->
|
||||
items(state.blockedUsers) { matrixUser ->
|
||||
BlockedUserItem(
|
||||
userId = userId,
|
||||
matrixUser = matrixUser,
|
||||
onClick = { state.eventSink(BlockedUsersEvents.Unblock(it)) }
|
||||
)
|
||||
}
|
||||
@@ -121,12 +121,12 @@ fun BlockedUsersView(
|
||||
|
||||
@Composable
|
||||
private fun BlockedUserItem(
|
||||
userId: UserId,
|
||||
matrixUser: MatrixUser,
|
||||
onClick: (UserId) -> Unit,
|
||||
) {
|
||||
MatrixUserRow(
|
||||
modifier = Modifier.clickable { onClick(userId) },
|
||||
matrixUser = MatrixUser(userId),
|
||||
modifier = Modifier.clickable { onClick(matrixUser.userId) },
|
||||
matrixUser = matrixUser,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.preferences.impl.blockedusers
|
||||
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
|
||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.performClick
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import io.element.android.features.preferences.impl.R
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import io.element.android.tests.testutils.EnsureNeverCalled
|
||||
import io.element.android.tests.testutils.EventsRecorder
|
||||
import io.element.android.tests.testutils.clickOn
|
||||
import io.element.android.tests.testutils.ensureCalledOnce
|
||||
import io.element.android.tests.testutils.pressBack
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.rules.TestRule
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class BlockedUserViewTest {
|
||||
@get:Rule
|
||||
val rule = createAndroidComposeRule<ComponentActivity>()
|
||||
|
||||
@Test
|
||||
fun `clicking on back invokes back callback`() {
|
||||
val eventsRecorder = EventsRecorder<BlockedUsersEvents>(expectEvents = false)
|
||||
ensureCalledOnce { callback ->
|
||||
rule.setLogoutView(
|
||||
aBlockedUsersState(
|
||||
eventSink = eventsRecorder
|
||||
),
|
||||
onBackClicked = callback,
|
||||
)
|
||||
rule.pressBack()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clicking on a user emits the expected Event`() {
|
||||
val eventsRecorder = EventsRecorder<BlockedUsersEvents>()
|
||||
val userList = aMatrixUserList()
|
||||
rule.setLogoutView(
|
||||
aBlockedUsersState(
|
||||
blockedUsers = userList,
|
||||
eventSink = eventsRecorder
|
||||
),
|
||||
)
|
||||
rule.onNodeWithText(userList.first().displayName.orEmpty()).performClick()
|
||||
eventsRecorder.assertSingle(BlockedUsersEvents.Unblock(userList.first().userId))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clicking on cancel sends a BlockedUsersEvents`() {
|
||||
val eventsRecorder = EventsRecorder<BlockedUsersEvents>()
|
||||
rule.setLogoutView(
|
||||
aBlockedUsersState(
|
||||
unblockUserAction = AsyncAction.Confirming,
|
||||
eventSink = eventsRecorder
|
||||
),
|
||||
)
|
||||
rule.clickOn(CommonStrings.action_cancel)
|
||||
eventsRecorder.assertSingle(BlockedUsersEvents.Cancel)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clicking on confirm sends a BlockedUsersEvents`() {
|
||||
val eventsRecorder = EventsRecorder<BlockedUsersEvents>()
|
||||
rule.setLogoutView(
|
||||
aBlockedUsersState(
|
||||
unblockUserAction = AsyncAction.Confirming,
|
||||
eventSink = eventsRecorder
|
||||
),
|
||||
)
|
||||
rule.clickOn(R.string.screen_blocked_users_unblock_alert_action)
|
||||
eventsRecorder.assertSingle(BlockedUsersEvents.ConfirmUnblock)
|
||||
}
|
||||
}
|
||||
|
||||
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setLogoutView(
|
||||
state: BlockedUsersState,
|
||||
onBackClicked: () -> Unit = EnsureNeverCalled(),
|
||||
) {
|
||||
setContent {
|
||||
BlockedUsersView(
|
||||
state = state,
|
||||
onBackPressed = onBackClicked,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,11 @@ import app.cash.molecule.moleculeFlow
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.matrix.test.AN_EXCEPTION
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID_2
|
||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||
@@ -52,7 +57,7 @@ class BlockedUsersPresenterTests {
|
||||
presenter.present()
|
||||
}.test {
|
||||
with(awaitItem()) {
|
||||
assertThat(blockedUsers).isEqualTo(persistentListOf(A_USER_ID))
|
||||
assertThat(blockedUsers).isEqualTo(persistentListOf(MatrixUser(A_USER_ID)))
|
||||
assertThat(unblockUserAction).isEqualTo(AsyncAction.Uninitialized)
|
||||
}
|
||||
}
|
||||
@@ -68,14 +73,39 @@ class BlockedUsersPresenterTests {
|
||||
presenter.present()
|
||||
}.test {
|
||||
with(awaitItem()) {
|
||||
assertThat(blockedUsers).containsAtLeastElementsIn(persistentListOf(A_USER_ID))
|
||||
assertThat(unblockUserAction).isEqualTo(AsyncAction.Uninitialized)
|
||||
assertThat(blockedUsers).isEqualTo(listOf(MatrixUser(A_USER_ID)))
|
||||
}
|
||||
|
||||
matrixClient.ignoredUsersFlow.value = persistentListOf(A_USER_ID, A_USER_ID_2)
|
||||
skipItems(1)
|
||||
with(awaitItem()) {
|
||||
assertThat(blockedUsers).isEqualTo(persistentListOf(A_USER_ID, A_USER_ID_2))
|
||||
assertThat(unblockUserAction).isEqualTo(AsyncAction.Uninitialized)
|
||||
assertThat(blockedUsers).isEqualTo(listOf(MatrixUser(A_USER_ID), MatrixUser(A_USER_ID_2)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - blocked users list with data`() = runTest {
|
||||
val alice = MatrixUser(A_USER_ID, displayName = "Alice", avatarUrl = "aliceAvatar")
|
||||
val matrixClient = FakeMatrixClient().apply {
|
||||
ignoredUsersFlow.value = persistentListOf(A_USER_ID, A_USER_ID_2)
|
||||
givenGetProfileResult(A_USER_ID, Result.success(alice))
|
||||
givenGetProfileResult(A_USER_ID_2, Result.failure(AN_EXCEPTION))
|
||||
}
|
||||
val presenter = aBlockedUsersPresenter(
|
||||
matrixClient = matrixClient,
|
||||
featureFlagService = FakeFeatureFlagService().apply {
|
||||
setFeatureEnabled(FeatureFlags.ShowBlockedUsersDetails, true)
|
||||
}
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
with(awaitItem()) {
|
||||
assertThat(blockedUsers).isEqualTo(listOf(MatrixUser(A_USER_ID), MatrixUser(A_USER_ID_2)))
|
||||
}
|
||||
// Alice is resolved
|
||||
with(awaitItem()) {
|
||||
assertThat(blockedUsers).isEqualTo(listOf(alice, MatrixUser(A_USER_ID_2)))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -157,5 +187,9 @@ class BlockedUsersPresenterTests {
|
||||
|
||||
private fun aBlockedUsersPresenter(
|
||||
matrixClient: FakeMatrixClient = FakeMatrixClient(),
|
||||
) = BlockedUsersPresenter(matrixClient)
|
||||
featureFlagService: FeatureFlagService = FakeFeatureFlagService(),
|
||||
) = BlockedUsersPresenter(
|
||||
matrixClient = matrixClient,
|
||||
featureFlagService = featureFlagService,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -81,5 +81,12 @@ enum class FeatureFlags(
|
||||
description = "Allow user to search for public rooms in their homeserver",
|
||||
defaultValue = false,
|
||||
isFinished = false,
|
||||
)
|
||||
),
|
||||
ShowBlockedUsersDetails(
|
||||
key = "feature.showBlockedUsersDetails",
|
||||
title = "Show blocked users details",
|
||||
description = "Show the name and avatar of blocked users in the blocked users list",
|
||||
defaultValue = false,
|
||||
isFinished = false,
|
||||
),
|
||||
}
|
||||
|
||||
@@ -41,6 +41,7 @@ class StaticFeatureFlagProvider @Inject constructor() :
|
||||
FeatureFlags.Mentions -> true
|
||||
FeatureFlags.MarkAsUnread -> true
|
||||
FeatureFlags.RoomDirectorySearch -> false
|
||||
FeatureFlags.ShowBlockedUsersDetails -> false
|
||||
}
|
||||
} else {
|
||||
false
|
||||
|
||||
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