RoomAvatarInfoTableViewController.swift 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402
  1. //
  2. // SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
  3. // SPDX-License-Identifier: GPL-3.0-or-later
  4. //
  5. import Foundation
  6. import UIKit
  7. enum RoomAvatarInfoSection: Int {
  8. case kRoomNameSection = 0
  9. case kRoomDescriptionSection
  10. }
  11. @objcMembers class RoomAvatarInfoTableViewController: UITableViewController,
  12. UINavigationControllerDelegate,
  13. UIImagePickerControllerDelegate,
  14. UITextFieldDelegate,
  15. AvatarEditViewDelegate,
  16. EmojiAvatarPickerViewControllerDelegate,
  17. RoomDescriptionTableViewCellDelegate,
  18. TOCropViewControllerDelegate {
  19. var room: NCRoom
  20. var imagePicker: UIImagePickerController?
  21. var headerView: AvatarEditView
  22. var rightBarButton = UIBarButtonItem()
  23. var modifyingView = UIActivityIndicatorView()
  24. var descriptionHeaderView = HeaderWithButton()
  25. var currentDescription = ""
  26. init(room: NCRoom) {
  27. self.room = room
  28. self.headerView = AvatarEditView()
  29. super.init(nibName: "RoomAvatarInfoTableViewController", bundle: nil)
  30. self.headerView.delegate = self
  31. self.headerView.scopeButton.isHidden = true
  32. self.headerView.nameLabel.isHidden = true
  33. self.descriptionHeaderView.label.text = NSLocalizedString("Description", comment: "").uppercased()
  34. self.descriptionHeaderView.button.setTitle(NSLocalizedString("Save", comment: "Save conversation description"), for: .normal)
  35. self.descriptionHeaderView.button.addTarget(self, action: #selector(setButtonPressed), for: .touchUpInside)
  36. self.descriptionHeaderView.button.isHidden = true
  37. self.currentDescription = self.room.roomDescription
  38. self.updateHeaderView()
  39. }
  40. required init?(coder: NSCoder) {
  41. fatalError("init(coder:) has not been implemented")
  42. }
  43. override func viewDidLoad() {
  44. super.viewDidLoad()
  45. self.navigationController?.navigationBar.titleTextAttributes = [NSAttributedString.Key.foregroundColor: NCAppBranding.themeTextColor()]
  46. self.navigationController?.navigationBar.tintColor = NCAppBranding.themeTextColor()
  47. self.navigationController?.navigationBar.barTintColor = NCAppBranding.themeColor()
  48. self.navigationController?.navigationBar.isTranslucent = false
  49. self.navigationItem.title = NSLocalizedString("Conversation details", comment: "")
  50. let appearance = UINavigationBarAppearance()
  51. appearance.configureWithOpaqueBackground()
  52. appearance.titleTextAttributes = [.foregroundColor: NCAppBranding.themeTextColor()]
  53. appearance.backgroundColor = NCAppBranding.themeColor()
  54. self.navigationItem.standardAppearance = appearance
  55. self.navigationItem.compactAppearance = appearance
  56. self.navigationItem.scrollEdgeAppearance = appearance
  57. self.tableView.register(UINib(nibName: kTextInputTableViewCellNibName, bundle: nil), forCellReuseIdentifier: kTextInputCellIdentifier)
  58. self.tableView.register(UINib(nibName: RoomDescriptionTableViewCell.nibName, bundle: nil), forCellReuseIdentifier: RoomDescriptionTableViewCell.identifier)
  59. self.tableView.tableHeaderView = self.headerView
  60. self.modifyingView.color = NCAppBranding.themeTextColor()
  61. }
  62. override func viewWillAppear(_ animated: Bool) {
  63. super.viewWillAppear(animated)
  64. }
  65. func updateHeaderView() {
  66. self.headerView.avatarImageView.setAvatar(for: self.room)
  67. self.headerView.editView.isHidden = !NCDatabaseManager.sharedInstance().serverHasTalkCapability(kCapabilityConversationAvatars, forAccountId: self.room.accountId)
  68. self.headerView.trashButton.isHidden = !self.room.isCustomAvatar
  69. // Need to have an explicit size here for the header view
  70. let size = self.headerView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
  71. self.headerView.frame = CGRect(origin: .zero, size: size)
  72. }
  73. func getAvatarInfoSections() -> [Int] {
  74. var sections = [Int]()
  75. // Room name section
  76. sections.append(RoomAvatarInfoSection.kRoomNameSection.rawValue)
  77. // Room description section
  78. if NCDatabaseManager.sharedInstance().serverHasTalkCapability(kCapabilityRoomDescription, forAccountId: self.room.accountId) {
  79. sections.append(RoomAvatarInfoSection.kRoomDescriptionSection.rawValue)
  80. }
  81. return sections
  82. }
  83. override func numberOfSections(in tableView: UITableView) -> Int {
  84. return self.getAvatarInfoSections().count
  85. }
  86. override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
  87. if section == RoomAvatarInfoSection.kRoomDescriptionSection.rawValue {
  88. return descriptionHeaderView
  89. }
  90. return nil
  91. }
  92. override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
  93. return 40
  94. }
  95. override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
  96. return 1
  97. }
  98. override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
  99. let textInputCell = tableView.dequeueReusableCell(withIdentifier: kTextInputCellIdentifier) as? TextInputTableViewCell ??
  100. TextInputTableViewCell(style: .default, reuseIdentifier: kTextInputCellIdentifier)
  101. if indexPath.section == RoomAvatarInfoSection.kRoomNameSection.rawValue {
  102. textInputCell.textField.delegate = self
  103. textInputCell.textField.text = self.room.displayName
  104. textInputCell.textField.autocapitalizationType = .sentences
  105. textInputCell.selectionStyle = .none
  106. return textInputCell
  107. } else if indexPath.section == RoomAvatarInfoSection.kRoomDescriptionSection.rawValue {
  108. let descriptionCell = tableView.dequeueReusableCell(withIdentifier: RoomDescriptionTableViewCell.identifier) as? RoomDescriptionTableViewCell ??
  109. RoomDescriptionTableViewCell(style: .default, reuseIdentifier: RoomDescriptionTableViewCell.identifier)
  110. descriptionCell.textView?.text = self.room.roomDescription
  111. descriptionCell.textView?.isEditable = true
  112. descriptionCell.delegate = self
  113. descriptionCell.characterLimit = 500
  114. descriptionCell.selectionStyle = .none
  115. return descriptionCell
  116. }
  117. return textInputCell
  118. }
  119. override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
  120. if section == RoomAvatarInfoSection.kRoomNameSection.rawValue {
  121. return NSLocalizedString("Name", comment: "")
  122. } else if section == RoomAvatarInfoSection.kRoomDescriptionSection.rawValue {
  123. return NSLocalizedString("Description", comment: "")
  124. }
  125. return nil
  126. }
  127. func updateRoomAndRemoveModifyingView() {
  128. NCRoomsManager.sharedInstance().updateRoom(self.room.token) { _, _ in
  129. guard let room = NCDatabaseManager.sharedInstance().room(withToken: self.room.token, forAccountId: self.room.accountId) else { return }
  130. self.room = room
  131. self.currentDescription = self.room.roomDescription
  132. self.updateHeaderView()
  133. self.tableView.reloadData()
  134. self.removeModifyingView()
  135. }
  136. }
  137. func setButtonPressed() {
  138. self.showModifyingView()
  139. self.descriptionHeaderView.button.isHidden = true
  140. let activeAccount = NCDatabaseManager.sharedInstance().activeAccount()
  141. NCAPIController.sharedInstance().setRoomDescription(currentDescription, forRoom: room.token, forAccount: activeAccount) { error in
  142. if error != nil {
  143. NCUserInterfaceController.sharedInstance().presentAlert(withTitle: NSLocalizedString("An error occurred while setting description", comment: ""), withMessage: nil)
  144. }
  145. self.updateRoomAndRemoveModifyingView()
  146. }
  147. }
  148. func showModifyingView() {
  149. modifyingView.startAnimating()
  150. self.headerView.changeButtonState(to: false)
  151. self.navigationItem.rightBarButtonItem = UIBarButtonItem(customView: modifyingView)
  152. self.tableView.isUserInteractionEnabled = false
  153. }
  154. func removeModifyingView() {
  155. modifyingView.stopAnimating()
  156. self.headerView.changeButtonState(to: true)
  157. self.tableView.isUserInteractionEnabled = true
  158. }
  159. // MARK: - Present camera/photo library
  160. func checkAndPresentCamera() {
  161. // https://stackoverflow.com/a/20464727/2512312
  162. let mediaType: String = AVMediaType.video.rawValue
  163. let authStatus = AVCaptureDevice.authorizationStatus(for: AVMediaType(mediaType))
  164. if authStatus == AVAuthorizationStatus.authorized {
  165. self.presentCamera()
  166. return
  167. } else if authStatus == AVAuthorizationStatus.notDetermined {
  168. AVCaptureDevice.requestAccess(for: AVMediaType(mediaType)) { granted in
  169. if granted {
  170. self.presentCamera()
  171. }
  172. }
  173. return
  174. }
  175. let alertTitle = NSLocalizedString("Could not access camera", comment: "")
  176. let alertMessage = NSLocalizedString("Camera access is not allowed. Check your settings.", comment: "")
  177. NCUserInterfaceController.sharedInstance().presentAlert(withTitle: alertTitle, withMessage: alertMessage)
  178. }
  179. func presentCamera() {
  180. DispatchQueue.main.async {
  181. self.imagePicker = UIImagePickerController()
  182. if let imagePicker = self.imagePicker {
  183. imagePicker.sourceType = .camera
  184. imagePicker.delegate = self
  185. self.present(imagePicker, animated: true)
  186. }
  187. }
  188. }
  189. func presentPhotoLibrary() {
  190. DispatchQueue.main.async {
  191. self.imagePicker = UIImagePickerController()
  192. if let imagePicker = self.imagePicker {
  193. imagePicker.sourceType = .photoLibrary
  194. imagePicker.delegate = self
  195. self.present(imagePicker, animated: true)
  196. }
  197. }
  198. }
  199. func presentEmojiAvatarPicker() {
  200. DispatchQueue.main.async {
  201. let emojiAvatarPickerVC = EmojiAvatarPickerViewController()
  202. emojiAvatarPickerVC.delegate = self
  203. let emojiAvatarPickerNC = UINavigationController(rootViewController: emojiAvatarPickerVC)
  204. self.present(emojiAvatarPickerNC, animated: true, completion: nil)
  205. }
  206. }
  207. // MARK: - UIImagePickerController Delegate
  208. func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) {
  209. let mediaType = info[UIImagePickerController.InfoKey.mediaType] as? String
  210. if mediaType == "public.image" {
  211. let image = info[UIImagePickerController.InfoKey.originalImage] as? UIImage
  212. self.dismiss(animated: true) {
  213. if let image = image {
  214. let cropViewController = TOCropViewController(croppingStyle: TOCropViewCroppingStyle.circular, image: image)
  215. cropViewController.delegate = self
  216. self.present(cropViewController, animated: true)
  217. }
  218. }
  219. }
  220. }
  221. func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
  222. self.dismiss(animated: true, completion: nil)
  223. }
  224. // MARK: - RoomDescriptionTableViewCellDelegate
  225. func roomDescriptionCellTextViewDidChange(_ cell: RoomDescriptionTableViewCell) {
  226. DispatchQueue.main.async {
  227. self.tableView?.beginUpdates()
  228. self.tableView?.endUpdates()
  229. self.currentDescription = cell.textView?.text ?? ""
  230. self.descriptionHeaderView.button.isHidden = self.currentDescription == self.room.roomDescription
  231. }
  232. }
  233. func roomDescriptionCellDidExceedLimit(_ cell: RoomDescriptionTableViewCell) {
  234. NotificationPresenter.shared().present(
  235. text: NSLocalizedString("Description cannot be longer than 500 characters", comment: ""),
  236. dismissAfterDelay: 3.0,
  237. includedStyle: .warning)
  238. }
  239. // MARK: - TOCROPViewControllerDelegate
  240. func cropViewController(_ cropViewController: TOCropViewController, didCropTo image: UIImage, with cropRect: CGRect, angle: Int) {
  241. // Fixes weird iOS 13 bug: https://github.com/TimOliver/TOCropViewController/issues/365
  242. cropViewController.transitioningDelegate = nil
  243. cropViewController.dismiss(animated: true) {
  244. // Need to dismiss cropViewController first before showing the activityIndicator
  245. self.showModifyingView()
  246. NCAPIController.sharedInstance().setAvatarFor(self.room, with: image) { error in
  247. if error != nil {
  248. let errorDialog = UIAlertController(title: NSLocalizedString("An error occurred while setting the avatar", comment: ""), message: nil, preferredStyle: .alert)
  249. let okAction = UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .default, handler: nil)
  250. errorDialog.addAction(okAction)
  251. self.present(errorDialog, animated: true, completion: nil)
  252. }
  253. self.updateRoomAndRemoveModifyingView()
  254. }
  255. }
  256. }
  257. func cropViewController(_ cropViewController: TOCropViewController, didFinishCancelled cancelled: Bool) {
  258. // Fixes weird iOS 13 bug: https://github.com/TimOliver/TOCropViewController/issues/365
  259. cropViewController.transitioningDelegate = nil
  260. cropViewController.dismiss(animated: true)
  261. }
  262. // MARK: - AvaterEditViewDelegate
  263. func avatarEditViewPresentCamera(_ controller: AvatarEditView?) {
  264. self.checkAndPresentCamera()
  265. }
  266. func avatarEditViewPresentPhotoLibrary(_ controller: AvatarEditView?) {
  267. self.presentPhotoLibrary()
  268. }
  269. func avatarEditViewPresentEmojiAvatarPicker(_ controller: AvatarEditView?) {
  270. self.presentEmojiAvatarPicker()
  271. }
  272. func avatarEditViewRemoveAvatar(_ controller: AvatarEditView?) {
  273. self.showModifyingView()
  274. NCAPIController.sharedInstance().removeAvatar(for: room) { error in
  275. if error != nil {
  276. NCUserInterfaceController.sharedInstance().presentAlert(withTitle: NSLocalizedString("An error occurred while removing the avatar", comment: ""), withMessage: nil)
  277. }
  278. self.updateRoomAndRemoveModifyingView()
  279. }
  280. }
  281. // MARK: - EmojiAvatarPickerViewControllerDelegate
  282. func didSelectEmoji(emoji: NSString, color: NSString, image: UIImage) {
  283. self.showModifyingView()
  284. NCAPIController.sharedInstance().setEmojiAvatarFor(room, withEmoji: emoji as String, andColor: color as String) { error in
  285. if error != nil {
  286. let errorDialog = UIAlertController(title: NSLocalizedString("An error occurred while setting the avatar", comment: ""), message: nil, preferredStyle: .alert)
  287. let okAction = UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .default, handler: nil)
  288. errorDialog.addAction(okAction)
  289. self.present(errorDialog, animated: true, completion: nil)
  290. }
  291. self.updateRoomAndRemoveModifyingView()
  292. }
  293. }
  294. // MARK: - UITextField delegate
  295. func textFieldShouldReturn(_ textField: UITextField) -> Bool {
  296. textField.resignFirstResponder()
  297. let newRoomValue = textField.text!.trimmingCharacters(in: CharacterSet.whitespaces)
  298. if newRoomValue == self.room.name {
  299. return true
  300. }
  301. if newRoomValue.isEmpty {
  302. let alertTitle = NSLocalizedString("Could not set conversation name", comment: "")
  303. let alertMessage = NSLocalizedString("Conversation name cannot be empty", comment: "")
  304. NCUserInterfaceController.sharedInstance().presentAlert(withTitle: alertTitle, withMessage: alertMessage)
  305. self.tableView.reloadData()
  306. return true
  307. }
  308. self.showModifyingView()
  309. let activeAccount = NCDatabaseManager.sharedInstance().activeAccount()
  310. NCAPIController.sharedInstance().renameRoom(self.room.token, forAccount: activeAccount, withName: newRoomValue) { error in
  311. if error != nil {
  312. let alertTitle = NSLocalizedString("Could not rename the conversation", comment: "")
  313. NCUserInterfaceController.sharedInstance().presentAlert(withTitle: alertTitle, withMessage: nil)
  314. }
  315. self.updateRoomAndRemoveModifyingView()
  316. }
  317. return true
  318. }
  319. }