mirror of
https://github.com/element-hq/element-android
synced 2024-11-26 19:35:42 +03:00
Merge pull request #7900 from vector-im/feature/ons/render_ended_poll
Render ended polls (PSG-904)
This commit is contained in:
commit
c012d559b7
24 changed files with 231 additions and 61 deletions
1
changelog.d/7900.feature
Normal file
1
changelog.d/7900.feature
Normal file
|
@ -0,0 +1 @@
|
|||
Render ended polls
|
|
@ -3178,7 +3178,8 @@
|
|||
<item quantity="other">Final result based on %1$d votes</item>
|
||||
</plurals>
|
||||
<string name="poll_end_action">End poll</string>
|
||||
<string name="a11y_poll_winner_option">winner option</string>
|
||||
<!-- TODO TO BE REMOVED -->
|
||||
<string name="a11y_poll_winner_option" tools:ignore="UnusedResources">winner option</string>
|
||||
<string name="end_poll_confirmation_title">End this poll?</string>
|
||||
<string name="end_poll_confirmation_description">This will stop people from being able to vote and will display the final results of the poll.</string>
|
||||
<string name="end_poll_confirmation_approve_button">End poll</string>
|
||||
|
@ -3192,6 +3193,7 @@
|
|||
<string name="open_poll_option_description">Voters see results as soon as they have voted</string>
|
||||
<string name="closed_poll_option_title">Closed poll</string>
|
||||
<string name="closed_poll_option_description">Results are only revealed when you end the poll</string>
|
||||
<string name="ended_poll_indicator">Ended the poll.</string>
|
||||
<string name="room_polls_active">Active polls</string>
|
||||
<string name="room_polls_active_no_item">There are no active polls in this room</string>
|
||||
<string name="room_polls_ended">Past polls</string>
|
||||
|
@ -3509,6 +3511,9 @@
|
|||
<string name="message_reply_to_sender_sent_video">sent a video.</string>
|
||||
<string name="message_reply_to_sender_sent_sticker">sent a sticker.</string>
|
||||
<string name="message_reply_to_sender_created_poll">created a poll.</string>
|
||||
<string name="message_reply_to_sender_ended_poll">ended a poll.</string>
|
||||
<string name="message_reply_to_poll_preview">Poll</string>
|
||||
<string name="message_reply_to_ended_poll_preview">Ended poll</string>
|
||||
|
||||
<string name="settings_access_token">Access Token</string>
|
||||
<string name="settings_access_token_summary">Your access token gives full access to your account. Do not share it with anyone.</string>
|
||||
|
|
|
@ -248,7 +248,7 @@ data class Event(
|
|||
if (isRedacted()) return "Message removed"
|
||||
val text = getDecryptedValue() ?: run {
|
||||
if (isPoll()) {
|
||||
return getPollQuestion() ?: "created a poll."
|
||||
return getTextSummaryForPoll()
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
@ -261,13 +261,23 @@ data class Event(
|
|||
isImageMessage() -> "sent an image."
|
||||
isVideoMessage() -> "sent a video."
|
||||
isSticker() -> "sent a sticker."
|
||||
isPoll() -> getPollQuestion() ?: "created a poll."
|
||||
isPoll() -> getTextSummaryForPoll()
|
||||
isLiveLocation() -> "Live location."
|
||||
isLocationMessage() -> "has shared their location."
|
||||
else -> text
|
||||
}
|
||||
}
|
||||
|
||||
private fun getTextSummaryForPoll(): String? {
|
||||
val pollQuestion = getPollQuestion()
|
||||
return when {
|
||||
pollQuestion != null -> pollQuestion
|
||||
isPollStart() -> "created a poll."
|
||||
isPollEnd() -> "ended a poll."
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
private fun Event.isQuote(): Boolean {
|
||||
if (isReplyRenderedInThread()) return false
|
||||
return getDecryptedValue("formatted_body")?.contains("<blockquote>") ?: false
|
||||
|
|
|
@ -18,6 +18,7 @@ package org.matrix.android.sdk.api.session.room.model.message
|
|||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import org.matrix.android.sdk.api.session.events.model.Content
|
||||
import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent
|
||||
|
||||
/**
|
||||
|
@ -25,5 +26,12 @@ import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultCon
|
|||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class MessageEndPollContent(
|
||||
@Json(name = "m.relates_to") val relatesTo: RelationDefaultContent? = null
|
||||
)
|
||||
/**
|
||||
* Local message type, not from server.
|
||||
*/
|
||||
@Transient
|
||||
override val msgType: String = MessageType.MSGTYPE_POLL_END,
|
||||
@Json(name = "body") override val body: String = "",
|
||||
@Json(name = "m.new_content") override val newContent: Content? = null,
|
||||
@Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null
|
||||
) : MessageContent
|
||||
|
|
|
@ -36,6 +36,7 @@ object MessageType {
|
|||
// Because poll events are not message events and they don't have msgtype field
|
||||
const val MSGTYPE_POLL_START = "org.matrix.android.sdk.poll.start"
|
||||
const val MSGTYPE_POLL_RESPONSE = "org.matrix.android.sdk.poll.response"
|
||||
const val MSGTYPE_POLL_END = "org.matrix.android.sdk.poll.end"
|
||||
|
||||
const val MSGTYPE_CONFETTI = "nic.custom.confetti"
|
||||
const val MSGTYPE_SNOWFALL = "io.element.effect.snowfall"
|
||||
|
|
|
@ -35,6 +35,7 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconInfoCo
|
|||
import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconLocationDataContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageContentWithFormattedBody
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageEndPollContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageStickerContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent
|
||||
|
@ -148,6 +149,7 @@ fun TimelineEvent.getLastMessageContent(): MessageContent? {
|
|||
// so toModel<MessageContent> won't parse them correctly
|
||||
// It's discriminated on event type instead. Maybe it shouldn't be MessageContent at all to avoid confusion?
|
||||
in EventType.POLL_START.values -> (getLastEditNewContent() ?: root.getClearContent()).toModel<MessagePollContent>()
|
||||
in EventType.POLL_END.values -> (getLastEditNewContent() ?: root.getClearContent()).toModel<MessageEndPollContent>()
|
||||
in EventType.STATE_ROOM_BEACON_INFO.values -> (getLastEditNewContent() ?: root.getClearContent()).toModel<MessageBeaconInfoContent>()
|
||||
in EventType.BEACON_LOCATION_DATA.values -> (getLastEditNewContent() ?: root.getClearContent()).toModel<MessageBeaconLocationDataContent>()
|
||||
else -> (getLastEditNewContent() ?: root.getClearContent()).toModel()
|
||||
|
|
|
@ -27,7 +27,7 @@ import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent
|
|||
|
||||
fun TimelineEvent.canReact(): Boolean {
|
||||
// Only event of type EventType.MESSAGE, EventType.STICKER and EventType.POLL_START are supported for the moment
|
||||
return root.getClearType() in listOf(EventType.MESSAGE, EventType.STICKER) + EventType.POLL_START.values &&
|
||||
return root.getClearType() in listOf(EventType.MESSAGE, EventType.STICKER) + EventType.POLL_START.values + EventType.POLL_END.values &&
|
||||
root.sendState == SendState.SYNCED &&
|
||||
!root.isRedacted()
|
||||
}
|
||||
|
|
|
@ -44,6 +44,7 @@ import org.commonmark.parser.Parser
|
|||
import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconInfoContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageEndPollContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageFormat
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent
|
||||
|
@ -181,6 +182,7 @@ class PlainTextComposerLayout @JvmOverloads constructor(
|
|||
is MessageAudioContent -> getAudioContentBodyText(messageContent)
|
||||
is MessagePollContent -> messageContent.getBestPollCreationInfo()?.question?.getBestQuestion()
|
||||
is MessageBeaconInfoContent -> resources.getString(R.string.live_location_description)
|
||||
is MessageEndPollContent -> resources.getString(R.string.message_reply_to_ended_poll_preview)
|
||||
else -> messageContent?.body.orEmpty()
|
||||
}
|
||||
var formattedBody: CharSequence? = null
|
||||
|
|
|
@ -25,8 +25,14 @@ import javax.inject.Inject
|
|||
class CheckIfCanReplyEventUseCase @Inject constructor() {
|
||||
|
||||
fun execute(event: TimelineEvent, messageContent: MessageContent?, actionPermissions: ActionPermissions): Boolean {
|
||||
// Only EventType.MESSAGE, EventType.POLL_START and EventType.STATE_ROOM_BEACON_INFO event types are supported for the moment
|
||||
if (event.root.getClearType() !in EventType.STATE_ROOM_BEACON_INFO.values + EventType.POLL_START.values + EventType.MESSAGE) return false
|
||||
// Only EventType.MESSAGE, EventType.POLL_START, EventType.POLL_END and EventType.STATE_ROOM_BEACON_INFO event types are supported for the moment
|
||||
if (event.root.getClearType() !in
|
||||
EventType.STATE_ROOM_BEACON_INFO.values +
|
||||
EventType.POLL_START.values +
|
||||
EventType.POLL_END.values +
|
||||
EventType.MESSAGE
|
||||
) return false
|
||||
|
||||
if (!actionPermissions.canSendMessage) return false
|
||||
return when (messageContent?.msgType) {
|
||||
MessageType.MSGTYPE_TEXT,
|
||||
|
@ -37,6 +43,7 @@ class CheckIfCanReplyEventUseCase @Inject constructor() {
|
|||
MessageType.MSGTYPE_AUDIO,
|
||||
MessageType.MSGTYPE_FILE,
|
||||
MessageType.MSGTYPE_POLL_START,
|
||||
MessageType.MSGTYPE_POLL_END,
|
||||
MessageType.MSGTYPE_BEACON_INFO,
|
||||
MessageType.MSGTYPE_LOCATION -> true
|
||||
else -> false
|
||||
|
|
|
@ -498,6 +498,7 @@ class MessageActionsViewModel @AssistedInject constructor(
|
|||
MessageType.MSGTYPE_AUDIO,
|
||||
MessageType.MSGTYPE_FILE,
|
||||
MessageType.MSGTYPE_POLL_START,
|
||||
MessageType.MSGTYPE_POLL_END,
|
||||
MessageType.MSGTYPE_STICKER_LOCAL -> event.root.threadDetails?.isRootThread ?: false
|
||||
else -> false
|
||||
}
|
||||
|
@ -529,8 +530,8 @@ class MessageActionsViewModel @AssistedInject constructor(
|
|||
}
|
||||
|
||||
private fun canViewReactions(event: TimelineEvent): Boolean {
|
||||
// Only event of type EventType.MESSAGE, EventType.STICKER and EventType.POLL_START are supported for the moment
|
||||
if (event.root.getClearType() !in listOf(EventType.MESSAGE, EventType.STICKER) + EventType.POLL_START.values) return false
|
||||
// Only event of type EventType.MESSAGE, EventType.STICKER, EventType.POLL_START, EventType.POLL_END are supported for the moment
|
||||
if (event.root.getClearType() !in listOf(EventType.MESSAGE, EventType.STICKER) + EventType.POLL_START.values + EventType.POLL_END.values) return false
|
||||
return event.annotations?.reactionsSummary?.isNotEmpty() ?: false
|
||||
}
|
||||
|
||||
|
|
|
@ -91,11 +91,13 @@ import org.matrix.android.sdk.api.session.events.model.RelationType
|
|||
import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent
|
||||
import org.matrix.android.sdk.api.session.events.model.isThread
|
||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||
import org.matrix.android.sdk.api.session.room.getTimelineEvent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconInfoContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageContentWithFormattedBody
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageEmoteContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageEndPollContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageFileContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageImageInfoContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageLocationContent
|
||||
|
@ -109,8 +111,10 @@ import org.matrix.android.sdk.api.session.room.model.message.asMessageAudioEvent
|
|||
import org.matrix.android.sdk.api.session.room.model.message.getFileUrl
|
||||
import org.matrix.android.sdk.api.session.room.model.message.getThumbnailUrl
|
||||
import org.matrix.android.sdk.api.session.room.model.relation.ReplyToContent
|
||||
import org.matrix.android.sdk.api.session.room.timeline.getRelationContent
|
||||
import org.matrix.android.sdk.api.settings.LightweightSettingsStorage
|
||||
import org.matrix.android.sdk.api.util.MimeTypes
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
class MessageItemFactory @Inject constructor(
|
||||
|
@ -202,7 +206,8 @@ class MessageItemFactory @Inject constructor(
|
|||
is MessageFileContent -> buildFileMessageItem(messageContent, highlight, attributes)
|
||||
is MessageAudioContent -> buildAudioContent(params, messageContent, informationData, highlight, 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, isEnded = false)
|
||||
is MessageEndPollContent -> buildEndedPollItem(event.getRelationContent()?.eventId, informationData, highlight, callback, attributes)
|
||||
is MessageLocationContent -> buildLocationItem(messageContent, informationData, highlight, attributes)
|
||||
is MessageBeaconInfoContent -> liveLocationShareMessageItemFactory.create(event, highlight, attributes)
|
||||
is MessageVoiceBroadcastInfoContent -> voiceBroadcastItemFactory.create(params, messageContent, highlight, attributes)
|
||||
|
@ -245,6 +250,7 @@ class MessageItemFactory @Inject constructor(
|
|||
highlight: Boolean,
|
||||
callback: TimelineEventController.Callback?,
|
||||
attributes: AbsMessageItem.Attributes,
|
||||
isEnded: Boolean,
|
||||
): PollItem {
|
||||
val pollViewState = pollItemViewStateFactory.create(pollContent, informationData)
|
||||
|
||||
|
@ -256,11 +262,35 @@ class MessageItemFactory @Inject constructor(
|
|||
.votesStatus(pollViewState.votesStatus)
|
||||
.optionViewStates(pollViewState.optionViewStates.orEmpty())
|
||||
.edited(informationData.hasBeenEdited)
|
||||
.ended(isEnded)
|
||||
.highlighted(highlight)
|
||||
.leftGuideline(avatarSizeProvider.leftGuideline)
|
||||
.callback(callback)
|
||||
}
|
||||
|
||||
private fun buildEndedPollItem(
|
||||
pollStartEventId: String?,
|
||||
informationData: MessageInformationData,
|
||||
highlight: Boolean,
|
||||
callback: TimelineEventController.Callback?,
|
||||
attributes: AbsMessageItem.Attributes,
|
||||
): PollItem? {
|
||||
pollStartEventId ?: return null.also {
|
||||
Timber.e("### buildEndedPollItem. Cannot render poll end event because poll start event id is null")
|
||||
}
|
||||
val pollStartEvent = session.roomService().getRoom(roomId)?.getTimelineEvent(pollStartEventId)
|
||||
val pollContent = pollStartEvent?.root?.getClearContent()?.toModel<MessagePollContent>() ?: return null
|
||||
|
||||
return buildPollItem(
|
||||
pollContent,
|
||||
informationData,
|
||||
highlight,
|
||||
callback,
|
||||
attributes,
|
||||
isEnded = true
|
||||
)
|
||||
}
|
||||
|
||||
private fun createPollQuestion(
|
||||
informationData: MessageInformationData,
|
||||
question: String,
|
||||
|
|
|
@ -102,6 +102,7 @@ class TimelineItemFactory @Inject constructor(
|
|||
// Message itemsX
|
||||
EventType.STICKER,
|
||||
in EventType.POLL_START.values,
|
||||
in EventType.POLL_END.values,
|
||||
EventType.MESSAGE -> messageItemFactory.create(params)
|
||||
EventType.REDACTION,
|
||||
EventType.KEY_VERIFICATION_ACCEPT,
|
||||
|
@ -114,8 +115,7 @@ class TimelineItemFactory @Inject constructor(
|
|||
EventType.CALL_SELECT_ANSWER,
|
||||
EventType.CALL_NEGOTIATE,
|
||||
EventType.REACTION,
|
||||
in EventType.POLL_RESPONSE.values,
|
||||
in EventType.POLL_END.values -> noticeItemFactory.create(params)
|
||||
in EventType.POLL_RESPONSE.values -> noticeItemFactory.create(params)
|
||||
in EventType.BEACON_LOCATION_DATA.values -> {
|
||||
if (event.root.isRedacted()) {
|
||||
messageItemFactory.create(params)
|
||||
|
|
|
@ -17,11 +17,14 @@
|
|||
package im.vector.app.features.home.room.detail.timeline.format
|
||||
|
||||
import android.content.Context
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.utils.TextUtils
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.api.session.events.model.isAudioMessage
|
||||
import org.matrix.android.sdk.api.session.events.model.isFileMessage
|
||||
import org.matrix.android.sdk.api.session.events.model.isImageMessage
|
||||
import org.matrix.android.sdk.api.session.events.model.isPollEnd
|
||||
import org.matrix.android.sdk.api.session.events.model.isPollStart
|
||||
import org.matrix.android.sdk.api.session.events.model.isVideoMessage
|
||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent
|
||||
|
@ -51,10 +54,16 @@ class EventDetailsFormatter @Inject constructor(
|
|||
event.isVideoMessage() -> formatForVideoMessage(event)
|
||||
event.isAudioMessage() -> formatForAudioMessage(event)
|
||||
event.isFileMessage() -> formatForFileMessage(event)
|
||||
event.isPollStart() -> formatPollMessage()
|
||||
event.isPollEnd() -> formatPollEndMessage()
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatPollMessage() = context.getString(R.string.message_reply_to_poll_preview)
|
||||
|
||||
private fun formatPollEndMessage() = context.getString(R.string.message_reply_to_ended_poll_preview)
|
||||
|
||||
/**
|
||||
* Example: "1024 x 720 - 670 kB".
|
||||
*/
|
||||
|
|
|
@ -23,8 +23,6 @@ import im.vector.app.core.extensions.localDateTime
|
|||
import im.vector.app.features.home.room.detail.timeline.factory.TimelineItemFactoryParams
|
||||
import im.vector.app.features.home.room.detail.timeline.item.E2EDecoration
|
||||
import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData
|
||||
import im.vector.app.features.home.room.detail.timeline.item.PollResponseData
|
||||
import im.vector.app.features.home.room.detail.timeline.item.PollVoteSummaryData
|
||||
import im.vector.app.features.home.room.detail.timeline.item.ReferencesInfoData
|
||||
import im.vector.app.features.home.room.detail.timeline.item.SendStateDecoration
|
||||
import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayoutFactory
|
||||
|
@ -54,7 +52,8 @@ class MessageInformationDataFactory @Inject constructor(
|
|||
private val session: Session,
|
||||
private val dateFormatter: VectorDateFormatter,
|
||||
private val messageLayoutFactory: TimelineMessageLayoutFactory,
|
||||
private val reactionsSummaryFactory: ReactionsSummaryFactory
|
||||
private val reactionsSummaryFactory: ReactionsSummaryFactory,
|
||||
private val pollResponseDataFactory: PollResponseDataFactory,
|
||||
) {
|
||||
|
||||
fun create(params: TimelineItemFactoryParams): MessageInformationData {
|
||||
|
@ -99,20 +98,7 @@ class MessageInformationDataFactory @Inject constructor(
|
|||
memberName = event.senderInfo.disambiguatedDisplayName,
|
||||
messageLayout = messageLayout,
|
||||
reactionsSummary = reactionsSummaryFactory.create(event),
|
||||
pollResponseAggregatedSummary = event.annotations?.pollResponseSummary?.let {
|
||||
PollResponseData(
|
||||
myVote = it.aggregatedContent?.myVote,
|
||||
isClosed = it.closedTime != null,
|
||||
votes = it.aggregatedContent?.votesSummary?.mapValues { votesSummary ->
|
||||
PollVoteSummaryData(
|
||||
total = votesSummary.value.total,
|
||||
percentage = votesSummary.value.percentage
|
||||
)
|
||||
},
|
||||
winnerVoteCount = it.aggregatedContent?.winnerVoteCount ?: 0,
|
||||
totalVotes = it.aggregatedContent?.totalVotes ?: 0
|
||||
)
|
||||
},
|
||||
pollResponseAggregatedSummary = pollResponseDataFactory.create(event),
|
||||
hasBeenEdited = event.hasBeenEdited(),
|
||||
hasPendingEdits = event.annotations?.editSummary?.localEchos?.any() ?: false,
|
||||
referencesInfoData = event.annotations?.referencesAggregatedSummary?.let { referencesAggregatedSummary ->
|
||||
|
|
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
* Copyright (c) 2023 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.home.room.detail.timeline.helper
|
||||
|
||||
import im.vector.app.core.di.ActiveSessionHolder
|
||||
import im.vector.app.features.home.room.detail.timeline.item.PollResponseData
|
||||
import im.vector.app.features.home.room.detail.timeline.item.PollVoteSummaryData
|
||||
import org.matrix.android.sdk.api.session.events.model.getRelationContent
|
||||
import org.matrix.android.sdk.api.session.events.model.isPollEnd
|
||||
import org.matrix.android.sdk.api.session.room.getTimelineEvent
|
||||
import org.matrix.android.sdk.api.session.room.model.PollResponseAggregatedSummary
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
class PollResponseDataFactory @Inject constructor(
|
||||
private val activeSessionHolder: ActiveSessionHolder,
|
||||
) {
|
||||
|
||||
fun create(event: TimelineEvent): PollResponseData? {
|
||||
val pollResponseSummary = getPollResponseSummary(event)
|
||||
return pollResponseSummary?.let {
|
||||
PollResponseData(
|
||||
myVote = it.aggregatedContent?.myVote,
|
||||
isClosed = it.closedTime != null,
|
||||
votes = it.aggregatedContent?.votesSummary?.mapValues { votesSummary ->
|
||||
PollVoteSummaryData(
|
||||
total = votesSummary.value.total,
|
||||
percentage = votesSummary.value.percentage
|
||||
)
|
||||
},
|
||||
winnerVoteCount = it.aggregatedContent?.winnerVoteCount ?: 0,
|
||||
totalVotes = it.aggregatedContent?.totalVotes ?: 0
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getPollResponseSummary(event: TimelineEvent): PollResponseAggregatedSummary? {
|
||||
return if (event.root.isPollEnd()) {
|
||||
val pollStartEventId = event.root.getRelationContent()?.eventId
|
||||
if (pollStartEventId.isNullOrEmpty()) {
|
||||
Timber.e("### Cannot render poll end event because poll start event id is null")
|
||||
null
|
||||
} else {
|
||||
activeSessionHolder
|
||||
.getSafeActiveSession()
|
||||
?.roomService()
|
||||
?.getRoom(event.roomId)
|
||||
?.getTimelineEvent(pollStartEventId)
|
||||
?.annotations
|
||||
?.pollResponseSummary
|
||||
}
|
||||
} else {
|
||||
event.annotations?.pollResponseSummary
|
||||
}
|
||||
}
|
||||
}
|
|
@ -55,6 +55,7 @@ object TimelineDisplayableEvents {
|
|||
VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO,
|
||||
) +
|
||||
EventType.POLL_START.values +
|
||||
EventType.POLL_END.values +
|
||||
EventType.STATE_ROOM_BEACON_INFO.values +
|
||||
EventType.BEACON_LOCATION_DATA.values
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ package im.vector.app.features.home.room.detail.timeline.item
|
|||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.core.view.children
|
||||
import androidx.core.view.isVisible
|
||||
import com.airbnb.epoxy.EpoxyAttribute
|
||||
import com.airbnb.epoxy.EpoxyModelClass
|
||||
import im.vector.app.R
|
||||
|
@ -50,6 +51,9 @@ abstract class PollItem : AbsMessageItem<PollItem.Holder>() {
|
|||
@EpoxyAttribute
|
||||
lateinit var optionViewStates: List<PollOptionViewState>
|
||||
|
||||
@EpoxyAttribute
|
||||
var ended: Boolean = false
|
||||
|
||||
override fun getViewStubId() = STUB_ID
|
||||
|
||||
override fun bind(holder: Holder) {
|
||||
|
@ -75,6 +79,8 @@ abstract class PollItem : AbsMessageItem<PollItem.Holder>() {
|
|||
it.setOnClickListener { onPollItemClick(optionViewState) }
|
||||
}
|
||||
}
|
||||
|
||||
holder.endedPollTextView.isVisible = ended
|
||||
}
|
||||
|
||||
private fun onPollItemClick(optionViewState: PollOptionViewState) {
|
||||
|
@ -89,6 +95,7 @@ abstract class PollItem : AbsMessageItem<PollItem.Holder>() {
|
|||
val questionTextView by bind<TextView>(R.id.questionTextView)
|
||||
val optionsContainer by bind<LinearLayout>(R.id.optionsContainer)
|
||||
val votesStatusTextView by bind<TextView>(R.id.optionsVotesStatusTextView)
|
||||
val endedPollTextView by bind<TextView>(R.id.endedPollTextView)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
|
|
@ -25,6 +25,7 @@ import androidx.core.view.isVisible
|
|||
import im.vector.app.R
|
||||
import im.vector.app.core.extensions.setAttributeTintedImageResource
|
||||
import im.vector.app.databinding.ItemPollOptionBinding
|
||||
import im.vector.app.features.themes.ThemeUtils
|
||||
|
||||
class PollOptionView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
|
@ -53,35 +54,40 @@ class PollOptionView @JvmOverloads constructor(
|
|||
|
||||
private fun renderPollSending() {
|
||||
views.optionCheckImageView.isVisible = false
|
||||
views.optionWinnerImageView.isVisible = false
|
||||
views.optionVoteCountTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0)
|
||||
hideVotes()
|
||||
renderVoteSelection(false)
|
||||
}
|
||||
|
||||
private fun renderPollEnded(state: PollOptionViewState.PollEnded) {
|
||||
views.optionCheckImageView.isVisible = false
|
||||
views.optionWinnerImageView.isVisible = state.isWinner
|
||||
val drawableStart = if (state.isWinner) R.drawable.ic_poll_winner else 0
|
||||
views.optionVoteCountTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(drawableStart, 0, 0, 0)
|
||||
views.optionVoteCountTextView.setTextColor(
|
||||
if (state.isWinner) ThemeUtils.getColor(context, R.attr.colorPrimary)
|
||||
else ThemeUtils.getColor(context, R.attr.vctr_content_secondary)
|
||||
)
|
||||
showVotes(state.voteCount, state.votePercentage)
|
||||
renderVoteSelection(state.isWinner)
|
||||
}
|
||||
|
||||
private fun renderPollReady() {
|
||||
views.optionCheckImageView.isVisible = true
|
||||
views.optionWinnerImageView.isVisible = false
|
||||
views.optionVoteCountTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0)
|
||||
hideVotes()
|
||||
renderVoteSelection(false)
|
||||
}
|
||||
|
||||
private fun renderPollVoted(state: PollOptionViewState.PollVoted) {
|
||||
views.optionCheckImageView.isVisible = true
|
||||
views.optionWinnerImageView.isVisible = false
|
||||
views.optionVoteCountTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0)
|
||||
showVotes(state.voteCount, state.votePercentage)
|
||||
renderVoteSelection(state.isSelected)
|
||||
}
|
||||
|
||||
private fun renderPollUndisclosed(state: PollOptionViewState.PollUndisclosed) {
|
||||
views.optionCheckImageView.isVisible = true
|
||||
views.optionWinnerImageView.isVisible = false
|
||||
views.optionVoteCountTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0)
|
||||
hideVotes()
|
||||
renderVoteSelection(state.isSelected)
|
||||
}
|
||||
|
|
|
@ -25,6 +25,8 @@ import org.matrix.android.sdk.api.session.events.model.isFileMessage
|
|||
import org.matrix.android.sdk.api.session.events.model.isImageMessage
|
||||
import org.matrix.android.sdk.api.session.events.model.isLiveLocation
|
||||
import org.matrix.android.sdk.api.session.events.model.isPoll
|
||||
import org.matrix.android.sdk.api.session.events.model.isPollEnd
|
||||
import org.matrix.android.sdk.api.session.events.model.isPollStart
|
||||
import org.matrix.android.sdk.api.session.events.model.isSticker
|
||||
import org.matrix.android.sdk.api.session.events.model.isVideoMessage
|
||||
import org.matrix.android.sdk.api.session.events.model.isVoiceMessage
|
||||
|
@ -93,10 +95,15 @@ class ProcessBodyOfReplyToEventUseCase @Inject constructor(
|
|||
)
|
||||
}
|
||||
repliedToEvent.isPoll() -> {
|
||||
val fallbackText = when {
|
||||
repliedToEvent.isPollStart() -> stringProvider.getString(R.string.message_reply_to_sender_created_poll)
|
||||
repliedToEvent.isPollEnd() -> stringProvider.getString(R.string.message_reply_to_sender_ended_poll)
|
||||
else -> ""
|
||||
}
|
||||
matrixFormattedBody.replaceRange(
|
||||
afterBreakingLineIndex,
|
||||
endOfBlockQuoteIndex,
|
||||
repliedToEvent.getPollQuestion() ?: stringProvider.getString(R.string.message_reply_to_sender_created_poll)
|
||||
repliedToEvent.getPollQuestion() ?: fallbackText
|
||||
)
|
||||
}
|
||||
repliedToEvent.isLiveLocation() -> {
|
||||
|
|
|
@ -50,6 +50,7 @@ class TimelineMessageLayoutFactory @Inject constructor(
|
|||
EventType.STICKER,
|
||||
) +
|
||||
EventType.POLL_START.values +
|
||||
EventType.POLL_END.values +
|
||||
EventType.STATE_ROOM_BEACON_INFO.values
|
||||
|
||||
// Can't be rendered in bubbles, so get back to default layout
|
||||
|
|
|
@ -36,34 +36,23 @@
|
|||
android:layout_marginStart="12dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginEnd="12dp"
|
||||
app:layout_constraintEnd_toEndOf="@id/optionWinnerImageView"
|
||||
app:layout_constraintEnd_toStartOf="@id/optionVoteCountTextView"
|
||||
app:layout_constraintStart_toEndOf="@id/optionCheckImageView"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="@sample/poll.json/data/answer" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/optionWinnerImageView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:contentDescription="@string/a11y_poll_winner_option"
|
||||
android:src="@drawable/ic_poll_winner"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/optionVoteCountTextView"
|
||||
style="@style/Widget.Vector.TextView.Caption"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginEnd="10dp"
|
||||
android:drawablePadding="6dp"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="@id/optionVoteProgress"
|
||||
app:layout_constraintBottom_toBottomOf="@id/optionNameTextView"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/optionVoteProgress"
|
||||
app:layout_constraintTop_toTopOf="@id/optionNameTextView"
|
||||
tools:drawableStartCompat="@drawable/ic_poll_winner"
|
||||
tools:text="@sample/poll.json/data/votes"
|
||||
tools:visibility="visible" />
|
||||
|
||||
|
@ -78,9 +67,9 @@
|
|||
android:layout_marginBottom="8dp"
|
||||
android:progressDrawable="@drawable/poll_option_progressbar_checked"
|
||||
app:layout_constraintBottom_toBottomOf="@id/optionBorderImageView"
|
||||
app:layout_constraintEnd_toStartOf="@id/optionVoteCountTextView"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/optionNameTextView"
|
||||
tools:progress="60" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
|
|
@ -2,9 +2,21 @@
|
|||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:minWidth="@dimen/chat_bubble_fixed_size"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
android:layout_height="wrap_content"
|
||||
android:minWidth="@dimen/chat_bubble_fixed_size">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/endedPollTextView"
|
||||
style="@style/Widget.Vector.TextView.Caption"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/ended_poll_indicator"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/questionTextView"
|
||||
|
@ -13,11 +25,10 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:textColor="?vctr_content_primary"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintHorizontal_bias="0"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/endedPollTextView"
|
||||
tools:text="@sample/poll.json/question" />
|
||||
|
||||
<LinearLayout
|
||||
|
|
|
@ -43,7 +43,7 @@ class CheckIfCanReplyEventUseCaseTest {
|
|||
|
||||
@Test
|
||||
fun `given reply is allowed for the event type when use case is executed then result is true`() {
|
||||
val eventTypes = EventType.STATE_ROOM_BEACON_INFO.values + EventType.POLL_START.values + EventType.MESSAGE
|
||||
val eventTypes = EventType.STATE_ROOM_BEACON_INFO.values + EventType.POLL_START.values + EventType.POLL_END.values + EventType.MESSAGE
|
||||
|
||||
eventTypes.forEach { eventType ->
|
||||
val event = givenAnEvent(eventType)
|
||||
|
@ -78,6 +78,7 @@ class CheckIfCanReplyEventUseCaseTest {
|
|||
MessageType.MSGTYPE_AUDIO,
|
||||
MessageType.MSGTYPE_FILE,
|
||||
MessageType.MSGTYPE_POLL_START,
|
||||
MessageType.MSGTYPE_POLL_END,
|
||||
MessageType.MSGTYPE_BEACON_INFO,
|
||||
MessageType.MSGTYPE_LOCATION
|
||||
)
|
||||
|
|
|
@ -29,6 +29,7 @@ import org.junit.After
|
|||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.events.model.getPollQuestion
|
||||
import org.matrix.android.sdk.api.session.events.model.isAudioMessage
|
||||
import org.matrix.android.sdk.api.session.events.model.isFileMessage
|
||||
|
@ -158,6 +159,7 @@ class ProcessBodyOfReplyToEventUseCaseTest {
|
|||
// Given
|
||||
givenTypeOfRepliedEvent(isPollMessage = true)
|
||||
givenNewContentForId(R.string.message_reply_to_sender_created_poll)
|
||||
every { fakeRepliedEvent.getClearType() } returns EventType.POLL_START.unstable
|
||||
every { fakeRepliedEvent.getPollQuestion() } returns null
|
||||
|
||||
executeAndAssertResult()
|
||||
|
@ -168,11 +170,23 @@ class ProcessBodyOfReplyToEventUseCaseTest {
|
|||
// Given
|
||||
givenTypeOfRepliedEvent(isPollMessage = true)
|
||||
givenNewContentForId(R.string.message_reply_to_sender_created_poll)
|
||||
every { fakeRepliedEvent.getClearType() } returns EventType.POLL_START.unstable
|
||||
every { fakeRepliedEvent.getPollQuestion() } returns A_NEW_CONTENT
|
||||
|
||||
executeAndAssertResult()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given a replied event of type poll end message when process the formatted body then content is replaced by correct string`() {
|
||||
// Given
|
||||
givenTypeOfRepliedEvent(isPollMessage = true)
|
||||
givenNewContentForId(R.string.message_reply_to_sender_ended_poll)
|
||||
every { fakeRepliedEvent.getClearType() } returns EventType.POLL_END.unstable
|
||||
every { fakeRepliedEvent.getPollQuestion() } returns null
|
||||
|
||||
executeAndAssertResult()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given a replied event of type live location message when process the formatted body then content is replaced by correct string`() {
|
||||
// Given
|
||||
|
|
Loading…
Reference in a new issue