// // ComposeViewController.swift // Tusker // // Created by Shadowfacts on 8/28/18. // Copyright © 2018 Shadowfacts. All rights reserved. // import UIKit import Pachyderm class ComposeViewController: UIViewController { static func create(inReplyTo inReplyToID: String? = nil, mentioning: Account? = nil) -> UIViewController { guard let navigationController = UIStoryboard(name: "Compose", bundle: nil).instantiateInitialViewController() as? UINavigationController, let composeVC = navigationController.topViewController as? ComposeViewController else { fatalError() } composeVC.inReplyToID = inReplyToID composeVC.mentioning = mentioning return navigationController } @IBOutlet weak var scrollView: UIScrollView! @IBOutlet weak var inReplyToContainerView: UIView! @IBOutlet weak var inReplyToAvatarImageView: UIImageView! @IBOutlet weak var inReplyToDisplayNameLabel: UILabel! @IBOutlet weak var inReplyToUsernameLabel: UILabel! @IBOutlet weak var inReplyToContentLabel: StatusContentLabel! @IBOutlet weak var inReplyToLabel: UILabel! @IBOutlet weak var statusTextView: UITextView! @IBOutlet weak var visibilityButton: UIButton! @IBOutlet weak var postButton: UIButton! @IBOutlet weak var contentWarningTextField: UITextField! @IBOutlet weak var mediaStackView: UIStackView! @IBOutlet weak var paddingView: UIView! @IBOutlet weak var progressView: SteppedProgressView! var scrolled = false var inReplyToID: String? var mentioning: Account? var contentWarning = false { didSet { contentWarningTextField.isHidden = !contentWarning } } var visibility = Preferences.shared.defaultPostVisibility { didSet { visibilityButton.setTitle(visibility.displayName, for: .normal) } } var status: Status? override func viewDidLoad() { super.viewDidLoad() statusTextView.placeholder = "What is on your mind?" statusTextView.layer.cornerRadius = 5 statusTextView.layer.masksToBounds = true visibilityButton.setTitle(visibility.displayName, for: .normal) contentWarningTextField.delegate = self let toolbar = UIToolbar(frame: CGRect(x: 0, y: 0, width: view.frame.width, height: 30)) let flexSpace = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil) let done = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(keyboardDoneButtonPressed)) toolbar.setItems([flexSpace, done], animated: false) toolbar.sizeToFit() statusTextView.inputAccessoryView = toolbar if let inReplyToID = inReplyToID, let inReplyTo = StatusCache.get(id: inReplyToID) { inReplyToDisplayNameLabel.text = inReplyTo.account.realDisplayName inReplyToUsernameLabel.text = "@\(inReplyTo.account.username)" inReplyToContentLabel.statusID = inReplyToID inReplyToAvatarImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: inReplyToAvatarImageView) inReplyToAvatarImageView.layer.masksToBounds = true inReplyToAvatarImageView.image = nil AvatarCache.shared.get(inReplyTo.account.avatar) { image in DispatchQueue.main.async { self.inReplyToAvatarImageView.image = image } } inReplyToLabel.text = "In reply to \(inReplyTo.account.realDisplayName)" if inReplyTo.account != MastodonController.shared.account { statusTextView.text = "@\(inReplyTo.account.acct) " } statusTextView.text += inReplyTo.mentions.filter({ $0.id != MastodonController.shared.account.id }).map({ "@\($0.acct) " }).joined() statusTextView.textViewDidChange(statusTextView) contentWarning = inReplyTo.sensitive contentWarningTextField.text = inReplyTo.spoilerText visibility = inReplyTo.visibility } else { inReplyToLabel.isHidden = true inReplyToContainerView.isHidden = true } if let mentioning = mentioning { statusTextView.text += "@\(mentioning.acct) " statusTextView.textViewDidChange(statusTextView) } progressView.progress = 0 } override func viewDidLayoutSubviews() { if inReplyToID != nil && !scrolled { scrollView.contentOffset = CGPoint(x: 0, y: inReplyToContainerView.bounds.height - 44) scrolled = true } } func addMedia(for image: UIImage) { let mediaView = ComposeMediaView(image: image) mediaView.delegate = self mediaStackView.addArrangedSubview(mediaView) } // MARK: - Navigation override func prepare(for segue: UIStoryboardSegue, sender: Any?) { statusTextView.resignFirstResponder() contentWarningTextField.resignFirstResponder() if segue.identifier == "postComplete" { guard let status = status else { fatalError("postComplete segue can't occur without Status") } guard let dest = segue.destination as? MainTabBarViewController, let navController = dest.selectedViewController as? UINavigationController, let topVC = navController.topViewController as? StatusTableViewCellDelegate else { return } topVC.selected(status: status.id) } } // MARK: - Interaction @IBAction func visibilityPressed(_ sender: Any) { let alertController = UIAlertController(title: "Post Visibility", message: nil, preferredStyle: .actionSheet) for visibility in Status.Visibility.allCases { let action = UIAlertAction(title: visibility.displayName, style: .default, handler: { _ in UIView.performWithoutAnimation { self.visibility = visibility self.visibilityButton.layoutIfNeeded() } }) if visibility == self.visibility { action.setValue(true, forKey: "checked") } alertController.addAction(action) } alertController.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil)) present(alertController, animated: true) } @IBAction func contentWarningPressed(_ sender: Any) { contentWarning = !contentWarning } @IBAction func mediaPressed(_ sender: Any) { let imagePicker = UIImagePickerController() imagePicker.delegate = self let alertController = UIAlertController(title: "Choose Image Source", message: nil, preferredStyle: .actionSheet) if UIImagePickerController.isSourceTypeAvailable(.camera) { alertController.addAction(UIAlertAction(title: "Camera", style: .default, handler: { _ in imagePicker.sourceType = .camera self.present(imagePicker, animated: true) })) } if UIImagePickerController.isSourceTypeAvailable(.photoLibrary) { alertController.addAction(UIAlertAction(title: "Photo Library", style: .default, handler: { __ in imagePicker.sourceType = .photoLibrary self.present(imagePicker, animated: true) })) } alertController.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil)) present(alertController, animated: true) } @IBAction func postPressed(_ sender: Any) { guard let text = statusTextView.text, !text.isEmpty else { return } postButton.isEnabled = false let contentWarning: String? if self.contentWarning, let text = contentWarningTextField.text, !text.isEmpty { contentWarning = text } else { contentWarning = nil } let sensitive = contentWarning != nil let visibility = self.visibility var attachments: [Attachment?] = [] let group = DispatchGroup() for view in mediaStackView.arrangedSubviews { guard let mediaView = view as? ComposeMediaView, let image = mediaView.image, let data = image.pngData() else { continue } let index = attachments.count attachments.append(nil) progressView.steps += 1 group.enter() let request = MastodonController.shared.client.upload(attachment: FormAttachment(pngData: data), description: mediaView.mediaDescription) MastodonController.shared.client.run(request) { response in guard case let .success(attachment, _) = response else { fatalError() } attachments[index] = attachment self.progressView.step() group.leave() } } progressView.steps = 2 + attachments.count progressView.currentStep = 1 group.notify(queue: .main) { let attachments = attachments.compactMap { $0 } let request = MastodonController.shared.client.createStatus(text: text, inReplyTo: self.inReplyToID, media: attachments, sensitive: sensitive, spoilerText: contentWarning, visiblity: visibility) MastodonController.shared.client.run(request) { response in guard case let .success(status, _) = response else { fatalError() } self.status = status StatusCache.add(status) DispatchQueue.main.async { self.progressView.step() self.performSegue(withIdentifier: "postComplete", sender: self) } } } } @objc func imagePressed(_ gesture: UITapGestureRecognizer) { gesture.view!.superview!.removeFromSuperview() } @objc func keyboardDoneButtonPressed() { statusTextView.endEditing(false) } } extension ComposeViewController: UITextFieldDelegate { func textFieldShouldReturn(_ textField: UITextField) -> Bool { textField.endEditing(false) return true } } extension ComposeViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate { func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) { if let selectedImage = info[.originalImage] as? UIImage { addMedia(for: selectedImage) dismiss(animated: true) } } } extension ComposeViewController: ComposeMediaViewDelegate { func editDescription(for media: ComposeMediaView) { let alertController = UIAlertController(title: "Media Description", message: nil, preferredStyle: .alert) alertController.addTextField { textField in textField.text = media.mediaDescription } alertController.addAction(UIAlertAction(title: "Confirm", style: .default, handler: { _ in let description = alertController.textFields![0].text media.mediaDescription = description })) alertController.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil)) present(alertController, animated: true) } }