mirror of
https://github.com/element-hq/element-android
synced 2024-11-28 13:38:49 +03:00
Merge pull request #5020 from vector-im/feature/ons/edit_polls
Edit Polls and Allow Undisclosed Polls
This commit is contained in:
commit
5bb06158c7
34 changed files with 486 additions and 72 deletions
1
changelog.d/5036.feature
Normal file
1
changelog.d/5036.feature
Normal file
|
@ -0,0 +1 @@
|
|||
Allow editing polls
|
1
changelog.d/5037.feature
Normal file
1
changelog.d/5037.feature
Normal file
|
@ -0,0 +1 @@
|
|||
Support undisclosed polls
|
|
@ -22,7 +22,7 @@ import com.squareup.moshi.JsonClass
|
|||
@JsonClass(generateAdapter = true)
|
||||
data class PollCreationInfo(
|
||||
@Json(name = "question") val question: PollQuestion? = null,
|
||||
@Json(name = "kind") val kind: String? = "org.matrix.msc3381.poll.disclosed",
|
||||
@Json(name = "kind") val kind: PollType? = PollType.DISCLOSED,
|
||||
@Json(name = "max_selections") val maxSelections: Int = 1,
|
||||
@Json(name = "answers") val answers: List<PollAnswer>? = null
|
||||
)
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.api.session.room.model.message
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = false)
|
||||
enum class PollType {
|
||||
/**
|
||||
* Voters should see results as soon as they have voted.
|
||||
*/
|
||||
@Json(name = "org.matrix.msc3381.poll.disclosed")
|
||||
DISCLOSED,
|
||||
|
||||
/**
|
||||
* Results should be only revealed when the poll is ended.
|
||||
*/
|
||||
@Json(name = "org.matrix.msc3381.poll.undisclosed")
|
||||
UNDISCLOSED
|
||||
}
|
|
@ -18,6 +18,7 @@ package org.matrix.android.sdk.api.session.room.model.relation
|
|||
import androidx.lifecycle.LiveData
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.api.session.room.model.EventAnnotationsSummary
|
||||
import org.matrix.android.sdk.api.session.room.model.message.PollType
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||
import org.matrix.android.sdk.api.util.Cancelable
|
||||
import org.matrix.android.sdk.api.util.Optional
|
||||
|
@ -64,6 +65,18 @@ interface RelationService {
|
|||
fun undoReaction(targetEventId: String,
|
||||
reaction: String): Cancelable
|
||||
|
||||
/**
|
||||
* Edit a poll.
|
||||
* @param pollType indicates open or closed polls
|
||||
* @param targetEvent The poll event to edit
|
||||
* @param question The edited question
|
||||
* @param options The edited options
|
||||
*/
|
||||
fun editPoll(targetEvent: TimelineEvent,
|
||||
pollType: PollType,
|
||||
question: String,
|
||||
options: List<String>): Cancelable
|
||||
|
||||
/**
|
||||
* Edit a text message body. Limited to "m.text" contentType
|
||||
* @param targetEvent The event to edit
|
||||
|
|
|
@ -20,6 +20,7 @@ import org.matrix.android.sdk.api.session.content.ContentAttachmentData
|
|||
import org.matrix.android.sdk.api.session.events.model.Content
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageType
|
||||
import org.matrix.android.sdk.api.session.room.model.message.PollType
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||
import org.matrix.android.sdk.api.util.Cancelable
|
||||
|
||||
|
@ -91,11 +92,12 @@ interface SendService {
|
|||
|
||||
/**
|
||||
* Send a poll to the room.
|
||||
* @param pollType indicates open or closed polls
|
||||
* @param question the question
|
||||
* @param options list of options
|
||||
* @return a [Cancelable]
|
||||
*/
|
||||
fun sendPoll(question: String, options: List<String>): Cancelable
|
||||
fun sendPoll(pollType: PollType, question: String, options: List<String>): Cancelable
|
||||
|
||||
/**
|
||||
* Method to send a poll response.
|
||||
|
|
|
@ -133,7 +133,7 @@ fun TimelineEvent.getEditedEventId(): String? {
|
|||
fun TimelineEvent.getLastMessageContent(): MessageContent? {
|
||||
return when (root.getClearType()) {
|
||||
EventType.STICKER -> root.getClearContent().toModel<MessageStickerContent>()
|
||||
EventType.POLL_START -> root.getClearContent().toModel<MessagePollContent>()
|
||||
EventType.POLL_START -> (annotations?.editSummary?.latestContent ?: root.getClearContent()).toModel<MessagePollContent>()
|
||||
else -> (annotations?.editSummary?.latestContent ?: root.getClearContent()).toModel()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,10 +34,13 @@ import org.matrix.android.sdk.api.session.room.model.VoteInfo
|
|||
import org.matrix.android.sdk.api.session.room.model.VoteSummary
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageEndPollContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessagePollResponseContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageRelationContent
|
||||
import org.matrix.android.sdk.api.session.room.model.relation.ReactionContent
|
||||
import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
|
||||
import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent
|
||||
import org.matrix.android.sdk.internal.SessionManager
|
||||
import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent
|
||||
import org.matrix.android.sdk.internal.crypto.verification.toState
|
||||
import org.matrix.android.sdk.internal.database.mapper.ContentMapper
|
||||
|
@ -55,6 +58,7 @@ import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
|
|||
import org.matrix.android.sdk.internal.database.query.create
|
||||
import org.matrix.android.sdk.internal.database.query.getOrCreate
|
||||
import org.matrix.android.sdk.internal.database.query.where
|
||||
import org.matrix.android.sdk.internal.di.SessionId
|
||||
import org.matrix.android.sdk.internal.di.UserId
|
||||
import org.matrix.android.sdk.internal.session.EventInsertLiveProcessor
|
||||
import org.matrix.android.sdk.internal.session.room.state.StateEventDataSource
|
||||
|
@ -63,7 +67,9 @@ import javax.inject.Inject
|
|||
|
||||
internal class EventRelationsAggregationProcessor @Inject constructor(
|
||||
@UserId private val userId: String,
|
||||
private val stateEventDataSource: StateEventDataSource
|
||||
private val stateEventDataSource: StateEventDataSource,
|
||||
@SessionId private val sessionId: String,
|
||||
private val sessionManager: SessionManager
|
||||
) : EventInsertLiveProcessor {
|
||||
|
||||
private val allowedTypes = listOf(
|
||||
|
@ -79,6 +85,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
|
|||
// EventType.KEY_VERIFICATION_READY,
|
||||
EventType.KEY_VERIFICATION_KEY,
|
||||
EventType.ENCRYPTED,
|
||||
EventType.POLL_START,
|
||||
EventType.POLL_RESPONSE,
|
||||
EventType.POLL_END
|
||||
)
|
||||
|
@ -208,6 +215,14 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
|
|||
}
|
||||
}
|
||||
}
|
||||
EventType.POLL_START -> {
|
||||
val content: MessagePollContent? = event.content.toModel()
|
||||
if (content?.relatesTo?.type == RelationType.REPLACE) {
|
||||
Timber.v("###REPLACE in room $roomId for event ${event.eventId}")
|
||||
// A replace!
|
||||
handleReplace(realm, event, content, roomId, isLocalEcho)
|
||||
}
|
||||
}
|
||||
EventType.POLL_RESPONSE -> {
|
||||
event.content.toModel<MessagePollResponseContent>(catchError = true)?.let {
|
||||
handleResponse(realm, event, it, roomId, isLocalEcho)
|
||||
|
@ -274,6 +289,20 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
|
|||
Timber.v("###REPLACE ignoring event for summary, it's known $eventId")
|
||||
return
|
||||
}
|
||||
|
||||
ContentMapper
|
||||
.map(eventAnnotationsSummaryEntity.pollResponseSummary?.aggregatedContent)
|
||||
?.toModel<PollSummaryContent>()
|
||||
?.apply {
|
||||
totalVotes = 0
|
||||
winnerVoteCount = 0
|
||||
votes = emptyList()
|
||||
votesSummary = emptyMap()
|
||||
}
|
||||
?.apply {
|
||||
eventAnnotationsSummaryEntity.pollResponseSummary?.aggregatedContent = ContentMapper.map(toContent())
|
||||
}
|
||||
|
||||
val txId = event.unsignedData?.transactionId
|
||||
// is it a remote echo?
|
||||
if (!isLocalEcho && existingSummary.editions.any { it.eventId == txId }) {
|
||||
|
@ -315,6 +344,16 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
|
|||
val targetEventId = relatedEventId ?: content.relatesTo?.eventId ?: return
|
||||
val eventTimestamp = event.originServerTs ?: return
|
||||
|
||||
val session = sessionManager.getSessionComponent(sessionId)?.session()
|
||||
|
||||
val targetPollEvent = session?.getRoom(roomId)?.getTimeLineEvent(targetEventId) ?: return Unit.also {
|
||||
Timber.v("## POLL target poll event $targetEventId not found in room $roomId")
|
||||
}
|
||||
|
||||
val targetPollContent = targetPollEvent.getLastMessageContent() as? MessagePollContent ?: return Unit.also {
|
||||
Timber.v("## POLL target poll event $targetEventId content is malformed")
|
||||
}
|
||||
|
||||
// ok, this is a poll response
|
||||
var existing = EventAnnotationsSummaryEntity.where(realm, roomId, targetEventId).findFirst()
|
||||
if (existing == null) {
|
||||
|
@ -355,6 +394,12 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
|
|||
Timber.d("## POLL Ignoring malformed response no option eventId:$eventId content: ${event.content}")
|
||||
}
|
||||
|
||||
// Check if this option is in available options
|
||||
if (!targetPollContent.pollCreationInfo?.answers?.map { it.id }?.contains(option).orFalse()) {
|
||||
Timber.v("## POLL $targetEventId doesn't contain option $option")
|
||||
return
|
||||
}
|
||||
|
||||
val votes = sumModel.votes?.toMutableList() ?: ArrayList()
|
||||
val existingVoteIndex = votes.indexOfFirst { it.userId == senderId }
|
||||
if (existingVoteIndex != -1) {
|
||||
|
|
|
@ -24,6 +24,7 @@ import dagger.assisted.AssistedInject
|
|||
import org.matrix.android.sdk.api.MatrixCallback
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.api.session.room.model.EventAnnotationsSummary
|
||||
import org.matrix.android.sdk.api.session.room.model.message.PollType
|
||||
import org.matrix.android.sdk.api.session.room.model.relation.RelationService
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||
import org.matrix.android.sdk.api.util.Cancelable
|
||||
|
@ -112,6 +113,13 @@ internal class DefaultRelationService @AssistedInject constructor(
|
|||
.executeBy(taskExecutor)
|
||||
}
|
||||
|
||||
override fun editPoll(targetEvent: TimelineEvent,
|
||||
pollType: PollType,
|
||||
question: String,
|
||||
options: List<String>): Cancelable {
|
||||
return eventEditor.editPoll(targetEvent, pollType, question, options)
|
||||
}
|
||||
|
||||
override fun editTextMessage(targetEvent: TimelineEvent,
|
||||
msgType: String,
|
||||
newBodyText: CharSequence,
|
||||
|
|
|
@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.session.room.relation
|
|||
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageType
|
||||
import org.matrix.android.sdk.api.session.room.model.message.PollType
|
||||
import org.matrix.android.sdk.api.session.room.send.SendState
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||
import org.matrix.android.sdk.api.util.Cancelable
|
||||
|
@ -46,13 +47,11 @@ internal class EventEditor @Inject constructor(private val eventSenderProcessor:
|
|||
val editedEvent = eventFactory.createTextEvent(roomId, msgType, newBodyText, newBodyAutoMarkdown).copy(
|
||||
eventId = targetEvent.eventId
|
||||
)
|
||||
updateFailedEchoWithEvent(roomId, targetEvent.eventId, editedEvent)
|
||||
return eventSenderProcessor.postEvent(editedEvent, cryptoSessionInfoProvider.isRoomEncrypted(roomId))
|
||||
return sendFailedEvent(targetEvent, editedEvent)
|
||||
} else if (targetEvent.root.sendState.isSent()) {
|
||||
val event = eventFactory
|
||||
.createReplaceTextEvent(roomId, targetEvent.eventId, newBodyText, newBodyAutoMarkdown, msgType, compatibilityBodyText)
|
||||
.also { localEchoRepository.createLocalEcho(it) }
|
||||
return eventSenderProcessor.postEvent(event, cryptoSessionInfoProvider.isRoomEncrypted(roomId))
|
||||
return sendReplaceEvent(roomId, event)
|
||||
} else {
|
||||
// Should we throw?
|
||||
Timber.w("Can't edit a sending event")
|
||||
|
@ -60,6 +59,37 @@ internal class EventEditor @Inject constructor(private val eventSenderProcessor:
|
|||
}
|
||||
}
|
||||
|
||||
fun editPoll(targetEvent: TimelineEvent,
|
||||
pollType: PollType,
|
||||
question: String,
|
||||
options: List<String>): Cancelable {
|
||||
val roomId = targetEvent.roomId
|
||||
if (targetEvent.root.sendState.hasFailed()) {
|
||||
val editedEvent = eventFactory.createPollEvent(roomId, pollType, question, options).copy(
|
||||
eventId = targetEvent.eventId
|
||||
)
|
||||
return sendFailedEvent(targetEvent, editedEvent)
|
||||
} else if (targetEvent.root.sendState.isSent()) {
|
||||
val event = eventFactory
|
||||
.createPollReplaceEvent(roomId, pollType, targetEvent.eventId, question, options)
|
||||
return sendReplaceEvent(roomId, event)
|
||||
} else {
|
||||
Timber.w("Can't edit a sending event")
|
||||
return NoOpCancellable
|
||||
}
|
||||
}
|
||||
|
||||
private fun sendFailedEvent(targetEvent: TimelineEvent, editedEvent: Event): Cancelable {
|
||||
val roomId = targetEvent.roomId
|
||||
updateFailedEchoWithEvent(roomId, targetEvent.eventId, editedEvent)
|
||||
return eventSenderProcessor.postEvent(editedEvent, cryptoSessionInfoProvider.isRoomEncrypted(roomId))
|
||||
}
|
||||
|
||||
private fun sendReplaceEvent(roomId: String, editedEvent: Event): Cancelable {
|
||||
localEchoRepository.createLocalEcho(editedEvent)
|
||||
return eventSenderProcessor.postEvent(editedEvent, cryptoSessionInfoProvider.isRoomEncrypted(roomId))
|
||||
}
|
||||
|
||||
fun editReply(replyToEdit: TimelineEvent,
|
||||
originalTimelineEvent: TimelineEvent,
|
||||
newBodyText: String,
|
||||
|
|
|
@ -37,6 +37,7 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageFileContent
|
|||
import org.matrix.android.sdk.api.session.room.model.message.MessageImageContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageWithAttachmentContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.PollType
|
||||
import org.matrix.android.sdk.api.session.room.model.message.getFileUrl
|
||||
import org.matrix.android.sdk.api.session.room.send.SendService
|
||||
import org.matrix.android.sdk.api.session.room.send.SendState
|
||||
|
@ -103,8 +104,8 @@ internal class DefaultSendService @AssistedInject constructor(
|
|||
.let { sendEvent(it) }
|
||||
}
|
||||
|
||||
override fun sendPoll(question: String, options: List<String>): Cancelable {
|
||||
return localEchoEventFactory.createPollEvent(roomId, question, options)
|
||||
override fun sendPoll(pollType: PollType, question: String, options: List<String>): Cancelable {
|
||||
return localEchoEventFactory.createPollEvent(roomId, pollType, question, options)
|
||||
.also { createLocalEcho(it) }
|
||||
.let { sendEvent(it) }
|
||||
}
|
||||
|
|
|
@ -48,6 +48,7 @@ import org.matrix.android.sdk.api.session.room.model.message.PollAnswer
|
|||
import org.matrix.android.sdk.api.session.room.model.message.PollCreationInfo
|
||||
import org.matrix.android.sdk.api.session.room.model.message.PollQuestion
|
||||
import org.matrix.android.sdk.api.session.room.model.message.PollResponse
|
||||
import org.matrix.android.sdk.api.session.room.model.message.PollType
|
||||
import org.matrix.android.sdk.api.session.room.model.message.ThumbnailInfo
|
||||
import org.matrix.android.sdk.api.session.room.model.message.VideoInfo
|
||||
import org.matrix.android.sdk.api.session.room.model.relation.ReactionContent
|
||||
|
@ -61,6 +62,7 @@ import org.matrix.android.sdk.internal.di.UserId
|
|||
import org.matrix.android.sdk.internal.session.content.ThumbnailExtractor
|
||||
import org.matrix.android.sdk.internal.session.permalinks.PermalinkFactory
|
||||
import org.matrix.android.sdk.internal.session.room.send.pills.TextPillsUtils
|
||||
import java.util.UUID
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
|
@ -124,6 +126,45 @@ internal class LocalEchoEventFactory @Inject constructor(
|
|||
))
|
||||
}
|
||||
|
||||
private fun createPollContent(question: String,
|
||||
options: List<String>,
|
||||
pollType: PollType): MessagePollContent {
|
||||
return MessagePollContent(
|
||||
pollCreationInfo = PollCreationInfo(
|
||||
question = PollQuestion(
|
||||
question = question
|
||||
),
|
||||
kind = pollType,
|
||||
answers = options.map { option ->
|
||||
PollAnswer(
|
||||
id = UUID.randomUUID().toString(),
|
||||
answer = option
|
||||
)
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun createPollReplaceEvent(roomId: String,
|
||||
pollType: PollType,
|
||||
targetEventId: String,
|
||||
question: String,
|
||||
options: List<String>): Event {
|
||||
val newContent = MessagePollContent(
|
||||
relatesTo = RelationDefaultContent(RelationType.REPLACE, targetEventId),
|
||||
newContent = createPollContent(question, options, pollType).toContent()
|
||||
)
|
||||
val localId = LocalEcho.createLocalEchoId()
|
||||
return Event(
|
||||
roomId = roomId,
|
||||
originServerTs = dummyOriginServerTs(),
|
||||
senderId = userId,
|
||||
eventId = localId,
|
||||
type = EventType.POLL_START,
|
||||
content = newContent.toContent()
|
||||
)
|
||||
}
|
||||
|
||||
fun createPollReplyEvent(roomId: String,
|
||||
pollEventId: String,
|
||||
answerId: String): Event {
|
||||
|
@ -149,21 +190,10 @@ internal class LocalEchoEventFactory @Inject constructor(
|
|||
}
|
||||
|
||||
fun createPollEvent(roomId: String,
|
||||
pollType: PollType,
|
||||
question: String,
|
||||
options: List<String>): Event {
|
||||
val content = MessagePollContent(
|
||||
pollCreationInfo = PollCreationInfo(
|
||||
question = PollQuestion(
|
||||
question = question
|
||||
),
|
||||
answers = options.mapIndexed { index, option ->
|
||||
PollAnswer(
|
||||
id = "$index-$option",
|
||||
answer = option
|
||||
)
|
||||
}
|
||||
)
|
||||
)
|
||||
val content = createPollContent(question, options, pollType)
|
||||
val localId = LocalEcho.createLocalEchoId()
|
||||
return Event(
|
||||
roomId = roomId,
|
||||
|
|
|
@ -48,8 +48,4 @@ class UserPreferencesProvider @Inject constructor(private val vectorPreferences:
|
|||
fun shouldShowAvatarDisplayNameChanges(): Boolean {
|
||||
return vectorPreferences.showAvatarDisplayNameChangeMessages()
|
||||
}
|
||||
|
||||
fun shouldShowPolls(): Boolean {
|
||||
return vectorPreferences.labsEnablePolls()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -176,6 +176,7 @@ import im.vector.app.features.notifications.NotificationDrawerManager
|
|||
import im.vector.app.features.notifications.NotificationUtils
|
||||
import im.vector.app.features.permalink.NavigationInterceptor
|
||||
import im.vector.app.features.permalink.PermalinkHandler
|
||||
import im.vector.app.features.poll.create.PollMode
|
||||
import im.vector.app.features.reactions.EmojiReactionPickerActivity
|
||||
import im.vector.app.features.roomprofile.RoomProfileActivity
|
||||
import im.vector.app.features.session.coroutineScope
|
||||
|
@ -203,6 +204,7 @@ import org.billcarsonfr.jsonviewer.JSonViewerDialog
|
|||
import org.commonmark.parser.Parser
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.session.content.ContentAttachmentData
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||
import org.matrix.android.sdk.api.session.room.model.Membership
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomSummary
|
||||
|
@ -1371,7 +1373,6 @@ class RoomDetailFragment @Inject constructor(
|
|||
override fun onAddAttachment() {
|
||||
if (!::attachmentTypeSelector.isInitialized) {
|
||||
attachmentTypeSelector = AttachmentTypeSelectorView(vectorBaseActivity, vectorBaseActivity.layoutInflater, this@RoomDetailFragment)
|
||||
attachmentTypeSelector.setAttachmentVisibility(AttachmentTypeSelectorView.Type.POLL, vectorPreferences.labsEnablePolls())
|
||||
}
|
||||
attachmentTypeSelector.show(views.composerLayout.views.attachmentButton)
|
||||
}
|
||||
|
@ -2020,7 +2021,9 @@ class RoomDetailFragment @Inject constructor(
|
|||
roomDetailViewModel.handle(RoomDetailAction.UpdateQuickReactAction(action.eventId, action.clickedOn, action.add))
|
||||
}
|
||||
is EventSharedAction.Edit -> {
|
||||
if (withState(messageComposerViewModel) { it.isVoiceMessageIdle }) {
|
||||
if (action.eventType == EventType.POLL_START) {
|
||||
navigator.openCreatePoll(requireContext(), roomDetailArgs.roomId, action.eventId, PollMode.EDIT)
|
||||
} else if (withState(messageComposerViewModel) { it.isVoiceMessageIdle }) {
|
||||
messageComposerViewModel.handle(MessageComposerAction.EnterEditMode(action.eventId, views.composerLayout.text.toString()))
|
||||
} else {
|
||||
requireActivity().toast(R.string.error_voice_message_cannot_reply_or_edit)
|
||||
|
@ -2232,7 +2235,7 @@ class RoomDetailFragment @Inject constructor(
|
|||
AttachmentTypeSelectorView.Type.GALLERY -> attachmentsHelper.selectGallery(attachmentMediaActivityResultLauncher)
|
||||
AttachmentTypeSelectorView.Type.CONTACT -> attachmentsHelper.selectContact(attachmentContactActivityResultLauncher)
|
||||
AttachmentTypeSelectorView.Type.STICKER -> roomDetailViewModel.handle(RoomDetailAction.SelectStickerAttachment)
|
||||
AttachmentTypeSelectorView.Type.POLL -> navigator.openCreatePoll(requireContext(), roomDetailArgs.roomId)
|
||||
AttachmentTypeSelectorView.Type.POLL -> navigator.openCreatePoll(requireContext(), roomDetailArgs.roomId, null, PollMode.CREATE)
|
||||
}.exhaustive
|
||||
}
|
||||
|
||||
|
|
|
@ -39,7 +39,7 @@ sealed class EventSharedAction(@StringRes val titleRes: Int,
|
|||
data class Copy(val content: String) :
|
||||
EventSharedAction(R.string.action_copy, R.drawable.ic_copy)
|
||||
|
||||
data class Edit(val eventId: String) :
|
||||
data class Edit(val eventId: String, val eventType: String) :
|
||||
EventSharedAction(R.string.edit, R.drawable.ic_edit)
|
||||
|
||||
data class Quote(val eventId: String) :
|
||||
|
|
|
@ -284,7 +284,7 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
|
|||
}
|
||||
add(EventSharedAction.Remove(eventId))
|
||||
if (canEdit(timelineEvent, session.myUserId, actionPermissions)) {
|
||||
add(EventSharedAction.Edit(eventId))
|
||||
add(EventSharedAction.Edit(eventId, timelineEvent.root.getClearType()))
|
||||
}
|
||||
if (canCopy(msgType)) {
|
||||
// TODO copy images? html? see ClipBoard
|
||||
|
@ -329,7 +329,7 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
|
|||
}
|
||||
|
||||
if (canEdit(timelineEvent, session.myUserId, actionPermissions)) {
|
||||
add(EventSharedAction.Edit(eventId))
|
||||
add(EventSharedAction.Edit(eventId, timelineEvent.root.getClearType()))
|
||||
}
|
||||
|
||||
if (canRedact(timelineEvent, actionPermissions)) {
|
||||
|
@ -466,14 +466,15 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
|
|||
}
|
||||
|
||||
private fun canEdit(event: TimelineEvent, myUserId: String, actionPermissions: ActionPermissions): Boolean {
|
||||
// Only event of type EventType.MESSAGE are supported for the moment
|
||||
if (event.root.getClearType() != EventType.MESSAGE) return false
|
||||
// Only event of type EventType.MESSAGE and EventType.POLL_START are supported for the moment
|
||||
if (event.root.getClearType() !in listOf(EventType.MESSAGE, EventType.POLL_START)) return false
|
||||
if (!actionPermissions.canSendMessage) return false
|
||||
// TODO if user is admin or moderator
|
||||
val messageContent = event.root.getClearContent().toModel<MessageContent>()
|
||||
return event.root.senderId == myUserId && (
|
||||
messageContent?.msgType == MessageType.MSGTYPE_TEXT ||
|
||||
messageContent?.msgType == MessageType.MSGTYPE_EMOTE
|
||||
messageContent?.msgType == MessageType.MSGTYPE_EMOTE ||
|
||||
canEditPoll(event)
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -516,4 +517,10 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
|
|||
canRedact(event, actionPermissions) &&
|
||||
event.annotations?.pollResponseSummary?.closedTime == null
|
||||
}
|
||||
|
||||
private fun canEditPoll(event: TimelineEvent): Boolean {
|
||||
return event.root.getClearType() == EventType.POLL_START &&
|
||||
event.annotations?.pollResponseSummary?.closedTime == null &&
|
||||
event.annotations?.pollResponseSummary?.aggregatedContent?.totalVotes ?: 0 == 0
|
||||
}
|
||||
}
|
||||
|
|
|
@ -89,6 +89,7 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent
|
|||
import org.matrix.android.sdk.api.session.room.model.message.MessageType
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationRequestContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.PollType
|
||||
import org.matrix.android.sdk.api.session.room.model.message.getFileName
|
||||
import org.matrix.android.sdk.api.session.room.model.message.getFileUrl
|
||||
import org.matrix.android.sdk.api.session.room.model.message.getThumbnailUrl
|
||||
|
@ -186,11 +187,14 @@ class MessageItemFactory @Inject constructor(
|
|||
val didUserVoted = pollResponseSummary?.myVote?.isNotEmpty().orFalse()
|
||||
val winnerVoteCount = pollResponseSummary?.winnerVoteCount
|
||||
val isPollSent = informationData.sendState.isSent()
|
||||
val isPollUndisclosed = pollContent.pollCreationInfo?.kind == PollType.UNDISCLOSED
|
||||
|
||||
val totalVotesText = (pollResponseSummary?.totalVotes ?: 0).let {
|
||||
when {
|
||||
isEnded -> stringProvider.getQuantityString(R.plurals.poll_total_vote_count_after_ended, it, it)
|
||||
didUserVoted -> stringProvider.getQuantityString(R.plurals.poll_total_vote_count_before_ended_and_voted, it, it)
|
||||
else -> if (it == 0) {
|
||||
isEnded -> stringProvider.getQuantityString(R.plurals.poll_total_vote_count_after_ended, it, it)
|
||||
isPollUndisclosed -> ""
|
||||
didUserVoted -> stringProvider.getQuantityString(R.plurals.poll_total_vote_count_before_ended_and_voted, it, it)
|
||||
else -> if (it == 0) {
|
||||
stringProvider.getString(R.string.poll_no_votes_cast)
|
||||
} else {
|
||||
stringProvider.getQuantityString(R.plurals.poll_total_vote_count_before_ended_and_not_voted, it, it)
|
||||
|
@ -214,6 +218,9 @@ class MessageItemFactory @Inject constructor(
|
|||
// Poll is ended. Disable option, show votes and mark the winner.
|
||||
val isWinner = winnerVoteCount != 0 && voteCount == winnerVoteCount
|
||||
PollOptionViewState.PollEnded(optionId, optionAnswer, voteCount, votePercentage, isWinner)
|
||||
} else if (isPollUndisclosed) {
|
||||
// Poll is closed. Enable option, hide votes and mark the user's selection.
|
||||
PollOptionViewState.PollUndisclosed(optionId, optionAnswer, isMyVote)
|
||||
} else if (didUserVoted) {
|
||||
// User voted to the poll, but poll is not ended. Enable option, show votes and mark the user's selection.
|
||||
PollOptionViewState.PollVoted(optionId, optionAnswer, voteCount, votePercentage, isMyVote)
|
||||
|
@ -224,13 +231,22 @@ class MessageItemFactory @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
val question = pollContent.pollCreationInfo?.question?.question ?: ""
|
||||
|
||||
return PollItem_()
|
||||
.attributes(attributes)
|
||||
.eventId(informationData.eventId)
|
||||
.pollQuestion(pollContent.pollCreationInfo?.question?.question ?: "")
|
||||
.pollQuestion(
|
||||
if (informationData.hasBeenEdited) {
|
||||
annotateWithEdited(question, callback, informationData)
|
||||
} else {
|
||||
question
|
||||
}.toEpoxyCharSequence()
|
||||
)
|
||||
.pollSent(isPollSent)
|
||||
.totalVotesText(totalVotesText)
|
||||
.optionViewStates(optionViewStates)
|
||||
.edited(informationData.hasBeenEdited)
|
||||
.highlighted(highlight)
|
||||
.leftGuideline(avatarSizeProvider.leftGuideline)
|
||||
.callback(callback)
|
||||
|
|
|
@ -119,8 +119,6 @@ class TimelineEventVisibilityHelper @Inject constructor(private val userPreferen
|
|||
val diff = computeMembershipDiff()
|
||||
if ((diff.isJoin || diff.isPart) && !userPreferencesProvider.shouldShowJoinLeaves()) return true
|
||||
if ((diff.isAvatarChange || diff.isDisplaynameChange) && !userPreferencesProvider.shouldShowAvatarDisplayNameChanges()) return true
|
||||
} else if (root.getClearType() == EventType.POLL_START && !userPreferencesProvider.shouldShowPolls()) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
|
|
@ -24,12 +24,13 @@ import com.airbnb.epoxy.EpoxyModelClass
|
|||
import im.vector.app.R
|
||||
import im.vector.app.features.home.room.detail.RoomDetailAction
|
||||
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
|
||||
import im.vector.lib.core.utils.epoxy.charsequence.EpoxyCharSequence
|
||||
|
||||
@EpoxyModelClass(layout = R.layout.item_timeline_event_base)
|
||||
abstract class PollItem : AbsMessageItem<PollItem.Holder>() {
|
||||
|
||||
@EpoxyAttribute
|
||||
var pollQuestion: String? = null
|
||||
var pollQuestion: EpoxyCharSequence? = null
|
||||
|
||||
@EpoxyAttribute
|
||||
var callback: TimelineEventController.Callback? = null
|
||||
|
@ -43,6 +44,9 @@ abstract class PollItem : AbsMessageItem<PollItem.Holder>() {
|
|||
@EpoxyAttribute
|
||||
var totalVotesText: String? = null
|
||||
|
||||
@EpoxyAttribute
|
||||
var edited: Boolean = false
|
||||
|
||||
@EpoxyAttribute
|
||||
lateinit var optionViewStates: List<PollOptionViewState>
|
||||
|
||||
|
@ -52,7 +56,7 @@ abstract class PollItem : AbsMessageItem<PollItem.Holder>() {
|
|||
|
||||
renderSendState(holder.view, holder.questionTextView)
|
||||
|
||||
holder.questionTextView.text = pollQuestion
|
||||
holder.questionTextView.text = pollQuestion?.charSequence
|
||||
holder.totalVotesTextView.text = totalVotesText
|
||||
|
||||
while (holder.optionsContainer.childCount < optionViewStates.size) {
|
||||
|
|
|
@ -23,6 +23,7 @@ import androidx.appcompat.content.res.AppCompatResources
|
|||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.core.view.isVisible
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.extensions.exhaustive
|
||||
import im.vector.app.core.extensions.setAttributeTintedImageResource
|
||||
import im.vector.app.databinding.ItemPollOptionBinding
|
||||
|
||||
|
@ -43,11 +44,12 @@ class PollOptionView @JvmOverloads constructor(
|
|||
views.optionNameTextView.text = state.optionAnswer
|
||||
|
||||
when (state) {
|
||||
is PollOptionViewState.PollSending -> renderPollSending()
|
||||
is PollOptionViewState.PollEnded -> renderPollEnded(state)
|
||||
is PollOptionViewState.PollReady -> renderPollReady()
|
||||
is PollOptionViewState.PollVoted -> renderPollVoted(state)
|
||||
}
|
||||
is PollOptionViewState.PollSending -> renderPollSending()
|
||||
is PollOptionViewState.PollEnded -> renderPollEnded(state)
|
||||
is PollOptionViewState.PollReady -> renderPollReady()
|
||||
is PollOptionViewState.PollVoted -> renderPollVoted(state)
|
||||
is PollOptionViewState.PollUndisclosed -> renderPollUndisclosed(state)
|
||||
}.exhaustive
|
||||
}
|
||||
|
||||
private fun renderPollSending() {
|
||||
|
@ -78,6 +80,12 @@ class PollOptionView @JvmOverloads constructor(
|
|||
renderVoteSelection(state.isSelected)
|
||||
}
|
||||
|
||||
private fun renderPollUndisclosed(state: PollOptionViewState.PollUndisclosed) {
|
||||
views.optionCheckImageView.isVisible = true
|
||||
views.optionWinnerImageView.isVisible = false
|
||||
renderVoteSelection(state.isSelected)
|
||||
}
|
||||
|
||||
private fun showVotes(voteCount: Int, votePercentage: Double) {
|
||||
views.optionVoteCountTextView.apply {
|
||||
isVisible = true
|
||||
|
|
|
@ -51,4 +51,12 @@ sealed class PollOptionViewState(open val optionId: String,
|
|||
val votePercentage: Double,
|
||||
val isWinner: Boolean
|
||||
) : PollOptionViewState(optionId, optionAnswer)
|
||||
|
||||
/**
|
||||
* Represent a poll that is undisclosed, votes will be hidden until the poll is ended.
|
||||
*/
|
||||
data class PollUndisclosed(override val optionId: String,
|
||||
override val optionAnswer: String,
|
||||
val isSelected: Boolean
|
||||
) : PollOptionViewState(optionId, optionAnswer)
|
||||
}
|
||||
|
|
|
@ -70,6 +70,7 @@ import im.vector.app.features.pin.PinArgs
|
|||
import im.vector.app.features.pin.PinMode
|
||||
import im.vector.app.features.poll.create.CreatePollActivity
|
||||
import im.vector.app.features.poll.create.CreatePollArgs
|
||||
import im.vector.app.features.poll.create.PollMode
|
||||
import im.vector.app.features.roomdirectory.RoomDirectoryActivity
|
||||
import im.vector.app.features.roomdirectory.RoomDirectoryData
|
||||
import im.vector.app.features.roomdirectory.createroom.CreateRoomActivity
|
||||
|
@ -524,10 +525,10 @@ class DefaultNavigator @Inject constructor(
|
|||
context.startActivity(intent)
|
||||
}
|
||||
|
||||
override fun openCreatePoll(context: Context, roomId: String) {
|
||||
override fun openCreatePoll(context: Context, roomId: String, editedEventId: String?, mode: PollMode) {
|
||||
val intent = CreatePollActivity.getIntent(
|
||||
context,
|
||||
CreatePollArgs(roomId = roomId)
|
||||
CreatePollArgs(roomId = roomId, editedEventId = editedEventId, mode = mode)
|
||||
)
|
||||
context.startActivity(intent)
|
||||
}
|
||||
|
|
|
@ -28,6 +28,7 @@ import im.vector.app.features.displayname.getBestName
|
|||
import im.vector.app.features.login.LoginConfig
|
||||
import im.vector.app.features.media.AttachmentData
|
||||
import im.vector.app.features.pin.PinMode
|
||||
import im.vector.app.features.poll.create.PollMode
|
||||
import im.vector.app.features.roomdirectory.RoomDirectoryData
|
||||
import im.vector.app.features.roomdirectory.roompreview.RoomPreviewData
|
||||
import im.vector.app.features.settings.VectorSettingsActivity
|
||||
|
@ -148,5 +149,5 @@ interface Navigator {
|
|||
|
||||
fun openCallTransfer(context: Context, callId: String)
|
||||
|
||||
fun openCreatePoll(context: Context, roomId: String)
|
||||
fun openCreatePoll(context: Context, roomId: String, editedEventId: String?, mode: PollMode)
|
||||
}
|
||||
|
|
|
@ -17,11 +17,13 @@
|
|||
package im.vector.app.features.poll.create
|
||||
|
||||
import im.vector.app.core.platform.VectorViewModelAction
|
||||
import org.matrix.android.sdk.api.session.room.model.message.PollType
|
||||
|
||||
sealed class CreatePollAction : VectorViewModelAction {
|
||||
data class OnQuestionChanged(val question: String) : CreatePollAction()
|
||||
data class OnOptionChanged(val index: Int, val option: String) : CreatePollAction()
|
||||
data class OnDeleteOption(val index: Int) : CreatePollAction()
|
||||
data class OnPollTypeChanged(val pollType: PollType) : CreatePollAction()
|
||||
object OnAddOption : CreatePollAction()
|
||||
object OnCreatePoll : CreatePollAction()
|
||||
}
|
||||
|
|
|
@ -28,6 +28,7 @@ import im.vector.app.core.ui.list.genericItem
|
|||
import im.vector.app.features.form.formEditTextItem
|
||||
import im.vector.app.features.form.formEditTextWithDeleteItem
|
||||
import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence
|
||||
import org.matrix.android.sdk.api.session.room.model.message.PollType
|
||||
import javax.inject.Inject
|
||||
|
||||
class CreatePollController @Inject constructor(
|
||||
|
@ -47,6 +48,26 @@ class CreatePollController @Inject constructor(
|
|||
val currentState = state ?: return
|
||||
val host = this
|
||||
|
||||
genericItem {
|
||||
id("poll_type_title")
|
||||
style(ItemStyle.BIG_TEXT)
|
||||
title(host.stringProvider.getString(R.string.poll_type_title).toEpoxyCharSequence())
|
||||
}
|
||||
|
||||
pollTypeSelectionItem {
|
||||
id("poll_type_selection")
|
||||
pollType(currentState.pollType)
|
||||
pollTypeChangedListener { _, id ->
|
||||
host.callback?.onPollTypeChanged(
|
||||
if (id == R.id.openPollTypeRadioButton) {
|
||||
PollType.DISCLOSED
|
||||
} else {
|
||||
PollType.UNDISCLOSED
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
genericItem {
|
||||
id("question_title")
|
||||
style(ItemStyle.BIG_TEXT)
|
||||
|
@ -110,5 +131,6 @@ class CreatePollController @Inject constructor(
|
|||
fun onOptionChanged(index: Int, option: String)
|
||||
fun onDeleteOption(index: Int)
|
||||
fun onAddOption()
|
||||
fun onPollTypeChanged(type: PollType)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,18 +23,23 @@ import android.view.View
|
|||
import android.view.ViewGroup
|
||||
import androidx.core.view.isVisible
|
||||
import com.airbnb.mvrx.activityViewModel
|
||||
import com.airbnb.mvrx.args
|
||||
import com.airbnb.mvrx.withState
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.extensions.configureWith
|
||||
import im.vector.app.core.extensions.exhaustive
|
||||
import im.vector.app.core.platform.VectorBaseFragment
|
||||
import im.vector.app.databinding.FragmentCreatePollBinding
|
||||
import im.vector.app.features.poll.create.CreatePollViewModel.Companion.MAX_OPTIONS_COUNT
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import org.matrix.android.sdk.api.session.room.model.message.PollType
|
||||
import javax.inject.Inject
|
||||
|
||||
@Parcelize
|
||||
data class CreatePollArgs(
|
||||
val roomId: String,
|
||||
val editedEventId: String?,
|
||||
val mode: PollMode
|
||||
) : Parcelable
|
||||
|
||||
class CreatePollFragment @Inject constructor(
|
||||
|
@ -42,6 +47,7 @@ class CreatePollFragment @Inject constructor(
|
|||
) : VectorBaseFragment<FragmentCreatePollBinding>(), CreatePollController.Callback {
|
||||
|
||||
private val viewModel: CreatePollViewModel by activityViewModel()
|
||||
private val args: CreatePollArgs by args()
|
||||
|
||||
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentCreatePollBinding {
|
||||
return FragmentCreatePollBinding.inflate(inflater, container, false)
|
||||
|
@ -53,9 +59,20 @@ class CreatePollFragment @Inject constructor(
|
|||
setupToolbar(views.createPollToolbar)
|
||||
.allowBack(useCross = true)
|
||||
|
||||
when (args.mode) {
|
||||
PollMode.CREATE -> {
|
||||
views.createPollToolbar.title = getString(R.string.create_poll_title)
|
||||
views.createPollButton.text = getString(R.string.create_poll_title)
|
||||
}
|
||||
PollMode.EDIT -> {
|
||||
views.createPollToolbar.title = getString(R.string.edit_poll_title)
|
||||
views.createPollButton.text = getString(R.string.edit_poll_title)
|
||||
}
|
||||
}.exhaustive
|
||||
|
||||
views.createPollRecyclerView.configureWith(controller, disableItemAnimation = true)
|
||||
// workaround for https://github.com/vector-im/element-android/issues/4735
|
||||
views.createPollRecyclerView.setItemViewCacheSize(MAX_OPTIONS_COUNT + 4)
|
||||
views.createPollRecyclerView.setItemViewCacheSize(MAX_OPTIONS_COUNT + 6)
|
||||
controller.callback = this
|
||||
|
||||
views.createPollButton.debouncedClicks {
|
||||
|
@ -101,6 +118,10 @@ class CreatePollFragment @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
override fun onPollTypeChanged(type: PollType) {
|
||||
viewModel.handle(CreatePollAction.OnPollTypeChanged(type))
|
||||
}
|
||||
|
||||
private fun handleSuccess() {
|
||||
requireActivity().finish()
|
||||
}
|
||||
|
|
|
@ -24,6 +24,9 @@ import im.vector.app.core.di.MavericksAssistedViewModelFactory
|
|||
import im.vector.app.core.di.hiltMavericksViewModelFactory
|
||||
import im.vector.app.core.platform.VectorViewModel
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.PollType
|
||||
import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent
|
||||
|
||||
class CreatePollViewModel @AssistedInject constructor(
|
||||
@Assisted private val initialState: CreatePollViewState,
|
||||
|
@ -45,6 +48,9 @@ class CreatePollViewModel @AssistedInject constructor(
|
|||
|
||||
init {
|
||||
observeState()
|
||||
initialState.editedEventId?.let {
|
||||
initializeEditedPoll(it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun observeState() {
|
||||
|
@ -61,6 +67,23 @@ class CreatePollViewModel @AssistedInject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun initializeEditedPoll(eventId: String) {
|
||||
val event = room.getTimeLineEvent(eventId) ?: return
|
||||
val content = event.getLastMessageContent() as? MessagePollContent ?: return
|
||||
|
||||
val pollType = content.pollCreationInfo?.kind ?: PollType.DISCLOSED
|
||||
val question = content.pollCreationInfo?.question?.question ?: ""
|
||||
val options = content.pollCreationInfo?.answers?.mapNotNull { it.answer } ?: List(MIN_OPTIONS_COUNT) { "" }
|
||||
|
||||
setState {
|
||||
copy(
|
||||
question = question,
|
||||
options = options,
|
||||
pollType = pollType
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun handle(action: CreatePollAction) {
|
||||
when (action) {
|
||||
CreatePollAction.OnCreatePoll -> handleOnCreatePoll()
|
||||
|
@ -68,6 +91,7 @@ class CreatePollViewModel @AssistedInject constructor(
|
|||
is CreatePollAction.OnDeleteOption -> handleOnDeleteOption(action.index)
|
||||
is CreatePollAction.OnOptionChanged -> handleOnOptionChanged(action.index, action.option)
|
||||
is CreatePollAction.OnQuestionChanged -> handleOnQuestionChanged(action.question)
|
||||
is CreatePollAction.OnPollTypeChanged -> handleOnPollTypeChanged(action.pollType)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -81,12 +105,20 @@ class CreatePollViewModel @AssistedInject constructor(
|
|||
_viewEvents.post(CreatePollViewEvents.NotEnoughOptionsError(requiredOptionsCount = MIN_OPTIONS_COUNT))
|
||||
}
|
||||
else -> {
|
||||
room.sendPoll(state.question, nonEmptyOptions)
|
||||
when (state.mode) {
|
||||
PollMode.CREATE -> room.sendPoll(state.pollType, state.question, nonEmptyOptions)
|
||||
PollMode.EDIT -> sendEditedPoll(state.editedEventId!!, state.pollType, state.question, nonEmptyOptions)
|
||||
}
|
||||
_viewEvents.post(CreatePollViewEvents.Success)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun sendEditedPoll(editedEventId: String, pollType: PollType, question: String, options: List<String>) {
|
||||
val editedEvent = room.getTimeLineEvent(editedEventId) ?: return
|
||||
room.editPoll(editedEvent, pollType, question, options)
|
||||
}
|
||||
|
||||
private fun handleOnAddOption() {
|
||||
setState {
|
||||
val extendedOptions = options + ""
|
||||
|
@ -122,6 +154,14 @@ class CreatePollViewModel @AssistedInject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun handleOnPollTypeChanged(pollType: PollType) {
|
||||
setState {
|
||||
copy(
|
||||
pollType = pollType
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun canCreatePoll(question: String, options: List<String>): Boolean {
|
||||
return question.isNotEmpty() &&
|
||||
options.filter { it.isNotEmpty() }.size >= MIN_OPTIONS_COUNT
|
||||
|
|
|
@ -17,16 +17,22 @@
|
|||
package im.vector.app.features.poll.create
|
||||
|
||||
import com.airbnb.mvrx.MavericksState
|
||||
import org.matrix.android.sdk.api.session.room.model.message.PollType
|
||||
|
||||
data class CreatePollViewState(
|
||||
val roomId: String,
|
||||
val editedEventId: String?,
|
||||
val mode: PollMode,
|
||||
val question: String = "",
|
||||
val options: List<String> = List(CreatePollViewModel.MIN_OPTIONS_COUNT) { "" },
|
||||
val canCreatePoll: Boolean = false,
|
||||
val canAddMoreOptions: Boolean = true
|
||||
val canAddMoreOptions: Boolean = true,
|
||||
val pollType: PollType = PollType.DISCLOSED
|
||||
) : MavericksState {
|
||||
|
||||
constructor(args: CreatePollArgs) : this(
|
||||
roomId = args.roomId
|
||||
roomId = args.roomId,
|
||||
editedEventId = args.editedEventId,
|
||||
mode = args.mode
|
||||
)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* Copyright (c) 2022 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.features.poll.create
|
||||
|
||||
enum class PollMode {
|
||||
CREATE,
|
||||
EDIT
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* Copyright (c) 2022 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.features.poll.create
|
||||
|
||||
import android.widget.RadioGroup
|
||||
import com.airbnb.epoxy.EpoxyAttribute
|
||||
import com.airbnb.epoxy.EpoxyModelClass
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.epoxy.VectorEpoxyHolder
|
||||
import im.vector.app.core.epoxy.VectorEpoxyModel
|
||||
import org.matrix.android.sdk.api.session.room.model.message.PollType
|
||||
|
||||
@EpoxyModelClass(layout = R.layout.item_poll_type_selection)
|
||||
abstract class PollTypeSelectionItem : VectorEpoxyModel<PollTypeSelectionItem.Holder>() {
|
||||
|
||||
@EpoxyAttribute
|
||||
var pollType: PollType = PollType.DISCLOSED
|
||||
|
||||
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash)
|
||||
var pollTypeChangedListener: RadioGroup.OnCheckedChangeListener? = null
|
||||
|
||||
override fun bind(holder: Holder) {
|
||||
super.bind(holder)
|
||||
|
||||
holder.pollTypeRadioGroup.check(
|
||||
when (pollType) {
|
||||
PollType.DISCLOSED -> R.id.openPollTypeRadioButton
|
||||
PollType.UNDISCLOSED -> R.id.closedPollTypeRadioButton
|
||||
}
|
||||
)
|
||||
|
||||
holder.pollTypeRadioGroup.setOnCheckedChangeListener(pollTypeChangedListener)
|
||||
}
|
||||
|
||||
override fun unbind(holder: Holder) {
|
||||
super.unbind(holder)
|
||||
holder.pollTypeRadioGroup.setOnCheckedChangeListener(null)
|
||||
}
|
||||
|
||||
class Holder : VectorEpoxyHolder() {
|
||||
val pollTypeRadioGroup by bind<RadioGroup>(R.id.pollTypeRadioGroup)
|
||||
}
|
||||
}
|
|
@ -196,8 +196,6 @@ class VectorPreferences @Inject constructor(private val context: Context) {
|
|||
|
||||
private const val TAKE_PHOTO_VIDEO_MODE = "TAKE_PHOTO_VIDEO_MODE"
|
||||
|
||||
private const val SETTINGS_LABS_ENABLE_POLLS = "SETTINGS_LABS_ENABLE_POLLS"
|
||||
|
||||
// Possible values for TAKE_PHOTO_VIDEO_MODE
|
||||
const val TAKE_PHOTO_VIDEO_MODE_ALWAYS_ASK = 0
|
||||
const val TAKE_PHOTO_VIDEO_MODE_PHOTO = 1
|
||||
|
@ -991,8 +989,4 @@ class VectorPreferences @Inject constructor(private val context: Context) {
|
|||
putInt(TAKE_PHOTO_VIDEO_MODE, mode)
|
||||
}
|
||||
}
|
||||
|
||||
fun labsEnablePolls(): Boolean {
|
||||
return defaultPrefs.getBoolean(SETTINGS_LABS_ENABLE_POLLS, false)
|
||||
}
|
||||
}
|
||||
|
|
40
vector/src/main/res/layout/item_poll_type_selection.xml
Normal file
40
vector/src/main/res/layout/item_poll_type_selection.xml
Normal file
|
@ -0,0 +1,40 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RadioGroup xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/pollTypeRadioGroup"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/layout_horizontal_margin"
|
||||
android:layout_marginEnd="@dimen/layout_horizontal_margin"
|
||||
android:orientation="vertical"
|
||||
android:paddingBottom="@dimen/layout_vertical_margin">
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/openPollTypeRadioButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="0dp"
|
||||
android:text="@string/open_poll_option_title" />
|
||||
|
||||
<TextView
|
||||
style="@style/TextAppearance.Vector.Caption"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="32dp"
|
||||
android:text="@string/open_poll_option_description" />
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/closedPollTypeRadioButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:minHeight="0dp"
|
||||
android:text="@string/closed_poll_option_title" />
|
||||
|
||||
<TextView
|
||||
style="@style/TextAppearance.Vector.Caption"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="32dp"
|
||||
android:text="@string/closed_poll_option_description" />
|
||||
|
||||
</RadioGroup>
|
|
@ -3707,11 +3707,19 @@
|
|||
<string name="end_poll_confirmation_title">End this poll?</string>
|
||||
<string name="end_poll_confirmation_description">This will stop people from being able to vote and will display the final results of the poll.</string>
|
||||
<string name="end_poll_confirmation_approve_button">End poll</string>
|
||||
<!-- TODO. Remove -->
|
||||
<string name="labs_enable_polls">Enable Polls</string>
|
||||
<string name="poll_response_room_list_preview">Vote casted</string>
|
||||
<string name="poll_response_room_list_preview">Vote cast</string>
|
||||
<string name="poll_end_room_list_preview">Poll ended</string>
|
||||
<string name="delete_poll_dialog_title">Remove poll</string>
|
||||
<string name="delete_poll_dialog_content">Are you sure you want to remove this poll? You won\'t be able to recover it once removed.</string>
|
||||
<string name="edit_poll_title">Edit poll</string>
|
||||
<string name="edit_poll_button">EDIT POLL</string>
|
||||
<string name="poll_type_title">Poll type</string>
|
||||
<string name="open_poll_option_title">Open poll</string>
|
||||
<string name="open_poll_option_description">Voters see results as soon as they have voted</string>
|
||||
<string name="closed_poll_option_title">Closed poll</string>
|
||||
<string name="closed_poll_option_description">Results are only revealed when you end the poll</string>
|
||||
|
||||
<string name="tooltip_attachment_photo">Open camera</string>
|
||||
<string name="tooltip_attachment_gallery">Send images and videos</string>
|
||||
|
|
|
@ -53,14 +53,8 @@
|
|||
|
||||
<im.vector.app.core.preference.VectorSwitchPreference
|
||||
android:defaultValue="false"
|
||||
android:key="SETTINGS_LABS_ENABLE_POLLS"
|
||||
android:title="@string/labs_enable_polls" />
|
||||
|
||||
|
||||
<im.vector.app.core.preference.VectorSwitchPreference
|
||||
android:defaultValue="false"
|
||||
android:key="SETTINGS_LABS_AUTO_REPORT_UISI"
|
||||
android:title="@string/labs_auto_report_uisi"
|
||||
android:summary="@string/labs_auto_report_uisi_desc"/>
|
||||
android:key="SETTINGS_LABS_AUTO_REPORT_UISI"
|
||||
android:summary="@string/labs_auto_report_uisi_desc"
|
||||
android:title="@string/labs_auto_report_uisi" />
|
||||
|
||||
</androidx.preference.PreferenceScreen>
|
Loading…
Reference in a new issue