ShareConfirmationViewController.swift 40 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972
  1. //
  2. // SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
  3. // SPDX-License-Identifier: GPL-3.0-or-later
  4. //
  5. import Foundation
  6. import NextcloudKit
  7. import QuickLook
  8. import SwiftyAttributes
  9. import TOCropViewController
  10. import AVFoundation
  11. @objc public protocol ShareConfirmationViewControllerDelegate {
  12. @objc func shareConfirmationViewControllerDidFailed(_ viewController: ShareConfirmationViewController)
  13. @objc func shareConfirmationViewControllerDidFinish(_ viewController: ShareConfirmationViewController)
  14. }
  15. @objcMembers public class ShareConfirmationViewController: InputbarViewController,
  16. NKCommonDelegate,
  17. ShareItemControllerDelegate,
  18. UIImagePickerControllerDelegate,
  19. UIDocumentPickerDelegate,
  20. UINavigationControllerDelegate,
  21. UICollectionViewDelegateFlowLayout,
  22. TOCropViewControllerDelegate,
  23. QLPreviewControllerDataSource,
  24. QLPreviewControllerDelegate {
  25. // MARK: - Public var
  26. public var account: TalkAccount
  27. public var isModal: Bool = false
  28. public var forwardingMessage: Bool = false
  29. public weak var delegate: ShareConfirmationViewControllerDelegate?
  30. public lazy var shareItemController: ShareItemController = {
  31. let controller = ShareItemController()
  32. controller.delegate = self
  33. return controller
  34. }()
  35. // MARK: - Private var
  36. private var serverCapabilities: ServerCapabilities
  37. private var shareType: ShareConfirmationType = .item
  38. private var shareContentView = UIView()
  39. private var shareSilently = false
  40. private var imagePicker: UIImagePickerController?
  41. private var hud: MBProgressHUD?
  42. private var objectShareMessage: NCChatMessage?
  43. private var uploadGroup = DispatchGroup()
  44. private var uploadFailed = false
  45. private var uploadErrors: [String] = []
  46. private var uploadSuccess: [ShareItem] = []
  47. private enum ShareConfirmationType {
  48. case text
  49. case item
  50. case objectShare
  51. }
  52. // MARK: - UI Controls
  53. private lazy var sendButton: UIBarButtonItem = {
  54. let sendButton = UIBarButtonItem(title: NSLocalizedString("Send", comment: ""), style: .done, target: self, action: #selector(sendButtonPressed))
  55. sendButton.accessibilityHint = NSLocalizedString("Double tap to share with selected conversations", comment: "")
  56. return sendButton
  57. }()
  58. private lazy var sharingIndicatorView: UIActivityIndicatorView = {
  59. let indicator = UIActivityIndicatorView()
  60. indicator.color = NCAppBranding.themeTextColor()
  61. return indicator
  62. }()
  63. private lazy var toLabel: UILabel = {
  64. var label = UILabel()
  65. label.translatesAutoresizingMaskIntoConstraints = false
  66. return label
  67. }()
  68. private lazy var toLabelView: UIView = {
  69. let view = UIView()
  70. view.translatesAutoresizingMaskIntoConstraints = false
  71. view.backgroundColor = .secondarySystemBackground
  72. view.addSubview(self.toLabel)
  73. NSLayoutConstraint.activate([
  74. self.toLabel.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor, constant: 20),
  75. self.toLabel.rightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.rightAnchor, constant: -20),
  76. self.toLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
  77. self.toLabel.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor)
  78. ])
  79. return view
  80. }()
  81. private lazy var itemToolbar: UIToolbar = {
  82. let toolbar = UIToolbar(frame: .init(x: 0, y: 0, width: 100, height: 44))
  83. let flexibleSpace = UIBarButtonItem(systemItem: .flexibleSpace)
  84. toolbar.barTintColor = .systemBackground
  85. toolbar.isTranslucent = false
  86. toolbar.setItems([removeItemButton, flexibleSpace, cropItemButton, previewItemButton, addItemButton], animated: false)
  87. toolbar.translatesAutoresizingMaskIntoConstraints = false
  88. return toolbar
  89. }()
  90. private lazy var removeItemButton: UIBarButtonItem = {
  91. let button = UIBarButtonItem(image: .init(systemName: "trash"))
  92. button.width = 56
  93. button.target = self
  94. button.action = #selector(removeItemButtonPressed)
  95. return button
  96. }()
  97. private lazy var cropItemButton: UIBarButtonItem = {
  98. let button = UIBarButtonItem(image: .init(systemName: "crop.rotate"))
  99. button.width = 56
  100. button.target = self
  101. button.action = #selector(cropItemButtonPressed)
  102. return button
  103. }()
  104. private lazy var previewItemButton: UIBarButtonItem = {
  105. let button = UIBarButtonItem(image: .init(systemName: "eye"))
  106. button.width = 56
  107. button.target = self
  108. button.action = #selector(previewItemButtonPressed)
  109. return button
  110. }()
  111. private lazy var addItemButton: UIBarButtonItem = {
  112. let button = UIBarButtonItem(image: .init(systemName: "plus"))
  113. button.width = 56
  114. var items: [UIAction] = []
  115. let cameraAction = UIAction(title: NSLocalizedString("Camera", comment: ""), image: UIImage(systemName: "camera")) { [unowned self] _ in
  116. self.textView.resignFirstResponder()
  117. self.checkAndPresentCamera()
  118. }
  119. let photoLibraryAction = UIAction(title: NSLocalizedString("Photo Library", comment: ""), image: UIImage(systemName: "photo")) { [unowned self] _ in
  120. self.textView.resignFirstResponder()
  121. self.presentPhotoLibrary()
  122. }
  123. let filesAction = UIAction(title: NSLocalizedString("Files", comment: ""), image: UIImage(systemName: "doc")) { [unowned self] _ in
  124. self.textView.resignFirstResponder()
  125. self.presentDocumentPicker()
  126. }
  127. #if !APP_EXTENSION
  128. // Camera access is not available in app extensions
  129. // https://developer.apple.com/library/archive/documentation/General/Conceptual/ExtensibilityPG/ExtensionOverview.html
  130. if UIImagePickerController.isSourceTypeAvailable(.camera) {
  131. items.append(cameraAction)
  132. }
  133. #endif
  134. items.append(photoLibraryAction)
  135. items.append(filesAction)
  136. button.menu = UIMenu(children: items)
  137. return button
  138. }()
  139. private lazy var shareCollectionViewLayout: UICollectionViewFlowLayout = {
  140. // Make sure that we use a layout that invalidates itself when the bounds changed
  141. let layout = BoundsChangedFlowLayout()
  142. layout.scrollDirection = .horizontal
  143. layout.minimumLineSpacing = 0
  144. layout.minimumInteritemSpacing = 0
  145. return layout
  146. }()
  147. private lazy var shareCollectionView: UICollectionView = {
  148. let collectionView = UICollectionView(frame: .init(x: 0, y: 0, width: 10, height: 10), collectionViewLayout: self.shareCollectionViewLayout)
  149. collectionView.translatesAutoresizingMaskIntoConstraints = false
  150. collectionView.delegate = self
  151. collectionView.dataSource = self
  152. collectionView.isPagingEnabled = true
  153. collectionView.showsVerticalScrollIndicator = false
  154. return collectionView
  155. }()
  156. private lazy var shareTextView: UITextView = {
  157. let textView = UITextView()
  158. textView.font = .preferredFont(forTextStyle: .body)
  159. textView.translatesAutoresizingMaskIntoConstraints = false
  160. textView.isHidden = true
  161. return textView
  162. }()
  163. private lazy var pageControl: UIPageControl = {
  164. let pageControl = UIPageControl()
  165. pageControl.translatesAutoresizingMaskIntoConstraints = false
  166. pageControl.currentPageIndicatorTintColor = NCAppBranding.elementColor()
  167. pageControl.pageIndicatorTintColor = NCAppBranding.placeholderColor()
  168. pageControl.hidesForSinglePage = true
  169. pageControl.numberOfPages = 1
  170. pageControl.addTarget(self, action: #selector(pageControlValueChanged), for: .valueChanged)
  171. return pageControl
  172. }()
  173. // MARK: - Init.
  174. public init?(room: NCRoom, account: TalkAccount, serverCapabilities: ServerCapabilities) {
  175. self.account = account
  176. self.serverCapabilities = serverCapabilities
  177. super.init(for: room, withView: self.shareContentView)
  178. self.shareContentView.addSubview(self.toLabelView)
  179. NSLayoutConstraint.activate([
  180. self.toLabelView.leftAnchor.constraint(equalTo: self.shareContentView.safeAreaLayoutGuide.leftAnchor),
  181. self.toLabelView.rightAnchor.constraint(equalTo: self.shareContentView.safeAreaLayoutGuide.rightAnchor),
  182. self.toLabelView.topAnchor.constraint(equalTo: self.shareContentView.safeAreaLayoutGuide.topAnchor),
  183. self.toLabelView.heightAnchor.constraint(equalToConstant: 36)
  184. ])
  185. self.shareContentView.addSubview(self.shareTextView)
  186. NSLayoutConstraint.activate([
  187. self.shareTextView.leftAnchor.constraint(equalTo: self.shareContentView.safeAreaLayoutGuide.leftAnchor, constant: 20),
  188. self.shareTextView.rightAnchor.constraint(equalTo: self.shareContentView.safeAreaLayoutGuide.rightAnchor, constant: -20),
  189. self.shareTextView.topAnchor.constraint(equalTo: self.toLabelView.bottomAnchor, constant: 20),
  190. self.shareTextView.bottomAnchor.constraint(equalTo: self.shareContentView.safeAreaLayoutGuide.bottomAnchor, constant: -20)
  191. ])
  192. self.shareContentView.addSubview(self.itemToolbar)
  193. NSLayoutConstraint.activate([
  194. self.itemToolbar.leftAnchor.constraint(equalTo: self.shareContentView.safeAreaLayoutGuide.leftAnchor),
  195. self.itemToolbar.rightAnchor.constraint(equalTo: self.shareContentView.safeAreaLayoutGuide.rightAnchor),
  196. self.itemToolbar.topAnchor.constraint(equalTo: self.toLabelView.bottomAnchor),
  197. self.itemToolbar.heightAnchor.constraint(equalToConstant: 44)
  198. ])
  199. self.shareContentView.addSubview(self.shareCollectionView)
  200. self.shareContentView.addSubview(self.pageControl)
  201. NSLayoutConstraint.activate([
  202. self.shareCollectionView.leftAnchor.constraint(equalTo: self.shareContentView.safeAreaLayoutGuide.leftAnchor),
  203. self.shareCollectionView.rightAnchor.constraint(equalTo: self.shareContentView.safeAreaLayoutGuide.rightAnchor),
  204. self.shareCollectionView.topAnchor.constraint(equalTo: self.itemToolbar.bottomAnchor, constant: 8),
  205. self.shareCollectionView.bottomAnchor.constraint(equalTo: self.pageControl.topAnchor, constant: -8),
  206. self.pageControl.leftAnchor.constraint(equalTo: self.shareContentView.safeAreaLayoutGuide.leftAnchor),
  207. self.pageControl.rightAnchor.constraint(equalTo: self.shareContentView.safeAreaLayoutGuide.rightAnchor),
  208. self.pageControl.heightAnchor.constraint(equalToConstant: 26),
  209. self.pageControl.bottomAnchor.constraint(equalTo: self.textInputbar.topAnchor)
  210. ])
  211. }
  212. required init?(coder decoder: NSCoder) {
  213. fatalError("init(coder:) has not been implemented")
  214. }
  215. public func shareText(_ sharedText: String) {
  216. self.shareType = .text
  217. DispatchQueue.main.async {
  218. self.setTextInputbarHidden(true, animated: false)
  219. self.shareCollectionView.isHidden = true
  220. self.itemToolbar.isHidden = true
  221. self.shareTextView.isHidden = false
  222. self.shareTextView.text = sharedText
  223. // When an item of type "public.url" or "public.plain-text" is shared,
  224. // we switch to text-sharing after viewWillAppear, so we need to add the sendButton here as well
  225. self.navigationItem.rightBarButtonItem = self.sendButton
  226. self.navigationItem.rightBarButtonItem?.tintColor = NCAppBranding.themeTextColor()
  227. }
  228. }
  229. public func shareObjectShareMessage(_ objectShareMessage: NCChatMessage) {
  230. self.shareType = .objectShare
  231. DispatchQueue.main.async {
  232. self.setTextInputbarHidden(true, animated: false)
  233. self.shareCollectionView.isHidden = true
  234. self.itemToolbar.isHidden = true
  235. self.shareTextView.isHidden = false
  236. self.shareTextView.isUserInteractionEnabled = false
  237. self.shareTextView.text = objectShareMessage.parsedMessage().string
  238. self.objectShareMessage = objectShareMessage
  239. }
  240. }
  241. // MARK: - View lifecycle
  242. public override func viewDidLoad() {
  243. super.viewDidLoad()
  244. // Configure communication lib
  245. let userToken = NCKeyChainController.sharedInstance().token(forAccountId: self.account.accountId)
  246. let userAgent = "Mozilla/5.0 (iOS) Nextcloud-Talk v\(Bundle.main.infoDictionary?["CFBundleShortVersionString"] ?? "Unknown")"
  247. NextcloudKit.shared.setup(account: self.account.accountId,
  248. user: self.account.user,
  249. userId: self.account.userId,
  250. password: userToken,
  251. urlBase: self.account.server,
  252. userAgent: userAgent,
  253. nextcloudVersion: self.serverCapabilities.versionMajor,
  254. delegate: self)
  255. let localizedToString = NSLocalizedString("To:", comment: "TRANSLATORS this is for sending something 'to' a user. E.g. 'To: John Doe'")
  256. let toString = localizedToString.withFont(.boldSystemFont(ofSize: 15)).withTextColor(.tertiaryLabel)
  257. let roomString = self.room.displayName.withFont(.systemFont(ofSize: 15)).withTextColor(.label)
  258. self.toLabel.attributedText = toString + NSAttributedString(string: " ") + roomString
  259. let bundle = Bundle(for: ShareConfirmationCollectionViewCell.self)
  260. self.shareCollectionView.register(UINib(nibName: kShareConfirmationTableCellNibName, bundle: bundle), forCellWithReuseIdentifier: kShareConfirmationCellIdentifier)
  261. self.shareCollectionView.delegate = self
  262. }
  263. public override func viewWillAppear(_ animated: Bool) {
  264. // Add the cancel button in viewWillAppear, so that the caller can change the isModal property after initialization
  265. if self.isModal {
  266. let cancelButton = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(self.cancelButtonPressed))
  267. cancelButton.accessibilityHint = NSLocalizedString("Double tap to dismiss sharing options", comment: "")
  268. self.navigationItem.leftBarButtonItem = cancelButton
  269. self.navigationItem.leftBarButtonItem?.tintColor = NCAppBranding.themeTextColor()
  270. }
  271. var captionAllowed = NCDatabaseManager.sharedInstance().serverHasTalkCapability(kCapabilityMediaCaption, forAccountId: account.accountId)
  272. captionAllowed = captionAllowed && self.shareType == .item
  273. if !captionAllowed {
  274. self.navigationItem.rightBarButtonItem = self.sendButton
  275. self.navigationItem.rightBarButtonItem?.tintColor = NCAppBranding.themeTextColor()
  276. self.setTextInputbarHidden(true, animated: false)
  277. } else {
  278. let silentSendAction = UIAction(title: NSLocalizedString("Send without notification", comment: ""), image: UIImage(systemName: "bell.slash")) { [unowned self] _ in
  279. self.silentSendPressed()
  280. }
  281. self.rightButton.menu = UIMenu(children: [silentSendAction])
  282. }
  283. }
  284. public override func viewDidAppear(_ animated: Bool) {
  285. super.viewDidAppear(animated)
  286. if self.shareType == .text {
  287. // When we are sharing a text, we want to start editing right away
  288. self.shareTextView.becomeFirstResponder()
  289. }
  290. }
  291. public override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
  292. super.viewWillTransition(to: size, with: coordinator)
  293. if self.shareType == .text {
  294. return
  295. }
  296. self.shareCollectionView.isHidden = true
  297. // Invalidate layout to remove warning about item size must be less than UICollectionView
  298. self.shareCollectionView.collectionViewLayout.invalidateLayout()
  299. let currentItem = self.getCurrentShareItem()
  300. coordinator.animate { _ in
  301. // Invalidate the view now so cell size is correctly calculated
  302. // The size of the collection view is correct at this moment
  303. self.shareCollectionView.collectionViewLayout.invalidateLayout()
  304. } completion: { _ in
  305. // Scroll to the element and make collection view appear
  306. if let currentItem {
  307. self.scroll(to: currentItem, animated: false)
  308. }
  309. self.shareCollectionView.isHidden = false
  310. }
  311. }
  312. override func setTitleView() {
  313. // We don't want a titleView in this case
  314. }
  315. public override func canPressRightButton() -> Bool {
  316. // We want to allow sending pictures even when no text is entered
  317. return true
  318. }
  319. // MARK: - Button Actions
  320. func removeItemButtonPressed() {
  321. if let item = self.getCurrentShareItem() {
  322. self.shareItemController.remove(item)
  323. }
  324. }
  325. func cropItemButtonPressed() {
  326. if let item = self.getCurrentShareItem(),
  327. let image = self.shareItemController.getImageFrom(item) {
  328. let cropViewController = TOCropViewController(image: image)
  329. cropViewController.delegate = self
  330. self.present(cropViewController, animated: true)
  331. }
  332. }
  333. func previewItemButtonPressed() {
  334. self.previewCurrentItem()
  335. }
  336. func cancelButtonPressed() {
  337. self.delegate?.shareConfirmationViewControllerDidFinish(self)
  338. }
  339. func sendButtonPressed() {
  340. self.sendCurrent(silently: false)
  341. }
  342. public override func didPressRightButton(_ sender: Any?) {
  343. self.sendCurrent(silently: false)
  344. }
  345. func silentSendPressed() {
  346. self.sendCurrent(silently: true)
  347. }
  348. func sendCurrent(silently: Bool) {
  349. self.shareSilently = silently
  350. if self.shareType == .text {
  351. self.sendSharedText()
  352. } else if self.shareType == .objectShare {
  353. self.sendObjectShare()
  354. } else {
  355. self.uploadAndShareFiles()
  356. }
  357. self.startAnimatingSharingIndicator()
  358. }
  359. // MARK: - Add additional items
  360. func checkAndPresentCamera() {
  361. // https://stackoverflow.com/a/20464727/2512312
  362. let mediaType = AVMediaType.video
  363. let authStatus = AVCaptureDevice.authorizationStatus(for: mediaType)
  364. if authStatus == AVAuthorizationStatus.authorized {
  365. self.presentCamera()
  366. return
  367. } else if authStatus == AVAuthorizationStatus.notDetermined {
  368. AVCaptureDevice.requestAccess(for: mediaType, completionHandler: { (granted: Bool) in
  369. if granted {
  370. self.presentCamera()
  371. }
  372. })
  373. return
  374. }
  375. let alert = UIAlertController(title: NSLocalizedString("Could not access camera", comment: ""),
  376. message: NSLocalizedString("Camera access is not allowed. Check your settings.", comment: ""),
  377. preferredStyle: .alert)
  378. alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .default))
  379. self.present(alert, animated: true)
  380. }
  381. func presentCamera() {
  382. DispatchQueue.main.async {
  383. self.imagePicker = UIImagePickerController()
  384. if let imagePicker = self.imagePicker,
  385. let sourceType = UIImagePickerController.availableMediaTypes(for: imagePicker.sourceType) {
  386. imagePicker.sourceType = .camera
  387. imagePicker.cameraFlashMode = UIImagePickerController.CameraFlashMode(rawValue: NCUserDefaults.preferredCameraFlashMode()) ?? .off
  388. imagePicker.mediaTypes = sourceType
  389. imagePicker.delegate = self
  390. self.present(imagePicker, animated: true)
  391. }
  392. }
  393. }
  394. func presentPhotoLibrary() {
  395. self.imagePicker = UIImagePickerController()
  396. if let imagePicker = self.imagePicker {
  397. imagePicker.sourceType = .photoLibrary
  398. imagePicker.mediaTypes = UIImagePickerController.availableMediaTypes(for: .photoLibrary) ?? []
  399. imagePicker.delegate = self
  400. self.present(imagePicker, animated: true)
  401. }
  402. }
  403. func presentDocumentPicker() {
  404. DispatchQueue.main.async {
  405. let documentPicker = UIDocumentPickerViewController(forOpeningContentTypes: [.item], asCopy: true)
  406. documentPicker.delegate = self
  407. self.present(documentPicker, animated: true)
  408. }
  409. }
  410. // MARK: - Actions
  411. func sendSharedText() {
  412. NCAPIController.sharedInstance().sendChatMessage(self.shareTextView.text, toRoom: self.room.token, displayName: nil, replyTo: -1, referenceId: nil, silently: false, for: self.account) { error in
  413. if let error {
  414. NCUtils.log(String(format: "Failed to share text. Error: %@", error.localizedDescription))
  415. self.delegate?.shareConfirmationViewControllerDidFailed(self)
  416. } else {
  417. NCIntentController.sharedInstance().donateSendMessageIntent(for: self.room)
  418. self.delegate?.shareConfirmationViewControllerDidFinish(self)
  419. }
  420. self.stopAnimatingSharingIndicator()
  421. }
  422. }
  423. func sendObjectShare() {
  424. NCAPIController.sharedInstance().shareRichObject(self.objectShareMessage?.richObjectFromObjectShare, inRoom: self.room.token, for: self.account) { error in
  425. if let error {
  426. NCUtils.log(String(format: "Failed to share rich object. Error: %@", error.localizedDescription))
  427. self.delegate?.shareConfirmationViewControllerDidFailed(self)
  428. } else {
  429. NCIntentController.sharedInstance().donateSendMessageIntent(for: self.room)
  430. self.delegate?.shareConfirmationViewControllerDidFinish(self)
  431. }
  432. self.stopAnimatingSharingIndicator()
  433. }
  434. }
  435. func updateHudProgress() {
  436. guard let hud = self.hud else { return }
  437. DispatchQueue.main.async {
  438. var progress: CGFloat = 0.0
  439. var items = 0
  440. for shareItem in self.shareItemController.shareItems {
  441. progress += shareItem.uploadProgress
  442. items += 1
  443. }
  444. hud.progress = Float(progress / CGFloat(items))
  445. }
  446. }
  447. func uploadAndShareFiles() {
  448. // TODO: This has no effect on ShareExtension
  449. let bgTask = BGTaskHelper.startBackgroundTask(withName: "uploadAndShareFiles")
  450. // Hide keyboard before upload to correctly display the HUD
  451. self.textView.resignFirstResponder()
  452. NCIntentController.sharedInstance().donateSendMessageIntent(for: self.room)
  453. self.hud = MBProgressHUD.showAdded(to: self.view, animated: true)
  454. self.hud?.mode = .annularDeterminate
  455. self.hud?.label.text = String(format: NSLocalizedString("Uploading %ld elements", comment: ""), self.shareItemController.shareItems.count)
  456. if self.shareItemController.shareItems.count == 1 {
  457. self.hud?.label.text = NSLocalizedString("Uploading 1 element", comment: "")
  458. }
  459. self.uploadGroup = DispatchGroup()
  460. self.uploadErrors = []
  461. self.uploadSuccess = []
  462. // Add caption to last shareItem
  463. if let shareItem = self.shareItemController.shareItems.last {
  464. if NCDatabaseManager.sharedInstance().serverHasTalkCapability(kCapabilityMediaCaption, forAccountId: self.account.accountId) {
  465. let messageParameters = NCMessageParameter.messageParametersJSONString(from: self.mentionsDict) ?? ""
  466. let message = NCChatMessage()
  467. message.message = self.replaceMentionsDisplayNamesWithMentionsKeysInMessage(message: self.textView.text, parameters: messageParameters)
  468. message.messageParametersJSONString = messageParameters
  469. shareItem.caption = message.sendingMessage
  470. }
  471. }
  472. for shareItem in self.shareItemController.shareItems {
  473. NSLog("Uploading \(shareItem.fileURL.absoluteString)")
  474. self.uploadGroup.enter()
  475. NCAPIController.sharedInstance().uniqueNameForFileUpload(withName: shareItem.fileName, originalName: true, for: self.account) { fileServerURL, fileServerPath, _, errorDescription in
  476. if let fileServerURL, let fileServerPath {
  477. self.uploadFile(to: fileServerURL, with: fileServerPath, with: shareItem)
  478. } else {
  479. NCUtils.log(String(format: "Error finding unique upload name. Error: %@", errorDescription ?? "Unknown error"))
  480. self.uploadErrors.append(errorDescription ?? "Unknown error")
  481. self.uploadGroup.leave()
  482. }
  483. }
  484. }
  485. self.uploadGroup.notify(queue: .main) {
  486. self.stopAnimatingSharingIndicator()
  487. self.hud?.hide(animated: true)
  488. // TODO: Do error reporting per item
  489. if self.uploadErrors.isEmpty {
  490. self.shareItemController.removeAllItems()
  491. self.delegate?.shareConfirmationViewControllerDidFinish(self)
  492. } else {
  493. // We remove the successfully uploaded items, so only the failed ones are kept
  494. self.shareItemController.remove(self.uploadSuccess)
  495. let alert = UIAlertController(title: NSLocalizedString("Upload failed", comment: ""),
  496. message: self.uploadErrors.joined(separator: "\n"),
  497. preferredStyle: .alert)
  498. alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .default))
  499. self.present(alert, animated: true)
  500. }
  501. bgTask.stopBackgroundTask()
  502. }
  503. }
  504. func uploadFile(to fileServerURL: String, with filePath: String, with item: ShareItem) {
  505. NextcloudKit.shared.upload(serverUrlFileName: fileServerURL, fileNameLocalPath: item.filePath) { _ in
  506. NSLog("Upload task")
  507. } progressHandler: { progress in
  508. item.uploadProgress = progress.fractionCompleted
  509. self.updateHudProgress()
  510. } completionHandler: { _, _, _, _, _, _, nkError in
  511. if nkError.errorCode == 0 {
  512. var talkMetaData: [String: Any] = [:]
  513. let itemCaption = item.caption.trimmingCharacters(in: .whitespaces)
  514. if !itemCaption.isEmpty {
  515. talkMetaData["caption"] = itemCaption
  516. }
  517. if self.shareSilently {
  518. talkMetaData["silent"] = self.shareSilently
  519. }
  520. NCAPIController.sharedInstance().shareFileOrFolder(for: self.account, atPath: filePath, toRoom: self.room.token, talkMetaData: talkMetaData) { error in
  521. if let error {
  522. NCUtils.log(String(format: "Failed to share file. Error: %@", error.localizedDescription))
  523. self.uploadErrors.append(error.localizedDescription)
  524. } else {
  525. self.uploadSuccess.append(item)
  526. }
  527. self.uploadGroup.leave()
  528. }
  529. } else if nkError.errorCode == 404 || nkError.errorCode == 409 {
  530. NCAPIController.sharedInstance().checkOrCreateAttachmentFolder(for: self.account) { created, _ in
  531. if created {
  532. self.uploadFile(to: fileServerURL, with: filePath, with: item)
  533. } else {
  534. self.uploadErrors.append(nkError.errorDescription)
  535. self.uploadGroup.leave()
  536. }
  537. }
  538. } else {
  539. NCUtils.log(String(format: "Failed to upload file. Error: %@", nkError.errorDescription))
  540. self.uploadErrors.append(nkError.errorDescription)
  541. self.uploadGroup.leave()
  542. }
  543. }
  544. }
  545. // MARK: - User Interface
  546. func startAnimatingSharingIndicator() {
  547. DispatchQueue.main.async {
  548. self.sharingIndicatorView.startAnimating()
  549. self.navigationItem.rightBarButtonItem = UIBarButtonItem(customView: self.sharingIndicatorView)
  550. }
  551. }
  552. func stopAnimatingSharingIndicator() {
  553. DispatchQueue.main.async {
  554. self.sharingIndicatorView.stopAnimating()
  555. self.navigationItem.rightBarButtonItem = self.sendButton
  556. }
  557. }
  558. func updateToolbarForCurrentItem() {
  559. if let item = self.getCurrentShareItem() {
  560. UIView.transition(with: self.itemToolbar, duration: 0.3, options: .transitionCrossDissolve) {
  561. self.cropItemButton.isEnabled = item.isImage
  562. self.previewItemButton.isEnabled = QLPreviewController.canPreview(item.fileURL as QLPreviewItem)
  563. self.addItemButton.isEnabled = self.shareItemController.shareItems.count < 5
  564. }
  565. } else {
  566. self.cropItemButton.isEnabled = false
  567. self.previewItemButton.isEnabled = false
  568. }
  569. self.removeItemButton.isEnabled = self.shareItemController.shareItems.count > 1
  570. self.removeItemButton.tintColor = self.shareItemController.shareItems.count > 1 ? nil : .clear
  571. }
  572. // MARK: - UIImagePickerController Delegate
  573. public func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) {
  574. self.saveImagePickerSettings(picker)
  575. guard let mediaType = info[.mediaType] as? String else { return }
  576. if mediaType == "public.image" {
  577. if let image = info[.originalImage] as? UIImage {
  578. self.dismiss(animated: true) {
  579. self.shareItemController.addItem(with: image)
  580. self.collectionViewScrollToEnd()
  581. }
  582. }
  583. } else if mediaType == "public.movie" {
  584. if let videoUrl = info[.mediaURL] as? URL {
  585. self.dismiss(animated: true) {
  586. self.shareItemController.addItem(with: videoUrl)
  587. self.collectionViewScrollToEnd()
  588. }
  589. }
  590. }
  591. }
  592. public func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
  593. self.saveImagePickerSettings(picker)
  594. self.dismiss(animated: true)
  595. }
  596. func saveImagePickerSettings(_ picker: UIImagePickerController) {
  597. if picker.sourceType == .camera && picker.cameraCaptureMode == .photo {
  598. NCUserDefaults.setPreferredCameraFlashMode(picker.cameraFlashMode.rawValue)
  599. }
  600. }
  601. // MARK: - UIDocumentPickerViewController Delegate
  602. public func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
  603. for documentURL in urls {
  604. self.shareItemController.addItem(with: documentURL)
  605. }
  606. self.collectionViewScrollToEnd()
  607. }
  608. // MARK: - ScrollView/CollectionView
  609. public override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
  610. guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: kShareConfirmationCellIdentifier, for: indexPath) as? ShareConfirmationCollectionViewCell
  611. else { return UICollectionViewCell() }
  612. let item = self.shareItemController.shareItems[indexPath.row]
  613. // Setting placeholder here in case we can't generate any other preview
  614. cell.setPlaceHolderImage(item.placeholderImage)
  615. cell.setPlaceHolderText(item.fileName)
  616. if let fileURL = item.fileURL, NCUtils.isImage(fileExtension: fileURL.pathExtension),
  617. let image = self.shareItemController.getImageFrom(item) {
  618. // We're able to get an image directly from the fileURL -> use it
  619. cell.setPreviewImage(image)
  620. } else {
  621. self.generatePreview(for: cell, with: collectionView, with: item)
  622. }
  623. return cell
  624. }
  625. func generatePreview(for cell: ShareConfirmationCollectionViewCell, with collectionView: UICollectionView, with item: ShareItem) {
  626. let size = CGSize(width: collectionView.bounds.width, height: collectionView.bounds.height)
  627. let scale = self.view.window?.screen.scale ?? UIScreen.main.scale
  628. // updateHandler might be called multiple times, starting from low quality representation to high-quality
  629. let request = QLThumbnailGenerator.Request(fileAt: item.fileURL, size: size, scale: scale, representationTypes: [.lowQualityThumbnail, .thumbnail])
  630. QLThumbnailGenerator.shared.generateRepresentations(for: request) { thumbnail, _, error in
  631. guard error == nil, let thumbnail else { return }
  632. DispatchQueue.main.async {
  633. cell.setPreviewImage(thumbnail.uiImage)
  634. }
  635. }
  636. }
  637. public override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
  638. return self.shareItemController.shareItems.count
  639. }
  640. public override func numberOfSections(in collectionView: UICollectionView) -> Int {
  641. return 1
  642. }
  643. public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
  644. return CGSize(width: collectionView.bounds.width, height: collectionView.bounds.height)
  645. }
  646. public override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
  647. if self.textView.isFirstResponder {
  648. self.textView.resignFirstResponder()
  649. } else {
  650. self.previewCurrentItem()
  651. }
  652. }
  653. public override func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
  654. self.updatePageControlPage()
  655. }
  656. public override func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
  657. self.updatePageControlPage()
  658. }
  659. func collectionViewScrollToEnd() {
  660. if let item = self.shareItemController.shareItems.last {
  661. self.scroll(to: item, animated: true)
  662. }
  663. }
  664. func scroll(to item: ShareItem, animated: Bool) {
  665. DispatchQueue.main.async {
  666. if let indexForItem = self.shareItemController.shareItems.firstIndex(of: item) {
  667. let indexPath = IndexPath(row: indexForItem, section: 0)
  668. self.shareCollectionView.scrollToItem(at: indexPath, at: [], animated: animated)
  669. }
  670. }
  671. }
  672. func getCurrentShareItem() -> ShareItem? {
  673. let currentIndex = Int(self.shareCollectionView.contentOffset.x / self.shareCollectionView.frame.size.width)
  674. if currentIndex >= self.shareItemController.shareItems.count {
  675. return nil
  676. }
  677. return self.shareItemController.shareItems[currentIndex]
  678. }
  679. // MARK: - PageControl
  680. func pageControlValueChanged() {
  681. let indexPath = IndexPath(row: self.pageControl.currentPage, section: 0)
  682. self.shareCollectionView.scrollToItem(at: indexPath, at: [], animated: true)
  683. }
  684. func updatePageControlPage() {
  685. // see: https://stackoverflow.com/a/46181277/2512312
  686. DispatchQueue.main.async {
  687. self.pageControl.currentPage = Int(self.shareCollectionView.contentOffset.x / self.shareCollectionView.frame.width)
  688. self.updateToolbarForCurrentItem()
  689. }
  690. }
  691. // MARK: - PreviewController
  692. func previewCurrentItem() {
  693. self.textView.resignFirstResponder()
  694. guard let item = self.getCurrentShareItem(),
  695. let fileURL = item.fileURL,
  696. QLPreviewController.canPreview(fileURL as QLPreviewItem)
  697. else { return }
  698. let preview = QLPreviewController()
  699. preview.dataSource = self
  700. preview.delegate = self
  701. preview.navigationController?.navigationBar.tintColor = NCAppBranding.themeTextColor()
  702. preview.navigationController?.navigationBar.barTintColor = NCAppBranding.themeColor()
  703. preview.tabBarController?.tabBar.tintColor = NCAppBranding.themeColor()
  704. let appearance = UINavigationBarAppearance()
  705. appearance.configureWithOpaqueBackground()
  706. appearance.titleTextAttributes = [.foregroundColor: NCAppBranding.themeTextColor()]
  707. appearance.backgroundColor = NCAppBranding.themeColor()
  708. self.navigationItem.standardAppearance = appearance
  709. self.navigationItem.compactAppearance = appearance
  710. self.navigationItem.scrollEdgeAppearance = appearance
  711. self.navigationController?.pushViewController(preview, animated: true)
  712. }
  713. public func numberOfPreviewItems(in controller: QLPreviewController) -> Int {
  714. return 1
  715. }
  716. public func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem {
  717. // Don't use index here, as this relates to numberOfPreviewItems
  718. // When we have numberOfPreviewItems > 1 this will show an additional list of items
  719. guard let item = self.getCurrentShareItem(),
  720. let fileURL = item.fileURL
  721. else { return URL(fileURLWithPath: "") as QLPreviewItem }
  722. return fileURL as QLPreviewItem
  723. }
  724. public func previewController(_ controller: QLPreviewController, editingModeFor previewItem: QLPreviewItem) -> QLPreviewItemEditingMode {
  725. return .createCopy
  726. }
  727. public func previewController(_ controller: QLPreviewController, didSaveEditedCopyOf previewItem: QLPreviewItem, at modifiedContentsURL: URL) {
  728. if let item = self.getCurrentShareItem() {
  729. self.shareItemController.update(item, with: modifiedContentsURL)
  730. }
  731. }
  732. // MARK: - ShareItemController Delegate
  733. public func shareItemControllerItemsChanged(_ shareItemController: ShareItemController) {
  734. DispatchQueue.main.async {
  735. if shareItemController.shareItems.isEmpty {
  736. if let extensionContext = self.extensionContext {
  737. let error = NSError(domain: NSCocoaErrorDomain, code: 0)
  738. extensionContext.cancelRequest(withError: error)
  739. } else {
  740. self.dismiss(animated: true)
  741. }
  742. } else {
  743. self.shareCollectionView.reloadData()
  744. // Make sure all changes are fully populated before we update our UI elements
  745. self.shareCollectionView.layoutIfNeeded()
  746. self.updateToolbarForCurrentItem()
  747. self.pageControl.numberOfPages = shareItemController.shareItems.count
  748. }
  749. }
  750. }
  751. // MARK: - TOCropViewController Delegate
  752. public func cropViewController(_ cropViewController: TOCropViewController, didCropTo image: UIImage, with cropRect: CGRect, angle: Int) {
  753. if let item = self.getCurrentShareItem() {
  754. self.shareItemController.update(item, with: image)
  755. // Fixes bug on iPad where collectionView is scrolled between two pages
  756. self.scroll(to: item, animated: true)
  757. }
  758. // Fixes weird iOS 13 bug: https://github.com/TimOliver/TOCropViewController/issues/365
  759. cropViewController.transitioningDelegate = nil
  760. cropViewController.dismiss(animated: true)
  761. }
  762. public func cropViewController(_ cropViewController: TOCropViewController, didFinishCancelled cancelled: Bool) {
  763. if let item = self.getCurrentShareItem() {
  764. self.scroll(to: item, animated: true)
  765. }
  766. // Fixes weird iOS 13 bug: https://github.com/TimOliver/TOCropViewController/issues/365
  767. cropViewController.transitioningDelegate = nil
  768. cropViewController.dismiss(animated: true)
  769. }
  770. // MARK: - NKCommon Delegate
  771. public func authenticationChallenge(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
  772. // The pinning check
  773. if CCCertificate.sharedManager().checkTrustedChallenge(challenge) {
  774. completionHandler(.useCredential, URLCredential(trust: challenge.protectionSpace.serverTrust!))
  775. } else {
  776. completionHandler(.performDefaultHandling, nil)
  777. }
  778. }
  779. }