Tusker/Tusker/Screens/Compose/ComposeViewController.swift

706 lines
30 KiB
Swift

//
// ComposeViewController.swift
// Tusker
//
// Created by Shadowfacts on 8/28/18.
// Copyright © 2018 Shadowfacts. All rights reserved.
//
import UIKit
import Pachyderm
import Intents
class ComposeViewController: UIViewController {
weak var mastodonController: MastodonController!
var inReplyToID: String?
var accountsToMention = [String]()
var initialText: String?
var contentWarningEnabled = false {
didSet {
contentWarningStateChanged()
}
}
var visibility: Status.Visibility! {
didSet {
visibilityChanged()
}
}
var selectedAttachments: [CompositionAttachment] = [] {
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?
var compositionState: CompositionState = .valid {
didSet {
postBarButtonItem.isEnabled = compositionState.isValid
}
}
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 inReplyToContainer: UIView!
@IBOutlet weak var inReplyToLabel: 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, mastodonController: MastodonController) {
self.mastodonController = mastodonController
self.inReplyToID = inReplyToID
if let inReplyToID = inReplyToID, let inReplyTo = mastodonController.cache.status(for: inReplyToID) {
accountsToMention = [inReplyTo.account.acct] + inReplyTo.mentions.map { $0.acct }
} else {
accountsToMention = []
}
if let mentioningAcct = mentioningAcct {
accountsToMention.append(mentioningAcct)
}
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: %@", 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)
}
}
updateInReplyTo()
// 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()
updateAttachmentDescriptionsRequired()
updatePlaceholder()
NotificationCenter.default.addObserver(self, selector: #selector(contentWarningTextFieldDidChange), name: UITextField.textDidChangeNotification, object: contentWarningTextField)
}
func updateInReplyTo() {
if let replyView = replyView {
replyView.removeFromSuperview()
}
if let inReplyToID = inReplyToID {
if let status = mastodonController.cache.status(for: inReplyToID) {
updateInReplyTo(inReplyTo: status)
} else {
let loadingVC = LoadingViewController()
embedChild(loadingVC)
mastodonController.cache.status(for: inReplyToID) { (status) in
guard let status = status else { return }
DispatchQueue.main.async {
self.updateInReplyTo(inReplyTo: status)
loadingVC.removeViewAndController()
}
}
}
} else {
visibility = Preferences.shared.defaultPostVisibility
contentWarningEnabled = false
inReplyToContainer.isHidden = true
}
}
func updateInReplyTo(inReplyTo: Status) {
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.mastodonController = mastodonController
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
inReplyToContainer.isHidden = false
inReplyToLabel.text = "In reply to \(inReplyTo.account.realDisplayName)"
}
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 updateAttachmentDescriptionsRequired() {
if Preferences.shared.requireAttachmentDescriptions {
for case let mediaView as ComposeMediaView in attachmentsStackView.arrangedSubviews {
if mediaView.descriptionTextView.text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
compositionState.formUnion(.requiresAttachmentDescriptions)
return
}
}
}
compositionState.subtract(.requiresAttachmentDescriptions)
}
func updateCharactersRemaining() {
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
compositionState.formUnion(.tooManyCharacters)
} else {
charactersRemainingLabel.textColor = .darkGray
compositionState.subtract(.tooManyCharacters)
}
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 = selectedAttachments.count <= 4 && !selectedAttachments.contains(where: { $0.type == .video })
}
}
func updateAttachmentViews() {
for view in attachmentsStackView.arrangedSubviews {
if view is ComposeMediaView {
view.removeFromSuperview()
}
}
for attachment in selectedAttachments {
let mediaView = ComposeMediaView.create()
mediaView.delegate = self
mediaView.update(attachment: attachment)
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: %@", comment: "compose visiblity accessibility label"), Preferences.shared.defaultPostVisibility.displayName)
}
func saveDraft() {
var attachments = [DraftsManager.DraftAttachment]()
for case let mediaView as ComposeMediaView in attachmentsStackView.arrangedSubviews
where mediaView.attachment.canSaveToDraft {
let attachment = mediaView.attachment!
let description = mediaView.descriptionTextView.text ?? ""
attachments.append(.init(attachment: attachment, description: description))
}
let cw = contentWarningEnabled ? contentWarningTextField.text : nil
let account = mastodonController.accountInfo!
if let currentDraft = self.currentDraft {
currentDraft.update(accountID: account.id, text: self.statusTextView.text, contentWarning: cw, attachments: attachments)
} else {
self.currentDraft = DraftsManager.shared.create(accountID: account.id, text: self.statusTextView.text, contentWarning: cw, inReplyToID: inReplyToID, attachments: attachments)
}
DraftsManager.save()
}
@objc func close() {
dismiss(animated: true)
xcbSession?.complete(with: .cancel)
}
// 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(account: mastodonController.accountInfo!)
draftsVC.delegate = self
present(UINavigationController(rootViewController: draftsVC), animated: true)
}
@IBAction func addAttachmentPressed(_ sender: Any) {
// hide keyboard before showing asset picker, so it doesn't re-appear when asset picker is closed
contentWarningTextField.resignFirstResponder()
statusTextView.resignFirstResponder()
let sheetContainer = AssetPickerSheetContainerViewController()
sheetContainer.assetPicker.assetPickerDelegate = self
present(sheetContainer, 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
compositionState.formUnion(.currentlyPosting)
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 compAttachment in selectedAttachments {
let index = attachments.count
attachments.append(nil)
let mediaView = attachmentsStackView.arrangedSubviews[index] as! ComposeMediaView
let description = mediaView.descriptionTextView.text
group.enter()
compAttachment.getData { (data, mimeType) in
self.postProgressView.step()
let request = Client.upload(attachment: FormAttachment(mimeType: mimeType, data: data, fileName: "file"), description: description)
self.mastodonController.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 = Client.createStatus(text: text,
contentType: Preferences.shared.statusContentType,
inReplyTo: self.inReplyToID,
media: attachments,
sensitive: sensitive,
spoilerText: contentWarning,
visibility: visibility,
language: nil)
self.mastodonController.run(request) { (response) in
guard case let .success(status, _) = response else { fatalError() }
self.postedStatus = status
self.mastodonController.cache.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, mastodonController: self.mastodonController)
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: AssetPickerViewControllerDelegate {
func assetPicker(_ assetPicker: AssetPickerViewController, shouldAllowAssetOfType type: CompositionAttachment.AttachmentType) -> Bool {
switch mastodonController.instance.instanceType {
case .pleroma:
return true
case .mastodon:
if (type == .video && selectedAttachments.count > 0) ||
selectedAttachments.contains(where: { $0.type == .video }) ||
assetPicker.currentCollectionSelectedAssets.contains(where: { $0.type == .video }) {
return false
}
return selectedAttachments.count + assetPicker.currentCollectionSelectedAssets.count < 4
}
}
func assetPicker(_ assetPicker: AssetPickerViewController, didSelectAttachments attachments: [CompositionAttachment]) {
selectedAttachments.append(contentsOf: attachments)
updateAttachmentDescriptionsRequired()
}
}
extension ComposeViewController: ComposeMediaViewDelegate {
func didRemoveMedia(_ mediaView: ComposeMediaView) {
let index = attachmentsStackView.arrangedSubviews.firstIndex(of: mediaView)!
selectedAttachments.remove(at: index)
updateAddAttachmentButton()
updateAttachmentDescriptionsRequired()
}
func descriptionTextViewDidChange(_ mediaView: ComposeMediaView) {
updateAttachmentDescriptionsRequired()
}
}
extension ComposeViewController: DraftsTableViewControllerDelegate {
func draftSelectionCanceled() {
}
func shouldSelectDraft(_ draft: DraftsManager.Draft, completion: @escaping (Bool) -> Void) {
if draft.inReplyToID != self.inReplyToID {
let alertController = UIAlertController(title: "Different Reply", message: "The selected draft is a reply to a different status, do you wish to use it?", preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: { (_) in
completion(false)
}))
alertController.addAction(UIAlertAction(title: "Restore Draft", style: .default, handler: { (_) in
completion(true)
}))
// we can't present the alert ourselves, since the compose VC is already presenting the draft selector
// but presenting on the presented view controller seems hacky, is there a better way to do this?
presentedViewController!.present(alertController, animated: true)
} else {
completion(true)
}
}
func draftSelected(_ draft: DraftsManager.Draft) {
self.currentDraft = draft
inReplyToID = draft.inReplyToID
updateInReplyTo()
statusTextView.text = draft.text
contentWarningEnabled = draft.contentWarning != nil
contentWarningTextField.text = draft.contentWarning
updatePlaceholder()
updateCharactersRemaining()
selectedAttachments = draft.attachments.map { $0.attachment }
updateAttachmentViews()
for case let mediaView as ComposeMediaView in attachmentsStackView.arrangedSubviews {
let attachment = draft.attachments.first(where: { $0.attachment == mediaView.attachment })!
mediaView.descriptionTextView.text = attachment.description
// call the delegate method manually, since setting the text property doesn't call it
mediaView.textViewDidChange(mediaView.descriptionTextView)
}
updateAttachmentDescriptionsRequired()
}
func draftSelectionCompleted() {
// check that all the assets from the draft have been added
if let currentDraft = currentDraft, selectedAttachments.count < currentDraft.attachments.count {
// some of the assets in the draft weren't loaded, so notify the user
let difference = currentDraft.attachments.count - selectedAttachments.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)
}
}