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,
isLocalEcho: Boolean) {
val pollEventId = content.relatesTo?.eventId ?: return
val pollOwnerId = getPollEvent(roomId, pollEventId)?.root?.senderId
val isPollOwner = pollOwnerId == event.senderId
val powerLevelsHelper = stateEventDataSource.getStateEvent(roomId, EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.NoCondition)
?.content?.toModel<PowerLevelsContent>()
?.let { PowerLevelsHelper(it) }
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")
return
}
var existing = EventAnnotationsSummaryEntity.where(realm, roomId, pollEventId).findFirst()
if (existing == null) {
var existingPoll = EventAnnotationsSummaryEntity.where(realm, roomId, pollEventId).findFirst()
if (existingPoll == null) {
Timber.v("## POLL creating new relation summary for $pollEventId")
existing = EventAnnotationsSummaryEntity.create(realm, roomId, pollEventId)
existingPoll = EventAnnotationsSummaryEntity.create(realm, roomId, pollEventId)
}
// we have it
val existingPollSummary = existing.pollResponseSummary
val existingPollSummary = existingPoll.pollResponseSummary
?: 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
existingPollSummary.closedTime = event.originServerTs
// is it a remote echo?
if (!isLocalEcho && existingPollSummary.sourceLocalEchoEvents.contains(txId)) {
// ok it has already been managed
Timber.v("## POLL Receiving remote echo of response eventId:$pollEventId")
existingPollSummary.sourceLocalEchoEvents.remove(txId)
existingPollSummary.sourceEvents.add(event.eventId)
return
}
existingPollSummary.closedTime = event.originServerTs
}
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.permalink.NavigationInterceptor
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.roomprofile.RoomProfileActivity
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.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.VerificationRequestItem
@ -73,6 +78,12 @@ import im.vector.app.features.location.UrlMapProvider
import im.vector.app.features.location.toLocationData
import im.vector.app.features.media.ImageContentRenderer
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.voice.AudioWaveformView
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.MessageVerificationRequestContent
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.getFileName
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
class MessageItemFactory @Inject constructor(
private val localFilesHelper: LocalFilesHelper,
private val colorProvider: ColorProvider,
private val dimensionConverter: DimensionConverter,
private val timelineMediaSizeProvider: TimelineMediaSizeProvider,
private val htmlRenderer: Lazy<EventHtmlRenderer>,
private val htmlCompressor: VectorHtmlCompressor,
private val textRendererFactory: EventTextRenderer.Factory,
private val stringProvider: StringProvider,
private val imageContentRenderer: ImageContentRenderer,
private val messageInformationDataFactory: MessageInformationDataFactory,
private val messageItemAttributesFactory: MessageItemAttributesFactory,
private val contentUploadStateTrackerBinder: ContentUploadStateTrackerBinder,
private val contentDownloadStateTrackerBinder: ContentDownloadStateTrackerBinder,
private val defaultItemFactory: DefaultItemFactory,
private val noticeItemFactory: NoticeItemFactory,
private val avatarSizeProvider: AvatarSizeProvider,
private val pillsPostProcessorFactory: PillsPostProcessor.Factory,
private val lightweightSettingsStorage: LightweightSettingsStorage,
private val spanUtils: SpanUtils,
private val session: Session,
private val voiceMessagePlaybackTracker: VoiceMessagePlaybackTracker,
private val locationPinProvider: LocationPinProvider,
private val vectorPreferences: VectorPreferences,
private val urlMapProvider: UrlMapProvider,
private val localFilesHelper: LocalFilesHelper,
private val colorProvider: ColorProvider,
private val dimensionConverter: DimensionConverter,
private val timelineMediaSizeProvider: TimelineMediaSizeProvider,
private val htmlRenderer: Lazy<EventHtmlRenderer>,
private val htmlCompressor: VectorHtmlCompressor,
private val textRendererFactory: EventTextRenderer.Factory,
private val stringProvider: StringProvider,
private val imageContentRenderer: ImageContentRenderer,
private val messageInformationDataFactory: MessageInformationDataFactory,
private val messageItemAttributesFactory: MessageItemAttributesFactory,
private val contentUploadStateTrackerBinder: ContentUploadStateTrackerBinder,
private val contentDownloadStateTrackerBinder: ContentDownloadStateTrackerBinder,
private val defaultItemFactory: DefaultItemFactory,
private val noticeItemFactory: NoticeItemFactory,
private val avatarSizeProvider: AvatarSizeProvider,
private val pillsPostProcessorFactory: PillsPostProcessor.Factory,
private val lightweightSettingsStorage: LightweightSettingsStorage,
private val spanUtils: SpanUtils,
private val session: Session,
private val voiceMessagePlaybackTracker: VoiceMessagePlaybackTracker,
private val locationPinProvider: LocationPinProvider,
private val vectorPreferences: VectorPreferences,
private val urlMapProvider: UrlMapProvider,
) {
// TODO inject this properly?
@ -166,7 +178,7 @@ class MessageItemFactory @Inject constructor(
return defaultItemFactory.create(malformedText, informationData, highlight, callback)
}
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
return noticeItemFactory.create(params)
@ -180,16 +192,16 @@ class MessageItemFactory @Inject constructor(
// always hide summary when we are on thread timeline
val attributes = messageItemAttributesFactory.create(messageContent, informationData, callback, params.reactionsSummaryEvents, threadDetails)
// val all = event.root.toContent()
// val ev = all.toModel<Event>()
// val all = event.root.toContent()
// val ev = all.toModel<Event>()
val messageItem = when (messageContent) {
is MessageEmoteContent -> buildEmoteMessageItem(messageContent, informationData, highlight, callback, attributes)
is MessageTextContent -> buildItemForTextContent(messageContent, informationData, highlight, callback, attributes)
is MessageImageInfoContent -> buildImageMessageItem(messageContent, informationData, highlight, callback, attributes)
is MessageNoticeContent -> buildNoticeMessageItem(messageContent, informationData, highlight, callback, attributes)
is MessageVideoContent -> buildVideoMessageItem(messageContent, informationData, highlight, callback, attributes)
is MessageFileContent -> buildFileMessageItem(messageContent, highlight, attributes)
is MessageAudioContent -> {
is MessageEmoteContent -> buildEmoteMessageItem(messageContent, informationData, highlight, callback, attributes)
is MessageTextContent -> buildItemForTextContent(messageContent, informationData, highlight, callback, attributes)
is MessageImageInfoContent -> buildImageMessageItem(messageContent, informationData, highlight, callback, attributes)
is MessageNoticeContent -> buildNoticeMessageItem(messageContent, informationData, highlight, callback, attributes)
is MessageVideoContent -> buildVideoMessageItem(messageContent, informationData, highlight, callback, attributes)
is MessageFileContent -> buildFileMessageItem(messageContent, highlight, attributes)
is MessageAudioContent -> {
if (messageContent.voiceMessageIndicator != null) {
buildVoiceMessageItem(params, messageContent, informationData, highlight, attributes)
} else {
@ -197,25 +209,27 @@ class MessageItemFactory @Inject constructor(
}
}
is MessageVerificationRequestContent -> buildVerificationRequestMessageItem(messageContent, informationData, highlight, callback, attributes)
is MessagePollContent -> buildPollItem(messageContent, informationData, highlight, callback, attributes)
is MessageLocationContent -> {
is MessagePollContent -> buildPollItem(messageContent, informationData, highlight, callback, attributes)
is MessageLocationContent -> {
if (vectorPreferences.labsRenderLocationsInTimeline()) {
buildLocationItem(messageContent, informationData, highlight, attributes)
} else {
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 {
layout(informationData.messageLayout.layoutRes)
}
}
private fun buildLocationItem(locationContent: MessageLocationContent,
informationData: MessageInformationData,
highlight: Boolean,
attributes: AbsMessageItem.Attributes): MessageLocationItem? {
private fun buildLocationItem(
locationContent: MessageLocationContent,
informationData: MessageInformationData,
highlight: Boolean,
attributes: AbsMessageItem.Attributes,
): MessageLocationItem? {
val width = timelineMediaSizeProvider.getMaxSize().first
val height = dimensionConverter.dpToPx(200)
@ -226,98 +240,110 @@ class MessageItemFactory @Inject constructor(
val userId = if (locationContent.isSelfLocation()) informationData.senderId else null
return MessageLocationItem_()
.attributes(attributes)
.locationUrl(locationUrl)
.mapWidth(width)
.mapHeight(height)
.userId(userId)
.locationPinProvider(locationPinProvider)
.highlighted(highlight)
.leftGuideline(avatarSizeProvider.leftGuideline)
.attributes(attributes)
.locationUrl(locationUrl)
.mapWidth(width)
.mapHeight(height)
.userId(userId)
.locationPinProvider(locationPinProvider)
.highlighted(highlight)
.leftGuideline(avatarSizeProvider.leftGuideline)
}
private fun buildPollItem(pollContent: MessagePollContent,
informationData: MessageInformationData,
highlight: Boolean,
callback: TimelineEventController.Callback?,
attributes: AbsMessageItem.Attributes): PollItem? {
val optionViewStates = mutableListOf<PollOptionViewState>()
private fun buildPollItem(
pollContent: MessagePollContent,
informationData: MessageInformationData,
highlight: Boolean,
callback: TimelineEventController.Callback?,
attributes: AbsMessageItem.Attributes,
): PollItem {
val pollResponseSummary = informationData.pollResponseAggregatedSummary
val isEnded = pollResponseSummary?.isClosed.orFalse()
val didUserVoted = pollResponseSummary?.myVote?.isNotEmpty().orFalse()
val winnerVoteCount = pollResponseSummary?.winnerVoteCount
val isPollSent = informationData.sendState.isSent()
val isPollUndisclosed = pollContent.getBestPollCreationInfo()?.kind == PollType.UNDISCLOSED_UNSTABLE
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() ?: ""
val pollState = createPollState(informationData, pollResponseSummary, pollContent)
val pollCreationInfo = pollContent.getBestPollCreationInfo()
val questionText = pollCreationInfo?.question?.getBestQuestion().orEmpty()
val question = createPollQuestion(informationData, questionText, callback)
val optionViewStates = pollCreationInfo?.answers?.mapToOptions(pollState, informationData)
val totalVotesText = createTotalVotesText(pollState, pollResponseSummary)
return PollItem_()
.attributes(attributes)
.eventId(informationData.eventId)
.pollQuestion(
if (informationData.hasBeenEdited) {
annotateWithEdited(question, callback, informationData)
} else {
question
}.toEpoxyCharSequence()
)
.pollSent(isPollSent)
.totalVotesText(totalVotesText)
.optionViewStates(optionViewStates)
.edited(informationData.hasBeenEdited)
.highlighted(highlight)
.leftGuideline(avatarSizeProvider.leftGuideline)
.callback(callback)
.attributes(attributes)
.eventId(informationData.eventId)
.pollQuestion(question)
.canVote(pollState.isVotable())
.totalVotesText(totalVotesText)
.optionViewStates(optionViewStates)
.edited(informationData.hasBeenEdited)
.highlighted(highlight)
.leftGuideline(avatarSizeProvider.leftGuideline)
.callback(callback)
}
private fun buildAudioMessageItem(messageContent: MessageAudioContent,
@Suppress("UNUSED_PARAMETER")
informationData: MessageInformationData,
highlight: Boolean,
attributes: AbsMessageItem.Attributes): MessageFileItem? {
private fun createPollState(
informationData: MessageInformationData,
pollResponseSummary: PollResponseData?,
pollContent: MessagePollContent,
): 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 {
if (informationData.sentByMe && !informationData.sendState.isSent()) {
it
@ -326,29 +352,31 @@ class MessageItemFactory @Inject constructor(
}
} ?: ""
return MessageFileItem_()
.attributes(attributes)
.izLocalFile(localFilesHelper.isLocalFile(fileUrl))
.izDownloaded(session.fileService().isFileInCache(
fileUrl,
messageContent.getFileName(),
messageContent.mimeType,
messageContent.encryptedFileInfo?.toElementToDecrypt())
)
.mxcUrl(fileUrl)
.contentUploadStateTrackerBinder(contentUploadStateTrackerBinder)
.contentDownloadStateTrackerBinder(contentDownloadStateTrackerBinder)
.highlighted(highlight)
.leftGuideline(avatarSizeProvider.leftGuideline)
.filename(messageContent.body)
.iconRes(R.drawable.ic_headphones)
.attributes(attributes)
.izLocalFile(localFilesHelper.isLocalFile(fileUrl))
.izDownloaded(session.fileService().isFileInCache(
fileUrl,
messageContent.getFileName(),
messageContent.mimeType,
messageContent.encryptedFileInfo?.toElementToDecrypt())
)
.mxcUrl(fileUrl)
.contentUploadStateTrackerBinder(contentUploadStateTrackerBinder)
.contentDownloadStateTrackerBinder(contentDownloadStateTrackerBinder)
.highlighted(highlight)
.leftGuideline(avatarSizeProvider.leftGuideline)
.filename(messageContent.body)
.iconRes(R.drawable.ic_headphones)
}
private fun buildVoiceMessageItem(params: TimelineItemFactoryParams,
messageContent: MessageAudioContent,
@Suppress("UNUSED_PARAMETER")
informationData: MessageInformationData,
highlight: Boolean,
attributes: AbsMessageItem.Attributes): MessageVoiceItem? {
private fun buildVoiceMessageItem(
params: TimelineItemFactoryParams,
messageContent: MessageAudioContent,
@Suppress("UNUSED_PARAMETER")
informationData: MessageInformationData,
highlight: Boolean,
attributes: AbsMessageItem.Attributes,
): MessageVoiceItem? {
val fileUrl = messageContent.getFileUrl()?.let {
if (informationData.sentByMe && !informationData.sendState.isSent()) {
it
@ -376,32 +404,34 @@ class MessageItemFactory @Inject constructor(
}
return MessageVoiceItem_()
.attributes(attributes)
.duration(messageContent.audioWaveformInfo?.duration ?: 0)
.waveform(messageContent.audioWaveformInfo?.waveform?.toFft().orEmpty())
.playbackControlButtonClickListener(playbackControlButtonClickListener)
.waveformTouchListener(waveformTouchListener)
.voiceMessagePlaybackTracker(voiceMessagePlaybackTracker)
.izLocalFile(localFilesHelper.isLocalFile(fileUrl))
.izDownloaded(session.fileService().isFileInCache(
fileUrl,
messageContent.getFileName(),
messageContent.mimeType,
messageContent.encryptedFileInfo?.toElementToDecrypt())
)
.mxcUrl(fileUrl)
.contentUploadStateTrackerBinder(contentUploadStateTrackerBinder)
.contentDownloadStateTrackerBinder(contentDownloadStateTrackerBinder)
.highlighted(highlight)
.leftGuideline(avatarSizeProvider.leftGuideline)
.attributes(attributes)
.duration(messageContent.audioWaveformInfo?.duration ?: 0)
.waveform(messageContent.audioWaveformInfo?.waveform?.toFft().orEmpty())
.playbackControlButtonClickListener(playbackControlButtonClickListener)
.waveformTouchListener(waveformTouchListener)
.voiceMessagePlaybackTracker(voiceMessagePlaybackTracker)
.izLocalFile(localFilesHelper.isLocalFile(fileUrl))
.izDownloaded(session.fileService().isFileInCache(
fileUrl,
messageContent.getFileName(),
messageContent.mimeType,
messageContent.encryptedFileInfo?.toElementToDecrypt())
)
.mxcUrl(fileUrl)
.contentUploadStateTrackerBinder(contentUploadStateTrackerBinder)
.contentDownloadStateTrackerBinder(contentDownloadStateTrackerBinder)
.highlighted(highlight)
.leftGuideline(avatarSizeProvider.leftGuideline)
}
private fun buildVerificationRequestMessageItem(messageContent: MessageVerificationRequestContent,
@Suppress("UNUSED_PARAMETER")
informationData: MessageInformationData,
highlight: Boolean,
callback: TimelineEventController.Callback?,
attributes: AbsMessageItem.Attributes): VerificationRequestItem? {
private fun buildVerificationRequestMessageItem(
messageContent: MessageVerificationRequestContent,
@Suppress("UNUSED_PARAMETER")
informationData: MessageInformationData,
highlight: Boolean,
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
val myUserId = session.myUserId
if (informationData.senderId != myUserId && messageContent.toUserId != myUserId) {
@ -415,140 +445,148 @@ class MessageItemFactory @Inject constructor(
informationData.memberName
}
return VerificationRequestItem_()
.attributes(
VerificationRequestItem.Attributes(
otherUserId = otherUserId,
otherUserName = otherUserName.toString(),
referenceId = informationData.eventId,
informationData = informationData,
avatarRenderer = attributes.avatarRenderer,
messageColorProvider = attributes.messageColorProvider,
itemLongClickListener = attributes.itemLongClickListener,
itemClickListener = attributes.itemClickListener,
reactionPillCallback = attributes.reactionPillCallback,
readReceiptsCallback = attributes.readReceiptsCallback,
emojiTypeFace = attributes.emojiTypeFace,
reactionsSummaryEvents = attributes.reactionsSummaryEvents
)
.attributes(
VerificationRequestItem.Attributes(
otherUserId = otherUserId,
otherUserName = otherUserName.toString(),
referenceId = informationData.eventId,
informationData = informationData,
avatarRenderer = attributes.avatarRenderer,
messageColorProvider = attributes.messageColorProvider,
itemLongClickListener = attributes.itemLongClickListener,
itemClickListener = attributes.itemClickListener,
reactionPillCallback = attributes.reactionPillCallback,
readReceiptsCallback = attributes.readReceiptsCallback,
emojiTypeFace = attributes.emojiTypeFace,
reactionsSummaryEvents = attributes.reactionsSummaryEvents,
)
.callback(callback)
.highlighted(highlight)
.leftGuideline(avatarSizeProvider.leftGuideline)
)
.callback(callback)
.highlighted(highlight)
.leftGuideline(avatarSizeProvider.leftGuideline)
}
private fun buildFileMessageItem(messageContent: MessageFileContent,
// informationData: MessageInformationData,
highlight: Boolean,
// callback: TimelineEventController.Callback?,
attributes: AbsMessageItem.Attributes): MessageFileItem? {
private fun buildFileMessageItem(
messageContent: MessageFileContent,
highlight: Boolean,
attributes: AbsMessageItem.Attributes,
): MessageFileItem? {
val mxcUrl = messageContent.getFileUrl() ?: ""
return MessageFileItem_()
.attributes(attributes)
.leftGuideline(avatarSizeProvider.leftGuideline)
.izLocalFile(localFilesHelper.isLocalFile(messageContent.getFileUrl()))
.izDownloaded(session.fileService().isFileInCache(messageContent))
.mxcUrl(mxcUrl)
.contentUploadStateTrackerBinder(contentUploadStateTrackerBinder)
.contentDownloadStateTrackerBinder(contentDownloadStateTrackerBinder)
.highlighted(highlight)
.filename(messageContent.body)
.iconRes(R.drawable.ic_paperclip)
.attributes(attributes)
.leftGuideline(avatarSizeProvider.leftGuideline)
.izLocalFile(localFilesHelper.isLocalFile(messageContent.getFileUrl()))
.izDownloaded(session.fileService().isFileInCache(messageContent))
.mxcUrl(mxcUrl)
.contentUploadStateTrackerBinder(contentUploadStateTrackerBinder)
.contentDownloadStateTrackerBinder(contentDownloadStateTrackerBinder)
.highlighted(highlight)
.filename(messageContent.body)
.iconRes(R.drawable.ic_paperclip)
}
private fun buildNotHandledMessageItem(messageContent: MessageContent,
informationData: MessageInformationData,
highlight: Boolean,
callback: TimelineEventController.Callback?,
attributes: AbsMessageItem.Attributes): MessageTextItem? {
private fun buildNotHandledMessageItem(
messageContent: MessageContent,
informationData: MessageInformationData,
highlight: Boolean,
callback: TimelineEventController.Callback?,
attributes: AbsMessageItem.Attributes,
): MessageTextItem? {
// For compatibility reason we should display the body
return buildMessageTextItem(messageContent.body, false, informationData, highlight, callback, attributes)
}
private fun buildImageMessageItem(messageContent: MessageImageInfoContent,
@Suppress("UNUSED_PARAMETER")
informationData: MessageInformationData,
highlight: Boolean,
callback: TimelineEventController.Callback?,
attributes: AbsMessageItem.Attributes): MessageImageVideoItem? {
private fun buildImageMessageItem(
messageContent: MessageImageInfoContent,
@Suppress("UNUSED_PARAMETER")
informationData: MessageInformationData,
highlight: Boolean,
callback: TimelineEventController.Callback?,
attributes: AbsMessageItem.Attributes,
): MessageImageVideoItem? {
val (maxWidth, maxHeight) = timelineMediaSizeProvider.getMaxSize()
val data = ImageContentRenderer.Data(
eventId = informationData.eventId,
filename = messageContent.body,
mimeType = messageContent.mimeType,
url = messageContent.getFileUrl(),
elementToDecrypt = messageContent.encryptedFileInfo?.toElementToDecrypt(),
height = messageContent.info?.height,
maxHeight = maxHeight,
width = messageContent.info?.width,
maxWidth = maxWidth,
allowNonMxcUrls = informationData.sendState.isSending()
eventId = informationData.eventId,
filename = messageContent.body,
mimeType = messageContent.mimeType,
url = messageContent.getFileUrl(),
elementToDecrypt = messageContent.encryptedFileInfo?.toElementToDecrypt(),
height = messageContent.info?.height,
maxHeight = maxHeight,
width = messageContent.info?.width,
maxWidth = maxWidth,
allowNonMxcUrls = informationData.sendState.isSending()
)
return MessageImageVideoItem_()
.attributes(attributes)
.leftGuideline(avatarSizeProvider.leftGuideline)
.imageContentRenderer(imageContentRenderer)
.contentUploadStateTrackerBinder(contentUploadStateTrackerBinder)
.playable(messageContent.mimeType == MimeTypes.Gif)
.highlighted(highlight)
.mediaData(data)
.apply {
if (messageContent.msgType == MessageType.MSGTYPE_STICKER_LOCAL) {
mode(ImageContentRenderer.Mode.STICKER)
clickListener { view ->
callback?.onImageMessageClicked(messageContent, data, view, listOf(data))
}
} else {
clickListener { view ->
callback?.onImageMessageClicked(messageContent, data, view, emptyList())
}
.attributes(attributes)
.leftGuideline(avatarSizeProvider.leftGuideline)
.imageContentRenderer(imageContentRenderer)
.contentUploadStateTrackerBinder(contentUploadStateTrackerBinder)
.playable(messageContent.mimeType == MimeTypes.Gif)
.highlighted(highlight)
.mediaData(data)
.apply {
if (messageContent.msgType == MessageType.MSGTYPE_STICKER_LOCAL) {
mode(ImageContentRenderer.Mode.STICKER)
clickListener { view ->
callback?.onImageMessageClicked(messageContent, data, view, listOf(data))
}
} else {
clickListener { view ->
callback?.onImageMessageClicked(messageContent, data, view, emptyList())
}
}
}
}
private fun buildVideoMessageItem(messageContent: MessageVideoContent,
informationData: MessageInformationData,
highlight: Boolean,
callback: TimelineEventController.Callback?,
attributes: AbsMessageItem.Attributes): MessageImageVideoItem? {
private fun buildVideoMessageItem(
messageContent: MessageVideoContent,
informationData: MessageInformationData,
highlight: Boolean,
callback: TimelineEventController.Callback?,
attributes: AbsMessageItem.Attributes,
): MessageImageVideoItem? {
val (maxWidth, maxHeight) = timelineMediaSizeProvider.getMaxSize()
val thumbnailData = ImageContentRenderer.Data(
eventId = informationData.eventId,
filename = messageContent.body,
mimeType = messageContent.mimeType,
url = messageContent.videoInfo?.getThumbnailUrl(),
elementToDecrypt = messageContent.videoInfo?.thumbnailFile?.toElementToDecrypt(),
height = messageContent.videoInfo?.height,
maxHeight = maxHeight,
width = messageContent.videoInfo?.width,
maxWidth = maxWidth,
allowNonMxcUrls = informationData.sendState.isSending()
eventId = informationData.eventId,
filename = messageContent.body,
mimeType = messageContent.mimeType,
url = messageContent.videoInfo?.getThumbnailUrl(),
elementToDecrypt = messageContent.videoInfo?.thumbnailFile?.toElementToDecrypt(),
height = messageContent.videoInfo?.height,
maxHeight = maxHeight,
width = messageContent.videoInfo?.width,
maxWidth = maxWidth,
allowNonMxcUrls = informationData.sendState.isSending()
)
val videoData = VideoContentRenderer.Data(
eventId = informationData.eventId,
filename = messageContent.body,
mimeType = messageContent.mimeType,
url = messageContent.getFileUrl(),
elementToDecrypt = messageContent.encryptedFileInfo?.toElementToDecrypt(),
thumbnailMediaData = thumbnailData
eventId = informationData.eventId,
filename = messageContent.body,
mimeType = messageContent.mimeType,
url = messageContent.getFileUrl(),
elementToDecrypt = messageContent.encryptedFileInfo?.toElementToDecrypt(),
thumbnailMediaData = thumbnailData
)
return MessageImageVideoItem_()
.leftGuideline(avatarSizeProvider.leftGuideline)
.attributes(attributes)
.imageContentRenderer(imageContentRenderer)
.contentUploadStateTrackerBinder(contentUploadStateTrackerBinder)
.playable(true)
.highlighted(highlight)
.mediaData(thumbnailData)
.clickListener { view -> callback?.onVideoMessageClicked(messageContent, videoData, view.findViewById(R.id.messageThumbnailView)) }
.leftGuideline(avatarSizeProvider.leftGuideline)
.attributes(attributes)
.imageContentRenderer(imageContentRenderer)
.contentUploadStateTrackerBinder(contentUploadStateTrackerBinder)
.playable(true)
.highlighted(highlight)
.mediaData(thumbnailData)
.clickListener { view -> callback?.onVideoMessageClicked(messageContent, videoData, view.findViewById(R.id.messageThumbnailView)) }
}
private fun buildItemForTextContent(messageContent: MessageTextContent,
informationData: MessageInformationData,
highlight: Boolean,
callback: TimelineEventController.Callback?,
attributes: AbsMessageItem.Attributes): VectorEpoxyModel<*>? {
private fun buildItemForTextContent(
messageContent: MessageTextContent,
informationData: MessageInformationData,
highlight: Boolean,
callback: TimelineEventController.Callback?,
attributes: AbsMessageItem.Attributes,
): VectorEpoxyModel<*>? {
val matrixFormattedBody = messageContent.matrixFormattedBody
return if (matrixFormattedBody != null) {
buildFormattedTextItem(matrixFormattedBody, informationData, highlight, callback, attributes)
@ -557,50 +595,56 @@ class MessageItemFactory @Inject constructor(
}
}
private fun buildFormattedTextItem(matrixFormattedBody: String,
informationData: MessageInformationData,
highlight: Boolean,
callback: TimelineEventController.Callback?,
attributes: AbsMessageItem.Attributes): MessageTextItem? {
private fun buildFormattedTextItem(
matrixFormattedBody: String,
informationData: MessageInformationData,
highlight: Boolean,
callback: TimelineEventController.Callback?,
attributes: AbsMessageItem.Attributes,
): MessageTextItem? {
val compressed = htmlCompressor.compress(matrixFormattedBody)
val renderedFormattedBody = htmlRenderer.get().render(compressed, pillsPostProcessor) as Spanned
return buildMessageTextItem(renderedFormattedBody, true, informationData, highlight, callback, attributes)
}
private fun buildMessageTextItem(body: CharSequence,
isFormatted: Boolean,
informationData: MessageInformationData,
highlight: Boolean,
callback: TimelineEventController.Callback?,
attributes: AbsMessageItem.Attributes): MessageTextItem? {
private fun buildMessageTextItem(
body: CharSequence,
isFormatted: Boolean,
informationData: MessageInformationData,
highlight: Boolean,
callback: TimelineEventController.Callback?,
attributes: AbsMessageItem.Attributes,
): MessageTextItem? {
val renderedBody = textRenderer.render(body)
val bindingOptions = spanUtils.getBindingOptions(renderedBody)
val linkifiedBody = renderedBody.linkify(callback)
return MessageTextItem_()
.message(
if (informationData.hasBeenEdited) {
annotateWithEdited(linkifiedBody, callback, informationData)
} else {
linkifiedBody
}.toEpoxyCharSequence()
)
.useBigFont(linkifiedBody.length <= MAX_NUMBER_OF_EMOJI_FOR_BIG_FONT * 2 && containsOnlyEmojis(linkifiedBody.toString()))
.bindingOptions(bindingOptions)
.markwonPlugins(htmlRenderer.get().plugins)
.searchForPills(isFormatted)
.previewUrlRetriever(callback?.getPreviewUrlRetriever())
.imageContentRenderer(imageContentRenderer)
.previewUrlCallback(callback)
.leftGuideline(avatarSizeProvider.leftGuideline)
.attributes(attributes)
.highlighted(highlight)
.movementMethod(createLinkMovementMethod(callback))
.message(
if (informationData.hasBeenEdited) {
annotateWithEdited(linkifiedBody, callback, informationData)
} else {
linkifiedBody
}.toEpoxyCharSequence()
)
.useBigFont(linkifiedBody.length <= MAX_NUMBER_OF_EMOJI_FOR_BIG_FONT * 2 && containsOnlyEmojis(linkifiedBody.toString()))
.bindingOptions(bindingOptions)
.markwonPlugins(htmlRenderer.get().plugins)
.searchForPills(isFormatted)
.previewUrlRetriever(callback?.getPreviewUrlRetriever())
.imageContentRenderer(imageContentRenderer)
.previewUrlCallback(callback)
.leftGuideline(avatarSizeProvider.leftGuideline)
.attributes(attributes)
.highlighted(highlight)
.movementMethod(createLinkMovementMethod(callback))
}
private fun annotateWithEdited(linkifiedBody: CharSequence,
callback: TimelineEventController.Callback?,
informationData: MessageInformationData): Spannable {
private fun annotateWithEdited(
linkifiedBody: CharSequence,
callback: TimelineEventController.Callback?,
informationData: MessageInformationData,
): Spannable {
val spannable = SpannableStringBuilder()
spannable.append(linkifiedBody)
val editedSuffix = stringProvider.getString(R.string.edited_suffix)
@ -609,17 +653,17 @@ class MessageItemFactory @Inject constructor(
val editStart = spannable.lastIndexOf(editedSuffix)
val editEnd = editStart + editedSuffix.length
spannable.setSpan(
ForegroundColorSpan(color),
editStart,
editEnd,
Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
ForegroundColorSpan(color),
editStart,
editEnd,
Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
// Note: text size is set to 14sp
spannable.setSpan(
AbsoluteSizeSpan(dimensionConverter.spToPx(13)),
editStart,
editEnd,
Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
AbsoluteSizeSpan(dimensionConverter.spToPx(13)),
editStart,
editEnd,
Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
spannable.setSpan(object : ClickableSpan() {
override fun onClick(widget: View) {
@ -630,18 +674,20 @@ class MessageItemFactory @Inject constructor(
// nop
}
},
editStart,
editEnd,
Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
editStart,
editEnd,
Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
return spannable
}
private fun buildNoticeMessageItem(messageContent: MessageNoticeContent,
@Suppress("UNUSED_PARAMETER")
informationData: MessageInformationData,
highlight: Boolean,
callback: TimelineEventController.Callback?,
attributes: AbsMessageItem.Attributes): MessageTextItem? {
private fun buildNoticeMessageItem(
messageContent: MessageNoticeContent,
@Suppress("UNUSED_PARAMETER")
informationData: MessageInformationData,
highlight: Boolean,
callback: TimelineEventController.Callback?,
attributes: AbsMessageItem.Attributes,
): MessageTextItem? {
val htmlBody = messageContent.getHtmlBody()
val formattedBody = span {
text = htmlBody
@ -653,22 +699,24 @@ class MessageItemFactory @Inject constructor(
val message = formattedBody.linkify(callback)
return MessageTextItem_()
.leftGuideline(avatarSizeProvider.leftGuideline)
.previewUrlRetriever(callback?.getPreviewUrlRetriever())
.imageContentRenderer(imageContentRenderer)
.previewUrlCallback(callback)
.attributes(attributes)
.message(message.toEpoxyCharSequence())
.bindingOptions(bindingOptions)
.highlighted(highlight)
.movementMethod(createLinkMovementMethod(callback))
.leftGuideline(avatarSizeProvider.leftGuideline)
.previewUrlRetriever(callback?.getPreviewUrlRetriever())
.imageContentRenderer(imageContentRenderer)
.previewUrlCallback(callback)
.attributes(attributes)
.message(message.toEpoxyCharSequence())
.bindingOptions(bindingOptions)
.highlighted(highlight)
.movementMethod(createLinkMovementMethod(callback))
}
private fun buildEmoteMessageItem(messageContent: MessageEmoteContent,
informationData: MessageInformationData,
highlight: Boolean,
callback: TimelineEventController.Callback?,
attributes: AbsMessageItem.Attributes): MessageTextItem? {
private fun buildEmoteMessageItem(
messageContent: MessageEmoteContent,
informationData: MessageInformationData,
highlight: Boolean,
callback: TimelineEventController.Callback?,
attributes: AbsMessageItem.Attributes,
): MessageTextItem? {
val formattedBody = SpannableStringBuilder()
formattedBody.append("* ${informationData.memberName} ")
formattedBody.append(messageContent.getHtmlBody())
@ -676,46 +724,48 @@ class MessageItemFactory @Inject constructor(
val message = formattedBody.linkify(callback)
return MessageTextItem_()
.message(
if (informationData.hasBeenEdited) {
annotateWithEdited(message, callback, informationData)
} else {
message
}.toEpoxyCharSequence()
)
.bindingOptions(bindingOptions)
.leftGuideline(avatarSizeProvider.leftGuideline)
.previewUrlRetriever(callback?.getPreviewUrlRetriever())
.imageContentRenderer(imageContentRenderer)
.previewUrlCallback(callback)
.attributes(attributes)
.highlighted(highlight)
.movementMethod(createLinkMovementMethod(callback))
.message(
if (informationData.hasBeenEdited) {
annotateWithEdited(message, callback, informationData)
} else {
message
}.toEpoxyCharSequence()
)
.bindingOptions(bindingOptions)
.leftGuideline(avatarSizeProvider.leftGuideline)
.previewUrlRetriever(callback?.getPreviewUrlRetriever())
.imageContentRenderer(imageContentRenderer)
.previewUrlCallback(callback)
.attributes(attributes)
.highlighted(highlight)
.movementMethod(createLinkMovementMethod(callback))
}
private fun MessageContentWithFormattedBody.getHtmlBody(): CharSequence {
return matrixFormattedBody
?.let { htmlCompressor.compress(it) }
?.let { htmlRenderer.get().render(it, pillsPostProcessor) }
?.let { htmlCompressor.compress(it) }
?.let { htmlRenderer.get().render(it, pillsPostProcessor) }
?: body
}
private fun buildRedactedItem(attributes: AbsMessageItem.Attributes,
highlight: Boolean): RedactedMessageItem? {
private fun buildRedactedItem(
attributes: AbsMessageItem.Attributes,
highlight: Boolean,
): RedactedMessageItem? {
return RedactedMessageItem_()
.layout(attributes.informationData.messageLayout.layoutRes)
.leftGuideline(avatarSizeProvider.leftGuideline)
.attributes(attributes)
.highlighted(highlight)
.layout(attributes.informationData.messageLayout.layoutRes)
.leftGuideline(avatarSizeProvider.leftGuideline)
.attributes(attributes)
.highlighted(highlight)
}
private fun List<Int?>?.toFft(): List<Int>? {
return this
?.filterNotNull()
?.map {
// Value comes from AudioWaveformView.MAX_FFT, and 1024 is the max value in the Matrix spec
it * AudioWaveformView.MAX_FFT / 1024
}
?.filterNotNull()
?.map {
// Value comes from AudioWaveformView.MAX_FFT, and 1024 is the max value in the Matrix spec
it * AudioWaveformView.MAX_FFT / 1024
}
}
companion object {

View File

@ -39,13 +39,13 @@ abstract class PollItem : AbsMessageItem<PollItem.Holder>() {
var eventId: String? = null
@EpoxyAttribute
var pollSent: Boolean = false
var canVote: Boolean = false
@EpoxyAttribute
var totalVotesText: String? = null
@EpoxyAttribute
var edited: Boolean = false
@EpoxyAttribute
var edited: Boolean = false
@EpoxyAttribute
lateinit var optionViewStates: List<PollOptionViewState>
@ -54,7 +54,6 @@ abstract class PollItem : AbsMessageItem<PollItem.Holder>() {
override fun bind(holder: Holder) {
super.bind(holder)
val relatedEventId = eventId ?: return
renderSendState(holder.view, holder.questionTextView)
@ -73,13 +72,19 @@ abstract class PollItem : AbsMessageItem<PollItem.Holder>() {
optionViewStates.forEachIndexed { index, optionViewState ->
views.getOrNull(index)?.let {
it.render(optionViewState)
it.setOnClickListener {
callback?.onTimelineItemAction(RoomDetailAction.VoteToPoll(relatedEventId, optionViewState.optionId))
}
it.setOnClickListener { onPollItemClick(optionViewState) }
}
}
}
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) {
val questionTextView by bind<TextView>(R.id.questionTextView)
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.PinArgs
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.CreatePollArgs
import im.vector.app.features.poll.create.PollMode
import im.vector.app.features.roomdirectory.RoomDirectoryActivity
import im.vector.app.features.roomdirectory.RoomDirectoryData
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.media.AttachmentData
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.roompreview.RoomPreviewData
import im.vector.app.features.settings.VectorSettingsActivity

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
package im.vector.app.features.poll.create
package im.vector.app.features.poll
enum class PollMode {
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.platform.VectorBaseFragment
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 kotlinx.parcelize.Parcelize
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.hiltMavericksViewModelFactory
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.room.model.message.MessagePollContent
import org.matrix.android.sdk.api.session.room.model.message.PollType

View File

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