Integrate mentions in the composer (#1799)
* Integrate mentions in the composer:
- Add `MentionSpanProvider`.
- Add custom colors needed for mentions.
- Use the span provider to render mentions in the composer.
- Allow selecting users from the mentions suggestions to insert a mention.
---------
Co-authored-by: ElementBot <benoitm+elementbot@element.io>
This commit is contained in:
parent
004804a7c8
commit
a8fbb882f2
21 changed files with 465 additions and 61 deletions
|
|
@ -41,6 +41,7 @@ import androidx.compose.material3.MaterialTheme
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
|
|
@ -354,12 +355,12 @@ private fun MessagesViewContent(
|
|||
|
||||
// This key is used to force the sheet to be remeasured when the content changes.
|
||||
// Any state change that should trigger a height size should be added to the list of remembered values here.
|
||||
val sheetResizeContentKey = remember(
|
||||
state.composerState.mode.relatedEventId,
|
||||
val sheetResizeContentKey = remember { mutableIntStateOf(0) }
|
||||
LaunchedEffect(
|
||||
state.composerState.richTextEditorState.lineCount,
|
||||
state.composerState.memberSuggestions.size
|
||||
state.composerState.showTextFormatting,
|
||||
) {
|
||||
Random.nextInt()
|
||||
sheetResizeContentKey.intValue = Random.nextInt()
|
||||
}
|
||||
|
||||
ExpandableBottomSheetScaffold(
|
||||
|
|
@ -396,7 +397,7 @@ private fun MessagesViewContent(
|
|||
state = state,
|
||||
)
|
||||
},
|
||||
sheetContentKey = sheetResizeContentKey,
|
||||
sheetContentKey = sheetResizeContentKey.intValue,
|
||||
sheetTonalElevation = 0.dp,
|
||||
sheetShadowElevation = if (state.composerState.memberSuggestions.isNotEmpty()) 16.dp else 0.dp,
|
||||
)
|
||||
|
|
@ -425,7 +426,7 @@ private fun MessagesViewComposerBottomSheetContents(
|
|||
roomAvatarData = state.roomAvatar.dataOrNull(),
|
||||
memberSuggestions = state.composerState.memberSuggestions,
|
||||
onSuggestionSelected = {
|
||||
// TODO pass the selected suggestion to the RTE so it can be inserted as a pill
|
||||
state.composerState.eventSink(MessageComposerEvents.InsertMention(it))
|
||||
}
|
||||
)
|
||||
MessageComposerView(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.messages.impl.mentions
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
|
||||
@Immutable
|
||||
sealed interface MentionSuggestion {
|
||||
data object Room : MentionSuggestion
|
||||
data class Member(val roomMember: RoomMember) : MentionSuggestion
|
||||
}
|
||||
|
|
@ -31,7 +31,6 @@ import androidx.compose.ui.res.stringResource
|
|||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.features.messages.impl.R
|
||||
import io.element.android.features.messages.impl.messagecomposer.RoomMemberSuggestion
|
||||
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
|
|
@ -52,8 +51,8 @@ fun MentionSuggestionsPickerView(
|
|||
roomId: RoomId,
|
||||
roomName: String?,
|
||||
roomAvatarData: AvatarData?,
|
||||
memberSuggestions: ImmutableList<RoomMemberSuggestion>,
|
||||
onSuggestionSelected: (RoomMemberSuggestion) -> Unit,
|
||||
memberSuggestions: ImmutableList<MentionSuggestion>,
|
||||
onSuggestionSelected: (MentionSuggestion) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
LazyColumn(
|
||||
|
|
@ -63,8 +62,8 @@ fun MentionSuggestionsPickerView(
|
|||
memberSuggestions,
|
||||
key = { suggestion ->
|
||||
when (suggestion) {
|
||||
is RoomMemberSuggestion.Room -> "@room"
|
||||
is RoomMemberSuggestion.Member -> suggestion.roomMember.userId.value
|
||||
is MentionSuggestion.Room -> "@room"
|
||||
is MentionSuggestion.Member -> suggestion.roomMember.userId.value
|
||||
}
|
||||
}
|
||||
) {
|
||||
|
|
@ -85,18 +84,18 @@ fun MentionSuggestionsPickerView(
|
|||
|
||||
@Composable
|
||||
private fun RoomMemberSuggestionItemView(
|
||||
memberSuggestion: RoomMemberSuggestion,
|
||||
memberSuggestion: MentionSuggestion,
|
||||
roomId: String,
|
||||
roomName: String?,
|
||||
roomAvatar: AvatarData?,
|
||||
onSuggestionSelected: (RoomMemberSuggestion) -> Unit,
|
||||
onSuggestionSelected: (MentionSuggestion) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Row(modifier = modifier.clickable { onSuggestionSelected(memberSuggestion) }, horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
val avatarSize = AvatarSize.TimelineRoom
|
||||
val avatarData = when (memberSuggestion) {
|
||||
is RoomMemberSuggestion.Room -> roomAvatar?.copy(size = avatarSize) ?: AvatarData(roomId, roomName, null, avatarSize)
|
||||
is RoomMemberSuggestion.Member -> AvatarData(
|
||||
is MentionSuggestion.Room -> roomAvatar?.copy(size = avatarSize) ?: AvatarData(roomId, roomName, null, avatarSize)
|
||||
is MentionSuggestion.Member -> AvatarData(
|
||||
memberSuggestion.roomMember.userId.value,
|
||||
memberSuggestion.roomMember.displayName,
|
||||
memberSuggestion.roomMember.avatarUrl,
|
||||
|
|
@ -104,13 +103,13 @@ private fun RoomMemberSuggestionItemView(
|
|||
)
|
||||
}
|
||||
val title = when (memberSuggestion) {
|
||||
is RoomMemberSuggestion.Room -> stringResource(R.string.screen_room_mentions_at_room_title)
|
||||
is RoomMemberSuggestion.Member -> memberSuggestion.roomMember.displayName
|
||||
is MentionSuggestion.Room -> stringResource(R.string.screen_room_mentions_at_room_title)
|
||||
is MentionSuggestion.Member -> memberSuggestion.roomMember.displayName
|
||||
}
|
||||
|
||||
val subtitle = when (memberSuggestion) {
|
||||
is RoomMemberSuggestion.Room -> "@room"
|
||||
is RoomMemberSuggestion.Member -> memberSuggestion.roomMember.userId.value
|
||||
is MentionSuggestion.Room -> "@room"
|
||||
is MentionSuggestion.Member -> memberSuggestion.roomMember.userId.value
|
||||
}
|
||||
|
||||
Avatar(avatarData = avatarData, modifier = Modifier.padding(start = 16.dp, top = 12.dp, bottom = 12.dp))
|
||||
|
|
@ -159,9 +158,9 @@ internal fun MentionSuggestionsPickerView_Preview() {
|
|||
roomName = "Room",
|
||||
roomAvatarData = null,
|
||||
memberSuggestions = persistentListOf(
|
||||
RoomMemberSuggestion.Room,
|
||||
RoomMemberSuggestion.Member(roomMember),
|
||||
RoomMemberSuggestion.Member(roomMember.copy(userId = UserId("@bob:server.org"), displayName = "Bob")),
|
||||
MentionSuggestion.Room,
|
||||
MentionSuggestion.Member(roomMember),
|
||||
MentionSuggestion.Member(roomMember.copy(userId = UserId("@bob:server.org"), displayName = "Bob")),
|
||||
),
|
||||
onSuggestionSelected = {}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
|
||||
package io.element.android.features.messages.impl.mentions
|
||||
|
||||
import io.element.android.features.messages.impl.messagecomposer.RoomMemberSuggestion
|
||||
import io.element.android.libraries.core.data.filterUpTo
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
|
|
@ -46,10 +46,8 @@ object MentionSuggestionsProcessor {
|
|||
roomMembersState: MatrixRoomMembersState,
|
||||
currentUserId: UserId,
|
||||
canSendRoomMention: suspend () -> Boolean,
|
||||
): List<RoomMemberSuggestion> {
|
||||
): List<MentionSuggestion> {
|
||||
val members = roomMembersState.roomMembers()
|
||||
// Take the first MAX_BATCH_ITEMS only
|
||||
?.take(MAX_BATCH_ITEMS)
|
||||
return when {
|
||||
members.isNullOrEmpty() || suggestion == null -> {
|
||||
// Clear suggestions
|
||||
|
|
@ -61,7 +59,7 @@ object MentionSuggestionsProcessor {
|
|||
// Replace suggestions
|
||||
val matchingMembers = getMemberSuggestions(
|
||||
query = suggestion.text,
|
||||
roomMembers = roomMembersState.roomMembers(),
|
||||
roomMembers = members,
|
||||
currentUserId = currentUserId,
|
||||
canSendRoomMention = canSendRoomMention()
|
||||
)
|
||||
|
|
@ -81,7 +79,7 @@ object MentionSuggestionsProcessor {
|
|||
roomMembers: List<RoomMember>?,
|
||||
currentUserId: UserId,
|
||||
canSendRoomMention: Boolean,
|
||||
): List<RoomMemberSuggestion> {
|
||||
): List<MentionSuggestion> {
|
||||
return if (roomMembers.isNullOrEmpty()) {
|
||||
emptyList()
|
||||
} else {
|
||||
|
|
@ -95,14 +93,14 @@ object MentionSuggestionsProcessor {
|
|||
}
|
||||
|
||||
val matchingMembers = roomMembers
|
||||
// Search only in joined members, exclude the current user
|
||||
.filter { member ->
|
||||
// Search only in joined members, up to MAX_BATCH_ITEMS, exclude the current user
|
||||
.filterUpTo(MAX_BATCH_ITEMS) { member ->
|
||||
isJoinedMemberAndNotSelf(member) && memberMatchesQuery(member, query)
|
||||
}
|
||||
.map(RoomMemberSuggestion::Member)
|
||||
.map(MentionSuggestion::Member)
|
||||
|
||||
if ("room".contains(query) && canSendRoomMention) {
|
||||
listOf(RoomMemberSuggestion.Room) + matchingMembers
|
||||
listOf(MentionSuggestion.Room) + matchingMembers
|
||||
} else {
|
||||
matchingMembers
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
package io.element.android.features.messages.impl.messagecomposer
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import io.element.android.features.messages.impl.mentions.MentionSuggestion
|
||||
import io.element.android.libraries.textcomposer.model.Message
|
||||
import io.element.android.libraries.textcomposer.model.MessageComposerMode
|
||||
import io.element.android.libraries.textcomposer.model.Suggestion
|
||||
|
|
@ -41,4 +42,5 @@ sealed interface MessageComposerEvents {
|
|||
data object CancelSendAttachment : MessageComposerEvents
|
||||
data class Error(val error: Throwable) : MessageComposerEvents
|
||||
data class SuggestionReceived(val suggestion: Suggestion?) : MessageComposerEvents
|
||||
data class InsertMention(val mention: MentionSuggestion) : MessageComposerEvents
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@ import android.Manifest
|
|||
import android.annotation.SuppressLint
|
||||
import android.net.Uri
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.getValue
|
||||
|
|
@ -36,6 +35,7 @@ import im.vector.app.features.analytics.plan.Composer
|
|||
import io.element.android.features.messages.impl.attachments.Attachment
|
||||
import io.element.android.features.messages.impl.attachments.preview.error.sendAttachmentError
|
||||
import io.element.android.features.messages.impl.media.local.LocalMediaFactory
|
||||
import io.element.android.features.messages.impl.mentions.MentionSuggestion
|
||||
import io.element.android.features.messages.impl.mentions.MentionSuggestionsProcessor
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
||||
|
|
@ -45,8 +45,8 @@ import io.element.android.libraries.di.SingleIn
|
|||
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||
import io.element.android.libraries.matrix.api.core.ProgressCallback
|
||||
import io.element.android.libraries.matrix.api.permalink.PermalinkBuilder
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
import io.element.android.libraries.matrix.api.user.CurrentSessionIdHolder
|
||||
import io.element.android.libraries.mediapickers.api.PickerProvider
|
||||
import io.element.android.libraries.mediaupload.api.MediaSender
|
||||
|
|
@ -67,6 +67,8 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
|||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.merge
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
|
@ -87,7 +89,7 @@ class MessageComposerPresenter @Inject constructor(
|
|||
private val messageComposerContext: MessageComposerContextImpl,
|
||||
private val richTextEditorStateFactory: RichTextEditorStateFactory,
|
||||
private val currentSessionIdHolder: CurrentSessionIdHolder,
|
||||
permissionsPresenterFactory: PermissionsPresenter.Factory
|
||||
permissionsPresenterFactory: PermissionsPresenter.Factory,
|
||||
) : Presenter<MessageComposerState> {
|
||||
|
||||
private val cameraPermissionPresenter = permissionsPresenterFactory.create(Manifest.permission.CAMERA)
|
||||
|
|
@ -173,7 +175,7 @@ class MessageComposerPresenter @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
val memberSuggestions = remember { mutableStateListOf<RoomMemberSuggestion>() }
|
||||
val memberSuggestions = remember { mutableStateListOf<MentionSuggestion>() }
|
||||
LaunchedEffect(isMentionsEnabled) {
|
||||
if (!isMentionsEnabled) return@LaunchedEffect
|
||||
val currentUserId = currentSessionIdHolder.current
|
||||
|
|
@ -184,8 +186,11 @@ class MessageComposerPresenter @Inject constructor(
|
|||
return !roomIsDm && userCanSendAtRoom
|
||||
}
|
||||
|
||||
suggestionSearchTrigger
|
||||
.debounce(0.5.seconds)
|
||||
// This will trigger a search immediately when `@` is typed
|
||||
val mentionStartTrigger = suggestionSearchTrigger.filter { it?.text.isNullOrEmpty() }
|
||||
// This will start a search when the user changes the text after the `@` with a debounce to prevent too much wasted work
|
||||
val mentionCompletionTrigger = suggestionSearchTrigger.filter { !it?.text.isNullOrEmpty() }.debounce(0.3.seconds)
|
||||
merge(mentionStartTrigger, mentionCompletionTrigger)
|
||||
.combine(room.membersStateFlow) { suggestion, roomMembersState ->
|
||||
memberSuggestions.clear()
|
||||
val result = MentionSuggestionsProcessor.process(
|
||||
|
|
@ -284,6 +289,20 @@ class MessageComposerPresenter @Inject constructor(
|
|||
is MessageComposerEvents.SuggestionReceived -> {
|
||||
suggestionSearchTrigger.value = event.suggestion
|
||||
}
|
||||
is MessageComposerEvents.InsertMention -> {
|
||||
localCoroutineScope.launch {
|
||||
when (val mention = event.mention) {
|
||||
is MentionSuggestion.Room -> {
|
||||
richTextEditorState.insertAtRoomMentionAtSuggestion()
|
||||
}
|
||||
is MentionSuggestion.Member -> {
|
||||
val text = mention.roomMember.displayName?.prependIndent("@") ?: mention.roomMember.userId.value
|
||||
val link = PermalinkBuilder.permalinkForUser(mention.roomMember.userId).getOrNull() ?: return@launch
|
||||
richTextEditorState.insertMentionAtSuggestion(text = text, link = link)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -297,6 +316,7 @@ class MessageComposerPresenter @Inject constructor(
|
|||
canCreatePoll = canCreatePoll.value,
|
||||
attachmentsState = attachmentsState.value,
|
||||
memberSuggestions = memberSuggestions.toPersistentList(),
|
||||
currentUserId = currentSessionIdHolder.current,
|
||||
eventSink = { handleEvents(it) }
|
||||
)
|
||||
}
|
||||
|
|
@ -410,8 +430,3 @@ class MessageComposerPresenter @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
@Immutable
|
||||
sealed interface RoomMemberSuggestion {
|
||||
data object Room : RoomMemberSuggestion
|
||||
data class Member(val roomMember: RoomMember) : RoomMemberSuggestion
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ package io.element.android.features.messages.impl.messagecomposer
|
|||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.Stable
|
||||
import io.element.android.features.messages.impl.attachments.Attachment
|
||||
import io.element.android.features.messages.impl.mentions.MentionSuggestion
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.textcomposer.model.MessageComposerMode
|
||||
import io.element.android.wysiwyg.compose.RichTextEditorState
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
|
@ -33,7 +35,8 @@ data class MessageComposerState(
|
|||
val canShareLocation: Boolean,
|
||||
val canCreatePoll: Boolean,
|
||||
val attachmentsState: AttachmentsState,
|
||||
val memberSuggestions: ImmutableList<RoomMemberSuggestion>,
|
||||
val memberSuggestions: ImmutableList<MentionSuggestion>,
|
||||
val currentUserId: UserId,
|
||||
val eventSink: (MessageComposerEvents) -> Unit,
|
||||
) {
|
||||
val hasFocus: Boolean = richTextEditorState.hasFocus
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@
|
|||
package io.element.android.features.messages.impl.messagecomposer
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.features.messages.impl.mentions.MentionSuggestion
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.textcomposer.model.MessageComposerMode
|
||||
import io.element.android.wysiwyg.compose.RichTextEditorState
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
|
@ -38,7 +40,7 @@ fun aMessageComposerState(
|
|||
canShareLocation: Boolean = true,
|
||||
canCreatePoll: Boolean = true,
|
||||
attachmentsState: AttachmentsState = AttachmentsState.None,
|
||||
memberSuggestions: ImmutableList<RoomMemberSuggestion> = persistentListOf(),
|
||||
memberSuggestions: ImmutableList<MentionSuggestion> = persistentListOf(),
|
||||
) = MessageComposerState(
|
||||
richTextEditorState = composerState,
|
||||
isFullScreen = isFullScreen,
|
||||
|
|
@ -49,5 +51,6 @@ fun aMessageComposerState(
|
|||
canCreatePoll = canCreatePoll,
|
||||
attachmentsState = attachmentsState,
|
||||
memberSuggestions = memberSuggestions,
|
||||
currentUserId = UserId("@alice:localhost"),
|
||||
eventSink = {},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import androidx.compose.foundation.layout.height
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerEvents
|
||||
|
|
@ -32,9 +33,9 @@ import io.element.android.libraries.designsystem.preview.ElementPreview
|
|||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.textcomposer.TextComposer
|
||||
import io.element.android.libraries.textcomposer.model.Message
|
||||
import io.element.android.libraries.textcomposer.model.VoiceMessageRecorderEvent
|
||||
import io.element.android.libraries.textcomposer.model.Suggestion
|
||||
import io.element.android.libraries.textcomposer.model.VoiceMessagePlayerEvent
|
||||
import io.element.android.libraries.textcomposer.model.VoiceMessageRecorderEvent
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
|
|
@ -46,6 +47,7 @@ internal fun MessageComposerView(
|
|||
enableVoiceMessages: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val view = LocalView.current
|
||||
fun sendMessage(message: Message) {
|
||||
state.eventSink(MessageComposerEvents.SendMessage(message))
|
||||
}
|
||||
|
|
@ -59,6 +61,7 @@ internal fun MessageComposerView(
|
|||
}
|
||||
|
||||
fun onDismissTextFormatting() {
|
||||
view.clearFocus()
|
||||
state.eventSink(MessageComposerEvents.ToggleTextFormatting(enabled = false))
|
||||
}
|
||||
|
||||
|
|
@ -113,6 +116,7 @@ internal fun MessageComposerView(
|
|||
onDeleteVoiceMessage = onDeleteVoiceMessage,
|
||||
onSuggestionReceived = ::onSuggestionReceived,
|
||||
onError = ::onError,
|
||||
currentUserId = state.currentUserId,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ import io.element.android.features.messages.impl.messagecomposer.MessageComposer
|
|||
import io.element.android.features.messages.impl.messagecomposer.MessageComposerEvents
|
||||
import io.element.android.features.messages.impl.messagecomposer.MessageComposerPresenter
|
||||
import io.element.android.features.messages.impl.messagecomposer.MessageComposerState
|
||||
import io.element.android.features.messages.impl.messagecomposer.RoomMemberSuggestion
|
||||
import io.element.android.features.messages.impl.mentions.MentionSuggestion
|
||||
import io.element.android.features.messages.media.FakeLocalMediaFactory
|
||||
import io.element.android.libraries.core.mimetype.MimeTypes
|
||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
||||
|
|
@ -754,19 +754,19 @@ class MessageComposerPresenterTest {
|
|||
// An empty suggestion returns the room and joined members that are not the current user
|
||||
initialState.eventSink(MessageComposerEvents.SuggestionReceived(Suggestion(0, 0, SuggestionType.Mention, "")))
|
||||
assertThat(awaitItem().memberSuggestions)
|
||||
.containsExactly(RoomMemberSuggestion.Room, RoomMemberSuggestion.Member(bob), RoomMemberSuggestion.Member(david))
|
||||
.containsExactly(MentionSuggestion.Room, MentionSuggestion.Member(bob), MentionSuggestion.Member(david))
|
||||
|
||||
// A suggestion containing a part of "room" will also return the room mention
|
||||
initialState.eventSink(MessageComposerEvents.SuggestionReceived(Suggestion(0, 0, SuggestionType.Mention, "roo")))
|
||||
assertThat(awaitItem().memberSuggestions).containsExactly(RoomMemberSuggestion.Room)
|
||||
assertThat(awaitItem().memberSuggestions).containsExactly(MentionSuggestion.Room)
|
||||
|
||||
// A non-empty suggestion will return those joined members whose user id matches it
|
||||
initialState.eventSink(MessageComposerEvents.SuggestionReceived(Suggestion(0, 0, SuggestionType.Mention, "bob")))
|
||||
assertThat(awaitItem().memberSuggestions).containsExactly(RoomMemberSuggestion.Member(bob))
|
||||
assertThat(awaitItem().memberSuggestions).containsExactly(MentionSuggestion.Member(bob))
|
||||
|
||||
// A non-empty suggestion will return those joined members whose display name matches it
|
||||
initialState.eventSink(MessageComposerEvents.SuggestionReceived(Suggestion(0, 0, SuggestionType.Mention, "dave")))
|
||||
assertThat(awaitItem().memberSuggestions).containsExactly(RoomMemberSuggestion.Member(david))
|
||||
assertThat(awaitItem().memberSuggestions).containsExactly(MentionSuggestion.Member(david))
|
||||
|
||||
// If the suggestion isn't a mention, no suggestions are returned
|
||||
initialState.eventSink(MessageComposerEvents.SuggestionReceived(Suggestion(0, 0, SuggestionType.Command, "")))
|
||||
|
|
@ -776,7 +776,7 @@ class MessageComposerPresenterTest {
|
|||
room.givenCanTriggerRoomNotification(Result.success(false))
|
||||
initialState.eventSink(MessageComposerEvents.SuggestionReceived(Suggestion(0, 0, SuggestionType.Mention, "")))
|
||||
assertThat(awaitItem().memberSuggestions)
|
||||
.containsExactly(RoomMemberSuggestion.Member(bob), RoomMemberSuggestion.Member(david))
|
||||
.containsExactly(MentionSuggestion.Member(bob), MentionSuggestion.Member(david))
|
||||
|
||||
// If room is a DM, `RoomMemberSuggestion.Room` is not returned
|
||||
room.givenCanTriggerRoomNotification(Result.success(true))
|
||||
|
|
@ -813,8 +813,25 @@ class MessageComposerPresenterTest {
|
|||
|
||||
// An empty suggestion returns the joined members that are not the current user, but not the room
|
||||
initialState.eventSink(MessageComposerEvents.SuggestionReceived(Suggestion(0, 0, SuggestionType.Mention, "")))
|
||||
skipItems(1)
|
||||
assertThat(awaitItem().memberSuggestions)
|
||||
.containsExactly(RoomMemberSuggestion.Member(bob), RoomMemberSuggestion.Member(david))
|
||||
.containsExactly(MentionSuggestion.Member(bob), MentionSuggestion.Member(david))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - insertMention`() = runTest {
|
||||
val presenter = createPresenter(this)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
skipItems(1)
|
||||
val initialState = awaitItem()
|
||||
initialState.richTextEditorState.setHtml("Hey @bo")
|
||||
initialState.eventSink(MessageComposerEvents.InsertMention(MentionSuggestion.Member(aRoomMember(userId = A_USER_ID_2))))
|
||||
|
||||
assertThat(initialState.richTextEditorState.messageHtml)
|
||||
.isEqualTo("Hey <a href='https://matrix.to/#/${A_USER_ID_2.value}'>${A_USER_ID_2.value}</a>")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue