Added auto markdown (as per preference)

Fix / show formatted message preview upon composer in edit/quote/reply
Fix / use aggregated content to decide for actions on long click
This commit is contained in:
Valere 2019-05-27 18:08:29 +02:00
parent 00d66ffd48
commit 4a4c0a3da1
10 changed files with 65 additions and 20 deletions

View File

@ -91,6 +91,7 @@ dependencies {
def moshi_version = '1.8.0' def moshi_version = '1.8.0'
def lifecycle_version = '2.0.0' def lifecycle_version = '2.0.0'
def coroutines_version = "1.0.1" def coroutines_version = "1.0.1"
def markwon_version = '3.0.0-SNAPSHOT'


implementation fileTree(dir: 'libs', include: ['*.aar']) implementation fileTree(dir: 'libs', include: ['*.aar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
@ -112,6 +113,8 @@ dependencies {
implementation "com.squareup.moshi:moshi-adapters:$moshi_version" implementation "com.squareup.moshi:moshi-adapters:$moshi_version"
kapt "com.squareup.moshi:moshi-kotlin-codegen:$moshi_version" kapt "com.squareup.moshi:moshi-kotlin-codegen:$moshi_version"


implementation "ru.noties.markwon:core:$markwon_version"

// Database // Database
implementation 'com.github.Zhuinden:realm-monarchy:0.5.1' implementation 'com.github.Zhuinden:realm-monarchy:0.5.1'
kapt 'dk.ilios:realmfieldnameshelper:1.1.1' kapt 'dk.ilios:realmfieldnameshelper:1.1.1'

View File

@ -60,7 +60,7 @@ interface RelationService {
* @param newBodyText The edited body * @param newBodyText The edited body
* @param compatibilityBodyText The text that will appear on clients that don't support yet edition * @param compatibilityBodyText The text that will appear on clients that don't support yet edition
*/ */
fun editTextMessage(targetEventId: String, newBodyText: String, compatibilityBodyText: String = "* $newBodyText"): Cancelable fun editTextMessage(targetEventId: String, newBodyText: String, newBodyAutoMarkdown: Boolean, compatibilityBodyText: String = "* $newBodyText"): Cancelable




fun replyToMessage(eventReplied: Event, replyText: String) : Cancelable? fun replyToMessage(eventReplied: Event, replyText: String) : Cancelable?

View File

@ -33,7 +33,7 @@ interface SendService {
* @param msgType the message type: MessageType.MSGTYPE_TEXT (default) or MessageType.MSGTYPE_EMOTE * @param msgType the message type: MessageType.MSGTYPE_TEXT (default) or MessageType.MSGTYPE_EMOTE
* @return a [Cancelable] * @return a [Cancelable]
*/ */
fun sendTextMessage(text: String, msgType: String = MessageType.MSGTYPE_TEXT): Cancelable fun sendTextMessage(text: String, msgType: String = MessageType.MSGTYPE_TEXT, autoMarkdown: Boolean = false): Cancelable
fun sendFormattedTextMessage(text: String,formattedText: String): Cancelable fun sendFormattedTextMessage(text: String,formattedText: String): Cancelable


/** /**

View File

@ -160,8 +160,8 @@ internal class DefaultRelationService(private val roomId: String,
.build() .build()
} }


override fun editTextMessage(targetEventId: String, newBodyText: String, compatibilityBodyText: String): Cancelable { override fun editTextMessage(targetEventId: String, newBodyText: String, newBodyAutoMarkdown: Boolean, compatibilityBodyText: String): Cancelable {
val event = eventFactory.createReplaceTextEvent(roomId, targetEventId, newBodyText, MessageType.MSGTYPE_TEXT, compatibilityBodyText) val event = eventFactory.createReplaceTextEvent(roomId, targetEventId, newBodyText, newBodyAutoMarkdown, MessageType.MSGTYPE_TEXT, compatibilityBodyText)
val sendContentWorkerParams = SendEventWorker.Params(roomId, event) val sendContentWorkerParams = SendEventWorker.Params(roomId, event)
val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams) val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams)



View File

@ -49,8 +49,8 @@ internal class DefaultSendService(private val roomId: String,
: SendService { : SendService {




override fun sendTextMessage(text: String, msgType: String): Cancelable { override fun sendTextMessage(text: String, msgType: String, autoMarkdown: Boolean): Cancelable {
val event = eventFactory.createTextEvent(roomId, msgType, text).also { val event = eventFactory.createTextEvent(roomId, msgType, text, autoMarkdown).also {
saveLocalEcho(it) saveLocalEcho(it)
} }
val sendWork = createSendEventWork(event) val sendWork = createSendEventWork(event)

View File

@ -17,6 +17,7 @@
package im.vector.matrix.android.internal.session.room.send package im.vector.matrix.android.internal.session.room.send


import android.media.MediaMetadataRetriever import android.media.MediaMetadataRetriever
import android.text.TextUtils
import im.vector.matrix.android.R import im.vector.matrix.android.R
import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.permalinks.PermalinkFactory import im.vector.matrix.android.api.permalinks.PermalinkFactory
@ -29,10 +30,21 @@ import im.vector.matrix.android.api.session.room.model.annotation.ReplyToContent
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.internal.session.content.ThumbnailExtractor import im.vector.matrix.android.internal.session.content.ThumbnailExtractor
import im.vector.matrix.android.internal.util.StringProvider import im.vector.matrix.android.internal.util.StringProvider
import org.commonmark.parser.Parser
import org.commonmark.renderer.html.HtmlRenderer


internal class LocalEchoEventFactory(private val credentials: Credentials, private val stringProvider: StringProvider) { internal class LocalEchoEventFactory(private val credentials: Credentials, private val stringProvider: StringProvider) {


fun createTextEvent(roomId: String, msgType: String, text: String): Event { 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 (!TextUtils.equals(text, htmlText)) {
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)
} }
@ -48,15 +60,32 @@ internal class LocalEchoEventFactory(private val credentials: Credentials, priva
} }




fun createReplaceTextEvent(roomId: String, targetEventId: String, newBodyText: String, msgType: String, compatibilityText: String): Event { 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 (!TextUtils.equals(newBodyText, htmlText)) {
newContent = MessageTextContent(
type = MessageType.MSGTYPE_TEXT,
format = MessageType.FORMAT_MATRIX_HTML,
body = newBodyText,
formattedBody = htmlText
)
}
}

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

View File

@ -23,7 +23,7 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineEvent


sealed class RoomDetailActions { sealed class RoomDetailActions {


data class SendMessage(val text: String) : RoomDetailActions() data class SendMessage(val text: String, val autoMarkdown: Boolean) : RoomDetailActions()
data class SendMedia(val mediaFiles: List<MediaFile>) : RoomDetailActions() data class SendMedia(val mediaFiles: List<MediaFile>) : RoomDetailActions()
object IsDisplayed : RoomDetailActions() object IsDisplayed : RoomDetailActions()
data class EventDisplayed(val event: TimelineEvent) : RoomDetailActions() data class EventDisplayed(val event: TimelineEvent) : RoomDetailActions()

View File

@ -93,13 +93,17 @@ import im.vector.riotredesign.features.media.ImageMediaViewerActivity
import im.vector.riotredesign.features.media.VideoContentRenderer import im.vector.riotredesign.features.media.VideoContentRenderer
import im.vector.riotredesign.features.media.VideoMediaViewerActivity import im.vector.riotredesign.features.media.VideoMediaViewerActivity
import im.vector.riotredesign.features.reactions.EmojiReactionPickerActivity import im.vector.riotredesign.features.reactions.EmojiReactionPickerActivity
import im.vector.riotredesign.features.settings.PreferencesManager
import kotlinx.android.parcel.Parcelize 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.koin.android.ext.android.inject import org.koin.android.ext.android.inject
import org.koin.android.scope.ext.android.bindScope import org.koin.android.scope.ext.android.bindScope
import org.koin.android.scope.ext.android.getOrCreateScope import org.koin.android.scope.ext.android.getOrCreateScope
import org.koin.core.parameter.parametersOf import org.koin.core.parameter.parametersOf
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


@ -227,12 +231,20 @@ class RoomDetailFragment :
val messageContent: MessageContent? = val messageContent: MessageContent? =
event.annotations?.editSummary?.aggregatedContent?.toModel() event.annotations?.editSummary?.aggregatedContent?.toModel()
?: event.root.content.toModel() ?: event.root.content.toModel()
val eventTextBody = messageContent?.body val nonFormattedBody = messageContent?.body ?: ""
composerLayout.composerRelatedMessageContent.text = eventTextBody 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) { if (mode == SendMode.EDIT) {
composerLayout.composerEditText.setText(eventTextBody) //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)) composerLayout.composerRelatedMessageActionIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_edit))
} else if (mode == SendMode.QUOTE) { } else if (mode == SendMode.QUOTE) {
composerLayout.composerEditText.setText("") composerLayout.composerEditText.setText("")
@ -378,7 +390,7 @@ class RoomDetailFragment :
composerLayout.sendButton.setOnClickListener { composerLayout.sendButton.setOnClickListener {
val textMessage = composerLayout.composerEditText.text.toString() val textMessage = composerLayout.composerEditText.text.toString()
if (textMessage.isNotBlank()) { if (textMessage.isNotBlank()) {
roomDetailViewModel.process(RoomDetailActions.SendMessage(textMessage)) roomDetailViewModel.process(RoomDetailActions.SendMessage(textMessage, PreferencesManager.isMarkdownEnabled(requireContext())))
} }
} }
} }

View File

@ -138,7 +138,7 @@ class RoomDetailViewModel(initialState: RoomDetailViewState,
when (slashCommandResult) { when (slashCommandResult) {
is ParsedCommand.ErrorNotACommand -> { is ParsedCommand.ErrorNotACommand -> {
// Send the text message to the room // Send the text message to the room
room.sendTextMessage(action.text) room.sendTextMessage(action.text, autoMarkdown = action.autoMarkdown)
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.MessageSent)) _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.MessageSent))
} }
is ParsedCommand.ErrorSyntax -> { is ParsedCommand.ErrorSyntax -> {
@ -199,7 +199,7 @@ class RoomDetailViewModel(initialState: RoomDetailViewState,
} }
} }
SendMode.EDIT -> { SendMode.EDIT -> {
room.editTextMessage(state?.selectedEvent?.root?.eventId ?: "", action.text) room.editTextMessage(state.selectedEvent?.root?.eventId ?: "", action.text, action.autoMarkdown)
setState { setState {
copy( copy(
sendMode = SendMode.REGULAR, sendMode = SendMode.REGULAR,

View File

@ -50,7 +50,8 @@ class MessageMenuViewModel(initialState: MessageMenuState) : VectorViewModel<Mes
val event = currentSession.getRoom(parcel.roomId)?.getTimeLineEvent(parcel.eventId) val event = currentSession.getRoom(parcel.roomId)?.getTimeLineEvent(parcel.eventId)
?: return null ?: return null


val messageContent: MessageContent = event.root.content.toModel() ?: return null val messageContent: MessageContent = event.annotations?.editSummary?.aggregatedContent?.toModel()
?: event.root.content.toModel() ?: return null
val type = messageContent.type val type = messageContent.type


if (event.sendState == SendState.UNSENT) { if (event.sendState == SendState.UNSENT) {