From 939429aacc648a0c09b8e6f9a97e0fe2c48e6deb Mon Sep 17 00:00:00 2001 From: CMK Date: Mon, 14 Nov 2022 00:05:43 +0800 Subject: [PATCH] feat: restore share action extension --- Mastodon.xcodeproj/project.pbxproj | 16 +- .../xcschemes/xcschememanagement.plist | 2 +- .../Scene/Compose/ComposeViewController.swift | 26 +- .../Scene/ComposeViewController.swift | 326 -------------- .../Scene/ComposeViewModel.swift | 416 ------------------ .../Scene/ShareViewController.swift | 330 ++++++++++++++ .../Scene/ShareViewModel.swift | 43 ++ 7 files changed, 386 insertions(+), 773 deletions(-) delete mode 100644 ShareActionExtension/Scene/ComposeViewController.swift delete mode 100644 ShareActionExtension/Scene/ComposeViewModel.swift create mode 100644 ShareActionExtension/Scene/ShareViewController.swift create mode 100644 ShareActionExtension/Scene/ShareViewModel.swift diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 0b7a5806a..fc8e6ff25 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -371,10 +371,10 @@ DBB8AB4F26AED13F00F6D281 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DB427DDE25BAA00100D1B89D /* Assets.xcassets */; }; DBB9759C262462E1004620BD /* ThreadMetaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB9759B262462E1004620BD /* ThreadMetaView.swift */; }; DBBE1B4525F3474B0081417A /* MastodonPickServerAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBE1B4425F3474B0081417A /* MastodonPickServerAppearance.swift */; }; - DBC6461526A170AB00B0E31B /* ComposeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBC6461426A170AB00B0E31B /* ComposeViewController.swift */; }; + DBC3872429214121001EC0FD /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBC3872329214121001EC0FD /* ShareViewController.swift */; }; DBC6461826A170AB00B0E31B /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DBC6461626A170AB00B0E31B /* MainInterface.storyboard */; }; DBC6461C26A170AB00B0E31B /* ShareActionExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = DBC6461226A170AB00B0E31B /* ShareActionExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; - DBC6462326A1712000B0E31B /* ComposeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBC6462226A1712000B0E31B /* ComposeViewModel.swift */; }; + DBC6462326A1712000B0E31B /* ShareViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBC6462226A1712000B0E31B /* ShareViewModel.swift */; }; DBC6462826A1736300B0E31B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DB427DDE25BAA00100D1B89D /* Assets.xcassets */; }; DBC7A672260C897100E57475 /* StatusContentWarningEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBC7A671260C897100E57475 /* StatusContentWarningEditorView.swift */; }; DBCA0EBC282BB38A0029E2B0 /* PageboyNavigateable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCA0EBB282BB38A0029E2B0 /* PageboyNavigateable.swift */; }; @@ -950,11 +950,11 @@ DBB9759B262462E1004620BD /* ThreadMetaView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadMetaView.swift; sourceTree = ""; }; DBBC24A726A52F9000398BB9 /* ComposeToolbarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeToolbarView.swift; sourceTree = ""; }; DBBE1B4425F3474B0081417A /* MastodonPickServerAppearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonPickServerAppearance.swift; sourceTree = ""; }; + DBC3872329214121001EC0FD /* ShareViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareViewController.swift; sourceTree = ""; }; DBC6461226A170AB00B0E31B /* ShareActionExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = ShareActionExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; - DBC6461426A170AB00B0E31B /* ComposeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeViewController.swift; sourceTree = ""; }; DBC6461726A170AB00B0E31B /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = ""; }; DBC6461926A170AB00B0E31B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - DBC6462226A1712000B0E31B /* ComposeViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ComposeViewModel.swift; sourceTree = ""; }; + DBC6462226A1712000B0E31B /* ShareViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShareViewModel.swift; sourceTree = ""; }; DBC7A671260C897100E57475 /* StatusContentWarningEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusContentWarningEditorView.swift; sourceTree = ""; }; DBC9E3A3282E13BB0063A4D9 /* eu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = eu; path = eu.lproj/Intents.strings; sourceTree = ""; }; DBC9E3A4282E13BB0063A4D9 /* eu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = eu; path = eu.lproj/InfoPlist.strings; sourceTree = ""; }; @@ -2686,8 +2686,8 @@ isa = PBXGroup; children = ( DBFEF05426A576EE006D7ED1 /* View */, - DBC6462226A1712000B0E31B /* ComposeViewModel.swift */, - DBC6461426A170AB00B0E31B /* ComposeViewController.swift */, + DBC6462226A1712000B0E31B /* ShareViewModel.swift */, + DBC3872329214121001EC0FD /* ShareViewController.swift */, ); path = Scene; sourceTree = ""; @@ -3539,9 +3539,9 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - DBC6462326A1712000B0E31B /* ComposeViewModel.swift in Sources */, + DBC6462326A1712000B0E31B /* ShareViewModel.swift in Sources */, DBB3BA2B26A81D060004F2D4 /* FLAnimatedImageView.swift in Sources */, - DBC6461526A170AB00B0E31B /* ComposeViewController.swift in Sources */, + DBC3872429214121001EC0FD /* ShareViewController.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist index 979c8c0e6..78a3a9e70 100644 --- a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist @@ -117,7 +117,7 @@ NotificationService.xcscheme_^#shared#^_ orderHint - 18 + 16 ShareActionExtension.xcscheme_^#shared#^_ diff --git a/Mastodon/Scene/Compose/ComposeViewController.swift b/Mastodon/Scene/Compose/ComposeViewController.swift index d2bc0a2cb..6de17e31f 100644 --- a/Mastodon/Scene/Compose/ComposeViewController.swift +++ b/Mastodon/Scene/Compose/ComposeViewController.swift @@ -120,9 +120,9 @@ extension ComposeViewController { guard let self = self else { return } guard self.traitCollection.userInterfaceIdiom == .pad else { return } var items = [self.publishBarButtonItem] - if self.traitCollection.horizontalSizeClass == .regular { - items.append(self.characterCountBarButtonItem) - } + // if self.traitCollection.horizontalSizeClass == .regular { + // items.append(self.characterCountBarButtonItem) + // } self.navigationItem.rightBarButtonItems = items } .store(in: &disposeBag) @@ -140,7 +140,7 @@ extension ComposeViewController { composeContentViewController.didMove(toParent: self) // bind navigation bar style - configureNavigationBarTitleStyle() + // configureNavigationBarTitleStyle() viewModel.traitCollectionDidChangePublisher .receive(on: DispatchQueue.main) .sink { [weak self] _ in @@ -163,24 +163,6 @@ extension ComposeViewController { .receive(on: DispatchQueue.main) .assign(to: \.isEnabled, on: publishButton) .store(in: &disposeBag) -// -// // bind content warning button state -// viewModel.$isContentWarningComposing -// .receive(on: DispatchQueue.main) -// .sink { [weak self] isContentWarningComposing in -// guard let self = self else { return } -// let accessibilityLabel = isContentWarningComposing ? L10n.Scene.Compose.Accessibility.disableContentWarning : L10n.Scene.Compose.Accessibility.enableContentWarning -// self.composeToolbarView.contentWarningBarButtonItem.accessibilityLabel = accessibilityLabel -// self.composeToolbarView.contentWarningButton.accessibilityLabel = accessibilityLabel -// } -// .store(in: &disposeBag) - - } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - -// viewModel.isViewAppeared = true } override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { diff --git a/ShareActionExtension/Scene/ComposeViewController.swift b/ShareActionExtension/Scene/ComposeViewController.swift deleted file mode 100644 index a7605da17..000000000 --- a/ShareActionExtension/Scene/ComposeViewController.swift +++ /dev/null @@ -1,326 +0,0 @@ -// -// ComposeViewController.swift -// MastodonShareAction -// -// Created by MainasuK Cirno on 2021-7-16. -// - -import os.log -import UIKit -import Combine -import SwiftUI -import MastodonAsset -import MastodonLocalization -import MastodonCore -import MastodonUI - -class ComposeViewController: UIViewController { - - let logger = Logger(subsystem: "ComposeViewController", category: "ViewController") - - let context = AppContext() - - var disposeBag = Set() - private(set) lazy var viewModel = ComposeViewModel(context: context) - - let publishButton: UIButton = { - let button = RoundedEdgesButton(type: .custom) - button.setTitle(L10n.Scene.Compose.composeAction, for: .normal) - button.titleLabel?.font = .systemFont(ofSize: 14, weight: .bold) - button.setBackgroundImage(.placeholder(color: Asset.Colors.brand.color), for: .normal) - button.setBackgroundImage(.placeholder(color: Asset.Colors.brand.color.withAlphaComponent(0.5)), for: .highlighted) - button.setBackgroundImage(.placeholder(color: Asset.Colors.Button.disabled.color), for: .disabled) - button.setTitleColor(.white, for: .normal) - button.contentEdgeInsets = UIEdgeInsets(top: 6, left: 16, bottom: 5, right: 16) // set 28pt height - button.adjustsImageWhenHighlighted = false - return button - }() - - private(set) lazy var cancelBarButtonItem = UIBarButtonItem(title: L10n.Common.Controls.Actions.cancel, style: .plain, target: self, action: #selector(ComposeViewController.cancelBarButtonItemPressed(_:))) - private(set) lazy var publishBarButtonItem: UIBarButtonItem = { - let barButtonItem = UIBarButtonItem(customView: publishButton) - publishButton.addTarget(self, action: #selector(ComposeViewController.publishBarButtonItemPressed(_:)), for: .touchUpInside) - return barButtonItem - }() - - let activityIndicatorBarButtonItem: UIBarButtonItem = { - let indicatorView = UIActivityIndicatorView(style: .medium) - let barButtonItem = UIBarButtonItem(customView: indicatorView) - indicatorView.startAnimating() - return barButtonItem - }() - - -// let viewSafeAreaDidChange = PassthroughSubject() -// let composeToolbarView = ComposeToolbarView() -// var composeToolbarViewBottomLayoutConstraint: NSLayoutConstraint! -// let composeToolbarBackgroundView = UIView() -} - -extension ComposeViewController { - - override func viewDidLoad() { - super.viewDidLoad() - -// navigationController?.presentationController?.delegate = self -// -// setupBackgroundColor(theme: ThemeService.shared.currentTheme.value) -// ThemeService.shared.currentTheme -// .receive(on: DispatchQueue.main) -// .sink { [weak self] theme in -// guard let self = self else { return } -// self.setupBackgroundColor(theme: theme) -// } -// .store(in: &disposeBag) -// -// navigationItem.leftBarButtonItem = cancelBarButtonItem -// viewModel.isBusy -// .receive(on: DispatchQueue.main) -// .sink { [weak self] isBusy in -// guard let self = self else { return } -// self.navigationItem.rightBarButtonItem = isBusy ? self.activityIndicatorBarButtonItem : self.publishBarButtonItem -// } -// .store(in: &disposeBag) -// -// let hostingViewController = UIHostingController( -// rootView: ComposeView().environmentObject(viewModel.composeViewModel) -// ) -// addChild(hostingViewController) -// view.addSubview(hostingViewController.view) -// hostingViewController.view.translatesAutoresizingMaskIntoConstraints = false -// view.addSubview(hostingViewController.view) -// NSLayoutConstraint.activate([ -// hostingViewController.view.topAnchor.constraint(equalTo: view.topAnchor), -// hostingViewController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor), -// hostingViewController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor), -// hostingViewController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor), -// ]) -// hostingViewController.didMove(toParent: self) -// -// composeToolbarView.translatesAutoresizingMaskIntoConstraints = false -// view.addSubview(composeToolbarView) -// composeToolbarViewBottomLayoutConstraint = view.bottomAnchor.constraint(equalTo: composeToolbarView.bottomAnchor) -// NSLayoutConstraint.activate([ -// composeToolbarView.leadingAnchor.constraint(equalTo: view.leadingAnchor), -// composeToolbarView.trailingAnchor.constraint(equalTo: view.trailingAnchor), -// composeToolbarViewBottomLayoutConstraint, -// composeToolbarView.heightAnchor.constraint(equalToConstant: ComposeToolbarView.toolbarHeight), -// ]) -// composeToolbarView.preservesSuperviewLayoutMargins = true -// composeToolbarView.delegate = self -// -// composeToolbarBackgroundView.translatesAutoresizingMaskIntoConstraints = false -// view.insertSubview(composeToolbarBackgroundView, belowSubview: composeToolbarView) -// NSLayoutConstraint.activate([ -// composeToolbarBackgroundView.topAnchor.constraint(equalTo: composeToolbarView.topAnchor), -// composeToolbarBackgroundView.leadingAnchor.constraint(equalTo: composeToolbarView.leadingAnchor), -// composeToolbarBackgroundView.trailingAnchor.constraint(equalTo: composeToolbarView.trailingAnchor), -// view.bottomAnchor.constraint(equalTo: composeToolbarBackgroundView.bottomAnchor), -// ]) -// -// // FIXME: using iOS 15 toolbar for .keyboard placement -// let keyboardEventPublishers = Publishers.CombineLatest3( -// KeyboardResponderService.shared.isShow, -// KeyboardResponderService.shared.state, -// KeyboardResponderService.shared.endFrame -// ) -// -// Publishers.CombineLatest( -// keyboardEventPublishers, -// viewSafeAreaDidChange -// ) -// .sink(receiveValue: { [weak self] keyboardEvents, _ in -// guard let self = self else { return } -// -// let (isShow, state, endFrame) = keyboardEvents -// guard isShow, state == .dock else { -// UIView.animate(withDuration: 0.3) { -// self.composeToolbarViewBottomLayoutConstraint.constant = self.view.safeAreaInsets.bottom -// self.view.layoutIfNeeded() -// } -// return -// } -// // isShow AND dock state -// -// UIView.animate(withDuration: 0.3) { -// self.composeToolbarViewBottomLayoutConstraint.constant = endFrame.height -// self.view.layoutIfNeeded() -// } -// }) -// .store(in: &disposeBag) -// -// // bind visibility toolbar UI -// Publishers.CombineLatest( -// viewModel.selectedStatusVisibility, -// viewModel.traitCollectionDidChangePublisher -// ) -// .receive(on: DispatchQueue.main) -// .sink { [weak self] type, _ in -// guard let self = self else { return } -// let image = type.image(interfaceStyle: self.traitCollection.userInterfaceStyle) -// self.composeToolbarView.visibilityButton.setImage(image, for: .normal) -// self.composeToolbarView.activeVisibilityType.value = type -// } -// .store(in: &disposeBag) -// -// // bind counter -// viewModel.characterCount -// .receive(on: DispatchQueue.main) -// .sink { [weak self] characterCount in -// guard let self = self else { return } -// let count = ShareViewModel.composeContentLimit - characterCount -// self.composeToolbarView.characterCountLabel.text = "\(count)" -// switch count { -// case _ where count < 0: -// self.composeToolbarView.characterCountLabel.font = .monospacedDigitSystemFont(ofSize: 24, weight: .bold) -// self.composeToolbarView.characterCountLabel.textColor = Asset.Colors.danger.color -// self.composeToolbarView.characterCountLabel.accessibilityLabel = L10n.A11y.Plural.Count.inputLimitExceeds(abs(count)) -// default: -// self.composeToolbarView.characterCountLabel.font = .monospacedDigitSystemFont(ofSize: 15, weight: .regular) -// self.composeToolbarView.characterCountLabel.textColor = Asset.Colors.Label.secondary.color -// self.composeToolbarView.characterCountLabel.accessibilityLabel = L10n.A11y.Plural.Count.inputLimitRemains(count) -// } -// } -// .store(in: &disposeBag) -// -// // bind valid -// viewModel.isValid -// .receive(on: DispatchQueue.main) -// .assign(to: \.isEnabled, on: publishButton) -// .store(in: &disposeBag) - } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - -// viewModel.viewDidAppear.value = true -// viewModel.inputItems.value = extensionContext?.inputItems.compactMap { $0 as? NSExtensionItem } ?? [] -// -// viewModel.composeViewModel.viewDidAppear = true - } - - override func viewSafeAreaInsetsDidChange() { - super.viewSafeAreaInsetsDidChange() - -// viewSafeAreaDidChange.send() - } - - override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - -// viewModel.traitCollectionDidChangePublisher.send() - } - -} - -//extension ComposeViewController { -// private func setupBackgroundColor(theme: Theme) { -// view.backgroundColor = theme.systemElevatedBackgroundColor -// viewModel.composeViewModel.backgroundColor = theme.systemElevatedBackgroundColor -// composeToolbarBackgroundView.backgroundColor = theme.composeToolbarBackgroundColor -// -// let barAppearance = UINavigationBarAppearance() -// barAppearance.configureWithDefaultBackground() -// barAppearance.backgroundColor = theme.navigationBarBackgroundColor -// navigationItem.standardAppearance = barAppearance -// navigationItem.compactAppearance = barAppearance -// navigationItem.scrollEdgeAppearance = barAppearance -// } -// -// private func showDismissConfirmAlertController() { -// let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) // can not use alert in extension -// let discardAction = UIAlertAction(title: L10n.Common.Controls.Actions.discard, style: .destructive) { _ in -// self.extensionContext?.cancelRequest(withError: ShareViewModel.ShareError.userCancelShare) -// } -// alertController.addAction(discardAction) -// let okAction = UIAlertAction(title: L10n.Common.Controls.Actions.ok, style: .cancel, handler: nil) -// alertController.addAction(okAction) -// self.present(alertController, animated: true, completion: nil) -// } -//} -// -extension ComposeViewController { - @objc private func cancelBarButtonItemPressed(_ sender: UIBarButtonItem) { - logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") - -// showDismissConfirmAlertController() - } - - @objc private func publishBarButtonItemPressed(_ sender: UIBarButtonItem) { - logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") - -// viewModel.isPublishing.value = true -// -// viewModel.publish() -// .delay(for: 2, scheduler: DispatchQueue.main) -// .receive(on: DispatchQueue.main) -// .sink { [weak self] completion in -// guard let self = self else { return } -// self.viewModel.isPublishing.value = false -// -// switch completion { -// case .failure: -// let alertController = UIAlertController( -// title: L10n.Common.Alerts.PublishPostFailure.title, -// message: L10n.Common.Alerts.PublishPostFailure.message, -// preferredStyle: .actionSheet // can not use alert in extension -// ) -// let okAction = UIAlertAction( -// title: L10n.Common.Controls.Actions.ok, -// style: .cancel, -// handler: nil -// ) -// alertController.addAction(okAction) -// self.present(alertController, animated: true, completion: nil) -// case .finished: -// self.publishButton.setTitle(L10n.Common.Controls.Actions.done, for: .normal) -// self.publishButton.isUserInteractionEnabled = false -// DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [weak self] in -// guard let self = self else { return } -// self.extensionContext?.completeRequest(returningItems: nil, completionHandler: nil) -// } -// } -// } receiveValue: { response in -// // do nothing -// } -// .store(in: &disposeBag) - } -} - -//// MARK - ComposeToolbarViewDelegate -//extension ComposeViewController: ComposeToolbarViewDelegate { -// -// func composeToolbarView(_ composeToolbarView: ComposeToolbarView, contentWarningButtonDidPressed sender: UIButton) { -// logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") -// -// withAnimation { -// viewModel.composeViewModel.isContentWarningComposing.toggle() -// } -// } -// -// func composeToolbarView(_ composeToolbarView: ComposeToolbarView, visibilityButtonDidPressed sender: UIButton, visibilitySelectionType type: ComposeToolbarView.VisibilitySelectionType) { -// logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") -// -// viewModel.selectedStatusVisibility.value = type -// } -// -//} -// -//// MARK: - UIAdaptivePresentationControllerDelegate -//extension ComposeViewController: UIAdaptivePresentationControllerDelegate { -// -// func presentationControllerShouldDismiss(_ presentationController: UIPresentationController) -> Bool { -// return viewModel.shouldDismiss.value -// } -// -// func presentationControllerDidAttemptToDismiss(_ presentationController: UIPresentationController) { -// os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) -// showDismissConfirmAlertController() -// -// } -// -// func presentationControllerDidDismiss(_ presentationController: UIPresentationController) { -// os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) -// } -// -//} diff --git a/ShareActionExtension/Scene/ComposeViewModel.swift b/ShareActionExtension/Scene/ComposeViewModel.swift deleted file mode 100644 index 93515b7dc..000000000 --- a/ShareActionExtension/Scene/ComposeViewModel.swift +++ /dev/null @@ -1,416 +0,0 @@ -// -// ComposeViewModel.swift -// MastodonShareAction -// -// Created by MainasuK Cirno on 2021-7-16. -// - -import os.log -import Foundation -import Combine -import CoreData -import CoreDataStack -import MastodonSDK -import SwiftUI -import UniformTypeIdentifiers -import MastodonAsset -import MastodonLocalization -import MastodonUI -import MastodonCore - -final class ComposeViewModel { - - let logger = Logger(subsystem: "ComposeViewModel", category: "ViewModel") - - var disposeBag = Set() - - static let composeContentLimit: Int = 500 - - // input - let context: AppContext - -// private var coreDataStack: CoreDataStack? -// var managedObjectContext: NSManagedObjectContext? -// var api: APIService? -// -// var inputItems = CurrentValueSubject<[NSExtensionItem], Never>([]) -// let viewDidAppear = CurrentValueSubject(false) -// let traitCollectionDidChangePublisher = CurrentValueSubject(Void()) // use CurrentValueSubject to make initial event emit -// let selectedStatusVisibility = CurrentValueSubject(.public) -// -// // output -// let authentication = CurrentValueSubject?, Never>(nil) -// let isFetchAuthentication = CurrentValueSubject(true) -// let isPublishing = CurrentValueSubject(false) -// let isBusy = CurrentValueSubject(true) -// let isValid = CurrentValueSubject(false) -// let shouldDismiss = CurrentValueSubject(true) -// let composeViewModel = ComposeViewModel() -// let characterCount = CurrentValueSubject(0) - - init(context: AppContext) { - self.context = context - // end init - -// viewDidAppear.receive(on: DispatchQueue.main) -// .removeDuplicates() -// .sink { [weak self] viewDidAppear in -// guard let self = self else { return } -// guard viewDidAppear else { return } -// self.setupCoreData() -// } -// .store(in: &disposeBag) -// -// Publishers.CombineLatest( -// inputItems.removeDuplicates(), -// viewDidAppear.removeDuplicates() -// ) -// .receive(on: DispatchQueue.main) -// .sink { [weak self] inputItems, _ in -// guard let self = self else { return } -// self.parse(inputItems: inputItems) -// } -// .store(in: &disposeBag) -// -// // bind authentication loading state -// authentication -// .map { result in result == nil } -// .assign(to: \.value, on: isFetchAuthentication) -// .store(in: &disposeBag) -// -// // bind user locked state -// authentication -// .compactMap { result -> Bool? in -// guard let result = result else { return nil } -// switch result { -// case .success(let authentication): -// return authentication.user.locked -// case .failure: -// return nil -// } -// } -// .map { locked -> ComposeToolbarView.VisibilitySelectionType in -// locked ? .private : .public -// } -// .assign(to: \.value, on: selectedStatusVisibility) -// .store(in: &disposeBag) -// -// // bind author -// authentication -// .receive(on: DispatchQueue.main) -// .sink { [weak self] result in -// guard let self = self else { return } -// guard let result = result else { return } -// switch result { -// case .success(let authentication): -// self.composeViewModel.avatarImageURL = authentication.user.avatarImageURL() -// self.composeViewModel.authorName = authentication.user.displayNameWithFallback -// self.composeViewModel.authorUsername = "@" + authentication.user.username -// case .failure: -// self.composeViewModel.avatarImageURL = nil -// self.composeViewModel.authorName = " " -// self.composeViewModel.authorUsername = " " -// } -// } -// .store(in: &disposeBag) -// -// // bind authentication to compose view model -// authentication -// .map { result -> MastodonAuthentication? in -// guard let result = result else { return nil } -// switch result { -// case .success(let authentication): -// return authentication -// case .failure: -// return nil -// } -// } -// .assign(to: &composeViewModel.$authentication) -// -// // bind isBusy -// Publishers.CombineLatest( -// isFetchAuthentication, -// isPublishing -// ) -// .receive(on: DispatchQueue.main) -// .map { $0 || $1 } -// .assign(to: \.value, on: isBusy) -// .store(in: &disposeBag) -// -// // pass initial i18n string -// composeViewModel.statusPlaceholder = L10n.Scene.Compose.contentInputPlaceholder -// composeViewModel.contentWarningPlaceholder = L10n.Scene.Compose.ContentWarning.placeholder -// composeViewModel.toolbarHeight = ComposeToolbarView.toolbarHeight -// -// // bind compose bar button item UI state -// let isComposeContentEmpty = composeViewModel.$statusContent -// .map { $0.isEmpty } -// -// isComposeContentEmpty -// .assign(to: \.value, on: shouldDismiss) -// .store(in: &disposeBag) -// -// let isComposeContentValid = composeViewModel.$characterCount -// .map { characterCount -> Bool in -// return characterCount <= ShareViewModel.composeContentLimit -// } -// let isMediaEmpty = composeViewModel.$attachmentViewModels -// .map { $0.isEmpty } -// let isMediaUploadAllSuccess = composeViewModel.$attachmentViewModels -// .map { viewModels in -// viewModels.allSatisfy { $0.uploadStateMachineSubject.value is StatusAttachmentViewModel.UploadState.Finish } -// } -// -// let isPublishBarButtonItemEnabledPrecondition1 = Publishers.CombineLatest4( -// isComposeContentEmpty, -// isComposeContentValid, -// isMediaEmpty, -// isMediaUploadAllSuccess -// ) -// .map { isComposeContentEmpty, isComposeContentValid, isMediaEmpty, isMediaUploadAllSuccess -> Bool in -// if isMediaEmpty { -// return isComposeContentValid && !isComposeContentEmpty -// } else { -// return isComposeContentValid && isMediaUploadAllSuccess -// } -// } -// .eraseToAnyPublisher() -// -// let isPublishBarButtonItemEnabledPrecondition2 = Publishers.CombineLatest( -// isComposeContentEmpty, -// isComposeContentValid -// ) -// .map { isComposeContentEmpty, isComposeContentValid -> Bool in -// return isComposeContentValid && !isComposeContentEmpty -// } -// .eraseToAnyPublisher() -// -// Publishers.CombineLatest( -// isPublishBarButtonItemEnabledPrecondition1, -// isPublishBarButtonItemEnabledPrecondition2 -// ) -// .map { $0 && $1 } -// .assign(to: \.value, on: isValid) -// .store(in: &disposeBag) -// -// // bind counter -// composeViewModel.$characterCount -// .assign(to: \.value, on: characterCount) -// .store(in: &disposeBag) -// -// // setup theme -// setupBackgroundColor(theme: ThemeService.shared.currentTheme.value) -// ThemeService.shared.currentTheme -// .receive(on: DispatchQueue.main) -// .sink { [weak self] theme in -// guard let self = self else { return } -// self.setupBackgroundColor(theme: theme) -// } -// .store(in: &disposeBag) - } - - private func setupBackgroundColor(theme: Theme) { -// composeViewModel.contentWarningBackgroundColor = Color(theme.contentWarningOverlayBackgroundColor) - } - -} - -//extension ShareViewModel { -// enum ShareError: Error { -// case `internal`(error: Error) -// case userCancelShare -// case missingAuthentication -// } -//} - -extension ComposeViewModel { -// private func setupCoreData() { -// logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") -// DispatchQueue.global().async { -// let _coreDataStack = CoreDataStack() -// self.coreDataStack = _coreDataStack -// self.managedObjectContext = _coreDataStack.persistentContainer.viewContext -// -// _coreDataStack.didFinishLoad -// .receive(on: RunLoop.main) -// .sink { [weak self] didFinishLoad in -// guard let self = self else { return } -// guard didFinishLoad else { return } -// guard let managedObjectContext = self.managedObjectContext else { return } -// -// -// self.api = APIService(backgroundManagedObjectContext: _coreDataStack.newTaskContext()) -// -// self.logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fetch authentication…") -// managedObjectContext.perform { -// do { -// let request = MastodonAuthentication.sortedFetchRequest -// let authentications = try managedObjectContext.fetch(request) -// let authentication = authentications.sorted(by: { $0.activedAt > $1.activedAt }).first -// guard let activeAuthentication = authentication else { -// self.authentication.value = .failure(ShareError.missingAuthentication) -// return -// } -// self.authentication.value = .success(activeAuthentication) -// self.logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fetch authentication success \(activeAuthentication.userID)") -// } catch { -// self.authentication.value = .failure(ShareError.internal(error: error)) -// self.logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fetch authentication fail \(error.localizedDescription)") -// assertionFailure(error.localizedDescription) -// } -// } -// } -// .store(in: &self.disposeBag) -// } -// } -} - -//extension ShareViewModel { -// func parse(inputItems: [NSExtensionItem]) { -// var itemProviders: [NSItemProvider] = [] -// -// for item in inputItems { -// itemProviders.append(contentsOf: item.attachments ?? []) -// } -// -// let _textProvider = itemProviders.first { provider in -// return provider.hasRepresentationConforming(toTypeIdentifier: UTType.plainText.identifier, fileOptions: []) -// } -// -// let _urlProvider = itemProviders.first { provider in -// return provider.hasRepresentationConforming(toTypeIdentifier: UTType.url.identifier, fileOptions: []) -// } -// -// let _movieProvider = itemProviders.first { provider in -// return provider.hasRepresentationConforming(toTypeIdentifier: UTType.movie.identifier, fileOptions: []) -// } -// -// let imageProviders = itemProviders.filter { provider in -// return provider.hasRepresentationConforming(toTypeIdentifier: UTType.image.identifier, fileOptions: []) -// } -// -// Task { @MainActor in -// async let text = ShareViewModel.loadText(textProvider: _textProvider) -// async let url = ShareViewModel.loadURL(textProvider: _urlProvider) -// -// let content = await [text, url] -// .compactMap { $0 } -// .joined(separator: " ") -// self.composeViewModel.statusContent = content -// } -// -// guard let api = self.api else { return } -// -// if let movieProvider = _movieProvider { -// composeViewModel.setupAttachmentViewModels([ -// StatusAttachmentViewModel(api: api, itemProvider: movieProvider) -// ]) -// } else if !imageProviders.isEmpty { -// let viewModels = imageProviders.map { provider in -// StatusAttachmentViewModel(api: api, itemProvider: provider) -// } -// composeViewModel.setupAttachmentViewModels(viewModels) -// } -// -// } -// -// private static func loadText(textProvider: NSItemProvider?) async -> String? { -// guard let textProvider = textProvider else { return nil } -// do { -// let item = try await textProvider.loadItem(forTypeIdentifier: UTType.plainText.identifier) -// guard let text = item as? String else { return nil } -// return text -// } catch { -// return nil -// } -// } -// -// private static func loadURL(textProvider: NSItemProvider?) async -> String? { -// guard let textProvider = textProvider else { return nil } -// do { -// let item = try await textProvider.loadItem(forTypeIdentifier: UTType.url.identifier) -// guard let url = item as? URL else { return nil } -// return url.absoluteString -// } catch { -// return nil -// } -// } -// -//} -// -//extension ShareViewModel { -// func publish() -> AnyPublisher, Error> { -// guard let authentication = composeViewModel.authentication, -// let api = self.api -// else { -// return Fail(error: APIService.APIError.implicit(.authenticationMissing)).eraseToAnyPublisher() -// } -// let authenticationBox = MastodonAuthenticationBox( -// authenticationRecord: .init(objectID: authentication.objectID), -// domain: authentication.domain, -// userID: authentication.userID, -// appAuthorization: Mastodon.API.OAuth.Authorization(accessToken: authentication.appAccessToken), -// userAuthorization: Mastodon.API.OAuth.Authorization(accessToken: authentication.userAccessToken) -// ) -// -// let domain = authentication.domain -// let attachmentViewModels = composeViewModel.attachmentViewModels -// let mediaIDs = attachmentViewModels.compactMap { viewModel in -// viewModel.attachment.value?.id -// } -// let sensitive: Bool = composeViewModel.isContentWarningComposing -// let spoilerText: String? = { -// let text = composeViewModel.contentWarningContent -// guard !text.isEmpty else { return nil } -// return text -// }() -// let visibility = selectedStatusVisibility.value.visibility -// -// let updateMediaQuerySubscriptions: [AnyPublisher, Error>] = { -// var subscriptions: [AnyPublisher, Error>] = [] -// for attachmentViewModel in attachmentViewModels { -// guard let attachmentID = attachmentViewModel.attachment.value?.id else { continue } -// let description = attachmentViewModel.descriptionContent.trimmingCharacters(in: .whitespacesAndNewlines) -// guard !description.isEmpty else { continue } -// let query = Mastodon.API.Media.UpdateMediaQuery( -// file: nil, -// thumbnail: nil, -// description: description, -// focus: nil -// ) -// let subscription = api.updateMedia( -// domain: domain, -// attachmentID: attachmentID, -// query: query, -// mastodonAuthenticationBox: authenticationBox -// ) -// subscriptions.append(subscription) -// } -// return subscriptions -// }() -// -// let status = composeViewModel.statusContent -// -// return Publishers.MergeMany(updateMediaQuerySubscriptions) -// .collect() -// .asyncMap { attachments in -// let query = Mastodon.API.Statuses.PublishStatusQuery( -// status: status, -// mediaIDs: mediaIDs.isEmpty ? nil : mediaIDs, -// pollOptions: nil, -// pollExpiresIn: nil, -// inReplyToID: nil, -// sensitive: sensitive, -// spoilerText: spoilerText, -// visibility: visibility -// ) -// return try await api.publishStatus( -// domain: domain, -// idempotencyKey: nil, // FIXME: -// query: query, -// authenticationBox: authenticationBox -// ) -// } -// .eraseToAnyPublisher() -// } -//} diff --git a/ShareActionExtension/Scene/ShareViewController.swift b/ShareActionExtension/Scene/ShareViewController.swift new file mode 100644 index 000000000..7757452cd --- /dev/null +++ b/ShareActionExtension/Scene/ShareViewController.swift @@ -0,0 +1,330 @@ +// +// ShareViewController.swift +// ShareActionExtension +// +// Created by MainasuK on 2022/11/13. +// + +import os.log +import UIKit +import Combine +import CoreDataStack +import MastodonCore +import MastodonUI +import MastodonAsset +import MastodonLocalization +import UniformTypeIdentifiers + +final class ShareViewController: UIViewController { + + let logger = Logger(subsystem: "ShareViewController", category: "ViewController") + + var disposeBag = Set() + + let context = AppContext() + private(set) lazy var viewModel = ShareViewModel(context: context) + + let publishButton: UIButton = { + let button = RoundedEdgesButton(type: .custom) + button.cornerRadius = 10 + button.contentEdgeInsets = UIEdgeInsets(top: 6, left: 16, bottom: 5, right: 16) // set 28pt height + button.titleLabel?.font = .systemFont(ofSize: 14, weight: .bold) + button.setTitle(L10n.Scene.Compose.composeAction, for: .normal) + return button + }() + private func configurePublishButtonApperance() { + publishButton.adjustsImageWhenHighlighted = false + publishButton.setBackgroundImage(.placeholder(color: Asset.Colors.Label.primary.color), for: .normal) + publishButton.setBackgroundImage(.placeholder(color: Asset.Colors.Label.primary.color.withAlphaComponent(0.5)), for: .highlighted) + publishButton.setBackgroundImage(.placeholder(color: Asset.Colors.Button.disabled.color), for: .disabled) + publishButton.setTitleColor(Asset.Colors.Label.primaryReverse.color, for: .normal) + } + + private(set) lazy var cancelBarButtonItem = UIBarButtonItem(title: L10n.Common.Controls.Actions.cancel, style: .plain, target: self, action: #selector(ShareViewController.cancelBarButtonItemPressed(_:))) + private(set) lazy var publishBarButtonItem: UIBarButtonItem = { + let barButtonItem = UIBarButtonItem(customView: publishButton) + publishButton.addTarget(self, action: #selector(ShareViewController.publishBarButtonItemPressed(_:)), for: .touchUpInside) + return barButtonItem + }() + let activityIndicatorBarButtonItem: UIBarButtonItem = { + let indicatorView = UIActivityIndicatorView(style: .medium) + let barButtonItem = UIBarButtonItem(customView: indicatorView) + indicatorView.startAnimating() + return barButtonItem + }() + + private var composeContentViewModel: ComposeContentViewModel? + private var composeContentViewController: ComposeContentViewController? + + let notSignInLabel: UILabel = { + let label = UILabel() + label.font = .preferredFont(forTextStyle: .subheadline) + label.textColor = .secondaryLabel + label.text = "No Available Account" // TODO: i18n + return label + }() + +} + +extension ShareViewController { + override func viewDidLoad() { + super.viewDidLoad() + + setupTheme(theme: ThemeService.shared.currentTheme.value) + ThemeService.shared.apply(theme: ThemeService.shared.currentTheme.value) + ThemeService.shared.currentTheme + .receive(on: DispatchQueue.main) + .sink { [weak self] theme in + guard let self = self else { return } + self.setupTheme(theme: theme) + } + .store(in: &disposeBag) + + view.backgroundColor = .systemBackground + title = L10n.Scene.Compose.Title.newPost + + navigationItem.leftBarButtonItem = cancelBarButtonItem + navigationItem.rightBarButtonItem = publishBarButtonItem + + do { + guard let authContext = try setupAuthContext() else { + setupHintLabel() + return + } + viewModel.authContext = authContext + let composeContentViewModel = ComposeContentViewModel( + context: context, + authContext: authContext, + kind: .post + ) + let composeContentViewController = ComposeContentViewController() + composeContentViewController.viewModel = composeContentViewModel + addChild(composeContentViewController) + composeContentViewController.view.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(composeContentViewController.view) + NSLayoutConstraint.activate([ + composeContentViewController.view.topAnchor.constraint(equalTo: view.topAnchor), + composeContentViewController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor), + composeContentViewController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor), + composeContentViewController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + composeContentViewController.didMove(toParent: self) + + self.composeContentViewModel = composeContentViewModel + self.composeContentViewController = composeContentViewController + + Task { @MainActor in + let inputItems = self.extensionContext?.inputItems.compactMap { $0 as? NSExtensionItem } ?? [] + await load(inputItems: inputItems) + } // end Task + } catch { + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): error: \(error.localizedDescription)") + } + + viewModel.$isPublishing + .receive(on: DispatchQueue.main) + .sink { [weak self] isBusy in + guard let self = self else { return } + self.navigationItem.rightBarButtonItem = isBusy ? self.activityIndicatorBarButtonItem : self.publishBarButtonItem + } + .store(in: &disposeBag) + } + + override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + super.traitCollectionDidChange(previousTraitCollection) + + configurePublishButtonApperance() + } +} + +extension ShareViewController { + @objc private func cancelBarButtonItemPressed(_ sender: UIBarButtonItem) { + logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") + + extensionContext?.cancelRequest(withError: NSError(domain: "org.joinmastodon.app.ShareActionExtension", code: -1)) + } + + @objc private func publishBarButtonItemPressed(_ sender: UIBarButtonItem) { + logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") + + + Task { @MainActor in + viewModel.isPublishing = true + do { + guard let statusPublisher = try composeContentViewModel?.statusPublisher(), + let authContext = viewModel.authContext + else { + throw AppError.badRequest + } + + _ = try await statusPublisher.publish(api: context.apiService, authContext: authContext) + + self.publishButton.setTitle(L10n.Common.Controls.Actions.done, for: .normal) + try await Task.sleep(nanoseconds: 1 * .second) + + self.extensionContext?.completeRequest(returningItems: nil, completionHandler: nil) + + } catch { + let alertController = UIAlertController.standardAlert(of: error) + present(alertController, animated: true) + return + } + viewModel.isPublishing = false + + } + } +} + +extension ShareViewController { + private func setupAuthContext() throws -> AuthContext? { + let request = MastodonAuthentication.activeSortedFetchRequest // use active order + let _authentication = try context.managedObjectContext.fetch(request).first + let _authContext = _authentication.flatMap { AuthContext(authentication: $0) } + return _authContext + } + + private func setupHintLabel() { + notSignInLabel.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(notSignInLabel) + NSLayoutConstraint.activate([ + notSignInLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor), + notSignInLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor), + ]) + } + + private func setupTheme(theme: Theme) { + view.backgroundColor = theme.systemElevatedBackgroundColor + + let barAppearance = UINavigationBarAppearance() + barAppearance.configureWithDefaultBackground() + barAppearance.backgroundColor = theme.navigationBarBackgroundColor + navigationItem.standardAppearance = barAppearance + navigationItem.compactAppearance = barAppearance + navigationItem.scrollEdgeAppearance = barAppearance + } + + private func showDismissConfirmAlertController() { + let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) // can not use alert in extension + let discardAction = UIAlertAction(title: L10n.Common.Controls.Actions.discard, style: .destructive) { _ in + self.extensionContext?.cancelRequest(withError: ShareError.userCancelShare) + } + alertController.addAction(discardAction) + let okAction = UIAlertAction(title: L10n.Common.Controls.Actions.ok, style: .cancel, handler: nil) + alertController.addAction(okAction) + self.present(alertController, animated: true, completion: nil) + } +} + +// MARK: - UIAdaptivePresentationControllerDelegate +extension ShareViewController: UIAdaptivePresentationControllerDelegate { + + func presentationControllerShouldDismiss(_ presentationController: UIPresentationController) -> Bool { + return composeContentViewModel?.shouldDismiss ?? true + } + + func presentationControllerDidAttemptToDismiss(_ presentationController: UIPresentationController) { + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) + showDismissConfirmAlertController() + } + + func presentationControllerDidDismiss(_ presentationController: UIPresentationController) { + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) + } + +} + +extension ShareViewController { + + private func load(inputItems: [NSExtensionItem]) async { + guard let composeContentViewModel = self.composeContentViewModel, + let authContext = viewModel.authContext + else { + assertionFailure() + return + } + var itemProviders: [NSItemProvider] = [] + + for item in inputItems { + itemProviders.append(contentsOf: item.attachments ?? []) + } + + let _textProvider = itemProviders.first { provider in + return provider.hasRepresentationConforming(toTypeIdentifier: UTType.plainText.identifier, fileOptions: []) + } + + let _urlProvider = itemProviders.first { provider in + return provider.hasRepresentationConforming(toTypeIdentifier: UTType.url.identifier, fileOptions: []) + } + + let _movieProvider = itemProviders.first { provider in + return provider.hasRepresentationConforming(toTypeIdentifier: UTType.movie.identifier, fileOptions: []) + } + + let imageProviders = itemProviders.filter { provider in + return provider.hasRepresentationConforming(toTypeIdentifier: UTType.image.identifier, fileOptions: []) + } + + async let text = ShareViewController.loadText(textProvider: _textProvider) + async let url = ShareViewController.loadURL(textProvider: _urlProvider) + + let content = await [text, url] + .compactMap { $0 } + .joined(separator: " ") + // passby the viewModel `content` value + if !content.isEmpty { + composeContentViewModel.content = content + " " + composeContentViewModel.contentMetaText?.textView.insertText(content + " ") + } + + if let movieProvider = _movieProvider { + let attachmentViewModel = AttachmentViewModel( + api: context.apiService, + authContext: authContext, + input: .itemProvider(movieProvider), + delegate: composeContentViewModel + ) + composeContentViewModel.attachmentViewModels.append(attachmentViewModel) + } else if !imageProviders.isEmpty { + let attachmentViewModels = imageProviders.map { provider in + AttachmentViewModel( + api: context.apiService, + authContext: authContext, + input: .itemProvider(provider), + delegate: composeContentViewModel + ) + } + composeContentViewModel.attachmentViewModels.append(contentsOf: attachmentViewModels) + } + } + + private static func loadText(textProvider: NSItemProvider?) async -> String? { + guard let textProvider = textProvider else { return nil } + do { + let item = try await textProvider.loadItem(forTypeIdentifier: UTType.plainText.identifier) + guard let text = item as? String else { return nil } + return text + } catch { + return nil + } + } + + private static func loadURL(textProvider: NSItemProvider?) async -> String? { + guard let textProvider = textProvider else { return nil } + do { + let item = try await textProvider.loadItem(forTypeIdentifier: UTType.url.identifier) + guard let url = item as? URL else { return nil } + return url.absoluteString + } catch { + return nil + } + } + +} + +extension ShareViewController { + enum ShareError: Error { + case `internal`(error: Error) + case userCancelShare + case missingAuthentication + } +} diff --git a/ShareActionExtension/Scene/ShareViewModel.swift b/ShareActionExtension/Scene/ShareViewModel.swift new file mode 100644 index 000000000..ef8e200a6 --- /dev/null +++ b/ShareActionExtension/Scene/ShareViewModel.swift @@ -0,0 +1,43 @@ +// +// ShareViewModel.swift +// MastodonShareAction +// +// Created by MainasuK Cirno on 2021-7-16. +// + +import os.log +import Foundation +import Combine +import CoreData +import CoreDataStack +import MastodonSDK +import SwiftUI +import UniformTypeIdentifiers +import MastodonAsset +import MastodonLocalization +import MastodonUI +import MastodonCore + +final class ShareViewModel { + + let logger = Logger(subsystem: "ComposeViewModel", category: "ViewModel") + + var disposeBag = Set() + + // input + let context: AppContext + @Published var authContext: AuthContext? + + @Published var isPublishing = false + + // output + + init( + context: AppContext + ) { + self.context = context + // end init + + } + +}