From 381dd5343ac124cde3bd0a24c69f088ece5a01e0 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Wed, 19 Jan 2022 13:00:21 +0300 Subject: [PATCH 01/10] Show edit action for poll messages if it is not voted and closed. --- .../timeline/action/MessageActionsViewModel.kt | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt index ff7d555ee3..3cecb59675 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt @@ -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() 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 + } } From c3d7a253e477fcf1d5499ae1ffbb076c16bdc3a7 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Thu, 20 Jan 2022 17:41:03 +0300 Subject: [PATCH 02/10] Allow editing polls. --- .../room/model/relation/RelationService.kt | 10 ++++ .../EventRelationsAggregationProcessor.kt | 10 ++++ .../room/relation/DefaultRelationService.kt | 6 +++ .../session/room/relation/EventEditor.kt | 36 ++++++++++++-- .../room/send/LocalEchoEventFactory.kt | 49 ++++++++++++++----- .../home/room/detail/RoomDetailFragment.kt | 8 ++- .../timeline/action/EventSharedAction.kt | 2 +- .../action/MessageActionsViewModel.kt | 4 +- .../features/navigation/DefaultNavigator.kt | 5 +- .../app/features/navigation/Navigator.kt | 3 +- .../poll/create/CreatePollFragment.kt | 16 ++++++ .../poll/create/CreatePollViewModel.kt | 30 +++++++++++- .../poll/create/CreatePollViewState.kt | 6 ++- .../app/features/poll/create/PollMode.kt | 22 +++++++++ vector/src/main/res/values/strings.xml | 2 + 15 files changed, 182 insertions(+), 27 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/features/poll/create/PollMode.kt diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationService.kt index 59d84ef40f..422ae8f74a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationService.kt @@ -64,6 +64,16 @@ interface RelationService { fun undoReaction(targetEventId: String, reaction: String): Cancelable + /** + * Edit a poll. + * @param targetEvent The poll event to edit + * @param question The edited question + * @param options The edited options + */ + fun editPoll(targetEvent: TimelineEvent, + question: String, + options: List): Cancelable + /** * Edit a text message body. Limited to "m.text" contentType * @param targetEvent The event to edit diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt index 62b6d626f5..f42d55ca2a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt @@ -34,6 +34,7 @@ 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 @@ -79,6 +80,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 +210,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(catchError = true)?.let { handleResponse(realm, event, it, roomId, isLocalEcho) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/DefaultRelationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/DefaultRelationService.kt index 07927b1412..41dadf7692 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/DefaultRelationService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/DefaultRelationService.kt @@ -112,6 +112,12 @@ internal class DefaultRelationService @AssistedInject constructor( .executeBy(taskExecutor) } + override fun editPoll(targetEvent: TimelineEvent, + question: String, + options: List): Cancelable { + return eventEditor.editPoll(targetEvent, question, options) + } + override fun editTextMessage(targetEvent: TimelineEvent, msgType: String, newBodyText: CharSequence, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/EventEditor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/EventEditor.kt index a666d40fc3..f6364f8945 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/EventEditor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/EventEditor.kt @@ -46,13 +46,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 +58,36 @@ internal class EventEditor @Inject constructor(private val eventSenderProcessor: } } + fun editPoll(targetEvent: TimelineEvent, + question: String, + options: List): Cancelable { + val roomId = targetEvent.roomId + if (targetEvent.root.sendState.hasFailed()) { + val editedEvent = eventFactory.createPollEvent(roomId, question, options).copy( + eventId = targetEvent.eventId + ) + return sendFailedEvent(targetEvent, editedEvent) + } else if (targetEvent.root.sendState.isSent()) { + val event = eventFactory + .createPollReplaceEvent(roomId, 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, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt index c4caedc407..6bef611568 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt @@ -124,6 +124,41 @@ internal class LocalEchoEventFactory @Inject constructor( )) } + private fun createPollContent(question: String, + options: List): MessagePollContent { + return MessagePollContent( + pollCreationInfo = PollCreationInfo( + question = PollQuestion( + question = question + ), + answers = options.mapIndexed { index, option -> + PollAnswer( + id = "$index-$option", + answer = option + ) + } + ) + ) + } + + fun createPollReplaceEvent(roomId: String, + targetEventId: String, + question: String, + options: List): Event { + val newContent = MessagePollContent( + relatesTo = RelationDefaultContent(RelationType.REPLACE, targetEventId), + newContent = createPollContent(question, options).toContent() + ) + return Event( + roomId = roomId, + originServerTs = dummyOriginServerTs(), + senderId = userId, + eventId = targetEventId, + type = EventType.POLL_START, + content = newContent.toContent() + ) + } + fun createPollReplyEvent(roomId: String, pollEventId: String, answerId: String): Event { @@ -151,19 +186,7 @@ internal class LocalEchoEventFactory @Inject constructor( fun createPollEvent(roomId: String, question: String, options: List): 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) val localId = LocalEcho.createLocalEchoId() return Event( roomId = roomId, diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt index deaa56776e..3c325449e5 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt @@ -174,6 +174,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 @@ -201,6 +202,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 @@ -2014,7 +2016,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) @@ -2226,7 +2230,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 } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/EventSharedAction.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/EventSharedAction.kt index 39e04e8ae4..d7a57e6577 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/EventSharedAction.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/EventSharedAction.kt @@ -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) : diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt index 3cecb59675..8e69e2d932 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt @@ -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)) { diff --git a/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt b/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt index c219d7feff..b8b663cfcf 100644 --- a/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt +++ b/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt @@ -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) } diff --git a/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt b/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt index 2f152b649f..a6d9268888 100644 --- a/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt +++ b/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt @@ -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) } diff --git a/vector/src/main/java/im/vector/app/features/poll/create/CreatePollFragment.kt b/vector/src/main/java/im/vector/app/features/poll/create/CreatePollFragment.kt index 1d807654e8..f1af321d45 100644 --- a/vector/src/main/java/im/vector/app/features/poll/create/CreatePollFragment.kt +++ b/vector/src/main/java/im/vector/app/features/poll/create/CreatePollFragment.kt @@ -23,9 +23,11 @@ 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 @@ -35,6 +37,8 @@ import javax.inject.Inject @Parcelize data class CreatePollArgs( val roomId: String, + val editedEventId: String?, + val mode: PollMode ) : Parcelable class CreatePollFragment @Inject constructor( @@ -42,6 +46,7 @@ class CreatePollFragment @Inject constructor( ) : VectorBaseFragment(), 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) @@ -51,6 +56,17 @@ class CreatePollFragment @Inject constructor( super.onViewCreated(view, savedInstanceState) vectorBaseActivity.setSupportActionBar(views.createPollToolbar) + when (args.mode) { + PollMode.CREATE -> { + views.createPollTitle.text = getString(R.string.create_poll_title) + views.createPollButton.text = getString(R.string.create_poll_title) + } + PollMode.EDIT -> { + views.createPollTitle.text = 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) diff --git a/vector/src/main/java/im/vector/app/features/poll/create/CreatePollViewModel.kt b/vector/src/main/java/im/vector/app/features/poll/create/CreatePollViewModel.kt index b5e66ae682..1d5ee8416e 100644 --- a/vector/src/main/java/im/vector/app/features/poll/create/CreatePollViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/poll/create/CreatePollViewModel.kt @@ -24,6 +24,8 @@ 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.events.model.toModel +import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent class CreatePollViewModel @AssistedInject constructor( @Assisted private val initialState: CreatePollViewState, @@ -45,6 +47,9 @@ class CreatePollViewModel @AssistedInject constructor( init { observeState() + initialState.editedEventId?.let { + initializeEditedPoll(it) + } } private fun observeState() { @@ -61,6 +66,21 @@ class CreatePollViewModel @AssistedInject constructor( } } + private fun initializeEditedPoll(eventId: String) { + val event = room.getTimeLineEvent(eventId) ?: return + val content = event.root.getClearContent()?.toModel(catchError = true) ?: return + + val question = content.pollCreationInfo?.question?.question ?: "" + val options = content.pollCreationInfo?.answers?.mapNotNull { it.answer } ?: List(MIN_OPTIONS_COUNT) { "" } + + setState { + copy( + question = question, + options = options + ) + } + } + override fun handle(action: CreatePollAction) { when (action) { CreatePollAction.OnCreatePoll -> handleOnCreatePoll() @@ -81,12 +101,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.question, nonEmptyOptions) + PollMode.EDIT -> sendEditedPoll(state.editedEventId!!, state.question, nonEmptyOptions) + } _viewEvents.post(CreatePollViewEvents.Success) } } } + private fun sendEditedPoll(editedEventId: String, question: String, options: List) { + val editedEvent = room.getTimeLineEvent(editedEventId) ?: return + room.editPoll(editedEvent, question, options) + } + private fun handleOnAddOption() { setState { val extendedOptions = options + "" diff --git a/vector/src/main/java/im/vector/app/features/poll/create/CreatePollViewState.kt b/vector/src/main/java/im/vector/app/features/poll/create/CreatePollViewState.kt index a9060cc89f..c56f0257e6 100644 --- a/vector/src/main/java/im/vector/app/features/poll/create/CreatePollViewState.kt +++ b/vector/src/main/java/im/vector/app/features/poll/create/CreatePollViewState.kt @@ -20,6 +20,8 @@ import com.airbnb.mvrx.MavericksState data class CreatePollViewState( val roomId: String, + val editedEventId: String?, + val mode: PollMode, val question: String = "", val options: List = List(CreatePollViewModel.MIN_OPTIONS_COUNT) { "" }, val canCreatePoll: Boolean = false, @@ -27,6 +29,8 @@ data class CreatePollViewState( ) : MavericksState { constructor(args: CreatePollArgs) : this( - roomId = args.roomId + roomId = args.roomId, + editedEventId = args.editedEventId, + mode = args.mode ) } diff --git a/vector/src/main/java/im/vector/app/features/poll/create/PollMode.kt b/vector/src/main/java/im/vector/app/features/poll/create/PollMode.kt new file mode 100644 index 0000000000..0007589d10 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/poll/create/PollMode.kt @@ -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 +} diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index 27bab7d66f..4f341e7854 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -3704,6 +3704,8 @@ Poll ended Remove poll Are you sure you want to remove this poll? You won\'t be able to recover it once removed. + Edit poll + EDIT POLL Open camera Send images and videos From ea9e5183dcb6277e072a9b5aab43f668ace0284d Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Fri, 21 Jan 2022 16:20:08 +0300 Subject: [PATCH 03/10] Fix rendering edited polls in timeline. --- .../android/sdk/api/session/room/timeline/TimelineEvent.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt index 45dc322420..3f7d2d1278 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt @@ -133,7 +133,7 @@ fun TimelineEvent.getEditedEventId(): String? { fun TimelineEvent.getLastMessageContent(): MessageContent? { return when (root.getClearType()) { EventType.STICKER -> root.getClearContent().toModel() - EventType.POLL_START -> root.getClearContent().toModel() + EventType.POLL_START -> (annotations?.editSummary?.latestContent ?: root.getClearContent()).toModel() else -> (annotations?.editSummary?.latestContent ?: root.getClearContent()).toModel() } } From a871ce26c22bc6fd8187dab875dae37f916f538b Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Fri, 21 Jan 2022 18:07:39 +0300 Subject: [PATCH 04/10] Fix event id of poll replace events. --- .../sdk/internal/session/room/send/LocalEchoEventFactory.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt index 6bef611568..8e44314371 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt @@ -149,11 +149,12 @@ internal class LocalEchoEventFactory @Inject constructor( relatesTo = RelationDefaultContent(RelationType.REPLACE, targetEventId), newContent = createPollContent(question, options).toContent() ) + val localId = LocalEcho.createLocalEchoId() return Event( roomId = roomId, originServerTs = dummyOriginServerTs(), senderId = userId, - eventId = targetEventId, + eventId = localId, type = EventType.POLL_START, content = newContent.toContent() ) From 5d07c71dcf478ea93a76219192b687eb00067217 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Fri, 21 Jan 2022 18:23:05 +0300 Subject: [PATCH 05/10] Open edit poll screen with the last edited content if exists. --- .../im/vector/app/features/poll/create/CreatePollViewModel.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/app/features/poll/create/CreatePollViewModel.kt b/vector/src/main/java/im/vector/app/features/poll/create/CreatePollViewModel.kt index 1d5ee8416e..875dfbe3a8 100644 --- a/vector/src/main/java/im/vector/app/features/poll/create/CreatePollViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/poll/create/CreatePollViewModel.kt @@ -26,6 +26,7 @@ import im.vector.app.core.platform.VectorViewModel import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent +import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent class CreatePollViewModel @AssistedInject constructor( @Assisted private val initialState: CreatePollViewState, @@ -68,7 +69,7 @@ class CreatePollViewModel @AssistedInject constructor( private fun initializeEditedPoll(eventId: String) { val event = room.getTimeLineEvent(eventId) ?: return - val content = event.root.getClearContent()?.toModel(catchError = true) ?: return + val content = event.getLastMessageContent() as? MessagePollContent ?: return val question = content.pollCreationInfo?.question?.question ?: "" val options = content.pollCreationInfo?.answers?.mapNotNull { it.answer } ?: List(MIN_OPTIONS_COUNT) { "" } From 9dd48045f69c188252e8dfd326bf8899ef090b9c Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Sun, 23 Jan 2022 19:32:13 +0300 Subject: [PATCH 06/10] Invalidate previous votes for edited polls. --- .../EventRelationsAggregationProcessor.kt | 37 ++++++++++++++++++- .../room/send/LocalEchoEventFactory.kt | 5 ++- .../timeline/factory/MessageItemFactory.kt | 11 +++++- .../room/detail/timeline/item/PollItem.kt | 8 +++- .../poll/create/CreatePollViewModel.kt | 1 - 5 files changed, 55 insertions(+), 7 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt index f42d55ca2a..1577f3057f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt @@ -39,6 +39,8 @@ import org.matrix.android.sdk.api.session.room.model.message.MessagePollResponse 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 @@ -56,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 @@ -64,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( @@ -284,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() + ?.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 }) { @@ -325,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) { @@ -365,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) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt index 8e44314371..3167a5b0b9 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt @@ -61,6 +61,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 /** @@ -131,9 +132,9 @@ internal class LocalEchoEventFactory @Inject constructor( question = PollQuestion( question = question ), - answers = options.mapIndexed { index, option -> + answers = options.map { option -> PollAnswer( - id = "$index-$option", + id = UUID.randomUUID().toString(), answer = option ) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt index 21af8b82cb..48d9efae47 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt @@ -220,13 +220,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) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollItem.kt index 1308fa49c8..0cad02827b 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollItem.kt @@ -22,6 +22,7 @@ import androidx.core.view.children import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import im.vector.app.R +import im.vector.app.core.epoxy.charsequence.EpoxyCharSequence import im.vector.app.features.home.room.detail.RoomDetailAction import im.vector.app.features.home.room.detail.timeline.TimelineEventController @@ -29,7 +30,7 @@ import im.vector.app.features.home.room.detail.timeline.TimelineEventController abstract class PollItem : AbsMessageItem() { @EpoxyAttribute - var pollQuestion: String? = null + var pollQuestion: EpoxyCharSequence? = null @EpoxyAttribute var callback: TimelineEventController.Callback? = null @@ -43,6 +44,9 @@ abstract class PollItem : AbsMessageItem() { @EpoxyAttribute var totalVotesText: String? = null + @EpoxyAttribute + var edited: Boolean = false + @EpoxyAttribute lateinit var optionViewStates: List @@ -52,7 +56,7 @@ abstract class PollItem : AbsMessageItem() { renderSendState(holder.view, holder.questionTextView) - holder.questionTextView.text = pollQuestion + holder.questionTextView.text = pollQuestion?.charSequence holder.totalVotesTextView.text = totalVotesText while (holder.optionsContainer.childCount < optionViewStates.size) { diff --git a/vector/src/main/java/im/vector/app/features/poll/create/CreatePollViewModel.kt b/vector/src/main/java/im/vector/app/features/poll/create/CreatePollViewModel.kt index 875dfbe3a8..4ac1b64aef 100644 --- a/vector/src/main/java/im/vector/app/features/poll/create/CreatePollViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/poll/create/CreatePollViewModel.kt @@ -24,7 +24,6 @@ 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.events.model.toModel import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent From b0b92c062e969e718178ee5002e1d3a13dcd9cca Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Mon, 24 Jan 2022 14:31:50 +0300 Subject: [PATCH 07/10] Undisclosed poll implementation. --- .../room/model/message/PollCreationInfo.kt | 2 +- .../session/room/model/message/PollType.kt | 35 ++++++++++++ .../room/model/relation/RelationService.kt | 3 + .../sdk/api/session/room/send/SendService.kt | 4 +- .../room/relation/DefaultRelationService.kt | 4 +- .../session/room/relation/EventEditor.kt | 6 +- .../session/room/send/DefaultSendService.kt | 5 +- .../room/send/LocalEchoEventFactory.kt | 11 +++- .../timeline/factory/MessageItemFactory.kt | 13 ++++- .../room/detail/timeline/item/PollItem.kt | 2 +- .../detail/timeline/item/PollOptionView.kt | 18 ++++-- .../timeline/item/PollOptionViewState.kt | 8 +++ .../features/poll/create/CreatePollAction.kt | 2 + .../poll/create/CreatePollController.kt | 22 +++++++ .../poll/create/CreatePollFragment.kt | 11 +++- .../poll/create/CreatePollViewModel.kt | 22 +++++-- .../poll/create/CreatePollViewState.kt | 4 +- .../poll/create/PollTypeSelectionItem.kt | 57 +++++++++++++++++++ .../res/layout/item_poll_type_selection.xml | 40 +++++++++++++ vector/src/main/res/values/strings.xml | 7 ++- 20 files changed, 247 insertions(+), 29 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollType.kt create mode 100644 vector/src/main/java/im/vector/app/features/poll/create/PollTypeSelectionItem.kt create mode 100644 vector/src/main/res/layout/item_poll_type_selection.xml diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollCreationInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollCreationInfo.kt index e652514b92..a82c01b159 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollCreationInfo.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollCreationInfo.kt @@ -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? = null ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollType.kt new file mode 100644 index 0000000000..3a8066b9bc --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollType.kt @@ -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 +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationService.kt index 422ae8f74a..763d4bb892 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationService.kt @@ -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 @@ -66,11 +67,13 @@ interface RelationService { /** * 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): Cancelable diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt index 606500c4e7..5e1b430207 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt @@ -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): Cancelable + fun sendPoll(pollType: PollType, question: String, options: List): Cancelable /** * Method to send a poll response. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/DefaultRelationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/DefaultRelationService.kt index 41dadf7692..cbcc108ddd 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/DefaultRelationService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/DefaultRelationService.kt @@ -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 @@ -113,9 +114,10 @@ internal class DefaultRelationService @AssistedInject constructor( } override fun editPoll(targetEvent: TimelineEvent, + pollType: PollType, question: String, options: List): Cancelable { - return eventEditor.editPoll(targetEvent, question, options) + return eventEditor.editPoll(targetEvent, pollType, question, options) } override fun editTextMessage(targetEvent: TimelineEvent, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/EventEditor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/EventEditor.kt index f6364f8945..a40a8df443 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/EventEditor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/EventEditor.kt @@ -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 @@ -59,17 +60,18 @@ internal class EventEditor @Inject constructor(private val eventSenderProcessor: } fun editPoll(targetEvent: TimelineEvent, + pollType: PollType, question: String, options: List): Cancelable { val roomId = targetEvent.roomId if (targetEvent.root.sendState.hasFailed()) { - val editedEvent = eventFactory.createPollEvent(roomId, question, options).copy( + 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, targetEvent.eventId, question, options) + .createPollReplaceEvent(roomId, pollType, targetEvent.eventId, question, options) return sendReplaceEvent(roomId, event) } else { Timber.w("Can't edit a sending event") diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt index fb2fb3950a..9d105120e2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt @@ -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): Cancelable { - return localEchoEventFactory.createPollEvent(roomId, question, options) + override fun sendPoll(pollType: PollType, question: String, options: List): Cancelable { + return localEchoEventFactory.createPollEvent(roomId, pollType, question, options) .also { createLocalEcho(it) } .let { sendEvent(it) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt index 3167a5b0b9..72ae688c4b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt @@ -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 @@ -126,12 +127,14 @@ internal class LocalEchoEventFactory @Inject constructor( } private fun createPollContent(question: String, - options: List): MessagePollContent { + options: List, + pollType: PollType): MessagePollContent { return MessagePollContent( pollCreationInfo = PollCreationInfo( question = PollQuestion( question = question ), + kind = pollType, answers = options.map { option -> PollAnswer( id = UUID.randomUUID().toString(), @@ -143,12 +146,13 @@ internal class LocalEchoEventFactory @Inject constructor( } fun createPollReplaceEvent(roomId: String, + pollType: PollType, targetEventId: String, question: String, options: List): Event { val newContent = MessagePollContent( relatesTo = RelationDefaultContent(RelationType.REPLACE, targetEventId), - newContent = createPollContent(question, options).toContent() + newContent = createPollContent(question, options, pollType).toContent() ) val localId = LocalEcho.createLocalEchoId() return Event( @@ -186,9 +190,10 @@ internal class LocalEchoEventFactory @Inject constructor( } fun createPollEvent(roomId: String, + pollType: PollType, question: String, options: List): Event { - val content = createPollContent(question, options) + val content = createPollContent(question, options, pollType) val localId = LocalEcho.createLocalEchoId() return Event( roomId = roomId, diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt index abe9648345..d6bfc4ece7 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt @@ -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) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollItem.kt index 0cad02827b..b660ee9a59 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollItem.kt @@ -22,9 +22,9 @@ import androidx.core.view.children import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import im.vector.app.R -import im.vector.app.core.epoxy.charsequence.EpoxyCharSequence 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() { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollOptionView.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollOptionView.kt index 2af445041b..2be933d9c3 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollOptionView.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollOptionView.kt @@ -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 diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollOptionViewState.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollOptionViewState.kt index 5291e7f20a..18b442b683 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollOptionViewState.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollOptionViewState.kt @@ -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 closed, 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) } diff --git a/vector/src/main/java/im/vector/app/features/poll/create/CreatePollAction.kt b/vector/src/main/java/im/vector/app/features/poll/create/CreatePollAction.kt index 182750fbd2..5fddcac568 100644 --- a/vector/src/main/java/im/vector/app/features/poll/create/CreatePollAction.kt +++ b/vector/src/main/java/im/vector/app/features/poll/create/CreatePollAction.kt @@ -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() } diff --git a/vector/src/main/java/im/vector/app/features/poll/create/CreatePollController.kt b/vector/src/main/java/im/vector/app/features/poll/create/CreatePollController.kt index 3b170ef799..2a977684e0 100644 --- a/vector/src/main/java/im/vector/app/features/poll/create/CreatePollController.kt +++ b/vector/src/main/java/im/vector/app/features/poll/create/CreatePollController.kt @@ -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) } } diff --git a/vector/src/main/java/im/vector/app/features/poll/create/CreatePollFragment.kt b/vector/src/main/java/im/vector/app/features/poll/create/CreatePollFragment.kt index b2547e7ff3..4483b00158 100644 --- a/vector/src/main/java/im/vector/app/features/poll/create/CreatePollFragment.kt +++ b/vector/src/main/java/im/vector/app/features/poll/create/CreatePollFragment.kt @@ -32,6 +32,7 @@ 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 @@ -60,18 +61,18 @@ class CreatePollFragment @Inject constructor( when (args.mode) { PollMode.CREATE -> { - views.createPollTitle.text = getString(R.string.create_poll_title) + views.createPollToolbar.title = getString(R.string.create_poll_title) views.createPollButton.text = getString(R.string.create_poll_title) } PollMode.EDIT -> { - views.createPollTitle.text = getString(R.string.edit_poll_title) + 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 { @@ -117,6 +118,10 @@ class CreatePollFragment @Inject constructor( } } + override fun onPollTypeChanged(type: PollType) { + viewModel.handle(CreatePollAction.OnPollTypeChanged(type)) + } + private fun handleSuccess() { requireActivity().finish() } diff --git a/vector/src/main/java/im/vector/app/features/poll/create/CreatePollViewModel.kt b/vector/src/main/java/im/vector/app/features/poll/create/CreatePollViewModel.kt index 4ac1b64aef..7750e6d909 100644 --- a/vector/src/main/java/im/vector/app/features/poll/create/CreatePollViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/poll/create/CreatePollViewModel.kt @@ -25,6 +25,7 @@ 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( @@ -70,13 +71,15 @@ class CreatePollViewModel @AssistedInject constructor( 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 + options = options, + pollType = pollType ) } } @@ -88,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) } } @@ -102,17 +106,17 @@ class CreatePollViewModel @AssistedInject constructor( } else -> { when (state.mode) { - PollMode.CREATE -> room.sendPoll(state.question, nonEmptyOptions) - PollMode.EDIT -> sendEditedPoll(state.editedEventId!!, state.question, nonEmptyOptions) + 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, question: String, options: List) { + private fun sendEditedPoll(editedEventId: String, pollType: PollType, question: String, options: List) { val editedEvent = room.getTimeLineEvent(editedEventId) ?: return - room.editPoll(editedEvent, question, options) + room.editPoll(editedEvent, pollType, question, options) } private fun handleOnAddOption() { @@ -150,6 +154,14 @@ class CreatePollViewModel @AssistedInject constructor( } } + private fun handleOnPollTypeChanged(pollType: PollType) { + setState { + copy( + pollType = pollType + ) + } + } + private fun canCreatePoll(question: String, options: List): Boolean { return question.isNotEmpty() && options.filter { it.isNotEmpty() }.size >= MIN_OPTIONS_COUNT diff --git a/vector/src/main/java/im/vector/app/features/poll/create/CreatePollViewState.kt b/vector/src/main/java/im/vector/app/features/poll/create/CreatePollViewState.kt index c56f0257e6..175d1b0116 100644 --- a/vector/src/main/java/im/vector/app/features/poll/create/CreatePollViewState.kt +++ b/vector/src/main/java/im/vector/app/features/poll/create/CreatePollViewState.kt @@ -17,6 +17,7 @@ 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, @@ -25,7 +26,8 @@ data class CreatePollViewState( val question: String = "", val options: List = 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( diff --git a/vector/src/main/java/im/vector/app/features/poll/create/PollTypeSelectionItem.kt b/vector/src/main/java/im/vector/app/features/poll/create/PollTypeSelectionItem.kt new file mode 100644 index 0000000000..1b24a70cb9 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/poll/create/PollTypeSelectionItem.kt @@ -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() { + + @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(R.id.pollTypeRadioGroup) + } +} diff --git a/vector/src/main/res/layout/item_poll_type_selection.xml b/vector/src/main/res/layout/item_poll_type_selection.xml new file mode 100644 index 0000000000..c1c7626c13 --- /dev/null +++ b/vector/src/main/res/layout/item_poll_type_selection.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index 235715e5d0..95d9c3fc5f 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -3708,12 +3708,17 @@ This will stop people from being able to vote and will display the final results of the poll. End poll Enable Polls - Vote casted + Vote cast Poll ended Remove poll Are you sure you want to remove this poll? You won\'t be able to recover it once removed. Edit poll EDIT POLL + Poll type + Open poll + Voters see results as soon as they have voted + Closed poll + Results are only revealed when you end the poll Open camera Send images and videos From 749194b27c03d436e4b4e0a99ac2164cd2c56fde Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Mon, 24 Jan 2022 14:32:27 +0300 Subject: [PATCH 08/10] Delabs polls. --- .../app/core/resources/UserPreferencesProvider.kt | 4 ---- .../features/home/room/detail/RoomDetailFragment.kt | 1 - .../timeline/helper/TimelineEventVisibilityHelper.kt | 2 -- .../app/features/settings/VectorPreferences.kt | 6 ------ vector/src/main/res/xml/vector_settings_labs.xml | 12 +++--------- 5 files changed, 3 insertions(+), 22 deletions(-) diff --git a/vector/src/main/java/im/vector/app/core/resources/UserPreferencesProvider.kt b/vector/src/main/java/im/vector/app/core/resources/UserPreferencesProvider.kt index e7cabd1540..9ab3b9bf45 100644 --- a/vector/src/main/java/im/vector/app/core/resources/UserPreferencesProvider.kt +++ b/vector/src/main/java/im/vector/app/core/resources/UserPreferencesProvider.kt @@ -48,8 +48,4 @@ class UserPreferencesProvider @Inject constructor(private val vectorPreferences: fun shouldShowAvatarDisplayNameChanges(): Boolean { return vectorPreferences.showAvatarDisplayNameChangeMessages() } - - fun shouldShowPolls(): Boolean { - return vectorPreferences.labsEnablePolls() - } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt index 041c8ef5a2..72dbf1436f 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt @@ -1373,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) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventVisibilityHelper.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventVisibilityHelper.kt index 01a92decc9..580d7d18cf 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventVisibilityHelper.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventVisibilityHelper.kt @@ -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 } diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt index f46ab86c7c..611debf339 100755 --- a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt @@ -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) - } } diff --git a/vector/src/main/res/xml/vector_settings_labs.xml b/vector/src/main/res/xml/vector_settings_labs.xml index d2e9df4985..7acb1d5016 100644 --- a/vector/src/main/res/xml/vector_settings_labs.xml +++ b/vector/src/main/res/xml/vector_settings_labs.xml @@ -53,14 +53,8 @@ - - - + android:key="SETTINGS_LABS_AUTO_REPORT_UISI" + android:summary="@string/labs_auto_report_uisi_desc" + android:title="@string/labs_auto_report_uisi" /> \ No newline at end of file From 95020a81c5f574881777aad293840e555f339274 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Mon, 24 Jan 2022 14:32:40 +0300 Subject: [PATCH 09/10] Changelog added. --- changelog.d/5036.feature | 1 + changelog.d/5037.feature | 1 + 2 files changed, 2 insertions(+) create mode 100644 changelog.d/5036.feature create mode 100644 changelog.d/5037.feature diff --git a/changelog.d/5036.feature b/changelog.d/5036.feature new file mode 100644 index 0000000000..1b72644a1f --- /dev/null +++ b/changelog.d/5036.feature @@ -0,0 +1 @@ +Allow editing polls \ No newline at end of file diff --git a/changelog.d/5037.feature b/changelog.d/5037.feature new file mode 100644 index 0000000000..45b27d4375 --- /dev/null +++ b/changelog.d/5037.feature @@ -0,0 +1 @@ +Support undisclosed polls \ No newline at end of file From 04ffb951c32e028c383695e6a0614ca953450158 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Tue, 25 Jan 2022 12:38:11 +0300 Subject: [PATCH 10/10] Code review fixes. --- .../home/room/detail/timeline/item/PollOptionViewState.kt | 2 +- vector/src/main/res/values/strings.xml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollOptionViewState.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollOptionViewState.kt index 18b442b683..ae900d0406 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollOptionViewState.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollOptionViewState.kt @@ -53,7 +53,7 @@ sealed class PollOptionViewState(open val optionId: String, ) : PollOptionViewState(optionId, optionAnswer) /** - * Represent a poll that is closed, votes will be hidden until the poll is ended. + * 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, diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index 95d9c3fc5f..1d25d58027 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -3707,6 +3707,7 @@ End this poll? This will stop people from being able to vote and will display the final results of the poll. End poll + Enable Polls Vote cast Poll ended