491 lines
21 KiB
Swift
491 lines
21 KiB
Swift
//
|
|
// ComposeViewModel+Diffable.swift
|
|
// Mastodon
|
|
//
|
|
// Created by MainasuK Cirno on 2021-3-11.
|
|
//
|
|
|
|
import os.log
|
|
import UIKit
|
|
import Combine
|
|
import CoreDataStack
|
|
import MetaTextKit
|
|
import MastodonMeta
|
|
import MastodonAsset
|
|
import MastodonCore
|
|
import MastodonLocalization
|
|
import MastodonSDK
|
|
|
|
extension ComposeViewModel {
|
|
|
|
// func setupDataSource(
|
|
// tableView: UITableView,
|
|
// metaTextDelegate: MetaTextDelegate,
|
|
// metaTextViewDelegate: UITextViewDelegate,
|
|
// customEmojiPickerInputViewModel: CustomEmojiPickerInputViewModel,
|
|
// composeStatusAttachmentCollectionViewCellDelegate: ComposeStatusAttachmentCollectionViewCellDelegate,
|
|
// composeStatusPollOptionCollectionViewCellDelegate: ComposeStatusPollOptionCollectionViewCellDelegate,
|
|
// composeStatusPollOptionAppendEntryCollectionViewCellDelegate: ComposeStatusPollOptionAppendEntryCollectionViewCellDelegate,
|
|
// composeStatusPollExpiresOptionCollectionViewCellDelegate: ComposeStatusPollExpiresOptionCollectionViewCellDelegate
|
|
// ) {
|
|
// // UI
|
|
// bind()
|
|
//
|
|
// // content
|
|
// bind(cell: composeStatusContentTableViewCell, tableView: tableView)
|
|
// composeStatusContentTableViewCell.metaText.delegate = metaTextDelegate
|
|
// composeStatusContentTableViewCell.metaText.textView.delegate = metaTextViewDelegate
|
|
//
|
|
// // attachment
|
|
// bind(cell: composeStatusAttachmentTableViewCell, tableView: tableView)
|
|
// composeStatusAttachmentTableViewCell.composeStatusAttachmentCollectionViewCellDelegate = composeStatusAttachmentCollectionViewCellDelegate
|
|
//
|
|
// // poll
|
|
// bind(cell: composeStatusPollTableViewCell, tableView: tableView)
|
|
// composeStatusPollTableViewCell.delegate = self
|
|
// composeStatusPollTableViewCell.customEmojiPickerInputViewModel = customEmojiPickerInputViewModel
|
|
// composeStatusPollTableViewCell.composeStatusPollOptionCollectionViewCellDelegate = composeStatusPollOptionCollectionViewCellDelegate
|
|
// composeStatusPollTableViewCell.composeStatusPollOptionAppendEntryCollectionViewCellDelegate = composeStatusPollOptionAppendEntryCollectionViewCellDelegate
|
|
// composeStatusPollTableViewCell.composeStatusPollExpiresOptionCollectionViewCellDelegate = composeStatusPollExpiresOptionCollectionViewCellDelegate
|
|
//
|
|
// // setup data source
|
|
// tableView.dataSource = self
|
|
// }
|
|
//
|
|
// func setupCustomEmojiPickerDiffableDataSource(
|
|
// for collectionView: UICollectionView,
|
|
// dependency: NeedsDependency
|
|
// ) {
|
|
// let diffableDataSource = CustomEmojiPickerSection.collectionViewDiffableDataSource(
|
|
// for: collectionView,
|
|
// dependency: dependency
|
|
// )
|
|
// self.customEmojiPickerDiffableDataSource = diffableDataSource
|
|
//
|
|
// let _domain = customEmojiViewModel?.domain
|
|
// customEmojiViewModel?.emojis
|
|
// .receive(on: DispatchQueue.main)
|
|
// .sink { [weak self, weak diffableDataSource] emojis in
|
|
// guard let _ = self else { return }
|
|
// guard let diffableDataSource = diffableDataSource else { return }
|
|
//
|
|
// var snapshot = NSDiffableDataSourceSnapshot<CustomEmojiPickerSection, CustomEmojiPickerItem>()
|
|
// let domain = _domain?.uppercased() ?? " "
|
|
// let customEmojiSection = CustomEmojiPickerSection.emoji(name: domain)
|
|
// snapshot.appendSections([customEmojiSection])
|
|
// let items: [CustomEmojiPickerItem] = {
|
|
// var items = [CustomEmojiPickerItem]()
|
|
// for emoji in emojis where emoji.visibleInPicker {
|
|
// let attribute = CustomEmojiPickerItem.CustomEmojiAttribute(emoji: emoji)
|
|
// let item = CustomEmojiPickerItem.emoji(attribute: attribute)
|
|
// items.append(item)
|
|
// }
|
|
// return items
|
|
// }()
|
|
// snapshot.appendItems(items, toSection: customEmojiSection)
|
|
//
|
|
// diffableDataSource.apply(snapshot)
|
|
// }
|
|
// .store(in: &disposeBag)
|
|
// }
|
|
|
|
}
|
|
|
|
//// MARK: - UITableViewDataSource
|
|
//extension ComposeViewModel: UITableViewDataSource {
|
|
|
|
// func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
|
// switch Section.allCases[indexPath.section] {
|
|
// case .repliedTo:
|
|
// let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: ComposeRepliedToStatusContentTableViewCell.self), for: indexPath) as! ComposeRepliedToStatusContentTableViewCell
|
|
// guard case let .reply(record) = composeKind else { return cell }
|
|
//
|
|
// // bind frame publisher
|
|
// cell.framePublisher
|
|
// .receive(on: DispatchQueue.main)
|
|
// .assign(to: \.repliedToCellFrame, on: self)
|
|
// .store(in: &cell.disposeBag)
|
|
//
|
|
// // set initial width
|
|
// if cell.statusView.frame.width == .zero {
|
|
// cell.statusView.frame.size.width = tableView.frame.width
|
|
// }
|
|
//
|
|
// // configure status
|
|
// context.managedObjectContext.performAndWait {
|
|
// guard let replyTo = record.object(in: context.managedObjectContext) else { return }
|
|
// cell.statusView.configure(status: replyTo)
|
|
// }
|
|
//
|
|
// return cell
|
|
// case .status:
|
|
// return composeStatusContentTableViewCell
|
|
// case .attachment:
|
|
// return composeStatusAttachmentTableViewCell
|
|
// case .poll:
|
|
// return composeStatusPollTableViewCell
|
|
// }
|
|
// }
|
|
//}
|
|
|
|
//// MARK: - ComposeStatusPollTableViewCellDelegate
|
|
//extension ComposeViewModel: ComposeStatusPollTableViewCellDelegate {
|
|
// func composeStatusPollTableViewCell(_ cell: ComposeStatusPollTableViewCell, pollOptionAttributesDidReorder options: [ComposeStatusPollItem.PollOptionAttribute]) {
|
|
// os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
|
//
|
|
// self.pollOptionAttributes = options
|
|
// }
|
|
//}
|
|
//
|
|
//extension ComposeViewModel {
|
|
// private func bind() {
|
|
// $isCustomEmojiComposing
|
|
// .assign(to: \.value, on: customEmojiPickerInputViewModel.isCustomEmojiComposing)
|
|
// .store(in: &disposeBag)
|
|
//
|
|
// $isContentWarningComposing
|
|
// .assign(to: \.isContentWarningComposing, on: composeStatusAttribute)
|
|
// .store(in: &disposeBag)
|
|
//
|
|
// // bind compose toolbar UI state
|
|
// Publishers.CombineLatest(
|
|
// $isPollComposing,
|
|
// $attachmentServices
|
|
// )
|
|
// .receive(on: DispatchQueue.main)
|
|
// .sink(receiveValue: { [weak self] isPollComposing, attachmentServices in
|
|
// guard let self = self else { return }
|
|
// let shouldMediaDisable = isPollComposing || attachmentServices.count >= self.maxMediaAttachments
|
|
// let shouldPollDisable = attachmentServices.count > 0
|
|
//
|
|
// self.isMediaToolbarButtonEnabled = !shouldMediaDisable
|
|
// self.isPollToolbarButtonEnabled = !shouldPollDisable
|
|
// })
|
|
// .store(in: &disposeBag)
|
|
//
|
|
// // calculate `Idempotency-Key`
|
|
// let content = Publishers.CombineLatest3(
|
|
// composeStatusAttribute.$isContentWarningComposing,
|
|
// composeStatusAttribute.$contentWarningContent,
|
|
// composeStatusAttribute.$composeContent
|
|
// )
|
|
// .map { isContentWarningComposing, contentWarningContent, composeContent -> String in
|
|
// if isContentWarningComposing {
|
|
// return contentWarningContent + (composeContent ?? "")
|
|
// } else {
|
|
// return composeContent ?? ""
|
|
// }
|
|
// }
|
|
// let attachmentIDs = $attachmentServices.map { attachments -> String in
|
|
// let attachmentIDs = attachments.compactMap { $0.attachment.value?.id }
|
|
// return attachmentIDs.joined(separator: ",")
|
|
// }
|
|
// let pollOptionsAndDuration = Publishers.CombineLatest3(
|
|
// $isPollComposing,
|
|
// $pollOptionAttributes,
|
|
// pollExpiresOptionAttribute.expiresOption
|
|
// )
|
|
// .map { isPollComposing, pollOptionAttributes, expiresOption -> String in
|
|
// guard isPollComposing else {
|
|
// return ""
|
|
// }
|
|
//
|
|
// let pollOptions = pollOptionAttributes.map { $0.option.value }.joined(separator: ",")
|
|
// return pollOptions + expiresOption.rawValue
|
|
// }
|
|
//
|
|
// Publishers.CombineLatest4(
|
|
// content,
|
|
// attachmentIDs,
|
|
// pollOptionsAndDuration,
|
|
// $selectedStatusVisibility
|
|
// )
|
|
// .map { content, attachmentIDs, pollOptionsAndDuration, selectedStatusVisibility -> String in
|
|
// var hasher = Hasher()
|
|
// hasher.combine(content)
|
|
// hasher.combine(attachmentIDs)
|
|
// hasher.combine(pollOptionsAndDuration)
|
|
// hasher.combine(selectedStatusVisibility.visibility.rawValue)
|
|
// let hashValue = hasher.finalize()
|
|
// return "\(hashValue)"
|
|
// }
|
|
// .assign(to: \.value, on: idempotencyKey)
|
|
// .store(in: &disposeBag)
|
|
//
|
|
// // bind modal dismiss state
|
|
// composeStatusAttribute.$composeContent
|
|
// .receive(on: DispatchQueue.main)
|
|
// .map { [weak self] content in
|
|
// let content = content ?? ""
|
|
// if content.isEmpty {
|
|
// return true
|
|
// }
|
|
// // if preInsertedContent plus a space is equal to the content, simply dismiss the modal
|
|
// if let preInsertedContent = self?.preInsertedContent {
|
|
// return content == preInsertedContent
|
|
// }
|
|
// return false
|
|
// }
|
|
// .assign(to: &$shouldDismiss)
|
|
//
|
|
// // bind compose bar button item UI state
|
|
// let isComposeContentEmpty = composeStatusAttribute.$composeContent
|
|
// .map { ($0 ?? "").isEmpty }
|
|
// let isComposeContentValid = $characterCount
|
|
// .compactMap { [weak self] characterCount -> Bool in
|
|
// guard let self = self else { return characterCount <= 500 }
|
|
// return characterCount <= self.composeContentLimit
|
|
// }
|
|
// let isMediaEmpty = $attachmentServices
|
|
// .map { $0.isEmpty }
|
|
// let isMediaUploadAllSuccess = $attachmentServices
|
|
// .map { services in
|
|
// services.allSatisfy { $0.uploadStateMachineSubject.value is MastodonAttachmentService.UploadState.Finish }
|
|
// }
|
|
// let isPollAttributeAllValid = $pollOptionAttributes
|
|
// .map { pollAttributes in
|
|
// pollAttributes.allSatisfy { attribute -> Bool in
|
|
// !attribute.option.value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
|
// }
|
|
// }
|
|
//
|
|
// 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.CombineLatest4(
|
|
// isComposeContentEmpty,
|
|
// isComposeContentValid,
|
|
// $isPollComposing,
|
|
// isPollAttributeAllValid
|
|
// )
|
|
// .map { isComposeContentEmpty, isComposeContentValid, isPollComposing, isPollAttributeAllValid -> Bool in
|
|
// if isPollComposing {
|
|
// return isComposeContentValid && !isComposeContentEmpty && isPollAttributeAllValid
|
|
// } else {
|
|
// return isComposeContentValid && !isComposeContentEmpty
|
|
// }
|
|
// }
|
|
// .eraseToAnyPublisher()
|
|
//
|
|
// Publishers.CombineLatest(
|
|
// isPublishBarButtonItemEnabledPrecondition1,
|
|
// isPublishBarButtonItemEnabledPrecondition2
|
|
// )
|
|
// .map { $0 && $1 }
|
|
// .assign(to: &$isPublishBarButtonItemEnabled)
|
|
// }
|
|
//}
|
|
//
|
|
//extension ComposeViewModel {
|
|
// private func bind(
|
|
// cell: ComposeStatusContentTableViewCell,
|
|
// tableView: UITableView
|
|
// ) {
|
|
// // bind status content character count
|
|
// Publishers.CombineLatest3(
|
|
// composeStatusAttribute.$composeContent,
|
|
// composeStatusAttribute.$isContentWarningComposing,
|
|
// composeStatusAttribute.$contentWarningContent
|
|
// )
|
|
// .map { composeContent, isContentWarningComposing, contentWarningContent -> Int in
|
|
// let composeContent = composeContent ?? ""
|
|
// var count = composeContent.count
|
|
// if isContentWarningComposing {
|
|
// count += contentWarningContent.count
|
|
// }
|
|
// return count
|
|
// }
|
|
// .assign(to: &$characterCount)
|
|
//
|
|
// // bind content warning
|
|
// composeStatusAttribute.$isContentWarningComposing
|
|
// .receive(on: DispatchQueue.main)
|
|
// .sink { [weak cell, weak tableView] isContentWarningComposing in
|
|
// guard let cell = cell else { return }
|
|
// guard let tableView = tableView else { return }
|
|
//
|
|
// // self size input cell
|
|
// cell.statusContentWarningEditorView.isHidden = !isContentWarningComposing
|
|
// cell.statusContentWarningEditorView.alpha = 0
|
|
// UIView.animate(withDuration: 0.33, delay: 0, options: [.curveEaseOut]) {
|
|
// cell.statusContentWarningEditorView.alpha = 1
|
|
// tableView.beginUpdates()
|
|
// tableView.endUpdates()
|
|
// } completion: { _ in
|
|
// // do nothing
|
|
// }
|
|
// }
|
|
// .store(in: &disposeBag)
|
|
//
|
|
// cell.contentWarningContent
|
|
// .removeDuplicates()
|
|
// .receive(on: DispatchQueue.main)
|
|
// .sink { [weak tableView, weak self] text in
|
|
// guard let self = self else { return }
|
|
// // bind input data
|
|
// self.composeStatusAttribute.contentWarningContent = text
|
|
//
|
|
// // self size input cell
|
|
// guard let tableView = tableView else { return }
|
|
// UIView.performWithoutAnimation {
|
|
// tableView.beginUpdates()
|
|
// tableView.endUpdates()
|
|
// }
|
|
// }
|
|
// .store(in: &cell.disposeBag)
|
|
//
|
|
// // configure custom emoji picker
|
|
// ComposeStatusSection.configureCustomEmojiPicker(
|
|
// viewModel: customEmojiPickerInputViewModel,
|
|
// customEmojiReplaceableTextInput: cell.metaText.textView,
|
|
// disposeBag: &disposeBag
|
|
// )
|
|
// ComposeStatusSection.configureCustomEmojiPicker(
|
|
// viewModel: customEmojiPickerInputViewModel,
|
|
// customEmojiReplaceableTextInput: cell.statusContentWarningEditorView.textView,
|
|
// disposeBag: &disposeBag
|
|
// )
|
|
// }
|
|
//}
|
|
//
|
|
//extension ComposeViewModel {
|
|
// private func bind(
|
|
// cell: ComposeStatusPollTableViewCell,
|
|
// tableView: UITableView
|
|
// ) {
|
|
// Publishers.CombineLatest(
|
|
// $isPollComposing,
|
|
// $pollOptionAttributes
|
|
// )
|
|
// .receive(on: DispatchQueue.main)
|
|
// .sink { [weak self] isPollComposing, pollOptionAttributes in
|
|
// guard let self = self else { return }
|
|
// guard self.isViewAppeared else { return }
|
|
//
|
|
// let cell = self.composeStatusPollTableViewCell
|
|
// guard let dataSource = cell.dataSource else { return }
|
|
//
|
|
// var snapshot = NSDiffableDataSourceSnapshot<ComposeStatusPollSection, ComposeStatusPollItem>()
|
|
// snapshot.appendSections([.main])
|
|
// var items: [ComposeStatusPollItem] = []
|
|
// if isPollComposing {
|
|
// for attribute in pollOptionAttributes {
|
|
// items.append(.pollOption(attribute: attribute))
|
|
// }
|
|
// if pollOptionAttributes.count < self.maxPollOptions {
|
|
// items.append(.pollOptionAppendEntry)
|
|
// }
|
|
// items.append(.pollExpiresOption(attribute: self.pollExpiresOptionAttribute))
|
|
// }
|
|
// snapshot.appendItems(items, toSection: .main)
|
|
//
|
|
// tableView.performBatchUpdates {
|
|
// if #available(iOS 15.0, *) {
|
|
// dataSource.apply(snapshot, animatingDifferences: false)
|
|
// } else {
|
|
// dataSource.apply(snapshot, animatingDifferences: true)
|
|
// }
|
|
// }
|
|
// }
|
|
// .store(in: &disposeBag)
|
|
//
|
|
// // bind delegate
|
|
// $pollOptionAttributes
|
|
// .sink { [weak self] pollAttributes in
|
|
// guard let self = self else { return }
|
|
// pollAttributes.forEach { $0.delegate = self }
|
|
// }
|
|
// .store(in: &disposeBag)
|
|
// }
|
|
//}
|
|
//
|
|
//extension ComposeViewModel {
|
|
// private func bind(
|
|
// cell: ComposeStatusAttachmentTableViewCell,
|
|
// tableView: UITableView
|
|
// ) {
|
|
// cell.collectionViewHeightDidUpdate
|
|
// .receive(on: DispatchQueue.main)
|
|
// .sink { [weak self] _ in
|
|
// guard let _ = self else { return }
|
|
// tableView.beginUpdates()
|
|
// tableView.endUpdates()
|
|
// }
|
|
// .store(in: &disposeBag)
|
|
//
|
|
// $attachmentServices
|
|
// .removeDuplicates()
|
|
// .receive(on: DispatchQueue.main)
|
|
// .sink { [weak self] attachmentServices in
|
|
// guard let self = self else { return }
|
|
// guard self.isViewAppeared else { return }
|
|
//
|
|
// let cell = self.composeStatusAttachmentTableViewCell
|
|
// guard let dataSource = cell.dataSource else { return }
|
|
//
|
|
// var snapshot = NSDiffableDataSourceSnapshot<ComposeStatusAttachmentSection, ComposeStatusAttachmentItem>()
|
|
// snapshot.appendSections([.main])
|
|
// let items = attachmentServices.map { ComposeStatusAttachmentItem.attachment(attachmentService: $0) }
|
|
// snapshot.appendItems(items, toSection: .main)
|
|
//
|
|
// if #available(iOS 15.0, *) {
|
|
// dataSource.applySnapshotUsingReloadData(snapshot)
|
|
// } else {
|
|
// dataSource.apply(snapshot, animatingDifferences: false)
|
|
// }
|
|
// }
|
|
// .store(in: &disposeBag)
|
|
//
|
|
// // setup attribute updater
|
|
// $attachmentServices
|
|
// .receive(on: DispatchQueue.main)
|
|
// .debounce(for: 0.3, scheduler: DispatchQueue.main)
|
|
// .sink { attachmentServices in
|
|
// // drive service upload state
|
|
// // make image upload in the queue
|
|
// for attachmentService in attachmentServices {
|
|
// // skip when prefix N task when task finish OR fail OR uploading
|
|
// guard let currentState = attachmentService.uploadStateMachine.currentState else { break }
|
|
// if currentState is MastodonAttachmentService.UploadState.Fail {
|
|
// continue
|
|
// }
|
|
// if currentState is MastodonAttachmentService.UploadState.Finish {
|
|
// continue
|
|
// }
|
|
// if currentState is MastodonAttachmentService.UploadState.Processing {
|
|
// continue
|
|
// }
|
|
// if currentState is MastodonAttachmentService.UploadState.Uploading {
|
|
// break
|
|
// }
|
|
// // trigger uploading one by one
|
|
// if currentState is MastodonAttachmentService.UploadState.Initial {
|
|
// attachmentService.uploadStateMachine.enter(MastodonAttachmentService.UploadState.Uploading.self)
|
|
// break
|
|
// }
|
|
// }
|
|
// }
|
|
// .store(in: &disposeBag)
|
|
//
|
|
// // bind delegate
|
|
// $attachmentServices
|
|
// .sink { [weak self] attachmentServices in
|
|
// guard let self = self else { return }
|
|
// attachmentServices.forEach { $0.delegate = self }
|
|
// }
|
|
// .store(in: &disposeBag)
|
|
// }
|
|
//}
|