Skip to content

Commit ad9997b

Browse files
authored
Click on userId / room alias to copy value to clipboard. (#4549)
1 parent 606910e commit ad9997b

File tree

17 files changed

+114
-22
lines changed

17 files changed

+114
-22
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,6 @@ sealed interface RoomDetailsEvent {
1111
data object LeaveRoom : RoomDetailsEvent
1212
data object MuteNotification : RoomDetailsEvent
1313
data object UnmuteNotification : RoomDetailsEvent
14+
data class CopyToClipboard(val text: String) : RoomDetailsEvent
1415
data class SetFavorite(val isFavorite: Boolean) : RoomDetailsEvent
1516
}

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,12 @@ import io.element.android.features.messages.api.pinned.IsPinnedMessagesFeatureEn
2424
import io.element.android.features.roomcall.api.RoomCallState
2525
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsPresenter
2626
import io.element.android.features.roomdetails.impl.securityandprivacy.permissions.securityAndPrivacyPermissionsAsState
27+
import io.element.android.libraries.androidutils.clipboard.ClipboardHelper
2728
import io.element.android.libraries.architecture.Presenter
2829
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
30+
import io.element.android.libraries.designsystem.utils.snackbar.LocalSnackbarDispatcher
31+
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
32+
import io.element.android.libraries.designsystem.utils.snackbar.collectSnackbarMessageAsState
2933
import io.element.android.libraries.featureflag.api.FeatureFlagService
3034
import io.element.android.libraries.featureflag.api.FeatureFlags
3135
import io.element.android.libraries.matrix.api.MatrixClient
@@ -45,6 +49,7 @@ import io.element.android.libraries.matrix.ui.room.getDirectRoomMember
4549
import io.element.android.libraries.matrix.ui.room.isDmAsState
4650
import io.element.android.libraries.matrix.ui.room.isOwnUserAdmin
4751
import io.element.android.libraries.matrix.ui.room.roomMemberIdentityStateChange
52+
import io.element.android.libraries.ui.strings.CommonStrings
4853
import io.element.android.services.analytics.api.AnalyticsService
4954
import io.element.android.services.analyticsproviders.api.trackers.captureInteraction
5055
import kotlinx.collections.immutable.toPersistentList
@@ -65,6 +70,7 @@ class RoomDetailsPresenter @Inject constructor(
6570
private val dispatchers: CoroutineDispatchers,
6671
private val analyticsService: AnalyticsService,
6772
private val isPinnedMessagesFeatureEnabled: IsPinnedMessagesFeatureEnabled,
73+
private val clipboardHelper: ClipboardHelper,
6874
) : Presenter<RoomDetailsState> {
6975
@Composable
7076
override fun present(): RoomDetailsState {
@@ -134,6 +140,9 @@ class RoomDetailsPresenter @Inject constructor(
134140

135141
val roomNotificationSettingsState by room.roomNotificationSettingsStateFlow.collectAsState()
136142

143+
val snackbarDispatcher = LocalSnackbarDispatcher.current
144+
val snackbarMessage by snackbarDispatcher.collectSnackbarMessageAsState()
145+
137146
fun handleEvents(event: RoomDetailsEvent) {
138147
when (event) {
139148
RoomDetailsEvent.LeaveRoom ->
@@ -149,6 +158,10 @@ class RoomDetailsPresenter @Inject constructor(
149158
}
150159
}
151160
is RoomDetailsEvent.SetFavorite -> scope.setFavorite(event.isFavorite)
161+
is RoomDetailsEvent.CopyToClipboard -> {
162+
clipboardHelper.copyPlainText(event.text)
163+
snackbarDispatcher.post(SnackbarMessage(CommonStrings.common_copied_to_clipboard))
164+
}
152165
}
153166
}
154167

@@ -190,6 +203,7 @@ class RoomDetailsPresenter @Inject constructor(
190203
canShowPinnedMessages = canShowPinnedMessages,
191204
canShowMediaGallery = canShowMediaGallery,
192205
pinnedMessagesCount = pinnedMessagesCount,
206+
snackbarMessage = snackbarMessage,
193207
canShowKnockRequests = canShowKnockRequests,
194208
knockRequestsCount = knockRequestsCount,
195209
canShowSecurityAndPrivacy = canShowSecurityAndPrivacy,

features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsState.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import androidx.compose.runtime.Immutable
1111
import io.element.android.features.leaveroom.api.LeaveRoomState
1212
import io.element.android.features.roomcall.api.RoomCallState
1313
import io.element.android.features.userprofile.api.UserProfileState
14+
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
1415
import io.element.android.libraries.matrix.api.core.RoomAlias
1516
import io.element.android.libraries.matrix.api.core.RoomId
1617
import io.element.android.libraries.matrix.api.room.RoomMember
@@ -42,6 +43,7 @@ data class RoomDetailsState(
4243
val canShowPinnedMessages: Boolean,
4344
val canShowMediaGallery: Boolean,
4445
val pinnedMessagesCount: Int?,
46+
val snackbarMessage: SnackbarMessage?,
4547
val canShowKnockRequests: Boolean,
4648
val knockRequestsCount: Int?,
4749
val canShowSecurityAndPrivacy: Boolean,

features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsStateProvider.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import io.element.android.features.userprofile.api.UserProfileState
1717
import io.element.android.features.userprofile.api.UserProfileVerificationState
1818
import io.element.android.features.userprofile.shared.aUserProfileState
1919
import io.element.android.libraries.architecture.AsyncData
20+
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
2021
import io.element.android.libraries.matrix.api.core.RoomAlias
2122
import io.element.android.libraries.matrix.api.core.RoomId
2223
import io.element.android.libraries.matrix.api.core.UserId
@@ -111,6 +112,7 @@ fun aRoomDetailsState(
111112
canShowPinnedMessages: Boolean = true,
112113
canShowMediaGallery: Boolean = true,
113114
pinnedMessagesCount: Int? = null,
115+
snackbarMessage: SnackbarMessage? = null,
114116
canShowKnockRequests: Boolean = false,
115117
knockRequestsCount: Int? = null,
116118
canShowSecurityAndPrivacy: Boolean = true,
@@ -139,11 +141,12 @@ fun aRoomDetailsState(
139141
canShowPinnedMessages = canShowPinnedMessages,
140142
canShowMediaGallery = canShowMediaGallery,
141143
pinnedMessagesCount = pinnedMessagesCount,
144+
snackbarMessage = snackbarMessage,
142145
canShowKnockRequests = canShowKnockRequests,
143146
knockRequestsCount = knockRequestsCount,
144147
canShowSecurityAndPrivacy = canShowSecurityAndPrivacy,
145148
hasMemberVerificationViolations = hasMemberVerificationViolations,
146-
eventSink = eventSink
149+
eventSink = eventSink,
147150
)
148151

149152
fun aRoomNotificationSettings(

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

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ import io.element.android.libraries.designsystem.components.button.MainActionBut
5555
import io.element.android.libraries.designsystem.components.list.ListItemContent
5656
import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory
5757
import io.element.android.libraries.designsystem.components.preferences.PreferenceSwitch
58+
import io.element.android.libraries.designsystem.modifiers.niceClickable
5859
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
5960
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
6061
import io.element.android.libraries.designsystem.preview.PreviewWithLargeHeight
@@ -69,6 +70,8 @@ import io.element.android.libraries.designsystem.theme.components.ListItemStyle
6970
import io.element.android.libraries.designsystem.theme.components.Scaffold
7071
import io.element.android.libraries.designsystem.theme.components.Text
7172
import io.element.android.libraries.designsystem.theme.components.TopAppBar
73+
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarHost
74+
import io.element.android.libraries.designsystem.utils.snackbar.rememberSnackbarHostState
7275
import io.element.android.libraries.matrix.api.core.RoomAlias
7376
import io.element.android.libraries.matrix.api.core.RoomId
7477
import io.element.android.libraries.matrix.api.core.UserId
@@ -106,6 +109,7 @@ fun RoomDetailsView(
106109
onProfileClick: (UserId) -> Unit,
107110
modifier: Modifier = Modifier,
108111
) {
112+
val snackbarHostState = rememberSnackbarHostState(snackbarMessage = state.snackbarMessage)
109113
Scaffold(
110114
modifier = modifier,
111115
topBar = {
@@ -115,6 +119,7 @@ fun RoomDetailsView(
115119
onActionClick = onActionClick
116120
)
117121
},
122+
snackbarHost = { SnackbarHost(snackbarHostState) },
118123
) { padding ->
119124
Column(
120125
modifier = Modifier
@@ -135,6 +140,9 @@ fun RoomDetailsView(
135140
openAvatarPreview = { avatarUrl ->
136141
openAvatarPreview(state.roomName, avatarUrl)
137142
},
143+
onSubtitleClick = { subtitle ->
144+
state.eventSink(RoomDetailsEvent.CopyToClipboard(subtitle))
145+
}
138146
)
139147
}
140148
is RoomDetailsType.Dm -> {
@@ -145,6 +153,9 @@ fun RoomDetailsView(
145153
openAvatarPreview = { name, avatarUrl ->
146154
openAvatarPreview(name, avatarUrl)
147155
},
156+
onSubtitleClick = { subtitle ->
157+
state.eventSink(RoomDetailsEvent.CopyToClipboard(subtitle))
158+
}
148159
)
149160
}
150161
}
@@ -368,6 +379,7 @@ private fun RoomHeaderSection(
368379
roomAlias: RoomAlias?,
369380
heroes: ImmutableList<MatrixUser>,
370381
openAvatarPreview: (url: String) -> Unit,
382+
onSubtitleClick: (String) -> Unit,
371383
) {
372384
Column(
373385
modifier = Modifier
@@ -384,7 +396,11 @@ private fun RoomHeaderSection(
384396
.clickable(enabled = avatarUrl != null) { openAvatarPreview(avatarUrl!!) }
385397
.testTag(TestTags.roomDetailAvatar)
386398
)
387-
TitleAndSubtitle(title = roomName, subtitle = roomAlias?.value)
399+
TitleAndSubtitle(
400+
title = roomName,
401+
subtitle = roomAlias?.value,
402+
onSubtitleClick = onSubtitleClick,
403+
)
388404
}
389405
}
390406

@@ -394,6 +410,7 @@ private fun DmHeaderSection(
394410
otherMember: RoomMember,
395411
roomName: String,
396412
openAvatarPreview: (name: String, url: String) -> Unit,
413+
onSubtitleClick: (String) -> Unit,
397414
modifier: Modifier = Modifier
398415
) {
399416
Column(
@@ -411,6 +428,7 @@ private fun DmHeaderSection(
411428
TitleAndSubtitle(
412429
title = roomName,
413430
subtitle = otherMember.userId.value,
431+
onSubtitleClick = onSubtitleClick,
414432
)
415433
}
416434
}
@@ -419,6 +437,7 @@ private fun DmHeaderSection(
419437
private fun TitleAndSubtitle(
420438
title: String,
421439
subtitle: String?,
440+
onSubtitleClick: (String) -> Unit,
422441
) {
423442
Column(horizontalAlignment = Alignment.CenterHorizontally) {
424443
Spacer(modifier = Modifier.height(24.dp))
@@ -430,6 +449,7 @@ private fun TitleAndSubtitle(
430449
if (subtitle != null) {
431450
Spacer(modifier = Modifier.height(6.dp))
432451
Text(
452+
modifier = Modifier.niceClickable { onSubtitleClick(subtitle) },
433453
text = subtitle,
434454
style = ElementTheme.typography.fontBodyLgRegular,
435455
color = ElementTheme.colors.textSecondary,
@@ -612,13 +632,13 @@ private fun PinnedMessagesItem(
612632
headlineContent = { Text(stringResource(R.string.screen_room_details_pinned_events_row_title)) },
613633
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Pin())),
614634
trailingContent =
615-
if (pinnedMessagesCount == null) {
616-
ListItemContent.Custom {
617-
CircularProgressIndicator(strokeWidth = 2.dp, modifier = Modifier.size(24.dp))
618-
}
619-
} else {
620-
ListItemContent.Text(pinnedMessagesCount.toString())
621-
},
635+
if (pinnedMessagesCount == null) {
636+
ListItemContent.Custom {
637+
CircularProgressIndicator(strokeWidth = 2.dp, modifier = Modifier.size(24.dp))
638+
}
639+
} else {
640+
ListItemContent.Text(pinnedMessagesCount.toString())
641+
},
622642
onClick = {
623643
analyticsService.captureInteraction(Interaction.Name.PinnedMessageRoomInfoButton)
624644
onPinnedMessagesClick()

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import dagger.Module
1212
import dagger.Provides
1313
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsPresenter
1414
import io.element.android.features.userprofile.api.UserProfilePresenterFactory
15+
import io.element.android.libraries.androidutils.clipboard.ClipboardHelper
1516
import io.element.android.libraries.di.RoomScope
1617
import io.element.android.libraries.matrix.api.core.UserId
1718
import io.element.android.libraries.matrix.api.encryption.EncryptionService
@@ -25,6 +26,7 @@ object RoomMemberModule {
2526
room: MatrixRoom,
2627
userProfilePresenterFactory: UserProfilePresenterFactory,
2728
encryptionService: EncryptionService,
29+
clipboardHelper: ClipboardHelper,
2830
): RoomMemberDetailsPresenter.Factory {
2931
return object : RoomMemberDetailsPresenter.Factory {
3032
override fun create(roomMemberId: UserId): RoomMemberDetailsPresenter {
@@ -33,6 +35,7 @@ object RoomMemberModule {
3335
room = room,
3436
userProfilePresenterFactory = userProfilePresenterFactory,
3537
encryptionService = encryptionService,
38+
clipboardHelper = clipboardHelper,
3639
)
3740
}
3841
}

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

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,19 @@ import io.element.android.features.userprofile.api.UserProfileEvents
1919
import io.element.android.features.userprofile.api.UserProfilePresenterFactory
2020
import io.element.android.features.userprofile.api.UserProfileState
2121
import io.element.android.features.userprofile.api.UserProfileVerificationState
22+
import io.element.android.libraries.androidutils.clipboard.ClipboardHelper
2223
import io.element.android.libraries.architecture.Presenter
24+
import io.element.android.libraries.designsystem.utils.snackbar.LocalSnackbarDispatcher
25+
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
26+
import io.element.android.libraries.designsystem.utils.snackbar.collectSnackbarMessageAsState
2327
import io.element.android.libraries.matrix.api.core.UserId
2428
import io.element.android.libraries.matrix.api.encryption.EncryptionService
2529
import io.element.android.libraries.matrix.api.encryption.identity.IdentityState
2630
import io.element.android.libraries.matrix.api.encryption.identity.IdentityStateChange
2731
import io.element.android.libraries.matrix.api.room.MatrixRoom
2832
import io.element.android.libraries.matrix.ui.room.getRoomMemberAsState
2933
import io.element.android.libraries.matrix.ui.room.roomMemberIdentityStateChange
34+
import io.element.android.libraries.ui.strings.CommonStrings
3035
import kotlinx.coroutines.ExperimentalCoroutinesApi
3136
import kotlinx.coroutines.flow.filter
3237
import kotlinx.coroutines.flow.filterNotNull
@@ -42,6 +47,7 @@ class RoomMemberDetailsPresenter @AssistedInject constructor(
4247
@Assisted private val roomMemberId: UserId,
4348
private val room: MatrixRoom,
4449
private val encryptionService: EncryptionService,
50+
private val clipboardHelper: ClipboardHelper,
4551
userProfilePresenterFactory: UserProfilePresenterFactory,
4652
) : Presenter<UserProfileState> {
4753
interface Factory {
@@ -55,6 +61,8 @@ class RoomMemberDetailsPresenter @AssistedInject constructor(
5561
override fun present(): UserProfileState {
5662
val coroutineScope = rememberCoroutineScope()
5763

64+
val snackbarDispatcher = LocalSnackbarDispatcher.current
65+
val snackbarMessage by snackbarDispatcher.collectSnackbarMessageAsState()
5866
val roomMember by room.getRoomMemberAsState(roomMemberId)
5967
LaunchedEffect(Unit) {
6068
// Update room member info when opening this screen
@@ -111,21 +119,20 @@ class RoomMemberDetailsPresenter @AssistedInject constructor(
111119
UserProfileEvents.WithdrawVerification -> coroutineScope.launch {
112120
encryptionService.withdrawVerification(roomMemberId)
113121
}
114-
else -> Unit
122+
is UserProfileEvents.CopyToClipboard -> {
123+
clipboardHelper.copyPlainText(event.text)
124+
snackbarDispatcher.post(SnackbarMessage(CommonStrings.common_copied_to_clipboard))
125+
}
126+
else -> userProfileState.eventSink(event)
115127
}
116128
}
117129

118130
return userProfileState.copy(
119131
userName = roomUserName ?: userProfileState.userName,
120132
avatarUrl = roomUserAvatar ?: userProfileState.avatarUrl,
121133
verificationState = verificationState,
122-
eventSink = { event ->
123-
if (event is UserProfileEvents.WithdrawVerification) {
124-
eventSink(UserProfileEvents.WithdrawVerification)
125-
} else {
126-
userProfileState.eventSink(event)
127-
}
128-
}
134+
snackbarMessage = snackbarMessage,
135+
eventSink = ::eventSink
129136
)
130137
}
131138
}

features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenterTest.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ import io.element.android.features.roomcall.api.aStandByCallState
1717
import io.element.android.features.roomdetails.impl.members.aRoomMember
1818
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsPresenter
1919
import io.element.android.features.userprofile.shared.aUserProfileState
20+
import io.element.android.libraries.androidutils.clipboard.ClipboardHelper
21+
import io.element.android.libraries.androidutils.clipboard.FakeClipboardHelper
2022
import io.element.android.libraries.architecture.Presenter
2123
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
2224
import io.element.android.libraries.featureflag.api.FeatureFlagService
@@ -81,6 +83,7 @@ class RoomDetailsPresenterTest {
8183
),
8284
isPinnedMessagesFeatureEnabled: Boolean = true,
8385
encryptionService: FakeEncryptionService = FakeEncryptionService(),
86+
clipboardHelper: ClipboardHelper = FakeClipboardHelper(),
8487
): RoomDetailsPresenter {
8588
val matrixClient = FakeMatrixClient(notificationSettingsService = notificationSettingsService)
8689
val roomMemberDetailsPresenterFactory = object : RoomMemberDetailsPresenter.Factory {
@@ -92,6 +95,7 @@ class RoomDetailsPresenterTest {
9295
Presenter { aUserProfileState() }
9396
},
9497
encryptionService = encryptionService,
98+
clipboardHelper = clipboardHelper,
9599
)
96100
}
97101
}
@@ -106,6 +110,7 @@ class RoomDetailsPresenterTest {
106110
dispatchers = dispatchers,
107111
isPinnedMessagesFeatureEnabled = { isPinnedMessagesFeatureEnabled },
108112
analyticsService = analyticsService,
113+
clipboardHelper = clipboardHelper,
109114
)
110115
}
111116

features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsPresenterTest.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ import io.element.android.features.userprofile.api.UserProfileEvents
1717
import io.element.android.features.userprofile.api.UserProfilePresenterFactory
1818
import io.element.android.features.userprofile.api.UserProfileVerificationState
1919
import io.element.android.features.userprofile.shared.aUserProfileState
20+
import io.element.android.libraries.androidutils.clipboard.ClipboardHelper
21+
import io.element.android.libraries.androidutils.clipboard.FakeClipboardHelper
2022
import io.element.android.libraries.architecture.Presenter
2123
import io.element.android.libraries.matrix.api.core.UserId
2224
import io.element.android.libraries.matrix.api.encryption.identity.IdentityState
@@ -350,12 +352,14 @@ class RoomMemberDetailsPresenterTest {
350352
}
351353
},
352354
encryptionService: FakeEncryptionService = FakeEncryptionService(getUserIdentityResult = { Result.success(null) }),
355+
clipboardHelper: ClipboardHelper = FakeClipboardHelper(),
353356
): RoomMemberDetailsPresenter {
354357
return RoomMemberDetailsPresenter(
355358
roomMemberId = UserId("@alice:server.org"),
356359
room = room,
357360
userProfilePresenterFactory = userProfilePresenterFactory,
358361
encryptionService = encryptionService,
362+
clipboardHelper = clipboardHelper,
359363
)
360364
}
361365
}

features/userprofile/api/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,6 @@ android {
1616

1717
dependencies {
1818
implementation(projects.libraries.architecture)
19+
implementation(projects.libraries.designsystem)
1920
implementation(projects.libraries.matrix.api)
2021
}

0 commit comments

Comments
 (0)