From bb6a1a6d92df75b690ff8ee0453fc4f4ebf4fc73 Mon Sep 17 00:00:00 2001 From: Joe Groocock Date: Tue, 12 Nov 2024 13:02:18 +0000 Subject: [PATCH] Insert a mention when clicking user name Signed-off-by: Joe Groocock --- .../features/messages/impl/MessagesView.kt | 17 +++++++-- .../messagecomposer/MessageComposerEvents.kt | 2 + .../MessageComposerPresenter.kt | 24 ++++++++++++ .../pinned/list/PinnedMessagesListView.kt | 3 +- .../messages/impl/timeline/TimelineView.kt | 9 +++-- .../TimelineViewMessageShieldPreview.kt | 3 +- .../components/ATimelineItemEventRow.kt | 3 +- .../components/TimelineItemEventRow.kt | 37 ++++++++++++++----- .../TimelineItemGroupedEventsRow.kt | 18 ++++++--- .../timeline/components/TimelineItemRow.kt | 9 +++-- 10 files changed, 98 insertions(+), 27 deletions(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt index e5d73fbed6..432d3e010a 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt @@ -102,6 +102,10 @@ 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.EventId import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.room.RoomMember +import io.element.android.libraries.matrix.api.room.RoomMember.Role +import io.element.android.libraries.matrix.api.room.RoomMembershipState +import io.element.android.libraries.textcomposer.mentions.ResolvedSuggestion import io.element.android.libraries.textcomposer.model.TextEditorState import io.element.android.libraries.ui.strings.CommonStrings import kotlinx.collections.immutable.ImmutableList @@ -142,6 +146,10 @@ fun MessagesView( // This is needed because the composer is inside an AndroidView that can't be affected by the FocusManager in Compose val localView = LocalView.current + fun onUserNameClick(userId: UserId) { + state.composerState.eventSink(MessageComposerEvents.InsertMention(userId)) + } + fun onMessageClick(event: TimelineItem.Event) { Timber.v("onMessageClick= ${event.id}") val hideKeyboard = onEventClick(event) @@ -208,7 +216,8 @@ fun MessagesView( .consumeWindowInsets(padding), onMessageClick = ::onMessageClick, onMessageLongClick = ::onMessageLongClick, - onUserDataClick = onUserDataClick, + onUserAvatarClick = onUserDataClick, + onUserNameClick = ::onUserNameClick, onLinkClick = onLinkClick, onReactionClick = ::onEmojiReactionClick, onReactionLongClick = ::onEmojiReactionLongClick, @@ -307,7 +316,8 @@ private fun AttachmentStateView( private fun MessagesViewContent( state: MessagesState, onMessageClick: (TimelineItem.Event) -> Unit, - onUserDataClick: (UserId) -> Unit, + onUserAvatarClick: (UserId) -> Unit, + onUserNameClick: (UserId) -> Unit, onLinkClick: (String) -> Unit, onReactionClick: (key: String, TimelineItem.Event) -> Unit, onReactionLongClick: (key: String, TimelineItem.Event) -> Unit, @@ -380,7 +390,8 @@ private fun MessagesViewContent( TimelineView( state = state.timelineState, timelineProtectionState = state.timelineProtectionState, - onUserDataClick = onUserDataClick, + onUserAvatarClick = onUserAvatarClick, + onUserNameClick = onUserNameClick, onLinkClick = onLinkClick, onMessageClick = onMessageClick, onMessageLongClick = onMessageLongClick, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerEvents.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerEvents.kt index fca9948339..0efab316c4 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerEvents.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerEvents.kt @@ -9,6 +9,7 @@ package io.element.android.features.messages.impl.messagecomposer import android.net.Uri import androidx.compose.runtime.Immutable +import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.textcomposer.mentions.ResolvedSuggestion import io.element.android.libraries.textcomposer.model.MessageComposerMode import io.element.android.libraries.textcomposer.model.Suggestion @@ -36,5 +37,6 @@ sealed interface MessageComposerEvents { data class TypingNotice(val isTyping: Boolean) : MessageComposerEvents data class SuggestionReceived(val suggestion: Suggestion?) : MessageComposerEvents data class InsertSuggestion(val resolvedSuggestion: ResolvedSuggestion) : MessageComposerEvents + data class InsertMention(val userId: UserId) : MessageComposerEvents data object SaveDraft : MessageComposerEvents } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt index 6101f48c1f..723b73e505 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt @@ -52,6 +52,7 @@ import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.draft.ComposerDraft import io.element.android.libraries.matrix.api.room.draft.ComposerDraftType import io.element.android.libraries.matrix.api.room.isDm +import io.element.android.libraries.matrix.api.room.joinedRoomMembers import io.element.android.libraries.matrix.api.timeline.TimelineException import io.element.android.libraries.matrix.api.timeline.item.event.toEventOrTransactionId import io.element.android.libraries.matrix.ui.messages.RoomMemberProfilesCache @@ -70,6 +71,7 @@ import io.element.android.libraries.textcomposer.model.MarkdownTextEditorState import io.element.android.libraries.textcomposer.model.Message import io.element.android.libraries.textcomposer.model.MessageComposerMode import io.element.android.libraries.textcomposer.model.Suggestion +import io.element.android.libraries.textcomposer.model.SuggestionType import io.element.android.libraries.textcomposer.model.TextEditorState import io.element.android.libraries.textcomposer.model.rememberMarkdownTextEditorState import io.element.android.services.analytics.api.AnalyticsService @@ -389,6 +391,28 @@ class MessageComposerPresenter @Inject constructor( } } } + is MessageComposerEvents.InsertMention -> { + localCoroutineScope.launch { + room.membersStateFlow.collect { + val member = it.joinedRoomMembers().find { it.userId == event.userId } ?: return@collect + if (showTextFormatting) { + val text = member.userId.value + val link = permalinkBuilder.permalinkForUser(member.userId).getOrNull() ?: return@collect + // FIXME: This should use the un-exported `insertMention()`, probably. Currently it fails because there's no active `suggestion` + richTextEditorState.insertMentionAtSuggestion(text = text, link = link) + } else { + val end = markdownTextEditorState.selection.last + // Create a suggestion at the current selection (or end) so insertSuggestion() knows where to add the pill + markdownTextEditorState.currentSuggestion = Suggestion(end, end, SuggestionType.Mention, "") + markdownTextEditorState.insertSuggestion( + resolvedSuggestion = ResolvedSuggestion.Member(member), + mentionSpanProvider = mentionSpanProvider, + permalinkBuilder = permalinkBuilder, + ) + } + } + } + } MessageComposerEvents.SaveDraft -> { val draft = createDraftFromState(markdownTextEditorState, richTextEditorState) appCoroutineScope.updateDraft(draft, isVolatile = false) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListView.kt index f4a3247cba..a7bb1f19e3 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListView.kt @@ -214,7 +214,8 @@ private fun PinnedMessagesListLoaded( timelineProtectionState = state.timelineProtectionState, isLastOutgoingMessage = false, focusedEventId = null, - onUserDataClick = onUserDataClick, + onUserAvatarClick = onUserDataClick, + onUserNameClick = onUserDataClick, onLinkClick = onLinkClick, onClick = onEventClick, onLongClick = ::onMessageLongClick, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt index f0e976e368..3cd5ee55ea 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt @@ -74,7 +74,8 @@ import kotlinx.coroutines.launch fun TimelineView( state: TimelineState, timelineProtectionState: TimelineProtectionState, - onUserDataClick: (UserId) -> Unit, + onUserAvatarClick: (UserId) -> Unit, + onUserNameClick: (UserId) -> Unit, onLinkClick: (String) -> Unit, onMessageClick: (TimelineItem.Event) -> Unit, onMessageLongClick: (TimelineItem.Event) -> Unit, @@ -139,7 +140,8 @@ fun TimelineView( renderReadReceipts = state.renderReadReceipts, isLastOutgoingMessage = state.isLastOutgoingMessage(timelineItem.identifier()), focusedEventId = state.focusedEventId, - onUserDataClick = onUserDataClick, + onUserAvatarClick = onUserAvatarClick, + onUserNameClick = onUserNameClick, onLinkClick = onLinkClick, onClick = onMessageClick, onLongClick = onMessageLongClick, @@ -320,7 +322,8 @@ internal fun TimelineViewPreview( focusedEventIndex = 0, ), timelineProtectionState = aTimelineProtectionState(), - onUserDataClick = {}, + onUserAvatarClick = {}, + onUserNameClick = {}, onLinkClick = {}, onMessageClick = {}, onMessageLongClick = {}, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewMessageShieldPreview.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewMessageShieldPreview.kt index 99f69675fa..e1ec91a8a7 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewMessageShieldPreview.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewMessageShieldPreview.kt @@ -39,7 +39,8 @@ internal fun TimelineViewMessageShieldPreview() = ElementPreview { messageShield = messageShield, ), timelineProtectionState = aTimelineProtectionState(), - onUserDataClick = {}, + onUserAvatarClick = {}, + onUserNameClick = {}, onLinkClick = {}, onMessageClick = {}, onMessageLongClick = {}, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/ATimelineItemEventRow.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/ATimelineItemEventRow.kt index 1fa5e7f9a1..5bf2ad8bfa 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/ATimelineItemEventRow.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/ATimelineItemEventRow.kt @@ -33,7 +33,8 @@ internal fun ATimelineItemEventRow( onClick = {}, onLongClick = {}, onLinkClick = {}, - onUserDataClick = {}, + onUserAvatarClick = {}, + onUserNameClick = {}, inReplyToClick = {}, onReactionClick = { _, _ -> }, onReactionLongClick = { _, _ -> }, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt index 2f0165cd7b..ce80404a35 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt @@ -25,6 +25,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider @@ -117,7 +118,8 @@ fun TimelineItemEventRow( onClick: () -> Unit, onLongClick: () -> Unit, onLinkClick: (String) -> Unit, - onUserDataClick: (UserId) -> Unit, + onUserAvatarClick: (UserId) -> Unit, + onUserNameClick: (UserId) -> Unit, inReplyToClick: (EventId) -> Unit, onReactionClick: (emoji: String, eventId: TimelineItem.Event) -> Unit, onReactionLongClick: (emoji: String, eventId: TimelineItem.Event) -> Unit, @@ -141,8 +143,11 @@ fun TimelineItemEventRow( val coroutineScope = rememberCoroutineScope() val interactionSource = remember { MutableInteractionSource() } - fun onUserDataClick() { - onUserDataClick(event.senderId) + fun onUserAvatarClick() { + onUserAvatarClick(event.senderId) + } + fun onUserNameClick() { + onUserNameClick(event.senderId) } fun inReplyToClick() { @@ -176,7 +181,8 @@ fun TimelineItemEventRow( onClick = onClick, onLongClick = onLongClick, inReplyToClick = ::inReplyToClick, - onUserDataClick = ::onUserDataClick, + onUserAvatarClick = ::onUserAvatarClick, + onUserNameClick = ::onUserNameClick, onReactionClick = { emoji -> onReactionClick(emoji, event) }, onReactionLongClick = { emoji -> onReactionLongClick(emoji, event) }, onMoreReactionsClick = { onMoreReactionsClick(event) }, @@ -210,7 +216,8 @@ fun TimelineItemEventRow( onClick = onClick, onLongClick = onLongClick, inReplyToClick = ::inReplyToClick, - onUserDataClick = ::onUserDataClick, + onUserAvatarClick = ::onUserAvatarClick, + onUserNameClick = ::onUserNameClick, onReactionClick = { emoji -> onReactionClick(emoji, event) }, onReactionLongClick = { emoji -> onReactionLongClick(emoji, event) }, onMoreReactionsClick = { onMoreReactionsClick(event) }, @@ -266,7 +273,8 @@ private fun TimelineItemEventRowContent( onClick: () -> Unit, onLongClick: () -> Unit, inReplyToClick: () -> Unit, - onUserDataClick: () -> Unit, + onUserAvatarClick: () -> Unit, + onUserNameClick: () -> Unit, onReactionClick: (emoji: String) -> Unit, onReactionLongClick: (emoji: String) -> Unit, onMoreReactionsClick: (event: TimelineItem.Event) -> Unit, @@ -298,6 +306,8 @@ private fun TimelineItemEventRowContent( event.senderId, event.senderProfile, event.senderAvatar, + onUserAvatarClick, + onUserNameClick, Modifier .constrainAs(sender) { top.linkTo(parent.top) @@ -306,7 +316,6 @@ private fun TimelineItemEventRowContent( } .padding(horizontal = 16.dp) .zIndex(1f) - .clickable(onClick = onUserDataClick) // This is redundant when using talkback .clearAndSetSemantics { invisibleToUser() @@ -409,16 +418,26 @@ private fun MessageSenderInformation( senderId: UserId, senderProfile: ProfileTimelineDetails, senderAvatar: AvatarData, + onUserAvatarClick: () -> Unit, + onUserNameClick: () -> Unit, modifier: Modifier = Modifier ) { val avatarColors = AvatarColorsProvider.provide(senderAvatar.id) Row(modifier = modifier) { - Avatar(senderAvatar) - Spacer(modifier = Modifier.width(4.dp)) + Avatar( + avatarData = senderAvatar, + modifier = Modifier + .clip(CircleShape) + .clickable(onClick = onUserAvatarClick) + ) SenderName( senderId = senderId, senderProfile = senderProfile, senderNameMode = SenderNameMode.Timeline(avatarColors.foreground), + modifier = Modifier + .clip(RoundedCornerShape(4.dp)) + .clickable(onClick = onUserNameClick) + .padding(horizontal = 4.dp), ) } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemGroupedEventsRow.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemGroupedEventsRow.kt index d280efa62c..7754d3cd2e 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemGroupedEventsRow.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemGroupedEventsRow.kt @@ -44,7 +44,8 @@ fun TimelineItemGroupedEventsRow( onClick: (TimelineItem.Event) -> Unit, onLongClick: (TimelineItem.Event) -> Unit, inReplyToClick: (EventId) -> Unit, - onUserDataClick: (UserId) -> Unit, + onUserAvatarClick: (UserId) -> Unit, + onUserNameClick: (UserId) -> Unit, onLinkClick: (String) -> Unit, onReactionClick: (key: String, TimelineItem.Event) -> Unit, onReactionLongClick: (key: String, TimelineItem.Event) -> Unit, @@ -83,7 +84,8 @@ fun TimelineItemGroupedEventsRow( onClick = onClick, onLongClick = onLongClick, inReplyToClick = inReplyToClick, - onUserDataClick = onUserDataClick, + onUserAvatarClick = onUserAvatarClick, + onUserNameClick = onUserNameClick, onLinkClick = onLinkClick, onReactionClick = onReactionClick, onReactionLongClick = onReactionLongClick, @@ -108,7 +110,8 @@ private fun TimelineItemGroupedEventsRowContent( onClick: (TimelineItem.Event) -> Unit, onLongClick: (TimelineItem.Event) -> Unit, inReplyToClick: (EventId) -> Unit, - onUserDataClick: (UserId) -> Unit, + onUserAvatarClick: (UserId) -> Unit, + onUserNameClick: (UserId) -> Unit, onLinkClick: (String) -> Unit, onReactionClick: (key: String, TimelineItem.Event) -> Unit, onReactionLongClick: (key: String, TimelineItem.Event) -> Unit, @@ -150,7 +153,8 @@ private fun TimelineItemGroupedEventsRowContent( renderReadReceipts = renderReadReceipts, isLastOutgoingMessage = isLastOutgoingMessage, focusedEventId = focusedEventId, - onUserDataClick = onUserDataClick, + onUserAvatarClick = onUserAvatarClick, + onUserNameClick = onUserNameClick, onLinkClick = onLinkClick, onClick = onClick, onLongClick = onLongClick, @@ -196,7 +200,8 @@ internal fun TimelineItemGroupedEventsRowContentExpandedPreview() = ElementPrevi onClick = {}, onLongClick = {}, inReplyToClick = {}, - onUserDataClick = {}, + onUserAvatarClick = {}, + onUserNameClick = {}, onLinkClick = {}, onReactionClick = { _, _ -> }, onReactionLongClick = { _, _ -> }, @@ -221,7 +226,8 @@ internal fun TimelineItemGroupedEventsRowContentCollapsePreview() = ElementPrevi onClick = {}, onLongClick = {}, inReplyToClick = {}, - onUserDataClick = {}, + onUserAvatarClick = {}, + onUserNameClick = {}, onLinkClick = {}, onReactionClick = { _, _ -> }, onReactionLongClick = { _, _ -> }, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemRow.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemRow.kt index 13c247645b..06b7ac79d9 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemRow.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemRow.kt @@ -42,7 +42,8 @@ internal fun TimelineItemRow( isLastOutgoingMessage: Boolean, timelineProtectionState: TimelineProtectionState, focusedEventId: EventId?, - onUserDataClick: (UserId) -> Unit, + onUserAvatarClick: (UserId) -> Unit, + onUserNameClick: (UserId) -> Unit, onLinkClick: (String) -> Unit, onClick: (TimelineItem.Event) -> Unit, onLongClick: (TimelineItem.Event) -> Unit, @@ -125,7 +126,8 @@ internal fun TimelineItemRow( }, onLongClick = { onLongClick(timelineItem) }, onLinkClick = onLinkClick, - onUserDataClick = onUserDataClick, + onUserAvatarClick = onUserAvatarClick, + onUserNameClick = onUserNameClick, inReplyToClick = inReplyToClick, onReactionClick = onReactionClick, onReactionLongClick = onReactionLongClick, @@ -151,7 +153,8 @@ internal fun TimelineItemRow( onClick = onClick, onLongClick = onLongClick, inReplyToClick = inReplyToClick, - onUserDataClick = onUserDataClick, + onUserAvatarClick = onUserAvatarClick, + onUserNameClick = onUserNameClick, onLinkClick = onLinkClick, onReactionClick = onReactionClick, onReactionLongClick = onReactionLongClick,