diff --git a/NextcloudTalk/NCAPIController.h b/NextcloudTalk/NCAPIController.h index 4ec8b5f4e..fd6b807a0 100644 --- a/NextcloudTalk/NCAPIController.h +++ b/NextcloudTalk/NCAPIController.h @@ -208,8 +208,9 @@ extern NSInteger const kReceivedChatMessagesLimit; // Polls Controller - (NSURLSessionDataTask *)createPollWithQuestion:(NSString *)question options:(NSArray *)options resultMode:(NCPollResultMode)resultMode maxVotes:(NSInteger)maxVotes inRoom:(NSString *)token asDraft:(BOOL)asDraft forAccount:(TalkAccount *)account withCompletionBlock:(PollCompletionBlock)block; -- (NSURLSessionDataTask *)getPollDraftsInRoom:(NSString *)token forAccount:(TalkAccount *)account withCompletionBlock:(PollDraftsCompletionBlock)block; - (NSURLSessionDataTask *)getPollWithId:(NSInteger)pollId inRoom:(NSString *)token forAccount:(TalkAccount *)account withCompletionBlock:(PollCompletionBlock)block; +- (NSURLSessionDataTask *)editPollDraftWithId:(NSInteger)draftId question:(NSString *)question options:(NSArray *)options resultMode:(NCPollResultMode)resultMode maxVotes:(NSInteger)maxVotes inRoom:(NSString *)token forAccount:(TalkAccount *)account withCompletionBlock:(PollCompletionBlock)block; +- (NSURLSessionDataTask *)getPollDraftsInRoom:(NSString *)token forAccount:(TalkAccount *)account withCompletionBlock:(PollDraftsCompletionBlock)block; - (NSURLSessionDataTask *)voteOnPollWithId:(NSInteger)pollId inRoom:(NSString *)token withOptions:(NSArray *)options forAccount:(TalkAccount *)account withCompletionBlock:(PollCompletionBlock)block; - (NSURLSessionDataTask *)closePollWithId:(NSInteger)pollId inRoom:(NSString *)token forAccount:(TalkAccount *)account withCompletionBlock:(PollCompletionBlock)block; diff --git a/NextcloudTalk/NCAPIController.m b/NextcloudTalk/NCAPIController.m index 8295f6627..ec8a7dce8 100644 --- a/NextcloudTalk/NCAPIController.m +++ b/NextcloudTalk/NCAPIController.m @@ -1646,6 +1646,36 @@ - (NSURLSessionDataTask *)getPollWithId:(NSInteger)pollId inRoom:(NSString *)tok return task; } +- (NSURLSessionDataTask *)editPollDraftWithId:(NSInteger)draftId question:(NSString *)question options:(NSArray *)options resultMode:(NCPollResultMode)resultMode maxVotes:(NSInteger)maxVotes inRoom:(NSString *)token forAccount:(TalkAccount *)account withCompletionBlock:(PollCompletionBlock)block +{ + NSString *encodedToken = [token stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLHostAllowedCharacterSet]]; + NSString *endpoint = [NSString stringWithFormat:@"poll/%@/draft/%ld", encodedToken, (long)draftId]; + NSInteger pollsAPIVersion = [self pollsAPIVersionForAccount:account]; + NSString *URLString = [self getRequestURLForEndpoint:endpoint withAPIVersion:pollsAPIVersion forAccount:account]; + NSDictionary *parameters = @{@"question" : question, + @"options" : options, + @"resultMode" : @(resultMode), + @"maxVotes" : @(maxVotes) + }; + NCAPISessionManager *apiSessionManager = [_apiSessionManagers objectForKey:account.accountId]; + NSURLSessionDataTask *task = [apiSessionManager POST:URLString parameters:parameters progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) { + NSDictionary *pollDict = [[responseObject objectForKey:@"ocs"] objectForKey:@"data"]; + NCPoll *poll = [NCPoll initWithPollDictionary:pollDict]; + if (block) { + NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)task.response; + block(poll, nil, httpResponse.statusCode); + } + } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) { + NSInteger statusCode = [self getResponseStatusCode:task.response]; + [self checkResponseStatusCode:statusCode forAccount:account]; + if (block) { + block(nil, error, statusCode); + } + }]; + + return task; +} + - (NSURLSessionDataTask *)getPollDraftsInRoom:(NSString *)token forAccount:(TalkAccount *)account withCompletionBlock:(PollDraftsCompletionBlock)block { NSString *encodedToken = [token stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLHostAllowedCharacterSet]]; diff --git a/NextcloudTalk/NCDatabaseManager.h b/NextcloudTalk/NCDatabaseManager.h index 6673a6a60..a52e6df8c 100644 --- a/NextcloudTalk/NCDatabaseManager.h +++ b/NextcloudTalk/NCDatabaseManager.h @@ -79,6 +79,7 @@ extern NSString * const kCapabilityArchivedConversationsV2; extern NSString * const kCapabilityCallNotificationState; extern NSString * const kCapabilityCallForceMute; extern NSString * const kCapabilityTalkPollsDrafts; +extern NSString * const kCapabilityEditDraftPoll; extern NSString * const kNotificationsCapabilityExists; extern NSString * const kNotificationsCapabilityTestPush; diff --git a/NextcloudTalk/NCDatabaseManager.m b/NextcloudTalk/NCDatabaseManager.m index 937ba4f6e..3c2ac1fde 100644 --- a/NextcloudTalk/NCDatabaseManager.m +++ b/NextcloudTalk/NCDatabaseManager.m @@ -80,6 +80,7 @@ NSString * const kCapabilityCallNotificationState = @"call-notification-state-api"; NSString * const kCapabilityForceMute = @"force-mute"; NSString * const kCapabilityTalkPollsDrafts = @"talk-polls-drafts"; +NSString * const kCapabilityEditDraftPoll = @"edit-draft-poll"; NSString * const kNotificationsCapabilityExists = @"exists"; NSString * const kNotificationsCapabilityTestPush = @"test-push"; diff --git a/NextcloudTalk/PollCreationViewController.swift b/NextcloudTalk/PollCreationViewController.swift index ab572e0e4..4764e0ae3 100644 --- a/NextcloudTalk/PollCreationViewController.swift +++ b/NextcloudTalk/PollCreationViewController.swift @@ -23,6 +23,7 @@ import UIKit let kQuestionTextFieldTag = 9999 var room: NCRoom + var editingDraftId: Int? var draftsAvailable: Bool = false var question: String = "" var options: [String] = ["", ""] @@ -58,7 +59,7 @@ import UIKit self.navigationController?.navigationBar.tintColor = NCAppBranding.themeTextColor() self.navigationController?.navigationBar.barTintColor = NCAppBranding.themeColor() self.navigationController?.navigationBar.isTranslucent = false - self.navigationItem.title = NSLocalizedString("New poll", comment: "") + self.setNavigationBarTitle() let appearance = UINavigationBarAppearance() appearance.configureWithOpaqueBackground() @@ -70,8 +71,8 @@ import UIKit self.tableView.isEditing = true - // Set footer buttons - self.tableView.tableFooterView = pollFooterView() + // Configure footer buttons + configureFooterButtons() self.navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(self.cancelButtonPressed)) self.navigationItem.leftBarButtonItem?.tintColor = NCAppBranding.themeTextColor() @@ -98,31 +99,61 @@ import UIKit present(navController, animated: true, completion: nil) } - func didSelectPollDraft(question: String, options: [String], resultMode: NCPollResultMode, maxVotes: Int) { + func didSelectPollDraft(draft: NCPoll, forEditing: Bool) { // End editing for any textfield self.view.endEditing(true) // Assign poll draft values - self.question = question - self.options = options - self.anonymousPollSwitch.isOn = resultMode == .hidden - self.multipleAnswersSwitch.isOn = maxVotes == 0 + self.question = draft.question + self.options = draft.options.compactMap { $0 as? String } + self.anonymousPollSwitch.isOn = draft.resultMode == .hidden + self.multipleAnswersSwitch.isOn = draft.maxVotes == 0 + + // Check if editing poll draft + if forEditing { + self.editingDraftId = draft.pollId + } else { + self.editingDraftId = nil + } + + // Refresh poll creation view + self.refreshPollCreationView() + } + + func refreshPollCreationView() { self.tableView.reloadData() + self.setNavigationBarTitle() + self.setMoreOptionsButton() + self.configureFooterButtons() self.checkIfPollIsReadyToCreate() } + func setNavigationBarTitle() { + if let editingDraftId = self.editingDraftId { + self.navigationItem.title = NSLocalizedString("Editing poll draft", comment: "") + } else { + self.navigationItem.title = NSLocalizedString("New poll", comment: "") + } + } + func showCreationError() { let alert = UIAlertController(title: NSLocalizedString("Creating poll failed", comment: ""), message: NSLocalizedString("An error occurred while creating the poll", comment: ""), preferredStyle: .alert) alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .cancel, handler: nil)) self.present(alert, animated: true) - removePollCreationUI() + } + + func showEditionError() { + let alert = UIAlertController(title: NSLocalizedString("Editing poll failed", comment: ""), + message: NSLocalizedString("An error occurred while editing the poll", comment: ""), + preferredStyle: .alert) + alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .cancel, handler: nil)) + self.present(alert, animated: true) } func showDraftCreationSuccess() { NotificationPresenter.shared().present(text: NSLocalizedString("Poll draft has been saved", comment: ""), dismissAfterDelay: 5.0, includedStyle: .dark) - removePollCreationUI() } func showPollCreationUI() { @@ -138,7 +169,7 @@ import UIKit } func setMoreOptionsButton() { - if draftsAvailable { + if draftsAvailable, editingDraftId == nil { let menuAction = UIAction( title: NSLocalizedString("Browse poll drafts", comment: ""), image: UIImage(systemName: "doc")) { _ in @@ -166,14 +197,19 @@ import UIKit footerView.secondaryButton.setButtonEnabled(enabled: false) } - func pollFooterView() -> UIView { - footerView.primaryButton.setTitle(NSLocalizedString("Create poll", comment: ""), for: .normal) - footerView.primaryButton.setButtonAction(target: self, selector: #selector(createPollButtonPressed)) + func configureFooterButtons() { + if editingDraftId != nil { + footerView.primaryButton.setTitle(NSLocalizedString("Save", comment: ""), for: .normal) + footerView.primaryButton.setButtonAction(target: self, selector: #selector(saveEditedPollDraftButtonPressed)) + } else { + footerView.primaryButton.setTitle(NSLocalizedString("Create poll", comment: ""), for: .normal) + footerView.primaryButton.setButtonAction(target: self, selector: #selector(createPollButtonPressed)) + } footerView.frame = CGRect(x: 0, y: 0, width: 0, height: PollFooterView.heightForOption) footerView.secondaryButtonContainerView.isHidden = true - if draftsAvailable { + if draftsAvailable, editingDraftId == nil { footerView.secondaryButton.setTitle(NSLocalizedString("Save as draft", comment: ""), for: .normal) footerView.secondaryButton.setButtonStyle(style: .tertiary) footerView.secondaryButton.setButtonAction(target: self, selector: #selector(createPollDraftButtonPressed)) @@ -183,7 +219,7 @@ import UIKit } checkIfPollIsReadyToCreate() - return footerView + self.tableView.tableFooterView = footerView } func createPollButtonPressed() { @@ -199,8 +235,8 @@ import UIKit let maxVotes: Int = multipleAnswersSwitch.isOn ? 0 : 1 showPollCreationUI() - NCAPIController.sharedInstance().createPoll(withQuestion: question, options: options, resultMode: resultMode, maxVotes: maxVotes, inRoom: room.token, asDraft: asDraft, for: room.account) { _, error, _ in + self.removePollCreationUI() if error != nil { self.showCreationError() } else if asDraft { @@ -211,6 +247,25 @@ import UIKit } } + func saveEditedPollDraftButtonPressed() { + guard let draftId = editingDraftId else { return } + + let resultMode: NCPollResultMode = anonymousPollSwitch.isOn ? .hidden : .public + let maxVotes: Int = multipleAnswersSwitch.isOn ? 0 : 1 + + showPollCreationUI() + NCAPIController.sharedInstance().editPollDraft(withId: draftId, question: question, options: options, resultMode: resultMode, maxVotes: maxVotes, inRoom: room.token, for: room.account) { _, error, _ in + self.removePollCreationUI() + if error != nil { + self.showEditionError() + } else { + self.editingDraftId = nil + self.refreshPollCreationView() + self.presentPollDraftsView() + } + } + } + func checkIfPollIsReadyToCreate() { disablePollCreationButtons() diff --git a/NextcloudTalk/PollDraftsViewController.swift b/NextcloudTalk/PollDraftsViewController.swift index 14d9f560a..75a27aa60 100644 --- a/NextcloudTalk/PollDraftsViewController.swift +++ b/NextcloudTalk/PollDraftsViewController.swift @@ -6,7 +6,7 @@ import UIKit protocol PollDraftsViewControllerDelegate: AnyObject { - func didSelectPollDraft(question: String, options: [String], resultMode: NCPollResultMode, maxVotes: Int) + func didSelectPollDraft(draft:NCPoll, forEditing: Bool) } class PollDraftsViewController: UITableViewController { @@ -83,6 +83,15 @@ class PollDraftsViewController: UITableViewController { } } + // MARK: - Error dialogs + func showDeletionError() { + let alert = UIAlertController(title: NSLocalizedString("Deleting poll draft failed", comment: ""), + message: NSLocalizedString("An error occurred while deleting the poll draft", comment: ""), + preferredStyle: .alert) + alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .cancel, handler: nil)) + self.present(alert, animated: true) + } + // MARK: - TableView DataSource override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return drafts.count @@ -105,7 +114,35 @@ class PollDraftsViewController: UITableViewController { override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { dismiss(animated: true) { let draft = self.drafts[indexPath.row] - self.delegate?.didSelectPollDraft(question: draft.question, options: draft.options.compactMap { $0 as? String }, resultMode: draft.resultMode, maxVotes: draft.maxVotes) + self.delegate?.didSelectPollDraft(draft: draft, forEditing: false) + } + } + + override func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? { + return UIContextMenuConfiguration(identifier: indexPath as NSIndexPath, previewProvider: nil) { _ in + let deleteAction = UIAction(title: NSLocalizedString("Delete", comment: ""), image: UIImage(systemName: "trash"), attributes: .destructive) { [unowned self] _ in + let draft = self.drafts[indexPath.row] + NCAPIController.sharedInstance().closePoll(withId: draft.pollId, inRoom: room.token, for: room.account) { _, error, _ in + if error == nil { + self.getPollDrafts() + } else { + self.showDeletionError() + } + } + } + + if NCDatabaseManager.sharedInstance().serverHasTalkCapability(kCapabilityEditDraftPoll, forAccountId: self.room.accountId) { + let editAction = UIAction(title: NSLocalizedString("Edit", comment: ""), image: UIImage(systemName: "pencil")) { [unowned self] _ in + dismiss(animated: true) { + let draft = self.drafts[indexPath.row] + self.delegate?.didSelectPollDraft(draft: draft, forEditing: true) + } + } + + return UIMenu(children: [editAction, deleteAction]) + } + + return UIMenu(children: [deleteAction]) } } } diff --git a/NextcloudTalk/en.lproj/Localizable.strings b/NextcloudTalk/en.lproj/Localizable.strings index e787768e2..3532df48b 100644 --- a/NextcloudTalk/en.lproj/Localizable.strings +++ b/NextcloudTalk/en.lproj/Localizable.strings @@ -262,9 +262,15 @@ /* No comment provided by engineer. */ "An error occurred while deleting the message" = "An error occurred while deleting the message"; +/* No comment provided by engineer. */ +"An error occurred while deleting the poll draft" = "An error occurred while deleting the poll draft"; + /* No comment provided by engineer. */ "An error occurred while disabling absence" = "An error occurred while disabling absence"; +/* No comment provided by engineer. */ +"An error occurred while editing the poll" = "An error occurred while editing the poll"; + /* No comment provided by engineer. */ "An error occurred while opening the file %@" = "An error occurred while opening the file %@"; @@ -709,6 +715,9 @@ /* No comment provided by engineer. */ "Deleting message" = "Deleting message"; +/* No comment provided by engineer. */ +"Deleting poll draft failed" = "Deleting poll draft failed"; + /* No comment provided by engineer. */ "Demote from moderator" = "Demote from moderator"; @@ -874,6 +883,12 @@ /* No comment provided by engineer. */ "Editing Message" = "Editing Message"; +/* No comment provided by engineer. */ +"Editing poll draft" = "Editing poll draft"; + +/* No comment provided by engineer. */ +"Editing poll failed" = "Editing poll failed"; + /* No comment provided by engineer. */ "Either you don't have chat permission or the conversation is read-only." = "Either you don't have chat permission or the conversation is read-only.";