Tusker/Tusker/Screens/Compose/NewComposeHostingController...

224 lines
8.5 KiB
Swift

//
// 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<NewComposeHostingController.View>, 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> = 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
}
}