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
2019-01-15 02:59:42 +00:00
import Photos
import GMImagePicker
import MobileCoreServices
2018-08-31 02:30:19 +00:00
class ComposeViewController : UIViewController {
2018-09-18 01:57:46 +00:00
var inReplyToID : String ?
2019-01-15 02:59:42 +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
}
}
2019-01-15 02:59:42 +00:00
var selectedAssets : [ PHAsset ] = [ ] {
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 ?
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-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
2019-01-19 19:31:31 +00:00
init ( inReplyTo inReplyToID : String ? = nil , mentioningAcct : String ? = nil , text : String ? = nil ) {
2019-01-15 02:59:42 +00:00
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 ( )
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-06-14 00:53:17 +00:00
visibilityBarButtonItem = UIBarButtonItem ( image : UIImage ( systemName : Preferences . shared . defaultPostVisibility . imageName ) , style : . plain , target : self , action : #selector ( visibilityButtonPressed ) )
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
MastodonController . getOwnAccount { ( account ) in
DispatchQueue . main . async {
self . selfDetailView . update ( account : account )
2018-08-31 02:30:19 +00:00
}
}
2019-01-15 02:59:42 +00:00
if let inReplyToID = inReplyToID , let inReplyTo = MastodonCache . status ( for : inReplyToID ) {
visibility = inReplyTo . visibility
2019-08-01 03:25:44 +00:00
if Preferences . shared . contentWarningCopyMode = = . doNotCopy {
contentWarningEnabled = false
contentWarningContainerView . isHidden = true
} else {
2019-09-25 03:41:20 +00:00
contentWarningEnabled = ! inReplyTo . spoilerText . isEmpty
2019-08-01 03:25:44 +00:00
contentWarningContainerView . isHidden = ! contentWarningEnabled
if Preferences . shared . contentWarningCopyMode = = . prependRe ,
2019-09-25 03:41:20 +00:00
! inReplyTo . spoilerText . lowercased ( ) . starts ( with : " re: " ) {
contentWarningTextField . text = " re: \( inReplyTo . spoilerText ) "
2019-08-01 03:25:44 +00:00
} else {
2019-09-25 03:41:20 +00:00
contentWarningTextField . text = inReplyTo . spoilerText
2019-08-01 03:25:44 +00:00
}
}
2019-01-15 02:59:42 +00:00
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 ( )
2019-06-04 21:57:31 +00:00
replyLabelContainer . backgroundColor = . clear
2019-01-15 02:59:42 +00:00
replyLabelContainer . translatesAutoresizingMaskIntoConstraints = false
let replyLabel = UILabel ( )
replyLabel . translatesAutoresizingMaskIntoConstraints = false
replyLabel . text = " In reply to \( inReplyTo . account . realDisplayName ) "
2019-06-04 21:57:31 +00:00
replyLabel . textColor = . secondaryLabel
2019-01-15 02:59:42 +00:00
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 )
2018-08-31 02:30:19 +00:00
}
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 ( )
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 )
2018-10-20 14:54:59 +00:00
2019-01-15 02:59:42 +00:00
if inReplyToID = = nil {
visibility = Preferences . shared . defaultPostVisibility
2019-06-14 01:12:29 +00:00
contentWarningEnabled = false
2018-10-20 14:54:59 +00:00
}
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
}
override func viewDidLayoutSubviews ( ) {
2019-01-15 02:59:42 +00:00
super . viewDidLayoutSubviews ( )
// i f i n R e p l y T o I D ! = n i l {
// s c r o l l V i e w . c o n t e n t O f f s e t = C G P o i n t ( x : 0 , y : s t a c k V i e w . a r r a n g e d S u b v i e w s . f i r s t ! . f r a m e . h e i g h t )
// }
}
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 ) !
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
}
2018-09-30 02:20:17 +00:00
func updateCharactersRemaining ( ) {
2019-01-15 02:59:42 +00:00
// TODO: i n c l u d e C W c h a r c o u n t
2018-09-30 02:20:17 +00:00
let count = CharacterCounter . count ( text : statusTextView . text )
2019-01-15 02:59:42 +00:00
let cwCount = contentWarningEnabled ? ( contentWarningTextField . text ? . count ? ? 0 ) : 0
let remaining = ( MastodonController . instance . maxStatusCharacters ? ? 500 ) - count - cwCount
2018-09-30 02:20:17 +00:00
if remaining < 0 {
charactersRemainingLabel . textColor = . red
2019-01-15 02:59:42 +00:00
postBarButtonItem . isEnabled = false
2018-09-30 02:20:17 +00:00
} else {
charactersRemainingLabel . textColor = . darkGray
2019-01-15 02:59:42 +00:00
postBarButtonItem . isEnabled = true
2018-09-30 02:20:17 +00:00
}
charactersRemainingLabel . text = remaining . description
}
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 ( ) {
2019-09-11 20:57:21 +00:00
switch MastodonController . instance . instanceType {
case . pleroma :
addAttachmentButton . isEnabled = true
case . mastodon :
addAttachmentButton . isEnabled = selectedAssets . count <= 4 && selectedAssets . first ( where : { $0 . mediaType = = . video } ) = = nil
}
2019-01-15 02:59:42 +00:00
}
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
}
func visibilityChanged ( ) {
2019-06-14 00:53:17 +00:00
visibilityBarButtonItem . image = UIImage ( systemName : visibility . imageName )
2019-01-15 02:59:42 +00:00
}
func saveDraft ( ) {
2019-02-22 18:53:38 +00:00
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 ) )
}
2019-09-08 21:45:33 +00:00
let cw = contentWarningEnabled ? contentWarningTextField . text : nil
2019-02-22 18:53:38 +00:00
if let currentDraft = self . currentDraft {
2019-09-08 21:45:33 +00:00
currentDraft . update ( text : self . statusTextView . text , contentWarning : cw , attachments : attachments )
2019-01-15 02:59:42 +00:00
} else {
2019-09-08 21:45:33 +00:00
DraftsManager . shared . create ( text : self . statusTextView . text , contentWarning : cw , attachments : attachments )
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: - N a v i g a t i o n
2018-10-20 16:03:18 +00:00
override func dismiss ( animated flag : Bool , completion : ( ( ) -> Void ) ? = nil ) {
2018-08-31 02:30:19 +00:00
statusTextView . resignFirstResponder ( )
2018-10-20 16:03:18 +00:00
super . dismiss ( animated : flag , completion : completion )
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 ( ) {
2019-01-19 19:31:31 +00:00
let draftsVC = DraftsTableViewController ( )
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 ) {
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 ( ) {
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
postBarButtonItem . isEnabled = false
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 ? ] = [ ]
2019-01-15 02:59:42 +00:00
for asset in selectedAssets {
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
let options = PHImageRequestOptions ( )
options . version = . current
options . deliveryMode = . highQualityFormat
options . resizeMode = . none
options . isNetworkAccessAllowed = true
2019-06-04 17:37:31 +00:00
PHImageManager . default ( ) . requestImageDataAndOrientation ( for : asset , options : options ) { ( data , dataUTI , orientation , info ) in
2019-01-15 02:59:42 +00:00
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 ( )
}
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
2018-10-02 23:31:00 +00:00
let request = MastodonController . 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 )
MastodonController . client . 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
2018-09-18 16:59:07 +00:00
MastodonCache . 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
2019-01-19 19:31:31 +00:00
let conversationVC = ConversationTableViewController ( for : status . id )
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
}
}
2019-01-15 02:59:42 +00:00
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 {
2019-09-11 20:57:21 +00:00
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
}
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 ) !
selectedAssets . remove ( at : index )
updateAddAttachmentButton ( )
2018-08-31 16:39:39 +00:00
}
}
2018-10-23 02:09:11 +00:00
extension ComposeViewController : DraftsTableViewControllerDelegate {
func draftSelectionCanceled ( ) {
}
func draftSelected ( _ draft : DraftsManager . Draft ) {
2019-01-15 02:59:42 +00:00
self . currentDraft = draft
2019-02-22 18:53:38 +00:00
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 ( )
2019-09-06 22:50:18 +00:00
2019-02-22 18:53:38 +00:00
let result = PHAsset . fetchAssets ( withLocalIdentifiers : draft . attachments . map { $0 . assetIdentifier } , options : nil )
2019-09-06 22:50:18 +00:00
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
2019-02-22 18:53:38 +00:00
}
2019-09-06 22:50:18 +00:00
self . selectedAssets = assets . values . map { $0 . asset }
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 {
let attachment = draft . attachments . first ( where : { $0 . assetIdentifier = = mediaView . assetIdentifier } ) !
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
}
}
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
if let currentDraft = currentDraft , selectedAssets . count < currentDraft . attachments . count {
// 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
let difference = currentDraft . attachments . count - selectedAssets . count
// 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 )
}
}