Markdown and sploiler in roomlist + spoiler in notifications (#4483)
Render markdown in room list and make notifications spoiler aware, per MSC3124 Reorder when case to put the most common on top Co-authored-by: Onuray Sahin <onurays@element.io> Co-authored-by: Wasabi\preston <1337paf92@gmail.com>
This commit is contained in:
parent
855b672f48
commit
0fd29d763c
1
changelog.d/3477.feature
Normal file
1
changelog.d/3477.feature
Normal file
@ -0,0 +1 @@
|
|||||||
|
Make notification text spoiler aware
|
||||||
1
changelog.d/452.bugfix
Normal file
1
changelog.d/452.bugfix
Normal file
@ -0,0 +1 @@
|
|||||||
|
Render markdown in room list
|
||||||
@ -28,8 +28,10 @@ import org.matrix.android.sdk.api.session.room.model.EventAnnotationsSummary
|
|||||||
import org.matrix.android.sdk.api.session.room.model.ReadReceipt
|
import org.matrix.android.sdk.api.session.room.model.ReadReceipt
|
||||||
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
|
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
|
||||||
import org.matrix.android.sdk.api.session.room.model.message.MessageStickerContent
|
import org.matrix.android.sdk.api.session.room.model.message.MessageStickerContent
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent
|
||||||
import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent
|
import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent
|
||||||
import org.matrix.android.sdk.api.session.room.sender.SenderInfo
|
import org.matrix.android.sdk.api.session.room.sender.SenderInfo
|
||||||
|
import org.matrix.android.sdk.api.util.ContentUtils
|
||||||
import org.matrix.android.sdk.api.util.ContentUtils.extractUsefulTextFromReply
|
import org.matrix.android.sdk.api.util.ContentUtils.extractUsefulTextFromReply
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -131,20 +133,6 @@ fun TimelineEvent.getLastMessageContent(): MessageContent? {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get last Message body, after a possible edition
|
|
||||||
*/
|
|
||||||
fun TimelineEvent.getLastMessageBody(): String? {
|
|
||||||
val lastMessageContent = getLastMessageContent()
|
|
||||||
|
|
||||||
if (lastMessageContent != null) {
|
|
||||||
return lastMessageContent.newContent?.toModel<MessageContent>()?.body
|
|
||||||
?: lastMessageContent.body
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true if it's a reply
|
* Returns true if it's a reply
|
||||||
*/
|
*/
|
||||||
@ -156,11 +144,25 @@ fun TimelineEvent.isEdition(): Boolean {
|
|||||||
return root.isEdition()
|
return root.isEdition()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun TimelineEvent.getTextEditableContent(): String? {
|
/**
|
||||||
val lastContent = getLastMessageContent()
|
* Get the latest message body, after a possible edition, stripping the reply prefix if necessary
|
||||||
|
*/
|
||||||
|
fun TimelineEvent.getTextEditableContent(): String {
|
||||||
|
val lastContentBody = getLastMessageContent()?.body ?: return ""
|
||||||
return if (isReply()) {
|
return if (isReply()) {
|
||||||
return extractUsefulTextFromReply(lastContent?.body ?: "")
|
extractUsefulTextFromReply(lastContentBody)
|
||||||
} else {
|
} else {
|
||||||
lastContent?.body ?: ""
|
lastContentBody
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the latest displayable content.
|
||||||
|
* Will take care to hide spoiler text
|
||||||
|
*/
|
||||||
|
fun MessageContent.getTextDisplayableContent(): String {
|
||||||
|
return newContent?.toModel<MessageTextContent>()?.matrixFormattedBody?.let { ContentUtils.formatSpoilerTextFromHtml(it) }
|
||||||
|
?: newContent?.toModel<MessageContent>()?.body
|
||||||
|
?: (this as MessageTextContent?)?.matrixFormattedBody?.let { ContentUtils.formatSpoilerTextFromHtml(it) }
|
||||||
|
?: body
|
||||||
|
}
|
||||||
|
|||||||
@ -15,6 +15,8 @@
|
|||||||
*/
|
*/
|
||||||
package org.matrix.android.sdk.api.util
|
package org.matrix.android.sdk.api.util
|
||||||
|
|
||||||
|
import org.matrix.android.sdk.internal.util.unescapeHtml
|
||||||
|
|
||||||
object ContentUtils {
|
object ContentUtils {
|
||||||
fun extractUsefulTextFromReply(repliedBody: String): String {
|
fun extractUsefulTextFromReply(repliedBody: String): String {
|
||||||
val lines = repliedBody.lines()
|
val lines = repliedBody.lines()
|
||||||
@ -44,4 +46,15 @@ object ContentUtils {
|
|||||||
}
|
}
|
||||||
return repliedBody
|
return repliedBody
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("RegExpRedundantEscape")
|
||||||
|
fun formatSpoilerTextFromHtml(formattedBody: String): String {
|
||||||
|
// var reason = "",
|
||||||
|
// can capture the spoiler reason for better formatting? ex. { reason = it.value; ">"}
|
||||||
|
return formattedBody.replace("(?<=<span data-mx-spoiler)=\\\".+?\\\">".toRegex(), ">")
|
||||||
|
.replace("(?<=<span data-mx-spoiler>).+?(?=</span>)".toRegex()) { SPOILER_CHAR.repeat(it.value.length) }
|
||||||
|
.unescapeHtml()
|
||||||
|
}
|
||||||
|
|
||||||
|
private const val SPOILER_CHAR = "█"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -122,7 +122,7 @@ class TextComposerViewModel @AssistedInject constructor(
|
|||||||
|
|
||||||
private fun handleEnterEditMode(action: TextComposerAction.EnterEditMode) {
|
private fun handleEnterEditMode(action: TextComposerAction.EnterEditMode) {
|
||||||
room.getTimeLineEvent(action.eventId)?.let { timelineEvent ->
|
room.getTimeLineEvent(action.eventId)?.let { timelineEvent ->
|
||||||
setState { copy(sendMode = SendMode.EDIT(timelineEvent, timelineEvent.getTextEditableContent() ?: "")) }
|
setState { copy(sendMode = SendMode.EDIT(timelineEvent, timelineEvent.getTextEditableContent())) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -16,29 +16,33 @@
|
|||||||
|
|
||||||
package im.vector.app.features.home.room.detail.timeline.format
|
package im.vector.app.features.home.room.detail.timeline.format
|
||||||
|
|
||||||
|
import dagger.Lazy
|
||||||
import im.vector.app.EmojiCompatWrapper
|
import im.vector.app.EmojiCompatWrapper
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
import im.vector.app.core.resources.ColorProvider
|
import im.vector.app.core.resources.ColorProvider
|
||||||
import im.vector.app.core.resources.StringProvider
|
import im.vector.app.core.resources.StringProvider
|
||||||
|
import im.vector.app.features.html.EventHtmlRenderer
|
||||||
import me.gujun.android.span.span
|
import me.gujun.android.span.span
|
||||||
|
import org.commonmark.node.Document
|
||||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||||
import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent
|
import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent
|
||||||
import org.matrix.android.sdk.api.session.room.model.message.MessageOptionsContent
|
import org.matrix.android.sdk.api.session.room.model.message.MessageOptionsContent
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent
|
||||||
import org.matrix.android.sdk.api.session.room.model.message.MessageType
|
import org.matrix.android.sdk.api.session.room.model.message.MessageType
|
||||||
import org.matrix.android.sdk.api.session.room.model.message.OPTION_TYPE_BUTTONS
|
import org.matrix.android.sdk.api.session.room.model.message.OPTION_TYPE_BUTTONS
|
||||||
import org.matrix.android.sdk.api.session.room.model.relation.ReactionContent
|
import org.matrix.android.sdk.api.session.room.model.relation.ReactionContent
|
||||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||||
import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent
|
import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent
|
||||||
import org.matrix.android.sdk.api.session.room.timeline.getTextEditableContent
|
import org.matrix.android.sdk.api.session.room.timeline.getTextDisplayableContent
|
||||||
import org.matrix.android.sdk.api.session.room.timeline.isReply
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class DisplayableEventFormatter @Inject constructor(
|
class DisplayableEventFormatter @Inject constructor(
|
||||||
private val stringProvider: StringProvider,
|
private val stringProvider: StringProvider,
|
||||||
private val colorProvider: ColorProvider,
|
private val colorProvider: ColorProvider,
|
||||||
private val emojiCompatWrapper: EmojiCompatWrapper,
|
private val emojiCompatWrapper: EmojiCompatWrapper,
|
||||||
private val noticeEventFormatter: NoticeEventFormatter
|
private val noticeEventFormatter: NoticeEventFormatter,
|
||||||
|
private val htmlRenderer: Lazy<EventHtmlRenderer>
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun format(timelineEvent: TimelineEvent, isDm: Boolean, appendAuthor: Boolean): CharSequence {
|
fun format(timelineEvent: TimelineEvent, isDm: Boolean, appendAuthor: Boolean): CharSequence {
|
||||||
@ -53,54 +57,45 @@ class DisplayableEventFormatter @Inject constructor(
|
|||||||
|
|
||||||
val senderName = timelineEvent.senderInfo.disambiguatedDisplayName
|
val senderName = timelineEvent.senderInfo.disambiguatedDisplayName
|
||||||
|
|
||||||
when (timelineEvent.root.getClearType()) {
|
return when (timelineEvent.root.getClearType()) {
|
||||||
EventType.STICKER -> {
|
|
||||||
return simpleFormat(senderName, stringProvider.getString(R.string.send_a_sticker), appendAuthor)
|
|
||||||
}
|
|
||||||
EventType.REACTION -> {
|
|
||||||
timelineEvent.root.getClearContent().toModel<ReactionContent>()?.relatesTo?.let {
|
|
||||||
val emojiSpanned = emojiCompatWrapper.safeEmojiSpanify(stringProvider.getString(R.string.sent_a_reaction, it.key))
|
|
||||||
return simpleFormat(senderName, emojiSpanned, appendAuthor)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
EventType.MESSAGE -> {
|
EventType.MESSAGE -> {
|
||||||
timelineEvent.getLastMessageContent()?.let { messageContent ->
|
timelineEvent.getLastMessageContent()?.let { messageContent ->
|
||||||
when (messageContent.msgType) {
|
when (messageContent.msgType) {
|
||||||
|
MessageType.MSGTYPE_TEXT -> {
|
||||||
|
val body = messageContent.getTextDisplayableContent()
|
||||||
|
if (messageContent is MessageTextContent && messageContent.matrixFormattedBody.isNullOrBlank().not()) {
|
||||||
|
val localFormattedBody = htmlRenderer.get().parse(body) as Document
|
||||||
|
val renderedBody = htmlRenderer.get().render(localFormattedBody) ?: body
|
||||||
|
simpleFormat(senderName, renderedBody, appendAuthor)
|
||||||
|
} else {
|
||||||
|
simpleFormat(senderName, body, appendAuthor)
|
||||||
|
}
|
||||||
|
}
|
||||||
MessageType.MSGTYPE_VERIFICATION_REQUEST -> {
|
MessageType.MSGTYPE_VERIFICATION_REQUEST -> {
|
||||||
return simpleFormat(senderName, stringProvider.getString(R.string.verification_request), appendAuthor)
|
simpleFormat(senderName, stringProvider.getString(R.string.verification_request), appendAuthor)
|
||||||
}
|
}
|
||||||
MessageType.MSGTYPE_IMAGE -> {
|
MessageType.MSGTYPE_IMAGE -> {
|
||||||
return simpleFormat(senderName, stringProvider.getString(R.string.sent_an_image), appendAuthor)
|
simpleFormat(senderName, stringProvider.getString(R.string.sent_an_image), appendAuthor)
|
||||||
}
|
}
|
||||||
MessageType.MSGTYPE_AUDIO -> {
|
MessageType.MSGTYPE_AUDIO -> {
|
||||||
if ((messageContent as? MessageAudioContent)?.voiceMessageIndicator != null) {
|
if ((messageContent as? MessageAudioContent)?.voiceMessageIndicator != null) {
|
||||||
return simpleFormat(senderName, stringProvider.getString(R.string.sent_a_voice_message), appendAuthor)
|
simpleFormat(senderName, stringProvider.getString(R.string.sent_a_voice_message), appendAuthor)
|
||||||
} else {
|
} else {
|
||||||
return simpleFormat(senderName, stringProvider.getString(R.string.sent_an_audio_file), appendAuthor)
|
simpleFormat(senderName, stringProvider.getString(R.string.sent_an_audio_file), appendAuthor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
MessageType.MSGTYPE_VIDEO -> {
|
MessageType.MSGTYPE_VIDEO -> {
|
||||||
return simpleFormat(senderName, stringProvider.getString(R.string.sent_a_video), appendAuthor)
|
simpleFormat(senderName, stringProvider.getString(R.string.sent_a_video), appendAuthor)
|
||||||
}
|
}
|
||||||
MessageType.MSGTYPE_FILE -> {
|
MessageType.MSGTYPE_FILE -> {
|
||||||
return simpleFormat(senderName, stringProvider.getString(R.string.sent_a_file), appendAuthor)
|
return simpleFormat(senderName, stringProvider.getString(R.string.sent_a_file), appendAuthor)
|
||||||
}
|
}
|
||||||
MessageType.MSGTYPE_TEXT -> {
|
|
||||||
return if (timelineEvent.isReply()) {
|
|
||||||
// Skip reply prefix, and show important
|
|
||||||
// TODO add a reply image span ?
|
|
||||||
simpleFormat(senderName, timelineEvent.getTextEditableContent()
|
|
||||||
?: messageContent.body, appendAuthor)
|
|
||||||
} else {
|
|
||||||
simpleFormat(senderName, messageContent.body, appendAuthor)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
MessageType.MSGTYPE_RESPONSE -> {
|
MessageType.MSGTYPE_RESPONSE -> {
|
||||||
// do not show that?
|
// do not show that?
|
||||||
return span { }
|
span { }
|
||||||
}
|
}
|
||||||
MessageType.MSGTYPE_OPTIONS -> {
|
MessageType.MSGTYPE_OPTIONS -> {
|
||||||
return when (messageContent) {
|
when (messageContent) {
|
||||||
is MessageOptionsContent -> {
|
is MessageOptionsContent -> {
|
||||||
val previewText = if (messageContent.optionType == OPTION_TYPE_BUTTONS) {
|
val previewText = if (messageContent.optionType == OPTION_TYPE_BUTTONS) {
|
||||||
stringProvider.getString(R.string.sent_a_bot_buttons)
|
stringProvider.getString(R.string.sent_a_bot_buttons)
|
||||||
@ -115,15 +110,24 @@ class DisplayableEventFormatter @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
return simpleFormat(senderName, messageContent.body, appendAuthor)
|
simpleFormat(senderName, messageContent.body, appendAuthor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
} ?: span { }
|
||||||
|
}
|
||||||
|
EventType.STICKER -> {
|
||||||
|
simpleFormat(senderName, stringProvider.getString(R.string.send_a_sticker), appendAuthor)
|
||||||
|
}
|
||||||
|
EventType.REACTION -> {
|
||||||
|
timelineEvent.root.getClearContent().toModel<ReactionContent>()?.relatesTo?.let {
|
||||||
|
val emojiSpanned = emojiCompatWrapper.safeEmojiSpanify(stringProvider.getString(R.string.sent_a_reaction, it.key))
|
||||||
|
simpleFormat(senderName, emojiSpanned, appendAuthor)
|
||||||
|
} ?: span { }
|
||||||
}
|
}
|
||||||
EventType.KEY_VERIFICATION_CANCEL,
|
EventType.KEY_VERIFICATION_CANCEL,
|
||||||
EventType.KEY_VERIFICATION_DONE -> {
|
EventType.KEY_VERIFICATION_DONE -> {
|
||||||
// cancel and done can appear in timeline, so should have representation
|
// cancel and done can appear in timeline, so should have representation
|
||||||
return simpleFormat(senderName, stringProvider.getString(R.string.sent_verification_conclusion), appendAuthor)
|
simpleFormat(senderName, stringProvider.getString(R.string.sent_verification_conclusion), appendAuthor)
|
||||||
}
|
}
|
||||||
EventType.KEY_VERIFICATION_START,
|
EventType.KEY_VERIFICATION_START,
|
||||||
EventType.KEY_VERIFICATION_ACCEPT,
|
EventType.KEY_VERIFICATION_ACCEPT,
|
||||||
@ -131,17 +135,15 @@ class DisplayableEventFormatter @Inject constructor(
|
|||||||
EventType.KEY_VERIFICATION_KEY,
|
EventType.KEY_VERIFICATION_KEY,
|
||||||
EventType.KEY_VERIFICATION_READY,
|
EventType.KEY_VERIFICATION_READY,
|
||||||
EventType.CALL_CANDIDATES -> {
|
EventType.CALL_CANDIDATES -> {
|
||||||
return span { }
|
span { }
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
return span {
|
span {
|
||||||
text = noticeEventFormatter.format(timelineEvent, isDm) ?: ""
|
text = noticeEventFormatter.format(timelineEvent, isDm) ?: ""
|
||||||
textStyle = "italic"
|
textStyle = "italic"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return span { }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun simpleFormat(senderName: String, body: CharSequence, appendAuthor: Boolean): CharSequence {
|
private fun simpleFormat(senderName: String, body: CharSequence, appendAuthor: Boolean): CharSequence {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user