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
|
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 viewModel: NewStatusViewModel
|
||||||
private let scrollView = UIScrollView()
|
private let scrollView = UIScrollView()
|
||||||
private let stackView = UIStackView()
|
private let stackView = UIStackView()
|
||||||
|
private let activityIndicatorView = UIActivityIndicatorView(style: .large)
|
||||||
private let postButton = UIBarButtonItem(
|
private let postButton = UIBarButtonItem(
|
||||||
title: NSLocalizedString("post", comment: ""),
|
title: NSLocalizedString("post", comment: ""),
|
||||||
style: .done,
|
style: .done,
|
||||||
|
@ -42,6 +43,10 @@ final class NewStatusViewController: UIViewController {
|
||||||
stackView.axis = .vertical
|
stackView.axis = .vertical
|
||||||
stackView.distribution = .equalSpacing
|
stackView.distribution = .equalSpacing
|
||||||
|
|
||||||
|
scrollView.addSubview(activityIndicatorView)
|
||||||
|
activityIndicatorView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
activityIndicatorView.hidesWhenStopped = true
|
||||||
|
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||||
scrollView.topAnchor.constraint(equalTo: view.topAnchor),
|
scrollView.topAnchor.constraint(equalTo: view.topAnchor),
|
||||||
|
@ -51,7 +56,9 @@ final class NewStatusViewController: UIViewController {
|
||||||
stackView.topAnchor.constraint(equalTo: scrollView.topAnchor),
|
stackView.topAnchor.constraint(equalTo: scrollView.topAnchor),
|
||||||
stackView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),
|
stackView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),
|
||||||
stackView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
|
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
|
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() {
|
func dismiss() {
|
||||||
if let extensionContext = extensionContext {
|
if let extensionContext = extensionContext {
|
||||||
extensionContext.completeRequest(returningItems: nil)
|
extensionContext.completeRequest(returningItems: nil)
|
||||||
|
@ -92,53 +144,23 @@ private extension NewStatusViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
func setupViewModelBindings() {
|
func setupViewModelBindings() {
|
||||||
viewModel.events.sink { [weak self] in self?.handle(event: $0) }.store(in: &cancellables)
|
viewModel.events
|
||||||
|
.sink { [weak self] in self?.handle(event: $0) }
|
||||||
viewModel.$canPost.sink { [weak self] in self?.postButton.isEnabled = $0 }.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
viewModel.$canPost
|
||||||
viewModel.$compositionViewModels.sink { [weak self] in
|
.sink { [weak self] in self?.postButton.isEnabled = $0 }
|
||||||
guard let self = self else { return }
|
.store(in: &cancellables)
|
||||||
|
viewModel.$compositionViewModels
|
||||||
let diff = [$0.map(\.id)].snapshot().itemIdentifiers.difference(
|
.sink { [weak self] in self?.set(compositionViewModels: $0) }
|
||||||
from: [self.stackView.arrangedSubviews.compactMap { ($0 as? CompositionView)?.id }]
|
.store(in: &cancellables)
|
||||||
.snapshot().itemIdentifiers)
|
viewModel.$identification
|
||||||
|
.sink { [weak self] in self?.setupBarButtonItems(identification: $0) }
|
||||||
for insertion in diff.insertions {
|
.store(in: &cancellables)
|
||||||
guard case let .insert(index, id, _) = insertion,
|
viewModel.$postingState
|
||||||
let compositionViewModel = $0.first(where: { $0.id == id })
|
.sink { [weak self] in self?.apply(postingState: $0) }
|
||||||
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)
|
|
||||||
}
|
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
viewModel.$alertItem
|
viewModel.$alertItem
|
||||||
.compactMap { $0 }
|
.compactMap { $0 }
|
||||||
.receive(on: DispatchQueue.main)
|
|
||||||
.sink { [weak self] in self?.present(alertItem: $0) }
|
.sink { [weak self] in self?.present(alertItem: $0) }
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,7 @@ public final class NewStatusViewModel: ObservableObject {
|
||||||
@Published public var canPost = false
|
@Published public var canPost = false
|
||||||
@Published public var canChangeIdentity = true
|
@Published public var canChangeIdentity = true
|
||||||
@Published public var alertItem: AlertItem?
|
@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>
|
public let events: AnyPublisher<Event, Never>
|
||||||
|
|
||||||
private let allIdentitiesService: AllIdentitiesService
|
private let allIdentitiesService: AllIdentitiesService
|
||||||
|
@ -36,8 +36,8 @@ public final class NewStatusViewModel: ObservableObject {
|
||||||
$compositionViewModels.flatMap { Publishers.MergeMany($0.map(\.$isPostable)) }
|
$compositionViewModels.flatMap { Publishers.MergeMany($0.map(\.$isPostable)) }
|
||||||
.receive(on: DispatchQueue.main) // hack to punt to next run loop, consider refactoring
|
.receive(on: DispatchQueue.main) // hack to punt to next run loop, consider refactoring
|
||||||
.compactMap { [weak self] _ in self?.compositionViewModels.allSatisfy(\.isPostable) }
|
.compactMap { [weak self] _ in self?.compositionViewModels.allSatisfy(\.isPostable) }
|
||||||
.combineLatest($loading)
|
.combineLatest($postingState)
|
||||||
.map { $0 && !$1 }
|
.map { $0 && $1 == .composing }
|
||||||
.assign(to: &$canPost)
|
.assign(to: &$canPost)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -47,6 +47,12 @@ public extension NewStatusViewModel {
|
||||||
case presentMediaPicker(CompositionViewModel)
|
case presentMediaPicker(CompositionViewModel)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum PostingState {
|
||||||
|
case composing
|
||||||
|
case posting
|
||||||
|
case done
|
||||||
|
}
|
||||||
|
|
||||||
func setIdentity(_ identity: Identity) {
|
func setIdentity(_ identity: Identity) {
|
||||||
let identityService: IdentityService
|
let identityService: IdentityService
|
||||||
|
|
||||||
|
@ -103,7 +109,7 @@ public extension NewStatusViewModel {
|
||||||
|
|
||||||
private extension NewStatusViewModel {
|
private extension NewStatusViewModel {
|
||||||
func post(viewModel: CompositionViewModel, inReplyToId: Status.Id?) {
|
func post(viewModel: CompositionViewModel, inReplyToId: Status.Id?) {
|
||||||
loading = true
|
postingState = .posting
|
||||||
identification.service.post(statusComponents: viewModel.components(
|
identification.service.post(statusComponents: viewModel.components(
|
||||||
inReplyToId: inReplyToId,
|
inReplyToId: inReplyToId,
|
||||||
visibility: visibility))
|
visibility: visibility))
|
||||||
|
@ -113,10 +119,12 @@ private extension NewStatusViewModel {
|
||||||
|
|
||||||
switch $0 {
|
switch $0 {
|
||||||
case .finished:
|
case .finished:
|
||||||
self.loading = self.compositionViewModels.allSatisfy(\.isPosted)
|
if self.compositionViewModels.allSatisfy(\.isPosted) {
|
||||||
|
self.postingState = .done
|
||||||
|
}
|
||||||
case let .failure(error):
|
case let .failure(error):
|
||||||
self.alertItem = AlertItem(error: error)
|
self.alertItem = AlertItem(error: error)
|
||||||
self.loading = false
|
self.postingState = .composing
|
||||||
}
|
}
|
||||||
} receiveValue: { [weak self] in
|
} receiveValue: { [weak self] in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
|
|
|
@ -141,7 +141,7 @@ private extension CompositionView {
|
||||||
viewModel.$attachmentViewModels
|
viewModel.$attachmentViewModels
|
||||||
.receive(on: DispatchQueue.main) // hack to punt to next run loop, consider refactoring
|
.receive(on: DispatchQueue.main) // hack to punt to next run loop, consider refactoring
|
||||||
.sink { [weak self] in
|
.sink { [weak self] in
|
||||||
self?.attachmentsDataSource.apply([$0.map(\.attachment)].snapshot())
|
self?.attachmentsDataSource.apply($0.map(\.attachment).snapshot())
|
||||||
self?.attachmentsCollectionView.isHidden = $0.isEmpty
|
self?.attachmentsCollectionView.isHidden = $0.isEmpty
|
||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
Loading…
Reference in a new issue