diff --git a/changelog.d/7900.feature b/changelog.d/7900.feature
new file mode 100644
index 0000000000..c3cce1e0e6
--- /dev/null
+++ b/changelog.d/7900.feature
@@ -0,0 +1 @@
+Render ended polls
diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml
index d9f94ba27b..f1c2eb2c6d 100644
--- a/library/ui-strings/src/main/res/values/strings.xml
+++ b/library/ui-strings/src/main/res/values/strings.xml
@@ -3178,7 +3178,8 @@
") ?: false diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageEndPollContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageEndPollContent.kt index f0511903d0..6e31320b13 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageEndPollContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageEndPollContent.kt @@ -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 diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageType.kt index e97a5be303..f6b7675d4f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageType.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageType.kt @@ -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" diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt index 9053425a39..6320ea964d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt @@ -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 toModelwon'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 () + in EventType.POLL_END.values -> (getLastEditNewContent() ?: root.getClearContent()).toModel () in EventType.STATE_ROOM_BEACON_INFO.values -> (getLastEditNewContent() ?: root.getClearContent()).toModel () in EventType.BEACON_LOCATION_DATA.values -> (getLastEditNewContent() ?: root.getClearContent()).toModel () else -> (getLastEditNewContent() ?: root.getClearContent()).toModel() diff --git a/vector/src/main/java/im/vector/app/core/extensions/TimelineEvent.kt b/vector/src/main/java/im/vector/app/core/extensions/TimelineEvent.kt index c94f9cd921..89bd28fc93 100644 --- a/vector/src/main/java/im/vector/app/core/extensions/TimelineEvent.kt +++ b/vector/src/main/java/im/vector/app/core/extensions/TimelineEvent.kt @@ -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() } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/PlainTextComposerLayout.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/PlainTextComposerLayout.kt index 8f4dd9b71d..cf127d834f 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/PlainTextComposerLayout.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/PlainTextComposerLayout.kt @@ -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 diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/CheckIfCanReplyEventUseCase.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/CheckIfCanReplyEventUseCase.kt index a9df059cc1..fdd94d1559 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/CheckIfCanReplyEventUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/CheckIfCanReplyEventUseCase.kt @@ -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 diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt index a6d7e8386f..646cfa50d2 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt @@ -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 } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt index 42e031a3c4..219ccbe11c 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt @@ -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 () ?: return null + + return buildPollItem( + pollContent, + informationData, + highlight, + callback, + attributes, + isEnded = true + ) + } + private fun createPollQuestion( informationData: MessageInformationData, question: String, diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt index ae3ea143a7..61b2385d1d 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt @@ -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) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/EventDetailsFormatter.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/EventDetailsFormatter.kt index 2233a53eda..1d3f016951 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/EventDetailsFormatter.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/EventDetailsFormatter.kt @@ -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". */ diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt index 57a4388f74..3ee309425a 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt @@ -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 -> diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/PollResponseDataFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/PollResponseDataFactory.kt new file mode 100644 index 0000000000..533397b4d8 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/PollResponseDataFactory.kt @@ -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 + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt index 51e961f247..2dcb6cc6d8 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt @@ -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 } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollItem.kt index 54be4092ed..6fe19e9762 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollItem.kt @@ -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 () { @EpoxyAttribute lateinit var optionViewStates: List + @EpoxyAttribute + var ended: Boolean = false + override fun getViewStubId() = STUB_ID override fun bind(holder: Holder) { @@ -75,6 +79,8 @@ abstract class PollItem : AbsMessageItem () { it.setOnClickListener { onPollItemClick(optionViewState) } } } + + holder.endedPollTextView.isVisible = ended } private fun onPollItemClick(optionViewState: PollOptionViewState) { @@ -89,6 +95,7 @@ abstract class PollItem : AbsMessageItem () { val questionTextView by bind (R.id.questionTextView) val optionsContainer by bind (R.id.optionsContainer) val votesStatusTextView by bind (R.id.optionsVotesStatusTextView) + val endedPollTextView by bind (R.id.endedPollTextView) } companion object { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollOptionView.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollOptionView.kt index 20aa6e3af2..e8d636e20b 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollOptionView.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollOptionView.kt @@ -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) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/render/ProcessBodyOfReplyToEventUseCase.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/render/ProcessBodyOfReplyToEventUseCase.kt index 2197d89a2c..ff814d4cbc 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/render/ProcessBodyOfReplyToEventUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/render/ProcessBodyOfReplyToEventUseCase.kt @@ -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() -> { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayoutFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayoutFactory.kt index c207a5f67e..6e34aeeca2 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayoutFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayoutFactory.kt @@ -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 diff --git a/vector/src/main/res/layout/item_poll_option.xml b/vector/src/main/res/layout/item_poll_option.xml index 986bfeaa35..ff7d4498fb 100644 --- a/vector/src/main/res/layout/item_poll_option.xml +++ b/vector/src/main/res/layout/item_poll_option.xml @@ -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" /> - - @@ -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" /> - \ No newline at end of file + diff --git a/vector/src/main/res/layout/item_timeline_event_poll.xml b/vector/src/main/res/layout/item_timeline_event_poll.xml index 393b736260..9151fc68cf 100644 --- a/vector/src/main/res/layout/item_timeline_event_poll.xml +++ b/vector/src/main/res/layout/item_timeline_event_poll.xml @@ -2,9 +2,21 @@ + android:layout_height="wrap_content" + android:minWidth="@dimen/chat_bubble_fixed_size"> + + 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 ) diff --git a/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/render/ProcessBodyOfReplyToEventUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/render/ProcessBodyOfReplyToEventUseCaseTest.kt index f612861511..c38afe20ec 100644 --- a/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/render/ProcessBodyOfReplyToEventUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/render/ProcessBodyOfReplyToEventUseCaseTest.kt @@ -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