// // NewComposeHostingController.swift // Tusker // // Created by Shadowfacts on 3/6/23. // Copyright © 2023 Shadowfacts. All rights reserved. // import SwiftUI import ComposeUI import Combine import PhotosUI import PencilKit import Pachyderm import CoreData import Duckable protocol NewComposeHostingControllerDelegate: AnyObject { func dismissCompose(mode: DismissMode) -> Bool } class NewComposeHostingController: UIHostingController, DuckableViewController { weak var delegate: NewComposeHostingControllerDelegate? weak var duckableDelegate: DuckableViewControllerDelegate? let controller: ComposeController let mastodonController: MastodonController private var assetPickerCompletion: (@MainActor ([PHPickerResult]) -> Void)? private var drawingCompletion: ((PKDrawing) -> Void)? init(draft: Draft?, mastodonController: MastodonController) { let draft = draft ?? mastodonController.createDraft() DraftsManager.shared.add(draft) self.controller = ComposeController( draft: draft, config: ComposeUIConfig(), mastodonController: mastodonController, fetchAvatar: { await ImageCache.avatars.get($0).1 }, fetchStatus: { mastodonController.persistentContainer.status(for: $0) }, displayNameLabel: { AnyView(AccountDisplayNameLabel(account: $0, textStyle: $1, emojiSize: $2)) }, replyContentView: { AnyView(ComposeReplyContentView(status: $0, mastodonController: mastodonController, heightChanged: $1)) }, emojiImageView: { AnyView(CustomEmojiImageView(emoji: $0)) } ) controller.currentAccount = mastodonController.account self.mastodonController = mastodonController super.init(rootView: View(mastodonController: mastodonController, controller: controller)) self.updateConfig() pasteConfiguration = UIPasteConfiguration(forAccepting: ComposeUI.DraftAttachment.self) NotificationCenter.default.addObserver(self, selector: #selector(updateConfig), name: .preferencesChanged, object: nil) } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } @objc private func updateConfig() { var config = ComposeUIConfig() config.backgroundColor = .appBackground config.groupedBackgroundColor = .appGroupedBackground config.groupedCellBackgroundColor = .appGroupedCellBackground config.fillColor = .appFill switch Preferences.shared.avatarStyle { case .roundRect: config.avatarStyle = .roundRect case .circle: config.avatarStyle = .circle } config.useTwitterKeyboard = Preferences.shared.useTwitterKeyboard config.contentType = Preferences.shared.statusContentType config.automaticallySaveDrafts = Preferences.shared.automaticallySaveDrafts config.requireAttachmentDescriptions = Preferences.shared.requireAttachmentDescriptions config.dismiss = { [unowned self] in self.dismiss(mode: $0) } config.presentAssetPicker = { [unowned self] in self.presentAssetPicker(completion: $0) } config.presentDrawing = { [unowned self] in self.presentDrawing($0, completion: $1) } config.userActivityForDraft = { [unowned self] in let activity = UserActivityManager.editDraftActivity(id: $0.id, accountID: self.mastodonController.accountInfo!.id) activity.displaysAuxiliaryScene = true return NSItemProvider(object: activity) } controller.config = config } override func canPaste(_ itemProviders: [NSItemProvider]) -> Bool { return controller.canPaste(itemProviders: itemProviders) } override func paste(itemProviders: [NSItemProvider]) { controller.paste(itemProviders: itemProviders) } private func dismiss(mode: DismissMode) { if delegate?.dismissCompose(mode: mode) == true { return } else { dismiss(animated: true) duckableDelegate?.duckableViewControllerWillDismiss(animated: true) } } private func presentAssetPicker(completion: @MainActor @escaping ([PHPickerResult]) -> Void) { self.assetPickerCompletion = completion var config = PHPickerConfiguration() config.selection = .ordered config.selectionLimit = 0 config.preferredAssetRepresentationMode = .compatible let picker = PHPickerViewController(configuration: config) picker.delegate = self picker.modalPresentationStyle = .pageSheet picker.overrideUserInterfaceStyle = .dark // sheet detents don't play nice with PHPickerViewController, see // let sheet = picker.sheetPresentationController! // sheet.detents = [.medium(), .large()] // sheet.prefersEdgeAttachedInCompactHeight = true // sheet.prefersGrabberVisible = true present(picker, animated: true) } private func presentDrawing(_ drawing: PKDrawing, completion: @escaping (PKDrawing) -> Void) { self.drawingCompletion = completion present(ComposeDrawingNavigationController(editing: drawing, delegate: self), animated: true) } // MARK: Duckable func duckableViewControllerWillAnimateDuck(withDuration duration: CGFloat, afterDelay delay: CGFloat) { withAnimation(.linear(duration: duration).delay(delay)) { controller.showToolbar = false } } func duckableViewControllerDidFinishAnimatingDuck() { controller.showToolbar = true } struct View: SwiftUI.View { let mastodonController: MastodonController let controller: ComposeController var body: some SwiftUI.View { ControllerView(controller: { controller }) .task { if let account = try? await mastodonController.getOwnAccount() { controller.currentAccount = account } } } } } extension MastodonController: ComposeMastodonContext { @MainActor func searchCachedAccounts(query: String) -> [AccountProtocol] { // todo: there's got to be something more efficient than this :/ let wildcardedQuery = query.map { "*\($0)" }.joined() + "*" let request: NSFetchRequest = AccountMO.fetchRequest() request.predicate = NSPredicate(format: "displayName LIKE[cd] %@ OR acct LIKE[cd] %@", wildcardedQuery, wildcardedQuery) if let results = try? persistentContainer.viewContext.fetch(request) { return results } else { return [] } } @MainActor func cachedRelationship(for accountID: String) -> RelationshipProtocol? { return persistentContainer.relationship(forAccount: accountID) } @MainActor func searchCachedHashtags(query: String) -> [Hashtag] { let wildcardedQuery = query.map { "*\($0)" }.joined() + "*" let predicate = NSPredicate(format: "name LIKE[cd] %@", wildcardedQuery) let savedReq = SavedHashtag.fetchRequest(account: accountInfo!) savedReq.predicate = predicate let followedReq = FollowedHashtag.fetchRequest() followedReq.predicate = predicate let saved = try? persistentContainer.viewContext.fetch(savedReq).map { Hashtag(name: $0.name, url: $0.url) } let followed = try? persistentContainer.viewContext.fetch(followedReq).map { Hashtag(name: $0.name, url: $0.url) } var results = saved ?? [] if let followed { results.append(contentsOf: followed) } return results } } extension NewComposeHostingController: PHPickerViewControllerDelegate { func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) { dismiss(animated: true) assetPickerCompletion?(results) assetPickerCompletion = nil } } extension NewComposeHostingController: ComposeDrawingViewControllerDelegate { func composeDrawingViewControllerClose(_ drawingController: ComposeDrawingViewController) { dismiss(animated: true) drawingCompletion = nil } func composeDrawingViewController(_ drawingController: ComposeDrawingViewController, saveDrawing drawing: PKDrawing) { dismiss(animated: true) drawingCompletion?(drawing) drawingCompletion = nil } }