Fix some quoted messages having 'null' message bodies (#7532)

* Fix some quoted messages having 'null' message bodies
This commit is contained in:
Jorge Martin Espinosa 2022-11-14 12:01:29 +01:00 committed by GitHub
parent 0d3c779455
commit a476544761
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 631 additions and 25 deletions

1
changelog.d/7530.sdk Normal file
View file

@ -0,0 +1 @@
Fix a bug that caused messages with no formatted text to be quoted as "null".

View file

@ -804,20 +804,12 @@ internal class LocalEchoEventFactory @Inject constructor(
additionalContent: Content? = null,
): Event {
val messageContent = quotedEvent.getLastMessageContent()
val textMsg = if (messageContent is MessageContentWithFormattedBody) {
messageContent.formattedBody
} else {
messageContent?.body
}
val quoteText = legacyRiotQuoteText(textMsg, text)
val quoteFormattedText = "<blockquote>$textMsg</blockquote>$formattedText"
val formattedQuotedText = (messageContent as? MessageContentWithFormattedBody)?.formattedBody
val textContent = createQuoteTextContent(messageContent?.body, formattedQuotedText, text, formattedText, autoMarkdown)
return if (rootThreadEventId != null) {
createMessageEvent(
roomId,
markdownParser
.parse(quoteText, force = true, advanced = autoMarkdown).copy(formattedText = quoteFormattedText)
.toThreadTextContent(
textContent.toThreadTextContent(
rootThreadEventId = rootThreadEventId,
latestThreadEventId = localEchoRepository.getLatestThreadEvent(rootThreadEventId),
msgType = MessageType.MSGTYPE_TEXT
@ -827,31 +819,54 @@ internal class LocalEchoEventFactory @Inject constructor(
} else {
createFormattedTextEvent(
roomId,
markdownParser.parse(quoteText, force = true, advanced = autoMarkdown).copy(formattedText = quoteFormattedText),
textContent,
MessageType.MSGTYPE_TEXT,
additionalContent,
)
}
}
private fun legacyRiotQuoteText(quotedText: String?, myText: String): String {
val messageParagraphs = quotedText?.split("\n\n".toRegex())?.dropLastWhile { it.isEmpty() }?.toTypedArray()
return buildString {
if (messageParagraphs != null) {
for (i in messageParagraphs.indices) {
if (messageParagraphs[i].isNotBlank()) {
append("> ")
append(messageParagraphs[i])
}
private fun createQuoteTextContent(
quotedText: String?,
formattedQuotedText: String?,
text: String,
formattedText: String?,
autoMarkdown: Boolean
): TextContent {
val currentFormattedText = formattedText ?: if (autoMarkdown) {
val parsed = markdownParser.parse(text, force = true, advanced = true)
// If formattedText == text, formattedText is returned as null
parsed.formattedText ?: parsed.text
} else {
text
}
val processedFormattedQuotedText = formattedQuotedText ?: quotedText
if (i != messageParagraphs.lastIndex) {
append("\n\n")
}
val plainTextBody = buildString {
val plainMessageParagraphs = quotedText?.split("\n\n".toRegex())?.dropLastWhile { it.isEmpty() }?.toTypedArray().orEmpty()
plainMessageParagraphs.forEachIndexed { index, paragraph ->
if (paragraph.isNotBlank()) {
append("> ")
append(paragraph)
}
if (index != plainMessageParagraphs.lastIndex) {
append("\n\n")
}
}
append("\n\n")
append(myText)
append(text)
}
val formattedTextBody = buildString {
if (!processedFormattedQuotedText.isNullOrBlank()) {
append("<blockquote>")
append(processedFormattedQuotedText)
append("</blockquote>")
}
append("<br/>")
append(currentFormattedText)
}
return TextContent(plainTextBody, formattedTextBody)
}
companion object {

View file

@ -0,0 +1,241 @@
/*
* Copyright (c) 2022 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.internal.session.room.send
import org.amshove.kluent.internal.assertEquals
import org.junit.Before
import org.junit.Test
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.toContent
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.model.EditAggregatedSummary
import org.matrix.android.sdk.api.session.room.model.EventAnnotationsSummary
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
import org.matrix.android.sdk.api.session.room.model.message.MessageContentWithFormattedBody
import org.matrix.android.sdk.api.session.room.sender.SenderInfo
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.api.util.TextContent
import org.matrix.android.sdk.test.fakes.FakeClock
import org.matrix.android.sdk.test.fakes.FakeContext
import org.matrix.android.sdk.test.fakes.internal.session.content.FakeThumbnailExtractor
import org.matrix.android.sdk.test.fakes.internal.session.permalinks.FakePermalinkFactory
import org.matrix.android.sdk.test.fakes.internal.session.room.send.FakeLocalEchoRepository
import org.matrix.android.sdk.test.fakes.internal.session.room.send.FakeMarkdownParser
import org.matrix.android.sdk.test.fakes.internal.session.room.send.FakeWaveFormSanitizer
import org.matrix.android.sdk.test.fakes.internal.session.room.send.pills.FakeTextPillsUtils
@Suppress("MaxLineLength")
class LocalEchoEventFactoryTests {
companion object {
internal const val A_USER_ID_1 = "@user_1:matrix.org"
internal const val A_ROOM_ID = "!sUeOGZKsBValPTUMax:matrix.org"
internal const val AN_EVENT_ID = "\$vApgexcL8Vfh-WxYKsFKCDooo67ttbjm3TiVKXaWijU"
internal const val AN_EPOCH = 1655210176L
val A_START_EVENT = Event(
type = EventType.STATE_ROOM_CREATE,
eventId = AN_EVENT_ID,
originServerTs = 1652435922563,
senderId = A_USER_ID_1,
roomId = A_ROOM_ID
)
}
private val fakeContext = FakeContext()
private val fakeMarkdownParser = FakeMarkdownParser()
private val fakeTextPillsUtils = FakeTextPillsUtils()
private val fakeThumbnailExtractor = FakeThumbnailExtractor()
private val fakeWaveFormSanitizer = FakeWaveFormSanitizer()
private val fakeLocalEchoRepository = FakeLocalEchoRepository()
private val fakePermalinkFactory = FakePermalinkFactory()
private val fakeClock = FakeClock()
private val localEchoEventFactory = LocalEchoEventFactory(
context = fakeContext.instance,
userId = A_USER_ID_1,
markdownParser = fakeMarkdownParser.instance,
textPillsUtils = fakeTextPillsUtils.instance,
thumbnailExtractor = fakeThumbnailExtractor.instance,
waveformSanitizer = fakeWaveFormSanitizer.instance,
localEchoRepository = fakeLocalEchoRepository.instance,
permalinkFactory = fakePermalinkFactory.instance,
clock = fakeClock
)
@Before
fun setup() {
fakeClock.givenEpoch(AN_EPOCH)
fakeMarkdownParser.givenBoldMarkdown()
}
@Test
fun `given a null quotedText, when a quote event is created, then the result message should only contain the new text after new lines`() {
val event = createTimelineEvent(null, null)
val quotedContent = localEchoEventFactory.createQuotedTextEvent(
roomId = A_ROOM_ID,
quotedEvent = event,
text = "Text",
formattedText = null,
autoMarkdown = false,
rootThreadEventId = null,
additionalContent = null,
).content.toModel<MessageContent>()
assertEquals("\n\nText", quotedContent?.body)
assertEquals("<br/>Text", (quotedContent as? MessageContentWithFormattedBody)?.formattedBody)
}
@Test
fun `given a plain text quoted message, when a quote event is created, then the result message should contain both the quoted and new text`() {
val event = createTimelineEvent("Quoted", null)
val quotedContent = localEchoEventFactory.createQuotedTextEvent(
roomId = A_ROOM_ID,
quotedEvent = event,
text = "Text",
formattedText = null,
autoMarkdown = false,
rootThreadEventId = null,
additionalContent = null,
).content.toModel<MessageContent>()
assertEquals("> Quoted\n\nText", quotedContent?.body)
assertEquals("<blockquote>Quoted</blockquote><br/>Text", (quotedContent as? MessageContentWithFormattedBody)?.formattedBody)
}
@Test
fun `given a formatted text quoted message, when a quote event is created, then the result message should contain both the formatted quote and new text`() {
val event = createTimelineEvent("Quoted", "<b>Quoted</b>")
val quotedContent = localEchoEventFactory.createQuotedTextEvent(
roomId = A_ROOM_ID,
quotedEvent = event,
text = "Text",
formattedText = null,
autoMarkdown = false,
rootThreadEventId = null,
additionalContent = null,
).content.toModel<MessageContent>()
// This still uses the plain text version
assertEquals("> Quoted\n\nText", quotedContent?.body)
// This one has the formatted one
assertEquals("<blockquote><b>Quoted</b></blockquote><br/>Text", (quotedContent as? MessageContentWithFormattedBody)?.formattedBody)
}
@Test
fun `given formatted text quoted message and new message, when a quote event is created, then the result message should contain both the formatted quote and new formatted text`() {
val event = createTimelineEvent("Quoted", "<b>Quoted</b>")
val quotedContent = localEchoEventFactory.createQuotedTextEvent(
roomId = A_ROOM_ID,
quotedEvent = event,
text = "Text",
formattedText = "<b>Formatted text</b>",
autoMarkdown = false,
rootThreadEventId = null,
additionalContent = null,
).content.toModel<MessageContent>()
// This still uses the plain text version
assertEquals("> Quoted\n\nText", quotedContent?.body)
// This one has the formatted one
assertEquals(
"<blockquote><b>Quoted</b></blockquote><br/><b>Formatted text</b>",
(quotedContent as? MessageContentWithFormattedBody)?.formattedBody
)
}
@Test
fun `given formatted text quoted message and new message with autoMarkdown, when a quote event is created, then the result message should contain both the formatted quote and new formatted text, not the markdown processed text`() {
val event = createTimelineEvent("Quoted", "<b>Quoted</b>")
val quotedContent = localEchoEventFactory.createQuotedTextEvent(
roomId = A_ROOM_ID,
quotedEvent = event,
text = "Text",
formattedText = "<b>Formatted text</b>",
autoMarkdown = true,
rootThreadEventId = null,
additionalContent = null,
).content.toModel<MessageContent>()
// This still uses the plain text version
assertEquals("> Quoted\n\nText", quotedContent?.body)
// This one has the formatted one
assertEquals(
"<blockquote><b>Quoted</b></blockquote><br/><b>Formatted text</b>",
(quotedContent as? MessageContentWithFormattedBody)?.formattedBody
)
}
@Test
fun `given a formatted text quoted message and a new message with autoMarkdown, when a quote event is created, then the result message should contain both the formatted quote and new processed formatted text`() {
val event = createTimelineEvent("Quoted", "<b>Quoted</b>")
val quotedContent = localEchoEventFactory.createQuotedTextEvent(
roomId = A_ROOM_ID,
quotedEvent = event,
text = "**Text**",
formattedText = null,
autoMarkdown = true,
rootThreadEventId = null,
additionalContent = null,
).content.toModel<MessageContent>()
// This still uses the markdown text version
assertEquals("> Quoted\n\n**Text**", quotedContent?.body)
// This one has the formatted one
assertEquals(
"<blockquote><b>Quoted</b></blockquote><br/><b>Text</b>",
(quotedContent as? MessageContentWithFormattedBody)?.formattedBody
)
}
@Test
fun `given a plain text quoted message and a new message with autoMarkdown, when a quote event is created, then the result message should the plain text quote and new processed formatted text`() {
val event = createTimelineEvent("Quoted", null)
val quotedContent = localEchoEventFactory.createQuotedTextEvent(
roomId = A_ROOM_ID,
quotedEvent = event,
text = "**Text**",
formattedText = null,
autoMarkdown = true,
rootThreadEventId = null,
additionalContent = null,
).content.toModel<MessageContent>()
// This still uses the markdown text version
assertEquals("> Quoted\n\n**Text**", quotedContent?.body)
// This one has the formatted one
assertEquals(
"<blockquote>Quoted</blockquote><br/><b>Text</b>",
(quotedContent as? MessageContentWithFormattedBody)?.formattedBody
)
}
private fun createTimelineEvent(quotedText: String?, formattedQuotedText: String?): TimelineEvent {
val textContent = quotedText?.let {
TextContent(
quotedText,
formattedQuotedText
).toMessageTextContent().toContent()
}
return TimelineEvent(
root = A_START_EVENT,
localId = 1234,
eventId = AN_EVENT_ID,
displayIndex = 0,
senderInfo = SenderInfo(A_USER_ID_1, A_USER_ID_1, true, null),
annotations = if (textContent != null) {
EventAnnotationsSummary(
editSummary = EditAggregatedSummary(latestContent = textContent, emptyList(), emptyList())
)
} else null
)
}
}

View file

@ -0,0 +1,37 @@
/*
* Copyright (c) 2022 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.test.fakes
import android.content.ClipData
import android.content.ClipboardManager
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import io.mockk.runs
import io.mockk.verify
class FakeClipboardManager {
val instance = mockk<ClipboardManager>()
fun givenSetPrimaryClip() {
every { instance.setPrimaryClip(any()) } just runs
}
fun verifySetPrimaryClip(clipData: ClipData) {
verify { instance.setPrimaryClip(clipData) }
}
}

View file

@ -0,0 +1,44 @@
/*
* Copyright (c) 2022 The Matrix.org Foundation C.I.C
*
* 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 org.matrix.android.sdk.test.fakes
import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkCapabilities
import io.mockk.every
import io.mockk.mockk
class FakeConnectivityManager {
val instance = mockk<ConnectivityManager>()
fun givenNoActiveConnection() {
every { instance.activeNetwork } returns null
}
fun givenHasActiveConnection() {
val network = mockk<Network>()
every { instance.activeNetwork } returns network
val networkCapabilities = FakeNetworkCapabilities()
networkCapabilities.givenTransports(
NetworkCapabilities.TRANSPORT_CELLULAR,
NetworkCapabilities.TRANSPORT_WIFI,
NetworkCapabilities.TRANSPORT_VPN
)
every { instance.getNetworkCapabilities(network) } returns networkCapabilities.instance
}
}

View file

@ -0,0 +1,84 @@
/*
* Copyright (c) 2022 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.test.fakes
import android.content.ClipboardManager
import android.content.ContentResolver
import android.content.Context
import android.content.Intent
import android.net.ConnectivityManager
import android.net.Uri
import android.os.ParcelFileDescriptor
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import io.mockk.runs
import java.io.OutputStream
class FakeContext(
private val contentResolver: ContentResolver = mockk()
) {
val instance = mockk<Context>()
init {
every { instance.contentResolver } returns contentResolver
every { instance.applicationContext } returns instance
}
fun givenFileDescriptor(uri: Uri, mode: String, factory: () -> ParcelFileDescriptor?) {
val fileDescriptor = factory()
every { contentResolver.openFileDescriptor(uri, mode, null) } returns fileDescriptor
}
fun givenSafeOutputStreamFor(uri: Uri): OutputStream {
val outputStream = mockk<OutputStream>(relaxed = true)
every { contentResolver.openOutputStream(uri, "wt") } returns outputStream
return outputStream
}
fun givenMissingSafeOutputStreamFor(uri: Uri) {
every { contentResolver.openOutputStream(uri, "wt") } returns null
}
fun givenNoConnection() {
val connectivityManager = FakeConnectivityManager()
connectivityManager.givenNoActiveConnection()
givenService(Context.CONNECTIVITY_SERVICE, ConnectivityManager::class.java, connectivityManager.instance)
}
fun <T> givenService(name: String, klass: Class<T>, service: T) {
every { instance.getSystemService(name) } returns service
every { instance.getSystemService(klass) } returns service
}
fun givenHasConnection() {
val connectivityManager = FakeConnectivityManager()
connectivityManager.givenHasActiveConnection()
givenService(Context.CONNECTIVITY_SERVICE, ConnectivityManager::class.java, connectivityManager.instance)
}
fun givenStartActivity(intent: Intent) {
every { instance.startActivity(intent) } just runs
}
fun givenClipboardManager(): FakeClipboardManager {
val fakeClipboardManager = FakeClipboardManager()
givenService(Context.CLIPBOARD_SERVICE, ClipboardManager::class.java, fakeClipboardManager.instance)
return fakeClipboardManager
}
}

View file

@ -0,0 +1,32 @@
/*
* Copyright (c) 2022 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.test.fakes
import android.net.NetworkCapabilities
import io.mockk.every
import io.mockk.mockk
class FakeNetworkCapabilities {
val instance = mockk<NetworkCapabilities>()
fun givenTransports(vararg type: Int) {
every { instance.hasTransport(any()) } answers {
val input = it.invocation.args.first() as Int
type.contains(input)
}
}
}

View file

@ -0,0 +1,24 @@
/*
* Copyright (c) 2022 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.test.fakes.internal.session.content
import io.mockk.mockk
import org.matrix.android.sdk.internal.session.content.ThumbnailExtractor
class FakeThumbnailExtractor {
internal val instance = mockk<ThumbnailExtractor>()
}

View file

@ -0,0 +1,24 @@
/*
* Copyright (c) 2022 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.test.fakes.internal.session.permalinks
import io.mockk.mockk
import org.matrix.android.sdk.internal.session.permalinks.PermalinkFactory
class FakePermalinkFactory {
internal val instance = mockk<PermalinkFactory>()
}

View file

@ -0,0 +1,24 @@
/*
* Copyright (c) 2022 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.test.fakes.internal.session.room.send
import io.mockk.mockk
import org.matrix.android.sdk.internal.session.room.send.LocalEchoRepository
class FakeLocalEchoRepository {
internal val instance = mockk<LocalEchoRepository>()
}

View file

@ -0,0 +1,32 @@
/*
* Copyright (c) 2022 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.test.fakes.internal.session.room.send
import io.mockk.every
import io.mockk.mockk
import org.matrix.android.sdk.api.util.TextContent
import org.matrix.android.sdk.internal.session.room.send.MarkdownParser
class FakeMarkdownParser {
internal val instance = mockk<MarkdownParser>()
fun givenBoldMarkdown() {
every { instance.parse(any(), any(), any()) } answers {
val text = arg<String>(0)
TextContent(text, "<b>${text.replace("*", "")}</b>")
}
}
}

View file

@ -0,0 +1,24 @@
/*
* Copyright (c) 2022 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.test.fakes.internal.session.room.send
import io.mockk.mockk
import org.matrix.android.sdk.internal.session.room.send.WaveFormSanitizer
class FakeWaveFormSanitizer {
internal val instance = mockk<WaveFormSanitizer>()
}

View file

@ -0,0 +1,24 @@
/*
* Copyright (c) 2022 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.test.fakes.internal.session.room.send.pills
import io.mockk.mockk
import org.matrix.android.sdk.internal.session.room.send.pills.TextPillsUtils
class FakeTextPillsUtils {
internal val instance = mockk<TextPillsUtils>()
}