mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-11-02 12:07:28 +00:00
Copy a blocked user's Matrix ID by long pressing them
The blocked users list displayed each Matrix ID but offered no way to get it out of the app, which is what someone who blocked a user for abuse needs in order to report them onwards. Long press now copies the Matrix ID and confirms with a snackbar, reusing the ClipboardHelper and SnackbarDispatcher idiom from the user profile screen. A short tap still opens the unblock confirmation. The row declares a long click label so the action is announced by a screen reader. No new string is needed.
This commit is contained in:
+1
@@ -12,6 +12,7 @@ import io.element.android.libraries.matrix.api.core.UserId
|
||||
|
||||
sealed interface BlockedUsersEvents {
|
||||
data class Unblock(val userId: UserId) : BlockedUsersEvents
|
||||
data class CopyToClipboard(val userId: UserId) : BlockedUsersEvents
|
||||
data object ConfirmUnblock : BlockedUsersEvents
|
||||
data object Cancel : BlockedUsersEvents
|
||||
}
|
||||
|
||||
+14
@@ -18,14 +18,19 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.libraries.androidutils.clipboard.ClipboardHelper
|
||||
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.designsystem.utils.snackbar.SnackbarDispatcher
|
||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
|
||||
import io.element.android.libraries.designsystem.utils.snackbar.collectSnackbarMessageAsState
|
||||
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 io.element.android.libraries.ui.strings.CommonStrings
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -34,6 +39,8 @@ import kotlinx.coroutines.launch
|
||||
class BlockedUsersPresenter(
|
||||
private val matrixClient: MatrixClient,
|
||||
private val featureFlagService: FeatureFlagService,
|
||||
private val clipboardHelper: ClipboardHelper,
|
||||
private val snackbarDispatcher: SnackbarDispatcher,
|
||||
) : Presenter<BlockedUsersState> {
|
||||
@Composable
|
||||
override fun present(): BlockedUsersState {
|
||||
@@ -65,12 +72,18 @@ class BlockedUsersPresenter(
|
||||
}
|
||||
}
|
||||
|
||||
val snackbarMessage by snackbarDispatcher.collectSnackbarMessageAsState()
|
||||
|
||||
fun handleEvent(event: BlockedUsersEvents) {
|
||||
when (event) {
|
||||
is BlockedUsersEvents.Unblock -> {
|
||||
pendingUserToUnblock = event.userId
|
||||
unblockUserAction.value = AsyncAction.ConfirmingNoParams
|
||||
}
|
||||
is BlockedUsersEvents.CopyToClipboard -> {
|
||||
clipboardHelper.copyPlainText(event.userId.value)
|
||||
snackbarDispatcher.post(SnackbarMessage(CommonStrings.common_copied_to_clipboard))
|
||||
}
|
||||
BlockedUsersEvents.ConfirmUnblock -> {
|
||||
pendingUserToUnblock?.let {
|
||||
coroutineScope.unblockUser(it, unblockUserAction)
|
||||
@@ -86,6 +99,7 @@ class BlockedUsersPresenter(
|
||||
return BlockedUsersState(
|
||||
blockedUsers = ignoredMatrixUser.toImmutableList(),
|
||||
unblockUserAction = unblockUserAction.value,
|
||||
snackbarMessage = snackbarMessage,
|
||||
eventSink = ::handleEvent,
|
||||
)
|
||||
}
|
||||
|
||||
+2
@@ -9,11 +9,13 @@
|
||||
package io.element.android.features.preferences.impl.blockedusers
|
||||
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
data class BlockedUsersState(
|
||||
val blockedUsers: ImmutableList<MatrixUser>,
|
||||
val unblockUserAction: AsyncAction<Unit>,
|
||||
val snackbarMessage: SnackbarMessage?,
|
||||
val eventSink: (BlockedUsersEvents) -> Unit,
|
||||
)
|
||||
|
||||
+3
@@ -10,6 +10,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.designsystem.utils.snackbar.SnackbarMessage
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
@@ -30,11 +31,13 @@ class BlockedUsersStateProvider : PreviewParameterProvider<BlockedUsersState> {
|
||||
internal fun aBlockedUsersState(
|
||||
blockedUsers: List<MatrixUser> = aMatrixUserList(),
|
||||
unblockUserAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||
snackbarMessage: SnackbarMessage? = null,
|
||||
eventSink: (BlockedUsersEvents) -> Unit = {},
|
||||
): BlockedUsersState {
|
||||
return BlockedUsersState(
|
||||
blockedUsers = blockedUsers.toImmutableList(),
|
||||
unblockUserAction = unblockUserAction,
|
||||
snackbarMessage = snackbarMessage,
|
||||
eventSink = eventSink,
|
||||
)
|
||||
}
|
||||
|
||||
+14
-4
@@ -8,7 +8,7 @@
|
||||
|
||||
package io.element.android.features.preferences.impl.blockedusers
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.statusBarsPadding
|
||||
@@ -31,6 +31,8 @@ import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarHost
|
||||
import io.element.android.libraries.designsystem.utils.snackbar.rememberSnackbarHostState
|
||||
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.MatrixUserRow
|
||||
@@ -43,6 +45,7 @@ fun BlockedUsersView(
|
||||
onBackClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val snackbarHostState = rememberSnackbarHostState(snackbarMessage = state.snackbarMessage)
|
||||
Box(modifier = modifier) {
|
||||
Scaffold(
|
||||
topBar = {
|
||||
@@ -52,7 +55,8 @@ fun BlockedUsersView(
|
||||
BackButton(onClick = onBackClick)
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
snackbarHost = { SnackbarHost(snackbarHostState) },
|
||||
) { padding ->
|
||||
LazyColumn(
|
||||
modifier = Modifier.padding(padding)
|
||||
@@ -60,7 +64,8 @@ fun BlockedUsersView(
|
||||
items(state.blockedUsers) { matrixUser ->
|
||||
BlockedUserItem(
|
||||
matrixUser = matrixUser,
|
||||
onClick = { state.eventSink(BlockedUsersEvents.Unblock(it)) }
|
||||
onClick = { state.eventSink(BlockedUsersEvents.Unblock(it)) },
|
||||
onLongClick = { state.eventSink(BlockedUsersEvents.CopyToClipboard(it)) },
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -107,9 +112,14 @@ fun BlockedUsersView(
|
||||
private fun BlockedUserItem(
|
||||
matrixUser: MatrixUser,
|
||||
onClick: (UserId) -> Unit,
|
||||
onLongClick: (UserId) -> Unit,
|
||||
) {
|
||||
MatrixUserRow(
|
||||
modifier = Modifier.clickable { onClick(matrixUser.userId) },
|
||||
modifier = Modifier.combinedClickable(
|
||||
onClick = { onClick(matrixUser.userId) },
|
||||
onLongClick = { onLongClick(matrixUser.userId) },
|
||||
onLongClickLabel = stringResource(CommonStrings.action_copy),
|
||||
),
|
||||
matrixUser = matrixUser,
|
||||
)
|
||||
}
|
||||
|
||||
+16
@@ -13,8 +13,10 @@ package io.element.android.features.preferences.impl.blockedusers
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.ui.test.AndroidComposeUiTest
|
||||
import androidx.compose.ui.test.ExperimentalTestApi
|
||||
import androidx.compose.ui.test.longClick
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.performClick
|
||||
import androidx.compose.ui.test.performTouchInput
|
||||
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
|
||||
import io.element.android.features.preferences.impl.R
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
@@ -57,6 +59,20 @@ class BlockedUserViewTest : RobolectricTest() {
|
||||
eventsRecorder.assertSingle(BlockedUsersEvents.Unblock(userList.first().userId))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `long clicking on a user emits the expected Event`() = runAndroidComposeUiTest {
|
||||
val eventsRecorder = EventsRecorder<BlockedUsersEvents>()
|
||||
val userList = aMatrixUserList()
|
||||
setBlockedUsersView(
|
||||
aBlockedUsersState(
|
||||
blockedUsers = userList,
|
||||
eventSink = eventsRecorder
|
||||
),
|
||||
)
|
||||
onNodeWithText(userList.first().displayName.orEmpty()).performTouchInput { longClick() }
|
||||
eventsRecorder.assertSingle(BlockedUsersEvents.CopyToClipboard(userList.first().userId))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clicking on cancel sends a BlockedUsersEvents`() = runAndroidComposeUiTest {
|
||||
val eventsRecorder = EventsRecorder<BlockedUsersEvents>()
|
||||
|
||||
+30
@@ -12,7 +12,9 @@ import app.cash.molecule.RecompositionMode
|
||||
import app.cash.molecule.moleculeFlow
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.androidutils.clipboard.FakeClipboardHelper
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
||||
@@ -21,6 +23,7 @@ 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
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.test.runTest
|
||||
@@ -169,6 +172,29 @@ class BlockedUsersPresenterTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - copy user id to clipboard`() = runTest {
|
||||
val clipboardHelper = FakeClipboardHelper()
|
||||
val matrixClient = FakeMatrixClient(
|
||||
ignoredUsersFlow = MutableStateFlow(persistentListOf(A_USER_ID))
|
||||
)
|
||||
val presenter = aBlockedUsersPresenter(
|
||||
matrixClient = matrixClient,
|
||||
clipboardHelper = clipboardHelper,
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.snackbarMessage).isNull()
|
||||
initialState.eventSink(BlockedUsersEvents.CopyToClipboard(A_USER_ID))
|
||||
|
||||
assertThat(clipboardHelper.clipboardContents).isEqualTo(A_USER_ID.value)
|
||||
assertThat(awaitItem().snackbarMessage?.messageResId).isEqualTo(CommonStrings.common_copied_to_clipboard)
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - confirm unblock without a pending blocked user does nothing`() = runTest {
|
||||
val presenter = aBlockedUsersPresenter()
|
||||
@@ -183,8 +209,12 @@ class BlockedUsersPresenterTest {
|
||||
private fun aBlockedUsersPresenter(
|
||||
matrixClient: FakeMatrixClient = FakeMatrixClient(),
|
||||
featureFlagService: FeatureFlagService = FakeFeatureFlagService(),
|
||||
clipboardHelper: FakeClipboardHelper = FakeClipboardHelper(),
|
||||
snackbarDispatcher: SnackbarDispatcher = SnackbarDispatcher(),
|
||||
) = BlockedUsersPresenter(
|
||||
matrixClient = matrixClient,
|
||||
featureFlagService = featureFlagService,
|
||||
clipboardHelper = clipboardHelper,
|
||||
snackbarDispatcher = snackbarDispatcher,
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user