adding support for sending replies

This commit is contained in:
Adam Brown 2022-09-30 20:17:37 +01:00
parent bccf947508
commit 81f15c4fca
5 changed files with 127 additions and 23 deletions

View File

@ -109,7 +109,7 @@ internal class MessengerViewModel(
when (val composerState = state.composerState) { when (val composerState = state.composerState) {
is ComposerState.Text -> { is ComposerState.Text -> {
val copy = composerState.copy() val copy = composerState.copy()
updateState { copy(composerState = composerState.copy(value = "")) } updateState { copy(composerState = composerState.copy(value = "", reply = null)) }
state.roomState.takeIfContent()?.let { content -> state.roomState.takeIfContent()?.let { content ->
val roomState = content.roomState val roomState = content.roomState
@ -121,6 +121,18 @@ internal class MessengerViewModel(
sendEncrypted = roomState.roomOverview.isEncrypted, sendEncrypted = roomState.roomOverview.isEncrypted,
localId = localIdFactory.create(), localId = localIdFactory.create(),
timestampUtc = clock.millis(), timestampUtc = clock.millis(),
reply = copy.reply?.let {
MessageService.Message.TextMessage.Reply(
authorId = it.author.id,
originalMessage = when (it) {
is RoomEvent.Image -> TODO()
is RoomEvent.Reply -> TODO()
is RoomEvent.Message -> it.content
},
copy.value,
it.eventId
)
}
) )
) )
} }

View File

@ -1,11 +1,7 @@
package app.dapk.st.matrix.message package app.dapk.st.matrix.message
import app.dapk.st.core.Base64
import app.dapk.st.matrix.* import app.dapk.st.matrix.*
import app.dapk.st.matrix.common.AlgorithmName import app.dapk.st.matrix.common.*
import app.dapk.st.matrix.common.EventId
import app.dapk.st.matrix.common.MessageType
import app.dapk.st.matrix.common.RoomId
import app.dapk.st.matrix.message.internal.DefaultMessageService import app.dapk.st.matrix.message.internal.DefaultMessageService
import app.dapk.st.matrix.message.internal.ImageContentReader import app.dapk.st.matrix.message.internal.ImageContentReader
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@ -43,7 +39,17 @@ interface MessageService : MatrixService {
@SerialName("room_id") val roomId: RoomId, @SerialName("room_id") val roomId: RoomId,
@SerialName("local_id") val localId: String, @SerialName("local_id") val localId: String,
@SerialName("timestamp") val timestampUtc: Long, @SerialName("timestamp") val timestampUtc: Long,
) : Message() @SerialName("reply") val reply: Reply? = null,
@SerialName("reply_id") val replyId: String? = null,
) : Message() {
@Serializable
data class Reply(
val authorId: UserId,
val originalMessage: String,
val replyContent: String,
val eventId: EventId
)
}
@Serializable @Serializable
@SerialName("image_message") @SerialName("image_message")

View File

@ -1,5 +1,6 @@
package app.dapk.st.matrix.message.internal package app.dapk.st.matrix.message.internal
import app.dapk.st.matrix.common.EventId
import app.dapk.st.matrix.common.MessageType import app.dapk.st.matrix.common.MessageType
import app.dapk.st.matrix.common.MxUrl import app.dapk.st.matrix.common.MxUrl
import app.dapk.st.matrix.common.RoomId import app.dapk.st.matrix.common.RoomId
@ -20,8 +21,26 @@ sealed class ApiMessage {
@Serializable @Serializable
data class TextContent( data class TextContent(
@SerialName("body") val body: String, @SerialName("body") val body: String,
@SerialName("msgtype") val type: String = MessageType.TEXT.value, @SerialName("m.relates_to") val relatesTo: RelatesTo? = null,
) : ApiMessageContent @SerialName("formatted_body") val formattedBody: String? = null,
@SerialName("format") val format: String? = null,
) : ApiMessageContent {
@SerialName("msgtype")
val type: String = MessageType.TEXT.value
}
}
@Serializable
data class RelatesTo(
@SerialName("m.in_reply_to") val inReplyTo: InReplyTo
) {
@Serializable
data class InReplyTo(
@SerialName("event_id") val eventId: EventId
)
} }
@Serializable @Serializable

