// // ShareViewModel.swift // MastodonShareAction // // Created by MainasuK Cirno on 2021-7-16. // import os.log import Foundation import Combine import CoreData import CoreDataStack import MastodonUI import SwiftUI import UniformTypeIdentifiers final class ShareViewModel { let logger = Logger(subsystem: "ShareViewModel", category: "logic") var disposeBag = Set() static let composeContentLimit: Int = 500 // input private var coreDataStack: CoreDataStack? var managedObjectContext: NSManagedObjectContext? 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 isBusy = CurrentValueSubject(true) let isValid = CurrentValueSubject(false) let composeViewModel = ComposeViewModel() let characterCount = CurrentValueSubject(0) 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) authentication .map { result in result == nil } .assign(to: \.value, on: isFetchAuthentication) .store(in: &disposeBag) 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) isFetchAuthentication .receive(on: DispatchQueue.main) .assign(to: \.value, on: isBusy) .store(in: &disposeBag) composeViewModel.statusPlaceholder = L10n.Scene.Compose.contentInputPlaceholder composeViewModel.contentWarningPlaceholder = L10n.Scene.Compose.ContentWarning.placeholder composeViewModel.toolbarHeight = ComposeToolbarView.toolbarHeight 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) composeViewModel.$characterCount .assign(to: \.value, on: characterCount) .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 ShareViewModel { 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.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 _movieProvider = itemProviders.first(where: { provider in return provider.hasRepresentationConforming(toTypeIdentifier: UTType.movie.identifier, fileOptions: []) }) let imageProviders = itemProviders.filter { provider in return provider.hasRepresentationConforming(toTypeIdentifier: UTType.image.identifier, fileOptions: []) } if let movieProvider = _movieProvider { composeViewModel.setupAttachmentViewModels([ StatusAttachmentViewModel(itemProvider: movieProvider) ]) } else { let viewModels = imageProviders.map { provider in StatusAttachmentViewModel(itemProvider: provider) } composeViewModel.setupAttachmentViewModels(viewModels) } } }