418 lines
17 KiB
Swift
418 lines
17 KiB
Swift
|
//
|
||
|
// 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 MastodonUI
|
||
|
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()
|
||
|
// }
|
||
|
//}
|