488 lines
22 KiB
Swift
488 lines
22 KiB
Swift
//
|
|
// ComposeViewController.swift
|
|
// Mastodon
|
|
//
|
|
// Created by MainasuK Cirno on 2021-3-11.
|
|
//
|
|
|
|
import os.log
|
|
import UIKit
|
|
import Combine
|
|
import PhotosUI
|
|
import Meta
|
|
import MetaTextKit
|
|
import MastodonMeta
|
|
import MastodonAsset
|
|
import MastodonCore
|
|
import MastodonUI
|
|
import MastodonLocalization
|
|
import MastodonSDK
|
|
|
|
final class ComposeViewController: UIViewController, NeedsDependency {
|
|
|
|
static let minAutoCompleteVisibleHeight: CGFloat = 100
|
|
|
|
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
|
|
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
|
|
|
|
var disposeBag = Set<AnyCancellable>()
|
|
var viewModel: ComposeViewModel!
|
|
|
|
let logger = Logger(subsystem: "ComposeViewController", category: "logic")
|
|
|
|
lazy var composeContentViewModel: ComposeContentViewModel = {
|
|
return ComposeContentViewModel(
|
|
context: context,
|
|
authContext: viewModel.authContext,
|
|
kind: viewModel.kind
|
|
)
|
|
}()
|
|
private(set) lazy var composeContentViewController: ComposeContentViewController = {
|
|
let composeContentViewController = ComposeContentViewController()
|
|
composeContentViewController.viewModel = composeContentViewModel
|
|
return composeContentViewController
|
|
}()
|
|
|
|
private(set) lazy var cancelBarButtonItem = UIBarButtonItem(title: L10n.Common.Controls.Actions.cancel, style: .plain, target: self, action: #selector(ComposeViewController.cancelBarButtonItemPressed(_:)))
|
|
let characterCountLabel: UILabel = {
|
|
let label = UILabel()
|
|
label.font = .systemFont(ofSize: 15, weight: .regular)
|
|
label.text = "500"
|
|
label.textColor = Asset.Colors.Label.secondary.color
|
|
label.accessibilityLabel = L10n.A11y.Plural.Count.inputLimitRemains(500)
|
|
return label
|
|
}()
|
|
private(set) lazy var characterCountBarButtonItem: UIBarButtonItem = {
|
|
let barButtonItem = UIBarButtonItem(customView: characterCountLabel)
|
|
return barButtonItem
|
|
}()
|
|
|
|
let publishButton: UIButton = {
|
|
let button = RoundedEdgesButton(type: .custom)
|
|
button.cornerRadius = 10
|
|
button.contentEdgeInsets = UIEdgeInsets(top: 6, left: 16, bottom: 5, right: 16) // set 28pt height
|
|
button.titleLabel?.font = .systemFont(ofSize: 14, weight: .bold)
|
|
button.setTitle(L10n.Scene.Compose.composeAction, for: .normal)
|
|
return button
|
|
}()
|
|
private(set) lazy var publishBarButtonItem: UIBarButtonItem = {
|
|
configurePublishButtonApperance()
|
|
let shadowBackgroundContainer = ShadowBackgroundContainer()
|
|
publishButton.translatesAutoresizingMaskIntoConstraints = false
|
|
shadowBackgroundContainer.addSubview(publishButton)
|
|
NSLayoutConstraint.activate([
|
|
publishButton.topAnchor.constraint(equalTo: shadowBackgroundContainer.topAnchor),
|
|
publishButton.leadingAnchor.constraint(equalTo: shadowBackgroundContainer.leadingAnchor),
|
|
publishButton.trailingAnchor.constraint(equalTo: shadowBackgroundContainer.trailingAnchor),
|
|
publishButton.bottomAnchor.constraint(equalTo: shadowBackgroundContainer.bottomAnchor),
|
|
])
|
|
let barButtonItem = UIBarButtonItem(customView: shadowBackgroundContainer)
|
|
return barButtonItem
|
|
}()
|
|
private func configurePublishButtonApperance() {
|
|
publishButton.adjustsImageWhenHighlighted = false
|
|
publishButton.setBackgroundImage(.placeholder(color: Asset.Colors.Label.primary.color), for: .normal)
|
|
publishButton.setBackgroundImage(.placeholder(color: Asset.Colors.Label.primary.color.withAlphaComponent(0.5)), for: .highlighted)
|
|
publishButton.setBackgroundImage(.placeholder(color: Asset.Colors.Button.disabled.color), for: .disabled)
|
|
publishButton.setTitleColor(Asset.Colors.Label.primaryReverse.color, for: .normal)
|
|
}
|
|
|
|
deinit {
|
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
|
}
|
|
|
|
}
|
|
|
|
extension ComposeViewController {
|
|
private static func createLayout() -> UICollectionViewLayout {
|
|
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(44))
|
|
let item = NSCollectionLayoutItem(layoutSize: itemSize)
|
|
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(44))
|
|
let group = NSCollectionLayoutGroup.vertical(layoutSize: groupSize, subitems: [item])
|
|
let section = NSCollectionLayoutSection(group: group)
|
|
section.contentInsetsReference = .readableContent
|
|
// section.interGroupSpacing = 10
|
|
// section.contentInsets = NSDirectionalEdgeInsets(top: 10, leading: 10, bottom: 10, trailing: 10)
|
|
return UICollectionViewCompositionalLayout(section: section)
|
|
}
|
|
}
|
|
|
|
extension ComposeViewController {
|
|
|
|
override func viewDidLoad() {
|
|
super.viewDidLoad()
|
|
|
|
navigationItem.leftBarButtonItem = cancelBarButtonItem
|
|
navigationItem.rightBarButtonItem = publishBarButtonItem
|
|
viewModel.traitCollectionDidChangePublisher
|
|
.receive(on: DispatchQueue.main)
|
|
.sink { [weak self] _ in
|
|
guard let self = self else { return }
|
|
guard self.traitCollection.userInterfaceIdiom == .pad else { return }
|
|
var items = [self.publishBarButtonItem]
|
|
if self.traitCollection.horizontalSizeClass == .regular {
|
|
items.append(self.characterCountBarButtonItem)
|
|
}
|
|
self.navigationItem.rightBarButtonItems = items
|
|
}
|
|
.store(in: &disposeBag)
|
|
publishButton.addTarget(self, action: #selector(ComposeViewController.publishBarButtonItemPressed(_:)), for: .touchUpInside)
|
|
|
|
addChild(composeContentViewController)
|
|
composeContentViewController.view.translatesAutoresizingMaskIntoConstraints = false
|
|
view.addSubview(composeContentViewController.view)
|
|
NSLayoutConstraint.activate([
|
|
composeContentViewController.view.topAnchor.constraint(equalTo: view.topAnchor),
|
|
composeContentViewController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
|
composeContentViewController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
|
composeContentViewController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
|
])
|
|
composeContentViewController.didMove(toParent: self)
|
|
|
|
// 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)
|
|
|
|
// 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)
|
|
|
|
// 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
|
|
// .receive(on: DispatchQueue.main)
|
|
// .sink { [weak self] isContentWarningComposing in
|
|
// guard let self = self else { return }
|
|
// let accessibilityLabel = isContentWarningComposing ? L10n.Scene.Compose.Accessibility.disableContentWarning : L10n.Scene.Compose.Accessibility.enableContentWarning
|
|
// self.composeToolbarView.contentWarningBarButtonItem.accessibilityLabel = accessibilityLabel
|
|
// self.composeToolbarView.contentWarningButton.accessibilityLabel = accessibilityLabel
|
|
// }
|
|
// .store(in: &disposeBag)
|
|
|
|
}
|
|
|
|
override func viewDidAppear(_ animated: Bool) {
|
|
super.viewDidAppear(animated)
|
|
|
|
// viewModel.isViewAppeared = true
|
|
}
|
|
|
|
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
|
super.traitCollectionDidChange(previousTraitCollection)
|
|
|
|
configurePublishButtonApperance()
|
|
viewModel.traitCollectionDidChangePublisher.send()
|
|
}
|
|
|
|
}
|
|
|
|
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 {
|
|
// case .light:
|
|
// return .systemBackground
|
|
// default:
|
|
// return theme.systemElevatedBackgroundColor
|
|
// }
|
|
// })
|
|
// view.backgroundColor = backgroundColor
|
|
//// tableView.backgroundColor = backgroundColor
|
|
//// composeToolbarBackgroundView.backgroundColor = theme.composeToolbarBackgroundColor
|
|
// }
|
|
//
|
|
// // keyboard shortcutBar
|
|
// private func setupInputAssistantItem(item: UITextInputAssistantItem) {
|
|
// let barButtonItems = [
|
|
// composeToolbarView.mediaBarButtonItem,
|
|
// composeToolbarView.pollBarButtonItem,
|
|
// composeToolbarView.contentWarningBarButtonItem,
|
|
// composeToolbarView.visibilityBarButtonItem,
|
|
// ]
|
|
// let group = UIBarButtonItemGroup(barButtonItems: barButtonItems, representativeItem: nil)
|
|
//
|
|
// item.trailingBarButtonGroups = [group]
|
|
// }
|
|
//
|
|
// private func configureToolbarDisplay(keyboardHasShortcutBar: Bool) {
|
|
// switch self.traitCollection.userInterfaceIdiom {
|
|
// case .pad:
|
|
// let shouldHideToolbar = keyboardHasShortcutBar && self.traitCollection.horizontalSizeClass == .regular
|
|
// self.composeToolbarView.alpha = shouldHideToolbar ? 0 : 1
|
|
// self.composeToolbarBackgroundView.alpha = shouldHideToolbar ? 0 : 1
|
|
// 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 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 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()
|
|
// let result = try await statusPublisher.publish(api: context.apiService, authContext: viewModel.authContext)
|
|
// if let reactor = presentingViewController?.topMostNotModal as? StatusPublisherReactor {
|
|
// statusPublisher.reactor = reactor
|
|
// }
|
|
viewModel.context.publisherService.enqueue(
|
|
statusPublisher: statusPublisher,
|
|
authContext: viewModel.authContext
|
|
)
|
|
} catch {
|
|
let alertController = UIAlertController.standardAlert(of: error)
|
|
present(alertController, animated: true)
|
|
return
|
|
}
|
|
|
|
dismiss(animated: true, completion: nil)
|
|
}
|
|
|
|
}
|
|
|
|
// MARK: - UIAdaptivePresentationControllerDelegate
|
|
extension ComposeViewController: UIAdaptivePresentationControllerDelegate {
|
|
|
|
func adaptivePresentationStyle(for controller: UIPresentationController, traitCollection: UITraitCollection) -> UIModalPresentationStyle {
|
|
switch traitCollection.horizontalSizeClass {
|
|
case .compact:
|
|
return .overFullScreen
|
|
default:
|
|
return .pageSheet
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
}
|
|
|
|
//extension ComposeViewController {
|
|
// override var keyCommands: [UIKeyCommand]? {
|
|
// composeKeyCommands
|
|
// }
|
|
//}
|
|
//
|
|
//extension ComposeViewController {
|
|
//
|
|
// enum ComposeKeyCommand: String, CaseIterable {
|
|
// case discardPost
|
|
// case publishPost
|
|
// case mediaBrowse
|
|
// case mediaPhotoLibrary
|
|
// case mediaCamera
|
|
// case togglePoll
|
|
// case toggleContentWarning
|
|
// case selectVisibilityPublic
|
|
// // TODO: remove selectVisibilityUnlisted from codebase
|
|
// // case selectVisibilityUnlisted
|
|
// case selectVisibilityPrivate
|
|
// case selectVisibilityDirect
|
|
//
|
|
// var title: String {
|
|
// switch self {
|
|
// case .discardPost: return L10n.Scene.Compose.Keyboard.discardPost
|
|
// case .publishPost: return L10n.Scene.Compose.Keyboard.publishPost
|
|
// case .mediaBrowse: return L10n.Scene.Compose.Keyboard.appendAttachmentEntry(L10n.Scene.Compose.MediaSelection.browse)
|
|
// case .mediaPhotoLibrary: return L10n.Scene.Compose.Keyboard.appendAttachmentEntry(L10n.Scene.Compose.MediaSelection.photoLibrary)
|
|
// case .mediaCamera: return L10n.Scene.Compose.Keyboard.appendAttachmentEntry(L10n.Scene.Compose.MediaSelection.camera)
|
|
// case .togglePoll: return L10n.Scene.Compose.Keyboard.togglePoll
|
|
// case .toggleContentWarning: return L10n.Scene.Compose.Keyboard.toggleContentWarning
|
|
// case .selectVisibilityPublic: return L10n.Scene.Compose.Keyboard.selectVisibilityEntry(L10n.Scene.Compose.Visibility.public)
|
|
// // case .selectVisibilityUnlisted: return L10n.Scene.Compose.Keyboard.selectVisibilityEntry(L10n.Scene.Compose.Visibility.unlisted)
|
|
// case .selectVisibilityPrivate: return L10n.Scene.Compose.Keyboard.selectVisibilityEntry(L10n.Scene.Compose.Visibility.private)
|
|
// case .selectVisibilityDirect: return L10n.Scene.Compose.Keyboard.selectVisibilityEntry(L10n.Scene.Compose.Visibility.direct)
|
|
// }
|
|
// }
|
|
//
|
|
// // UIKeyCommand input
|
|
// var input: String {
|
|
// switch self {
|
|
// case .discardPost: return "w" // + command
|
|
// case .publishPost: return "\r" // (enter) + command
|
|
// case .mediaBrowse: return "b" // + option + command
|
|
// case .mediaPhotoLibrary: return "p" // + option + command
|
|
// case .mediaCamera: return "c" // + option + command
|
|
// case .togglePoll: return "p" // + shift + command
|
|
// case .toggleContentWarning: return "c" // + shift + command
|
|
// case .selectVisibilityPublic: return "1" // + command
|
|
// // case .selectVisibilityUnlisted: return "2" // + command
|
|
// case .selectVisibilityPrivate: return "2" // + command
|
|
// case .selectVisibilityDirect: return "3" // + command
|
|
// }
|
|
// }
|
|
//
|
|
// var modifierFlags: UIKeyModifierFlags {
|
|
// switch self {
|
|
// case .discardPost: return [.command]
|
|
// case .publishPost: return [.command]
|
|
// case .mediaBrowse: return [.alternate, .command]
|
|
// case .mediaPhotoLibrary: return [.alternate, .command]
|
|
// case .mediaCamera: return [.alternate, .command]
|
|
// case .togglePoll: return [.shift, .command]
|
|
// case .toggleContentWarning: return [.shift, .command]
|
|
// case .selectVisibilityPublic: return [.command]
|
|
// // case .selectVisibilityUnlisted: return [.command]
|
|
// case .selectVisibilityPrivate: return [.command]
|
|
// case .selectVisibilityDirect: return [.command]
|
|
// }
|
|
// }
|
|
//
|
|
// var propertyList: Any {
|
|
// return rawValue
|
|
// }
|
|
// }
|
|
//
|
|
// var composeKeyCommands: [UIKeyCommand]? {
|
|
// ComposeKeyCommand.allCases.map { command in
|
|
// UIKeyCommand(
|
|
// title: command.title,
|
|
// image: nil,
|
|
// action: #selector(Self.composeKeyCommandHandler(_:)),
|
|
// input: command.input,
|
|
// modifierFlags: command.modifierFlags,
|
|
// propertyList: command.propertyList,
|
|
// alternates: [],
|
|
// discoverabilityTitle: nil,
|
|
// attributes: [],
|
|
// state: .off
|
|
// )
|
|
// }
|
|
// }
|
|
//
|
|
// @objc private func composeKeyCommandHandler(_ sender: UIKeyCommand) {
|
|
// guard let rawValue = sender.propertyList as? String,
|
|
// let command = ComposeKeyCommand(rawValue: rawValue) else { return }
|
|
//
|
|
// switch command {
|
|
// case .discardPost:
|
|
// cancelBarButtonItemPressed(cancelBarButtonItem)
|
|
// case .publishPost:
|
|
// publishBarButtonItemPressed(publishBarButtonItem)
|
|
// case .mediaBrowse:
|
|
// present(documentPickerController, animated: true, completion: nil)
|
|
// case .mediaPhotoLibrary:
|
|
// present(photoLibraryPicker, animated: true, completion: nil)
|
|
// case .mediaCamera:
|
|
// guard UIImagePickerController.isSourceTypeAvailable(.camera) else {
|
|
// return
|
|
// }
|
|
// present(imagePickerController, animated: true, completion: nil)
|
|
// case .togglePoll:
|
|
// composeToolbarView.pollButton.sendActions(for: .touchUpInside)
|
|
// case .toggleContentWarning:
|
|
// composeToolbarView.contentWarningButton.sendActions(for: .touchUpInside)
|
|
// case .selectVisibilityPublic:
|
|
// viewModel.selectedStatusVisibility = .public
|
|
// // case .selectVisibilityUnlisted:
|
|
// // viewModel.selectedStatusVisibility.value = .unlisted
|
|
// case .selectVisibilityPrivate:
|
|
// viewModel.selectedStatusVisibility = .private
|
|
// case .selectVisibilityDirect:
|
|
// viewModel.selectedStatusVisibility = .direct
|
|
// }
|
|
// }
|
|
//
|
|
//}
|
|
|
|
extension ComposeViewController {
|
|
public override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
|
|
|
|
// Enable pasting images
|
|
if (action == #selector(UIResponderStandardEditActions.paste(_:))) {
|
|
return UIPasteboard.general.hasStrings || UIPasteboard.general.hasImages;
|
|
}
|
|
|
|
return super.canPerformAction(action, withSender: sender);
|
|
}
|
|
|
|
override func paste(_ sender: Any?) {
|
|
logger.debug("Paste event received")
|
|
|
|
// Look for images on the clipboard
|
|
if (UIPasteboard.general.hasImages) {
|
|
if let images = UIPasteboard.general.images {
|
|
logger.warning("Got image paste event, however attachments are not yet re-implemented.");
|
|
// viewModel.attachmentServices = viewModel.attachmentServices + images.map({ image in
|
|
// MastodonAttachmentService(
|
|
// context: context,
|
|
// image: image,
|
|
// initialAuthenticationBox: viewModel.authenticationBox
|
|
// )
|
|
// })
|
|
}
|
|
}
|
|
}
|
|
}
|