Merge pull request #322 from vector-im/feature/clean

Improve reply feature
This commit is contained in:
Valere 2019-07-11 11:46:00 +02:00 committed by GitHub
commit 98306e223b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 297 additions and 232 deletions

View File

@ -16,8 +16,8 @@
package im.vector.matrix.android.api.session.room.model.relation package im.vector.matrix.android.api.session.room.model.relation


import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.api.util.Cancelable


/** /**
@ -77,8 +77,9 @@ interface RelationService {
* https://matrix.org/docs/spec/client_server/r0.4.0.html#id350 * https://matrix.org/docs/spec/client_server/r0.4.0.html#id350
* @param eventReplied the event referenced by the reply * @param eventReplied the event referenced by the reply
* @param replyText the reply text * @param replyText the reply text
* @param autoMarkdown If true, the SDK will generate a formatted HTML message from the body text if markdown syntax is present
*/ */
fun replyToMessage(eventReplied: Event, replyText: String): Cancelable? fun replyToMessage(eventReplied: TimelineEvent, replyText: String, autoMarkdown: Boolean = false): Cancelable?


fun getEventSummaryLive(eventId: String): LiveData<EventAnnotationsSummary> fun getEventSummaryLive(eventId: String): LiveData<EventAnnotationsSummary>
} }

View File

@ -18,7 +18,9 @@ package im.vector.matrix.android.api.session.room.timeline


import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary
import im.vector.matrix.android.api.session.room.model.message.MessageContent
import im.vector.matrix.android.api.session.room.send.SendState import im.vector.matrix.android.api.session.room.send.SendState


/** /**
@ -80,3 +82,9 @@ data class TimelineEvent(
return EventType.ENCRYPTED == root.type return EventType.ENCRYPTED == root.type
} }
} }

/**
* Get last MessageContent, after a possible edition
*/
fun TimelineEvent.getLastMessageContent(): MessageContent? = annotations?.editSummary?.aggregatedContent?.toModel()
?: root.getClearContent().toModel()

View File

@ -73,6 +73,7 @@ import im.vector.matrix.android.internal.session.room.membership.RoomMembers
import im.vector.matrix.android.internal.session.sync.model.SyncResponse import im.vector.matrix.android.internal.session.sync.model.SyncResponse
import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith import im.vector.matrix.android.internal.task.configureWith
import im.vector.matrix.android.internal.task.toConfigurableTask
import im.vector.matrix.android.internal.util.JsonCanonicalizer import im.vector.matrix.android.internal.util.JsonCanonicalizer
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import im.vector.matrix.android.internal.util.fetchCopied import im.vector.matrix.android.internal.util.fetchCopied
@ -197,7 +198,7 @@ internal class CryptoManager @Inject constructor(


override fun getDevicesList(callback: MatrixCallback<DevicesListResponse>) { override fun getDevicesList(callback: MatrixCallback<DevicesListResponse>) {
getDevicesTask getDevicesTask
.configureWith(Unit) .toConfigurableTask()
.dispatchTo(callback) .dispatchTo(callback)
.executeBy(taskExecutor) .executeBy(taskExecutor)
} }
@ -1054,7 +1055,8 @@ internal class CryptoManager @Inject constructor(
} }


override fun clearCryptoCache(callback: MatrixCallback<Unit>) { override fun clearCryptoCache(callback: MatrixCallback<Unit>) {
clearCryptoDataTask.configureWith(Unit) clearCryptoDataTask
.toConfigurableTask()
.dispatchTo(callback) .dispatchTo(callback)
.executeBy(taskExecutor) .executeBy(taskExecutor)
} }

View File