View File

@ -1,9 +1,6 @@
package app.dapk.st.matrix.message.internal package app.dapk.st.matrix.message.internal
import app.dapk.st.matrix.common.EventId import app.dapk.st.matrix.common.*
import app.dapk.st.matrix.common.EventType
import app.dapk.st.matrix.common.JsonString
import app.dapk.st.matrix.common.RoomId
import app.dapk.st.matrix.http.MatrixHttpClient import app.dapk.st.matrix.http.MatrixHttpClient
import app.dapk.st.matrix.http.MatrixHttpClient.HttpRequest import app.dapk.st.matrix.http.MatrixHttpClient.HttpRequest
import app.dapk.st.matrix.message.ApiSendResponse import app.dapk.st.matrix.message.ApiSendResponse
@ -37,7 +34,7 @@ internal class SendMessageUseCase(
} }
private suspend fun ApiMessageMapper.textMessageRequest(message: Message.TextMessage): HttpRequest<ApiSendResponse> { private suspend fun ApiMessageMapper.textMessageRequest(message: Message.TextMessage): HttpRequest<ApiSendResponse> {
val contents = message.toContents() val contents = message.toContents(message.reply)
return when (message.sendEncrypted) { return when (message.sendEncrypted) {
true -> sendRequest( true -> sendRequest(
roomId = message.roomId, roomId = message.roomId,
@ -49,6 +46,7 @@ internal class SendMessageUseCase(
contents.toMessageJson(message.roomId) contents.toMessageJson(message.roomId)
) )
), ),
relatesTo = contents.relatesTo
) )
false -> sendRequest( false -> sendRequest(
@ -115,6 +113,7 @@ internal class SendMessageUseCase(
eventType = EventType.ENCRYPTED, eventType = EventType.ENCRYPTED,
txId = message.localId, txId = message.localId,
content = messageEncrypter.encrypt(MessageEncrypter.ClearMessagePayload(message.roomId, json)), content = messageEncrypter.encrypt(MessageEncrypter.ClearMessagePayload(message.roomId, json)),
relatesTo = null
) )
} }
@ -148,14 +147,23 @@ internal class SendMessageUseCase(
} }
private val MX_REPLY_REGEX = "<mx-reply>.*</mx-reply>".toRegex()
class ApiMessageMapper { class ApiMessageMapper {
fun Message.TextMessage.toContents() = ApiMessage.TextMessage.TextContent( fun Message.TextMessage.toContents(reply: Message.TextMessage.Reply?) = when (reply) {
this.content.body, null -> ApiMessage.TextMessage.TextContent(
this.content.type, body = this.content.body,
) )
else -> ApiMessage.TextMessage.TextContent(
body = buildReplyFallback(reply.originalMessage, reply.authorId, reply.replyContent),
relatesTo = ApiMessage.RelatesTo(ApiMessage.RelatesTo.InReplyTo(reply.eventId)),
formattedBody = buildFormattedReply(reply.authorId, reply.originalMessage, reply.replyContent, this.roomId, reply.eventId),
format = "org.matrix.custom.html"
)
}
fun ApiMessage.TextMessage.TextContent.toMessageJson(roomId: RoomId) = JsonString( fun ApiMessage.TextMessage.TextContent.toMessageJson(roomId: RoomId) = JsonString(
MatrixHttpClient.jsonWithDefaults.encodeToString( MatrixHttpClient.jsonWithDefaults.encodeToString(
ApiMessage.TextMessage.serializer(), ApiMessage.TextMessage.serializer(),
@ -167,4 +175,33 @@ class ApiMessageMapper {
) )
) )
private fun buildReplyFallback(originalMessage: String, originalSenderId: UserId, reply: String): String {
return buildString {
append("> <")
append(originalSenderId.value)
append(">")
val lines = originalMessage.split("\n")
lines.forEachIndexed { index, s ->
if (index == 0) {
append(" $s")
} else {
append("\n> $s")
}
}
append("\n\n")
append(reply)
}
}
private fun buildFormattedReply(userId: UserId, originalMessage: String, reply: String, roomId: RoomId, eventId: EventId): String {
val permalink = "https://matrix.to/#/${roomId.value}/${eventId.value}"
val userLink = "https://matrix.to/#/${userId.value}"
val cleanOriginalMessage = originalMessage.replace(MX_REPLY_REGEX, "")
return """
<mx-reply><blockquote><a href="$permalink">In reply to</a> <a href="$userLink">${userId.value}</a><br>${cleanOriginalMessage}</blockquote></mx-reply>$reply
""".trimIndent()
}
} }

View File

@ -1,7 +1,6 @@
package app.dapk.st.matrix.message.internal package app.dapk.st.matrix.message.internal
import app.dapk.st.matrix.common.EventType import app.dapk.st.matrix.common.*
import app.dapk.st.matrix.common.RoomId
import app.dapk.st.matrix.http.MatrixHttpClient import app.dapk.st.matrix.http.MatrixHttpClient
import app.dapk.st.matrix.http.MatrixHttpClient.HttpRequest.Companion.httpRequest import app.dapk.st.matrix.http.MatrixHttpClient.HttpRequest.Companion.httpRequest
import app.dapk.st.matrix.http.jsonBody import app.dapk.st.matrix.http.jsonBody
@ -14,6 +13,8 @@ import app.dapk.st.matrix.message.internal.ApiMessage.TextMessage
import io.ktor.http.* import io.ktor.http.*
import io.ktor.http.content.* import io.ktor.http.content.*
import io.ktor.utils.io.jvm.javaio.* import io.ktor.utils.io.jvm.javaio.*
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import java.io.InputStream import java.io.InputStream
import java.util.* import java.util.*
@ -26,10 +27,29 @@ internal fun sendRequest(roomId: RoomId, eventType: EventType, txId: String, con
} }
) )
internal fun sendRequest(roomId: RoomId, eventType: EventType, txId: String, content: MessageEncrypter.EncryptedMessagePayload) = httpRequest<ApiSendResponse>( internal fun sendRequest(
roomId: RoomId,
eventType: EventType,
txId: String,
content: MessageEncrypter.EncryptedMessagePayload,
relatesTo: ApiMessage.RelatesTo?
) = httpRequest<ApiSendResponse>(
path = "_matrix/client/r0/rooms/${roomId.value}/send/${eventType.value}/${txId}", path = "_matrix/client/r0/rooms/${roomId.value}/send/${eventType.value}/${txId}",
method = MatrixHttpClient.Method.PUT, method = MatrixHttpClient.Method.PUT,
body = jsonBody(MessageEncrypter.EncryptedMessagePayload.serializer(), content) body = jsonBody(ApiEncryptedMessage.serializer(), content.let {
val apiEncryptedMessage = ApiEncryptedMessage(
algorithmName = content.algorithmName,
senderKey = content.senderKey,
cipherText = content.cipherText,
sessionId = content.sessionId,
deviceId = content.deviceId,
)
when (relatesTo) {
null -> apiEncryptedMessage
else -> apiEncryptedMessage.copy(relatesTo = relatesTo)
}
})
) )
internal fun sendRequest(roomId: RoomId, eventType: EventType, content: EventMessage) = httpRequest<ApiSendResponse>( internal fun sendRequest(roomId: RoomId, eventType: EventType, content: EventMessage) = httpRequest<ApiSendResponse>(
@ -52,3 +72,13 @@ internal fun uploadRequest(stream: InputStream, contentLength: Long, filename: S
) )
fun txId() = "local.${UUID.randomUUID()}" fun txId() = "local.${UUID.randomUUID()}"
@Serializable
data class ApiEncryptedMessage(
@SerialName("algorithm") val algorithmName: AlgorithmName,
@SerialName("sender_key") val senderKey: String,
@SerialName("ciphertext") val cipherText: CipherText,
@SerialName("session_id") val sessionId: SessionId,
@SerialName("device_id") val deviceId: DeviceId,
@SerialName("m.relates_to") val relatesTo: ApiMessage.RelatesTo? = null,
)