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:
Benoit Marty 2024-08-21 14:09:29 +02:00
parent 4385a7779c
commit 4b8985e501
34 changed files with 399 additions and 222 deletions

View file

@ -111,13 +111,13 @@ fun MarkdownTextInput(
state.text.update(editable, false)
state.lineCount = lineCount
state.currentMentionSuggestion = editable?.checkSuggestionNeeded()
onReceiveSuggestion(state.currentMentionSuggestion)
state.currentSuggestion = editable?.checkSuggestionNeeded()
onReceiveSuggestion(state.currentSuggestion)
}
onSelectionChangeListener = { selStart, selEnd ->
state.selection = selStart..selEnd
state.currentMentionSuggestion = editableText.checkSuggestionNeeded()
onReceiveSuggestion(state.currentMentionSuggestion)
state.currentSuggestion = editableText.checkSuggestionNeeded()
onReceiveSuggestion(state.currentSuggestion)
}
if (onSelectRichContent != null) {
ViewCompat.setOnReceiveContentListener(

View file

@ -17,10 +17,13 @@
package io.element.android.libraries.textcomposer.mentions
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.roomlist.RoomSummary
@Immutable
sealed interface ResolvedMentionSuggestion {
data object AtRoom : ResolvedMentionSuggestion
data class Member(val roomMember: RoomMember) : ResolvedMentionSuggestion
sealed interface ResolvedSuggestion {
data object AtRoom : ResolvedSuggestion
data class Member(val roomMember: RoomMember) : ResolvedSuggestion
data class Alias(val roomAlias: RoomAlias, val roomSummary: RoomSummary) : ResolvedSuggestion
}

View file

@ -31,13 +31,14 @@ import androidx.compose.runtime.saveable.SaverScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
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.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.mentions.MentionSpan
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 kotlinx.parcelize.Parcelize
@ -51,16 +52,16 @@ class MarkdownTextEditorState(
var hasFocus by mutableStateOf(initialFocus)
var requestFocusAction by mutableStateOf({})
var lineCount by mutableIntStateOf(1)
var currentMentionSuggestion by mutableStateOf<Suggestion?>(null)
var currentSuggestion by mutableStateOf<Suggestion?>(null)
fun insertMention(
mention: ResolvedMentionSuggestion,
fun insertSuggestion(
resolvedSuggestion: ResolvedSuggestion,
mentionSpanProvider: MentionSpanProvider,
permalinkBuilder: PermalinkBuilder,
) {
val suggestion = currentMentionSuggestion ?: return
when (mention) {
is ResolvedMentionSuggestion.AtRoom -> {
val suggestion = currentSuggestion ?: return
when (resolvedSuggestion) {
is ResolvedSuggestion.AtRoom -> {
val currentText = SpannableStringBuilder(text.value())
val replaceText = "@room"
val roomPill = mentionSpanProvider.getMentionSpanFor(replaceText, "")
@ -70,10 +71,21 @@ class MarkdownTextEditorState(
text.update(currentText, true)
selection = IntRange(end + 1, end + 1)
}
is ResolvedMentionSuggestion.Member -> {
is ResolvedSuggestion.Member -> {
val currentText = SpannableStringBuilder(text.value())
val text = mention.roomMember.displayName?.prependIndent("@") ?: mention.roomMember.userId.value
val link = permalinkBuilder.permalinkForUser(mention.roomMember.userId).getOrNull() ?: return
val text = resolvedSuggestion.roomMember.displayName?.prependIndent("@") ?: resolvedSuggestion.roomMember.userId.value
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)
currentText.replace(suggestion.start, suggestion.end, "@ ")
val end = suggestion.start + 1
@ -96,14 +108,18 @@ class MarkdownTextEditorState(
val end = charSequence.getSpanEnd(mention)
when (mention.type) {
MentionSpan.Type.USER -> {
val link = permalinkBuilder.permalinkForUser(UserId(mention.rawValue)).getOrNull() ?: continue
replace(start, end, "[${mention.rawValue}]($link)")
permalinkBuilder.permalinkForUser(UserId(mention.rawValue)).getOrNull()?.let { link ->
replace(start, end, "[${mention.rawValue}]($link)")
}
}
MentionSpan.Type.EVERYONE -> {
replace(start, end, "@room")
}
// Nothing to do here yet
MentionSpan.Type.ROOM -> Unit
MentionSpan.Type.ROOM -> {
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 mentionSpans = text.getSpans<MentionSpan>(0, text.length)
return mentionSpans.mapNotNull { mentionSpan ->
when (mentionSpan.type) {
MentionSpan.Type.USER -> Mention.User(UserId(mentionSpan.rawValue))
MentionSpan.Type.EVERYONE -> Mention.AtRoom
MentionSpan.Type.USER -> IntentionalMention.User(UserId(mentionSpan.rawValue))
MentionSpan.Type.EVERYONE -> IntentionalMention.Room
MentionSpan.Type.ROOM -> null
}
}

View file

@ -16,10 +16,10 @@
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(
val html: String?,
val markdown: String,
val mentions: List<Mention>,
val intentionalMentions: List<IntentionalMention>,
)

View file

@ -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.mentions.MentionSpan
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.Suggestion
import io.element.android.libraries.textcomposer.model.SuggestionType
@ -157,13 +157,13 @@ class MarkdownTextInputTest {
val permalinkParser = FakePermalinkParser(result = { PermalinkData.UserLink(A_SESSION_ID) })
val permalinkBuilder = FakePermalinkBuilder(permalinkForUserLambda = { Result.success("https://matrix.to/#/$A_SESSION_ID") })
val state = aMarkdownTextEditorState(initialText = "@", initialFocus = true)
state.currentMentionSuggestion = Suggestion(0, 1, SuggestionType.Mention, "")
state.currentSuggestion = Suggestion(0, 1, SuggestionType.Mention, "")
rule.setMarkdownTextInput(state = state)
var editor: EditText? = null
rule.activityRule.scenario.onActivity {
editor = it.findEditor()
state.insertMention(
ResolvedMentionSuggestion.Member(roomMember = aRoomMember()),
state.insertSuggestion(
ResolvedSuggestion.Member(roomMember = aRoomMember()),
MentionSpanProvider(permalinkParser = permalinkParser),
permalinkBuilder,
)

View file

@ -32,7 +32,7 @@ import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
@RunWith(RobolectricTestRunner::class)
class MentionSpanProviderTest {
class IntentionalMentionSpanProviderTest {
@JvmField @Rule
val warmUpRule = WarmUpRule()

View file

@ -22,13 +22,13 @@ import androidx.core.text.inSpans
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.common.truth.Truth.assertThat
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.FakePermalinkParser
import io.element.android.libraries.matrix.test.room.aRoomMember
import io.element.android.libraries.textcomposer.mentions.MentionSpan
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.Suggestion
import io.element.android.libraries.textcomposer.model.SuggestionType
@ -41,11 +41,11 @@ class MarkdownTextEditorStateTest {
fun `insertMention - with no currentMentionSuggestion does nothing`() {
val state = MarkdownTextEditorState(initialText = "Hello @", initialFocus = true)
val member = aRoomMember()
val mention = ResolvedMentionSuggestion.Member(member)
val mention = ResolvedSuggestion.Member(member)
val permalinkBuilder = FakePermalinkBuilder()
val mentionSpanProvider = aMentionSpanProvider()
state.insertMention(mention, mentionSpanProvider, permalinkBuilder)
state.insertSuggestion(mention, mentionSpanProvider, permalinkBuilder)
assertThat(state.getMentions()).isEmpty()
}
@ -53,15 +53,15 @@ class MarkdownTextEditorStateTest {
@Test
fun `insertMention - with member but failed PermalinkBuilder result`() {
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 mention = ResolvedMentionSuggestion.Member(member)
val mention = ResolvedSuggestion.Member(member)
val permalinkParser = FakePermalinkParser(result = { PermalinkData.UserLink(member.userId) })
val permalinkBuilder = FakePermalinkBuilder(permalinkForUserLambda = { Result.failure(IllegalStateException("Failed")) })
val mentionSpanProvider = aMentionSpanProvider(permalinkParser = permalinkParser)
state.insertMention(mention, mentionSpanProvider, permalinkBuilder)
state.insertSuggestion(mention, mentionSpanProvider, permalinkBuilder)
val mentions = state.getMentions()
assertThat(mentions).isEmpty()
@ -70,36 +70,36 @@ class MarkdownTextEditorStateTest {
@Test
fun `insertMention - with member`() {
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 mention = ResolvedMentionSuggestion.Member(member)
val mention = ResolvedSuggestion.Member(member)
val permalinkParser = FakePermalinkParser(result = { PermalinkData.UserLink(member.userId) })
val permalinkBuilder = FakePermalinkBuilder(permalinkForUserLambda = { Result.success("https://matrix.to/#/${member.userId}") })
val mentionSpanProvider = aMentionSpanProvider(permalinkParser = permalinkParser)
state.insertMention(mention, mentionSpanProvider, permalinkBuilder)
state.insertSuggestion(mention, mentionSpanProvider, permalinkBuilder)
val mentions = state.getMentions()
assertThat(mentions).isNotEmpty()
assertThat((mentions.firstOrNull() as? Mention.User)?.userId).isEqualTo(member.userId)
assertThat((mentions.firstOrNull() as? IntentionalMention.User)?.userId).isEqualTo(member.userId)
}
@Test
fun `insertMention - with @room`() {
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 permalinkParser = FakePermalinkParser(result = { PermalinkData.FallbackLink(Uri.EMPTY, false) })
val mentionSpanProvider = aMentionSpanProvider(permalinkParser = permalinkParser)
state.insertMention(mention, mentionSpanProvider, permalinkBuilder)
state.insertSuggestion(mention, mentionSpanProvider, permalinkBuilder)
val mentions = state.getMentions()
assertThat(mentions).isNotEmpty()
assertThat(mentions.firstOrNull()).isInstanceOf(Mention.AtRoom::class.java)
assertThat(mentions.firstOrNull()).isInstanceOf(IntentionalMention.Room::class.java)
}
@Test
@ -115,14 +115,18 @@ class MarkdownTextEditorStateTest {
@Test
fun `getMessageMarkdown - when there are MentionSpans returns the same text with links to the mentions`() {
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)
state.text.update(aMarkdownTextWithMentions(), needsDisplaying = false)
val markdown = state.getMessageMarkdown(permalinkBuilder = permalinkBuilder)
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()
assertThat(mentions).isNotEmpty()
assertThat((mentions.firstOrNull() as? Mention.User)?.userId?.value).isEqualTo("@alice:matrix.org")
assertThat(mentions.lastOrNull()).isInstanceOf(Mention.AtRoom::class.java)
assertThat((mentions.firstOrNull() as? IntentionalMention.User)?.userId?.value).isEqualTo("@alice:matrix.org")
assertThat(mentions.lastOrNull()).isInstanceOf(IntentionalMention.Room::class.java)
}
private fun aMentionSpanProvider(
@ -154,6 +158,7 @@ class MarkdownTextEditorStateTest {
private fun aMarkdownTextWithMentions(): CharSequence {
val userMentionSpan = MentionSpan("@Alice", "@alice:matrix.org", MentionSpan.Type.USER)
val atRoomMentionSpan = MentionSpan("@room", "@room", MentionSpan.Type.EVERYONE)
val roomMentionSpan = MentionSpan("#room:domain.org", "#room:domain.org", MentionSpan.Type.ROOM)
return buildSpannedString {
append("Hello ")
inSpans(userMentionSpan) {
@ -163,6 +168,10 @@ class MarkdownTextEditorStateTest {
inSpans(atRoomMentionSpan) {
append("@")
}
append(" and a room ")
inSpans(roomMentionSpan) {
append("#room:domain.org")
}
}
}
}