2021-03-11 08:41:27 +01:00
|
|
|
//
|
|
|
|
// ComposeViewModel+Diffable.swift
|
|
|
|
// Mastodon
|
|
|
|
//
|
|
|
|
// Created by MainasuK Cirno on 2021-3-11.
|
|
|
|
//
|
|
|
|
|
2021-06-29 10:41:58 +02:00
|
|
|
import os.log
|
2021-03-11 08:41:27 +01:00
|
|
|
import UIKit
|
2021-03-25 08:56:17 +01:00
|
|
|
import Combine
|
2021-06-28 13:41:41 +02:00
|
|
|
import CoreDataStack
|
2021-03-25 08:56:17 +01:00
|
|
|
import MastodonSDK
|
2021-06-28 13:41:41 +02:00
|
|
|
import MastodonMeta
|
2021-07-22 13:34:24 +02:00
|
|
|
import MetaTextKit
|
2021-03-11 08:41:27 +01:00
|
|
|
|
|
|
|
extension ComposeViewModel {
|
2021-06-28 13:41:41 +02:00
|
|
|
|
2021-07-19 11:12:45 +02:00
|
|
|
func setupDataSource(
|
2021-06-28 13:41:41 +02:00
|
|
|
tableView: UITableView,
|
|
|
|
metaTextDelegate: MetaTextDelegate,
|
|
|
|
metaTextViewDelegate: UITextViewDelegate,
|
2021-03-25 08:56:17 +01:00
|
|
|
customEmojiPickerInputViewModel: CustomEmojiPickerInputViewModel,
|
2021-06-29 10:41:58 +02:00
|
|
|
composeStatusAttachmentCollectionViewCellDelegate: ComposeStatusAttachmentCollectionViewCellDelegate,
|
2021-03-23 11:47:21 +01:00
|
|
|
composeStatusPollOptionCollectionViewCellDelegate: ComposeStatusPollOptionCollectionViewCellDelegate,
|
2021-06-29 10:41:58 +02:00
|
|
|
composeStatusPollOptionAppendEntryCollectionViewCellDelegate: ComposeStatusPollOptionAppendEntryCollectionViewCellDelegate,
|
2021-03-24 07:49:27 +01:00
|
|
|
composeStatusPollExpiresOptionCollectionViewCellDelegate: ComposeStatusPollExpiresOptionCollectionViewCellDelegate
|
2021-03-12 07:18:07 +01:00
|
|
|
) {
|
2021-06-29 10:41:58 +02:00
|
|
|
// content
|
|
|
|
composeStatusContentTableViewCell.metaText.delegate = metaTextDelegate
|
|
|
|
composeStatusContentTableViewCell.metaText.textView.delegate = metaTextViewDelegate
|
|
|
|
// attachment
|
|
|
|
composeStatusAttachmentTableViewCell.composeStatusAttachmentCollectionViewCellDelegate = composeStatusAttachmentCollectionViewCellDelegate
|
|
|
|
// poll
|
|
|
|
composeStatusPollTableViewCell.delegate = self
|
|
|
|
composeStatusPollTableViewCell.customEmojiPickerInputViewModel = customEmojiPickerInputViewModel
|
|
|
|
composeStatusPollTableViewCell.composeStatusPollOptionCollectionViewCellDelegate = composeStatusPollOptionCollectionViewCellDelegate
|
|
|
|
composeStatusPollTableViewCell.composeStatusPollOptionAppendEntryCollectionViewCellDelegate = composeStatusPollOptionAppendEntryCollectionViewCellDelegate
|
|
|
|
composeStatusPollTableViewCell.composeStatusPollExpiresOptionCollectionViewCellDelegate = composeStatusPollExpiresOptionCollectionViewCellDelegate
|
|
|
|
|
|
|
|
// setup data source
|
|
|
|
tableView.dataSource = self
|
2021-09-29 10:27:35 +02:00
|
|
|
|
|
|
|
composeStatusAttachmentTableViewCell.collectionViewHeightDidUpdate
|
|
|
|
.receive(on: DispatchQueue.main)
|
|
|
|
.sink { [weak self] _ in
|
|
|
|
guard let _ = self else { return }
|
|
|
|
tableView.beginUpdates()
|
|
|
|
tableView.endUpdates()
|
|
|
|
}
|
|
|
|
.store(in: &disposeBag)
|
2021-06-29 10:41:58 +02:00
|
|
|
|
|
|
|
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)
|
2021-03-22 11:40:32 +01:00
|
|
|
|
2021-09-29 10:27:35 +02:00
|
|
|
if #available(iOS 15.0, *) {
|
|
|
|
dataSource.applySnapshotUsingReloadData(snapshot)
|
|
|
|
} else {
|
|
|
|
dataSource.apply(snapshot, animatingDifferences: false)
|
2021-06-29 10:41:58 +02:00
|
|
|
}
|
2021-03-24 08:46:40 +01:00
|
|
|
}
|
2021-06-29 10:41:58 +02:00
|
|
|
.store(in: &disposeBag)
|
|
|
|
|
|
|
|
Publishers.CombineLatest(
|
|
|
|
isPollComposing,
|
|
|
|
pollOptionAttributes
|
|
|
|
)
|
|
|
|
.receive(on: DispatchQueue.main)
|
|
|
|
.sink { [weak self] isPollComposing, pollOptionAttributes in
|
2021-03-24 08:46:40 +01:00
|
|
|
guard let self = self else { return }
|
2021-06-29 10:41:58 +02:00
|
|
|
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))
|
|
|
|
}
|
2021-10-09 13:01:08 +02:00
|
|
|
if pollOptionAttributes.count < self.maxPollOptions {
|
2021-06-29 10:41:58 +02:00
|
|
|
items.append(.pollOptionAppendEntry)
|
|
|
|
}
|
|
|
|
items.append(.pollExpiresOption(attribute: self.pollExpiresOptionAttribute))
|
|
|
|
}
|
|
|
|
snapshot.appendItems(items, toSection: .main)
|
|
|
|
|
|
|
|
tableView.performBatchUpdates {
|
2021-09-29 10:27:35 +02:00
|
|
|
if #available(iOS 15.0, *) {
|
|
|
|
dataSource.apply(snapshot, animatingDifferences: false)
|
|
|
|
} else {
|
|
|
|
dataSource.apply(snapshot, animatingDifferences: true)
|
|
|
|
}
|
2021-03-24 08:46:40 +01:00
|
|
|
}
|
2021-03-11 08:41:27 +01:00
|
|
|
}
|
2021-06-29 10:41:58 +02:00
|
|
|
.store(in: &disposeBag)
|
2021-03-11 08:41:27 +01:00
|
|
|
}
|
|
|
|
|
2021-03-25 08:56:17 +01:00
|
|
|
func setupCustomEmojiPickerDiffableDataSource(
|
|
|
|
for collectionView: UICollectionView,
|
|
|
|
dependency: NeedsDependency
|
|
|
|
) {
|
|
|
|
let diffableDataSource = CustomEmojiPickerSection.collectionViewDiffableDataSource(
|
|
|
|
for: collectionView,
|
|
|
|
dependency: dependency
|
|
|
|
)
|
|
|
|
self.customEmojiPickerDiffableDataSource = diffableDataSource
|
|
|
|
|
|
|
|
customEmojiViewModel
|
|
|
|
.sink { [weak self, weak diffableDataSource] customEmojiViewModel in
|
|
|
|
guard let self = self else { return }
|
|
|
|
guard let diffableDataSource = diffableDataSource else { return }
|
|
|
|
guard let customEmojiViewModel = customEmojiViewModel else {
|
|
|
|
self.customEmojiViewModelSubscription = nil
|
|
|
|
let snapshot = NSDiffableDataSourceSnapshot<CustomEmojiPickerSection, CustomEmojiPickerItem>()
|
|
|
|
diffableDataSource.apply(snapshot)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
self.customEmojiViewModelSubscription = 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 customEmojiSection = CustomEmojiPickerSection.emoji(name: customEmojiViewModel.domain.uppercased())
|
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
2021-03-11 08:41:27 +01:00
|
|
|
}
|
2021-06-29 10:41:58 +02:00
|
|
|
|
|
|
|
// MARK: - UITableViewDataSource
|
|
|
|
extension ComposeViewModel: UITableViewDataSource {
|
|
|
|
|
|
|
|
enum Section: CaseIterable {
|
|
|
|
case repliedTo
|
|
|
|
case status
|
|
|
|
case attachment
|
|
|
|
case poll
|
|
|
|
}
|
|
|
|
|
|
|
|
func numberOfSections(in tableView: UITableView) -> Int {
|
|
|
|
return Section.allCases.count
|
|
|
|
}
|
|
|
|
|
|
|
|
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
|
|
|
switch Section.allCases[section] {
|
|
|
|
case .repliedTo:
|
|
|
|
switch composeKind {
|
|
|
|
case .reply: return 1
|
|
|
|
default: return 0
|
|
|
|
}
|
|
|
|
case .status: return 1
|
|
|
|
case .attachment:
|
|
|
|
return 1
|
|
|
|
case .poll:
|
|
|
|
return 1
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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(statusObjectID) = composeKind else { return cell }
|
2021-06-29 11:54:38 +02:00
|
|
|
cell.framePublisher
|
|
|
|
.receive(on: DispatchQueue.main)
|
|
|
|
.assign(to: \.value, on: self.repliedToCellFrame)
|
|
|
|
.store(in: &cell.disposeBag)
|
2021-06-29 10:41:58 +02:00
|
|
|
let managedObjectContext = context.managedObjectContext
|
|
|
|
managedObjectContext.performAndWait {
|
|
|
|
guard let replyTo = managedObjectContext.object(with: statusObjectID) as? Status else {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
let status = replyTo.reblog ?? replyTo
|
|
|
|
|
|
|
|
// set avatar
|
|
|
|
cell.statusView.configure(with: AvatarConfigurableViewConfiguration(avatarImageURL: status.author.avatarImageURL()))
|
2021-07-23 13:10:27 +02:00
|
|
|
// set name, username
|
|
|
|
do {
|
|
|
|
let mastodonContent = MastodonContent(content: status.author.displayNameWithFallback, emojis: status.author.emojiMeta)
|
|
|
|
let metaContent = try MastodonMetaContent.convert(document: mastodonContent)
|
|
|
|
cell.statusView.nameLabel.configure(content: metaContent)
|
|
|
|
} catch {
|
|
|
|
let metaContent = PlaintextMetaContent(string: status.author.displayNameWithFallback)
|
|
|
|
cell.statusView.nameLabel.configure(content: metaContent)
|
|
|
|
}
|
2021-06-29 11:08:41 +02:00
|
|
|
cell.statusView.usernameLabel.text = "@" + status.author.acct
|
2021-06-29 10:41:58 +02:00
|
|
|
// set text
|
|
|
|
let content = MastodonContent(content: status.content, emojis: status.emojiMeta)
|
|
|
|
do {
|
|
|
|
let metaContent = try MastodonMetaContent.convert(document: content)
|
|
|
|
cell.statusView.contentMetaText.configure(content: metaContent)
|
|
|
|
} catch {
|
|
|
|
cell.statusView.contentMetaText.textView.text = " "
|
|
|
|
assertionFailure()
|
|
|
|
}
|
|
|
|
// set date
|
2021-08-09 09:14:08 +02:00
|
|
|
cell.statusView.dateLabel.text = status.createdAt.localizedSlowedTimeAgoSinceNow
|
2021-06-29 10:41:58 +02:00
|
|
|
}
|
|
|
|
return cell
|
|
|
|
case .status:
|
|
|
|
let cell = self.composeStatusContentTableViewCell
|
|
|
|
// configure header
|
|
|
|
let managedObjectContext = context.managedObjectContext
|
|
|
|
managedObjectContext.performAndWait {
|
|
|
|
guard case let .reply(replyToStatusObjectID) = self.composeKind,
|
|
|
|
let replyTo = managedObjectContext.object(with: replyToStatusObjectID) as? Status else {
|
|
|
|
cell.statusView.headerContainerView.isHidden = true
|
|
|
|
return
|
|
|
|
}
|
|
|
|
cell.statusView.headerContainerView.isHidden = false
|
2021-10-11 06:59:15 +02:00
|
|
|
cell.statusView.headerIconLabel.configure(attributedString: StatusView.iconAttributedString(image: StatusView.replyIconImage))
|
2021-06-29 11:46:43 +02:00
|
|
|
let headerText: String = {
|
|
|
|
let author = replyTo.author
|
|
|
|
let name = author.displayName.isEmpty ? author.username : author.displayName
|
|
|
|
return L10n.Scene.Compose.replyingToUser(name)
|
|
|
|
}()
|
2021-07-23 13:10:27 +02:00
|
|
|
do {
|
|
|
|
let mastodonContent = MastodonContent(content: headerText, emojis: replyTo.author.emojiMeta)
|
|
|
|
let metaContent = try MastodonMetaContent.convert(document: mastodonContent)
|
|
|
|
cell.statusView.headerInfoLabel.configure(content: metaContent)
|
|
|
|
} catch {
|
|
|
|
let metaContent = PlaintextMetaContent(string: headerText)
|
|
|
|
cell.statusView.headerInfoLabel.configure(content: metaContent)
|
|
|
|
}
|
2021-06-29 10:41:58 +02:00
|
|
|
}
|
|
|
|
// configure author
|
|
|
|
ComposeStatusSection.configureStatusContent(cell: cell, attribute: composeStatusAttribute)
|
2021-06-30 09:43:19 +02:00
|
|
|
// configure content. bind text in UITextViewDelegate
|
|
|
|
if let composeContent = composeStatusAttribute.composeContent.value {
|
|
|
|
cell.metaText.textView.text = composeContent
|
|
|
|
}
|
2021-06-29 11:08:41 +02:00
|
|
|
// configure content warning
|
|
|
|
cell.statusContentWarningEditorView.textView.text = composeStatusAttribute.contentWarningContent.value
|
2021-06-29 10:41:58 +02:00
|
|
|
// 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
|
2021-07-06 10:38:14 +02:00
|
|
|
tableView.beginUpdates()
|
|
|
|
tableView.endUpdates()
|
2021-06-29 10:41:58 +02:00
|
|
|
} completion: { _ in
|
|
|
|
// do nothing
|
|
|
|
}
|
|
|
|
}
|
|
|
|
.store(in: &cell.disposeBag)
|
|
|
|
cell.contentWarningContent
|
|
|
|
.removeDuplicates()
|
|
|
|
.receive(on: DispatchQueue.main)
|
|
|
|
.sink { [weak tableView, weak self] text in
|
|
|
|
guard let self = self else { return }
|
2021-06-29 11:08:41 +02:00
|
|
|
// bind input data
|
|
|
|
self.composeStatusAttribute.contentWarningContent.value = text
|
|
|
|
|
2021-06-29 10:41:58 +02:00
|
|
|
// self size input cell
|
2021-06-29 11:08:41 +02:00
|
|
|
guard let tableView = tableView else { return }
|
2021-06-29 10:41:58 +02:00
|
|
|
UIView.performWithoutAnimation {
|
|
|
|
tableView.beginUpdates()
|
|
|
|
tableView.endUpdates()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
.store(in: &cell.disposeBag)
|
|
|
|
// configure custom emoji picker
|
|
|
|
ComposeStatusSection.configureCustomEmojiPicker(viewModel: customEmojiPickerInputViewModel, customEmojiReplaceableTextInput: cell.metaText.textView, disposeBag: &cell.disposeBag)
|
|
|
|
ComposeStatusSection.configureCustomEmojiPicker(viewModel: customEmojiPickerInputViewModel, customEmojiReplaceableTextInput: cell.statusContentWarningEditorView.textView, disposeBag: &cell.disposeBag)
|
|
|
|
return cell
|
|
|
|
case .attachment:
|
|
|
|
let cell = self.composeStatusAttachmentTableViewCell
|
|
|
|
return cell
|
|
|
|
case .poll:
|
|
|
|
let cell = self.composeStatusPollTableViewCell
|
|
|
|
return cell
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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.value = options
|
|
|
|
}
|
|
|
|
}
|