ContactDetailsViewController.swift 38 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881
  1. // _____ _
  2. // |_ _| |_ _ _ ___ ___ _ __ __ _
  3. // | | | ' \| '_/ -_) -_) ' \/ _` |_
  4. // |_| |_||_|_| \___\___|_|_|_\__,_(_)
  5. //
  6. // Threema iOS Client
  7. // Copyright (c) 2019-2020 Threema GmbH
  8. //
  9. // This program is free software: you can redistribute it and/or modify
  10. // it under the terms of the GNU Affero General Public License, version 3,
  11. // as published by the Free Software Foundation.
  12. //
  13. // This program is distributed in the hope that it will be useful,
  14. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. // GNU Affero General Public License for more details.
  17. //
  18. // You should have received a copy of the GNU Affero General Public License
  19. // along with this program. If not, see <https://www.gnu.org/licenses/>.
  20. import UIKit
  21. import ThreemaFramework
  22. import QuartzCore
  23. import Contacts
  24. import ContactsUI
  25. @objc protocol ContactDetailsViewControllerDelegate: class {
  26. @objc func present(contactDetailsViewController: ContactDetailsViewController, onCompletion: @escaping ((_ contactsDetailsViewController: ContactDetailsViewController) -> Void))
  27. }
  28. class ContactDetailsViewController: ThemedTableViewController {
  29. @IBOutlet weak var headerView: UIView!
  30. @IBOutlet weak var disclosureButton: UIButton!
  31. @IBOutlet weak var imageView: UIImageView!
  32. @IBOutlet weak var nameLabel: UILabel!
  33. @IBOutlet weak var companyNameLabel: UILabel!
  34. @IBOutlet weak var threemaTypeIcon: UIButton!
  35. @IBOutlet weak var scanQrCodeBarButtonItem: UIBarButtonItem!
  36. @objc var contact: Contact?
  37. @objc var hideActionButtons: Bool = false
  38. @objc weak var delegate : ContactDetailsViewControllerDelegate?
  39. private var didHideTabBar: Bool = false
  40. private var callNumbers: [String]?
  41. private var cnAddressBook: CNContactStore = CNContactStore()
  42. private var cnContact: CNContact?
  43. private var cnContactViewShowing: Bool = false
  44. private var canExportConversation: Bool = false
  45. private var conversation: Conversation?
  46. private var showcase: MaterialShowcase?
  47. private var kvoContact: NSKeyValueObservation?
  48. private let THREEMA_ID_SHARE_LINK = "https://threema.id/"
  49. override internal var shouldAutorotate : Bool {
  50. return true
  51. }
  52. override internal var supportedInterfaceOrientations: UIInterfaceOrientationMask {
  53. if UIDevice.current.userInterfaceIdiom == .pad {
  54. return UIInterfaceOrientationMask.all
  55. }
  56. return UIInterfaceOrientationMask.allButUpsideDown
  57. }
  58. override internal var previewActionItems: [UIPreviewActionItem] {
  59. let sendMessageAction = UIPreviewAction.init(title: BundleUtil.localizedString(forKey: "send_message"), style: .default) { (action, previewController) in
  60. self.sendMessageAction()
  61. }
  62. let scanQrCodeAction = UIPreviewAction.init(title: BundleUtil.localizedString(forKey: "scan_qr"), style: .default) { (action, previewController) in
  63. // we need to present contact details first and present qr scanner on top of that
  64. self.delegate?.present(contactDetailsViewController: self, onCompletion: { (contactsDetailsViewController) in
  65. contactsDetailsViewController.scanIdentityAction()
  66. })
  67. }
  68. return [sendMessageAction, scanQrCodeAction]
  69. }
  70. override func viewDidLoad() {
  71. super.viewDidLoad()
  72. if ScanIdentityController.canScan() == false {
  73. navigationItem.rightBarButtonItem = nil
  74. }
  75. navigationController?.interactivePopGestureRecognizer?.isEnabled = true
  76. navigationController?.interactivePopGestureRecognizer?.delegate = nil
  77. NotificationCenter.default.addObserver(forName: Notification.Name(kNotificationColorThemeChanged), object: nil, queue: nil) { (notification) in
  78. self.setupColors()
  79. }
  80. NotificationCenter.default.addObserver(forName: Notification.Name(kNotificationShowProfilePictureChanged), object: nil, queue: nil) { (notification) in
  81. self.updateView()
  82. }
  83. let disclosureTapRecognizer = UITapGestureRecognizer.init(target: self, action: #selector(tappedHeaderView))
  84. disclosureButton.addGestureRecognizer(disclosureTapRecognizer)
  85. disclosureButton.accessibilityLabel = BundleUtil.localizedString(forKey: "edit_contact")
  86. let tapRecognizer = UITapGestureRecognizer.init(target: self, action: #selector(tappedImage))
  87. imageView.addGestureRecognizer(tapRecognizer)
  88. threemaTypeIcon.setTitle("", for: .normal)
  89. threemaTypeIcon.setBackgroundImage(Utils.threemaTypeIcon(), for: .normal)
  90. setupColors()
  91. }
  92. override func viewWillAppear(_ animated: Bool) {
  93. super.viewWillAppear(animated)
  94. if cnContactViewShowing {
  95. cnContactViewShowing = false
  96. ContactStore.shared()?.update(contact)
  97. let statusNavigationBar = navigationController?.navigationBar as! StatusNavigationBar
  98. statusNavigationBar.showOrHideStatusView()
  99. Colors.update(navigationController?.navigationBar)
  100. }
  101. view.alpha = 1.0
  102. updateView()
  103. }
  104. override func viewDidAppear(_ animated: Bool) {
  105. super.viewDidAppear(animated)
  106. if navigationController != nil {
  107. if navigationController!.isNavigationBarHidden {
  108. navigationController?.isNavigationBarHidden = false
  109. }
  110. }
  111. if UserSettings.shared().workInfoShown == false && !Utils.hideThreemaTypeIcon(for: contact) {
  112. showWorkInfo()
  113. }
  114. if #available(iOS 13.0, *) {
  115. kvoContact = contact!.observe(\.verificationLevel, options: .new) { (changedContact, change) in
  116. DispatchQueue.main.async {
  117. self.updateView()
  118. }
  119. }
  120. }
  121. }
  122. override func viewWillDisappear(_ animated: Bool) {
  123. super.viewWillDisappear(animated)
  124. if #available(iOS 13.0, *) {
  125. kvoContact?.invalidate()
  126. }
  127. }
  128. override func viewWillLayoutSubviews() {
  129. super.viewWillLayoutSubviews()
  130. let fontDescriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: .title3)
  131. nameLabel.font = UIFont.boldSystemFont(ofSize: fontDescriptor.pointSize)
  132. }
  133. override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
  134. if segue.identifier == "EditName" {
  135. let editVC = segue.destination as? EditContactViewController
  136. editVC?.contact = contact
  137. }
  138. else if segue.identifier == "ShowPushSetting" {
  139. let notificationSettingViewController = segue.destination as? NotificationSettingViewController
  140. notificationSettingViewController?.identity = contact?.identity
  141. notificationSettingViewController?.isGroup = false
  142. notificationSettingViewController?.conversation = conversation
  143. }
  144. }
  145. }
  146. extension ContactDetailsViewController {
  147. // MARK: public functions
  148. @objc public func sendMessageAction() {
  149. if let selectedRow = self.tableView!.indexPathForSelectedRow {
  150. self.tableView.deselectRow(at: selectedRow, animated: true)
  151. }
  152. let info: [AnyHashable: Any] = [kKeyContact : contact!, kKeyForceCompose: NSNumber.init(value: true)]
  153. NotificationCenter.default.post(name: NSNotification.Name(rawValue: kNotificationShowConversation), object: nil, userInfo: info)
  154. }
  155. @objc public func scanIdentityAction() {
  156. let scanController = ScanIdentityController.init()
  157. scanController.containingViewController = self
  158. scanController.expectedIdentity = contact?.identity
  159. scanController.popupScanResults = false
  160. scanController.startScan()
  161. }
  162. @objc public func startProfilePictureAction() {
  163. let sender = ContactPhotoSender.init()
  164. sender.startWithImage(toMember: contact, onCompletion: {
  165. UIAlertTemplate .showAlert(owner: self, title: BundleUtil.localizedString(forKey: "my_profilepicture"), message: BundleUtil.localizedString(forKey: "contact_send_profilepicture_success"))
  166. }) { (error) in
  167. UIAlertTemplate .showAlert(owner: self, title: BundleUtil.localizedString(forKey: "my_profilepicture"), message: BundleUtil.localizedString(forKey: "contact_send_profilepicture_error"))
  168. }
  169. if let selectedRow = self.tableView!.indexPathForSelectedRow {
  170. self.tableView.deselectRow(at: selectedRow, animated: true)
  171. }
  172. }
  173. @objc public func startThreemaCallAction(_ startWithVideo: Bool = false) {
  174. if VoIPCallStateManager.shared.currentCallState() == .idle {
  175. var contactSet = Set<Contact>()
  176. contactSet.insert(contact!)
  177. FeatureMask.check(Int(FEATURE_MASK_VOIP), forContacts: contactSet) { (unsupportedContacts) in
  178. if let selectedRow = self.tableView!.indexPathForSelectedRow {
  179. self.tableView.deselectRow(at: selectedRow, animated: true)
  180. }
  181. if unsupportedContacts == nil {
  182. UIAlertTemplate.showAlert(owner: self, title: BundleUtil.localizedString(forKey: "call_voip_not_supported_title"), message: BundleUtil.localizedString(forKey: "call_voip_not_supported_text"))
  183. return
  184. }
  185. if unsupportedContacts!.count == 0 {
  186. self.startVoipCall(startWithVideo)
  187. } else {
  188. UIAlertTemplate.showAlert(owner: self, title: BundleUtil.localizedString(forKey: "call_voip_not_supported_title"), message: BundleUtil.localizedString(forKey: "call_voip_not_supported_text"))
  189. }
  190. }
  191. } else {
  192. if let selectedRow = self.tableView!.indexPathForSelectedRow {
  193. self.tableView.deselectRow(at: selectedRow, animated: true)
  194. }
  195. }
  196. }
  197. }
  198. extension ContactDetailsViewController {
  199. // MARK: private functions
  200. private func setupColors() {
  201. nameLabel.textColor = Colors.fontNormal()
  202. nameLabel.shadowColor = nil
  203. companyNameLabel.textColor = Colors.fontNormal()
  204. companyNameLabel.shadowColor = nil
  205. let disclosureImage: UIImage
  206. if #available(iOS 13.0, *) {
  207. disclosureImage = disclosureButton.imageView!.image!.withTintColor(Colors.main())
  208. } else {
  209. disclosureImage = disclosureButton.imageView!.image!.withTint(Colors.main())
  210. }
  211. disclosureButton.setImage(disclosureImage, for: .normal)
  212. if #available(iOS 11.0, *) {
  213. imageView.accessibilityIgnoresInvertColors = true
  214. threemaTypeIcon.accessibilityIgnoresInvertColors = true
  215. }
  216. }
  217. private func updateView() {
  218. if scanQrCodeBarButtonItem != nil {
  219. scanQrCodeBarButtonItem.accessibilityLabel = BundleUtil.localizedString(forKey: "scan_identity")
  220. }
  221. navigationItem.title = contact?.displayName
  222. nameLabel.text = contact?.displayName
  223. headerView.accessibilityLabel = contact?.displayName
  224. imageView.image = AvatarMaker.shared()?.avatar(for: contact, size: imageView.frame.size.width, masked: false)
  225. imageView.contentMode = .scaleAspectFill
  226. imageView.layer.masksToBounds = true
  227. imageView.layer.cornerRadius = imageView.bounds.size.width / 2
  228. threemaTypeIcon.isHidden = Utils.hideThreemaTypeIcon(for: contact)
  229. companyNameLabel.text = ""
  230. cnContact = nil
  231. if contact?.cnContactId != nil {
  232. updateViewWithCNContact()
  233. }
  234. companyNameLabel.isHidden = companyNameLabel.text?.count == 0
  235. let headerHeight: CGFloat = companyNameLabel.text?.count == 0 ? 275.0 : 300.0
  236. headerView.frame = CGRect.init(x: headerView.frame.origin.x, y: headerView.frame.origin.y, width: headerView.frame.size.width, height: headerHeight)
  237. isExportConversationEnabled()
  238. if didHideTabBar {
  239. tabBarController?.tabBar.isHidden = false
  240. didHideTabBar = false
  241. }
  242. tableView.reloadData()
  243. }
  244. private func updateViewWithCNContact() {
  245. cnAddressBook.requestAccess(for: .contacts) { (granted, error) in
  246. if granted {
  247. let predicate: NSPredicate = CNContact.predicateForContacts(withIdentifiers: [(self.contact!.cnContactId)])
  248. let cnContactKeys = [CNContactFamilyNameKey, CNContactGivenNameKey, CNContactMiddleNameKey, CNContactOrganizationNameKey, CNContactPhoneNumbersKey, CNContactEmailAddressesKey, CNContactImageDataKey, CNContactImageDataAvailableKey, CNContactThumbnailImageDataKey, CNContactFormatter.descriptorForRequiredKeys(for: .fullName), CNContactViewController.descriptorForRequiredKeys()] as [Any]
  249. do {
  250. let contacts = try self.cnAddressBook.unifiedContacts(matching: predicate, keysToFetch: cnContactKeys as! [CNKeyDescriptor])
  251. if contacts.count > 0 {
  252. self.cnContact = contacts.first
  253. self.phoneCallNumbers()
  254. DispatchQueue.main.async {
  255. self.companyNameLabel.text = self.cnContact?.organizationName
  256. self.tableView.reloadData()
  257. }
  258. }
  259. }
  260. catch let err{
  261. DDLogNotice("Can't get CNContact form addressbook \(err.localizedDescription)")
  262. }
  263. }
  264. }
  265. }
  266. private func phoneCallNumbers() {
  267. if cnContact != nil {
  268. callNumbers = nil
  269. callNumbers = [String]()
  270. for phone: CNLabeledValue in cnContact!.phoneNumbers {
  271. let number = phone.value.stringValue
  272. if callNumbers!.contains(number) {
  273. continue
  274. }
  275. callNumbers?.append(number)
  276. }
  277. }
  278. }
  279. private func isExportConversationEnabled() {
  280. let mdmSetup = MDMSetup.init(setup: false)
  281. let entityManager = EntityManager.init()
  282. conversation = entityManager.entityFetcher.conversation(for: contact)
  283. canExportConversation = conversation != nil && !mdmSetup!.disableExport()
  284. }
  285. @objc private func tappedImage() {
  286. if contact != nil {
  287. var image: UIImage?
  288. if (contact!.contactImage != nil && UserSettings.shared().showProfilePictures) {
  289. image = UIImage.init(data: contact!.contactImage.data)
  290. }
  291. else if contact!.imageData != nil {
  292. image = UIImage.init(data: contact!.imageData)
  293. }
  294. if image != nil {
  295. guard let imageController = FullscreenImageViewController.init(for: image) else { return }
  296. if UIDevice.current.userInterfaceIdiom == .pad {
  297. let nav = ModalNavigationController.init(rootViewController: imageController)
  298. nav.showDoneButton = true
  299. nav.showFullScreenOnIPad = true
  300. present(nav, animated: true, completion: nil)
  301. } else {
  302. navigationController?.pushViewController(imageController, animated: true)
  303. }
  304. } else {
  305. tappedHeaderView()
  306. }
  307. }
  308. }
  309. @objc private func tappedHeaderView() {
  310. if contact != nil {
  311. if cnContact != nil {
  312. let personVC = CNContactViewController.init(for: cnContact!)
  313. personVC.allowsActions = true
  314. personVC.allowsEditing = true
  315. cnContactViewShowing = true
  316. if tabBarController?.tabBar.isHidden == false {
  317. didHideTabBar = true
  318. tabBarController?.tabBar.isHidden = true
  319. }
  320. let statusNavigationBar = navigationController?.navigationBar as! StatusNavigationBar
  321. statusNavigationBar.hideStatusView()
  322. navigationController?.navigationBar.barStyle = .default
  323. navigationController?.pushViewController(personVC, animated: true)
  324. } else {
  325. showEditContactVC()
  326. }
  327. }
  328. }
  329. private func showEditContactVC() {
  330. let editVC = storyboard!.instantiateViewController(withIdentifier: "EditContactViewController") as! EditContactViewController
  331. editVC.contact = contact
  332. navigationController?.pushViewController(editVC, animated: true)
  333. }
  334. private func conversationAction(sender: Any?) {
  335. let title = String(format: BundleUtil.localizedString(forKey: "include_media_title"), kExportConversationMediaSizeLimit)
  336. let actionSheet = UIAlertController(title: title, message: nil, preferredStyle: .actionSheet)
  337. actionSheet.addAction(UIAlertAction(title: BundleUtil.localizedString(forKey: "include_media"), style: .default, handler: { (action) in
  338. let em = EntityManager()
  339. let exporter = ConversationExporter(viewController: self, contact: self.contact!, entityManager: em, withMedia: true)
  340. exporter.exportConversation()
  341. }))
  342. actionSheet.addAction(UIAlertAction(title: BundleUtil.localizedString(forKey: "without_media"), style: .default, handler: { (action) in
  343. let em = EntityManager()
  344. let exporter = ConversationExporter(viewController: self, contact: self.contact!, entityManager: em, withMedia: false)
  345. exporter.exportConversation()
  346. }))
  347. actionSheet.addAction(UIAlertAction(title: BundleUtil.localizedString(forKey: "cancel"), style: .cancel, handler: { (action) in
  348. self.tableView.deselectRow(at: IndexPath.init(row: 1, section: 1), animated: true)
  349. }))
  350. if sender is UIView {
  351. let senderView = sender as! UIView
  352. actionSheet.popoverPresentationController?.sourceRect = senderView.frame
  353. actionSheet.popoverPresentationController?.sourceView = view
  354. }
  355. AppDelegate.shared()?.currentTopViewController()?.present(actionSheet, animated: true, completion: nil)
  356. if let selectedRow = self.tableView!.indexPathForSelectedRow {
  357. self.tableView.deselectRow(at: selectedRow, animated: true)
  358. }
  359. }
  360. private func startVoipCall(_ startWithVideo: Bool = false) {
  361. if let selectedRow = self.tableView!.indexPathForSelectedRow {
  362. self.tableView.deselectRow(at: selectedRow, animated: true)
  363. }
  364. if ServerConnector.shared().connectionState == ConnectionStateLoggedIn {
  365. let action = VoIPCallUserAction.init(action: startWithVideo ? .callWithVideo : .call, contact:contact!, callId: nil, completion: nil)
  366. VoIPCallStateManager.shared.processUserAction(action)
  367. } else {
  368. let title = BundleUtil.localizedString(forKey: "cannot_connect_title")
  369. let message = BundleUtil.localizedString(forKey: "cannot_connect_message")
  370. UIAlertTemplate.showAlert(owner: self, title: title, message: message) { (action) in
  371. self.extensionContext?.completeRequest(returningItems: [Any](), completionHandler: nil)
  372. }
  373. }
  374. }
  375. private func makeTelUrlForPhone(_ phoneNumber: String) -> URL {
  376. let urlString = "tel:\(phoneNumber.addingPercentEncoding(withAllowedCharacters: CharacterSet.urlHostAllowed) ?? "")"
  377. return URL.init(string: urlString)!
  378. }
  379. private func linkNewContact(view: UIView) {
  380. if cnContact != nil {
  381. let actionSheet = UIAlertController.init(title: nil, message: nil, preferredStyle: .actionSheet)
  382. actionSheet.addAction(UIAlertAction.init(title: BundleUtil.localizedString(forKey: "unlink_contact"), style: .destructive, handler: { (action) in
  383. ContactStore.shared()?.unlinkContact(self.contact)
  384. self.updateView()
  385. }))
  386. actionSheet.addAction(UIAlertAction.init(title: BundleUtil.localizedString(forKey: "choose_new_contact"), style: .default, handler: { (action) in
  387. self.linkNewContactCheckAuthorization()
  388. }))
  389. actionSheet.addAction(UIAlertAction.init(title: BundleUtil.localizedString(forKey: "cancel"), style: .cancel, handler: { (action) in
  390. if let selectedRow = self.tableView!.indexPathForSelectedRow {
  391. self.tableView.deselectRow(at: selectedRow, animated: true)
  392. }
  393. }))
  394. actionSheet.popoverPresentationController?.sourceRect = view.frame
  395. actionSheet.popoverPresentationController?.sourceView = view
  396. present(actionSheet, animated: true, completion: nil)
  397. } else {
  398. linkNewContactCheckAuthorization()
  399. }
  400. }
  401. private func linkNewContactCheckAuthorization() {
  402. if CNContactStore.authorizationStatus(for: .contacts) != .authorized {
  403. cnAddressBook.requestAccess(for: .contacts) { (granted, error) in
  404. if granted {
  405. DispatchQueue.main.async {
  406. self.linkNewContactPick()
  407. }
  408. } else {
  409. DispatchQueue.main.async {
  410. let accessAlert = UIAlertController.init(title: BundleUtil.localizedString(forKey: "no_contacts_permission_title"), message: BundleUtil.localizedString(forKey: "no_contacts_permission_message"), preferredStyle: .alert)
  411. if self.contact!.cnContactId != nil {
  412. accessAlert.addAction(UIAlertAction.init(title: BundleUtil.localizedString(forKey: "unlink_contact"), style: .default, handler: { (action) in
  413. ContactStore.shared()?.unlinkContact(self.contact!)
  414. self.updateView()
  415. }))
  416. }
  417. accessAlert.addAction(UIAlertAction.init(title: BundleUtil.localizedString(forKey: "ok"), style: .default, handler: nil))
  418. self.present(accessAlert, animated: true, completion: nil)
  419. if let selectedRow = self.tableView!.indexPathForSelectedRow {
  420. self.tableView.deselectRow(at: selectedRow, animated: true)
  421. }
  422. }
  423. }
  424. }
  425. } else {
  426. linkNewContactPick()
  427. }
  428. }
  429. private func linkNewContactPick() {
  430. let picker = CNContactPickerViewController.init()
  431. picker.delegate = self
  432. picker.modalPresentationStyle = .formSheet
  433. present(picker, animated: true, completion: nil)
  434. }
  435. private func shouldHideCell(_ indexPath: IndexPath) -> Bool{
  436. switch indexPath.section {
  437. case 0:
  438. switch indexPath.row {
  439. case 2:
  440. if contact?.publicNickname == nil {
  441. return true
  442. } else {
  443. if contact?.publicNickname.count == 0 || contact?.publicNickname == contact?.identity {
  444. return true
  445. }
  446. }
  447. break
  448. case 3:
  449. if contact!.isGatewayId() {
  450. return true
  451. }
  452. break
  453. default:
  454. break
  455. }
  456. break
  457. case 1:
  458. switch indexPath.row {
  459. case 1:
  460. if !UserSettings.shared().enableThreemaCall || is64Bit != 1 {
  461. return true
  462. }
  463. break
  464. case 2:
  465. if canExportConversation == false {
  466. return true
  467. }
  468. break
  469. case 3:
  470. if ScanIdentityController.canScan() == false {
  471. return true
  472. }
  473. break
  474. case 4:
  475. if contact!.isGatewayId() || contact!.isEchoEcho() || UserSettings.shared().sendProfilePicture == SendProfilePictureNone || (UserSettings.shared().sendProfilePicture == SendProfilePictureContacts && !UserSettings.shared().profilePictureContactList.contains(where: {($0 as! String) == contact!.identity})) {
  476. return true
  477. }
  478. break
  479. default:
  480. break
  481. }
  482. break
  483. default:
  484. break
  485. }
  486. return false
  487. }
  488. private func showWorkInfo(_ autoDismiss: Bool = true) {
  489. threemaTypeIcon.isHighlighted = false
  490. threemaTypeIcon.isSelected = false
  491. if showcase == nil {
  492. showcase = MaterialShowcase()
  493. showcase!.setTargetView(button: threemaTypeIcon)
  494. if LicenseStore.requiresLicenseKey() == false {
  495. showcase!.primaryText = BundleUtil.localizedString(forKey: "contact_threema_work_title")
  496. showcase!.secondaryText = BundleUtil.localizedString(forKey: "contact_threema_work_info")
  497. showcase!.backgroundPromptColor = Colors.workBlue()
  498. } else {
  499. showcase!.primaryText = BundleUtil.localizedString(forKey: "contact_threema_title")
  500. showcase!.secondaryText = BundleUtil.localizedString(forKey: "contact_threema_info")
  501. showcase!.backgroundPromptColor = Colors.green()
  502. }
  503. showcase!.backgroundPromptColorAlpha = 0.93
  504. showcase!.primaryTextSize = 24.0
  505. showcase!.secondaryTextSize = 20.0
  506. showcase!.primaryTextColor = Colors.white()
  507. showcase!.secondaryTextColor = Colors.white()
  508. showcase!.delegate = self
  509. }
  510. showcase!.show(completion: nil)
  511. if autoDismiss == true {
  512. DispatchQueue.main.asyncAfter(deadline: .now() + 6, execute: {
  513. if self.showcase != nil {
  514. self.showcase!.completeShowcase()
  515. }
  516. })
  517. }
  518. }
  519. // MARK: IBAction
  520. @IBAction func shareButtonTapped(sender: UIButton) {
  521. let contactShareLink = String.init(format: "%@%@", THREEMA_ID_SHARE_LINK, contact!.identity)
  522. let contactShareText = "\(contact!.displayName!): \(contactShareLink)"
  523. let activityViewController = UIActivityViewController.init(activityItems: [contactShareText], applicationActivities: nil)
  524. if UIDevice.current.userInterfaceIdiom == .pad {
  525. activityViewController.popoverPresentationController?.sourceRect = sender.frame
  526. activityViewController.popoverPresentationController?.sourceView = view
  527. }
  528. present(activityViewController, animated: true, completion: nil)
  529. }
  530. @IBAction func scanQrCodeAction(sender: UIButton) {
  531. scanIdentityAction()
  532. }
  533. @IBAction func workInfoButtonTapped(sender: UIButton) {
  534. showWorkInfo(false)
  535. }
  536. }
  537. extension ContactDetailsViewController {
  538. // MARK: - Table view data source
  539. override func numberOfSections(in tableView: UITableView) -> Int {
  540. if !(contact!.isGatewayId()) && !contact!.isEchoEcho() && UserSettings.shared()?.sendProfilePicture == SendProfilePictureContacts {
  541. return 4
  542. }
  543. return 3
  544. }
  545. override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
  546. switch section {
  547. case 0:
  548. return 6
  549. case 1:
  550. return 5
  551. case 2:
  552. if !(contact!.isGatewayId()) && !contact!.isEchoEcho() && UserSettings.shared()?.sendProfilePicture == SendProfilePictureContacts {
  553. return 1
  554. } else {
  555. return 2
  556. }
  557. case 3:
  558. return 2
  559. default:
  560. return 0
  561. }
  562. }
  563. override func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? {
  564. if section == 2 && !contact!.isGatewayId() && !contact!.isEchoEcho() && UserSettings.shared().sendProfilePicture == SendProfilePictureContacts {
  565. if UserSettings.shared().profilePictureContactList.contains(where: {($0 as! String) == contact!.identity}) {
  566. return BundleUtil.localizedString(forKey: "contact_added_to_profilepicture_list")
  567. } else {
  568. return BundleUtil.localizedString(forKey: "contact_removed_from_profilepicture_list")
  569. }
  570. }
  571. return nil
  572. }
  573. override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
  574. return shouldHideCell(indexPath) == true ? 0 : UITableView.automaticDimension
  575. }
  576. override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
  577. super.tableView(tableView, willDisplay: cell, forRowAt: indexPath)
  578. cell.isHidden = shouldHideCell(indexPath)
  579. }
  580. override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
  581. switch indexPath.section {
  582. case 0:
  583. switch indexPath.row {
  584. case 0:
  585. let identityCell = tableView.dequeueReusableCell(withIdentifier: "IdentityCell")
  586. identityCell!.detailTextLabel?.text = contact?.identity
  587. identityCell!.detailTextLabel!.isAccessibilityElement = false
  588. let shareButton = identityCell?.accessoryView as! UIButton
  589. let shareImage: UIImage
  590. if #available(iOS 13.0, *) {
  591. shareImage = shareButton.imageView!.image!.withTintColor(Colors.main())
  592. } else {
  593. shareImage = shareButton.imageView!.image!.withTint(Colors.main())
  594. }
  595. shareButton.setImage(shareImage, for: .normal)
  596. return identityCell!
  597. case 1:
  598. let vlc = tableView.dequeueReusableCell(withIdentifier: "VerificationLevelCell") as! VerificationLevelCell
  599. vlc.contact = contact
  600. vlc.accessibilityTraits = .button
  601. return vlc
  602. case 2:
  603. let publicNicknameCell = tableView.dequeueReusableCell(withIdentifier: "PublicNicknameCell")
  604. publicNicknameCell!.detailTextLabel?.text = contact?.publicNickname
  605. return publicNicknameCell!
  606. case 3:
  607. let lcc = tableView.dequeueReusableCell(withIdentifier: "LinkedContactCell") as! LinkedContactCell
  608. lcc.accessibilityTraits = .button
  609. if cnContact != nil {
  610. lcc.displayNameLabel.text = CNContactFormatter.string(from: cnContact!, style: .fullName)
  611. if lcc.displayNameLabel.text == nil || lcc.displayNameLabel.text?.count == 0 {
  612. if cnContact!.emailAddresses.count > 0 {
  613. let first:CNLabeledValue = cnContact!.emailAddresses.first!
  614. lcc.displayNameLabel.text = first.value as String
  615. }
  616. }
  617. } else {
  618. lcc.displayNameLabel.text = BundleUtil.localizedString(forKey: "(none)")
  619. }
  620. return lcc
  621. case 4:
  622. let groupMembershipCell = tableView.dequeueReusableCell(withIdentifier: "GroupMembershipCell")
  623. groupMembershipCell?.textLabel?.text = BundleUtil.localizedString(forKey: "member_in_groups")
  624. groupMembershipCell?.detailTextLabel?.text = String.init(format: "%lu", contact?.groupConversations.count ?? 0)
  625. groupMembershipCell?.accessibilityTraits = .button
  626. return groupMembershipCell!
  627. case 5:
  628. let kfc = tableView.dequeueReusableCell(withIdentifier: "KeyFingerprintCell") as! KeyFingerprintCell
  629. kfc.fingerprintValueLabel.text = CryptoUtils.fingerprint(forPublicKey: contact?.publicKey)
  630. return kfc
  631. default:
  632. break
  633. }
  634. case 1:
  635. var cellIdentifier = ""
  636. switch indexPath.row {
  637. case 0:
  638. cellIdentifier = "SendMessageCell"
  639. break
  640. case 1:
  641. cellIdentifier = "ThreemaCallCell"
  642. break
  643. case 2:
  644. cellIdentifier = "ExportConversationCell"
  645. break
  646. case 3:
  647. cellIdentifier = "ScanIDCell"
  648. break
  649. case 4:
  650. cellIdentifier = "SendProfilePictureCell"
  651. break
  652. default:
  653. cellIdentifier = "SendMessageCell"
  654. }
  655. let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier)
  656. cell?.accessibilityTraits = .button
  657. return cell!
  658. case 2:
  659. if !(contact!.isGatewayId()) && !contact!.isEchoEcho() && UserSettings.shared()?.sendProfilePicture == SendProfilePictureContacts {
  660. let profilePictureRecipientCell = tableView.dequeueReusableCell(withIdentifier: "ProfilePictureRecipientCell") as! ProfilePictureRecipientCell
  661. profilePictureRecipientCell.identity = contact?.identity
  662. profilePictureRecipientCell.delegate = self
  663. return profilePictureRecipientCell
  664. } else {
  665. switch indexPath.row {
  666. case 0:
  667. let pushSettingCell = tableView.dequeueReusableCell(withIdentifier: "PushSettingCell")
  668. pushSettingCell?.textLabel?.text = BundleUtil.localizedString(forKey: "pushSetting_title")
  669. return pushSettingCell!
  670. case 1:
  671. let bcc = tableView.dequeueReusableCell(withIdentifier: "BlockCell") as! BlockContactCell
  672. bcc.identity = contact?.identity
  673. return bcc
  674. default:
  675. break
  676. }
  677. }
  678. break
  679. case 3:
  680. switch indexPath.row {
  681. case 0:
  682. let pushSettingCell = tableView.dequeueReusableCell(withIdentifier: "PushSettingCell")
  683. pushSettingCell?.textLabel?.text = BundleUtil.localizedString(forKey: "pushSetting_title")
  684. return pushSettingCell!
  685. case 1:
  686. let bcc = tableView.dequeueReusableCell(withIdentifier: "BlockCell") as! BlockContactCell
  687. bcc.identity = contact?.identity
  688. return bcc
  689. default:
  690. break
  691. }
  692. break
  693. default:
  694. break
  695. }
  696. return UITableViewCell()
  697. }
  698. override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
  699. let cell = tableView.cellForRow(at: indexPath)
  700. switch indexPath.section {
  701. case 0:
  702. if indexPath.row == 3 {
  703. linkNewContact(view: cell!)
  704. }
  705. else if indexPath.row == 4 {
  706. let vc = storyboard!.instantiateViewController(withIdentifier: "contactGroupMembershipViewController") as! ContactGroupMembershipViewController
  707. vc.groupContact = contact
  708. navigationController?.pushViewController(vc, animated: true)
  709. }
  710. break
  711. case 1:
  712. if cell?.reuseIdentifier == "SendMessageCell" {
  713. sendMessageAction()
  714. }
  715. else if cell?.reuseIdentifier == "ThreemaCallCell" {
  716. startThreemaCallAction()
  717. }
  718. else if cell?.reuseIdentifier == "ExportConversationCell" {
  719. tableView.deselectRow(at: indexPath, animated: true)
  720. conversationAction(sender: cell)
  721. }
  722. else if cell?.reuseIdentifier == "ScanIDCell" {
  723. scanIdentityAction()
  724. }
  725. else if cell?.reuseIdentifier == "SendProfilePictureCell" {
  726. startProfilePictureAction()
  727. }
  728. break
  729. default:
  730. break
  731. }
  732. }
  733. override func tableView(_ tableView: UITableView, accessoryButtonTappedForRowWith indexPath: IndexPath) {
  734. if indexPath.section == 0 && indexPath.row == 1 {
  735. performSegue(withIdentifier: "VerificationSegue", sender: nil)
  736. }
  737. }
  738. }
  739. extension ContactDetailsViewController: ProfilePictureRecipientCellDelegate {
  740. func valueChanged(_ cell: ProfilePictureRecipientCell) {
  741. let indexSet: IndexSet = [1]
  742. tableView.beginUpdates()
  743. tableView.reloadSections(indexSet, with: .automatic)
  744. tableView.endUpdates()
  745. DispatchQueue.main.async {
  746. self.tableView.reloadData()
  747. }
  748. }
  749. }
  750. extension ContactDetailsViewController: CNContactPickerDelegate {
  751. func contactPicker(_ picker: CNContactPickerViewController, didSelect contact: CNContact) {
  752. let statusNavigationBar = navigationController?.navigationBar as! StatusNavigationBar
  753. statusNavigationBar.showOrHideStatusView()
  754. ContactStore.shared()?.linkContact(self.contact, toCnContactId: contact.identifier)
  755. dismiss(animated: true, completion: nil)
  756. updateView()
  757. }
  758. func contactPicker(_ picker: CNContactPickerViewController, didSelect contactProperty: CNContactProperty) {
  759. dismiss(animated: true, completion: nil)
  760. }
  761. func contactPickerDidCancel(_ picker: CNContactPickerViewController) {
  762. if let selectedRow = self.tableView!.indexPathForSelectedRow {
  763. self.tableView.deselectRow(at: selectedRow, animated: true)
  764. }
  765. let statusNavigationBar = navigationController?.navigationBar as! StatusNavigationBar
  766. statusNavigationBar.showOrHideStatusView()
  767. dismiss(animated: true, completion: nil)
  768. }
  769. }
  770. extension ContactDetailsViewController: MaterialShowcaseDelegate {
  771. func showCaseWillDismiss(showcase: MaterialShowcase, didTapTarget: Bool) {
  772. UserSettings.shared()?.workInfoShown = true
  773. }
  774. func showCaseDidDismiss(showcase: MaterialShowcase, didTapTarget: Bool) {
  775. }
  776. }