Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/3477.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Make notification text spoiler aware
1 change: 1 addition & 0 deletions changelog.d/452.bugfix
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Render markdown in room list
Original file line number Diff line number Diff line change
Expand Up @@ -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

/**
Expand Down Expand Up @@ -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
*/
Expand All @@ -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
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a small clean up here, should not change anything.

}
}

/**
* 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
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fun should now be used when we want to display an Event, but not when user wants to edit it

Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -44,4 +46,15 @@ object ContentUtils {
}
return repliedBody
}

@Suppress("RegExpRedundantEscape")
fun formatSpoilerTextFromHtml(formattedBody: String): String {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would deserve some unit test...

Copy link
Contributor

@ouchadam ouchadam Nov 17, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

although it uses androidx.core.text.HtmlCompat.fromHtml so we wouldn't be able to write a unit test unless we use a java equivalent

// 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 = "█"
}
Original file line number Diff line number Diff line change
Expand Up @@ -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())) }
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 -> {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move below since this is less common than EventType.MESSAGE

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 -> {
Copy link
Member Author

@bmarty bmarty Nov 17, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved to the beginning since this is the most common case

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)
Expand All @@ -115,33 +110,40 @@ 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,
EventType.KEY_VERIFICATION_MAC,
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 {
Expand Down