Tusker/Tusker/Screens/Compose/ComposeViewController.swift

328 lines
14 KiB
Swift
Raw Normal View History

2018-08-31 02:30:19 +00:00
//
// ComposeViewController.swift
// Tusker
//
// Created by Shadowfacts on 8/28/18.
// Copyright © 2018 Shadowfacts. All rights reserved.
//
import UIKit
2018-09-11 14:52:21 +00:00
import Pachyderm
2018-08-31 02:30:19 +00:00
class ComposeViewController: UIViewController {
static func create(inReplyTo inReplyToID: String? = nil, mentioning: String? = nil, text: String? = nil) -> UIViewController {
2018-08-31 02:30:19 +00:00
guard let navigationController = UIStoryboard(name: "Compose", bundle: nil).instantiateInitialViewController() as? UINavigationController,
let composeVC = navigationController.topViewController as? ComposeViewController else { fatalError() }
2018-09-18 01:57:46 +00:00
composeVC.inReplyToID = inReplyToID
composeVC.mentioningAcct = mentioning
composeVC.text = text
return navigationController
}
static func create(for session: XCBSession, mentioning: String? = nil, text: String? = nil) -> UIViewController {
guard let navigationController = UIStoryboard(name: "Compose", bundle: nil).instantiateInitialViewController() as? UINavigationController,
let composeVC = navigationController.topViewController as? ComposeViewController else { fatalError() }
composeVC.mentioningAcct = mentioning
composeVC.text = text
composeVC.xcbSession = session
2018-08-31 02:30:19 +00:00
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!
2018-09-30 02:20:17 +00:00
@IBOutlet weak var charactersRemainingLabel: UILabel!
2018-08-31 02:30:19 +00:00
@IBOutlet weak var visibilityButton: UIButton!
2018-09-12 13:19:51 +00:00
@IBOutlet weak var postButton: UIButton!
2018-08-31 02:30:19 +00:00
@IBOutlet weak var contentWarningTextField: UITextField!
@IBOutlet weak var mediaStackView: UIStackView!
@IBOutlet weak var paddingView: UIView!
2018-09-12 13:19:51 +00:00
@IBOutlet weak var progressView: SteppedProgressView!
2018-08-31 02:30:19 +00:00
var scrolled = false
2018-09-18 01:57:46 +00:00
var inReplyToID: String?
var mentioningAcct: String?
var text: String?
// Weak so that if a new session is initiated (i.e. XCBManager.currentSession is changed) while the current one is in progress, this one will be released
weak var xcbSession: XCBSession?
2018-08-31 02:30:19 +00:00
var contentWarning = false {
didSet {
contentWarningTextField.isHidden = !contentWarning
}
}
var visibility = Preferences.shared.defaultPostVisibility {
didSet {
visibilityButton.setTitle(visibility.displayName, for: .normal)
}
}
2018-08-31 02:30:19 +00:00
var status: Status?
override func viewDidLoad() {
super.viewDidLoad()
statusTextView.placeholder = "What is on your mind?"
statusTextView.layer.cornerRadius = 5
statusTextView.layer.masksToBounds = true
2018-09-30 02:20:17 +00:00
statusTextView.delegate = self
2018-08-31 02:30:19 +00:00
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
2018-09-18 01:57:46 +00:00
if let inReplyToID = inReplyToID,
2018-09-18 16:59:07 +00:00
let inReplyTo = MastodonCache.status(for: inReplyToID) {
2018-08-31 02:30:19 +00:00
inReplyToDisplayNameLabel.text = inReplyTo.account.realDisplayName
inReplyToUsernameLabel.text = "@\(inReplyTo.account.username)"
2018-09-18 01:57:46 +00:00
inReplyToContentLabel.statusID = inReplyToID
2018-08-31 02:30:19 +00:00
inReplyToAvatarImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: inReplyToAvatarImageView)
inReplyToAvatarImageView.layer.masksToBounds = true
inReplyToAvatarImageView.image = nil
2018-09-11 14:52:21 +00:00
AvatarCache.shared.get(inReplyTo.account.avatar) { image in
DispatchQueue.main.async {
self.inReplyToAvatarImageView.image = image
2018-08-31 02:30:19 +00:00
}
}
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)
2018-09-11 14:52:21 +00:00
contentWarning = inReplyTo.sensitive
contentWarningTextField.text = inReplyTo.spoilerText
visibility = inReplyTo.visibility
2018-08-31 02:30:19 +00:00
} else {
inReplyToLabel.isHidden = true
inReplyToContainerView.isHidden = true
}
if let mentioningAcct = mentioningAcct {
statusTextView.text += "@\(mentioningAcct) "
statusTextView.textViewDidChange(statusTextView)
}
if let text = text {
statusTextView.text += text
2018-08-31 02:30:19 +00:00
statusTextView.textViewDidChange(statusTextView)
}
2018-09-12 13:19:51 +00:00
2018-09-30 02:20:17 +00:00
updateCharactersRemaining()
2018-09-12 13:19:51 +00:00
progressView.progress = 0
2018-08-31 02:30:19 +00:00
}
override func viewDidLayoutSubviews() {
2018-09-18 01:57:46 +00:00
if inReplyToID != nil && !scrolled {
2018-08-31 02:30:19 +00:00
scrollView.contentOffset = CGPoint(x: 0, y: inReplyToContainerView.bounds.height - 44)
scrolled = true
}
}
func addMedia(for image: UIImage) {
let mediaView = ComposeMediaView(image: image)
2018-08-31 16:39:39 +00:00
mediaView.delegate = self
2018-08-31 02:30:19 +00:00
mediaStackView.addArrangedSubview(mediaView)
}
2018-09-30 02:20:17 +00:00
func updateCharactersRemaining() {
let count = CharacterCounter.count(text: statusTextView.text)
let remaining = (MastodonController.shared.instance.maxStatusCharacters ?? 500) - count
if remaining < 0 {
charactersRemainingLabel.textColor = .red
postButton.isEnabled = false
} else {
charactersRemainingLabel.textColor = .darkGray
postButton.isEnabled = true
}
charactersRemainingLabel.text = remaining.description
}
2018-08-31 02:30:19 +00:00
// 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 }
2018-09-18 01:57:46 +00:00
topVC.selected(status: status.id)
} else if segue.identifier == "cancel" {
xcbSession?.complete(with: .cancel)
2018-08-31 02:30:19 +00:00
}
}
// MARK: - Interaction
@IBAction func visibilityPressed(_ sender: Any) {
let alertController = UIAlertController(title: "Post Visibility", message: nil, preferredStyle: .actionSheet)
2018-09-11 14:52:21 +00:00
for visibility in Status.Visibility.allCases {
2018-08-31 02:30:19 +00:00
let action = UIAlertAction(title: visibility.displayName, style: .default, handler: { _ in
UIView.performWithoutAnimation {
self.visibility = visibility
2018-08-31 02:30:19 +00:00
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 }
2018-09-12 13:19:51 +00:00
postButton.isEnabled = false
2018-08-31 02:30:19 +00:00
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 {
2018-08-31 16:39:39 +00:00
guard let mediaView = view as? ComposeMediaView,
let image = mediaView.image,
2018-08-31 02:30:19 +00:00
let data = image.pngData() else { continue }
let index = attachments.count
attachments.append(nil)
2018-09-12 13:19:51 +00:00
progressView.steps += 1
2018-08-31 02:30:19 +00:00
group.enter()
2018-09-18 00:58:05 +00:00
let request = MastodonController.shared.client.upload(attachment: FormAttachment(pngData: data), description: mediaView.mediaDescription)
MastodonController.shared.client.run(request) { response in
2018-09-11 14:52:21 +00:00
guard case let .success(attachment, _) = response else { fatalError() }
2018-08-31 02:30:19 +00:00
attachments[index] = attachment
2018-09-12 13:19:51 +00:00
self.progressView.step()
2018-08-31 02:30:19 +00:00
group.leave()
}
}
2018-09-12 13:19:51 +00:00
progressView.steps = 2 + attachments.count
progressView.currentStep = 1
2018-08-31 02:30:19 +00:00
group.notify(queue: .main) {
2018-09-11 14:52:21 +00:00
let attachments = attachments.compactMap { $0 }
2018-08-31 02:30:19 +00:00
2018-09-18 00:58:05 +00:00
let request = MastodonController.shared.client.createStatus(text: text,
2018-09-18 01:57:46 +00:00
inReplyTo: self.inReplyToID,
2018-09-18 00:58:05 +00:00
media: attachments,
sensitive: sensitive,
spoilerText: contentWarning,
2018-09-24 01:10:45 +00:00
visibility: visibility)
2018-09-18 00:58:05 +00:00
MastodonController.shared.client.run(request) { response in
2018-09-11 14:52:21 +00:00
guard case let .success(status, _) = response else { fatalError() }
2018-08-31 02:30:19 +00:00
self.status = status
2018-09-18 16:59:07 +00:00
MastodonCache.add(status: status)
2018-08-31 02:30:19 +00:00
DispatchQueue.main.async {
2018-09-12 13:19:51 +00:00
self.progressView.step()
2018-08-31 02:30:19 +00:00
self.performSegue(withIdentifier: "postComplete", sender: self)
2018-09-23 22:43:33 +00:00
self.xcbSession?.complete(with: .success, additionalData: [
"statusURL": status.url?.absoluteString,
2018-09-23 22:43:33 +00:00
"statusURI": status.uri
])
2018-08-31 02:30:19 +00:00
}
}
}
}
@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
}
}
2018-09-30 02:20:17 +00:00
extension ComposeViewController: UITextViewDelegate {
func textViewDidChange(_ textView: UITextView) {
updateCharactersRemaining()
}
}
2018-08-31 02:30:19 +00:00
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)
}
}
}
2018-08-31 16:39:39 +00:00
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)
}
}