PollVotingView.swift 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316
  1. //
  2. // SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
  3. // SPDX-License-Identifier: GPL-3.0-or-later
  4. //
  5. import UIKit
  6. @objcMembers class PollVotingView: UITableViewController {
  7. enum PollSection: Int {
  8. case kPollSectionQuestion = 0
  9. case kPollSectionOptions
  10. case kPollSectionCount
  11. }
  12. var poll: NCPoll?
  13. var room: NCRoom?
  14. var isPollOpen: Bool = false
  15. var isOwnPoll: Bool = false
  16. var canModeratePoll: Bool = false
  17. var userVoted: Bool = false
  18. var userVotedOptions: [Int] = []
  19. var editingVote: Bool = false
  20. var showPollResults: Bool = false
  21. var showIntermediateResults: Bool = false
  22. let footerView = PollFooterView(frame: CGRect.zero)
  23. var pollBackgroundView: PlaceholderView = PlaceholderView(for: .grouped)
  24. var userSelectedOptions: [Int] = []
  25. required init?(coder aDecoder: NSCoder) {
  26. super.init(coder: aDecoder)
  27. self.initPollView()
  28. }
  29. required override init(style: UITableView.Style) {
  30. super.init(style: style)
  31. self.initPollView()
  32. }
  33. override func viewDidLoad() {
  34. super.viewDidLoad()
  35. self.navigationController?.navigationBar.titleTextAttributes = [NSAttributedString.Key.foregroundColor: NCAppBranding.themeTextColor()]
  36. self.navigationController?.navigationBar.tintColor = NCAppBranding.themeTextColor()
  37. self.navigationController?.navigationBar.barTintColor = NCAppBranding.themeColor()
  38. self.navigationController?.navigationBar.isTranslucent = false
  39. self.navigationItem.title = NSLocalizedString("Poll", comment: "")
  40. let appearance = UINavigationBarAppearance()
  41. appearance.configureWithOpaqueBackground()
  42. appearance.titleTextAttributes = [.foregroundColor: NCAppBranding.themeTextColor()]
  43. appearance.backgroundColor = NCAppBranding.themeColor()
  44. self.navigationItem.standardAppearance = appearance
  45. self.navigationItem.compactAppearance = appearance
  46. self.navigationItem.scrollEdgeAppearance = appearance
  47. pollBackgroundView.placeholderView.isHidden = true
  48. pollBackgroundView.loadingView.startAnimating()
  49. self.tableView.backgroundView = pollBackgroundView
  50. self.navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(self.cancelButtonPressed))
  51. self.navigationItem.leftBarButtonItem?.tintColor = NCAppBranding.themeTextColor()
  52. }
  53. func cancelButtonPressed() {
  54. self.dismiss(animated: true, completion: nil)
  55. }
  56. func initPollView() {
  57. self.tableView.dataSource = self
  58. self.tableView.delegate = self
  59. self.tableView.register(UINib(nibName: "PollResultTableViewCell", bundle: .main), forCellReuseIdentifier: "PollResultCellIdentifier")
  60. }
  61. func setupPollView() {
  62. guard let poll = poll else {return}
  63. // Set poll settings
  64. let activeAccountUserId = NCDatabaseManager.sharedInstance().activeAccount().userId
  65. self.isPollOpen = poll.status == .open
  66. self.isOwnPoll = poll.actorId == activeAccountUserId && poll.actorType == "users"
  67. self.canModeratePoll = self.isOwnPoll || room?.isUserOwnerOrModerator ?? false
  68. self.userVoted = !poll.votedSelf.isEmpty
  69. self.userVotedOptions = poll.votedSelf as? [Int] ?? []
  70. self.userSelectedOptions = self.userVotedOptions
  71. self.showPollResults = (userVoted && !editingVote) || !isPollOpen
  72. self.showIntermediateResults = showPollResults && isPollOpen && poll.resultMode == .hidden
  73. // Set footer buttons
  74. self.tableView.tableFooterView = pollFooterView()
  75. // Set vote button state
  76. self.setVoteButtonState()
  77. // Reload table view
  78. self.tableView.reloadData()
  79. }
  80. func pollFooterView() -> UIView {
  81. var footerRect = CGRect.zero
  82. footerView.primaryButtonContainerView.isHidden = true
  83. footerView.secondaryButtonContainerView.isHidden = true
  84. if isPollOpen {
  85. // Primary button
  86. if userVoted && !editingVote {
  87. footerView.primaryButton.setTitle(NSLocalizedString("Change your vote", comment: ""), for: .normal)
  88. footerView.primaryButton.setButtonStyle(style: .secondary)
  89. footerView.primaryButton.setButtonAction(target: self, selector: #selector(editVoteButtonPressed))
  90. } else {
  91. footerView.primaryButton.setTitle(NSLocalizedString("Submit vote", comment: ""), for: .normal)
  92. footerView.primaryButton.setButtonStyle(style: .primary)
  93. footerView.primaryButton.setButtonAction(target: self, selector: #selector(voteButtonPressed))
  94. }
  95. footerRect.size.height += PollFooterView.heightForOption
  96. footerView.primaryButtonContainerView.isHidden = false
  97. // Secondary button
  98. if canModeratePoll {
  99. footerView.secondaryButton.setTitle(NSLocalizedString("End poll", comment: ""), for: .normal)
  100. footerView.secondaryButton.setButtonStyle(style: .destructive)
  101. footerView.secondaryButton.setButtonAction(target: self, selector: #selector(endPollButtonPressed))
  102. }
  103. if editingVote {
  104. footerView.secondaryButton.setTitle(NSLocalizedString("Dismiss", comment: ""), for: .normal)
  105. footerView.secondaryButton.setButtonStyle(style: .tertiary)
  106. footerView.secondaryButton.setButtonAction(target: self, selector: #selector(dismissButtonPressed))
  107. }
  108. if canModeratePoll || editingVote {
  109. footerRect.size.height += PollFooterView.heightForOption
  110. footerView.secondaryButtonContainerView.isHidden = false
  111. }
  112. }
  113. footerView.frame = footerRect
  114. return footerView
  115. }
  116. func voteButtonPressed() {
  117. guard let poll = poll, let room = room else {return}
  118. footerView.primaryButton.isEnabled = false
  119. NCAPIController.sharedInstance().voteOnPoll(withId: poll.pollId, inRoom: room.token, withOptions: userSelectedOptions,
  120. for: NCDatabaseManager.sharedInstance().activeAccount()) { responsePoll, error, _ in
  121. if let responsePoll = responsePoll, error == nil {
  122. self.poll = responsePoll
  123. self.editingVote = false
  124. }
  125. self.setupPollView()
  126. }
  127. }
  128. func editVoteButtonPressed() {
  129. self.editingVote = true
  130. self.setupPollView()
  131. }
  132. func dismissButtonPressed() {
  133. self.editingVote = false
  134. self.setupPollView()
  135. }
  136. func endPollButtonPressed() {
  137. self.showClosePollConfirmationDialog()
  138. }
  139. func setVoteButtonState() {
  140. if (userSelectedOptions.isEmpty || userSelectedOptions.sorted() == userVotedOptions.sorted()) &&
  141. isPollOpen && (!userVoted || editingVote) {
  142. footerView.primaryButton.setButtonEnabled(enabled: false)
  143. } else {
  144. footerView.primaryButton.setButtonEnabled(enabled: true)
  145. }
  146. }
  147. func updatePoll(poll: NCPoll) {
  148. self.poll = poll
  149. pollBackgroundView.loadingView.stopAnimating()
  150. pollBackgroundView.loadingView.isHidden = true
  151. setupPollView()
  152. }
  153. func showClosePollConfirmationDialog() {
  154. let closePollDialog = UIAlertController(
  155. title: NSLocalizedString("End poll", comment: ""),
  156. message: NSLocalizedString("Do you really want to end this poll?", comment: ""),
  157. preferredStyle: .alert)
  158. let endAction = UIAlertAction(title: NSLocalizedString("End poll", comment: ""), style: .destructive) { _ in
  159. self.closePoll()
  160. }
  161. closePollDialog.addAction(endAction)
  162. let cancelAction = UIAlertAction(title: NSLocalizedString("Cancel", comment: ""), style: .cancel, handler: nil)
  163. closePollDialog.addAction(cancelAction)
  164. self.present(closePollDialog, animated: true, completion: nil)
  165. }
  166. func closePoll() {
  167. guard let poll = poll, let room = room else {return}
  168. NCAPIController.sharedInstance().closePoll(withId: poll.pollId, inRoom: room.token, for: NCDatabaseManager.sharedInstance().activeAccount()) { responsePoll, error, _ in
  169. if let responsePoll = responsePoll, error == nil {
  170. self.poll = responsePoll
  171. self.editingVote = false
  172. }
  173. self.setupPollView()
  174. }
  175. }
  176. override func numberOfSections(in tableView: UITableView) -> Int {
  177. return PollSection.kPollSectionCount.rawValue
  178. }
  179. override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
  180. switch section {
  181. case PollSection.kPollSectionQuestion.rawValue:
  182. return poll?.question != nil ? 1 : 0
  183. case PollSection.kPollSectionOptions.rawValue:
  184. return poll?.options?.count ?? 0
  185. default:
  186. return 0
  187. }
  188. }
  189. override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
  190. if section == PollSection.kPollSectionOptions.rawValue {
  191. let votes = poll?.numVoters ?? 0
  192. let votesString = String.localizedStringWithFormat(NSLocalizedString("%d votes", comment: "Votes in a poll"), votes)
  193. let resultsString = NSLocalizedString("Results", comment: "Results of a poll")
  194. if showPollResults && !showIntermediateResults {
  195. return resultsString + " - " + votesString
  196. } else if canModeratePoll {
  197. return votesString
  198. }
  199. }
  200. return nil
  201. }
  202. override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
  203. let pollQuestionCellIdentifier = "pollQuestionCellIdentifier"
  204. let pollOptionCellIdentifier = "pollOptionCellIdentifier"
  205. var cell = UITableViewCell()
  206. switch indexPath.section {
  207. case PollSection.kPollSectionQuestion.rawValue:
  208. cell = UITableViewCell(style: .default, reuseIdentifier: pollQuestionCellIdentifier)
  209. cell.textLabel?.text = poll?.question
  210. cell.textLabel?.numberOfLines = 4
  211. cell.textLabel?.lineBreakMode = .byWordWrapping
  212. cell.textLabel?.sizeToFit()
  213. cell.imageView?.image = UIImage(systemName: "chart.bar")
  214. cell.imageView?.tintColor = UIColor.label
  215. case PollSection.kPollSectionOptions.rawValue:
  216. if !showPollResults || showIntermediateResults {
  217. cell = UITableViewCell(style: .value1, reuseIdentifier: pollOptionCellIdentifier)
  218. cell.textLabel?.text = poll?.options[indexPath.row] as? String
  219. cell.textLabel?.numberOfLines = 4
  220. cell.textLabel?.lineBreakMode = .byWordWrapping
  221. cell.textLabel?.sizeToFit()
  222. var checkboxImageView = UIImageView(image: UIImage(systemName: "circle"))
  223. checkboxImageView.tintColor = UIColor.tertiaryLabel
  224. let votedSelf = poll?.votedSelf as? [Int] ?? []
  225. if userSelectedOptions.contains(indexPath.row) || (showIntermediateResults && votedSelf.contains(indexPath.row)) {
  226. checkboxImageView = UIImageView(image: UIImage(systemName: "checkmark.circle.fill"))
  227. checkboxImageView.tintColor = NCAppBranding.elementColor()
  228. }
  229. if showIntermediateResults {
  230. checkboxImageView.tintColor = checkboxImageView.tintColor.withAlphaComponent(0.3)
  231. }
  232. cell.accessoryView = checkboxImageView
  233. } else {
  234. let resultCell = tableView.dequeueReusableCell(withIdentifier: "PollResultCellIdentifier", for: indexPath) as? PollResultTableViewCell
  235. resultCell?.optionLabel.text = poll?.options[indexPath.row] as? String
  236. let votesDict = poll?.votes as? [String: Int] ?? [:]
  237. let optionVotes = votesDict["option-" + String(indexPath.row)] ?? 0
  238. let totalVotes = poll?.numVoters == 0 ? 1: poll?.numVoters ?? 1
  239. let progress = Float(optionVotes) / Float(totalVotes)
  240. resultCell?.optionProgressView.progress = progress
  241. resultCell?.resultLabel.text = String(Int(progress * 100)) + "%"
  242. let votedSelf = poll?.votedSelf as? [Int] ?? []
  243. if votedSelf.contains(indexPath.row) {
  244. resultCell?.highlightResult()
  245. }
  246. cell = resultCell ?? PollResultTableViewCell()
  247. }
  248. default:
  249. break
  250. }
  251. return cell
  252. }
  253. override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
  254. tableView.deselectRow(at: indexPath, animated: true)
  255. if indexPath.section != PollSection.kPollSectionOptions.rawValue || showIntermediateResults {
  256. return
  257. }
  258. guard let poll, let room else {return}
  259. if showPollResults {
  260. if poll.details.isEmpty {return}
  261. let pollResultsDetailsVC = PollResultsDetailsViewController(poll: poll, room: room)
  262. self.navigationController?.pushViewController(pollResultsDetailsVC, animated: true)
  263. }
  264. if let index = userSelectedOptions.firstIndex(of: indexPath.row), poll.maxVotes != 1 {
  265. userSelectedOptions.remove(at: index)
  266. } else {
  267. if poll.maxVotes == 1 {
  268. userSelectedOptions.removeAll()
  269. } else if poll.maxVotes > 1 && poll.maxVotes == userSelectedOptions.count {
  270. return
  271. }
  272. userSelectedOptions.append(indexPath.row)
  273. }
  274. setVoteButtonState()
  275. tableView.reloadSections(IndexSet(integer: PollSection.kPollSectionOptions.rawValue), with: .automatic)
  276. tableView.deselectRow(at: indexPath, animated: true)
  277. }
  278. }