mirror of
https://github.com/ouchadam/small-talk.git
synced 2025-03-26 00:40:25 +01:00
adding custom html parsing with tag styling
This commit is contained in:
parent
e13ce95b83
commit
fddcdaa50c
chat-engine/src/main/kotlin/app/dapk/st/engine
core/src/main/kotlin/app/dapk/st/core
design-library/src/main/kotlin/app/dapk/st/design/components
domains/store/src/main/kotlin/app/dapk/st/domain/sync
features
messenger/src/main/kotlin/app/dapk/st/messenger
notifications/src/main/kotlin/app/dapk/st/notifications
matrix-chat-engine/src/main/kotlin/app/dapk/st/engine
matrix
common/src/main/kotlin/app/dapk/st/matrix/common
services
message/src/main/kotlin/app/dapk/st/matrix/message
sync/src
main/kotlin/app/dapk/st/matrix/sync
test/kotlin/app/dapk/st/matrix/sync/internal
room
sync
testFixtures/kotlin/fixture
@ -125,7 +125,7 @@ sealed class RoomEvent {
|
||||
data class Message(
|
||||
override val eventId: EventId,
|
||||
override val utcTimestamp: Long,
|
||||
val content: String,
|
||||
val content: RichText,
|
||||
override val author: RoomMember,
|
||||
override val meta: MessageMeta,
|
||||
override val edited: Boolean = false,
|
||||
|
13
core/src/main/kotlin/app/dapk/st/core/RichText.kt
Normal file
13
core/src/main/kotlin/app/dapk/st/core/RichText.kt
Normal file
@ -0,0 +1,13 @@
|
||||
package app.dapk.st.core
|
||||
|
||||
data class RichText(val parts: Set<Part>) {
|
||||
sealed interface Part {
|
||||
data class Normal(val content: String) : Part
|
||||
data class Link(val url: String, val label: String) : Part
|
||||
data class Bold(val content: String) : Part
|
||||
data class Italic(val content: String) : Part
|
||||
data class BoldItalic(val content: String) : Part
|
||||
}
|
||||
}
|
||||
|
||||
fun RichText.asString() = parts.joinToString(separator = "")
|
@ -7,6 +7,7 @@ import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.ClickableText
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
@ -15,18 +16,29 @@ import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import androidx.compose.ui.unit.Density
|
||||
import androidx.compose.ui.unit.DpSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import app.dapk.st.core.RichText
|
||||
import coil.compose.rememberAsyncImagePainter
|
||||
import coil.request.ImageRequest
|
||||
|
||||
private val ENCRYPTED_MESSAGE = RichText(setOf(RichText.Part.Normal("Encrypted message")))
|
||||
|
||||
sealed interface BubbleModel {
|
||||
val event: Event
|
||||
|
||||
data class Text(val content: String, override val event: Event) : BubbleModel
|
||||
data class Text(val content: RichText, override val event: Event) : BubbleModel
|
||||
data class Encrypted(override val event: Event) : BubbleModel
|
||||
data class Image(val imageContent: ImageContent, val imageRequest: ImageRequest, override val event: Event) : BubbleModel {
|
||||
data class ImageContent(val width: Int?, val height: Int?, val url: String)
|
||||
@ -66,7 +78,7 @@ private fun TextBubble(bubble: BubbleMeta, model: BubbleModel.Text, status: @Com
|
||||
|
||||
@Composable
|
||||
private fun EncryptedBubble(bubble: BubbleMeta, model: BubbleModel.Encrypted, status: @Composable () -> Unit, onLongClick: () -> Unit) {
|
||||
TextBubble(bubble, BubbleModel.Text(content = "Encrypted message", model.event), status, onLongClick)
|
||||
TextBubble(bubble, BubbleModel.Text(content = ENCRYPTED_MESSAGE, model.event), status, onLongClick)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@ -111,7 +123,7 @@ private fun ReplyBubble(bubble: BubbleMeta, model: BubbleModel.Reply, status: @C
|
||||
when (val replyingTo = model.replyingTo) {
|
||||
is BubbleModel.Text -> {
|
||||
Text(
|
||||
text = replyingTo.content,
|
||||
text = replyingTo.content.toAnnotatedText(),
|
||||
color = bubble.textColor().copy(alpha = 0.8f),
|
||||
fontSize = 14.sp,
|
||||
modifier = Modifier.wrapContentSize(),
|
||||
@ -153,7 +165,7 @@ private fun ReplyBubble(bubble: BubbleMeta, model: BubbleModel.Reply, status: @C
|
||||
|
||||
when (val message = model.reply) {
|
||||
is BubbleModel.Text -> TextContent(bubble, message.content)
|
||||
is BubbleModel.Encrypted -> TextContent(bubble, "Encrypted message")
|
||||
is BubbleModel.Encrypted -> TextContent(bubble, ENCRYPTED_MESSAGE)
|
||||
is BubbleModel.Image -> {
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Image(
|
||||
@ -233,16 +245,43 @@ private fun Footer(event: BubbleModel.Event, bubble: BubbleMeta, status: @Compos
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TextContent(bubble: BubbleMeta, text: String) {
|
||||
Text(
|
||||
text = text,
|
||||
color = bubble.textColor(),
|
||||
fontSize = 15.sp,
|
||||
private fun TextContent(bubble: BubbleMeta, text: RichText) {
|
||||
val annotatedText = text.toAnnotatedText()
|
||||
val uriHandler = LocalUriHandler.current
|
||||
ClickableText(
|
||||
text = annotatedText,
|
||||
style = TextStyle(color = bubble.textColor(), fontSize = 15.sp, textAlign = TextAlign.Start),
|
||||
modifier = Modifier.wrapContentSize(),
|
||||
textAlign = TextAlign.Start,
|
||||
onClick = {
|
||||
annotatedText.getStringAnnotations("url", it, it).firstOrNull()?.let {
|
||||
uriHandler.openUri(it.item)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
val hyperLinkStyle = SpanStyle(
|
||||
color = Color(0xff64B5F6),
|
||||
textDecoration = TextDecoration.Underline
|
||||
)
|
||||
|
||||
fun RichText.toAnnotatedText() = buildAnnotatedString {
|
||||
parts.forEach {
|
||||
when (it) {
|
||||
is RichText.Part.Bold -> withStyle(SpanStyle(fontWeight = FontWeight.Bold)) { append(it.content) }
|
||||
is RichText.Part.BoldItalic -> append(it.content)
|
||||
is RichText.Part.Italic -> withStyle(SpanStyle(fontStyle = FontStyle.Italic)) { append(it.content) }
|
||||
is RichText.Part.Link -> {
|
||||
pushStringAnnotation("url", annotation = it.url)
|
||||
withStyle(hyperLinkStyle) { append(it.label) }
|
||||
pop()
|
||||
}
|
||||
|
||||
is RichText.Part.Normal -> append(it.content)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AuthorName(event: BubbleModel.Event, bubble: BubbleMeta) {
|
||||
Text(
|
||||
|
@ -13,7 +13,10 @@ import app.dapk.st.matrix.sync.RoomStore
|
||||
import com.squareup.sqldelight.runtime.coroutines.asFlow
|
||||
import com.squareup.sqldelight.runtime.coroutines.mapToList
|
||||
import com.squareup.sqldelight.runtime.coroutines.mapToOneNotNull
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
private val json = Json
|
||||
|
@ -8,7 +8,6 @@ import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
@ -43,6 +42,7 @@ import app.dapk.st.engine.MessageMeta
|
||||
import app.dapk.st.engine.MessengerState
|
||||
import app.dapk.st.engine.RoomEvent
|
||||
import app.dapk.st.engine.RoomState
|
||||
import app.dapk.st.matrix.common.RichText
|
||||
import app.dapk.st.matrix.common.RoomId
|
||||
import app.dapk.st.matrix.common.UserId
|
||||
import app.dapk.st.messenger.gallery.ImageGalleryActivityPayload
|
||||
@ -210,7 +210,7 @@ private fun ColumnScope.RoomContent(self: UserId, state: RoomState, messageActio
|
||||
|
||||
@Composable
|
||||
private fun RoomEvent.toModel(event: BubbleModel.Event): BubbleModel = when (this) {
|
||||
is RoomEvent.Message -> BubbleModel.Text(this.content, event)
|
||||
is RoomEvent.Message -> BubbleModel.Text(this.content.toApp(), event)
|
||||
is RoomEvent.Encrypted -> BubbleModel.Encrypted(event)
|
||||
is RoomEvent.Image -> {
|
||||
val imageRequest = LocalImageRequestFactory.current
|
||||
@ -226,6 +226,18 @@ private fun RoomEvent.toModel(event: BubbleModel.Event): BubbleModel = when (thi
|
||||
}
|
||||
}
|
||||
|
||||
private fun RichText.toApp(): app.dapk.st.core.RichText {
|
||||
return app.dapk.st.core.RichText(this.parts.map {
|
||||
when (it) {
|
||||
is RichText.Part.Bold -> app.dapk.st.core.RichText.Part.Bold(it.content)
|
||||
is RichText.Part.BoldItalic -> app.dapk.st.core.RichText.Part.BoldItalic(it.content)
|
||||
is RichText.Part.Italic -> app.dapk.st.core.RichText.Part.Italic(it.content)
|
||||
is RichText.Part.Link -> app.dapk.st.core.RichText.Part.Link(it.url, it.label)
|
||||
is RichText.Part.Normal -> app.dapk.st.core.RichText.Part.Normal(it.content)
|
||||
}
|
||||
}.toSet())
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SendStatus(message: RoomEvent) {
|
||||
when (val meta = message.meta) {
|
||||
@ -319,7 +331,7 @@ private fun TextComposer(state: ComposerState.Text, onTextChange: (String) -> Un
|
||||
)
|
||||
|
||||
Text(
|
||||
text = it.content,
|
||||
text = it.content.toApp().toAnnotatedText(),
|
||||
color = SmallTalkTheme.extendedColors.onOthersBubble,
|
||||
fontSize = 14.sp,
|
||||
maxLines = 2,
|
||||
|
@ -4,6 +4,7 @@ import android.os.Build
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import app.dapk.st.core.DeviceMeta
|
||||
import app.dapk.st.core.Lce
|
||||
import app.dapk.st.core.asString
|
||||
import app.dapk.st.core.extensions.takeIfContent
|
||||
import app.dapk.st.design.components.BubbleModel
|
||||
import app.dapk.st.domain.application.message.MessageOptionsStore
|
||||
@ -11,6 +12,7 @@ import app.dapk.st.engine.ChatEngine
|
||||
import app.dapk.st.engine.RoomEvent
|
||||
import app.dapk.st.engine.SendMessage
|
||||
import app.dapk.st.matrix.common.RoomId
|
||||
import app.dapk.st.matrix.common.asString
|
||||
import app.dapk.st.navigator.MessageAttachment
|
||||
import app.dapk.st.viewmodel.DapkViewModel
|
||||
import app.dapk.st.viewmodel.MutableStateFactory
|
||||
@ -116,7 +118,7 @@ internal class MessengerViewModel(
|
||||
originalMessage = when (it) {
|
||||
is RoomEvent.Image -> TODO()
|
||||
is RoomEvent.Reply -> TODO()
|
||||
is RoomEvent.Message -> it.content
|
||||
is RoomEvent.Message -> it.content.asString()
|
||||
is RoomEvent.Encrypted -> error("Should never happen")
|
||||
},
|
||||
eventId = it.eventId,
|
||||
@ -161,7 +163,7 @@ private fun BubbleModel.findCopyableContent(): CopyableResult = when (this) {
|
||||
is BubbleModel.Encrypted -> CopyableResult.NothingToCopy
|
||||
is BubbleModel.Image -> CopyableResult.NothingToCopy
|
||||
is BubbleModel.Reply -> this.reply.findCopyableContent()
|
||||
is BubbleModel.Text -> CopyableResult.Content(CopyToClipboard.Copyable.Text(this.content))
|
||||
is BubbleModel.Text -> CopyableResult.Content(CopyToClipboard.Copyable.Text(this.content.asString()))
|
||||
}
|
||||
|
||||
private sealed interface CopyableResult {
|
||||
|
@ -2,6 +2,7 @@ package app.dapk.st.notifications
|
||||
|
||||
import app.dapk.st.engine.RoomEvent
|
||||
import app.dapk.st.matrix.common.RoomMember
|
||||
import app.dapk.st.matrix.common.asString
|
||||
|
||||
class RoomEventsToNotifiableMapper {
|
||||
|
||||
@ -11,7 +12,7 @@ class RoomEventsToNotifiableMapper {
|
||||
|
||||
private fun RoomEvent.toNotifiableContent(): String = when (this) {
|
||||
is RoomEvent.Image -> "\uD83D\uDCF7"
|
||||
is RoomEvent.Message -> this.content
|
||||
is RoomEvent.Message -> this.content.asString()
|
||||
is RoomEvent.Reply -> this.message.toNotifiableContent()
|
||||
is RoomEvent.Encrypted -> "Encrypted message"
|
||||
}
|
||||
|
@ -68,7 +68,7 @@ internal class DirectoryUseCase(
|
||||
this.copy(
|
||||
lastMessage = RoomOverview.LastMessage(
|
||||
content = when (val message = latestEcho.message) {
|
||||
is MessageService.Message.TextMessage -> message.content.body
|
||||
is MessageService.Message.TextMessage -> message.content.body.parts.joinToString("")
|
||||
is MessageService.Message.ImageMessage -> "\uD83D\uDCF7"
|
||||
},
|
||||
utcTimestamp = latestEcho.timestampUtc,
|
||||
|
@ -1,5 +1,6 @@
|
||||
package app.dapk.st.engine
|
||||
|
||||
import app.dapk.st.matrix.common.RichText
|
||||
import app.dapk.st.matrix.message.MessageService
|
||||
import app.dapk.st.matrix.message.internal.ImageContentReader
|
||||
import java.time.Clock
|
||||
@ -41,7 +42,7 @@ internal class SendMessageUseCase(
|
||||
}
|
||||
|
||||
private fun createTextMessage(message: SendMessage.TextMessage, room: RoomOverview) = MessageService.Message.TextMessage(
|
||||
content = MessageService.Message.Content.TextContent(message.content),
|
||||
content = MessageService.Message.Content.TextContent(RichText.of(message.content)),
|
||||
roomId = room.roomId,
|
||||
sendEncrypted = room.isEncrypted,
|
||||
localId = localIdFactory.create(),
|
||||
@ -49,7 +50,7 @@ internal class SendMessageUseCase(
|
||||
reply = message.reply?.let {
|
||||
MessageService.Message.TextMessage.Reply(
|
||||
author = it.author,
|
||||
originalMessage = it.originalMessage,
|
||||
originalMessage = RichText.of(it.originalMessage),
|
||||
replyContent = message.content,
|
||||
eventId = it.eventId,
|
||||
timestampUtc = it.timestampUtc,
|
||||
|
@ -9,11 +9,31 @@ data class RichText(@SerialName("parts") val parts: Set<Part>) {
|
||||
sealed interface Part {
|
||||
@Serializable
|
||||
data class Normal(@SerialName("content") val content: String) : Part
|
||||
|
||||
@Serializable
|
||||
data class Link(@SerialName("url") val url: String, @SerialName("label") val label: String) : Part
|
||||
|
||||
@Serializable
|
||||
data class Bold(@SerialName("content") val content: String) : Part
|
||||
|
||||
@Serializable
|
||||
data class Italic(@SerialName("content") val content: String) : Part
|
||||
|
||||
@Serializable
|
||||
data class BoldItalic(@SerialName("content") val content: String) : Part
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun of(text: String) = RichText(setOf(RichText.Part.Normal(text)))
|
||||
}
|
||||
}
|
||||
|
||||
fun RichText.asString() = parts.joinToString(separator = "")
|
||||
fun RichText.asString() = parts.joinToString(separator = "") {
|
||||
when(it) {
|
||||
is RichText.Part.Bold -> it.content
|
||||
is RichText.Part.BoldItalic -> it.content
|
||||
is RichText.Part.Italic -> it.content
|
||||
is RichText.Part.Link -> it.label
|
||||
is RichText.Part.Normal -> it.content
|
||||
}
|
||||
}
|
@ -44,7 +44,7 @@ interface MessageService : MatrixService {
|
||||
@Serializable
|
||||
data class Reply(
|
||||
val author: RoomMember,
|
||||
val originalMessage: String,
|
||||
val originalMessage: RichText,
|
||||
val replyContent: String,
|
||||
val eventId: EventId,
|
||||
val timestampUtc: Long,
|
||||
@ -65,7 +65,7 @@ interface MessageService : MatrixService {
|
||||
sealed class Content {
|
||||
@Serializable
|
||||
data class TextContent(
|
||||
@SerialName("body") val body: String,
|
||||
@SerialName("body") val body: RichText,
|
||||
@SerialName("msgtype") val type: String = MessageType.TEXT.value,
|
||||
) : Content()
|
||||
|
||||
|
@ -153,13 +153,13 @@ class ApiMessageMapper {
|
||||
|
||||
fun Message.TextMessage.toContents(reply: Message.TextMessage.Reply?) = when (reply) {
|
||||
null -> ApiMessage.TextMessage.TextContent(
|
||||
body = this.content.body,
|
||||
body = this.content.body.parts.joinToString(""),
|
||||
)
|
||||
|
||||
else -> ApiMessage.TextMessage.TextContent(
|
||||
body = buildReplyFallback(reply.originalMessage, reply.author.id, reply.replyContent),
|
||||
body = buildReplyFallback(reply.originalMessage.parts.joinToString(""), reply.author.id, reply.replyContent),
|
||||
relatesTo = ApiMessage.RelatesTo(ApiMessage.RelatesTo.InReplyTo(reply.eventId)),
|
||||
formattedBody = buildFormattedReply(reply.author.id, reply.originalMessage, reply.replyContent, this.roomId, reply.eventId),
|
||||
formattedBody = buildFormattedReply(reply.author.id, reply.originalMessage.parts.joinToString(""), reply.replyContent, this.roomId, reply.eventId),
|
||||
format = "org.matrix.custom.html"
|
||||
)
|
||||
}
|
||||
|
@ -45,7 +45,7 @@ sealed class RoomEvent {
|
||||
data class Message(
|
||||
@SerialName("event_id") override val eventId: EventId,
|
||||
@SerialName("timestamp") override val utcTimestamp: Long,
|
||||
@SerialName("content") val content: String,
|
||||
@SerialName("content") val content: RichText,
|
||||
@SerialName("author") override val author: RoomMember,
|
||||
@SerialName("meta") override val meta: MessageMeta,
|
||||
@SerialName("edited") val edited: Boolean = false,
|
||||
|
@ -8,6 +8,7 @@ import app.dapk.st.matrix.sync.internal.DefaultSyncService
|
||||
import app.dapk.st.matrix.sync.internal.request.*
|
||||
import app.dapk.st.matrix.sync.internal.room.MessageDecrypter
|
||||
import app.dapk.st.matrix.sync.internal.room.MissingMessageDecrypter
|
||||
import app.dapk.st.matrix.sync.internal.sync.RichMessageParser
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@ -53,6 +54,7 @@ fun MatrixServiceInstaller.installSyncService(
|
||||
roomMembersService: ServiceDepFactory<RoomMembersService>,
|
||||
errorTracker: ErrorTracker,
|
||||
coroutineDispatchers: CoroutineDispatchers,
|
||||
|
||||
syncConfig: SyncConfig = SyncConfig(),
|
||||
): InstallExtender<SyncService> {
|
||||
this.serializers {
|
||||
@ -96,6 +98,7 @@ fun MatrixServiceInstaller.installSyncService(
|
||||
errorTracker = errorTracker,
|
||||
coroutineDispatchers = coroutineDispatchers,
|
||||
syncConfig = syncConfig,
|
||||
richMessageParser = RichMessageParser()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -41,13 +41,14 @@ internal class DefaultSyncService(
|
||||
errorTracker: ErrorTracker,
|
||||
private val coroutineDispatchers: CoroutineDispatchers,
|
||||
syncConfig: SyncConfig,
|
||||
richMessageParser: RichMessageParser,
|
||||
) : SyncService {
|
||||
|
||||
private val syncEventsFlow = MutableStateFlow<List<SyncService.SyncEvent>>(emptyList())
|
||||
|
||||
private val roomDataSource by lazy { RoomDataSource(roomStore, logger) }
|
||||
private val eventDecrypter by lazy { SyncEventDecrypter(messageDecrypter, json, logger) }
|
||||
private val roomEventsDecrypter by lazy { RoomEventsDecrypter(messageDecrypter, json, logger) }
|
||||
private val roomEventsDecrypter by lazy { RoomEventsDecrypter(messageDecrypter, richMessageParser, json, logger) }
|
||||
private val roomRefresher by lazy { RoomRefresher(roomDataSource, roomEventsDecrypter, logger) }
|
||||
|
||||
private val sync2 by lazy {
|
||||
@ -57,7 +58,7 @@ internal class DefaultSyncService(
|
||||
roomMembersService,
|
||||
roomDataSource,
|
||||
TimelineEventsProcessor(
|
||||
RoomEventCreator(roomMembersService, errorTracker, RoomEventFactory(roomMembersService)),
|
||||
RoomEventCreator(roomMembersService, errorTracker, RoomEventFactory(roomMembersService, richMessageParser)),
|
||||
roomEventsDecrypter,
|
||||
eventDecrypter,
|
||||
EventLookupUseCase(roomStore)
|
||||
|
@ -6,10 +6,12 @@ import app.dapk.st.matrix.sync.internal.request.ApiTimelineEvent
|
||||
import app.dapk.st.matrix.sync.internal.request.ApiTimelineEvent.TimelineMessage.Content.Image
|
||||
import app.dapk.st.matrix.sync.internal.request.ApiTimelineEvent.TimelineMessage.Content.Text
|
||||
import app.dapk.st.matrix.sync.internal.request.DecryptedContent
|
||||
import app.dapk.st.matrix.sync.internal.sync.RichMessageParser
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
internal class RoomEventsDecrypter(
|
||||
private val messageDecrypter: MessageDecrypter,
|
||||
private val richMessageParser: RichMessageParser,
|
||||
private val json: Json,
|
||||
private val logger: MatrixLogger,
|
||||
) {
|
||||
@ -50,7 +52,7 @@ internal class RoomEventsDecrypter(
|
||||
meta = this.meta,
|
||||
edited = this.edited,
|
||||
redacted = this.redacted,
|
||||
content = content.body ?: ""
|
||||
content = richMessageParser.parse(content.body ?: "")
|
||||
)
|
||||
|
||||
private fun RoomEvent.Encrypted.createImageEvent(content: Image, userCredentials: UserCredentials) = RoomEvent.Image(
|
||||
|
@ -1,57 +1,116 @@
|
||||
package app.dapk.st.matrix.sync.internal.sync
|
||||
|
||||
import app.dapk.st.matrix.common.RichText
|
||||
|
||||
data class Tag(val name: String, val inner: String, val content: String)
|
||||
import app.dapk.st.matrix.common.RichText.Part.*
|
||||
|
||||
class RichMessageParser {
|
||||
|
||||
fun parse(input: String): RichText {
|
||||
val buffer = mutableSetOf<RichText.Part>()
|
||||
var openIndex = 0
|
||||
var closeIndex = 0
|
||||
while (openIndex != -1) {
|
||||
val foundIndex = input.indexOf('<', startIndex = openIndex)
|
||||
if (foundIndex != -1) {
|
||||
closeIndex = input.indexOf('>', startIndex = openIndex)
|
||||
return kotlin.runCatching {
|
||||
val buffer = mutableSetOf<RichText.Part>()
|
||||
var openIndex = 0
|
||||
var closeIndex = 0
|
||||
var lastStartIndex = 0
|
||||
while (openIndex != -1) {
|
||||
val foundIndex = input.indexOf('<', startIndex = openIndex)
|
||||
if (foundIndex != -1) {
|
||||
closeIndex = input.indexOf('>', startIndex = foundIndex)
|
||||
if (closeIndex == -1) {
|
||||
openIndex++
|
||||
} else {
|
||||
val wholeTag = input.substring(foundIndex, closeIndex + 1)
|
||||
val tagName = wholeTag.substring(1, wholeTag.indexOfFirst { it == '>' || it == ' ' })
|
||||
|
||||
if (closeIndex == -1) {
|
||||
openIndex++
|
||||
} else {
|
||||
val wholeTag = input.substring(foundIndex, closeIndex + 1)
|
||||
val tagName = wholeTag.substring(1, wholeTag.indexOfFirst { it == '>' || it == ' ' })
|
||||
val exitTag = "<$tagName/>"
|
||||
val exitIndex = input.indexOf(exitTag, startIndex = closeIndex + 1)
|
||||
val tagContent = input.substring(closeIndex + 1, exitIndex)
|
||||
|
||||
val tag = Tag(name = tagName, wholeTag, tagContent)
|
||||
|
||||
println("found $tag")
|
||||
if (openIndex != foundIndex) {
|
||||
buffer.add(RichText.Part.Normal(input.substring(openIndex, foundIndex)))
|
||||
}
|
||||
openIndex = exitIndex + exitTag.length
|
||||
|
||||
when (tagName) {
|
||||
"a" -> {
|
||||
val findHrefUrl = wholeTag.substringAfter("href=").replace("\"", "").removeSuffix(">")
|
||||
buffer.add(RichText.Part.Link(url = findHrefUrl, label = tag.content))
|
||||
if (tagName == "br") {
|
||||
if (openIndex != foundIndex) {
|
||||
buffer.add(Normal(input.substring(openIndex, foundIndex)))
|
||||
}
|
||||
buffer.add(Normal("\n"))
|
||||
openIndex = foundIndex + "<br />".length
|
||||
lastStartIndex = openIndex
|
||||
continue
|
||||
}
|
||||
|
||||
"b" -> buffer.add(RichText.Part.Bold(tagContent))
|
||||
"i" -> buffer.add(RichText.Part.Italic(tagContent))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// exit
|
||||
if (openIndex < input.length) {
|
||||
buffer.add(RichText.Part.Normal(input.substring(openIndex)))
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
val exitTag = "</$tagName>"
|
||||
val exitIndex = input.indexOf(exitTag, startIndex = closeIndex)
|
||||
|
||||
return RichText(buffer)
|
||||
println("$exitTag : $exitIndex")
|
||||
|
||||
if (exitIndex == -1) {
|
||||
openIndex++
|
||||
} else {
|
||||
if (openIndex != foundIndex) {
|
||||
buffer.add(Normal(input.substring(openIndex, foundIndex)))
|
||||
}
|
||||
val tagContent = input.substring(closeIndex + 1, exitIndex)
|
||||
openIndex = exitIndex + exitTag.length
|
||||
lastStartIndex = openIndex
|
||||
|
||||
when (tagName) {
|
||||
"a" -> {
|
||||
val findHrefUrl = wholeTag.substringAfter("href=").replace("\"", "").removeSuffix(">")
|
||||
buffer.add(Link(url = findHrefUrl, label = tagContent))
|
||||
}
|
||||
|
||||
"b" -> buffer.add(Bold(tagContent))
|
||||
"strong" -> buffer.add(Bold(tagContent))
|
||||
"i" -> buffer.add(Italic(tagContent))
|
||||
"em" -> buffer.add(Italic(tagContent))
|
||||
|
||||
else -> buffer.add(Normal(tagContent))
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// check for urls
|
||||
val urlIndex = input.indexOf("http", startIndex = openIndex)
|
||||
if (urlIndex != -1) {
|
||||
if (lastStartIndex != urlIndex) {
|
||||
buffer.add(Normal(input.substring(lastStartIndex, urlIndex)))
|
||||
}
|
||||
|
||||
val substring1 = input.substring(urlIndex)
|
||||
val urlEndIndex = substring1.indexOfFirst { it == '\n' || it == ' ' }
|
||||
when {
|
||||
urlEndIndex == -1 -> {
|
||||
val last = substring1.last()
|
||||
val url = substring1.removeSuffix(".").removeSuffix(",")
|
||||
buffer.add(Link(url = url, label = url))
|
||||
if (last == '.' || last == ',') {
|
||||
buffer.add(Normal(last.toString()))
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
else -> {
|
||||
val substring = input.substring(urlIndex, urlEndIndex)
|
||||
|
||||
val last = substring.last()
|
||||
if (last == '.' || last == ',') {
|
||||
substring.dropLast(1)
|
||||
}
|
||||
|
||||
val url = substring.removeSuffix(".").removeSuffix(",")
|
||||
buffer.add(Link(url = url, label = url))
|
||||
openIndex = if (substring.endsWith('.') || substring.endsWith(',')) urlEndIndex - 1 else urlEndIndex
|
||||
lastStartIndex = openIndex
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// exit
|
||||
if (lastStartIndex < input.length) {
|
||||
buffer.add(Normal(input.substring(lastStartIndex)))
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
RichText(buffer)
|
||||
}.onFailure {
|
||||
it.printStackTrace()
|
||||
println(input)
|
||||
}.getOrThrow()
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -51,7 +51,7 @@ class RoomDataSource(
|
||||
}
|
||||
}
|
||||
|
||||
private fun RoomEvent.redact() = RoomEvent.Message(this.eventId, this.utcTimestamp, "Redacted", this.author, this.meta, redacted = true)
|
||||
private fun RoomEvent.redact() = RoomEvent.Message(this.eventId, this.utcTimestamp, RichText.of("Redacted"), this.author, this.meta, redacted = true)
|
||||
|
||||
private fun RoomState.replaceEvent(old: RoomEvent, new: RoomEvent): RoomState {
|
||||
val updatedEvents = this.events.toMutableList().apply {
|
||||
|
@ -147,8 +147,8 @@ internal class TimelineEventMapper(
|
||||
}
|
||||
}
|
||||
|
||||
// TODO handle edits
|
||||
private fun RoomEvent.Message.edited(edit: ApiTimelineEvent.TimelineMessage) = this.copy(
|
||||
content = edit.asTextContent().body?.removePrefix(" * ")?.trim() ?: "redacted",
|
||||
utcTimestamp = edit.utcTimestamp,
|
||||
edited = true,
|
||||
)
|
||||
@ -156,7 +156,7 @@ internal class TimelineEventMapper(
|
||||
private suspend fun RoomEventFactory.mapToRoomEvent(source: ApiTimelineEvent.TimelineMessage): RoomEvent {
|
||||
return when (source.content) {
|
||||
is ApiTimelineEvent.TimelineMessage.Content.Image -> source.toImageMessage(userCredentials, roomId)
|
||||
is ApiTimelineEvent.TimelineMessage.Content.Text -> source.toTextMessage(roomId)
|
||||
is ApiTimelineEvent.TimelineMessage.Content.Text -> source.toTextMessage(roomId, content = source.asTextContent().formattedBody ?: source.content.body ?: "")
|
||||
ApiTimelineEvent.TimelineMessage.Content.Ignored -> throw IllegalStateException()
|
||||
}
|
||||
}
|
||||
|
@ -10,17 +10,18 @@ import app.dapk.st.matrix.sync.internal.request.ApiTimelineEvent
|
||||
private val UNKNOWN_AUTHOR = RoomMember(id = UserId("unknown"), displayName = null, avatarUrl = null)
|
||||
|
||||
internal class RoomEventFactory(
|
||||
private val roomMembersService: RoomMembersService
|
||||
private val roomMembersService: RoomMembersService,
|
||||
private val richMessageParser: RichMessageParser,
|
||||
) {
|
||||
|
||||
suspend fun ApiTimelineEvent.TimelineMessage.toTextMessage(
|
||||
roomId: RoomId,
|
||||
content: String = this.asTextContent().formattedBody?.stripTags() ?: this.asTextContent().body ?: "redacted",
|
||||
content: String,
|
||||
edited: Boolean = false,
|
||||
utcTimestamp: Long = this.utcTimestamp,
|
||||
) = RoomEvent.Message(
|
||||
eventId = this.id,
|
||||
content = content,
|
||||
content = richMessageParser.parse(content),
|
||||
author = roomMembersService.find(roomId, this.senderId) ?: UNKNOWN_AUTHOR,
|
||||
utcTimestamp = utcTimestamp,
|
||||
meta = MessageMeta.FromServer,
|
||||
|
@ -1,9 +1,6 @@
|
||||
package app.dapk.st.matrix.sync.internal.sync
|
||||
|
||||
import app.dapk.st.matrix.common.AvatarUrl
|
||||
import app.dapk.st.matrix.common.RoomMember
|
||||
import app.dapk.st.matrix.common.UserCredentials
|
||||
import app.dapk.st.matrix.common.convertMxUrToUrl
|
||||
import app.dapk.st.matrix.common.*
|
||||
import app.dapk.st.matrix.sync.*
|
||||
import app.dapk.st.matrix.sync.internal.request.ApiSyncRoom
|
||||
import app.dapk.st.matrix.sync.internal.request.ApiTimelineEvent
|
||||
@ -79,7 +76,7 @@ internal fun List<RoomEvent>.findLastMessage(): LastMessage? {
|
||||
|
||||
private fun RoomEvent.toTextContent(): String = when (this) {
|
||||
is RoomEvent.Image -> "\uD83D\uDCF7"
|
||||
is RoomEvent.Message -> this.content
|
||||
is RoomEvent.Message -> this.content.asString()
|
||||
is RoomEvent.Reply -> this.message.toTextContent()
|
||||
is RoomEvent.Encrypted -> "Encrypted message"
|
||||
}
|
@ -2,8 +2,10 @@ package app.dapk.st.matrix.sync.internal.room
|
||||
|
||||
import app.dapk.st.matrix.common.EncryptedMessageContent
|
||||
import app.dapk.st.matrix.common.JsonString
|
||||
import app.dapk.st.matrix.common.RichText
|
||||
import app.dapk.st.matrix.sync.RoomEvent
|
||||
import app.dapk.st.matrix.sync.internal.request.DecryptedContent
|
||||
import app.dapk.st.matrix.sync.internal.sync.RichMessageParser
|
||||
import fake.FakeMatrixLogger
|
||||
import fake.FakeMessageDecrypter
|
||||
import fixture.*
|
||||
@ -31,6 +33,7 @@ class RoomEventsDecrypterTest {
|
||||
|
||||
private val roomEventsDecrypter = RoomEventsDecrypter(
|
||||
fakeMessageDecrypter,
|
||||
RichMessageParser(),
|
||||
Json,
|
||||
FakeMatrixLogger(),
|
||||
)
|
||||
@ -88,7 +91,7 @@ private fun RoomEvent.Encrypted.MegOlmV1.toModel() = EncryptedMessageContent.Meg
|
||||
private fun RoomEvent.Encrypted.toText(text: String) = RoomEvent.Message(
|
||||
this.eventId,
|
||||
this.utcTimestamp,
|
||||
content = text,
|
||||
content = RichText.of(text),
|
||||
this.author,
|
||||
this.meta,
|
||||
this.edited,
|
||||
|
5
matrix/services/sync/src/test/kotlin/app/dapk/st/matrix/sync/internal/sync/EventLookupUseCaseTest.kt
5
matrix/services/sync/src/test/kotlin/app/dapk/st/matrix/sync/internal/sync/EventLookupUseCaseTest.kt
@ -1,5 +1,6 @@
|
||||
package app.dapk.st.matrix.sync.internal.sync
|
||||
|
||||
import app.dapk.st.matrix.common.RichText
|
||||
import fake.FakeRoomStore
|
||||
import fixture.aMatrixRoomMessageEvent
|
||||
import fixture.anEventId
|
||||
@ -11,8 +12,8 @@ import org.junit.Test
|
||||
|
||||
private val AN_EVENT_ID = anEventId()
|
||||
private val A_TIMELINE_EVENT = anApiTimelineTextEvent(AN_EVENT_ID, content = aTimelineTextEventContent(body = "timeline event"))
|
||||
private val A_ROOM_EVENT = aMatrixRoomMessageEvent(AN_EVENT_ID, content = "previous room event")
|
||||
private val A_PERSISTED_EVENT = aMatrixRoomMessageEvent(AN_EVENT_ID, content = "persisted event")
|
||||
private val A_ROOM_EVENT = aMatrixRoomMessageEvent(AN_EVENT_ID, content = RichText.of("previous room event"))
|
||||
private val A_PERSISTED_EVENT = aMatrixRoomMessageEvent(AN_EVENT_ID, content = RichText.of("persisted event"))
|
||||
|
||||
class EventLookupUseCaseTest {
|
||||
|
||||
|
98
matrix/services/sync/src/test/kotlin/app/dapk/st/matrix/sync/internal/sync/RichMessageParserTest.kt
98
matrix/services/sync/src/test/kotlin/app/dapk/st/matrix/sync/internal/sync/RichMessageParserTest.kt
@ -1,6 +1,8 @@
|
||||
package app.dapk.st.matrix.sync.internal.sync
|
||||
|
||||
import app.dapk.st.matrix.common.RichText
|
||||
import app.dapk.st.matrix.common.RichText.Part.Link
|
||||
import app.dapk.st.matrix.common.RichText.Part.Normal
|
||||
import org.amshove.kluent.shouldBeEqualTo
|
||||
import org.junit.Ignore
|
||||
import org.junit.Test
|
||||
@ -12,32 +14,88 @@ class RichMessageParserTest {
|
||||
@Test
|
||||
fun `parses plain text`() = runParserTest(
|
||||
input = "Hello world!",
|
||||
expected = RichText(setOf(RichText.Part.Normal("Hello world!")))
|
||||
expected = RichText(setOf(Normal("Hello world!")))
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `parses nested b tags`() = runParserTest(
|
||||
fun `skips p tags`() = runParserTest(
|
||||
input = "Hello world! <p>foo bar</p> after paragraph",
|
||||
expected = RichText(setOf(Normal("Hello world! "), Normal("foo bar"), Normal(" after paragraph")))
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `skips header tags`() = runParserTest(
|
||||
Case(
|
||||
input = """hello <b>wor<b/>ld""",
|
||||
input = "<h1>hello</h1>",
|
||||
expected = RichText(setOf(Normal("hello")))
|
||||
),
|
||||
Case(
|
||||
input = "<h2>hello</h2>",
|
||||
expected = RichText(setOf(Normal("hello")))
|
||||
),
|
||||
Case(
|
||||
input = "<h3>hello</h3>",
|
||||
expected = RichText(setOf(Normal("hello")))
|
||||
),
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `replaces br tags`() = runParserTest(
|
||||
input = "Hello world!<br />next line<br />another line",
|
||||
expected = RichText(setOf(Normal("Hello world!"), Normal("\n"), Normal("next line"), Normal("\n"), Normal("another line")))
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `parses urls`() = runParserTest(
|
||||
Case(
|
||||
input = "https://google.com",
|
||||
expected = RichText(setOf(Link("https://google.com", "https://google.com")))
|
||||
),
|
||||
Case(
|
||||
input = "https://google.com. after link",
|
||||
expected = RichText(setOf(Link("https://google.com", "https://google.com"), Normal(". after link")))
|
||||
),
|
||||
Case(
|
||||
input = "ending sentence with url https://google.com.",
|
||||
expected = RichText(setOf(Normal("ending sentence with url "), Link("https://google.com", "https://google.com"), Normal(".")))
|
||||
),
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `parses styling text`() = runParserTest(
|
||||
input = "<em>hello</em> <strong>world</strong>",
|
||||
expected = RichText(setOf(RichText.Part.Italic("hello"), Normal(" "), RichText.Part.Bold("world")))
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `parses raw reply text`() = runParserTest(
|
||||
input = "> <@a-matrix-id:domain.foo> This is a reply",
|
||||
expected = RichText(setOf(Normal("> <@a-matrix-id:domain.foo> This is a reply")))
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `parses strong tags`() = runParserTest(
|
||||
Case(
|
||||
input = """hello <strong>wor</strong>ld""",
|
||||
expected = RichText(
|
||||
setOf(
|
||||
RichText.Part.Normal("hello "),
|
||||
Normal("hello "),
|
||||
RichText.Part.Bold("wor"),
|
||||
RichText.Part.Normal("ld"),
|
||||
Normal("ld"),
|
||||
)
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `parses nested i tags`() = runParserTest(
|
||||
fun `parses em tags`() = runParserTest(
|
||||
Case(
|
||||
input = """hello <i>wor<i/>ld""",
|
||||
input = """hello <em>wor</em>ld""",
|
||||
expected = RichText(
|
||||
setOf(
|
||||
RichText.Part.Normal("hello "),
|
||||
Normal("hello "),
|
||||
RichText.Part.Italic("wor"),
|
||||
RichText.Part.Normal("ld"),
|
||||
Normal("ld"),
|
||||
)
|
||||
)
|
||||
),
|
||||
@ -50,9 +108,9 @@ class RichMessageParserTest {
|
||||
input = """hello <b><i>wor<i/><b/>ld""",
|
||||
expected = RichText(
|
||||
setOf(
|
||||
RichText.Part.Normal("hello "),
|
||||
Normal("hello "),
|
||||
RichText.Part.BoldItalic("wor"),
|
||||
RichText.Part.Normal("ld"),
|
||||
Normal("ld"),
|
||||
)
|
||||
)
|
||||
),
|
||||
@ -60,8 +118,8 @@ class RichMessageParserTest {
|
||||
input = """<a href="www.google.com"><a href="www.google.com">www.google.com<a/><a/>""",
|
||||
expected = RichText(
|
||||
setOf(
|
||||
RichText.Part.Link(url = "www.google.com", label = "www.google.com"),
|
||||
RichText.Part.Link(url = "www.bing.com", label = "www.bing.com"),
|
||||
Link(url = "www.google.com", label = "www.google.com"),
|
||||
Link(url = "www.bing.com", label = "www.bing.com"),
|
||||
)
|
||||
)
|
||||
)
|
||||
@ -70,21 +128,21 @@ class RichMessageParserTest {
|
||||
@Test
|
||||
fun `parses 'a' tags`() = runParserTest(
|
||||
Case(
|
||||
input = """hello world <a href="www.google.com">a link!<a/> more content.""",
|
||||
input = """hello world <a href="www.google.com">a link!</a> more content.""",
|
||||
expected = RichText(
|
||||
setOf(
|
||||
RichText.Part.Normal("hello world "),
|
||||
RichText.Part.Link(url = "www.google.com", label = "a link!"),
|
||||
RichText.Part.Normal(" more content."),
|
||||
Normal("hello world "),
|
||||
Link(url = "www.google.com", label = "a link!"),
|
||||
Normal(" more content."),
|
||||
)
|
||||
)
|
||||
),
|
||||
Case(
|
||||
input = """<a href="www.google.com">www.google.com<a/><a href="www.bing.com">www.bing.com<a/>""",
|
||||
input = """<a href="www.google.com">www.google.com</a><a href="www.bing.com">www.bing.com</a>""",
|
||||
expected = RichText(
|
||||
setOf(
|
||||
RichText.Part.Link(url = "www.google.com", label = "www.google.com"),
|
||||
RichText.Part.Link(url = "www.bing.com", label = "www.bing.com"),
|
||||
Link(url = "www.google.com", label = "www.google.com"),
|
||||
Link(url = "www.bing.com", label = "www.bing.com"),
|
||||
)
|
||||
)
|
||||
),
|
||||
|
38
matrix/services/sync/src/test/kotlin/app/dapk/st/matrix/sync/internal/sync/RoomEventCreatorTest.kt
38
matrix/services/sync/src/test/kotlin/app/dapk/st/matrix/sync/internal/sync/RoomEventCreatorTest.kt
@ -1,6 +1,8 @@
|
||||
package app.dapk.st.matrix.sync.internal.sync
|
||||
|
||||
import app.dapk.st.matrix.common.EventId
|
||||
import app.dapk.st.matrix.common.RichText
|
||||
import app.dapk.st.matrix.common.asString
|
||||
import app.dapk.st.matrix.sync.RoomEvent
|
||||
import app.dapk.st.matrix.sync.internal.request.ApiEncryptedContent
|
||||
import app.dapk.st.matrix.sync.internal.request.ApiTimelineEvent
|
||||
@ -15,11 +17,11 @@ import org.junit.Test
|
||||
private val A_ROOM_ID = aRoomId()
|
||||
private val A_SENDER = aRoomMember()
|
||||
private val EMPTY_LOOKUP = FakeLookup(LookupResult(apiTimelineEvent = null, roomEvent = null))
|
||||
private const val A_TEXT_EVENT_MESSAGE = "a text message"
|
||||
private const val A_REPLY_EVENT_MESSAGE = "a reply to another message"
|
||||
private val A_TEXT_EVENT_MESSAGE = RichText.of("a text message")
|
||||
private val A_REPLY_EVENT_MESSAGE = RichText.of("a reply to another message")
|
||||
private val A_TEXT_EVENT = anApiTimelineTextEvent(
|
||||
senderId = A_SENDER.id,
|
||||
content = aTimelineTextEventContent(body = A_TEXT_EVENT_MESSAGE)
|
||||
content = aTimelineTextEventContent(body = A_TEXT_EVENT_MESSAGE.asString())
|
||||
)
|
||||
private val A_TEXT_EVENT_WITHOUT_CONTENT = anApiTimelineTextEvent(
|
||||
senderId = A_SENDER.id,
|
||||
@ -31,7 +33,7 @@ internal class RoomEventCreatorTest {
|
||||
|
||||
private val fakeRoomMembersService = FakeRoomMembersService()
|
||||
|
||||
private val roomEventCreator = RoomEventCreator(fakeRoomMembersService, FakeErrorTracker(), RoomEventFactory(fakeRoomMembersService))
|
||||
private val roomEventCreator = RoomEventCreator(fakeRoomMembersService, FakeErrorTracker(), RoomEventFactory(fakeRoomMembersService, RichMessageParser()))
|
||||
|
||||
@Test
|
||||
fun `given Megolm encrypted event then maps to encrypted room message`() = runTest {
|
||||
@ -89,7 +91,7 @@ internal class RoomEventCreatorTest {
|
||||
result shouldBeEqualTo aMatrixRoomMessageEvent(
|
||||
eventId = A_TEXT_EVENT_WITHOUT_CONTENT.id,
|
||||
utcTimestamp = A_TEXT_EVENT_WITHOUT_CONTENT.utcTimestamp,
|
||||
content = "redacted",
|
||||
content = RichText.of("redacted"),
|
||||
author = A_SENDER,
|
||||
)
|
||||
}
|
||||
@ -97,14 +99,14 @@ internal class RoomEventCreatorTest {
|
||||
@Test
|
||||
fun `given edited event with no relation then maps to new room message`() = runTest {
|
||||
fakeRoomMembersService.givenMember(A_ROOM_ID, A_SENDER.id, A_SENDER)
|
||||
val editEvent = anApiTimelineTextEvent().toEditEvent(newTimestamp = 0, messageContent = A_TEXT_EVENT_MESSAGE)
|
||||
val editEvent = anApiTimelineTextEvent().toEditEvent(newTimestamp = 0, messageContent = A_TEXT_EVENT_MESSAGE.asString())
|
||||
|
||||
val result = with(roomEventCreator) { editEvent.toRoomEvent(A_USER_CREDENTIALS, A_ROOM_ID, EMPTY_LOOKUP) }
|
||||
|
||||
result shouldBeEqualTo aMatrixRoomMessageEvent(
|
||||
eventId = editEvent.id,
|
||||
utcTimestamp = editEvent.utcTimestamp,
|
||||
content = editEvent.asTextContent().body!!,
|
||||
content = RichText.of(editEvent.asTextContent().body!!),
|
||||
author = A_SENDER,
|
||||
edited = true
|
||||
)
|
||||
@ -114,7 +116,7 @@ internal class RoomEventCreatorTest {
|
||||
fun `given edited event which relates to a timeline event then updates existing message`() = runTest {
|
||||
fakeRoomMembersService.givenMember(A_ROOM_ID, A_SENDER.id, A_SENDER)
|
||||
val originalMessage = anApiTimelineTextEvent(utcTimestamp = 0)
|
||||
val editedMessage = originalMessage.toEditEvent(newTimestamp = 1000, messageContent = A_TEXT_EVENT_MESSAGE)
|
||||
val editedMessage = originalMessage.toEditEvent(newTimestamp = 1000, messageContent = A_TEXT_EVENT_MESSAGE.asString())
|
||||
val lookup = givenLookup(originalMessage)
|
||||
|
||||
val result = with(roomEventCreator) { editedMessage.toRoomEvent(A_USER_CREDENTIALS, A_ROOM_ID, lookup) }
|
||||
@ -132,7 +134,7 @@ internal class RoomEventCreatorTest {
|
||||
fun `given edited event which relates to a room event then updates existing message`() = runTest {
|
||||
fakeRoomMembersService.givenMember(A_ROOM_ID, A_SENDER.id, A_SENDER)
|
||||
val originalMessage = aMatrixRoomMessageEvent()
|
||||
val editedMessage = originalMessage.toEditEvent(newTimestamp = 1000, messageContent = A_TEXT_EVENT_MESSAGE)
|
||||
val editedMessage = originalMessage.toEditEvent(newTimestamp = 1000, messageContent = A_TEXT_EVENT_MESSAGE.asString())
|
||||
val lookup = givenLookup(originalMessage)
|
||||
|
||||
val result = with(roomEventCreator) { editedMessage.toRoomEvent(A_USER_CREDENTIALS, A_ROOM_ID, lookup) }
|
||||
@ -150,7 +152,7 @@ internal class RoomEventCreatorTest {
|
||||
fun `given edited event which relates to a room reply event then only updates message`() = runTest {
|
||||
fakeRoomMembersService.givenMember(A_ROOM_ID, A_SENDER.id, A_SENDER)
|
||||
val originalMessage = aRoomReplyMessageEvent(message = aMatrixRoomMessageEvent())
|
||||
val editedMessage = (originalMessage.message as RoomEvent.Message).toEditEvent(newTimestamp = 1000, messageContent = A_TEXT_EVENT_MESSAGE)
|
||||
val editedMessage = (originalMessage.message as RoomEvent.Message).toEditEvent(newTimestamp = 1000, messageContent = A_TEXT_EVENT_MESSAGE.asString())
|
||||
val lookup = givenLookup(originalMessage)
|
||||
|
||||
val result = with(roomEventCreator) { editedMessage.toRoomEvent(A_USER_CREDENTIALS, A_ROOM_ID, lookup) }
|
||||
@ -170,7 +172,7 @@ internal class RoomEventCreatorTest {
|
||||
@Test
|
||||
fun `given edited event is older than related known timeline event then ignores edit`() = runTest {
|
||||
val originalMessage = anApiTimelineTextEvent(utcTimestamp = 1000)
|
||||
val editedMessage = originalMessage.toEditEvent(newTimestamp = 0, messageContent = A_TEXT_EVENT_MESSAGE)
|
||||
val editedMessage = originalMessage.toEditEvent(newTimestamp = 0, messageContent = A_TEXT_EVENT_MESSAGE.asString())
|
||||
val lookup = givenLookup(originalMessage)
|
||||
|
||||
val result = with(roomEventCreator) { editedMessage.toRoomEvent(A_USER_CREDENTIALS, A_ROOM_ID, lookup) }
|
||||
@ -181,7 +183,7 @@ internal class RoomEventCreatorTest {
|
||||
@Test
|
||||
fun `given edited event is older than related room event then ignores edit`() = runTest {
|
||||
val originalMessage = aMatrixRoomMessageEvent(utcTimestamp = 1000)
|
||||
val editedMessage = originalMessage.toEditEvent(newTimestamp = 0, messageContent = A_TEXT_EVENT_MESSAGE)
|
||||
val editedMessage = originalMessage.toEditEvent(newTimestamp = 0, messageContent = A_TEXT_EVENT_MESSAGE.asString())
|
||||
val lookup = givenLookup(originalMessage)
|
||||
|
||||
val result = with(roomEventCreator) { editedMessage.toRoomEvent(A_USER_CREDENTIALS, A_ROOM_ID, lookup) }
|
||||
@ -192,7 +194,7 @@ internal class RoomEventCreatorTest {
|
||||
@Test
|
||||
fun `given reply event with no relation then maps to new room message using the full body`() = runTest {
|
||||
fakeRoomMembersService.givenMember(A_ROOM_ID, A_SENDER.id, A_SENDER)
|
||||
val replyEvent = anApiTimelineTextEvent().toReplyEvent(messageContent = A_TEXT_EVENT_MESSAGE)
|
||||
val replyEvent = anApiTimelineTextEvent().toReplyEvent(messageContent = A_TEXT_EVENT_MESSAGE.asString())
|
||||
|
||||
println(replyEvent.content)
|
||||
val result = with(roomEventCreator) { replyEvent.toRoomEvent(A_USER_CREDENTIALS, A_ROOM_ID, EMPTY_LOOKUP) }
|
||||
@ -200,7 +202,7 @@ internal class RoomEventCreatorTest {
|
||||
result shouldBeEqualTo aMatrixRoomMessageEvent(
|
||||
eventId = replyEvent.id,
|
||||
utcTimestamp = replyEvent.utcTimestamp,
|
||||
content = replyEvent.asTextContent().body!!,
|
||||
content = RichText.of(replyEvent.asTextContent().body!!),
|
||||
author = A_SENDER,
|
||||
)
|
||||
}
|
||||
@ -209,7 +211,7 @@ internal class RoomEventCreatorTest {
|
||||
fun `given reply event which relates to a timeline event then maps to reply`() = runTest {
|
||||
fakeRoomMembersService.givenMember(A_ROOM_ID, A_SENDER.id, A_SENDER)
|
||||
val originalMessage = anApiTimelineTextEvent(content = aTimelineTextEventContent(body = "message being replied to"))
|
||||
val replyMessage = originalMessage.toReplyEvent(messageContent = A_REPLY_EVENT_MESSAGE)
|
||||
val replyMessage = originalMessage.toReplyEvent(messageContent = A_REPLY_EVENT_MESSAGE.asString())
|
||||
val lookup = givenLookup(originalMessage)
|
||||
|
||||
val result = with(roomEventCreator) { replyMessage.toRoomEvent(A_USER_CREDENTIALS, A_ROOM_ID, lookup) }
|
||||
@ -218,7 +220,7 @@ internal class RoomEventCreatorTest {
|
||||
replyingTo = aMatrixRoomMessageEvent(
|
||||
eventId = originalMessage.id,
|
||||
utcTimestamp = originalMessage.utcTimestamp,
|
||||
content = originalMessage.asTextContent().body!!,
|
||||
content = RichText.of(originalMessage.asTextContent().body!!),
|
||||
author = A_SENDER,
|
||||
),
|
||||
message = aMatrixRoomMessageEvent(
|
||||
@ -234,7 +236,7 @@ internal class RoomEventCreatorTest {
|
||||
fun `given reply event which relates to a room event then maps to reply`() = runTest {
|
||||
fakeRoomMembersService.givenMember(A_ROOM_ID, A_SENDER.id, A_SENDER)
|
||||
val originalMessage = aMatrixRoomMessageEvent()
|
||||
val replyMessage = originalMessage.toReplyEvent(messageContent = A_REPLY_EVENT_MESSAGE)
|
||||
val replyMessage = originalMessage.toReplyEvent(messageContent = A_REPLY_EVENT_MESSAGE.asString())
|
||||
val lookup = givenLookup(originalMessage)
|
||||
|
||||
val result = with(roomEventCreator) { replyMessage.toRoomEvent(A_USER_CREDENTIALS, A_ROOM_ID, lookup) }
|
||||
@ -254,7 +256,7 @@ internal class RoomEventCreatorTest {
|
||||
fun `given reply event which relates to another room reply event then maps to reply with the reply's message`() = runTest {
|
||||
fakeRoomMembersService.givenMember(A_ROOM_ID, A_SENDER.id, A_SENDER)
|
||||
val originalMessage = aRoomReplyMessageEvent()
|
||||
val replyMessage = (originalMessage.message as RoomEvent.Message).toReplyEvent(messageContent = A_REPLY_EVENT_MESSAGE)
|
||||
val replyMessage = (originalMessage.message as RoomEvent.Message).toReplyEvent(messageContent = A_REPLY_EVENT_MESSAGE.asString())
|
||||
val lookup = givenLookup(originalMessage)
|
||||
|
||||
val result = with(roomEventCreator) { replyMessage.toRoomEvent(A_USER_CREDENTIALS, A_ROOM_ID, lookup) }
|
||||
|
@ -1,6 +1,7 @@
|
||||
package app.dapk.st.matrix.sync.internal.sync
|
||||
|
||||
import app.dapk.st.matrix.common.RoomId
|
||||
import app.dapk.st.matrix.common.asString
|
||||
import app.dapk.st.matrix.sync.RoomEvent
|
||||
import app.dapk.st.matrix.sync.RoomState
|
||||
import fake.FakeMatrixLogger
|
||||
@ -60,7 +61,7 @@ internal class RoomRefresherTest {
|
||||
}
|
||||
|
||||
private fun RoomEvent.Message.asLastMessage() = aLastMessage(
|
||||
this.content,
|
||||
this.content.asString(),
|
||||
this.utcTimestamp,
|
||||
this.author,
|
||||
)
|
||||
|
@ -7,7 +7,7 @@ import app.dapk.st.matrix.sync.RoomEvent
|
||||
fun aMatrixRoomMessageEvent(
|
||||
eventId: EventId = anEventId(),
|
||||
utcTimestamp: Long = 0L,
|
||||
content: String = "message-content",
|
||||
content: RichText = RichText.of("message-content"),
|
||||
author: RoomMember = aRoomMember(),
|
||||
meta: MessageMeta = MessageMeta.FromServer,
|
||||
edited: Boolean = false,
|
||||
|
Loading…
x
Reference in New Issue
Block a user