123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394 |
- //
- // 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 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 {
-
- 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 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)
- }
-
- 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)
- }
- }
- 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)
- }
-
- }
- 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, let images = UIPasteboard.general.images {
- logger.warning("Got image paste event, however attachments are not yet re-implemented.");
- let attachmentViewModels = images.map { image in
- return AttachmentViewModel(
- api: viewModel.context.apiService,
- authContext: viewModel.authContext,
- input: .image(image),
- delegate: composeContentViewModel
- )
- }
- composeContentViewModel.attachmentViewModels += attachmentViewModels
- }
- }
- }
- // 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:
- guard !isViewControllerIsAlreadyModal(composeContentViewController.documentPickerController) else { return }
- present(composeContentViewController.documentPickerController, animated: true, completion: nil)
- case .mediaPhotoLibrary:
- guard !isViewControllerIsAlreadyModal(composeContentViewController.photoLibraryPicker) else { return }
- present(composeContentViewController.photoLibraryPicker, animated: true, completion: nil)
- case .mediaCamera:
- guard UIImagePickerController.isSourceTypeAvailable(.camera) else {
- return
- }
- guard !isViewControllerIsAlreadyModal(composeContentViewController.imagePickerController) else { return }
- present(composeContentViewController.imagePickerController, animated: true, completion: nil)
- case .togglePoll:
- composeContentViewModel.isPollActive.toggle()
- case .toggleContentWarning:
- composeContentViewModel.isContentWarningActive.toggle()
- case .selectVisibilityPublic:
- composeContentViewModel.visibility = .public
- // case .selectVisibilityUnlisted:
- // viewModel.selectedStatusVisibility.value = .unlisted
- case .selectVisibilityPrivate:
- composeContentViewModel.visibility = .private
- case .selectVisibilityDirect:
- composeContentViewModel.visibility = .direct
- }
- }
-
- private func isViewControllerIsAlreadyModal(_ viewController: UIViewController) -> Bool {
- return viewController.presentingViewController != nil
- }
-
- }
|