@ -51,6 +51,7 @@ import im.vector.matrix.android.internal.crypto.store.db.model.KeysBackupDataEnt
import im.vector.matrix.android.internal.di.MoshiProvider import im.vector.matrix.android.internal.di.MoshiProvider
import im.vector.matrix.android.internal.extensions.foldToCallback import im.vector.matrix.android.internal.extensions.foldToCallback
import im.vector.matrix.android.internal.session.SessionScope import im.vector.matrix.android.internal.session.SessionScope
import im.vector.matrix.android.internal.task.*
import im.vector.matrix.android.internal.task.Task import im.vector.matrix.android.internal.task.Task
import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.TaskThread import im.vector.matrix.android.internal.task.TaskThread
@ -876,7 +877,7 @@ internal class KeysBackup @Inject constructor(


override fun getCurrentVersion(callback: MatrixCallback<KeysVersionResult?>) { override fun getCurrentVersion(callback: MatrixCallback<KeysVersionResult?>) {
getKeysBackupLastVersionTask getKeysBackupLastVersionTask
.configureWith(Unit) .toConfigurableTask()
.dispatchTo(object : MatrixCallback<KeysVersionResult> { .dispatchTo(object : MatrixCallback<KeysVersionResult> {
override fun onSuccess(data: KeysVersionResult) { override fun onSuccess(data: KeysVersionResult) {
callback.onSuccess(data) callback.onSuccess(data)

View File

@ -19,9 +19,8 @@ package im.vector.matrix.android.internal.session.cache
import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.cache.CacheService import im.vector.matrix.android.api.session.cache.CacheService
import im.vector.matrix.android.internal.di.SessionDatabase import im.vector.matrix.android.internal.di.SessionDatabase
import im.vector.matrix.android.internal.session.SessionScope
import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith import im.vector.matrix.android.internal.task.toConfigurableTask
import javax.inject.Inject import javax.inject.Inject


internal class DefaultCacheService @Inject constructor(@SessionDatabase private val clearCacheTask: ClearCacheTask, internal class DefaultCacheService @Inject constructor(@SessionDatabase private val clearCacheTask: ClearCacheTask,
@ -29,7 +28,7 @@ internal class DefaultCacheService @Inject constructor(@SessionDatabase private


override fun clearCache(callback: MatrixCallback<Unit>) { override fun clearCache(callback: MatrixCallback<Unit>) {
clearCacheTask clearCacheTask
.configureWith(Unit) .toConfigurableTask()
.dispatchTo(callback) .dispatchTo(callback)
.executeBy(taskExecutor) .executeBy(taskExecutor)
} }

View File

@ -29,6 +29,7 @@ import im.vector.matrix.android.internal.database.model.PusherEntity
import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith import im.vector.matrix.android.internal.task.configureWith
import im.vector.matrix.android.internal.task.toConfigurableTask
import im.vector.matrix.android.internal.worker.WorkManagerUtil import im.vector.matrix.android.internal.worker.WorkManagerUtil
import im.vector.matrix.android.internal.worker.WorkManagerUtil.matrixOneTimeWorkRequestBuilder import im.vector.matrix.android.internal.worker.WorkManagerUtil.matrixOneTimeWorkRequestBuilder
import im.vector.matrix.android.internal.worker.WorkerParamsFactory import im.vector.matrix.android.internal.worker.WorkerParamsFactory
@ -49,7 +50,7 @@ internal class DefaultPusherService @Inject constructor(


override fun refreshPushers() { override fun refreshPushers() {
getPusherTask getPusherTask
.configureWith(Unit) .toConfigurableTask()
.executeBy(taskExecutor) .executeBy(taskExecutor)
} }



View File

@ -27,6 +27,7 @@ import im.vector.matrix.android.internal.session.room.directory.GetThirdPartyPro
import im.vector.matrix.android.internal.session.room.membership.joining.JoinRoomTask import im.vector.matrix.android.internal.session.room.membership.joining.JoinRoomTask
import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith import im.vector.matrix.android.internal.task.configureWith
import im.vector.matrix.android.internal.task.toConfigurableTask
import javax.inject.Inject import javax.inject.Inject


internal class DefaultRoomDirectoryService @Inject constructor(private val getPublicRoomTask: GetPublicRoomTask, internal class DefaultRoomDirectoryService @Inject constructor(private val getPublicRoomTask: GetPublicRoomTask,
@ -52,7 +53,7 @@ internal class DefaultRoomDirectoryService @Inject constructor(private val getPu


override fun getThirdPartyProtocol(callback: MatrixCallback<Map<String, ThirdPartyProtocol>>) { override fun getThirdPartyProtocol(callback: MatrixCallback<Map<String, ThirdPartyProtocol>>) {
getThirdPartyProtocolsTask getThirdPartyProtocolsTask
.configureWith(Unit) .toConfigurableTask()
.dispatchTo(callback) .dispatchTo(callback)
.executeBy(taskExecutor) .executeBy(taskExecutor)
} }

View File

@ -27,6 +27,7 @@ import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary
import im.vector.matrix.android.api.session.room.model.message.MessageType import im.vector.matrix.android.api.session.room.model.message.MessageType
import im.vector.matrix.android.api.session.room.model.relation.RelationService import im.vector.matrix.android.api.session.room.model.relation.RelationService
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.internal.database.RealmLiveData import im.vector.matrix.android.internal.database.RealmLiveData
import im.vector.matrix.android.internal.database.helper.addSendingEvent import im.vector.matrix.android.internal.database.helper.addSendingEvent
@ -127,8 +128,8 @@ internal class DefaultRelationService @Inject constructor(private val context: C


} }


override fun replyToMessage(eventReplied: Event, replyText: String): Cancelable? { override fun replyToMessage(eventReplied: TimelineEvent, replyText: String, autoMarkdown: Boolean): Cancelable? {
val event = eventFactory.createReplyTextEvent(roomId, eventReplied, replyText)?.also { val event = eventFactory.createReplyTextEvent(roomId, eventReplied, replyText, autoMarkdown)?.also {
saveLocalEcho(it) saveLocalEcho(it)
} ?: return null } ?: return null



View File

@ -59,7 +59,7 @@ internal class DefaultSendService @Inject constructor(private val context: Conte
} }


override fun sendFormattedTextMessage(text: String, formattedText: String): Cancelable { override fun sendFormattedTextMessage(text: String, formattedText: String): Cancelable {
val event = localEchoEventFactory.createFormattedTextEvent(roomId, text, formattedText).also { val event = localEchoEventFactory.createFormattedTextEvent(roomId, TextContent(text, formattedText)).also {
saveLocalEcho(it) saveLocalEcho(it)
} }



View File

@ -28,6 +28,8 @@ import im.vector.matrix.android.api.session.room.model.relation.ReactionContent
import im.vector.matrix.android.api.session.room.model.relation.ReactionInfo import im.vector.matrix.android.api.session.room.model.relation.ReactionInfo
import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent
import im.vector.matrix.android.api.session.room.model.relation.ReplyToContent import im.vector.matrix.android.api.session.room.model.relation.ReplyToContent
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent
import im.vector.matrix.android.internal.database.helper.addSendingEvent import im.vector.matrix.android.internal.database.helper.addSendingEvent
import im.vector.matrix.android.internal.database.model.RoomEntity import im.vector.matrix.android.internal.database.model.RoomEntity
import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.database.query.where
@ -52,68 +54,54 @@ import javax.inject.Inject
internal class LocalEchoEventFactory @Inject constructor(private val credentials: Credentials, internal class LocalEchoEventFactory @Inject constructor(private val credentials: Credentials,
private val stringProvider: StringProvider, private val stringProvider: StringProvider,
private val roomSummaryUpdater: RoomSummaryUpdater) { private val roomSummaryUpdater: RoomSummaryUpdater) {
// TODO Inject
private val parser = Parser.builder().build()
// TODO Inject
private val renderer = HtmlRenderer.builder().build()


fun createTextEvent(roomId: String, msgType: String, text: String, autoMarkdown: Boolean): Event { fun createTextEvent(roomId: String, msgType: String, text: String, autoMarkdown: Boolean): Event {
if (autoMarkdown && msgType == MessageType.MSGTYPE_TEXT) { if (msgType == MessageType.MSGTYPE_TEXT) {
val parser = Parser.builder().build() return createFormattedTextEvent(roomId, createTextContent(text, autoMarkdown))
val document = parser.parse(text)
val renderer = HtmlRenderer.builder().build()
val htmlText = renderer.render(document)
if (isFormattedTextPertinent(text, htmlText)) { //FIXME
return createFormattedTextEvent(roomId, text, htmlText)
}
} }
val content = MessageTextContent(type = msgType, body = text) val content = MessageTextContent(type = msgType, body = text)
return createEvent(roomId, content) return createEvent(roomId, content)
} }


private fun createTextContent(text: String, autoMarkdown: Boolean): TextContent {
if (autoMarkdown) {
val document = parser.parse(text)
val htmlText = renderer.render(document)

if (isFormattedTextPertinent(text, htmlText)) {
return TextContent(text, htmlText)
}
}

return TextContent(text)
}

private fun isFormattedTextPertinent(text: String, htmlText: String?) = private fun isFormattedTextPertinent(text: String, htmlText: String?) =
text != htmlText && htmlText != "<p>${text.trim()}</p>\n" text != htmlText && htmlText != "<p>${text.trim()}</p>\n"


fun createFormattedTextEvent(roomId: String, text: String, formattedText: String): Event { fun createFormattedTextEvent(roomId: String, textContent: TextContent): Event {
val content = MessageTextContent( return createEvent(roomId, textContent.toMessageTextContent())
type = MessageType.MSGTYPE_TEXT,
format = MessageType.FORMAT_MATRIX_HTML,
body = text,
formattedBody = formattedText
)
return createEvent(roomId, content)
} }



fun createReplaceTextEvent(roomId: String, fun createReplaceTextEvent(roomId: String,
targetEventId: String, targetEventId: String,
newBodyText: String, newBodyText: String,
newBodyAutoMarkdown: Boolean, newBodyAutoMarkdown: Boolean,
msgType: String, msgType: String,
compatibilityText: String): Event { compatibilityText: String): Event {

return createEvent(roomId,
var newContent = MessageTextContent( MessageTextContent(
type = MessageType.MSGTYPE_TEXT,
body = newBodyText
)
if (newBodyAutoMarkdown) {
val parser = Parser.builder().build()
val document = parser.parse(newBodyText)
val renderer = HtmlRenderer.builder().build()
val htmlText = renderer.render(document)
if (isFormattedTextPertinent(newBodyText, htmlText)) {
newContent = MessageTextContent(
type = MessageType.MSGTYPE_TEXT,
format = MessageType.FORMAT_MATRIX_HTML,
body = newBodyText,
formattedBody = htmlText
)
}
}

val content = MessageTextContent(
type = msgType, type = msgType,
body = compatibilityText, body = compatibilityText,
relatesTo = RelationDefaultContent(RelationType.REPLACE, targetEventId), relatesTo = RelationDefaultContent(RelationType.REPLACE, targetEventId),
newContent = newContent.toContent() newContent = createTextContent(newBodyText, newBodyAutoMarkdown)
) .toMessageTextContent()
return createEvent(roomId, content) .toContent()
))
} }


fun createMediaEvent(roomId: String, attachment: ContentAttachmentData): Event { fun createMediaEvent(roomId: String, attachment: ContentAttachmentData): Event {
@ -202,7 +190,7 @@ internal class LocalEchoEventFactory @Inject constructor(private val credentials
type = MessageType.MSGTYPE_AUDIO, type = MessageType.MSGTYPE_AUDIO,
body = attachment.name ?: "audio", body = attachment.name ?: "audio",
audioInfo = AudioInfo( audioInfo = AudioInfo(
mimeType = attachment.mimeType ?: "audio/mpeg", mimeType = attachment.mimeType.takeIf { it.isNotBlank() } ?: "audio/mpeg",
size = attachment.size size = attachment.size
), ),
url = attachment.path url = attachment.path
@ -215,7 +203,8 @@ internal class LocalEchoEventFactory @Inject constructor(private val credentials
type = MessageType.MSGTYPE_FILE, type = MessageType.MSGTYPE_FILE,
body = attachment.name ?: "file", body = attachment.name ?: "file",
info = FileInfo( info = FileInfo(
mimeType = attachment.mimeType ?: "application/octet-stream", mimeType = attachment.mimeType.takeIf { it.isNotBlank() }
?: "application/octet-stream",
size = attachment.size size = attachment.size
), ),
url = attachment.path url = attachment.path
@ -244,11 +233,11 @@ internal class LocalEchoEventFactory @Inject constructor(private val credentials
return "$LOCAL_ID_PREFIX${UUID.randomUUID()}" return "$LOCAL_ID_PREFIX${UUID.randomUUID()}"
} }


fun createReplyTextEvent(roomId: String, eventReplied: Event, replyText: String): Event? { fun createReplyTextEvent(roomId: String, eventReplied: TimelineEvent, replyText: String, autoMarkdown: Boolean): Event? {
//Fallbacks and event representation //Fallbacks and event representation
//TODO Add error/warning logs when any of this is null //TODO Add error/warning logs when any of this is null
val permalink = PermalinkFactory.createPermalink(eventReplied) ?: return null val permalink = PermalinkFactory.createPermalink(eventReplied.root) ?: return null
val userId = eventReplied.senderId ?: return null val userId = eventReplied.root.senderId ?: return null
val userLink = PermalinkFactory.createPermalink(userId) ?: return null val userLink = PermalinkFactory.createPermalink(userId) ?: return null
// <mx-reply> // <mx-reply>
// <blockquote> // <blockquote>
@ -259,67 +248,66 @@ internal class LocalEchoEventFactory @Inject constructor(private val credentials
// </blockquote> // </blockquote>
// </mx-reply> // </mx-reply>
// This is where the reply goes. // This is where the reply goes.
val body = bodyForReply(eventReplied.getClearContent().toModel<MessageContent>()) val body = bodyForReply(eventReplied.getLastMessageContent())
val replyFallbackTemplateFormatted = """<mx-reply> val replyFormatted = REPLY_PATTERN.format(
<blockquote> permalink,
<a href="%s">${stringProvider.getString(R.string.message_reply_to_prefix)}</a> stringProvider.getString(R.string.message_reply_to_prefix),
<a href="%s">%s</a> userLink,
<br /> userId,
%s body.takeFormatted(),
</blockquote> createTextContent(replyText, autoMarkdown).takeFormatted()
</mx-reply> )
%s""".trimIndent().format(permalink, userLink, userId, body.second ?: body.first, replyText)
// //
// > <@alice:example.org> This is the original body // > <@alice:example.org> This is the original body
// //
// This is where the reply goes val lines = body.text.split("\n")
val lines = body.first.split("\n") val replyFallback = StringBuffer("><$userId>")
val plainTextBody = StringBuffer("><${userId}>")
lines.firstOrNull()?.also { plainTextBody.append(" $it") }
lines.forEachIndexed { index, s -> lines.forEachIndexed { index, s ->
if (index > 0) { if (index == 0) {
plainTextBody.append("\n>$s") replyFallback.append(" $s")
} else {
replyFallback.append("\n>$s")
} }
} }
plainTextBody.append("\n\n").append(replyText) replyFallback.append("\n\n").append(replyText)


val eventId = eventReplied.eventId ?: return null val eventId = eventReplied.root.eventId ?: return null
val content = MessageTextContent( val content = MessageTextContent(
type = MessageType.MSGTYPE_TEXT, type = MessageType.MSGTYPE_TEXT,
format = MessageType.FORMAT_MATRIX_HTML, format = MessageType.FORMAT_MATRIX_HTML,
body = plainTextBody.toString(), body = replyFallback.toString(),
formattedBody = replyFallbackTemplateFormatted, formattedBody = replyFormatted,
relatesTo = RelationDefaultContent(null, null, ReplyToContent(eventId)) relatesTo = RelationDefaultContent(null, null, ReplyToContent(eventId))
) )
return createEvent(roomId, content) return createEvent(roomId, content)
} }


/** /**
* Returns a pair of <Plain Text, Formatted Text?> used for the fallback event representation * Returns a TextContent used for the fallback event representation in a reply message.
* in a reply message.
*/ */
private fun bodyForReply(content: MessageContent?): Pair<String, String?> { private fun bodyForReply(content: MessageContent?): TextContent {
when (content?.type) { when (content?.type) {
MessageType.MSGTYPE_EMOTE, MessageType.MSGTYPE_EMOTE,
MessageType.MSGTYPE_TEXT, MessageType.MSGTYPE_TEXT,
MessageType.MSGTYPE_NOTICE -> { MessageType.MSGTYPE_NOTICE -> {
//If we already have formatted body, return it?
var formattedText: String? = null var formattedText: String? = null
if (content is MessageTextContent) { if (content is MessageTextContent) {
if (content.format == MessageType.FORMAT_MATRIX_HTML) { if (content.format == MessageType.FORMAT_MATRIX_HTML) {
formattedText = content.formattedBody formattedText = content.formattedBody
} }
} }
return content.body to formattedText val isReply = content.relatesTo?.inReplyTo?.eventId != null
return if (isReply)
TextContent(content.body, formattedText).removeInReplyFallbacks()
else
TextContent(content.body, formattedText)
} }
MessageType.MSGTYPE_FILE -> return stringProvider.getString(R.string.reply_to_a_file) to null MessageType.MSGTYPE_FILE -> return TextContent(stringProvider.getString(R.string.reply_to_a_file))
MessageType.MSGTYPE_AUDIO -> return stringProvider.getString(R.string.reply_to_an_audio_file) to null MessageType.MSGTYPE_AUDIO -> return TextContent(stringProvider.getString(R.string.reply_to_an_audio_file))
MessageType.MSGTYPE_IMAGE -> return stringProvider.getString(R.string.reply_to_an_image) to null MessageType.MSGTYPE_IMAGE -> return TextContent(stringProvider.getString(R.string.reply_to_an_image))
MessageType.MSGTYPE_VIDEO -> return stringProvider.getString(R.string.reply_to_a_video) to null MessageType.MSGTYPE_VIDEO -> return TextContent(stringProvider.getString(R.string.reply_to_a_video))
else -> return (content?.body ?: "") to null else -> return TextContent(content?.body ?: "")

} }

} }


/* /*
@ -365,6 +353,9 @@ internal class LocalEchoEventFactory @Inject constructor(private val credentials
companion object { companion object {
const val LOCAL_ID_PREFIX = "local." const val LOCAL_ID_PREFIX = "local."


// No whitespace
const val REPLY_PATTERN = """<mx-reply><blockquote><a href="%s">%s</a><a href="%s">%s</a><br />%s</blockquote></mx-reply>%s"""

fun isLocalEchoId(eventId: String): Boolean = eventId.startsWith(LOCAL_ID_PREFIX) fun isLocalEchoId(eventId: String): Boolean = eventId.startsWith(LOCAL_ID_PREFIX)
} }
} }

View File

@ -0,0 +1,74 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package im.vector.matrix.android.internal.session.room.send

import im.vector.matrix.android.api.session.room.model.message.MessageTextContent
import im.vector.matrix.android.api.session.room.model.message.MessageType

/**
* Contains a text and eventually a formatted text
*/
data class TextContent(
val text: String,

val formattedText: String? = null
) {
fun takeFormatted() = formattedText ?: text
}


fun TextContent.toMessageTextContent(): MessageTextContent {
return MessageTextContent(
type = MessageType.MSGTYPE_TEXT,
format = MessageType.FORMAT_MATRIX_HTML.takeIf { formattedText != null },
body = text,
formattedBody = formattedText
)
}

fun TextContent.removeInReplyFallbacks(): TextContent {
return copy(
text = extractUsefulTextFromReply(this.text),
formattedText = this.formattedText?.let { extractUsefulTextFromHtmlReply(it) }
)
}

private fun extractUsefulTextFromReply(repliedBody: String): String {
val lines = repliedBody.lines()
var wellFormed = repliedBody.startsWith(">")
var endOfPreviousFound = false
val usefullines = ArrayList<String>()
lines.forEach {
if (it == "") {
endOfPreviousFound = true
return@forEach
}
if (!endOfPreviousFound) {
wellFormed = wellFormed && it.startsWith(">")
} else {
usefullines.add(it)
}
}
return usefullines.joinToString("\n").takeIf { wellFormed } ?: repliedBody
}

private fun extractUsefulTextFromHtmlReply(repliedBody: String): String {
if (repliedBody.startsWith("<mx-reply>")) {
return repliedBody.substring(repliedBody.lastIndexOf("</mx-reply>") + "</mx-reply>".length).trim()
}
return repliedBody
}

View File

@ -19,7 +19,7 @@ package im.vector.matrix.android.internal.session.signout
import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.signout.SignOutService import im.vector.matrix.android.api.session.signout.SignOutService
import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith import im.vector.matrix.android.internal.task.toConfigurableTask
import javax.inject.Inject import javax.inject.Inject


internal class DefaultSignOutService @Inject constructor(private val signOutTask: SignOutTask, internal class DefaultSignOutService @Inject constructor(private val signOutTask: SignOutTask,
@ -27,7 +27,7 @@ internal class DefaultSignOutService @Inject constructor(private val signOutTask


override fun signOut(callback: MatrixCallback<Unit>) { override fun signOut(callback: MatrixCallback<Unit>) {
signOutTask signOutTask
.configureWith(Unit) .toConfigurableTask()
.dispatchTo(callback) .dispatchTo(callback)
.executeBy(taskExecutor) .executeBy(taskExecutor)
} }

View File

@ -24,6 +24,13 @@ internal fun <PARAMS, RESULT> Task<PARAMS, RESULT>.configureWith(params: PARAMS)
return ConfigurableTask(this, params) return ConfigurableTask(this, params)
} }


/**
* Convert a Task to a ConfigurableTask without parameter
*/
internal fun <RESULT> Task<Unit, RESULT>.toConfigurableTask(): ConfigurableTask<Unit, RESULT> {
return ConfigurableTask(this, Unit)
}

internal data class ConfigurableTask<PARAMS, RESULT>( internal data class ConfigurableTask<PARAMS, RESULT>(
val task: Task<PARAMS, RESULT>, val task: Task<PARAMS, RESULT>,
val params: PARAMS, val params: PARAMS,

View File

@ -38,7 +38,6 @@ import org.junit.Test


class PushrulesConditionTest { class PushrulesConditionTest {



@Test @Test
fun test_eventmatch_type_condition() { fun test_eventmatch_type_condition() {
val condition = EventMatchCondition("type", "m.room.message") val condition = EventMatchCondition("type", "m.room.message")
@ -286,7 +285,7 @@ class PushrulesConditionTest {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates. TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
} }


override fun replyToMessage(eventReplied: Event, replyText: String): Cancelable? { override fun replyToMessage(eventReplied: TimelineEvent, replyText: String, autoMarkdown: Boolean): Cancelable? {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates. TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
} }



View File

@ -33,6 +33,7 @@ import android.view.View
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
import android.widget.TextView import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.annotation.DrawableRes
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.lifecycle.ViewModelProviders import androidx.lifecycle.ViewModelProviders
@ -53,11 +54,11 @@ import com.otaliastudios.autocomplete.AutocompleteCallback
import com.otaliastudios.autocomplete.CharPolicy import com.otaliastudios.autocomplete.CharPolicy
import im.vector.matrix.android.api.permalinks.PermalinkFactory import im.vector.matrix.android.api.permalinks.PermalinkFactory
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.EditAggregatedSummary import im.vector.matrix.android.api.session.room.model.EditAggregatedSummary
import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.message.* import im.vector.matrix.android.api.session.room.model.message.*
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent
import im.vector.matrix.android.api.session.user.model.User import im.vector.matrix.android.api.session.user.model.User
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.di.ScreenComponent
@ -90,6 +91,7 @@ import im.vector.riotx.features.home.room.detail.timeline.action.MessageMenuView
import im.vector.riotx.features.home.room.detail.timeline.action.ViewReactionBottomSheet import im.vector.riotx.features.home.room.detail.timeline.action.ViewReactionBottomSheet
import im.vector.riotx.features.home.room.detail.timeline.helper.EndlessRecyclerViewScrollListener import im.vector.riotx.features.home.room.detail.timeline.helper.EndlessRecyclerViewScrollListener
import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
import im.vector.riotx.features.html.EventHtmlRenderer
import im.vector.riotx.features.html.PillImageSpan import im.vector.riotx.features.html.PillImageSpan
import im.vector.riotx.features.invite.VectorInviteView import im.vector.riotx.features.invite.VectorInviteView
import im.vector.riotx.features.media.ImageContentRenderer import im.vector.riotx.features.media.ImageContentRenderer
@ -104,8 +106,6 @@ import kotlinx.android.parcel.Parcelize
import kotlinx.android.synthetic.main.fragment_room_detail.* import kotlinx.android.synthetic.main.fragment_room_detail.*
import kotlinx.android.synthetic.main.merge_composer_layout.view.* import kotlinx.android.synthetic.main.merge_composer_layout.view.*
import org.commonmark.parser.Parser import org.commonmark.parser.Parser
import ru.noties.markwon.Markwon
import ru.noties.markwon.html.HtmlPlugin
import timber.log.Timber import timber.log.Timber
import java.io.File import java.io.File
import javax.inject.Inject import javax.inject.Inject
@ -178,6 +178,7 @@ class RoomDetailFragment :
@Inject lateinit var errorFormatter: ErrorFormatter @Inject lateinit var errorFormatter: ErrorFormatter
private lateinit var scrollOnNewMessageCallback: ScrollOnNewMessageCallback private lateinit var scrollOnNewMessageCallback: ScrollOnNewMessageCallback
private lateinit var scrollOnHighlightedEventCallback: ScrollOnHighlightedEventCallback private lateinit var scrollOnHighlightedEventCallback: ScrollOnHighlightedEventCallback
@Inject lateinit var eventHtmlRenderer: EventHtmlRenderer




override fun getLayoutResId() = R.layout.fragment_room_detail override fun getLayoutResId() = R.layout.fragment_room_detail
@ -225,61 +226,43 @@ class RoomDetailFragment :
} }
} }


roomDetailViewModel.selectSubscribe( roomDetailViewModel.selectSubscribe(RoomDetailViewState::sendMode) { mode ->
RoomDetailViewState::sendMode,
RoomDetailViewState::selectedEvent,
RoomDetailViewState::roomId) { mode, event, roomId ->
when (mode) { when (mode) {
SendMode.REGULAR -> { SendMode.REGULAR -> exitSpecialMode()
is SendMode.EDIT -> enterSpecialMode(mode.timelineEvent, R.drawable.ic_edit, true)
is SendMode.QUOTE -> enterSpecialMode(mode.timelineEvent, R.drawable.ic_quote, false)
is SendMode.REPLY -> enterSpecialMode(mode.timelineEvent, R.drawable.ic_reply, false)
}
}
}

private fun exitSpecialMode() {
commandAutocompletePolicy.enabled = true commandAutocompletePolicy.enabled = true
val uid = session.sessionParams.credentials.userId
val meMember = session.getRoom(roomId)?.getRoomMember(uid)
avatarRenderer.render(meMember?.avatarUrl, uid, meMember?.displayName, composerLayout.composerAvatarImageView)
composerLayout.collapse() composerLayout.collapse()
} }
SendMode.EDIT,
SendMode.QUOTE, private fun enterSpecialMode(event: TimelineEvent, @DrawableRes iconRes: Int, useText: Boolean) {
SendMode.REPLY -> {
commandAutocompletePolicy.enabled = false commandAutocompletePolicy.enabled = false
if (event == null) {
//we should ignore? can this happen?
Timber.e("Enter edit mode with no event selected")
return@selectSubscribe
}
//switch to expanded bar //switch to expanded bar
composerLayout.composerRelatedMessageTitle.apply { composerLayout.composerRelatedMessageTitle.apply {
text = event.getDisambiguatedDisplayName() text = event.getDisambiguatedDisplayName()
setTextColor(ContextCompat.getColor(requireContext(), getColorFromUserId(event.root.senderId))) setTextColor(ContextCompat.getColor(requireContext(), getColorFromUserId(event.root.senderId)))
} }


//TODO this is used at several places, find way to refactor? val messageContent: MessageContent? = event.getLastMessageContent()
val messageContent: MessageContent? =
event.annotations?.editSummary?.aggregatedContent?.toModel()
?: event.root.getClearContent().toModel()
val nonFormattedBody = messageContent?.body ?: "" val nonFormattedBody = messageContent?.body ?: ""
var formattedBody: CharSequence? = null var formattedBody: CharSequence? = null
if (messageContent is MessageTextContent && messageContent.format == MessageType.FORMAT_MATRIX_HTML) { if (messageContent is MessageTextContent && messageContent.format == MessageType.FORMAT_MATRIX_HTML) {
val parser = Parser.builder().build() val parser = Parser.builder().build()
val document = parser.parse(messageContent.formattedBody val document = parser.parse(messageContent.formattedBody
?: messageContent.body) ?: messageContent.body)
formattedBody = Markwon.builder(requireContext()) formattedBody = eventHtmlRenderer.render(document)
.usePlugin(HtmlPlugin.create()).build().render(document)
} }
composerLayout.composerRelatedMessageContent.text = formattedBody composerLayout.composerRelatedMessageContent.text = formattedBody
?: nonFormattedBody ?: nonFormattedBody



composerLayout.composerEditText.setText(if (useText) nonFormattedBody else "")
if (mode == SendMode.EDIT) { composerLayout.composerRelatedMessageActionIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), iconRes))
//TODO if it's a reply we should trim the top part of message
composerLayout.composerEditText.setText(nonFormattedBody)
composerLayout.composerRelatedMessageActionIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_edit))
} else if (mode == SendMode.QUOTE) {
composerLayout.composerEditText.setText("")
composerLayout.composerRelatedMessageActionIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_quote))
} else if (mode == SendMode.REPLY) {
composerLayout.composerEditText.setText("")
composerLayout.composerRelatedMessageActionIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_reply))
}


avatarRenderer.render(event.senderAvatar, event.root.senderId avatarRenderer.render(event.senderAvatar, event.root.senderId
?: "", event.senderName, composerLayout.composerRelatedMessageAvatar) ?: "", event.senderName, composerLayout.composerRelatedMessageAvatar)
@ -288,14 +271,6 @@ class RoomDetailFragment :
composerLayout.expand { composerLayout.expand {
focusComposerAndShowKeyboard() focusComposerAndShowKeyboard()
} }
composerLayout.composerRelatedMessageCloseButton.setOnClickListener {
composerLayout.composerEditText.setText("")
roomDetailViewModel.resetSendMode()
}

}
}
}
} }


override fun onResume() { override fun onResume() {
@ -422,6 +397,10 @@ class RoomDetailFragment :
roomDetailViewModel.process(RoomDetailActions.SendMessage(textMessage, VectorPreferences.isMarkdownEnabled(requireContext()))) roomDetailViewModel.process(RoomDetailActions.SendMessage(textMessage, VectorPreferences.isMarkdownEnabled(requireContext())))
} }
} }
composerLayout.composerRelatedMessageCloseButton.setOnClickListener {
composerLayout.composerEditText.setText("")
roomDetailViewModel.resetSendMode()
}
} }


private fun setupAttachmentButton() { private fun setupAttachmentButton() {

View File

@ -47,8 +47,6 @@ import im.vector.riotx.core.utils.LiveEvent
import im.vector.riotx.features.command.CommandParser import im.vector.riotx.features.command.CommandParser
import im.vector.riotx.features.command.ParsedCommand import im.vector.riotx.features.command.ParsedCommand
import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineDisplayableEvents import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineDisplayableEvents
import io.reactivex.Observable
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.rxkotlin.subscribeBy import io.reactivex.rxkotlin.subscribeBy
import org.commonmark.parser.Parser import org.commonmark.parser.Parser
import org.commonmark.renderer.html.HtmlRenderer import org.commonmark.renderer.html.HtmlRenderer
@ -126,11 +124,10 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
} }
} }


fun enterEditMode(event: TimelineEvent) { private fun enterEditMode(event: TimelineEvent) {
setState { setState {
copy( copy(
sendMode = SendMode.EDIT, sendMode = SendMode.EDIT(event)
selectedEvent = event
) )
} }
} }
@ -138,8 +135,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
fun resetSendMode() { fun resetSendMode() {
setState { setState {
copy( copy(
sendMode = SendMode.REGULAR, sendMode = SendMode.REGULAR
selectedEvent = null
) )
} }
} }
@ -233,21 +229,29 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
} }
} }
} }
SendMode.EDIT -> { is SendMode.EDIT -> {
room.editTextMessage(state.selectedEvent?.root?.eventId val messageContent: MessageContent? =
state.sendMode.timelineEvent.annotations?.editSummary?.aggregatedContent.toModel()
?: state.sendMode.timelineEvent.root.getClearContent().toModel()
val nonFormattedBody = messageContent?.body ?: ""

if (nonFormattedBody != action.text) {
room.editTextMessage(state.sendMode.timelineEvent.root.eventId
?: "", action.text, action.autoMarkdown) ?: "", action.text, action.autoMarkdown)
} else {
Timber.w("Same message content, do not send edition")
}
setState { setState {
copy( copy(
sendMode = SendMode.REGULAR, sendMode = SendMode.REGULAR
selectedEvent = null
) )
} }
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.MessageSent)) _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.MessageSent))
} }
SendMode.QUOTE -> { is SendMode.QUOTE -> {
val messageContent: MessageContent? = val messageContent: MessageContent? =
state.selectedEvent?.annotations?.editSummary?.aggregatedContent?.toModel() state.sendMode.timelineEvent.annotations?.editSummary?.aggregatedContent.toModel()
?: state.selectedEvent?.root?.getClearContent().toModel() ?: state.sendMode.timelineEvent.root.getClearContent().toModel()
val textMsg = messageContent?.body val textMsg = messageContent?.body


val finalText = legacyRiotQuoteText(textMsg, action.text) val finalText = legacyRiotQuoteText(textMsg, action.text)
@ -264,19 +268,17 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
} }
setState { setState {
copy( copy(
sendMode = SendMode.REGULAR, sendMode = SendMode.REGULAR
selectedEvent = null
) )
} }
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.MessageSent)) _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.MessageSent))
} }
SendMode.REPLY -> { is SendMode.REPLY -> {
state.selectedEvent?.let { state.sendMode.timelineEvent.let {
room.replyToMessage(it.root, action.text) room.replyToMessage(it, action.text, action.autoMarkdown)
setState { setState {
copy( copy(
sendMode = SendMode.REGULAR, sendMode = SendMode.REGULAR
selectedEvent = null
) )
} }
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.MessageSent)) _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.MessageSent))
@ -427,8 +429,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
room.getTimeLineEvent(action.eventId)?.let { room.getTimeLineEvent(action.eventId)?.let {
setState { setState {
copy( copy(
sendMode = SendMode.QUOTE, sendMode = SendMode.QUOTE(it)
selectedEvent = it
) )
} }
} }
@ -438,8 +439,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
room.getTimeLineEvent(action.eventId)?.let { room.getTimeLineEvent(action.eventId)?.let {
setState { setState {
copy( copy(
sendMode = SendMode.REPLY, sendMode = SendMode.REPLY(it)
selectedEvent = it
) )
} }
} }

