// // ComposeViewModel.swift // Mastodon // // Created by MainasuK Cirno on 2021-3-11. // import os.log import UIKit import Combine import CoreData import CoreDataStack import GameplayKit import MastodonSDK import MastodonAsset import MastodonCore import MastodonLocalization import MastodonMeta import MastodonUI final class ComposeViewModel: NSObject { let logger = Logger(subsystem: "ComposeViewModel", category: "ViewModel") var disposeBag = Set() let id = UUID() // input let context: AppContext let authContext: AuthContext let kind: ComposeContentViewModel.Kind // var authenticationBox: MastodonAuthenticationBox { // authContext.mastodonAuthenticationBox // } // // @Published var isPollComposing = false // @Published var isCustomEmojiComposing = false // @Published var isContentWarningComposing = false // // @Published var selectedStatusVisibility: ComposeToolbarView.VisibilitySelectionType // @Published var repliedToCellFrame: CGRect = .zero // @Published var autoCompleteRetryLayoutTimes = 0 // @Published var autoCompleteInfo: ComposeViewController.AutoCompleteInfo? = nil let traitCollectionDidChangePublisher = CurrentValueSubject(Void()) // use CurrentValueSubject to make initial event emit // var isViewAppeared = false // output // let instanceConfiguration: Mastodon.Entity.Instance.Configuration? // var composeContentLimit: Int { // guard let maxCharacters = instanceConfiguration?.statuses?.maxCharacters else { return 500 } // return max(1, maxCharacters) // } // var maxMediaAttachments: Int { // guard let maxMediaAttachments = instanceConfiguration?.statuses?.maxMediaAttachments else { // return 4 // } // // FIXME: update timeline media preview UI // return min(4, max(1, maxMediaAttachments)) // // return max(1, maxMediaAttachments) // } // var maxPollOptions: Int { // guard let maxOptions = instanceConfiguration?.polls?.maxOptions else { return 4 } // return max(2, maxOptions) // } // // let composeStatusAttribute = ComposeStatusItem.ComposeStatusAttribute() // let composeStatusContentTableViewCell = ComposeStatusContentTableViewCell() // let composeStatusAttachmentTableViewCell = ComposeStatusAttachmentTableViewCell() // let composeStatusPollTableViewCell = ComposeStatusPollTableViewCell() // // // var dataSource: UITableViewDiffableDataSource? // var customEmojiPickerDiffableDataSource: UICollectionViewDiffableDataSource? // private(set) lazy var publishStateMachine: GKStateMachine = { // // exclude timeline middle fetcher state // let stateMachine = GKStateMachine(states: [ // PublishState.Initial(viewModel: self), // PublishState.Publishing(viewModel: self), // PublishState.Fail(viewModel: self), // PublishState.Discard(viewModel: self), // PublishState.Finish(viewModel: self), // ]) // stateMachine.enter(PublishState.Initial.self) // return stateMachine // }() // private(set) lazy var publishStateMachinePublisher = CurrentValueSubject(nil) // private(set) var publishDate = Date() // update it when enter Publishing state // // // TODO: group post material into Hashable class // var idempotencyKey = CurrentValueSubject(UUID().uuidString) // // // UI & UX // @Published var title: String // @Published var shouldDismiss = true // @Published var isPublishBarButtonItemEnabled = false // @Published var isMediaToolbarButtonEnabled = true // @Published var isPollToolbarButtonEnabled = true // @Published var characterCount = 0 // @Published var collectionViewState: CollectionViewState = .fold // // // for hashtag: "# " // // for mention: "@ " // var preInsertedContent: String? // // // custom emojis // let customEmojiViewModel: EmojiService.CustomEmojiViewModel? // let customEmojiPickerInputViewModel = CustomEmojiPickerInputViewModel() // @Published var isLoadingCustomEmoji = false // // // attachment // @Published var attachmentServices: [MastodonAttachmentService] = [] // // // polls // @Published var pollOptionAttributes: [ComposeStatusPollItem.PollOptionAttribute] = [] // let pollExpiresOptionAttribute = ComposeStatusPollItem.PollExpiresOptionAttribute() init( context: AppContext, authContext: AuthContext, kind: ComposeContentViewModel.Kind ) { self.context = context self.authContext = authContext self.kind = kind // self.title = { // switch composeKind { // case .post, .hashtag, .mention: return L10n.Scene.Compose.Title.newPost // case .reply: return L10n.Scene.Compose.Title.newReply // } // }() // self.selectedStatusVisibility = { // // default private when user locked // var visibility: ComposeToolbarView.VisibilitySelectionType = { // guard let author = authContext.mastodonAuthenticationBox.authenticationRecord.object(in: context.managedObjectContext)?.user // else { // return .public // } // return author.locked ? .private : .public // }() // // set visibility for reply post // switch composeKind { // case .reply(let record): // context.managedObjectContext.performAndWait { // guard let status = record.object(in: context.managedObjectContext) else { // assertionFailure() // return // } // let repliedStatusVisibility = status.visibility // switch repliedStatusVisibility { // case .public, .unlisted: // // keep default // break // case .private: // visibility = .private // case .direct: // visibility = .direct // case ._other: // assertionFailure() // break // } // } // default: // break // } // return visibility // }() // // set limit // self.instanceConfiguration = { // var configuration: Mastodon.Entity.Instance.Configuration? = nil // context.managedObjectContext.performAndWait { // guard let authentication = authContext.mastodonAuthenticationBox.authenticationRecord.object(in: context.managedObjectContext) else { return } // configuration = authentication.instance?.configuration // } // return configuration // }() // self.customEmojiViewModel = context.emojiService.dequeueCustomEmojiViewModel(for: authContext.mastodonAuthenticationBox.domain) // super.init() // // end init // // setup(cell: composeStatusContentTableViewCell) } deinit { os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) } } extension ComposeViewModel { // func createNewPollOptionIfPossible() { // guard pollOptionAttributes.count < maxPollOptions else { return } // // let attribute = ComposeStatusPollItem.PollOptionAttribute() // pollOptionAttributes = pollOptionAttributes + [attribute] // } // // func updatePublishDate() { // publishDate = Date() // } } //extension ComposeViewModel { // // enum AttachmentPrecondition: Error, LocalizedError { // case videoAttachWithPhoto // case moreThanOneVideo // // var errorDescription: String? { // return L10n.Common.Alerts.PublishPostFailure.title // } // // var failureReason: String? { // switch self { // case .videoAttachWithPhoto: // return L10n.Common.Alerts.PublishPostFailure.AttachmentsMessage.videoAttachWithPhoto // case .moreThanOneVideo: // return L10n.Common.Alerts.PublishPostFailure.AttachmentsMessage.moreThanOneVideo // } // } // } // // // check exclusive limit: // // - up to 1 video // // - up to N photos // func checkAttachmentPrecondition() throws { // let attachmentServices = self.attachmentServices // guard !attachmentServices.isEmpty else { return } // var photoAttachmentServices: [MastodonAttachmentService] = [] // var videoAttachmentServices: [MastodonAttachmentService] = [] // attachmentServices.forEach { service in // guard let file = service.file.value else { // assertionFailure() // return // } // switch file { // case .jpeg, .png, .gif: // photoAttachmentServices.append(service) // case .other: // videoAttachmentServices.append(service) // } // } // // if !videoAttachmentServices.isEmpty { // guard videoAttachmentServices.count == 1 else { // throw AttachmentPrecondition.moreThanOneVideo // } // guard photoAttachmentServices.isEmpty else { // throw AttachmentPrecondition.videoAttachWithPhoto // } // } // } // //} // //// MARK: - MastodonAttachmentServiceDelegate //extension ComposeViewModel: MastodonAttachmentServiceDelegate { // func mastodonAttachmentService(_ service: MastodonAttachmentService, uploadStateDidChange state: MastodonAttachmentService.UploadState?) { // // trigger new output event // attachmentServices = attachmentServices // } //} // //// MARK: - ComposePollAttributeDelegate //extension ComposeViewModel: ComposePollAttributeDelegate { // func composePollAttribute(_ attribute: ComposeStatusPollItem.PollOptionAttribute, pollOptionDidChange: String?) { // // trigger update // pollOptionAttributes = pollOptionAttributes // } //} // //extension ComposeViewModel { // private func setup( // cell: ComposeStatusContentTableViewCell // ) { // setupStatusHeader(cell: cell) // setupStatusAuthor(cell: cell) // setupStatusContent(cell: cell) // } // // private func setupStatusHeader( // cell: ComposeStatusContentTableViewCell // ) { // // configure header // let managedObjectContext = context.managedObjectContext // managedObjectContext.performAndWait { // guard case let .reply(record) = self.composeKind, // let replyTo = record.object(in: managedObjectContext) // else { // cell.statusView.viewModel.header = .none // return // } // // let info: StatusView.ViewModel.Header.ReplyInfo // do { // let content = MastodonContent( // content: replyTo.author.displayNameWithFallback, // emojis: replyTo.author.emojis.asDictionary // ) // let metaContent = try MastodonMetaContent.convert(document: content) // info = .init(header: metaContent) // } catch { // let metaContent = PlaintextMetaContent(string: replyTo.author.displayNameWithFallback) // info = .init(header: metaContent) // } // cell.statusView.viewModel.header = .reply(info: info) // } // } // // private func setupStatusAuthor( // cell: ComposeStatusContentTableViewCell // ) { // self.context.managedObjectContext.performAndWait { // guard let author = authenticationBox.authenticationRecord.object(in: self.context.managedObjectContext)?.user else { return } // cell.statusView.configureAuthor(author: author) // } // } // // private func setupStatusContent( // cell: ComposeStatusContentTableViewCell // ) { // switch composeKind { // case .reply(let record): // context.managedObjectContext.performAndWait { // guard let status = record.object(in: context.managedObjectContext) else { return } // let author = self.authenticationBox.authenticationRecord.object(in: context.managedObjectContext)?.user // // var mentionAccts: [String] = [] // if author?.id != status.author.id { // mentionAccts.append("@" + status.author.acct) // } // let mentions = status.mentions // .filter { author?.id != $0.id } // for mention in mentions { // let acct = "@" + mention.acct // guard !mentionAccts.contains(acct) else { continue } // mentionAccts.append(acct) // } // for acct in mentionAccts { // UITextChecker.learnWord(acct) // } // if let spoilerText = status.spoilerText, !spoilerText.isEmpty { // self.isContentWarningComposing = true // self.composeStatusAttribute.contentWarningContent = spoilerText // } // // let initialComposeContent = mentionAccts.joined(separator: " ") // let preInsertedContent: String? = initialComposeContent.isEmpty ? nil : initialComposeContent + " " // self.preInsertedContent = preInsertedContent // self.composeStatusAttribute.composeContent = preInsertedContent // } // case .hashtag(let hashtag): // let initialComposeContent = "#" + hashtag // UITextChecker.learnWord(initialComposeContent) // let preInsertedContent = initialComposeContent + " " // self.preInsertedContent = preInsertedContent // self.composeStatusAttribute.composeContent = preInsertedContent // case .mention(let record): // context.managedObjectContext.performAndWait { // guard let user = record.object(in: context.managedObjectContext) else { return } // let initialComposeContent = "@" + user.acct // UITextChecker.learnWord(initialComposeContent) // let preInsertedContent = initialComposeContent + " " // self.preInsertedContent = preInsertedContent // self.composeStatusAttribute.composeContent = preInsertedContent // } // case .post: // self.preInsertedContent = nil // } // // // configure content warning // if let composeContent = composeStatusAttribute.composeContent { // cell.metaText.textView.text = composeContent // } // // // configure content warning // cell.statusContentWarningEditorView.textView.text = composeStatusAttribute.contentWarningContent // } //}