mirror of
https://github.com/metabolist/metatext.git
synced 2024-12-20 04:36:28 +00:00
Select multiple images for upload at once
This commit is contained in:
parent
d0709f718c
commit
1b95b493a1
7 changed files with 147 additions and 79 deletions
|
@ -17,7 +17,7 @@ final class NewStatusViewController: UIViewController {
|
||||||
private let postButton = UIBarButtonItem(title: nil, style: .done, target: nil, action: nil)
|
private let postButton = UIBarButtonItem(title: nil, style: .done, target: nil, action: nil)
|
||||||
private let mediaSelections = PassthroughSubject<[PHPickerResult], Never>()
|
private let mediaSelections = PassthroughSubject<[PHPickerResult], Never>()
|
||||||
private let imagePickerResults = PassthroughSubject<[UIImagePickerController.InfoKey: Any]?, Never>()
|
private let imagePickerResults = PassthroughSubject<[UIImagePickerController.InfoKey: Any]?, Never>()
|
||||||
private let documentPickerResuls = PassthroughSubject<[URL]?, Never>()
|
private let documentPickerResults = PassthroughSubject<[URL]?, Never>()
|
||||||
private var cancellables = Set<AnyCancellable>()
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
init(viewModel: NewStatusViewModel, rootViewModel: RootViewModel?) {
|
init(viewModel: NewStatusViewModel, rootViewModel: RootViewModel?) {
|
||||||
|
@ -127,11 +127,11 @@ extension NewStatusViewController: UIImagePickerControllerDelegate {
|
||||||
|
|
||||||
extension NewStatusViewController: UIDocumentPickerDelegate {
|
extension NewStatusViewController: UIDocumentPickerDelegate {
|
||||||
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
|
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
|
||||||
documentPickerResuls.send(urls)
|
documentPickerResults.send(urls)
|
||||||
}
|
}
|
||||||
|
|
||||||
func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) {
|
func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) {
|
||||||
documentPickerResuls.send(nil)
|
documentPickerResults.send(nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -282,19 +282,21 @@ private extension NewStatusViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
func presentMediaPicker(compositionViewModel: CompositionViewModel) {
|
func presentMediaPicker(compositionViewModel: CompositionViewModel) {
|
||||||
mediaSelections.first().sink { [weak self] results in
|
mediaSelections.first().sink { [weak self] in
|
||||||
guard let self = self, let result = results.first else { return }
|
self?.viewModel.attach(itemProviders: $0.map(\.itemProvider), to: compositionViewModel)
|
||||||
|
|
||||||
self.viewModel.attach(itemProvider: result.itemProvider, to: compositionViewModel)
|
|
||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
var configuration = PHPickerConfiguration()
|
var configuration = PHPickerConfiguration()
|
||||||
|
|
||||||
configuration.preferredAssetRepresentationMode = .current
|
configuration.preferredAssetRepresentationMode = .current
|
||||||
|
configuration.selectionLimit = CompositionViewModel.maxAttachmentCount
|
||||||
|
|
||||||
if !compositionViewModel.canAddNonImageAttachment {
|
if !compositionViewModel.canAddNonImageAttachment {
|
||||||
configuration.filter = .images
|
configuration.filter = .images
|
||||||
|
configuration.selectionLimit = CompositionViewModel.maxAttachmentCount
|
||||||
|
- compositionViewModel.attachmentViewModels.count
|
||||||
|
- compositionViewModel.attachmentUploadViewModels.count
|
||||||
}
|
}
|
||||||
|
|
||||||
let picker = PHPickerViewController(configuration: configuration)
|
let picker = PHPickerViewController(configuration: configuration)
|
||||||
|
@ -332,9 +334,9 @@ private extension NewStatusViewController {
|
||||||
guard let self = self, let info = $0 else { return }
|
guard let self = self, let info = $0 else { return }
|
||||||
|
|
||||||
if let url = info[.mediaURL] as? URL, let itemProvider = NSItemProvider(contentsOf: url) {
|
if let url = info[.mediaURL] as? URL, let itemProvider = NSItemProvider(contentsOf: url) {
|
||||||
self.viewModel.attach(itemProvider: itemProvider, to: compositionViewModel)
|
self.viewModel.attach(itemProviders: [itemProvider], to: compositionViewModel)
|
||||||
} else if let image = info[.editedImage] as? UIImage ?? info[.originalImage] as? UIImage {
|
} else if let image = info[.editedImage] as? UIImage ?? info[.originalImage] as? UIImage {
|
||||||
self.viewModel.attach(itemProvider: NSItemProvider(object: image), to: compositionViewModel)
|
self.viewModel.attach(itemProviders: [NSItemProvider(object: image)], to: compositionViewModel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
@ -356,22 +358,27 @@ private extension NewStatusViewController {
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
func presentDocumentPicker(compositionViewModel: CompositionViewModel) {
|
func presentDocumentPicker(compositionViewModel: CompositionViewModel) {
|
||||||
documentPickerResuls.first().sink { [weak self] in
|
documentPickerResults.first().sink { [weak self] in
|
||||||
guard let self = self,
|
guard let self = self, let results = $0 else { return }
|
||||||
let result = $0?.first,
|
|
||||||
result.startAccessingSecurityScopedResource(),
|
|
||||||
let itemProvider = NSItemProvider(contentsOf: result)
|
|
||||||
else { return }
|
|
||||||
|
|
||||||
self.viewModel.attach(itemProvider: itemProvider, to: compositionViewModel)
|
let itemProviders = results.compactMap { result -> NSItemProvider? in
|
||||||
|
guard result.startAccessingSecurityScopedResource() else { return nil }
|
||||||
|
|
||||||
|
return NSItemProvider(contentsOf: result)
|
||||||
|
}
|
||||||
|
|
||||||
|
self.viewModel.attach(itemProviders: itemProviders, to: compositionViewModel)
|
||||||
|
|
||||||
|
for result in results {
|
||||||
result.stopAccessingSecurityScopedResource()
|
result.stopAccessingSecurityScopedResource()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
let documentPickerController = UIDocumentPickerViewController(forOpeningContentTypes: [.image, .movie, .audio])
|
let documentPickerController = UIDocumentPickerViewController(forOpeningContentTypes: [.image, .movie, .audio])
|
||||||
|
|
||||||
documentPickerController.delegate = self
|
documentPickerController.delegate = self
|
||||||
documentPickerController.allowsMultipleSelection = false
|
documentPickerController.allowsMultipleSelection = true
|
||||||
documentPickerController.modalPresentationStyle = .overFullScreen
|
documentPickerController.modalPresentationStyle = .overFullScreen
|
||||||
present(documentPickerController, animated: true)
|
present(documentPickerController, animated: true)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,37 @@
|
||||||
// Copyright © 2020 Metabolist. All rights reserved.
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Combine
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import Mastodon
|
||||||
|
import ServiceLayer
|
||||||
|
|
||||||
public struct AttachmentUpload: Hashable {
|
public class AttachmentUploadViewModel: ObservableObject {
|
||||||
public let progress: Progress
|
public let id = Id()
|
||||||
public let data: Data
|
public let progress = Progress(totalUnitCount: 1)
|
||||||
public let mimeType: String
|
public let parentViewModel: NewStatusViewModel
|
||||||
|
|
||||||
|
let data: Data
|
||||||
|
let mimeType: String
|
||||||
|
var cancellable: AnyCancellable?
|
||||||
|
|
||||||
|
init(data: Data, mimeType: String, parentViewModel: NewStatusViewModel) {
|
||||||
|
self.data = data
|
||||||
|
self.mimeType = mimeType
|
||||||
|
self.parentViewModel = parentViewModel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public extension AttachmentUploadViewModel {
|
||||||
|
typealias Id = UUID
|
||||||
|
|
||||||
|
func upload() -> AnyPublisher<Attachment, Error> {
|
||||||
|
parentViewModel.identityContext.service.uploadAttachment(
|
||||||
|
data: data,
|
||||||
|
mimeType: mimeType,
|
||||||
|
progress: progress)
|
||||||
|
}
|
||||||
|
|
||||||
|
func cancel() {
|
||||||
|
cancellable?.cancel()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,7 +22,7 @@ public final class CompositionViewModel: AttachmentsRenderingViewModel, Observab
|
||||||
@Published public private(set) var contentWarningAutocompleteQuery: String?
|
@Published public private(set) var contentWarningAutocompleteQuery: String?
|
||||||
@Published public private(set) var pollOptions = [PollOption(text: ""), PollOption(text: "")]
|
@Published public private(set) var pollOptions = [PollOption(text: ""), PollOption(text: "")]
|
||||||
@Published public private(set) var attachmentViewModels = [AttachmentViewModel]()
|
@Published public private(set) var attachmentViewModels = [AttachmentViewModel]()
|
||||||
@Published public private(set) var attachmentUpload: AttachmentUpload?
|
@Published public private(set) var attachmentUploadViewModels = [AttachmentUploadViewModel]()
|
||||||
@Published public private(set) var isPostable = false
|
@Published public private(set) var isPostable = false
|
||||||
@Published public private(set) var canAddAttachment = true
|
@Published public private(set) var canAddAttachment = true
|
||||||
@Published public private(set) var canAddNonImageAttachment = true
|
@Published public private(set) var canAddNonImageAttachment = true
|
||||||
|
@ -30,7 +30,7 @@ public final class CompositionViewModel: AttachmentsRenderingViewModel, Observab
|
||||||
public let canRemoveAttachments = true
|
public let canRemoveAttachments = true
|
||||||
|
|
||||||
private let eventsSubject: PassthroughSubject<Event, Never>
|
private let eventsSubject: PassthroughSubject<Event, Never>
|
||||||
private var attachmentUploadCancellable: AnyCancellable?
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
init(eventsSubject: PassthroughSubject<Event, Never>) {
|
init(eventsSubject: PassthroughSubject<Event, Never>) {
|
||||||
self.eventsSubject = eventsSubject
|
self.eventsSubject = eventsSubject
|
||||||
|
@ -44,12 +44,17 @@ public final class CompositionViewModel: AttachmentsRenderingViewModel, Observab
|
||||||
.assign(to: &$isPostable)
|
.assign(to: &$isPostable)
|
||||||
|
|
||||||
$attachmentViewModels
|
$attachmentViewModels
|
||||||
.combineLatest($attachmentUpload, $displayPoll)
|
.combineLatest($attachmentUploadViewModels, $displayPoll)
|
||||||
.map { $0.count < Self.maxAttachmentCount && $1 == nil && !$2 }
|
.map { $0.count < Self.maxAttachmentCount && $1.isEmpty && !$2 }
|
||||||
.assign(to: &$canAddAttachment)
|
.assign(to: &$canAddAttachment)
|
||||||
|
|
||||||
$attachmentViewModels.map(\.isEmpty).assign(to: &$canAddNonImageAttachment)
|
$attachmentViewModels.map(\.isEmpty).assign(to: &$canAddNonImageAttachment)
|
||||||
|
|
||||||
|
$attachmentUploadViewModels
|
||||||
|
.compactMap(\.first)
|
||||||
|
.sink { [weak self] in self?.upload(viewModel: $0) }
|
||||||
|
.store(in: &cancellables)
|
||||||
|
|
||||||
$text.map {
|
$text.map {
|
||||||
let tokens = $0.components(separatedBy: " ")
|
let tokens = $0.components(separatedBy: " ")
|
||||||
|
|
||||||
|
@ -83,6 +88,7 @@ public final class CompositionViewModel: AttachmentsRenderingViewModel, Observab
|
||||||
|
|
||||||
public extension CompositionViewModel {
|
public extension CompositionViewModel {
|
||||||
static let maxCharacters = 500
|
static let maxCharacters = 500
|
||||||
|
static let maxAttachmentCount = 4
|
||||||
static let minPollOptionCount = 2
|
static let minPollOptionCount = 2
|
||||||
static let maxPollOptionCount = 4
|
static let maxPollOptionCount = 4
|
||||||
|
|
||||||
|
@ -172,7 +178,7 @@ public extension CompositionViewModel {
|
||||||
self.text = text
|
self.text = text
|
||||||
}
|
}
|
||||||
} else if let itemProvider = inputItem.attachments?.first {
|
} else if let itemProvider = inputItem.attachments?.first {
|
||||||
attach(itemProvider: itemProvider, parentViewModel: parentViewModel)
|
attach(itemProviders: [itemProvider], parentViewModel: parentViewModel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -197,39 +203,39 @@ public extension CompositionViewModel {
|
||||||
pollOptions.removeAll { $0 === pollOption }
|
pollOptions.removeAll { $0 === pollOption }
|
||||||
}
|
}
|
||||||
|
|
||||||
func attach(itemProvider: NSItemProvider, parentViewModel: NewStatusViewModel) {
|
func attach(itemProviders: [NSItemProvider], parentViewModel: NewStatusViewModel) {
|
||||||
attachmentUploadCancellable = MediaProcessingService.dataAndMimeType(itemProvider: itemProvider)
|
Publishers.MergeMany(itemProviders.map {
|
||||||
.flatMap { [weak self] data, mimeType -> AnyPublisher<Attachment, Error> in
|
MediaProcessingService.dataAndMimeType(itemProvider: $0)
|
||||||
guard let self = self else { return Empty().eraseToAnyPublisher() }
|
|
||||||
|
|
||||||
let progress = Progress(totalUnitCount: 1)
|
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.attachmentUpload = AttachmentUpload(progress: progress, data: data, mimeType: mimeType)
|
|
||||||
}
|
|
||||||
|
|
||||||
return parentViewModel.identityContext.service.uploadAttachment(
|
|
||||||
data: data,
|
|
||||||
mimeType: mimeType,
|
|
||||||
progress: progress)
|
|
||||||
}
|
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
.assignErrorsToAlertItem(to: \.alertItem, on: parentViewModel)
|
.assignErrorsToAlertItem(to: \.alertItem, on: parentViewModel)
|
||||||
.handleEvents(receiveCancel: { [weak self] in self?.attachmentUpload = nil })
|
.map { result in
|
||||||
|
AttachmentUploadViewModel(
|
||||||
|
data: result.data,
|
||||||
|
mimeType: result.mimeType,
|
||||||
|
parentViewModel: parentViewModel)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
.assign(to: &$attachmentUploadViewModels)
|
||||||
|
}
|
||||||
|
|
||||||
|
func upload(viewModel: AttachmentUploadViewModel) {
|
||||||
|
viewModel.cancellable = viewModel.upload()
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.assignErrorsToAlertItem(to: \.alertItem, on: viewModel.parentViewModel)
|
||||||
|
.handleEvents(receiveCancel: { [weak self] in
|
||||||
|
self?.attachmentUploadViewModels.removeAll { $0 === viewModel }
|
||||||
|
})
|
||||||
.sink { [weak self] _ in
|
.sink { [weak self] _ in
|
||||||
self?.attachmentUpload = nil
|
self?.attachmentUploadViewModels.removeAll { $0 === viewModel }
|
||||||
} receiveValue: { [weak self] in
|
} receiveValue: { [weak self] in
|
||||||
self?.attachmentViewModels.append(
|
self?.attachmentViewModels.append(
|
||||||
AttachmentViewModel(
|
AttachmentViewModel(
|
||||||
attachment: $0,
|
attachment: $0,
|
||||||
identityContext: parentViewModel.identityContext))
|
identityContext: viewModel.parentViewModel.identityContext))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func cancelUpload() {
|
|
||||||
attachmentUploadCancellable?.cancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
func update(attachmentViewModel: AttachmentViewModel) {
|
func update(attachmentViewModel: AttachmentViewModel) {
|
||||||
let publisher = attachmentViewModel.updated()
|
let publisher = attachmentViewModel.updated()
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
|
@ -259,7 +265,6 @@ public extension CompositionViewModel.PollOption {
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension CompositionViewModel {
|
private extension CompositionViewModel {
|
||||||
static let maxAttachmentCount = 4
|
|
||||||
static let autocompleteQueryRegularExpression = #"([@#:]\S+)\z"#
|
static let autocompleteQueryRegularExpression = #"([@#:]\S+)\z"#
|
||||||
static let emojiOnlyAutocompleteQueryRegularExpression = #"(:\S+)\z"#
|
static let emojiOnlyAutocompleteQueryRegularExpression = #"(:\S+)\z"#
|
||||||
|
|
||||||
|
|
|
@ -187,8 +187,8 @@ public extension NewStatusViewModel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func attach(itemProvider: NSItemProvider, to compositionViewModel: CompositionViewModel) {
|
func attach(itemProviders: [NSItemProvider], to compositionViewModel: CompositionViewModel) {
|
||||||
compositionViewModel.attach(itemProvider: itemProvider, parentViewModel: self)
|
compositionViewModel.attach(itemProviders: itemProviders, parentViewModel: self)
|
||||||
}
|
}
|
||||||
|
|
||||||
func post() {
|
func post() {
|
||||||
|
|
|
@ -9,12 +9,10 @@ final class AttachmentUploadView: UIView {
|
||||||
let cancelButton = UIButton(type: .system)
|
let cancelButton = UIButton(type: .system)
|
||||||
let progressView = UIProgressView(progressViewStyle: .default)
|
let progressView = UIProgressView(progressViewStyle: .default)
|
||||||
|
|
||||||
private let viewModel: CompositionViewModel
|
private let viewModel: AttachmentUploadViewModel
|
||||||
private var progressCancellable: AnyCancellable?
|
|
||||||
private var cancellables = Set<AnyCancellable>()
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
// swiftlint:disable:next function_body_length
|
init(viewModel: AttachmentUploadViewModel) {
|
||||||
init(viewModel: CompositionViewModel) {
|
|
||||||
self.viewModel = viewModel
|
self.viewModel = viewModel
|
||||||
|
|
||||||
super.init(frame: .zero)
|
super.init(frame: .zero)
|
||||||
|
@ -33,7 +31,7 @@ final class AttachmentUploadView: UIView {
|
||||||
cancelButton.titleLabel?.adjustsFontForContentSizeCategory = true
|
cancelButton.titleLabel?.adjustsFontForContentSizeCategory = true
|
||||||
cancelButton.titleLabel?.font = .preferredFont(forTextStyle: .callout)
|
cancelButton.titleLabel?.font = .preferredFont(forTextStyle: .callout)
|
||||||
cancelButton.setTitle(NSLocalizedString("cancel", comment: ""), for: .normal)
|
cancelButton.setTitle(NSLocalizedString("cancel", comment: ""), for: .normal)
|
||||||
cancelButton.addAction(UIAction { _ in viewModel.cancelUpload() }, for: .touchUpInside)
|
cancelButton.addAction(UIAction { _ in viewModel.cancel() }, for: .touchUpInside)
|
||||||
cancelButton.accessibilityLabel =
|
cancelButton.accessibilityLabel =
|
||||||
NSLocalizedString("compose.attachment.cancel-upload.accessibility-label", comment: "")
|
NSLocalizedString("compose.attachment.cancel-upload.accessibility-label", comment: "")
|
||||||
|
|
||||||
|
@ -53,20 +51,9 @@ final class AttachmentUploadView: UIView {
|
||||||
progressView.bottomAnchor.constraint(equalTo: layoutMarginsGuide.bottomAnchor)
|
progressView.bottomAnchor.constraint(equalTo: layoutMarginsGuide.bottomAnchor)
|
||||||
])
|
])
|
||||||
|
|
||||||
viewModel.$attachmentUpload.sink { [weak self] attachmentUpload in
|
viewModel.progress.publisher(for: \.fractionCompleted)
|
||||||
guard let self = self else { return }
|
|
||||||
|
|
||||||
UIView.animate(withDuration: .zeroIfReduceMotion(.shortAnimationDuration)) {
|
|
||||||
if let attachmentUpload = attachmentUpload {
|
|
||||||
self.progressCancellable = attachmentUpload.progress.publisher(for: \.fractionCompleted)
|
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
.sink { self.progressView.progress = Float($0) }
|
.sink { [weak self] in self?.progressView.progress = Float($0) }
|
||||||
self.isHidden = false
|
|
||||||
} else {
|
|
||||||
self.isHidden = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -75,3 +62,7 @@ final class AttachmentUploadView: UIView {
|
||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension AttachmentUploadView {
|
||||||
|
var id: AttachmentUploadViewModel.Id { viewModel.id }
|
||||||
|
}
|
||||||
|
|
|
@ -205,8 +205,8 @@ private extension CompositionInputAccessoryView {
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
viewModel.$attachmentViewModels
|
viewModel.$attachmentViewModels
|
||||||
.combineLatest(viewModel.$attachmentUpload)
|
.combineLatest(viewModel.$attachmentUploadViewModels)
|
||||||
.sink { pollButton.isEnabled = $0.isEmpty && $1 == nil }
|
.sink { pollButton.isEnabled = $0.isEmpty && $1.isEmpty }
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
viewModel.$remainingCharacters.sink {
|
viewModel.$remainingCharacters.sink {
|
||||||
|
|
|
@ -15,7 +15,7 @@ final class CompositionView: UIView {
|
||||||
let inReplyToView = UIView()
|
let inReplyToView = UIView()
|
||||||
let hasReplyFollowingView = UIView()
|
let hasReplyFollowingView = UIView()
|
||||||
let attachmentsView = AttachmentsView()
|
let attachmentsView = AttachmentsView()
|
||||||
let attachmentUploadView: AttachmentUploadView
|
let attachmentUploadsStackView = UIStackView()
|
||||||
let pollView: CompositionPollView
|
let pollView: CompositionPollView
|
||||||
let markAttachmentsSensitiveView: MarkAttachmentsSensitiveView
|
let markAttachmentsSensitiveView: MarkAttachmentsSensitiveView
|
||||||
|
|
||||||
|
@ -27,7 +27,6 @@ final class CompositionView: UIView {
|
||||||
self.viewModel = viewModel
|
self.viewModel = viewModel
|
||||||
self.parentViewModel = parentViewModel
|
self.parentViewModel = parentViewModel
|
||||||
|
|
||||||
attachmentUploadView = AttachmentUploadView(viewModel: viewModel)
|
|
||||||
markAttachmentsSensitiveView = MarkAttachmentsSensitiveView(viewModel: viewModel)
|
markAttachmentsSensitiveView = MarkAttachmentsSensitiveView(viewModel: viewModel)
|
||||||
pollView = CompositionPollView(viewModel: viewModel, parentViewModel: parentViewModel)
|
pollView = CompositionPollView(viewModel: viewModel, parentViewModel: parentViewModel)
|
||||||
|
|
||||||
|
@ -128,8 +127,9 @@ private extension CompositionView {
|
||||||
|
|
||||||
stackView.addArrangedSubview(attachmentsView)
|
stackView.addArrangedSubview(attachmentsView)
|
||||||
attachmentsView.isHidden_stackViewSafe = true
|
attachmentsView.isHidden_stackViewSafe = true
|
||||||
stackView.addArrangedSubview(attachmentUploadView)
|
stackView.addArrangedSubview(attachmentUploadsStackView)
|
||||||
attachmentUploadView.isHidden_stackViewSafe = true
|
attachmentUploadsStackView.axis = .vertical
|
||||||
|
attachmentUploadsStackView.isHidden_stackViewSafe = true
|
||||||
stackView.addArrangedSubview(markAttachmentsSensitiveView)
|
stackView.addArrangedSubview(markAttachmentsSensitiveView)
|
||||||
markAttachmentsSensitiveView.isHidden_stackViewSafe = true
|
markAttachmentsSensitiveView.isHidden_stackViewSafe = true
|
||||||
stackView.addArrangedSubview(pollView)
|
stackView.addArrangedSubview(pollView)
|
||||||
|
@ -227,10 +227,22 @@ private extension CompositionView {
|
||||||
.sink { [weak self] in self?.textView.canPasteImage = $0 }
|
.sink { [weak self] in self?.textView.canPasteImage = $0 }
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
|
viewModel.$attachmentUploadViewModels
|
||||||
|
.throttle(for: .seconds(TimeInterval.zeroIfReduceMotion(.shortAnimationDuration)),
|
||||||
|
scheduler: DispatchQueue.main,
|
||||||
|
latest: true)
|
||||||
|
.sink { [weak self] attachmentUploadViewModels in
|
||||||
|
UIView.animate(withDuration: .zeroIfReduceMotion(.shortAnimationDuration)) {
|
||||||
|
self?.attachmentUploadsStackView.isHidden_stackViewSafe = attachmentUploadViewModels.isEmpty
|
||||||
|
self?.update(attachmentUploadViewModels: attachmentUploadViewModels)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
|
|
||||||
textView.pastedItemProviders.sink { [weak self] in
|
textView.pastedItemProviders.sink { [weak self] in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
|
|
||||||
self.viewModel.attach(itemProvider: $0,
|
self.viewModel.attach(itemProviders: [$0],
|
||||||
parentViewModel: self.parentViewModel)
|
parentViewModel: self.parentViewModel)
|
||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
@ -366,4 +378,29 @@ private extension CompositionView {
|
||||||
range: textToSelectedRangeRange)
|
range: textToSelectedRangeRange)
|
||||||
spoilerTextFieldEditingChanged()
|
spoilerTextFieldEditingChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func update(attachmentUploadViewModels: [AttachmentUploadViewModel]) {
|
||||||
|
let diff = attachmentUploadViewModels.map(\.id)
|
||||||
|
.difference(from: attachmentUploadsStackView
|
||||||
|
.arrangedSubviews
|
||||||
|
.compactMap { ($0 as? AttachmentUploadView)?.id })
|
||||||
|
|
||||||
|
for insertion in diff.insertions {
|
||||||
|
guard case let .insert(index, id, _) = insertion,
|
||||||
|
let attachmentUploadViewModel = attachmentUploadViewModels.first(where: { $0.id == id })
|
||||||
|
else { continue }
|
||||||
|
|
||||||
|
let attachmentUploadView = AttachmentUploadView(viewModel: attachmentUploadViewModel)
|
||||||
|
|
||||||
|
attachmentUploadsStackView.insertArrangedSubview(attachmentUploadView, at: index)
|
||||||
|
}
|
||||||
|
|
||||||
|
for removal in diff.removals {
|
||||||
|
guard case let .remove(_, id, _) = removal,
|
||||||
|
let index = attachmentUploadsStackView.arrangedSubviews.firstIndex(where: { ($0 as? AttachmentUploadView)?.id == id })
|
||||||
|
else { continue }
|
||||||
|
|
||||||
|
attachmentUploadsStackView.arrangedSubviews[index].removeFromSuperview()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue