Allow editing polls.
This commit is contained in:
parent
381dd5343a
commit
c3d7a253e4
@ -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<String>): Cancelable
|
||||
|
||||
/**
|
||||
* Edit a text message body. Limited to "m.text" contentType
|
||||
* @param targetEvent The event to edit
|
||||
|
@ -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<MessagePollResponseContent>(catchError = true)?.let {
|
||||
handleResponse(realm, event, it, roomId, isLocalEcho)
|
||||
|
@ -112,6 +112,12 @@ internal class DefaultRelationService @AssistedInject constructor(
|
||||
.executeBy(taskExecutor)
|
||||
}
|
||||
|
||||
override fun editPoll(targetEvent: TimelineEvent,
|
||||
question: String,
|
||||
options: List<String>): Cancelable {
|
||||
return eventEditor.editPoll(targetEvent, question, options)
|
||||
}
|
||||
|
||||
override fun editTextMessage(targetEvent: TimelineEvent,
|
||||
msgType: String,
|
||||
newBodyText: CharSequence,
|
||||
|
@ -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<String>): 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,
|
||||
|
@ -124,6 +124,41 @@ internal class LocalEchoEventFactory @Inject constructor(
|
||||
))
|
||||
}
|
||||
|
||||
private fun createPollContent(question: String,
|
||||
options: List<String>): 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<String>): 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<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)
|
||||
val localId = LocalEcho.createLocalEchoId()
|
||||
return Event(
|
||||
roomId = roomId,
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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)) {
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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<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)
|
||||
@ -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)
|
||||
|
@ -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<MessagePollContent>(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<String>) {
|
||||
val editedEvent = room.getTimeLineEvent(editedEventId) ?: return
|
||||
room.editPoll(editedEvent, question, options)
|
||||
}
|
||||
|
||||
private fun handleOnAddOption() {
|
||||
setState {
|
||||
val extendedOptions = options + ""
|
||||
|
@ -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<String> = 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
|
||||
)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
@ -3704,6 +3704,8 @@
|
||||
<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="tooltip_attachment_photo">Open camera</string>
|
||||
<string name="tooltip_attachment_gallery">Send images and videos</string>
|
||||
|
Loading…
x
Reference in New Issue
Block a user