mirror of
https://github.com/element-hq/element-android
synced 2024-11-24 18:35:40 +03:00
Support incremental poll response aggregation + display
This commit is contained in:
parent
a0aebed3f7
commit
577c5a16b3
19 changed files with 369 additions and 32 deletions
|
@ -19,5 +19,6 @@ data class EventAnnotationsSummary(
|
|||
var eventId: String,
|
||||
var reactionsSummary: List<ReactionAggregatedSummary>,
|
||||
var editSummary: EditAggregatedSummary?,
|
||||
var pollResponseSummary: PollResponseAggregatedSummary?,
|
||||
var referencesAggregatedSummary: ReferencesAggregatedSummary? = null
|
||||
)
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* 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.matrix.android.api.session.room.model
|
||||
|
||||
data class PollResponseAggregatedSummary(
|
||||
|
||||
var aggregatedContent: PollSummaryContent? = null,
|
||||
|
||||
// If set the poll is closed (Clients SHOULD NOT consider responses after the close event)
|
||||
var closedTime: Long? = null,
|
||||
// Clients SHOULD validate that the option in the relationship is a valid option, and ignore the response if invalid
|
||||
var nbOptions: Int = 0,
|
||||
// The list of the eventIDs used to build the summary (might be out of sync if chunked received from message chunk)
|
||||
val sourceEvents: List<String>,
|
||||
val localEchos: List<String>
|
||||
)
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* 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.matrix.android.api.session.room.model
|
||||
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/**
|
||||
* Contains an aggregated summary info of the poll response.
|
||||
* Put pre-computed info that you want to access quickly without having
|
||||
* to go through all references events
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class PollSummaryContent(
|
||||
//Index of my vote
|
||||
var myVote: Int? = 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
|
||||
) {
|
||||
|
||||
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 VoteInfo(
|
||||
val userId: String,
|
||||
val optionIndex: Int,
|
||||
val voteTimestamp: Long
|
||||
)
|
|
@ -16,12 +16,16 @@
|
|||
|
||||
package im.vector.matrix.android.internal.database.mapper
|
||||
|
||||
import im.vector.matrix.android.api.session.events.model.toContent
|
||||
import im.vector.matrix.android.api.session.events.model.toModel
|
||||
import im.vector.matrix.android.api.session.room.model.EditAggregatedSummary
|
||||
import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary
|
||||
import im.vector.matrix.android.api.session.room.model.PollResponseAggregatedSummary
|
||||
import im.vector.matrix.android.api.session.room.model.ReactionAggregatedSummary
|
||||
import im.vector.matrix.android.api.session.room.model.ReferencesAggregatedSummary
|
||||
import im.vector.matrix.android.internal.database.model.EditAggregatedSummaryEntity
|
||||
import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryEntity
|
||||
import im.vector.matrix.android.internal.database.model.PollResponseAggregatedSummaryEntity
|
||||
import im.vector.matrix.android.internal.database.model.ReactionAggregatedSummaryEntity
|
||||
import im.vector.matrix.android.internal.database.model.ReferencesAggregatedSummaryEntity
|
||||
import io.realm.RealmList
|
||||
|
@ -55,7 +59,11 @@ internal object EventAnnotationsSummaryMapper {
|
|||
it.sourceEvents.toList(),
|
||||
it.sourceLocalEcho.toList()
|
||||
)
|
||||
},
|
||||
pollResponseSummary = annotationsSummary.pollResponseSummary?.let {
|
||||
PollResponseAggregatedSummaryEntityMapper.map(it)
|
||||
}
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -93,6 +101,9 @@ internal object EventAnnotationsSummaryMapper {
|
|||
RealmList<String>().apply { addAll(it.localEchos) }
|
||||
)
|
||||
}
|
||||
eventAnnotationsSummaryEntity.pollResponseSummary = annotationsSummary.pollResponseSummary?.let {
|
||||
PollResponseAggregatedSummaryEntityMapper.map(it)
|
||||
}
|
||||
return eventAnnotationsSummaryEntity
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
package im.vector.matrix.android.internal.database.mapper
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import im.vector.matrix.android.api.session.events.model.toContent
|
||||
import im.vector.matrix.android.api.session.events.model.toModel
|
||||
import im.vector.matrix.android.api.session.room.model.PollResponseAggregatedSummary
|
||||
import im.vector.matrix.android.internal.database.model.PollResponseAggregatedSummaryEntity
|
||||
import io.realm.RealmList
|
||||
|
||||
internal object PollResponseAggregatedSummaryEntityMapper {
|
||||
|
||||
fun map(entity: PollResponseAggregatedSummaryEntity): PollResponseAggregatedSummary {
|
||||
return PollResponseAggregatedSummary(
|
||||
aggregatedContent = ContentMapper.map(entity.aggregatedContent).toModel(),
|
||||
closedTime = entity.closedTime,
|
||||
localEchos = entity.sourceLocalEchoEvents.toList(),
|
||||
sourceEvents = entity.sourceEvents.toList(),
|
||||
nbOptions = entity.nbOptions
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
fun map(model: PollResponseAggregatedSummary): PollResponseAggregatedSummaryEntity {
|
||||
return PollResponseAggregatedSummaryEntity(
|
||||
aggregatedContent = ContentMapper.map(model.aggregatedContent.toContent()),
|
||||
nbOptions = model.nbOptions,
|
||||
closedTime = model.closedTime,
|
||||
sourceEvents = RealmList<String>().apply { addAll(model.sourceEvents) },
|
||||
sourceLocalEchoEvents = RealmList<String>().apply { addAll(model.localEchos) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun PollResponseAggregatedSummaryEntity.asDomain(): PollResponseAggregatedSummary {
|
||||
return PollResponseAggregatedSummaryEntityMapper.map(this)
|
||||
}
|
|
@ -25,7 +25,8 @@ internal open class EventAnnotationsSummaryEntity(
|
|||
var roomId: String? = null,
|
||||
var reactionsSummary: RealmList<ReactionAggregatedSummaryEntity> = RealmList(),
|
||||
var editSummary: EditAggregatedSummaryEntity? = null,
|
||||
var referencesSummaryEntity: ReferencesAggregatedSummaryEntity? = null
|
||||
var referencesSummaryEntity: ReferencesAggregatedSummaryEntity? = null,
|
||||
var pollResponseSummary: PollResponseAggregatedSummaryEntity? = null
|
||||
) : RealmObject() {
|
||||
|
||||
companion object
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* 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.matrix.android.internal.database.model
|
||||
|
||||
import io.realm.RealmList
|
||||
import io.realm.RealmObject
|
||||
|
||||
/**
|
||||
* Keep the latest state of edition of a message
|
||||
*/
|
||||
internal open class PollResponseAggregatedSummaryEntity(
|
||||
// For now we persist this a JSON for greater flexibility
|
||||
// #see PollSummaryContent
|
||||
var aggregatedContent: String? = null,
|
||||
|
||||
// If set the poll is closed (Clients SHOULD NOT consider responses after the close event)
|
||||
var closedTime: Long? = null,
|
||||
// Clients SHOULD validate that the option in the relationship is a valid option, and ignore the response if invalid
|
||||
var nbOptions: Int = 0,
|
||||
|
||||
// The list of the eventIDs used to build the summary (might be out of sync if chunked received from message chunk)
|
||||
var sourceEvents: RealmList<String> = RealmList(),
|
||||
var sourceLocalEchoEvents: RealmList<String> = RealmList()
|
||||
) : RealmObject() {
|
||||
|
||||
companion object
|
||||
}
|
|
@ -40,6 +40,7 @@ import io.realm.annotations.RealmModule
|
|||
EventAnnotationsSummaryEntity::class,
|
||||
ReactionAggregatedSummaryEntity::class,
|
||||
EditAggregatedSummaryEntity::class,
|
||||
PollResponseAggregatedSummaryEntity::class,
|
||||
ReferencesAggregatedSummaryEntity::class,
|
||||
PushRulesEntity::class,
|
||||
PushRuleEntity::class,
|
||||
|
|
|
@ -19,4 +19,5 @@ package im.vector.matrix.android.internal.database.query
|
|||
internal object FilterContent {
|
||||
|
||||
internal const val EDIT_TYPE = """{*"m.relates_to"*"rel_type":*"m.replace"*}"""
|
||||
internal const val RESPONSE_TYPE = """{*"m.relates_to"*"rel_type":*"m.response"*}"""
|
||||
}
|
||||
|
|
|
@ -48,6 +48,7 @@ object MoshiProvider {
|
|||
.registerSubtype(MessageFileContent::class.java, MessageType.MSGTYPE_FILE)
|
||||
.registerSubtype(MessageOptionsContent::class.java, MessageType.MSGTYPE_OPTIONS)
|
||||
.registerSubtype(MessageVerificationRequestContent::class.java, MessageType.MSGTYPE_VERIFICATION_REQUEST)
|
||||
.registerSubtype(MessagePollResponseContent::class.java, MessageType.MSGTYPE_RESPONSE)
|
||||
)
|
||||
.add(SerializeNulls.JSON_ADAPTER_FACTORY)
|
||||
.build()
|
||||
|
|
|
@ -26,8 +26,12 @@ import im.vector.matrix.android.api.session.events.model.RelationType
|
|||
import im.vector.matrix.android.api.session.events.model.toContent
|
||||
import im.vector.matrix.android.api.session.events.model.toModel
|
||||
import im.vector.matrix.android.api.session.room.model.ReferencesAggregatedContent
|
||||
import im.vector.matrix.android.api.session.events.model.*
|
||||
import im.vector.matrix.android.api.session.room.model.PollSummaryContent
|
||||
import im.vector.matrix.android.api.session.room.model.VoteInfo
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageRelationContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessagePollResponseContent
|
||||
import im.vector.matrix.android.api.session.room.model.relation.ReactionContent
|
||||
import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult
|
||||
import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent
|
||||
|
@ -36,6 +40,7 @@ import im.vector.matrix.android.internal.database.mapper.EventMapper
|
|||
import im.vector.matrix.android.internal.database.model.EditAggregatedSummaryEntity
|
||||
import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryEntity
|
||||
import im.vector.matrix.android.internal.database.model.EventEntity
|
||||
import im.vector.matrix.android.internal.database.model.PollResponseAggregatedSummaryEntity
|
||||
import im.vector.matrix.android.internal.database.model.ReactionAggregatedSummaryEntity
|
||||
import im.vector.matrix.android.internal.database.model.ReactionAggregatedSummaryEntityFields
|
||||
import im.vector.matrix.android.internal.database.model.ReferencesAggregatedSummaryEntity
|
||||
|
@ -123,6 +128,9 @@ internal class DefaultEventRelationsAggregationTask @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,userId,event, content, roomId, isLocalEcho)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -144,13 +152,20 @@ internal class DefaultEventRelationsAggregationTask @Inject constructor(
|
|||
EventType.ENCRYPTED -> {
|
||||
// Relation type is in clear
|
||||
val encryptedEventContent = event.content.toModel<EncryptedEventContent>()
|
||||
if (encryptedEventContent?.relatesTo?.type == RelationType.REPLACE) {
|
||||
if (encryptedEventContent?.relatesTo?.type == RelationType.REPLACE
|
||||
|| encryptedEventContent?.relatesTo?.type == RelationType.RESPONSE
|
||||
) {
|
||||
// we need to decrypt if needed
|
||||
decryptIfNeeded(event)
|
||||
event.getClearContent().toModel<MessageContent>()?.let {
|
||||
Timber.v("###REPLACE in room $roomId for event ${event.eventId}")
|
||||
// A replace!
|
||||
handleReplace(realm, event, it, roomId, isLocalEcho, encryptedEventContent.relatesTo.eventId)
|
||||
if (encryptedEventContent.relatesTo.type == RelationType.REPLACE) {
|
||||
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, userId, event, it, roomId, isLocalEcho, encryptedEventContent.relatesTo.eventId)
|
||||
}
|
||||
}
|
||||
} else if (encryptedEventContent?.relatesTo?.type == RelationType.REFERENCE) {
|
||||
decryptIfNeeded(event)
|
||||
|
@ -276,6 +291,90 @@ internal class DefaultEventRelationsAggregationTask @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun handleResponse(realm: Realm, userId: String, event: Event, content: MessageContent, roomId: String, isLocalEcho: Boolean, relatedEventId: String? = null) {
|
||||
val eventId = event.eventId ?: return
|
||||
val senderId = event.senderId ?: return
|
||||
val targetEventId = relatedEventId ?: content.relatesTo?.eventId ?: return
|
||||
val eventTimestamp = event.originServerTs ?: return
|
||||
|
||||
// ok, this is a poll response
|
||||
var existing = EventAnnotationsSummaryEntity.where(realm, targetEventId).findFirst()
|
||||
if (existing == null) {
|
||||
Timber.v("## POLL creating new relation summary for $targetEventId")
|
||||
existing = EventAnnotationsSummaryEntity.create(realm, roomId, targetEventId)
|
||||
}
|
||||
|
||||
// we have it
|
||||
val existingPollSummary = existing.pollResponseSummary
|
||||
?: realm.createObject(PollResponseAggregatedSummaryEntity::class.java).also {
|
||||
existing.pollResponseSummary = it
|
||||
}
|
||||
|
||||
val closedTime = existingPollSummary?.closedTime
|
||||
if (closedTime != null && eventTimestamp > closedTime) {
|
||||
Timber.v("## POLL is closed ignore event poll:$targetEventId, event :${event.eventId}")
|
||||
return
|
||||
}
|
||||
|
||||
val sumModel = ContentMapper.map(existingPollSummary?.aggregatedContent).toModel<PollSummaryContent>() ?: PollSummaryContent()
|
||||
|
||||
if (existingPollSummary!!.sourceEvents.contains(eventId)) {
|
||||
// ignore this event, we already know it (??)
|
||||
Timber.v("## POLL ignoring event for summary, it's known eventId:$eventId")
|
||||
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:$eventId")
|
||||
existingPollSummary.sourceLocalEchoEvents.remove(txId)
|
||||
existingPollSummary.sourceEvents.add(event.eventId)
|
||||
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 {
|
||||
Timber.d("## POLL Ignoring malformed response no option eventId:$eventId content: ${event.content}")
|
||||
}
|
||||
|
||||
val votes = sumModel.votes?.toMutableList() ?: ArrayList()
|
||||
val existingVoteIndex = votes.indexOfFirst { it.userId == senderId }
|
||||
if (existingVoteIndex != -1) {
|
||||
//Is the vote newer?
|
||||
val existingVote = votes[existingVoteIndex]
|
||||
if (existingVote.voteTimestamp < eventTimestamp) {
|
||||
//Take the new one
|
||||
votes[existingVoteIndex] = VoteInfo(senderId,optionIndex, eventTimestamp)
|
||||
if (userId == senderId) {
|
||||
sumModel.myVote = optionIndex
|
||||
}
|
||||
Timber.v("## POLL adding vote $optionIndex for user $senderId in poll :$relatedEventId ")
|
||||
} else {
|
||||
Timber.v("## POLL Ignoring vote (older than known one) eventId:$eventId ")
|
||||
}
|
||||
} else {
|
||||
votes.add(VoteInfo(senderId,optionIndex, eventTimestamp))
|
||||
if (userId == senderId) {
|
||||
sumModel.myVote = optionIndex
|
||||
}
|
||||
Timber.v("## POLL adding vote $optionIndex for user $senderId in poll :$relatedEventId ")
|
||||
}
|
||||
sumModel.votes = votes
|
||||
if (isLocalEcho) {
|
||||
existingPollSummary.sourceLocalEchoEvents.add(eventId)
|
||||
} else {
|
||||
existingPollSummary.sourceEvents.add(eventId)
|
||||
}
|
||||
|
||||
existingPollSummary.aggregatedContent = ContentMapper.map(sumModel.toContent())
|
||||
}
|
||||
|
||||
|
||||
|
||||
private fun handleInitialAggregatedRelations(event: Event, roomId: String, aggregation: AggregatedAnnotation, realm: Realm) {
|
||||
if (SHOULD_HANDLE_SERVER_AGREGGATION) {
|
||||
aggregation.chunk?.forEach {
|
||||
|
|
|
@ -34,6 +34,7 @@ import im.vector.matrix.android.api.session.room.model.message.MessageFileConten
|
|||
import im.vector.matrix.android.api.session.room.model.message.MessageImageInfoContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageNoticeContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageOptionsContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessagePollResponseContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageTextContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageType
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageVerificationRequestContent
|
||||
|
@ -65,6 +66,7 @@ import im.vector.riotx.features.home.room.detail.timeline.item.MessageFileItem_
|
|||
import im.vector.riotx.features.home.room.detail.timeline.item.MessageImageVideoItem
|
||||
import im.vector.riotx.features.home.room.detail.timeline.item.MessageImageVideoItem_
|
||||
import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
|
||||
import im.vector.riotx.features.home.room.detail.timeline.item.MessagePollItem_
|
||||
import im.vector.riotx.features.home.room.detail.timeline.item.MessageTextItem
|
||||
import im.vector.riotx.features.home.room.detail.timeline.item.MessageTextItem_
|
||||
import im.vector.riotx.features.home.room.detail.timeline.item.RedactedMessageItem
|
||||
|
@ -121,7 +123,7 @@ class MessageItemFactory @Inject constructor(
|
|||
if (messageContent.relatesTo?.type == RelationType.REPLACE
|
||||
|| event.isEncrypted() && event.root.content.toModel<EncryptedEventContent>()?.relatesTo?.type == RelationType.REPLACE
|
||||
) {
|
||||
// This is an edit event, we should it when debugging as a notice event
|
||||
// This is an edit event, we should display it when debugging as a notice event
|
||||
return noticeItemFactory.create(event, highlight, callback)
|
||||
}
|
||||
val attributes = messageItemAttributesFactory.create(messageContent, informationData, callback)
|
||||
|
@ -138,7 +140,8 @@ class MessageItemFactory @Inject constructor(
|
|||
is MessageAudioContent -> buildAudioMessageItem(messageContent, informationData, highlight, callback, attributes)
|
||||
is MessageVerificationRequestContent -> buildVerificationRequestMessageItem(messageContent, informationData, highlight, callback, attributes)
|
||||
is MessageOptionsContent -> buildPollMessageItem(messageContent, informationData, highlight, callback, attributes)
|
||||
else -> buildNotHandledMessageItem(messageContent, informationData, highlight, callback)
|
||||
is MessagePollResponseContent -> noticeItemFactory.create(event, highlight, callback)
|
||||
else -> buildNotHandledMessageItem(messageContent, informationData, highlight, callback, attributes)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -30,6 +30,7 @@ import im.vector.riotx.core.extensions.localDateTime
|
|||
import im.vector.riotx.core.resources.ColorProvider
|
||||
import im.vector.riotx.core.utils.getColorFromUserId
|
||||
import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
|
||||
import im.vector.riotx.features.home.room.detail.timeline.item.PollResponseData
|
||||
import im.vector.riotx.features.home.room.detail.timeline.item.ReactionInfoData
|
||||
import im.vector.riotx.features.home.room.detail.timeline.item.ReadReceiptData
|
||||
import im.vector.riotx.features.home.room.detail.timeline.item.ReferencesInfoData
|
||||
|
@ -82,6 +83,15 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses
|
|||
?.map {
|
||||
ReactionInfoData(it.key, it.count, it.addedByMe, it.localEchoEvents.isEmpty())
|
||||
},
|
||||
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 }
|
||||
)
|
||||
},
|
||||
hasBeenEdited = event.hasBeenEdited(),
|
||||
hasPendingEdits = event.annotations?.editSummary?.localEchos?.any() ?: false,
|
||||
readReceipts = event.readReceipts
|
||||
|
|
|
@ -34,6 +34,8 @@ data class MessageInformationData(
|
|||
val showInformation: Boolean = true,
|
||||
/*List of reactions (emoji,count,isSelected)*/
|
||||
val orderedReactionList: List<ReactionInfoData>? = null,
|
||||
val pollResponseAggregatedSummary: PollResponseData? = null,
|
||||
|
||||
val hasBeenEdited: Boolean = false,
|
||||
val hasPendingEdits: Boolean = false,
|
||||
val readReceipts: List<ReadReceiptData> = emptyList(),
|
||||
|
@ -66,4 +68,11 @@ data class ReadReceiptData(
|
|||
val timestamp: Long
|
||||
) : Parcelable
|
||||
|
||||
@Parcelize
|
||||
data class PollResponseData(
|
||||
val myVote: Int?,
|
||||
val votes: Map<Int, Int>?,
|
||||
val isClosed: Boolean = false
|
||||
) : Parcelable
|
||||
|
||||
fun ReadReceiptData.toMatrixItem() = MatrixItem.UserItem(userId, displayName, avatarUrl)
|
||||
|
|
|
@ -28,6 +28,7 @@ import im.vector.riotx.core.extensions.setTextOrHide
|
|||
import im.vector.riotx.core.utils.DebouncedClickListener
|
||||
import im.vector.riotx.features.home.room.detail.RoomDetailAction
|
||||
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@EpoxyModelClass(layout = R.layout.item_timeline_event_base)
|
||||
abstract class MessagePollItem : AbsMessageItem<MessagePollItem.Holder>() {
|
||||
|
@ -55,32 +56,51 @@ abstract class MessagePollItem : AbsMessageItem<MessagePollItem.Holder>() {
|
|||
holder.labelText.setTextOrHide(optionsContent?.label)
|
||||
|
||||
val buttons = listOf(holder.button1, holder.button2, holder.button3, holder.button4, holder.button5)
|
||||
|
||||
buttons.forEach { it.isVisible = false }
|
||||
|
||||
optionsContent?.options?.forEachIndexed { index, item ->
|
||||
if (index < buttons.size) {
|
||||
buttons[index].let {
|
||||
it.text = item.label
|
||||
it.isVisible = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val resultLines = listOf(holder.result1, holder.result2, holder.result3, holder.result4, holder.result5)
|
||||
|
||||
buttons.forEach { it.isVisible = false }
|
||||
resultLines.forEach { it.isVisible = false }
|
||||
optionsContent?.options?.forEachIndexed { index, item ->
|
||||
if (index < resultLines.size) {
|
||||
resultLines[index].let {
|
||||
it.label = item.label
|
||||
it.optionSelected = index == 0
|
||||
it.percent = "20%"
|
||||
it.isVisible = true
|
||||
|
||||
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
|
||||
optionsContent?.options?.forEachIndexed { index, item ->
|
||||
if (index < buttons.size) {
|
||||
buttons[index].let {
|
||||
it.text = item.label
|
||||
it.isVisible = true
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val maxCount = votes?.maxBy { 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, 0, 0)
|
||||
holder.infoText.text = holder.view.context.resources.getQuantityString(R.plurals.poll_info, totalVotes, totalVotes)
|
||||
}
|
||||
|
||||
override fun unbind(holder: Holder) {
|
||||
|
@ -93,7 +113,7 @@ abstract class MessagePollItem : AbsMessageItem<MessagePollItem.Holder>() {
|
|||
class Holder : AbsMessageItem.Holder(STUB_ID) {
|
||||
|
||||
var pollId: String? = null
|
||||
var optionValues : List<String?>? = null
|
||||
var optionValues: List<String?>? = null
|
||||
var callback: TimelineEventController.Callback? = null
|
||||
|
||||
val button1 by bind<Button>(R.id.pollButton1)
|
||||
|
@ -118,7 +138,8 @@ abstract class MessagePollItem : AbsMessageItem<MessagePollItem.Holder>() {
|
|||
val optionIndex = buttons.indexOf(it)
|
||||
if (optionIndex != -1 && pollId != null) {
|
||||
val compatValue = if (optionIndex < optionValues?.size ?: 0) optionValues?.get(optionIndex) else null
|
||||
callback?.onAction(RoomDetailAction.ReplyToPoll(pollId!!, optionIndex, compatValue ?: "$optionIndex"))
|
||||
callback?.onTimelineItemAction(RoomDetailAction.ReplyToPoll(pollId!!, optionIndex, compatValue
|
||||
?: "$optionIndex"))
|
||||
}
|
||||
})
|
||||
buttons.forEach { it.setOnClickListener(clickListener) }
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
package im.vector.riotx.features.home.room.detail.timeline.item
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Typeface
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
|
@ -25,6 +26,7 @@ import butterknife.BindView
|
|||
import butterknife.ButterKnife
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.extensions.setTextOrHide
|
||||
import im.vector.riotx.features.themes.ThemeUtils
|
||||
|
||||
class PollResultLineView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
|
@ -59,6 +61,15 @@ class PollResultLineView @JvmOverloads constructor(
|
|||
selectedIcon.visibility = if (value) View.VISIBLE else View.INVISIBLE
|
||||
}
|
||||
|
||||
var isWinner: Boolean = false
|
||||
set(value) {
|
||||
field = value
|
||||
// Text in main color
|
||||
labelTextView.setTypeface(labelTextView.getTypeface(), if (value) Typeface.BOLD else Typeface.NORMAL)
|
||||
percentTextView.setTypeface(percentTextView.getTypeface(), if (value) Typeface.BOLD else Typeface.NORMAL)
|
||||
}
|
||||
|
||||
|
||||
init {
|
||||
inflate(context, R.layout.item_timeline_event_poll_result_item, this)
|
||||
orientation = HORIZONTAL
|
||||
|
@ -68,6 +79,7 @@ class PollResultLineView @JvmOverloads constructor(
|
|||
R.styleable.PollResultLineView, 0, 0)
|
||||
label = typedArray.getString(R.styleable.PollResultLineView_optionName) ?: ""
|
||||
percent = typedArray.getString(R.styleable.PollResultLineView_optionCount) ?: ""
|
||||
optionSelected = typedArray.getBoolean(R.styleable.PollResultLineView_optionSelected, false)
|
||||
optionSelected = typedArray.getBoolean(R.styleable.PollResultLineView_optionSelected, false)
|
||||
isWinner = typedArray.getBoolean(R.styleable.PollResultLineView_optionIsWinner, false)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,7 +25,6 @@
|
|||
android:layout_weight="1"
|
||||
android:textColor="?riotx_text_primary"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="bold"
|
||||
tools:text="Open a Github Issue" />
|
||||
|
||||
|
||||
|
@ -36,6 +35,5 @@
|
|||
android:layout_gravity="center_vertical"
|
||||
android:textColor="?riotx_text_primary"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="bold"
|
||||
tools:text="47%" />
|
||||
</merge>
|
|
@ -67,6 +67,7 @@
|
|||
tools:optionName="Search Github"
|
||||
tools:optionCount="60%"
|
||||
tools:optionSelected="false"
|
||||
tools:optionIsWinner="true"
|
||||
/>
|
||||
|
||||
<im.vector.riotx.features.home.room.detail.timeline.item.PollResultLineView
|
||||
|
|
|
@ -102,6 +102,7 @@
|
|||
<attr name="optionName" format="string" localization="suggested" />
|
||||
<attr name="optionCount" format="string" />
|
||||
<attr name="optionSelected" format="boolean" />
|
||||
<attr name="optionIsWinner" format="boolean" />
|
||||
</declare-styleable>
|
||||
|
||||
</resources>
|
||||
|
|
Loading…
Reference in a new issue