SendMediaNavigationController.swift 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701
  1. //
  2. // Copyright (c) 2019 Open Whisper Systems. All rights reserved.
  3. //
  4. import Foundation
  5. import Photos
  6. import PromiseKit
  7. @objc
  8. protocol SendMediaNavDelegate: AnyObject {
  9. func sendMediaNavDidCancel(_ sendMediaNavigationController: SendMediaNavigationController)
  10. func sendMediaNav(_ sendMediaNavigationController: SendMediaNavigationController, didApproveAttachments attachments: [SignalAttachment], messageText: String?)
  11. func sendMediaNavInitialMessageText(_ sendMediaNavigationController: SendMediaNavigationController) -> String?
  12. func sendMediaNav(_ sendMediaNavigationController: SendMediaNavigationController, didChangeMessageText newMessageText: String?)
  13. }
  14. @objc
  15. class SendMediaNavigationController: OWSNavigationController {
  16. // This is a sensitive constant, if you change it make sure to check
  17. // on iPhone5, 6, 6+, X, layouts.
  18. static let bottomButtonsCenterOffset: CGFloat = -34
  19. var attachmentCount: Int {
  20. return attachmentDraftCollection.count - attachmentDraftCollection.pickerAttachments.count + mediaLibrarySelections.count
  21. }
  22. // MARK: - Overrides
  23. override var prefersStatusBarHidden: Bool {
  24. guard !OWSWindowManager.shared().hasCall() else {
  25. return false
  26. }
  27. return true
  28. }
  29. override func viewDidLoad() {
  30. super.viewDidLoad()
  31. self.delegate = self
  32. let bottomButtonsCenterOffset = SendMediaNavigationController.bottomButtonsCenterOffset
  33. view.addSubview(batchModeButton)
  34. batchModeButton.setCompressionResistanceHigh()
  35. batchModeButton.centerYAnchor.constraint(equalTo: view.layoutMarginsGuide.bottomAnchor, constant: bottomButtonsCenterOffset).isActive = true
  36. batchModeButton.autoPinEdge(toSuperviewMargin: .trailing)
  37. view.addSubview(doneButton)
  38. doneButton.setCompressionResistanceHigh()
  39. doneButton.centerYAnchor.constraint(equalTo: view.layoutMarginsGuide.bottomAnchor, constant: bottomButtonsCenterOffset).isActive = true
  40. doneButton.autoPinEdge(toSuperviewMargin: .trailing)
  41. view.addSubview(cameraModeButton)
  42. cameraModeButton.setCompressionResistanceHigh()
  43. cameraModeButton.centerYAnchor.constraint(equalTo: view.layoutMarginsGuide.bottomAnchor, constant: bottomButtonsCenterOffset).isActive = true
  44. cameraModeButton.autoPinEdge(toSuperviewMargin: .leading)
  45. view.addSubview(mediaLibraryModeButton)
  46. mediaLibraryModeButton.setCompressionResistanceHigh()
  47. mediaLibraryModeButton.centerYAnchor.constraint(equalTo: view.layoutMarginsGuide.bottomAnchor, constant: bottomButtonsCenterOffset).isActive = true
  48. mediaLibraryModeButton.autoPinEdge(toSuperviewMargin: .leading)
  49. }
  50. // MARK: -
  51. @objc
  52. public weak var sendMediaNavDelegate: SendMediaNavDelegate?
  53. @objc
  54. public class func showingCameraFirst() -> SendMediaNavigationController {
  55. let navController = SendMediaNavigationController()
  56. navController.setViewControllers([navController.captureViewController], animated: false)
  57. return navController
  58. }
  59. @objc
  60. public class func showingMediaLibraryFirst() -> SendMediaNavigationController {
  61. let navController = SendMediaNavigationController()
  62. navController.setViewControllers([navController.mediaLibraryViewController], animated: false)
  63. return navController
  64. }
  65. var isInBatchSelectMode = false {
  66. didSet {
  67. if oldValue != isInBatchSelectMode {
  68. mediaLibraryViewController.batchSelectModeDidChange()
  69. guard let topViewController = viewControllers.last else {
  70. return
  71. }
  72. updateButtons(topViewController: topViewController)
  73. }
  74. }
  75. }
  76. func updateButtons(topViewController: UIViewController) {
  77. switch topViewController {
  78. case is AttachmentApprovalViewController:
  79. batchModeButton.isHidden = true
  80. doneButton.isHidden = true
  81. cameraModeButton.isHidden = true
  82. mediaLibraryModeButton.isHidden = true
  83. case is ImagePickerGridController:
  84. batchModeButton.isHidden = isInBatchSelectMode
  85. doneButton.isHidden = !isInBatchSelectMode || (attachmentDraftCollection.count == 0 && mediaLibrarySelections.count == 0)
  86. cameraModeButton.isHidden = false
  87. mediaLibraryModeButton.isHidden = true
  88. case is PhotoCaptureViewController:
  89. batchModeButton.isHidden = isInBatchSelectMode
  90. doneButton.isHidden = !isInBatchSelectMode || (attachmentDraftCollection.count == 0 && mediaLibrarySelections.count == 0)
  91. cameraModeButton.isHidden = true
  92. mediaLibraryModeButton.isHidden = false
  93. default:
  94. owsFailDebug("unexpected topViewController: \(topViewController)")
  95. }
  96. doneButton.updateCount()
  97. }
  98. func fadeTo(viewControllers: [UIViewController]) {
  99. let transition: CATransition = CATransition()
  100. transition.duration = 0.1
  101. transition.type = CATransitionType.fade
  102. view.layer.add(transition, forKey: nil)
  103. setViewControllers(viewControllers, animated: false)
  104. }
  105. // MARK: - Events
  106. private func didTapBatchModeButton() {
  107. // There's no way to _disable_ batch mode.
  108. isInBatchSelectMode = true
  109. }
  110. private func didTapCameraModeButton() {
  111. fadeTo(viewControllers: [captureViewController])
  112. }
  113. private func didTapMediaLibraryModeButton() {
  114. fadeTo(viewControllers: [mediaLibraryViewController])
  115. }
  116. // MARK: Views
  117. public static let bottomButtonWidth: CGFloat = 44
  118. private lazy var doneButton: DoneButton = {
  119. let button = DoneButton()
  120. button.delegate = self
  121. button.setShadow()
  122. return button
  123. }()
  124. private lazy var batchModeButton: UIButton = {
  125. let button = OWSButton(imageName: "media_send_batch_mode_disabled",
  126. tintColor: .ows_gray60,
  127. block: { [weak self] in self?.didTapBatchModeButton() })
  128. let width: CGFloat = type(of: self).bottomButtonWidth
  129. button.autoSetDimensions(to: CGSize(width: width, height: width))
  130. button.layer.cornerRadius = width / 2
  131. button.imageEdgeInsets = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
  132. button.backgroundColor = .ows_white
  133. button.setShadow()
  134. return button
  135. }()
  136. private lazy var cameraModeButton: UIButton = {
  137. let button = OWSButton(imageName: "settings-avatar-camera-2",
  138. tintColor: .ows_gray60,
  139. block: { [weak self] in self?.didTapCameraModeButton() })
  140. let width: CGFloat = type(of: self).bottomButtonWidth
  141. button.autoSetDimensions(to: CGSize(width: width, height: width))
  142. button.layer.cornerRadius = width / 2
  143. button.imageEdgeInsets = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
  144. button.backgroundColor = .ows_white
  145. button.setShadow()
  146. return button
  147. }()
  148. private lazy var mediaLibraryModeButton: UIButton = {
  149. let button = OWSButton(imageName: "actionsheet_camera_roll_black",
  150. tintColor: .ows_gray60,
  151. block: { [weak self] in self?.didTapMediaLibraryModeButton() })
  152. let width: CGFloat = type(of: self).bottomButtonWidth
  153. button.autoSetDimensions(to: CGSize(width: width, height: width))
  154. button.layer.cornerRadius = width / 2
  155. button.imageEdgeInsets = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
  156. button.backgroundColor = .ows_white
  157. button.setShadow()
  158. return button
  159. }()
  160. // MARK: State
  161. private var attachmentDraftCollection: AttachmentDraftCollection = .empty
  162. private var attachments: [SignalAttachment] {
  163. return attachmentDraftCollection.attachmentDrafts.map { $0.attachment }
  164. }
  165. private let mediaLibrarySelections: OrderedDictionary<PHAsset, MediaLibrarySelection> = OrderedDictionary()
  166. // MARK: Child VC's
  167. private lazy var captureViewController: PhotoCaptureViewController = {
  168. let vc = PhotoCaptureViewController()
  169. vc.delegate = self
  170. return vc
  171. }()
  172. private lazy var mediaLibraryViewController: ImagePickerGridController = {
  173. let vc = ImagePickerGridController()
  174. vc.delegate = self
  175. return vc
  176. }()
  177. private func pushApprovalViewController() {
  178. guard let sendMediaNavDelegate = self.sendMediaNavDelegate else {
  179. owsFailDebug("sendMediaNavDelegate was unexpectedly nil")
  180. return
  181. }
  182. let approvalViewController = AttachmentApprovalViewController(mode: .sharedNavigation, attachments: self.attachments)
  183. approvalViewController.approvalDelegate = self
  184. approvalViewController.messageText = sendMediaNavDelegate.sendMediaNavInitialMessageText(self)
  185. pushViewController(approvalViewController, animated: true)
  186. }
  187. private func didRequestExit(dontAbandonText: String) {
  188. if attachmentDraftCollection.count == 0 {
  189. self.sendMediaNavDelegate?.sendMediaNavDidCancel(self)
  190. } else {
  191. let alertTitle = NSLocalizedString("SEND_MEDIA_ABANDON_TITLE", comment: "alert title when user attempts to leave the send media flow when they have an in-progress album")
  192. let alert = UIAlertController(title: alertTitle, message: nil, preferredStyle: .alert)
  193. let confirmAbandonText = NSLocalizedString("SEND_MEDIA_CONFIRM_ABANDON_ALBUM", comment: "alert action, confirming the user wants to exit the media flow and abandon any photos they've taken")
  194. let confirmAbandonAction = UIAlertAction(title: confirmAbandonText,
  195. style: .destructive,
  196. handler: { [weak self] _ in
  197. guard let self = self else { return }
  198. self.sendMediaNavDelegate?.sendMediaNavDidCancel(self)
  199. })
  200. alert.addAction(confirmAbandonAction)
  201. let dontAbandonAction = UIAlertAction(title: dontAbandonText,
  202. style: .default,
  203. handler: { _ in })
  204. alert.addAction(dontAbandonAction)
  205. self.presentAlert(alert)
  206. }
  207. }
  208. }
  209. extension SendMediaNavigationController: UINavigationControllerDelegate {
  210. func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) {
  211. if let navbarTheme = preferredNavbarTheme(viewController: viewController) {
  212. if let owsNavBar = navigationBar as? OWSNavigationBar {
  213. owsNavBar.overrideTheme(type: navbarTheme)
  214. } else {
  215. owsFailDebug("unexpected navigationBar: \(navigationBar)")
  216. }
  217. }
  218. switch viewController {
  219. case is PhotoCaptureViewController:
  220. if attachmentDraftCollection.count == 1 && !isInBatchSelectMode {
  221. // User is navigating "back" to the previous view, indicating
  222. // they want to discard the previously captured item
  223. discardDraft()
  224. }
  225. case is ImagePickerGridController:
  226. if attachmentDraftCollection.count == 1 && !isInBatchSelectMode {
  227. isInBatchSelectMode = true
  228. self.mediaLibraryViewController.batchSelectModeDidChange()
  229. }
  230. default:
  231. break
  232. }
  233. self.updateButtons(topViewController: viewController)
  234. }
  235. // In case back navigation was canceled, we re-apply whatever is showing.
  236. func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) {
  237. if let navbarTheme = preferredNavbarTheme(viewController: viewController) {
  238. if let owsNavBar = navigationBar as? OWSNavigationBar {
  239. owsNavBar.overrideTheme(type: navbarTheme)
  240. } else {
  241. owsFailDebug("unexpected navigationBar: \(navigationBar)")
  242. }
  243. }
  244. self.updateButtons(topViewController: viewController)
  245. }
  246. // MARK: - Helpers
  247. private func preferredNavbarTheme(viewController: UIViewController) -> OWSNavigationBar.NavigationBarThemeOverride? {
  248. switch viewController {
  249. case is AttachmentApprovalViewController:
  250. return .clear
  251. case is ImagePickerGridController:
  252. return .alwaysDark
  253. case is PhotoCaptureViewController:
  254. return .clear
  255. default:
  256. owsFailDebug("unexpected viewController: \(viewController)")
  257. return nil
  258. }
  259. }
  260. // MARK: - Too Many
  261. func showTooManySelectedToast() {
  262. Logger.info("")
  263. let toastFormat = NSLocalizedString("IMAGE_PICKER_CAN_SELECT_NO_MORE_TOAST_FORMAT",
  264. comment: "Momentarily shown to the user when attempting to select more images than is allowed. Embeds {{max number of items}} that can be shared.")
  265. let toastText = String(format: toastFormat, NSNumber(value: SignalAttachment.maxAttachmentsAllowed))
  266. let toastController = ToastController(text: toastText)
  267. let kToastInset: CGFloat = 10
  268. let bottomInset = kToastInset + view.layoutMargins.bottom
  269. toastController.presentToastView(fromBottomOfView: view, inset: bottomInset)
  270. }
  271. }
  272. extension SendMediaNavigationController: PhotoCaptureViewControllerDelegate {
  273. func photoCaptureViewController(_ photoCaptureViewController: PhotoCaptureViewController, didFinishProcessingAttachment attachment: SignalAttachment) {
  274. attachmentDraftCollection.append(.camera(attachment: attachment))
  275. if isInBatchSelectMode {
  276. updateButtons(topViewController: photoCaptureViewController)
  277. } else {
  278. pushApprovalViewController()
  279. }
  280. }
  281. func photoCaptureViewControllerDidCancel(_ photoCaptureViewController: PhotoCaptureViewController) {
  282. let dontAbandonText = NSLocalizedString("SEND_MEDIA_RETURN_TO_CAMERA", comment: "alert action when the user decides not to cancel the media flow after all.")
  283. didRequestExit(dontAbandonText: dontAbandonText)
  284. }
  285. func photoCaptureViewControllerDidTryToCaptureTooMany(_ photoCaptureViewController: PhotoCaptureViewController) {
  286. showTooManySelectedToast()
  287. }
  288. func photoCaptureViewControllerCanCaptureMoreItems(_ photoCaptureViewController: PhotoCaptureViewController) -> Bool {
  289. return attachmentCount < SignalAttachment.maxAttachmentsAllowed
  290. }
  291. func discardDraft() {
  292. assert(attachmentDraftCollection.attachmentDrafts.count <= 1)
  293. if let lastAttachmentDraft = attachmentDraftCollection.attachmentDrafts.last {
  294. attachmentDraftCollection.remove(attachment: lastAttachmentDraft.attachment)
  295. }
  296. assert(attachmentDraftCollection.attachmentDrafts.count == 0)
  297. }
  298. }
  299. extension SendMediaNavigationController: ImagePickerGridControllerDelegate {
  300. func imagePickerDidCompleteSelection(_ imagePicker: ImagePickerGridController) {
  301. showApprovalAfterProcessingAnyMediaLibrarySelections()
  302. }
  303. func imagePickerDidCancel(_ imagePicker: ImagePickerGridController) {
  304. let dontAbandonText = NSLocalizedString("SEND_MEDIA_RETURN_TO_MEDIA_LIBRARY", comment: "alert action when the user decides not to cancel the media flow after all.")
  305. didRequestExit(dontAbandonText: dontAbandonText)
  306. }
  307. func showApprovalAfterProcessingAnyMediaLibrarySelections() {
  308. let mediaLibrarySelections: [MediaLibrarySelection] = self.mediaLibrarySelections.orderedValues
  309. let backgroundBlock: (ModalActivityIndicatorViewController) -> Void = { modal in
  310. let attachmentPromises: [Promise<MediaLibraryAttachment>] = mediaLibrarySelections.map { $0.promise }
  311. when(fulfilled: attachmentPromises).map { attachments in
  312. Logger.debug("built all attachments")
  313. modal.dismiss {
  314. self.attachmentDraftCollection.selectedFromPicker(attachments: attachments)
  315. self.pushApprovalViewController()
  316. }
  317. }.catch { error in
  318. Logger.error("failed to prepare attachments. error: \(error)")
  319. modal.dismiss {
  320. OWSAlerts.showAlert(title: NSLocalizedString("IMAGE_PICKER_FAILED_TO_PROCESS_ATTACHMENTS", comment: "alert title"))
  321. }
  322. }.retainUntilComplete()
  323. }
  324. ModalActivityIndicatorViewController.present(fromViewController: self,
  325. canCancel: false,
  326. backgroundBlock: backgroundBlock)
  327. }
  328. func imagePicker(_ imagePicker: ImagePickerGridController, isAssetSelected asset: PHAsset) -> Bool {
  329. return mediaLibrarySelections.hasValue(forKey: asset)
  330. }
  331. func imagePicker(_ imagePicker: ImagePickerGridController, didSelectAsset asset: PHAsset, attachmentPromise: Promise<SignalAttachment>) {
  332. guard !mediaLibrarySelections.hasValue(forKey: asset) else {
  333. return
  334. }
  335. let libraryMedia = MediaLibrarySelection(asset: asset, signalAttachmentPromise: attachmentPromise)
  336. mediaLibrarySelections.append(key: asset, value: libraryMedia)
  337. updateButtons(topViewController: imagePicker)
  338. }
  339. func imagePicker(_ imagePicker: ImagePickerGridController, didDeselectAsset asset: PHAsset) {
  340. guard mediaLibrarySelections.hasValue(forKey: asset) else {
  341. return
  342. }
  343. mediaLibrarySelections.remove(key: asset)
  344. updateButtons(topViewController: imagePicker)
  345. }
  346. func imagePickerCanSelectMoreItems(_ imagePicker: ImagePickerGridController) -> Bool {
  347. return attachmentCount < SignalAttachment.maxAttachmentsAllowed
  348. }
  349. func imagePickerDidTryToSelectTooMany(_ imagePicker: ImagePickerGridController) {
  350. showTooManySelectedToast()
  351. }
  352. }
  353. extension SendMediaNavigationController: AttachmentApprovalViewControllerDelegate {
  354. func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, didChangeMessageText newMessageText: String?) {
  355. sendMediaNavDelegate?.sendMediaNav(self, didChangeMessageText: newMessageText)
  356. }
  357. func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, didRemoveAttachment attachment: SignalAttachment) {
  358. guard let removedDraft = attachmentDraftCollection.attachmentDrafts.first(where: { $0.attachment == attachment}) else {
  359. owsFailDebug("removedDraft was unexpectedly nil")
  360. return
  361. }
  362. switch removedDraft.source {
  363. case .picker(attachment: let pickerAttachment):
  364. mediaLibrarySelections.remove(key: pickerAttachment.asset)
  365. case .camera(attachment: _):
  366. break
  367. }
  368. attachmentDraftCollection.remove(attachment: attachment)
  369. }
  370. func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, didApproveAttachments attachments: [SignalAttachment], messageText: String?) {
  371. sendMediaNavDelegate?.sendMediaNav(self, didApproveAttachments: attachments, messageText: messageText)
  372. }
  373. func attachmentApprovalDidCancel(_ attachmentApproval: AttachmentApprovalViewController) {
  374. sendMediaNavDelegate?.sendMediaNavDidCancel(self)
  375. }
  376. func attachmentApprovalDidTapAddMore(_ attachmentApproval: AttachmentApprovalViewController) {
  377. // Current design dicates we'll go "back" to the single thing before us.
  378. assert(viewControllers.count == 2)
  379. // regardless of which VC we're going "back" to, we're in "batch" mode at this point.
  380. isInBatchSelectMode = true
  381. mediaLibraryViewController.batchSelectModeDidChange()
  382. popViewController(animated: true)
  383. }
  384. }
  385. private enum AttachmentDraft {
  386. case camera(attachment: SignalAttachment)
  387. case picker(attachment: MediaLibraryAttachment)
  388. }
  389. private extension AttachmentDraft {
  390. var attachment: SignalAttachment {
  391. switch self {
  392. case .camera(let cameraAttachment):
  393. return cameraAttachment
  394. case .picker(let pickerAttachment):
  395. return pickerAttachment.signalAttachment
  396. }
  397. }
  398. var source: AttachmentDraft {
  399. return self
  400. }
  401. }
  402. private struct AttachmentDraftCollection {
  403. private(set) var attachmentDrafts: [AttachmentDraft]
  404. static var empty: AttachmentDraftCollection {
  405. return AttachmentDraftCollection(attachmentDrafts: [])
  406. }
  407. // MARK: -
  408. var count: Int {
  409. return attachmentDrafts.count
  410. }
  411. var pickerAttachments: [MediaLibraryAttachment] {
  412. return attachmentDrafts.compactMap { attachmentDraft in
  413. switch attachmentDraft.source {
  414. case .picker(let pickerAttachment):
  415. return pickerAttachment
  416. case .camera:
  417. return nil
  418. }
  419. }
  420. }
  421. var cameraAttachments: [SignalAttachment] {
  422. return attachmentDrafts.compactMap { attachmentDraft in
  423. switch attachmentDraft.source {
  424. case .picker:
  425. return nil
  426. case .camera(let cameraAttachment):
  427. return cameraAttachment
  428. }
  429. }
  430. }
  431. mutating func append(_ element: AttachmentDraft) {
  432. attachmentDrafts.append(element)
  433. }
  434. mutating func remove(attachment: SignalAttachment) {
  435. attachmentDrafts = attachmentDrafts.filter { $0.attachment != attachment }
  436. }
  437. mutating func selectedFromPicker(attachments: [MediaLibraryAttachment]) {
  438. let pickedAttachments: Set<MediaLibraryAttachment> = Set(attachments)
  439. let oldPickerAttachments: Set<MediaLibraryAttachment> = Set(self.pickerAttachments)
  440. for removedAttachment in oldPickerAttachments.subtracting(pickedAttachments) {
  441. remove(attachment: removedAttachment.signalAttachment)
  442. }
  443. // enumerate over new attachments to maintain order from picker
  444. for attachment in attachments {
  445. guard !oldPickerAttachments.contains(attachment) else {
  446. continue
  447. }
  448. append(.picker(attachment: attachment))
  449. }
  450. }
  451. }
  452. private struct MediaLibrarySelection: Hashable, Equatable {
  453. let asset: PHAsset
  454. let signalAttachmentPromise: Promise<SignalAttachment>
  455. var hashValue: Int {
  456. return asset.hashValue
  457. }
  458. var promise: Promise<MediaLibraryAttachment> {
  459. let asset = self.asset
  460. return signalAttachmentPromise.map { signalAttachment in
  461. return MediaLibraryAttachment(asset: asset, signalAttachment: signalAttachment)
  462. }
  463. }
  464. static func ==(lhs: MediaLibrarySelection, rhs: MediaLibrarySelection) -> Bool {
  465. return lhs.asset == rhs.asset
  466. }
  467. }
  468. private struct MediaLibraryAttachment: Hashable, Equatable {
  469. let asset: PHAsset
  470. let signalAttachment: SignalAttachment
  471. public var hashValue: Int {
  472. return asset.hashValue
  473. }
  474. public static func == (lhs: MediaLibraryAttachment, rhs: MediaLibraryAttachment) -> Bool {
  475. return lhs.asset == rhs.asset
  476. }
  477. }
  478. extension SendMediaNavigationController: DoneButtonDelegate {
  479. var doneButtonCount: Int {
  480. return attachmentCount
  481. }
  482. fileprivate func doneButtonWasTapped(_ doneButton: DoneButton) {
  483. assert(attachmentDraftCollection.count > 0 || mediaLibrarySelections.count > 0)
  484. showApprovalAfterProcessingAnyMediaLibrarySelections()
  485. }
  486. }
  487. private protocol DoneButtonDelegate: AnyObject {
  488. func doneButtonWasTapped(_ doneButton: DoneButton)
  489. var doneButtonCount: Int { get }
  490. }
  491. private class DoneButton: UIView {
  492. weak var delegate: DoneButtonDelegate?
  493. init() {
  494. super.init(frame: .zero)
  495. let tapGesture = UITapGestureRecognizer(target: self, action: #selector(didTap(tapGesture:)))
  496. addGestureRecognizer(tapGesture)
  497. let container = UIView()
  498. container.backgroundColor = .ows_white
  499. container.layer.cornerRadius = 20
  500. container.layoutMargins = UIEdgeInsets(top: 7, leading: 8, bottom: 7, trailing: 8)
  501. addSubview(container)
  502. container.autoPinEdgesToSuperviewMargins()
  503. let stackView = UIStackView(arrangedSubviews: [badge, chevron])
  504. stackView.axis = .horizontal
  505. stackView.alignment = .center
  506. stackView.spacing = 9
  507. container.addSubview(stackView)
  508. stackView.autoPinEdgesToSuperviewMargins()
  509. }
  510. let numberFormatter: NumberFormatter = NumberFormatter()
  511. func updateCount() {
  512. guard let delegate = delegate else {
  513. return
  514. }
  515. badgeLabel.text = numberFormatter.string(for: delegate.doneButtonCount)
  516. }
  517. required init?(coder aDecoder: NSCoder) {
  518. fatalError("init(coder:) has not been implemented")
  519. }
  520. // MARK: - Subviews
  521. private lazy var badge: UIView = {
  522. let badge = CircleView()
  523. badge.layoutMargins = UIEdgeInsets(top: 4, leading: 4, bottom: 4, trailing: 4)
  524. badge.backgroundColor = .ows_signalBlue
  525. badge.addSubview(badgeLabel)
  526. badgeLabel.autoPinEdgesToSuperviewMargins()
  527. // Constrain to be a pill that is at least a circle, and maybe wider.
  528. badgeLabel.autoPin(toAspectRatio: 1.0, relation: .greaterThanOrEqual)
  529. NSLayoutConstraint.autoSetPriority(.defaultLow) {
  530. badgeLabel.autoPinToSquareAspectRatio()
  531. }
  532. return badge
  533. }()
  534. private lazy var badgeLabel: UILabel = {
  535. let label = UILabel()
  536. label.textColor = .ows_white
  537. label.font = UIFont.ows_dynamicTypeSubheadline.ows_monospaced()
  538. label.textAlignment = .center
  539. return label
  540. }()
  541. private lazy var chevron: UIView = {
  542. let image: UIImage
  543. if CurrentAppContext().isRTL {
  544. image = #imageLiteral(resourceName: "small_chevron_left")
  545. } else {
  546. image = #imageLiteral(resourceName: "small_chevron_right")
  547. }
  548. let chevron = UIImageView(image: image.withRenderingMode(.alwaysTemplate))
  549. chevron.contentMode = .scaleAspectFit
  550. chevron.tintColor = .ows_gray60
  551. chevron.autoSetDimensions(to: CGSize(width: 10, height: 18))
  552. return chevron
  553. }()
  554. @objc
  555. func didTap(tapGesture: UITapGestureRecognizer) {
  556. delegate?.doneButtonWasTapped(self)
  557. }
  558. }