2022-09-30 13:28:09 +02:00
//
// C o m p o s e C o n t e n t V i e w C o n t r o l l e r . s w i f t
//
//
// C r e a t e d b y M a i n a s u K o n 2 2 / 9 / 3 0 .
//
import os . log
import UIKit
2022-10-10 13:14:52 +02:00
import SwiftUI
2022-10-11 12:31:40 +02:00
import Combine
2022-10-21 13:12:44 +02:00
import PhotosUI
2022-10-18 13:01:31 +02:00
import MastodonCore
2022-09-30 13:28:09 +02:00
public final class ComposeContentViewController : UIViewController {
2022-11-13 15:08:26 +01:00
static let minAutoCompleteVisibleHeight : CGFloat = 100
2022-09-30 13:28:09 +02:00
let logger = Logger ( subsystem : " ComposeContentViewController " , category : " ViewController " )
2022-10-11 12:31:40 +02:00
var disposeBag = Set < AnyCancellable > ( )
2022-10-10 13:14:52 +02:00
public var viewModel : ComposeContentViewModel !
2022-10-21 13:12:44 +02:00
private ( set ) lazy var composeContentToolbarViewModel = ComposeContentToolbarView . ViewModel ( delegate : self )
2022-09-30 13:28:09 +02:00
2022-11-13 09:04:29 +01:00
// t a b l e V i e w c o n t a i n e r
2022-10-10 13:14:52 +02:00
let tableView : ComposeTableView = {
let tableView = ComposeTableView ( )
2022-10-11 12:31:40 +02:00
tableView . estimatedRowHeight = UITableView . automaticDimension
2022-10-10 13:14:52 +02:00
tableView . alwaysBounceVertical = true
tableView . separatorStyle = . none
tableView . tableFooterView = UIView ( )
return tableView
} ( )
2022-10-18 13:01:31 +02:00
2022-11-13 09:04:29 +01:00
// a u t o c o m p l e t e
private ( set ) lazy var autoCompleteViewController : AutoCompleteViewController = {
let viewController = AutoCompleteViewController ( )
viewController . viewModel = AutoCompleteViewModel ( context : viewModel . context , authContext : viewModel . authContext )
viewController . delegate = self
// v i e w C o n t r o l l e r . v i e w M o d e l . c u s t o m E m o j i V i e w M o d e l . v a l u e = v i e w M o d e l . c u s t o m E m o j i V i e w M o d e l
return viewController
} ( )
// t o o l b a r
2022-10-18 13:01:31 +02:00
lazy var composeContentToolbarView = ComposeContentToolbarView ( viewModel : composeContentToolbarViewModel )
var composeContentToolbarViewBottomLayoutConstraint : NSLayoutConstraint !
let composeContentToolbarBackgroundView = UIView ( )
2022-10-21 13:12:44 +02:00
// m e d i a p i c k e r
static func createPhotoLibraryPickerConfiguration ( selectionLimit : Int = 4 ) -> PHPickerConfiguration {
var configuration = PHPickerConfiguration ( )
configuration . filter = . any ( of : [ . images , . videos ] )
configuration . selectionLimit = selectionLimit
return configuration
}
2022-11-14 12:43:32 +01:00
public private ( set ) lazy var photoLibraryPicker : PHPickerViewController = {
2022-10-21 13:12:44 +02:00
let imagePicker = PHPickerViewController ( configuration : ComposeContentViewController . createPhotoLibraryPickerConfiguration ( ) )
imagePicker . delegate = self
return imagePicker
} ( )
2022-11-14 12:43:32 +01:00
public private ( set ) lazy var imagePickerController : UIImagePickerController = {
2022-10-21 13:12:44 +02:00
let imagePickerController = UIImagePickerController ( )
imagePickerController . sourceType = . camera
imagePickerController . delegate = self
return imagePickerController
} ( )
2022-11-14 12:43:32 +01:00
public private ( set ) lazy var documentPickerController : UIDocumentPickerViewController = {
2022-10-21 13:12:44 +02:00
let documentPickerController = UIDocumentPickerViewController ( forOpeningContentTypes : [ . image , . movie ] )
documentPickerController . delegate = self
return documentPickerController
} ( )
2022-11-13 12:42:50 +01:00
// e m o j i p i c k e r i n p u t V i e w
let customEmojiPickerInputView : CustomEmojiPickerInputView = {
let view = CustomEmojiPickerInputView (
frame : CGRect ( x : 0 , y : 0 , width : 0 , height : 300 ) ,
inputViewStyle : . keyboard
)
return view
} ( )
2022-10-10 13:14:52 +02:00
deinit {
os_log ( . info , log : . debug , " %{public}s[%{public}ld], %{public}s " , ( ( #file as NSString ) . lastPathComponent ) , #line , #function )
}
2022-09-30 13:28:09 +02:00
}
extension ComposeContentViewController {
public override func viewDidLoad ( ) {
super . viewDidLoad ( )
2022-11-13 12:42:50 +01:00
viewModel . delegate = self
2022-10-18 13:01:31 +02:00
// s e t u p v i e w
self . setupBackgroundColor ( theme : ThemeService . shared . currentTheme . value )
ThemeService . shared . currentTheme
. receive ( on : RunLoop . main )
. sink { [ weak self ] theme in
guard let self = self else { return }
self . setupBackgroundColor ( theme : theme )
}
. store ( in : & disposeBag )
// s e t u p t a b l e V i e w
2022-10-10 13:14:52 +02:00
tableView . translatesAutoresizingMaskIntoConstraints = false
view . addSubview ( tableView )
2022-11-17 17:45:27 +01:00
tableView . pinToParent ( )
2022-10-10 13:14:52 +02:00
tableView . delegate = self
viewModel . setupDataSource ( tableView : tableView )
2022-11-13 12:42:50 +01:00
// s e t u p e m o j i p i c k e r
customEmojiPickerInputView . collectionView . delegate = self
viewModel . customEmojiPickerInputViewModel . customEmojiPickerInputView = customEmojiPickerInputView
viewModel . setupCustomEmojiPickerDiffableDataSource ( collectionView : customEmojiPickerInputView . collectionView )
// s e t u p t o o l b a r
2022-10-18 13:01:31 +02:00
let toolbarHostingView = UIHostingController ( rootView : composeContentToolbarView )
toolbarHostingView . view . translatesAutoresizingMaskIntoConstraints = false
view . addSubview ( toolbarHostingView . view )
composeContentToolbarViewBottomLayoutConstraint = view . bottomAnchor . constraint ( equalTo : toolbarHostingView . view . bottomAnchor )
NSLayoutConstraint . activate ( [
toolbarHostingView . view . leadingAnchor . constraint ( equalTo : view . leadingAnchor ) ,
toolbarHostingView . view . trailingAnchor . constraint ( equalTo : view . trailingAnchor ) ,
composeContentToolbarViewBottomLayoutConstraint ,
toolbarHostingView . view . heightAnchor . constraint ( equalToConstant : ComposeContentToolbarView . toolbarHeight ) ,
] )
toolbarHostingView . view . preservesSuperviewLayoutMargins = true
// c o m p o s e T o o l b a r V i e w . d e l e g a t e = s e l f
composeContentToolbarBackgroundView . translatesAutoresizingMaskIntoConstraints = false
view . insertSubview ( composeContentToolbarBackgroundView , belowSubview : toolbarHostingView . view )
NSLayoutConstraint . activate ( [
composeContentToolbarBackgroundView . topAnchor . constraint ( equalTo : toolbarHostingView . view . topAnchor ) ,
composeContentToolbarBackgroundView . leadingAnchor . constraint ( equalTo : toolbarHostingView . view . leadingAnchor ) ,
composeContentToolbarBackgroundView . trailingAnchor . constraint ( equalTo : toolbarHostingView . view . trailingAnchor ) ,
view . bottomAnchor . constraint ( equalTo : composeContentToolbarBackgroundView . bottomAnchor ) ,
] )
2022-11-13 12:42:50 +01:00
// b i n d k e y b o a r d
2022-10-18 13:01:31 +02:00
let keyboardEventPublishers = Publishers . CombineLatest3 (
KeyboardResponderService . shared . isShow ,
KeyboardResponderService . shared . state ,
KeyboardResponderService . shared . endFrame
)
2022-11-13 15:08:26 +01:00
Publishers . CombineLatest3 (
keyboardEventPublishers ,
viewModel . $ isEmojiActive ,
viewModel . $ autoCompleteInfo
)
. sink ( receiveValue : { [ weak self ] keyboardEvents , isEmojiActive , autoCompleteInfo in
2022-10-18 13:01:31 +02:00
guard let self = self else { return }
let ( isShow , state , endFrame ) = keyboardEvents
2022-11-13 15:08:26 +01:00
2022-10-18 13:01:31 +02:00
let extraMargin : CGFloat = {
var margin = ComposeContentToolbarView . toolbarHeight
2022-11-13 15:08:26 +01:00
if autoCompleteInfo != nil {
margin += ComposeContentViewController . minAutoCompleteVisibleHeight
}
2022-10-18 13:01:31 +02:00
return margin
} ( )
2022-11-13 15:08:26 +01:00
2022-10-18 13:01:31 +02:00
guard isShow , state = = . dock else {
self . tableView . contentInset . bottom = extraMargin
self . tableView . verticalScrollIndicatorInsets . bottom = extraMargin
2022-11-13 15:08:26 +01:00
if let superView = self . autoCompleteViewController . tableView . superview {
let autoCompleteTableViewBottomInset : CGFloat = {
let tableViewFrameInWindow = superView . convert ( self . autoCompleteViewController . tableView . frame , to : nil )
let padding = tableViewFrameInWindow . maxY + ComposeContentToolbarView . toolbarHeight + AutoCompleteViewController . chevronViewHeight - self . view . frame . maxY
return max ( 0 , padding )
} ( )
self . autoCompleteViewController . tableView . contentInset . bottom = autoCompleteTableViewBottomInset
self . autoCompleteViewController . tableView . verticalScrollIndicatorInsets . bottom = autoCompleteTableViewBottomInset
}
2022-10-18 13:01:31 +02:00
UIView . animate ( withDuration : 0.3 ) {
self . composeContentToolbarViewBottomLayoutConstraint . constant = self . view . safeAreaInsets . bottom
if self . view . window != nil {
self . view . layoutIfNeeded ( )
}
}
return
}
// i s S h o w A N D d o c k s t a t e
// a d j u s t i n s e t f o r a u t o - c o m p l e t e
2022-11-13 15:08:26 +01:00
let autoCompleteTableViewBottomInset : CGFloat = {
guard let superview = self . autoCompleteViewController . tableView . superview else { return . zero }
let tableViewFrameInWindow = superview . convert ( self . autoCompleteViewController . tableView . frame , to : nil )
let padding = tableViewFrameInWindow . maxY + ComposeContentToolbarView . toolbarHeight + AutoCompleteViewController . chevronViewHeight - endFrame . minY
return max ( 0 , padding )
} ( )
self . autoCompleteViewController . tableView . contentInset . bottom = autoCompleteTableViewBottomInset
self . autoCompleteViewController . tableView . verticalScrollIndicatorInsets . bottom = autoCompleteTableViewBottomInset
2022-10-18 13:01:31 +02:00
// a d j u s t i n s e t f o r t a b l e V i e w
let contentFrame = self . view . convert ( self . tableView . frame , to : nil )
let padding = contentFrame . maxY + extraMargin - endFrame . minY
guard padding > 0 else {
self . tableView . contentInset . bottom = self . view . safeAreaInsets . bottom + extraMargin
self . tableView . verticalScrollIndicatorInsets . bottom = self . view . safeAreaInsets . bottom + extraMargin
return
}
self . tableView . contentInset . bottom = padding - self . view . safeAreaInsets . bottom
self . tableView . verticalScrollIndicatorInsets . bottom = padding - self . view . safeAreaInsets . bottom
UIView . animate ( withDuration : 0.3 ) {
self . composeContentToolbarViewBottomLayoutConstraint . constant = endFrame . height
self . view . layoutIfNeeded ( )
}
} )
. store ( in : & disposeBag )
2022-10-11 12:31:40 +02:00
// s e t u p s n a p b e h a v i o r
Publishers . CombineLatest (
viewModel . $ replyToCellFrame ,
viewModel . $ scrollViewState
)
. receive ( on : DispatchQueue . main )
. sink { [ weak self ] replyToCellFrame , scrollViewState in
guard let self = self else { return }
guard replyToCellFrame != . zero else { return }
switch scrollViewState {
case . fold :
self . tableView . contentInset . top = - replyToCellFrame . height
os_log ( . info , log : . debug , " %{public}s[%{public}ld], %{public}s: set contentInset.top: -%s " , ( ( #file as NSString ) . lastPathComponent ) , #line , #function , replyToCellFrame . height . description )
case . expand :
self . tableView . contentInset . top = 0
}
}
. store ( in : & disposeBag )
2022-10-21 13:12:44 +02:00
2022-11-13 09:04:29 +01:00
// b i n d a u t o - c o m p l e t e
viewModel . $ autoCompleteInfo
. receive ( on : DispatchQueue . main )
. sink { [ weak self ] info in
guard let self = self else { return }
guard let textView = self . viewModel . contentMetaText ? . textView else { return }
if self . autoCompleteViewController . view . superview = = nil {
self . autoCompleteViewController . view . frame = self . view . bounds
// a d d t o c o n t a i n e r v i e w . s e e a l s o : ` v i e w D i d L a y o u t S u b v i e w s ( ) `
self . viewModel . composeContentTableViewCell . contentView . addSubview ( self . autoCompleteViewController . view )
self . addChild ( self . autoCompleteViewController )
self . autoCompleteViewController . didMove ( toParent : self )
self . autoCompleteViewController . view . isHidden = true
self . tableView . autoCompleteViewController = self . autoCompleteViewController
}
self . updateAutoCompleteViewControllerLayout ( )
self . autoCompleteViewController . view . isHidden = info = = nil
guard let info = info else { return }
let symbolBoundingRectInContainer = textView . convert ( info . symbolBoundingRect , to : self . autoCompleteViewController . chevronView )
print ( info . symbolBoundingRect )
self . autoCompleteViewController . view . frame . origin . y = info . textBoundingRect . maxY + self . viewModel . contentTextViewFrame . minY
self . autoCompleteViewController . viewModel . symbolBoundingRect . value = symbolBoundingRectInContainer
self . autoCompleteViewController . viewModel . inputText . value = String ( info . inputText )
}
. store ( in : & disposeBag )
2022-11-13 12:42:50 +01:00
// b i n d e m o j i p i c k e r
viewModel . customEmojiViewModel ? . emojis
. receive ( on : DispatchQueue . main )
. sink ( receiveValue : { [ weak self ] emojis in
guard let self = self else { return }
if emojis . isEmpty {
self . customEmojiPickerInputView . activityIndicatorView . startAnimating ( )
} else {
self . customEmojiPickerInputView . activityIndicatorView . stopAnimating ( )
}
} )
. store ( in : & disposeBag )
2022-10-21 13:12:44 +02:00
// b i n d t o o l b a r
bindToolbarViewModel ( )
2022-11-13 15:08:26 +01:00
// b i n d a t t a c h m e n t p i c k e r
viewModel . $ attachmentViewModels
. receive ( on : DispatchQueue . main )
. sink { [ weak self ] _ in
guard let self = self else { return }
self . resetImagePicker ( )
}
. store ( in : & disposeBag )
2022-10-11 12:31:40 +02:00
}
public override func viewDidLayoutSubviews ( ) {
super . viewDidLayoutSubviews ( )
viewModel . viewLayoutFrame . update ( view : view )
2022-11-13 09:04:29 +01:00
updateAutoCompleteViewControllerLayout ( )
2022-10-11 12:31:40 +02:00
}
public override func viewSafeAreaInsetsDidChange ( ) {
super . viewSafeAreaInsetsDidChange ( )
viewModel . viewLayoutFrame . update ( view : view )
}
public override func viewWillTransition ( to size : CGSize , with coordinator : UIViewControllerTransitionCoordinator ) {
super . viewWillTransition ( to : size , with : coordinator )
coordinator . animate { [ weak self ] coordinatorContext in
guard let self = self else { return }
self . viewModel . viewLayoutFrame . update ( view : self . view )
}
}
}
2022-10-18 13:01:31 +02:00
extension ComposeContentViewController {
private func setupBackgroundColor ( theme : Theme ) {
let backgroundColor = UIColor ( dynamicProvider : { traitCollection in
switch traitCollection . userInterfaceStyle {
case . light : return . systemBackground
default : return theme . systemElevatedBackgroundColor
}
} )
view . backgroundColor = backgroundColor
tableView . backgroundColor = backgroundColor
composeContentToolbarBackgroundView . backgroundColor = theme . composeToolbarBackgroundColor
}
2022-10-21 13:12:44 +02:00
private func bindToolbarViewModel ( ) {
2022-11-13 15:08:26 +01:00
viewModel . $ isAttachmentButtonEnabled . assign ( to : & composeContentToolbarViewModel . $ isAttachmentButtonEnabled )
viewModel . $ isPollButtonEnabled . assign ( to : & composeContentToolbarViewModel . $ isPollButtonEnabled )
2022-10-21 13:12:44 +02:00
viewModel . $ isPollActive . assign ( to : & composeContentToolbarViewModel . $ isPollActive )
viewModel . $ isEmojiActive . assign ( to : & composeContentToolbarViewModel . $ isEmojiActive )
viewModel . $ isContentWarningActive . assign ( to : & composeContentToolbarViewModel . $ isContentWarningActive )
2022-11-14 11:41:54 +01:00
viewModel . $ visibility . assign ( to : & composeContentToolbarViewModel . $ visibility )
2022-10-28 13:06:18 +02:00
viewModel . $ maxTextInputLimit . assign ( to : & composeContentToolbarViewModel . $ maxTextInputLimit )
viewModel . $ contentWeightedLength . assign ( to : & composeContentToolbarViewModel . $ contentWeightedLength )
viewModel . $ contentWarningWeightedLength . assign ( to : & composeContentToolbarViewModel . $ contentWarningWeightedLength )
2022-11-14 11:41:54 +01:00
// b i n d b a c k t o s o u r c e d u e t o v i s i b i l i t y n o t u p d a t e v i a d e l e g a t e
composeContentToolbarViewModel . $ visibility
. dropFirst ( )
2022-11-14 12:43:32 +01:00
. receive ( on : DispatchQueue . main )
2022-11-14 11:41:54 +01:00
. sink { [ weak self ] visibility in
guard let self = self else { return }
if self . viewModel . visibility != visibility {
self . viewModel . visibility = visibility
}
}
. store ( in : & disposeBag )
2022-10-21 13:12:44 +02:00
}
2022-11-13 09:04:29 +01:00
private func updateAutoCompleteViewControllerLayout ( ) {
// p i n a u t o C o m p l e t e V i e w C o n t r o l l e r f r a m e t o c u r r e n t v i e w
if let containerView = autoCompleteViewController . view . superview {
let viewFrameInWindow = containerView . convert ( autoCompleteViewController . view . frame , to : view )
if viewFrameInWindow . origin . x != 0 {
autoCompleteViewController . view . frame . origin . x = - viewFrameInWindow . origin . x
}
autoCompleteViewController . view . frame . size . width = view . frame . width
}
}
2022-11-13 15:08:26 +01:00
private func resetImagePicker ( ) {
let selectionLimit = max ( 1 , viewModel . maxMediaAttachmentLimit - viewModel . attachmentViewModels . count )
let configuration = ComposeContentViewController . createPhotoLibraryPickerConfiguration ( selectionLimit : selectionLimit )
photoLibraryPicker = createImagePicker ( configuration : configuration )
}
private func createImagePicker ( configuration : PHPickerConfiguration ) -> PHPickerViewController {
let imagePicker = PHPickerViewController ( configuration : configuration )
imagePicker . delegate = self
return imagePicker
}
2022-10-18 13:01:31 +02:00
}
2022-10-11 12:31:40 +02:00
// MARK: - U I S c r o l l V i e w D e l e g a t e
extension ComposeContentViewController {
public func scrollViewWillEndDragging ( _ scrollView : UIScrollView , withVelocity velocity : CGPoint , targetContentOffset : UnsafeMutablePointer < CGPoint > ) {
guard scrollView = = = tableView else { return }
let replyToCellFrame = viewModel . replyToCellFrame
guard replyToCellFrame != . zero else { return }
// t r y t o f i n d s o m e p a t t e r n s :
// p r i n t ( " " "
// r e p l i e d T o C e l l F r a m e : \ ( v i e w M o d e l . r e p l i e d T o C e l l F r a m e . v a l u e . h e i g h t )
// s c r o l l V i e w . c o n t e n t O f f s e t . y : \ ( s c r o l l V i e w . c o n t e n t O f f s e t . y )
// s c r o l l V i e w . c o n t e n t S i z e . h e i g h t : \ ( s c r o l l V i e w . c o n t e n t S i z e . h e i g h t )
// s c r o l l V i e w . f r a m e : \ ( s c r o l l V i e w . f r a m e )
// s c r o l l V i e w . a d j u s t e d C o n t e n t I n s e t . t o p : \ ( s c r o l l V i e w . a d j u s t e d C o n t e n t I n s e t . t o p )
// s c r o l l V i e w . a d j u s t e d C o n t e n t I n s e t . b o t t o m : \ ( s c r o l l V i e w . a d j u s t e d C o n t e n t I n s e t . b o t t o m )
// " " " )
switch viewModel . scrollViewState {
case . fold :
logger . log ( level : . debug , " \( ( #file as NSString ) . lastPathComponent , privacy : . public ) [ \( #line , privacy : . public ) ], \( #function , privacy : . public ) : fold " )
guard velocity . y < 0 else { return }
let offsetY = scrollView . contentOffset . y + scrollView . adjustedContentInset . top
if offsetY < - 44 {
tableView . contentInset . top = 0
targetContentOffset . pointee = CGPoint ( x : 0 , y : - scrollView . adjustedContentInset . top )
viewModel . scrollViewState = . expand
}
case . expand :
logger . log ( level : . debug , " \( ( #file as NSString ) . lastPathComponent , privacy : . public ) [ \( #line , privacy : . public ) ], \( #function , privacy : . public ) : expand " )
guard velocity . y > 0 else { return }
// c h e c k i f t o p a c r o s s
let topOffset = ( scrollView . contentOffset . y + scrollView . adjustedContentInset . top ) - replyToCellFrame . height
// c h e c k i f b o t t o m b o u n c e
let bottomOffsetY = scrollView . contentOffset . y + ( scrollView . frame . height - scrollView . adjustedContentInset . bottom )
let bottomOffset = bottomOffsetY - scrollView . contentSize . height
if topOffset > 44 {
// d o n o t i n t e r r u p t u s e r s c r o l l i n g
viewModel . scrollViewState = . fold
} else if bottomOffset > 44 {
tableView . contentInset . top = - replyToCellFrame . height
targetContentOffset . pointee = CGPoint ( x : 0 , y : - replyToCellFrame . height )
viewModel . scrollViewState = . fold
}
}
2022-09-30 13:28:09 +02:00
}
}
2022-10-10 13:14:52 +02:00
// MARK: - U I T a b l e V i e w D e l e g a t e
extension ComposeContentViewController : UITableViewDelegate { }
2022-10-21 13:12:44 +02:00
// MARK: - P H P i c k e r V i e w C o n t r o l l e r D e l e g a t e
extension ComposeContentViewController : PHPickerViewControllerDelegate {
public func picker ( _ picker : PHPickerViewController , didFinishPicking results : [ PHPickerResult ] ) {
picker . dismiss ( animated : true , completion : nil )
2022-11-08 09:39:19 +01:00
let attachmentViewModels : [ AttachmentViewModel ] = results . map { result in
2022-11-08 12:40:58 +01:00
AttachmentViewModel (
api : viewModel . context . apiService ,
authContext : viewModel . authContext ,
2022-11-11 11:10:13 +01:00
input : . pickerResult ( result ) ,
delegate : viewModel
2022-11-08 12:40:58 +01:00
)
2022-11-08 09:39:19 +01:00
}
viewModel . attachmentViewModels += attachmentViewModels
2022-10-21 13:12:44 +02:00
}
}
// MARK: - U I I m a g e P i c k e r C o n t r o l l e r D e l e g a t e
extension ComposeContentViewController : UIImagePickerControllerDelegate & UINavigationControllerDelegate {
public func imagePickerController ( _ picker : UIImagePickerController , didFinishPickingMediaWithInfo info : [ UIImagePickerController . InfoKey : Any ] ) {
picker . dismiss ( animated : true , completion : nil )
guard let image = info [ . originalImage ] as ? UIImage else { return }
2022-11-13 15:40:03 +01:00
let attachmentViewModel = AttachmentViewModel (
api : viewModel . context . apiService ,
authContext : viewModel . authContext ,
input : . image ( image ) ,
delegate : viewModel
)
viewModel . attachmentViewModels += [ attachmentViewModel ]
2022-10-21 13:12:44 +02:00
}
public func imagePickerControllerDidCancel ( _ picker : UIImagePickerController ) {
os_log ( " %{public}s[%{public}ld], %{public}s " , ( ( #file as NSString ) . lastPathComponent ) , #line , #function )
picker . dismiss ( animated : true , completion : nil )
}
}
// MARK: - U I D o c u m e n t P i c k e r D e l e g a t e
extension ComposeContentViewController : UIDocumentPickerDelegate {
public func documentPicker ( _ controller : UIDocumentPickerViewController , didPickDocumentsAt urls : [ URL ] ) {
guard let url = urls . first else { return }
2022-11-13 15:40:03 +01:00
let attachmentViewModel = AttachmentViewModel (
api : viewModel . context . apiService ,
authContext : viewModel . authContext ,
input : . url ( url ) ,
delegate : viewModel
)
viewModel . attachmentViewModels += [ attachmentViewModel ]
2022-10-21 13:12:44 +02:00
}
}
// MARK: - C o m p o s e C o n t e n t T o o l b a r V i e w D e l e g a t e
extension ComposeContentViewController : ComposeContentToolbarViewDelegate {
func composeContentToolbarView (
_ viewModel : ComposeContentToolbarView . ViewModel ,
toolbarItemDidPressed action : ComposeContentToolbarView . ViewModel . Action
) {
switch action {
case . attachment :
assertionFailure ( )
case . poll :
self . viewModel . isPollActive . toggle ( )
case . emoji :
self . viewModel . isEmojiActive . toggle ( )
case . contentWarning :
self . viewModel . isContentWarningActive . toggle ( )
2022-10-28 13:06:18 +02:00
if self . viewModel . isContentWarningActive {
Task { @ MainActor in
try ? await Task . sleep ( nanoseconds : . second / 20 ) // 0 . 0 5 s
self . viewModel . setContentWarningTextViewFirstResponderIfNeeds ( )
} // e n d T a s k
} else {
if self . viewModel . contentWarningMetaText ? . textView . isFirstResponder = = true {
self . viewModel . setContentTextViewFirstResponderIfNeeds ( )
}
}
2022-10-21 13:12:44 +02:00
case . visibility :
assertionFailure ( )
}
}
func composeContentToolbarView (
_ viewModel : ComposeContentToolbarView . ViewModel ,
attachmentMenuDidPressed action : ComposeContentToolbarView . ViewModel . AttachmentAction
) {
switch action {
case . photoLibrary :
present ( photoLibraryPicker , animated : true , completion : nil )
case . camera :
present ( imagePickerController , animated : true , completion : nil )
case . browse :
#if SNAPSHOT
guard let image = UIImage ( named : " Athens " ) else { return }
let attachmentService = MastodonAttachmentService (
context : context ,
image : image ,
initialAuthenticationBox : viewModel . authenticationBox
)
viewModel . attachmentServices = viewModel . attachmentServices + [ attachmentService ]
#else
present ( documentPickerController , animated : true , completion : nil )
#endif
}
}
}
2022-11-13 09:04:29 +01:00
// MARK: - A u t o C o m p l e t e V i e w C o n t r o l l e r D e l e g a t e
extension ComposeContentViewController : AutoCompleteViewControllerDelegate {
func autoCompleteViewController (
_ viewController : AutoCompleteViewController ,
didSelectItem item : AutoCompleteItem
) {
logger . log ( level : . debug , " \( ( #file as NSString ) . lastPathComponent , privacy : . public ) [ \( #line , privacy : . public ) ], \( #function , privacy : . public ) : did select item: \( String ( describing : item ) ) " )
2022-11-13 12:42:50 +01:00
guard let info = viewModel . autoCompleteInfo else { return }
guard let metaText = viewModel . contentMetaText else { return }
let _replacedText : String ? = {
var text : String
switch item {
case . hashtag ( let hashtag ) :
text = " # " + hashtag . name
case . hashtagV1 ( let hashtagName ) :
text = " # " + hashtagName
case . account ( let account ) :
text = " @ " + account . acct
case . emoji ( let emoji ) :
text = " : " + emoji . shortcode + " : "
case . bottomLoader :
return nil
}
return text
} ( )
guard let replacedText = _replacedText else { return }
guard let text = metaText . textView . text else { return }
let range = NSRange ( info . toHighlightEndRange , in : text )
metaText . textStorage . replaceCharacters ( in : range , with : replacedText )
viewModel . autoCompleteInfo = nil
// s e t s e l e c t e d r a n g e
let newRange = NSRange ( location : range . location + ( replacedText as NSString ) . length , length : 0 )
guard metaText . textStorage . length <= newRange . location else { return }
metaText . textView . selectedRange = newRange
// a p p e n d a s p a c e a n d t r i g g e r t e x t V i e w d e l e g a t e u p d a t e
DispatchQueue . main . async {
metaText . textView . insertText ( " " )
}
}
}
// MARK: - U I C o l l e c t i o n V i e w D e l e g a t e
extension ComposeContentViewController : UICollectionViewDelegate {
public func collectionView ( _ collectionView : UICollectionView , didSelectItemAt indexPath : IndexPath ) {
os_log ( . info , log : . debug , " %{public}s[%{public}ld], %{public}s: select %s " , ( ( #file as NSString ) . lastPathComponent ) , #line , #function , indexPath . debugDescription )
switch collectionView {
case customEmojiPickerInputView . collectionView :
guard let diffableDataSource = viewModel . customEmojiPickerDiffableDataSource else { return }
let item = diffableDataSource . itemIdentifier ( for : indexPath )
guard case let . emoji ( attribute ) = item else { return }
let emoji = attribute . emoji
// m a k e c l i c k s o u n d
UIDevice . current . playInputClick ( )
// r e t r i e v e a c t i v e t e x t i n p u t a n d i n s e r t e m o j i
// t h e t r a i l i n g s p a c e i s R E Q U I R E D t o m a k e r e g e x h a p p y
_ = viewModel . customEmojiPickerInputViewModel . insertText ( " : \( emoji . shortcode ) : " )
default :
assertionFailure ( )
}
} // e n d f u n c
}
// MARK: - C o m p o s e C o n t e n t V i e w M o d e l D e l e g a t e
extension ComposeContentViewController : ComposeContentViewModelDelegate {
public func composeContentViewModel (
_ viewModel : ComposeContentViewModel ,
handleAutoComplete info : ComposeContentViewModel . AutoCompleteInfo
) -> Bool {
let snapshot = autoCompleteViewController . viewModel . diffableDataSource . snapshot ( )
guard let item = snapshot . itemIdentifiers . first else { return false }
// FIXME: r e d u n d a n t c o d e
guard let metaText = viewModel . contentMetaText else { return false }
guard let text = metaText . textView . text else { return false }
let _replacedText : String ? = {
var text : String
switch item {
2022-11-18 17:56:44 +01:00
case . hashtag , . hashtagV1 :
// d o n o f i l l t h e h a s h t a g
// a l l o w u s e r d e l e t e s u f f i x a n d p o s t t h e y w a n t
return nil
2022-11-13 12:42:50 +01:00
case . account ( let account ) :
text = " @ " + account . acct
case . emoji ( let emoji ) :
text = " : " + emoji . shortcode + " : "
case . bottomLoader :
return nil
}
return text
} ( )
guard let replacedText = _replacedText else { return false }
let range = NSRange ( info . toHighlightEndRange , in : text )
metaText . textStorage . replaceCharacters ( in : range , with : replacedText )
viewModel . autoCompleteInfo = nil
// s e t s e l e c t e d r a n g e
let newRange = NSRange ( location : range . location + ( replacedText as NSString ) . length , length : 0 )
guard metaText . textStorage . length <= newRange . location else { return true }
metaText . textView . selectedRange = newRange
// a p p e n d a s p a c e a n d t r i g g e r t e x t V i e w d e l e g a t e u p d a t e
DispatchQueue . main . async {
metaText . textView . insertText ( " " )
}
return true
2022-11-13 09:04:29 +01:00
}
}