Show start event of live without conditions

This commit is contained in:
Maxime NATUREL 2022-04-07 17:06:08 +02:00
parent e93e51d03c
commit aabfc81816
5 changed files with 341 additions and 305 deletions

View file

@ -18,12 +18,27 @@ package org.matrix.android.sdk.api.session.room.model.livelocation
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import org.matrix.android.sdk.api.session.events.model.Content
import org.matrix.android.sdk.api.session.room.model.message.LocationAsset import org.matrix.android.sdk.api.session.room.model.message.LocationAsset
import org.matrix.android.sdk.api.session.room.model.message.LocationAssetType import org.matrix.android.sdk.api.session.room.model.message.LocationAssetType
import org.matrix.android.sdk.api.session.room.model.message.MessageLiveLocationContent import org.matrix.android.sdk.api.session.room.model.message.MessageLiveLocationContent
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
import org.matrix.android.sdk.api.session.room.model.message.MessageType
import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class LiveLocationBeaconContent( data class LiveLocationBeaconContent(
// TODO check if there is a better way than implementing MessageContent
/**
* Local message type, not from server
*/
@Transient
override val msgType: String = MessageType.MSGTYPE_LIVE_LOCATION_STATE,
@Json(name = "body") override val body: String = "",
@Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,
@Json(name = "m.new_content") override val newContent: Content? = null,
/** /**
* Indicates user's intent to share ephemeral location. * Indicates user's intent to share ephemeral location.
*/ */
@ -44,7 +59,7 @@ data class LiveLocationBeaconContent(
* Client side tracking of the last location * Client side tracking of the last location
*/ */
var lastLocationContent: MessageLiveLocationContent? = null var lastLocationContent: MessageLiveLocationContent? = null
) { ) : MessageContent {
fun getBestBeaconInfo() = beaconInfo ?: unstableBeaconInfo fun getBestBeaconInfo() = beaconInfo ?: unstableBeaconInfo

View file

@ -41,5 +41,6 @@ object MessageType {
const val MSGTYPE_SNOWFALL = "io.element.effect.snowfall" const val MSGTYPE_SNOWFALL = "io.element.effect.snowfall"
// Fake message types for live location events to be able to inherit them from MessageContent // Fake message types for live location events to be able to inherit them from MessageContent
const val MSGTYPE_LIVE_LOCATION_STATE = "org.matrix.android.sdk.livelocation.state"
const val MSGTYPE_LIVE_LOCATION = "org.matrix.android.sdk.livelocation" const val MSGTYPE_LIVE_LOCATION = "org.matrix.android.sdk.livelocation"
} }

View file

@ -29,6 +29,7 @@ import org.matrix.android.sdk.api.session.events.model.isSticker
import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.model.EventAnnotationsSummary 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.ReadReceipt
import org.matrix.android.sdk.api.session.room.model.livelocation.LiveLocationBeaconContent
import org.matrix.android.sdk.api.session.room.model.message.MessageContent 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.MessagePollContent
import org.matrix.android.sdk.api.session.room.model.message.MessageStickerContent import org.matrix.android.sdk.api.session.room.model.message.MessageStickerContent
@ -136,9 +137,10 @@ fun TimelineEvent.getEditedEventId(): String? {
*/ */
fun TimelineEvent.getLastMessageContent(): MessageContent? { fun TimelineEvent.getLastMessageContent(): MessageContent? {
return when (root.getClearType()) { return when (root.getClearType()) {
EventType.STICKER -> root.getClearContent().toModel<MessageStickerContent>() EventType.STICKER -> root.getClearContent().toModel<MessageStickerContent>()
in EventType.POLL_START -> (annotations?.editSummary?.latestContent ?: root.getClearContent()).toModel<MessagePollContent>() in EventType.POLL_START -> (annotations?.editSummary?.latestContent ?: root.getClearContent()).toModel<MessagePollContent>()
else -> (annotations?.editSummary?.latestContent ?: root.getClearContent()).toModel() in EventType.STATE_ROOM_BEACON_INFO -> (annotations?.editSummary?.latestContent ?: root.getClearContent()).toModel<LiveLocationBeaconContent>()
else -> (annotations?.editSummary?.latestContent ?: root.getClearContent()).toModel()
} }
} }

View file

@ -50,6 +50,7 @@ import im.vector.app.features.home.room.detail.timeline.item.MessageFileItem_
import im.vector.app.features.home.room.detail.timeline.item.MessageImageVideoItem import im.vector.app.features.home.room.detail.timeline.item.MessageImageVideoItem
import im.vector.app.features.home.room.detail.timeline.item.MessageImageVideoItem_ import im.vector.app.features.home.room.detail.timeline.item.MessageImageVideoItem_
import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData
import im.vector.app.features.home.room.detail.timeline.item.MessageLiveLocationStartItem_
import im.vector.app.features.home.room.detail.timeline.item.MessageLocationItem import im.vector.app.features.home.room.detail.timeline.item.MessageLocationItem
import im.vector.app.features.home.room.detail.timeline.item.MessageLocationItem_ import im.vector.app.features.home.room.detail.timeline.item.MessageLocationItem_
import im.vector.app.features.home.room.detail.timeline.item.MessageTextItem import im.vector.app.features.home.room.detail.timeline.item.MessageTextItem
@ -96,6 +97,7 @@ import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.events.model.RelationType import org.matrix.android.sdk.api.session.events.model.RelationType
import org.matrix.android.sdk.api.session.events.model.isThread import org.matrix.android.sdk.api.session.events.model.isThread
import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.model.livelocation.LiveLocationBeaconContent
import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent 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.MessageContent
import org.matrix.android.sdk.api.session.room.model.message.MessageContentWithFormattedBody import org.matrix.android.sdk.api.session.room.model.message.MessageContentWithFormattedBody
@ -121,30 +123,30 @@ import org.matrix.android.sdk.internal.database.lightweight.LightweightSettingsS
import javax.inject.Inject import javax.inject.Inject
class MessageItemFactory @Inject constructor( class MessageItemFactory @Inject constructor(
private val localFilesHelper: LocalFilesHelper, private val localFilesHelper: LocalFilesHelper,
private val colorProvider: ColorProvider, private val colorProvider: ColorProvider,
private val dimensionConverter: DimensionConverter, private val dimensionConverter: DimensionConverter,
private val timelineMediaSizeProvider: TimelineMediaSizeProvider, private val timelineMediaSizeProvider: TimelineMediaSizeProvider,
private val htmlRenderer: Lazy<EventHtmlRenderer>, private val htmlRenderer: Lazy<EventHtmlRenderer>,
private val htmlCompressor: VectorHtmlCompressor, private val htmlCompressor: VectorHtmlCompressor,
private val textRendererFactory: EventTextRenderer.Factory, private val textRendererFactory: EventTextRenderer.Factory,
private val stringProvider: StringProvider, private val stringProvider: StringProvider,
private val imageContentRenderer: ImageContentRenderer, private val imageContentRenderer: ImageContentRenderer,
private val messageInformationDataFactory: MessageInformationDataFactory, private val messageInformationDataFactory: MessageInformationDataFactory,
private val messageItemAttributesFactory: MessageItemAttributesFactory, private val messageItemAttributesFactory: MessageItemAttributesFactory,
private val contentUploadStateTrackerBinder: ContentUploadStateTrackerBinder, private val contentUploadStateTrackerBinder: ContentUploadStateTrackerBinder,
private val contentDownloadStateTrackerBinder: ContentDownloadStateTrackerBinder, private val contentDownloadStateTrackerBinder: ContentDownloadStateTrackerBinder,
private val defaultItemFactory: DefaultItemFactory, private val defaultItemFactory: DefaultItemFactory,
private val noticeItemFactory: NoticeItemFactory, private val noticeItemFactory: NoticeItemFactory,
private val avatarSizeProvider: AvatarSizeProvider, private val avatarSizeProvider: AvatarSizeProvider,
private val pillsPostProcessorFactory: PillsPostProcessor.Factory, private val pillsPostProcessorFactory: PillsPostProcessor.Factory,
private val lightweightSettingsStorage: LightweightSettingsStorage, private val lightweightSettingsStorage: LightweightSettingsStorage,
private val spanUtils: SpanUtils, private val spanUtils: SpanUtils,
private val session: Session, private val session: Session,
private val audioMessagePlaybackTracker: AudioMessagePlaybackTracker, private val audioMessagePlaybackTracker: AudioMessagePlaybackTracker,
private val locationPinProvider: LocationPinProvider, private val locationPinProvider: LocationPinProvider,
private val vectorPreferences: VectorPreferences, private val vectorPreferences: VectorPreferences,
private val urlMapProvider: UrlMapProvider, private val urlMapProvider: UrlMapProvider,
) { ) {
// TODO inject this properly? // TODO inject this properly?
@ -179,7 +181,7 @@ class MessageItemFactory @Inject constructor(
return defaultItemFactory.create(malformedText, informationData, highlight, callback) return defaultItemFactory.create(malformedText, informationData, highlight, callback)
} }
if (messageContent.relatesTo?.type == RelationType.REPLACE || if (messageContent.relatesTo?.type == RelationType.REPLACE ||
event.isEncrypted() && event.root.content.toModel<EncryptedEventContent>()?.relatesTo?.type == RelationType.REPLACE event.isEncrypted() && event.root.content.toModel<EncryptedEventContent>()?.relatesTo?.type == RelationType.REPLACE
) { ) {
// This is an edit event, we should display it when debugging as a notice event // This is an edit event, we should display it when debugging as a notice event
return noticeItemFactory.create(params) return noticeItemFactory.create(params)
@ -196,23 +198,36 @@ class MessageItemFactory @Inject constructor(
// val all = event.root.toContent() // val all = event.root.toContent()
// val ev = all.toModel<Event>() // val ev = all.toModel<Event>()
val messageItem = when (messageContent) { val messageItem = when (messageContent) {
is MessageEmoteContent -> buildEmoteMessageItem(messageContent, informationData, highlight, callback, attributes) is MessageEmoteContent -> buildEmoteMessageItem(messageContent, informationData, highlight, callback, attributes)
is MessageTextContent -> buildItemForTextContent(messageContent, informationData, highlight, callback, attributes) is MessageTextContent -> buildItemForTextContent(messageContent, informationData, highlight, callback, attributes)
is MessageImageInfoContent -> buildImageMessageItem(messageContent, informationData, highlight, callback, attributes) is MessageImageInfoContent -> buildImageMessageItem(messageContent, informationData, highlight, callback, attributes)
is MessageNoticeContent -> buildNoticeMessageItem(messageContent, informationData, highlight, callback, attributes) is MessageNoticeContent -> buildNoticeMessageItem(messageContent, informationData, highlight, callback, attributes)
is MessageVideoContent -> buildVideoMessageItem(messageContent, informationData, highlight, callback, attributes) is MessageVideoContent -> buildVideoMessageItem(messageContent, informationData, highlight, callback, attributes)
is MessageFileContent -> buildFileMessageItem(messageContent, highlight, attributes) is MessageFileContent -> buildFileMessageItem(messageContent, highlight, attributes)
is MessageAudioContent -> buildAudioContent(params, messageContent, informationData, highlight, attributes) is MessageAudioContent -> buildAudioContent(params, messageContent, informationData, highlight, attributes)
is MessageVerificationRequestContent -> buildVerificationRequestMessageItem(messageContent, informationData, highlight, callback, attributes) is MessageVerificationRequestContent -> buildVerificationRequestMessageItem(messageContent, informationData, highlight, callback, attributes)
is MessagePollContent -> buildPollItem(messageContent, informationData, highlight, callback, attributes) is MessagePollContent -> buildPollItem(messageContent, informationData, highlight, callback, attributes)
is MessageLocationContent -> { is MessageLocationContent -> {
if (vectorPreferences.labsRenderLocationsInTimeline()) { if (vectorPreferences.labsRenderLocationsInTimeline()) {
buildLocationItem(messageContent, informationData, highlight, attributes) buildLocationItem(messageContent, informationData, highlight, attributes)
} else { } else {
buildMessageTextItem(messageContent.body, false, informationData, highlight, callback, attributes) buildMessageTextItem(messageContent.body, false, informationData, highlight, callback, attributes)
} }
} }
else -> buildNotHandledMessageItem(messageContent, informationData, highlight, callback, attributes) is LiveLocationBeaconContent -> {
// TODO extract in method and in a dedicated factory class
// TODO check if it is still live and that the timeout has not elapsed
val width = timelineMediaSizeProvider.getMaxSize().first
val height = dimensionConverter.dpToPx(200)
return MessageLiveLocationStartItem_()
.attributes(attributes)
.mapWidth(width)
.mapHeight(height)
.highlighted(highlight)
.leftGuideline(avatarSizeProvider.leftGuideline)
}
else -> buildNotHandledMessageItem(messageContent, informationData, highlight, callback, attributes)
} }
return messageItem?.apply { return messageItem?.apply {
layout(informationData.messageLayout.layoutRes) layout(informationData.messageLayout.layoutRes)
@ -220,10 +235,10 @@ class MessageItemFactory @Inject constructor(
} }
private fun buildLocationItem( private fun buildLocationItem(
locationContent: MessageLocationContent, locationContent: MessageLocationContent,
informationData: MessageInformationData, informationData: MessageInformationData,
highlight: Boolean, highlight: Boolean,
attributes: AbsMessageItem.Attributes, attributes: AbsMessageItem.Attributes,
): MessageLocationItem? { ): MessageLocationItem? {
val width = timelineMediaSizeProvider.getMaxSize().first val width = timelineMediaSizeProvider.getMaxSize().first
val height = dimensionConverter.dpToPx(200) val height = dimensionConverter.dpToPx(200)
@ -235,22 +250,22 @@ class MessageItemFactory @Inject constructor(
val userId = if (locationContent.isSelfLocation()) informationData.senderId else null val userId = if (locationContent.isSelfLocation()) informationData.senderId else null
return MessageLocationItem_() return MessageLocationItem_()
.attributes(attributes) .attributes(attributes)
.locationUrl(locationUrl) .locationUrl(locationUrl)
.mapWidth(width) .mapWidth(width)
.mapHeight(height) .mapHeight(height)
.userId(userId) .userId(userId)
.locationPinProvider(locationPinProvider) .locationPinProvider(locationPinProvider)
.highlighted(highlight) .highlighted(highlight)
.leftGuideline(avatarSizeProvider.leftGuideline) .leftGuideline(avatarSizeProvider.leftGuideline)
} }
private fun buildPollItem( private fun buildPollItem(
pollContent: MessagePollContent, pollContent: MessagePollContent,
informationData: MessageInformationData, informationData: MessageInformationData,
highlight: Boolean, highlight: Boolean,
callback: TimelineEventController.Callback?, callback: TimelineEventController.Callback?,
attributes: AbsMessageItem.Attributes, attributes: AbsMessageItem.Attributes,
): PollItem { ): PollItem {
val pollResponseSummary = informationData.pollResponseAggregatedSummary val pollResponseSummary = informationData.pollResponseAggregatedSummary
val pollState = createPollState(informationData, pollResponseSummary, pollContent) val pollState = createPollState(informationData, pollResponseSummary, pollContent)
@ -261,16 +276,16 @@ class MessageItemFactory @Inject constructor(
val totalVotesText = createTotalVotesText(pollState, pollResponseSummary) val totalVotesText = createTotalVotesText(pollState, pollResponseSummary)
return PollItem_() return PollItem_()
.attributes(attributes) .attributes(attributes)
.eventId(informationData.eventId) .eventId(informationData.eventId)
.pollQuestion(question) .pollQuestion(question)
.canVote(pollState.isVotable()) .canVote(pollState.isVotable())
.totalVotesText(totalVotesText) .totalVotesText(totalVotesText)
.optionViewStates(optionViewStates) .optionViewStates(optionViewStates)
.edited(informationData.hasBeenEdited) .edited(informationData.hasBeenEdited)
.highlighted(highlight) .highlighted(highlight)
.leftGuideline(avatarSizeProvider.leftGuideline) .leftGuideline(avatarSizeProvider.leftGuideline)
.callback(callback) .callback(callback)
} }
private fun createPollState( private fun createPollState(
@ -278,11 +293,11 @@ class MessageItemFactory @Inject constructor(
pollResponseSummary: PollResponseData?, pollResponseSummary: PollResponseData?,
pollContent: MessagePollContent, pollContent: MessagePollContent,
): PollState = when { ): PollState = when {
!informationData.sendState.isSent() -> Sending !informationData.sendState.isSent() -> Sending
pollResponseSummary?.isClosed.orFalse() -> Ended pollResponseSummary?.isClosed.orFalse() -> Ended
pollContent.getBestPollCreationInfo()?.kind == PollType.UNDISCLOSED -> Undisclosed pollContent.getBestPollCreationInfo()?.kind == PollType.UNDISCLOSED -> Undisclosed
pollResponseSummary?.myVote?.isNotEmpty().orFalse() -> Voted(pollResponseSummary?.totalVotes ?: 0) pollResponseSummary?.myVote?.isNotEmpty().orFalse() -> Voted(pollResponseSummary?.totalVotes ?: 0)
else -> Ready else -> Ready
} }
private fun List<PollAnswer>.mapToOptions( private fun List<PollAnswer>.mapToOptions(
@ -300,11 +315,11 @@ class MessageItemFactory @Inject constructor(
val isWinner = winnerVoteCount != 0 && voteCount == winnerVoteCount val isWinner = winnerVoteCount != 0 && voteCount == winnerVoteCount
when (pollState) { when (pollState) {
Sending -> PollSending(optionId, optionAnswer) Sending -> PollSending(optionId, optionAnswer)
Ready -> PollReady(optionId, optionAnswer) Ready -> PollReady(optionId, optionAnswer)
is Voted -> PollVoted(optionId, optionAnswer, voteCount, votePercentage, isMyVote) is Voted -> PollVoted(optionId, optionAnswer, voteCount, votePercentage, isMyVote)
Undisclosed -> PollUndisclosed(optionId, optionAnswer, isMyVote) Undisclosed -> PollUndisclosed(optionId, optionAnswer, isMyVote)
Ended -> PollEnded(optionId, optionAnswer, voteCount, votePercentage, isWinner) Ended -> PollEnded(optionId, optionAnswer, voteCount, votePercentage, isWinner)
} }
} }
@ -324,11 +339,11 @@ class MessageItemFactory @Inject constructor(
): String { ): String {
val votes = pollResponseSummary?.totalVotes ?: 0 val votes = pollResponseSummary?.totalVotes ?: 0
return when { return when {
pollState is Ended -> stringProvider.getQuantityString(R.plurals.poll_total_vote_count_after_ended, votes, votes) pollState is Ended -> stringProvider.getQuantityString(R.plurals.poll_total_vote_count_after_ended, votes, votes)
pollState is Undisclosed -> "" pollState is Undisclosed -> ""
pollState is Voted -> stringProvider.getQuantityString(R.plurals.poll_total_vote_count_before_ended_and_voted, votes, votes) pollState is Voted -> stringProvider.getQuantityString(R.plurals.poll_total_vote_count_before_ended_and_voted, votes, votes)
votes == 0 -> stringProvider.getString(R.string.poll_no_votes_cast) votes == 0 -> stringProvider.getString(R.string.poll_no_votes_cast)
else -> stringProvider.getQuantityString(R.plurals.poll_total_vote_count_before_ended_and_not_voted, votes, votes) else -> stringProvider.getQuantityString(R.plurals.poll_total_vote_count_before_ended_and_not_voted, votes, votes)
} }
} }
@ -403,27 +418,27 @@ class MessageItemFactory @Inject constructor(
} }
return MessageVoiceItem_() return MessageVoiceItem_()
.attributes(attributes) .attributes(attributes)
.duration(messageContent.audioWaveformInfo?.duration ?: 0) .duration(messageContent.audioWaveformInfo?.duration ?: 0)
.waveform(messageContent.audioWaveformInfo?.waveform?.toFft().orEmpty()) .waveform(messageContent.audioWaveformInfo?.waveform?.toFft().orEmpty())
.playbackControlButtonClickListener(playbackControlButtonClickListener) .playbackControlButtonClickListener(playbackControlButtonClickListener)
.waveformTouchListener(waveformTouchListener) .waveformTouchListener(waveformTouchListener)
.audioMessagePlaybackTracker(audioMessagePlaybackTracker) .audioMessagePlaybackTracker(audioMessagePlaybackTracker)
.isLocalFile(localFilesHelper.isLocalFile(fileUrl)) .isLocalFile(localFilesHelper.isLocalFile(fileUrl))
.mxcUrl(fileUrl) .mxcUrl(fileUrl)
.contentUploadStateTrackerBinder(contentUploadStateTrackerBinder) .contentUploadStateTrackerBinder(contentUploadStateTrackerBinder)
.contentDownloadStateTrackerBinder(contentDownloadStateTrackerBinder) .contentDownloadStateTrackerBinder(contentDownloadStateTrackerBinder)
.highlighted(highlight) .highlighted(highlight)
.leftGuideline(avatarSizeProvider.leftGuideline) .leftGuideline(avatarSizeProvider.leftGuideline)
} }
private fun buildVerificationRequestMessageItem( private fun buildVerificationRequestMessageItem(
messageContent: MessageVerificationRequestContent, messageContent: MessageVerificationRequestContent,
@Suppress("UNUSED_PARAMETER") @Suppress("UNUSED_PARAMETER")
informationData: MessageInformationData, informationData: MessageInformationData,
highlight: Boolean, highlight: Boolean,
callback: TimelineEventController.Callback?, callback: TimelineEventController.Callback?,
attributes: AbsMessageItem.Attributes, attributes: AbsMessageItem.Attributes,
): VerificationRequestItem? { ): VerificationRequestItem? {
// If this request is not sent by me or sent to me, we should ignore it in timeline // If this request is not sent by me or sent to me, we should ignore it in timeline
val myUserId = session.myUserId val myUserId = session.myUserId
@ -438,44 +453,44 @@ class MessageItemFactory @Inject constructor(
informationData.memberName informationData.memberName
} }
return VerificationRequestItem_() return VerificationRequestItem_()
.attributes( .attributes(
VerificationRequestItem.Attributes( VerificationRequestItem.Attributes(
otherUserId = otherUserId, otherUserId = otherUserId,
otherUserName = otherUserName.toString(), otherUserName = otherUserName.toString(),
referenceId = informationData.eventId, referenceId = informationData.eventId,
informationData = informationData, informationData = informationData,
avatarRenderer = attributes.avatarRenderer, avatarRenderer = attributes.avatarRenderer,
messageColorProvider = attributes.messageColorProvider, messageColorProvider = attributes.messageColorProvider,
itemLongClickListener = attributes.itemLongClickListener, itemLongClickListener = attributes.itemLongClickListener,
itemClickListener = attributes.itemClickListener, itemClickListener = attributes.itemClickListener,
reactionPillCallback = attributes.reactionPillCallback, reactionPillCallback = attributes.reactionPillCallback,
readReceiptsCallback = attributes.readReceiptsCallback, readReceiptsCallback = attributes.readReceiptsCallback,
emojiTypeFace = attributes.emojiTypeFace, emojiTypeFace = attributes.emojiTypeFace,
reactionsSummaryEvents = attributes.reactionsSummaryEvents, reactionsSummaryEvents = attributes.reactionsSummaryEvents,
)
) )
) .callback(callback)
.callback(callback) .highlighted(highlight)
.highlighted(highlight) .leftGuideline(avatarSizeProvider.leftGuideline)
.leftGuideline(avatarSizeProvider.leftGuideline)
} }
private fun buildFileMessageItem( private fun buildFileMessageItem(
messageContent: MessageFileContent, messageContent: MessageFileContent,
highlight: Boolean, highlight: Boolean,
attributes: AbsMessageItem.Attributes, attributes: AbsMessageItem.Attributes,
): MessageFileItem { ): MessageFileItem {
val mxcUrl = messageContent.getFileUrl() ?: "" val mxcUrl = messageContent.getFileUrl() ?: ""
return MessageFileItem_() return MessageFileItem_()
.attributes(attributes) .attributes(attributes)
.leftGuideline(avatarSizeProvider.leftGuideline) .leftGuideline(avatarSizeProvider.leftGuideline)
.isLocalFile(localFilesHelper.isLocalFile(messageContent.getFileUrl())) .isLocalFile(localFilesHelper.isLocalFile(messageContent.getFileUrl()))
.isDownloaded(session.fileService().isFileInCache(messageContent)) .isDownloaded(session.fileService().isFileInCache(messageContent))
.mxcUrl(mxcUrl) .mxcUrl(mxcUrl)
.contentUploadStateTrackerBinder(contentUploadStateTrackerBinder) .contentUploadStateTrackerBinder(contentUploadStateTrackerBinder)
.contentDownloadStateTrackerBinder(contentDownloadStateTrackerBinder) .contentDownloadStateTrackerBinder(contentDownloadStateTrackerBinder)
.highlighted(highlight) .highlighted(highlight)
.filename(messageContent.body) .filename(messageContent.body)
.iconRes(R.drawable.ic_paperclip) .iconRes(R.drawable.ic_paperclip)
} }
private fun buildAudioContent( private fun buildAudioContent(
@ -485,10 +500,10 @@ class MessageItemFactory @Inject constructor(
highlight: Boolean, highlight: Boolean,
attributes: AbsMessageItem.Attributes, attributes: AbsMessageItem.Attributes,
) = if (messageContent.voiceMessageIndicator != null) { ) = if (messageContent.voiceMessageIndicator != null) {
buildVoiceMessageItem(params, messageContent, informationData, highlight, attributes) buildVoiceMessageItem(params, messageContent, informationData, highlight, attributes)
} else { } else {
buildAudioMessageItem(params, messageContent, informationData, highlight, attributes) buildAudioMessageItem(params, messageContent, informationData, highlight, attributes)
} }
private fun buildNotHandledMessageItem( private fun buildNotHandledMessageItem(
messageContent: MessageContent, messageContent: MessageContent,
@ -501,95 +516,95 @@ class MessageItemFactory @Inject constructor(
} }
private fun buildImageMessageItem( private fun buildImageMessageItem(
messageContent: MessageImageInfoContent, messageContent: MessageImageInfoContent,
@Suppress("UNUSED_PARAMETER") @Suppress("UNUSED_PARAMETER")
informationData: MessageInformationData, informationData: MessageInformationData,
highlight: Boolean, highlight: Boolean,
callback: TimelineEventController.Callback?, callback: TimelineEventController.Callback?,
attributes: AbsMessageItem.Attributes, attributes: AbsMessageItem.Attributes,
): MessageImageVideoItem? { ): MessageImageVideoItem? {
val (maxWidth, maxHeight) = timelineMediaSizeProvider.getMaxSize() val (maxWidth, maxHeight) = timelineMediaSizeProvider.getMaxSize()
val data = ImageContentRenderer.Data( val data = ImageContentRenderer.Data(
eventId = informationData.eventId, eventId = informationData.eventId,
filename = messageContent.body, filename = messageContent.body,
mimeType = messageContent.mimeType, mimeType = messageContent.mimeType,
url = messageContent.getFileUrl(), url = messageContent.getFileUrl(),
elementToDecrypt = messageContent.encryptedFileInfo?.toElementToDecrypt(), elementToDecrypt = messageContent.encryptedFileInfo?.toElementToDecrypt(),
height = messageContent.info?.height, height = messageContent.info?.height,
maxHeight = maxHeight, maxHeight = maxHeight,
width = messageContent.info?.width, width = messageContent.info?.width,
maxWidth = maxWidth, maxWidth = maxWidth,
allowNonMxcUrls = informationData.sendState.isSending() allowNonMxcUrls = informationData.sendState.isSending()
) )
return MessageImageVideoItem_() return MessageImageVideoItem_()
.attributes(attributes) .attributes(attributes)
.leftGuideline(avatarSizeProvider.leftGuideline) .leftGuideline(avatarSizeProvider.leftGuideline)
.imageContentRenderer(imageContentRenderer) .imageContentRenderer(imageContentRenderer)
.contentUploadStateTrackerBinder(contentUploadStateTrackerBinder) .contentUploadStateTrackerBinder(contentUploadStateTrackerBinder)
.playable(messageContent.mimeType == MimeTypes.Gif) .playable(messageContent.mimeType == MimeTypes.Gif)
.highlighted(highlight) .highlighted(highlight)
.mediaData(data) .mediaData(data)
.apply { .apply {
if (messageContent.msgType == MessageType.MSGTYPE_STICKER_LOCAL) { if (messageContent.msgType == MessageType.MSGTYPE_STICKER_LOCAL) {
mode(ImageContentRenderer.Mode.STICKER) mode(ImageContentRenderer.Mode.STICKER)
clickListener { view -> clickListener { view ->
callback?.onImageMessageClicked(messageContent, data, view, listOf(data)) callback?.onImageMessageClicked(messageContent, data, view, listOf(data))
} }
} else { } else {
clickListener { view -> clickListener { view ->
callback?.onImageMessageClicked(messageContent, data, view, emptyList()) callback?.onImageMessageClicked(messageContent, data, view, emptyList())
}
} }
} }
}
} }
private fun buildVideoMessageItem( private fun buildVideoMessageItem(
messageContent: MessageVideoContent, messageContent: MessageVideoContent,
informationData: MessageInformationData, informationData: MessageInformationData,
highlight: Boolean, highlight: Boolean,
callback: TimelineEventController.Callback?, callback: TimelineEventController.Callback?,
attributes: AbsMessageItem.Attributes, attributes: AbsMessageItem.Attributes,
): MessageImageVideoItem? { ): MessageImageVideoItem? {
val (maxWidth, maxHeight) = timelineMediaSizeProvider.getMaxSize() val (maxWidth, maxHeight) = timelineMediaSizeProvider.getMaxSize()
val thumbnailData = ImageContentRenderer.Data( val thumbnailData = ImageContentRenderer.Data(
eventId = informationData.eventId, eventId = informationData.eventId,
filename = messageContent.body, filename = messageContent.body,
mimeType = messageContent.mimeType, mimeType = messageContent.mimeType,
url = messageContent.videoInfo?.getThumbnailUrl(), url = messageContent.videoInfo?.getThumbnailUrl(),
elementToDecrypt = messageContent.videoInfo?.thumbnailFile?.toElementToDecrypt(), elementToDecrypt = messageContent.videoInfo?.thumbnailFile?.toElementToDecrypt(),
height = messageContent.videoInfo?.height, height = messageContent.videoInfo?.height,
maxHeight = maxHeight, maxHeight = maxHeight,
width = messageContent.videoInfo?.width, width = messageContent.videoInfo?.width,
maxWidth = maxWidth, maxWidth = maxWidth,
allowNonMxcUrls = informationData.sendState.isSending() allowNonMxcUrls = informationData.sendState.isSending()
) )
val videoData = VideoContentRenderer.Data( val videoData = VideoContentRenderer.Data(
eventId = informationData.eventId, eventId = informationData.eventId,
filename = messageContent.body, filename = messageContent.body,
mimeType = messageContent.mimeType, mimeType = messageContent.mimeType,
url = messageContent.getFileUrl(), url = messageContent.getFileUrl(),
elementToDecrypt = messageContent.encryptedFileInfo?.toElementToDecrypt(), elementToDecrypt = messageContent.encryptedFileInfo?.toElementToDecrypt(),
thumbnailMediaData = thumbnailData thumbnailMediaData = thumbnailData
) )
return MessageImageVideoItem_() return MessageImageVideoItem_()
.leftGuideline(avatarSizeProvider.leftGuideline) .leftGuideline(avatarSizeProvider.leftGuideline)
.attributes(attributes) .attributes(attributes)
.imageContentRenderer(imageContentRenderer) .imageContentRenderer(imageContentRenderer)
.contentUploadStateTrackerBinder(contentUploadStateTrackerBinder) .contentUploadStateTrackerBinder(contentUploadStateTrackerBinder)
.playable(true) .playable(true)
.highlighted(highlight) .highlighted(highlight)
.mediaData(thumbnailData) .mediaData(thumbnailData)
.clickListener { view -> callback?.onVideoMessageClicked(messageContent, videoData, view.findViewById(R.id.messageThumbnailView)) } .clickListener { view -> callback?.onVideoMessageClicked(messageContent, videoData, view.findViewById(R.id.messageThumbnailView)) }
} }
private fun buildItemForTextContent( private fun buildItemForTextContent(
messageContent: MessageTextContent, messageContent: MessageTextContent,
informationData: MessageInformationData, informationData: MessageInformationData,
highlight: Boolean, highlight: Boolean,
callback: TimelineEventController.Callback?, callback: TimelineEventController.Callback?,
attributes: AbsMessageItem.Attributes, attributes: AbsMessageItem.Attributes,
): VectorEpoxyModel<*>? { ): VectorEpoxyModel<*>? {
val matrixFormattedBody = messageContent.matrixFormattedBody val matrixFormattedBody = messageContent.matrixFormattedBody
return if (matrixFormattedBody != null) { return if (matrixFormattedBody != null) {
@ -600,11 +615,11 @@ class MessageItemFactory @Inject constructor(
} }
private fun buildFormattedTextItem( private fun buildFormattedTextItem(
matrixFormattedBody: String, matrixFormattedBody: String,
informationData: MessageInformationData, informationData: MessageInformationData,
highlight: Boolean, highlight: Boolean,
callback: TimelineEventController.Callback?, callback: TimelineEventController.Callback?,
attributes: AbsMessageItem.Attributes, attributes: AbsMessageItem.Attributes,
): MessageTextItem? { ): MessageTextItem? {
val compressed = htmlCompressor.compress(matrixFormattedBody) val compressed = htmlCompressor.compress(matrixFormattedBody)
val renderedFormattedBody = htmlRenderer.get().render(compressed, pillsPostProcessor) as Spanned val renderedFormattedBody = htmlRenderer.get().render(compressed, pillsPostProcessor) as Spanned
@ -612,42 +627,42 @@ class MessageItemFactory @Inject constructor(
} }
private fun buildMessageTextItem( private fun buildMessageTextItem(
body: CharSequence, body: CharSequence,
isFormatted: Boolean, isFormatted: Boolean,
informationData: MessageInformationData, informationData: MessageInformationData,
highlight: Boolean, highlight: Boolean,
callback: TimelineEventController.Callback?, callback: TimelineEventController.Callback?,
attributes: AbsMessageItem.Attributes, attributes: AbsMessageItem.Attributes,
): MessageTextItem? { ): MessageTextItem? {
val renderedBody = textRenderer.render(body) val renderedBody = textRenderer.render(body)
val bindingOptions = spanUtils.getBindingOptions(renderedBody) val bindingOptions = spanUtils.getBindingOptions(renderedBody)
val linkifiedBody = renderedBody.linkify(callback) val linkifiedBody = renderedBody.linkify(callback)
return MessageTextItem_() return MessageTextItem_()
.message( .message(
if (informationData.hasBeenEdited) { if (informationData.hasBeenEdited) {
annotateWithEdited(linkifiedBody, callback, informationData) annotateWithEdited(linkifiedBody, callback, informationData)
} else { } else {
linkifiedBody linkifiedBody
}.toEpoxyCharSequence() }.toEpoxyCharSequence()
) )
.useBigFont(linkifiedBody.length <= MAX_NUMBER_OF_EMOJI_FOR_BIG_FONT * 2 && containsOnlyEmojis(linkifiedBody.toString())) .useBigFont(linkifiedBody.length <= MAX_NUMBER_OF_EMOJI_FOR_BIG_FONT * 2 && containsOnlyEmojis(linkifiedBody.toString()))
.bindingOptions(bindingOptions) .bindingOptions(bindingOptions)
.markwonPlugins(htmlRenderer.get().plugins) .markwonPlugins(htmlRenderer.get().plugins)
.searchForPills(isFormatted) .searchForPills(isFormatted)
.previewUrlRetriever(callback?.getPreviewUrlRetriever()) .previewUrlRetriever(callback?.getPreviewUrlRetriever())
.imageContentRenderer(imageContentRenderer) .imageContentRenderer(imageContentRenderer)
.previewUrlCallback(callback) .previewUrlCallback(callback)
.leftGuideline(avatarSizeProvider.leftGuideline) .leftGuideline(avatarSizeProvider.leftGuideline)
.attributes(attributes) .attributes(attributes)
.highlighted(highlight) .highlighted(highlight)
.movementMethod(createLinkMovementMethod(callback)) .movementMethod(createLinkMovementMethod(callback))
} }
private fun annotateWithEdited( private fun annotateWithEdited(
linkifiedBody: CharSequence, linkifiedBody: CharSequence,
callback: TimelineEventController.Callback?, callback: TimelineEventController.Callback?,
informationData: MessageInformationData, informationData: MessageInformationData,
): Spannable { ): Spannable {
val spannable = SpannableStringBuilder() val spannable = SpannableStringBuilder()
spannable.append(linkifiedBody) spannable.append(linkifiedBody)
@ -657,17 +672,17 @@ class MessageItemFactory @Inject constructor(
val editStart = spannable.lastIndexOf(editedSuffix) val editStart = spannable.lastIndexOf(editedSuffix)
val editEnd = editStart + editedSuffix.length val editEnd = editStart + editedSuffix.length
spannable.setSpan( spannable.setSpan(
ForegroundColorSpan(color), ForegroundColorSpan(color),
editStart, editStart,
editEnd, editEnd,
Spanned.SPAN_INCLUSIVE_EXCLUSIVE) Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
// Note: text size is set to 14sp // Note: text size is set to 14sp
spannable.setSpan( spannable.setSpan(
AbsoluteSizeSpan(dimensionConverter.spToPx(13)), AbsoluteSizeSpan(dimensionConverter.spToPx(13)),
editStart, editStart,
editEnd, editEnd,
Spanned.SPAN_INCLUSIVE_EXCLUSIVE) Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
spannable.setSpan(object : ClickableSpan() { spannable.setSpan(object : ClickableSpan() {
override fun onClick(widget: View) { override fun onClick(widget: View) {
@ -678,19 +693,19 @@ class MessageItemFactory @Inject constructor(
// nop // nop
} }
}, },
editStart, editStart,
editEnd, editEnd,
Spanned.SPAN_INCLUSIVE_EXCLUSIVE) Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
return spannable return spannable
} }
private fun buildNoticeMessageItem( private fun buildNoticeMessageItem(
messageContent: MessageNoticeContent, messageContent: MessageNoticeContent,
@Suppress("UNUSED_PARAMETER") @Suppress("UNUSED_PARAMETER")
informationData: MessageInformationData, informationData: MessageInformationData,
highlight: Boolean, highlight: Boolean,
callback: TimelineEventController.Callback?, callback: TimelineEventController.Callback?,
attributes: AbsMessageItem.Attributes, attributes: AbsMessageItem.Attributes,
): MessageTextItem? { ): MessageTextItem? {
val htmlBody = messageContent.getHtmlBody() val htmlBody = messageContent.getHtmlBody()
val formattedBody = span { val formattedBody = span {
@ -703,23 +718,23 @@ class MessageItemFactory @Inject constructor(
val message = formattedBody.linkify(callback) val message = formattedBody.linkify(callback)
return MessageTextItem_() return MessageTextItem_()
.leftGuideline(avatarSizeProvider.leftGuideline) .leftGuideline(avatarSizeProvider.leftGuideline)
.previewUrlRetriever(callback?.getPreviewUrlRetriever()) .previewUrlRetriever(callback?.getPreviewUrlRetriever())
.imageContentRenderer(imageContentRenderer) .imageContentRenderer(imageContentRenderer)
.previewUrlCallback(callback) .previewUrlCallback(callback)
.attributes(attributes) .attributes(attributes)
.message(message.toEpoxyCharSequence()) .message(message.toEpoxyCharSequence())
.bindingOptions(bindingOptions) .bindingOptions(bindingOptions)
.highlighted(highlight) .highlighted(highlight)
.movementMethod(createLinkMovementMethod(callback)) .movementMethod(createLinkMovementMethod(callback))
} }
private fun buildEmoteMessageItem( private fun buildEmoteMessageItem(
messageContent: MessageEmoteContent, messageContent: MessageEmoteContent,
informationData: MessageInformationData, informationData: MessageInformationData,
highlight: Boolean, highlight: Boolean,
callback: TimelineEventController.Callback?, callback: TimelineEventController.Callback?,
attributes: AbsMessageItem.Attributes, attributes: AbsMessageItem.Attributes,
): MessageTextItem? { ): MessageTextItem? {
val formattedBody = SpannableStringBuilder() val formattedBody = SpannableStringBuilder()
formattedBody.append("* ${informationData.memberName} ") formattedBody.append("* ${informationData.memberName} ")
@ -728,48 +743,48 @@ class MessageItemFactory @Inject constructor(
val message = formattedBody.linkify(callback) val message = formattedBody.linkify(callback)
return MessageTextItem_() return MessageTextItem_()
.message( .message(
if (informationData.hasBeenEdited) { if (informationData.hasBeenEdited) {
annotateWithEdited(message, callback, informationData) annotateWithEdited(message, callback, informationData)
} else { } else {
message message
}.toEpoxyCharSequence() }.toEpoxyCharSequence()
) )
.bindingOptions(bindingOptions) .bindingOptions(bindingOptions)
.leftGuideline(avatarSizeProvider.leftGuideline) .leftGuideline(avatarSizeProvider.leftGuideline)
.previewUrlRetriever(callback?.getPreviewUrlRetriever()) .previewUrlRetriever(callback?.getPreviewUrlRetriever())
.imageContentRenderer(imageContentRenderer) .imageContentRenderer(imageContentRenderer)
.previewUrlCallback(callback) .previewUrlCallback(callback)
.attributes(attributes) .attributes(attributes)
.highlighted(highlight) .highlighted(highlight)
.movementMethod(createLinkMovementMethod(callback)) .movementMethod(createLinkMovementMethod(callback))
} }
private fun MessageContentWithFormattedBody.getHtmlBody(): CharSequence { private fun MessageContentWithFormattedBody.getHtmlBody(): CharSequence {
return matrixFormattedBody return matrixFormattedBody
?.let { htmlCompressor.compress(it) } ?.let { htmlCompressor.compress(it) }
?.let { htmlRenderer.get().render(it, pillsPostProcessor) } ?.let { htmlRenderer.get().render(it, pillsPostProcessor) }
?: body ?: body
} }
private fun buildRedactedItem( private fun buildRedactedItem(
attributes: AbsMessageItem.Attributes, attributes: AbsMessageItem.Attributes,
highlight: Boolean, highlight: Boolean,
): RedactedMessageItem? { ): RedactedMessageItem? {
return RedactedMessageItem_() return RedactedMessageItem_()
.layout(attributes.informationData.messageLayout.layoutRes) .layout(attributes.informationData.messageLayout.layoutRes)
.leftGuideline(avatarSizeProvider.leftGuideline) .leftGuideline(avatarSizeProvider.leftGuideline)
.attributes(attributes) .attributes(attributes)
.highlighted(highlight) .highlighted(highlight)
} }
private fun List<Int?>?.toFft(): List<Int>? { private fun List<Int?>?.toFft(): List<Int>? {
return this return this
?.filterNotNull() ?.filterNotNull()
?.map { ?.map {
// Value comes from AudioWaveformView.MAX_FFT, and 1024 is the max value in the Matrix spec // Value comes from AudioWaveformView.MAX_FFT, and 1024 is the max value in the Matrix spec
it * AudioWaveformView.MAX_FFT / 1024 it * AudioWaveformView.MAX_FFT / 1024
} }
} }
companion object { companion object {

View file

@ -26,17 +26,19 @@ import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
class TimelineItemFactory @Inject constructor(private val messageItemFactory: MessageItemFactory, class TimelineItemFactory @Inject constructor(
private val encryptedItemFactory: EncryptedItemFactory, private val messageItemFactory: MessageItemFactory,
private val noticeItemFactory: NoticeItemFactory, private val encryptedItemFactory: EncryptedItemFactory,
private val defaultItemFactory: DefaultItemFactory, private val noticeItemFactory: NoticeItemFactory,
private val encryptionItemFactory: EncryptionItemFactory, private val defaultItemFactory: DefaultItemFactory,
private val roomCreateItemFactory: RoomCreateItemFactory, private val encryptionItemFactory: EncryptionItemFactory,
private val widgetItemFactory: WidgetItemFactory, private val roomCreateItemFactory: RoomCreateItemFactory,
private val verificationConclusionItemFactory: VerificationItemFactory, private val widgetItemFactory: WidgetItemFactory,
private val callItemFactory: CallItemFactory, private val verificationConclusionItemFactory: VerificationItemFactory,
private val decryptionFailureTracker: DecryptionFailureTracker, private val callItemFactory: CallItemFactory,
private val timelineEventVisibilityHelper: TimelineEventVisibilityHelper) { private val decryptionFailureTracker: DecryptionFailureTracker,
private val timelineEventVisibilityHelper: TimelineEventVisibilityHelper,
) {
/** /**
* Reminder: nextEvent is older and prevEvent is newer. * Reminder: nextEvent is older and prevEvent is newer.
@ -75,16 +77,17 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
EventType.STATE_ROOM_ALIASES, EventType.STATE_ROOM_ALIASES,
EventType.STATE_SPACE_CHILD, EventType.STATE_SPACE_CHILD,
EventType.STATE_SPACE_PARENT, EventType.STATE_SPACE_PARENT,
EventType.STATE_ROOM_POWER_LEVELS -> { EventType.STATE_ROOM_POWER_LEVELS -> {
noticeItemFactory.create(params) noticeItemFactory.create(params)
} }
EventType.STATE_ROOM_WIDGET_LEGACY, EventType.STATE_ROOM_WIDGET_LEGACY,
EventType.STATE_ROOM_WIDGET -> widgetItemFactory.create(params) EventType.STATE_ROOM_WIDGET -> widgetItemFactory.create(params)
EventType.STATE_ROOM_ENCRYPTION -> encryptionItemFactory.create(params) EventType.STATE_ROOM_ENCRYPTION -> encryptionItemFactory.create(params)
// State room create // State room create
EventType.STATE_ROOM_CREATE -> roomCreateItemFactory.create(params) EventType.STATE_ROOM_CREATE -> roomCreateItemFactory.create(params)
in EventType.STATE_ROOM_BEACON_INFO -> messageItemFactory.create(params)
// Unhandled state event types // Unhandled state event types
else -> { else -> {
// Should only happen when shouldShowHiddenEvents() settings is ON // Should only happen when shouldShowHiddenEvents() settings is ON
Timber.v("State event type ${event.root.type} not handled") Timber.v("State event type ${event.root.type} not handled")
defaultItemFactory.create(params) defaultItemFactory.create(params)