253 lines
9.2 KiB
Swift
253 lines
9.2 KiB
Swift
//
|
|
// ComposeHostingController.swift
|
|
// Tusker
|
|
//
|
|
// Created by Shadowfacts on 3/6/23.
|
|
// Copyright © 2023 Shadowfacts. All rights reserved.
|
|
//
|
|
|
|
import UIKit
|
|
import SwiftUI
|
|
import ComposeUI
|
|
import Combine
|
|
import PhotosUI
|
|
import PencilKit
|
|
import Pachyderm
|
|
import CoreData
|
|
#if canImport(Duckable)
|
|
import Duckable
|
|
#endif
|
|
|
|
@MainActor
|
|
protocol ComposeHostingControllerDelegate: AnyObject {
|
|
func dismissCompose(mode: DismissMode) -> Bool
|
|
}
|
|
|
|
class ComposeHostingController: UIHostingController<ComposeHostingController.View> {
|
|
|
|
weak var delegate: ComposeHostingControllerDelegate?
|
|
|
|
let controller: ComposeController
|
|
let mastodonController: MastodonController
|
|
|
|
private var cancellables = Set<AnyCancellable>()
|
|
|
|
private var assetPickerCompletion: (@MainActor ([PHPickerResult]) -> Void)?
|
|
private var drawingCompletion: ((PKDrawing) -> Void)?
|
|
|
|
init(draft: Draft?, mastodonController: MastodonController) {
|
|
let draft = draft ?? mastodonController.createDraft()
|
|
|
|
self.controller = ComposeController(
|
|
draft: draft,
|
|
config: ComposeUIConfig(),
|
|
mastodonController: mastodonController,
|
|
fetchAvatar: { @MainActor in await ImageCache.avatars.get($0).1 },
|
|
fetchAttachment: { @MainActor in await ImageCache.attachments.get($0).1 },
|
|
fetchStatus: { mastodonController.persistentContainer.status(for: $0) },
|
|
displayNameLabel: { AnyView(AccountDisplayNameView(account: $0, textStyle: $1, emojiSize: $2)) },
|
|
replyContentView: { AnyView(ComposeReplyContentView(status: $0, mastodonController: mastodonController, heightChanged: $1)) },
|
|
emojiImageView: { AnyView(CustomEmojiImageView(emoji: $0)) }
|
|
)
|
|
|
|
self.mastodonController = mastodonController
|
|
|
|
super.init(rootView: View(mastodonController: mastodonController, controller: controller))
|
|
|
|
self.updateConfig()
|
|
|
|
pasteConfiguration = UIPasteConfiguration(forAccepting: DraftAttachment.self)
|
|
|
|
NotificationCenter.default.addObserver(self, selector: #selector(updateConfig), name: .preferencesChanged, object: nil)
|
|
|
|
// set an initial title immediately, in case we're starting ducked
|
|
self.navigationItem.title = self.controller.navigationTitle
|
|
|
|
mastodonController.$account
|
|
.sink { [unowned self] in
|
|
self.controller.currentAccount = $0
|
|
}
|
|
.store(in: &cancellables)
|
|
}
|
|
|
|
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.requireAttachmentDescriptions = Preferences.shared.requireAttachmentDescriptions
|
|
|
|
config.dismiss = { [weak 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)
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#if canImport(Duckable)
|
|
extension ComposeHostingController: DuckableViewController {
|
|
func duckableViewControllerShouldDuck() -> DuckAttemptAction {
|
|
if controller.isPosting {
|
|
return .block
|
|
} else if controller.draft.hasContent {
|
|
return .duck
|
|
} else {
|
|
return .dismiss
|
|
}
|
|
}
|
|
|
|
func duckableViewControllerWillAnimateDuck(withDuration duration: CGFloat, afterDelay delay: CGFloat) {
|
|
controller.deleteDraftOnDisappear = false
|
|
|
|
withAnimation(.linear(duration: duration).delay(delay)) {
|
|
controller.showToolbar = false
|
|
}
|
|
}
|
|
|
|
func duckableViewControllerDidFinishAnimatingDuck() {
|
|
controller.showToolbar = true
|
|
}
|
|
}
|
|
#endif
|
|
|
|
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
|
|
}
|
|
|
|
func storeCreatedStatus(_ status: Status) {
|
|
persistentContainer.addOrUpdate(status: status)
|
|
}
|
|
}
|
|
|
|
extension ComposeHostingController: PHPickerViewControllerDelegate {
|
|
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
|
|
dismiss(animated: true)
|
|
|
|
assetPickerCompletion?(results)
|
|
assetPickerCompletion = nil
|
|
}
|
|
}
|
|
|
|
extension ComposeHostingController: ComposeDrawingViewControllerDelegate {
|
|
func composeDrawingViewControllerClose(_ drawingController: ComposeDrawingViewController) {
|
|
dismiss(animated: true)
|
|
drawingCompletion = nil
|
|
}
|
|
|
|
func composeDrawingViewController(_ drawingController: ComposeDrawingViewController, saveDrawing drawing: PKDrawing) {
|
|
dismiss(animated: true)
|
|
drawingCompletion?(drawing)
|
|
drawingCompletion = nil
|
|
}
|
|
}
|