feat: [WIP] restore publish button and compose pre-insert content

This commit is contained in:
CMK 2022-11-13 22:08:26 +08:00
parent 88307057c0
commit 929a27d572
11 changed files with 401 additions and 1564 deletions

View File

@ -253,7 +253,6 @@
DB64BA452851F23000ADF1B7 /* MastodonAuthentication+Fetch.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB64BA442851F23000ADF1B7 /* MastodonAuthentication+Fetch.swift */; };
DB64BA482851F29300ADF1B7 /* Account+Fetch.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB64BA472851F29300ADF1B7 /* Account+Fetch.swift */; };
DB65C63727A2AF6C008BAC2E /* ReportItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB65C63627A2AF6C008BAC2E /* ReportItem.swift */; };
DB66728C25F9F8DC00D60309 /* ComposeViewModel+DataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB66728B25F9F8DC00D60309 /* ComposeViewModel+DataSource.swift */; };
DB6746EB278ED8B0008A6B94 /* PollOptionView+Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6746EA278ED8B0008A6B94 /* PollOptionView+Configuration.swift */; };
DB6746ED278F45F0008A6B94 /* AutoGenerateProtocolRelayDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6746EC278F45F0008A6B94 /* AutoGenerateProtocolRelayDelegate.swift */; };
DB6746F0278F463B008A6B94 /* AutoGenerateProtocolDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6746EF278F463B008A6B94 /* AutoGenerateProtocolDelegate.swift */; };
@ -333,7 +332,6 @@
DB98EB6927B21A7C0082E365 /* ReportResultActionTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98EB6827B21A7C0082E365 /* ReportResultActionTableViewCell.swift */; };
DB98EB6B27B243470082E365 /* SettingsAppearanceTableViewCell+ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98EB6A27B243470082E365 /* SettingsAppearanceTableViewCell+ViewModel.swift */; };
DB9A486C26032AC1008B817C /* AttachmentContainerView+EmptyStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9A486B26032AC1008B817C /* AttachmentContainerView+EmptyStateView.swift */; };
DB9A488A26034D40008B817C /* ComposeViewModel+PublishState.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9A488926034D40008B817C /* ComposeViewModel+PublishState.swift */; };
DB9D6BE925E4F5340051B173 /* SearchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9D6BE825E4F5340051B173 /* SearchViewController.swift */; };
DB9D6BF825E4F5690051B173 /* NotificationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9D6BF725E4F5690051B173 /* NotificationViewController.swift */; };
DB9D6BFF25E4F5940051B173 /* ProfileViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9D6BFE25E4F5940051B173 /* ProfileViewController.swift */; };
@ -811,7 +809,6 @@
DB64BA442851F23000ADF1B7 /* MastodonAuthentication+Fetch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MastodonAuthentication+Fetch.swift"; sourceTree = "<group>"; };
DB64BA472851F29300ADF1B7 /* Account+Fetch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Account+Fetch.swift"; sourceTree = "<group>"; };
DB65C63627A2AF6C008BAC2E /* ReportItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportItem.swift; sourceTree = "<group>"; };
DB66728B25F9F8DC00D60309 /* ComposeViewModel+DataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ComposeViewModel+DataSource.swift"; sourceTree = "<group>"; };
DB6746EA278ED8B0008A6B94 /* PollOptionView+Configuration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PollOptionView+Configuration.swift"; sourceTree = "<group>"; };
DB6746EC278F45F0008A6B94 /* AutoGenerateProtocolRelayDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoGenerateProtocolRelayDelegate.swift; sourceTree = "<group>"; };
DB6746EF278F463B008A6B94 /* AutoGenerateProtocolDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoGenerateProtocolDelegate.swift; sourceTree = "<group>"; };
@ -903,7 +900,6 @@
DB98EB6827B21A7C0082E365 /* ReportResultActionTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportResultActionTableViewCell.swift; sourceTree = "<group>"; };
DB98EB6A27B243470082E365 /* SettingsAppearanceTableViewCell+ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SettingsAppearanceTableViewCell+ViewModel.swift"; sourceTree = "<group>"; };
DB9A486B26032AC1008B817C /* AttachmentContainerView+EmptyStateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AttachmentContainerView+EmptyStateView.swift"; sourceTree = "<group>"; };
DB9A488926034D40008B817C /* ComposeViewModel+PublishState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ComposeViewModel+PublishState.swift"; sourceTree = "<group>"; };
DB9D6BE825E4F5340051B173 /* SearchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchViewController.swift; sourceTree = "<group>"; };
DB9D6BF725E4F5690051B173 /* NotificationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationViewController.swift; sourceTree = "<group>"; };
DB9D6BFE25E4F5940051B173 /* ProfileViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileViewController.swift; sourceTree = "<group>"; };
@ -2134,8 +2130,6 @@
DB789A2125F9F76D0071ACA0 /* CollectionViewCell */,
DB789A0A25F9F2950071ACA0 /* ComposeViewController.swift */,
DB789A1125F9F2CC0071ACA0 /* ComposeViewModel.swift */,
DB66728B25F9F8DC00D60309 /* ComposeViewModel+DataSource.swift */,
DB9A488926034D40008B817C /* ComposeViewModel+PublishState.swift */,
);
path = Compose;
sourceTree = "<group>";
@ -3183,7 +3177,6 @@
0F1E2D0B2615C39400C38565 /* DoubleTitleLabelNavigationBarTitleView.swift in Sources */,
DBDFF1902805543100557A48 /* DiscoveryPostsViewController.swift in Sources */,
DB697DD9278F4CED004EF2F7 /* HomeTimelineViewController+DataSourceProvider.swift in Sources */,
DB9A488A26034D40008B817C /* ComposeViewModel+PublishState.swift in Sources */,
DB45FAD725CA6C76005A8AC7 /* UIBarButtonItem.swift in Sources */,
2D8434F525FF465D00EECE90 /* HomeTimelineNavigationBarTitleViewModel.swift in Sources */,
DB938F0F2624119800E5B6C1 /* ThreadViewModel+LoadThreadState.swift in Sources */,
@ -3285,7 +3278,6 @@
DB63F7492799126300455B82 /* FollowerListViewController+DataSourceProvider.swift in Sources */,
2D198643261BF09500F0B013 /* SearchResultItem.swift in Sources */,
2DAC9E38262FC2320062E1A6 /* SuggestionAccountViewController.swift in Sources */,
DB66728C25F9F8DC00D60309 /* ComposeViewModel+DataSource.swift in Sources */,
DB6180E02639194B0018D199 /* MediaPreviewPagingViewController.swift in Sources */,
DBE0822425CD3F1E00FD6BBD /* MastodonRegisterViewModel.swift in Sources */,
5B90C45E262599800002E742 /* SettingsViewModel.swift in Sources */,

View File

