mirror of
https://github.com/vector-im/element-android.git
synced 2024-11-15 01:35:07 +08:00
Merge pull request #810 from vector-im/feature/msc_2192
Feature/msc 2192
This commit is contained in:
commit
e1342d096b
@ -2,7 +2,7 @@ Changes in RiotX 0.16.0 (2020-XX-XX)
|
||||
===================================================
|
||||
|
||||
Features ✨:
|
||||
-
|
||||
- Polls and Bot Buttons (MSC 2192 matrix-org/matrix-doc#2192)
|
||||
|
||||
Improvements 🙌:
|
||||
- Show confirmation dialog before deleting a message (#967)
|
||||
|
@ -19,11 +19,12 @@ package im.vector.matrix.android.api.session.events.model
|
||||
* Constants defining known event relation types from Matrix specifications
|
||||
*/
|
||||
object RelationType {
|
||||
|
||||
/** Lets you define an event which annotates an existing event.*/
|
||||
const val ANNOTATION = "m.annotation"
|
||||
/** Lets you define an event which replaces an existing event.*/
|
||||
const val REPLACE = "m.replace"
|
||||
/** Lets you define an event which references an existing event.*/
|
||||
const val REFERENCE = "m.reference"
|
||||
/** Lets you define an event which adds a response to an existing event.*/
|
||||
const val RESPONSE = "m.response"
|
||||
}
|
||||
|
@ -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
|
||||
)
|
@ -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.api.session.room.model.message
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import im.vector.matrix.android.api.session.events.model.Content
|
||||
import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent
|
||||
|
||||
// Possible values for optionType
|
||||
const val OPTION_TYPE_POLL = "m.pool"
|
||||
const val OPTION_TYPE_BUTTONS = "m.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 = "msgtype") 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
|
@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright (c) 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.message
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import im.vector.matrix.android.api.session.events.model.Content
|
||||
import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent
|
||||
|
||||
/**
|
||||
* Ref: https://github.com/matrix-org/matrix-doc/pull/2192
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class MessagePollResponseContent(
|
||||
@Json(name = "msgtype") override val msgType: String = MessageType.MSGTYPE_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
|
||||
) : MessageContent
|
@ -25,6 +25,9 @@ object MessageType {
|
||||
const val MSGTYPE_VIDEO = "m.video"
|
||||
const val MSGTYPE_LOCATION = "m.location"
|
||||
const val MSGTYPE_FILE = "m.file"
|
||||
const val MSGTYPE_OPTIONS = "m.options"
|
||||
const val MSGTYPE_RESPONSE = "m.response"
|
||||
const val MSGTYPE_POLL_CLOSED = "m.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
|
||||
|
@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright (c) 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.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?
|
||||
)
|
@ -25,5 +25,6 @@ data class ReactionInfo(
|
||||
@Json(name = "event_id") override val eventId: String,
|
||||
val key: String,
|
||||
// always null for reaction
|
||||
@Json(name = "m.in_reply_to") override val inReplyTo: ReplyToContent? = null
|
||||
@Json(name = "m.in_reply_to") override val inReplyTo: ReplyToContent? = null,
|
||||
@Json(name = "option") override val option: Int? = null
|
||||
) : RelationContent
|
||||
|
@ -23,4 +23,5 @@ interface RelationContent {
|
||||
val type: String?
|
||||
val eventId: String?
|
||||
val inReplyTo: ReplyToContent?
|
||||
val option: Int?
|
||||
}
|
||||
|
@ -22,5 +22,6 @@ import com.squareup.moshi.JsonClass
|
||||
data class RelationDefaultContent(
|
||||
@Json(name = "rel_type") override val type: String?,
|
||||
@Json(name = "event_id") override val eventId: String?,
|
||||
@Json(name = "m.in_reply_to") override val inReplyTo: ReplyToContent? = null
|
||||
@Json(name = "m.in_reply_to") override val inReplyTo: ReplyToContent? = null,
|
||||
@Json(name = "option") override val option: Int? = null
|
||||
) : RelationContent
|
||||
|
@ -19,6 +19,7 @@ package im.vector.matrix.android.api.session.room.send
|
||||
import im.vector.matrix.android.api.session.content.ContentAttachmentData
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageType
|
||||
import im.vector.matrix.android.api.session.room.model.message.OptionItem
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||
import im.vector.matrix.android.api.util.Cancelable
|
||||
|
||||
@ -62,7 +63,24 @@ interface SendService {
|
||||
fun sendMedias(attachments: List<ContentAttachmentData>): Cancelable
|
||||
|
||||
/**
|
||||
* Redacts (delete) the given event.
|
||||
* Send a poll to the room.
|
||||
* @param question the question
|
||||
* @param options list of (label, value)
|
||||
* @return a [Cancelable]
|
||||
*/
|
||||
fun sendPoll(question: String, options: List<OptionItem>): Cancelable
|
||||
|
||||
/**
|
||||
* 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)
|
||||
* @return a [Cancelable]
|
||||
*/
|
||||
fun sendOptionsReply(pollEventId: String, optionIndex: Int, optionValue: String): Cancelable
|
||||
|
||||
/**
|
||||
* Redact (delete) the given event.
|
||||
* @param event The event to redact
|
||||
* @param reason Optional reason string
|
||||
*/
|
||||
|
@ -55,7 +55,11 @@ internal object EventAnnotationsSummaryMapper {
|
||||
it.sourceEvents.toList(),
|
||||
it.sourceLocalEcho.toList()
|
||||
)
|
||||
},
|
||||
pollResponseSummary = annotationsSummary.pollResponseSummary?.let {
|
||||
PollResponseAggregatedSummaryEntityMapper.map(it)
|
||||
}
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
@ -93,6 +97,9 @@ internal object EventAnnotationsSummaryMapper {
|
||||
RealmList<String>().apply { addAll(it.localEchos) }
|
||||
)
|
||||
}
|
||||
eventAnnotationsSummaryEntity.pollResponseSummary = annotationsSummary.pollResponseSummary?.let {
|
||||
PollResponseAggregatedSummaryEntityMapper.map(it)
|
||||
}
|
||||
return eventAnnotationsSummaryEntity
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,50 @@
|
||||
/*
|
||||
* 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.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.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 a poll
|
||||
*/
|
||||
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"*}"""
|
||||
}
|
||||
|
@ -17,8 +17,15 @@
|
||||
package im.vector.matrix.android.internal.database.query
|
||||
|
||||
import im.vector.matrix.android.api.session.room.send.SendState
|
||||
import im.vector.matrix.android.internal.database.model.*
|
||||
import io.realm.*
|
||||
import im.vector.matrix.android.internal.database.model.ChunkEntity
|
||||
import im.vector.matrix.android.internal.database.model.RoomEntity
|
||||
import im.vector.matrix.android.internal.database.model.TimelineEventEntity
|
||||
import im.vector.matrix.android.internal.database.model.TimelineEventEntityFields
|
||||
import io.realm.Realm
|
||||
import io.realm.RealmList
|
||||
import io.realm.RealmQuery
|
||||
import io.realm.RealmResults
|
||||
import io.realm.Sort
|
||||
import io.realm.kotlin.where
|
||||
|
||||
internal fun TimelineEventEntity.Companion.where(realm: Realm, roomId: String, eventId: String): RealmQuery<TimelineEventEntity> {
|
||||
@ -48,10 +55,16 @@ internal fun TimelineEventEntity.Companion.findWithSenderMembershipEvent(realm:
|
||||
internal fun TimelineEventEntity.Companion.latestEvent(realm: Realm,
|
||||
roomId: String,
|
||||
includesSending: Boolean,
|
||||
filterContentRelation: Boolean = false,
|
||||
filterTypes: List<String> = emptyList()): TimelineEventEntity? {
|
||||
val roomEntity = RoomEntity.where(realm, roomId).findFirst() ?: return null
|
||||
val sendingTimelineEvents = roomEntity.sendingTimelineEvents.where().filterTypes(filterTypes)
|
||||
val liveEvents = ChunkEntity.findLastLiveChunkFromRoom(realm, roomId)?.timelineEvents?.where()?.filterTypes(filterTypes)
|
||||
if (filterContentRelation) {
|
||||
liveEvents
|
||||
?.not()?.like(TimelineEventEntityFields.ROOT.CONTENT, FilterContent.EDIT_TYPE)
|
||||
?.not()?.like(TimelineEventEntityFields.ROOT.CONTENT, FilterContent.RESPONSE_TYPE)
|
||||
}
|
||||
val query = if (includesSending && sendingTimelineEvents.findAll().isNotEmpty()) {
|
||||
sendingTimelineEvents
|
||||
} else {
|
||||
|
@ -47,6 +47,8 @@ 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)
|
||||
)
|
||||
.add(SerializeNulls.JSON_ADAPTER_FACTORY)
|
||||
.build()
|
||||
|
@ -25,8 +25,11 @@ import im.vector.matrix.android.api.session.events.model.LocalEcho
|
||||
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.PollSummaryContent
|
||||
import im.vector.matrix.android.api.session.room.model.ReferencesAggregatedContent
|
||||
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.MessagePollResponseContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageRelationContent
|
||||
import im.vector.matrix.android.api.session.room.model.relation.ReactionContent
|
||||
import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult
|
||||
@ -36,6 +39,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 +127,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 +151,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 +290,94 @@ 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 {
|
||||
@ -459,7 +561,7 @@ internal class DefaultEventRelationsAggregationTask @Inject constructor(
|
||||
EventType.KEY_VERIFICATION_ACCEPT -> {
|
||||
updateVerificationState(currentState, VerificationState.WAITING)
|
||||
}
|
||||
EventType.KEY_VERIFICATION_READY -> {
|
||||
EventType.KEY_VERIFICATION_READY -> {
|
||||
updateVerificationState(currentState, VerificationState.WAITING)
|
||||
}
|
||||
EventType.KEY_VERIFICATION_KEY -> {
|
||||
|
@ -102,7 +102,8 @@ internal class RoomSummaryUpdater @Inject constructor(
|
||||
roomSummaryEntity.membership = membership
|
||||
}
|
||||
|
||||
val latestPreviewableEvent = TimelineEventEntity.latestEvent(realm, roomId, includesSending = true, filterTypes = PREVIEWABLE_TYPES)
|
||||
val latestPreviewableEvent = TimelineEventEntity.latestEvent(realm, roomId, includesSending = true,
|
||||
filterTypes = PREVIEWABLE_TYPES, filterContentRelation = true)
|
||||
|
||||
val lastTopicEvent = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_TOPIC, stateKey = "")?.root
|
||||
val lastCanonicalAliasEvent = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_CANONICAL_ALIAS, stateKey = "")?.root
|
||||
|
@ -28,6 +28,7 @@ import im.vector.matrix.android.api.session.crypto.CryptoService
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.api.session.events.model.isImageMessage
|
||||
import im.vector.matrix.android.api.session.events.model.isTextMessage
|
||||
import im.vector.matrix.android.api.session.room.model.message.OptionItem
|
||||
import im.vector.matrix.android.api.session.room.send.SendService
|
||||
import im.vector.matrix.android.api.session.room.send.SendState
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||
@ -80,7 +81,20 @@ internal class DefaultSendService @AssistedInject constructor(
|
||||
val event = localEchoEventFactory.createFormattedTextEvent(roomId, TextContent(text, formattedText), msgType).also {
|
||||
createLocalEcho(it)
|
||||
}
|
||||
return sendEvent(event)
|
||||
}
|
||||
|
||||
override fun sendPoll(question: String, options: List<OptionItem>): Cancelable {
|
||||
val event = localEchoEventFactory.createPollEvent(roomId, question, options).also {
|
||||
createLocalEcho(it)
|
||||
}
|
||||
return sendEvent(event)
|
||||
}
|
||||
|
||||
override fun sendOptionsReply(pollEventId: String, optionIndex: Int, optionValue: String): Cancelable {
|
||||
val event = localEchoEventFactory.createOptionsReplyEvent(roomId, pollEventId, optionIndex, optionValue).also {
|
||||
createLocalEcho(it)
|
||||
}
|
||||
return sendEvent(event)
|
||||
}
|
||||
|
||||
@ -158,123 +172,123 @@ internal class DefaultSendService @AssistedInject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
override fun clearSendingQueue() {
|
||||
timelineSendEventWorkCommon.cancelAllWorks(roomId)
|
||||
workManagerProvider.workManager.cancelUniqueWork(buildWorkName(UPLOAD_WORK))
|
||||
override fun clearSendingQueue() {
|
||||
timelineSendEventWorkCommon.cancelAllWorks(roomId)
|
||||
workManagerProvider.workManager.cancelUniqueWork(buildWorkName(UPLOAD_WORK))
|
||||
|
||||
// Replace the worker chains with a AlwaysSuccessfulWorker, to ensure the queues are well emptied
|
||||
workManagerProvider.matrixOneTimeWorkRequestBuilder<AlwaysSuccessfulWorker>()
|
||||
.build().let {
|
||||
timelineSendEventWorkCommon.postWork(roomId, it, ExistingWorkPolicy.REPLACE)
|
||||
// Replace the worker chains with a AlwaysSuccessfulWorker, to ensure the queues are well emptied
|
||||
workManagerProvider.matrixOneTimeWorkRequestBuilder<AlwaysSuccessfulWorker>()
|
||||
.build().let {
|
||||
timelineSendEventWorkCommon.postWork(roomId, it, ExistingWorkPolicy.REPLACE)
|
||||
|
||||
// need to clear also image sending queue
|
||||
workManagerProvider.workManager
|
||||
.beginUniqueWork(buildWorkName(UPLOAD_WORK), ExistingWorkPolicy.REPLACE, it)
|
||||
.enqueue()
|
||||
}
|
||||
taskExecutor.executorScope.launch {
|
||||
localEchoRepository.clearSendingQueue(roomId)
|
||||
}
|
||||
}
|
||||
|
||||
override fun resendAllFailedMessages() {
|
||||
taskExecutor.executorScope.launch {
|
||||
val eventsToResend = localEchoRepository.getAllFailedEventsToResend(roomId)
|
||||
eventsToResend.forEach {
|
||||
sendEvent(it)
|
||||
// need to clear also image sending queue
|
||||
workManagerProvider.workManager
|
||||
.beginUniqueWork(buildWorkName(UPLOAD_WORK), ExistingWorkPolicy.REPLACE, it)
|
||||
.enqueue()
|
||||
}
|
||||
localEchoRepository.updateSendState(roomId, eventsToResend.mapNotNull { it.eventId }, SendState.UNSENT)
|
||||
}
|
||||
}
|
||||
|
||||
override fun sendMedia(attachment: ContentAttachmentData): Cancelable {
|
||||
// Create an event with the media file path
|
||||
val event = localEchoEventFactory.createMediaEvent(roomId, attachment).also {
|
||||
createLocalEcho(it)
|
||||
}
|
||||
return internalSendMedia(event, attachment)
|
||||
}
|
||||
|
||||
private fun internalSendMedia(localEcho: Event, attachment: ContentAttachmentData): Cancelable {
|
||||
val isRoomEncrypted = cryptoService.isRoomEncrypted(roomId)
|
||||
|
||||
val uploadWork = createUploadMediaWork(localEcho, attachment, isRoomEncrypted, startChain = true)
|
||||
val sendWork = createSendEventWork(localEcho, false)
|
||||
|
||||
if (isRoomEncrypted) {
|
||||
val encryptWork = createEncryptEventWork(localEcho, false /*not start of chain, take input error*/)
|
||||
|
||||
val op: Operation = workManagerProvider.workManager
|
||||
.beginUniqueWork(buildWorkName(UPLOAD_WORK), ExistingWorkPolicy.APPEND, uploadWork)
|
||||
.then(encryptWork)
|
||||
.then(sendWork)
|
||||
.enqueue()
|
||||
op.result.addListener(Runnable {
|
||||
if (op.result.isCancelled) {
|
||||
Timber.e("CHAIN WAS CANCELLED")
|
||||
} else if (op.state.value is Operation.State.FAILURE) {
|
||||
Timber.e("CHAIN DID FAIL")
|
||||
}
|
||||
}, workerFutureListenerExecutor)
|
||||
} else {
|
||||
workManagerProvider.workManager
|
||||
.beginUniqueWork(buildWorkName(UPLOAD_WORK), ExistingWorkPolicy.APPEND, uploadWork)
|
||||
.then(sendWork)
|
||||
.enqueue()
|
||||
}
|
||||
|
||||
return CancelableWork(workManagerProvider.workManager, sendWork.id)
|
||||
}
|
||||
|
||||
private fun createLocalEcho(event: Event) {
|
||||
localEchoEventFactory.createLocalEcho(event)
|
||||
}
|
||||
|
||||
private fun buildWorkName(identifier: String): String {
|
||||
return "${roomId}_$identifier"
|
||||
}
|
||||
|
||||
private fun createEncryptEventWork(event: Event, startChain: Boolean): OneTimeWorkRequest {
|
||||
// Same parameter
|
||||
val params = EncryptEventWorker.Params(sessionId, roomId, event)
|
||||
val sendWorkData = WorkerParamsFactory.toData(params)
|
||||
|
||||
return workManagerProvider.matrixOneTimeWorkRequestBuilder<EncryptEventWorker>()
|
||||
.setConstraints(WorkManagerProvider.workConstraints)
|
||||
.setInputData(sendWorkData)
|
||||
.startChain(startChain)
|
||||
.setBackoffCriteria(BackoffPolicy.LINEAR, BACKOFF_DELAY, TimeUnit.MILLISECONDS)
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun createSendEventWork(event: Event, startChain: Boolean): OneTimeWorkRequest {
|
||||
val sendContentWorkerParams = SendEventWorker.Params(sessionId, roomId, event)
|
||||
val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams)
|
||||
|
||||
return timelineSendEventWorkCommon.createWork<SendEventWorker>(sendWorkData, startChain)
|
||||
}
|
||||
|
||||
private fun createRedactEventWork(event: Event, reason: String?): OneTimeWorkRequest {
|
||||
val redactEvent = localEchoEventFactory.createRedactEvent(roomId, event.eventId!!, reason).also {
|
||||
createLocalEcho(it)
|
||||
}
|
||||
val sendContentWorkerParams = RedactEventWorker.Params(sessionId, redactEvent.eventId!!, roomId, event.eventId, reason)
|
||||
val redactWorkData = WorkerParamsFactory.toData(sendContentWorkerParams)
|
||||
return timelineSendEventWorkCommon.createWork<RedactEventWorker>(redactWorkData, true)
|
||||
}
|
||||
|
||||
private fun createUploadMediaWork(event: Event,
|
||||
attachment: ContentAttachmentData,
|
||||
isRoomEncrypted: Boolean,
|
||||
startChain: Boolean): OneTimeWorkRequest {
|
||||
val uploadMediaWorkerParams = UploadContentWorker.Params(sessionId, roomId, event, attachment, isRoomEncrypted)
|
||||
val uploadWorkData = WorkerParamsFactory.toData(uploadMediaWorkerParams)
|
||||
|
||||
return workManagerProvider.matrixOneTimeWorkRequestBuilder<UploadContentWorker>()
|
||||
.setConstraints(WorkManagerProvider.workConstraints)
|
||||
.startChain(startChain)
|
||||
.setInputData(uploadWorkData)
|
||||
.setBackoffCriteria(BackoffPolicy.LINEAR, BACKOFF_DELAY, TimeUnit.MILLISECONDS)
|
||||
.build()
|
||||
taskExecutor.executorScope.launch {
|
||||
localEchoRepository.clearSendingQueue(roomId)
|
||||
}
|
||||
}
|
||||
|
||||
override fun resendAllFailedMessages() {
|
||||
taskExecutor.executorScope.launch {
|
||||
val eventsToResend = localEchoRepository.getAllFailedEventsToResend(roomId)
|
||||
eventsToResend.forEach {
|
||||
sendEvent(it)
|
||||
}
|
||||
localEchoRepository.updateSendState(roomId, eventsToResend.mapNotNull { it.eventId }, SendState.UNSENT)
|
||||
}
|
||||
}
|
||||
|
||||
override fun sendMedia(attachment: ContentAttachmentData): Cancelable {
|
||||
// Create an event with the media file path
|
||||
val event = localEchoEventFactory.createMediaEvent(roomId, attachment).also {
|
||||
createLocalEcho(it)
|
||||
}
|
||||
return internalSendMedia(event, attachment)
|
||||
}
|
||||
|
||||
private fun internalSendMedia(localEcho: Event, attachment: ContentAttachmentData): Cancelable {
|
||||
val isRoomEncrypted = cryptoService.isRoomEncrypted(roomId)
|
||||
|
||||
val uploadWork = createUploadMediaWork(localEcho, attachment, isRoomEncrypted, startChain = true)
|
||||
val sendWork = createSendEventWork(localEcho, false)
|
||||
|
||||
if (isRoomEncrypted) {
|
||||
val encryptWork = createEncryptEventWork(localEcho, false /*not start of chain, take input error*/)
|
||||
|
||||
val op: Operation = workManagerProvider.workManager
|
||||
.beginUniqueWork(buildWorkName(UPLOAD_WORK), ExistingWorkPolicy.APPEND, uploadWork)
|
||||
.then(encryptWork)
|
||||
.then(sendWork)
|
||||
.enqueue()
|
||||
op.result.addListener(Runnable {
|
||||
if (op.result.isCancelled) {
|
||||
Timber.e("CHAIN WAS CANCELLED")
|
||||
} else if (op.state.value is Operation.State.FAILURE) {
|
||||
Timber.e("CHAIN DID FAIL")
|
||||
}
|
||||
}, workerFutureListenerExecutor)
|
||||
} else {
|
||||
workManagerProvider.workManager
|
||||
.beginUniqueWork(buildWorkName(UPLOAD_WORK), ExistingWorkPolicy.APPEND, uploadWork)
|
||||
.then(sendWork)
|
||||
.enqueue()
|
||||
}
|
||||
|
||||
return CancelableWork(workManagerProvider.workManager, sendWork.id)
|
||||
}
|
||||
|
||||
private fun createLocalEcho(event: Event) {
|
||||
localEchoEventFactory.createLocalEcho(event)
|
||||
}
|
||||
|
||||
private fun buildWorkName(identifier: String): String {
|
||||
return "${roomId}_$identifier"
|
||||
}
|
||||
|
||||
private fun createEncryptEventWork(event: Event, startChain: Boolean): OneTimeWorkRequest {
|
||||
// Same parameter
|
||||
val params = EncryptEventWorker.Params(sessionId, roomId, event)
|
||||
val sendWorkData = WorkerParamsFactory.toData(params)
|
||||
|
||||
return workManagerProvider.matrixOneTimeWorkRequestBuilder<EncryptEventWorker>()
|
||||
.setConstraints(WorkManagerProvider.workConstraints)
|
||||
.setInputData(sendWorkData)
|
||||
.startChain(startChain)
|
||||
.setBackoffCriteria(BackoffPolicy.LINEAR, BACKOFF_DELAY, TimeUnit.MILLISECONDS)
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun createSendEventWork(event: Event, startChain: Boolean): OneTimeWorkRequest {
|
||||
val sendContentWorkerParams = SendEventWorker.Params(sessionId, roomId, event)
|
||||
val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams)
|
||||
|
||||
return timelineSendEventWorkCommon.createWork<SendEventWorker>(sendWorkData, startChain)
|
||||
}
|
||||
|
||||
private fun createRedactEventWork(event: Event, reason: String?): OneTimeWorkRequest {
|
||||
val redactEvent = localEchoEventFactory.createRedactEvent(roomId, event.eventId!!, reason).also {
|
||||
createLocalEcho(it)
|
||||
}
|
||||
val sendContentWorkerParams = RedactEventWorker.Params(sessionId, redactEvent.eventId!!, roomId, event.eventId, reason)
|
||||
val redactWorkData = WorkerParamsFactory.toData(sendContentWorkerParams)
|
||||
return timelineSendEventWorkCommon.createWork<RedactEventWorker>(redactWorkData, true)
|
||||
}
|
||||
|
||||
private fun createUploadMediaWork(event: Event,
|
||||
attachment: ContentAttachmentData,
|
||||
isRoomEncrypted: Boolean,
|
||||
startChain: Boolean): OneTimeWorkRequest {
|
||||
val uploadMediaWorkerParams = UploadContentWorker.Params(sessionId, roomId, event, attachment, isRoomEncrypted)
|
||||
val uploadWorkData = WorkerParamsFactory.toData(uploadMediaWorkerParams)
|
||||
|
||||
return workManagerProvider.matrixOneTimeWorkRequestBuilder<UploadContentWorker>()
|
||||
.setConstraints(WorkManagerProvider.workConstraints)
|
||||
.startChain(startChain)
|
||||
.setInputData(uploadWorkData)
|
||||
.setBackoffCriteria(BackoffPolicy.LINEAR, BACKOFF_DELAY, TimeUnit.MILLISECONDS)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
@ -36,10 +36,14 @@ import im.vector.matrix.android.api.session.room.model.message.MessageContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageFileContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageFormat
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageImageContent
|
||||
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
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageVideoContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.OPTION_TYPE_POLL
|
||||
import im.vector.matrix.android.api.session.room.model.message.OptionItem
|
||||
import im.vector.matrix.android.api.session.room.model.message.ThumbnailInfo
|
||||
import im.vector.matrix.android.api.session.room.model.message.VideoInfo
|
||||
import im.vector.matrix.android.api.session.room.model.message.isReply
|
||||
@ -132,6 +136,43 @@ internal class LocalEchoEventFactory @Inject constructor(
|
||||
))
|
||||
}
|
||||
|
||||
fun createOptionsReplyEvent(roomId: String,
|
||||
pollEventId: String,
|
||||
optionIndex: Int,
|
||||
optionLabel: String): Event {
|
||||
return createEvent(roomId,
|
||||
MessagePollResponseContent(
|
||||
body = optionLabel,
|
||||
relatesTo = RelationDefaultContent(
|
||||
type = RelationType.RESPONSE,
|
||||
option = optionIndex,
|
||||
eventId = pollEventId)
|
||||
|
||||
))
|
||||
}
|
||||
|
||||
fun createPollEvent(roomId: String,
|
||||
question: String,
|
||||
options: List<OptionItem>): Event {
|
||||
val compatLabel = buildString {
|
||||
append("[Poll] ")
|
||||
append(question)
|
||||
options.forEach {
|
||||
append("\n")
|
||||
append(it.value)
|
||||
}
|
||||
}
|
||||
return createEvent(
|
||||
roomId,
|
||||
MessageOptionsContent(
|
||||
body = compatLabel,
|
||||
label = question,
|
||||
optionType = OPTION_TYPE_POLL,
|
||||
options = options.toList()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun createReplaceTextOfReply(roomId: String,
|
||||
eventReplaced: TimelineEvent,
|
||||
originalEvent: TimelineEvent,
|
||||
|
@ -725,6 +725,7 @@ internal class DefaultTimeline(
|
||||
}
|
||||
if (settings.filterEdits) {
|
||||
not().like(TimelineEventEntityFields.ROOT.CONTENT, FilterContent.EDIT_TYPE)
|
||||
not().like(TimelineEventEntityFields.ROOT.CONTENT, FilterContent.RESPONSE_TYPE)
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
@ -157,6 +157,8 @@ internal class TimelineHiddenReadReceipts constructor(private val readReceiptsSu
|
||||
}
|
||||
if (settings.filterEdits) {
|
||||
like("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.CONTENT}", FilterContent.EDIT_TYPE)
|
||||
or()
|
||||
like("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.CONTENT}", FilterContent.RESPONSE_TYPE)
|
||||
}
|
||||
endGroup()
|
||||
return this
|
||||
|
@ -41,6 +41,7 @@ enum class Command(val command: String, val parameters: String, @StringRes val d
|
||||
RAINBOW_EMOTE("/rainbowme", "<message>", R.string.command_description_rainbow_emote),
|
||||
CLEAR_SCALAR_TOKEN("/clear_scalar_token", "", R.string.command_description_clear_scalar_token),
|
||||
SPOILER("/spoiler", "<message>", R.string.command_description_spoiler),
|
||||
POLL("/poll", "Question | Option 1 | Option 2 ...", R.string.command_description_poll),
|
||||
SHRUG("/shrug", "<message>", R.string.command_description_shrug),
|
||||
// TODO temporary command
|
||||
VERIFY_USER("/verify", "<user-id>", R.string.command_description_verify);
|
||||
|
@ -80,12 +80,12 @@ object CommandParser {
|
||||
|
||||
ParsedCommand.SendEmote(message)
|
||||
}
|
||||
Command.RAINBOW.command -> {
|
||||
Command.RAINBOW.command -> {
|
||||
val message = textMessage.subSequence(Command.RAINBOW.command.length, textMessage.length).trim()
|
||||
|
||||
ParsedCommand.SendRainbow(message)
|
||||
}
|
||||
Command.RAINBOW_EMOTE.command -> {
|
||||
Command.RAINBOW_EMOTE.command -> {
|
||||
val message = textMessage.subSequence(Command.RAINBOW_EMOTE.command.length, textMessage.length).trim()
|
||||
|
||||
ParsedCommand.SendRainbowEmote(message)
|
||||
@ -251,7 +251,6 @@ object CommandParser {
|
||||
}
|
||||
Command.SPOILER.command -> {
|
||||
val message = textMessage.substring(Command.SPOILER.command.length).trim()
|
||||
|
||||
ParsedCommand.SendSpoiler(message)
|
||||
}
|
||||
Command.SHRUG.command -> {
|
||||
@ -259,12 +258,20 @@ object CommandParser {
|
||||
|
||||
ParsedCommand.SendShrug(message)
|
||||
}
|
||||
|
||||
Command.VERIFY_USER.command -> {
|
||||
val message = textMessage.substring(Command.VERIFY_USER.command.length).trim()
|
||||
|
||||
ParsedCommand.VerifyUser(message)
|
||||
}
|
||||
Command.POLL.command -> {
|
||||
val rawCommand = textMessage.substring(Command.POLL.command.length).trim()
|
||||
val split = rawCommand.split("|").map { it.trim() }
|
||||
if (split.size > 2) {
|
||||
ParsedCommand.SendPoll(split[0], split.subList(1, split.size))
|
||||
} else {
|
||||
ParsedCommand.ErrorSyntax(Command.POLL)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
// Unknown command
|
||||
ParsedCommand.ErrorUnknownSlashCommand(slashCommand)
|
||||
|
@ -50,4 +50,5 @@ sealed class ParsedCommand {
|
||||
class SendSpoiler(val message: String) : ParsedCommand()
|
||||
class SendShrug(val message: CharSequence) : ParsedCommand()
|
||||
class VerifyUser(val userId: String) : ParsedCommand()
|
||||
class SendPoll(val question: String, val options: List<String>) : ParsedCommand()
|
||||
}
|
||||
|
@ -53,6 +53,8 @@ sealed class RoomDetailAction : VectorViewModelAction {
|
||||
data class ResendMessage(val eventId: String) : RoomDetailAction()
|
||||
data class RemoveFailedEcho(val eventId: String) : RoomDetailAction()
|
||||
|
||||
data class ReplyToOptions(val eventId: String, val optionIndex: Int, val optionValue: String) : RoomDetailAction()
|
||||
|
||||
data class ReportContent(
|
||||
val eventId: String,
|
||||
val senderId: String?,
|
||||
|
@ -43,6 +43,7 @@ import im.vector.matrix.android.api.session.room.model.RoomMemberSummary
|
||||
import im.vector.matrix.android.api.session.room.model.RoomSummary
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageType
|
||||
import im.vector.matrix.android.api.session.room.model.message.OptionItem
|
||||
import im.vector.matrix.android.api.session.room.model.message.getFileUrl
|
||||
import im.vector.matrix.android.api.session.room.model.tombstone.RoomTombstoneContent
|
||||
import im.vector.matrix.android.api.session.room.read.ReadService
|
||||
@ -199,6 +200,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
||||
is RoomDetailAction.IgnoreUser -> handleIgnoreUser(action)
|
||||
is RoomDetailAction.EnterTrackingUnreadMessagesState -> startTrackingUnreadMessages()
|
||||
is RoomDetailAction.ExitTrackingUnreadMessagesState -> stopTrackingUnreadMessages()
|
||||
is RoomDetailAction.ReplyToOptions -> handleReplyToOptions(action)
|
||||
is RoomDetailAction.AcceptVerificationRequest -> handleAcceptVerification(action)
|
||||
is RoomDetailAction.DeclineVerificationRequest -> handleDeclineVerification(action)
|
||||
is RoomDetailAction.RequestVerification -> handleRequestVerification(action)
|
||||
@ -422,6 +424,11 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
||||
_viewEvents.post(RoomDetailViewEvents.SlashCommandHandled())
|
||||
popDraft()
|
||||
}
|
||||
is ParsedCommand.SendPoll -> {
|
||||
room.sendPoll(slashCommandResult.question, slashCommandResult.options.mapIndexed { index, s -> OptionItem(s, "$index. $s") })
|
||||
_viewEvents.post(RoomDetailViewEvents.SlashCommandHandled())
|
||||
popDraft()
|
||||
}
|
||||
is ParsedCommand.ChangeTopic -> {
|
||||
handleChangeTopicSlashCommand(slashCommandResult)
|
||||
popDraft()
|
||||
@ -855,6 +862,10 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleReplyToOptions(action: RoomDetailAction.ReplyToOptions) {
|
||||
room.sendOptionsReply(action.eventId, action.optionIndex, action.optionValue)
|
||||
}
|
||||
|
||||
private fun observeSyncState() {
|
||||
session.rx()
|
||||
.liveSyncState()
|
||||
|
@ -33,10 +33,14 @@ import im.vector.matrix.android.api.session.room.model.message.MessageEmoteConte
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageFileContent
|
||||
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
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageVideoContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.OPTION_TYPE_BUTTONS
|
||||
import im.vector.matrix.android.api.session.room.model.message.OPTION_TYPE_POLL
|
||||
import im.vector.matrix.android.api.session.room.model.message.getFileUrl
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||
import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent
|
||||
@ -57,7 +61,6 @@ import im.vector.riotx.features.home.room.detail.timeline.helper.MessageInformat
|
||||
import im.vector.riotx.features.home.room.detail.timeline.helper.MessageItemAttributesFactory
|
||||
import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider
|
||||
import im.vector.riotx.features.home.room.detail.timeline.item.AbsMessageItem
|
||||
import im.vector.riotx.features.home.room.detail.timeline.item.DefaultItem
|
||||
import im.vector.riotx.features.home.room.detail.timeline.item.MessageBlockCodeItem
|
||||
import im.vector.riotx.features.home.room.detail.timeline.item.MessageBlockCodeItem_
|
||||
import im.vector.riotx.features.home.room.detail.timeline.item.MessageFileItem
|
||||
@ -65,6 +68,8 @@ 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.MessageOptionsItem_
|
||||
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 +126,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)
|
||||
@ -137,7 +142,40 @@ class MessageItemFactory @Inject constructor(
|
||||
is MessageFileContent -> buildFileMessageItem(messageContent, informationData, highlight, callback, attributes)
|
||||
is MessageAudioContent -> buildAudioMessageItem(messageContent, informationData, highlight, callback, attributes)
|
||||
is MessageVerificationRequestContent -> buildVerificationRequestMessageItem(messageContent, informationData, highlight, callback, attributes)
|
||||
else -> buildNotHandledMessageItem(messageContent, informationData, highlight, callback)
|
||||
is MessageOptionsContent -> buildOptionsMessageItem(messageContent, informationData, highlight, callback, attributes)
|
||||
is MessagePollResponseContent -> noticeItemFactory.create(event, highlight, callback)
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -228,9 +266,10 @@ class MessageItemFactory @Inject constructor(
|
||||
private fun buildNotHandledMessageItem(messageContent: MessageContent,
|
||||
informationData: MessageInformationData,
|
||||
highlight: Boolean,
|
||||
callback: TimelineEventController.Callback?): DefaultItem? {
|
||||
val text = stringProvider.getString(R.string.rendering_event_error_type_of_message_not_handled, messageContent.msgType)
|
||||
return defaultItemFactory.create(text, informationData, highlight, callback)
|
||||
callback: TimelineEventController.Callback?,
|
||||
attributes: AbsMessageItem.Attributes): MessageTextItem? {
|
||||
// For compatibility reason we should display the body
|
||||
return buildMessageTextItem(messageContent.body, false, informationData, highlight, callback, attributes)
|
||||
}
|
||||
|
||||
private fun buildImageMessageItem(messageContent: MessageImageInfoContent,
|
||||
|
@ -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)
|
||||
|
@ -0,0 +1,78 @@
|
||||
/*
|
||||
* 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.riotx.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.matrix.android.api.session.room.model.message.MessageOptionsContent
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.extensions.setTextOrHide
|
||||
import im.vector.riotx.features.home.room.detail.RoomDetailAction
|
||||
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
|
||||
|
||||
@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.setOnClickListener {
|
||||
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
|
||||
}
|
||||
}
|
@ -0,0 +1,158 @@
|
||||
/*
|
||||
* 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.riotx.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.matrix.android.api.session.room.model.message.MessageOptionsContent
|
||||
import im.vector.riotx.R
|
||||
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>() {
|
||||
|
||||
@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 {
|
||||
it.text = item.label
|
||||
it.isVisible = true
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
holder.resultWrapper.isVisible = true
|
||||
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, 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 = DebouncedClickListener(View.OnClickListener {
|
||||
val optionIndex = buttons.indexOf(it)
|
||||
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.setOnClickListener(clickListener) }
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val STUB_ID = R.id.messagePollStub
|
||||
}
|
||||
}
|
@ -0,0 +1,83 @@
|
||||
/*
|
||||
* 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.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
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import butterknife.BindView
|
||||
import butterknife.ButterKnife
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.extensions.setTextOrHide
|
||||
|
||||
class PollResultLineView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = 0
|
||||
) : LinearLayout(context, attrs, defStyleAttr) {
|
||||
|
||||
@BindView(R.id.pollResultItemLabel)
|
||||
lateinit var labelTextView: TextView
|
||||
|
||||
@BindView(R.id.pollResultItemPercent)
|
||||
lateinit var percentTextView: TextView
|
||||
|
||||
@BindView(R.id.pollResultItemSelectedIcon)
|
||||
lateinit var selectedIcon: ImageView
|
||||
|
||||
var label: String? = null
|
||||
set(value) {
|
||||
field = value
|
||||
labelTextView.setTextOrHide(value)
|
||||
}
|
||||
|
||||
var percent: String? = null
|
||||
set(value) {
|
||||
field = value
|
||||
percentTextView.setTextOrHide(value)
|
||||
}
|
||||
|
||||
var optionSelected: Boolean = false
|
||||
set(value) {
|
||||
field = value
|
||||
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
|
||||
ButterKnife.bind(this)
|
||||
|
||||
val typedArray = context.obtainStyledAttributes(attrs,
|
||||
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)
|
||||
isWinner = typedArray.getBoolean(R.styleable.PollResultLineView_optionIsWinner, false)
|
||||
}
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:color="@color/riotx_button_disabled_alpha12" android:state_enabled="false" />
|
||||
<item android:color="@color/riotx_button_primary_accent_alpha12" android:state_enabled="true" />
|
||||
</selector>
|
@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:color="@color/button_bot_disabled_text_color" android:state_enabled="false" />
|
||||
<item android:color="@color/button_bot_enabled_text_color" />
|
||||
</selector>
|
30
vector/src/main/res/drawable/ic_poll.xml
Normal file
30
vector/src/main/res/drawable/ic_poll.xml
Normal file
@ -0,0 +1,30 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M3.5,0.5L20.5,0.5A3,3 0,0 1,23.5 3.5L23.5,20.5A3,3 0,0 1,20.5 23.5L3.5,23.5A3,3 0,0 1,0.5 20.5L0.5,3.5A3,3 0,0 1,3.5 0.5z"
|
||||
android:strokeWidth="1"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M5.5,12L6.5,12A1.5,1.5 0,0 1,8 13.5L8,19.5A1.5,1.5 0,0 1,6.5 21L5.5,21A1.5,1.5 0,0 1,4 19.5L4,13.5A1.5,1.5 0,0 1,5.5 12z"
|
||||
android:strokeWidth="1"
|
||||
android:fillColor="#000000"
|
||||
android:fillType="evenOdd"
|
||||
android:strokeColor="#00000000"/>
|
||||
<path
|
||||
android:pathData="M11.5,9L12.5,9A1.5,1.5 0,0 1,14 10.5L14,19.5A1.5,1.5 0,0 1,12.5 21L11.5,21A1.5,1.5 0,0 1,10 19.5L10,10.5A1.5,1.5 0,0 1,11.5 9z"
|
||||
android:strokeWidth="1"
|
||||
android:fillColor="#000000"
|
||||
android:fillType="evenOdd"
|
||||
android:strokeColor="#00000000"/>
|
||||
<path
|
||||
android:pathData="M17.5,6L18.5,6A1.5,1.5 0,0 1,20 7.5L20,19.5A1.5,1.5 0,0 1,18.5 21L17.5,21A1.5,1.5 0,0 1,16 19.5L16,7.5A1.5,1.5 0,0 1,17.5 6z"
|
||||
android:strokeWidth="1"
|
||||
android:fillColor="#000000"
|
||||
android:fillType="evenOdd"
|
||||
android:strokeColor="#00000000"/>
|
||||
</vector>
|
@ -105,6 +105,20 @@
|
||||
android:layout_marginEnd="56dp"
|
||||
android:layout="@layout/item_timeline_event_redacted_stub" />
|
||||
|
||||
<ViewStub
|
||||
android:id="@+id/messagePollStub"
|
||||
style="@style/TimelineContentStubBaseParams"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="56dp"
|
||||
android:layout="@layout/item_timeline_event_poll_stub" />
|
||||
|
||||
<ViewStub
|
||||
android:id="@+id/messageOptionsStub"
|
||||
style="@style/TimelineContentStubBaseParams"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="56dp"
|
||||
android:layout="@layout/item_timeline_event_option_buttons_stub" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<LinearLayout
|
||||
|
@ -0,0 +1,35 @@
|
||||
<?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">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/optionLabelText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:textColor="?riotx_text_primary"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="normal"
|
||||
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 -->
|
||||
<!--com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/pollButton1"
|
||||
style="@style/Style.Vector.Poll.Button"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
tools:text="Create Github issue" /-->
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
</LinearLayout>
|
@ -0,0 +1,39 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<merge xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="40dp"
|
||||
android:orientation="horizontal"
|
||||
tools:parentTag="android.widget.LinearLayout">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/pollResultItemSelectedIcon"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:contentDescription="@string/poll_item_selected_aria"
|
||||
android:paddingStart="2dp"
|
||||
android:paddingEnd="2dp"
|
||||
android:src="@drawable/ic_check_white_24dp"
|
||||
android:tint="?riotx_text_secondary" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/pollResultItemLabel"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_weight="1"
|
||||
android:textColor="?riotx_text_primary"
|
||||
android:textSize="14sp"
|
||||
tools:text="Open a Github Issue" />
|
||||
|
||||
|
||||
<TextView
|
||||
android:id="@+id/pollResultItemPercent"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:textColor="?riotx_text_primary"
|
||||
android:textSize="14sp"
|
||||
tools:text="47%" />
|
||||
</merge>
|
143
vector/src/main/res/layout/item_timeline_event_poll_stub.xml
Normal file
143
vector/src/main/res/layout/item_timeline_event_poll_stub.xml
Normal file
@ -0,0 +1,143 @@
|
||||
<?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">
|
||||
|
||||
<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:src="@drawable/ic_poll"
|
||||
android:tint="@color/riotx_accent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/pollLabelText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginStart="4dp"
|
||||
android:textColor="?riotx_text_primary"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="bold"
|
||||
tools:text="What would you like to do?" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/pollButton1"
|
||||
style="@style/Style.Vector.Poll.Button"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone"
|
||||
tools:text="Create Github issue"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/pollButton2"
|
||||
style="@style/Style.Vector.Poll.Button"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone"
|
||||
tools:text="Search Github"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/pollButton3"
|
||||
style="@style/Style.Vector.Poll.Button"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone"
|
||||
tools:text="Logout"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/pollButton4"
|
||||
style="@style/Style.Vector.Poll.Button"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone"
|
||||
tools:text="Option 4"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/pollButton5"
|
||||
style="@style/Style.Vector.Poll.Button"
|
||||
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.riotx.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.riotx.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.riotx.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.riotx.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.riotx.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"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="2dp"
|
||||
android:textColor="?riotx_text_secondary"
|
||||
android:textSize="12sp"
|
||||
android:visibility="gone"
|
||||
tools:text="12 votes - Final Results"
|
||||
tools:visibility="visible" />
|
||||
|
||||
</LinearLayout>
|
7
vector/src/main/res/layout/option_buttons.xml
Normal file
7
vector/src/main/res/layout/option_buttons.xml
Normal file
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<com.google.android.material.button.MaterialButton xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
style="@style/VectorButtonStyleInlineBot"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
tools:text="Create Github issue" />
|
@ -97,4 +97,12 @@
|
||||
<attr name="riotx_highlighted_message_background" format="reference" />
|
||||
|
||||
</declare-styleable>
|
||||
|
||||
<declare-styleable name="PollResultLineView">
|
||||
<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>
|
||||
|
@ -125,6 +125,8 @@
|
||||
<color name="button_disabled_text_color">#FFFFFFFF</color>
|
||||
<color name="button_destructive_enabled_text_color">#FF4B55</color>
|
||||
<color name="button_destructive_disabled_text_color">#FF4B55</color>
|
||||
<color name="button_bot_enabled_text_color">#FF368BD6</color>
|
||||
<color name="button_bot_disabled_text_color">#61708B</color>
|
||||
|
||||
|
||||
<!-- Link color -->
|
||||
|
@ -10,6 +10,7 @@
|
||||
|
||||
<color name="riotx_destructive_accent">#FFFF4B55</color>
|
||||
<color name="riotx_destructive_accent_alpha12">#1EFF4B55</color>
|
||||
<color name="riotx_button_primary_accent_alpha12">#14368BD6</color>
|
||||
|
||||
|
||||
<color name="riotx_positive_accent">#03B381</color>
|
||||
|
@ -6,7 +6,16 @@
|
||||
<!-- Sections has been created to avoid merge conflict. Let's see if it's better -->
|
||||
|
||||
<!-- BEGIN Strings added by Valere -->
|
||||
|
||||
<plurals name="poll_info">
|
||||
<item quantity="zero">%d vote</item>
|
||||
<item quantity="other">%d votes</item>
|
||||
</plurals>
|
||||
<plurals name="poll_info_final">
|
||||
<item quantity="zero">%d vote - Final results</item>
|
||||
<item quantity="other">%d votes - Final results</item>
|
||||
</plurals>
|
||||
<string name="poll_item_selected_aria">Selected Option</string>
|
||||
<string name="command_description_poll">Creates a simple poll</string>
|
||||
<!-- END Strings added by Valere -->
|
||||
|
||||
|
||||
|
@ -151,6 +151,11 @@
|
||||
<item name="android:textColor">@color/button_positive_text_color_selector</item>
|
||||
</style>
|
||||
|
||||
<style name="VectorButtonStyleInlineBot" parent="VectorButtonStyleDestructive">
|
||||
<item name="backgroundTint">@color/button_bot_background_selector</item>
|
||||
<item name="android:textColor">@color/button_bot_enabled_text_color</item>
|
||||
</style>
|
||||
|
||||
<!--Widget.AppCompat.Button.Borderless.Colored, which sets the text color to colorAccent,
|
||||
using colorControlHighlight as an overlay for focused and pressed states.-->
|
||||
<style name="VectorButtonStyleText" parent="Widget.MaterialComponents.Button.TextButton">
|
||||
@ -183,6 +188,14 @@
|
||||
<item name="colorControlHighlight">@android:color/white</item>
|
||||
</style>
|
||||
|
||||
|
||||
<style name="Style.Vector.Poll.Button" parent="Widget.MaterialComponents.Button.OutlinedButton">
|
||||
<item name="android:minHeight">44dp</item>
|
||||
<item name="android:textAllCaps">false</item>
|
||||
<item name="cornerRadius">10dp</item>
|
||||
</style>
|
||||
|
||||
|
||||
<style name="VectorSearchView" parent="Widget.AppCompat.SearchView">
|
||||
<item name="searchIcon">@drawable/ic_search</item>
|
||||
<item name="closeIcon">@drawable/ic_x_green</item>
|
||||
|
Loading…
Reference in New Issue
Block a user