Add mentions to rich text editor

This commit is contained in:
jonnyandrew 2023-05-05 09:29:57 +01:00 committed by jonnyandrew
parent e2afa0ccd3
commit 2d1dcd34c0
No known key found for this signature in database
GPG key ID: 0D58D4EF33D27015
12 changed files with 375 additions and 24 deletions

View file

@ -101,7 +101,7 @@ ext.libs = [
],
element : [
'opusencoder' : "io.element.android:opusencoder:1.1.0",
'wysiwyg' : "io.element.android:wysiwyg:1.2.2"
'wysiwyg' : "io.element.android:wysiwyg:2.2.0"
],
squareup : [
'moshi' : "com.squareup.moshi:moshi:$moshi",
@ -172,6 +172,7 @@ ext.libs = [
'kluent' : "org.amshove.kluent:kluent-android:1.73",
'timberJunitRule' : "net.lachlanmckee:timber-junit-rule:1.0.1",
'junit' : "junit:junit:4.13.2",
'robolectric' : "org.robolectric:robolectric:4.9",
]
]

View file

@ -189,6 +189,7 @@ ext.groups = [
'org.codehaus.groovy',
'org.codehaus.mojo',
'org.codehaus.woodstox',
'org.conscrypt',
'org.eclipse.ee4j',
'org.ec4j.core',
'org.freemarker',
@ -221,6 +222,7 @@ ext.groups = [
'org.ow2.asm',
'org.ow2.asm',
'org.reactivestreams',
'org.robolectric',
'org.slf4j',
'org.sonatype.oss',
'org.testng',

View file

@ -299,6 +299,7 @@ dependencies {
testImplementation libs.tests.kluent
testImplementation libs.mockk.mockk
testImplementation libs.androidx.coreTesting
testImplementation libs.tests.robolectric
// Plant Timber tree for test
testImplementation libs.tests.timberJunitRule
testImplementation libs.airbnb.mavericksTesting

View file

@ -23,7 +23,7 @@ import android.text.Spanned
import android.text.style.StrikethroughSpan
import androidx.core.text.getSpans
import im.vector.app.features.html.HtmlCodeSpan
import io.element.android.wysiwyg.spans.InlineCodeSpan
import io.element.android.wysiwyg.view.spans.InlineCodeSpan
import io.mockk.justRun
import io.mockk.mockk
import io.mockk.slot

View file

@ -40,23 +40,31 @@ import im.vector.app.features.displayname.getBestName
import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.html.PillImageSpan
import im.vector.app.features.themes.ThemeUtils
import io.element.android.wysiwyg.EditorEditText
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.permalinks.PermalinkService
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.util.MatrixItem
import org.matrix.android.sdk.api.util.toEveryoneInRoomMatrixItem
import org.matrix.android.sdk.api.util.toMatrixItem
import org.matrix.android.sdk.api.util.toRoomAliasMatrixItem
import timber.log.Timber
class AutoCompleter @AssistedInject constructor(
@Assisted val roomId: String,
@Assisted val isInThreadTimeline: Boolean,
private val session: Session,
private val avatarRenderer: AvatarRenderer,
private val commandAutocompletePolicy: CommandAutocompletePolicy,
autocompleteCommandPresenterFactory: AutocompleteCommandPresenter.Factory,
private val autocompleteMemberPresenterFactory: AutocompleteMemberPresenter.Factory,
private val autocompleteRoomPresenter: AutocompleteRoomPresenter,
private val autocompleteEmojiPresenter: AutocompleteEmojiPresenter
private val autocompleteEmojiPresenter: AutocompleteEmojiPresenter,
) {
private val permalinkService: PermalinkService
get() = session.permalinkService()
private lateinit var autocompleteMemberPresenter: AutocompleteMemberPresenter
@AssistedFactory
@ -99,6 +107,9 @@ class AutoCompleter @AssistedInject constructor(
}
private fun setupCommands(backgroundDrawable: Drawable, editText: EditText) {
// Rich text editor is not yet supported
if (editText is EditorEditText) return
Autocomplete.on<Command>(editText)
.with(commandAutocompletePolicy)
.with(autocompleteCommandPresenter)
@ -128,17 +139,15 @@ class AutoCompleter @AssistedInject constructor(
.with(backgroundDrawable)
.with(object : AutocompleteCallback<AutocompleteMemberItem> {
override fun onPopupItemClicked(editable: Editable, item: AutocompleteMemberItem): Boolean {
return when (item) {
is AutocompleteMemberItem.Header -> false // do nothing header is not clickable
is AutocompleteMemberItem.RoomMember -> {
insertMatrixItem(editText, editable, TRIGGER_AUTO_COMPLETE_MEMBERS, item.roomMemberSummary.toMatrixItem())
true
}
is AutocompleteMemberItem.Everyone -> {
insertMatrixItem(editText, editable, TRIGGER_AUTO_COMPLETE_MEMBERS, item.roomSummary.toEveryoneInRoomMatrixItem())
true
}
}
val matrixItem = when (item) {
is AutocompleteMemberItem.Header -> null // do nothing header is not clickable
is AutocompleteMemberItem.RoomMember -> item.roomMemberSummary.toMatrixItem()
is AutocompleteMemberItem.Everyone -> item.roomSummary.toEveryoneInRoomMatrixItem()
} ?: return false
insertMatrixItem(editText, editable, TRIGGER_AUTO_COMPLETE_MEMBERS, matrixItem)
return true
}
override fun onPopupVisibilityChanged(shown: Boolean) {
@ -166,6 +175,9 @@ class AutoCompleter @AssistedInject constructor(
}
private fun setupEmojis(backgroundDrawable: Drawable, editText: EditText) {
// Rich text editor is not yet supported
if (editText is EditorEditText) return
Autocomplete.on<String>(editText)
.with(CharPolicy(TRIGGER_AUTO_COMPLETE_EMOJIS, false))
.with(autocompleteEmojiPresenter)
@ -197,7 +209,41 @@ class AutoCompleter @AssistedInject constructor(
.build()
}
private fun insertMatrixItem(editText: EditText, editable: Editable, firstChar: Char, matrixItem: MatrixItem) {
private fun insertMatrixItem(editText: EditText, editable: Editable, firstChar: Char, matrixItem: MatrixItem) =
if (editText is EditorEditText) {
insertMatrixItemIntoRichTextEditor(editText, matrixItem)
} else {
insertMatrixItemIntoEditable(editText, editable, firstChar, matrixItem)
}
private fun insertMatrixItemIntoRichTextEditor(editorEditText: EditorEditText, matrixItem: MatrixItem) {
if (matrixItem is MatrixItem.EveryoneInRoomItem) {
editorEditText.replaceTextSuggestion(matrixItem.displayName)
return
}
val permalink = permalinkService.createPermalink(matrixItem.id)
if (permalink == null) {
Timber.e(NullPointerException("Cannot autocomplete as permalink is null"))
return
}
val linkText = when (matrixItem) {
is MatrixItem.RoomAliasItem,
is MatrixItem.RoomItem,
is MatrixItem.SpaceItem ->
matrixItem.id
is MatrixItem.EveryoneInRoomItem,
is MatrixItem.UserItem,
is MatrixItem.EventItem ->
matrixItem.getBestName()
}
editorEditText.setLinkSuggestion(url = permalink, text = linkText)
}
private fun insertMatrixItemIntoEditable(editText: EditText, editable: Editable, firstChar: Char, matrixItem: MatrixItem) {
// Detect last firstChar and remove it
var startIndex = editable.lastIndexOf(firstChar)
if (startIndex == -1) {

View file

@ -765,6 +765,9 @@ class TimelineViewModel @AssistedInject constructor(
return room?.membershipService()?.getRoomMember(userId)
}
fun getRoom(roomId: String): RoomSummary? =
session.roomService().getRoomSummary(roomId)
private fun handleComposerFocusChange(action: RoomDetailAction.ComposerFocusChange) {
if (room == null) return
// Ensure outbound session keys

View file

@ -83,6 +83,7 @@ import im.vector.app.features.home.room.detail.TimelineViewModel
import im.vector.app.features.home.room.detail.composer.link.SetLinkFragment
import im.vector.app.features.home.room.detail.composer.link.SetLinkSharedAction
import im.vector.app.features.home.room.detail.composer.link.SetLinkSharedActionViewModel
import im.vector.app.features.home.room.detail.composer.mentions.PillDisplayHandler
import im.vector.app.features.home.room.detail.composer.voice.VoiceMessageRecorderView
import im.vector.app.features.home.room.detail.timeline.action.MessageSharedActionViewModel
import im.vector.app.features.home.room.detail.upgrade.MigrateRoomBottomSheet
@ -315,9 +316,7 @@ class MessageComposerFragment : VectorBaseFragment<FragmentComposerBinding>(), A
val composerEditText = composer.editText
composerEditText.setHint(R.string.room_message_placeholder)
if (!vectorPreferences.isRichTextEditorEnabled()) {
autoCompleter.setup(composerEditText)
}
autoCompleter.setup(composerEditText)
observerUserTyping()
@ -404,6 +403,13 @@ class MessageComposerFragment : VectorBaseFragment<FragmentComposerBinding>(), A
SetLinkFragment.show(isTextSupported, initialLink, childFragmentManager)
}
}
(composer as? RichTextComposerLayout)?.pillDisplayHandler = PillDisplayHandler(
roomId = roomId,
getRoom = timelineViewModel::getRoom,
getMember = timelineViewModel::getMember,
) { matrixItem: MatrixItem ->
PillImageSpan(glideRequests, avatarRenderer, requireContext(), matrixItem)
}
}
private fun sendTextMessage(text: CharSequence, formattedText: String? = null) {

View file

@ -49,10 +49,14 @@ import im.vector.app.core.utils.DimensionConverter
import im.vector.app.databinding.ComposerRichTextLayoutBinding
import im.vector.app.databinding.ViewRichTextMenuButtonBinding
import im.vector.app.features.home.room.detail.composer.images.UriContentListener
import im.vector.app.features.home.room.detail.composer.mentions.PillDisplayHandler
import io.element.android.wysiwyg.EditorEditText
import io.element.android.wysiwyg.inputhandlers.models.InlineFormat
import io.element.android.wysiwyg.inputhandlers.models.LinkAction
import io.element.android.wysiwyg.display.KeywordDisplayHandler
import io.element.android.wysiwyg.display.LinkDisplayHandler
import io.element.android.wysiwyg.display.TextDisplay
import io.element.android.wysiwyg.utils.RustErrorCollector
import io.element.android.wysiwyg.view.models.InlineFormat
import io.element.android.wysiwyg.view.models.LinkAction
import uniffi.wysiwyg_composer.ActionState
import uniffi.wysiwyg_composer.ComposerAction
@ -102,6 +106,8 @@ internal class RichTextComposerLayout @JvmOverloads constructor(
override val attachmentButton: ImageButton
get() = views.attachmentButton
var pillDisplayHandler: PillDisplayHandler? = null
// Border of the EditText
private val borderShapeDrawable: MaterialShapeDrawable by lazy {
MaterialShapeDrawable().apply {
@ -227,6 +233,16 @@ internal class RichTextComposerLayout @JvmOverloads constructor(
views.composerEditTextOuterBorder.background = borderShapeDrawable
setupRichTextMenu()
views.richTextComposerEditText.linkDisplayHandler = LinkDisplayHandler { text, url ->
pillDisplayHandler?.resolveLinkDisplay(text, url) ?: TextDisplay.Plain
}
views.richTextComposerEditText.keywordDisplayHandler = object : KeywordDisplayHandler {
override val keywords: List<String>
get() = pillDisplayHandler?.keywords.orEmpty()
override fun resolveKeywordDisplay(text: String): TextDisplay =
pillDisplayHandler?.resolveKeywordDisplay(text) ?: TextDisplay.Plain
}
updateTextFieldBorder(isFullScreen)
}
@ -269,7 +285,7 @@ internal class RichTextComposerLayout @JvmOverloads constructor(
views.richTextComposerEditText.getLinkAction()?.let {
when (it) {
LinkAction.InsertLink -> callback?.onSetLink(isTextSupported = true, initialLink = null)
is LinkAction.SetLink -> callback?.onSetLink(isTextSupported = false, initialLink = it.currentLink)
is LinkAction.SetLink -> callback?.onSetLink(isTextSupported = false, initialLink = it.currentUrl)
}
}
}

View file

@ -0,0 +1,78 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.home.room.detail.composer.mentions
import android.text.style.ReplacementSpan
import io.element.android.wysiwyg.display.KeywordDisplayHandler
import io.element.android.wysiwyg.display.LinkDisplayHandler
import io.element.android.wysiwyg.display.TextDisplay
import org.matrix.android.sdk.api.session.permalinks.PermalinkData
import org.matrix.android.sdk.api.session.permalinks.PermalinkParser
import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.util.MatrixItem
import org.matrix.android.sdk.api.util.toEveryoneInRoomMatrixItem
import org.matrix.android.sdk.api.util.toMatrixItem
/**
* A rich text editor [LinkDisplayHandler] and [KeywordDisplayHandler]
* that helps with replacing user and room links with pills.
*/
internal class PillDisplayHandler(
private val roomId: String,
private val getRoom: (roomId: String) -> RoomSummary?,
private val getMember: (userId: String) -> RoomMemberSummary?,
private val replacementSpanFactory: (matrixItem: MatrixItem) -> ReplacementSpan,
) : LinkDisplayHandler, KeywordDisplayHandler {
override fun resolveLinkDisplay(text: String, url: String): TextDisplay {
val matrixItem = when (val permalink = PermalinkParser.parse(url)) {
is PermalinkData.UserLink -> {
val userId = permalink.userId
when (val roomMember = getMember(userId)) {
null -> MatrixItem.UserItem(userId, userId, null)
else -> roomMember.toMatrixItem()
}
}
is PermalinkData.RoomLink -> {
val roomId = permalink.roomIdOrAlias
val room = getRoom(roomId)
when {
room == null -> MatrixItem.RoomItem(roomId, roomId, null)
text == MatrixItem.NOTIFY_EVERYONE -> room.toEveryoneInRoomMatrixItem()
else -> room.toMatrixItem()
}
}
else ->
return TextDisplay.Plain
}
val replacement = replacementSpanFactory.invoke(matrixItem)
return TextDisplay.Custom(customSpan = replacement)
}
override val keywords: List<String>
get() = listOf(MatrixItem.NOTIFY_EVERYONE)
override fun resolveKeywordDisplay(text: String): TextDisplay =
when (text) {
MatrixItem.NOTIFY_EVERYONE -> {
val matrixItem = getRoom(roomId)?.toEveryoneInRoomMatrixItem()
?: MatrixItem.EveryoneInRoomItem(roomId)
TextDisplay.Custom(replacementSpanFactory.invoke(matrixItem))
}
else -> TextDisplay.Plain
}
}

View file

@ -43,7 +43,7 @@ import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.resources.ColorProvider
import im.vector.app.core.utils.DimensionConverter
import im.vector.app.features.settings.VectorPreferences
import io.element.android.wysiwyg.spans.InlineCodeSpan
import io.element.android.wysiwyg.view.spans.InlineCodeSpan
import io.noties.markwon.AbstractMarkwonPlugin
import io.noties.markwon.Markwon
import io.noties.markwon.MarkwonPlugin

View file

@ -18,8 +18,8 @@ package im.vector.app.features.html
import im.vector.app.core.utils.DimensionConverter
import im.vector.app.features.settings.VectorPreferences
import io.element.android.wysiwyg.spans.CodeBlockSpan
import io.element.android.wysiwyg.spans.InlineCodeSpan
import io.element.android.wysiwyg.view.spans.CodeBlockSpan
import io.element.android.wysiwyg.view.spans.InlineCodeSpan
import io.noties.markwon.MarkwonVisitor
import io.noties.markwon.SpannableBuilder
import io.noties.markwon.core.MarkwonTheme

View file

@ -0,0 +1,198 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.home.room.detail.composer.mentions
import android.graphics.Canvas
import android.graphics.Paint
import android.text.style.ReplacementSpan
import io.element.android.wysiwyg.display.TextDisplay
import io.mockk.every
import io.mockk.mockk
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.util.MatrixItem
import org.matrix.android.sdk.api.util.MatrixItem.Companion.NOTIFY_EVERYONE
import org.robolectric.RobolectricTestRunner
@RunWith(RobolectricTestRunner::class)
internal class PillDisplayHandlerTest {
private val mockGetMember = mockk<(userId: String) -> RoomMemberSummary?>()
private val mockGetRoom = mockk<(roomId: String) -> RoomSummary?>()
private val fakeReplacementSpanFactory = { matrixItem: MatrixItem -> MatrixItemHolderSpan(matrixItem) }
private companion object {
const val ROOM_ID = "!thisroom:matrix.org"
const val NON_MATRIX_URL = "https://example.com"
const val UNKNOWN_MATRIX_ROOM_ID = "!unknown:matrix.org"
const val UNKNOWN_MATRIX_ROOM_URL = "https://matrix.to/#/$UNKNOWN_MATRIX_ROOM_ID"
const val KNOWN_MATRIX_ROOM_ID = "!known:matrix.org"
const val KNOWN_MATRIX_ROOM_URL = "https://matrix.to/#/$KNOWN_MATRIX_ROOM_ID"
const val KNOWN_MATRIX_ROOM_AVATAR = "https://example.com/avatar.png"
const val KNOWN_MATRIX_ROOM_NAME = "known room"
const val UNKNOWN_MATRIX_USER_ID = "@unknown:matrix.org"
const val UNKNOWN_MATRIX_USER_URL = "https://matrix.to/#/$UNKNOWN_MATRIX_USER_ID"
const val KNOWN_MATRIX_USER_ID = "@known:matrix.org"
const val KNOWN_MATRIX_USER_URL = "https://matrix.to/#/$KNOWN_MATRIX_USER_ID"
const val KNOWN_MATRIX_USER_AVATAR = "https://example.com/avatar.png"
const val KNOWN_MATRIX_USER_NAME = "known user"
}
@Before
fun setUp() {
every { mockGetMember(UNKNOWN_MATRIX_USER_ID) } returns null
every { mockGetMember(KNOWN_MATRIX_USER_ID) } returns createFakeRoomMember(KNOWN_MATRIX_USER_NAME, KNOWN_MATRIX_USER_ID, KNOWN_MATRIX_USER_AVATAR)
every { mockGetRoom(UNKNOWN_MATRIX_ROOM_ID) } returns null
every { mockGetRoom(KNOWN_MATRIX_ROOM_ID) } returns createFakeRoom(KNOWN_MATRIX_ROOM_ID, KNOWN_MATRIX_ROOM_NAME, KNOWN_MATRIX_ROOM_AVATAR)
every { mockGetRoom(ROOM_ID) } returns createFakeRoom(ROOM_ID, KNOWN_MATRIX_ROOM_NAME, KNOWN_MATRIX_ROOM_AVATAR)
}
@Test
fun `when resolve non-matrix link, then it returns plain text`() {
val subject = createSubject()
val result = subject.resolveLinkDisplay("text", NON_MATRIX_URL)
assertEquals(TextDisplay.Plain, result)
}
@Test
fun `when resolve unknown user link, then it returns generic custom pill`() {
val subject = createSubject()
val matrixItem = subject.resolveLinkDisplay("text", UNKNOWN_MATRIX_USER_URL)
.getMatrixItem()
assertEquals(MatrixItem.UserItem(UNKNOWN_MATRIX_USER_ID, UNKNOWN_MATRIX_USER_ID, null), matrixItem)
}
@Test
fun `when resolve known user link, then it returns named custom pill`() {
val subject = createSubject()
val matrixItem = subject.resolveLinkDisplay("text", KNOWN_MATRIX_USER_URL)
.getMatrixItem()
assertEquals(MatrixItem.UserItem(KNOWN_MATRIX_USER_ID, KNOWN_MATRIX_USER_NAME, KNOWN_MATRIX_USER_AVATAR), matrixItem)
}
@Test
fun `when resolve unknown room link, then it returns generic custom pill`() {
val subject = createSubject()
val matrixItem = subject.resolveLinkDisplay("text", UNKNOWN_MATRIX_ROOM_URL)
.getMatrixItem()
assertEquals(MatrixItem.RoomItem(UNKNOWN_MATRIX_ROOM_ID, UNKNOWN_MATRIX_ROOM_ID, null), matrixItem)
}
@Test
fun `when resolve known room link, then it returns named custom pill`() {
val subject = createSubject()
val matrixItem = subject.resolveLinkDisplay("text", KNOWN_MATRIX_ROOM_URL)
.getMatrixItem()
assertEquals(MatrixItem.RoomItem(KNOWN_MATRIX_ROOM_ID, KNOWN_MATRIX_ROOM_NAME, KNOWN_MATRIX_ROOM_AVATAR), matrixItem)
}
@Test
fun `when resolve @room link, then it returns room notification custom pill`() {
val subject = createSubject()
val matrixItem = subject.resolveLinkDisplay("@room", KNOWN_MATRIX_ROOM_URL)
.getMatrixItem()
assertEquals(MatrixItem.EveryoneInRoomItem(KNOWN_MATRIX_ROOM_ID, NOTIFY_EVERYONE, KNOWN_MATRIX_ROOM_AVATAR, KNOWN_MATRIX_ROOM_NAME), matrixItem)
}
@Test
fun `when resolve @room keyword, then it returns room notification custom pill`() {
val subject = createSubject()
val matrixItem = subject.resolveKeywordDisplay("@room")
.getMatrixItem()
assertEquals(MatrixItem.EveryoneInRoomItem(ROOM_ID, NOTIFY_EVERYONE, KNOWN_MATRIX_ROOM_AVATAR, KNOWN_MATRIX_ROOM_NAME), matrixItem)
}
@Test
fun `given cannot get current room, when resolve @room keyword, then it returns room notification custom pill`() {
val subject = createSubject()
every { mockGetRoom(ROOM_ID) } returns null
val matrixItem = subject.resolveKeywordDisplay("@room")
.getMatrixItem()
assertEquals(MatrixItem.EveryoneInRoomItem(ROOM_ID, NOTIFY_EVERYONE, null, null), matrixItem)
}
@Test
fun `when get keywords, then it returns @room`() {
val subject = createSubject()
assertEquals(listOf("@room"), subject.keywords)
}
private fun TextDisplay.getMatrixItem(): MatrixItem? {
val customSpan = this as? TextDisplay.Custom
assertNotNull("The URL did not resolve to a custom link display method", customSpan)
val matrixItemHolderSpan = customSpan!!.customSpan as MatrixItemHolderSpan
return matrixItemHolderSpan.matrixItem
}
private fun createSubject(): PillDisplayHandler = PillDisplayHandler(
roomId = ROOM_ID,
getRoom = mockGetRoom,
getMember = mockGetMember,
replacementSpanFactory = fakeReplacementSpanFactory
)
private fun createFakeRoomMember(displayName: String, userId: String, avatarUrl: String): RoomMemberSummary = RoomMemberSummary(
membership = Membership.JOIN,
userId = userId,
displayName = displayName,
avatarUrl = avatarUrl,
)
private fun createFakeRoom(roomId: String, roomName: String, avatarUrl: String): RoomSummary = RoomSummary(
roomId = roomId,
displayName = roomName,
avatarUrl = avatarUrl,
encryptionEventTs = null,
typingUsers = emptyList(),
isEncrypted = false
)
data class MatrixItemHolderSpan(
val matrixItem: MatrixItem
) : ReplacementSpan() {
override fun draw(canvas: Canvas, text: CharSequence?, start: Int, end: Int, x: Float, top: Int, y: Int, bottom: Int, paint: Paint) {
// Do nothing
}
override fun getSize(paint: Paint, text: CharSequence?, start: Int, end: Int, fm: Paint.FontMetricsInt?): Int {
return 0
}
}
}