mirror of
https://github.com/element-hq/element-android
synced 2024-11-28 13:38:49 +03:00
Merge pull request #322 from vector-im/feature/clean
Improve reply feature
This commit is contained in:
commit
98306e223b
22 changed files with 297 additions and 232 deletions
|
@ -16,8 +16,8 @@
|
|||
package im.vector.matrix.android.api.session.room.model.relation
|
||||
|
||||
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.timeline.TimelineEvent
|
||||
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
|
||||
* @param eventReplied the event referenced by the reply
|
||||
* @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>
|
||||
}
|
|
@ -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.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.message.MessageContent
|
||||
import im.vector.matrix.android.api.session.room.send.SendState
|
||||
|
||||
/**
|
||||
|
@ -80,3 +82,9 @@ data class TimelineEvent(
|
|||
return EventType.ENCRYPTED == root.type
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get last MessageContent, after a possible edition
|
||||
*/
|
||||
fun TimelineEvent.getLastMessageContent(): MessageContent? = annotations?.editSummary?.aggregatedContent?.toModel()
|
||||
?: root.getClearContent().toModel()
|
||||
|
|
|
@ -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.task.TaskExecutor
|
||||
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.MatrixCoroutineDispatchers
|
||||
import im.vector.matrix.android.internal.util.fetchCopied
|
||||
|
@ -197,7 +198,7 @@ internal class CryptoManager @Inject constructor(
|
|||
|
||||
override fun getDevicesList(callback: MatrixCallback<DevicesListResponse>) {
|
||||
getDevicesTask
|
||||
.configureWith(Unit)
|
||||
.toConfigurableTask()
|
||||
.dispatchTo(callback)
|
||||
.executeBy(taskExecutor)
|
||||
}
|
||||
|
@ -1054,7 +1055,8 @@ internal class CryptoManager @Inject constructor(
|
|||
}
|
||||
|
||||
override fun clearCryptoCache(callback: MatrixCallback<Unit>) {
|
||||
clearCryptoDataTask.configureWith(Unit)
|
||||
clearCryptoDataTask
|
||||
.toConfigurableTask()
|
||||
.dispatchTo(callback)
|
||||
.executeBy(taskExecutor)
|
||||
}
|
||||
|
|
|
@ -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.extensions.foldToCallback
|
||||
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.TaskExecutor
|
||||
import im.vector.matrix.android.internal.task.TaskThread
|
||||
|
@ -876,7 +877,7 @@ internal class KeysBackup @Inject constructor(
|
|||
|
||||
override fun getCurrentVersion(callback: MatrixCallback<KeysVersionResult?>) {
|
||||
getKeysBackupLastVersionTask
|
||||
.configureWith(Unit)
|
||||
.toConfigurableTask()
|
||||
.dispatchTo(object : MatrixCallback<KeysVersionResult> {
|
||||
override fun onSuccess(data: KeysVersionResult) {
|
||||
callback.onSuccess(data)
|
||||
|
|
|
@ -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.session.cache.CacheService
|
||||
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.configureWith
|
||||
import im.vector.matrix.android.internal.task.toConfigurableTask
|
||||
import javax.inject.Inject
|
||||
|
||||
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>) {
|
||||
clearCacheTask
|
||||
.configureWith(Unit)
|
||||
.toConfigurableTask()
|
||||
.dispatchTo(callback)
|
||||
.executeBy(taskExecutor)
|
||||
}
|
||||
|
|
|
@ -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.task.TaskExecutor
|
||||
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.matrixOneTimeWorkRequestBuilder
|
||||
import im.vector.matrix.android.internal.worker.WorkerParamsFactory
|
||||
|
@ -49,7 +50,7 @@ internal class DefaultPusherService @Inject constructor(
|
|||
|
||||
override fun refreshPushers() {
|
||||
getPusherTask
|
||||
.configureWith(Unit)
|
||||
.toConfigurableTask()
|
||||
.executeBy(taskExecutor)
|
||||
}
|
||||
|
||||
|
|
|
@ -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.task.TaskExecutor
|
||||
import im.vector.matrix.android.internal.task.configureWith
|
||||
import im.vector.matrix.android.internal.task.toConfigurableTask
|
||||
import javax.inject.Inject
|
||||
|
||||
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>>) {
|
||||
getThirdPartyProtocolsTask
|
||||
.configureWith(Unit)
|
||||
.toConfigurableTask()
|
||||
.dispatchTo(callback)
|
||||
.executeBy(taskExecutor)
|
||||
}
|
||||
|
|
|
@ -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.message.MessageType
|
||||
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.internal.database.RealmLiveData
|
||||
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? {
|
||||
val event = eventFactory.createReplyTextEvent(roomId, eventReplied, replyText)?.also {
|
||||
override fun replyToMessage(eventReplied: TimelineEvent, replyText: String, autoMarkdown: Boolean): Cancelable? {
|
||||
val event = eventFactory.createReplyTextEvent(roomId, eventReplied, replyText, autoMarkdown)?.also {
|
||||
saveLocalEcho(it)
|
||||
} ?: return null
|
||||
|
||||
|
|
|
@ -59,7 +59,7 @@ internal class DefaultSendService @Inject constructor(private val context: Conte
|
|||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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.RelationDefaultContent
|
||||
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.model.RoomEntity
|
||||
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,
|
||||
private val stringProvider: StringProvider,
|
||||
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 {
|
||||
if (autoMarkdown && msgType == MessageType.MSGTYPE_TEXT) {
|
||||
val parser = Parser.builder().build()
|
||||
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)
|
||||
}
|
||||
if (msgType == MessageType.MSGTYPE_TEXT) {
|
||||
return createFormattedTextEvent(roomId, createTextContent(text, autoMarkdown))
|
||||
}
|
||||
val content = MessageTextContent(type = msgType, body = text)
|
||||
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?) =
|
||||
text != htmlText && htmlText != "<p>${text.trim()}</p>\n"
|
||||
|
||||
fun createFormattedTextEvent(roomId: String, text: String, formattedText: String): Event {
|
||||
val content = MessageTextContent(
|
||||
type = MessageType.MSGTYPE_TEXT,
|
||||
format = MessageType.FORMAT_MATRIX_HTML,
|
||||
body = text,
|
||||
formattedBody = formattedText
|
||||
)
|
||||
return createEvent(roomId, content)
|
||||
fun createFormattedTextEvent(roomId: String, textContent: TextContent): Event {
|
||||
return createEvent(roomId, textContent.toMessageTextContent())
|
||||
}
|
||||
|
||||
|
||||
fun createReplaceTextEvent(roomId: String,
|
||||
targetEventId: String,
|
||||
newBodyText: String,
|
||||
newBodyAutoMarkdown: Boolean,
|
||||
msgType: String,
|
||||
compatibilityText: String): Event {
|
||||
|
||||
var newContent = 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,
|
||||
body = compatibilityText,
|
||||
relatesTo = RelationDefaultContent(RelationType.REPLACE, targetEventId),
|
||||
newContent = newContent.toContent()
|
||||
)
|
||||
return createEvent(roomId, content)
|
||||
return createEvent(roomId,
|
||||
MessageTextContent(
|
||||
type = msgType,
|
||||
body = compatibilityText,
|
||||
relatesTo = RelationDefaultContent(RelationType.REPLACE, targetEventId),
|
||||
newContent = createTextContent(newBodyText, newBodyAutoMarkdown)
|
||||
.toMessageTextContent()
|
||||
.toContent()
|
||||
))
|
||||
}
|
||||
|
||||
fun createMediaEvent(roomId: String, attachment: ContentAttachmentData): Event {
|
||||
|
@ -202,7 +190,7 @@ internal class LocalEchoEventFactory @Inject constructor(private val credentials
|
|||
type = MessageType.MSGTYPE_AUDIO,
|
||||
body = attachment.name ?: "audio",
|
||||
audioInfo = AudioInfo(
|
||||
mimeType = attachment.mimeType ?: "audio/mpeg",
|
||||
mimeType = attachment.mimeType.takeIf { it.isNotBlank() } ?: "audio/mpeg",
|
||||
size = attachment.size
|
||||
),
|
||||
url = attachment.path
|
||||
|
@ -215,7 +203,8 @@ internal class LocalEchoEventFactory @Inject constructor(private val credentials
|
|||
type = MessageType.MSGTYPE_FILE,
|
||||
body = attachment.name ?: "file",
|
||||
info = FileInfo(
|
||||
mimeType = attachment.mimeType ?: "application/octet-stream",
|
||||
mimeType = attachment.mimeType.takeIf { it.isNotBlank() }
|
||||
?: "application/octet-stream",
|
||||
size = attachment.size
|
||||
),
|
||||
url = attachment.path
|
||||
|
@ -244,82 +233,81 @@ internal class LocalEchoEventFactory @Inject constructor(private val credentials
|
|||
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
|
||||
//TODO Add error/warning logs when any of this is null
|
||||
val permalink = PermalinkFactory.createPermalink(eventReplied) ?: return null
|
||||
val userId = eventReplied.senderId ?: return null
|
||||
val permalink = PermalinkFactory.createPermalink(eventReplied.root) ?: return null
|
||||
val userId = eventReplied.root.senderId ?: return null
|
||||
val userLink = PermalinkFactory.createPermalink(userId) ?: return null
|
||||
// <mx-reply>
|
||||
// <blockquote>
|
||||
// <a href="https://matrix.to/#/!somewhere:domain.com/$event:domain.com">In reply to</a>
|
||||
// <a href="https://matrix.to/#/@alice:example.org">@alice:example.org</a>
|
||||
// <br />
|
||||
// <!-- This is where the related event's HTML would be. -->
|
||||
// </blockquote>
|
||||
// </mx-reply>
|
||||
// This is where the reply goes.
|
||||
val body = bodyForReply(eventReplied.getClearContent().toModel<MessageContent>())
|
||||
val replyFallbackTemplateFormatted = """<mx-reply>
|
||||
<blockquote>
|
||||
<a href="%s">${stringProvider.getString(R.string.message_reply_to_prefix)}</a>
|
||||
<a href="%s">%s</a>
|
||||
<br />
|
||||
%s
|
||||
</blockquote>
|
||||
</mx-reply>
|
||||
%s""".trimIndent().format(permalink, userLink, userId, body.second ?: body.first, replyText)
|
||||
//
|
||||
// > <@alice:example.org> This is the original body
|
||||
//
|
||||
// This is where the reply goes
|
||||
val lines = body.first.split("\n")
|
||||
val plainTextBody = StringBuffer("><${userId}>")
|
||||
lines.firstOrNull()?.also { plainTextBody.append(" $it") }
|
||||
// <mx-reply>
|
||||
// <blockquote>
|
||||
// <a href="https://matrix.to/#/!somewhere:domain.com/$event:domain.com">In reply to</a>
|
||||
// <a href="https://matrix.to/#/@alice:example.org">@alice:example.org</a>
|
||||
// <br />
|
||||
// <!-- This is where the related event's HTML would be. -->
|
||||
// </blockquote>
|
||||
// </mx-reply>
|
||||
// This is where the reply goes.
|
||||
val body = bodyForReply(eventReplied.getLastMessageContent())
|
||||
val replyFormatted = REPLY_PATTERN.format(
|
||||
permalink,
|
||||
stringProvider.getString(R.string.message_reply_to_prefix),
|
||||
userLink,
|
||||
userId,
|
||||
body.takeFormatted(),
|
||||
createTextContent(replyText, autoMarkdown).takeFormatted()
|
||||
)
|
||||
//
|
||||
// > <@alice:example.org> This is the original body
|
||||
//
|
||||
val lines = body.text.split("\n")
|
||||
val replyFallback = StringBuffer("><$userId>")
|
||||
lines.forEachIndexed { index, s ->
|
||||
if (index > 0) {
|
||||
plainTextBody.append("\n>$s")
|
||||
if (index == 0) {
|
||||
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(
|
||||
type = MessageType.MSGTYPE_TEXT,
|
||||
format = MessageType.FORMAT_MATRIX_HTML,
|
||||
body = plainTextBody.toString(),
|
||||
formattedBody = replyFallbackTemplateFormatted,
|
||||
body = replyFallback.toString(),
|
||||
formattedBody = replyFormatted,
|
||||
relatesTo = RelationDefaultContent(null, null, ReplyToContent(eventId))
|
||||
)
|
||||
return createEvent(roomId, content)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a pair of <Plain Text, Formatted Text?> used for the fallback event representation
|
||||
* in a reply message.
|
||||
* Returns a TextContent used for the fallback event representation in a reply message.
|
||||
*/
|
||||
private fun bodyForReply(content: MessageContent?): Pair<String, String?> {
|
||||
private fun bodyForReply(content: MessageContent?): TextContent {
|
||||
when (content?.type) {
|
||||
MessageType.MSGTYPE_EMOTE,
|
||||
MessageType.MSGTYPE_TEXT,
|
||||
MessageType.MSGTYPE_NOTICE -> {
|
||||
//If we already have formatted body, return it?
|
||||
var formattedText: String? = null
|
||||
if (content is MessageTextContent) {
|
||||
if (content.format == MessageType.FORMAT_MATRIX_HTML) {
|
||||
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_AUDIO -> return stringProvider.getString(R.string.reply_to_an_audio_file) to null
|
||||
MessageType.MSGTYPE_IMAGE -> return stringProvider.getString(R.string.reply_to_an_image) to null
|
||||
MessageType.MSGTYPE_VIDEO -> return stringProvider.getString(R.string.reply_to_a_video) to null
|
||||
else -> return (content?.body ?: "") to null
|
||||
|
||||
MessageType.MSGTYPE_FILE -> return TextContent(stringProvider.getString(R.string.reply_to_a_file))
|
||||
MessageType.MSGTYPE_AUDIO -> return TextContent(stringProvider.getString(R.string.reply_to_an_audio_file))
|
||||
MessageType.MSGTYPE_IMAGE -> return TextContent(stringProvider.getString(R.string.reply_to_an_image))
|
||||
MessageType.MSGTYPE_VIDEO -> return TextContent(stringProvider.getString(R.string.reply_to_a_video))
|
||||
else -> return TextContent(content?.body ?: "")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
|
@ -365,6 +353,9 @@ internal class LocalEchoEventFactory @Inject constructor(private val credentials
|
|||
companion object {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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.session.signout.SignOutService
|
||||
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
|
||||
|
||||
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>) {
|
||||
signOutTask
|
||||
.configureWith(Unit)
|
||||
.toConfigurableTask()
|
||||
.dispatchTo(callback)
|
||||
.executeBy(taskExecutor)
|
||||
}
|
||||
|
|
|
@ -21,7 +21,14 @@ import im.vector.matrix.android.api.MatrixCallback
|
|||
import im.vector.matrix.android.api.util.Cancelable
|
||||
|
||||
internal fun <PARAMS, RESULT> Task<PARAMS, RESULT>.configureWith(params: PARAMS): ConfigurableTask<PARAMS, RESULT> {
|
||||
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>(
|
||||
|
|
|
@ -38,7 +38,6 @@ import org.junit.Test
|
|||
|
||||
class PushrulesConditionTest {
|
||||
|
||||
|
||||
@Test
|
||||
fun test_eventmatch_type_condition() {
|
||||
val condition = EventMatchCondition("type", "m.room.message")
|
||||
|
@ -158,9 +157,9 @@ class PushrulesConditionTest {
|
|||
content = MessageTextContent("m.text", "A").toContent(),
|
||||
originServerTs = 0,
|
||||
roomId = "3joined").also {
|
||||
Assert.assertTrue("This room has 3 members",conditionEqual3.isSatisfied(it, session))
|
||||
Assert.assertTrue("This room has 3 members",conditionEqual3Bis.isSatisfied(it, session))
|
||||
Assert.assertFalse("This room has more than 3 members",conditionLessThan3.isSatisfied(it, session))
|
||||
Assert.assertTrue("This room has 3 members", conditionEqual3.isSatisfied(it, session))
|
||||
Assert.assertTrue("This room has 3 members", conditionEqual3Bis.isSatisfied(it, session))
|
||||
Assert.assertFalse("This room has more than 3 members", conditionLessThan3.isSatisfied(it, session))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -286,7 +285,7 @@ class PushrulesConditionTest {
|
|||
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.
|
||||
}
|
||||
|
||||
|
|
|
@ -33,6 +33,7 @@ import android.view.View
|
|||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.ViewModelProviders
|
||||
|
@ -53,11 +54,11 @@ import com.otaliastudios.autocomplete.AutocompleteCallback
|
|||
import com.otaliastudios.autocomplete.CharPolicy
|
||||
import im.vector.matrix.android.api.permalinks.PermalinkFactory
|
||||
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.Membership
|
||||
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.getLastMessageContent
|
||||
import im.vector.matrix.android.api.session.user.model.User
|
||||
import im.vector.riotx.R
|
||||
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.helper.EndlessRecyclerViewScrollListener
|
||||
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.invite.VectorInviteView
|
||||
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.merge_composer_layout.view.*
|
||||
import org.commonmark.parser.Parser
|
||||
import ru.noties.markwon.Markwon
|
||||
import ru.noties.markwon.html.HtmlPlugin
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
|
@ -178,6 +178,7 @@ class RoomDetailFragment :
|
|||
@Inject lateinit var errorFormatter: ErrorFormatter
|
||||
private lateinit var scrollOnNewMessageCallback: ScrollOnNewMessageCallback
|
||||
private lateinit var scrollOnHighlightedEventCallback: ScrollOnHighlightedEventCallback
|
||||
@Inject lateinit var eventHtmlRenderer: EventHtmlRenderer
|
||||
|
||||
|
||||
override fun getLayoutResId() = R.layout.fragment_room_detail
|
||||
|
@ -225,79 +226,53 @@ class RoomDetailFragment :
|
|||
}
|
||||
}
|
||||
|
||||
roomDetailViewModel.selectSubscribe(
|
||||
RoomDetailViewState::sendMode,
|
||||
RoomDetailViewState::selectedEvent,
|
||||
RoomDetailViewState::roomId) { mode, event, roomId ->
|
||||
roomDetailViewModel.selectSubscribe(RoomDetailViewState::sendMode) { mode ->
|
||||
when (mode) {
|
||||
SendMode.REGULAR -> {
|
||||
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()
|
||||
}
|
||||
SendMode.EDIT,
|
||||
SendMode.QUOTE,
|
||||
SendMode.REPLY -> {
|
||||
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
|
||||
composerLayout.composerRelatedMessageTitle.apply {
|
||||
text = event.getDisambiguatedDisplayName()
|
||||
setTextColor(ContextCompat.getColor(requireContext(), getColorFromUserId(event.root.senderId)))
|
||||
}
|
||||
|
||||
//TODO this is used at several places, find way to refactor?
|
||||
val messageContent: MessageContent? =
|
||||
event.annotations?.editSummary?.aggregatedContent?.toModel()
|
||||
?: event.root.getClearContent().toModel()
|
||||
val nonFormattedBody = messageContent?.body ?: ""
|
||||
var formattedBody: CharSequence? = null
|
||||
if (messageContent is MessageTextContent && messageContent.format == MessageType.FORMAT_MATRIX_HTML) {
|
||||
val parser = Parser.builder().build()
|
||||
val document = parser.parse(messageContent.formattedBody
|
||||
?: messageContent.body)
|
||||
formattedBody = Markwon.builder(requireContext())
|
||||
.usePlugin(HtmlPlugin.create()).build().render(document)
|
||||
}
|
||||
composerLayout.composerRelatedMessageContent.text = formattedBody
|
||||
?: nonFormattedBody
|
||||
|
||||
|
||||
if (mode == SendMode.EDIT) {
|
||||
//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
|
||||
?: "", event.senderName, composerLayout.composerRelatedMessageAvatar)
|
||||
|
||||
composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text.length)
|
||||
composerLayout.expand {
|
||||
focusComposerAndShowKeyboard()
|
||||
}
|
||||
composerLayout.composerRelatedMessageCloseButton.setOnClickListener {
|
||||
composerLayout.composerEditText.setText("")
|
||||
roomDetailViewModel.resetSendMode()
|
||||
}
|
||||
|
||||
}
|
||||
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
|
||||
composerLayout.collapse()
|
||||
}
|
||||
|
||||
private fun enterSpecialMode(event: TimelineEvent, @DrawableRes iconRes: Int, useText: Boolean) {
|
||||
commandAutocompletePolicy.enabled = false
|
||||
//switch to expanded bar
|
||||
composerLayout.composerRelatedMessageTitle.apply {
|
||||
text = event.getDisambiguatedDisplayName()
|
||||
setTextColor(ContextCompat.getColor(requireContext(), getColorFromUserId(event.root.senderId)))
|
||||
}
|
||||
|
||||
val messageContent: MessageContent? = event.getLastMessageContent()
|
||||
val nonFormattedBody = messageContent?.body ?: ""
|
||||
var formattedBody: CharSequence? = null
|
||||
if (messageContent is MessageTextContent && messageContent.format == MessageType.FORMAT_MATRIX_HTML) {
|
||||
val parser = Parser.builder().build()
|
||||
val document = parser.parse(messageContent.formattedBody
|
||||
?: messageContent.body)
|
||||
formattedBody = eventHtmlRenderer.render(document)
|
||||
}
|
||||
composerLayout.composerRelatedMessageContent.text = formattedBody
|
||||
?: nonFormattedBody
|
||||
|
||||
composerLayout.composerEditText.setText(if (useText) nonFormattedBody else "")
|
||||
composerLayout.composerRelatedMessageActionIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), iconRes))
|
||||
|
||||
avatarRenderer.render(event.senderAvatar, event.root.senderId
|
||||
?: "", event.senderName, composerLayout.composerRelatedMessageAvatar)
|
||||
|
||||
composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text.length)
|
||||
composerLayout.expand {
|
||||
focusComposerAndShowKeyboard()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
|
@ -422,6 +397,10 @@ class RoomDetailFragment :
|
|||
roomDetailViewModel.process(RoomDetailActions.SendMessage(textMessage, VectorPreferences.isMarkdownEnabled(requireContext())))
|
||||
}
|
||||
}
|
||||
composerLayout.composerRelatedMessageCloseButton.setOnClickListener {
|
||||
composerLayout.composerEditText.setText("")
|
||||
roomDetailViewModel.resetSendMode()
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupAttachmentButton() {
|
||||
|
|
|
@ -47,8 +47,6 @@ import im.vector.riotx.core.utils.LiveEvent
|
|||
import im.vector.riotx.features.command.CommandParser
|
||||
import im.vector.riotx.features.command.ParsedCommand
|
||||
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 org.commonmark.parser.Parser
|
||||
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 {
|
||||
copy(
|
||||
sendMode = SendMode.EDIT,
|
||||
selectedEvent = event
|
||||
sendMode = SendMode.EDIT(event)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -138,8 +135,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
|||
fun resetSendMode() {
|
||||
setState {
|
||||
copy(
|
||||
sendMode = SendMode.REGULAR,
|
||||
selectedEvent = null
|
||||
sendMode = SendMode.REGULAR
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -167,7 +163,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
|||
private fun handleSendMessage(action: RoomDetailActions.SendMessage) {
|
||||
withState { state ->
|
||||
when (state.sendMode) {
|
||||
SendMode.REGULAR -> {
|
||||
SendMode.REGULAR -> {
|
||||
val slashCommandResult = CommandParser.parseSplashCommand(action.text)
|
||||
|
||||
when (slashCommandResult) {
|
||||
|
@ -233,21 +229,29 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
|||
}
|
||||
}
|
||||
}
|
||||
SendMode.EDIT -> {
|
||||
room.editTextMessage(state.selectedEvent?.root?.eventId
|
||||
?: "", action.text, action.autoMarkdown)
|
||||
is SendMode.EDIT -> {
|
||||
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)
|
||||
} else {
|
||||
Timber.w("Same message content, do not send edition")
|
||||
}
|
||||
setState {
|
||||
copy(
|
||||
sendMode = SendMode.REGULAR,
|
||||
selectedEvent = null
|
||||
sendMode = SendMode.REGULAR
|
||||
)
|
||||
}
|
||||
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.MessageSent))
|
||||
}
|
||||
SendMode.QUOTE -> {
|
||||
is SendMode.QUOTE -> {
|
||||
val messageContent: MessageContent? =
|
||||
state.selectedEvent?.annotations?.editSummary?.aggregatedContent?.toModel()
|
||||
?: state.selectedEvent?.root?.getClearContent().toModel()
|
||||
state.sendMode.timelineEvent.annotations?.editSummary?.aggregatedContent.toModel()
|
||||
?: state.sendMode.timelineEvent.root.getClearContent().toModel()
|
||||
val textMsg = messageContent?.body
|
||||
|
||||
val finalText = legacyRiotQuoteText(textMsg, action.text)
|
||||
|
@ -264,19 +268,17 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
|||
}
|
||||
setState {
|
||||
copy(
|
||||
sendMode = SendMode.REGULAR,
|
||||
selectedEvent = null
|
||||
sendMode = SendMode.REGULAR
|
||||
)
|
||||
}
|
||||
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.MessageSent))
|
||||
}
|
||||
SendMode.REPLY -> {
|
||||
state.selectedEvent?.let {
|
||||
room.replyToMessage(it.root, action.text)
|
||||
is SendMode.REPLY -> {
|
||||
state.sendMode.timelineEvent.let {
|
||||
room.replyToMessage(it, action.text, action.autoMarkdown)
|
||||
setState {
|
||||
copy(
|
||||
sendMode = SendMode.REGULAR,
|
||||
selectedEvent = null
|
||||
sendMode = SendMode.REGULAR
|
||||
)
|
||||
}
|
||||
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.MessageSent))
|
||||
|
@ -427,8 +429,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
|||
room.getTimeLineEvent(action.eventId)?.let {
|
||||
setState {
|
||||
copy(
|
||||
sendMode = SendMode.QUOTE,
|
||||
selectedEvent = it
|
||||
sendMode = SendMode.QUOTE(it)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -438,8 +439,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
|||
room.getTimeLineEvent(action.eventId)?.let {
|
||||
setState {
|
||||
copy(
|
||||
sendMode = SendMode.REPLY,
|
||||
selectedEvent = it
|
||||
sendMode = SendMode.REPLY(it)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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...)
|
||||
*/
|
||||
enum class SendMode {
|
||||
REGULAR,
|
||||
QUOTE,
|
||||
EDIT,
|
||||
REPLY
|
||||
sealed class SendMode {
|
||||
object REGULAR : SendMode()
|
||||
data class QUOTE(val timelineEvent: TimelineEvent) : SendMode()
|
||||
data class EDIT(val timelineEvent: TimelineEvent) : SendMode()
|
||||
data class REPLY(val timelineEvent: TimelineEvent) : SendMode()
|
||||
}
|
||||
|
||||
data class RoomDetailViewState(
|
||||
|
@ -46,7 +46,6 @@ data class RoomDetailViewState(
|
|||
val asyncInviter: Async<User> = Uninitialized,
|
||||
val asyncRoomSummary: Async<RoomSummary> = Uninitialized,
|
||||
val sendMode: SendMode = SendMode.REGULAR,
|
||||
val selectedEvent: TimelineEvent? = null,
|
||||
val isEncrypted: Boolean = false
|
||||
) : MvRxState {
|
||||
|
||||
|
|
|
@ -21,11 +21,11 @@ import com.squareup.inject.assisted.AssistedInject
|
|||
import dagger.Lazy
|
||||
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.toModel
|
||||
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.MessageType
|
||||
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.riotx.core.extensions.canReact
|
||||
import im.vector.riotx.core.platform.VectorViewModel
|
||||
|
@ -58,8 +58,7 @@ data class MessageActionState(
|
|||
fun messageBody(eventHtmlRenderer: EventHtmlRenderer?, noticeEventFormatter: NoticeEventFormatter?): CharSequence? {
|
||||
return when (timelineEvent()?.root?.getClearType()) {
|
||||
EventType.MESSAGE -> {
|
||||
val messageContent: MessageContent? = timelineEvent()?.annotations?.editSummary?.aggregatedContent?.toModel()
|
||||
?: timelineEvent()?.root?.getClearContent().toModel()
|
||||
val messageContent: MessageContent? = timelineEvent()?.getLastMessageContent()
|
||||
if (messageContent is MessageTextContent && messageContent.format == MessageType.FORMAT_MATRIX_HTML) {
|
||||
eventHtmlRenderer?.render(messageContent.formattedBody
|
||||
?: messageContent.body)
|
||||
|
|
|
@ -27,11 +27,11 @@ import dagger.Lazy
|
|||
import im.vector.matrix.android.api.permalinks.MatrixLinkify
|
||||
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.toModel
|
||||
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.send.SendState
|
||||
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.riotx.EmojiCompatFontProvider
|
||||
import im.vector.riotx.R
|
||||
|
@ -79,8 +79,7 @@ class MessageItemFactory @Inject constructor(
|
|||
}
|
||||
|
||||
val messageContent: MessageContent =
|
||||
event.annotations?.editSummary?.aggregatedContent?.toModel()
|
||||
?: event.root.getClearContent().toModel()
|
||||
event.getLastMessageContent()
|
||||
?: //Malformed content, we should echo something on screen
|
||||
return DefaultItem_().text(stringProvider.getString(R.string.malformed_message))
|
||||
|
||||
|
@ -311,8 +310,7 @@ class MessageItemFactory @Inject constructor(
|
|||
editSummary: EditAggregatedSummary?): SpannableStringBuilder {
|
||||
val spannable = SpannableStringBuilder()
|
||||
spannable.append(linkifiedBody)
|
||||
// TODO i18n
|
||||
val editedSuffix = "(edited)"
|
||||
val editedSuffix = stringProvider.getString(R.string.edited_suffix)
|
||||
spannable.append(" ").append(editedSuffix)
|
||||
val color = colorProvider.getColorFromAttribute(R.attr.vctr_list_header_secondary_text_color)
|
||||
val editStart = spannable.indexOf(editedSuffix)
|
||||
|
|
|
@ -50,6 +50,10 @@ class EventHtmlRenderer @Inject constructor(context: Context,
|
|||
return markwon.toMarkdown(text)
|
||||
}
|
||||
|
||||
fun render(node: Node) : CharSequence {
|
||||
return markwon.render(node)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private class MatrixPlugin private constructor(private val glideRequests: GlideRequests,
|
||||
|
|
|
@ -18,15 +18,14 @@ package im.vector.riotx.features.notifications
|
|||
import androidx.core.app.NotificationCompat
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
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.events.model.Event
|
||||
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.Membership
|
||||
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.getLastMessageContent
|
||||
import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult
|
||||
import im.vector.riotx.BuildConfig
|
||||
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}]")
|
||||
// Ok room is not known in store, but we can still display something
|
||||
val body =
|
||||
event.annotations?.editSummary?.aggregatedContent?.toModel<MessageContent>()?.body
|
||||
?: event.root.getClearContent().toModel<MessageContent>()?.body
|
||||
event.getLastMessageContent()
|
||||
?.body
|
||||
?: stringProvider.getString(R.string.notification_unknown_new_event)
|
||||
val roomName = stringProvider.getString(R.string.notification_unknown_room_name)
|
||||
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
|
||||
?: event.root.getClearContent().toModel<MessageContent>()?.body
|
||||
val body = event.getLastMessageContent()
|
||||
?.body
|
||||
?: stringProvider.getString(R.string.notification_unknown_new_event)
|
||||
val roomName = room.roomSummary()?.displayName ?: ""
|
||||
val senderDisplayName = event.senderName ?: event.root.senderId
|
||||
|
|
|
@ -14,5 +14,7 @@
|
|||
<string name="downloading_file">Downloading file %1$s…</string>
|
||||
<string name="downloaded_file">File %1$s has been downloaded!</string>
|
||||
|
||||
<string name="edited_suffix">"(edited)"</string>
|
||||
|
||||
|
||||
</resources>
|
Loading…
Reference in a new issue