mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-11-04 10:32:34 +00:00
Merge pull request #7372 from hayaksi1/fix/4893-copy-blocked-user-id
Copy a blocked user's Matrix ID by long pressing them
This commit is contained in:
+1
@@ -12,6 +12,7 @@ import io.element.android.libraries.matrix.api.core.UserId
|
|||||||
|
|
||||||
sealed interface BlockedUsersEvents {
|
sealed interface BlockedUsersEvents {
|
||||||
data class Unblock(val userId: UserId) : BlockedUsersEvents
|
data class Unblock(val userId: UserId) : BlockedUsersEvents
|
||||||
|
data class CopyToClipboard(val userId: UserId) : BlockedUsersEvents
|
||||||
data object ConfirmUnblock : BlockedUsersEvents
|
data object ConfirmUnblock : BlockedUsersEvents
|
||||||
data object Cancel : BlockedUsersEvents
|
data object Cancel : BlockedUsersEvents
|
||||||
}
|
}
|
||||||
|
|||||||
+14
@@ -18,14 +18,19 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import dev.zacsweers.metro.Inject
|
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.AsyncAction
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.architecture.runUpdatingState
|
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.FeatureFlagService
|
||||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -34,6 +39,8 @@ import kotlinx.coroutines.launch
|
|||||||
class BlockedUsersPresenter(
|
class BlockedUsersPresenter(
|
||||||
private val matrixClient: MatrixClient,
|
private val matrixClient: MatrixClient,
|
||||||
private val featureFlagService: FeatureFlagService,
|
private val featureFlagService: FeatureFlagService,
|
||||||
|
private val clipboardHelper: ClipboardHelper,
|
||||||
|
private val snackbarDispatcher: SnackbarDispatcher,
|
||||||
) : Presenter<BlockedUsersState> {
|
) : Presenter<BlockedUsersState> {
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): BlockedUsersState {
|
override fun present(): BlockedUsersState {
|
||||||
@@ -65,12 +72,18 @@ class BlockedUsersPresenter(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val snackbarMessage by snackbarDispatcher.collectSnackbarMessageAsState()
|
||||||
|
|
||||||
fun handleEvent(event: BlockedUsersEvents) {
|
fun handleEvent(event: BlockedUsersEvents) {
|
||||||
when (event) {
|
when (event) {
|
||||||
is BlockedUsersEvents.Unblock -> {
|
is BlockedUsersEvents.Unblock -> {
|
||||||
pendingUserToUnblock = event.userId
|
pendingUserToUnblock = event.userId
|
||||||
unblockUserAction.value = AsyncAction.ConfirmingNoParams
|
unblockUserAction.value = AsyncAction.ConfirmingNoParams
|
||||||
}
|
}
|
||||||
|
is BlockedUsersEvents.CopyToClipboard -> {
|
||||||
|
clipboardHelper.copyPlainText(event.userId.value)
|
||||||
|
snackbarDispatcher.post(SnackbarMessage(CommonStrings.common_copied_to_clipboard))
|
||||||
|
}
|
||||||
BlockedUsersEvents.ConfirmUnblock -> {
|
BlockedUsersEvents.ConfirmUnblock -> {
|
||||||
pendingUserToUnblock?.let {
|
pendingUserToUnblock?.let {
|
||||||
coroutineScope.unblockUser(it, unblockUserAction)
|
coroutineScope.unblockUser(it, unblockUserAction)
|
||||||
@@ -86,6 +99,7 @@ class BlockedUsersPresenter(
|
|||||||
return BlockedUsersState(
|
return BlockedUsersState(
|
||||||
blockedUsers = ignoredMatrixUser.toImmutableList(),
|
blockedUsers = ignoredMatrixUser.toImmutableList(),
|
||||||
unblockUserAction = unblockUserAction.value,
|
unblockUserAction = unblockUserAction.value,
|
||||||
|
snackbarMessage = snackbarMessage,
|
||||||
eventSink = ::handleEvent,
|
eventSink = ::handleEvent,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+2
@@ -9,11 +9,13 @@
|
|||||||
package io.element.android.features.preferences.impl.blockedusers
|
package io.element.android.features.preferences.impl.blockedusers
|
||||||
|
|
||||||
import io.element.android.libraries.architecture.AsyncAction
|
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.api.user.MatrixUser
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
data class BlockedUsersState(
|
data class BlockedUsersState(
|
||||||
val blockedUsers: ImmutableList<MatrixUser>,
|
val blockedUsers: ImmutableList<MatrixUser>,
|
||||||
val unblockUserAction: AsyncAction<Unit>,
|
val unblockUserAction: AsyncAction<Unit>,
|
||||||
|
val snackbarMessage: SnackbarMessage?,
|
||||||
val eventSink: (BlockedUsersEvents) -> Unit,
|
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 androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
import io.element.android.libraries.architecture.AsyncAction
|
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.api.user.MatrixUser
|
||||||
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
|
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
@@ -30,11 +31,13 @@ class BlockedUsersStateProvider : PreviewParameterProvider<BlockedUsersState> {
|
|||||||
internal fun aBlockedUsersState(
|
internal fun aBlockedUsersState(
|
||||||
blockedUsers: List<MatrixUser> = aMatrixUserList(),
|
blockedUsers: List<MatrixUser> = aMatrixUserList(),
|
||||||
unblockUserAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
unblockUserAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||||
|
snackbarMessage: SnackbarMessage? = null,
|
||||||
eventSink: (BlockedUsersEvents) -> Unit = {},
|
eventSink: (BlockedUsersEvents) -> Unit = {},
|
||||||
): BlockedUsersState {
|
): BlockedUsersState {
|
||||||
return BlockedUsersState(
|
return BlockedUsersState(
|
||||||
blockedUsers = blockedUsers.toImmutableList(),
|
blockedUsers = blockedUsers.toImmutableList(),
|
||||||
unblockUserAction = unblockUserAction,
|
unblockUserAction = unblockUserAction,
|
||||||
|
snackbarMessage = snackbarMessage,
|
||||||
eventSink = eventSink,
|
eventSink = eventSink,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-4
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
package io.element.android.features.preferences.impl.blockedusers
|
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.Box
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.statusBarsPadding
|
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.preview.PreviewsDayNight
|
||||||
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
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.core.UserId
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
import io.element.android.libraries.matrix.ui.components.MatrixUserRow
|
import io.element.android.libraries.matrix.ui.components.MatrixUserRow
|
||||||
@@ -43,6 +45,7 @@ fun BlockedUsersView(
|
|||||||
onBackClick: () -> Unit,
|
onBackClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
|
val snackbarHostState = rememberSnackbarHostState(snackbarMessage = state.snackbarMessage)
|
||||||
Box(modifier = modifier) {
|
Box(modifier = modifier) {
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
@@ -52,7 +55,8 @@ fun BlockedUsersView(
|
|||||||
BackButton(onClick = onBackClick)
|
BackButton(onClick = onBackClick)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
},
|
||||||
|
snackbarHost = { SnackbarHost(snackbarHostState) },
|
||||||
) { padding ->
|
) { padding ->
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier.padding(padding)
|
modifier = Modifier.padding(padding)
|
||||||
@@ -60,7 +64,8 @@ fun BlockedUsersView(
|
|||||||
items(state.blockedUsers) { matrixUser ->
|
items(state.blockedUsers) { matrixUser ->
|
||||||
BlockedUserItem(
|
BlockedUserItem(
|
||||||
matrixUser = matrixUser,
|
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(
|
private fun BlockedUserItem(
|
||||||
matrixUser: MatrixUser,
|
matrixUser: MatrixUser,
|
||||||
onClick: (UserId) -> Unit,
|
onClick: (UserId) -> Unit,
|
||||||
|
onLongClick: (UserId) -> Unit,
|
||||||
) {
|
) {
|
||||||
MatrixUserRow(
|
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,
|
matrixUser = matrixUser,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+16
@@ -13,8 +13,10 @@ package io.element.android.features.preferences.impl.blockedusers
|
|||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.compose.ui.test.AndroidComposeUiTest
|
import androidx.compose.ui.test.AndroidComposeUiTest
|
||||||
import androidx.compose.ui.test.ExperimentalTestApi
|
import androidx.compose.ui.test.ExperimentalTestApi
|
||||||
|
import androidx.compose.ui.test.longClick
|
||||||
import androidx.compose.ui.test.onNodeWithText
|
import androidx.compose.ui.test.onNodeWithText
|
||||||
import androidx.compose.ui.test.performClick
|
import androidx.compose.ui.test.performClick
|
||||||
|
import androidx.compose.ui.test.performTouchInput
|
||||||
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
|
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
|
||||||
import io.element.android.features.preferences.impl.R
|
import io.element.android.features.preferences.impl.R
|
||||||
import io.element.android.libraries.architecture.AsyncAction
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
@@ -57,6 +59,20 @@ class BlockedUserViewTest : RobolectricTest() {
|
|||||||
eventsRecorder.assertSingle(BlockedUsersEvents.Unblock(userList.first().userId))
|
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
|
@Test
|
||||||
fun `clicking on cancel sends a BlockedUsersEvents`() = runAndroidComposeUiTest {
|
fun `clicking on cancel sends a BlockedUsersEvents`() = runAndroidComposeUiTest {
|
||||||
val eventsRecorder = EventsRecorder<BlockedUsersEvents>()
|
val eventsRecorder = EventsRecorder<BlockedUsersEvents>()
|
||||||
|
|||||||
+30
@@ -12,7 +12,9 @@ import app.cash.molecule.RecompositionMode
|
|||||||
import app.cash.molecule.moleculeFlow
|
import app.cash.molecule.moleculeFlow
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth.assertThat
|
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.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.FeatureFlagService
|
||||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||||
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
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
|
||||||
import io.element.android.libraries.matrix.test.A_USER_ID_2
|
import io.element.android.libraries.matrix.test.A_USER_ID_2
|
||||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.test.runTest
|
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
|
@Test
|
||||||
fun `present - confirm unblock without a pending blocked user does nothing`() = runTest {
|
fun `present - confirm unblock without a pending blocked user does nothing`() = runTest {
|
||||||
val presenter = aBlockedUsersPresenter()
|
val presenter = aBlockedUsersPresenter()
|
||||||
@@ -183,8 +209,12 @@ class BlockedUsersPresenterTest {
|
|||||||
private fun aBlockedUsersPresenter(
|
private fun aBlockedUsersPresenter(
|
||||||
matrixClient: FakeMatrixClient = FakeMatrixClient(),
|
matrixClient: FakeMatrixClient = FakeMatrixClient(),
|
||||||
featureFlagService: FeatureFlagService = FakeFeatureFlagService(),
|
featureFlagService: FeatureFlagService = FakeFeatureFlagService(),
|
||||||
|
clipboardHelper: FakeClipboardHelper = FakeClipboardHelper(),
|
||||||
|
snackbarDispatcher: SnackbarDispatcher = SnackbarDispatcher(),
|
||||||
) = BlockedUsersPresenter(
|
) = BlockedUsersPresenter(
|
||||||
matrixClient = matrixClient,
|
matrixClient = matrixClient,
|
||||||
featureFlagService = featureFlagService,
|
featureFlagService = featureFlagService,
|
||||||
|
clipboardHelper = clipboardHelper,
|
||||||
|
snackbarDispatcher = snackbarDispatcher,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user