mirror of
https://github.com/metabolist/metatext.git
synced 2024-11-22 00:01:00 +00:00
wip
This commit is contained in:
parent
86b9e4c903
commit
3b0f0bf82f
4 changed files with 88 additions and 52 deletions
|
@ -17,3 +17,9 @@ extension Array where Element: Sequence, Element.Element: Hashable {
|
|||
return snapshot
|
||||
}
|
||||
}
|
||||
|
||||
extension Array where Element: Hashable {
|
||||
func snapshot() -> NSDiffableDataSourceSnapshot<Int, Element> {
|
||||
[self].snapshot()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ final class NewStatusViewController: UIViewController {
|
|||
private let viewModel: NewStatusViewModel
|
||||
private let scrollView = UIScrollView()
|
||||
private let stackView = UIStackView()
|
||||
private let activityIndicatorView = UIActivityIndicatorView(style: .large)
|
||||
private let postButton = UIBarButtonItem(
|
||||
title: NSLocalizedString("post", comment: ""),
|
||||
style: .done,
|
||||
|
@ -42,6 +43,10 @@ final class NewStatusViewController: UIViewController {
|
|||
stackView.axis = .vertical
|
||||
stackView.distribution = .equalSpacing
|
||||
|
||||
scrollView.addSubview(activityIndicatorView)
|
||||
activityIndicatorView.translatesAutoresizingMaskIntoConstraints = false
|
||||
activityIndicatorView.hidesWhenStopped = true
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
scrollView.topAnchor.constraint(equalTo: view.topAnchor),
|
||||
|
@ -51,7 +56,9 @@ final class NewStatusViewController: UIViewController {
|
|||
stackView.topAnchor.constraint(equalTo: scrollView.topAnchor),
|
||||
stackView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),
|
||||
stackView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
|
||||
stackView.widthAnchor.constraint(equalTo: scrollView.widthAnchor)
|
||||
stackView.widthAnchor.constraint(equalTo: scrollView.widthAnchor),
|
||||
activityIndicatorView.centerXAnchor.constraint(equalTo: scrollView.centerXAnchor),
|
||||
activityIndicatorView.centerYAnchor.constraint(equalTo: scrollView.centerYAnchor)
|
||||
])
|
||||
|
||||
postButton.primaryAction = UIAction(title: NSLocalizedString("post", comment: "")) { [weak self] _ in
|
||||
|
@ -83,6 +90,51 @@ private extension NewStatusViewController {
|
|||
}
|
||||
}
|
||||
|
||||
func apply(postingState: NewStatusViewModel.PostingState) {
|
||||
switch postingState {
|
||||
case .composing:
|
||||
activityIndicatorView.stopAnimating()
|
||||
stackView.isUserInteractionEnabled = true
|
||||
stackView.alpha = 1
|
||||
case .posting:
|
||||
activityIndicatorView.startAnimating()
|
||||
stackView.isUserInteractionEnabled = false
|
||||
stackView.alpha = 0.5
|
||||
case .done:
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
func set(compositionViewModels: [CompositionViewModel]) {
|
||||
let diff = compositionViewModels.map(\.id).snapshot().itemIdentifiers.difference(
|
||||
from: stackView.arrangedSubviews.compactMap { ($0 as? CompositionView)?.id }
|
||||
.snapshot().itemIdentifiers)
|
||||
|
||||
for insertion in diff.insertions {
|
||||
guard case let .insert(index, id, _) = insertion,
|
||||
let compositionViewModel = compositionViewModels.first(where: { $0.id == id })
|
||||
else { continue }
|
||||
|
||||
let compositionView = CompositionView(
|
||||
viewModel: compositionViewModel,
|
||||
parentViewModel: viewModel)
|
||||
stackView.insertArrangedSubview(compositionView, at: index)
|
||||
compositionView.textView.becomeFirstResponder()
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.scrollView.scrollRectToVisible(
|
||||
self.scrollView.convert(compositionView.frame, from: self.stackView),
|
||||
animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
for removal in diff.removals {
|
||||
guard case let .remove(_, id, _) = removal else { continue }
|
||||
|
||||
stackView.arrangedSubviews.first { ($0 as? CompositionView)?.id == id }?.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
func dismiss() {
|
||||
if let extensionContext = extensionContext {
|
||||
extensionContext.completeRequest(returningItems: nil)
|
||||
|
@ -92,53 +144,23 @@ private extension NewStatusViewController {
|
|||
}
|
||||
|
||||
func setupViewModelBindings() {
|
||||
viewModel.events.sink { [weak self] in self?.handle(event: $0) }.store(in: &cancellables)
|
||||
|
||||
viewModel.$canPost.sink { [weak self] in self?.postButton.isEnabled = $0 }.store(in: &cancellables)
|
||||
|
||||
viewModel.$compositionViewModels.sink { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
||||
let diff = [$0.map(\.id)].snapshot().itemIdentifiers.difference(
|
||||
from: [self.stackView.arrangedSubviews.compactMap { ($0 as? CompositionView)?.id }]
|
||||
.snapshot().itemIdentifiers)
|
||||
|
||||
for insertion in diff.insertions {
|
||||
guard case let .insert(index, id, _) = insertion,
|
||||
let compositionViewModel = $0.first(where: { $0.id == id })
|
||||
else { continue }
|
||||
|
||||
let compositionView = CompositionView(
|
||||
viewModel: compositionViewModel,
|
||||
parentViewModel: self.viewModel)
|
||||
self.stackView.insertArrangedSubview(compositionView, at: index)
|
||||
compositionView.textView.becomeFirstResponder()
|
||||
DispatchQueue.main.async {
|
||||
self.scrollView.scrollRectToVisible(
|
||||
self.scrollView.convert(compositionView.frame, from: self.stackView),
|
||||
animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
for removal in diff.removals {
|
||||
guard case let .remove(_, id, _) = removal else { continue }
|
||||
|
||||
self.stackView.arrangedSubviews.first { ($0 as? CompositionView)?.id == id }?.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
viewModel.$identification
|
||||
.sink { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
||||
self.setupBarButtonItems(identification: $0)
|
||||
}
|
||||
viewModel.events
|
||||
.sink { [weak self] in self?.handle(event: $0) }
|
||||
.store(in: &cancellables)
|
||||
viewModel.$canPost
|
||||
.sink { [weak self] in self?.postButton.isEnabled = $0 }
|
||||
.store(in: &cancellables)
|
||||
viewModel.$compositionViewModels
|
||||
.sink { [weak self] in self?.set(compositionViewModels: $0) }
|
||||
.store(in: &cancellables)
|
||||
viewModel.$identification
|
||||
.sink { [weak self] in self?.setupBarButtonItems(identification: $0) }
|
||||
.store(in: &cancellables)
|
||||
viewModel.$postingState
|
||||
.sink { [weak self] in self?.apply(postingState: $0) }
|
||||
.store(in: &cancellables)
|
||||
|
||||
viewModel.$alertItem
|
||||
.compactMap { $0 }
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] in self?.present(alertItem: $0) }
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ public final class NewStatusViewModel: ObservableObject {
|
|||
@Published public var canPost = false
|
||||
@Published public var canChangeIdentity = true
|
||||
@Published public var alertItem: AlertItem?
|
||||
@Published public private(set) var loading = false
|
||||
@Published public private(set) var postingState = PostingState.composing
|
||||
public let events: AnyPublisher<Event, Never>
|
||||
|
||||
private let allIdentitiesService: AllIdentitiesService
|
||||
|
@ -36,8 +36,8 @@ public final class NewStatusViewModel: ObservableObject {
|
|||
$compositionViewModels.flatMap { Publishers.MergeMany($0.map(\.$isPostable)) }
|
||||
.receive(on: DispatchQueue.main) // hack to punt to next run loop, consider refactoring
|
||||
.compactMap { [weak self] _ in self?.compositionViewModels.allSatisfy(\.isPostable) }
|
||||
.combineLatest($loading)
|
||||
.map { $0 && !$1 }
|
||||
.combineLatest($postingState)
|
||||
.map { $0 && $1 == .composing }
|
||||
.assign(to: &$canPost)
|
||||
}
|
||||
}
|
||||
|
@ -47,6 +47,12 @@ public extension NewStatusViewModel {
|
|||
case presentMediaPicker(CompositionViewModel)
|
||||
}
|
||||
|
||||
enum PostingState {
|
||||
case composing
|
||||
case posting
|
||||
case done
|
||||
}
|
||||
|
||||
func setIdentity(_ identity: Identity) {
|
||||
let identityService: IdentityService
|
||||
|
||||
|
@ -103,7 +109,7 @@ public extension NewStatusViewModel {
|
|||
|
||||
private extension NewStatusViewModel {
|
||||
func post(viewModel: CompositionViewModel, inReplyToId: Status.Id?) {
|
||||
loading = true
|
||||
postingState = .posting
|
||||
identification.service.post(statusComponents: viewModel.components(
|
||||
inReplyToId: inReplyToId,
|
||||
visibility: visibility))
|
||||
|
@ -113,10 +119,12 @@ private extension NewStatusViewModel {
|
|||
|
||||
switch $0 {
|
||||
case .finished:
|
||||
self.loading = self.compositionViewModels.allSatisfy(\.isPosted)
|
||||
if self.compositionViewModels.allSatisfy(\.isPosted) {
|
||||
self.postingState = .done
|
||||
}
|
||||
case let .failure(error):
|
||||
self.alertItem = AlertItem(error: error)
|
||||
self.loading = false
|
||||
self.postingState = .composing
|
||||
}
|
||||
} receiveValue: { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
|
|
@ -141,7 +141,7 @@ private extension CompositionView {
|
|||
viewModel.$attachmentViewModels
|
||||
.receive(on: DispatchQueue.main) // hack to punt to next run loop, consider refactoring
|
||||
.sink { [weak self] in
|
||||
self?.attachmentsDataSource.apply([$0.map(\.attachment)].snapshot())
|
||||
self?.attachmentsDataSource.apply($0.map(\.attachment).snapshot())
|
||||
self?.attachmentsCollectionView.isHidden = $0.isEmpty
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
|
Loading…
Reference in a new issue