// // ComposeAttachmentsViewController.swift // Tusker // // Created by Shadowfacts on 3/11/20. // Copyright © 2020 Shadowfacts. All rights reserved. // import UIKit import Pachyderm protocol ComposeAttachmentsViewControllerDelegate: class { func composeSelectedAttachmentsDidChange() func composeRequiresAttachmentDescriptionsDidChange() } class ComposeAttachmentsViewController: UITableViewController { weak var mastodonController: MastodonController! weak var delegate: ComposeAttachmentsViewControllerDelegate? private var heightConstraint: NSLayoutConstraint! var attachments: [CompositionAttachment] = [] { didSet { delegate?.composeSelectedAttachmentsDidChange() updateAddAttachmentsButtonEnabled() } } var requiresAttachmentDescriptions: Bool { if Preferences.shared.requireAttachmentDescriptions { return !attachments.allSatisfy { $0.description.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty } } else { return false } } init(attachments: [CompositionAttachment], mastodonController: MastodonController) { self.attachments = attachments self.mastodonController = mastodonController super.init(style: .plain) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func viewDidLoad() { super.viewDidLoad() tableView.rowHeight = UITableView.automaticDimension tableView.estimatedRowHeight = 96 tableView.register(UINib(nibName: "AddAttachmentTableViewCell", bundle: .main), forCellReuseIdentifier: "addAttachment") tableView.register(UINib(nibName: "ComposeAttachmentTableViewCell", bundle: .main), forCellReuseIdentifier: "composeAttachment") heightConstraint = tableView.heightAnchor.constraint(equalToConstant: tableView.contentSize.height) heightConstraint.isActive = true } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) updateHeightConstraint() } func setAttachments(_ attachments: [CompositionAttachment]) { self.attachments = attachments tableView.reloadData() updateHeightConstraint() delegate?.composeRequiresAttachmentDescriptionsDidChange() } private func updateHeightConstraint() { heightConstraint.constant = tableView.contentSize.height } private func isAddAttachmentsButtonEnabled() -> Bool { switch mastodonController.instance.instanceType { case .pleroma: return true case .mastodon: return !attachments.contains(where: { $0.data.type == .video }) && attachments.count < 4 } } private func updateAddAttachmentsButtonEnabled() { let cell = tableView.cellForRow(at: IndexPath(row: 0, section: 1)) as! AddAttachmentTableViewCell cell.setEnabled(isAddAttachmentsButtonEnabled()) } func uploadAll(stepProgress: @escaping () -> Void, completion: @escaping (_ success: Bool, _ uploadedAttachments: [Attachment]) -> Void) { let group = DispatchGroup() var anyFailed = false var uploadedAttachments: [Result?] = [] for (index, compAttachment) in attachments.enumerated() { group.enter() uploadedAttachments.append(nil) compAttachment.data.getData { (data, mimeType) in stepProgress() let formAttachment = FormAttachment(mimeType: mimeType, data: data, fileName: "file") let request = Client.upload(attachment: formAttachment, description: compAttachment.description) self.mastodonController.run(request) { (response) in switch response { case let .failure(error): uploadedAttachments[index] = .failure(error) anyFailed = true case let .success(attachment, _): uploadedAttachments[index] = .success(attachment) } stepProgress() group.leave() } } } group.notify(queue: .main) { if anyFailed { let errors: [(Int, Error)] = uploadedAttachments.enumerated().compactMap { (index, result) in switch result { case let .failure(error): return (index, error) default: return nil } } let title: String var message: String if errors.count == 1 { title = NSLocalizedString("Could not upload attachment", comment: "single attachment upload failed alert title") message = errors[0].1.localizedDescription } else { title = NSLocalizedString("Could not upload the following attachments", comment: "multiple attachment upload failures alert title") message = "" for (index, error) in errors { message.append("Attachment \(index + 1): \(error.localizedDescription)") } } let alert = UIAlertController(title: title, message: message, preferredStyle: .alert) alert.addAction(UIAlertAction(title: "Ok", style: .default, handler: { (_) in completion(false, []) })) } else { let uploadedAttachments: [Attachment] = uploadedAttachments.compactMap { switch $0 { case let .success(attachment): return attachment default: return nil } } completion(true, uploadedAttachments) } } } // MARK: Table view data source override func numberOfSections(in tableView: UITableView) -> Int { return 2 } override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { switch section { case 0: return attachments.count case 1: return 1 default: fatalError("invalid section \(section)") } } override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { switch indexPath.section { case 0: let attachment = attachments[indexPath.row] let cell = tableView.dequeueReusableCell(withIdentifier: "composeAttachment", for: indexPath) as! ComposeAttachmentTableViewCell cell.delegate = self cell.updateUI(for: attachment) return cell case 1: let cell = tableView.dequeueReusableCell(withIdentifier: "addAttachment", for: indexPath) as! AddAttachmentTableViewCell cell.setEnabled(isAddAttachmentsButtonEnabled()) return cell default: fatalError("invalid section \(indexPath.section)") } } // MARK: Table view delegate override func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? { if indexPath.section == 1, isAddAttachmentsButtonEnabled() { return indexPath } return nil } override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { tableView.deselectRow(at: indexPath, animated: true) if indexPath.section == 1 { addAttachmentPressed() } } func addAttachmentPressed() { let sheetContainer = AssetPickerSheetContainerViewController() sheetContainer.assetPicker.assetPickerDelegate = self present(sheetContainer, animated: true) } } extension ComposeAttachmentsViewController: AssetPickerViewControllerDelegate { func assetPicker(_ assetPicker: AssetPickerViewController, shouldAllowAssetOfType type: CompositionAttachmentData.AttachmentType) -> Bool { switch mastodonController.instance.instanceType { case .pleroma: return true case .mastodon: if (type == .video && attachments.count > 0) || attachments.contains(where: { $0.data.type == .video }) || assetPicker.currentCollectionSelectedAssets.contains(where: { $0.type == .video }) { return false } return attachments.count + assetPicker.currentCollectionSelectedAssets.count < 4 } } func assetPicker(_ assetPicker: AssetPickerViewController, didSelectAttachments attachments: [CompositionAttachmentData]) { let attachments = attachments.map { CompositionAttachment(data: $0) } let indexPaths = attachments.indices.map { IndexPath(row: $0 + self.attachments.count, section: 0) } self.attachments.append(contentsOf: attachments) tableView.insertRows(at: indexPaths, with: .automatic) updateHeightConstraint() } } extension ComposeAttachmentsViewController: ComposeAttachmentTableViewCellDelegate { func removeAttachment(_ cell: ComposeAttachmentTableViewCell) { guard let indexPath = tableView.indexPath(for: cell) else { return } attachments.remove(at: indexPath.row) tableView.performBatchUpdates({ tableView.deleteRows(at: [indexPath], with: .automatic) }, completion: { (_) in // when removing cells, we don't trigger the container height update until after the animation has completed // otherwise, during the animation, the height is too short and the last row briefly disappears self.updateHeightConstraint() }) } func attachmentDescriptionChanged(_ cell: ComposeAttachmentTableViewCell) { delegate?.composeRequiresAttachmentDescriptionsDidChange() } }