Suggestion for room alias.
Rename `Mention` to `IntentionalMention` for clarity Remove dead code, there is no intentional mention for Room or RoomAlias. Rename `IntentionalMention.AtRoom` to `IntentionalMention.Room` to match Rust naming
This commit is contained in:
@@ -66,7 +66,7 @@ import io.element.android.features.messages.impl.actionlist.ActionListEvents
|
|||||||
import io.element.android.features.messages.impl.actionlist.ActionListView
|
import io.element.android.features.messages.impl.actionlist.ActionListView
|
||||||
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
|
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
|
||||||
import io.element.android.features.messages.impl.attachments.Attachment
|
import io.element.android.features.messages.impl.attachments.Attachment
|
||||||
import io.element.android.features.messages.impl.mentions.MentionSuggestionsPickerView
|
import io.element.android.features.messages.impl.mentions.SuggestionsPickerView
|
||||||
import io.element.android.features.messages.impl.messagecomposer.AttachmentsBottomSheet
|
import io.element.android.features.messages.impl.messagecomposer.AttachmentsBottomSheet
|
||||||
import io.element.android.features.messages.impl.messagecomposer.AttachmentsState
|
import io.element.android.features.messages.impl.messagecomposer.AttachmentsState
|
||||||
import io.element.android.features.messages.impl.messagecomposer.MessageComposerEvents
|
import io.element.android.features.messages.impl.messagecomposer.MessageComposerEvents
|
||||||
@@ -377,7 +377,7 @@ private fun MessagesViewContent(
|
|||||||
@Composable {}
|
@Composable {}
|
||||||
},
|
},
|
||||||
sheetSwipeEnabled = state.composerState.showTextFormatting,
|
sheetSwipeEnabled = state.composerState.showTextFormatting,
|
||||||
sheetShape = if (state.composerState.showTextFormatting || state.composerState.memberSuggestions.isNotEmpty()) {
|
sheetShape = if (state.composerState.showTextFormatting || state.composerState.suggestions.isNotEmpty()) {
|
||||||
MaterialTheme.shapes.large
|
MaterialTheme.shapes.large
|
||||||
} else {
|
} else {
|
||||||
RectangleShape
|
RectangleShape
|
||||||
@@ -427,7 +427,7 @@ private fun MessagesViewContent(
|
|||||||
},
|
},
|
||||||
sheetContentKey = sheetResizeContentKey.intValue,
|
sheetContentKey = sheetResizeContentKey.intValue,
|
||||||
sheetTonalElevation = 0.dp,
|
sheetTonalElevation = 0.dp,
|
||||||
sheetShadowElevation = if (state.composerState.memberSuggestions.isNotEmpty()) 16.dp else 0.dp,
|
sheetShadowElevation = if (state.composerState.suggestions.isNotEmpty()) 16.dp else 0.dp,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -439,7 +439,7 @@ private fun MessagesViewComposerBottomSheetContents(
|
|||||||
) {
|
) {
|
||||||
if (state.userEventPermissions.canSendMessage) {
|
if (state.userEventPermissions.canSendMessage) {
|
||||||
Column(modifier = Modifier.fillMaxWidth()) {
|
Column(modifier = Modifier.fillMaxWidth()) {
|
||||||
MentionSuggestionsPickerView(
|
SuggestionsPickerView(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.heightIn(max = 230.dp)
|
.heightIn(max = 230.dp)
|
||||||
// Consume all scrolling, preventing the bottom sheet from being dragged when interacting with the list of suggestions
|
// Consume all scrolling, preventing the bottom sheet from being dragged when interacting with the list of suggestions
|
||||||
@@ -451,9 +451,9 @@ private fun MessagesViewComposerBottomSheetContents(
|
|||||||
roomId = state.roomId,
|
roomId = state.roomId,
|
||||||
roomName = state.roomName.dataOrNull(),
|
roomName = state.roomName.dataOrNull(),
|
||||||
roomAvatarData = state.roomAvatar.dataOrNull(),
|
roomAvatarData = state.roomAvatar.dataOrNull(),
|
||||||
memberSuggestions = state.composerState.memberSuggestions,
|
suggestions = state.composerState.suggestions,
|
||||||
onSelectSuggestion = {
|
onSelectSuggestion = {
|
||||||
state.composerState.eventSink(MessageComposerEvents.InsertMention(it))
|
state.composerState.eventSink(MessageComposerEvents.InsertSuggestion(it))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
MessageComposerView(
|
MessageComposerView(
|
||||||
|
|||||||
@@ -16,13 +16,14 @@
|
|||||||
|
|
||||||
package io.element.android.features.messages.impl.mentions
|
package io.element.android.features.messages.impl.mentions
|
||||||
|
|
||||||
|
import io.element.android.features.messages.impl.messagecomposer.RoomAliasSuggestion
|
||||||
import io.element.android.libraries.core.data.filterUpTo
|
import io.element.android.libraries.core.data.filterUpTo
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
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.MatrixRoomMembersState
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMembershipState
|
import io.element.android.libraries.matrix.api.room.RoomMembershipState
|
||||||
import io.element.android.libraries.matrix.api.room.roomMembers
|
import io.element.android.libraries.matrix.api.room.roomMembers
|
||||||
import io.element.android.libraries.textcomposer.mentions.ResolvedMentionSuggestion
|
import io.element.android.libraries.textcomposer.mentions.ResolvedSuggestion
|
||||||
import io.element.android.libraries.textcomposer.model.Suggestion
|
import io.element.android.libraries.textcomposer.model.Suggestion
|
||||||
import io.element.android.libraries.textcomposer.model.SuggestionType
|
import io.element.android.libraries.textcomposer.model.SuggestionType
|
||||||
|
|
||||||
@@ -37,6 +38,7 @@ object MentionSuggestionsProcessor {
|
|||||||
* Process the mention suggestions.
|
* Process the mention suggestions.
|
||||||
* @param suggestion The current suggestion input
|
* @param suggestion The current suggestion input
|
||||||
* @param roomMembersState The room members state, it contains the current users in the room
|
* @param roomMembersState The room members state, it contains the current users in the room
|
||||||
|
* @param roomAliasSuggestions The available room alias suggestions
|
||||||
* @param currentUserId The current user id
|
* @param currentUserId The current user id
|
||||||
* @param canSendRoomMention Should return true if the current user can send room mentions
|
* @param canSendRoomMention Should return true if the current user can send room mentions
|
||||||
* @return The list of mentions to display
|
* @return The list of mentions to display
|
||||||
@@ -44,9 +46,10 @@ object MentionSuggestionsProcessor {
|
|||||||
suspend fun process(
|
suspend fun process(
|
||||||
suggestion: Suggestion?,
|
suggestion: Suggestion?,
|
||||||
roomMembersState: MatrixRoomMembersState,
|
roomMembersState: MatrixRoomMembersState,
|
||||||
|
roomAliasSuggestions: List<RoomAliasSuggestion>,
|
||||||
currentUserId: UserId,
|
currentUserId: UserId,
|
||||||
canSendRoomMention: suspend () -> Boolean,
|
canSendRoomMention: suspend () -> Boolean,
|
||||||
): List<ResolvedMentionSuggestion> {
|
): List<ResolvedSuggestion> {
|
||||||
val members = roomMembersState.roomMembers()
|
val members = roomMembersState.roomMembers()
|
||||||
return when {
|
return when {
|
||||||
members.isNullOrEmpty() || suggestion == null -> {
|
members.isNullOrEmpty() || suggestion == null -> {
|
||||||
@@ -65,6 +68,11 @@ object MentionSuggestionsProcessor {
|
|||||||
)
|
)
|
||||||
matchingMembers
|
matchingMembers
|
||||||
}
|
}
|
||||||
|
SuggestionType.Room -> {
|
||||||
|
roomAliasSuggestions
|
||||||
|
.filter { it.roomAlias.value.contains(suggestion.text, ignoreCase = true) }
|
||||||
|
.map { ResolvedSuggestion.Alias(it.roomAlias, it.roomSummary) }
|
||||||
|
}
|
||||||
else -> {
|
else -> {
|
||||||
// Clear suggestions
|
// Clear suggestions
|
||||||
emptyList()
|
emptyList()
|
||||||
@@ -79,7 +87,7 @@ object MentionSuggestionsProcessor {
|
|||||||
roomMembers: List<RoomMember>?,
|
roomMembers: List<RoomMember>?,
|
||||||
currentUserId: UserId,
|
currentUserId: UserId,
|
||||||
canSendRoomMention: Boolean,
|
canSendRoomMention: Boolean,
|
||||||
): List<ResolvedMentionSuggestion> {
|
): List<ResolvedSuggestion> {
|
||||||
return if (roomMembers.isNullOrEmpty()) {
|
return if (roomMembers.isNullOrEmpty()) {
|
||||||
emptyList()
|
emptyList()
|
||||||
} else {
|
} else {
|
||||||
@@ -97,10 +105,10 @@ object MentionSuggestionsProcessor {
|
|||||||
.filterUpTo(MAX_BATCH_ITEMS) { member ->
|
.filterUpTo(MAX_BATCH_ITEMS) { member ->
|
||||||
isJoinedMemberAndNotSelf(member) && memberMatchesQuery(member, query)
|
isJoinedMemberAndNotSelf(member) && memberMatchesQuery(member, query)
|
||||||
}
|
}
|
||||||
.map(ResolvedMentionSuggestion::Member)
|
.map(ResolvedSuggestion::Member)
|
||||||
|
|
||||||
if ("room".contains(query) && canSendRoomMention) {
|
if ("room".contains(query) && canSendRoomMention) {
|
||||||
listOf(ResolvedMentionSuggestion.AtRoom) + matchingMembers
|
listOf(ResolvedSuggestion.AtRoom) + matchingMembers
|
||||||
} else {
|
} else {
|
||||||
matchingMembers
|
matchingMembers
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import androidx.compose.foundation.layout.padding
|
|||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
@@ -39,38 +40,41 @@ import io.element.android.libraries.designsystem.preview.ElementPreview
|
|||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
|
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
|
import io.element.android.libraries.matrix.api.core.RoomAlias
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
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
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMembershipState
|
import io.element.android.libraries.matrix.api.room.RoomMembershipState
|
||||||
import io.element.android.libraries.textcomposer.mentions.ResolvedMentionSuggestion
|
import io.element.android.libraries.matrix.ui.components.aRoomSummaryDetails
|
||||||
|
import io.element.android.libraries.textcomposer.mentions.ResolvedSuggestion
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MentionSuggestionsPickerView(
|
fun SuggestionsPickerView(
|
||||||
roomId: RoomId,
|
roomId: RoomId,
|
||||||
roomName: String?,
|
roomName: String?,
|
||||||
roomAvatarData: AvatarData?,
|
roomAvatarData: AvatarData?,
|
||||||
memberSuggestions: ImmutableList<ResolvedMentionSuggestion>,
|
suggestions: ImmutableList<ResolvedSuggestion>,
|
||||||
onSelectSuggestion: (ResolvedMentionSuggestion) -> Unit,
|
onSelectSuggestion: (ResolvedSuggestion) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = modifier.fillMaxWidth(),
|
modifier = modifier.fillMaxWidth(),
|
||||||
) {
|
) {
|
||||||
items(
|
items(
|
||||||
memberSuggestions,
|
suggestions,
|
||||||
key = { suggestion ->
|
key = { suggestion ->
|
||||||
when (suggestion) {
|
when (suggestion) {
|
||||||
is ResolvedMentionSuggestion.AtRoom -> "@room"
|
is ResolvedSuggestion.AtRoom -> "@room"
|
||||||
is ResolvedMentionSuggestion.Member -> suggestion.roomMember.userId.value
|
is ResolvedSuggestion.Member -> suggestion.roomMember.userId.value
|
||||||
|
is ResolvedSuggestion.Alias -> suggestion.roomAlias.value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
Column(modifier = Modifier.fillParentMaxWidth()) {
|
Column(modifier = Modifier.fillParentMaxWidth()) {
|
||||||
RoomMemberSuggestionItemView(
|
SuggestionItemView(
|
||||||
memberSuggestion = it,
|
suggestion = it,
|
||||||
roomId = roomId.value,
|
roomId = roomId.value,
|
||||||
roomName = roomName,
|
roomName = roomName,
|
||||||
roomAvatar = roomAvatarData,
|
roomAvatar = roomAvatarData,
|
||||||
@@ -84,33 +88,44 @@ fun MentionSuggestionsPickerView(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun RoomMemberSuggestionItemView(
|
private fun SuggestionItemView(
|
||||||
memberSuggestion: ResolvedMentionSuggestion,
|
suggestion: ResolvedSuggestion,
|
||||||
roomId: String,
|
roomId: String,
|
||||||
roomName: String?,
|
roomName: String?,
|
||||||
roomAvatar: AvatarData?,
|
roomAvatar: AvatarData?,
|
||||||
onSelectSuggestion: (ResolvedMentionSuggestion) -> Unit,
|
onSelectSuggestion: (ResolvedSuggestion) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
Row(modifier = modifier.clickable { onSelectSuggestion(memberSuggestion) }, horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
Row(
|
||||||
val avatarData = when (memberSuggestion) {
|
modifier = modifier.clickable { onSelectSuggestion(suggestion) },
|
||||||
is ResolvedMentionSuggestion.AtRoom -> roomAvatar?.copy(size = AvatarSize.Suggestion)
|
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
?: AvatarData(roomId, roomName, null, AvatarSize.Suggestion)
|
) {
|
||||||
is ResolvedMentionSuggestion.Member -> AvatarData(
|
val avatarSize = AvatarSize.Suggestion
|
||||||
id = memberSuggestion.roomMember.userId.value,
|
val avatarData = when (suggestion) {
|
||||||
name = memberSuggestion.roomMember.displayName,
|
is ResolvedSuggestion.AtRoom -> roomAvatar?.copy(size = avatarSize) ?: AvatarData(roomId, roomName, null, avatarSize)
|
||||||
url = memberSuggestion.roomMember.avatarUrl,
|
is ResolvedSuggestion.Member -> AvatarData(
|
||||||
size = AvatarSize.Suggestion,
|
suggestion.roomMember.userId.value,
|
||||||
|
suggestion.roomMember.displayName,
|
||||||
|
suggestion.roomMember.avatarUrl,
|
||||||
|
avatarSize,
|
||||||
|
)
|
||||||
|
is ResolvedSuggestion.Alias -> AvatarData(
|
||||||
|
suggestion.roomSummary.roomId.value,
|
||||||
|
suggestion.roomSummary.name,
|
||||||
|
suggestion.roomSummary.avatarUrl,
|
||||||
|
avatarSize,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
val title = when (memberSuggestion) {
|
val title = when (suggestion) {
|
||||||
is ResolvedMentionSuggestion.AtRoom -> stringResource(R.string.screen_room_mentions_at_room_title)
|
is ResolvedSuggestion.AtRoom -> stringResource(R.string.screen_room_mentions_at_room_title)
|
||||||
is ResolvedMentionSuggestion.Member -> memberSuggestion.roomMember.displayName
|
is ResolvedSuggestion.Member -> suggestion.roomMember.displayName
|
||||||
|
is ResolvedSuggestion.Alias -> suggestion.roomSummary.name
|
||||||
}
|
}
|
||||||
|
|
||||||
val subtitle = when (memberSuggestion) {
|
val subtitle = when (suggestion) {
|
||||||
is ResolvedMentionSuggestion.AtRoom -> "@room"
|
is ResolvedSuggestion.AtRoom -> "@room"
|
||||||
is ResolvedMentionSuggestion.Member -> memberSuggestion.roomMember.userId.value
|
is ResolvedSuggestion.Member -> suggestion.roomMember.userId.value
|
||||||
|
is ResolvedSuggestion.Alias -> suggestion.roomAlias.value
|
||||||
}
|
}
|
||||||
|
|
||||||
Avatar(avatarData = avatarData, modifier = Modifier.padding(start = 16.dp, top = 12.dp, bottom = 12.dp))
|
Avatar(avatarData = avatarData, modifier = Modifier.padding(start = 16.dp, top = 12.dp, bottom = 12.dp))
|
||||||
@@ -142,7 +157,7 @@ private fun RoomMemberSuggestionItemView(
|
|||||||
|
|
||||||
@PreviewsDayNight
|
@PreviewsDayNight
|
||||||
@Composable
|
@Composable
|
||||||
internal fun MentionSuggestionsPickerViewPreview() {
|
internal fun SuggestionsPickerViewPreview() {
|
||||||
ElementPreview {
|
ElementPreview {
|
||||||
val roomMember = RoomMember(
|
val roomMember = RoomMember(
|
||||||
userId = UserId("@alice:server.org"),
|
userId = UserId("@alice:server.org"),
|
||||||
@@ -155,14 +170,24 @@ internal fun MentionSuggestionsPickerViewPreview() {
|
|||||||
isIgnored = false,
|
isIgnored = false,
|
||||||
role = RoomMember.Role.USER,
|
role = RoomMember.Role.USER,
|
||||||
)
|
)
|
||||||
MentionSuggestionsPickerView(
|
val anAlias = remember { RoomAlias("#room:domain.org") }
|
||||||
|
val roomSummaryDetails = remember {
|
||||||
|
aRoomSummaryDetails(
|
||||||
|
name = "My room",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
SuggestionsPickerView(
|
||||||
roomId = RoomId("!room:matrix.org"),
|
roomId = RoomId("!room:matrix.org"),
|
||||||
roomName = "Room",
|
roomName = "Room",
|
||||||
roomAvatarData = null,
|
roomAvatarData = null,
|
||||||
memberSuggestions = persistentListOf(
|
suggestions = persistentListOf(
|
||||||
ResolvedMentionSuggestion.AtRoom,
|
ResolvedSuggestion.AtRoom,
|
||||||
ResolvedMentionSuggestion.Member(roomMember),
|
ResolvedSuggestion.Member(roomMember),
|
||||||
ResolvedMentionSuggestion.Member(roomMember.copy(userId = UserId("@bob:server.org"), displayName = "Bob")),
|
ResolvedSuggestion.Member(roomMember.copy(userId = UserId("@bob:server.org"), displayName = "Bob")),
|
||||||
|
ResolvedSuggestion.Alias(
|
||||||
|
anAlias,
|
||||||
|
roomSummaryDetails,
|
||||||
|
)
|
||||||
),
|
),
|
||||||
onSelectSuggestion = {}
|
onSelectSuggestion = {}
|
||||||
)
|
)
|
||||||
@@ -18,7 +18,7 @@ package io.element.android.features.messages.impl.messagecomposer
|
|||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import io.element.android.libraries.textcomposer.mentions.ResolvedMentionSuggestion
|
import io.element.android.libraries.textcomposer.mentions.ResolvedSuggestion
|
||||||
import io.element.android.libraries.textcomposer.model.MessageComposerMode
|
import io.element.android.libraries.textcomposer.model.MessageComposerMode
|
||||||
import io.element.android.libraries.textcomposer.model.Suggestion
|
import io.element.android.libraries.textcomposer.model.Suggestion
|
||||||
|
|
||||||
@@ -44,6 +44,6 @@ sealed interface MessageComposerEvents {
|
|||||||
data class Error(val error: Throwable) : MessageComposerEvents
|
data class Error(val error: Throwable) : MessageComposerEvents
|
||||||
data class TypingNotice(val isTyping: Boolean) : MessageComposerEvents
|
data class TypingNotice(val isTyping: Boolean) : MessageComposerEvents
|
||||||
data class SuggestionReceived(val suggestion: Suggestion?) : MessageComposerEvents
|
data class SuggestionReceived(val suggestion: Suggestion?) : MessageComposerEvents
|
||||||
data class InsertMention(val mention: ResolvedMentionSuggestion) : MessageComposerEvents
|
data class InsertSuggestion(val resolvedSuggestion: ResolvedSuggestion) : MessageComposerEvents
|
||||||
data object SaveDraft : MessageComposerEvents
|
data object SaveDraft : MessageComposerEvents
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,8 +55,8 @@ import io.element.android.libraries.matrix.api.core.UserId
|
|||||||
import io.element.android.libraries.matrix.api.permalink.PermalinkBuilder
|
import io.element.android.libraries.matrix.api.permalink.PermalinkBuilder
|
||||||
import io.element.android.libraries.matrix.api.permalink.PermalinkData
|
import io.element.android.libraries.matrix.api.permalink.PermalinkData
|
||||||
import io.element.android.libraries.matrix.api.permalink.PermalinkParser
|
import io.element.android.libraries.matrix.api.permalink.PermalinkParser
|
||||||
|
import io.element.android.libraries.matrix.api.room.IntentionalMention
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
import io.element.android.libraries.matrix.api.room.Mention
|
|
||||||
import io.element.android.libraries.matrix.api.room.draft.ComposerDraft
|
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.draft.ComposerDraftType
|
||||||
import io.element.android.libraries.matrix.api.room.isDm
|
import io.element.android.libraries.matrix.api.room.isDm
|
||||||
@@ -72,7 +72,7 @@ import io.element.android.libraries.permissions.api.PermissionsPresenter
|
|||||||
import io.element.android.libraries.preferences.api.store.SessionPreferencesStore
|
import io.element.android.libraries.preferences.api.store.SessionPreferencesStore
|
||||||
import io.element.android.libraries.textcomposer.mentions.LocalMentionSpanTheme
|
import io.element.android.libraries.textcomposer.mentions.LocalMentionSpanTheme
|
||||||
import io.element.android.libraries.textcomposer.mentions.MentionSpanProvider
|
import io.element.android.libraries.textcomposer.mentions.MentionSpanProvider
|
||||||
import io.element.android.libraries.textcomposer.mentions.ResolvedMentionSuggestion
|
import io.element.android.libraries.textcomposer.mentions.ResolvedSuggestion
|
||||||
import io.element.android.libraries.textcomposer.model.MarkdownTextEditorState
|
import io.element.android.libraries.textcomposer.model.MarkdownTextEditorState
|
||||||
import io.element.android.libraries.textcomposer.model.Message
|
import io.element.android.libraries.textcomposer.model.Message
|
||||||
import io.element.android.libraries.textcomposer.model.MessageComposerMode
|
import io.element.android.libraries.textcomposer.model.MessageComposerMode
|
||||||
@@ -117,6 +117,7 @@ class MessageComposerPresenter @Inject constructor(
|
|||||||
private val analyticsService: AnalyticsService,
|
private val analyticsService: AnalyticsService,
|
||||||
private val messageComposerContext: DefaultMessageComposerContext,
|
private val messageComposerContext: DefaultMessageComposerContext,
|
||||||
private val richTextEditorStateFactory: RichTextEditorStateFactory,
|
private val richTextEditorStateFactory: RichTextEditorStateFactory,
|
||||||
|
private val roomAliasSuggestionsDataSource: RoomAliasSuggestionsDataSource,
|
||||||
private val permalinkParser: PermalinkParser,
|
private val permalinkParser: PermalinkParser,
|
||||||
private val permalinkBuilder: PermalinkBuilder,
|
private val permalinkBuilder: PermalinkBuilder,
|
||||||
permissionsPresenterFactory: PermissionsPresenter.Factory,
|
permissionsPresenterFactory: PermissionsPresenter.Factory,
|
||||||
@@ -189,6 +190,8 @@ class MessageComposerPresenter @Inject constructor(
|
|||||||
|
|
||||||
val sendTypingNotifications by sessionPreferencesStore.isSendTypingNotificationsEnabled().collectAsState(initial = true)
|
val sendTypingNotifications by sessionPreferencesStore.isSendTypingNotificationsEnabled().collectAsState(initial = true)
|
||||||
|
|
||||||
|
val roomAliasSuggestions by roomAliasSuggestionsDataSource.getAllRoomAliasSuggestions().collectAsState(initial = emptyList())
|
||||||
|
|
||||||
LaunchedEffect(attachmentsState.value) {
|
LaunchedEffect(attachmentsState.value) {
|
||||||
when (val attachmentStateValue = attachmentsState.value) {
|
when (val attachmentStateValue = attachmentsState.value) {
|
||||||
is AttachmentsState.Sending.Processing -> {
|
is AttachmentsState.Sending.Processing -> {
|
||||||
@@ -212,7 +215,7 @@ class MessageComposerPresenter @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val memberSuggestions = remember { mutableStateListOf<ResolvedMentionSuggestion>() }
|
val suggestions = remember { mutableStateListOf<ResolvedSuggestion>() }
|
||||||
LaunchedEffect(isMentionsEnabled) {
|
LaunchedEffect(isMentionsEnabled) {
|
||||||
if (!isMentionsEnabled) return@LaunchedEffect
|
if (!isMentionsEnabled) return@LaunchedEffect
|
||||||
val currentUserId = room.sessionId
|
val currentUserId = room.sessionId
|
||||||
@@ -228,15 +231,16 @@ class MessageComposerPresenter @Inject constructor(
|
|||||||
val mentionCompletionTrigger = suggestionSearchTrigger.debounce(0.3.seconds).filter { !it?.text.isNullOrEmpty() }
|
val mentionCompletionTrigger = suggestionSearchTrigger.debounce(0.3.seconds).filter { !it?.text.isNullOrEmpty() }
|
||||||
merge(mentionStartTrigger, mentionCompletionTrigger)
|
merge(mentionStartTrigger, mentionCompletionTrigger)
|
||||||
.combine(room.membersStateFlow) { suggestion, roomMembersState ->
|
.combine(room.membersStateFlow) { suggestion, roomMembersState ->
|
||||||
memberSuggestions.clear()
|
suggestions.clear()
|
||||||
val result = MentionSuggestionsProcessor.process(
|
val result = MentionSuggestionsProcessor.process(
|
||||||
suggestion = suggestion,
|
suggestion = suggestion,
|
||||||
roomMembersState = roomMembersState,
|
roomMembersState = roomMembersState,
|
||||||
|
roomAliasSuggestions = roomAliasSuggestions,
|
||||||
currentUserId = currentUserId,
|
currentUserId = currentUserId,
|
||||||
canSendRoomMention = ::canSendRoomMention,
|
canSendRoomMention = ::canSendRoomMention,
|
||||||
)
|
)
|
||||||
if (result.isNotEmpty()) {
|
if (result.isNotEmpty()) {
|
||||||
memberSuggestions.addAll(result)
|
suggestions.addAll(result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.collect()
|
.collect()
|
||||||
@@ -362,22 +366,27 @@ class MessageComposerPresenter @Inject constructor(
|
|||||||
is MessageComposerEvents.SuggestionReceived -> {
|
is MessageComposerEvents.SuggestionReceived -> {
|
||||||
suggestionSearchTrigger.value = event.suggestion
|
suggestionSearchTrigger.value = event.suggestion
|
||||||
}
|
}
|
||||||
is MessageComposerEvents.InsertMention -> {
|
is MessageComposerEvents.InsertSuggestion -> {
|
||||||
localCoroutineScope.launch {
|
localCoroutineScope.launch {
|
||||||
if (showTextFormatting) {
|
if (showTextFormatting) {
|
||||||
when (val mention = event.mention) {
|
when (val suggestion = event.resolvedSuggestion) {
|
||||||
is ResolvedMentionSuggestion.AtRoom -> {
|
is ResolvedSuggestion.AtRoom -> {
|
||||||
richTextEditorState.insertAtRoomMentionAtSuggestion()
|
richTextEditorState.insertAtRoomMentionAtSuggestion()
|
||||||
}
|
}
|
||||||
is ResolvedMentionSuggestion.Member -> {
|
is ResolvedSuggestion.Member -> {
|
||||||
val text = mention.roomMember.userId.value
|
val text = suggestion.roomMember.userId.value
|
||||||
val link = permalinkBuilder.permalinkForUser(mention.roomMember.userId).getOrNull() ?: return@launch
|
val link = permalinkBuilder.permalinkForUser(suggestion.roomMember.userId).getOrNull() ?: return@launch
|
||||||
|
richTextEditorState.insertMentionAtSuggestion(text = text, link = link)
|
||||||
|
}
|
||||||
|
is ResolvedSuggestion.Alias -> {
|
||||||
|
val text = suggestion.roomAlias.value
|
||||||
|
val link = permalinkBuilder.permalinkForRoomAlias(suggestion.roomAlias).getOrNull() ?: return@launch
|
||||||
richTextEditorState.insertMentionAtSuggestion(text = text, link = link)
|
richTextEditorState.insertMentionAtSuggestion(text = text, link = link)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (markdownTextEditorState.currentMentionSuggestion != null) {
|
} else if (markdownTextEditorState.currentSuggestion != null) {
|
||||||
markdownTextEditorState.insertMention(
|
markdownTextEditorState.insertSuggestion(
|
||||||
mention = event.mention,
|
resolvedSuggestion = event.resolvedSuggestion,
|
||||||
mentionSpanProvider = mentionSpanProvider,
|
mentionSpanProvider = mentionSpanProvider,
|
||||||
permalinkBuilder = permalinkBuilder,
|
permalinkBuilder = permalinkBuilder,
|
||||||
)
|
)
|
||||||
@@ -417,7 +426,7 @@ class MessageComposerPresenter @Inject constructor(
|
|||||||
canShareLocation = canShareLocation.value,
|
canShareLocation = canShareLocation.value,
|
||||||
canCreatePoll = canCreatePoll.value,
|
canCreatePoll = canCreatePoll.value,
|
||||||
attachmentsState = attachmentsState.value,
|
attachmentsState = attachmentsState.value,
|
||||||
memberSuggestions = memberSuggestions.toPersistentList(),
|
suggestions = suggestions.toPersistentList(),
|
||||||
resolveMentionDisplay = resolveMentionDisplay,
|
resolveMentionDisplay = resolveMentionDisplay,
|
||||||
eventSink = { handleEvents(it) },
|
eventSink = { handleEvents(it) },
|
||||||
)
|
)
|
||||||
@@ -432,17 +441,21 @@ class MessageComposerPresenter @Inject constructor(
|
|||||||
// Reset composer right away
|
// Reset composer right away
|
||||||
resetComposer(markdownTextEditorState, richTextEditorState, fromEdit = capturedMode is MessageComposerMode.Edit)
|
resetComposer(markdownTextEditorState, richTextEditorState, fromEdit = capturedMode is MessageComposerMode.Edit)
|
||||||
when (capturedMode) {
|
when (capturedMode) {
|
||||||
is MessageComposerMode.Normal -> room.sendMessage(body = message.markdown, htmlBody = message.html, mentions = message.mentions)
|
is MessageComposerMode.Normal -> room.sendMessage(
|
||||||
|
body = message.markdown,
|
||||||
|
htmlBody = message.html,
|
||||||
|
intentionalMentions = message.intentionalMentions
|
||||||
|
)
|
||||||
is MessageComposerMode.Edit -> {
|
is MessageComposerMode.Edit -> {
|
||||||
val eventId = capturedMode.eventId
|
val eventId = capturedMode.eventId
|
||||||
val transactionId = capturedMode.transactionId
|
val transactionId = capturedMode.transactionId
|
||||||
timelineController.invokeOnCurrentTimeline {
|
timelineController.invokeOnCurrentTimeline {
|
||||||
// First try to edit the message in the current timeline
|
// First try to edit the message in the current timeline
|
||||||
editMessage(eventId, transactionId, message.markdown, message.html, message.mentions)
|
editMessage(eventId, transactionId, message.markdown, message.html, message.intentionalMentions)
|
||||||
.onFailure { cause ->
|
.onFailure { cause ->
|
||||||
if (cause is TimelineException.EventNotFound && eventId != null) {
|
if (cause is TimelineException.EventNotFound && eventId != null) {
|
||||||
// if the event is not found in the timeline, try to edit the message directly
|
// if the event is not found in the timeline, try to edit the message directly
|
||||||
room.editMessage(eventId, message.markdown, message.html, message.mentions)
|
room.editMessage(eventId, message.markdown, message.html, message.intentionalMentions)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -450,7 +463,7 @@ class MessageComposerPresenter @Inject constructor(
|
|||||||
|
|
||||||
is MessageComposerMode.Reply -> {
|
is MessageComposerMode.Reply -> {
|
||||||
timelineController.invokeOnCurrentTimeline {
|
timelineController.invokeOnCurrentTimeline {
|
||||||
replyMessage(capturedMode.eventId, message.markdown, message.html, message.mentions)
|
replyMessage(capturedMode.eventId, message.markdown, message.html, message.intentionalMentions)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -623,15 +636,15 @@ class MessageComposerPresenter @Inject constructor(
|
|||||||
?.let { state ->
|
?.let { state ->
|
||||||
buildList {
|
buildList {
|
||||||
if (state.hasAtRoomMention) {
|
if (state.hasAtRoomMention) {
|
||||||
add(Mention.AtRoom)
|
add(IntentionalMention.Room)
|
||||||
}
|
}
|
||||||
for (userId in state.userIds) {
|
for (userId in state.userIds) {
|
||||||
add(Mention.User(UserId(userId)))
|
add(IntentionalMention.User(UserId(userId)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.orEmpty()
|
.orEmpty()
|
||||||
Message(html = html, markdown = markdown, mentions = mentions)
|
Message(html = html, markdown = markdown, intentionalMentions = mentions)
|
||||||
} else {
|
} else {
|
||||||
val markdown = markdownTextEditorState.getMessageMarkdown(permalinkBuilder)
|
val markdown = markdownTextEditorState.getMessageMarkdown(permalinkBuilder)
|
||||||
val mentions = if (withMentions) {
|
val mentions = if (withMentions) {
|
||||||
@@ -639,7 +652,7 @@ class MessageComposerPresenter @Inject constructor(
|
|||||||
} else {
|
} else {
|
||||||
emptyList()
|
emptyList()
|
||||||
}
|
}
|
||||||
Message(html = null, markdown = markdown, mentions = mentions)
|
Message(html = null, markdown = markdown, intentionalMentions = mentions)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ package io.element.android.features.messages.impl.messagecomposer
|
|||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import androidx.compose.runtime.Stable
|
import androidx.compose.runtime.Stable
|
||||||
import io.element.android.features.messages.impl.attachments.Attachment
|
import io.element.android.features.messages.impl.attachments.Attachment
|
||||||
import io.element.android.libraries.textcomposer.mentions.ResolvedMentionSuggestion
|
import io.element.android.libraries.textcomposer.mentions.ResolvedSuggestion
|
||||||
import io.element.android.libraries.textcomposer.model.MessageComposerMode
|
import io.element.android.libraries.textcomposer.model.MessageComposerMode
|
||||||
import io.element.android.libraries.textcomposer.model.TextEditorState
|
import io.element.android.libraries.textcomposer.model.TextEditorState
|
||||||
import io.element.android.wysiwyg.display.TextDisplay
|
import io.element.android.wysiwyg.display.TextDisplay
|
||||||
@@ -35,7 +35,7 @@ data class MessageComposerState(
|
|||||||
val canShareLocation: Boolean,
|
val canShareLocation: Boolean,
|
||||||
val canCreatePoll: Boolean,
|
val canCreatePoll: Boolean,
|
||||||
val attachmentsState: AttachmentsState,
|
val attachmentsState: AttachmentsState,
|
||||||
val memberSuggestions: ImmutableList<ResolvedMentionSuggestion>,
|
val suggestions: ImmutableList<ResolvedSuggestion>,
|
||||||
val resolveMentionDisplay: (String, String) -> TextDisplay,
|
val resolveMentionDisplay: (String, String) -> TextDisplay,
|
||||||
val eventSink: (MessageComposerEvents) -> Unit,
|
val eventSink: (MessageComposerEvents) -> Unit,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ package io.element.android.features.messages.impl.messagecomposer
|
|||||||
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
import io.element.android.libraries.textcomposer.aRichTextEditorState
|
import io.element.android.libraries.textcomposer.aRichTextEditorState
|
||||||
import io.element.android.libraries.textcomposer.mentions.ResolvedMentionSuggestion
|
import io.element.android.libraries.textcomposer.mentions.ResolvedSuggestion
|
||||||
import io.element.android.libraries.textcomposer.model.MessageComposerMode
|
import io.element.android.libraries.textcomposer.model.MessageComposerMode
|
||||||
import io.element.android.libraries.textcomposer.model.TextEditorState
|
import io.element.android.libraries.textcomposer.model.TextEditorState
|
||||||
import io.element.android.wysiwyg.display.TextDisplay
|
import io.element.android.wysiwyg.display.TextDisplay
|
||||||
@@ -41,7 +41,7 @@ fun aMessageComposerState(
|
|||||||
canShareLocation: Boolean = true,
|
canShareLocation: Boolean = true,
|
||||||
canCreatePoll: Boolean = true,
|
canCreatePoll: Boolean = true,
|
||||||
attachmentsState: AttachmentsState = AttachmentsState.None,
|
attachmentsState: AttachmentsState = AttachmentsState.None,
|
||||||
memberSuggestions: ImmutableList<ResolvedMentionSuggestion> = persistentListOf(),
|
suggestions: ImmutableList<ResolvedSuggestion> = persistentListOf(),
|
||||||
) = MessageComposerState(
|
) = MessageComposerState(
|
||||||
textEditorState = textEditorState,
|
textEditorState = textEditorState,
|
||||||
isFullScreen = isFullScreen,
|
isFullScreen = isFullScreen,
|
||||||
@@ -51,7 +51,7 @@ fun aMessageComposerState(
|
|||||||
canShareLocation = canShareLocation,
|
canShareLocation = canShareLocation,
|
||||||
canCreatePoll = canCreatePoll,
|
canCreatePoll = canCreatePoll,
|
||||||
attachmentsState = attachmentsState,
|
attachmentsState = attachmentsState,
|
||||||
memberSuggestions = memberSuggestions,
|
suggestions = suggestions,
|
||||||
resolveMentionDisplay = { _, _ -> TextDisplay.Plain },
|
resolveMentionDisplay = { _, _ -> TextDisplay.Plain },
|
||||||
eventSink = {},
|
eventSink = {},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2024 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.messagecomposer
|
||||||
|
|
||||||
|
import com.squareup.anvil.annotations.ContributesBinding
|
||||||
|
import io.element.android.libraries.di.SessionScope
|
||||||
|
import io.element.android.libraries.matrix.api.core.RoomAlias
|
||||||
|
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
||||||
|
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
data class RoomAliasSuggestion(
|
||||||
|
val roomAlias: RoomAlias,
|
||||||
|
val roomSummary: RoomSummary,
|
||||||
|
)
|
||||||
|
|
||||||
|
interface RoomAliasSuggestionsDataSource {
|
||||||
|
fun getAllRoomAliasSuggestions(): Flow<List<RoomAliasSuggestion>>
|
||||||
|
}
|
||||||
|
|
||||||
|
@ContributesBinding(SessionScope::class)
|
||||||
|
class DefaultRoomAliasSuggestionsDataSource @Inject constructor(
|
||||||
|
private val roomListService: RoomListService,
|
||||||
|
) : RoomAliasSuggestionsDataSource {
|
||||||
|
override fun getAllRoomAliasSuggestions(): Flow<List<RoomAliasSuggestion>> {
|
||||||
|
return roomListService
|
||||||
|
.allRooms
|
||||||
|
.filteredSummaries
|
||||||
|
.map { roomSummaries ->
|
||||||
|
roomSummaries
|
||||||
|
.mapNotNull { roomSummary ->
|
||||||
|
roomSummary.canonicalAlias?.let { roomAlias ->
|
||||||
|
RoomAliasSuggestion(
|
||||||
|
roomAlias = roomAlias,
|
||||||
|
roomSummary = roomSummary,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -29,9 +29,10 @@ import io.element.android.features.messages.impl.draft.FakeComposerDraftService
|
|||||||
import io.element.android.features.messages.impl.fixtures.aMessageEvent
|
import io.element.android.features.messages.impl.fixtures.aMessageEvent
|
||||||
import io.element.android.features.messages.impl.fixtures.aTimelineItemsFactory
|
import io.element.android.features.messages.impl.fixtures.aTimelineItemsFactory
|
||||||
import io.element.android.features.messages.impl.messagecomposer.DefaultMessageComposerContext
|
import io.element.android.features.messages.impl.messagecomposer.DefaultMessageComposerContext
|
||||||
|
import io.element.android.features.messages.impl.messagecomposer.FakeRoomAliasSuggestionsDataSource
|
||||||
import io.element.android.features.messages.impl.messagecomposer.MessageComposerPresenter
|
import io.element.android.features.messages.impl.messagecomposer.MessageComposerPresenter
|
||||||
|
import io.element.android.features.messages.impl.messagecomposer.TestRichTextEditorStateFactory
|
||||||
import io.element.android.features.messages.impl.pinned.banner.aLoadedPinnedMessagesBannerState
|
import io.element.android.features.messages.impl.pinned.banner.aLoadedPinnedMessagesBannerState
|
||||||
import io.element.android.features.messages.impl.textcomposer.TestRichTextEditorStateFactory
|
|
||||||
import io.element.android.features.messages.impl.timeline.TimelineController
|
import io.element.android.features.messages.impl.timeline.TimelineController
|
||||||
import io.element.android.features.messages.impl.timeline.TimelineItemIndexer
|
import io.element.android.features.messages.impl.timeline.TimelineItemIndexer
|
||||||
import io.element.android.features.messages.impl.timeline.TimelinePresenter
|
import io.element.android.features.messages.impl.timeline.TimelinePresenter
|
||||||
@@ -1008,6 +1009,7 @@ class MessagesPresenterTest {
|
|||||||
analyticsService = analyticsService,
|
analyticsService = analyticsService,
|
||||||
messageComposerContext = DefaultMessageComposerContext(),
|
messageComposerContext = DefaultMessageComposerContext(),
|
||||||
richTextEditorStateFactory = TestRichTextEditorStateFactory(),
|
richTextEditorStateFactory = TestRichTextEditorStateFactory(),
|
||||||
|
roomAliasSuggestionsDataSource = FakeRoomAliasSuggestionsDataSource(),
|
||||||
permissionsPresenterFactory = permissionsPresenterFactory,
|
permissionsPresenterFactory = permissionsPresenterFactory,
|
||||||
permalinkParser = FakePermalinkParser(),
|
permalinkParser = FakePermalinkParser(),
|
||||||
permalinkBuilder = FakePermalinkBuilder(),
|
permalinkBuilder = FakePermalinkBuilder(),
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2024 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.messagecomposer
|
||||||
|
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
|
||||||
|
class FakeRoomAliasSuggestionsDataSource(
|
||||||
|
initialData: List<RoomAliasSuggestion> = emptyList()
|
||||||
|
) : RoomAliasSuggestionsDataSource {
|
||||||
|
private val roomAliasSuggestions = MutableStateFlow(initialData)
|
||||||
|
|
||||||
|
override fun getAllRoomAliasSuggestions(): Flow<List<RoomAliasSuggestion>> {
|
||||||
|
return roomAliasSuggestions
|
||||||
|
}
|
||||||
|
|
||||||
|
fun emitRoomAliasSuggestions(newData: List<RoomAliasSuggestion>) {
|
||||||
|
roomAliasSuggestions.value = newData
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
@file:OptIn(ExperimentalCoroutinesApi::class)
|
@file:OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
|
||||||
package io.element.android.features.messages.impl.textcomposer
|
package io.element.android.features.messages.impl.messagecomposer
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
@@ -29,11 +29,6 @@ import im.vector.app.features.analytics.plan.Composer
|
|||||||
import im.vector.app.features.analytics.plan.Interaction
|
import im.vector.app.features.analytics.plan.Interaction
|
||||||
import io.element.android.features.messages.impl.draft.ComposerDraftService
|
import io.element.android.features.messages.impl.draft.ComposerDraftService
|
||||||
import io.element.android.features.messages.impl.draft.FakeComposerDraftService
|
import io.element.android.features.messages.impl.draft.FakeComposerDraftService
|
||||||
import io.element.android.features.messages.impl.messagecomposer.AttachmentsState
|
|
||||||
import io.element.android.features.messages.impl.messagecomposer.DefaultMessageComposerContext
|
|
||||||
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.timeline.TimelineController
|
import io.element.android.features.messages.impl.timeline.TimelineController
|
||||||
import io.element.android.features.messages.impl.utils.FakeTextPillificationHelper
|
import io.element.android.features.messages.impl.utils.FakeTextPillificationHelper
|
||||||
import io.element.android.features.messages.impl.utils.TextPillificationHelper
|
import io.element.android.features.messages.impl.utils.TextPillificationHelper
|
||||||
@@ -52,7 +47,7 @@ import io.element.android.libraries.matrix.api.permalink.PermalinkBuilder
|
|||||||
import io.element.android.libraries.matrix.api.permalink.PermalinkParser
|
import io.element.android.libraries.matrix.api.permalink.PermalinkParser
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
|
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
|
||||||
import io.element.android.libraries.matrix.api.room.Mention
|
import io.element.android.libraries.matrix.api.room.IntentionalMention
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMembershipState
|
import io.element.android.libraries.matrix.api.room.RoomMembershipState
|
||||||
import io.element.android.libraries.matrix.api.room.draft.ComposerDraft
|
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.draft.ComposerDraftType
|
||||||
@@ -90,7 +85,7 @@ import io.element.android.libraries.permissions.test.FakePermissionsPresenterFac
|
|||||||
import io.element.android.libraries.preferences.api.store.SessionPreferencesStore
|
import io.element.android.libraries.preferences.api.store.SessionPreferencesStore
|
||||||
import io.element.android.libraries.preferences.test.InMemorySessionPreferencesStore
|
import io.element.android.libraries.preferences.test.InMemorySessionPreferencesStore
|
||||||
import io.element.android.libraries.textcomposer.mentions.MentionSpanProvider
|
import io.element.android.libraries.textcomposer.mentions.MentionSpanProvider
|
||||||
import io.element.android.libraries.textcomposer.mentions.ResolvedMentionSuggestion
|
import io.element.android.libraries.textcomposer.mentions.ResolvedSuggestion
|
||||||
import io.element.android.libraries.textcomposer.model.MessageComposerMode
|
import io.element.android.libraries.textcomposer.model.MessageComposerMode
|
||||||
import io.element.android.libraries.textcomposer.model.Suggestion
|
import io.element.android.libraries.textcomposer.model.Suggestion
|
||||||
import io.element.android.libraries.textcomposer.model.SuggestionType
|
import io.element.android.libraries.textcomposer.model.SuggestionType
|
||||||
@@ -368,7 +363,7 @@ class MessageComposerPresenterTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - edit sent message`() = runTest {
|
fun `present - edit sent message`() = runTest {
|
||||||
val editMessageLambda = lambdaRecorder { _: EventId?, _: TransactionId?, _: String, _: String?, _: List<Mention> ->
|
val editMessageLambda = lambdaRecorder { _: EventId?, _: TransactionId?, _: String, _: String?, _: List<IntentionalMention> ->
|
||||||
Result.success(Unit)
|
Result.success(Unit)
|
||||||
}
|
}
|
||||||
val timeline = FakeTimeline().apply {
|
val timeline = FakeTimeline().apply {
|
||||||
@@ -420,13 +415,13 @@ class MessageComposerPresenterTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - edit sent message event not found`() = runTest {
|
fun `present - edit sent message event not found`() = runTest {
|
||||||
val timelineEditMessageLambda = lambdaRecorder { _: EventId?, _: TransactionId?, _: String, _: String?, _: List<Mention> ->
|
val timelineEditMessageLambda = lambdaRecorder { _: EventId?, _: TransactionId?, _: String, _: String?, _: List<IntentionalMention> ->
|
||||||
Result.failure<Unit>(TimelineException.EventNotFound)
|
Result.failure<Unit>(TimelineException.EventNotFound)
|
||||||
}
|
}
|
||||||
val timeline = FakeTimeline().apply {
|
val timeline = FakeTimeline().apply {
|
||||||
this.editMessageLambda = timelineEditMessageLambda
|
this.editMessageLambda = timelineEditMessageLambda
|
||||||
}
|
}
|
||||||
val roomEditMessageLambda = lambdaRecorder { _: EventId?, _: String, _: String?, _: List<Mention> ->
|
val roomEditMessageLambda = lambdaRecorder { _: EventId?, _: String, _: String?, _: List<IntentionalMention> ->
|
||||||
Result.success(Unit)
|
Result.success(Unit)
|
||||||
}
|
}
|
||||||
val fakeMatrixRoom = FakeMatrixRoom(
|
val fakeMatrixRoom = FakeMatrixRoom(
|
||||||
@@ -480,7 +475,7 @@ class MessageComposerPresenterTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - edit not sent message`() = runTest {
|
fun `present - edit not sent message`() = runTest {
|
||||||
val editMessageLambda = lambdaRecorder { _: EventId?, _: TransactionId?, _: String, _: String?, _: List<Mention> ->
|
val editMessageLambda = lambdaRecorder { _: EventId?, _: TransactionId?, _: String, _: String?, _: List<IntentionalMention> ->
|
||||||
Result.success(Unit)
|
Result.success(Unit)
|
||||||
}
|
}
|
||||||
val timeline = FakeTimeline().apply {
|
val timeline = FakeTimeline().apply {
|
||||||
@@ -532,7 +527,7 @@ class MessageComposerPresenterTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - reply message`() = runTest {
|
fun `present - reply message`() = runTest {
|
||||||
val replyMessageLambda = lambdaRecorder { _: EventId, _: String, _: String?, _: List<Mention>, _: Boolean ->
|
val replyMessageLambda = lambdaRecorder { _: EventId, _: String, _: String?, _: List<IntentionalMention>, _: Boolean ->
|
||||||
Result.success(Unit)
|
Result.success(Unit)
|
||||||
}
|
}
|
||||||
val timeline = FakeTimeline().apply {
|
val timeline = FakeTimeline().apply {
|
||||||
@@ -974,34 +969,34 @@ class MessageComposerPresenterTest {
|
|||||||
|
|
||||||
// A null suggestion (no suggestion was received) returns nothing
|
// A null suggestion (no suggestion was received) returns nothing
|
||||||
initialState.eventSink(MessageComposerEvents.SuggestionReceived(null))
|
initialState.eventSink(MessageComposerEvents.SuggestionReceived(null))
|
||||||
assertThat(awaitItem().memberSuggestions).isEmpty()
|
assertThat(awaitItem().suggestions).isEmpty()
|
||||||
|
|
||||||
// An empty suggestion returns the room and joined members that are not the current user
|
// An empty suggestion returns the room and joined members that are not the current user
|
||||||
initialState.eventSink(MessageComposerEvents.SuggestionReceived(Suggestion(0, 0, SuggestionType.Mention, "")))
|
initialState.eventSink(MessageComposerEvents.SuggestionReceived(Suggestion(0, 0, SuggestionType.Mention, "")))
|
||||||
assertThat(awaitItem().memberSuggestions)
|
assertThat(awaitItem().suggestions)
|
||||||
.containsExactly(ResolvedMentionSuggestion.AtRoom, ResolvedMentionSuggestion.Member(bob), ResolvedMentionSuggestion.Member(david))
|
.containsExactly(ResolvedSuggestion.AtRoom, ResolvedSuggestion.Member(bob), ResolvedSuggestion.Member(david))
|
||||||
|
|
||||||
// A suggestion containing a part of "room" will also return the room mention
|
// A suggestion containing a part of "room" will also return the room mention
|
||||||
initialState.eventSink(MessageComposerEvents.SuggestionReceived(Suggestion(0, 0, SuggestionType.Mention, "roo")))
|
initialState.eventSink(MessageComposerEvents.SuggestionReceived(Suggestion(0, 0, SuggestionType.Mention, "roo")))
|
||||||
assertThat(awaitItem().memberSuggestions).containsExactly(ResolvedMentionSuggestion.AtRoom)
|
assertThat(awaitItem().suggestions).containsExactly(ResolvedSuggestion.AtRoom)
|
||||||
|
|
||||||
// A non-empty suggestion will return those joined members whose user id matches it
|
// A non-empty suggestion will return those joined members whose user id matches it
|
||||||
initialState.eventSink(MessageComposerEvents.SuggestionReceived(Suggestion(0, 0, SuggestionType.Mention, "bob")))
|
initialState.eventSink(MessageComposerEvents.SuggestionReceived(Suggestion(0, 0, SuggestionType.Mention, "bob")))
|
||||||
assertThat(awaitItem().memberSuggestions).containsExactly(ResolvedMentionSuggestion.Member(bob))
|
assertThat(awaitItem().suggestions).containsExactly(ResolvedSuggestion.Member(bob))
|
||||||
|
|
||||||
// A non-empty suggestion will return those joined members whose display name matches it
|
// A non-empty suggestion will return those joined members whose display name matches it
|
||||||
initialState.eventSink(MessageComposerEvents.SuggestionReceived(Suggestion(0, 0, SuggestionType.Mention, "dave")))
|
initialState.eventSink(MessageComposerEvents.SuggestionReceived(Suggestion(0, 0, SuggestionType.Mention, "dave")))
|
||||||
assertThat(awaitItem().memberSuggestions).containsExactly(ResolvedMentionSuggestion.Member(david))
|
assertThat(awaitItem().suggestions).containsExactly(ResolvedSuggestion.Member(david))
|
||||||
|
|
||||||
// If the suggestion isn't a mention, no suggestions are returned
|
// If the suggestion isn't a mention, no suggestions are returned
|
||||||
initialState.eventSink(MessageComposerEvents.SuggestionReceived(Suggestion(0, 0, SuggestionType.Command, "")))
|
initialState.eventSink(MessageComposerEvents.SuggestionReceived(Suggestion(0, 0, SuggestionType.Command, "")))
|
||||||
assertThat(awaitItem().memberSuggestions).isEmpty()
|
assertThat(awaitItem().suggestions).isEmpty()
|
||||||
|
|
||||||
// If user has no permission to send `@room` mentions, `RoomMemberSuggestion.Room` is not returned
|
// If user has no permission to send `@room` mentions, `RoomMemberSuggestion.Room` is not returned
|
||||||
canUserTriggerRoomNotificationResult = false
|
canUserTriggerRoomNotificationResult = false
|
||||||
initialState.eventSink(MessageComposerEvents.SuggestionReceived(Suggestion(0, 0, SuggestionType.Mention, "")))
|
initialState.eventSink(MessageComposerEvents.SuggestionReceived(Suggestion(0, 0, SuggestionType.Mention, "")))
|
||||||
assertThat(awaitItem().memberSuggestions)
|
assertThat(awaitItem().suggestions)
|
||||||
.containsExactly(ResolvedMentionSuggestion.Member(bob), ResolvedMentionSuggestion.Member(david))
|
.containsExactly(ResolvedSuggestion.Member(bob), ResolvedSuggestion.Member(david))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1039,13 +1034,12 @@ class MessageComposerPresenterTest {
|
|||||||
// An empty suggestion returns the joined members that are not the current user, but not the room
|
// 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, "")))
|
initialState.eventSink(MessageComposerEvents.SuggestionReceived(Suggestion(0, 0, SuggestionType.Mention, "")))
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
assertThat(awaitItem().memberSuggestions)
|
assertThat(awaitItem().suggestions)
|
||||||
.containsExactly(ResolvedMentionSuggestion.Member(bob), ResolvedMentionSuggestion.Member(david))
|
.containsExactly(ResolvedSuggestion.Member(bob), ResolvedSuggestion.Member(david))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
fun `present - InsertSuggestion`() = runTest {
|
||||||
fun `present - insertMention for user in rich text editor`() = runTest {
|
|
||||||
val presenter = createPresenter(
|
val presenter = createPresenter(
|
||||||
coroutineScope = this,
|
coroutineScope = this,
|
||||||
permalinkBuilder = FakePermalinkBuilder(
|
permalinkBuilder = FakePermalinkBuilder(
|
||||||
@@ -1059,7 +1053,7 @@ class MessageComposerPresenterTest {
|
|||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitFirstItem()
|
val initialState = awaitFirstItem()
|
||||||
initialState.textEditorState.setHtml("Hey @bo")
|
initialState.textEditorState.setHtml("Hey @bo")
|
||||||
initialState.eventSink(MessageComposerEvents.InsertMention(ResolvedMentionSuggestion.Member(aRoomMember(userId = A_USER_ID_2))))
|
initialState.eventSink(MessageComposerEvents.InsertSuggestion(ResolvedSuggestion.Member(aRoomMember(userId = A_USER_ID_2))))
|
||||||
|
|
||||||
assertThat(initialState.textEditorState.messageHtml())
|
assertThat(initialState.textEditorState.messageHtml())
|
||||||
.isEqualTo("Hey <a href='https://matrix.to/#/${A_USER_ID_2.value}'>${A_USER_ID_2.value}</a>")
|
.isEqualTo("Hey <a href='https://matrix.to/#/${A_USER_ID_2.value}'>${A_USER_ID_2.value}</a>")
|
||||||
@@ -1069,17 +1063,17 @@ class MessageComposerPresenterTest {
|
|||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
@Test
|
@Test
|
||||||
fun `present - send messages with intentional mentions`() = runTest {
|
fun `present - send messages with intentional mentions`() = runTest {
|
||||||
val replyMessageLambda = lambdaRecorder { _: EventId, _: String, _: String?, _: List<Mention>, _: Boolean ->
|
val replyMessageLambda = lambdaRecorder { _: EventId, _: String, _: String?, _: List<IntentionalMention>, _: Boolean ->
|
||||||
Result.success(Unit)
|
Result.success(Unit)
|
||||||
}
|
}
|
||||||
val editMessageLambda = lambdaRecorder { _: EventId?, _: TransactionId?, _: String, _: String?, _: List<Mention> ->
|
val editMessageLambda = lambdaRecorder { _: EventId?, _: TransactionId?, _: String, _: String?, _: List<IntentionalMention> ->
|
||||||
Result.success(Unit)
|
Result.success(Unit)
|
||||||
}
|
}
|
||||||
val timeline = FakeTimeline().apply {
|
val timeline = FakeTimeline().apply {
|
||||||
this.replyMessageLambda = replyMessageLambda
|
this.replyMessageLambda = replyMessageLambda
|
||||||
this.editMessageLambda = editMessageLambda
|
this.editMessageLambda = editMessageLambda
|
||||||
}
|
}
|
||||||
val sendMessageResult = lambdaRecorder { _: String, _: String?, _: List<Mention> ->
|
val sendMessageResult = lambdaRecorder { _: String, _: String?, _: List<IntentionalMention> ->
|
||||||
Result.success(Unit)
|
Result.success(Unit)
|
||||||
}
|
}
|
||||||
val room = FakeMatrixRoom(
|
val room = FakeMatrixRoom(
|
||||||
@@ -1107,7 +1101,7 @@ class MessageComposerPresenterTest {
|
|||||||
advanceUntilIdle()
|
advanceUntilIdle()
|
||||||
|
|
||||||
sendMessageResult.assertions().isCalledOnce()
|
sendMessageResult.assertions().isCalledOnce()
|
||||||
.with(value(A_MESSAGE), any(), value(listOf(Mention.User(A_USER_ID))))
|
.with(value(A_MESSAGE), any(), value(listOf(IntentionalMention.User(A_USER_ID))))
|
||||||
|
|
||||||
// Check intentional mentions on reply sent
|
// Check intentional mentions on reply sent
|
||||||
initialState.eventSink(MessageComposerEvents.SetMode(aReplyMode()))
|
initialState.eventSink(MessageComposerEvents.SetMode(aReplyMode()))
|
||||||
@@ -1124,7 +1118,7 @@ class MessageComposerPresenterTest {
|
|||||||
|
|
||||||
assert(replyMessageLambda)
|
assert(replyMessageLambda)
|
||||||
.isCalledOnce()
|
.isCalledOnce()
|
||||||
.with(any(), any(), any(), value(listOf(Mention.User(A_USER_ID_2))), value(false))
|
.with(any(), any(), any(), value(listOf(IntentionalMention.User(A_USER_ID_2))), value(false))
|
||||||
|
|
||||||
// Check intentional mentions on edit message
|
// Check intentional mentions on edit message
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
@@ -1142,7 +1136,7 @@ class MessageComposerPresenterTest {
|
|||||||
|
|
||||||
assert(editMessageLambda)
|
assert(editMessageLambda)
|
||||||
.isCalledOnce()
|
.isCalledOnce()
|
||||||
.with(any(), any(), any(), any(), value(listOf(Mention.User(A_USER_ID_3))))
|
.with(any(), any(), any(), any(), value(listOf(IntentionalMention.User(A_USER_ID_3))))
|
||||||
|
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
}
|
}
|
||||||
@@ -1507,6 +1501,7 @@ class MessageComposerPresenterTest {
|
|||||||
analyticsService,
|
analyticsService,
|
||||||
DefaultMessageComposerContext(),
|
DefaultMessageComposerContext(),
|
||||||
TestRichTextEditorStateFactory(),
|
TestRichTextEditorStateFactory(),
|
||||||
|
roomAliasSuggestionsDataSource = FakeRoomAliasSuggestionsDataSource(),
|
||||||
permissionsPresenterFactory = FakePermissionsPresenterFactory(permissionPresenter),
|
permissionsPresenterFactory = FakePermissionsPresenterFactory(permissionPresenter),
|
||||||
permalinkParser = permalinkParser,
|
permalinkParser = permalinkParser,
|
||||||
permalinkBuilder = permalinkBuilder,
|
permalinkBuilder = permalinkBuilder,
|
||||||
@@ -14,10 +14,9 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.features.messages.impl.textcomposer
|
package io.element.android.features.messages.impl.messagecomposer
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import io.element.android.features.messages.impl.messagecomposer.RichTextEditorStateFactory
|
|
||||||
import io.element.android.wysiwyg.compose.RichTextEditorState
|
import io.element.android.wysiwyg.compose.RichTextEditorState
|
||||||
import io.element.android.wysiwyg.compose.rememberRichTextEditorState
|
import io.element.android.wysiwyg.compose.rememberRichTextEditorState
|
||||||
|
|
||||||
@@ -18,7 +18,7 @@ package io.element.android.features.messages.impl.timeline
|
|||||||
|
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.libraries.matrix.api.room.Mention
|
import io.element.android.libraries.matrix.api.room.IntentionalMention
|
||||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
||||||
import io.element.android.libraries.matrix.api.timeline.Timeline
|
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||||
@@ -162,10 +162,10 @@ class TimelineControllerTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `test invokeOnCurrentTimeline use the detached timeline and not the live timeline`() = runTest {
|
fun `test invokeOnCurrentTimeline use the detached timeline and not the live timeline`() = runTest {
|
||||||
val lambdaForDetached = lambdaRecorder { _: String, _: String?, _: List<Mention> ->
|
val lambdaForDetached = lambdaRecorder { _: String, _: String?, _: List<IntentionalMention> ->
|
||||||
Result.success(Unit)
|
Result.success(Unit)
|
||||||
}
|
}
|
||||||
val lambdaForLive = lambdaRecorder(ensureNeverCalled = true) { _: String, _: String?, _: List<Mention> ->
|
val lambdaForLive = lambdaRecorder(ensureNeverCalled = true) { _: String, _: String?, _: List<IntentionalMention> ->
|
||||||
Result.success(Unit)
|
Result.success(Unit)
|
||||||
}
|
}
|
||||||
val liveTimeline = FakeTimeline(name = "live").apply {
|
val liveTimeline = FakeTimeline(name = "live").apply {
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ class SharePresenter @AssistedInject constructor(
|
|||||||
matrixClient.getRoom(roomId)?.sendMessage(
|
matrixClient.getRoom(roomId)?.sendMessage(
|
||||||
body = text,
|
body = text,
|
||||||
htmlBody = null,
|
htmlBody = null,
|
||||||
mentions = emptyList(),
|
intentionalMentions = emptyList(),
|
||||||
)?.isSuccess.orFalse()
|
)?.isSuccess.orFalse()
|
||||||
}
|
}
|
||||||
.all { it }
|
.all { it }
|
||||||
|
|||||||
@@ -25,6 +25,5 @@ interface PermalinkBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sealed class PermalinkBuilderError : Throwable() {
|
sealed class PermalinkBuilderError : Throwable() {
|
||||||
data object InvalidUserId : PermalinkBuilderError()
|
data object InvalidData : PermalinkBuilderError()
|
||||||
data object InvalidRoomAlias : PermalinkBuilderError()
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,12 +16,9 @@
|
|||||||
|
|
||||||
package io.element.android.libraries.matrix.api.room
|
package io.element.android.libraries.matrix.api.room
|
||||||
|
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
|
|
||||||
sealed interface Mention {
|
sealed interface IntentionalMention {
|
||||||
data class User(val userId: UserId) : Mention
|
data class User(val userId: UserId) : IntentionalMention
|
||||||
data object AtRoom : Mention
|
data object Room : IntentionalMention
|
||||||
data class Room(val roomId: RoomId) : Mention
|
|
||||||
data class RoomAlias(val roomAlias: RoomAlias?) : Mention
|
|
||||||
}
|
}
|
||||||
@@ -129,9 +129,9 @@ interface MatrixRoom : Closeable {
|
|||||||
|
|
||||||
suspend fun userAvatarUrl(userId: UserId): Result<String?>
|
suspend fun userAvatarUrl(userId: UserId): Result<String?>
|
||||||
|
|
||||||
suspend fun sendMessage(body: String, htmlBody: String?, mentions: List<Mention>): Result<Unit>
|
suspend fun sendMessage(body: String, htmlBody: String?, intentionalMentions: List<IntentionalMention>): Result<Unit>
|
||||||
|
|
||||||
suspend fun editMessage(eventId: EventId, body: String, htmlBody: String?, mentions: List<Mention>): Result<Unit>
|
suspend fun editMessage(eventId: EventId, body: String, htmlBody: String?, intentionalMentions: List<IntentionalMention>): Result<Unit>
|
||||||
|
|
||||||
suspend fun sendImage(
|
suspend fun sendImage(
|
||||||
file: File,
|
file: File,
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import io.element.android.libraries.matrix.api.media.ImageInfo
|
|||||||
import io.element.android.libraries.matrix.api.media.MediaUploadHandler
|
import io.element.android.libraries.matrix.api.media.MediaUploadHandler
|
||||||
import io.element.android.libraries.matrix.api.media.VideoInfo
|
import io.element.android.libraries.matrix.api.media.VideoInfo
|
||||||
import io.element.android.libraries.matrix.api.poll.PollKind
|
import io.element.android.libraries.matrix.api.poll.PollKind
|
||||||
import io.element.android.libraries.matrix.api.room.Mention
|
import io.element.android.libraries.matrix.api.room.IntentionalMention
|
||||||
import io.element.android.libraries.matrix.api.room.location.AssetType
|
import io.element.android.libraries.matrix.api.room.location.AssetType
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.InReplyTo
|
import io.element.android.libraries.matrix.api.timeline.item.event.InReplyTo
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
@@ -52,15 +52,24 @@ interface Timeline : AutoCloseable {
|
|||||||
fun paginationStatus(direction: PaginationDirection): StateFlow<PaginationStatus>
|
fun paginationStatus(direction: PaginationDirection): StateFlow<PaginationStatus>
|
||||||
val timelineItems: Flow<List<MatrixTimelineItem>>
|
val timelineItems: Flow<List<MatrixTimelineItem>>
|
||||||
|
|
||||||
suspend fun sendMessage(body: String, htmlBody: String?, mentions: List<Mention>): Result<Unit>
|
suspend fun sendMessage(
|
||||||
|
body: String,
|
||||||
|
htmlBody: String?,
|
||||||
|
intentionalMentions: List<IntentionalMention>,
|
||||||
|
): Result<Unit>
|
||||||
|
|
||||||
suspend fun editMessage(originalEventId: EventId?, transactionId: TransactionId?, body: String, htmlBody: String?, mentions: List<Mention>): Result<Unit>
|
suspend fun editMessage(
|
||||||
|
originalEventId: EventId?,
|
||||||
|
transactionId: TransactionId?,
|
||||||
|
body: String, htmlBody: String?,
|
||||||
|
intentionalMentions: List<IntentionalMention>,
|
||||||
|
): Result<Unit>
|
||||||
|
|
||||||
suspend fun replyMessage(
|
suspend fun replyMessage(
|
||||||
eventId: EventId,
|
eventId: EventId,
|
||||||
body: String,
|
body: String,
|
||||||
htmlBody: String?,
|
htmlBody: String?,
|
||||||
mentions: List<Mention>,
|
intentionalMentions: List<IntentionalMention>,
|
||||||
fromNotification: Boolean = false,
|
fromNotification: Boolean = false,
|
||||||
): Result<Unit>
|
): Result<Unit>
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ import javax.inject.Inject
|
|||||||
class DefaultPermalinkBuilder @Inject constructor() : PermalinkBuilder {
|
class DefaultPermalinkBuilder @Inject constructor() : PermalinkBuilder {
|
||||||
override fun permalinkForUser(userId: UserId): Result<String> {
|
override fun permalinkForUser(userId: UserId): Result<String> {
|
||||||
if (!MatrixPatterns.isUserId(userId.value)) {
|
if (!MatrixPatterns.isUserId(userId.value)) {
|
||||||
return Result.failure(PermalinkBuilderError.InvalidUserId)
|
return Result.failure(PermalinkBuilderError.InvalidData)
|
||||||
}
|
}
|
||||||
return runCatching {
|
return runCatching {
|
||||||
matrixToUserPermalink(userId.value)
|
matrixToUserPermalink(userId.value)
|
||||||
@@ -40,7 +40,7 @@ class DefaultPermalinkBuilder @Inject constructor() : PermalinkBuilder {
|
|||||||
|
|
||||||
override fun permalinkForRoomAlias(roomAlias: RoomAlias): Result<String> {
|
override fun permalinkForRoomAlias(roomAlias: RoomAlias): Result<String> {
|
||||||
if (!MatrixPatterns.isRoomAlias(roomAlias.value)) {
|
if (!MatrixPatterns.isRoomAlias(roomAlias.value)) {
|
||||||
return Result.failure(PermalinkBuilderError.InvalidRoomAlias)
|
return Result.failure(PermalinkBuilderError.InvalidData)
|
||||||
}
|
}
|
||||||
return runCatching {
|
return runCatching {
|
||||||
matrixToRoomAliasPermalink(roomAlias.value)
|
matrixToRoomAliasPermalink(roomAlias.value)
|
||||||
|
|||||||
@@ -16,11 +16,11 @@
|
|||||||
|
|
||||||
package io.element.android.libraries.matrix.impl.room
|
package io.element.android.libraries.matrix.impl.room
|
||||||
|
|
||||||
import io.element.android.libraries.matrix.api.room.Mention
|
import io.element.android.libraries.matrix.api.room.IntentionalMention
|
||||||
import org.matrix.rustcomponents.sdk.Mentions
|
import org.matrix.rustcomponents.sdk.Mentions
|
||||||
|
|
||||||
fun List<Mention>.map(): Mentions {
|
fun List<IntentionalMention>.map(): Mentions {
|
||||||
val hasAtRoom = any { it is Mention.AtRoom }
|
val hasRoom = any { it is IntentionalMention.Room }
|
||||||
val userIds = filterIsInstance<Mention.User>().map { it.userId.value }
|
val userIds = filterIsInstance<IntentionalMention.User>().map { it.userId.value }
|
||||||
return Mentions(userIds, hasAtRoom)
|
return Mentions(userIds, hasRoom)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,11 +33,11 @@ import io.element.android.libraries.matrix.api.media.MediaUploadHandler
|
|||||||
import io.element.android.libraries.matrix.api.media.VideoInfo
|
import io.element.android.libraries.matrix.api.media.VideoInfo
|
||||||
import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService
|
import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService
|
||||||
import io.element.android.libraries.matrix.api.poll.PollKind
|
import io.element.android.libraries.matrix.api.poll.PollKind
|
||||||
|
import io.element.android.libraries.matrix.api.room.IntentionalMention
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoomInfo
|
import io.element.android.libraries.matrix.api.room.MatrixRoomInfo
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
|
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoomNotificationSettingsState
|
import io.element.android.libraries.matrix.api.room.MatrixRoomNotificationSettingsState
|
||||||
import io.element.android.libraries.matrix.api.room.Mention
|
|
||||||
import io.element.android.libraries.matrix.api.room.MessageEventType
|
import io.element.android.libraries.matrix.api.room.MessageEventType
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||||
import io.element.android.libraries.matrix.api.room.StateEventType
|
import io.element.android.libraries.matrix.api.room.StateEventType
|
||||||
@@ -340,16 +340,21 @@ class RustMatrixRoom(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun editMessage(eventId: EventId, body: String, htmlBody: String?, mentions: List<Mention>): Result<Unit> = withContext(roomDispatcher) {
|
override suspend fun editMessage(
|
||||||
|
eventId: EventId,
|
||||||
|
body: String,
|
||||||
|
htmlBody: String?,
|
||||||
|
intentionalMentions: List<IntentionalMention>
|
||||||
|
): Result<Unit> = withContext(roomDispatcher) {
|
||||||
runCatching {
|
runCatching {
|
||||||
MessageEventContent.from(body, htmlBody, mentions).use { newContent ->
|
MessageEventContent.from(body, htmlBody, intentionalMentions).use { newContent ->
|
||||||
innerRoom.edit(eventId.value, newContent)
|
innerRoom.edit(eventId.value, newContent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun sendMessage(body: String, htmlBody: String?, mentions: List<Mention>): Result<Unit> {
|
override suspend fun sendMessage(body: String, htmlBody: String?, intentionalMentions: List<IntentionalMention>): Result<Unit> {
|
||||||
return liveTimeline.sendMessage(body, htmlBody, mentions)
|
return liveTimeline.sendMessage(body, htmlBody, intentionalMentions)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun leave(): Result<Unit> = withContext(roomDispatcher) {
|
override suspend fun leave(): Result<Unit> = withContext(roomDispatcher) {
|
||||||
|
|||||||
@@ -26,8 +26,8 @@ import io.element.android.libraries.matrix.api.media.ImageInfo
|
|||||||
import io.element.android.libraries.matrix.api.media.MediaUploadHandler
|
import io.element.android.libraries.matrix.api.media.MediaUploadHandler
|
||||||
import io.element.android.libraries.matrix.api.media.VideoInfo
|
import io.element.android.libraries.matrix.api.media.VideoInfo
|
||||||
import io.element.android.libraries.matrix.api.poll.PollKind
|
import io.element.android.libraries.matrix.api.poll.PollKind
|
||||||
|
import io.element.android.libraries.matrix.api.room.IntentionalMention
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
import io.element.android.libraries.matrix.api.room.Mention
|
|
||||||
import io.element.android.libraries.matrix.api.room.isDm
|
import io.element.android.libraries.matrix.api.room.isDm
|
||||||
import io.element.android.libraries.matrix.api.room.location.AssetType
|
import io.element.android.libraries.matrix.api.room.location.AssetType
|
||||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
||||||
@@ -263,8 +263,12 @@ class RustTimeline(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun sendMessage(body: String, htmlBody: String?, mentions: List<Mention>): Result<Unit> = withContext(dispatcher) {
|
override suspend fun sendMessage(
|
||||||
MessageEventContent.from(body, htmlBody, mentions).use { content ->
|
body: String,
|
||||||
|
htmlBody: String?,
|
||||||
|
intentionalMentions: List<IntentionalMention>,
|
||||||
|
): Result<Unit> = withContext(dispatcher) {
|
||||||
|
MessageEventContent.from(body, htmlBody, intentionalMentions).use { content ->
|
||||||
runCatching<Unit> {
|
runCatching<Unit> {
|
||||||
inner.send(content)
|
inner.send(content)
|
||||||
}
|
}
|
||||||
@@ -284,13 +288,13 @@ class RustTimeline(
|
|||||||
transactionId: TransactionId?,
|
transactionId: TransactionId?,
|
||||||
body: String,
|
body: String,
|
||||||
htmlBody: String?,
|
htmlBody: String?,
|
||||||
mentions: List<Mention>,
|
intentionalMentions: List<IntentionalMention>,
|
||||||
): Result<Unit> =
|
): Result<Unit> =
|
||||||
withContext(dispatcher) {
|
withContext(dispatcher) {
|
||||||
runCatching<Unit> {
|
runCatching<Unit> {
|
||||||
getEventTimelineItem(originalEventId, transactionId).use { item ->
|
getEventTimelineItem(originalEventId, transactionId).use { item ->
|
||||||
inner.edit(
|
inner.edit(
|
||||||
newContent = MessageEventContent.from(body, htmlBody, mentions),
|
newContent = MessageEventContent.from(body, htmlBody, intentionalMentions),
|
||||||
item = item,
|
item = item,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -301,11 +305,11 @@ class RustTimeline(
|
|||||||
eventId: EventId,
|
eventId: EventId,
|
||||||
body: String,
|
body: String,
|
||||||
htmlBody: String?,
|
htmlBody: String?,
|
||||||
mentions: List<Mention>,
|
intentionalMentions: List<IntentionalMention>,
|
||||||
fromNotification: Boolean,
|
fromNotification: Boolean,
|
||||||
): Result<Unit> = withContext(dispatcher) {
|
): Result<Unit> = withContext(dispatcher) {
|
||||||
runCatching {
|
runCatching {
|
||||||
val msg = MessageEventContent.from(body, htmlBody, mentions)
|
val msg = MessageEventContent.from(body, htmlBody, intentionalMentions)
|
||||||
inner.sendReply(msg, eventId.value)
|
inner.sendReply(msg, eventId.value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
package io.element.android.libraries.matrix.impl.util
|
package io.element.android.libraries.matrix.impl.util
|
||||||
|
|
||||||
import io.element.android.libraries.matrix.api.room.Mention
|
import io.element.android.libraries.matrix.api.room.IntentionalMention
|
||||||
import io.element.android.libraries.matrix.impl.room.map
|
import io.element.android.libraries.matrix.impl.room.map
|
||||||
import org.matrix.rustcomponents.sdk.RoomMessageEventContentWithoutRelation
|
import org.matrix.rustcomponents.sdk.RoomMessageEventContentWithoutRelation
|
||||||
import org.matrix.rustcomponents.sdk.messageEventContentFromHtml
|
import org.matrix.rustcomponents.sdk.messageEventContentFromHtml
|
||||||
@@ -26,11 +26,11 @@ import org.matrix.rustcomponents.sdk.messageEventContentFromMarkdown
|
|||||||
* Creates a [RoomMessageEventContentWithoutRelation] from a body, an html body and a list of mentions.
|
* Creates a [RoomMessageEventContentWithoutRelation] from a body, an html body and a list of mentions.
|
||||||
*/
|
*/
|
||||||
object MessageEventContent {
|
object MessageEventContent {
|
||||||
fun from(body: String, htmlBody: String?, mentions: List<Mention>): RoomMessageEventContentWithoutRelation {
|
fun from(body: String, htmlBody: String?, intentionalMentions: List<IntentionalMention>): RoomMessageEventContentWithoutRelation {
|
||||||
return if (htmlBody != null) {
|
return if (htmlBody != null) {
|
||||||
messageEventContentFromHtml(body, htmlBody)
|
messageEventContentFromHtml(body, htmlBody)
|
||||||
} else {
|
} else {
|
||||||
messageEventContentFromMarkdown(body)
|
messageEventContentFromMarkdown(body)
|
||||||
}.withMentions(mentions.map())
|
}.withMentions(intentionalMentions.map())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,10 +19,11 @@ package io.element.android.libraries.matrix.test.permalink
|
|||||||
import io.element.android.libraries.matrix.api.core.RoomAlias
|
import io.element.android.libraries.matrix.api.core.RoomAlias
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
import io.element.android.libraries.matrix.api.permalink.PermalinkBuilder
|
import io.element.android.libraries.matrix.api.permalink.PermalinkBuilder
|
||||||
|
import io.element.android.tests.testutils.lambda.lambdaError
|
||||||
|
|
||||||
class FakePermalinkBuilder(
|
class FakePermalinkBuilder(
|
||||||
private val permalinkForUserLambda: (UserId) -> Result<String> = { Result.failure(Exception("Not implemented")) },
|
private val permalinkForUserLambda: (UserId) -> Result<String> = { lambdaError() },
|
||||||
private val permalinkForRoomAliasLambda: (RoomAlias) -> Result<String> = { Result.failure(Exception("Not implemented")) },
|
private val permalinkForRoomAliasLambda: (RoomAlias) -> Result<String> = { lambdaError() },
|
||||||
) : PermalinkBuilder {
|
) : PermalinkBuilder {
|
||||||
override fun permalinkForUser(userId: UserId): Result<String> {
|
override fun permalinkForUser(userId: UserId): Result<String> {
|
||||||
return permalinkForUserLambda(userId)
|
return permalinkForUserLambda(userId)
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ import io.element.android.libraries.matrix.api.room.MatrixRoom
|
|||||||
import io.element.android.libraries.matrix.api.room.MatrixRoomInfo
|
import io.element.android.libraries.matrix.api.room.MatrixRoomInfo
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
|
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoomNotificationSettingsState
|
import io.element.android.libraries.matrix.api.room.MatrixRoomNotificationSettingsState
|
||||||
import io.element.android.libraries.matrix.api.room.Mention
|
import io.element.android.libraries.matrix.api.room.IntentionalMention
|
||||||
import io.element.android.libraries.matrix.api.room.MessageEventType
|
import io.element.android.libraries.matrix.api.room.MessageEventType
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||||
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
|
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
|
||||||
@@ -105,8 +105,8 @@ class FakeMatrixRoom(
|
|||||||
private val setTopicResult: (String) -> Result<Unit> = { lambdaError() },
|
private val setTopicResult: (String) -> Result<Unit> = { lambdaError() },
|
||||||
private val updateAvatarResult: (String, ByteArray) -> Result<Unit> = { _, _ -> lambdaError() },
|
private val updateAvatarResult: (String, ByteArray) -> Result<Unit> = { _, _ -> lambdaError() },
|
||||||
private val removeAvatarResult: () -> Result<Unit> = { lambdaError() },
|
private val removeAvatarResult: () -> Result<Unit> = { lambdaError() },
|
||||||
private val editMessageLambda: (EventId, String, String?, List<Mention>) -> Result<Unit> = { _, _, _, _ -> lambdaError() },
|
private val editMessageLambda: (EventId, String, String?, List<IntentionalMention>) -> Result<Unit> = { _, _, _, _ -> lambdaError() },
|
||||||
private val sendMessageResult: (String, String?, List<Mention>) -> Result<Unit> = { _, _, _ -> lambdaError() },
|
private val sendMessageResult: (String, String?, List<IntentionalMention>) -> Result<Unit> = { _, _, _ -> lambdaError() },
|
||||||
private val updateUserRoleResult: () -> Result<Unit> = { lambdaError() },
|
private val updateUserRoleResult: () -> Result<Unit> = { lambdaError() },
|
||||||
private val toggleReactionResult: (String, EventId) -> Result<Unit> = { _, _ -> lambdaError() },
|
private val toggleReactionResult: (String, EventId) -> Result<Unit> = { _, _ -> lambdaError() },
|
||||||
private val retrySendMessageResult: (TransactionId) -> Result<Unit> = { lambdaError() },
|
private val retrySendMessageResult: (TransactionId) -> Result<Unit> = { lambdaError() },
|
||||||
@@ -222,12 +222,12 @@ class FakeMatrixRoom(
|
|||||||
return updateUserRoleResult()
|
return updateUserRoleResult()
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun editMessage(eventId: EventId, body: String, htmlBody: String?, mentions: List<Mention>) = simulateLongTask {
|
override suspend fun editMessage(eventId: EventId, body: String, htmlBody: String?, intentionalMentions: List<IntentionalMention>) = simulateLongTask {
|
||||||
editMessageLambda(eventId, body, htmlBody, mentions)
|
editMessageLambda(eventId, body, htmlBody, intentionalMentions)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun sendMessage(body: String, htmlBody: String?, mentions: List<Mention>) = simulateLongTask {
|
override suspend fun sendMessage(body: String, htmlBody: String?, intentionalMentions: List<IntentionalMention>) = simulateLongTask {
|
||||||
sendMessageResult(body, htmlBody, mentions)
|
sendMessageResult(body, htmlBody, intentionalMentions)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun toggleReaction(emoji: String, eventId: EventId): Result<Unit> {
|
override suspend fun toggleReaction(emoji: String, eventId: EventId): Result<Unit> {
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import io.element.android.libraries.matrix.api.media.ImageInfo
|
|||||||
import io.element.android.libraries.matrix.api.media.MediaUploadHandler
|
import io.element.android.libraries.matrix.api.media.MediaUploadHandler
|
||||||
import io.element.android.libraries.matrix.api.media.VideoInfo
|
import io.element.android.libraries.matrix.api.media.VideoInfo
|
||||||
import io.element.android.libraries.matrix.api.poll.PollKind
|
import io.element.android.libraries.matrix.api.poll.PollKind
|
||||||
import io.element.android.libraries.matrix.api.room.Mention
|
import io.element.android.libraries.matrix.api.room.IntentionalMention
|
||||||
import io.element.android.libraries.matrix.api.room.location.AssetType
|
import io.element.android.libraries.matrix.api.room.location.AssetType
|
||||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
||||||
import io.element.android.libraries.matrix.api.timeline.ReceiptType
|
import io.element.android.libraries.matrix.api.timeline.ReceiptType
|
||||||
@@ -60,7 +60,7 @@ class FakeTimeline(
|
|||||||
var sendMessageLambda: (
|
var sendMessageLambda: (
|
||||||
body: String,
|
body: String,
|
||||||
htmlBody: String?,
|
htmlBody: String?,
|
||||||
mentions: List<Mention>,
|
intentionalMentions: List<IntentionalMention>,
|
||||||
) -> Result<Unit> = { _, _, _ ->
|
) -> Result<Unit> = { _, _, _ ->
|
||||||
Result.success(Unit)
|
Result.success(Unit)
|
||||||
}
|
}
|
||||||
@@ -68,8 +68,8 @@ class FakeTimeline(
|
|||||||
override suspend fun sendMessage(
|
override suspend fun sendMessage(
|
||||||
body: String,
|
body: String,
|
||||||
htmlBody: String?,
|
htmlBody: String?,
|
||||||
mentions: List<Mention>,
|
intentionalMentions: List<IntentionalMention>,
|
||||||
): Result<Unit> = sendMessageLambda(body, htmlBody, mentions)
|
): Result<Unit> = sendMessageLambda(body, htmlBody, intentionalMentions)
|
||||||
|
|
||||||
var redactEventLambda: (eventId: EventId?, transactionId: TransactionId?, reason: String?) -> Result<Boolean> = { _, _, _ ->
|
var redactEventLambda: (eventId: EventId?, transactionId: TransactionId?, reason: String?) -> Result<Boolean> = { _, _, _ ->
|
||||||
Result.success(true)
|
Result.success(true)
|
||||||
@@ -86,7 +86,7 @@ class FakeTimeline(
|
|||||||
transactionId: TransactionId?,
|
transactionId: TransactionId?,
|
||||||
body: String,
|
body: String,
|
||||||
htmlBody: String?,
|
htmlBody: String?,
|
||||||
mentions: List<Mention>,
|
intentionalMentions: List<IntentionalMention>,
|
||||||
) -> Result<Unit> = { _, _, _, _, _ ->
|
) -> Result<Unit> = { _, _, _, _, _ ->
|
||||||
Result.success(Unit)
|
Result.success(Unit)
|
||||||
}
|
}
|
||||||
@@ -96,20 +96,20 @@ class FakeTimeline(
|
|||||||
transactionId: TransactionId?,
|
transactionId: TransactionId?,
|
||||||
body: String,
|
body: String,
|
||||||
htmlBody: String?,
|
htmlBody: String?,
|
||||||
mentions: List<Mention>,
|
intentionalMentions: List<IntentionalMention>,
|
||||||
): Result<Unit> = editMessageLambda(
|
): Result<Unit> = editMessageLambda(
|
||||||
originalEventId,
|
originalEventId,
|
||||||
transactionId,
|
transactionId,
|
||||||
body,
|
body,
|
||||||
htmlBody,
|
htmlBody,
|
||||||
mentions
|
intentionalMentions
|
||||||
)
|
)
|
||||||
|
|
||||||
var replyMessageLambda: (
|
var replyMessageLambda: (
|
||||||
eventId: EventId,
|
eventId: EventId,
|
||||||
body: String,
|
body: String,
|
||||||
htmlBody: String?,
|
htmlBody: String?,
|
||||||
mentions: List<Mention>,
|
intentionalMentions: List<IntentionalMention>,
|
||||||
fromNotification: Boolean,
|
fromNotification: Boolean,
|
||||||
) -> Result<Unit> = { _, _, _, _, _ ->
|
) -> Result<Unit> = { _, _, _, _, _ ->
|
||||||
Result.success(Unit)
|
Result.success(Unit)
|
||||||
@@ -119,13 +119,13 @@ class FakeTimeline(
|
|||||||
eventId: EventId,
|
eventId: EventId,
|
||||||
body: String,
|
body: String,
|
||||||
htmlBody: String?,
|
htmlBody: String?,
|
||||||
mentions: List<Mention>,
|
intentionalMentions: List<IntentionalMention>,
|
||||||
fromNotification: Boolean,
|
fromNotification: Boolean,
|
||||||
): Result<Unit> = replyMessageLambda(
|
): Result<Unit> = replyMessageLambda(
|
||||||
eventId,
|
eventId,
|
||||||
body,
|
body,
|
||||||
htmlBody,
|
htmlBody,
|
||||||
mentions,
|
intentionalMentions,
|
||||||
fromNotification,
|
fromNotification,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -171,14 +171,14 @@ class NotificationBroadcastReceiverHandler @Inject constructor(
|
|||||||
eventId = threadId.asEventId(),
|
eventId = threadId.asEventId(),
|
||||||
body = message,
|
body = message,
|
||||||
htmlBody = null,
|
htmlBody = null,
|
||||||
mentions = emptyList(),
|
intentionalMentions = emptyList(),
|
||||||
fromNotification = true,
|
fromNotification = true,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
room.liveTimeline.sendMessage(
|
room.liveTimeline.sendMessage(
|
||||||
body = message,
|
body = message,
|
||||||
htmlBody = null,
|
htmlBody = null,
|
||||||
mentions = emptyList()
|
intentionalMentions = emptyList()
|
||||||
)
|
)
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
Timber.e(it, "Failed to send smart reply message")
|
Timber.e(it, "Failed to send smart reply message")
|
||||||
|
|||||||
@@ -111,13 +111,13 @@ fun MarkdownTextInput(
|
|||||||
state.text.update(editable, false)
|
state.text.update(editable, false)
|
||||||
state.lineCount = lineCount
|
state.lineCount = lineCount
|
||||||
|
|
||||||
state.currentMentionSuggestion = editable?.checkSuggestionNeeded()
|
state.currentSuggestion = editable?.checkSuggestionNeeded()
|
||||||
onReceiveSuggestion(state.currentMentionSuggestion)
|
onReceiveSuggestion(state.currentSuggestion)
|
||||||
}
|
}
|
||||||
onSelectionChangeListener = { selStart, selEnd ->
|
onSelectionChangeListener = { selStart, selEnd ->
|
||||||
state.selection = selStart..selEnd
|
state.selection = selStart..selEnd
|
||||||
state.currentMentionSuggestion = editableText.checkSuggestionNeeded()
|
state.currentSuggestion = editableText.checkSuggestionNeeded()
|
||||||
onReceiveSuggestion(state.currentMentionSuggestion)
|
onReceiveSuggestion(state.currentSuggestion)
|
||||||
}
|
}
|
||||||
if (onSelectRichContent != null) {
|
if (onSelectRichContent != null) {
|
||||||
ViewCompat.setOnReceiveContentListener(
|
ViewCompat.setOnReceiveContentListener(
|
||||||
|
|||||||
@@ -17,10 +17,13 @@
|
|||||||
package io.element.android.libraries.textcomposer.mentions
|
package io.element.android.libraries.textcomposer.mentions
|
||||||
|
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
|
import io.element.android.libraries.matrix.api.core.RoomAlias
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||||
|
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
sealed interface ResolvedMentionSuggestion {
|
sealed interface ResolvedSuggestion {
|
||||||
data object AtRoom : ResolvedMentionSuggestion
|
data object AtRoom : ResolvedSuggestion
|
||||||
data class Member(val roomMember: RoomMember) : ResolvedMentionSuggestion
|
data class Member(val roomMember: RoomMember) : ResolvedSuggestion
|
||||||
|
data class Alias(val roomAlias: RoomAlias, val roomSummary: RoomSummary) : ResolvedSuggestion
|
||||||
}
|
}
|
||||||
@@ -31,13 +31,14 @@ import androidx.compose.runtime.saveable.SaverScope
|
|||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.core.text.getSpans
|
import androidx.core.text.getSpans
|
||||||
|
import io.element.android.libraries.matrix.api.core.RoomAlias
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
import io.element.android.libraries.matrix.api.permalink.PermalinkBuilder
|
import io.element.android.libraries.matrix.api.permalink.PermalinkBuilder
|
||||||
import io.element.android.libraries.matrix.api.room.Mention
|
import io.element.android.libraries.matrix.api.room.IntentionalMention
|
||||||
import io.element.android.libraries.textcomposer.components.markdown.StableCharSequence
|
import io.element.android.libraries.textcomposer.components.markdown.StableCharSequence
|
||||||
import io.element.android.libraries.textcomposer.mentions.MentionSpan
|
import io.element.android.libraries.textcomposer.mentions.MentionSpan
|
||||||
import io.element.android.libraries.textcomposer.mentions.MentionSpanProvider
|
import io.element.android.libraries.textcomposer.mentions.MentionSpanProvider
|
||||||
import io.element.android.libraries.textcomposer.mentions.ResolvedMentionSuggestion
|
import io.element.android.libraries.textcomposer.mentions.ResolvedSuggestion
|
||||||
import io.element.android.libraries.textcomposer.mentions.getMentionSpans
|
import io.element.android.libraries.textcomposer.mentions.getMentionSpans
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
@@ -51,16 +52,16 @@ class MarkdownTextEditorState(
|
|||||||
var hasFocus by mutableStateOf(initialFocus)
|
var hasFocus by mutableStateOf(initialFocus)
|
||||||
var requestFocusAction by mutableStateOf({})
|
var requestFocusAction by mutableStateOf({})
|
||||||
var lineCount by mutableIntStateOf(1)
|
var lineCount by mutableIntStateOf(1)
|
||||||
var currentMentionSuggestion by mutableStateOf<Suggestion?>(null)
|
var currentSuggestion by mutableStateOf<Suggestion?>(null)
|
||||||
|
|
||||||
fun insertMention(
|
fun insertSuggestion(
|
||||||
mention: ResolvedMentionSuggestion,
|
resolvedSuggestion: ResolvedSuggestion,
|
||||||
mentionSpanProvider: MentionSpanProvider,
|
mentionSpanProvider: MentionSpanProvider,
|
||||||
permalinkBuilder: PermalinkBuilder,
|
permalinkBuilder: PermalinkBuilder,
|
||||||
) {
|
) {
|
||||||
val suggestion = currentMentionSuggestion ?: return
|
val suggestion = currentSuggestion ?: return
|
||||||
when (mention) {
|
when (resolvedSuggestion) {
|
||||||
is ResolvedMentionSuggestion.AtRoom -> {
|
is ResolvedSuggestion.AtRoom -> {
|
||||||
val currentText = SpannableStringBuilder(text.value())
|
val currentText = SpannableStringBuilder(text.value())
|
||||||
val replaceText = "@room"
|
val replaceText = "@room"
|
||||||
val roomPill = mentionSpanProvider.getMentionSpanFor(replaceText, "")
|
val roomPill = mentionSpanProvider.getMentionSpanFor(replaceText, "")
|
||||||
@@ -70,10 +71,21 @@ class MarkdownTextEditorState(
|
|||||||
text.update(currentText, true)
|
text.update(currentText, true)
|
||||||
selection = IntRange(end + 1, end + 1)
|
selection = IntRange(end + 1, end + 1)
|
||||||
}
|
}
|
||||||
is ResolvedMentionSuggestion.Member -> {
|
is ResolvedSuggestion.Member -> {
|
||||||
val currentText = SpannableStringBuilder(text.value())
|
val currentText = SpannableStringBuilder(text.value())
|
||||||
val text = mention.roomMember.displayName?.prependIndent("@") ?: mention.roomMember.userId.value
|
val text = resolvedSuggestion.roomMember.displayName?.prependIndent("@") ?: resolvedSuggestion.roomMember.userId.value
|
||||||
val link = permalinkBuilder.permalinkForUser(mention.roomMember.userId).getOrNull() ?: return
|
val link = permalinkBuilder.permalinkForUser(resolvedSuggestion.roomMember.userId).getOrNull() ?: return
|
||||||
|
val mentionPill = mentionSpanProvider.getMentionSpanFor(text, link)
|
||||||
|
currentText.replace(suggestion.start, suggestion.end, ". ")
|
||||||
|
val end = suggestion.start + 1
|
||||||
|
currentText.setSpan(mentionPill, suggestion.start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||||
|
this.text.update(currentText, true)
|
||||||
|
this.selection = IntRange(end + 1, end + 1)
|
||||||
|
}
|
||||||
|
is ResolvedSuggestion.Alias -> {
|
||||||
|
val currentText = SpannableStringBuilder(text.value())
|
||||||
|
val text = resolvedSuggestion.roomAlias.value
|
||||||
|
val link = permalinkBuilder.permalinkForRoomAlias(resolvedSuggestion.roomAlias).getOrNull() ?: return
|
||||||
val mentionPill = mentionSpanProvider.getMentionSpanFor(text, link)
|
val mentionPill = mentionSpanProvider.getMentionSpanFor(text, link)
|
||||||
currentText.replace(suggestion.start, suggestion.end, "@ ")
|
currentText.replace(suggestion.start, suggestion.end, "@ ")
|
||||||
val end = suggestion.start + 1
|
val end = suggestion.start + 1
|
||||||
@@ -96,14 +108,18 @@ class MarkdownTextEditorState(
|
|||||||
val end = charSequence.getSpanEnd(mention)
|
val end = charSequence.getSpanEnd(mention)
|
||||||
when (mention.type) {
|
when (mention.type) {
|
||||||
MentionSpan.Type.USER -> {
|
MentionSpan.Type.USER -> {
|
||||||
val link = permalinkBuilder.permalinkForUser(UserId(mention.rawValue)).getOrNull() ?: continue
|
permalinkBuilder.permalinkForUser(UserId(mention.rawValue)).getOrNull()?.let { link ->
|
||||||
replace(start, end, "[${mention.rawValue}]($link)")
|
replace(start, end, "[${mention.rawValue}]($link)")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
MentionSpan.Type.EVERYONE -> {
|
MentionSpan.Type.EVERYONE -> {
|
||||||
replace(start, end, "@room")
|
replace(start, end, "@room")
|
||||||
}
|
}
|
||||||
// Nothing to do here yet
|
MentionSpan.Type.ROOM -> {
|
||||||
MentionSpan.Type.ROOM -> Unit
|
permalinkBuilder.permalinkForRoomAlias(RoomAlias(mention.rawValue)).getOrNull()?.let { link ->
|
||||||
|
replace(start, end, "[${mention.text}]($link)")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -113,13 +129,13 @@ class MarkdownTextEditorState(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getMentions(): List<Mention> {
|
fun getMentions(): List<IntentionalMention> {
|
||||||
val text = SpannableString(text.value())
|
val text = SpannableString(text.value())
|
||||||
val mentionSpans = text.getSpans<MentionSpan>(0, text.length)
|
val mentionSpans = text.getSpans<MentionSpan>(0, text.length)
|
||||||
return mentionSpans.mapNotNull { mentionSpan ->
|
return mentionSpans.mapNotNull { mentionSpan ->
|
||||||
when (mentionSpan.type) {
|
when (mentionSpan.type) {
|
||||||
MentionSpan.Type.USER -> Mention.User(UserId(mentionSpan.rawValue))
|
MentionSpan.Type.USER -> IntentionalMention.User(UserId(mentionSpan.rawValue))
|
||||||
MentionSpan.Type.EVERYONE -> Mention.AtRoom
|
MentionSpan.Type.EVERYONE -> IntentionalMention.Room
|
||||||
MentionSpan.Type.ROOM -> null
|
MentionSpan.Type.ROOM -> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,10 +16,10 @@
|
|||||||
|
|
||||||
package io.element.android.libraries.textcomposer.model
|
package io.element.android.libraries.textcomposer.model
|
||||||
|
|
||||||
import io.element.android.libraries.matrix.api.room.Mention
|
import io.element.android.libraries.matrix.api.room.IntentionalMention
|
||||||
|
|
||||||
data class Message(
|
data class Message(
|
||||||
val html: String?,
|
val html: String?,
|
||||||
val markdown: String,
|
val markdown: String,
|
||||||
val mentions: List<Mention>,
|
val intentionalMentions: List<IntentionalMention>,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ import io.element.android.libraries.textcomposer.components.markdown.MarkdownTex
|
|||||||
import io.element.android.libraries.textcomposer.components.markdown.aMarkdownTextEditorState
|
import io.element.android.libraries.textcomposer.components.markdown.aMarkdownTextEditorState
|
||||||
import io.element.android.libraries.textcomposer.mentions.MentionSpan
|
import io.element.android.libraries.textcomposer.mentions.MentionSpan
|
||||||
import io.element.android.libraries.textcomposer.mentions.MentionSpanProvider
|
import io.element.android.libraries.textcomposer.mentions.MentionSpanProvider
|
||||||
import io.element.android.libraries.textcomposer.mentions.ResolvedMentionSuggestion
|
import io.element.android.libraries.textcomposer.mentions.ResolvedSuggestion
|
||||||
import io.element.android.libraries.textcomposer.model.MarkdownTextEditorState
|
import io.element.android.libraries.textcomposer.model.MarkdownTextEditorState
|
||||||
import io.element.android.libraries.textcomposer.model.Suggestion
|
import io.element.android.libraries.textcomposer.model.Suggestion
|
||||||
import io.element.android.libraries.textcomposer.model.SuggestionType
|
import io.element.android.libraries.textcomposer.model.SuggestionType
|
||||||
@@ -157,13 +157,13 @@ class MarkdownTextInputTest {
|
|||||||
val permalinkParser = FakePermalinkParser(result = { PermalinkData.UserLink(A_SESSION_ID) })
|
val permalinkParser = FakePermalinkParser(result = { PermalinkData.UserLink(A_SESSION_ID) })
|
||||||
val permalinkBuilder = FakePermalinkBuilder(permalinkForUserLambda = { Result.success("https://matrix.to/#/$A_SESSION_ID") })
|
val permalinkBuilder = FakePermalinkBuilder(permalinkForUserLambda = { Result.success("https://matrix.to/#/$A_SESSION_ID") })
|
||||||
val state = aMarkdownTextEditorState(initialText = "@", initialFocus = true)
|
val state = aMarkdownTextEditorState(initialText = "@", initialFocus = true)
|
||||||
state.currentMentionSuggestion = Suggestion(0, 1, SuggestionType.Mention, "")
|
state.currentSuggestion = Suggestion(0, 1, SuggestionType.Mention, "")
|
||||||
rule.setMarkdownTextInput(state = state)
|
rule.setMarkdownTextInput(state = state)
|
||||||
var editor: EditText? = null
|
var editor: EditText? = null
|
||||||
rule.activityRule.scenario.onActivity {
|
rule.activityRule.scenario.onActivity {
|
||||||
editor = it.findEditor()
|
editor = it.findEditor()
|
||||||
state.insertMention(
|
state.insertSuggestion(
|
||||||
ResolvedMentionSuggestion.Member(roomMember = aRoomMember()),
|
ResolvedSuggestion.Member(roomMember = aRoomMember()),
|
||||||
MentionSpanProvider(permalinkParser = permalinkParser),
|
MentionSpanProvider(permalinkParser = permalinkParser),
|
||||||
permalinkBuilder,
|
permalinkBuilder,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ import org.junit.runner.RunWith
|
|||||||
import org.robolectric.RobolectricTestRunner
|
import org.robolectric.RobolectricTestRunner
|
||||||
|
|
||||||
@RunWith(RobolectricTestRunner::class)
|
@RunWith(RobolectricTestRunner::class)
|
||||||
class MentionSpanProviderTest {
|
class IntentionalMentionSpanProviderTest {
|
||||||
@JvmField @Rule
|
@JvmField @Rule
|
||||||
val warmUpRule = WarmUpRule()
|
val warmUpRule = WarmUpRule()
|
||||||
|
|
||||||
@@ -22,13 +22,13 @@ import androidx.core.text.inSpans
|
|||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.libraries.matrix.api.permalink.PermalinkData
|
import io.element.android.libraries.matrix.api.permalink.PermalinkData
|
||||||
import io.element.android.libraries.matrix.api.room.Mention
|
import io.element.android.libraries.matrix.api.room.IntentionalMention
|
||||||
import io.element.android.libraries.matrix.test.permalink.FakePermalinkBuilder
|
import io.element.android.libraries.matrix.test.permalink.FakePermalinkBuilder
|
||||||
import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser
|
import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser
|
||||||
import io.element.android.libraries.matrix.test.room.aRoomMember
|
import io.element.android.libraries.matrix.test.room.aRoomMember
|
||||||
import io.element.android.libraries.textcomposer.mentions.MentionSpan
|
import io.element.android.libraries.textcomposer.mentions.MentionSpan
|
||||||
import io.element.android.libraries.textcomposer.mentions.MentionSpanProvider
|
import io.element.android.libraries.textcomposer.mentions.MentionSpanProvider
|
||||||
import io.element.android.libraries.textcomposer.mentions.ResolvedMentionSuggestion
|
import io.element.android.libraries.textcomposer.mentions.ResolvedSuggestion
|
||||||
import io.element.android.libraries.textcomposer.model.MarkdownTextEditorState
|
import io.element.android.libraries.textcomposer.model.MarkdownTextEditorState
|
||||||
import io.element.android.libraries.textcomposer.model.Suggestion
|
import io.element.android.libraries.textcomposer.model.Suggestion
|
||||||
import io.element.android.libraries.textcomposer.model.SuggestionType
|
import io.element.android.libraries.textcomposer.model.SuggestionType
|
||||||
@@ -41,11 +41,11 @@ class MarkdownTextEditorStateTest {
|
|||||||
fun `insertMention - with no currentMentionSuggestion does nothing`() {
|
fun `insertMention - with no currentMentionSuggestion does nothing`() {
|
||||||
val state = MarkdownTextEditorState(initialText = "Hello @", initialFocus = true)
|
val state = MarkdownTextEditorState(initialText = "Hello @", initialFocus = true)
|
||||||
val member = aRoomMember()
|
val member = aRoomMember()
|
||||||
val mention = ResolvedMentionSuggestion.Member(member)
|
val mention = ResolvedSuggestion.Member(member)
|
||||||
val permalinkBuilder = FakePermalinkBuilder()
|
val permalinkBuilder = FakePermalinkBuilder()
|
||||||
val mentionSpanProvider = aMentionSpanProvider()
|
val mentionSpanProvider = aMentionSpanProvider()
|
||||||
|
|
||||||
state.insertMention(mention, mentionSpanProvider, permalinkBuilder)
|
state.insertSuggestion(mention, mentionSpanProvider, permalinkBuilder)
|
||||||
|
|
||||||
assertThat(state.getMentions()).isEmpty()
|
assertThat(state.getMentions()).isEmpty()
|
||||||
}
|
}
|
||||||
@@ -53,15 +53,15 @@ class MarkdownTextEditorStateTest {
|
|||||||
@Test
|
@Test
|
||||||
fun `insertMention - with member but failed PermalinkBuilder result`() {
|
fun `insertMention - with member but failed PermalinkBuilder result`() {
|
||||||
val state = MarkdownTextEditorState(initialText = "Hello @", initialFocus = true).apply {
|
val state = MarkdownTextEditorState(initialText = "Hello @", initialFocus = true).apply {
|
||||||
currentMentionSuggestion = Suggestion(start = 6, end = 7, type = SuggestionType.Mention, text = "")
|
currentSuggestion = Suggestion(start = 6, end = 7, type = SuggestionType.Mention, text = "")
|
||||||
}
|
}
|
||||||
val member = aRoomMember()
|
val member = aRoomMember()
|
||||||
val mention = ResolvedMentionSuggestion.Member(member)
|
val mention = ResolvedSuggestion.Member(member)
|
||||||
val permalinkParser = FakePermalinkParser(result = { PermalinkData.UserLink(member.userId) })
|
val permalinkParser = FakePermalinkParser(result = { PermalinkData.UserLink(member.userId) })
|
||||||
val permalinkBuilder = FakePermalinkBuilder(permalinkForUserLambda = { Result.failure(IllegalStateException("Failed")) })
|
val permalinkBuilder = FakePermalinkBuilder(permalinkForUserLambda = { Result.failure(IllegalStateException("Failed")) })
|
||||||
val mentionSpanProvider = aMentionSpanProvider(permalinkParser = permalinkParser)
|
val mentionSpanProvider = aMentionSpanProvider(permalinkParser = permalinkParser)
|
||||||
|
|
||||||
state.insertMention(mention, mentionSpanProvider, permalinkBuilder)
|
state.insertSuggestion(mention, mentionSpanProvider, permalinkBuilder)
|
||||||
|
|
||||||
val mentions = state.getMentions()
|
val mentions = state.getMentions()
|
||||||
assertThat(mentions).isEmpty()
|
assertThat(mentions).isEmpty()
|
||||||
@@ -70,36 +70,36 @@ class MarkdownTextEditorStateTest {
|
|||||||
@Test
|
@Test
|
||||||
fun `insertMention - with member`() {
|
fun `insertMention - with member`() {
|
||||||
val state = MarkdownTextEditorState(initialText = "Hello @", initialFocus = true).apply {
|
val state = MarkdownTextEditorState(initialText = "Hello @", initialFocus = true).apply {
|
||||||
currentMentionSuggestion = Suggestion(start = 6, end = 7, type = SuggestionType.Mention, text = "")
|
currentSuggestion = Suggestion(start = 6, end = 7, type = SuggestionType.Mention, text = "")
|
||||||
}
|
}
|
||||||
val member = aRoomMember()
|
val member = aRoomMember()
|
||||||
val mention = ResolvedMentionSuggestion.Member(member)
|
val mention = ResolvedSuggestion.Member(member)
|
||||||
val permalinkParser = FakePermalinkParser(result = { PermalinkData.UserLink(member.userId) })
|
val permalinkParser = FakePermalinkParser(result = { PermalinkData.UserLink(member.userId) })
|
||||||
val permalinkBuilder = FakePermalinkBuilder(permalinkForUserLambda = { Result.success("https://matrix.to/#/${member.userId}") })
|
val permalinkBuilder = FakePermalinkBuilder(permalinkForUserLambda = { Result.success("https://matrix.to/#/${member.userId}") })
|
||||||
val mentionSpanProvider = aMentionSpanProvider(permalinkParser = permalinkParser)
|
val mentionSpanProvider = aMentionSpanProvider(permalinkParser = permalinkParser)
|
||||||
|
|
||||||
state.insertMention(mention, mentionSpanProvider, permalinkBuilder)
|
state.insertSuggestion(mention, mentionSpanProvider, permalinkBuilder)
|
||||||
|
|
||||||
val mentions = state.getMentions()
|
val mentions = state.getMentions()
|
||||||
assertThat(mentions).isNotEmpty()
|
assertThat(mentions).isNotEmpty()
|
||||||
assertThat((mentions.firstOrNull() as? Mention.User)?.userId).isEqualTo(member.userId)
|
assertThat((mentions.firstOrNull() as? IntentionalMention.User)?.userId).isEqualTo(member.userId)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `insertMention - with @room`() {
|
fun `insertMention - with @room`() {
|
||||||
val state = MarkdownTextEditorState(initialText = "Hello @", initialFocus = true).apply {
|
val state = MarkdownTextEditorState(initialText = "Hello @", initialFocus = true).apply {
|
||||||
currentMentionSuggestion = Suggestion(start = 6, end = 7, type = SuggestionType.Mention, text = "")
|
currentSuggestion = Suggestion(start = 6, end = 7, type = SuggestionType.Mention, text = "")
|
||||||
}
|
}
|
||||||
val mention = ResolvedMentionSuggestion.AtRoom
|
val mention = ResolvedSuggestion.AtRoom
|
||||||
val permalinkBuilder = FakePermalinkBuilder()
|
val permalinkBuilder = FakePermalinkBuilder()
|
||||||
val permalinkParser = FakePermalinkParser(result = { PermalinkData.FallbackLink(Uri.EMPTY, false) })
|
val permalinkParser = FakePermalinkParser(result = { PermalinkData.FallbackLink(Uri.EMPTY, false) })
|
||||||
val mentionSpanProvider = aMentionSpanProvider(permalinkParser = permalinkParser)
|
val mentionSpanProvider = aMentionSpanProvider(permalinkParser = permalinkParser)
|
||||||
|
|
||||||
state.insertMention(mention, mentionSpanProvider, permalinkBuilder)
|
state.insertSuggestion(mention, mentionSpanProvider, permalinkBuilder)
|
||||||
|
|
||||||
val mentions = state.getMentions()
|
val mentions = state.getMentions()
|
||||||
assertThat(mentions).isNotEmpty()
|
assertThat(mentions).isNotEmpty()
|
||||||
assertThat(mentions.firstOrNull()).isInstanceOf(Mention.AtRoom::class.java)
|
assertThat(mentions.firstOrNull()).isInstanceOf(IntentionalMention.Room::class.java)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -115,14 +115,18 @@ class MarkdownTextEditorStateTest {
|
|||||||
@Test
|
@Test
|
||||||
fun `getMessageMarkdown - when there are MentionSpans returns the same text with links to the mentions`() {
|
fun `getMessageMarkdown - when there are MentionSpans returns the same text with links to the mentions`() {
|
||||||
val text = "No mentions here"
|
val text = "No mentions here"
|
||||||
val permalinkBuilder = FakePermalinkBuilder(permalinkForUserLambda = { Result.success("https://matrix.to/#/$it") })
|
val permalinkBuilder = FakePermalinkBuilder(
|
||||||
|
permalinkForUserLambda = { Result.success("https://matrix.to/#/$it") },
|
||||||
|
permalinkForRoomAliasLambda = { Result.success("https://matrix.to/#/$it") },
|
||||||
|
)
|
||||||
val state = MarkdownTextEditorState(initialText = text, initialFocus = true)
|
val state = MarkdownTextEditorState(initialText = text, initialFocus = true)
|
||||||
state.text.update(aMarkdownTextWithMentions(), needsDisplaying = false)
|
state.text.update(aMarkdownTextWithMentions(), needsDisplaying = false)
|
||||||
|
|
||||||
val markdown = state.getMessageMarkdown(permalinkBuilder = permalinkBuilder)
|
val markdown = state.getMessageMarkdown(permalinkBuilder = permalinkBuilder)
|
||||||
|
|
||||||
assertThat(markdown).isEqualTo(
|
assertThat(markdown).isEqualTo(
|
||||||
"Hello [@alice:matrix.org](https://matrix.to/#/@alice:matrix.org) and everyone in @room"
|
"Hello [@alice:matrix.org](https://matrix.to/#/@alice:matrix.org) and everyone in @room" +
|
||||||
|
" and a room [#room:domain.org](https://matrix.to/#/#room:domain.org)"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,8 +145,8 @@ class MarkdownTextEditorStateTest {
|
|||||||
val mentions = state.getMentions()
|
val mentions = state.getMentions()
|
||||||
|
|
||||||
assertThat(mentions).isNotEmpty()
|
assertThat(mentions).isNotEmpty()
|
||||||
assertThat((mentions.firstOrNull() as? Mention.User)?.userId?.value).isEqualTo("@alice:matrix.org")
|
assertThat((mentions.firstOrNull() as? IntentionalMention.User)?.userId?.value).isEqualTo("@alice:matrix.org")
|
||||||
assertThat(mentions.lastOrNull()).isInstanceOf(Mention.AtRoom::class.java)
|
assertThat(mentions.lastOrNull()).isInstanceOf(IntentionalMention.Room::class.java)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun aMentionSpanProvider(
|
private fun aMentionSpanProvider(
|
||||||
@@ -154,6 +158,7 @@ class MarkdownTextEditorStateTest {
|
|||||||
private fun aMarkdownTextWithMentions(): CharSequence {
|
private fun aMarkdownTextWithMentions(): CharSequence {
|
||||||
val userMentionSpan = MentionSpan("@Alice", "@alice:matrix.org", MentionSpan.Type.USER)
|
val userMentionSpan = MentionSpan("@Alice", "@alice:matrix.org", MentionSpan.Type.USER)
|
||||||
val atRoomMentionSpan = MentionSpan("@room", "@room", MentionSpan.Type.EVERYONE)
|
val atRoomMentionSpan = MentionSpan("@room", "@room", MentionSpan.Type.EVERYONE)
|
||||||
|
val roomMentionSpan = MentionSpan("#room:domain.org", "#room:domain.org", MentionSpan.Type.ROOM)
|
||||||
return buildSpannedString {
|
return buildSpannedString {
|
||||||
append("Hello ")
|
append("Hello ")
|
||||||
inSpans(userMentionSpan) {
|
inSpans(userMentionSpan) {
|
||||||
@@ -163,6 +168,10 @@ class MarkdownTextEditorStateTest {
|
|||||||
inSpans(atRoomMentionSpan) {
|
inSpans(atRoomMentionSpan) {
|
||||||
append("@")
|
append("@")
|
||||||
}
|
}
|
||||||
|
append(" and a room ")
|
||||||
|
inSpans(roomMentionSpan) {
|
||||||
|
append("#room:domain.org")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user