View File

@ -32,11 +32,11 @@ import im.vector.matrix.android.api.session.user.model.User
* *
* Depending on the state the bottom toolbar will change (icons/preview/actions...) * Depending on the state the bottom toolbar will change (icons/preview/actions...)
*/ */
enum class SendMode { sealed class SendMode {
REGULAR, object REGULAR : SendMode()
QUOTE, data class QUOTE(val timelineEvent: TimelineEvent) : SendMode()
EDIT, data class EDIT(val timelineEvent: TimelineEvent) : SendMode()
REPLY data class REPLY(val timelineEvent: TimelineEvent) : SendMode()
} }


data class RoomDetailViewState( data class RoomDetailViewState(
@ -46,7 +46,6 @@ data class RoomDetailViewState(
val asyncInviter: Async<User> = Uninitialized, val asyncInviter: Async<User> = Uninitialized,
val asyncRoomSummary: Async<RoomSummary> = Uninitialized, val asyncRoomSummary: Async<RoomSummary> = Uninitialized,
val sendMode: SendMode = SendMode.REGULAR, val sendMode: SendMode = SendMode.REGULAR,
val selectedEvent: TimelineEvent? = null,
val isEncrypted: Boolean = false val isEncrypted: Boolean = false
) : MvRxState { ) : MvRxState {



View File

@ -21,11 +21,11 @@ import com.squareup.inject.assisted.AssistedInject
import dagger.Lazy import dagger.Lazy
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.message.MessageContent import im.vector.matrix.android.api.session.room.model.message.MessageContent
import im.vector.matrix.android.api.session.room.model.message.MessageTextContent import im.vector.matrix.android.api.session.room.model.message.MessageTextContent
import im.vector.matrix.android.api.session.room.model.message.MessageType import im.vector.matrix.android.api.session.room.model.message.MessageType
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent
import im.vector.matrix.rx.RxRoom import im.vector.matrix.rx.RxRoom
import im.vector.riotx.core.extensions.canReact import im.vector.riotx.core.extensions.canReact
import im.vector.riotx.core.platform.VectorViewModel import im.vector.riotx.core.platform.VectorViewModel
@ -58,8 +58,7 @@ data class MessageActionState(
fun messageBody(eventHtmlRenderer: EventHtmlRenderer?, noticeEventFormatter: NoticeEventFormatter?): CharSequence? { fun messageBody(eventHtmlRenderer: EventHtmlRenderer?, noticeEventFormatter: NoticeEventFormatter?): CharSequence? {
return when (timelineEvent()?.root?.getClearType()) { return when (timelineEvent()?.root?.getClearType()) {
EventType.MESSAGE -> { EventType.MESSAGE -> {
val messageContent: MessageContent? = timelineEvent()?.annotations?.editSummary?.aggregatedContent?.toModel() val messageContent: MessageContent? = timelineEvent()?.getLastMessageContent()
?: timelineEvent()?.root?.getClearContent().toModel()
if (messageContent is MessageTextContent && messageContent.format == MessageType.FORMAT_MATRIX_HTML) { if (messageContent is MessageTextContent && messageContent.format == MessageType.FORMAT_MATRIX_HTML) {
eventHtmlRenderer?.render(messageContent.formattedBody eventHtmlRenderer?.render(messageContent.formattedBody
?: messageContent.body) ?: messageContent.body)

View File

@ -27,11 +27,11 @@ import dagger.Lazy
import im.vector.matrix.android.api.permalinks.MatrixLinkify import im.vector.matrix.android.api.permalinks.MatrixLinkify
import im.vector.matrix.android.api.permalinks.MatrixPermalinkSpan import im.vector.matrix.android.api.permalinks.MatrixPermalinkSpan
import im.vector.matrix.android.api.session.events.model.RelationType import im.vector.matrix.android.api.session.events.model.RelationType
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.EditAggregatedSummary import im.vector.matrix.android.api.session.room.model.EditAggregatedSummary
import im.vector.matrix.android.api.session.room.model.message.* import im.vector.matrix.android.api.session.room.model.message.*
import im.vector.matrix.android.api.session.room.send.SendState import im.vector.matrix.android.api.session.room.send.SendState
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent
import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt
import im.vector.riotx.EmojiCompatFontProvider import im.vector.riotx.EmojiCompatFontProvider
import im.vector.riotx.R import im.vector.riotx.R
@ -79,8 +79,7 @@ class MessageItemFactory @Inject constructor(
} }


val messageContent: MessageContent = val messageContent: MessageContent =
event.annotations?.editSummary?.aggregatedContent?.toModel() event.getLastMessageContent()
?: event.root.getClearContent().toModel()
?: //Malformed content, we should echo something on screen ?: //Malformed content, we should echo something on screen
return DefaultItem_().text(stringProvider.getString(R.string.malformed_message)) return DefaultItem_().text(stringProvider.getString(R.string.malformed_message))


@ -311,8 +310,7 @@ class MessageItemFactory @Inject constructor(
editSummary: EditAggregatedSummary?): SpannableStringBuilder { editSummary: EditAggregatedSummary?): SpannableStringBuilder {
val spannable = SpannableStringBuilder() val spannable = SpannableStringBuilder()
spannable.append(linkifiedBody) spannable.append(linkifiedBody)
// TODO i18n val editedSuffix = stringProvider.getString(R.string.edited_suffix)
val editedSuffix = "(edited)"
spannable.append(" ").append(editedSuffix) spannable.append(" ").append(editedSuffix)
val color = colorProvider.getColorFromAttribute(R.attr.vctr_list_header_secondary_text_color) val color = colorProvider.getColorFromAttribute(R.attr.vctr_list_header_secondary_text_color)
val editStart = spannable.indexOf(editedSuffix) val editStart = spannable.indexOf(editedSuffix)

View File

@ -50,6 +50,10 @@ class EventHtmlRenderer @Inject constructor(context: Context,
return markwon.toMarkdown(text) return markwon.toMarkdown(text)
} }


fun render(node: Node) : CharSequence {
return markwon.render(node)
}

} }


private class MatrixPlugin private constructor(private val glideRequests: GlideRequests, private class MatrixPlugin private constructor(private val glideRequests: GlideRequests,

View File

@ -18,15 +18,14 @@ package im.vector.riotx.features.notifications
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.content.ContentUrlResolver import im.vector.matrix.android.api.session.content.ContentUrlResolver
import im.vector.matrix.android.api.session.crypto.CryptoService
import im.vector.matrix.android.api.session.crypto.MXCryptoError import im.vector.matrix.android.api.session.crypto.MXCryptoError
import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.RoomMember import im.vector.matrix.android.api.session.room.model.RoomMember
import im.vector.matrix.android.api.session.room.model.message.MessageContent
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent
import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult
import im.vector.riotx.BuildConfig import im.vector.riotx.BuildConfig
import im.vector.riotx.R import im.vector.riotx.R
@ -94,8 +93,8 @@ class NotifiableEventResolver @Inject constructor(private val stringProvider: St
Timber.e("## Unable to resolve room for eventId [${event}]") Timber.e("## Unable to resolve room for eventId [${event}]")
// Ok room is not known in store, but we can still display something // Ok room is not known in store, but we can still display something
val body = val body =
event.annotations?.editSummary?.aggregatedContent?.toModel<MessageContent>()?.body event.getLastMessageContent()
?: event.root.getClearContent().toModel<MessageContent>()?.body ?.body
?: stringProvider.getString(R.string.notification_unknown_new_event) ?: stringProvider.getString(R.string.notification_unknown_new_event)
val roomName = stringProvider.getString(R.string.notification_unknown_room_name) val roomName = stringProvider.getString(R.string.notification_unknown_room_name)
val senderDisplayName = event.senderName ?: event.root.senderId val senderDisplayName = event.senderName ?: event.root.senderId
@ -129,8 +128,8 @@ class NotifiableEventResolver @Inject constructor(private val stringProvider: St
} }
} }


val body = event.annotations?.editSummary?.aggregatedContent?.toModel<MessageContent>()?.body val body = event.getLastMessageContent()
?: event.root.getClearContent().toModel<MessageContent>()?.body ?.body
?: stringProvider.getString(R.string.notification_unknown_new_event) ?: stringProvider.getString(R.string.notification_unknown_new_event)
val roomName = room.roomSummary()?.displayName ?: "" val roomName = room.roomSummary()?.displayName ?: ""
val senderDisplayName = event.senderName ?: event.root.senderId val senderDisplayName = event.senderName ?: event.root.senderId

View File

@ -14,5 +14,7 @@
<string name="downloading_file">Downloading file %1$s…</string> <string name="downloading_file">Downloading file %1$s…</string>
<string name="downloaded_file">File %1$s has been downloaded!</string> <string name="downloaded_file">File %1$s has been downloaded!</string>


<string name="edited_suffix">"(edited)"</string>



</resources> </resources>