A WIP iOS app for Mastodon and Pleroma.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

279 lines
12 KiB

//
// 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)
}
}