diff --git a/NextcloudTalk.xcodeproj/project.pbxproj b/NextcloudTalk.xcodeproj/project.pbxproj index e1086feab..fa2cad1ea 100644 --- a/NextcloudTalk.xcodeproj/project.pbxproj +++ b/NextcloudTalk.xcodeproj/project.pbxproj @@ -481,6 +481,7 @@ 2C7A12422017872600864818 /* AddParticipantsTableViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C7A12402017872600864818 /* AddParticipantsTableViewController.m */; }; 2C7A12432017872600864818 /* AddParticipantsTableViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 2C7A12412017872600864818 /* AddParticipantsTableViewController.xib */; }; 2C7F47AA20289B9600081CC7 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 2C7F47AC20289B9600081CC7 /* Localizable.strings */; }; + 2C8001D92D3529AF00DDBADC /* PollDraftsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C8001D82D3529AF00DDBADC /* PollDraftsViewController.swift */; }; 2C84BCCC29EEB9C6001BA6DA /* CallReactionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C84BCCB29EEB9C6001BA6DA /* CallReactionView.swift */; }; 2C84BCCE29EEDCE8001BA6DA /* CallReactionView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 2C84BCCD29EEDCE8001BA6DA /* CallReactionView.xib */; }; 2C8A2BC9221F094F00DE6D2C /* DirectoryTableViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C8A2BC8221F094F00DE6D2C /* DirectoryTableViewController.m */; }; @@ -1009,6 +1010,7 @@ 2C7A12402017872600864818 /* AddParticipantsTableViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AddParticipantsTableViewController.m; sourceTree = ""; }; 2C7A12412017872600864818 /* AddParticipantsTableViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = AddParticipantsTableViewController.xib; sourceTree = ""; }; 2C7F47AB20289B9600081CC7 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; + 2C8001D82D3529AF00DDBADC /* PollDraftsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollDraftsViewController.swift; sourceTree = ""; }; 2C84BCCB29EEB9C6001BA6DA /* CallReactionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallReactionView.swift; sourceTree = ""; }; 2C84BCCD29EEDCE8001BA6DA /* CallReactionView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = CallReactionView.xib; sourceTree = ""; }; 2C8A2BC7221F094F00DE6D2C /* DirectoryTableViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = DirectoryTableViewController.h; sourceTree = ""; }; @@ -1408,6 +1410,7 @@ 2C5BFBF92891598900E75118 /* PollResultTableViewCell.swift */, 2C5BFBFA2891598900E75118 /* PollResultTableViewCell.xib */, 2C5BFBEE288A947800E75118 /* PollVotingView.swift */, + 2C8001D82D3529AF00DDBADC /* PollDraftsViewController.swift */, ); name = Polls; sourceTree = ""; @@ -2904,6 +2907,7 @@ 2CB6ACCA26401D5200D3D641 /* GeoLocationRichObject.m in Sources */, 2C78EF9C1F826B22008AFA74 /* NCCallController.m in Sources */, 1F1B50442B9095D100B0F2F4 /* FederatedCapabilities.m in Sources */, + 2C8001D92D3529AF00DDBADC /* PollDraftsViewController.swift in Sources */, 2C5BFBF628902E0300E75118 /* PollFooterView.swift in Sources */, 2C4D7D761F30F7B600FF4A0D /* ARDUtilities.m in Sources */, 1FF4DA7E2C0237D000C1B952 /* DirectoryTableViewCell.swift in Sources */, diff --git a/NextcloudTalk/BaseChatViewController.swift b/NextcloudTalk/BaseChatViewController.swift index 41cd2f371..2f34bf057 100644 --- a/NextcloudTalk/BaseChatViewController.swift +++ b/NextcloudTalk/BaseChatViewController.swift @@ -17,7 +17,6 @@ import SwiftUI UIImagePickerControllerDelegate, PHPickerViewControllerDelegate, UINavigationControllerDelegate, - PollCreationViewControllerDelegate, ShareLocationViewControllerDelegate, CNContactPickerDelegate, UIDocumentPickerDelegate, @@ -884,8 +883,7 @@ import SwiftUI } func presentPollCreation() { - let pollCreationVC = PollCreationViewController(style: .insetGrouped) - pollCreationVC.pollCreationDelegate = self + let pollCreationVC = PollCreationViewController(room: room) self.presentWithNavigation(pollCreationVC, animated: true) } @@ -3551,8 +3549,7 @@ import SwiftUI // MARK: - ObjectShareMessageTableViewCell public func cellWants(toOpenPoll poll: NCMessageParameter) { - let pollVC = PollVotingView(style: .insetGrouped) - pollVC.room = self.room + let pollVC = PollVotingView(room: room) self.presentWithNavigation(pollVC, animated: true) guard let pollId = Int(poll.parameterId) else { return } @@ -3564,18 +3561,6 @@ import SwiftUI } } - // MARK: - PollCreationViewControllerDelegate - - func pollCreationViewControllerWantsToCreatePoll(pollCreationViewController: PollCreationViewController, question: String, options: [String], resultMode: NCPollResultMode, maxVotes: Int) { - NCAPIController.sharedInstance().createPoll(withQuestion: question, options: options, resultMode: resultMode, maxVotes: maxVotes, inRoom: self.room.token, for: self.account) { _, error, _ in - if error != nil { - pollCreationViewController.showCreationError() - } else { - pollCreationViewController.close() - } - } - } - // MARK: - SystemMessageTableViewCellDelegate public func cellWantsToCollapseMessages(with message: NCChatMessage!) { diff --git a/NextcloudTalk/NCAPIController.h b/NextcloudTalk/NCAPIController.h index dfbb34b72..1f48b2a51 100644 --- a/NextcloudTalk/NCAPIController.h +++ b/NextcloudTalk/NCAPIController.h @@ -56,6 +56,7 @@ typedef void (^MessageTranslationCompletionBlock)(NSDictionary *translationDict, typedef void (^MessageReactionCompletionBlock)(NSDictionary *reactionsDict, NSError *error, NSInteger statusCode); typedef void (^PollCompletionBlock)(NCPoll *poll, NSError *error, NSInteger statusCode); +typedef void (^PollDraftsCompletionBlock)(NSArray *polls, NSError *error, NSInteger statusCode); typedef void (^SendSignalingMessagesCompletionBlock)(NSError *error); typedef void (^PullSignalingMessagesCompletionBlock)(NSDictionary *messages, NSError *error); @@ -204,7 +205,8 @@ extern NSInteger const kReceivedChatMessagesLimit; - (NSURLSessionDataTask *)getReactions:(NSString *)reaction fromMessage:(NSInteger)messageId inRoom:(NSString *)token forAccount:(TalkAccount *)account withCompletionBlock:(MessageReactionCompletionBlock)block; // Polls Controller -- (NSURLSessionDataTask *)createPollWithQuestion:(NSString *)question options:(NSArray *)options resultMode:(NCPollResultMode)resultMode maxVotes:(NSInteger)maxVotes inRoom:(NSString *)token forAccount:(TalkAccount *)account withCompletionBlock:(PollCompletionBlock)block; +- (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 *)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 ca6b60546..3d0cb00f8 100644 --- a/NextcloudTalk/NCAPIController.m +++ b/NextcloudTalk/NCAPIController.m @@ -1554,7 +1554,7 @@ - (NSURLSessionDataTask *)getReactions:(NSString *)reaction fromMessage:(NSInteg #pragma mark - Polls Controller -- (NSURLSessionDataTask *)createPollWithQuestion:(NSString *)question options:(NSArray *)options resultMode:(NCPollResultMode)resultMode maxVotes:(NSInteger)maxVotes inRoom:(NSString *)token forAccount:(TalkAccount *)account withCompletionBlock:(PollCompletionBlock)block +- (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 { NSString *encodedToken = [token stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLHostAllowedCharacterSet]]; NSString *endpoint = [NSString stringWithFormat:@"poll/%@", encodedToken]; @@ -1563,6 +1563,7 @@ - (NSURLSessionDataTask *)createPollWithQuestion:(NSString *)question options:(N NSDictionary *parameters = @{@"question" : question, @"options" : options, @"resultMode" : @(resultMode), + @"draft" : @(asDraft), @"maxVotes" : @(maxVotes) }; NCAPISessionManager *apiSessionManager = [_apiSessionManagers objectForKey:account.accountId]; @@ -1610,6 +1611,31 @@ - (NSURLSessionDataTask *)getPollWithId:(NSInteger)pollId inRoom:(NSString *)tok return task; } +- (NSURLSessionDataTask *)getPollDraftsInRoom:(NSString *)token forAccount:(TalkAccount *)account withCompletionBlock:(PollDraftsCompletionBlock)block +{ + NSString *encodedToken = [token stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLHostAllowedCharacterSet]]; + NSString *endpoint = [NSString stringWithFormat:@"poll/%@/drafts", encodedToken]; + NSInteger pollsAPIVersion = [self pollsAPIVersionForAccount:account]; + NSString *URLString = [self getRequestURLForEndpoint:endpoint withAPIVersion:pollsAPIVersion forAccount:account]; + + NCAPISessionManager *apiSessionManager = [_apiSessionManagers objectForKey:account.accountId]; + NSURLSessionDataTask *task = [apiSessionManager GET:URLString parameters:nil progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) { + NSArray *pollDrafts = [[responseObject objectForKey:@"ocs"] objectForKey:@"data"]; + if (block) { + NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)task.response; + block(pollDrafts, 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 *)voteOnPollWithId:(NSInteger)pollId inRoom:(NSString *)token withOptions:(NSArray *)options forAccount:(TalkAccount *)account withCompletionBlock:(PollCompletionBlock)block { NSString *encodedToken = [token stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLHostAllowedCharacterSet]]; diff --git a/NextcloudTalk/NCDatabaseManager.h b/NextcloudTalk/NCDatabaseManager.h index 39afbcf4e..9f7a1bd69 100644 --- a/NextcloudTalk/NCDatabaseManager.h +++ b/NextcloudTalk/NCDatabaseManager.h @@ -78,6 +78,7 @@ extern NSString * const kCapabilityChatSummary; extern NSString * const kCapabilityArchivedConversationsV2; extern NSString * const kCapabilityCallNotificationState; extern NSString * const kCapabilityCallForceMute; +extern NSString * const kCapabilityTalkPollsDrafts; extern NSString * const kNotificationsCapabilityExists; extern NSString * const kNotificationsCapabilityTestPush; diff --git a/NextcloudTalk/NCDatabaseManager.m b/NextcloudTalk/NCDatabaseManager.m index 7a50882ab..219133be2 100644 --- a/NextcloudTalk/NCDatabaseManager.m +++ b/NextcloudTalk/NCDatabaseManager.m @@ -79,6 +79,7 @@ NSString * const kCapabilityArchivedConversationsV2 = @"archived-conversations-v2"; NSString * const kCapabilityCallNotificationState = @"call-notification-state-api"; NSString * const kCapabilityForceMute = @"force-mute"; +NSString * const kCapabilityTalkPollsDrafts = @"talk-polls-drafts"; NSString * const kNotificationsCapabilityExists = @"exists"; NSString * const kNotificationsCapabilityTestPush = @"test-push"; diff --git a/NextcloudTalk/PlaceholderView.xib b/NextcloudTalk/PlaceholderView.xib index 85879f5d6..bfffefbc8 100644 --- a/NextcloudTalk/PlaceholderView.xib +++ b/NextcloudTalk/PlaceholderView.xib @@ -1,9 +1,9 @@ - + - + @@ -23,7 +23,7 @@ - + diff --git a/NextcloudTalk/PollCreationViewController.swift b/NextcloudTalk/PollCreationViewController.swift index 081ffe656..ab572e0e4 100644 --- a/NextcloudTalk/PollCreationViewController.swift +++ b/NextcloudTalk/PollCreationViewController.swift @@ -5,11 +5,7 @@ import UIKit -@objc protocol PollCreationViewControllerDelegate { - func pollCreationViewControllerWantsToCreatePoll(pollCreationViewController: PollCreationViewController, question: String, options: [String], resultMode: NCPollResultMode, maxVotes: Int) -} - -@objcMembers class PollCreationViewController: UITableViewController, UITextFieldDelegate { +@objcMembers class PollCreationViewController: UITableViewController, UITextFieldDelegate, PollDraftsViewControllerDelegate { enum PollCreationSection: Int { case kPollCreationSectionQuestion = 0 @@ -26,26 +22,38 @@ import UIKit let kQuestionTextFieldTag = 9999 - public weak var pollCreationDelegate: PollCreationViewControllerDelegate? + var room: NCRoom + var draftsAvailable: Bool = false var question: String = "" var options: [String] = ["", ""] - var privateSwitch = UISwitch() - var multipleSwitch = UISwitch() + var anonymousPollSwitch = UISwitch() + var multipleAnswersSwitch = UISwitch() + var creatingPollIndicatorView = UIActivityIndicatorView() let footerView = PollFooterView(frame: CGRect.zero) required init?(coder aDecoder: NSCoder) { + self.room = NCRoom() + super.init(coder: aDecoder) self.initPollCreationView() } - required override init(style: UITableView.Style) { - super.init(style: style) + init(room: NCRoom) { + self.room = room + self.draftsAvailable = room.canModerate && NCDatabaseManager.sharedInstance().serverHasTalkCapability(kCapabilityTalkPollsDrafts, forAccountId: room.accountId) + + super.init(style: .insetGrouped) self.initPollCreationView() } override func viewDidLoad() { super.viewDidLoad() + self.creatingPollIndicatorView = UIActivityIndicatorView() + self.creatingPollIndicatorView.color = NCAppBranding.themeTextColor() + + self.setMoreOptionsButton() + self.navigationController?.navigationBar.titleTextAttributes = [NSAttributedString.Key.foregroundColor: NCAppBranding.themeTextColor()] self.navigationController?.navigationBar.tintColor = NCAppBranding.themeTextColor() self.navigationController?.navigationBar.barTintColor = NCAppBranding.themeColor() @@ -83,35 +91,131 @@ import UIKit self.dismiss(animated: true, completion: nil) } + func presentPollDraftsView() { + let pollDraftsVC = PollDraftsViewController(room: room) + pollDraftsVC.delegate = self + let navController = UINavigationController(rootViewController: pollDraftsVC) + present(navController, animated: true, completion: nil) + } + + func didSelectPollDraft(question: String, options: [String], resultMode: NCPollResultMode, maxVotes: Int) { + // 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.tableView.reloadData() + self.checkIfPollIsReadyToCreate() + } + 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 showDraftCreationSuccess() { + NotificationPresenter.shared().present(text: NSLocalizedString("Poll draft has been saved", comment: ""), dismissAfterDelay: 5.0, includedStyle: .dark) + removePollCreationUI() + } + + func showPollCreationUI() { + disablePollCreationButtons() + creatingPollIndicatorView.startAnimating() + navigationItem.rightBarButtonItem = UIBarButtonItem(customView: creatingPollIndicatorView) + } + + func removePollCreationUI() { + enablePollCreationButtons() + creatingPollIndicatorView.stopAnimating() + setMoreOptionsButton() + } + + func setMoreOptionsButton() { + if draftsAvailable { + let menuAction = UIAction( + title: NSLocalizedString("Browse poll drafts", comment: ""), + image: UIImage(systemName: "doc")) { _ in + self.presentPollDraftsView() + } + let menu = UIMenu(children: [menuAction]) + let menuButton = UIBarButtonItem( + image: UIImage(systemName: "ellipsis.circle"), + menu: menu + ) + + navigationItem.rightBarButtonItem = menuButton + } else { + navigationItem.rightBarButtonItem = nil + } + } + + func enablePollCreationButtons() { footerView.primaryButton.setButtonEnabled(enabled: true) + footerView.secondaryButton.setButtonEnabled(enabled: true) + } + + func disablePollCreationButtons() { + footerView.primaryButton.setButtonEnabled(enabled: false) + 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)) + footerView.frame = CGRect(x: 0, y: 0, width: 0, height: PollFooterView.heightForOption) footerView.secondaryButtonContainerView.isHidden = true + + if draftsAvailable { + footerView.secondaryButton.setTitle(NSLocalizedString("Save as draft", comment: ""), for: .normal) + footerView.secondaryButton.setButtonStyle(style: .tertiary) + footerView.secondaryButton.setButtonAction(target: self, selector: #selector(createPollDraftButtonPressed)) + + footerView.frame.size.height += PollFooterView.heightForOption + footerView.secondaryButtonContainerView.isHidden = false + } + checkIfPollIsReadyToCreate() return footerView } func createPollButtonPressed() { - let resultMode: NCPollResultMode = privateSwitch.isOn ? .hidden : .public - let maxVotes: Int = multipleSwitch.isOn ? 0 : 1 - footerView.primaryButton.setButtonEnabled(enabled: false) - self.pollCreationDelegate?.pollCreationViewControllerWantsToCreatePoll(pollCreationViewController: self, question: question, options: options, resultMode: resultMode, maxVotes: maxVotes) + createPoll(asDraft: false) + } + + func createPollDraftButtonPressed() { + createPoll(asDraft: true) + } + + func createPoll(asDraft: Bool) { + let resultMode: NCPollResultMode = anonymousPollSwitch.isOn ? .hidden : .public + 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 + if error != nil { + self.showCreationError() + } else if asDraft { + self.showDraftCreationSuccess() + } else { + self.close() + } + } } func checkIfPollIsReadyToCreate() { - footerView.primaryButton.setButtonEnabled(enabled: false) + disablePollCreationButtons() + if !question.isEmpty && options.filter({!$0.isEmpty}).count >= 2 { - footerView.primaryButton.setButtonEnabled(enabled: true) + enablePollCreationButtons() } } @@ -215,13 +319,13 @@ import UIKit } else if indexPath.section == PollCreationSection.kPollCreationSectionSettings.rawValue { if indexPath.row == PollSetting.kPollSettingPrivate.rawValue { let actionCell = tableView.dequeueOrCreateCell(withIdentifier: "PollSettingCellIdentifier") - actionCell.textLabel?.text = NSLocalizedString("Private poll", comment: "") - actionCell.accessoryView = privateSwitch + actionCell.textLabel?.text = NSLocalizedString("Anonymous poll", comment: "") + actionCell.accessoryView = anonymousPollSwitch return actionCell } else if indexPath.row == PollSetting.kPollSettingMultiple.rawValue { let actionCell = tableView.dequeueOrCreateCell(withIdentifier: "PollSettingCellIdentifier") actionCell.textLabel?.text = NSLocalizedString("Multiple answers", comment: "") - actionCell.accessoryView = multipleSwitch + actionCell.accessoryView = multipleAnswersSwitch return actionCell } } diff --git a/NextcloudTalk/PollDraftsViewController.swift b/NextcloudTalk/PollDraftsViewController.swift new file mode 100644 index 000000000..14d9f560a --- /dev/null +++ b/NextcloudTalk/PollDraftsViewController.swift @@ -0,0 +1,111 @@ +// +// SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors +// SPDX-License-Identifier: GPL-3.0-or-later +// + +import UIKit + +protocol PollDraftsViewControllerDelegate: AnyObject { + func didSelectPollDraft(question: String, options: [String], resultMode: NCPollResultMode, maxVotes: Int) +} + +class PollDraftsViewController: UITableViewController { + + weak var delegate: PollDraftsViewControllerDelegate? + + var room: NCRoom + var drafts: [NCPoll] = [] + var pollDraftsBackgroundView: PlaceholderView = PlaceholderView(for: .grouped) + + init(room: NCRoom) { + self.room = room + super.init(style: .insetGrouped) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + self.navigationController?.navigationBar.titleTextAttributes = [NSAttributedString.Key.foregroundColor: NCAppBranding.themeTextColor()] + self.navigationController?.navigationBar.tintColor = NCAppBranding.themeTextColor() + self.navigationController?.navigationBar.barTintColor = NCAppBranding.themeColor() + self.navigationController?.navigationBar.isTranslucent = false + self.navigationItem.title = NSLocalizedString("Poll drafts", comment: "") + + let appearance = UINavigationBarAppearance() + appearance.configureWithOpaqueBackground() + appearance.titleTextAttributes = [.foregroundColor: NCAppBranding.themeTextColor()] + appearance.backgroundColor = NCAppBranding.themeColor() + self.navigationItem.standardAppearance = appearance + self.navigationItem.compactAppearance = appearance + self.navigationItem.scrollEdgeAppearance = appearance + + let closeButton = UIBarButtonItem(title: nil, style: .plain, target: nil, action: nil) + closeButton.primaryAction = UIAction(title: NSLocalizedString("Close", comment: ""), handler: { [unowned self] _ in + self.dismiss(animated: true) + }) + self.navigationItem.rightBarButtonItems = [closeButton] + + setupBackgroundView() + getPollDrafts() + } + + // MARK: - Backgroud view + private func setupBackgroundView() { + pollDraftsBackgroundView.placeholderView.isHidden = true + pollDraftsBackgroundView.loadingView.startAnimating() + pollDraftsBackgroundView.placeholderTextView.text = NSLocalizedString("No poll drafts saved yet", comment: "") + pollDraftsBackgroundView.setImage(UIImage(systemName: "chart.bar")) + + tableView.backgroundView = pollDraftsBackgroundView + } + + // MARK: - Poll drafts + private func getPollDrafts() { + NCAPIController.sharedInstance().getPollDrafts(inRoom: room.token, for: room.account) { drafts, error, _ in + if error == nil, let drafts { + var draftsArray: [NCPoll] = [] + let draftDicts: [[String: Any]] = drafts.compactMap { $0 as? [String: Any] } + for draftDict in draftDicts { + if let draft = NCPoll.initWithPollDictionary(draftDict) { + draftsArray.append(draft) + } + } + self.drafts = draftsArray + self.tableView.reloadData() + } + + self.pollDraftsBackgroundView.placeholderView.isHidden = drafts?.count ?? 0 > 0 + self.pollDraftsBackgroundView.loadingView.stopAnimating() + } + } + + // MARK: - TableView DataSource + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return drafts.count + } + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cellIdentifier = "PollDraftCellIdentifier" + let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier) ?? UITableViewCell(style: .subtitle, reuseIdentifier: cellIdentifier) + let draft = drafts[indexPath.row] + + cell.imageView?.image = UIImage(systemName: "chart.bar") + cell.imageView?.tintColor = UIColor.label + cell.textLabel?.text = draft.question + cell.detailTextLabel?.text = NSLocalizedString("Poll draft", comment: "") + " • " + String.localizedStringWithFormat(NSLocalizedString("%d options", comment: "Number of options in a poll"), draft.options.count) + + return cell + } + + // MARK: - TableView Delegate + 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) + } + } +} diff --git a/NextcloudTalk/PollVotingView.swift b/NextcloudTalk/PollVotingView.swift index 6ac0016e2..d7b7040bc 100644 --- a/NextcloudTalk/PollVotingView.swift +++ b/NextcloudTalk/PollVotingView.swift @@ -14,7 +14,8 @@ import UIKit } var poll: NCPoll? - var room: NCRoom? + var room: NCRoom + var draftsAvailable: Bool = false var isPollOpen: Bool = false var isOwnPoll: Bool = false var canModeratePoll: Bool = false @@ -26,20 +27,31 @@ import UIKit let footerView = PollFooterView(frame: CGRect.zero) var pollBackgroundView: PlaceholderView = PlaceholderView(for: .grouped) var userSelectedOptions: [Int] = [] + var activityIndicatorView = UIActivityIndicatorView() required init?(coder aDecoder: NSCoder) { + self.room = NCRoom() + super.init(coder: aDecoder) self.initPollView() } - required override init(style: UITableView.Style) { - super.init(style: style) + init(room: NCRoom) { + self.room = room + self.draftsAvailable = room.canModerate && NCDatabaseManager.sharedInstance().serverHasTalkCapability(kCapabilityTalkPollsDrafts, forAccountId: room.accountId) + + super.init(style: .insetGrouped) self.initPollView() } override func viewDidLoad() { super.viewDidLoad() + self.activityIndicatorView = UIActivityIndicatorView() + self.activityIndicatorView.color = NCAppBranding.themeTextColor() + + self.setMoreOptionsButton() + self.navigationController?.navigationBar.titleTextAttributes = [NSAttributedString.Key.foregroundColor: NCAppBranding.themeTextColor()] self.navigationController?.navigationBar.tintColor = NCAppBranding.themeTextColor() self.navigationController?.navigationBar.barTintColor = NCAppBranding.themeColor() @@ -79,7 +91,7 @@ import UIKit let activeAccountUserId = NCDatabaseManager.sharedInstance().activeAccount().userId self.isPollOpen = poll.status == .open self.isOwnPoll = poll.actorId == activeAccountUserId && poll.actorType == "users" - self.canModeratePoll = self.isOwnPoll || room?.isUserOwnerOrModerator ?? false + self.canModeratePoll = self.isOwnPoll || room.isUserOwnerOrModerator self.userVoted = !poll.votedSelf.isEmpty self.userVotedOptions = poll.votedSelf as? [Int] ?? [] self.userSelectedOptions = self.userVotedOptions @@ -131,7 +143,7 @@ import UIKit } func voteButtonPressed() { - guard let poll, let room else {return} + guard let poll else {return} footerView.primaryButton.isEnabled = false NCAPIController.sharedInstance().voteOnPoll(withId: poll.pollId, inRoom: room.token, withOptions: userSelectedOptions, @@ -192,7 +204,7 @@ import UIKit } func closePoll() { - guard let poll, let room else {return} + guard let poll else {return} NCAPIController.sharedInstance().closePoll(withId: poll.pollId, inRoom: room.token, for: NCDatabaseManager.sharedInstance().activeAccount()) { responsePoll, error, _ in if let responsePoll = responsePoll, error == nil { @@ -203,6 +215,63 @@ import UIKit } } + func showActivityIndicatorView() { + activityIndicatorView.startAnimating() + navigationItem.rightBarButtonItem = UIBarButtonItem(customView: activityIndicatorView) + } + + func removeActivityIndicatorView() { + activityIndicatorView.stopAnimating() + setMoreOptionsButton() + } + + func setMoreOptionsButton() { + if draftsAvailable { + let menuAction = UIAction( + title: NSLocalizedString("Save as draft", comment: ""), + image: UIImage(systemName: "doc")) { _ in + self.createPollDraft() + } + let menu = UIMenu(children: [menuAction]) + let menuButton = UIBarButtonItem( + image: UIImage(systemName: "ellipsis.circle"), + menu: menu + ) + + navigationItem.rightBarButtonItem = menuButton + } else { + navigationItem.rightBarButtonItem = nil + } + } + + func showDraftCreationSuccess() { + NotificationPresenter.shared().present(text: NSLocalizedString("Poll draft has been saved", comment: ""), dismissAfterDelay: 5.0, includedStyle: .dark) + } + + func showDraftCreationError() { + let alert = UIAlertController(title: NSLocalizedString("Creating poll draft failed", comment: ""), + message: NSLocalizedString("An error occurred while creating poll draft", comment: ""), + preferredStyle: .alert) + alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .cancel, handler: nil)) + self.present(alert, animated: true) + } + + func createPollDraft() { + guard let poll else {return} + + showActivityIndicatorView() + + NCAPIController.sharedInstance().createPoll(withQuestion: poll.question, options: poll.options, resultMode: poll.resultMode, maxVotes: poll.maxVotes, inRoom: room.token, asDraft: true, for: room.account) { _, error, _ in + if error == nil { + self.showDraftCreationSuccess() + } else { + self.showDraftCreationError() + } + + self.removeActivityIndicatorView() + } + } + override func numberOfSections(in tableView: UITableView) -> Int { return PollSection.kPollSectionCount.rawValue } @@ -294,7 +363,7 @@ import UIKit return } - guard let poll, let room else {return} + guard let poll else {return} if showPollResults { if poll.details.isEmpty {return} diff --git a/NextcloudTalk/RoomSharedItemsTableViewController.swift b/NextcloudTalk/RoomSharedItemsTableViewController.swift index d56341f69..222ab5149 100644 --- a/NextcloudTalk/RoomSharedItemsTableViewController.swift +++ b/NextcloudTalk/RoomSharedItemsTableViewController.swift @@ -331,8 +331,7 @@ import QuickLook // MARK: - Polls func presentPoll(pollId: Int) { - let pollViewController = PollVotingView(style: .insetGrouped) - pollViewController.room = room + let pollViewController = PollVotingView(room: room) let navigationViewController = NCNavigationController(rootViewController: pollViewController) self.present(navigationViewController, animated: true, completion: nil) diff --git a/NextcloudTalk/en.lproj/Localizable.strings b/NextcloudTalk/en.lproj/Localizable.strings index bf290415e..cfdfab809 100644 --- a/NextcloudTalk/en.lproj/Localizable.strings +++ b/NextcloudTalk/en.lproj/Localizable.strings @@ -22,6 +22,9 @@ /* No comment provided by engineer. */ "%d minutes ago" = "%d minutes ago"; +/* Number of options in a poll */ +"%d options" = "%d options"; + /* Votes in a poll */ "%d votes" = "%d votes"; @@ -247,6 +250,9 @@ /* No comment provided by engineer. */ "An error occurred while clearing status message" = "An error occurred while clearing status message"; +/* No comment provided by engineer. */ +"An error occurred while creating poll draft" = "An error occurred while creating poll draft"; + /* No comment provided by engineer. */ "An error occurred while creating the poll" = "An error occurred while creating the poll"; @@ -298,6 +304,9 @@ /* Alice, Bob, Charlie and 1 other is typing… */ "and 1 other is typing…" = "and 1 other is typing…"; +/* No comment provided by engineer. */ +"Anonymous poll" = "Anonymous poll"; + /* No comment provided by engineer. */ "Answer" = "Answer"; @@ -364,6 +373,9 @@ /* No comment provided by engineer. */ "bot" = "bot"; +/* No comment provided by engineer. */ +"Browse poll drafts" = "Browse poll drafts"; + /* No comment provided by engineer. */ "Cached files" = "Cached files"; @@ -637,6 +649,9 @@ /* No comment provided by engineer. */ "Create poll" = "Create poll"; +/* No comment provided by engineer. */ +"Creating poll draft failed" = "Creating poll draft failed"; + /* No comment provided by engineer. */ "Creating poll failed" = "Creating poll failed"; @@ -1255,6 +1270,9 @@ /* No comment provided by engineer. */ "No permission to join this conversation" = "No permission to join this conversation"; +/* No comment provided by engineer. */ +"No poll drafts saved yet" = "No poll drafts saved yet"; + /* No comment provided by engineer. */ "No response from server" = "No response from server"; @@ -1393,6 +1411,15 @@ /* No comment provided by engineer. */ "Poll" = "Poll"; +/* No comment provided by engineer. */ +"Poll draft" = "Poll draft"; + +/* No comment provided by engineer. */ +"Poll draft has been saved" = "Poll draft has been saved"; + +/* No comment provided by engineer. */ +"Poll drafts" = "Poll drafts"; + /* No comment provided by engineer. */ "Poll results" = "Poll results"; @@ -1408,9 +1435,6 @@ /* No comment provided by engineer. */ "Private" = "Private"; -/* No comment provided by engineer. */ -"Private poll" = "Private poll"; - /* No comment provided by engineer. */ "Profile" = "Profile"; @@ -1531,6 +1555,9 @@ /* Save conversation description */ "Save" = "Save"; +/* No comment provided by engineer. */ +"Save as draft" = "Save as draft"; + /* No comment provided by engineer. */ "Save to 'Note to self'" = "Save to 'Note to self'"; diff --git a/NextcloudTalk/en.lproj/Localizable.stringsdict b/NextcloudTalk/en.lproj/Localizable.stringsdict index 51af24f93..9bf37f565 100644 --- a/NextcloudTalk/en.lproj/Localizable.stringsdict +++ b/NextcloudTalk/en.lproj/Localizable.stringsdict @@ -120,5 +120,22 @@ You have %ld pending invitations + + %d options + + NSStringLocalizedFormatKey + %#@options@ + options + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %d option + other + %d options + +