mirror of
https://github.com/element-hq/element-android
synced 2024-11-28 21:48:50 +03:00
Merge pull request #4624 from vector-im/feature/ons/poll_timeline
Poll Feature - Timeline
This commit is contained in:
commit
38e7e2fe4e
61 changed files with 1035 additions and 663 deletions
1
changelog.d/4653.feature
Normal file
1
changelog.d/4653.feature
Normal file
|
@ -0,0 +1 @@
|
|||
Poll Feature - Render in timeline
|
|
@ -104,6 +104,8 @@ object EventType {
|
|||
|
||||
// Poll
|
||||
const val POLL_START = "org.matrix.msc3381.poll.start"
|
||||
const val POLL_RESPONSE = "org.matrix.msc3381.poll.response"
|
||||
const val POLL_END = "org.matrix.msc3381.poll.end"
|
||||
|
||||
// Unwedging
|
||||
internal const val DUMMY = "m.dummy"
|
||||
|
|
|
@ -24,25 +24,24 @@ import com.squareup.moshi.JsonClass
|
|||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class PollSummaryContent(
|
||||
// Index of my vote
|
||||
var myVote: Int? = null,
|
||||
var myVote: String? = null,
|
||||
// Array of VoteInfo, list is constructed so that there is only one vote by user
|
||||
// And that optionIndex is valid
|
||||
var votes: List<VoteInfo>? = null
|
||||
) {
|
||||
var votes: List<VoteInfo>? = null,
|
||||
var votesSummary: Map<String, VoteSummary>? = null,
|
||||
var totalVotes: Int = 0,
|
||||
var winnerVoteCount: Int = 0
|
||||
)
|
||||
|
||||
fun voteCount(): Int {
|
||||
return votes?.size ?: 0
|
||||
}
|
||||
|
||||
fun voteCountForOption(optionIndex: Int): Int {
|
||||
return votes?.filter { it.optionIndex == optionIndex }?.count() ?: 0
|
||||
}
|
||||
}
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class VoteSummary(
|
||||
val total: Int = 0,
|
||||
val percentage: Double = 0.0
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class VoteInfo(
|
||||
val userId: String,
|
||||
val optionIndex: Int,
|
||||
val option: String,
|
||||
val voteTimestamp: Long
|
||||
)
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* 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 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.room.model.relation.RelationDefaultContent
|
||||
|
||||
/**
|
||||
* Class representing the org.matrix.msc3381.poll.end event content
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class MessageEndPollContent(
|
||||
@Json(name = "m.relates_to") val relatesTo: RelationDefaultContent? = null
|
||||
)
|
|
@ -1,40 +0,0 @@
|
|||
/*
|
||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* 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 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
|
||||
|
||||
// Possible values for optionType
|
||||
const val OPTION_TYPE_POLL = "org.matrix.poll"
|
||||
const val OPTION_TYPE_BUTTONS = "org.matrix.buttons"
|
||||
|
||||
/**
|
||||
* Polls and bot buttons are m.room.message events with a msgtype of m.options,
|
||||
* Ref: https://github.com/matrix-org/matrix-doc/pull/2192
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class MessageOptionsContent(
|
||||
@Json(name = MessageContent.MSG_TYPE_JSON_KEY) override val msgType: String = MessageType.MSGTYPE_OPTIONS,
|
||||
@Json(name = "type") val optionType: String? = null,
|
||||
@Json(name = "body") override val body: String,
|
||||
@Json(name = "label") val label: String?,
|
||||
@Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,
|
||||
@Json(name = "options") val options: List<OptionItem>? = null,
|
||||
@Json(name = "m.new_content") override val newContent: Content? = null
|
||||
) : MessageContent
|
|
@ -18,8 +18,18 @@ 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
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class MessagePollContent(
|
||||
@Json(name = "org.matrix.msc3381.poll.start") val pollCreationInfo: PollCreationInfo? = null
|
||||
)
|
||||
/**
|
||||
* Local message type, not from server
|
||||
*/
|
||||
@Transient
|
||||
override val msgType: String = MessageType.MSGTYPE_POLL_START,
|
||||
@Json(name = "body") override val body: String = "",
|
||||
@Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,
|
||||
@Json(name = "m.new_content") override val newContent: Content? = null,
|
||||
@Json(name = "org.matrix.msc3381.poll.start") val pollCreationInfo: PollCreationInfo? = null
|
||||
) : MessageContent
|
||||
|
|
|
@ -21,13 +21,15 @@ 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
|
||||
|
||||
/**
|
||||
* Ref: https://github.com/matrix-org/matrix-doc/pull/2192
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class MessagePollResponseContent(
|
||||
@Json(name = MessageContent.MSG_TYPE_JSON_KEY) override val msgType: String = MessageType.MSGTYPE_RESPONSE,
|
||||
@Json(name = "body") override val body: String,
|
||||
/**
|
||||
* Local message type, not from server
|
||||
*/
|
||||
@Transient
|
||||
override val msgType: String = MessageType.MSGTYPE_POLL_RESPONSE,
|
||||
@Json(name = "body") override val body: String = "",
|
||||
@Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,
|
||||
@Json(name = "m.new_content") override val newContent: Content? = null
|
||||
@Json(name = "m.new_content") override val newContent: Content? = null,
|
||||
@Json(name = "org.matrix.msc3381.poll.response") val response: PollResponse? = null
|
||||
) : MessageContent
|
||||
|
|
|
@ -25,15 +25,18 @@ object MessageType {
|
|||
const val MSGTYPE_VIDEO = "m.video"
|
||||
const val MSGTYPE_LOCATION = "m.location"
|
||||
const val MSGTYPE_FILE = "m.file"
|
||||
const val MSGTYPE_OPTIONS = "org.matrix.options"
|
||||
const val MSGTYPE_RESPONSE = "org.matrix.response"
|
||||
const val MSGTYPE_POLL_CLOSED = "org.matrix.poll_closed"
|
||||
|
||||
const val MSGTYPE_VERIFICATION_REQUEST = "m.key.verification.request"
|
||||
|
||||
// Add, in local, a fake message type in order to StickerMessage can inherit Message class
|
||||
// Because sticker isn't a message type but a event type without msgtype field
|
||||
const val MSGTYPE_STICKER_LOCAL = "org.matrix.android.sdk.sticker"
|
||||
|
||||
// Fake message types for poll events to be able to inherit them from MessageContent
|
||||
// Because poll events are not message events and they don't hanve 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_CONFETTI = "nic.custom.confetti"
|
||||
const val MSGTYPE_SNOWFALL = "io.element.effect.snowfall"
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
* Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
@ -19,11 +19,7 @@ package org.matrix.android.sdk.api.session.room.model.message
|
|||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/**
|
||||
* Ref: https://github.com/matrix-org/matrix-doc/pull/2192
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class OptionItem(
|
||||
@Json(name = "label") val label: String?,
|
||||
@Json(name = "value") val value: String?
|
||||
data class PollResponse(
|
||||
@Json(name = "answers") val answers: List<String>? = null
|
||||
)
|
|
@ -91,11 +91,17 @@ interface SendService {
|
|||
/**
|
||||
* Method to send a poll response.
|
||||
* @param pollEventId the poll currently replied to
|
||||
* @param optionIndex The reply index
|
||||
* @param optionValue The option value (for compatibility)
|
||||
* @param answerId The id of the answer
|
||||
* @return a [Cancelable]
|
||||
*/
|
||||
fun sendOptionsReply(pollEventId: String, optionIndex: Int, optionValue: String): Cancelable
|
||||
fun voteToPoll(pollEventId: String, answerId: String): Cancelable
|
||||
|
||||
/**
|
||||
* End a poll in the room.
|
||||
* @param pollEventId event id of the poll
|
||||
* @return a [Cancelable]
|
||||
*/
|
||||
fun endPoll(pollEventId: String): Cancelable
|
||||
|
||||
/**
|
||||
* Redact (delete) the given event.
|
||||
|
|
|
@ -32,6 +32,7 @@ object RoomSummaryConstants {
|
|||
EventType.CALL_ANSWER,
|
||||
EventType.ENCRYPTED,
|
||||
EventType.STICKER,
|
||||
EventType.REACTION
|
||||
EventType.REACTION,
|
||||
EventType.POLL_START
|
||||
)
|
||||
}
|
||||
|
|
|
@ -27,6 +27,7 @@ import org.matrix.android.sdk.api.session.events.model.toModel
|
|||
import org.matrix.android.sdk.api.session.room.model.EventAnnotationsSummary
|
||||
import org.matrix.android.sdk.api.session.room.model.ReadReceipt
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
|
||||
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
|
||||
import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent
|
||||
|
@ -126,10 +127,10 @@ fun TimelineEvent.getEditedEventId(): String? {
|
|||
* Get last MessageContent, after a possible edition
|
||||
*/
|
||||
fun TimelineEvent.getLastMessageContent(): MessageContent? {
|
||||
return if (root.getClearType() == EventType.STICKER) {
|
||||
root.getClearContent().toModel<MessageStickerContent>()
|
||||
} else {
|
||||
(annotations?.editSummary?.latestContent ?: root.getClearContent()).toModel()
|
||||
return when (root.getClearType()) {
|
||||
EventType.STICKER -> root.getClearContent().toModel<MessageStickerContent>()
|
||||
EventType.POLL_START -> root.getClearContent().toModel<MessagePollContent>()
|
||||
else -> (annotations?.editSummary?.latestContent ?: root.getClearContent()).toModel()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -25,7 +25,6 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageFileContent
|
|||
import org.matrix.android.sdk.api.session.room.model.message.MessageImageContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageLocationContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageNoticeContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageOptionsContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessagePollResponseContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageType
|
||||
|
@ -57,8 +56,7 @@ object MoshiProvider {
|
|||
.registerSubtype(MessageLocationContent::class.java, MessageType.MSGTYPE_LOCATION)
|
||||
.registerSubtype(MessageFileContent::class.java, MessageType.MSGTYPE_FILE)
|
||||
.registerSubtype(MessageVerificationRequestContent::class.java, MessageType.MSGTYPE_VERIFICATION_REQUEST)
|
||||
.registerSubtype(MessageOptionsContent::class.java, MessageType.MSGTYPE_OPTIONS)
|
||||
.registerSubtype(MessagePollResponseContent::class.java, MessageType.MSGTYPE_RESPONSE)
|
||||
.registerSubtype(MessagePollResponseContent::class.java, MessageType.MSGTYPE_POLL_RESPONSE)
|
||||
)
|
||||
.add(SerializeNulls.JSON_ADAPTER_FACTORY)
|
||||
.build()
|
||||
|
|
|
@ -56,6 +56,7 @@ internal class DefaultProcessEventForPushTask @Inject constructor(
|
|||
|
||||
val allEvents = (newJoinEvents + inviteEvents).filter { event ->
|
||||
when (event.type) {
|
||||
EventType.POLL_START,
|
||||
EventType.MESSAGE,
|
||||
EventType.REDACTION,
|
||||
EventType.ENCRYPTED,
|
||||
|
|
|
@ -17,20 +17,27 @@ package org.matrix.android.sdk.internal.session.room
|
|||
|
||||
import io.realm.Realm
|
||||
import org.matrix.android.sdk.api.crypto.VerificationState
|
||||
import org.matrix.android.sdk.api.extensions.orFalse
|
||||
import org.matrix.android.sdk.api.query.QueryStringValue
|
||||
import org.matrix.android.sdk.api.session.events.model.AggregatedAnnotation
|
||||
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.LocalEcho
|
||||
import org.matrix.android.sdk.api.session.events.model.RelationType
|
||||
import org.matrix.android.sdk.api.session.events.model.getRelationContent
|
||||
import org.matrix.android.sdk.api.session.events.model.toContent
|
||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||
import org.matrix.android.sdk.api.session.room.model.PollSummaryContent
|
||||
import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent
|
||||
import org.matrix.android.sdk.api.session.room.model.ReferencesAggregatedContent
|
||||
import org.matrix.android.sdk.api.session.room.model.VoteInfo
|
||||
import org.matrix.android.sdk.api.session.room.model.VoteSummary
|
||||
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.MessagePollResponseContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageRelationContent
|
||||
import org.matrix.android.sdk.api.session.room.model.relation.ReactionContent
|
||||
import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
|
||||
import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent
|
||||
import org.matrix.android.sdk.internal.crypto.verification.toState
|
||||
import org.matrix.android.sdk.internal.database.mapper.ContentMapper
|
||||
|
@ -50,11 +57,13 @@ import org.matrix.android.sdk.internal.database.query.getOrCreate
|
|||
import org.matrix.android.sdk.internal.database.query.where
|
||||
import org.matrix.android.sdk.internal.di.UserId
|
||||
import org.matrix.android.sdk.internal.session.EventInsertLiveProcessor
|
||||
import org.matrix.android.sdk.internal.session.room.state.StateEventDataSource
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class EventRelationsAggregationProcessor @Inject constructor(
|
||||
@UserId private val userId: String
|
||||
@UserId private val userId: String,
|
||||
private val stateEventDataSource: StateEventDataSource
|
||||
) : EventInsertLiveProcessor {
|
||||
|
||||
private val allowedTypes = listOf(
|
||||
|
@ -69,7 +78,9 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
|
|||
// TODO Add ?
|
||||
// EventType.KEY_VERIFICATION_READY,
|
||||
EventType.KEY_VERIFICATION_KEY,
|
||||
EventType.ENCRYPTED
|
||||
EventType.ENCRYPTED,
|
||||
EventType.POLL_RESPONSE,
|
||||
EventType.POLL_END
|
||||
)
|
||||
|
||||
override fun shouldProcess(eventId: String, eventType: String, insertType: EventInsertType): Boolean {
|
||||
|
@ -107,9 +118,6 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
|
|||
Timber.v("###REPLACE in room $roomId for event ${event.eventId}")
|
||||
// A replace!
|
||||
handleReplace(realm, event, content, roomId, isLocalEcho)
|
||||
} else if (content?.relatesTo?.type == RelationType.RESPONSE) {
|
||||
Timber.v("###RESPONSE in room $roomId for event ${event.eventId}")
|
||||
handleResponse(realm, event, content, roomId, isLocalEcho)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -139,9 +147,11 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
|
|||
Timber.v("###REPLACE in room $roomId for event ${event.eventId}")
|
||||
// A replace!
|
||||
handleReplace(realm, event, it, roomId, isLocalEcho, encryptedEventContent.relatesTo.eventId)
|
||||
} else if (encryptedEventContent.relatesTo.type == RelationType.RESPONSE) {
|
||||
Timber.v("###RESPONSE in room $roomId for event ${event.eventId}")
|
||||
handleResponse(realm, event, it, roomId, isLocalEcho, encryptedEventContent.relatesTo.eventId)
|
||||
} else if (event.getClearType() == EventType.POLL_RESPONSE) {
|
||||
event.getClearContent().toModel<MessagePollResponseContent>(catchError = true)?.let { pollResponseContent ->
|
||||
Timber.v("###RESPONSE in room $roomId for event ${event.eventId}")
|
||||
handleResponse(realm, event, pollResponseContent, roomId, isLocalEcho, encryptedEventContent.relatesTo.eventId)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (encryptedEventContent?.relatesTo?.type == RelationType.REFERENCE) {
|
||||
|
@ -158,6 +168,16 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
|
|||
handleVerification(realm, event, roomId, isLocalEcho, it)
|
||||
}
|
||||
}
|
||||
EventType.POLL_RESPONSE -> {
|
||||
event.getClearContent().toModel<MessagePollResponseContent>(catchError = true)?.let {
|
||||
handleResponse(realm, event, it, roomId, isLocalEcho, event.getRelationContent()?.eventId)
|
||||
}
|
||||
}
|
||||
EventType.POLL_END -> {
|
||||
event.content.toModel<MessageEndPollContent>(catchError = true)?.let {
|
||||
handleEndPoll(realm, event, it, roomId, isLocalEcho)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (encryptedEventContent?.relatesTo?.type == RelationType.ANNOTATION) {
|
||||
// Reaction
|
||||
|
@ -188,6 +208,16 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
|
|||
}
|
||||
}
|
||||
}
|
||||
EventType.POLL_RESPONSE -> {
|
||||
event.content.toModel<MessagePollResponseContent>(catchError = true)?.let {
|
||||
handleResponse(realm, event, it, roomId, isLocalEcho)
|
||||
}
|
||||
}
|
||||
EventType.POLL_END -> {
|
||||
event.content.toModel<MessageEndPollContent>(catchError = true)?.let {
|
||||
handleEndPoll(realm, event, it, roomId, isLocalEcho)
|
||||
}
|
||||
}
|
||||
else -> Timber.v("UnHandled event ${event.eventId}")
|
||||
}
|
||||
} catch (t: Throwable) {
|
||||
|
@ -276,7 +306,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
|
|||
|
||||
private fun handleResponse(realm: Realm,
|
||||
event: Event,
|
||||
content: MessageContent,
|
||||
content: MessagePollResponseContent,
|
||||
roomId: String,
|
||||
isLocalEcho: Boolean,
|
||||
relatedEventId: String? = null) {
|
||||
|
@ -321,11 +351,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
|
|||
return
|
||||
}
|
||||
|
||||
val responseContent = event.content.toModel<MessagePollResponseContent>() ?: return Unit.also {
|
||||
Timber.d("## POLL Receiving malformed response eventId:$eventId content: ${event.content}")
|
||||
}
|
||||
|
||||
val optionIndex = responseContent.relatesTo?.option ?: return Unit.also {
|
||||
val option = content.response?.answers?.first() ?: return Unit.also {
|
||||
Timber.d("## POLL Ignoring malformed response no option eventId:$eventId content: ${event.content}")
|
||||
}
|
||||
|
||||
|
@ -336,22 +362,36 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
|
|||
val existingVote = votes[existingVoteIndex]
|
||||
if (existingVote.voteTimestamp < eventTimestamp) {
|
||||
// Take the new one
|
||||
votes[existingVoteIndex] = VoteInfo(senderId, optionIndex, eventTimestamp)
|
||||
votes[existingVoteIndex] = VoteInfo(senderId, option, eventTimestamp)
|
||||
if (userId == senderId) {
|
||||
sumModel.myVote = optionIndex
|
||||
sumModel.myVote = option
|
||||
}
|
||||
Timber.v("## POLL adding vote $optionIndex for user $senderId in poll :$targetEventId ")
|
||||
Timber.v("## POLL adding vote $option for user $senderId in poll :$targetEventId ")
|
||||
} else {
|
||||
Timber.v("## POLL Ignoring vote (older than known one) eventId:$eventId ")
|
||||
}
|
||||
} else {
|
||||
votes.add(VoteInfo(senderId, optionIndex, eventTimestamp))
|
||||
votes.add(VoteInfo(senderId, option, eventTimestamp))
|
||||
if (userId == senderId) {
|
||||
sumModel.myVote = optionIndex
|
||||
sumModel.myVote = option
|
||||
}
|
||||
Timber.v("## POLL adding vote $optionIndex for user $senderId in poll :$targetEventId ")
|
||||
Timber.v("## POLL adding vote $option for user $senderId in poll :$targetEventId ")
|
||||
}
|
||||
sumModel.votes = votes
|
||||
|
||||
// Precompute the percentage of votes for all options
|
||||
val totalVotes = votes.size
|
||||
sumModel.totalVotes = totalVotes
|
||||
sumModel.votesSummary = votes
|
||||
.groupBy({ it.option }, { it.userId })
|
||||
.mapValues {
|
||||
VoteSummary(
|
||||
total = it.value.size,
|
||||
percentage = if (totalVotes == 0 && it.value.isEmpty()) 0.0 else it.value.size.toDouble() / totalVotes
|
||||
)
|
||||
}
|
||||
sumModel.winnerVoteCount = sumModel.votesSummary?.maxOf { it.value.total } ?: 0
|
||||
|
||||
if (isLocalEcho) {
|
||||
existingPollSummary.sourceLocalEchoEvents.add(eventId)
|
||||
} else {
|
||||
|
@ -361,6 +401,51 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
|
|||
existingPollSummary.aggregatedContent = ContentMapper.map(sumModel.toContent())
|
||||
}
|
||||
|
||||
private fun handleEndPoll(realm: Realm,
|
||||
event: Event,
|
||||
content: MessageEndPollContent,
|
||||
roomId: String,
|
||||
isLocalEcho: Boolean) {
|
||||
val pollEventId = content.relatesTo?.eventId ?: return
|
||||
|
||||
var existing = EventAnnotationsSummaryEntity.where(realm, roomId, pollEventId).findFirst()
|
||||
if (existing == null) {
|
||||
Timber.v("## POLL creating new relation summary for $pollEventId")
|
||||
existing = EventAnnotationsSummaryEntity.create(realm, roomId, pollEventId)
|
||||
}
|
||||
|
||||
// we have it
|
||||
val existingPollSummary = existing.pollResponseSummary
|
||||
?: realm.createObject(PollResponseAggregatedSummaryEntity::class.java).also {
|
||||
existing.pollResponseSummary = it
|
||||
}
|
||||
|
||||
if (existingPollSummary.closedTime != null) {
|
||||
Timber.v("## Received poll.end event for already ended poll $pollEventId")
|
||||
return
|
||||
}
|
||||
|
||||
val powerLevelsHelper = stateEventDataSource.getStateEvent(roomId, EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.NoCondition)
|
||||
?.content?.toModel<PowerLevelsContent>()
|
||||
?.let { PowerLevelsHelper(it) }
|
||||
if (!powerLevelsHelper?.isUserAbleToRedact(event.senderId ?: "").orFalse()) {
|
||||
Timber.v("## Received poll.end event $pollEventId but user ${event.senderId} doesn't have enough power level in room $roomId")
|
||||
return
|
||||
}
|
||||
|
||||
val txId = event.unsignedData?.transactionId
|
||||
// is it a remote echo?
|
||||
if (!isLocalEcho && existingPollSummary.sourceLocalEchoEvents.contains(txId)) {
|
||||
// ok it has already been managed
|
||||
Timber.v("## POLL Receiving remote echo of response eventId:$pollEventId")
|
||||
existingPollSummary.sourceLocalEchoEvents.remove(txId)
|
||||
existingPollSummary.sourceEvents.add(event.eventId)
|
||||
return
|
||||
}
|
||||
|
||||
existingPollSummary.closedTime = event.originServerTs
|
||||
}
|
||||
|
||||
private fun handleInitialAggregatedRelations(realm: Realm,
|
||||
event: Event,
|
||||
roomId: String,
|
||||
|
|
|
@ -70,7 +70,8 @@ internal class RedactionEventProcessor @Inject constructor() : EventInsertLivePr
|
|||
} else {
|
||||
when (typeToPrune) {
|
||||
EventType.ENCRYPTED,
|
||||
EventType.MESSAGE -> {
|
||||
EventType.MESSAGE,
|
||||
EventType.POLL_START -> {
|
||||
Timber.d("REDACTION for message ${eventToPrune.eventId}")
|
||||
val unsignedData = EventMapper.map(eventToPrune).unsignedData
|
||||
?: UnsignedData(null, null)
|
||||
|
|
|
@ -103,8 +103,14 @@ internal class DefaultSendService @AssistedInject constructor(
|
|||
.let { sendEvent(it) }
|
||||
}
|
||||
|
||||
override fun sendOptionsReply(pollEventId: String, optionIndex: Int, optionValue: String): Cancelable {
|
||||
return localEchoEventFactory.createOptionsReplyEvent(roomId, pollEventId, optionIndex, optionValue)
|
||||
override fun voteToPoll(pollEventId: String, answerId: String): Cancelable {
|
||||
return localEchoEventFactory.createPollReplyEvent(roomId, pollEventId, answerId)
|
||||
.also { createLocalEcho(it) }
|
||||
.let { sendEvent(it) }
|
||||
}
|
||||
|
||||
override fun endPoll(pollEventId: String): Cancelable {
|
||||
return localEchoEventFactory.createEndPollEvent(roomId, pollEventId)
|
||||
.also { createLocalEcho(it) }
|
||||
.let { sendEvent(it) }
|
||||
}
|
||||
|
|
|
@ -35,6 +35,7 @@ import org.matrix.android.sdk.api.session.room.model.message.ImageInfo
|
|||
import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent
|
||||
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.MessageFileContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageFormat
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageImageContent
|
||||
|
@ -46,6 +47,7 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent
|
|||
import org.matrix.android.sdk.api.session.room.model.message.PollAnswer
|
||||
import org.matrix.android.sdk.api.session.room.model.message.PollCreationInfo
|
||||
import org.matrix.android.sdk.api.session.room.model.message.PollQuestion
|
||||
import org.matrix.android.sdk.api.session.room.model.message.PollResponse
|
||||
import org.matrix.android.sdk.api.session.room.model.message.ThumbnailInfo
|
||||
import org.matrix.android.sdk.api.session.room.model.message.VideoInfo
|
||||
import org.matrix.android.sdk.api.session.room.model.relation.ReactionContent
|
||||
|
@ -122,19 +124,28 @@ internal class LocalEchoEventFactory @Inject constructor(
|
|||
))
|
||||
}
|
||||
|
||||
fun createOptionsReplyEvent(roomId: String,
|
||||
pollEventId: String,
|
||||
optionIndex: Int,
|
||||
optionLabel: String): Event {
|
||||
return createMessageEvent(roomId,
|
||||
MessagePollResponseContent(
|
||||
body = optionLabel,
|
||||
relatesTo = RelationDefaultContent(
|
||||
type = RelationType.RESPONSE,
|
||||
option = optionIndex,
|
||||
eventId = pollEventId)
|
||||
fun createPollReplyEvent(roomId: String,
|
||||
pollEventId: String,
|
||||
answerId: String): Event {
|
||||
val content = MessagePollResponseContent(
|
||||
body = answerId,
|
||||
relatesTo = RelationDefaultContent(
|
||||
type = RelationType.REFERENCE,
|
||||
eventId = pollEventId),
|
||||
response = PollResponse(
|
||||
answers = listOf(answerId)
|
||||
)
|
||||
|
||||
))
|
||||
)
|
||||
val localId = LocalEcho.createLocalEchoId()
|
||||
return Event(
|
||||
roomId = roomId,
|
||||
originServerTs = dummyOriginServerTs(),
|
||||
senderId = userId,
|
||||
eventId = localId,
|
||||
type = EventType.POLL_RESPONSE,
|
||||
content = content.toContent(),
|
||||
unsignedData = UnsignedData(age = null, transactionId = localId))
|
||||
}
|
||||
|
||||
fun createPollEvent(roomId: String,
|
||||
|
@ -147,7 +158,7 @@ internal class LocalEchoEventFactory @Inject constructor(
|
|||
),
|
||||
answers = options.mapIndexed { index, option ->
|
||||
PollAnswer(
|
||||
id = index.toString(),
|
||||
id = "$index-$option",
|
||||
answer = option
|
||||
)
|
||||
}
|
||||
|
@ -164,6 +175,25 @@ internal class LocalEchoEventFactory @Inject constructor(
|
|||
unsignedData = UnsignedData(age = null, transactionId = localId))
|
||||
}
|
||||
|
||||
fun createEndPollEvent(roomId: String,
|
||||
eventId: String): Event {
|
||||
val content = MessageEndPollContent(
|
||||
relatesTo = RelationDefaultContent(
|
||||
type = RelationType.REFERENCE,
|
||||
eventId = eventId
|
||||
)
|
||||
)
|
||||
val localId = LocalEcho.createLocalEchoId()
|
||||
return Event(
|
||||
roomId = roomId,
|
||||
originServerTs = dummyOriginServerTs(),
|
||||
senderId = userId,
|
||||
eventId = localId,
|
||||
type = EventType.POLL_END,
|
||||
content = content.toContent(),
|
||||
unsignedData = UnsignedData(age = null, transactionId = localId))
|
||||
}
|
||||
|
||||
fun createReplaceTextOfReply(roomId: String,
|
||||
eventReplaced: TimelineEvent,
|
||||
originalEvent: TimelineEvent,
|
||||
|
@ -413,7 +443,7 @@ internal class LocalEchoEventFactory @Inject constructor(
|
|||
when (content?.msgType) {
|
||||
MessageType.MSGTYPE_EMOTE,
|
||||
MessageType.MSGTYPE_TEXT,
|
||||
MessageType.MSGTYPE_NOTICE -> {
|
||||
MessageType.MSGTYPE_NOTICE -> {
|
||||
var formattedText: String? = null
|
||||
if (content is MessageContentWithFormattedBody) {
|
||||
formattedText = content.matrixFormattedBody
|
||||
|
@ -424,11 +454,12 @@ internal class LocalEchoEventFactory @Inject constructor(
|
|||
TextContent(content.body, formattedText)
|
||||
}
|
||||
}
|
||||
MessageType.MSGTYPE_FILE -> return TextContent("sent a file.")
|
||||
MessageType.MSGTYPE_AUDIO -> return TextContent("sent an audio file.")
|
||||
MessageType.MSGTYPE_IMAGE -> return TextContent("sent an image.")
|
||||
MessageType.MSGTYPE_VIDEO -> return TextContent("sent a video.")
|
||||
else -> return TextContent(content?.body ?: "")
|
||||
MessageType.MSGTYPE_FILE -> return TextContent("sent a file.")
|
||||
MessageType.MSGTYPE_AUDIO -> return TextContent("sent an audio file.")
|
||||
MessageType.MSGTYPE_IMAGE -> return TextContent("sent an image.")
|
||||
MessageType.MSGTYPE_VIDEO -> return TextContent("sent a video.")
|
||||
MessageType.MSGTYPE_POLL_START -> return TextContent((content as? MessagePollContent)?.pollCreationInfo?.question?.question ?: "")
|
||||
else -> return TextContent(content?.body ?: "")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
22
vector/sampledata/poll.json
Normal file
22
vector/sampledata/poll.json
Normal file
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"question": "What type of food should we have at the party?",
|
||||
"data": [
|
||||
{
|
||||
"answer": "Italian \uD83C\uDDEE\uD83C\uDDF9",
|
||||
"votes": "9 votes"
|
||||
},
|
||||
{
|
||||
"answer": "Chinese \uD83C\uDDE8\uD83C\uDDF3",
|
||||
"votes": "1 vote"
|
||||
},
|
||||
{
|
||||
"answer": "Brazilian \uD83C\uDDE7\uD83C\uDDF7",
|
||||
"votes": "0 votes"
|
||||
},
|
||||
{
|
||||
"answer": "French \uD83C\uDDEB\uD83C\uDDF7",
|
||||
"votes": "15 votes"
|
||||
}
|
||||
],
|
||||
"totalVotes": "Based on 20 votes"
|
||||
}
|
|
@ -21,8 +21,8 @@ import org.matrix.android.sdk.api.session.room.send.SendState
|
|||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||
|
||||
fun TimelineEvent.canReact(): Boolean {
|
||||
// Only event of type EventType.MESSAGE or EventType.STICKER are supported for the moment
|
||||
return root.getClearType() in listOf(EventType.MESSAGE, EventType.STICKER) &&
|
||||
// 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) &&
|
||||
root.sendState == SendState.SYNCED &&
|
||||
!root.isRedacted()
|
||||
}
|
||||
|
|
|
@ -48,4 +48,8 @@ class UserPreferencesProvider @Inject constructor(private val vectorPreferences:
|
|||
fun shouldShowAvatarDisplayNameChanges(): Boolean {
|
||||
return vectorPreferences.showAvatarDisplayNameChangeMessages()
|
||||
}
|
||||
|
||||
fun shouldShowPolls(): Boolean {
|
||||
return vectorPreferences.labsEnablePolls()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,6 +34,7 @@ import android.widget.ImageButton
|
|||
import android.widget.LinearLayout
|
||||
import android.widget.PopupWindow
|
||||
import androidx.core.view.doOnNextLayout
|
||||
import androidx.core.view.isVisible
|
||||
import com.amulyakhare.textdrawable.TextDrawable
|
||||
import com.amulyakhare.textdrawable.util.ColorGenerator
|
||||
import im.vector.app.R
|
||||
|
@ -121,6 +122,20 @@ class AttachmentTypeSelectorView(context: Context,
|
|||
}
|
||||
}
|
||||
|
||||
fun setAttachmentVisibility(type: Type, isVisible: Boolean) {
|
||||
when (type) {
|
||||
Type.CAMERA -> views.attachmentCameraButtonContainer
|
||||
Type.GALLERY -> views.attachmentGalleryButtonContainer
|
||||
Type.FILE -> views.attachmentFileButtonContainer
|
||||
Type.STICKER -> views.attachmentStickersButtonContainer
|
||||
Type.AUDIO -> views.attachmentAudioButtonContainer
|
||||
Type.CONTACT -> views.attachmentContactButtonContainer
|
||||
Type.POLL -> views.attachmentPollButtonContainer
|
||||
}.let {
|
||||
it.isVisible = isVisible
|
||||
}
|
||||
}
|
||||
|
||||
private fun animateButtonIn(button: View, delay: Int) {
|
||||
val animation = AnimationSet(true)
|
||||
val scale = ScaleAnimation(0.0f, 1.0f, 0.0f, 1.0f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.0f)
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
package im.vector.app.features.form
|
||||
|
||||
import android.text.Editable
|
||||
import android.text.InputFilter
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.widget.ImageButton
|
||||
import com.airbnb.epoxy.EpoxyAttribute
|
||||
|
@ -51,6 +52,9 @@ abstract class FormEditTextWithDeleteItem : VectorEpoxyModel<FormEditTextWithDel
|
|||
@EpoxyAttribute
|
||||
var imeOptions: Int? = null
|
||||
|
||||
@EpoxyAttribute
|
||||
var maxLength: Int? = null
|
||||
|
||||
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash)
|
||||
var onTextChange: TextListener? = null
|
||||
|
||||
|
@ -68,6 +72,12 @@ abstract class FormEditTextWithDeleteItem : VectorEpoxyModel<FormEditTextWithDel
|
|||
holder.textInputLayout.isEnabled = enabled
|
||||
holder.textInputLayout.hint = hint
|
||||
|
||||
if (maxLength != null) {
|
||||
holder.textInputEditText.filters = arrayOf(InputFilter.LengthFilter(maxLength!!))
|
||||
holder.textInputLayout.counterMaxLength = maxLength!!
|
||||
} else {
|
||||
holder.textInputEditText.filters = arrayOf()
|
||||
}
|
||||
holder.textInputEditText.setTextIfDifferent(value)
|
||||
|
||||
holder.textInputEditText.isEnabled = enabled
|
||||
|
|
|
@ -52,7 +52,7 @@ sealed class RoomDetailAction : VectorViewModelAction {
|
|||
data class RemoveFailedEcho(val eventId: String) : RoomDetailAction()
|
||||
data class CancelSend(val eventId: String, val force: Boolean) : RoomDetailAction()
|
||||
|
||||
data class ReplyToOptions(val eventId: String, val optionIndex: Int, val optionValue: String) : RoomDetailAction()
|
||||
data class VoteToPoll(val eventId: String, val optionKey: String) : RoomDetailAction()
|
||||
|
||||
data class ReportContent(
|
||||
val eventId: String,
|
||||
|
@ -107,4 +107,7 @@ sealed class RoomDetailAction : VectorViewModelAction {
|
|||
object RemoveAllFailedMessages : RoomDetailAction()
|
||||
|
||||
data class RoomUpgradeSuccess(val replacementRoomId: String) : RoomDetailAction()
|
||||
|
||||
// Poll
|
||||
data class EndPoll(val eventId: String) : RoomDetailAction()
|
||||
}
|
||||
|
|
|
@ -203,6 +203,7 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent
|
|||
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageFormat
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageImageInfoContent
|
||||
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
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationRequestContent
|
||||
|
@ -1077,6 +1078,8 @@ class RoomDetailFragment @Inject constructor(
|
|||
val nonFormattedBody = if (messageContent is MessageAudioContent && messageContent.voiceMessageIndicator != null) {
|
||||
val formattedDuration = DateUtils.formatElapsedTime(((messageContent.audioInfo?.duration ?: 0) / 1000).toLong())
|
||||
getString(R.string.voice_message_reply_content, formattedDuration)
|
||||
} else if (messageContent is MessagePollContent) {
|
||||
messageContent.pollCreationInfo?.question?.question
|
||||
} else {
|
||||
messageContent?.body ?: ""
|
||||
}
|
||||
|
@ -1362,6 +1365,7 @@ class RoomDetailFragment @Inject constructor(
|
|||
override fun onAddAttachment() {
|
||||
if (!::attachmentTypeSelector.isInitialized) {
|
||||
attachmentTypeSelector = AttachmentTypeSelectorView(vectorBaseActivity, vectorBaseActivity.layoutInflater, this@RoomDetailFragment)
|
||||
attachmentTypeSelector.setAttachmentVisibility(AttachmentTypeSelectorView.Type.POLL, vectorPreferences.labsEnablePolls())
|
||||
}
|
||||
attachmentTypeSelector.show(views.composerLayout.views.attachmentButton, keyboardStateUtils.isKeyboardShowing)
|
||||
}
|
||||
|
@ -1576,10 +1580,10 @@ class RoomDetailFragment @Inject constructor(
|
|||
.show(
|
||||
activity = requireActivity(),
|
||||
askForReason = action.askForReason,
|
||||
confirmationRes = R.string.delete_event_dialog_content,
|
||||
confirmationRes = action.dialogDescriptionRes,
|
||||
positiveRes = R.string.remove,
|
||||
reasonHintRes = R.string.delete_event_dialog_reason_hint,
|
||||
titleRes = R.string.delete_event_dialog_title
|
||||
titleRes = action.dialogTitleRes
|
||||
) { reason ->
|
||||
roomDetailViewModel.handle(RoomDetailAction.RedactAction(action.eventId, reason))
|
||||
}
|
||||
|
@ -2059,9 +2063,23 @@ class RoomDetailFragment @Inject constructor(
|
|||
startActivity(KeysBackupRestoreActivity.intent(it))
|
||||
}
|
||||
}
|
||||
is EventSharedAction.EndPoll -> {
|
||||
askConfirmationToEndPoll(action.eventId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun askConfirmationToEndPoll(eventId: String) {
|
||||
MaterialAlertDialogBuilder(requireContext(), R.style.ThemeOverlay_Vector_MaterialAlertDialog)
|
||||
.setTitle(R.string.end_poll_confirmation_title)
|
||||
.setMessage(R.string.end_poll_confirmation_description)
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.setPositiveButton(R.string.end_poll_confirmation_approve_button) { _, _ ->
|
||||
roomDetailViewModel.handle(RoomDetailAction.EndPoll(eventId))
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun askConfirmationToIgnoreUser(senderId: String) {
|
||||
MaterialAlertDialogBuilder(requireContext(), R.style.ThemeOverlay_Vector_MaterialAlertDialog_Destructive)
|
||||
.setTitle(R.string.room_participants_action_ignore_title)
|
||||
|
|
|
@ -289,7 +289,7 @@ class RoomDetailViewModel @AssistedInject constructor(
|
|||
is RoomDetailAction.IgnoreUser -> handleIgnoreUser(action)
|
||||
is RoomDetailAction.EnterTrackingUnreadMessagesState -> startTrackingUnreadMessages()
|
||||
is RoomDetailAction.ExitTrackingUnreadMessagesState -> stopTrackingUnreadMessages()
|
||||
is RoomDetailAction.ReplyToOptions -> handleReplyToOptions(action)
|
||||
is RoomDetailAction.VoteToPoll -> handleVoteToPoll(action)
|
||||
is RoomDetailAction.AcceptVerificationRequest -> handleAcceptVerification(action)
|
||||
is RoomDetailAction.DeclineVerificationRequest -> handleDeclineVerification(action)
|
||||
is RoomDetailAction.RequestVerification -> handleRequestVerification(action)
|
||||
|
@ -329,6 +329,7 @@ class RoomDetailViewModel @AssistedInject constructor(
|
|||
}
|
||||
_viewEvents.post(RoomDetailViewEvents.OpenRoom(action.replacementRoomId, closeCurrentRoom = true))
|
||||
}
|
||||
is RoomDetailAction.EndPoll -> handleEndPoll(action.eventId)
|
||||
}.exhaustive
|
||||
}
|
||||
|
||||
|
@ -907,10 +908,20 @@ class RoomDetailViewModel @AssistedInject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun handleReplyToOptions(action: RoomDetailAction.ReplyToOptions) {
|
||||
// Do not allow to reply to unsent local echo
|
||||
private fun handleVoteToPoll(action: RoomDetailAction.VoteToPoll) {
|
||||
// Do not allow to vote unsent local echo of the poll event
|
||||
if (LocalEcho.isLocalEchoId(action.eventId)) return
|
||||
room.sendOptionsReply(action.eventId, action.optionIndex, action.optionValue)
|
||||
// Do not allow to vote the same option twice
|
||||
room.getTimeLineEvent(action.eventId)?.let { pollTimelineEvent ->
|
||||
val currentVote = pollTimelineEvent.annotations?.pollResponseSummary?.aggregatedContent?.myVote
|
||||
if (currentVote != action.optionKey) {
|
||||
room.voteToPoll(action.eventId, action.optionKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleEndPoll(eventId: String) {
|
||||
room.endPoll(eventId)
|
||||
}
|
||||
|
||||
private fun observeSyncState() {
|
||||
|
|
|
@ -60,7 +60,7 @@ sealed class EventSharedAction(@StringRes val titleRes: Int,
|
|||
data class Remove(val eventId: String) :
|
||||
EventSharedAction(R.string.remove, R.drawable.ic_trash, true)
|
||||
|
||||
data class Redact(val eventId: String, val askForReason: Boolean) :
|
||||
data class Redact(val eventId: String, val askForReason: Boolean, val dialogTitleRes: Int, val dialogDescriptionRes: Int) :
|
||||
EventSharedAction(R.string.message_action_item_redact, R.drawable.ic_delete, true)
|
||||
|
||||
data class Cancel(val eventId: String, val force: Boolean) :
|
||||
|
@ -112,4 +112,7 @@ sealed class EventSharedAction(@StringRes val titleRes: Int,
|
|||
|
||||
object UseKeyBackup :
|
||||
EventSharedAction(R.string.e2e_use_keybackup, R.drawable.shield)
|
||||
|
||||
data class EndPoll(val eventId: String) :
|
||||
EventSharedAction(R.string.poll_end_action, R.drawable.ic_check_on)
|
||||
}
|
||||
|
|
|
@ -49,6 +49,7 @@ import org.matrix.android.sdk.api.session.events.model.isTextMessage
|
|||
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
|
||||
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
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageType
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationRequestContent
|
||||
|
@ -206,6 +207,9 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
|
|||
EventType.CALL_ANSWER -> {
|
||||
noticeEventFormatter.format(timelineEvent, room?.roomSummary()?.isDirect.orFalse())
|
||||
}
|
||||
EventType.POLL_START -> {
|
||||
timelineEvent.root.getClearContent().toModel<MessagePollContent>(catchError = true)?.pollCreationInfo?.question?.question ?: ""
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
@ -320,12 +324,30 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
|
|||
add(EventSharedAction.Reply(eventId))
|
||||
}
|
||||
|
||||
if (canEndPoll(timelineEvent, actionPermissions)) {
|
||||
add(EventSharedAction.EndPoll(timelineEvent.eventId))
|
||||
}
|
||||
|
||||
if (canEdit(timelineEvent, session.myUserId, actionPermissions)) {
|
||||
add(EventSharedAction.Edit(eventId))
|
||||
}
|
||||
|
||||
if (canRedact(timelineEvent, actionPermissions)) {
|
||||
add(EventSharedAction.Redact(eventId, askForReason = informationData.senderId != session.myUserId))
|
||||
if (timelineEvent.root.getClearType() == EventType.POLL_START) {
|
||||
add(EventSharedAction.Redact(
|
||||
eventId,
|
||||
askForReason = informationData.senderId != session.myUserId,
|
||||
dialogTitleRes = R.string.delete_poll_dialog_title,
|
||||
dialogDescriptionRes = R.string.delete_poll_dialog_content
|
||||
))
|
||||
} else {
|
||||
add(EventSharedAction.Redact(
|
||||
eventId,
|
||||
askForReason = informationData.senderId != session.myUserId,
|
||||
dialogTitleRes = R.string.delete_event_dialog_title,
|
||||
dialogDescriptionRes = R.string.delete_event_dialog_content
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
if (canCopy(msgType)) {
|
||||
|
@ -391,8 +413,8 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
|
|||
}
|
||||
|
||||
private fun canReply(event: TimelineEvent, messageContent: MessageContent?, actionPermissions: ActionPermissions): Boolean {
|
||||
// Only event of type EventType.MESSAGE are supported for the moment
|
||||
if (event.root.getClearType() != EventType.MESSAGE) return false
|
||||
// Only EventType.MESSAGE and EventType.POLL_START event types are supported for the moment
|
||||
if (event.root.getClearType() !in listOf(EventType.MESSAGE, EventType.POLL_START)) return false
|
||||
if (!actionPermissions.canSendMessage) return false
|
||||
return when (messageContent?.msgType) {
|
||||
MessageType.MSGTYPE_TEXT,
|
||||
|
@ -401,8 +423,9 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
|
|||
MessageType.MSGTYPE_IMAGE,
|
||||
MessageType.MSGTYPE_VIDEO,
|
||||
MessageType.MSGTYPE_AUDIO,
|
||||
MessageType.MSGTYPE_FILE -> true
|
||||
else -> false
|
||||
MessageType.MSGTYPE_FILE,
|
||||
MessageType.MSGTYPE_POLL_START -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -422,8 +445,8 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
|
|||
}
|
||||
|
||||
private fun canRedact(event: TimelineEvent, actionPermissions: ActionPermissions): Boolean {
|
||||
// Only event of type EventType.MESSAGE or EventType.STICKER are supported for the moment
|
||||
if (event.root.getClearType() !in listOf(EventType.MESSAGE, EventType.STICKER)) return false
|
||||
// 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)) return false
|
||||
// Message sent by the current user can always be redacted
|
||||
if (event.root.senderId == session.myUserId) return true
|
||||
// Check permission for messages sent by other users
|
||||
|
@ -437,8 +460,8 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
|
|||
}
|
||||
|
||||
private fun canViewReactions(event: TimelineEvent): Boolean {
|
||||
// Only event of type EventType.MESSAGE and EventType.STICKER are supported for the moment
|
||||
if (event.root.getClearType() !in listOf(EventType.MESSAGE, EventType.STICKER)) return false
|
||||
// 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)) return false
|
||||
return event.annotations?.reactionsSummary?.isNotEmpty() ?: false
|
||||
}
|
||||
|
||||
|
@ -487,4 +510,10 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
|
|||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
private fun canEndPoll(event: TimelineEvent, actionPermissions: ActionPermissions): Boolean {
|
||||
return event.root.getClearType() == EventType.POLL_START &&
|
||||
canRedact(event, actionPermissions) &&
|
||||
event.annotations?.pollResponseSummary?.closedTime == null
|
||||
}
|
||||
}
|
||||
|
|
|
@ -48,12 +48,13 @@ import im.vector.app.features.home.room.detail.timeline.item.MessageFileItem_
|
|||
import im.vector.app.features.home.room.detail.timeline.item.MessageImageVideoItem
|
||||
import im.vector.app.features.home.room.detail.timeline.item.MessageImageVideoItem_
|
||||
import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData
|
||||
import im.vector.app.features.home.room.detail.timeline.item.MessageOptionsItem_
|
||||
import im.vector.app.features.home.room.detail.timeline.item.MessagePollItem_
|
||||
import im.vector.app.features.home.room.detail.timeline.item.MessageTextItem
|
||||
import im.vector.app.features.home.room.detail.timeline.item.MessageTextItem_
|
||||
import im.vector.app.features.home.room.detail.timeline.item.MessageVoiceItem
|
||||
import im.vector.app.features.home.room.detail.timeline.item.MessageVoiceItem_
|
||||
import im.vector.app.features.home.room.detail.timeline.item.PollItem
|
||||
import im.vector.app.features.home.room.detail.timeline.item.PollItem_
|
||||
import im.vector.app.features.home.room.detail.timeline.item.PollOptionViewState
|
||||
import im.vector.app.features.home.room.detail.timeline.item.RedactedMessageItem
|
||||
import im.vector.app.features.home.room.detail.timeline.item.RedactedMessageItem_
|
||||
import im.vector.app.features.home.room.detail.timeline.item.VerificationRequestItem
|
||||
|
@ -70,6 +71,7 @@ import im.vector.app.features.media.VideoContentRenderer
|
|||
import me.gujun.android.span.span
|
||||
import org.commonmark.node.Document
|
||||
import org.matrix.android.sdk.api.MatrixUrls.isMxcUrl
|
||||
import org.matrix.android.sdk.api.extensions.orFalse
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.session.events.model.RelationType
|
||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||
|
@ -80,14 +82,11 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageEmoteContent
|
|||
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.MessageNoticeContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageOptionsContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessagePollResponseContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageType
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationRequestContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.OPTION_TYPE_BUTTONS
|
||||
import org.matrix.android.sdk.api.session.room.model.message.OPTION_TYPE_POLL
|
||||
import org.matrix.android.sdk.api.session.room.model.message.getFileName
|
||||
import org.matrix.android.sdk.api.session.room.model.message.getFileUrl
|
||||
import org.matrix.android.sdk.api.session.room.model.message.getThumbnailUrl
|
||||
|
@ -168,41 +167,67 @@ class MessageItemFactory @Inject constructor(
|
|||
}
|
||||
}
|
||||
is MessageVerificationRequestContent -> buildVerificationRequestMessageItem(messageContent, informationData, highlight, callback, attributes)
|
||||
is MessageOptionsContent -> buildOptionsMessageItem(messageContent, informationData, highlight, callback, attributes)
|
||||
is MessagePollResponseContent -> noticeItemFactory.create(params)
|
||||
is MessagePollContent -> buildPollContent(messageContent, informationData, highlight, callback, attributes)
|
||||
else -> buildNotHandledMessageItem(messageContent, informationData, highlight, callback, attributes)
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildOptionsMessageItem(messageContent: MessageOptionsContent,
|
||||
informationData: MessageInformationData,
|
||||
highlight: Boolean,
|
||||
callback: TimelineEventController.Callback?,
|
||||
attributes: AbsMessageItem.Attributes): VectorEpoxyModel<*>? {
|
||||
return when (messageContent.optionType) {
|
||||
OPTION_TYPE_POLL -> {
|
||||
MessagePollItem_()
|
||||
.attributes(attributes)
|
||||
.callback(callback)
|
||||
.informationData(informationData)
|
||||
.leftGuideline(avatarSizeProvider.leftGuideline)
|
||||
.optionsContent(messageContent)
|
||||
.highlighted(highlight)
|
||||
}
|
||||
OPTION_TYPE_BUTTONS -> {
|
||||
MessageOptionsItem_()
|
||||
.attributes(attributes)
|
||||
.callback(callback)
|
||||
.informationData(informationData)
|
||||
.leftGuideline(avatarSizeProvider.leftGuideline)
|
||||
.optionsContent(messageContent)
|
||||
.highlighted(highlight)
|
||||
}
|
||||
else -> {
|
||||
// Not supported optionType
|
||||
buildNotHandledMessageItem(messageContent, informationData, highlight, callback, attributes)
|
||||
private fun buildPollContent(pollContent: MessagePollContent,
|
||||
informationData: MessageInformationData,
|
||||
highlight: Boolean,
|
||||
callback: TimelineEventController.Callback?,
|
||||
attributes: AbsMessageItem.Attributes): PollItem? {
|
||||
val optionViewStates = mutableListOf<PollOptionViewState>()
|
||||
|
||||
val pollResponseSummary = informationData.pollResponseAggregatedSummary
|
||||
val isEnded = pollResponseSummary?.isClosed.orFalse()
|
||||
val didUserVoted = pollResponseSummary?.myVote?.isNotEmpty().orFalse()
|
||||
val winnerVoteCount = pollResponseSummary?.winnerVoteCount
|
||||
val isPollSent = informationData.sendState.isSent()
|
||||
val totalVotesText = (pollResponseSummary?.totalVotes ?: 0).let {
|
||||
when {
|
||||
isEnded -> stringProvider.getQuantityString(R.plurals.poll_total_vote_count_after_ended, it, it)
|
||||
didUserVoted -> stringProvider.getQuantityString(R.plurals.poll_total_vote_count_before_ended_and_voted, it, it)
|
||||
else -> stringProvider.getQuantityString(R.plurals.poll_total_vote_count_before_ended_and_not_voted, it, it)
|
||||
}
|
||||
}
|
||||
|
||||
pollContent.pollCreationInfo?.answers?.forEach { option ->
|
||||
val voteSummary = pollResponseSummary?.votes?.get(option.id)
|
||||
val isMyVote = pollResponseSummary?.myVote == option.id
|
||||
val voteCount = voteSummary?.total ?: 0
|
||||
val votePercentage = voteSummary?.percentage ?: 0.0
|
||||
val optionId = option.id ?: ""
|
||||
val optionAnswer = option.answer ?: ""
|
||||
|
||||
optionViewStates.add(
|
||||
if (!isPollSent) {
|
||||
// Poll event is not send yet. Disable option.
|
||||
PollOptionViewState.PollSending(optionId, optionAnswer)
|
||||
} else if (isEnded) {
|
||||
// Poll is ended. Disable option, show votes and mark the winner.
|
||||
val isWinner = winnerVoteCount != 0 && voteCount == winnerVoteCount
|
||||
PollOptionViewState.PollEnded(optionId, optionAnswer, voteCount, votePercentage, isWinner)
|
||||
} else if (didUserVoted) {
|
||||
// User voted to the poll, but poll is not ended. Enable option, show votes and mark the user's selection.
|
||||
PollOptionViewState.PollVoted(optionId, optionAnswer, voteCount, votePercentage, isMyVote)
|
||||
} else {
|
||||
// User didn't voted yet and poll is not ended yet. Enable options, hide votes.
|
||||
PollOptionViewState.PollReady(optionId, optionAnswer)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
return PollItem_()
|
||||
.attributes(attributes)
|
||||
.eventId(informationData.eventId)
|
||||
.pollQuestion(pollContent.pollCreationInfo?.question?.question ?: "")
|
||||
.pollSent(isPollSent)
|
||||
.totalVotesText(totalVotesText)
|
||||
.optionViewStates(optionViewStates)
|
||||
.highlighted(highlight)
|
||||
.leftGuideline(avatarSizeProvider.leftGuideline)
|
||||
.callback(callback)
|
||||
}
|
||||
|
||||
private fun buildAudioMessageItem(messageContent: MessageAudioContent,
|
||||
|
|
|
@ -48,6 +48,7 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
|
|||
when (event.root.getClearType()) {
|
||||
// Message itemsX
|
||||
EventType.STICKER,
|
||||
EventType.POLL_START,
|
||||
EventType.MESSAGE -> messageItemFactory.create(params)
|
||||
EventType.STATE_ROOM_TOMBSTONE,
|
||||
EventType.STATE_ROOM_NAME,
|
||||
|
@ -74,7 +75,9 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
|
|||
EventType.REACTION,
|
||||
EventType.STATE_SPACE_CHILD,
|
||||
EventType.STATE_SPACE_PARENT,
|
||||
EventType.STATE_ROOM_POWER_LEVELS -> noticeItemFactory.create(params)
|
||||
EventType.STATE_ROOM_POWER_LEVELS,
|
||||
EventType.POLL_RESPONSE,
|
||||
EventType.POLL_END -> noticeItemFactory.create(params)
|
||||
EventType.STATE_ROOM_WIDGET_LEGACY,
|
||||
EventType.STATE_ROOM_WIDGET -> widgetItemFactory.create(params)
|
||||
EventType.STATE_ROOM_ENCRYPTION -> encryptionItemFactory.create(params)
|
||||
|
|
|
@ -27,10 +27,9 @@ import org.commonmark.node.Document
|
|||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageOptionsContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageType
|
||||
import org.matrix.android.sdk.api.session.room.model.message.OPTION_TYPE_BUTTONS
|
||||
import org.matrix.android.sdk.api.session.room.model.relation.ReactionContent
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||
import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent
|
||||
|
@ -88,26 +87,7 @@ class DisplayableEventFormatter @Inject constructor(
|
|||
simpleFormat(senderName, stringProvider.getString(R.string.sent_a_video), appendAuthor)
|
||||
}
|
||||
MessageType.MSGTYPE_FILE -> {
|
||||
return simpleFormat(senderName, stringProvider.getString(R.string.sent_a_file), appendAuthor)
|
||||
}
|
||||
MessageType.MSGTYPE_RESPONSE -> {
|
||||
// do not show that?
|
||||
span { }
|
||||
}
|
||||
MessageType.MSGTYPE_OPTIONS -> {
|
||||
when (messageContent) {
|
||||
is MessageOptionsContent -> {
|
||||
val previewText = if (messageContent.optionType == OPTION_TYPE_BUTTONS) {
|
||||
stringProvider.getString(R.string.sent_a_bot_buttons)
|
||||
} else {
|
||||
stringProvider.getString(R.string.sent_a_poll)
|
||||
}
|
||||
simpleFormat(senderName, previewText, appendAuthor)
|
||||
}
|
||||
else -> {
|
||||
span { }
|
||||
}
|
||||
}
|
||||
simpleFormat(senderName, stringProvider.getString(R.string.sent_a_file), appendAuthor)
|
||||
}
|
||||
else -> {
|
||||
simpleFormat(senderName, messageContent.body, appendAuthor)
|
||||
|
@ -137,6 +117,16 @@ class DisplayableEventFormatter @Inject constructor(
|
|||
EventType.CALL_CANDIDATES -> {
|
||||
span { }
|
||||
}
|
||||
EventType.POLL_START -> {
|
||||
timelineEvent.root.getClearContent().toModel<MessagePollContent>(catchError = true)?.pollCreationInfo?.question?.question
|
||||
?: stringProvider.getString(R.string.sent_a_poll)
|
||||
}
|
||||
EventType.POLL_RESPONSE -> {
|
||||
stringProvider.getString(R.string.poll_response_room_list_preview)
|
||||
}
|
||||
EventType.POLL_END -> {
|
||||
stringProvider.getString(R.string.poll_end_room_list_preview)
|
||||
}
|
||||
else -> {
|
||||
span {
|
||||
text = noticeEventFormatter.format(timelineEvent, isDm) ?: ""
|
||||
|
|
|
@ -103,7 +103,9 @@ class NoticeEventFormatter @Inject constructor(
|
|||
EventType.KEY_VERIFICATION_READY,
|
||||
EventType.STATE_SPACE_CHILD,
|
||||
EventType.STATE_SPACE_PARENT,
|
||||
EventType.REDACTION -> formatDebug(timelineEvent.root)
|
||||
EventType.REDACTION,
|
||||
EventType.POLL_RESPONSE,
|
||||
EventType.POLL_END -> formatDebug(timelineEvent.root)
|
||||
else -> {
|
||||
Timber.v("Type $type not handled by this formatter")
|
||||
null
|
||||
|
|
|
@ -23,6 +23,7 @@ import im.vector.app.features.home.room.detail.timeline.factory.TimelineItemFact
|
|||
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.ReactionInfoData
|
||||
import im.vector.app.features.home.room.detail.timeline.item.ReferencesInfoData
|
||||
import im.vector.app.features.home.room.detail.timeline.item.SendStateDecoration
|
||||
|
@ -107,10 +108,15 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses
|
|||
pollResponseAggregatedSummary = event.annotations?.pollResponseSummary?.let {
|
||||
PollResponseData(
|
||||
myVote = it.aggregatedContent?.myVote,
|
||||
isClosed = it.closedTime ?: Long.MAX_VALUE > System.currentTimeMillis(),
|
||||
votes = it.aggregatedContent?.votes
|
||||
?.groupBy({ it.optionIndex }, { it.userId })
|
||||
?.mapValues { it.value.size }
|
||||
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
|
||||
)
|
||||
},
|
||||
hasBeenEdited = event.hasBeenEdited(),
|
||||
|
|
|
@ -50,7 +50,8 @@ object TimelineDisplayableEvents {
|
|||
EventType.STATE_ROOM_TOMBSTONE,
|
||||
EventType.STATE_ROOM_JOIN_RULES,
|
||||
EventType.KEY_VERIFICATION_DONE,
|
||||
EventType.KEY_VERIFICATION_CANCEL
|
||||
EventType.KEY_VERIFICATION_CANCEL,
|
||||
EventType.POLL_START
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -119,6 +119,8 @@ class TimelineEventVisibilityHelper @Inject constructor(private val userPreferen
|
|||
val diff = computeMembershipDiff()
|
||||
if ((diff.isJoin || diff.isPart) && !userPreferencesProvider.shouldShowJoinLeaves()) return true
|
||||
if ((diff.isAvatarChange || diff.isDisplaynameChange) && !userPreferencesProvider.shouldShowAvatarDisplayNameChanges()) return true
|
||||
} else if (root.getClearType() == EventType.POLL_START && !userPreferencesProvider.shouldShowPolls()) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
|
|
@ -1,17 +0,0 @@
|
|||
/*
|
||||
* Copyright 2019 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.item
|
|
@ -71,11 +71,19 @@ data class ReadReceiptData(
|
|||
|
||||
@Parcelize
|
||||
data class PollResponseData(
|
||||
val myVote: Int?,
|
||||
val votes: Map<Int, Int>?,
|
||||
val myVote: String?,
|
||||
val votes: Map<String, PollVoteSummaryData>?,
|
||||
val totalVotes: Int = 0,
|
||||
val winnerVoteCount: Int = 0,
|
||||
val isClosed: Boolean = false
|
||||
) : Parcelable
|
||||
|
||||
@Parcelize
|
||||
data class PollVoteSummaryData(
|
||||
val total: Int = 0,
|
||||
val percentage: Double = 0.0
|
||||
) : Parcelable
|
||||
|
||||
enum class E2EDecoration {
|
||||
NONE,
|
||||
WARN_IN_CLEAR,
|
||||
|
|
|
@ -1,79 +0,0 @@
|
|||
/*
|
||||
* Copyright 2020 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.item
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import com.airbnb.epoxy.EpoxyAttribute
|
||||
import com.airbnb.epoxy.EpoxyModelClass
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.epoxy.onClick
|
||||
import im.vector.app.core.extensions.setTextOrHide
|
||||
import im.vector.app.features.home.room.detail.RoomDetailAction
|
||||
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageOptionsContent
|
||||
|
||||
@EpoxyModelClass(layout = R.layout.item_timeline_event_base)
|
||||
abstract class MessageOptionsItem : AbsMessageItem<MessageOptionsItem.Holder>() {
|
||||
|
||||
@EpoxyAttribute
|
||||
var optionsContent: MessageOptionsContent? = null
|
||||
|
||||
@EpoxyAttribute
|
||||
var callback: TimelineEventController.Callback? = null
|
||||
|
||||
@EpoxyAttribute
|
||||
var informationData: MessageInformationData? = null
|
||||
|
||||
override fun getViewType() = STUB_ID
|
||||
|
||||
override fun bind(holder: Holder) {
|
||||
super.bind(holder)
|
||||
|
||||
renderSendState(holder.view, holder.labelText)
|
||||
|
||||
holder.labelText.setTextOrHide(optionsContent?.label)
|
||||
|
||||
holder.buttonContainer.removeAllViews()
|
||||
|
||||
val relatedEventId = informationData?.eventId ?: return
|
||||
val options = optionsContent?.options?.takeIf { it.isNotEmpty() } ?: return
|
||||
// Now add back the buttons
|
||||
options.forEachIndexed { index, option ->
|
||||
val materialButton = LayoutInflater.from(holder.view.context).inflate(R.layout.option_buttons, holder.buttonContainer, false)
|
||||
as MaterialButton
|
||||
holder.buttonContainer.addView(materialButton, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
|
||||
materialButton.text = option.label
|
||||
materialButton.onClick {
|
||||
callback?.onTimelineItemAction(RoomDetailAction.ReplyToOptions(relatedEventId, index, option.value ?: "$index"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Holder : AbsMessageItem.Holder(STUB_ID) {
|
||||
|
||||
val labelText by bind<TextView>(R.id.optionLabelText)
|
||||
|
||||
val buttonContainer by bind<ViewGroup>(R.id.optionsButtonContainer)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val STUB_ID = R.id.messageOptionsStub
|
||||
}
|
||||
}
|
|
@ -1,163 +0,0 @@
|
|||
/*
|
||||
* Copyright 2020 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.item
|
||||
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Button
|
||||
import android.widget.TextView
|
||||
import androidx.core.view.isVisible
|
||||
import com.airbnb.epoxy.EpoxyAttribute
|
||||
import com.airbnb.epoxy.EpoxyModelClass
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.epoxy.ClickListener
|
||||
import im.vector.app.core.epoxy.onClick
|
||||
import im.vector.app.core.extensions.setTextOrHide
|
||||
import im.vector.app.features.home.room.detail.RoomDetailAction
|
||||
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageOptionsContent
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@EpoxyModelClass(layout = R.layout.item_timeline_event_base)
|
||||
abstract class MessagePollItem : AbsMessageItem<MessagePollItem.Holder>() {
|
||||
|
||||
@EpoxyAttribute
|
||||
var optionsContent: MessageOptionsContent? = null
|
||||
|
||||
@EpoxyAttribute
|
||||
var callback: TimelineEventController.Callback? = null
|
||||
|
||||
@EpoxyAttribute
|
||||
var informationData: MessageInformationData? = null
|
||||
|
||||
override fun getViewType() = STUB_ID
|
||||
|
||||
override fun bind(holder: Holder) {
|
||||
super.bind(holder)
|
||||
|
||||
holder.pollId = informationData?.eventId
|
||||
holder.callback = callback
|
||||
holder.optionValues = optionsContent?.options?.map { it.value ?: it.label }
|
||||
|
||||
renderSendState(holder.view, holder.labelText)
|
||||
|
||||
holder.labelText.setTextOrHide(optionsContent?.label)
|
||||
|
||||
val buttons = listOf(holder.button1, holder.button2, holder.button3, holder.button4, holder.button5)
|
||||
val resultLines = listOf(holder.result1, holder.result2, holder.result3, holder.result4, holder.result5)
|
||||
|
||||
buttons.forEach { it.isVisible = false }
|
||||
resultLines.forEach { it.isVisible = false }
|
||||
|
||||
val myVote = informationData?.pollResponseAggregatedSummary?.myVote
|
||||
val iHaveVoted = myVote != null
|
||||
val votes = informationData?.pollResponseAggregatedSummary?.votes
|
||||
val totalVotes = votes?.values
|
||||
?.fold(0) { acc, count -> acc + count } ?: 0
|
||||
val percentMode = totalVotes > 100
|
||||
|
||||
if (!iHaveVoted) {
|
||||
// Show buttons if i have not voted
|
||||
holder.resultWrapper.isVisible = false
|
||||
optionsContent?.options?.forEachIndexed { index, item ->
|
||||
if (index < buttons.size) {
|
||||
buttons[index].let {
|
||||
// current limitation, have to wait for event to be sent in order to reply
|
||||
it.isEnabled = informationData?.sendState?.isSent() ?: false
|
||||
it.text = item.label
|
||||
it.isVisible = true
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
holder.resultWrapper.isVisible = true
|
||||
val maxCount = votes?.maxByOrNull { it.value }?.value ?: 0
|
||||
optionsContent?.options?.forEachIndexed { index, item ->
|
||||
if (index < resultLines.size) {
|
||||
val optionCount = votes?.get(index) ?: 0
|
||||
val count = if (percentMode) {
|
||||
if (totalVotes > 0) {
|
||||
(optionCount / totalVotes.toFloat() * 100).roundToInt().let { "$it%" }
|
||||
} else {
|
||||
""
|
||||
}
|
||||
} else {
|
||||
optionCount.toString()
|
||||
}
|
||||
resultLines[index].let {
|
||||
it.label = item.label
|
||||
it.isWinner = optionCount == maxCount
|
||||
it.optionSelected = index == myVote
|
||||
it.percent = count
|
||||
it.isVisible = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
holder.infoText.text = holder.view.context.resources.getQuantityString(R.plurals.poll_info, totalVotes, totalVotes)
|
||||
}
|
||||
|
||||
override fun unbind(holder: Holder) {
|
||||
holder.pollId = null
|
||||
holder.callback = null
|
||||
holder.optionValues = null
|
||||
super.unbind(holder)
|
||||
}
|
||||
|
||||
class Holder : AbsMessageItem.Holder(STUB_ID) {
|
||||
|
||||
var pollId: String? = null
|
||||
var optionValues: List<String?>? = null
|
||||
var callback: TimelineEventController.Callback? = null
|
||||
|
||||
val button1 by bind<Button>(R.id.pollButton1)
|
||||
val button2 by bind<Button>(R.id.pollButton2)
|
||||
val button3 by bind<Button>(R.id.pollButton3)
|
||||
val button4 by bind<Button>(R.id.pollButton4)
|
||||
val button5 by bind<Button>(R.id.pollButton5)
|
||||
|
||||
val result1 by bind<PollResultLineView>(R.id.pollResult1)
|
||||
val result2 by bind<PollResultLineView>(R.id.pollResult2)
|
||||
val result3 by bind<PollResultLineView>(R.id.pollResult3)
|
||||
val result4 by bind<PollResultLineView>(R.id.pollResult4)
|
||||
val result5 by bind<PollResultLineView>(R.id.pollResult5)
|
||||
|
||||
val labelText by bind<TextView>(R.id.pollLabelText)
|
||||
val infoText by bind<TextView>(R.id.pollInfosText)
|
||||
|
||||
val resultWrapper by bind<ViewGroup>(R.id.pollResultsWrapper)
|
||||
|
||||
override fun bindView(itemView: View) {
|
||||
super.bindView(itemView)
|
||||
val buttons = listOf(button1, button2, button3, button4, button5)
|
||||
val clickListener = object : ClickListener {
|
||||
override fun invoke(p1: View) {
|
||||
val optionIndex = buttons.indexOf(p1)
|
||||
if (optionIndex != -1 && pollId != null) {
|
||||
val compatValue = if (optionIndex < optionValues?.size ?: 0) optionValues?.get(optionIndex) else null
|
||||
callback?.onTimelineItemAction(RoomDetailAction.ReplyToOptions(pollId!!, optionIndex, compatValue ?: "$optionIndex"))
|
||||
}
|
||||
}
|
||||
}
|
||||
buttons.forEach { it.onClick(clickListener) }
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val STUB_ID = R.id.messagePollStub
|
||||
}
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
/*
|
||||
* Copyright 2020 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.item
|
||||
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.core.view.children
|
||||
import com.airbnb.epoxy.EpoxyAttribute
|
||||
import com.airbnb.epoxy.EpoxyModelClass
|
||||
import im.vector.app.R
|
||||
import im.vector.app.features.home.room.detail.RoomDetailAction
|
||||
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
|
||||
|
||||
@EpoxyModelClass(layout = R.layout.item_timeline_event_base)
|
||||
abstract class PollItem : AbsMessageItem<PollItem.Holder>() {
|
||||
|
||||
@EpoxyAttribute
|
||||
var pollQuestion: String? = null
|
||||
|
||||
@EpoxyAttribute
|
||||
var callback: TimelineEventController.Callback? = null
|
||||
|
||||
@EpoxyAttribute
|
||||
var eventId: String? = null
|
||||
|
||||
@EpoxyAttribute
|
||||
var pollSent: Boolean = false
|
||||
|
||||
@EpoxyAttribute
|
||||
var totalVotesText: String? = null
|
||||
|
||||
@EpoxyAttribute
|
||||
lateinit var optionViewStates: List<PollOptionViewState>
|
||||
|
||||
override fun bind(holder: Holder) {
|
||||
super.bind(holder)
|
||||
val relatedEventId = eventId ?: return
|
||||
|
||||
renderSendState(holder.view, holder.questionTextView)
|
||||
|
||||
holder.questionTextView.text = pollQuestion
|
||||
holder.totalVotesTextView.text = totalVotesText
|
||||
|
||||
while (holder.optionsContainer.childCount < optionViewStates.size) {
|
||||
holder.optionsContainer.addView(PollOptionView(holder.view.context))
|
||||
}
|
||||
while (holder.optionsContainer.childCount > optionViewStates.size) {
|
||||
holder.optionsContainer.removeViewAt(0)
|
||||
}
|
||||
|
||||
val views = holder.optionsContainer.children.toList().filterIsInstance<PollOptionView>()
|
||||
|
||||
optionViewStates.forEachIndexed { index, optionViewState ->
|
||||
views.getOrNull(index)?.let {
|
||||
it.render(optionViewState)
|
||||
it.setOnClickListener {
|
||||
callback?.onTimelineItemAction(RoomDetailAction.VoteToPoll(relatedEventId, optionViewState.optionId))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Holder : AbsMessageItem.Holder(STUB_ID) {
|
||||
val questionTextView by bind<TextView>(R.id.questionTextView)
|
||||
val optionsContainer by bind<LinearLayout>(R.id.optionsContainer)
|
||||
val totalVotesTextView by bind<TextView>(R.id.optionsTotalVotesTextView)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val STUB_ID = R.id.messageContentPollStub
|
||||
}
|
||||
}
|
|
@ -0,0 +1,112 @@
|
|||
/*
|
||||
* Copyright (c) 2021 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.item
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.util.AttributeSet
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.core.view.isVisible
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.extensions.setAttributeTintedImageResource
|
||||
import im.vector.app.databinding.ItemPollOptionBinding
|
||||
|
||||
class PollOptionView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = 0
|
||||
) : ConstraintLayout(context, attrs, defStyleAttr) {
|
||||
|
||||
private val views: ItemPollOptionBinding
|
||||
|
||||
init {
|
||||
inflate(context, R.layout.item_poll_option, this)
|
||||
views = ItemPollOptionBinding.bind(this)
|
||||
}
|
||||
|
||||
fun render(state: PollOptionViewState) {
|
||||
views.optionNameTextView.text = state.optionAnswer
|
||||
|
||||
when (state) {
|
||||
is PollOptionViewState.PollSending -> renderPollSending()
|
||||
is PollOptionViewState.PollEnded -> renderPollEnded(state)
|
||||
is PollOptionViewState.PollReady -> renderPollReady()
|
||||
is PollOptionViewState.PollVoted -> renderPollVoted(state)
|
||||
}
|
||||
}
|
||||
|
||||
private fun renderPollSending() {
|
||||
views.optionCheckImageView.isVisible = false
|
||||
views.optionWinnerImageView.isVisible = false
|
||||
hideVotes()
|
||||
renderVoteSelection(false)
|
||||
}
|
||||
|
||||
private fun renderPollEnded(state: PollOptionViewState.PollEnded) {
|
||||
views.optionCheckImageView.isVisible = false
|
||||
views.optionWinnerImageView.isVisible = state.isWinner
|
||||
showVotes(state.voteCount, state.votePercentage)
|
||||
renderVoteSelection(state.isWinner)
|
||||
}
|
||||
|
||||
private fun renderPollReady() {
|
||||
views.optionCheckImageView.isVisible = true
|
||||
views.optionWinnerImageView.isVisible = false
|
||||
hideVotes()
|
||||
renderVoteSelection(false)
|
||||
}
|
||||
|
||||
private fun renderPollVoted(state: PollOptionViewState.PollVoted) {
|
||||
views.optionCheckImageView.isVisible = true
|
||||
views.optionWinnerImageView.isVisible = false
|
||||
showVotes(state.voteCount, state.votePercentage)
|
||||
renderVoteSelection(state.isSelected)
|
||||
}
|
||||
|
||||
private fun showVotes(voteCount: Int, votePercentage: Double) {
|
||||
views.optionVoteCountTextView.apply {
|
||||
isVisible = true
|
||||
text = resources.getQuantityString(R.plurals.poll_option_vote_count, voteCount, voteCount)
|
||||
}
|
||||
views.optionVoteProgress.apply {
|
||||
val progressValue = (votePercentage * 100).toInt()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
setProgress(progressValue, true)
|
||||
} else {
|
||||
progress = progressValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun hideVotes() {
|
||||
views.optionVoteCountTextView.isVisible = false
|
||||
views.optionVoteProgress.progress = 0
|
||||
}
|
||||
|
||||
private fun renderVoteSelection(isSelected: Boolean) {
|
||||
if (isSelected) {
|
||||
views.optionBorderImageView.setAttributeTintedImageResource(R.drawable.bg_poll_option, R.attr.colorPrimary)
|
||||
views.optionVoteProgress.progressDrawable = AppCompatResources.getDrawable(context, R.drawable.poll_option_progressbar_checked)
|
||||
views.optionCheckImageView.setImageResource(R.drawable.poll_option_checked)
|
||||
} else {
|
||||
views.optionBorderImageView.setAttributeTintedImageResource(R.drawable.bg_poll_option, R.attr.vctr_content_quinary)
|
||||
views.optionVoteProgress.progressDrawable = AppCompatResources.getDrawable(context, R.drawable.poll_option_progressbar_unchecked)
|
||||
views.optionCheckImageView.setImageResource(R.drawable.poll_option_unchecked)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* Copyright (c) 2021 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.item
|
||||
|
||||
sealed class PollOptionViewState(open val optionId: String,
|
||||
open val optionAnswer: String) {
|
||||
/**
|
||||
* Represents a poll that is not sent to the server yet.
|
||||
*/
|
||||
data class PollSending(override val optionId: String,
|
||||
override val optionAnswer: String
|
||||
) : PollOptionViewState(optionId, optionAnswer)
|
||||
|
||||
/**
|
||||
* Represents a poll that is sent but not voted by the user
|
||||
*/
|
||||
data class PollReady(override val optionId: String,
|
||||
override val optionAnswer: String
|
||||
) : PollOptionViewState(optionId, optionAnswer)
|
||||
|
||||
/**
|
||||
* Represents a poll that user already voted.
|
||||
*/
|
||||
data class PollVoted(override val optionId: String,
|
||||
override val optionAnswer: String,
|
||||
val voteCount: Int,
|
||||
val votePercentage: Double,
|
||||
val isSelected: Boolean
|
||||
) : PollOptionViewState(optionId, optionAnswer)
|
||||
|
||||
/**
|
||||
* Represents a poll that is ended.
|
||||
*/
|
||||
data class PollEnded(override val optionId: String,
|
||||
override val optionAnswer: String,
|
||||
val voteCount: Int,
|
||||
val votePercentage: Double,
|
||||
val isWinner: Boolean
|
||||
) : PollOptionViewState(optionId, optionAnswer)
|
||||
}
|
|
@ -60,7 +60,7 @@ class CreatePollController @Inject constructor(
|
|||
hint(host.stringProvider.getString(R.string.create_poll_question_hint))
|
||||
singleLine(true)
|
||||
imeOptions(questionImeAction)
|
||||
maxLength(500)
|
||||
maxLength(340)
|
||||
onTextChange {
|
||||
host.callback?.onQuestionChanged(it)
|
||||
}
|
||||
|
@ -80,6 +80,7 @@ class CreatePollController @Inject constructor(
|
|||
hint(host.stringProvider.getString(R.string.create_poll_options_hint, (index + 1)))
|
||||
singleLine(true)
|
||||
imeOptions(imeOptions)
|
||||
maxLength(340)
|
||||
onTextChange {
|
||||
host.callback?.onOptionChanged(index, it)
|
||||
}
|
||||
|
|
|
@ -197,6 +197,8 @@ class VectorPreferences @Inject constructor(private val context: Context) {
|
|||
|
||||
private const val TAKE_PHOTO_VIDEO_MODE = "TAKE_PHOTO_VIDEO_MODE"
|
||||
|
||||
private const val SETTINGS_LABS_ENABLE_POLLS = "SETTINGS_LABS_ENABLE_POLLS"
|
||||
|
||||
// Possible values for TAKE_PHOTO_VIDEO_MODE
|
||||
const val TAKE_PHOTO_VIDEO_MODE_ALWAYS_ASK = 0
|
||||
const val TAKE_PHOTO_VIDEO_MODE_PHOTO = 1
|
||||
|
@ -1007,4 +1009,8 @@ class VectorPreferences @Inject constructor(private val context: Context) {
|
|||
putInt(TAKE_PHOTO_VIDEO_MODE, mode)
|
||||
}
|
||||
}
|
||||
|
||||
fun labsEnablePolls(): Boolean {
|
||||
return defaultPrefs.getBoolean(SETTINGS_LABS_ENABLE_POLLS, false)
|
||||
}
|
||||
}
|
||||
|
|
9
vector/src/main/res/drawable/bg_poll_option.xml
Normal file
9
vector/src/main/res/drawable/bg_poll_option.xml
Normal file
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="@android:color/transparent" />
|
||||
<stroke
|
||||
android:width="1dp"
|
||||
android:color="?vctr_content_quinary" />
|
||||
<corners android:radius="4dp" />
|
||||
</shape>
|
8
vector/src/main/res/drawable/divider_poll_options.xml
Normal file
8
vector/src/main/res/drawable/divider_poll_options.xml
Normal file
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<size
|
||||
android:width="1dp"
|
||||
android:height="16dp" />
|
||||
|
||||
</shape>
|
13
vector/src/main/res/drawable/ic_check_on_white.xml
Normal file
13
vector/src/main/res/drawable/ic_check_on_white.xml
Normal file
|
@ -0,0 +1,13 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="16dp"
|
||||
android:height="16dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M20,7L9,18L4,13"
|
||||
android:strokeWidth="2"
|
||||
android:strokeColor="#FFFFFF"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round" />
|
||||
</vector>
|
9
vector/src/main/res/drawable/ic_poll_winner.xml
Normal file
9
vector/src/main/res/drawable/ic_poll_winner.xml
Normal file
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="16dp"
|
||||
android:height="16dp"
|
||||
android:viewportWidth="16"
|
||||
android:viewportHeight="16">
|
||||
<path
|
||||
android:pathData="M12.6667,3.3333H11.3333V2.6667C11.3333,2.3 11.0333,2 10.6667,2H5.3333C4.9667,2 4.6667,2.3 4.6667,2.6667V3.3333H3.3333C2.6,3.3333 2,3.9333 2,4.6667V5.3333C2,7.0333 3.28,8.42 4.9267,8.6267C5.3467,9.6267 6.2467,10.38 7.3333,10.6V12.6667H5.3333C4.9667,12.6667 4.6667,12.9667 4.6667,13.3333C4.6667,13.7 4.9667,14 5.3333,14H10.6667C11.0333,14 11.3333,13.7 11.3333,13.3333C11.3333,12.9667 11.0333,12.6667 10.6667,12.6667H8.6667V10.6C9.7533,10.38 10.6533,9.6267 11.0733,8.6267C12.72,8.42 14,7.0333 14,5.3333V4.6667C14,3.9333 13.4,3.3333 12.6667,3.3333ZM3.3333,5.3333V4.6667H4.6667V7.2133C3.8933,6.9333 3.3333,6.2 3.3333,5.3333ZM12.6667,5.3333C12.6667,6.2 12.1067,6.9333 11.3333,7.2133V4.6667H12.6667V5.3333Z"
|
||||
android:fillColor="#0DBD8B"/>
|
||||
</vector>
|
14
vector/src/main/res/drawable/poll_option_checked.xml
Normal file
14
vector/src/main/res/drawable/poll_option_checked.xml
Normal file
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<shape android:shape="oval">
|
||||
<solid android:color="?colorPrimary" />
|
||||
<size
|
||||
android:width="20dp"
|
||||
android:height="20dp" />
|
||||
</shape>
|
||||
</item>
|
||||
<item
|
||||
android:drawable="@drawable/ic_check_on_white"
|
||||
android:gravity="center" />
|
||||
</layer-list>
|
|
@ -0,0 +1,18 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:id="@android:id/background">
|
||||
<shape>
|
||||
<corners android:radius="4dp" />
|
||||
<solid android:color="?vctr_system" />
|
||||
</shape>
|
||||
</item>
|
||||
|
||||
<item android:id="@android:id/progress">
|
||||
<clip>
|
||||
<shape>
|
||||
<corners android:radius="4dp" />
|
||||
<solid android:color="?colorPrimary" />
|
||||
</shape>
|
||||
</clip>
|
||||
</item>
|
||||
</layer-list>
|
|
@ -0,0 +1,18 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:id="@android:id/background">
|
||||
<shape>
|
||||
<corners android:radius="4dp" />
|
||||
<solid android:color="?vctr_system" />
|
||||
</shape>
|
||||
</item>
|
||||
|
||||
<item android:id="@android:id/progress">
|
||||
<clip>
|
||||
<shape>
|
||||
<corners android:radius="4dp" />
|
||||
<solid android:color="?vctr_content_quaternary" />
|
||||
</shape>
|
||||
</clip>
|
||||
</item>
|
||||
</layer-list>
|
13
vector/src/main/res/drawable/poll_option_unchecked.xml
Normal file
13
vector/src/main/res/drawable/poll_option_unchecked.xml
Normal file
|
@ -0,0 +1,13 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="oval">
|
||||
|
||||
<solid android:color="@android:color/transparent" />
|
||||
<stroke
|
||||
android:width="1dp"
|
||||
android:color="?vctr_content_quaternary" />
|
||||
<size
|
||||
android:width="20dp"
|
||||
android:height="20dp" />
|
||||
|
||||
</shape>
|
86
vector/src/main/res/layout/item_poll_option.xml
Normal file
86
vector/src/main/res/layout/item_poll_option.xml
Normal file
|
@ -0,0 +1,86 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<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:id="@+id/optionContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/optionBorderImageView"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:importantForAccessibility="no"
|
||||
android:src="@drawable/bg_poll_option"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/optionCheckImageView"
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="20dp"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:importantForAccessibility="no"
|
||||
android:src="@drawable/poll_option_unchecked"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/optionNameTextView"
|
||||
style="@style/Widget.Vector.TextView.Body"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginEnd="12dp"
|
||||
app:layout_constraintEnd_toEndOf="@id/optionWinnerImageView"
|
||||
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:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="@id/optionVoteProgress"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/optionVoteProgress"
|
||||
tools:text="@sample/poll.json/data/votes"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/optionVoteProgress"
|
||||
style="?android:attr/progressBarStyleHorizontal"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="6dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
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_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/optionNameTextView"
|
||||
tools:progress="60" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -119,24 +119,17 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:layout="@layout/item_timeline_event_redacted_stub" />
|
||||
|
||||
<ViewStub
|
||||
android:id="@+id/messagePollStub"
|
||||
style="@style/TimelineContentStubBaseParams"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout="@layout/item_timeline_event_poll_stub" />
|
||||
|
||||
<ViewStub
|
||||
android:id="@+id/messageOptionsStub"
|
||||
style="@style/TimelineContentStubBaseParams"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout="@layout/item_timeline_event_option_buttons_stub" />
|
||||
|
||||
<ViewStub
|
||||
android:id="@+id/messageContentVoiceStub"
|
||||
style="@style/TimelineContentStubBaseParams"
|
||||
android:layout="@layout/item_timeline_event_voice_stub"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<ViewStub
|
||||
android:id="@+id/messageContentPollStub"
|
||||
style="@style/TimelineContentStubBaseParams"
|
||||
android:layout="@layout/item_timeline_event_poll" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<im.vector.app.core.ui.views.SendStateImageView
|
||||
|
|
|
@ -1,34 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
tools:viewBindingIgnore="true">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/optionLabelText"
|
||||
style="@style/Widget.Vector.TextView.Body"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:textColor="?vctr_content_primary"
|
||||
tools:text="What would you like to do?" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/optionsButtonContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<!-- Filled at runtime with buttons -->
|
||||
<!--Button
|
||||
android:id="@+id/pollButton1"
|
||||
style="@style/Widget.Vector.Button.Outlined.Poll"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
tools:text="Create Github issue" /-->
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
43
vector/src/main/res/layout/item_timeline_event_poll.xml
Normal file
43
vector/src/main/res/layout/item_timeline_event_poll.xml
Normal file
|
@ -0,0 +1,43 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<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:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/questionTextView"
|
||||
style="@style/Widget.Vector.TextView.Subtitle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:textColor="?vctr_content_primary"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="@sample/poll.json/question" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/optionsContainer"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:divider="@drawable/divider_poll_options"
|
||||
android:orientation="vertical"
|
||||
android:showDividers="middle"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/questionTextView" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/optionsTotalVotesTextView"
|
||||
style="@style/Widget.Vector.TextView.Caption"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/optionsContainer"
|
||||
tools:text="@sample/poll.json/totalVotes" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/pollItemContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical" />
|
|
@ -1,147 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout 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:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
tools:viewBindingIgnore="true">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_margin="4dp"
|
||||
android:importantForAccessibility="no"
|
||||
android:src="@drawable/ic_poll"
|
||||
app:tint="?colorPrimary"
|
||||
tools:ignore="MissingPrefix" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/pollLabelText"
|
||||
style="@style/Widget.Vector.TextView.Body"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginStart="4dp"
|
||||
android:textColor="?vctr_content_primary"
|
||||
android:textStyle="bold"
|
||||
tools:text="What would you like to do?" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<Button
|
||||
android:id="@+id/pollButton1"
|
||||
style="@style/Widget.Vector.Button.Outlined.Poll"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone"
|
||||
tools:text="Create Github issue"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/pollButton2"
|
||||
style="@style/Widget.Vector.Button.Outlined.Poll"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone"
|
||||
tools:text="Search Github"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/pollButton3"
|
||||
style="@style/Widget.Vector.Button.Outlined.Poll"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone"
|
||||
tools:text="Logout"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/pollButton4"
|
||||
style="@style/Widget.Vector.Button.Outlined.Poll"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone"
|
||||
tools:text="Option 4"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/pollButton5"
|
||||
style="@style/Widget.Vector.Button.Outlined.Poll"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone"
|
||||
tools:text="Option 5"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/pollResultsWrapper"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/bg_attachment_type_selector"
|
||||
android:orientation="vertical"
|
||||
android:paddingStart="8dp"
|
||||
android:paddingEnd="8dp"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible">
|
||||
|
||||
<im.vector.app.features.home.room.detail.timeline.item.PollResultLineView
|
||||
android:id="@+id/pollResult1"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="40dp"
|
||||
tools:optionCount="40%"
|
||||
tools:optionName="Create Github issue"
|
||||
tools:optionSelected="true" />
|
||||
|
||||
<im.vector.app.features.home.room.detail.timeline.item.PollResultLineView
|
||||
android:id="@+id/pollResult2"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="40dp"
|
||||
tools:optionCount="60%"
|
||||
tools:optionIsWinner="true"
|
||||
tools:optionName="Search Github"
|
||||
tools:optionSelected="false" />
|
||||
|
||||
<im.vector.app.features.home.room.detail.timeline.item.PollResultLineView
|
||||
android:id="@+id/pollResult3"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="40dp"
|
||||
tools:optionCount="0%"
|
||||
tools:optionName="Logout"
|
||||
tools:optionSelected="false" />
|
||||
|
||||
<im.vector.app.features.home.room.detail.timeline.item.PollResultLineView
|
||||
android:id="@+id/pollResult4"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="40dp"
|
||||
tools:optionCount="0%"
|
||||
tools:optionName="Option 4"
|
||||
tools:optionSelected="false" />
|
||||
|
||||
<im.vector.app.features.home.room.detail.timeline.item.PollResultLineView
|
||||
android:id="@+id/pollResult5"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="40dp"
|
||||
tools:optionCount="0%"
|
||||
tools:optionName="Option 5"
|
||||
tools:optionSelected="false" />
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/pollInfosText"
|
||||
style="@style/Widget.Vector.TextView.Caption"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="2dp"
|
||||
android:textColor="?vctr_content_secondary"
|
||||
android:visibility="gone"
|
||||
tools:text="12 votes - Final Results"
|
||||
tools:visibility="visible" />
|
||||
|
||||
</LinearLayout>
|
|
@ -24,6 +24,7 @@
|
|||
android:weightSum="3">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/attachmentCameraButtonContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
|
@ -45,6 +46,7 @@
|
|||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/attachmentGalleryButtonContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
|
@ -66,6 +68,7 @@
|
|||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/attachmentFileButtonContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
|
@ -97,6 +100,7 @@
|
|||
android:weightSum="3">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/attachmentAudioButtonContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
|
@ -118,6 +122,7 @@
|
|||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/attachmentContactButtonContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
|
@ -139,6 +144,7 @@
|
|||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/attachmentStickersButtonContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
|
@ -166,10 +172,10 @@
|
|||
android:layout_margin="16dp"
|
||||
android:baselineAligned="false"
|
||||
android:orientation="horizontal"
|
||||
android:visibility="gone"
|
||||
android:weightSum="3">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/attachmentPollButtonContainer"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
|
|
|
@ -3664,4 +3664,31 @@
|
|||
<item quantity="one">At least %1$s option is required</item>
|
||||
<item quantity="other">At least %1$s options are required</item>
|
||||
</plurals>
|
||||
<plurals name="poll_option_vote_count">
|
||||
<item quantity="one">%1$d vote</item>
|
||||
<item quantity="other">%1$d votes</item>
|
||||
</plurals>
|
||||
<plurals name="poll_total_vote_count_before_ended_and_voted">
|
||||
<item quantity="one">Based on %1$d vote</item>
|
||||
<item quantity="other">Based on %1$d votes</item>
|
||||
</plurals>
|
||||
<plurals name="poll_total_vote_count_before_ended_and_not_voted">
|
||||
<item quantity="zero">No votes cast</item>
|
||||
<item quantity="one">%1$d vote cast. Vote to the see the results</item>
|
||||
<item quantity="other">%1$d votes cast. Vote to the see the results</item>
|
||||
</plurals>
|
||||
<plurals name="poll_total_vote_count_after_ended">
|
||||
<item quantity="one">Final result based on %1$d vote</item>
|
||||
<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>
|
||||
<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>
|
||||
<string name="labs_enable_polls">Enable Polls</string>
|
||||
<string name="poll_response_room_list_preview">Vote casted</string>
|
||||
<string name="poll_end_room_list_preview">Poll ended</string>
|
||||
<string name="delete_poll_dialog_title">Remove poll</string>
|
||||
<string name="delete_poll_dialog_content">Are you sure you want to remove this poll? You won\'t be able to recover it once removed.</string>
|
||||
</resources>
|
||||
|
|
|
@ -51,4 +51,10 @@
|
|||
android:summary="@string/labs_use_restricted_join_rule_desc"/>
|
||||
<!--</im.vector.app.core.preference.VectorPreferenceCategory>-->
|
||||
|
||||
<im.vector.app.core.preference.VectorSwitchPreference
|
||||
android:defaultValue="false"
|
||||
android:key="SETTINGS_LABS_ENABLE_POLLS"
|
||||
android:title="@string/labs_enable_polls" />
|
||||
|
||||
|
||||
</androidx.preference.PreferenceScreen>
|
Loading…
Reference in a new issue