@ -86,22 +86,6 @@ final class ComposeViewController: UIViewController, NeedsDependency {
publishButton.setBackgroundImage(.placeholder(color: Asset.Colors.Button.disabled.color), for: .disabled)
publishButton.setTitleColor(Asset.Colors.Label.primaryReverse.color, for: .normal)
}
// var systemKeyboardHeight: CGFloat = .zero {
// didSet {
// // note: some system AutoLayout warning here
// let height = max(300, systemKeyboardHeight)
// customEmojiPickerInputView.frame.size.height = height
// }
// }
//
//
// let composeToolbarView = ComposeToolbarView()
// var composeToolbarViewBottomLayoutConstraint: NSLayoutConstraint!
// let composeToolbarBackgroundView = UIView()
//
//
deinit {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
@ -155,132 +139,30 @@ extension ComposeViewController {
])
composeContentViewController.didMove(toParent: self)
// configureNavigationBarTitleStyle()
// viewModel.traitCollectionDidChangePublisher
// .receive(on: DispatchQueue.main)
// .sink { [weak self] _ in
// guard let self = self else { return }
// self.configureNavigationBarTitleStyle()
// }
// .store(in: &disposeBag)
//
// viewModel.$title
// .receive(on: DispatchQueue.main)
// .sink { [weak self] title in
// guard let self = self else { return }
// self.title = title
// }
// .store(in: &disposeBag)
//
// composeToolbarView.translatesAutoresizingMaskIntoConstraints = false
// view.addSubview(composeToolbarView)
// composeToolbarViewBottomLayoutConstraint = view.bottomAnchor.constraint(equalTo: composeToolbarView.bottomAnchor)
// NSLayoutConstraint.activate([
// composeToolbarView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
// composeToolbarView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
// composeToolbarViewBottomLayoutConstraint,
// composeToolbarView.heightAnchor.constraint(equalToConstant: ComposeToolbarView.toolbarHeight),
// ])
// composeToolbarView.preservesSuperviewLayoutMargins = true
// composeToolbarView.delegate = self
//
// composeToolbarBackgroundView.translatesAutoresizingMaskIntoConstraints = false
// view.insertSubview(composeToolbarBackgroundView, belowSubview: composeToolbarView)
// NSLayoutConstraint.activate([
// composeToolbarBackgroundView.topAnchor.constraint(equalTo: composeToolbarView.topAnchor),
// composeToolbarBackgroundView.leadingAnchor.constraint(equalTo: composeToolbarView.leadingAnchor),
// composeToolbarBackgroundView.trailingAnchor.constraint(equalTo: composeToolbarView.trailingAnchor),
// view.bottomAnchor.constraint(equalTo: composeToolbarBackgroundView.bottomAnchor),
// ])
// bind navigation bar style
configureNavigationBarTitleStyle()
viewModel.traitCollectionDidChangePublisher
.receive(on: DispatchQueue.main)
.sink { [weak self] _ in
guard let self = self else { return }
self.configureNavigationBarTitleStyle()
}
.store(in: &disposeBag)
// tableView.delegate = self
// viewModel.setupDataSource(
// tableView: tableView,
// metaTextDelegate: self,
// metaTextViewDelegate: self,
// customEmojiPickerInputViewModel: viewModel.customEmojiPickerInputViewModel,
// composeStatusAttachmentCollectionViewCellDelegate: self,
// composeStatusPollOptionCollectionViewCellDelegate: self,
// composeStatusPollOptionAppendEntryCollectionViewCellDelegate: self,
// composeStatusPollExpiresOptionCollectionViewCellDelegate: self
// )
// bind title
viewModel.$title
.receive(on: DispatchQueue.main)
.sink { [weak self] title in
guard let self = self else { return }
self.title = title
}
.store(in: &disposeBag)
// viewModel.composeStatusAttribute.$composeContent
// .removeDuplicates()
// .receive(on: DispatchQueue.main)
// .sink { [weak self] _ in
// guard let self = self else { return }
// guard self.view.window != nil else { return }
// UIView.performWithoutAnimation {
// self.tableView.beginUpdates()
// self.tableView.setNeedsLayout()
// self.tableView.layoutIfNeeded()
// self.tableView.endUpdates()
// }
// }
// .store(in: &disposeBag)
// viewModel.composeStatusContentTableViewCell.delegate = self
//
// // update layout when keyboard show/dismiss
// view.layoutIfNeeded()
//
// // bind publish bar button state
// viewModel.$isPublishBarButtonItemEnabled
// .receive(on: DispatchQueue.main)
// .assign(to: \.isEnabled, on: publishButton)
// .store(in: &disposeBag)
//
// // bind media button toolbar state
// viewModel.$isMediaToolbarButtonEnabled
// .receive(on: DispatchQueue.main)
// .sink { [weak self] isMediaToolbarButtonEnabled in
// guard let self = self else { return }
// self.composeToolbarView.mediaBarButtonItem.isEnabled = isMediaToolbarButtonEnabled
// self.composeToolbarView.mediaButton.isEnabled = isMediaToolbarButtonEnabled
// }
// .store(in: &disposeBag)
//
// // bind poll button toolbar state
// viewModel.$isPollToolbarButtonEnabled
// .receive(on: DispatchQueue.main)
// .sink { [weak self] isPollToolbarButtonEnabled in
// guard let self = self else { return }
// self.composeToolbarView.pollBarButtonItem.isEnabled = isPollToolbarButtonEnabled
// self.composeToolbarView.pollButton.isEnabled = isPollToolbarButtonEnabled
// }
// .store(in: &disposeBag)
//
// Publishers.CombineLatest(
// viewModel.$isPollComposing,
// viewModel.$isPollToolbarButtonEnabled
// )
// .receive(on: DispatchQueue.main)
// .sink { [weak self] isPollComposing, isPollToolbarButtonEnabled in
// guard let self = self else { return }
// guard isPollToolbarButtonEnabled else {
// let accessibilityLabel = L10n.Scene.Compose.Accessibility.appendPoll
// self.composeToolbarView.pollBarButtonItem.accessibilityLabel = accessibilityLabel
// self.composeToolbarView.pollButton.accessibilityLabel = accessibilityLabel
// return
// }
// let accessibilityLabel = isPollComposing ? L10n.Scene.Compose.Accessibility.removePoll : L10n.Scene.Compose.Accessibility.appendPoll
// self.composeToolbarView.pollBarButtonItem.accessibilityLabel = accessibilityLabel
// self.composeToolbarView.pollButton.accessibilityLabel = accessibilityLabel
// }
// .store(in: &disposeBag)
//
// // bind image picker toolbar state
// viewModel.$attachmentServices
// .receive(on: DispatchQueue.main)
// .sink { [weak self] attachmentServices in
// guard let self = self else { return }
// let isEnabled = attachmentServices.count < self.viewModel.maxMediaAttachments
// self.composeToolbarView.mediaBarButtonItem.isEnabled = isEnabled
// self.composeToolbarView.mediaButton.isEnabled = isEnabled
// self.resetImagePicker()
// }
// .store(in: &disposeBag)
// bind publish bar button state
composeContentViewModel.$isPublishBarButtonItemEnabled
.receive(on: DispatchQueue.main)
.assign(to: \.isEnabled, on: publishButton)
.store(in: &disposeBag)
//
// // bind content warning button state
// viewModel.$isContentWarningComposing
@ -292,72 +174,7 @@ extension ComposeViewController {
// self.composeToolbarView.contentWarningButton.accessibilityLabel = accessibilityLabel
// }
// .store(in: &disposeBag)
//
// // bind visibility toolbar UI
// Publishers.CombineLatest(
// viewModel.$selectedStatusVisibility,
// viewModel.traitCollectionDidChangePublisher
// )
// .receive(on: DispatchQueue.main)
// .sink { [weak self] type, _ in
// guard let self = self else { return }
// let image = type.image(interfaceStyle: self.traitCollection.userInterfaceStyle)
// self.composeToolbarView.visibilityBarButtonItem.image = image
// self.composeToolbarView.visibilityButton.setImage(image, for: .normal)
// self.composeToolbarView.activeVisibilityType.value = type
// }
// .store(in: &disposeBag)
//
// viewModel.$characterCount
// .receive(on: DispatchQueue.main)
// .sink { [weak self] characterCount in
// guard let self = self else { return }
// let count = self.viewModel.composeContentLimit - characterCount
// self.composeToolbarView.characterCountLabel.text = "\(count)"
// self.characterCountLabel.text = "\(count)"
// let font: UIFont
// let textColor: UIColor
// let accessibilityLabel: String
// switch count {
// case _ where count < 0:
// font = .monospacedDigitSystemFont(ofSize: 24, weight: .bold)
// textColor = Asset.Colors.danger.color
// accessibilityLabel = L10n.A11y.Plural.Count.inputLimitExceeds(abs(count))
// default:
// font = .monospacedDigitSystemFont(ofSize: 15, weight: .regular)
// textColor = Asset.Colors.Label.secondary.color
// accessibilityLabel = L10n.A11y.Plural.Count.inputLimitRemains(count)
// }
// self.composeToolbarView.characterCountLabel.font = font
// self.composeToolbarView.characterCountLabel.textColor = textColor
// self.composeToolbarView.characterCountLabel.accessibilityLabel = accessibilityLabel
// self.characterCountLabel.font = font
// self.characterCountLabel.textColor = textColor
// self.characterCountLabel.accessibilityLabel = accessibilityLabel
// self.characterCountLabel.sizeToFit()
// }
// .store(in: &disposeBag)
//
// configureToolbarDisplay(keyboardHasShortcutBar: keyboardHasShortcutBar.value)
// Publishers.CombineLatest(
// keyboardHasShortcutBar,
// viewModel.traitCollectionDidChangePublisher
// )
// .receive(on: DispatchQueue.main)
// .sink { [weak self] keyboardHasShortcutBar, _ in
// guard let self = self else { return }
// self.configureToolbarDisplay(keyboardHasShortcutBar: keyboardHasShortcutBar)
// }
// .store(in: &disposeBag)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// // update MetaText without trigger call underlaying `UITextStorage.processEditing`
// _ = textEditorView.processEditing(textEditorView.textStorage)
// markTextEditorViewBecomeFirstResponser()
}
override func viewDidAppear(_ animated: Bool) {
@ -369,102 +186,27 @@ extension ComposeViewController {
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
// configurePublishButtonApperance()
// viewModel.traitCollectionDidChangePublisher.send()
configurePublishButtonApperance()
viewModel.traitCollectionDidChangePublisher.send()
}
}
//extension ComposeViewController {
//
// private var textEditorView: MetaText {
// return viewModel.composeStatusContentTableViewCell.metaText
// }
//
// private func markTextEditorViewBecomeFirstResponser() {
// textEditorView.textView.becomeFirstResponder()
// }
//
// private func contentWarningEditorTextView() -> UITextView? {
// viewModel.composeStatusContentTableViewCell.statusContentWarningEditorView.textView
// }
//
// private func pollOptionCollectionViewCell(of item: ComposeStatusPollItem) -> ComposeStatusPollOptionCollectionViewCell? {
// guard case .pollOption = item else { return nil }
// guard let dataSource = viewModel.composeStatusPollTableViewCell.dataSource else { return nil }
// guard let indexPath = dataSource.indexPath(for: item),
// let cell = viewModel.composeStatusPollTableViewCell.collectionView.cellForItem(at: indexPath) as? ComposeStatusPollOptionCollectionViewCell else {
// return nil
// }
//
// return cell
// }
//
// private func firstPollOptionCollectionViewCell() -> ComposeStatusPollOptionCollectionViewCell? {
// guard let dataSource = viewModel.composeStatusPollTableViewCell.dataSource else { return nil }
// let items = dataSource.snapshot().itemIdentifiers(inSection: .main)
// let firstPollItem = items.first { item -> Bool in
// guard case .pollOption = item else { return false }
// return true
// }
//
// guard let item = firstPollItem else {
// return nil
// }
//
// return pollOptionCollectionViewCell(of: item)
// }
//
// private func lastPollOptionCollectionViewCell() -> ComposeStatusPollOptionCollectionViewCell? {
// guard let dataSource = viewModel.composeStatusPollTableViewCell.dataSource else { return nil }
// let items = dataSource.snapshot().itemIdentifiers(inSection: .main)
// let lastPollItem = items.last { item -> Bool in
// guard case .pollOption = item else { return false }
// return true
// }
//
// guard let item = lastPollItem else {
// return nil
// }
//
// return pollOptionCollectionViewCell(of: item)
// }
//
// private func markFirstPollOptionCollectionViewCellBecomeFirstResponser() {
// guard let cell = firstPollOptionCollectionViewCell() else { return }
// cell.pollOptionView.optionTextField.becomeFirstResponder()
// }
//
// private func markLastPollOptionCollectionViewCellBecomeFirstResponser() {
// guard let cell = lastPollOptionCollectionViewCell() else { return }
// cell.pollOptionView.optionTextField.becomeFirstResponder()
// }
//
// private func showDismissConfirmAlertController() {
// let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
// let discardAction = UIAlertAction(title: L10n.Common.Controls.Actions.discard, style: .destructive) { [weak self] _ in
// guard let self = self else { return }
// self.dismiss(animated: true, completion: nil)
// }
// alertController.addAction(discardAction)
// let cancelAction = UIAlertAction(title: L10n.Common.Controls.Actions.cancel, style: .cancel)
// alertController.addAction(cancelAction)
// alertController.popoverPresentationController?.barButtonItem = cancelBarButtonItem
// present(alertController, animated: true, completion: nil)
// }
//
// private func resetImagePicker() {
// let selectionLimit = max(1, viewModel.maxMediaAttachments - viewModel.attachmentServices.count)
// let configuration = ComposeViewController.createPhotoLibraryPickerConfiguration(selectionLimit: selectionLimit)
// photoLibraryPicker = createImagePicker(configuration: configuration)
// }
//
// private func createImagePicker(configuration: PHPickerConfiguration) -> PHPickerViewController {
// let imagePicker = PHPickerViewController(configuration: configuration)
// imagePicker.delegate = self
// return imagePicker
// }
//
extension ComposeViewController {
private func showDismissConfirmAlertController() {
let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
let discardAction = UIAlertAction(title: L10n.Common.Controls.Actions.discard, style: .destructive) { [weak self] _ in
guard let self = self else { return }
self.dismiss(animated: true, completion: nil)
}
alertController.addAction(discardAction)
let cancelAction = UIAlertAction(title: L10n.Common.Controls.Actions.cancel, style: .cancel)
alertController.addAction(cancelAction)
alertController.popoverPresentationController?.barButtonItem = cancelBarButtonItem
present(alertController, animated: true, completion: nil)
}
// private func setupBackgroundColor(theme: Theme) {
// let backgroundColor = UIColor(dynamicProvider: { traitCollection in
// switch traitCollection.userInterfaceStyle {
@ -503,46 +245,40 @@ extension ComposeViewController {
// }
// }
//
// private func configureNavigationBarTitleStyle() {
// switch traitCollection.userInterfaceIdiom {
// case .pad:
// navigationController?.navigationBar.prefersLargeTitles = traitCollection.horizontalSizeClass == .regular
// default:
// break
// }
// }
//
//}
//
private func configureNavigationBarTitleStyle() {
switch traitCollection.userInterfaceIdiom {
case .pad:
navigationController?.navigationBar.prefersLargeTitles = traitCollection.horizontalSizeClass == .regular
default:
break
}
}
}
extension ComposeViewController {
@objc private func cancelBarButtonItemPressed(_ sender: UIBarButtonItem) {
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)")
// guard viewModel.shouldDismiss else {
// showDismissConfirmAlertController()
// return
// }
guard composeContentViewModel.shouldDismiss else {
showDismissConfirmAlertController()
return
}
dismiss(animated: true, completion: nil)
}
@objc private func publishBarButtonItemPressed(_ sender: UIBarButtonItem) {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
// do {
// try viewModel.checkAttachmentPrecondition()
// } catch {
// let alertController = UIAlertController(for: error, title: nil, preferredStyle: .alert)
// let okAction = UIAlertAction(title: L10n.Common.Controls.Actions.ok, style: .default, handler: nil)
// alertController.addAction(okAction)
// coordinator.present(scene: .alertController(alertController: alertController), from: nil, transition: .alertController(animated: true, completion: nil))
// return
// }
// guard viewModel.publishStateMachine.enter(ComposeViewModel.PublishState.Publishing.self) else {
// // TODO: handle error
// return
// }
// context.statusPublishService.publish(composeViewModel: viewModel)
do {
try composeContentViewModel.checkAttachmentPrecondition()
} catch {
let alertController = UIAlertController(for: error, title: nil, preferredStyle: .alert)
let okAction = UIAlertAction(title: L10n.Common.Controls.Actions.ok, style: .default, handler: nil)
alertController.addAction(okAction)
coordinator.present(scene: .alertController(alertController: alertController), from: nil, transition: .alertController(animated: true, completion: nil))
return
}
do {
let statusPublisher = try composeContentViewModel.statusPublisher()
@ -565,111 +301,6 @@ extension ComposeViewController {
}
//// MARK: - MetaTextDelegate
//extension ComposeViewController: MetaTextDelegate {
// func metaText(_ metaText: MetaText, processEditing textStorage: MetaTextStorage) -> MetaContent? {
// let string = metaText.textStorage.string
// let content = MastodonContent(
// content: string,
// emojis: viewModel.customEmojiViewModel?.emojiMapping.value ?? [:]
// )
// let metaContent = MastodonMetaContent.convert(text: content)
// return metaContent
// }
//}
//
//// MARK: - UITextViewDelegate
//extension ComposeViewController: UITextViewDelegate {
//
// func textViewShouldBeginEditing(_ textView: UITextView) -> Bool {
// setupInputAssistantItem(item: textView.inputAssistantItem)
// return true
// }
//
//
//
//
// func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
// switch textView {
// case textEditorView.textView:
// return false
// default:
// return true
// }
// }
//
// func textView(_ textView: UITextView, shouldInteractWith textAttachment: NSTextAttachment, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
// switch textView {
// case textEditorView.textView:
// return false
// default:
// return true
// }
// }
//
//}
//
//// MARK: - ComposeToolbarViewDelegate
//extension ComposeViewController: ComposeToolbarViewDelegate {
// func composeToolbarView(_ composeToolbarView: ComposeToolbarView, pollButtonDidPressed sender: Any) {
// // toggle poll composing state
// viewModel.isPollComposing.toggle()
//
// // cancel custom picker input
// viewModel.isCustomEmojiComposing = false
//
// // setup initial poll option if needs
// if viewModel.isPollComposing, viewModel.pollOptionAttributes.isEmpty {
// viewModel.pollOptionAttributes = [ComposeStatusPollItem.PollOptionAttribute(), ComposeStatusPollItem.PollOptionAttribute()]
// }
//
// if viewModel.isPollComposing {
// // Magic RunLoop
// DispatchQueue.main.async {
// self.markFirstPollOptionCollectionViewCellBecomeFirstResponser()
// }
// } else {
// markTextEditorViewBecomeFirstResponser()
// }
// }
//
// func composeToolbarView(_ composeToolbarView: ComposeToolbarView, emojiButtonDidPressed sender: Any) {
// viewModel.isCustomEmojiComposing.toggle()
// }
//
// func composeToolbarView(_ composeToolbarView: ComposeToolbarView, contentWarningButtonDidPressed sender: Any) {
// // cancel custom picker input
// viewModel.isCustomEmojiComposing = false
//
// // restore first responder for text editor when content warning dismiss
// if viewModel.isContentWarningComposing {
// if contentWarningEditorTextView()?.isFirstResponder == true {
// markTextEditorViewBecomeFirstResponser()
// }
// }
//
// // toggle composing status
// viewModel.isContentWarningComposing.toggle()
//
// // active content warning after toggled
// if viewModel.isContentWarningComposing {
// contentWarningEditorTextView()?.becomeFirstResponder()
// }
// }
//
// func composeToolbarView(_ composeToolbarView: ComposeToolbarView, visibilityButtonDidPressed sender: Any, visibilitySelectionType type: ComposeToolbarView.VisibilitySelectionType) {
// viewModel.selectedStatusVisibility = type
// }
//
//}
//// MARK: - UITableViewDelegate
//extension ComposeViewController: UITableViewDelegate { }
// MARK: - UIAdaptivePresentationControllerDelegate
extension ComposeViewController: UIAdaptivePresentationControllerDelegate {
@ -681,15 +312,15 @@ extension ComposeViewController: UIAdaptivePresentationControllerDelegate {
return .pageSheet
}
}
// func presentationControllerShouldDismiss(_ presentationController: UIPresentationController) -> Bool {
// return viewModel.shouldDismiss
// }
// func presentationControllerDidAttemptToDismiss(_ presentationController: UIPresentationController) {
// os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
// showDismissConfirmAlertController()
// }
func presentationControllerShouldDismiss(_ presentationController: UIPresentationController) -> Bool {
return composeContentViewModel.shouldDismiss
}
func presentationControllerDidAttemptToDismiss(_ presentationController: UIPresentationController) {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
showDismissConfirmAlertController()
}
func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
@ -697,138 +328,6 @@ extension ComposeViewController: UIAdaptivePresentationControllerDelegate {
}
//// MARK: - ComposeStatusAttachmentTableViewCellDelegate
//extension ComposeViewController: ComposeStatusAttachmentCollectionViewCellDelegate {
//
// func composeStatusAttachmentCollectionViewCell(_ cell: ComposeStatusAttachmentCollectionViewCell, removeButtonDidPressed button: UIButton) {
// guard let diffableDataSource = viewModel.composeStatusAttachmentTableViewCell.dataSource else { return }
// guard let indexPath = viewModel.composeStatusAttachmentTableViewCell.collectionView.indexPath(for: cell) else { return }
// guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return }
// guard case let .attachment(attachmentService) = item else { return }
//
// var attachmentServices = viewModel.attachmentServices
// guard let index = attachmentServices.firstIndex(of: attachmentService) else { return }
// let removedItem = attachmentServices[index]
// attachmentServices.remove(at: index)
// viewModel.attachmentServices = attachmentServices
//
// // cancel task
// removedItem.disposeBag.removeAll()
// }
//
//}
//
//// MARK: - ComposeStatusPollOptionCollectionViewCellDelegate
//extension ComposeViewController: ComposeStatusPollOptionCollectionViewCellDelegate {
//
// func composeStatusPollOptionCollectionViewCell(_ cell: ComposeStatusPollOptionCollectionViewCell, textFieldDidBeginEditing textField: UITextField) {
//
// setupInputAssistantItem(item: textField.inputAssistantItem)
//
// // FIXME: make poll section visible
// // DispatchQueue.main.async {
// // self.collectionView.scroll(to: .bottom, animated: true)
// // }
// }
//
//
// // handle delete backward event for poll option input
// func composeStatusPollOptionCollectionViewCell(_ cell: ComposeStatusPollOptionCollectionViewCell, textBeforeDeleteBackward text: String?) {
// guard (text ?? "").isEmpty else { return }
// guard let dataSource = viewModel.composeStatusPollTableViewCell.dataSource else { return }
// guard let indexPath = viewModel.composeStatusPollTableViewCell.collectionView.indexPath(for: cell) else { return }
// guard let item = dataSource.itemIdentifier(for: indexPath) else { return }
// guard case let .pollOption(attribute) = item else { return }
//
// var pollAttributes = viewModel.pollOptionAttributes
// guard let index = pollAttributes.firstIndex(of: attribute) else { return }
//
// // mark previous (fallback to next) item of removed middle poll option become first responder
// let pollItems = dataSource.snapshot().itemIdentifiers(inSection: .main)
// if let indexOfItem = pollItems.firstIndex(of: item), index > 0 {
// func cellBeforeRemoved() -> ComposeStatusPollOptionCollectionViewCell? {
// guard index > 0 else { return nil }
// let indexBeforeRemoved = pollItems.index(before: indexOfItem)
// let itemBeforeRemoved = pollItems[indexBeforeRemoved]
// return pollOptionCollectionViewCell(of: itemBeforeRemoved)
// }
//
// func cellAfterRemoved() -> ComposeStatusPollOptionCollectionViewCell? {
// guard index < pollItems.count - 1 else { return nil }
// let indexAfterRemoved = pollItems.index(after: index)
// let itemAfterRemoved = pollItems[indexAfterRemoved]
// return pollOptionCollectionViewCell(of: itemAfterRemoved)
// }
//
// var cell: ComposeStatusPollOptionCollectionViewCell? = cellBeforeRemoved()
// if cell == nil {
// cell = cellAfterRemoved()
// }
// cell?.pollOptionView.optionTextField.becomeFirstResponder()
// }
//
// guard pollAttributes.count > 2 else {
// return
// }
// pollAttributes.remove(at: index)
//
// // update data source
// viewModel.pollOptionAttributes = pollAttributes
// }
//
// // handle keyboard return event for poll option input
// func composeStatusPollOptionCollectionViewCell(_ cell: ComposeStatusPollOptionCollectionViewCell, pollOptionTextFieldDidReturn: UITextField) {
// guard let dataSource = viewModel.composeStatusPollTableViewCell.dataSource else { return }
// guard let indexPath = viewModel.composeStatusPollTableViewCell.collectionView.indexPath(for: cell) else { return }
// let pollItems = dataSource.snapshot().itemIdentifiers(inSection: .main).filter { item in
// guard case .pollOption = item else { return false }
// return true
// }
// guard let item = dataSource.itemIdentifier(for: indexPath) else { return }
// guard let index = pollItems.firstIndex(of: item) else { return }
//
// if index == pollItems.count - 1 {
// // is the last
// viewModel.createNewPollOptionIfPossible()
// DispatchQueue.main.async {
// self.markLastPollOptionCollectionViewCellBecomeFirstResponser()
// }
// } else {
// // not the last
// let indexAfter = pollItems.index(after: index)
// let itemAfter = pollItems[indexAfter]
// let cell = pollOptionCollectionViewCell(of: itemAfter)
// cell?.pollOptionView.optionTextField.becomeFirstResponder()
// }
// }
//
//}
//
//// MARK: - ComposeStatusPollOptionAppendEntryCollectionViewCellDelegate
//extension ComposeViewController: ComposeStatusPollOptionAppendEntryCollectionViewCellDelegate {
// func composeStatusPollOptionAppendEntryCollectionViewCellDidPressed(_ cell: ComposeStatusPollOptionAppendEntryCollectionViewCell) {
// viewModel.createNewPollOptionIfPossible()
// DispatchQueue.main.async {
// self.markLastPollOptionCollectionViewCellBecomeFirstResponser()
// }
// }
//}
//
//// MARK: - ComposeStatusPollExpiresOptionCollectionViewCellDelegate
//extension ComposeViewController: ComposeStatusPollExpiresOptionCollectionViewCellDelegate {
// func composeStatusPollExpiresOptionCollectionViewCell(_ cell: ComposeStatusPollExpiresOptionCollectionViewCell, didSelectExpiresOption expiresOption: ComposeStatusPollItem.PollExpiresOptionAttribute.ExpiresOption) {
// viewModel.pollExpiresOptionAttribute.expiresOption.value = expiresOption
// }
//}
//
//// MARK: - ComposeStatusContentTableViewCellDelegate
//extension ComposeViewController: ComposeStatusContentTableViewCellDelegate {
// func composeStatusContentTableViewCell(_ cell: ComposeStatusContentTableViewCell, textViewShouldBeginEditing textView: UITextView) -> Bool {
// setupInputAssistantItem(item: textView.inputAssistantItem)
// return true
// }
//}
//extension ComposeViewController {
// override var keyCommands: [UIKeyCommand]? {
// composeKeyCommands

View File

@ -1,453 +0,0 @@
//
// 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
// }
}
//// 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)
// }
//}

View File

@ -1,164 +0,0 @@
//
// ComposeViewModel+PublishState.swift
// Mastodon
//
// Created by MainasuK Cirno on 2021-3-18.
//
import os.log
import Foundation
import Combine
import CoreDataStack
import GameplayKit
import MastodonSDK
//extension ComposeViewModel {
// class PublishState: GKState {
// weak var viewModel: ComposeViewModel?
//
// init(viewModel: ComposeViewModel) {
// self.viewModel = viewModel
// }
//
// override func didEnter(from previousState: GKState?) {
// os_log("%{public}s[%{public}ld], %{public}s: enter %s, previous: %s", ((#file as NSString).lastPathComponent), #line, #function, self.debugDescription, previousState.debugDescription)
// viewModel?.publishStateMachinePublisher.value = self
// }
// }
//}
//extension ComposeViewModel.PublishState {
// class Initial: ComposeViewModel.PublishState {
// override func isValidNextState(_ stateClass: AnyClass) -> Bool {
// return stateClass == Publishing.self
// }
// }
//
// class Publishing: ComposeViewModel.PublishState {
//
// var publishingSubscription: AnyCancellable?
//
// override func isValidNextState(_ stateClass: AnyClass) -> Bool {
// return stateClass == Fail.self || stateClass == Finish.self
// }
//
// override func didEnter(from previousState: GKState?) {
// super.didEnter(from: previousState)
// guard let viewModel = viewModel, let stateMachine = stateMachine else { return }
//
// viewModel.updatePublishDate()
//
// let authenticationBox = viewModel.authenticationBox
// let domain = authenticationBox.domain
// let attachmentServices = viewModel.attachmentServices
// let mediaIDs = attachmentServices.compactMap { attachmentService in
// attachmentService.attachment.value?.id
// }
// let pollOptions: [String]? = {
// guard viewModel.isPollComposing else { return nil }
// return viewModel.pollOptionAttributes.map { attribute in attribute.option.value }
// }()
// let pollExpiresIn: Int? = {
// guard viewModel.isPollComposing else { return nil }
// return viewModel.pollExpiresOptionAttribute.expiresOption.value.seconds
// }()
// let inReplyToID: Mastodon.Entity.Status.ID? = {
// guard case let .reply(status) = viewModel.composeKind else { return nil }
// var id: Mastodon.Entity.Status.ID?
// viewModel.context.managedObjectContext.performAndWait {
// guard let replyTo = status.object(in: viewModel.context.managedObjectContext) else { return }
// id = replyTo.id
// }
// return id
// }()
// let sensitive: Bool = viewModel.isContentWarningComposing
// let spoilerText: String? = {
// let text = viewModel.composeStatusAttribute.contentWarningContent.trimmingCharacters(in: .whitespacesAndNewlines)
// guard !text.isEmpty else {
// return nil
// }
// return text
// }()
// let visibility = viewModel.selectedStatusVisibility.visibility
//
// let updateMediaQuerySubscriptions: [AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Attachment>, Error>] = {
// var subscriptions: [AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Attachment>, Error>] = []
// for attachmentService in attachmentServices {
// guard let attachmentID = attachmentService.attachment.value?.id else { continue }
// let description = attachmentService.description.value?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
// guard !description.isEmpty else { continue }
// let query = Mastodon.API.Media.UpdateMediaQuery(
// file: nil,
// thumbnail: nil,
// description: description,
// focus: nil
// )
// let subscription = viewModel.context.apiService.updateMedia(
// domain: domain,
// attachmentID: attachmentID,
// query: query,
// mastodonAuthenticationBox: authenticationBox
// )
// subscriptions.append(subscription)
// }
// return subscriptions
// }()
//
// let idempotencyKey = viewModel.idempotencyKey.value
//
// publishingSubscription = Publishers.MergeMany(updateMediaQuerySubscriptions)
// .collect()
// .asyncMap { attachments -> Mastodon.Response.Content<Mastodon.Entity.Status> in
// let query = Mastodon.API.Statuses.PublishStatusQuery(
// status: viewModel.composeStatusAttribute.composeContent,
// mediaIDs: mediaIDs.isEmpty ? nil : mediaIDs,
// pollOptions: pollOptions,
// pollExpiresIn: pollExpiresIn,
// inReplyToID: inReplyToID,
// sensitive: sensitive,
// spoilerText: spoilerText,
// visibility: visibility
// )
// return try await viewModel.context.apiService.publishStatus(
// domain: domain,
// idempotencyKey: idempotencyKey,
// query: query,
// authenticationBox: authenticationBox
// )
// }
// .receive(on: DispatchQueue.main)
// .sink { completion in
// switch completion {
// case .failure(let error):
// os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: publish status %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription)
// stateMachine.enter(Fail.self)
// case .finished:
// os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: publish status success", ((#file as NSString).lastPathComponent), #line, #function)
// stateMachine.enter(Finish.self)
// }
// } receiveValue: { response in
// os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: status %s published: %s", ((#file as NSString).lastPathComponent), #line, #function, response.value.id, response.value.uri)
// }
// }
// }
//
// class Fail: ComposeViewModel.PublishState {
// override func isValidNextState(_ stateClass: AnyClass) -> Bool {
// // allow discard publishing
// return stateClass == Publishing.self || stateClass == Discard.self
// }
// }
//
// class Discard: ComposeViewModel.PublishState {
// override func isValidNextState(_ stateClass: AnyClass) -> Bool {
// return false
// }
// }
//
// class Finish: ComposeViewModel.PublishState {
// override func isValidNextState(_ stateClass: AnyClass) -> Bool {
// return false
// }
// }
//
//}

View File

@ -18,7 +18,7 @@ import MastodonLocalization
import MastodonMeta
import MastodonUI
final class ComposeViewModel: NSObject {
final class ComposeViewModel {
let logger = Logger(subsystem: "ComposeViewModel", category: "ViewModel")
@ -30,84 +30,13 @@ final class ComposeViewModel: NSObject {
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
let traitCollectionDidChangePublisher = CurrentValueSubject<Void, Never>(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<ComposeStatusSection, ComposeStatusItem>?
// var customEmojiPickerDiffableDataSource: UICollectionViewDiffableDataSource<CustomEmojiPickerSection, CustomEmojiPickerItem>?
// 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<PublishState?, Never>(nil)
// private(set) var publishDate = Date() // update it when enter Publishing state
//
// // TODO: group post material into Hashable class
// var idempotencyKey = CurrentValueSubject<String, Never>(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: "#<hashtag> "
// // for mention: "@<mention> "
// var preInsertedContent: String?
//
// // attachment
// @Published var attachmentServices: [MastodonAttachmentService] = []
//
// // polls
// @Published var pollOptionAttributes: [ComposeStatusPollItem.PollOptionAttribute] = []
// let pollExpiresOptionAttribute = ComposeStatusPollItem.PollExpiresOptionAttribute()
// UI & UX
@Published var title: String
init(
context: AppContext,
@ -117,63 +46,14 @@ final class ComposeViewModel: NSObject {
self.context = context
self.authContext = authContext
self.kind = kind
// end init
// 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)
self.title = {
switch kind {
case .post, .hashtag, .mention: return L10n.Scene.Compose.Title.newPost
case .reply: return L10n.Scene.Compose.Title.newReply
}
}()
}
deinit {
@ -181,194 +61,3 @@ final class ComposeViewModel: NSObject {
}
}
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
// }
//}

View File

@ -57,7 +57,6 @@ final public class AttachmentViewModel: NSObject, ObservableObject, Identifiable
@Published public private(set) var thumbnail: UIImage? // original size image thumbnail
@Published public private(set) var outputSizeInByte: Int64 = 0
@MainActor
@Published public private(set) var uploadState: UploadState = .none
@Published public private(set) var uploadResult: UploadResult?
@Published var error: Error?

View File

@ -14,6 +14,8 @@ import MastodonCore
public final class ComposeContentViewController: UIViewController {
static let minAutoCompleteVisibleHeight: CGFloat = 100
let logger = Logger(subsystem: "ComposeContentViewController", category: "ViewController")
var disposeBag = Set<AnyCancellable>()
@ -40,7 +42,6 @@ public final class ComposeContentViewController: UIViewController {
}()
// toolbar
lazy var composeContentToolbarView = ComposeContentToolbarView(viewModel: composeContentToolbarViewModel)
var composeContentToolbarViewBottomLayoutConstraint: NSLayoutConstraint!
let composeContentToolbarBackgroundView = UIView()
@ -146,49 +147,42 @@ extension ComposeContentViewController {
])
// bind keyboard
let keyboardHasShortcutBar = CurrentValueSubject<Bool, Never>(traitCollection.userInterfaceIdiom == .pad) // update default value later
let keyboardEventPublishers = Publishers.CombineLatest3(
KeyboardResponderService.shared.isShow,
KeyboardResponderService.shared.state,
KeyboardResponderService.shared.endFrame
)
// Publishers.CombineLatest3(
// viewModel.$isCustomEmojiComposing,
// )
keyboardEventPublishers
.sink(receiveValue: { [weak self] keyboardEvents in
Publishers.CombineLatest3(
keyboardEventPublishers,
viewModel.$isEmojiActive,
viewModel.$autoCompleteInfo
)
.sink(receiveValue: { [weak self] keyboardEvents, isEmojiActive, autoCompleteInfo in
guard let self = self else { return }
let (isShow, state, endFrame) = keyboardEvents
// switch self.traitCollection.userInterfaceIdiom {
// case .pad:
// keyboardHasShortcutBar.value = state != .floating
// default:
// keyboardHasShortcutBar.value = false
// }
//
let extraMargin: CGFloat = {
var margin = ComposeContentToolbarView.toolbarHeight
// if autoCompleteInfo != nil {
//// margin += ComposeViewController.minAutoCompleteVisibleHeight
// }
if autoCompleteInfo != nil {
margin += ComposeContentViewController.minAutoCompleteVisibleHeight
}
return margin
}()
//
guard isShow, state == .dock else {
self.tableView.contentInset.bottom = extraMargin
self.tableView.verticalScrollIndicatorInsets.bottom = extraMargin
// if let superView = self.autoCompleteViewController.tableView.superview {
// let autoCompleteTableViewBottomInset: CGFloat = {
// let tableViewFrameInWindow = superView.convert(self.autoCompleteViewController.tableView.frame, to: nil)
// let padding = tableViewFrameInWindow.maxY + self.composeToolbarView.frame.height + AutoCompleteViewController.chevronViewHeight - self.view.frame.maxY
// return max(0, padding)
// }()
// self.autoCompleteViewController.tableView.contentInset.bottom = autoCompleteTableViewBottomInset
// self.autoCompleteViewController.tableView.verticalScrollIndicatorInsets.bottom = autoCompleteTableViewBottomInset
// }
if let superView = self.autoCompleteViewController.tableView.superview {
let autoCompleteTableViewBottomInset: CGFloat = {
let tableViewFrameInWindow = superView.convert(self.autoCompleteViewController.tableView.frame, to: nil)
let padding = tableViewFrameInWindow.maxY + ComposeContentToolbarView.toolbarHeight + AutoCompleteViewController.chevronViewHeight - self.view.frame.maxY
return max(0, padding)
}()
self.autoCompleteViewController.tableView.contentInset.bottom = autoCompleteTableViewBottomInset
self.autoCompleteViewController.tableView.verticalScrollIndicatorInsets.bottom = autoCompleteTableViewBottomInset
}
UIView.animate(withDuration: 0.3) {
self.composeContentToolbarViewBottomLayoutConstraint.constant = self.view.safeAreaInsets.bottom
@ -199,17 +193,16 @@ extension ComposeContentViewController {
return
}
// isShow AND dock state
// self.systemKeyboardHeight = endFrame.height
// adjust inset for auto-complete
// let autoCompleteTableViewBottomInset: CGFloat = {
// guard let superview = self.autoCompleteViewController.tableView.superview else { return .zero }
// let tableViewFrameInWindow = superview.convert(self.autoCompleteViewController.tableView.frame, to: nil)
// let padding = tableViewFrameInWindow.maxY + self.composeToolbarView.frame.height + AutoCompleteViewController.chevronViewHeight - endFrame.minY
// return max(0, padding)
// }()
// self.autoCompleteViewController.tableView.contentInset.bottom = autoCompleteTableViewBottomInset
// self.autoCompleteViewController.tableView.verticalScrollIndicatorInsets.bottom = autoCompleteTableViewBottomInset
let autoCompleteTableViewBottomInset: CGFloat = {
guard let superview = self.autoCompleteViewController.tableView.superview else { return .zero }
let tableViewFrameInWindow = superview.convert(self.autoCompleteViewController.tableView.frame, to: nil)
let padding = tableViewFrameInWindow.maxY + ComposeContentToolbarView.toolbarHeight + AutoCompleteViewController.chevronViewHeight - endFrame.minY
return max(0, padding)
}()
self.autoCompleteViewController.tableView.contentInset.bottom = autoCompleteTableViewBottomInset
self.autoCompleteViewController.tableView.verticalScrollIndicatorInsets.bottom = autoCompleteTableViewBottomInset
// adjust inset for tableView
let contentFrame = self.view.convert(self.tableView.frame, to: nil)
@ -289,6 +282,15 @@ extension ComposeContentViewController {
// bind toolbar
bindToolbarViewModel()
// bind attachment picker
viewModel.$attachmentViewModels
.receive(on: DispatchQueue.main)
.sink { [weak self] _ in
guard let self = self else { return }
self.resetImagePicker()
}
.store(in: &disposeBag)
}
public override func viewDidLayoutSubviews() {
@ -327,6 +329,8 @@ extension ComposeContentViewController {
}
private func bindToolbarViewModel() {
viewModel.$isAttachmentButtonEnabled.assign(to: &composeContentToolbarViewModel.$isAttachmentButtonEnabled)
viewModel.$isPollButtonEnabled.assign(to: &composeContentToolbarViewModel.$isPollButtonEnabled)
viewModel.$isPollActive.assign(to: &composeContentToolbarViewModel.$isPollActive)
viewModel.$isEmojiActive.assign(to: &composeContentToolbarViewModel.$isEmojiActive)
viewModel.$isContentWarningActive.assign(to: &composeContentToolbarViewModel.$isContentWarningActive)
@ -345,6 +349,18 @@ extension ComposeContentViewController {
autoCompleteViewController.view.frame.size.width = view.frame.width
}
}
private func resetImagePicker() {
let selectionLimit = max(1, viewModel.maxMediaAttachmentLimit - viewModel.attachmentViewModels.count)
let configuration = ComposeContentViewController.createPhotoLibraryPickerConfiguration(selectionLimit: selectionLimit)
photoLibraryPicker = createImagePicker(configuration: configuration)
}
private func createImagePicker(configuration: PHPickerConfiguration) -> PHPickerViewController {
let imagePicker = PHPickerViewController(configuration: configuration)
imagePicker.delegate = self
return imagePicker
}
}
// MARK: - UIScrollViewDelegate

View File

@ -14,6 +14,7 @@ import MetaTextKit
import MastodonMeta
import MastodonCore
import MastodonSDK
import MastodonLocalization
public protocol ComposeContentViewModelDelegate: AnyObject {
func composeContentViewModel(_ viewModel: ComposeContentViewModel, handleAutoComplete info: ComposeContentViewModel.AutoCompleteInfo) -> Bool
@ -58,6 +59,8 @@ public final class ComposeContentViewModel: NSObject, ObservableObject {
customEmojiPickerInputViewModel.configure(textInput: textView)
}
}
// for hashtag: "#<hashtag> "
// for mention: "@<mention> "
@Published public var initialContent = ""
@Published public var content = ""
@Published public var contentWeightedLength = 0
@ -115,6 +118,14 @@ public final class ComposeContentViewModel: NSObject, ObservableObject {
@Published var contentCellFrame: CGRect = .zero
@Published var contentTextViewFrame: CGRect = .zero
@Published var scrollViewState: ScrollViewState = .fold
@Published var characterCount: Int = 0
@Published public private(set) var isPublishBarButtonItemEnabled = true
@Published var isAttachmentButtonEnabled = false
@Published var isPollButtonEnabled = false
@Published public private(set) var shouldDismiss = true
public init(
context: AppContext,
@ -165,6 +176,70 @@ public final class ComposeContentViewModel: NSObject, ObservableObject {
super.init()
// end init
// setup initial value
switch kind {
case .reply(let record):
context.managedObjectContext.performAndWait {
guard let status = record.object(in: context.managedObjectContext) else {
assertionFailure()
return
}
let author = authContext.mastodonAuthenticationBox.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.isContentWarningActive = true
self.contentWarning = spoilerText
}
let initialComposeContent = mentionAccts.joined(separator: " ")
let preInsertedContent: String? = initialComposeContent.isEmpty ? nil : initialComposeContent + " "
self.initialContent = preInsertedContent ?? ""
self.content = preInsertedContent ?? ""
}
case .hashtag(let hashtag):
let initialComposeContent = "#" + hashtag
UITextChecker.learnWord(initialComposeContent)
let preInsertedContent = initialComposeContent + " "
self.initialContent = preInsertedContent
self.content = 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.initialContent = preInsertedContent
self.content = preInsertedContent
}
case .post:
break
}
bind()
}
deinit {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
}
}
extension ComposeContentViewModel {
private func bind() {
// bind author
$authContext
.sink { [weak self] authContext in
@ -210,12 +285,129 @@ public final class ComposeContentViewModel: NSObject, ObservableObject {
// bind emoji inputView
$isEmojiActive.assign(to: &customEmojiPickerInputViewModel.$isCustomEmojiComposing)
}
deinit {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
}
// bind toolbar
Publishers.CombineLatest3(
$isPollActive,
$attachmentViewModels,
$maxMediaAttachmentLimit
)
.receive(on: DispatchQueue.main)
.sink { [weak self] isPollActive, attachmentViewModels, maxMediaAttachmentLimit in
guard let self = self else { return }
let shouldMediaDisable = isPollActive || attachmentViewModels.count >= maxMediaAttachmentLimit
let shouldPollDisable = attachmentViewModels.count > 0
self.isAttachmentButtonEnabled = !shouldMediaDisable
self.isPollButtonEnabled = !shouldPollDisable
}
.store(in: &disposeBag)
// bind status content character count
Publishers.CombineLatest3(
$contentWeightedLength,
$contentWarningWeightedLength,
$isContentWarningActive
)
.map { contentWeightedLength, contentWarningWeightedLength, isContentWarningActive -> Int in
var count = contentWeightedLength
if isContentWarningActive {
count += contentWarningWeightedLength
}
return count
}
.assign(to: &$characterCount)
// bind compose bar button item UI state
let isComposeContentEmpty = $content
.map { $0.isEmpty }
let isComposeContentValid = Publishers.CombineLatest(
$characterCount,
$maxTextInputLimit
)
.map { characterCount, maxTextInputLimit in
characterCount <= maxTextInputLimit
}
let isMediaEmpty = $attachmentViewModels
.map { $0.isEmpty }
let isMediaUploadAllSuccess = $attachmentViewModels
.map { attachmentViewModels in
return Publishers.MergeMany(attachmentViewModels.map { $0.$uploadState })
.delay(for: 0.5, scheduler: DispatchQueue.main) // convert to outputs with delay. Due to @Published emit before changes
.map { _ in attachmentViewModels.map { $0.uploadState } }
}
.switchToLatest()
.map { outputs in
guard outputs.allSatisfy({ $0 == .finish }) else { return false }
return true
}
isMediaUploadAllSuccess.sink { result in
print(result)
}
.store(in: &disposeBag)
let isPollOptionsAllValid = $pollOptions
.map { options in
return Publishers.MergeMany(options.map { $0.$text })
.delay(for: 0.5, scheduler: DispatchQueue.main) // convert to outputs with delay. Due to @Published emit before changes
.map { _ in options.map { $0.text } }
}
.switchToLatest()
.map { outputs in
return outputs.allSatisfy { !$0.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,
$isPollActive,
isPollOptionsAllValid
)
.map { isComposeContentEmpty, isComposeContentValid, isPollComposing, isPollOptionsAllValid -> Bool in
if isPollComposing {
return isComposeContentValid && !isComposeContentEmpty && isPollOptionsAllValid
} else {
return isComposeContentValid && !isComposeContentEmpty
}
}
.eraseToAnyPublisher()
Publishers.CombineLatest(
isPublishBarButtonItemEnabledPrecondition1,
isPublishBarButtonItemEnabledPrecondition2
)
.map { $0 && $1 }
.assign(to: &$isPublishBarButtonItemEnabled)
// bind modal dismiss state
$content
.receive(on: DispatchQueue.main)
.map { [weak self] content in
guard let self = self else { return }
if content.isEmpty {
return true
}
// if the trimmed content equal to initial content
return content.trimmingCharacters(in: .whitespacesAndNewlines) == self.initialContent
}
.assign(to: &$shouldDismiss)
}
}
extension ComposeContentViewModel {
@ -325,6 +517,58 @@ extension ComposeContentViewModel {
} // end func publisher()
}
extension ComposeContentViewModel {
public enum AttachmentPrecondition: Error, LocalizedError {
case videoAttachWithPhoto
case moreThanOneVideo
public var errorDescription: String? {
return L10n.Common.Alerts.PublishPostFailure.title
}
public 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
public func checkAttachmentPrecondition() throws {
let attachmentViewModels = self.attachmentViewModels
guard !attachmentViewModels.isEmpty else { return }
var photoAttachmentViewModels: [AttachmentViewModel] = []
var videoAttachmentViewModels: [AttachmentViewModel] = []
attachmentViewModels.forEach { attachmentViewModel in
guard let output = attachmentViewModel.output else {
assertionFailure()
return
}
switch output {
case .image:
photoAttachmentViewModels.append(attachmentViewModel)
case .video:
videoAttachmentViewModels.append(attachmentViewModel)
}
}
if !videoAttachmentViewModels.isEmpty {
guard videoAttachmentViewModels.count == 1 else {
throw AttachmentPrecondition.moreThanOneVideo
}
guard photoAttachmentViewModels.isEmpty else {
throw AttachmentPrecondition.videoAttachWithPhoto
}
}
}
}
// MARK: - DeleteBackwardResponseTextFieldRelayDelegate
extension ComposeContentViewModel: DeleteBackwardResponseTextFieldRelayDelegate {

View File

@ -39,7 +39,7 @@ public struct PollOptionTextField: UIViewRepresentable {
textField.text = text
textField.placeholder = {
if index >= 0 {
return L10n.Scene.Compose.Poll.optionNumber(index)
return L10n.Scene.Compose.Poll.optionNumber(index + 1)
} else {
assertionFailure()
return ""

View File

@ -27,6 +27,9 @@ extension ComposeContentToolbarView {
@Published var isEmojiActive = false
@Published var isContentWarningActive = false
@Published var isAttachmentButtonEnabled = false
@Published var isPollButtonEnabled = false
@Published public var maxTextInputLimit = 500
@Published public var contentWeightedLength = 0
@Published public var contentWarningWeightedLength = 0

View File

@ -44,7 +44,9 @@ struct ComposeContentToolbarView: View {
}
} label: {
label(for: action)
.opacity(viewModel.isAttachmentButtonEnabled ? 1.0 : 0.5)
}
.disabled(!viewModel.isAttachmentButtonEnabled)
.frame(width: 48, height: 48)
case .visibility:
Menu {
@ -63,6 +65,16 @@ struct ComposeContentToolbarView: View {
label(for: viewModel.visibility.image)
}
.frame(width: 48, height: 48)
case .poll:
Button {
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): \(String(describing: action))")
viewModel.delegate?.composeContentToolbarView(viewModel, toolbarItemDidPressed: action)
} label: {
label(for: action)
.opacity(viewModel.isPollButtonEnabled ? 1.0 : 0.5)
}
.disabled(!viewModel.isPollButtonEnabled)
.frame(width: 48, height: 48)
default:
Button {
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): \(String(describing: action))")