feat: [WIP] restore publish button and compose pre-insert content
This commit is contained in:
parent
88307057c0
commit
929a27d572
|
@ -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 */,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
// }
|
||||
//}
|
|
@ -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
|
||||
// }
|
||||
// }
|
||||
//
|
||||
//}
|
|
@ -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
|
||||
// }
|
||||
//}
|
||||
|
|
|
@ -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?
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
||||
|
|
|
@ -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 ""
|
||||
|
|
|
@ -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
|
|
@ -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))")
|
Loading…
Reference in New Issue