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:
Benoit Marty 2021-11-17 11:21:48 +01:00 committed by GitHub
parent 855b672f48
commit 0fd29d763c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 75 additions and 56 deletions

1
changelog.d/3477.feature Normal file
View File

@ -0,0 +1 @@
Make notification text spoiler aware

1
changelog.d/452.bugfix Normal file
View File

@ -0,0 +1 @@
Render markdown in room list

View File

@ -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.message.MessageContent
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.sender.SenderInfo
import org.matrix.android.sdk.api.util.ContentUtils
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
*/
@ -156,11 +144,25 @@ fun TimelineEvent.isEdition(): Boolean {
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 extractUsefulTextFromReply(lastContent?.body ?: "")
extractUsefulTextFromReply(lastContentBody)
} 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
}

View File

@ -15,6 +15,8 @@
*/
package org.matrix.android.sdk.api.util
import org.matrix.android.sdk.internal.util.unescapeHtml
object ContentUtils {
fun extractUsefulTextFromReply(repliedBody: String): String {
val lines = repliedBody.lines()
@ -44,4 +46,15 @@ object ContentUtils {
}
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 = ""
}

View File

@ -122,7 +122,7 @@ class TextComposerViewModel @AssistedInject constructor(
private fun handleEnterEditMode(action: TextComposerAction.EnterEditMode) {
room.getTimeLineEvent(action.eventId)?.let { timelineEvent ->
setState { copy(sendMode = SendMode.EDIT(timelineEvent, timelineEvent.getTextEditableContent() ?: "")) }
setState { copy(sendMode = SendMode.EDIT(timelineEvent, timelineEvent.getTextEditableContent())) }
}
}

View File

@ -16,29 +16,33 @@
package im.vector.app.features.home.room.detail.timeline.format
import dagger.Lazy
import im.vector.app.EmojiCompatWrapper
import im.vector.app.R
import im.vector.app.core.resources.ColorProvider
import im.vector.app.core.resources.StringProvider
import im.vector.app.features.html.EventHtmlRenderer
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.toModel
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.MessageTextContent
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.relation.ReactionContent
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.getTextEditableContent
import org.matrix.android.sdk.api.session.room.timeline.isReply
import org.matrix.android.sdk.api.session.room.timeline.getTextDisplayableContent
import javax.inject.Inject
class DisplayableEventFormatter @Inject constructor(
private val stringProvider: StringProvider,
private val colorProvider: ColorProvider,
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 {
@ -53,54 +57,45 @@ class DisplayableEventFormatter @Inject constructor(
val senderName = timelineEvent.senderInfo.disambiguatedDisplayName
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)
}
}
return when (timelineEvent.root.getClearType()) {
EventType.MESSAGE -> {
timelineEvent.getLastMessageContent()?.let { messageContent ->
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 -> {
return simpleFormat(senderName, stringProvider.getString(R.string.verification_request), appendAuthor)
simpleFormat(senderName, stringProvider.getString(R.string.verification_request), appendAuthor)
}
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 -> {
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 {
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 -> {
return simpleFormat(senderName, stringProvider.getString(R.string.sent_a_video), appendAuthor)
simpleFormat(senderName, stringProvider.getString(R.string.sent_a_video), appendAuthor)
}
MessageType.MSGTYPE_FILE -> {
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 -> {
// do not show that?
return span { }
span { }
}
MessageType.MSGTYPE_OPTIONS -> {
return when (messageContent) {
when (messageContent) {
is MessageOptionsContent -> {
val previewText = if (messageContent.optionType == OPTION_TYPE_BUTTONS) {
stringProvider.getString(R.string.sent_a_bot_buttons)
@ -115,15 +110,24 @@ class DisplayableEventFormatter @Inject constructor(
}
}
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_DONE -> {
// 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_ACCEPT,
@ -131,17 +135,15 @@ class DisplayableEventFormatter @Inject constructor(
EventType.KEY_VERIFICATION_KEY,
EventType.KEY_VERIFICATION_READY,
EventType.CALL_CANDIDATES -> {
return span { }
span { }
}
else -> {
return span {
span {
text = noticeEventFormatter.format(timelineEvent, isDm) ?: ""
textStyle = "italic"
}
}
}
return span { }
}
private fun simpleFormat(senderName: String, body: CharSequence, appendAuthor: Boolean): CharSequence {