2018-08-31 02:30:19 +00:00
//
// C o m p o s e V i e w C o n t r o l l e r . s w i f t
// T u s k e r
//
// C r e a t e d b y S h a d o w f a c t s o n 8 / 2 8 / 1 8 .
// C o p y r i g h t © 2 0 1 8 S h a d o w f a c t s . A l l r i g h t s r e s e r v e d .
//
import UIKit
2018-09-11 14:52:21 +00:00
import Pachyderm
2018-10-20 14:54:59 +00:00
import Intents
2018-08-31 02:30:19 +00:00
class ComposeViewController : UIViewController {
2020-01-23 03:27:58 +00:00
weak var mastodonController : MastodonController !
2020-01-05 20:25:07 +00:00
2018-09-18 01:57:46 +00:00
var inReplyToID : String ?
2020-01-20 04:48:36 +00:00
var accountsToMention = [ String ] ( )
2018-10-23 02:09:11 +00:00
var initialText : String ?
2019-01-15 02:59:42 +00:00
var contentWarningEnabled = false {
2018-09-02 22:11:00 +00:00
didSet {
2019-01-15 02:59:42 +00:00
contentWarningStateChanged ( )
2018-09-02 22:11:00 +00:00
}
}
2019-01-15 02:59:42 +00:00
var visibility : Status . Visibility ! {
2018-09-02 22:11:00 +00:00
didSet {
2019-01-15 02:59:42 +00:00
visibilityChanged ( )
2018-09-02 22:11:00 +00:00
}
}
2020-01-04 21:25:15 +00:00
var selectedAttachments : [ CompositionAttachment ] = [ ] {
2019-01-15 02:59:42 +00:00
didSet {
updateAttachmentViews ( )
}
}
2019-06-13 19:06:19 +00:00
var hasChanges = false
2019-01-15 02:59:42 +00:00
var currentDraft : DraftsManager . Draft ?
// W e a k s o t h a t i f a n e w s e s s i o n i s i n i t i a t e d ( i . e . X C B M a n a g e r . c u r r e n t S e s s i o n i s c h a n g e d ) w h i l e t h e c u r r e n t o n e i s i n p r o g r e s s , t h i s o n e w i l l b e r e l e a s e d
weak var xcbSession : XCBSession ?
var postedStatus : Status ?
2020-01-18 02:55:21 +00:00
var compositionState : CompositionState = . valid {
didSet {
postBarButtonItem . isEnabled = compositionState . isValid
}
}
2019-01-15 02:59:42 +00:00
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 !
2018-08-31 02:30:19 +00:00
2019-12-14 16:30:35 +00:00
@IBOutlet weak var inReplyToContainer : UIView !
@IBOutlet weak var inReplyToLabel : UILabel !
2019-01-15 02:59:42 +00:00
@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 !
2018-08-31 02:30:19 +00:00
2020-01-05 20:25:07 +00:00
init ( inReplyTo inReplyToID : String ? = nil , mentioningAcct : String ? = nil , text : String ? = nil , mastodonController : MastodonController ) {
self . mastodonController = mastodonController
2019-01-15 02:59:42 +00:00
self . inReplyToID = inReplyToID
2020-01-06 00:54:28 +00:00
if let inReplyToID = inReplyToID , let inReplyTo = mastodonController . cache . status ( for : inReplyToID ) {
2019-01-15 02:59:42 +00:00
accountsToMention = [ inReplyTo . account . acct ] + inReplyTo . mentions . map { $0 . acct }
} else {
accountsToMention = [ ]
}
2020-01-20 04:48:36 +00:00
if let mentioningAcct = mentioningAcct {
accountsToMention . append ( mentioningAcct )
}
2020-01-05 20:25:07 +00:00
if let ownAccount = mastodonController . account {
2019-01-15 02:59:42 +00:00
accountsToMention . removeAll ( where : { acct in ownAccount . acct = = acct } )
}
accountsToMention = accountsToMention . uniques ( )
2018-10-21 02:07:04 +00:00
super . init ( nibName : " ComposeViewController " , bundle : nil )
2019-01-05 17:59:55 +00:00
title = " Compose "
2019-06-11 17:21:22 +00:00
tabBarItem . image = UIImage ( systemName : " pencil " )
2019-01-05 17:59:55 +00:00
2019-06-13 19:06:19 +00:00
navigationItem . leftBarButtonItem = UIBarButtonItem ( barButtonSystemItem : . cancel , target : self , action : #selector ( showSaveAndClosePrompt ) )
2019-01-15 02:59:42 +00:00
navigationItem . rightBarButtonItem = UIBarButtonItem ( title : " Post " , style : . done , target : self , action : #selector ( postButtonPressed ) )
postBarButtonItem = navigationItem . rightBarButtonItem
2018-10-20 16:03:18 +00:00
}
required init ? ( coder aDecoder : NSCoder ) {
fatalError ( " init(coder:) has not been implemented " )
}
2018-08-31 02:30:19 +00:00
override func viewDidLoad ( ) {
super . viewDidLoad ( )
2019-01-15 02:59:42 +00:00
scrollView . delegate = self
2018-09-30 02:20:17 +00:00
statusTextView . delegate = self
2019-01-15 02:59:42 +00:00
statusTextView . becomeFirstResponder ( )
2018-08-31 02:30:19 +00:00
2019-01-15 02:59:42 +00:00
let toolbar = UIToolbar ( )
contentWarningBarButtonItem = UIBarButtonItem ( title : " CW " , style : . plain , target : self , action : #selector ( contentWarningButtonPressed ) )
2019-09-27 00:53:22 +00:00
contentWarningBarButtonItem . accessibilityLabel = NSLocalizedString ( " Add Content Warning " , comment : " add CW accessibility label " )
2019-06-14 00:53:17 +00:00
visibilityBarButtonItem = UIBarButtonItem ( image : UIImage ( systemName : Preferences . shared . defaultPostVisibility . imageName ) , style : . plain , target : self , action : #selector ( visibilityButtonPressed ) )
2019-09-28 04:37:43 +00:00
visibilityBarButtonItem . accessibilityLabel = String ( format : NSLocalizedString ( " Visibility: %@ " , comment : " compose visiblity accessibility label " ) , Preferences . shared . defaultPostVisibility . displayName )
2019-01-15 02:59:42 +00:00
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
2018-08-31 02:30:19 +00:00
statusTextView . inputAccessoryView = toolbar
2019-01-15 02:59:42 +00:00
contentWarningTextField . inputAccessoryView = toolbar
2018-08-31 02:30:19 +00:00
2019-01-15 02:59:42 +00:00
statusTextView . text = accountsToMention . map ( { acct in " @ \( acct ) " } ) . joined ( )
initialText = statusTextView . text
2020-01-05 20:25:07 +00:00
mastodonController . getOwnAccount { ( account ) in
2019-01-15 02:59:42 +00:00
DispatchQueue . main . async {
self . selfDetailView . update ( account : account )
2018-08-31 02:30:19 +00:00
}
}
2019-12-14 16:30:35 +00:00
updateInReplyTo ( )
2018-09-12 13:19:51 +00:00
2019-07-27 22:27:47 +00:00
// w e h a v e t o s e t t h e f o n t h e r e , b e c a u s e t h e m o n o s p a c e d d i g i t f o n t i s n o t a v a i l a b l e i n I B
charactersRemainingLabel . font = . monospacedDigitSystemFont ( ofSize : 17 , weight : . regular )
2018-09-30 02:20:17 +00:00
updateCharactersRemaining ( )
2020-01-18 02:55:21 +00:00
updateAttachmentDescriptionsRequired ( )
2018-09-30 02:28:17 +00:00
updatePlaceholder ( )
2018-09-30 02:20:17 +00:00
2019-01-15 02:59:42 +00:00
NotificationCenter . default . addObserver ( self , selector : #selector ( contentWarningTextFieldDidChange ) , name : UITextField . textDidChangeNotification , object : contentWarningTextField )
2019-12-14 16:30:35 +00:00
}
func updateInReplyTo ( ) {
if let replyView = replyView {
replyView . removeFromSuperview ( )
}
2018-10-20 14:54:59 +00:00
2019-12-14 16:30:35 +00:00
if let inReplyToID = inReplyToID {
2020-01-06 00:54:28 +00:00
if let status = mastodonController . cache . status ( for : inReplyToID ) {
2019-12-14 16:30:35 +00:00
updateInReplyTo ( inReplyTo : status )
} else {
let loadingVC = LoadingViewController ( )
embedChild ( loadingVC )
2020-01-06 00:54:28 +00:00
mastodonController . cache . status ( for : inReplyToID ) { ( status ) in
2019-12-14 16:30:35 +00:00
guard let status = status else { return }
DispatchQueue . main . async {
self . updateInReplyTo ( inReplyTo : status )
loadingVC . removeViewAndController ( )
}
}
}
} else {
2019-01-15 02:59:42 +00:00
visibility = Preferences . shared . defaultPostVisibility
2019-06-14 01:12:29 +00:00
contentWarningEnabled = false
2019-12-14 16:30:35 +00:00
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
}
2018-10-20 14:54:59 +00:00
}
2019-12-14 16:30:35 +00:00
let replyView = ComposeStatusReplyView . create ( )
2020-01-20 20:25:23 +00:00
replyView . mastodonController = mastodonController
2019-12-14 16:30:35 +00:00
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
2020-03-02 00:40:32 +00:00
inReplyToLabel . text = " In reply to \( inReplyTo . account . displayOrUserName ) "
2019-01-15 02:59:42 +00:00
}
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 )
2018-10-20 14:54:59 +00:00
2019-01-15 02:59:42 +00:00
NotificationCenter . default . removeObserver ( self , name : UIResponder . keyboardWillHideNotification , object : nil )
NotificationCenter . default . removeObserver ( self , name : UIResponder . keyboardWillChangeFrameNotification , object : nil )
2018-08-31 02:30:19 +00:00
}
2020-03-03 03:31:37 +00:00
2019-06-13 20:13:53 +00:00
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 )
}
2019-01-15 02:59:42 +00:00
func createFormattingButtons ( ) -> [ UIBarButtonItem ] {
guard Preferences . shared . statusContentType != . plain else {
return [ ]
}
return StatusFormat . allCases . map { ( format ) in
2019-06-13 19:38:40 +00:00
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 " )
}
2019-01-15 02:59:42 +00:00
item . tag = StatusFormat . allCases . firstIndex ( of : format ) !
2019-09-27 00:53:22 +00:00
item . accessibilityLabel = format . accessibilityLabel
2019-01-15 02:59:42 +00:00
return item
2018-08-31 02:30:19 +00:00
}
}
2019-01-15 02:59:42 +00:00
@objc func adjustForKeyboard ( notification : NSNotification ) {
let userInfo = notification . userInfo !
2018-08-31 02:30:19 +00:00
2019-01-15 02:59:42 +00:00
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 {
// l e t a c c e s s o r y F r a m e = v i e w . c o n v e r t ( s t a t u s T e x t V i e w . i n p u t A c c e s s o r y V i e w ! . f r a m e , f r o m : v i e w . w i n d o w )
let offset = keyboardViewEndFrame . height // + a c c e s s o r y F r a m e . h e i g h t
// TODO: r a d a r f o r i n c o r r e c t k e y b o a r d e n d f r a m e h e i g h t ( e i t h e r c o n v e r t e d o r s c r e e n )
// t h e v a l u e r e t u r n e d i s s o m e w h e r e b e t w e e n t h e h e i g h t o f t h e k e y b o a r d a n d t h e h e i g h t o f t h e k e y b o a r d + a c c e s s o r y
// a c t u a l l y m a y b e n o t ? ?
scrollView . contentInset = UIEdgeInsets ( top : 0 , left : 0 , bottom : offset , right : 0 )
}
scrollView . scrollIndicatorInsets = scrollView . contentInset
2018-08-31 02:30:19 +00:00
}
2020-01-18 02:55:21 +00:00
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 )
}
2018-09-30 02:20:17 +00:00
func updateCharactersRemaining ( ) {
let count = CharacterCounter . count ( text : statusTextView . text )
2019-01-15 02:59:42 +00:00
let cwCount = contentWarningEnabled ? ( contentWarningTextField . text ? . count ? ? 0 ) : 0
2020-01-05 20:25:07 +00:00
let remaining = ( mastodonController . instance . maxStatusCharacters ? ? 500 ) - count - cwCount
2018-09-30 02:20:17 +00:00
if remaining < 0 {
charactersRemainingLabel . textColor = . red
2020-01-18 02:55:21 +00:00
compositionState . formUnion ( . tooManyCharacters )
2018-09-30 02:20:17 +00:00
} else {
charactersRemainingLabel . textColor = . darkGray
2020-01-18 02:55:21 +00:00
compositionState . subtract ( . tooManyCharacters )
2018-09-30 02:20:17 +00:00
}
2019-09-27 00:53:22 +00:00
charactersRemainingLabel . text = String ( remaining )
charactersRemainingLabel . accessibilityLabel = String ( format : NSLocalizedString ( " %d characters remaining " , comment : " compose characters remaining accessibility label " ) , remaining )
2018-09-30 02:20:17 +00:00
}
2019-06-13 19:06:19 +00:00
func updateHasChanges ( ) {
if let currentDraft = currentDraft {
2019-09-08 21:45:33 +00:00
let cw = contentWarningEnabled ? contentWarningTextField . text : nil
hasChanges = statusTextView . text != currentDraft . text || cw != currentDraft . contentWarning
2019-06-13 19:06:19 +00:00
} else {
2019-09-08 21:45:33 +00:00
hasChanges = ! statusTextView . text . isEmpty || ( contentWarningEnabled && ! ( contentWarningTextField . text ? . isEmpty ? ? true ) )
2019-06-13 19:06:19 +00:00
}
}
2018-09-30 02:28:17 +00:00
func updatePlaceholder ( ) {
placeholderLabel . isHidden = ! statusTextView . text . isEmpty
}
2019-01-15 02:59:42 +00:00
func updateAddAttachmentButton ( ) {
2020-01-05 20:25:07 +00:00
switch mastodonController . instance . instanceType {
2019-09-11 20:57:21 +00:00
case . pleroma :
addAttachmentButton . isEnabled = true
case . mastodon :
2020-01-04 21:25:15 +00:00
addAttachmentButton . isEnabled = selectedAttachments . count <= 4 && ! selectedAttachments . contains ( where : { $0 . type = = . video } )
2019-09-11 20:57:21 +00:00
}
2019-01-15 02:59:42 +00:00
}
func updateAttachmentViews ( ) {
for view in attachmentsStackView . arrangedSubviews {
if view is ComposeMediaView {
view . removeFromSuperview ( )
}
}
2020-01-04 21:25:15 +00:00
for attachment in selectedAttachments {
2019-01-15 02:59:42 +00:00
let mediaView = ComposeMediaView . create ( )
mediaView . delegate = self
2020-01-04 21:25:15 +00:00
mediaView . update ( attachment : attachment )
2019-01-15 02:59:42 +00:00
attachmentsStackView . insertArrangedSubview ( mediaView , at : attachmentsStackView . arrangedSubviews . count - 1 )
updateAddAttachmentButton ( )
}
}
func contentWarningStateChanged ( ) {
contentWarningContainerView . isHidden = ! contentWarningEnabled
2019-09-27 00:53:22 +00:00
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 " )
}
2019-01-15 02:59:42 +00:00
}
func visibilityChanged ( ) {
2019-06-14 00:53:17 +00:00
visibilityBarButtonItem . image = UIImage ( systemName : visibility . imageName )
2019-09-28 04:37:43 +00:00
visibilityBarButtonItem . accessibilityLabel = String ( format : NSLocalizedString ( " Visibility: %@ " , comment : " compose visiblity accessibility label " ) , Preferences . shared . defaultPostVisibility . displayName )
2019-01-15 02:59:42 +00:00
}
func saveDraft ( ) {
2019-02-22 18:53:38 +00:00
var attachments = [ DraftsManager . DraftAttachment ] ( )
2020-01-04 21:25:15 +00:00
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 ) )
2019-02-22 18:53:38 +00:00
}
2020-02-22 20:43:17 +00:00
let statusText = statusTextView . text . trimmingCharacters ( in : . whitespacesAndNewlines )
let cw = contentWarningEnabled ? contentWarningTextField . text ? . trimmingCharacters ( in : . whitespacesAndNewlines ) : nil
2020-01-20 20:26:25 +00:00
let account = mastodonController . accountInfo !
2020-02-22 20:43:17 +00:00
if attachments . count = = 0 , statusText . isEmpty , cw ? . isEmpty ? ? true {
if let currentDraft = self . currentDraft {
DraftsManager . shared . remove ( currentDraft )
} else {
return
}
2019-01-15 02:59:42 +00:00
} else {
2020-02-22 20:43:17 +00:00
if let currentDraft = self . currentDraft {
currentDraft . update ( accountID : account . id , text : statusText , contentWarning : cw , attachments : attachments )
} else {
self . currentDraft = DraftsManager . shared . create ( accountID : account . id , text : statusText , contentWarning : cw , inReplyToID : inReplyToID , attachments : attachments )
}
2019-01-15 02:59:42 +00:00
}
2019-11-29 03:26:37 +00:00
DraftsManager . save ( )
2019-01-15 02:59:42 +00:00
}
@objc func close ( ) {
dismiss ( animated : true )
xcbSession ? . complete ( with : . cancel )
}
2018-08-31 02:30:19 +00:00
// MARK: - I n t e r a c t i o n
2019-01-15 02:59:42 +00:00
2019-06-13 19:06:19 +00:00
@objc func showSaveAndClosePrompt ( ) {
2019-01-15 02:59:42 +00:00
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 ) )
2019-01-19 19:31:31 +00:00
present ( alert , animated : true )
2019-01-15 02:59:42 +00:00
}
@objc func contentWarningButtonPressed ( ) {
contentWarningEnabled = ! contentWarningEnabled
2019-06-14 01:12:29 +00:00
if contentWarningEnabled {
contentWarningTextField . becomeFirstResponder ( )
} else {
statusTextView . becomeFirstResponder ( )
}
2019-01-15 02:59:42 +00:00
}
@objc func contentWarningTextFieldDidChange ( ) {
updateCharactersRemaining ( )
2019-06-13 19:06:19 +00:00
updateHasChanges ( )
2019-01-15 02:59:42 +00:00
}
2019-06-13 19:38:40 +00:00
@objc func visibilityButtonPressed ( ) {
2018-10-26 01:54:07 +00:00
let alertController = UIAlertController ( currentVisibility : self . visibility ) { ( visibility ) in
guard let visibility = visibility else { return }
2019-01-15 02:59:42 +00:00
self . visibility = visibility
2018-08-31 02:30:19 +00:00
}
present ( alertController , animated : true )
}
2019-01-15 02:59:42 +00:00
@objc func formatButtonPressed ( _ button : UIBarButtonItem ) {
guard statusTextView . isFirstResponder else {
return
}
2018-08-31 02:30:19 +00:00
2019-01-15 02:59:42 +00:00
let format = StatusFormat . allCases [ button . tag ]
guard let insertionResult = format . insertionResult else {
return
2018-08-31 02:30:19 +00:00
}
2019-01-15 02:59:42 +00:00
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 )
2018-08-31 02:30:19 +00:00
}
}
2019-01-15 02:59:42 +00:00
@objc func draftsButtonPressed ( ) {
2020-01-23 03:27:58 +00:00
let draftsVC = DraftsTableViewController ( account : mastodonController . accountInfo ! )
2019-01-15 02:59:42 +00:00
draftsVC . delegate = self
2019-01-19 19:31:31 +00:00
present ( UINavigationController ( rootViewController : draftsVC ) , animated : true )
2019-01-15 02:59:42 +00:00
}
@IBAction func addAttachmentPressed ( _ sender : Any ) {
2020-01-04 21:25:15 +00:00
// h i d e k e y b o a r d b e f o r e s h o w i n g a s s e t p i c k e r , s o i t d o e s n ' t r e - a p p e a r w h e n a s s e t p i c k e r i s c l o s e d
contentWarningTextField . resignFirstResponder ( )
statusTextView . resignFirstResponder ( )
let sheetContainer = AssetPickerSheetContainerViewController ( )
sheetContainer . assetPicker . assetPickerDelegate = self
present ( sheetContainer , animated : true )
2019-01-15 02:59:42 +00:00
}
@objc func postButtonPressed ( ) {
2018-08-31 02:30:19 +00:00
guard let text = statusTextView . text ,
! text . isEmpty else { return }
2019-09-06 21:09:28 +00:00
// s a v e a d r a f t b e f o r e p o s t i n g t h e s t a t u s , s o i f a c r a s h o c c u r s d u r i n g p o s t i n g , t h e s t a t u s w o n ' t b e l o s t
saveDraft ( )
2019-01-15 02:59:42 +00:00
// d i s a b l e p o s t b u t t o n w h i l e s e n d i n g p o s t r e q u e s t
2020-01-18 02:55:21 +00:00
compositionState . formUnion ( . currentlyPosting )
2018-09-12 13:19:51 +00:00
2018-08-31 02:30:19 +00:00
let contentWarning : String ?
2019-01-15 02:59:42 +00:00
if contentWarningEnabled , let cwText = contentWarningTextField . text , ! cwText . isEmpty {
contentWarning = cwText
2018-08-31 02:30:19 +00:00
} else {
contentWarning = nil
}
let sensitive = contentWarning != nil
2019-01-15 02:59:42 +00:00
let visibility = self . visibility !
2018-08-31 02:30:19 +00:00
2019-01-15 02:59:42 +00:00
let group = DispatchGroup ( )
2018-08-31 02:30:19 +00:00
var attachments : [ Attachment ? ] = [ ]
2020-01-04 21:25:15 +00:00
for compAttachment in selectedAttachments {
2018-08-31 02:30:19 +00:00
let index = attachments . count
attachments . append ( nil )
2019-01-15 02:59:42 +00:00
let mediaView = attachmentsStackView . arrangedSubviews [ index ] as ! ComposeMediaView
let description = mediaView . descriptionTextView . text
2018-08-31 02:30:19 +00:00
group . enter ( )
2019-01-15 02:59:42 +00:00
2020-01-04 21:25:15 +00:00
compAttachment . getData { ( data , mimeType ) in
2019-01-15 02:59:42 +00:00
self . postProgressView . step ( )
2020-01-05 19:00:39 +00:00
let request = Client . upload ( attachment : FormAttachment ( mimeType : mimeType , data : data , fileName : " file " ) , description : description )
2020-01-05 20:25:07 +00:00
self . mastodonController . run ( request ) { ( response ) in
2019-01-15 02:59:42 +00:00
guard case let . success ( attachment , _ ) = response else { fatalError ( ) }
2020-01-04 21:25:15 +00:00
2019-01-15 02:59:42 +00:00
attachments [ index ] = attachment
2020-01-04 21:25:15 +00:00
2019-01-15 02:59:42 +00:00
self . postProgressView . step ( )
2020-01-04 21:25:15 +00:00
2019-01-15 02:59:42 +00:00
group . leave ( )
}
2018-08-31 02:30:19 +00:00
}
}
2019-01-15 02:59:42 +00:00
postProgressView . steps = 2 + ( attachments . count * 2 ) // 2 s t e p s ( r e q u e s t d a t a , t h e n u p l o a d ) f o r e a c h a t t a c h m e n t
postProgressView . currentStep = 1
2018-09-12 13:19:51 +00:00
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
2020-01-05 19:00:39 +00:00
let request = Client . createStatus ( text : text ,
2019-01-15 02:59:42 +00:00
contentType : Preferences . shared . statusContentType ,
inReplyTo : self . inReplyToID ,
media : attachments ,
sensitive : sensitive ,
spoilerText : contentWarning ,
visibility : visibility ,
language : nil )
2020-01-05 20:25:07 +00:00
self . mastodonController . run ( request ) { ( response ) in
2018-09-11 14:52:21 +00:00
guard case let . success ( status , _ ) = response else { fatalError ( ) }
2019-01-15 02:59:42 +00:00
self . postedStatus = status
2020-01-06 00:54:28 +00:00
self . mastodonController . cache . add ( status : status )
2018-10-23 02:09:11 +00:00
2019-01-15 02:59:42 +00:00
if let draft = self . currentDraft {
2018-10-23 02:09:11 +00:00
DraftsManager . shared . remove ( draft )
}
2018-08-31 02:30:19 +00:00
DispatchQueue . main . async {
2019-01-15 02:59:42 +00:00
self . postProgressView . step ( )
2018-10-20 16:03:18 +00:00
self . dismiss ( animated : true )
2019-01-15 02:59:42 +00:00
2020-01-05 20:25:07 +00:00
let conversationVC = ConversationTableViewController ( for : status . id , mastodonController : self . mastodonController )
2019-01-19 19:31:31 +00:00
self . show ( conversationVC , sender : self )
2018-09-23 16:01:05 +00:00
2018-09-23 22:43:33 +00:00
self . xcbSession ? . complete ( with : . success , additionalData : [
2018-09-23 23:04:39 +00:00
" statusURL " : status . url ? . absoluteString ,
2018-09-23 22:43:33 +00:00
" statusURI " : status . uri
2019-01-15 02:59:42 +00:00
] )
2018-08-31 02:30:19 +00:00
}
}
}
}
2019-01-15 02:59:42 +00:00
}
extension ComposeViewController : UIScrollViewDelegate {
func scrollViewDidScroll ( _ scrollView : UIScrollView ) {
guard let replyView = replyView else { return }
2018-10-23 02:09:11 +00:00
2019-01-15 02:59:42 +00:00
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
2018-10-23 02:09:11 +00:00
}
2019-01-15 02:59:42 +00:00
replyAvatarImageViewTopConstraint ? . constant = constant
2018-08-31 02:30:19 +00:00
}
}
2018-09-30 02:20:17 +00:00
extension ComposeViewController : UITextViewDelegate {
func textViewDidChange ( _ textView : UITextView ) {
updateCharactersRemaining ( )
2018-09-30 02:28:17 +00:00
updatePlaceholder ( )
2019-06-13 19:06:19 +00:00
updateHasChanges ( )
2018-09-30 02:20:17 +00:00
}
}
2020-01-04 21:25:15 +00:00
extension ComposeViewController : AssetPickerViewControllerDelegate {
func assetPicker ( _ assetPicker : AssetPickerViewController , shouldAllowAssetOfType type : CompositionAttachment . AttachmentType ) -> Bool {
2020-01-05 20:25:07 +00:00
switch mastodonController . instance . instanceType {
2019-09-11 20:57:21 +00:00
case . pleroma :
return true
case . mastodon :
2020-01-04 21:25:15 +00:00
if ( type = = . video && selectedAttachments . count > 0 ) ||
selectedAttachments . contains ( where : { $0 . type = = . video } ) ||
assetPicker . currentCollectionSelectedAssets . contains ( where : { $0 . type = = . video } ) {
2019-09-11 20:57:21 +00:00
return false
}
2020-01-04 21:25:15 +00:00
return selectedAttachments . count + assetPicker . currentCollectionSelectedAssets . count < 4
2019-09-11 20:57:21 +00:00
}
2018-08-31 02:30:19 +00:00
}
2020-01-04 21:25:15 +00:00
func assetPicker ( _ assetPicker : AssetPickerViewController , didSelectAttachments attachments : [ CompositionAttachment ] ) {
selectedAttachments . append ( contentsOf : attachments )
2020-01-18 02:55:21 +00:00
updateAttachmentDescriptionsRequired ( )
2020-01-04 21:25:15 +00:00
}
2018-08-31 02:30:19 +00:00
}
2018-08-31 16:39:39 +00:00
extension ComposeViewController : ComposeMediaViewDelegate {
2019-01-15 02:59:42 +00:00
func didRemoveMedia ( _ mediaView : ComposeMediaView ) {
let index = attachmentsStackView . arrangedSubviews . firstIndex ( of : mediaView ) !
2020-01-04 21:25:15 +00:00
selectedAttachments . remove ( at : index )
2019-01-15 02:59:42 +00:00
updateAddAttachmentButton ( )
2020-01-18 02:55:21 +00:00
updateAttachmentDescriptionsRequired ( )
}
func descriptionTextViewDidChange ( _ mediaView : ComposeMediaView ) {
updateAttachmentDescriptionsRequired ( )
2018-08-31 16:39:39 +00:00
}
}
2018-10-23 02:09:11 +00:00
extension ComposeViewController : DraftsTableViewControllerDelegate {
func draftSelectionCanceled ( ) {
}
2019-12-14 16:30:35 +00:00
func shouldSelectDraft ( _ draft : DraftsManager . Draft , completion : @ escaping ( Bool ) -> Void ) {
2020-02-29 00:50:04 +00:00
if draft . inReplyToID != self . inReplyToID , hasChanges {
2019-12-14 16:30:35 +00:00
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 )
} ) )
// w e c a n ' t p r e s e n t t h e a l e r t o u r s e l v e s , s i n c e t h e c o m p o s e V C i s a l r e a d y p r e s e n t i n g t h e d r a f t s e l e c t o r
// b u t p r e s e n t i n g o n t h e p r e s e n t e d v i e w c o n t r o l l e r s e e m s h a c k y , i s t h e r e a b e t t e r w a y t o d o t h i s ?
presentedViewController ! . present ( alertController , animated : true )
} else {
completion ( true )
}
}
2018-10-23 02:09:11 +00:00
func draftSelected ( _ draft : DraftsManager . Draft ) {
2020-02-29 00:50:04 +00:00
if hasChanges {
saveDraft ( )
}
2019-01-15 02:59:42 +00:00
self . currentDraft = draft
2019-02-22 18:53:38 +00:00
2019-12-14 16:30:35 +00:00
inReplyToID = draft . inReplyToID
updateInReplyTo ( )
2018-10-23 02:09:11 +00:00
statusTextView . text = draft . text
2019-09-08 21:45:33 +00:00
contentWarningEnabled = draft . contentWarning != nil
contentWarningTextField . text = draft . contentWarning
2018-10-23 02:09:11 +00:00
updatePlaceholder ( )
2019-01-15 02:59:42 +00:00
updateCharactersRemaining ( )
2020-01-18 02:55:21 +00:00
2020-01-04 21:25:15 +00:00
selectedAttachments = draft . attachments . map { $0 . attachment }
2019-02-22 18:53:38 +00:00
updateAttachmentViews ( )
2019-09-06 22:50:18 +00:00
for case let mediaView as ComposeMediaView in attachmentsStackView . arrangedSubviews {
2020-01-04 21:25:15 +00:00
let attachment = draft . attachments . first ( where : { $0 . attachment = = mediaView . attachment } ) !
2019-09-06 22:50:18 +00:00
mediaView . descriptionTextView . text = attachment . description
// c a l l t h e d e l e g a t e m e t h o d m a n u a l l y , s i n c e s e t t i n g t h e t e x t p r o p e r t y d o e s n ' t c a l l i t
2019-02-22 18:53:38 +00:00
mediaView . textViewDidChange ( mediaView . descriptionTextView )
2019-09-06 22:50:18 +00:00
}
2020-01-18 02:55:21 +00:00
updateAttachmentDescriptionsRequired ( )
2019-09-06 22:50:18 +00:00
}
func draftSelectionCompleted ( ) {
// c h e c k t h a t a l l t h e a s s e t s f r o m t h e d r a f t h a v e b e e n a d d e d
2020-01-04 21:25:15 +00:00
if let currentDraft = currentDraft , selectedAttachments . count < currentDraft . attachments . count {
2019-09-06 22:50:18 +00:00
// s o m e o f t h e a s s e t s i n t h e d r a f t w e r e n ' t l o a d e d , s o n o t i f y t h e u s e r
2020-01-04 21:25:15 +00:00
let difference = currentDraft . attachments . count - selectedAttachments . count
2019-09-06 22:50:18 +00:00
// t o d o : l o c a l i z e m e
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 )
2019-02-22 18:53:38 +00:00
}
2018-10-23 02:09:11 +00:00
}
}
2019-06-13 19:06:19 +00:00
extension ComposeViewController : UIAdaptivePresentationControllerDelegate {
func presentationControllerShouldDismiss ( _ presentationController : UIPresentationController ) -> Bool {
return Preferences . shared . automaticallySaveDrafts || ! hasChanges
}
func presentationControllerDidAttemptToDismiss ( _ presentationController : UIPresentationController ) {
showSaveAndClosePrompt ( )
}
// w h e n t h e c o m p o s e s c r e e n i s d i s m i s s e d i n t e r a c t i v e l y , c l o s e ( ) i s n ' t c a l l e d , s o w e m a k e s u r e t o
// c o m p l e t e t h e X - C a l l b a c k - U R L s e s s i o n a n d s a v e t h e d r a f t i s a u t o m a t i c s a v i n g i s e n a b l e d
// ( i f a u t o m a t i c s a v i n g i s o f f , t h e d r a f t w i l l g e t s a v e d / d i s c a r d e d b y t h e u s e r w h e n d i d A t t e m p t T o D i s m i s s i s c a l l e d
func presentationControllerDidDismiss ( _ presentationController : UIPresentationController ) {
if Preferences . shared . automaticallySaveDrafts {
saveDraft ( )
}
xcbSession ? . complete ( with : . cancel )
}
}