diff --git a/changelog.d/3477.feature b/changelog.d/3477.feature new file mode 100644 index 00000000000..c6f8820b4cc --- /dev/null +++ b/changelog.d/3477.feature @@ -0,0 +1 @@ +Make notification text spoiler aware \ No newline at end of file diff --git a/changelog.d/452.bugfix b/changelog.d/452.bugfix new file mode 100644 index 00000000000..6e3d2539b54 --- /dev/null +++ b/changelog.d/452.bugfix @@ -0,0 +1 @@ +Render markdown in room list \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt index 4a6462477dc..86cb10bfe80 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt @@ -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()?.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()?.matrixFormattedBody?.let { ContentUtils.formatSpoilerTextFromHtml(it) } + ?: newContent?.toModel()?.body + ?: (this as MessageTextContent?)?.matrixFormattedBody?.let { ContentUtils.formatSpoilerTextFromHtml(it) } + ?: body +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/ContentUtils.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/ContentUtils.kt index 1a00b85ff4e..e453cb2df51 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/ContentUtils.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/ContentUtils.kt @@ -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("(?<=".toRegex(), ">") + .replace("(?<=).+?(?=)".toRegex()) { SPOILER_CHAR.repeat(it.value.length) } + .unescapeHtml() + } + + private const val SPOILER_CHAR = "█" } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerViewModel.kt index dd985bcdbec..66d49f98194 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerViewModel.kt @@ -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())) } } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt index b5831b33b86..624f22e09b4 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt @@ -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 ) { 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()?.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()?.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 {