From 214e0efcd990a6b4c9d491819e010dff9cc13069 Mon Sep 17 00:00:00 2001 From: ariskotsomitopoulos Date: Wed, 2 Mar 2022 13:47:08 +0200 Subject: [PATCH] Add Markdown support to thread summaries and thread list --- .../sdk/api/session/threads/ThreadDetails.kt | 3 +- .../internal/database/mapper/EventMapper.kt | 2 +- .../detail/search/SearchResultController.kt | 3 + .../room/detail/search/SearchResultItem.kt | 4 +- .../format/DisplayableEventFormatter.kt | 100 +++++++++++++++++- .../helper/MessageItemAttributesFactory.kt | 3 + .../detail/timeline/item/AbsMessageItem.kt | 3 +- .../list/viewmodel/ThreadListController.kt | 38 +++++-- 8 files changed, 139 insertions(+), 17 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/threads/ThreadDetails.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/threads/ThreadDetails.kt index fafe17b2c0..d6937d5b26 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/threads/ThreadDetails.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/threads/ThreadDetails.kt @@ -16,6 +16,7 @@ package org.matrix.android.sdk.api.session.threads +import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.room.sender.SenderInfo /** @@ -26,7 +27,7 @@ data class ThreadDetails( val isRootThread: Boolean = false, val numberOfThreads: Int = 0, val threadSummarySenderInfo: SenderInfo? = null, - val threadSummaryLatestTextMessage: String? = null, + val threadSummaryLatestEvent: Event? = null, val lastMessageTimestamp: Long? = null, var threadNotificationState: ThreadNotificationState = ThreadNotificationState.NO_NEW_MESSAGE, val isThread: Boolean = false, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventMapper.kt index 9c420e81fd..c3302f5ccb 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventMapper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventMapper.kt @@ -114,7 +114,7 @@ internal object EventMapper { ) }, threadNotificationState = eventEntity.threadNotificationState, - threadSummaryLatestTextMessage = eventEntity.threadSummaryLatestMessage?.root?.asDomain()?.getDecryptedTextSummary(), + threadSummaryLatestEvent = eventEntity.threadSummaryLatestMessage?.root?.asDomain(), lastMessageTimestamp = eventEntity.threadSummaryLatestMessage?.root?.originServerTs ) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultController.kt index 2cdc1a0d90..5b1f17cfe2 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultController.kt @@ -32,6 +32,7 @@ import im.vector.app.core.resources.StringProvider import im.vector.app.core.resources.UserPreferencesProvider import im.vector.app.core.ui.list.GenericHeaderItem_ import im.vector.app.features.home.AvatarRenderer +import im.vector.app.features.home.room.detail.timeline.format.DisplayableEventFormatter import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.events.model.Content @@ -45,6 +46,7 @@ class SearchResultController @Inject constructor( private val avatarRenderer: AvatarRenderer, private val stringProvider: StringProvider, private val dateFormatter: VectorDateFormatter, + private val displayableEventFormatter: DisplayableEventFormatter, private val userPreferencesProvider: UserPreferencesProvider ) : TypedEpoxyController() { @@ -125,6 +127,7 @@ class SearchResultController @Inject constructor( .sender(eventAndSender.sender ?: eventAndSender.event.senderId?.let { session.getRoomMember(it, data.roomId) }?.toMatrixItem()) .threadDetails(event.threadDetails) + .threadSummaryFormatted(displayableEventFormatter.formatThreadSummary(event.threadDetails?.threadSummaryLatestEvent).toString()) .areThreadMessagesEnabled(userPreferencesProvider.areThreadMessagesEnabled()) .listener { listener?.onItemClicked(eventAndSender.event) } .let { result.add(it) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultItem.kt index 2ec786fab2..3e141ab0e9 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultItem.kt @@ -42,6 +42,7 @@ abstract class SearchResultItem : VectorEpoxyModel() { @EpoxyAttribute lateinit var spannable: EpoxyCharSequence @EpoxyAttribute var sender: MatrixItem? = null @EpoxyAttribute var threadDetails: ThreadDetails? = null + @EpoxyAttribute var threadSummaryFormatted: String? = null @EpoxyAttribute var areThreadMessagesEnabled: Boolean = false @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) var listener: ClickListener? = null @@ -60,8 +61,7 @@ abstract class SearchResultItem : VectorEpoxyModel() { if (it.isRootThread) { showThreadSummary(holder) holder.threadSummaryCounterTextView.text = it.numberOfThreads.toString() - holder.threadSummaryInfoTextView.text = it.threadSummaryLatestTextMessage.orEmpty() - + holder.threadSummaryInfoTextView.text = threadSummaryFormatted.orEmpty() val userId = it.threadSummarySenderInfo?.userId ?: return@let val displayName = it.threadSummarySenderInfo?.displayName val avatarUrl = it.threadSummarySenderInfo?.avatarUrl 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 d5f3a74e4e..d4a6f2ee87 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 @@ -24,9 +24,11 @@ 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.Event 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.MessageContent import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent import org.matrix.android.sdk.api.session.room.model.message.MessageType @@ -120,14 +122,14 @@ class DisplayableEventFormatter @Inject constructor( EventType.CALL_CANDIDATES -> { span { } } - EventType.POLL_START -> { + EventType.POLL_START -> { timelineEvent.root.getClearContent().toModel(catchError = true)?.pollCreationInfo?.question?.question ?: stringProvider.getString(R.string.sent_a_poll) } - EventType.POLL_RESPONSE -> { + EventType.POLL_RESPONSE -> { stringProvider.getString(R.string.poll_response_room_list_preview) } - EventType.POLL_END -> { + EventType.POLL_END -> { stringProvider.getString(R.string.poll_end_room_list_preview) } else -> { @@ -139,6 +141,98 @@ class DisplayableEventFormatter @Inject constructor( } } + fun formatThreadSummary( + event: Event?, + latestEdition: String? = null): CharSequence { + event ?: return "" + + // There event have been edited + if (latestEdition != null) { + return run { + val localFormattedBody = htmlRenderer.get().parse(latestEdition) as Document + val renderedBody = htmlRenderer.get().render(localFormattedBody) ?: latestEdition + renderedBody + } + } + + // The event have been redacted + if (event.isRedacted()) { + return noticeEventFormatter.formatRedactedEvent(event) + } + + // The event is encrypted + if (event.isEncrypted() && + event.mxDecryptionResult == null) { + return stringProvider.getString(R.string.encrypted_message) + } + + return when (event.getClearType()) { + EventType.MESSAGE -> { + (event.getClearContent().toModel() as? MessageContent)?.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 + renderedBody + } else { + body + } + } + MessageType.MSGTYPE_VERIFICATION_REQUEST -> { + stringProvider.getString(R.string.verification_request) + } + MessageType.MSGTYPE_IMAGE -> { + stringProvider.getString(R.string.sent_an_image) + } + MessageType.MSGTYPE_AUDIO -> { + if ((messageContent as? MessageAudioContent)?.voiceMessageIndicator != null) { + stringProvider.getString(R.string.sent_a_voice_message) + } else { + stringProvider.getString(R.string.sent_an_audio_file) + } + } + MessageType.MSGTYPE_VIDEO -> { + stringProvider.getString(R.string.sent_a_video) + } + MessageType.MSGTYPE_FILE -> { + stringProvider.getString(R.string.sent_a_file) + } + MessageType.MSGTYPE_LOCATION -> { + stringProvider.getString(R.string.sent_location) + } + else -> { + messageContent.body + } + } + } ?: span { } + } + EventType.STICKER -> { + stringProvider.getString(R.string.send_a_sticker) + } + EventType.REACTION -> { + event.getClearContent().toModel()?.relatesTo?.let { + emojiSpanify.spanify(stringProvider.getString(R.string.sent_a_reaction, it.key)) + } ?: span { } + } + EventType.POLL_START -> { + event.getClearContent().toModel(catchError = true)?.pollCreationInfo?.question?.question + ?: stringProvider.getString(R.string.sent_a_poll) + } + EventType.POLL_RESPONSE -> { + stringProvider.getString(R.string.poll_response_room_list_preview) + } + EventType.POLL_END -> { + stringProvider.getString(R.string.poll_end_room_list_preview) + } + else -> { + span { + } + } + } + } + private fun simpleFormat(senderName: String, body: CharSequence, appendAuthor: Boolean): CharSequence { return if (appendAuthor) { span { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageItemAttributesFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageItemAttributesFactory.kt index 845b765101..ef42e32a76 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageItemAttributesFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageItemAttributesFactory.kt @@ -22,6 +22,7 @@ import im.vector.app.core.resources.UserPreferencesProvider import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.room.detail.timeline.MessageColorProvider import im.vector.app.features.home.room.detail.timeline.TimelineEventController +import im.vector.app.features.home.room.detail.timeline.format.DisplayableEventFormatter import im.vector.app.features.home.room.detail.timeline.item.AbsMessageItem import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData import org.matrix.android.sdk.api.session.threads.ThreadDetails @@ -32,6 +33,7 @@ class MessageItemAttributesFactory @Inject constructor( private val messageColorProvider: MessageColorProvider, private val avatarSizeProvider: AvatarSizeProvider, private val stringProvider: StringProvider, + private val displayableEventFormatter: DisplayableEventFormatter, private val preferencesProvider: UserPreferencesProvider, private val emojiCompatFontProvider: EmojiCompatFontProvider) { @@ -59,6 +61,7 @@ class MessageItemAttributesFactory @Inject constructor( readReceiptsCallback = callback, emojiTypeFace = emojiCompatFontProvider.typeface, decryptionErrorMessage = stringProvider.getString(R.string.encrypted_message), + threadSummaryFormatted = displayableEventFormatter.formatThreadSummary(threadDetails?.threadSummaryLatestEvent).toString(), threadDetails = threadDetails, areThreadMessagesEnabled = preferencesProvider.areThreadMessagesEnabled() ) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageItem.kt index 9e8f86c26e..bad29bd444 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageItem.kt @@ -115,7 +115,7 @@ abstract class AbsMessageItem : AbsBaseMessageItem attributes.threadDetails?.let { threadDetails -> holder.threadSummaryConstraintLayout.isVisible = threadDetails.isRootThread holder.threadSummaryCounterTextView.text = threadDetails.numberOfThreads.toString() - holder.threadSummaryInfoTextView.text = threadDetails.threadSummaryLatestTextMessage ?: attributes.decryptionErrorMessage + holder.threadSummaryInfoTextView.text = attributes.threadSummaryFormatted ?: attributes.decryptionErrorMessage val userId = threadDetails.threadSummarySenderInfo?.userId ?: return@let val displayName = threadDetails.threadSummarySenderInfo?.displayName @@ -183,6 +183,7 @@ abstract class AbsMessageItem : AbsBaseMessageItem override val readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null, val emojiTypeFace: Typeface? = null, val decryptionErrorMessage: String? = null, + val threadSummaryFormatted: String? = null, val threadDetails: ThreadDetails? = null, val areThreadMessagesEnabled: Boolean = false ) : AbsBaseMessageItem.Attributes { diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListController.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListController.kt index d3a5497d63..aeef69c6dc 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListController.kt @@ -17,11 +17,11 @@ package im.vector.app.features.home.room.threads.list.viewmodel import com.airbnb.epoxy.EpoxyController -import im.vector.app.R import im.vector.app.core.date.DateFormatKind import im.vector.app.core.date.VectorDateFormatter import im.vector.app.core.resources.StringProvider import im.vector.app.features.home.AvatarRenderer +import im.vector.app.features.home.room.detail.timeline.format.DisplayableEventFormatter import im.vector.app.features.home.room.threads.list.model.threadListItem import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummary @@ -35,6 +35,7 @@ class ThreadListController @Inject constructor( private val avatarRenderer: AvatarRenderer, private val stringProvider: StringProvider, private val dateFormatter: VectorDateFormatter, + private val displayableEventFormatter: DisplayableEventFormatter, private val session: Session ) : EpoxyController() { @@ -70,9 +71,18 @@ class ThreadListController @Inject constructor( } ?.forEach { threadSummary -> val date = dateFormatter.format(threadSummary.latestEvent?.originServerTs, DateFormatKind.ROOM_LIST) - val decryptionErrorMessage = stringProvider.getString(R.string.encrypted_message) - val rootThreadEdition = threadSummary.threadEditions.rootThreadEdition - val latestThreadEdition = threadSummary.threadEditions.latestThreadEdition + val lastMessageFormatted = threadSummary.let { + displayableEventFormatter.formatThreadSummary( + event = it.latestEvent, + latestEdition = it.threadEditions.latestThreadEdition + ).toString() + } + val rootMessageFormatted = threadSummary.let { + displayableEventFormatter.formatThreadSummary( + event = it.rootEvent, + latestEdition = it.threadEditions.rootThreadEdition + ).toString() + } threadListItem { id(threadSummary.rootEvent?.eventId) avatarRenderer(host.avatarRenderer) @@ -82,8 +92,8 @@ class ThreadListController @Inject constructor( rootMessageDeleted(threadSummary.rootEvent?.isRedacted() ?: false) // TODO refactor notifications that with the new thread summary threadNotificationState(threadSummary.rootEvent?.threadDetails?.threadNotificationState ?: ThreadNotificationState.NO_NEW_MESSAGE) - rootMessage(rootThreadEdition ?: threadSummary.rootEvent?.getDecryptedTextSummary() ?: decryptionErrorMessage) - lastMessage(latestThreadEdition ?: threadSummary.latestEvent?.getDecryptedTextSummary() ?: decryptionErrorMessage) + rootMessage(rootMessageFormatted) + lastMessage(lastMessageFormatted) lastMessageCounter(threadSummary.numberOfThreads.toString()) lastMessageMatrixItem(threadSummary.latestThreadSenderInfo.toMatrixItemOrNull()) itemClickListener { @@ -112,8 +122,18 @@ class ThreadListController @Inject constructor( } ?.forEach { timelineEvent -> val date = dateFormatter.format(timelineEvent.root.threadDetails?.lastMessageTimestamp, DateFormatKind.ROOM_LIST) - val decryptionErrorMessage = stringProvider.getString(R.string.encrypted_message) val lastRootThreadEdition = timelineEvent.root.threadDetails?.lastRootThreadEdition + val lastMessageFormatted = timelineEvent.root.threadDetails?.threadSummaryLatestEvent.let { + displayableEventFormatter.formatThreadSummary( + event = it, + ).toString() + } + val rootMessageFormatted = timelineEvent.root.let { + displayableEventFormatter.formatThreadSummary( + event = it, + latestEdition = lastRootThreadEdition + ).toString() + } threadListItem { id(timelineEvent.eventId) avatarRenderer(host.avatarRenderer) @@ -122,8 +142,8 @@ class ThreadListController @Inject constructor( date(date) rootMessageDeleted(timelineEvent.root.isRedacted()) threadNotificationState(timelineEvent.root.threadDetails?.threadNotificationState ?: ThreadNotificationState.NO_NEW_MESSAGE) - rootMessage(lastRootThreadEdition ?: timelineEvent.root.getDecryptedTextSummary() ?: decryptionErrorMessage) - lastMessage(timelineEvent.root.threadDetails?.threadSummaryLatestTextMessage ?: decryptionErrorMessage) + rootMessage(rootMessageFormatted) + lastMessage(lastMessageFormatted) lastMessageCounter(timelineEvent.root.threadDetails?.numberOfThreads.toString()) lastMessageMatrixItem(timelineEvent.root.threadDetails?.threadSummarySenderInfo?.toMatrixItem()) itemClickListener {