Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Insert a mention when clicking user's name in the timeline #3858

Draft
wants to merge 1 commit into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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, "")
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels like a bit of a hack; not sure I like it. Maybe it would be better to move this to MarkdownTextEditorState and tidy it up a bit

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
Loading