Tusker/ShareExtension/ShareViewController.swift

158 lines
5.6 KiB
Swift
Raw Normal View History

2023-04-19 01:55:14 +00:00
//
// ShareViewController.swift
// ShareExtension
//
// Created by Shadowfacts on 4/17/23.
// Copyright © 2023 Shadowfacts. All rights reserved.
//
import SwiftUI
import UserAccounts
import ComposeUI
import UniformTypeIdentifiers
import TuskerPreferences
import Combine
import Pachyderm
2023-04-19 01:55:14 +00:00
class ShareViewController: UIViewController {
private var state: State = .loading
required init?(coder: NSCoder) {
super.init(coder: coder)
}
override func viewDidLoad() {
super.viewDidLoad()
view.tintColor = Preferences.shared.accentColor.color
if let account = UserAccountsManager.shared.getMostRecentAccount() {
Task { @MainActor in
let draft = await createDraft(account: account)
state = .ok
let context = ShareMastodonContext(accountInfo: account)
let host = ShareHostingController(draft: draft, mastodonContext: context)
host.view.translatesAutoresizingMaskIntoConstraints = false
addChild(host)
self.view.addSubview(host.view)
2023-04-19 01:55:14 +00:00
NSLayoutConstraint.activate([
host.view.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
host.view.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
host.view.topAnchor.constraint(equalTo: self.view.topAnchor),
host.view.bottomAnchor.constraint(equalTo: self.view.bottomAnchor),
2023-04-19 01:55:14 +00:00
])
host.didMove(toParent: self)
2023-04-19 01:55:14 +00:00
}
} else {
state = .notLoggedIn
}
}
private func createDraft(account: UserAccountInfo) async -> Draft {
async let (text, attachments) = getDraftConfigurationFromExtensionContext()
// TODO: I really don't like that there's a network request in the hot path here, but we don't have easy access to AccountPreferences :/
let serverPrefs = try? await Client(baseURL: account.instanceURL, accessToken: account.accessToken).run(Client.getPreferences()).0
let visibility = Preferences.shared.defaultPostVisibility.resolved(withServerDefault: serverPrefs?.postingDefaultVisibility)
2023-04-23 01:16:30 +00:00
let draft = DraftsPersistentContainer.shared.createDraft(
2023-04-19 01:55:14 +00:00
accountID: account.id,
text: await text,
2023-04-19 01:55:14 +00:00
contentWarning: "",
inReplyToID: nil,
visibility: visibility,
language: serverPrefs?.postingDefaultLanguage,
localOnly: !(serverPrefs?.postingDefaultFederation ?? true)
2023-04-19 01:55:14 +00:00
)
2023-04-23 01:16:30 +00:00
for attachment in await attachments {
2023-04-23 01:16:30 +00:00
DraftsPersistentContainer.shared.viewContext.insert(attachment)
}
draft.draftAttachments = await attachments
2023-04-23 01:16:30 +00:00
2023-04-19 01:55:14 +00:00
return draft
}
private func getDraftConfigurationFromExtensionContext() async -> (String, [DraftAttachment]) {
guard let extensionContext,
let inputItem = (extensionContext.inputItems as? [NSExtensionItem])?.first else {
2023-04-19 01:55:14 +00:00
return ("", [])
}
var text: String = ""
var url: URL?
var attachments: [DraftAttachment] = []
for itemProvider in inputItem.attachments ?? [] {
// attachments have the highest priority, but only given this heuristic
// otherwise attachment decoding ends up being overzealous
let likelyAttachment = [UTType.image, .movie].contains(where: { itemProvider.hasItemConformingToTypeIdentifier($0.identifier) })
if likelyAttachment,
let attachment: DraftAttachment = await getObject(from: itemProvider) {
attachments.append(attachment)
} else if let attached: NSURL = await getObject(from: itemProvider) {
if url == nil {
url = attached as URL
}
} else if let s: NSString = await getObject(from: itemProvider) {
if text.isEmpty {
text = s as String
}
}
}
if text.isEmpty,
let s = inputItem.attributedTitle ?? inputItem.attributedContentText {
text = s.string
}
if let url {
if !text.isEmpty {
text += "\n"
}
text += url.absoluteString
}
if !text.isEmpty {
text = "\n\n\(text)"
2023-04-19 01:55:14 +00:00
}
return (text, attachments)
2023-04-19 01:55:14 +00:00
}
private func getObject<T: NSItemProviderReading>(from itemProvider: NSItemProvider) async -> T? {
guard itemProvider.canLoadObject(ofClass: T.self) else {
return nil
}
return await withCheckedContinuation({ continuation in
itemProvider.loadObject(ofClass: T.self) { object, error in
continuation.resume(returning: object as? T)
}
})
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if case .notLoggedIn = state {
let alert = UIAlertController(title: "Not Logged In", message: "You need to log in to an account through the app before you can post.", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default, handler: { [unowned self] _ in
self.extensionContext!.cancelRequest(withError: Error.notLoggedIn)
}))
present(alert, animated: true)
}
}
enum State {
case loading
case notLoggedIn
case ok
}
enum Error: Swift.Error {
case notLoggedIn
}
}