Skip to content

Commit

Permalink
Copy user-id/room-alias to clipboard on click
Browse files Browse the repository at this point in the history
Make user id and room alias text in room/user view pages clickable and
copy the text to the clipboard on click.

Fixes #3496

Signed-off-by: Joe Groocock <me@frebib.net>
  • Loading branch information
frebib committed Sep 30, 2024
1 parent 249104b commit c88465c
Show file tree
Hide file tree
Showing 18 changed files with 153 additions and 15 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,6 @@ sealed interface RoomDetailsEvent {
data object LeaveRoom : RoomDetailsEvent
data object MuteNotification : RoomDetailsEvent
data object UnmuteNotification : RoomDetailsEvent
data class CopyID(val text: String) : RoomDetailsEvent

Check warning on line 14 in features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsEvent.kt

View check run for this annotation

Codecov / codecov/patch

features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsEvent.kt#L14

Added line #L14 was not covered by tests
data class SetFavorite(val isFavorite: Boolean) : RoomDetailsEvent
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,13 @@ import io.element.android.features.leaveroom.api.LeaveRoomEvent
import io.element.android.features.leaveroom.api.LeaveRoomPresenter
import io.element.android.features.messages.api.pinned.IsPinnedMessagesFeatureEnabled
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsPresenter
import io.element.android.libraries.androidutils.clipboard.ClipboardHelper
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.bool.orFalse
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
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
Expand All @@ -41,6 +45,7 @@ import io.element.android.libraries.matrix.ui.room.canCall
import io.element.android.libraries.matrix.ui.room.getCurrentRoomMember
import io.element.android.libraries.matrix.ui.room.getDirectRoomMember
import io.element.android.libraries.matrix.ui.room.isOwnUserAdmin
import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.services.analytics.api.AnalyticsService
import io.element.android.services.analyticsproviders.api.trackers.captureInteraction
import kotlinx.collections.immutable.toPersistentList
Expand All @@ -60,6 +65,8 @@ class RoomDetailsPresenter @Inject constructor(
private val dispatchers: CoroutineDispatchers,
private val analyticsService: AnalyticsService,
private val isPinnedMessagesFeatureEnabled: IsPinnedMessagesFeatureEnabled,
private val clipboardHelper: ClipboardHelper,
private val snackbarDispatcher: SnackbarDispatcher,
) : Presenter<RoomDetailsState> {
@Composable
override fun present(): RoomDetailsState {
Expand Down Expand Up @@ -110,6 +117,7 @@ class RoomDetailsPresenter @Inject constructor(
}
}

val snackbarMessage by snackbarDispatcher.collectSnackbarMessageAsState()
val roomNotificationSettingsState by room.roomNotificationSettingsStateFlow.collectAsState()

fun handleEvents(event: RoomDetailsEvent) {
Expand All @@ -126,6 +134,12 @@ class RoomDetailsPresenter @Inject constructor(
client.notificationSettingsService().unmuteRoom(room.roomId, room.isEncrypted, room.isOneToOne)
}
}
is RoomDetailsEvent.CopyID -> {
scope.launch(dispatchers.io) {

Check warning on line 138 in features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt

View check run for this annotation

Codecov / codecov/patch

features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt#L138

Added line #L138 was not covered by tests
clipboardHelper.copyPlainText(event.text)
snackbarDispatcher.post(SnackbarMessage(CommonStrings.common_copied_to_clipboard))
}
}
is RoomDetailsEvent.SetFavorite -> scope.setFavorite(event.isFavorite)
}
}
Expand Down Expand Up @@ -154,6 +168,7 @@ class RoomDetailsPresenter @Inject constructor(
heroes = roomInfo?.heroes.orEmpty().toPersistentList(),
canShowPinnedMessages = canShowPinnedMessages,
pinnedMessagesCount = pinnedMessagesCount,
snackbarMessage = snackbarMessage,
eventSink = ::handleEvents,
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ package io.element.android.features.roomdetails.impl
import androidx.compose.runtime.Immutable
import io.element.android.features.leaveroom.api.LeaveRoomState
import io.element.android.features.userprofile.shared.UserProfileState
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.RoomMember
Expand Down Expand Up @@ -39,6 +40,7 @@ data class RoomDetailsState(
val heroes: ImmutableList<MatrixUser>,
val canShowPinnedMessages: Boolean,
val pinnedMessagesCount: Int?,
val snackbarMessage: SnackbarMessage?,
val eventSink: (RoomDetailsEvent) -> Unit
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import io.element.android.features.leaveroom.api.aLeaveRoomState
import io.element.android.features.roomdetails.impl.members.aRoomMember
import io.element.android.features.userprofile.shared.UserProfileState
import io.element.android.features.userprofile.shared.aUserProfileState
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.UserId
Expand Down Expand Up @@ -99,6 +100,7 @@ fun aRoomDetailsState(
heroes: List<MatrixUser> = emptyList(),
canShowPinnedMessages: Boolean = true,
pinnedMessagesCount: Int? = null,
snackbarMessage: SnackbarMessage? = null,
eventSink: (RoomDetailsEvent) -> Unit = {},
) = RoomDetailsState(
roomId = roomId,
Expand All @@ -122,7 +124,8 @@ fun aRoomDetailsState(
heroes = heroes.toPersistentList(),
canShowPinnedMessages = canShowPinnedMessages,
pinnedMessagesCount = pinnedMessagesCount,
eventSink = eventSink
snackbarMessage = snackbarMessage,
eventSink = eventSink,
)

fun aRoomNotificationSettings(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.MoreVert
Expand All @@ -33,6 +34,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.PreviewParameter
Expand Down Expand Up @@ -71,6 +73,8 @@ import io.element.android.libraries.designsystem.theme.components.Scaffold
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.designsystem.utils.CommonDrawables
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.RoomAlias
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.RoomMember
Expand Down Expand Up @@ -102,6 +106,8 @@ fun RoomDetailsView(
onPinnedMessagesClick: () -> Unit,
modifier: Modifier = Modifier,
) {
val snackbarHostState = rememberSnackbarHostState(snackbarMessage = state.snackbarMessage)

Scaffold(
modifier = modifier,
topBar = {
Expand All @@ -111,6 +117,7 @@ fun RoomDetailsView(
onActionClick = onActionClick
)
},
snackbarHost = { SnackbarHost(snackbarHostState) },
) { padding ->
Column(
modifier = Modifier
Expand All @@ -131,6 +138,9 @@ fun RoomDetailsView(
openAvatarPreview = { avatarUrl ->
openAvatarPreview(state.roomName, avatarUrl)
},
onSubtitleClick = { subtitle ->
state.eventSink(RoomDetailsEvent.CopyID(subtitle))

Check warning on line 142 in features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt

View check run for this annotation

Codecov / codecov/patch

features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt#L142

Added line #L142 was not covered by tests
},
)
}
is RoomDetailsType.Dm -> {
Expand All @@ -141,6 +151,9 @@ fun RoomDetailsView(
openAvatarPreview = { name, avatarUrl ->
openAvatarPreview(name, avatarUrl)
},
onSubtitleClick = { subtitle ->
state.eventSink(RoomDetailsEvent.CopyID(subtitle))

Check warning on line 155 in features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt

View check run for this annotation

Codecov / codecov/patch

features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt#L155

Added line #L155 was not covered by tests
},
)
}
}
Expand Down Expand Up @@ -330,6 +343,7 @@ private fun RoomHeaderSection(
roomAlias: RoomAlias?,
heroes: ImmutableList<MatrixUser>,
openAvatarPreview: (url: String) -> Unit,
onSubtitleClick: (String) -> Unit,
) {
Column(
modifier = Modifier
Expand All @@ -346,7 +360,11 @@ private fun RoomHeaderSection(
.clickable(enabled = avatarUrl != null) { openAvatarPreview(avatarUrl!!) }
.testTag(TestTags.roomDetailAvatar)
)
TitleAndSubtitle(title = roomName, subtitle = roomAlias?.value)
TitleAndSubtitle(
title = roomName,
subtitle = roomAlias?.value,
onSubtitleClick = onSubtitleClick,
)
}
}

Expand All @@ -356,6 +374,7 @@ private fun DmHeaderSection(
otherMember: RoomMember,
roomName: String,
openAvatarPreview: (name: String, url: String) -> Unit,
onSubtitleClick: (String) -> Unit,
modifier: Modifier = Modifier
) {
Column(
Expand All @@ -373,6 +392,7 @@ private fun DmHeaderSection(
TitleAndSubtitle(
title = roomName,
subtitle = otherMember.userId.value,
onSubtitleClick = onSubtitleClick,
)
}
}
Expand All @@ -381,6 +401,7 @@ private fun DmHeaderSection(
private fun ColumnScope.TitleAndSubtitle(
title: String,
subtitle: String?,
onSubtitleClick: (String) -> Unit,
) {
Spacer(modifier = Modifier.height(24.dp))
Text(
Expand All @@ -391,6 +412,10 @@ private fun ColumnScope.TitleAndSubtitle(
if (subtitle != null) {
Spacer(modifier = Modifier.height(6.dp))
Text(
modifier = Modifier
.clip(RoundedCornerShape(4.dp))
.clickable { onSubtitleClick(subtitle) }
.padding(horizontal = 4.dp),
text = subtitle,
style = ElementTheme.typography.fontBodyLgRegular,
color = MaterialTheme.colorScheme.secondary,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ import dagger.Module
import dagger.Provides
import io.element.android.features.createroom.api.StartDMAction
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsPresenter
import io.element.android.libraries.androidutils.clipboard.ClipboardHelper
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.UserId
Expand All @@ -25,10 +28,13 @@ object RoomMemberModule {
matrixClient: MatrixClient,
room: MatrixRoom,
startDMAction: StartDMAction,
dispatchers: CoroutineDispatchers,
clipboardHelper: ClipboardHelper,
snackbarDispatcher: SnackbarDispatcher,
): RoomMemberDetailsPresenter.Factory {
return object : RoomMemberDetailsPresenter.Factory {
override fun create(roomMemberId: UserId): RoomMemberDetailsPresenter {
return RoomMemberDetailsPresenter(roomMemberId, matrixClient, room, startDMAction)
return RoomMemberDetailsPresenter(roomMemberId, matrixClient, room, startDMAction, dispatchers, clipboardHelper, snackbarDispatcher)

Check warning on line 37 in features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/di/RoomMemberModule.kt

View check run for this annotation

Codecov / codecov/patch

features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/di/RoomMemberModule.kt#L37

Added line #L37 was not covered by tests
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,22 @@ import io.element.android.features.userprofile.shared.UserProfileEvents
import io.element.android.features.userprofile.shared.UserProfilePresenterHelper
import io.element.android.features.userprofile.shared.UserProfileState
import io.element.android.features.userprofile.shared.UserProfileState.ConfirmationDialog
import io.element.android.libraries.androidutils.clipboard.ClipboardHelper
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.bool.orFalse
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
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.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.ui.room.getRoomMemberAsState
import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
Expand All @@ -44,6 +50,9 @@ class RoomMemberDetailsPresenter @AssistedInject constructor(
private val client: MatrixClient,
private val room: MatrixRoom,
private val startDMAction: StartDMAction,
private val dispatchers: CoroutineDispatchers,
private val clipboardHelper: ClipboardHelper,
private val snackbarDispatcher: SnackbarDispatcher,
) : Presenter<UserProfileState> {
interface Factory {
fun create(roomMemberId: UserId): RoomMemberDetailsPresenter
Expand All @@ -65,6 +74,7 @@ class RoomMemberDetailsPresenter @AssistedInject constructor(
val isCurrentUser = remember { client.isMe(roomMemberId) }
val dmRoomId by userProfilePresenterHelper.getDmRoomId()
val canCall by userProfilePresenterHelper.getCanCall(dmRoomId)
val snackbarMessage by snackbarDispatcher.collectSnackbarMessageAsState()
LaunchedEffect(Unit) {
client.ignoredUsersFlow
.map { ignoredUsers -> roomMemberId in ignoredUsers }
Expand Down Expand Up @@ -112,6 +122,12 @@ class RoomMemberDetailsPresenter @AssistedInject constructor(
UserProfileEvents.ClearStartDMState -> {
startDmActionState.value = AsyncAction.Uninitialized
}
is UserProfileEvents.CopyID -> {
coroutineScope.launch(dispatchers.io) {

Check warning on line 126 in features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsPresenter.kt

View check run for this annotation

Codecov / codecov/patch

features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsPresenter.kt#L126

Added line #L126 was not covered by tests
clipboardHelper.copyPlainText(event.text)
snackbarDispatcher.post(SnackbarMessage(CommonStrings.common_copied_to_clipboard))
}
}
}
}

Expand Down Expand Up @@ -155,7 +171,8 @@ class RoomMemberDetailsPresenter @AssistedInject constructor(
isCurrentUser = isCurrentUser,
dmRoomId = dmRoomId,
canCall = canCall,
eventSink = ::handleEvents
snackbarMessage = snackbarMessage,
eventSink = ::handleEvents,
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ import io.element.android.features.roomdetails.impl.RoomDetailsType
import io.element.android.features.roomdetails.impl.RoomTopicState
import io.element.android.features.roomdetails.impl.members.aRoomMember
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsPresenter
import io.element.android.libraries.androidutils.clipboard.FakeClipboardHelper
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import io.element.android.libraries.matrix.api.core.UserId
Expand Down Expand Up @@ -77,11 +79,13 @@ class RoomDetailsPresenterTest {
notificationSettingsService: FakeNotificationSettingsService = FakeNotificationSettingsService(),
analyticsService: AnalyticsService = FakeAnalyticsService(),
isPinnedMessagesFeatureEnabled: Boolean = true,
clipboardHelper: FakeClipboardHelper = FakeClipboardHelper(),
snackbarDispatcher: SnackbarDispatcher = SnackbarDispatcher(),
): RoomDetailsPresenter {
val matrixClient = FakeMatrixClient(notificationSettingsService = notificationSettingsService)
val roomMemberDetailsPresenterFactory = object : RoomMemberDetailsPresenter.Factory {
override fun create(roomMemberId: UserId): RoomMemberDetailsPresenter {
return RoomMemberDetailsPresenter(roomMemberId, matrixClient, room, FakeStartDMAction())
return RoomMemberDetailsPresenter(roomMemberId, matrixClient, room, FakeStartDMAction(), dispatchers, clipboardHelper, snackbarDispatcher)
}
}
val featureFlagService = FakeFeatureFlagService(
Expand All @@ -97,6 +101,8 @@ class RoomDetailsPresenterTest {
dispatchers = dispatchers,
isPinnedMessagesFeatureEnabled = { isPinnedMessagesFeatureEnabled },
analyticsService = analyticsService,
clipboardHelper = clipboardHelper,
snackbarDispatcher = snackbarDispatcher,
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@ import io.element.android.features.roomdetails.impl.members.aRoomMember
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsPresenter
import io.element.android.features.userprofile.shared.UserProfileEvents
import io.element.android.features.userprofile.shared.UserProfileState
import io.element.android.libraries.androidutils.clipboard.FakeClipboardHelper
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.room.MatrixRoom
Expand All @@ -31,8 +33,10 @@ import io.element.android.libraries.matrix.test.A_THROWABLE
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.ui.components.aMatrixUser
import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.testCoroutineDispatchers
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
Expand Down Expand Up @@ -332,17 +336,22 @@ class RoomMemberDetailsPresenterTest {
return awaitItem()
}

private fun createRoomMemberDetailsPresenter(
private fun TestScope.createRoomMemberDetailsPresenter(
room: MatrixRoom,
client: MatrixClient = FakeMatrixClient(),
roomMemberId: UserId = UserId("@alice:server.org"),
startDMAction: StartDMAction = FakeStartDMAction()
startDMAction: StartDMAction = FakeStartDMAction(),
clipboardHelper: FakeClipboardHelper = FakeClipboardHelper(),
snackbarDispatcher: SnackbarDispatcher = SnackbarDispatcher(),
): RoomMemberDetailsPresenter {
return RoomMemberDetailsPresenter(
roomMemberId = roomMemberId,
client = client,
room = room,
startDMAction = startDMAction
startDMAction = startDMAction,
dispatchers = testCoroutineDispatchers(),
clipboardHelper = clipboardHelper,
snackbarDispatcher = snackbarDispatcher,
)
}
}
Loading

0 comments on commit c88465c

Please sign in to comment.