forked from shadowfacts/Tusker
669 lines
29 KiB
Swift
669 lines
29 KiB
Swift
//
|
|
// ComposeViewController.swift
|
|
// Tusker
|
|
//
|
|
// Created by Shadowfacts on 8/28/18.
|
|
// Copyright © 2018 Shadowfacts. All rights reserved.
|
|
//
|
|
|
|
import UIKit
|
|
import Pachyderm
|
|
import Intents
|
|
import Photos
|
|
import GMImagePicker
|
|
import MobileCoreServices
|
|
|
|
class ComposeViewController: UIViewController {
|
|
|
|
var inReplyToID: String?
|
|
var accountsToMention: [String]
|
|
var initialText: String?
|
|
var contentWarningEnabled = false {
|
|
didSet {
|
|
contentWarningStateChanged()
|
|
}
|
|
}
|
|
var visibility: Status.Visibility! {
|
|
didSet {
|
|
visibilityChanged()
|
|
}
|
|
}
|
|
var selectedAssets: [PHAsset] = [] {
|
|
didSet {
|
|
updateAttachmentViews()
|
|
}
|
|
}
|
|
|
|
var hasChanges = false
|
|
var currentDraft: DraftsManager.Draft?
|
|
// 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?
|
|
var postedStatus: Status?
|
|
|
|
weak var postBarButtonItem: UIBarButtonItem!
|
|
var visibilityBarButtonItem: UIBarButtonItem!
|
|
var contentWarningBarButtonItem: UIBarButtonItem!
|
|
|
|
@IBOutlet weak var scrollView: UIScrollView!
|
|
@IBOutlet weak var contentView: UIView!
|
|
@IBOutlet weak var stackView: UIStackView!
|
|
|
|
var replyView: ComposeStatusReplyView?
|
|
var replyAvatarImageViewTopConstraint: NSLayoutConstraint?
|
|
|
|
@IBOutlet weak var selfDetailView: LargeAccountDetailView!
|
|
|
|
@IBOutlet weak var charactersRemainingLabel: UILabel!
|
|
@IBOutlet weak var statusTextView: UITextView!
|
|
@IBOutlet weak var placeholderLabel: UILabel!
|
|
|
|
@IBOutlet weak var contentWarningContainerView: UIView!
|
|
@IBOutlet weak var contentWarningTextField: UITextField!
|
|
|
|
@IBOutlet weak var attachmentsStackView: UIStackView!
|
|
@IBOutlet weak var addAttachmentButton: UIButton!
|
|
|
|
@IBOutlet weak var postProgressView: SteppedProgressView!
|
|
|
|
init(inReplyTo inReplyToID: String? = nil, mentioningAcct: String? = nil, text: String? = nil) {
|
|
self.inReplyToID = inReplyToID
|
|
if let inReplyToID = inReplyToID, let inReplyTo = MastodonCache.status(for: inReplyToID) {
|
|
accountsToMention = [inReplyTo.account.acct] + inReplyTo.mentions.map { $0.acct }
|
|
} else if let mentioningAcct = mentioningAcct {
|
|
accountsToMention = [mentioningAcct]
|
|
} else {
|
|
accountsToMention = []
|
|
}
|
|
if let ownAccount = MastodonController.account {
|
|
accountsToMention.removeAll(where: { acct in ownAccount.acct == acct })
|
|
}
|
|
accountsToMention = accountsToMention.uniques()
|
|
|
|
super.init(nibName: "ComposeViewController", bundle: nil)
|
|
|
|
title = "Compose"
|
|
tabBarItem.image = UIImage(systemName: "pencil")
|
|
|
|
navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(showSaveAndClosePrompt))
|
|
navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Post", style: .done, target: self, action: #selector(postButtonPressed))
|
|
postBarButtonItem = navigationItem.rightBarButtonItem
|
|
}
|
|
|
|
required init?(coder aDecoder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
override func viewDidLoad() {
|
|
super.viewDidLoad()
|
|
|
|
scrollView.delegate = self
|
|
|
|
statusTextView.delegate = self
|
|
statusTextView.becomeFirstResponder()
|
|
|
|
let toolbar = UIToolbar()
|
|
contentWarningBarButtonItem = UIBarButtonItem(title: "CW", style: .plain, target: self, action: #selector(contentWarningButtonPressed))
|
|
contentWarningBarButtonItem.accessibilityLabel = NSLocalizedString("Add Content Warning", comment: "add CW accessibility label")
|
|
visibilityBarButtonItem = UIBarButtonItem(image: UIImage(systemName: Preferences.shared.defaultPostVisibility.imageName), style: .plain, target: self, action: #selector(visibilityButtonPressed))
|
|
visibilityBarButtonItem.accessibilityLabel = String(format: NSLocalizedString("Visibility: %s", comment: "compose visiblity accessibility label"), Preferences.shared.defaultPostVisibility.displayName)
|
|
toolbar.items = [
|
|
contentWarningBarButtonItem,
|
|
visibilityBarButtonItem,
|
|
UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
|
|
] + createFormattingButtons() + [
|
|
UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil),
|
|
UIBarButtonItem(title: "Drafts", style: .plain, target: self, action: #selector(draftsButtonPressed))
|
|
]
|
|
toolbar.translatesAutoresizingMaskIntoConstraints = false
|
|
statusTextView.inputAccessoryView = toolbar
|
|
contentWarningTextField.inputAccessoryView = toolbar
|
|
|
|
statusTextView.text = accountsToMention.map({ acct in "@\(acct) " }).joined()
|
|
initialText = statusTextView.text
|
|
|
|
MastodonController.getOwnAccount { (account) in
|
|
DispatchQueue.main.async {
|
|
self.selfDetailView.update(account: account)
|
|
}
|
|
}
|
|
|
|
if let inReplyToID = inReplyToID, let inReplyTo = MastodonCache.status(for: inReplyToID) {
|
|
visibility = inReplyTo.visibility
|
|
if Preferences.shared.contentWarningCopyMode == .doNotCopy {
|
|
contentWarningEnabled = false
|
|
contentWarningContainerView.isHidden = true
|
|
} else {
|
|
contentWarningEnabled = !inReplyTo.spoilerText.isEmpty
|
|
contentWarningContainerView.isHidden = !contentWarningEnabled
|
|
if Preferences.shared.contentWarningCopyMode == .prependRe,
|
|
!inReplyTo.spoilerText.lowercased().starts(with: "re:") {
|
|
contentWarningTextField.text = "re: \(inReplyTo.spoilerText)"
|
|
} else {
|
|
contentWarningTextField.text = inReplyTo.spoilerText
|
|
}
|
|
}
|
|
|
|
let replyView = ComposeStatusReplyView.create()
|
|
replyView.updateUI(for: inReplyTo)
|
|
stackView.insertArrangedSubview(replyView, at: 0)
|
|
self.replyView = replyView
|
|
|
|
replyAvatarImageViewTopConstraint = replyView.avatarImageView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 8)
|
|
replyAvatarImageViewTopConstraint!.isActive = true
|
|
|
|
let replyLabelContainer = UIView()
|
|
replyLabelContainer.backgroundColor = .clear
|
|
replyLabelContainer.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
let replyLabel = UILabel()
|
|
replyLabel.translatesAutoresizingMaskIntoConstraints = false
|
|
replyLabel.text = "In reply to \(inReplyTo.account.realDisplayName)"
|
|
replyLabel.textColor = .secondaryLabel
|
|
replyLabelContainer.addSubview(replyLabel)
|
|
|
|
NSLayoutConstraint.activate([
|
|
replyLabel.leadingAnchor.constraint(equalTo: replyLabelContainer.leadingAnchor, constant: 8),
|
|
replyLabel.trailingAnchor.constraint(equalTo: replyLabelContainer.trailingAnchor, constant: -8),
|
|
replyLabel.topAnchor.constraint(equalTo: replyLabelContainer.topAnchor),
|
|
replyLabel.bottomAnchor.constraint(equalTo: replyLabelContainer.bottomAnchor)
|
|
])
|
|
|
|
stackView.insertArrangedSubview(replyLabelContainer, at: 1)
|
|
}
|
|
|
|
// we have to set the font here, because the monospaced digit font is not available in IB
|
|
charactersRemainingLabel.font = .monospacedDigitSystemFont(ofSize: 17, weight: .regular)
|
|
updateCharactersRemaining()
|
|
updatePlaceholder()
|
|
|
|
NotificationCenter.default.addObserver(self, selector: #selector(contentWarningTextFieldDidChange), name: UITextField.textDidChangeNotification, object: contentWarningTextField)
|
|
|
|
if inReplyToID == nil {
|
|
visibility = Preferences.shared.defaultPostVisibility
|
|
contentWarningEnabled = false
|
|
}
|
|
}
|
|
|
|
override func viewWillAppear(_ animated: Bool) {
|
|
NotificationCenter.default.addObserver(self, selector: #selector(adjustForKeyboard), name: UIResponder.keyboardWillHideNotification, object: nil)
|
|
NotificationCenter.default.addObserver(self, selector: #selector(adjustForKeyboard), name: UIResponder.keyboardWillChangeFrameNotification, object: nil)
|
|
}
|
|
|
|
override func viewWillDisappear(_ animated: Bool) {
|
|
super.viewWillDisappear(animated)
|
|
|
|
NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillHideNotification, object: nil)
|
|
NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillChangeFrameNotification, object: nil)
|
|
}
|
|
|
|
override func viewDidLayoutSubviews() {
|
|
super.viewDidLayoutSubviews()
|
|
|
|
// if inReplyToID != nil {
|
|
// scrollView.contentOffset = CGPoint(x: 0, y: stackView.arrangedSubviews.first!.frame.height)
|
|
// }
|
|
}
|
|
|
|
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
|
super.traitCollectionDidChange(previousTraitCollection)
|
|
|
|
let imageName: String
|
|
if traitCollection.userInterfaceStyle == .dark {
|
|
imageName = "photo.fill"
|
|
} else {
|
|
imageName = "photo"
|
|
}
|
|
addAttachmentButton.setImage(UIImage(systemName: imageName), for: .normal)
|
|
}
|
|
|
|
func createFormattingButtons() -> [UIBarButtonItem] {
|
|
guard Preferences.shared.statusContentType != .plain else {
|
|
return []
|
|
}
|
|
|
|
return StatusFormat.allCases.map { (format) in
|
|
let item: UIBarButtonItem
|
|
if let image = format.image {
|
|
item = UIBarButtonItem(image: image, style: .plain, target: self, action: #selector(formatButtonPressed(_:)))
|
|
} else if let (str, attributes) = format.title {
|
|
item = UIBarButtonItem(title: str, style: .plain, target: self, action: #selector(formatButtonPressed(_:)))
|
|
item.setTitleTextAttributes(attributes, for: .normal)
|
|
item.setTitleTextAttributes(attributes, for: .highlighted)
|
|
} else {
|
|
fatalError("StatusFormat must have either an image or a title")
|
|
}
|
|
item.tag = StatusFormat.allCases.firstIndex(of: format)!
|
|
item.accessibilityLabel = format.accessibilityLabel
|
|
return item
|
|
}
|
|
}
|
|
|
|
@objc func adjustForKeyboard(notification: NSNotification) {
|
|
let userInfo = notification.userInfo!
|
|
|
|
let keyboardScreenEndFrame = (userInfo[UIResponder.keyboardFrameEndUserInfoKey] as! NSValue).cgRectValue
|
|
let keyboardViewEndFrame = view.convert(keyboardScreenEndFrame, from: view.window)
|
|
|
|
if notification.name == UIResponder.keyboardWillHideNotification {
|
|
scrollView.contentInset = .zero
|
|
} else {
|
|
// let accessoryFrame = view.convert(statusTextView.inputAccessoryView!.frame, from: view.window)
|
|
let offset = keyboardViewEndFrame.height// + accessoryFrame.height
|
|
// TODO: radar for incorrect keyboard end frame height (either converted or screen)
|
|
// the value returned is somewhere between the height of the keyboard and the height of the keyboard + accessory
|
|
// actually maybe not??
|
|
scrollView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: offset, right: 0)
|
|
}
|
|
scrollView.scrollIndicatorInsets = scrollView.contentInset
|
|
}
|
|
|
|
func updateCharactersRemaining() {
|
|
// TODO: include CW char count
|
|
let count = CharacterCounter.count(text: statusTextView.text)
|
|
let cwCount = contentWarningEnabled ? (contentWarningTextField.text?.count ?? 0) : 0
|
|
let remaining = (MastodonController.instance.maxStatusCharacters ?? 500) - count - cwCount
|
|
if remaining < 0 {
|
|
charactersRemainingLabel.textColor = .red
|
|
postBarButtonItem.isEnabled = false
|
|
} else {
|
|
charactersRemainingLabel.textColor = .darkGray
|
|
postBarButtonItem.isEnabled = true
|
|
}
|
|
charactersRemainingLabel.text = String(remaining)
|
|
charactersRemainingLabel.accessibilityLabel = String(format: NSLocalizedString("%d characters remaining", comment: "compose characters remaining accessibility label"), remaining)
|
|
}
|
|
|
|
func updateHasChanges() {
|
|
if let currentDraft = currentDraft {
|
|
let cw = contentWarningEnabled ? contentWarningTextField.text : nil
|
|
hasChanges = statusTextView.text != currentDraft.text || cw != currentDraft.contentWarning
|
|
} else {
|
|
hasChanges = !statusTextView.text.isEmpty || (contentWarningEnabled && !(contentWarningTextField.text?.isEmpty ?? true))
|
|
}
|
|
}
|
|
|
|
func updatePlaceholder() {
|
|
placeholderLabel.isHidden = !statusTextView.text.isEmpty
|
|
}
|
|
|
|
func updateAddAttachmentButton() {
|
|
switch MastodonController.instance.instanceType {
|
|
case .pleroma:
|
|
addAttachmentButton.isEnabled = true
|
|
case .mastodon:
|
|
addAttachmentButton.isEnabled = selectedAssets.count <= 4 && selectedAssets.first(where: { $0.mediaType == .video }) == nil
|
|
}
|
|
}
|
|
|
|
func updateAttachmentViews() {
|
|
for view in attachmentsStackView.arrangedSubviews {
|
|
if view is ComposeMediaView {
|
|
view.removeFromSuperview()
|
|
}
|
|
}
|
|
|
|
for asset in selectedAssets {
|
|
let mediaView = ComposeMediaView.create()
|
|
mediaView.delegate = self
|
|
mediaView.update(asset: asset)
|
|
attachmentsStackView.insertArrangedSubview(mediaView, at: attachmentsStackView.arrangedSubviews.count - 1)
|
|
updateAddAttachmentButton()
|
|
}
|
|
}
|
|
|
|
func contentWarningStateChanged() {
|
|
contentWarningContainerView.isHidden = !contentWarningEnabled
|
|
if contentWarningEnabled {
|
|
contentWarningBarButtonItem.accessibilityLabel = NSLocalizedString("Remove Content Warning", comment: "remove CW accessibility label")
|
|
} else {
|
|
contentWarningBarButtonItem.accessibilityLabel = NSLocalizedString("Add Content Warning", comment: "add CW accessibility label")
|
|
}
|
|
}
|
|
|
|
func visibilityChanged() {
|
|
visibilityBarButtonItem.image = UIImage(systemName: visibility.imageName)
|
|
visibilityBarButtonItem.accessibilityLabel = String(format: NSLocalizedString("Visibility: %s", comment: "compose visiblity accessibility label"), Preferences.shared.defaultPostVisibility.displayName)
|
|
}
|
|
|
|
func saveDraft() {
|
|
var attachments = [DraftsManager.DraftAttachment]()
|
|
for asset in selectedAssets {
|
|
let index = attachments.count
|
|
let mediaView = attachmentsStackView.arrangedSubviews[index] as! ComposeMediaView
|
|
let description = mediaView.descriptionTextView.text!
|
|
|
|
attachments.append(DraftsManager.DraftAttachment(assetIdentifier: asset.localIdentifier, description: description))
|
|
}
|
|
let cw = contentWarningEnabled ? contentWarningTextField.text : nil
|
|
if let currentDraft = self.currentDraft {
|
|
currentDraft.update(text: self.statusTextView.text, contentWarning: cw, attachments: attachments)
|
|
} else {
|
|
DraftsManager.shared.create(text: self.statusTextView.text, contentWarning: cw, attachments: attachments)
|
|
}
|
|
}
|
|
|
|
@objc func close() {
|
|
dismiss(animated: true)
|
|
xcbSession?.complete(with: .cancel)
|
|
}
|
|
|
|
// MARK: - Navigation
|
|
|
|
override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
|
|
statusTextView.resignFirstResponder()
|
|
super.dismiss(animated: flag, completion: completion)
|
|
}
|
|
|
|
// MARK: - Interaction
|
|
|
|
@objc func showSaveAndClosePrompt() {
|
|
guard statusTextView.text.trimmingCharacters(in: .whitespacesAndNewlines) != initialText else {
|
|
close()
|
|
return
|
|
}
|
|
|
|
if Preferences.shared.automaticallySaveDrafts {
|
|
saveDraft()
|
|
close()
|
|
return
|
|
}
|
|
|
|
let alert = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
|
|
alert.addAction(UIAlertAction(title: "Save draft", style: .default, handler: { (_) in
|
|
self.saveDraft()
|
|
self.close()
|
|
}))
|
|
alert.addAction(UIAlertAction(title: "Delete draft", style: .destructive, handler: { (_) in
|
|
if let currentDraft = self.currentDraft {
|
|
DraftsManager.shared.remove(currentDraft)
|
|
}
|
|
self.close()
|
|
}))
|
|
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
|
|
present(alert, animated: true)
|
|
}
|
|
|
|
@objc func contentWarningButtonPressed() {
|
|
contentWarningEnabled = !contentWarningEnabled
|
|
if contentWarningEnabled {
|
|
contentWarningTextField.becomeFirstResponder()
|
|
} else {
|
|
statusTextView.becomeFirstResponder()
|
|
}
|
|
}
|
|
|
|
@objc func contentWarningTextFieldDidChange() {
|
|
updateCharactersRemaining()
|
|
updateHasChanges()
|
|
}
|
|
|
|
@objc func visibilityButtonPressed() {
|
|
let alertController = UIAlertController(currentVisibility: self.visibility) { (visibility) in
|
|
guard let visibility = visibility else { return }
|
|
self.visibility = visibility
|
|
}
|
|
present(alertController, animated: true)
|
|
}
|
|
|
|
@objc func formatButtonPressed(_ button: UIBarButtonItem) {
|
|
guard statusTextView.isFirstResponder else {
|
|
return
|
|
}
|
|
|
|
let format = StatusFormat.allCases[button.tag]
|
|
guard let insertionResult = format.insertionResult else {
|
|
return
|
|
}
|
|
|
|
let currentSelectedRange = statusTextView.selectedRange
|
|
if currentSelectedRange.length == 0 {
|
|
statusTextView.insertText(insertionResult.prefix + insertionResult.suffix)
|
|
statusTextView.selectedRange = NSRange(location: currentSelectedRange.location + insertionResult.insertionPoint, length: 0)
|
|
} else {
|
|
let start = statusTextView.text.index(statusTextView.text.startIndex, offsetBy: currentSelectedRange.lowerBound)
|
|
let end = statusTextView.text.index(statusTextView.text.startIndex, offsetBy: currentSelectedRange.upperBound)
|
|
let selectedText = statusTextView.text[start..<end]
|
|
statusTextView.insertText(String(insertionResult.prefix + selectedText + insertionResult.suffix))
|
|
statusTextView.selectedRange = NSRange(location: currentSelectedRange.location + insertionResult.prefix.count, length: currentSelectedRange.length)
|
|
}
|
|
}
|
|
|
|
@objc func draftsButtonPressed() {
|
|
let draftsVC = DraftsTableViewController()
|
|
draftsVC.delegate = self
|
|
present(UINavigationController(rootViewController: draftsVC), animated: true)
|
|
}
|
|
|
|
@IBAction func addAttachmentPressed(_ sender: Any) {
|
|
let picker = GMImagePickerController()
|
|
picker.delegate = self
|
|
picker.toolbarTintColor = view.tintColor
|
|
picker.navigationBarTintColor = view.tintColor
|
|
picker.title = "Choose Attachment"
|
|
present(picker, animated: true)
|
|
}
|
|
|
|
@objc func postButtonPressed() {
|
|
guard let text = statusTextView.text,
|
|
!text.isEmpty else { return }
|
|
|
|
// save a draft before posting the status, so if a crash occurs during posting, the status won't be lost
|
|
saveDraft()
|
|
|
|
// disable post button while sending post request
|
|
postBarButtonItem.isEnabled = false
|
|
|
|
let contentWarning: String?
|
|
if contentWarningEnabled, let cwText = contentWarningTextField.text, !cwText.isEmpty {
|
|
contentWarning = cwText
|
|
} else {
|
|
contentWarning = nil
|
|
}
|
|
let sensitive = contentWarning != nil
|
|
let visibility = self.visibility!
|
|
|
|
let group = DispatchGroup()
|
|
|
|
var attachments: [Attachment?] = []
|
|
for asset in selectedAssets {
|
|
let index = attachments.count
|
|
attachments.append(nil)
|
|
|
|
let mediaView = attachmentsStackView.arrangedSubviews[index] as! ComposeMediaView
|
|
let description = mediaView.descriptionTextView.text
|
|
|
|
group.enter()
|
|
|
|
let options = PHImageRequestOptions()
|
|
options.version = .current
|
|
options.deliveryMode = .highQualityFormat
|
|
options.resizeMode = .none
|
|
options.isNetworkAccessAllowed = true
|
|
PHImageManager.default().requestImageDataAndOrientation(for: asset, options: options) { (data, dataUTI, orientation, info) in
|
|
guard let data = data, let dataUTI = dataUTI else { fatalError() }
|
|
|
|
let mimeType = UTTypeCopyPreferredTagWithClass(dataUTI as CFString, kUTTagClassMIMEType)!.takeRetainedValue() as String
|
|
|
|
self.postProgressView.step()
|
|
|
|
let request = MastodonController.client.upload(attachment: FormAttachment(mimeType: mimeType, data: data, fileName: "file"), description: description)
|
|
MastodonController.client.run(request) { (response) in
|
|
guard case let .success(attachment, _) = response else { fatalError() }
|
|
|
|
attachments[index] = attachment
|
|
|
|
self.postProgressView.step()
|
|
|
|
group.leave()
|
|
}
|
|
}
|
|
}
|
|
|
|
postProgressView.steps = 2 + (attachments.count * 2) // 2 steps (request data, then upload) for each attachment
|
|
postProgressView.currentStep = 1
|
|
|
|
group.notify(queue: .main) {
|
|
let attachments = attachments.compactMap { $0 }
|
|
|
|
let request = MastodonController.client.createStatus(text: text,
|
|
contentType: Preferences.shared.statusContentType,
|
|
inReplyTo: self.inReplyToID,
|
|
media: attachments,
|
|
sensitive: sensitive,
|
|
spoilerText: contentWarning,
|
|
visibility: visibility,
|
|
language: nil)
|
|
MastodonController.client.run(request) { (response) in
|
|
guard case let .success(status, _) = response else { fatalError() }
|
|
self.postedStatus = status
|
|
MastodonCache.add(status: status)
|
|
|
|
if let draft = self.currentDraft {
|
|
DraftsManager.shared.remove(draft)
|
|
}
|
|
|
|
DispatchQueue.main.async {
|
|
self.postProgressView.step()
|
|
self.dismiss(animated: true)
|
|
|
|
let conversationVC = ConversationTableViewController(for: status.id)
|
|
self.show(conversationVC, sender: self)
|
|
|
|
self.xcbSession?.complete(with: .success, additionalData: [
|
|
"statusURL": status.url?.absoluteString,
|
|
"statusURI": status.uri
|
|
])
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
extension ComposeViewController: UIScrollViewDelegate {
|
|
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
|
guard let replyView = replyView else { return }
|
|
|
|
var constant: CGFloat = 8
|
|
|
|
if scrollView.contentOffset.y < 0 {
|
|
constant -= scrollView.contentOffset.y
|
|
replyAvatarImageViewTopConstraint?.constant = 8 - scrollView.contentOffset.y
|
|
} else if scrollView.contentOffset.y > replyView.frame.height - replyView.avatarImageView.frame.height - 16 {
|
|
constant += replyView.frame.height - replyView.avatarImageView.frame.height - 16 - scrollView.contentOffset.y
|
|
}
|
|
|
|
replyAvatarImageViewTopConstraint?.constant = constant
|
|
}
|
|
}
|
|
|
|
extension ComposeViewController: UITextViewDelegate {
|
|
func textViewDidChange(_ textView: UITextView) {
|
|
updateCharactersRemaining()
|
|
updatePlaceholder()
|
|
updateHasChanges()
|
|
}
|
|
}
|
|
|
|
extension ComposeViewController: GMImagePickerControllerDelegate {
|
|
func assetsPickerController(_ picker: GMImagePickerController!, didFinishPickingAssets assets: [Any]!) {
|
|
let assets = assets as! [PHAsset]
|
|
selectedAssets.append(contentsOf: assets)
|
|
picker.dismiss(animated: true)
|
|
}
|
|
|
|
func assetsPickerController(_ picker: GMImagePickerController!, shouldSelect asset: PHAsset!) -> Bool {
|
|
switch MastodonController.instance.instanceType {
|
|
case .pleroma:
|
|
return true
|
|
case .mastodon:
|
|
if (asset.mediaType == .video && selectedAssets.count > 0) || selectedAssets.first(where: { $0.mediaType == .video }) != nil {
|
|
return false
|
|
}
|
|
return selectedAssets.count + picker.selectedAssets.count < 4
|
|
}
|
|
}
|
|
}
|
|
|
|
extension ComposeViewController: ComposeMediaViewDelegate {
|
|
func didRemoveMedia(_ mediaView: ComposeMediaView) {
|
|
let index = attachmentsStackView.arrangedSubviews.firstIndex(of: mediaView)!
|
|
selectedAssets.remove(at: index)
|
|
updateAddAttachmentButton()
|
|
}
|
|
}
|
|
|
|
extension ComposeViewController: DraftsTableViewControllerDelegate {
|
|
func draftSelectionCanceled() {
|
|
}
|
|
|
|
func draftSelected(_ draft: DraftsManager.Draft) {
|
|
self.currentDraft = draft
|
|
|
|
statusTextView.text = draft.text
|
|
contentWarningEnabled = draft.contentWarning != nil
|
|
contentWarningTextField.text = draft.contentWarning
|
|
|
|
updatePlaceholder()
|
|
updateCharactersRemaining()
|
|
|
|
let result = PHAsset.fetchAssets(withLocalIdentifiers: draft.attachments.map { $0.assetIdentifier }, options: nil)
|
|
var assets = [String: (asset: PHAsset, description: String)]()
|
|
var addedAssets = 0
|
|
while addedAssets < result.count {
|
|
let asset = result[addedAssets]
|
|
let attachment = draft.attachments.first(where: { $0.assetIdentifier == asset.localIdentifier })!
|
|
assets[asset.localIdentifier] = (asset, attachment.description)
|
|
addedAssets += 1
|
|
}
|
|
|
|
self.selectedAssets = assets.values.map { $0.asset }
|
|
updateAttachmentViews()
|
|
|
|
for case let mediaView as ComposeMediaView in attachmentsStackView.arrangedSubviews {
|
|
let attachment = draft.attachments.first(where: { $0.assetIdentifier == mediaView.assetIdentifier })!
|
|
mediaView.descriptionTextView.text = attachment.description
|
|
|
|
// call the delegate method manually, since setting the text property doesn't call it
|
|
mediaView.textViewDidChange(mediaView.descriptionTextView)
|
|
}
|
|
}
|
|
|
|
func draftSelectionCompleted() {
|
|
// check that all the assets from the draft have been added
|
|
if let currentDraft = currentDraft, selectedAssets.count < currentDraft.attachments.count {
|
|
// some of the assets in the draft weren't loaded, so notify the user
|
|
|
|
let difference = currentDraft.attachments.count - selectedAssets.count
|
|
// todo: localize me
|
|
let suffix = difference == 1 ? "" : "s"
|
|
let verb = difference == 1 ? "was" : "were"
|
|
let alertController = UIAlertController(title: "Missing Attachments", message: "\(difference) attachment\(suffix) \(verb) removed from the Photos Library and could not be loaded.", preferredStyle: .alert)
|
|
alertController.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
|
|
|
|
present(alertController, animated: true)
|
|
}
|
|
}
|
|
}
|
|
|
|
extension ComposeViewController: UIAdaptivePresentationControllerDelegate {
|
|
func presentationControllerShouldDismiss(_ presentationController: UIPresentationController) -> Bool {
|
|
return Preferences.shared.automaticallySaveDrafts || !hasChanges
|
|
}
|
|
|
|
func presentationControllerDidAttemptToDismiss(_ presentationController: UIPresentationController) {
|
|
showSaveAndClosePrompt()
|
|
}
|
|
|
|
// when the compose screen is dismissed interactively, close() isn't called, so we make sure to
|
|
// complete the X-Callback-URL session and save the draft is automatic saving is enabled
|
|
// (if automatic saving is off, the draft will get saved/discarded by the user when didAttemptToDismiss is called
|
|
func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
|
|
if Preferences.shared.automaticallySaveDrafts {
|
|
saveDraft()
|
|
}
|
|
xcbSession?.complete(with: .cancel)
|
|
}
|
|
}
|
|
|