diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index 899f88db0..498591a72 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -1836,6 +1836,10 @@ Tap the + to start adding people."; "poll_edit_form_post_failure_subtitle" = "Please try again"; +"poll_edit_form_update_failure_title" = "Failed to update poll"; + +"poll_edit_form_update_failure_subtitle" = "Please try again"; + "poll_timeline_one_vote" = "1 vote"; "poll_timeline_votes_count" = "%lu votes"; diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index 96d4f6299..f6f0d6a19 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -2548,6 +2548,14 @@ public class VectorL10n: NSObject { return VectorL10n.tr("Vector", "poll_edit_form_question_or_topic") } /// Please try again + public static var pollEditFormUpdateFailureSubtitle: String { + return VectorL10n.tr("Vector", "poll_edit_form_update_failure_subtitle") + } + /// Failed to update poll + public static var pollEditFormUpdateFailureTitle: String { + return VectorL10n.tr("Vector", "poll_edit_form_update_failure_title") + } + /// Please try again public static var pollTimelineNotClosedSubtitle: String { return VectorL10n.tr("Vector", "poll_timeline_not_closed_subtitle") } diff --git a/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m b/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m index 46abe46ab..1533da277 100644 --- a/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m +++ b/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m @@ -1577,6 +1577,11 @@ static NSString *const kHTMLATagRegexPattern = @"([^<]*)"; } case MXEventTypePollStart: { + if (event.isEditEvent) + { + return nil; + } + displayText = [MXEventContentPollStart modelFromJSON:event.content].question; break; } diff --git a/Riot/Modules/Room/CellData/RoomBubbleCellData.m b/Riot/Modules/Room/CellData/RoomBubbleCellData.m index 81867be76..c90287f4d 100644 --- a/Riot/Modules/Room/CellData/RoomBubbleCellData.m +++ b/Riot/Modules/Room/CellData/RoomBubbleCellData.m @@ -279,6 +279,10 @@ NSString *const URLPreviewDidUpdateNotification = @"URLPreviewDidUpdateNotificat if (self.tag == RoomBubbleCellDataTagPoll) { + if (self.events.lastObject.isEditEvent) { + return YES; + } + return NO; } diff --git a/Riot/Modules/Room/RoomCoordinator.swift b/Riot/Modules/Room/RoomCoordinator.swift index fec16dd93..da10cfbcf 100644 --- a/Riot/Modules/Room/RoomCoordinator.swift +++ b/Riot/Modules/Room/RoomCoordinator.swift @@ -246,6 +246,29 @@ final class RoomCoordinator: NSObject, RoomCoordinatorProtocol { navigationRouter.present(coordinator, animated: true) coordinator.start() } + + private func startEditPollCoordinator(startEvent: MXEvent? = nil) { + guard #available(iOS 14.0, *) else { + return + } + + let parameters = PollEditFormCoordinatorParameters(room: roomViewController.roomDataSource.room, pollStartEvent: startEvent) + let coordinator = PollEditFormCoordinator(parameters: parameters) + + coordinator.completion = { [weak self, weak coordinator] in + guard let self = self, let coordinator = coordinator else { + return + } + + self.navigationRouter?.dismissModule(animated: true, completion: nil) + self.remove(childCoordinator: coordinator) + } + + add(childCoordinator: coordinator) + + navigationRouter?.present(coordinator, animated: true) + coordinator.start() + } } // MARK: - RoomIdentifiable @@ -305,26 +328,7 @@ extension RoomCoordinator: RoomViewControllerDelegate { } func roomViewControllerDidRequestPollCreationFormPresentation(_ roomViewController: RoomViewController) { - guard #available(iOS 14.0, *) else { - return - } - - let parameters = PollEditFormCoordinatorParameters(room: roomViewController.roomDataSource.room) - let coordinator = PollEditFormCoordinator(parameters: parameters) - - coordinator.completion = { [weak self, weak coordinator] in - guard let self = self, let coordinator = coordinator else { - return - } - - self.navigationRouter?.dismissModule(animated: true, completion: nil) - self.remove(childCoordinator: coordinator) - } - - add(childCoordinator: coordinator) - - navigationRouter?.present(coordinator, animated: true) - coordinator.start() + startEditPollCoordinator() } func roomViewControllerDidRequestLocationSharingFormPresentation(_ roomViewController: RoomViewController) { @@ -350,4 +354,16 @@ extension RoomCoordinator: RoomViewControllerDelegate { PollTimelineProvider.shared.pollTimelineCoordinatorForEventIdentifier(eventIdentifier)?.endPoll() } + + func roomViewController(_ roomViewController: RoomViewController, canEditPollWithEventIdentifier eventIdentifier: String) -> Bool { + guard #available(iOS 14.0, *) else { + return false + } + + return PollTimelineProvider.shared.pollTimelineCoordinatorForEventIdentifier(eventIdentifier)?.canEditPoll() ?? false + } + + func roomViewController(_ roomViewController: RoomViewController, didRequestEditForPollWithStart startEvent: MXEvent) { + startEditPollCoordinator(startEvent: startEvent) + } } diff --git a/Riot/Modules/Room/RoomViewController.h b/Riot/Modules/Room/RoomViewController.h index f3b9f6fa6..6e30853af 100644 --- a/Riot/Modules/Room/RoomViewController.h +++ b/Riot/Modules/Room/RoomViewController.h @@ -207,6 +207,12 @@ canEndPollWithEventIdentifier:(NSString *)eventIdentifier; - (void)roomViewController:(RoomViewController *)roomViewController endPollWithEventIdentifier:(NSString *)eventIdentifier; +- (BOOL)roomViewController:(RoomViewController *)roomViewController +canEditPollWithEventIdentifier:(NSString *)eventIdentifier; + +- (void)roomViewController:(RoomViewController *)roomViewController +didRequestEditForPollWithStartEvent:(MXEvent *)startEvent; + @end NS_ASSUME_NONNULL_END diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index 10af1f3bc..059c01be8 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -6103,16 +6103,34 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; MXWeakify(self); RoomContextualMenuItem *editMenuItem = [[RoomContextualMenuItem alloc] initWithMenuAction:RoomContextualMenuActionEdit]; - editMenuItem.action = ^{ - MXStrongifyAndReturnIfNil(self); - [self hideContextualMenuAnimated:YES cancelEventSelection:NO completion:nil]; - [self editEventContentWithId:event.eventId]; - - // And display the keyboard - [self.inputToolbarView becomeFirstResponder]; - }; - editMenuItem.isEnabled = [self.roomDataSource canEditEventWithId:event.eventId]; + switch (event.eventType) { + case MXEventTypePollStart: { + editMenuItem.action = ^{ + MXStrongifyAndReturnIfNil(self); + [self hideContextualMenuAnimated:YES cancelEventSelection:YES completion:nil]; + [self.delegate roomViewController:self didRequestEditForPollWithStartEvent:event]; + }; + + editMenuItem.isEnabled = [self.delegate roomViewController:self canEditPollWithEventIdentifier:event.eventId]; + + break; + } + default: { + editMenuItem.action = ^{ + MXStrongifyAndReturnIfNil(self); + [self hideContextualMenuAnimated:YES cancelEventSelection:NO completion:nil]; + [self editEventContentWithId:event.eventId]; + + // And display the keyboard + [self.inputToolbarView becomeFirstResponder]; + }; + + editMenuItem.isEnabled = [self.roomDataSource canEditEventWithId:event.eventId]; + + break; + } + } return editMenuItem; } diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingModels.swift b/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingModels.swift index be9c188f9..88b0ee9cb 100644 --- a/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingModels.swift +++ b/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingModels.swift @@ -66,11 +66,11 @@ struct LocationSharingViewState: BindableState { } struct LocationSharingViewStateBindings { - var alertInfo: ErrorAlertInfo? + var alertInfo: LocationSharingErrorAlertInfo? var userLocation: CLLocationCoordinate2D? } -struct ErrorAlertInfo: Identifiable { +struct LocationSharingErrorAlertInfo: Identifiable { enum AlertType { case mapLoadingError case userLocatingError diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingViewModel.swift b/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingViewModel.swift index 765c0b558..40b750f85 100644 --- a/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingViewModel.swift +++ b/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingViewModel.swift @@ -72,24 +72,24 @@ class LocationSharingViewModel: LocationSharingViewModelType { switch error { case .failedLoadingMap: - state.bindings.alertInfo = ErrorAlertInfo(id: .mapLoadingError, - title: VectorL10n.locationSharingLoadingMapErrorTitle(AppInfo.current.displayName) , - primaryButton: (VectorL10n.ok, { completion?(.cancel) }), - secondaryButton: nil) + state.bindings.alertInfo = LocationSharingErrorAlertInfo(id: .mapLoadingError, + title: VectorL10n.locationSharingLoadingMapErrorTitle(AppInfo.current.displayName) , + primaryButton: (VectorL10n.ok, { completion?(.cancel) }), + secondaryButton: nil) case .failedLocatingUser: - state.bindings.alertInfo = ErrorAlertInfo(id: .userLocatingError, - title: VectorL10n.locationSharingLocatingUserErrorTitle(AppInfo.current.displayName), - primaryButton: (VectorL10n.ok, { completion?(.cancel) }), - secondaryButton: nil) + state.bindings.alertInfo = LocationSharingErrorAlertInfo(id: .userLocatingError, + title: VectorL10n.locationSharingLocatingUserErrorTitle(AppInfo.current.displayName), + primaryButton: (VectorL10n.ok, { completion?(.cancel) }), + secondaryButton: nil) case .invalidLocationAuthorization: - state.bindings.alertInfo = ErrorAlertInfo(id: .authorizationError, - title: VectorL10n.locationSharingInvalidAuthorizationErrorTitle(AppInfo.current.displayName), - primaryButton: (VectorL10n.locationSharingInvalidAuthorizationNotNow, { completion?(.cancel) }), - secondaryButton: (VectorL10n.locationSharingInvalidAuthorizationSettings, { - if let applicationSettingsURL = URL(string:UIApplication.openSettingsURLString) { - UIApplication.shared.open(applicationSettingsURL) - } - })) + state.bindings.alertInfo = LocationSharingErrorAlertInfo(id: .authorizationError, + title: VectorL10n.locationSharingInvalidAuthorizationErrorTitle(AppInfo.current.displayName), + primaryButton: (VectorL10n.locationSharingInvalidAuthorizationNotNow, { completion?(.cancel) }), + secondaryButton: (VectorL10n.locationSharingInvalidAuthorizationSettings, { + if let applicationSettingsURL = URL(string:UIApplication.openSettingsURLString) { + UIApplication.shared.open(applicationSettingsURL) + } + })) default: break } @@ -100,10 +100,10 @@ class LocationSharingViewModel: LocationSharingViewModelType { state.showLoadingIndicator = false if error != nil { - state.bindings.alertInfo = ErrorAlertInfo(id: .locationSharingError, - title: VectorL10n.locationSharingInvalidAuthorizationErrorTitle(AppInfo.current.displayName), - primaryButton: (VectorL10n.ok, nil), - secondaryButton: nil) + state.bindings.alertInfo = LocationSharingErrorAlertInfo(id: .locationSharingError, + title: VectorL10n.locationSharingInvalidAuthorizationErrorTitle(AppInfo.current.displayName), + primaryButton: (VectorL10n.ok, nil), + secondaryButton: nil) } } } diff --git a/RiotSwiftUI/Modules/Room/PollEditForm/Coordinator/PollEditFormCoordinator.swift b/RiotSwiftUI/Modules/Room/PollEditForm/Coordinator/PollEditFormCoordinator.swift index ee0f60a78..45527fb1a 100644 --- a/RiotSwiftUI/Modules/Room/PollEditForm/Coordinator/PollEditFormCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/PollEditForm/Coordinator/PollEditFormCoordinator.swift @@ -22,6 +22,7 @@ import SwiftUI struct PollEditFormCoordinatorParameters { let room: MXRoom + let pollStartEvent: MXEvent? } final class PollEditFormCoordinator: Coordinator, Presentable { @@ -51,9 +52,21 @@ final class PollEditFormCoordinator: Coordinator, Presentable { init(parameters: PollEditFormCoordinatorParameters) { self.parameters = parameters - let viewModel = PollEditFormViewModel() - let view = PollEditForm(viewModel: viewModel.context) + var viewModel: PollEditFormViewModel + if let startEvent = parameters.pollStartEvent, + let pollContent = MXEventContentPollStart(fromJSON: startEvent.content) { + viewModel = PollEditFormViewModel(parameters: PollEditFormViewModelParameters(mode: .editing, + pollDetails: PollDetails(question: pollContent.question, + answerOptions: pollContent.answerOptions.map { $0.text }))) + } else { + viewModel = PollEditFormViewModel(parameters: PollEditFormViewModelParameters(mode: .creation, + pollDetails: PollDetails(question: "", + answerOptions: ["", ""]))) + } + + let view = PollEditForm(viewModel: viewModel.context) + _pollEditFormViewModel = viewModel pollEditFormHostingController = VectorHostingController(rootView: view) } @@ -70,16 +83,9 @@ final class PollEditFormCoordinator: Coordinator, Presentable { switch result { case .cancel: self.completion?() - case .create(let question, let answerOptions): - var options = [MXEventContentPollStartAnswerOption]() - for answerOption in answerOptions { - options.append(MXEventContentPollStartAnswerOption(uuid: UUID().uuidString, text: answerOption)) - } + case .create(let details): - let pollStartContent = MXEventContentPollStart(question: question, - kind: kMXMessageContentKeyExtensiblePollKindDisclosed, - maxSelections: 1, - answerOptions: options) + let pollStartContent = self.buildPollContentWithDetails(details) self.pollEditFormViewModel.dispatch(action: .startLoading) @@ -92,15 +98,58 @@ final class PollEditFormCoordinator: Coordinator, Presentable { guard let self = self else { return } MXLog.error("Failed creating poll with error: \(String(describing: error))") - self.pollEditFormViewModel.dispatch(action: .stopLoading(error)) + self.pollEditFormViewModel.dispatch(action: .stopLoading(.failedCreatingPoll)) } + + case .update(let details): + guard let pollStartEvent = self.parameters.pollStartEvent else { + fatalError() + } + + self.pollEditFormViewModel.dispatch(action: .startLoading) + + guard let oldPollContent = MXEventContentPollStart(fromJSON: pollStartEvent.content) else { + self.pollEditFormViewModel.dispatch(action: .stopLoading(.failedUpdatingPoll)) + return + } + + let newPollContent = self.buildPollContentWithDetails(details) + + self.parameters.room.sendPollUpdate(for: pollStartEvent, + oldContent: oldPollContent, + newContent: newPollContent, localEcho: nil) { [weak self] result in + guard let self = self else { return } + + self.pollEditFormViewModel.dispatch(action: .stopLoading(nil)) + self.completion?() + } failure: { [weak self] error in + guard let self = self else { return } + + MXLog.error("Failed updating poll with error: \(String(describing: error))") + self.pollEditFormViewModel.dispatch(action: .stopLoading(.failedUpdatingPoll)) + } } } } - // MARK: - Private + // MARK: - Presentable func toPresentable() -> UIViewController { return pollEditFormHostingController } + + // MARK: - Private + + private func buildPollContentWithDetails(_ details: PollDetails) -> MXEventContentPollStart { + var options = [MXEventContentPollStartAnswerOption]() + for answerOption in details.answerOptions { + options.append(MXEventContentPollStartAnswerOption(uuid: UUID().uuidString, text: answerOption)) + } + + return MXEventContentPollStart(question: details.question, + kind: (details.disclosed ? kMXMessageContentKeyExtensiblePollKindDisclosed : kMXMessageContentKeyExtensiblePollKindUndisclosed) , + maxSelections: NSNumber(value: details.maxSelections), + answerOptions: options) + + } } diff --git a/RiotSwiftUI/Modules/Room/PollEditForm/PollEditFormModels.swift b/RiotSwiftUI/Modules/Room/PollEditForm/PollEditFormModels.swift index 9c7d856c8..86835ff74 100644 --- a/RiotSwiftUI/Modules/Room/PollEditForm/PollEditFormModels.swift +++ b/RiotSwiftUI/Modules/Room/PollEditForm/PollEditFormModels.swift @@ -19,10 +19,22 @@ import Foundation import SwiftUI +struct PollDetails { + let question: String + let answerOptions: [String] + let maxSelections: UInt = 1 + let disclosed: Bool = true +} + +enum PollEditFormMode { + case creation + case editing +} + enum PollEditFormStateAction { case viewAction(PollEditFormViewAction) case startLoading - case stopLoading(Error?) + case stopLoading(PollEditFormErrorAlertInfo.AlertType?) } enum PollEditFormViewAction { @@ -30,11 +42,13 @@ enum PollEditFormViewAction { case deleteAnswerOption(PollEditFormAnswerOption) case cancel case create + case update } enum PollEditFormViewModelResult { case cancel - case create(String, [String]) + case create(PollDetails) + case update(PollDetails) } struct PollEditFormQuestion { @@ -61,6 +75,7 @@ struct PollEditFormAnswerOption: Identifiable, Equatable { struct PollEditFormViewState: BindableState { var maxAnswerOptionsCount: Int + var mode: PollEditFormMode var bindings: PollEditFormViewStateBindings var confirmationButtonEnabled: Bool { @@ -79,5 +94,16 @@ struct PollEditFormViewStateBindings { var question: PollEditFormQuestion var answerOptions: [PollEditFormAnswerOption] - var showsFailureAlert: Bool = false + var alertInfo: PollEditFormErrorAlertInfo? +} + +struct PollEditFormErrorAlertInfo: Identifiable { + enum AlertType { + case failedCreatingPoll + case failedUpdatingPoll + } + + let id: AlertType + let title: String + let subtitle: String } diff --git a/RiotSwiftUI/Modules/Room/PollEditForm/PollEditFormScreenState.swift b/RiotSwiftUI/Modules/Room/PollEditForm/PollEditFormScreenState.swift index 2e545a9d4..5cd584854 100644 --- a/RiotSwiftUI/Modules/Room/PollEditForm/PollEditFormScreenState.swift +++ b/RiotSwiftUI/Modules/Room/PollEditForm/PollEditFormScreenState.swift @@ -28,7 +28,8 @@ enum MockPollEditFormScreenState: MockScreenState, CaseIterable { } var screenView: ([Any], AnyView) { - let viewModel = PollEditFormViewModel() + let viewModel = PollEditFormViewModel(parameters: PollEditFormViewModelParameters(mode: .editing, + pollDetails: PollDetails(question: "", answerOptions: ["", ""]))) return ([viewModel], AnyView(PollEditForm(viewModel: viewModel.context))) } } diff --git a/RiotSwiftUI/Modules/Room/PollEditForm/PollEditFormViewModel.swift b/RiotSwiftUI/Modules/Room/PollEditForm/PollEditFormViewModel.swift index f38ac9fc9..183aef356 100644 --- a/RiotSwiftUI/Modules/Room/PollEditForm/PollEditFormViewModel.swift +++ b/RiotSwiftUI/Modules/Room/PollEditForm/PollEditFormViewModel.swift @@ -19,6 +19,11 @@ import SwiftUI import Combine +struct PollEditFormViewModelParameters { + let mode: PollEditFormMode + let pollDetails: PollDetails +} + @available(iOS 14, *) typealias PollEditFormViewModelType = StateStoreViewModel< PollEditFormViewState, PollEditFormStateAction, @@ -42,20 +47,17 @@ class PollEditFormViewModel: PollEditFormViewModelType { // MARK: - Setup - init() { - super.init(initialViewState: Self.defaultState()) - } - - private static func defaultState() -> PollEditFormViewState { - return PollEditFormViewState( + init(parameters: PollEditFormViewModelParameters) { + let state = PollEditFormViewState( maxAnswerOptionsCount: Constants.maxAnswerOptionsCount, + mode: parameters.mode, bindings: PollEditFormViewStateBindings( - question: PollEditFormQuestion(text: "", maxLength: Constants.maxQuestionLength), - answerOptions: [PollEditFormAnswerOption(text: "", maxLength: Constants.maxAnswerOptionLength), - PollEditFormAnswerOption(text: "", maxLength: Constants.maxAnswerOptionLength) - ] + question: PollEditFormQuestion(text: parameters.pollDetails.question, maxLength: Constants.maxQuestionLength), + answerOptions: parameters.pollDetails.answerOptions.map { PollEditFormAnswerOption(text: $0, maxLength: Constants.maxAnswerOptionLength) } ) ) + + super.init(initialViewState: state) } // MARK: - Public @@ -65,11 +67,9 @@ class PollEditFormViewModel: PollEditFormViewModelType { case .cancel: completion?(.cancel) case .create: - completion?(.create(state.bindings.question.text.trimmingCharacters(in: .whitespacesAndNewlines), - state.bindings.answerOptions.compactMap({ answerOption in - let text = answerOption.text.trimmingCharacters(in: .whitespacesAndNewlines) - return text.isEmpty ? nil : text - }))) + completion?(.create(buildPollDetails())) + case .update: + completion?(.update(buildPollDetails())) default: dispatch(action: .viewAction(viewAction)) } @@ -92,10 +92,29 @@ class PollEditFormViewModel: PollEditFormViewModelType { case .stopLoading(let error): state.showLoadingIndicator = false - if error != nil { - state.bindings.showsFailureAlert = true + switch error { + case .failedCreatingPoll: + state.bindings.alertInfo = PollEditFormErrorAlertInfo(id: .failedCreatingPoll, + title: VectorL10n.pollEditFormPostFailureTitle, + subtitle: VectorL10n.pollEditFormPostFailureSubtitle) + case .failedUpdatingPoll: + state.bindings.alertInfo = PollEditFormErrorAlertInfo(id: .failedCreatingPoll, + title: VectorL10n.pollEditFormUpdateFailureTitle, + subtitle: VectorL10n.pollEditFormUpdateFailureSubtitle) + case .none: + break } break } } + + // MARK: - Private + + private func buildPollDetails() -> PollDetails { + return PollDetails(question: state.bindings.question.text.trimmingCharacters(in: .whitespacesAndNewlines), + answerOptions: state.bindings.answerOptions.compactMap({ answerOption in + let text = answerOption.text.trimmingCharacters(in: .whitespacesAndNewlines) + return text.isEmpty ? nil : text + })) + } } diff --git a/RiotSwiftUI/Modules/Room/PollEditForm/View/PollEditForm.swift b/RiotSwiftUI/Modules/Room/PollEditForm/View/PollEditForm.swift index 57f3a63a3..be7434bd6 100644 --- a/RiotSwiftUI/Modules/Room/PollEditForm/View/PollEditForm.swift +++ b/RiotSwiftUI/Modules/Room/PollEditForm/View/PollEditForm.swift @@ -76,17 +76,19 @@ struct PollEditForm: View { Spacer() - Button(VectorL10n.pollEditFormCreatePoll) { - viewModel.send(viewAction: .create) + if viewModel.viewState.mode == .creation { + Button(VectorL10n.pollEditFormCreatePoll) { + viewModel.send(viewAction: .create) + } + .buttonStyle(PrimaryActionButtonStyle()) + .disabled(!viewModel.viewState.confirmationButtonEnabled) } - .buttonStyle(PrimaryActionButtonStyle()) - .disabled(!viewModel.viewState.confirmationButtonEnabled) } .padding() .activityIndicator(show: viewModel.viewState.showLoadingIndicator) - .alert(isPresented: $viewModel.showsFailureAlert) { - Alert(title: Text(VectorL10n.pollEditFormPostFailureTitle), - message: Text(VectorL10n.pollEditFormPostFailureSubtitle), + .alert(item: $viewModel.alertInfo) { info in + Alert(title: Text(info.title), + message: Text(info.subtitle), dismissButton: .default(Text(VectorL10n.ok))) } .frame(minHeight: proxy.size.height) // Make the VStack fill the ScrollView's parent @@ -101,6 +103,15 @@ struct PollEditForm: View { .font(.headline) .foregroundColor(theme.colors.primaryContent) } + + ToolbarItem(placement: .navigationBarTrailing) { + if viewModel.viewState.mode == .editing { + Button(VectorL10n.save, action: { + viewModel.send(viewAction: .update) + }) + .disabled(!viewModel.viewState.confirmationButtonEnabled) + } + } } .navigationBarTitleDisplayMode(.inline) } diff --git a/RiotSwiftUI/Modules/Room/PollTimeline/Coordinator/PollTimelineCoordinator.swift b/RiotSwiftUI/Modules/Room/PollTimeline/Coordinator/PollTimelineCoordinator.swift index 78c53acec..d7b51a8bf 100644 --- a/RiotSwiftUI/Modules/Room/PollTimeline/Coordinator/PollTimelineCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/PollTimeline/Coordinator/PollTimelineCoordinator.swift @@ -51,7 +51,7 @@ final class PollTimelineCoordinator: Coordinator, Presentable, PollAggregatorDel init(parameters: PollTimelineCoordinatorParameters) throws { self.parameters = parameters - try pollAggregator = PollAggregator(session: parameters.session, room: parameters.room, pollStartEvent: parameters.pollStartEvent) + try pollAggregator = PollAggregator(session: parameters.session, room: parameters.room, pollStartEventId: parameters.pollStartEvent.eventId) pollAggregator.delegate = self pollTimelineViewModel = PollTimelineViewModel(timelinePoll: buildTimelinePollFrom(pollAggregator.poll)) @@ -96,6 +96,10 @@ final class PollTimelineCoordinator: Coordinator, Presentable, PollAggregatorDel return pollAggregator.poll.isClosed == false } + func canEditPoll() -> Bool { + return (pollAggregator.poll.isClosed == false && pollAggregator.poll.totalAnswerCount == 0) + } + func endPoll() { parameters.room.sendPollEnd(for: parameters.pollStartEvent, localEcho: nil, success: nil) { [weak self] error in self?.pollTimelineViewModel.dispatch(action: .showClosingFailure)