UserProfileTableViewController.swift 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332
  1. //
  2. // SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
  3. // SPDX-License-Identifier: GPL-3.0-or-later
  4. //
  5. import UIKit
  6. enum ProfileSection: Int {
  7. case kProfileSectionName = 0
  8. case kProfileSectionEmail
  9. case kProfileSectionPhoneNumber
  10. case kProfileSectionAddress
  11. case kProfileSectionWebsite
  12. case kProfileSectionTwitter
  13. case kProfileSectionSummary
  14. case kProfileSectionRemoveAccount
  15. }
  16. enum SummaryRow: Int {
  17. case kSummaryRowEmail = 0
  18. case kSummaryRowPhoneNumber
  19. case kSummaryRowAddress
  20. case kSummaryRowWebsite
  21. case kSummaryRowTwitter
  22. }
  23. @objcMembers
  24. class UserProfileTableViewController: UITableViewController, DetailedOptionsSelectorTableViewControllerDelegate, TOCropViewControllerDelegate {
  25. let kNameTextFieldTag = 99
  26. let kEmailTextFieldTag = 98
  27. let kPhoneTextFieldTag = 97
  28. let kAddressTextFieldTag = 96
  29. let kWebsiteTextFieldTag = 95
  30. let kTwitterTextFieldTag = 94
  31. let kAvatarScopeButtonTag = 93
  32. let iconConfiguration = UIImage.SymbolConfiguration(pointSize: 18)
  33. let iconHeaderConfiguration = UIImage.SymbolConfiguration(pointSize: 13)
  34. var account = TalkAccount()
  35. var isEditable = Bool()
  36. var waitingForModification = Bool()
  37. var editButton = UIBarButtonItem()
  38. var activeTextField: UITextField?
  39. var modifyingProfileView = UIActivityIndicatorView()
  40. var imagePicker: UIImagePickerController?
  41. var setPhoneAction = UIAlertAction()
  42. var phoneUtil = NBPhoneNumberUtil()
  43. var editableFields = NSArray()
  44. var showScopes = Bool()
  45. override func viewDidLoad() {
  46. super.viewDidLoad()
  47. self.navigationItem.title = NSLocalizedString("Profile", comment: "")
  48. self.navigationController?.navigationBar.titleTextAttributes = [NSAttributedString.Key.foregroundColor: NCAppBranding.themeTextColor()]
  49. self.navigationController?.navigationBar.tintColor = NCAppBranding.themeTextColor()
  50. self.navigationController?.navigationBar.barTintColor = NCAppBranding.themeColor()
  51. self.tabBarController?.tabBar.tintColor = NCAppBranding.themeColor()
  52. let themeColor: UIColor = NCAppBranding.themeColor()
  53. let appearance = UINavigationBarAppearance()
  54. appearance.configureWithOpaqueBackground()
  55. appearance.backgroundColor = themeColor
  56. appearance.titleTextAttributes = [.foregroundColor: NCAppBranding.themeTextColor()]
  57. self.navigationItem.standardAppearance = appearance
  58. self.navigationItem.compactAppearance = appearance
  59. self.navigationItem.scrollEdgeAppearance = appearance
  60. self.tableView.tableHeaderView = self.avatarHeaderView()
  61. self.showEditButton()
  62. self.getUserProfileEditableFields()
  63. if let serverCapabilities = NCDatabaseManager.sharedInstance().serverCapabilities(forAccountId: account.accountId) {
  64. showScopes = serverCapabilities.accountPropertyScopesVersion2
  65. }
  66. modifyingProfileView = UIActivityIndicatorView()
  67. modifyingProfileView.color = NCAppBranding.themeTextColor()
  68. tableView.keyboardDismissMode = UIScrollView.KeyboardDismissMode.onDrag
  69. self.tableView.register(UINib(nibName: kTextInputTableViewCellNibName, bundle: nil), forCellReuseIdentifier: kTextInputCellIdentifier)
  70. NotificationCenter.default.addObserver(self, selector: #selector(userProfileImageUpdated), name: NSNotification.Name.NCUserProfileImageUpdated, object: nil)
  71. if navigationController?.viewControllers.first == self {
  72. let barButtonItem = UIBarButtonItem(title: nil, style: .plain, target: nil, action: nil)
  73. barButtonItem.primaryAction = UIAction(title: NSLocalizedString("Close", comment: ""), handler: { [unowned self] _ in
  74. self.dismiss(animated: true)
  75. })
  76. self.navigationItem.leftBarButtonItems = [barButtonItem]
  77. }
  78. }
  79. override func viewDidLayoutSubviews() {
  80. super.viewDidLayoutSubviews()
  81. // Workaround to fix label width
  82. guard let headerView = self.tableView.tableHeaderView as? AvatarEditView else {return}
  83. guard var labelFrame = headerView.nameLabel?.frame else {return}
  84. let padding: CGFloat = 16
  85. labelFrame.origin.x = padding
  86. labelFrame.size.width = self.tableView.bounds.size.width - padding * 2
  87. headerView.nameLabel?.frame = labelFrame
  88. }
  89. init(withAccount account: TalkAccount) {
  90. super.init(style: .insetGrouped)
  91. self.account = account
  92. }
  93. required init?(coder: NSCoder) {
  94. fatalError("init(coder:) has not been implemented")
  95. }
  96. // MARK: - Table view data source
  97. override func numberOfSections(in tableView: UITableView) -> Int {
  98. return self.getProfileSections().count
  99. }
  100. override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
  101. let sections = self.getProfileSections()
  102. let profileSection = sections[section]
  103. if profileSection == ProfileSection.kProfileSectionSummary.rawValue {
  104. return self.rowsInSummarySection().count
  105. }
  106. return 1
  107. }
  108. override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
  109. let sections = self.getProfileSections()
  110. let profileSection = sections[section]
  111. switch profileSection {
  112. case ProfileSection.kProfileSectionName.rawValue,
  113. ProfileSection.kProfileSectionEmail.rawValue,
  114. ProfileSection.kProfileSectionPhoneNumber.rawValue,
  115. ProfileSection.kProfileSectionAddress.rawValue,
  116. ProfileSection.kProfileSectionWebsite.rawValue,
  117. ProfileSection.kProfileSectionTwitter.rawValue,
  118. ProfileSection.kProfileSectionRemoveAccount.rawValue:
  119. return 40
  120. case ProfileSection.kProfileSectionSummary.rawValue:
  121. return 20
  122. default:
  123. return 0
  124. }
  125. }
  126. override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
  127. let sections = self.getProfileSections()
  128. let profileSection = sections[section]
  129. let headerView = setupViewforHeaderInSection(profileSection: profileSection)
  130. if headerView.button.tag != 0 {
  131. return headerView
  132. }
  133. return nil
  134. }
  135. override func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? {
  136. let sections = self.getProfileSections()
  137. let profileSection = sections[section]
  138. if profileSection == ProfileSection.kProfileSectionEmail.rawValue {
  139. return NSLocalizedString("For password reset and notifications", comment: "")
  140. }
  141. return nil
  142. }
  143. override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
  144. let section = self.getProfileSections()[indexPath.section]
  145. switch section {
  146. case ProfileSection.kProfileSectionName.rawValue:
  147. return textInputCellWith(text: account.userDisplayName, tag: kNameTextFieldTag, interactionEnabled: editableFields.contains(kUserProfileDisplayName),
  148. keyBoardType: nil, autocapitalizationType: .sentences, placeHolder: nil)
  149. case ProfileSection.kProfileSectionEmail.rawValue:
  150. return textInputCellWith(text: account.email, tag: kEmailTextFieldTag, interactionEnabled: editableFields.contains(kUserProfileEmail),
  151. keyBoardType: .emailAddress, autocapitalizationType: nil, placeHolder: NSLocalizedString("Your email address", comment: ""))
  152. case ProfileSection.kProfileSectionPhoneNumber.rawValue:
  153. let phoneNumber = try? phoneUtil.parse(account.phone, defaultRegion: nil)
  154. let text = (phoneNumber != nil) ? try? phoneUtil.format(phoneNumber, numberFormat: NBEPhoneNumberFormat.INTERNATIONAL) : nil
  155. return textInputCellWith(text: text, tag: kPhoneTextFieldTag, interactionEnabled: false,
  156. keyBoardType: .phonePad, autocapitalizationType: nil, placeHolder: NSLocalizedString("Your phone number", comment: ""))
  157. case ProfileSection.kProfileSectionAddress.rawValue:
  158. return textInputCellWith(text: account.address, tag: kAddressTextFieldTag, interactionEnabled: editableFields.contains(kUserProfileAddress),
  159. keyBoardType: nil, autocapitalizationType: .sentences, placeHolder: NSLocalizedString("Your postal address", comment: ""))
  160. case ProfileSection.kProfileSectionWebsite.rawValue:
  161. return textInputCellWith(text: account.website, tag: kWebsiteTextFieldTag, interactionEnabled: editableFields.contains(kUserProfileWebsite),
  162. keyBoardType: .URL, autocapitalizationType: nil, placeHolder: NSLocalizedString("Link https://…", comment: ""))
  163. case ProfileSection.kProfileSectionTwitter.rawValue:
  164. return textInputCellWith(text: account.twitter, tag: kTwitterTextFieldTag, interactionEnabled: editableFields.contains(kUserProfileTwitter),
  165. keyBoardType: .emailAddress, autocapitalizationType: nil, placeHolder: NSLocalizedString("Twitter handle @…", comment: ""))
  166. case ProfileSection.kProfileSectionSummary.rawValue:
  167. return summaryCellForRow(row: indexPath.row)
  168. case ProfileSection.kProfileSectionRemoveAccount.rawValue:
  169. let actionTitle = multiAccountEnabled.boolValue ? NSLocalizedString("Remove account", comment: "") : NSLocalizedString("Log out", comment: "")
  170. let actionImage = multiAccountEnabled.boolValue ?
  171. UIImage(systemName: "trash")?.applyingSymbolConfiguration(iconConfiguration) :
  172. UIImage(systemName: "arrow.right.square")?.applyingSymbolConfiguration(iconConfiguration)
  173. return actionCellWith(identifier: "RemoveAccountCellIdentifier", text: actionTitle, textColor: .systemRed, image: actionImage, tintColor: .systemRed)
  174. default:
  175. break
  176. }
  177. return UITableViewCell()
  178. }
  179. override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
  180. let sections = getProfileSections()
  181. let section = sections[indexPath.section]
  182. if section == ProfileSection.kProfileSectionRemoveAccount.rawValue {
  183. self.showLogoutConfirmationDialog()
  184. } else if section == ProfileSection.kProfileSectionPhoneNumber.rawValue {
  185. self.presentSetPhoneNumberDialog()
  186. }
  187. self.tableView.deselectRow(at: indexPath, animated: true)
  188. }
  189. }
  190. extension UserProfileTableViewController {
  191. // MARK: Header View Setup
  192. func setupViewForSection(headerView: inout HeaderWithButton, title: String, buttonTag: Int, enabled: Bool?, scopeForImage: String) {
  193. headerView.label.text = title.uppercased()
  194. headerView.button.tag = buttonTag
  195. if let enabled = enabled {
  196. headerView.button.isEnabled = enabled
  197. }
  198. headerView.button.setImage(self.imageForScope(scope: scopeForImage)?.applyingSymbolConfiguration(iconHeaderConfiguration), for: .normal)
  199. }
  200. func setupViewforHeaderInSection(profileSection: Int) -> HeaderWithButton {
  201. var headerView = HeaderWithButton()
  202. headerView.button.addTarget(self, action: #selector(showScopeSelectionDialog(_:)), for: .touchUpInside)
  203. var shouldEnableNameAndEmailScopeButton = false
  204. if let serverCapabilities = NCDatabaseManager.sharedInstance().serverCapabilities(forAccountId: account.accountId) {
  205. shouldEnableNameAndEmailScopeButton = serverCapabilities.accountPropertyScopesFederationEnabled ||
  206. serverCapabilities.accountPropertyScopesFederatedEnabled || serverCapabilities.accountPropertyScopesPublishedEnabled
  207. }
  208. switch profileSection {
  209. case ProfileSection.kProfileSectionName.rawValue:
  210. setupViewForSection(headerView: &headerView, title: NSLocalizedString("Full name", comment: ""), buttonTag: kNameTextFieldTag,
  211. enabled: shouldEnableNameAndEmailScopeButton, scopeForImage: account.userDisplayNameScope)
  212. case ProfileSection.kProfileSectionEmail.rawValue:
  213. setupViewForSection(headerView: &headerView, title: NSLocalizedString("Email", comment: ""), buttonTag: kEmailTextFieldTag,
  214. enabled: shouldEnableNameAndEmailScopeButton, scopeForImage: account.emailScope)
  215. case ProfileSection.kProfileSectionPhoneNumber.rawValue:
  216. setupViewForSection(headerView: &headerView, title: NSLocalizedString("Phone number", comment: ""), buttonTag: kPhoneTextFieldTag,
  217. enabled: nil, scopeForImage: account.phoneScope)
  218. case ProfileSection.kProfileSectionAddress.rawValue:
  219. setupViewForSection(headerView: &headerView, title: NSLocalizedString("Address", comment: ""), buttonTag: kAddressTextFieldTag,
  220. enabled: nil, scopeForImage: account.addressScope)
  221. case ProfileSection.kProfileSectionWebsite.rawValue:
  222. setupViewForSection(headerView: &headerView, title: NSLocalizedString("Website", comment: ""), buttonTag: kWebsiteTextFieldTag,
  223. enabled: nil, scopeForImage: account.websiteScope)
  224. case ProfileSection.kProfileSectionTwitter.rawValue:
  225. setupViewForSection(headerView: &headerView, title: NSLocalizedString("Twitter", comment: ""), buttonTag: kTwitterTextFieldTag,
  226. enabled: nil, scopeForImage: account.twitterScope)
  227. default:
  228. break
  229. }
  230. return headerView
  231. }
  232. // MARK: Setup cells
  233. func textInputCellWith(text: String?, tag: Int?, interactionEnabled: Bool?, keyBoardType: UIKeyboardType?, autocapitalizationType: UITextAutocapitalizationType?, placeHolder: String?) -> TextInputTableViewCell {
  234. let textInputCell = tableView.dequeueReusableCell(withIdentifier: kTextInputCellIdentifier) as? TextInputTableViewCell ??
  235. TextInputTableViewCell(style: .default, reuseIdentifier: kTextInputCellIdentifier)
  236. textInputCell.textField.delegate = self
  237. if let text = text {
  238. textInputCell.textField.text = text
  239. }
  240. if let tag = tag {
  241. textInputCell.textField.tag = tag
  242. }
  243. if let interactionEnabled = interactionEnabled {
  244. textInputCell.textField.isUserInteractionEnabled = interactionEnabled
  245. }
  246. if let keyBoardType = keyBoardType {
  247. textInputCell.textField.keyboardType = keyBoardType
  248. }
  249. if let autocapitalizationType = autocapitalizationType {
  250. textInputCell.textField.autocapitalizationType = autocapitalizationType
  251. }
  252. if let placeHolder = placeHolder {
  253. textInputCell.textField.placeholder = placeHolder
  254. }
  255. return textInputCell
  256. }
  257. func summaryCellForRow(row: Int) -> UITableViewCell {
  258. let summaryCell = tableView.dequeueReusableCell(withIdentifier: "SummaryCellIdentifier") ?? UITableViewCell(style: .default, reuseIdentifier: "SummaryCellIdentifier")
  259. let summaryRow = self.rowsInSummarySection()[row]
  260. switch summaryRow {
  261. case SummaryRow.kSummaryRowEmail.rawValue:
  262. summaryCell.textLabel?.text = account.email
  263. summaryCell.imageView?.image = UIImage(systemName: "envelope")?.applyingSymbolConfiguration(iconConfiguration)
  264. case SummaryRow.kSummaryRowPhoneNumber.rawValue:
  265. let phoneNumber = try? phoneUtil.parse(account.phone, defaultRegion: nil)
  266. let text = (phoneNumber != nil) ? try? phoneUtil.format(phoneNumber, numberFormat: NBEPhoneNumberFormat.INTERNATIONAL) : nil
  267. summaryCell.textLabel?.text = text
  268. summaryCell.imageView?.image = UIImage(systemName: "iphone")?.applyingSymbolConfiguration(iconConfiguration)
  269. case SummaryRow.kSummaryRowAddress.rawValue:
  270. summaryCell.textLabel?.text = account.address
  271. summaryCell.imageView?.image = UIImage(systemName: "mappin")?.applyingSymbolConfiguration(iconConfiguration)
  272. case SummaryRow.kSummaryRowWebsite.rawValue:
  273. summaryCell.textLabel?.text = account.website
  274. summaryCell.imageView?.image = UIImage(systemName: "network")?.applyingSymbolConfiguration(iconConfiguration)
  275. case SummaryRow.kSummaryRowTwitter.rawValue:
  276. summaryCell.textLabel?.text = account.twitter
  277. summaryCell.imageView?.image = UIImage(named: "twitter")?.withRenderingMode(.alwaysTemplate)
  278. default:
  279. break
  280. }
  281. summaryCell.imageView?.tintColor = .secondaryLabel
  282. return summaryCell
  283. }
  284. func actionCellWith(identifier: String, text: String, textColor: UIColor, image: UIImage?, tintColor: UIColor) -> UITableViewCell {
  285. let actionCell = tableView.dequeueReusableCell(withIdentifier: identifier) ?? UITableViewCell(style: .default, reuseIdentifier: identifier)
  286. actionCell.textLabel?.text = text
  287. actionCell.textLabel?.textColor = textColor
  288. actionCell.imageView?.image = image?.withRenderingMode(.alwaysTemplate)
  289. actionCell.imageView?.tintColor = tintColor
  290. return actionCell
  291. }
  292. }