Merge pull request #7900 from vector-im/feature/ons/render_ended_poll

Render ended polls (PSG-904)
This commit is contained in:
Onuray Sahin 2023-01-10 21:06:03 +03:00 committed by GitHub
commit c012d559b7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 231 additions and 61 deletions

1
changelog.d/7900.feature Normal file
View file

@ -0,0 +1 @@
Render ended polls

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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".
*/

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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