Skip to content

Commit

Permalink
Insert a mention when clicking user name
Browse files Browse the repository at this point in the history
Signed-off-by: Joe Groocock <[email protected]>
  • Loading branch information
frebib committed Nov 12, 2024
1 parent cffd2da commit bb6a1a6
Show file tree
Hide file tree
Showing 10 changed files with 98 additions and 27 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -208,7 +216,8 @@ fun MessagesView(
.consumeWindowInsets(padding),
onMessageClick = ::onMessageClick,
onMessageLongClick = ::onMessageLongClick,
onUserDataClick = onUserDataClick,
onUserAvatarClick = onUserDataClick,
onUserNameClick = ::onUserNameClick,
onLinkClick = onLinkClick,
onReactionClick = ::onEmojiReactionClick,
onReactionLongClick = ::onEmojiReactionLongClick,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -320,7 +322,8 @@ internal fun TimelineViewPreview(
focusedEventIndex = 0,
),
timelineProtectionState = aTimelineProtectionState(),
onUserDataClick = {},
onUserAvatarClick = {},
onUserNameClick = {},
onLinkClick = {},
onMessageClick = {},
onMessageLongClick = {},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ internal fun TimelineViewMessageShieldPreview() = ElementPreview {
messageShield = messageShield,
),
timelineProtectionState = aTimelineProtectionState(),
onUserDataClick = {},
onUserAvatarClick = {},
onUserNameClick = {},
onLinkClick = {},
onMessageClick = {},
onMessageLongClick = {},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ internal fun ATimelineItemEventRow(
onClick = {},
onLongClick = {},
onLinkClick = {},
onUserDataClick = {},
onUserAvatarClick = {},
onUserNameClick = {},
inReplyToClick = {},
onReactionClick = { _, _ -> },
onReactionLongClick = { _, _ -> },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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() {
Expand Down Expand Up @@ -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) },
Expand Down Expand Up @@ -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) },
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -298,6 +306,8 @@ private fun TimelineItemEventRowContent(
event.senderId,
event.senderProfile,
event.senderAvatar,
onUserAvatarClick,
onUserNameClick,
Modifier
.constrainAs(sender) {
top.linkTo(parent.top)
Expand All @@ -306,7 +316,6 @@ private fun TimelineItemEventRowContent(
}
.padding(horizontal = 16.dp)
.zIndex(1f)
.clickable(onClick = onUserDataClick)
// This is redundant when using talkback
.clearAndSetSemantics {
invisibleToUser()
Expand Down Expand Up @@ -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),
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -83,7 +84,8 @@ fun TimelineItemGroupedEventsRow(
onClick = onClick,
onLongClick = onLongClick,
inReplyToClick = inReplyToClick,
onUserDataClick = onUserDataClick,
onUserAvatarClick = onUserAvatarClick,
onUserNameClick = onUserNameClick,
onLinkClick = onLinkClick,
onReactionClick = onReactionClick,
onReactionLongClick = onReactionLongClick,
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -196,7 +200,8 @@ internal fun TimelineItemGroupedEventsRowContentExpandedPreview() = ElementPrevi
onClick = {},
onLongClick = {},
inReplyToClick = {},
onUserDataClick = {},
onUserAvatarClick = {},
onUserNameClick = {},
onLinkClick = {},
onReactionClick = { _, _ -> },
onReactionLongClick = { _, _ -> },
Expand All @@ -221,7 +226,8 @@ internal fun TimelineItemGroupedEventsRowContentCollapsePreview() = ElementPrevi
onClick = {},
onLongClick = {},
inReplyToClick = {},
onUserDataClick = {},
onUserAvatarClick = {},
onUserNameClick = {},
onLinkClick = {},
onReactionClick = { _, _ -> },
onReactionLongClick = { _, _ -> },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -125,7 +126,8 @@ internal fun TimelineItemRow(
},
onLongClick = { onLongClick(timelineItem) },
onLinkClick = onLinkClick,
onUserDataClick = onUserDataClick,
onUserAvatarClick = onUserAvatarClick,
onUserNameClick = onUserNameClick,
inReplyToClick = inReplyToClick,
onReactionClick = onReactionClick,
onReactionLongClick = onReactionLongClick,
Expand All @@ -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,
Expand Down

0 comments on commit bb6a1a6

Please sign in to comment.