Add Markdown support to thread summaries and thread list

This commit is contained in:
ariskotsomitopoulos 2022-03-02 13:47:08 +02:00
parent eda723c230
commit 214e0efcd9
8 changed files with 139 additions and 17 deletions

View file

@ -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,

View file

@ -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
)

View file

@ -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<SearchViewState>() {
@ -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) }

View file

@ -42,6 +42,7 @@ abstract class SearchResultItem : VectorEpoxyModel<SearchResultItem.Holder>() {
@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<SearchResultItem.Holder>() {
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

View file

@ -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<MessagePollContent>(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<ReactionContent>()?.relatesTo?.let {
emojiSpanify.spanify(stringProvider.getString(R.string.sent_a_reaction, it.key))
} ?: span { }
}
EventType.POLL_START -> {
event.getClearContent().toModel<MessagePollContent>(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 {

View file

@ -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()
)

View file

@ -115,7 +115,7 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : AbsBaseMessageItem<H>
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<H : AbsMessageItem.Holder> : AbsBaseMessageItem<H>
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 {

View file

@ -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 {