feat: restore share action extension

This commit is contained in:
CMK 2022-11-14 00:05:43 +08:00
parent 91bfc8ad5a
commit 939429aacc
7 changed files with 386 additions and 773 deletions

View File

@ -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 = "<group>"; };
DBBC24A726A52F9000398BB9 /* ComposeToolbarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeToolbarView.swift; sourceTree = "<group>"; };
DBBE1B4425F3474B0081417A /* MastodonPickServerAppearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonPickServerAppearance.swift; sourceTree = "<group>"; };
DBC3872329214121001EC0FD /* ShareViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareViewController.swift; sourceTree = "<group>"; };
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 = "<group>"; };
DBC6461726A170AB00B0E31B /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = "<group>"; };
DBC6461926A170AB00B0E31B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
DBC6462226A1712000B0E31B /* ComposeViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ComposeViewModel.swift; sourceTree = "<group>"; };
DBC6462226A1712000B0E31B /* ShareViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShareViewModel.swift; sourceTree = "<group>"; };
DBC7A671260C897100E57475 /* StatusContentWarningEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusContentWarningEditorView.swift; sourceTree = "<group>"; };
DBC9E3A3282E13BB0063A4D9 /* eu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = eu; path = eu.lproj/Intents.strings; sourceTree = "<group>"; };
DBC9E3A4282E13BB0063A4D9 /* eu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = eu; path = eu.lproj/InfoPlist.strings; sourceTree = "<group>"; };
@ -2686,8 +2686,8 @@
isa = PBXGroup;
children = (
DBFEF05426A576EE006D7ED1 /* View */,
DBC6462226A1712000B0E31B /* ComposeViewModel.swift */,
DBC6461426A170AB00B0E31B /* ComposeViewController.swift */,
DBC6462226A1712000B0E31B /* ShareViewModel.swift */,
DBC3872329214121001EC0FD /* ShareViewController.swift */,
);
path = Scene;
sourceTree = "<group>";
@ -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;
};

View File

@ -117,7 +117,7 @@
<key>NotificationService.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>18</integer>
<integer>16</integer>
</dict>
<key>ShareActionExtension.xcscheme_^#shared#^_</key>
<dict>

View File

@ -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?) {

View File

@ -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<AnyCancellable>()
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<Void, Never>()
// 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)
// }
//
//}

View File

@ -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<AnyCancellable>()
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<Bool, Never>(false)
// let traitCollectionDidChangePublisher = CurrentValueSubject<Void, Never>(Void()) // use CurrentValueSubject to make initial event emit
// let selectedStatusVisibility = CurrentValueSubject<ComposeToolbarView.VisibilitySelectionType, Never>(.public)
//
// // output
// let authentication = CurrentValueSubject<Result<MastodonAuthentication, Error>?, Never>(nil)
// let isFetchAuthentication = CurrentValueSubject<Bool, Never>(true)
// let isPublishing = CurrentValueSubject<Bool, Never>(false)
// let isBusy = CurrentValueSubject<Bool, Never>(true)
// let isValid = CurrentValueSubject<Bool, Never>(false)
// let shouldDismiss = CurrentValueSubject<Bool, Never>(true)
// let composeViewModel = ComposeViewModel()
// let characterCount = CurrentValueSubject<Int, Never>(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<Mastodon.Response.Content<Mastodon.Entity.Status>, 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<Mastodon.Response.Content<Mastodon.Entity.Attachment>, Error>] = {
// var subscriptions: [AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Attachment>, 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()
// }
//}

View File

@ -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<AnyCancellable>()
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
}
}

View File

@ -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<AnyCancellable>()
// input
let context: AppContext
@Published var authContext: AuthContext?
@Published var isPublishing = false
// output
init(
context: AppContext
) {
self.context = context
// end init
}
}