diff --git a/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerViewModel.kt b/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerViewModel.kt index c533243..a31b1b0 100644 --- a/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerViewModel.kt +++ b/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerViewModel.kt @@ -109,7 +109,7 @@ internal class MessengerViewModel( when (val composerState = state.composerState) { is ComposerState.Text -> { val copy = composerState.copy() - updateState { copy(composerState = composerState.copy(value = "")) } + updateState { copy(composerState = composerState.copy(value = "", reply = null)) } state.roomState.takeIfContent()?.let { content -> val roomState = content.roomState @@ -121,6 +121,18 @@ internal class MessengerViewModel( sendEncrypted = roomState.roomOverview.isEncrypted, localId = localIdFactory.create(), 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 + ) + } ) ) } diff --git a/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/MessageService.kt b/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/MessageService.kt index 35b6297..2c2ff69 100644 --- a/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/MessageService.kt +++ b/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/MessageService.kt @@ -1,11 +1,7 @@ package app.dapk.st.matrix.message -import app.dapk.st.core.Base64 import app.dapk.st.matrix.* -import app.dapk.st.matrix.common.AlgorithmName -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.common.* import app.dapk.st.matrix.message.internal.DefaultMessageService import app.dapk.st.matrix.message.internal.ImageContentReader import kotlinx.coroutines.flow.Flow @@ -43,7 +39,17 @@ interface MessageService : MatrixService { @SerialName("room_id") val roomId: RoomId, @SerialName("local_id") val localId: String, @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 @SerialName("image_message") diff --git a/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/internal/ApiMessage.kt b/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/internal/ApiMessage.kt index 84bc9f7..d71b6da 100644 --- a/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/internal/ApiMessage.kt +++ b/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/internal/ApiMessage.kt @@ -1,5 +1,6 @@ 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.MxUrl import app.dapk.st.matrix.common.RoomId @@ -20,8 +21,26 @@ sealed class ApiMessage { @Serializable data class TextContent( @SerialName("body") val body: String, - @SerialName("msgtype") val type: String = MessageType.TEXT.value, - ) : ApiMessageContent + @SerialName("m.relates_to") val relatesTo: RelatesTo? = null, + @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 diff --git a/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/internal/SendMessageUseCase.kt b/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/internal/SendMessageUseCase.kt index a75e088..aa573f2 100644 --- a/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/internal/SendMessageUseCase.kt +++ b/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/internal/SendMessageUseCase.kt @@ -1,9 +1,6 @@ package app.dapk.st.matrix.message.internal -import app.dapk.st.matrix.common.EventId -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.common.* import app.dapk.st.matrix.http.MatrixHttpClient import app.dapk.st.matrix.http.MatrixHttpClient.HttpRequest import app.dapk.st.matrix.message.ApiSendResponse @@ -37,7 +34,7 @@ internal class SendMessageUseCase( } private suspend fun ApiMessageMapper.textMessageRequest(message: Message.TextMessage): HttpRequest { - val contents = message.toContents() + val contents = message.toContents(message.reply) return when (message.sendEncrypted) { true -> sendRequest( roomId = message.roomId, @@ -49,6 +46,7 @@ internal class SendMessageUseCase( contents.toMessageJson(message.roomId) ) ), + relatesTo = contents.relatesTo ) false -> sendRequest( @@ -115,6 +113,7 @@ internal class SendMessageUseCase( eventType = EventType.ENCRYPTED, txId = message.localId, content = messageEncrypter.encrypt(MessageEncrypter.ClearMessagePayload(message.roomId, json)), + relatesTo = null ) } @@ -148,13 +147,22 @@ internal class SendMessageUseCase( } +private val MX_REPLY_REGEX = ".*".toRegex() class ApiMessageMapper { - fun Message.TextMessage.toContents() = ApiMessage.TextMessage.TextContent( - this.content.body, - this.content.type, - ) + fun Message.TextMessage.toContents(reply: Message.TextMessage.Reply?) = when (reply) { + null -> ApiMessage.TextMessage.TextContent( + 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( MatrixHttpClient.jsonWithDefaults.encodeToString( @@ -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 """ +
In reply to ${userId.value}
${cleanOriginalMessage}
$reply + """.trimIndent() + + } + } diff --git a/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/internal/SendRequest.kt b/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/internal/SendRequest.kt index 2057084..1d64872 100644 --- a/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/internal/SendRequest.kt +++ b/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/internal/SendRequest.kt @@ -1,7 +1,6 @@ package app.dapk.st.matrix.message.internal -import app.dapk.st.matrix.common.EventType -import app.dapk.st.matrix.common.RoomId +import app.dapk.st.matrix.common.* import app.dapk.st.matrix.http.MatrixHttpClient import app.dapk.st.matrix.http.MatrixHttpClient.HttpRequest.Companion.httpRequest 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.content.* import io.ktor.utils.io.jvm.javaio.* +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable import java.io.InputStream 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( +internal fun sendRequest( + roomId: RoomId, + eventType: EventType, + txId: String, + content: MessageEncrypter.EncryptedMessagePayload, + relatesTo: ApiMessage.RelatesTo? +) = httpRequest( path = "_matrix/client/r0/rooms/${roomId.value}/send/${eventType.value}/${txId}", 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( @@ -51,4 +71,14 @@ internal fun uploadRequest(stream: InputStream, contentLength: Long, filename: S ), ) -fun txId() = "local.${UUID.randomUUID()}" \ No newline at end of file +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, +) \ No newline at end of file