Merge pull request #5473 from vector-im/bugfix/eric/voting-ended-poll

Fixes ended poll voting
This commit is contained in:
Eric Decanini 2022-03-24 20:23:38 +01:00 committed by GitHub
commit 10974366fb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 481 additions and 402 deletions

1
changelog.d/5473.bugfix Normal file
View file

@ -0,0 +1 @@
Fixes polls being votable after being ended

View file

@ -482,46 +482,39 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
roomId: String, roomId: String,
isLocalEcho: Boolean) { isLocalEcho: Boolean) {
val pollEventId = content.relatesTo?.eventId ?: return val pollEventId = content.relatesTo?.eventId ?: return
val pollOwnerId = getPollEvent(roomId, pollEventId)?.root?.senderId val pollOwnerId = getPollEvent(roomId, pollEventId)?.root?.senderId
val isPollOwner = pollOwnerId == event.senderId val isPollOwner = pollOwnerId == event.senderId
val powerLevelsHelper = stateEventDataSource.getStateEvent(roomId, EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.NoCondition) val powerLevelsHelper = stateEventDataSource.getStateEvent(roomId, EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.NoCondition)
?.content?.toModel<PowerLevelsContent>() ?.content?.toModel<PowerLevelsContent>()
?.let { PowerLevelsHelper(it) } ?.let { PowerLevelsHelper(it) }
if (!isPollOwner && !powerLevelsHelper?.isUserAbleToRedact(event.senderId ?: "").orFalse()) { if (!isPollOwner && !powerLevelsHelper?.isUserAbleToRedact(event.senderId ?: "").orFalse()) {
Timber.v("## Received poll.end event $pollEventId but user ${event.senderId} doesn't have enough power level in room $roomId") Timber.v("## Received poll.end event $pollEventId but user ${event.senderId} doesn't have enough power level in room $roomId")
return return
} }
var existing = EventAnnotationsSummaryEntity.where(realm, roomId, pollEventId).findFirst() var existingPoll = EventAnnotationsSummaryEntity.where(realm, roomId, pollEventId).findFirst()
if (existing == null) { if (existingPoll == null) {
Timber.v("## POLL creating new relation summary for $pollEventId") Timber.v("## POLL creating new relation summary for $pollEventId")
existing = EventAnnotationsSummaryEntity.create(realm, roomId, pollEventId) existingPoll = EventAnnotationsSummaryEntity.create(realm, roomId, pollEventId)
} }
// we have it // we have it
val existingPollSummary = existing.pollResponseSummary val existingPollSummary = existingPoll.pollResponseSummary
?: realm.createObject(PollResponseAggregatedSummaryEntity::class.java).also { ?: realm.createObject(PollResponseAggregatedSummaryEntity::class.java).also {
existing.pollResponseSummary = it existingPoll.pollResponseSummary = it
} }
if (existingPollSummary.closedTime != null) {
Timber.v("## Received poll.end event for already ended poll $pollEventId")
return
}
val txId = event.unsignedData?.transactionId val txId = event.unsignedData?.transactionId
existingPollSummary.closedTime = event.originServerTs
// is it a remote echo? // is it a remote echo?
if (!isLocalEcho && existingPollSummary.sourceLocalEchoEvents.contains(txId)) { if (!isLocalEcho && existingPollSummary.sourceLocalEchoEvents.contains(txId)) {
// ok it has already been managed // ok it has already been managed
Timber.v("## POLL Receiving remote echo of response eventId:$pollEventId") Timber.v("## POLL Receiving remote echo of response eventId:$pollEventId")
existingPollSummary.sourceLocalEchoEvents.remove(txId) existingPollSummary.sourceLocalEchoEvents.remove(txId)
existingPollSummary.sourceEvents.add(event.eventId) existingPollSummary.sourceEvents.add(event.eventId)
return
} }
existingPollSummary.closedTime = event.originServerTs
} }
private fun getPollEvent(roomId: String, eventId: String): TimelineEvent? { private fun getPollEvent(roomId: String, eventId: String): TimelineEvent? {

View file

@ -183,7 +183,7 @@ import im.vector.app.features.notifications.NotificationDrawerManager
import im.vector.app.features.notifications.NotificationUtils import im.vector.app.features.notifications.NotificationUtils
import im.vector.app.features.permalink.NavigationInterceptor import im.vector.app.features.permalink.NavigationInterceptor
import im.vector.app.features.permalink.PermalinkHandler import im.vector.app.features.permalink.PermalinkHandler
import im.vector.app.features.poll.create.PollMode import im.vector.app.features.poll.PollMode
import im.vector.app.features.reactions.EmojiReactionPickerActivity import im.vector.app.features.reactions.EmojiReactionPickerActivity
import im.vector.app.features.roomprofile.RoomProfileActivity import im.vector.app.features.roomprofile.RoomProfileActivity
import im.vector.app.features.session.coroutineScope import im.vector.app.features.session.coroutineScope

View file

@ -56,7 +56,12 @@ import im.vector.app.features.home.room.detail.timeline.item.MessageVoiceItem
import im.vector.app.features.home.room.detail.timeline.item.MessageVoiceItem_ import im.vector.app.features.home.room.detail.timeline.item.MessageVoiceItem_
import im.vector.app.features.home.room.detail.timeline.item.PollItem import im.vector.app.features.home.room.detail.timeline.item.PollItem
import im.vector.app.features.home.room.detail.timeline.item.PollItem_ import im.vector.app.features.home.room.detail.timeline.item.PollItem_
import im.vector.app.features.home.room.detail.timeline.item.PollOptionViewState import im.vector.app.features.home.room.detail.timeline.item.PollOptionViewState.PollEnded
import im.vector.app.features.home.room.detail.timeline.item.PollOptionViewState.PollReady
import im.vector.app.features.home.room.detail.timeline.item.PollOptionViewState.PollSending
import im.vector.app.features.home.room.detail.timeline.item.PollOptionViewState.PollUndisclosed
import im.vector.app.features.home.room.detail.timeline.item.PollOptionViewState.PollVoted
import im.vector.app.features.home.room.detail.timeline.item.PollResponseData
import im.vector.app.features.home.room.detail.timeline.item.RedactedMessageItem import im.vector.app.features.home.room.detail.timeline.item.RedactedMessageItem
import im.vector.app.features.home.room.detail.timeline.item.RedactedMessageItem_ import im.vector.app.features.home.room.detail.timeline.item.RedactedMessageItem_
import im.vector.app.features.home.room.detail.timeline.item.VerificationRequestItem import im.vector.app.features.home.room.detail.timeline.item.VerificationRequestItem
@ -73,6 +78,12 @@ import im.vector.app.features.location.UrlMapProvider
import im.vector.app.features.location.toLocationData import im.vector.app.features.location.toLocationData
import im.vector.app.features.media.ImageContentRenderer import im.vector.app.features.media.ImageContentRenderer
import im.vector.app.features.media.VideoContentRenderer import im.vector.app.features.media.VideoContentRenderer
import im.vector.app.features.poll.PollState
import im.vector.app.features.poll.PollState.Ended
import im.vector.app.features.poll.PollState.Ready
import im.vector.app.features.poll.PollState.Sending
import im.vector.app.features.poll.PollState.Undisclosed
import im.vector.app.features.poll.PollState.Voted
import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.settings.VectorPreferences
import im.vector.app.features.voice.AudioWaveformView import im.vector.app.features.voice.AudioWaveformView
import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence
@ -96,6 +107,7 @@ 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.MessageType
import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationRequestContent import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationRequestContent
import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent
import org.matrix.android.sdk.api.session.room.model.message.PollAnswer
import org.matrix.android.sdk.api.session.room.model.message.PollType import org.matrix.android.sdk.api.session.room.model.message.PollType
import org.matrix.android.sdk.api.session.room.model.message.getFileName import org.matrix.android.sdk.api.session.room.model.message.getFileName
import org.matrix.android.sdk.api.session.room.model.message.getFileUrl import org.matrix.android.sdk.api.session.room.model.message.getFileUrl
@ -108,30 +120,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 voiceMessagePlaybackTracker: VoiceMessagePlaybackTracker, private val voiceMessagePlaybackTracker: VoiceMessagePlaybackTracker,
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?
@ -166,7 +178,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)
@ -180,16 +192,16 @@ class MessageItemFactory @Inject constructor(
// always hide summary when we are on thread timeline // always hide summary when we are on thread timeline
val attributes = messageItemAttributesFactory.create(messageContent, informationData, callback, params.reactionsSummaryEvents, threadDetails) val attributes = messageItemAttributesFactory.create(messageContent, informationData, callback, params.reactionsSummaryEvents, threadDetails)
// 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 -> { is MessageAudioContent -> {
if (messageContent.voiceMessageIndicator != null) { if (messageContent.voiceMessageIndicator != null) {
buildVoiceMessageItem(params, messageContent, informationData, highlight, attributes) buildVoiceMessageItem(params, messageContent, informationData, highlight, attributes)
} else { } else {
@ -197,25 +209,27 @@ class MessageItemFactory @Inject constructor(
} }
} }
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) else -> buildNotHandledMessageItem(messageContent, informationData, highlight, callback, attributes)
} }
return messageItem?.apply { return messageItem?.apply {
layout(informationData.messageLayout.layoutRes) layout(informationData.messageLayout.layoutRes)
} }
} }
private fun buildLocationItem(locationContent: MessageLocationContent, private fun buildLocationItem(
informationData: MessageInformationData, locationContent: MessageLocationContent,
highlight: Boolean, informationData: MessageInformationData,
attributes: AbsMessageItem.Attributes): MessageLocationItem? { highlight: Boolean,
attributes: AbsMessageItem.Attributes,
): MessageLocationItem? {
val width = timelineMediaSizeProvider.getMaxSize().first val width = timelineMediaSizeProvider.getMaxSize().first
val height = dimensionConverter.dpToPx(200) val height = dimensionConverter.dpToPx(200)
@ -226,98 +240,110 @@ 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(pollContent: MessagePollContent, private fun buildPollItem(
informationData: MessageInformationData, pollContent: MessagePollContent,
highlight: Boolean, informationData: MessageInformationData,
callback: TimelineEventController.Callback?, highlight: Boolean,
attributes: AbsMessageItem.Attributes): PollItem? { callback: TimelineEventController.Callback?,
val optionViewStates = mutableListOf<PollOptionViewState>() attributes: AbsMessageItem.Attributes,
): PollItem {
val pollResponseSummary = informationData.pollResponseAggregatedSummary val pollResponseSummary = informationData.pollResponseAggregatedSummary
val isEnded = pollResponseSummary?.isClosed.orFalse() val pollState = createPollState(informationData, pollResponseSummary, pollContent)
val didUserVoted = pollResponseSummary?.myVote?.isNotEmpty().orFalse() val pollCreationInfo = pollContent.getBestPollCreationInfo()
val winnerVoteCount = pollResponseSummary?.winnerVoteCount val questionText = pollCreationInfo?.question?.getBestQuestion().orEmpty()
val isPollSent = informationData.sendState.isSent() val question = createPollQuestion(informationData, questionText, callback)
val isPollUndisclosed = pollContent.getBestPollCreationInfo()?.kind == PollType.UNDISCLOSED_UNSTABLE val optionViewStates = pollCreationInfo?.answers?.mapToOptions(pollState, informationData)
val totalVotesText = createTotalVotesText(pollState, pollResponseSummary)
val totalVotesText = (pollResponseSummary?.totalVotes ?: 0).let {
when {
isEnded -> stringProvider.getQuantityString(R.plurals.poll_total_vote_count_after_ended, it, it)
isPollUndisclosed -> ""
didUserVoted -> stringProvider.getQuantityString(R.plurals.poll_total_vote_count_before_ended_and_voted, it, it)
else -> if (it == 0) {
stringProvider.getString(R.string.poll_no_votes_cast)
} else {
stringProvider.getQuantityString(R.plurals.poll_total_vote_count_before_ended_and_not_voted, it, it)
}
}
}
pollContent.getBestPollCreationInfo()?.answers?.forEach { option ->
val voteSummary = pollResponseSummary?.votes?.get(option.id)
val isMyVote = pollResponseSummary?.myVote == option.id
val voteCount = voteSummary?.total ?: 0
val votePercentage = voteSummary?.percentage ?: 0.0
val optionId = option.id ?: ""
val optionAnswer = option.getBestAnswer() ?: ""
optionViewStates.add(
if (!isPollSent) {
// Poll event is not send yet. Disable option.
PollOptionViewState.PollSending(optionId, optionAnswer)
} else if (isEnded) {
// Poll is ended. Disable option, show votes and mark the winner.
val isWinner = winnerVoteCount != 0 && voteCount == winnerVoteCount
PollOptionViewState.PollEnded(optionId, optionAnswer, voteCount, votePercentage, isWinner)
} else if (isPollUndisclosed) {
// Poll is closed. Enable option, hide votes and mark the user's selection.
PollOptionViewState.PollUndisclosed(optionId, optionAnswer, isMyVote)
} else if (didUserVoted) {
// User voted to the poll, but poll is not ended. Enable option, show votes and mark the user's selection.
PollOptionViewState.PollVoted(optionId, optionAnswer, voteCount, votePercentage, isMyVote)
} else {
// User didn't voted yet and poll is not ended yet. Enable options, hide votes.
PollOptionViewState.PollReady(optionId, optionAnswer)
}
)
}
val question = pollContent.getBestPollCreationInfo()?.question?.getBestQuestion() ?: ""
return PollItem_() return PollItem_()
.attributes(attributes) .attributes(attributes)
.eventId(informationData.eventId) .eventId(informationData.eventId)
.pollQuestion( .pollQuestion(question)
if (informationData.hasBeenEdited) { .canVote(pollState.isVotable())
annotateWithEdited(question, callback, informationData) .totalVotesText(totalVotesText)
} else { .optionViewStates(optionViewStates)
question .edited(informationData.hasBeenEdited)
}.toEpoxyCharSequence() .highlighted(highlight)
) .leftGuideline(avatarSizeProvider.leftGuideline)
.pollSent(isPollSent) .callback(callback)
.totalVotesText(totalVotesText)
.optionViewStates(optionViewStates)
.edited(informationData.hasBeenEdited)
.highlighted(highlight)
.leftGuideline(avatarSizeProvider.leftGuideline)
.callback(callback)
} }
private fun buildAudioMessageItem(messageContent: MessageAudioContent, private fun createPollState(
@Suppress("UNUSED_PARAMETER") informationData: MessageInformationData,
informationData: MessageInformationData, pollResponseSummary: PollResponseData?,
highlight: Boolean, pollContent: MessagePollContent,
attributes: AbsMessageItem.Attributes): MessageFileItem? { ): PollState = when {
!informationData.sendState.isSent() -> Sending
pollResponseSummary?.isClosed.orFalse() -> Ended
pollContent.getBestPollCreationInfo()?.kind == PollType.UNDISCLOSED -> Undisclosed
pollResponseSummary?.myVote?.isNotEmpty().orFalse() -> Voted(pollResponseSummary?.totalVotes ?: 0)
else -> Ready
}
private fun List<PollAnswer>.mapToOptions(
pollState: PollState,
informationData: MessageInformationData,
) = map { answer ->
val pollResponseSummary = informationData.pollResponseAggregatedSummary
val winnerVoteCount = pollResponseSummary?.winnerVoteCount
val optionId = answer.id ?: ""
val optionAnswer = answer.getBestAnswer() ?: ""
val voteSummary = pollResponseSummary?.votes?.get(answer.id)
val voteCount = voteSummary?.total ?: 0
val votePercentage = voteSummary?.percentage ?: 0.0
val isMyVote = pollResponseSummary?.myVote == answer.id
val isWinner = winnerVoteCount != 0 && voteCount == winnerVoteCount
when (pollState) {
Sending -> PollSending(optionId, optionAnswer)
Ready -> PollReady(optionId, optionAnswer)
is Voted -> PollVoted(optionId, optionAnswer, voteCount, votePercentage, isMyVote)
Undisclosed -> PollUndisclosed(optionId, optionAnswer, isMyVote)
Ended -> PollEnded(optionId, optionAnswer, voteCount, votePercentage, isWinner)
}
}
private fun createPollQuestion(
informationData: MessageInformationData,
question: String,
callback: TimelineEventController.Callback?,
) = if (informationData.hasBeenEdited) {
annotateWithEdited(question, callback, informationData)
} else {
question
}.toEpoxyCharSequence()
private fun createTotalVotesText(
pollState: PollState,
pollResponseSummary: PollResponseData?,
): String {
val votes = pollResponseSummary?.totalVotes ?: 0
return when {
pollState is Ended -> stringProvider.getQuantityString(R.plurals.poll_total_vote_count_after_ended, votes, votes)
pollState is Undisclosed -> ""
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)
else -> stringProvider.getQuantityString(R.plurals.poll_total_vote_count_before_ended_and_not_voted, votes, votes)
}
}
private fun buildAudioMessageItem(
messageContent: MessageAudioContent,
@Suppress("UNUSED_PARAMETER")
informationData: MessageInformationData,
highlight: Boolean,
attributes: AbsMessageItem.Attributes,
): MessageFileItem? {
val fileUrl = messageContent.getFileUrl()?.let { val fileUrl = messageContent.getFileUrl()?.let {
if (informationData.sentByMe && !informationData.sendState.isSent()) { if (informationData.sentByMe && !informationData.sendState.isSent()) {
it it
@ -326,29 +352,31 @@ class MessageItemFactory @Inject constructor(
} }
} ?: "" } ?: ""
return MessageFileItem_() return MessageFileItem_()
.attributes(attributes) .attributes(attributes)
.izLocalFile(localFilesHelper.isLocalFile(fileUrl)) .izLocalFile(localFilesHelper.isLocalFile(fileUrl))
.izDownloaded(session.fileService().isFileInCache( .izDownloaded(session.fileService().isFileInCache(
fileUrl, fileUrl,
messageContent.getFileName(), messageContent.getFileName(),
messageContent.mimeType, messageContent.mimeType,
messageContent.encryptedFileInfo?.toElementToDecrypt()) messageContent.encryptedFileInfo?.toElementToDecrypt())
) )
.mxcUrl(fileUrl) .mxcUrl(fileUrl)
.contentUploadStateTrackerBinder(contentUploadStateTrackerBinder) .contentUploadStateTrackerBinder(contentUploadStateTrackerBinder)
.contentDownloadStateTrackerBinder(contentDownloadStateTrackerBinder) .contentDownloadStateTrackerBinder(contentDownloadStateTrackerBinder)
.highlighted(highlight) .highlighted(highlight)
.leftGuideline(avatarSizeProvider.leftGuideline) .leftGuideline(avatarSizeProvider.leftGuideline)
.filename(messageContent.body) .filename(messageContent.body)
.iconRes(R.drawable.ic_headphones) .iconRes(R.drawable.ic_headphones)
} }
private fun buildVoiceMessageItem(params: TimelineItemFactoryParams, private fun buildVoiceMessageItem(
messageContent: MessageAudioContent, params: TimelineItemFactoryParams,
@Suppress("UNUSED_PARAMETER") messageContent: MessageAudioContent,
informationData: MessageInformationData, @Suppress("UNUSED_PARAMETER")
highlight: Boolean, informationData: MessageInformationData,
attributes: AbsMessageItem.Attributes): MessageVoiceItem? { highlight: Boolean,
attributes: AbsMessageItem.Attributes,
): MessageVoiceItem? {
val fileUrl = messageContent.getFileUrl()?.let { val fileUrl = messageContent.getFileUrl()?.let {
if (informationData.sentByMe && !informationData.sendState.isSent()) { if (informationData.sentByMe && !informationData.sendState.isSent()) {
it it
@ -376,32 +404,34 @@ 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)
.voiceMessagePlaybackTracker(voiceMessagePlaybackTracker) .voiceMessagePlaybackTracker(voiceMessagePlaybackTracker)
.izLocalFile(localFilesHelper.isLocalFile(fileUrl)) .izLocalFile(localFilesHelper.isLocalFile(fileUrl))
.izDownloaded(session.fileService().isFileInCache( .izDownloaded(session.fileService().isFileInCache(
fileUrl, fileUrl,
messageContent.getFileName(), messageContent.getFileName(),
messageContent.mimeType, messageContent.mimeType,
messageContent.encryptedFileInfo?.toElementToDecrypt()) messageContent.encryptedFileInfo?.toElementToDecrypt())
) )
.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(messageContent: MessageVerificationRequestContent, private fun buildVerificationRequestMessageItem(
@Suppress("UNUSED_PARAMETER") messageContent: MessageVerificationRequestContent,
informationData: MessageInformationData, @Suppress("UNUSED_PARAMETER")
highlight: Boolean, informationData: MessageInformationData,
callback: TimelineEventController.Callback?, highlight: Boolean,
attributes: AbsMessageItem.Attributes): VerificationRequestItem? { callback: TimelineEventController.Callback?,
attributes: AbsMessageItem.Attributes,
): 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
if (informationData.senderId != myUserId && messageContent.toUserId != myUserId) { if (informationData.senderId != myUserId && messageContent.toUserId != myUserId) {
@ -415,140 +445,148 @@ 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) )
.highlighted(highlight) .callback(callback)
.leftGuideline(avatarSizeProvider.leftGuideline) .highlighted(highlight)
.leftGuideline(avatarSizeProvider.leftGuideline)
} }
private fun buildFileMessageItem(messageContent: MessageFileContent, private fun buildFileMessageItem(
// informationData: MessageInformationData, messageContent: MessageFileContent,
highlight: Boolean, highlight: Boolean,
// callback: TimelineEventController.Callback?, 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)
.izLocalFile(localFilesHelper.isLocalFile(messageContent.getFileUrl())) .izLocalFile(localFilesHelper.isLocalFile(messageContent.getFileUrl()))
.izDownloaded(session.fileService().isFileInCache(messageContent)) .izDownloaded(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 buildNotHandledMessageItem(messageContent: MessageContent, private fun buildNotHandledMessageItem(
informationData: MessageInformationData, messageContent: MessageContent,
highlight: Boolean, informationData: MessageInformationData,
callback: TimelineEventController.Callback?, highlight: Boolean,
attributes: AbsMessageItem.Attributes): MessageTextItem? { callback: TimelineEventController.Callback?,
attributes: AbsMessageItem.Attributes,
): MessageTextItem? {
// For compatibility reason we should display the body // For compatibility reason we should display the body
return buildMessageTextItem(messageContent.body, false, informationData, highlight, callback, attributes) return buildMessageTextItem(messageContent.body, false, informationData, highlight, callback, attributes)
} }
private fun buildImageMessageItem(messageContent: MessageImageInfoContent, private fun buildImageMessageItem(
@Suppress("UNUSED_PARAMETER") messageContent: MessageImageInfoContent,
informationData: MessageInformationData, @Suppress("UNUSED_PARAMETER")
highlight: Boolean, informationData: MessageInformationData,
callback: TimelineEventController.Callback?, highlight: Boolean,
attributes: AbsMessageItem.Attributes): MessageImageVideoItem? { callback: TimelineEventController.Callback?,
attributes: AbsMessageItem.Attributes,
): 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(messageContent: MessageVideoContent, private fun buildVideoMessageItem(
informationData: MessageInformationData, messageContent: MessageVideoContent,
highlight: Boolean, informationData: MessageInformationData,
callback: TimelineEventController.Callback?, highlight: Boolean,
attributes: AbsMessageItem.Attributes): MessageImageVideoItem? { callback: TimelineEventController.Callback?,
attributes: AbsMessageItem.Attributes,
): 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(messageContent: MessageTextContent, private fun buildItemForTextContent(
informationData: MessageInformationData, messageContent: MessageTextContent,
highlight: Boolean, informationData: MessageInformationData,
callback: TimelineEventController.Callback?, highlight: Boolean,
attributes: AbsMessageItem.Attributes): VectorEpoxyModel<*>? { callback: TimelineEventController.Callback?,
attributes: AbsMessageItem.Attributes,
): VectorEpoxyModel<*>? {
val matrixFormattedBody = messageContent.matrixFormattedBody val matrixFormattedBody = messageContent.matrixFormattedBody
return if (matrixFormattedBody != null) { return if (matrixFormattedBody != null) {
buildFormattedTextItem(matrixFormattedBody, informationData, highlight, callback, attributes) buildFormattedTextItem(matrixFormattedBody, informationData, highlight, callback, attributes)
@ -557,50 +595,56 @@ class MessageItemFactory @Inject constructor(
} }
} }
private fun buildFormattedTextItem(matrixFormattedBody: String, private fun buildFormattedTextItem(
informationData: MessageInformationData, matrixFormattedBody: String,
highlight: Boolean, informationData: MessageInformationData,
callback: TimelineEventController.Callback?, highlight: Boolean,
attributes: AbsMessageItem.Attributes): MessageTextItem? { callback: TimelineEventController.Callback?,
attributes: AbsMessageItem.Attributes,
): 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
return buildMessageTextItem(renderedFormattedBody, true, informationData, highlight, callback, attributes) return buildMessageTextItem(renderedFormattedBody, true, informationData, highlight, callback, attributes)
} }
private fun buildMessageTextItem(body: CharSequence, private fun buildMessageTextItem(
isFormatted: Boolean, body: CharSequence,
informationData: MessageInformationData, isFormatted: Boolean,
highlight: Boolean, informationData: MessageInformationData,
callback: TimelineEventController.Callback?, highlight: Boolean,
attributes: AbsMessageItem.Attributes): MessageTextItem? { callback: TimelineEventController.Callback?,
attributes: AbsMessageItem.Attributes,
): 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(linkifiedBody: CharSequence, private fun annotateWithEdited(
callback: TimelineEventController.Callback?, linkifiedBody: CharSequence,
informationData: MessageInformationData): Spannable { callback: TimelineEventController.Callback?,
informationData: MessageInformationData,
): Spannable {
val spannable = SpannableStringBuilder() val spannable = SpannableStringBuilder()
spannable.append(linkifiedBody) spannable.append(linkifiedBody)
val editedSuffix = stringProvider.getString(R.string.edited_suffix) val editedSuffix = stringProvider.getString(R.string.edited_suffix)
@ -609,17 +653,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) {
@ -630,18 +674,20 @@ 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(messageContent: MessageNoticeContent, private fun buildNoticeMessageItem(
@Suppress("UNUSED_PARAMETER") messageContent: MessageNoticeContent,
informationData: MessageInformationData, @Suppress("UNUSED_PARAMETER")
highlight: Boolean, informationData: MessageInformationData,
callback: TimelineEventController.Callback?, highlight: Boolean,
attributes: AbsMessageItem.Attributes): MessageTextItem? { callback: TimelineEventController.Callback?,
attributes: AbsMessageItem.Attributes,
): MessageTextItem? {
val htmlBody = messageContent.getHtmlBody() val htmlBody = messageContent.getHtmlBody()
val formattedBody = span { val formattedBody = span {
text = htmlBody text = htmlBody
@ -653,22 +699,24 @@ 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(messageContent: MessageEmoteContent, private fun buildEmoteMessageItem(
informationData: MessageInformationData, messageContent: MessageEmoteContent,
highlight: Boolean, informationData: MessageInformationData,
callback: TimelineEventController.Callback?, highlight: Boolean,
attributes: AbsMessageItem.Attributes): MessageTextItem? { callback: TimelineEventController.Callback?,
attributes: AbsMessageItem.Attributes,
): MessageTextItem? {
val formattedBody = SpannableStringBuilder() val formattedBody = SpannableStringBuilder()
formattedBody.append("* ${informationData.memberName} ") formattedBody.append("* ${informationData.memberName} ")
formattedBody.append(messageContent.getHtmlBody()) formattedBody.append(messageContent.getHtmlBody())
@ -676,46 +724,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(attributes: AbsMessageItem.Attributes, private fun buildRedactedItem(
highlight: Boolean): RedactedMessageItem? { attributes: AbsMessageItem.Attributes,
highlight: Boolean,
): 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

@ -39,13 +39,13 @@ abstract class PollItem : AbsMessageItem<PollItem.Holder>() {
var eventId: String? = null var eventId: String? = null
@EpoxyAttribute @EpoxyAttribute
var pollSent: Boolean = false var canVote: Boolean = false
@EpoxyAttribute @EpoxyAttribute
var totalVotesText: String? = null var totalVotesText: String? = null
@EpoxyAttribute @EpoxyAttribute
var edited: Boolean = false var edited: Boolean = false
@EpoxyAttribute @EpoxyAttribute
lateinit var optionViewStates: List<PollOptionViewState> lateinit var optionViewStates: List<PollOptionViewState>
@ -54,7 +54,6 @@ abstract class PollItem : AbsMessageItem<PollItem.Holder>() {
override fun bind(holder: Holder) { override fun bind(holder: Holder) {
super.bind(holder) super.bind(holder)
val relatedEventId = eventId ?: return
renderSendState(holder.view, holder.questionTextView) renderSendState(holder.view, holder.questionTextView)
@ -73,13 +72,19 @@ abstract class PollItem : AbsMessageItem<PollItem.Holder>() {
optionViewStates.forEachIndexed { index, optionViewState -> optionViewStates.forEachIndexed { index, optionViewState ->
views.getOrNull(index)?.let { views.getOrNull(index)?.let {
it.render(optionViewState) it.render(optionViewState)
it.setOnClickListener { it.setOnClickListener { onPollItemClick(optionViewState) }
callback?.onTimelineItemAction(RoomDetailAction.VoteToPoll(relatedEventId, optionViewState.optionId))
}
} }
} }
} }
private fun onPollItemClick(optionViewState: PollOptionViewState) {
val relatedEventId = eventId
if (canVote && relatedEventId != null) {
callback?.onTimelineItemAction(RoomDetailAction.VoteToPoll(relatedEventId, optionViewState.optionId))
}
}
class Holder : AbsMessageItem.Holder(STUB_ID) { class Holder : AbsMessageItem.Holder(STUB_ID) {
val questionTextView by bind<TextView>(R.id.questionTextView) val questionTextView by bind<TextView>(R.id.questionTextView)
val optionsContainer by bind<LinearLayout>(R.id.optionsContainer) val optionsContainer by bind<LinearLayout>(R.id.optionsContainer)

View file

@ -75,9 +75,9 @@ import im.vector.app.features.onboarding.OnboardingActivity
import im.vector.app.features.pin.PinActivity import im.vector.app.features.pin.PinActivity
import im.vector.app.features.pin.PinArgs import im.vector.app.features.pin.PinArgs
import im.vector.app.features.pin.PinMode import im.vector.app.features.pin.PinMode
import im.vector.app.features.poll.PollMode
import im.vector.app.features.poll.create.CreatePollActivity import im.vector.app.features.poll.create.CreatePollActivity
import im.vector.app.features.poll.create.CreatePollArgs import im.vector.app.features.poll.create.CreatePollArgs
import im.vector.app.features.poll.create.PollMode
import im.vector.app.features.roomdirectory.RoomDirectoryActivity import im.vector.app.features.roomdirectory.RoomDirectoryActivity
import im.vector.app.features.roomdirectory.RoomDirectoryData import im.vector.app.features.roomdirectory.RoomDirectoryData
import im.vector.app.features.roomdirectory.createroom.CreateRoomActivity import im.vector.app.features.roomdirectory.createroom.CreateRoomActivity

View file

@ -31,7 +31,7 @@ import im.vector.app.features.location.LocationSharingMode
import im.vector.app.features.login.LoginConfig import im.vector.app.features.login.LoginConfig
import im.vector.app.features.media.AttachmentData import im.vector.app.features.media.AttachmentData
import im.vector.app.features.pin.PinMode import im.vector.app.features.pin.PinMode
import im.vector.app.features.poll.create.PollMode import im.vector.app.features.poll.PollMode
import im.vector.app.features.roomdirectory.RoomDirectoryData import im.vector.app.features.roomdirectory.RoomDirectoryData
import im.vector.app.features.roomdirectory.roompreview.RoomPreviewData import im.vector.app.features.roomdirectory.roompreview.RoomPreviewData
import im.vector.app.features.settings.VectorSettingsActivity import im.vector.app.features.settings.VectorSettingsActivity

View file

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package im.vector.app.features.poll.create package im.vector.app.features.poll
enum class PollMode { enum class PollMode {
CREATE, CREATE,

View file

@ -0,0 +1,27 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.poll
sealed interface PollState {
object Sending : PollState
object Ready : PollState
data class Voted(val votes: Int) : PollState
object Undisclosed : PollState
object Ended : PollState
fun isVotable() = this !is Sending && this !is Ended
}

View file

@ -29,6 +29,7 @@ import im.vector.app.R
import im.vector.app.core.extensions.configureWith import im.vector.app.core.extensions.configureWith
import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.databinding.FragmentCreatePollBinding import im.vector.app.databinding.FragmentCreatePollBinding
import im.vector.app.features.poll.PollMode
import im.vector.app.features.poll.create.CreatePollViewModel.Companion.MAX_OPTIONS_COUNT import im.vector.app.features.poll.create.CreatePollViewModel.Companion.MAX_OPTIONS_COUNT
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import org.matrix.android.sdk.api.session.room.model.message.PollType import org.matrix.android.sdk.api.session.room.model.message.PollType

View file

@ -23,6 +23,7 @@ import dagger.assisted.AssistedInject
import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.MavericksAssistedViewModelFactory
import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory
import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.VectorViewModel
import im.vector.app.features.poll.PollMode
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
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.PollType import org.matrix.android.sdk.api.session.room.model.message.PollType

View file

@ -17,17 +17,18 @@
package im.vector.app.features.poll.create package im.vector.app.features.poll.create
import com.airbnb.mvrx.MavericksState import com.airbnb.mvrx.MavericksState
import im.vector.app.features.poll.PollMode
import org.matrix.android.sdk.api.session.room.model.message.PollType import org.matrix.android.sdk.api.session.room.model.message.PollType
data class CreatePollViewState( data class CreatePollViewState(
val roomId: String, val roomId: String,
val editedEventId: String?, val editedEventId: String?,
val mode: PollMode, val mode: PollMode,
val question: String = "", val question: String = "",
val options: List<String> = List(CreatePollViewModel.MIN_OPTIONS_COUNT) { "" }, val options: List<String> = List(CreatePollViewModel.MIN_OPTIONS_COUNT) { "" },
val canCreatePoll: Boolean = false, val canCreatePoll: Boolean = false,
val canAddMoreOptions: Boolean = true, val canAddMoreOptions: Boolean = true,
val pollType: PollType = PollType.DISCLOSED_UNSTABLE val pollType: PollType = PollType.DISCLOSED_UNSTABLE
) : MavericksState { ) : MavericksState {
constructor(args: CreatePollArgs) : this( constructor(args: CreatePollArgs) : this(