diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUsersEvents.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUsersEvents.kt index 0db5c4c576..31e516e730 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUsersEvents.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUsersEvents.kt @@ -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 } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUsersPresenter.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUsersPresenter.kt index fc150d0bfc..2944089fa1 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUsersPresenter.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUsersPresenter.kt @@ -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 { @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, ) } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUsersState.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUsersState.kt index 8c478371ad..e4b07386c7 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUsersState.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUsersState.kt @@ -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, val unblockUserAction: AsyncAction, + val snackbarMessage: SnackbarMessage?, val eventSink: (BlockedUsersEvents) -> Unit, ) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUsersStateProvider.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUsersStateProvider.kt index 92cc8a0936..0e0e9e5ca5 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUsersStateProvider.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUsersStateProvider.kt @@ -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 { internal fun aBlockedUsersState( blockedUsers: List = aMatrixUserList(), unblockUserAction: AsyncAction = AsyncAction.Uninitialized, + snackbarMessage: SnackbarMessage? = null, eventSink: (BlockedUsersEvents) -> Unit = {}, ): BlockedUsersState { return BlockedUsersState( blockedUsers = blockedUsers.toImmutableList(), unblockUserAction = unblockUserAction, + snackbarMessage = snackbarMessage, eventSink = eventSink, ) } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUsersView.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUsersView.kt index 40e58022bd..c4d8387295 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUsersView.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUsersView.kt @@ -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, ) } diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUserViewTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUserViewTest.kt index 467ace4675..58e0cf467f 100644 --- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUserViewTest.kt +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUserViewTest.kt @@ -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() + 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() diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUsersPresenterTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUsersPresenterTest.kt index d3ac29df9a..4e0164b2a0 100644 --- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUsersPresenterTest.kt +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUsersPresenterTest.kt @@ -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, ) }