This commit is contained in:
Justin Mazzocchi 2020-12-18 22:30:19 -08:00
parent 94cbbb2f04
commit e6e1816e11
No known key found for this signature in database
GPG key ID: E223E6937AAFB01C
14 changed files with 240 additions and 58 deletions

View file

@ -5,9 +5,28 @@ import UIKit
import ViewModels
final class CompositionAttachmentsDataSource: UICollectionViewDiffableDataSource<Int, Attachment> {
// init(collectionView: UICollectionView, composition: Composition) {
// super.init(collectionView: collectionView) { collectionView, indexPath, attachment in
//
// }
// }
private let updateQueue =
DispatchQueue(label: "com.metabolist.metatext.composition-attachments-data-source.update-queue")
init(collectionView: UICollectionView, viewModelProvider: @escaping (IndexPath) -> CompositionAttachmentViewModel) {
let registration = UICollectionView.CellRegistration
<CompositionAttachmentCollectionViewCell, CompositionAttachmentViewModel> {
$0.viewModel = $2
}
super.init(collectionView: collectionView) { collectionView, indexPath, _ in
collectionView.dequeueConfiguredReusableCell(
using: registration,
for: indexPath,
item: viewModelProvider(indexPath))
}
}
override func apply(_ snapshot: NSDiffableDataSourceSnapshot<Int, Attachment>,
animatingDifferences: Bool = true,
completion: (() -> Void)? = nil) {
updateQueue.async {
super.apply(snapshot, animatingDifferences: animatingDifferences, completion: completion)
}
}
}

View file

@ -15,9 +15,15 @@ public enum StatusEndpoint {
public extension StatusEndpoint {
struct Components {
public var text: String?
public let inReplyToId: Status.Id?
public let text: String
public let mediaIds: [Attachment.Id]
public init() {}
public init(inReplyToId: Status.Id?, text: String, mediaIds: [Attachment.Id]) {
self.inReplyToId = inReplyToId
self.text = text
self.mediaIds = mediaIds
}
}
}
@ -25,7 +31,15 @@ extension StatusEndpoint.Components {
var jsonBody: [String: Any]? {
var params = [String: Any]()
params["status"] = text
if !text.isEmpty {
params["status"] = text
}
if !mediaIds.isEmpty {
params["media_ids"] = mediaIds
}
params["in_reply_to_id"] = inReplyToId
return params
}

View file

@ -33,6 +33,10 @@
D065966725899E910096AC5D /* CompositionAttachmentsDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D065965A25899DAE0096AC5D /* CompositionAttachmentsDataSource.swift */; };
D06B492324D4611300642749 /* KingfisherSwiftUI in Frameworks */ = {isa = PBXBuildFile; productRef = D06B492224D4611300642749 /* KingfisherSwiftUI */; };
D06BC5E625202AD90079541D /* ProfileViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D06BC5E525202AD90079541D /* ProfileViewController.swift */; };
D0804133258D902900AD6139 /* CompositionAttachmentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0804132258D902900AD6139 /* CompositionAttachmentView.swift */; };
D0804134258D902900AD6139 /* CompositionAttachmentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0804132258D902900AD6139 /* CompositionAttachmentView.swift */; };
D080413E258D904400AD6139 /* CompositionAttachmentContentConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D080413D258D904400AD6139 /* CompositionAttachmentContentConfiguration.swift */; };
D080413F258D904400AD6139 /* CompositionAttachmentContentConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D080413D258D904400AD6139 /* CompositionAttachmentContentConfiguration.swift */; };
D08B8D3D253F929E00B1EBEF /* ImageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08B8D3C253F929E00B1EBEF /* ImageViewController.swift */; };
D08B8D42253F92B600B1EBEF /* ImagePageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08B8D41253F92B600B1EBEF /* ImagePageViewController.swift */; };
D08B8D4A253FC36500B1EBEF /* ImageNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08B8D49253FC36500B1EBEF /* ImageNavigationController.swift */; };
@ -191,6 +195,8 @@
D0666A2124C677B400F3F04B /* Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Tests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
D0666A2524C677B400F3F04B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
D06BC5E525202AD90079541D /* ProfileViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileViewController.swift; sourceTree = "<group>"; };
D0804132258D902900AD6139 /* CompositionAttachmentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompositionAttachmentView.swift; sourceTree = "<group>"; };
D080413D258D904400AD6139 /* CompositionAttachmentContentConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompositionAttachmentContentConfiguration.swift; sourceTree = "<group>"; };
D085C3BB25008DEC008A6C5E /* DB */ = {isa = PBXFileReference; lastKnownFileType = folder; path = DB; sourceTree = "<group>"; };
D08B8D3C253F929E00B1EBEF /* ImageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageViewController.swift; sourceTree = "<group>"; };
D08B8D41253F92B600B1EBEF /* ImagePageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePageViewController.swift; sourceTree = "<group>"; };
@ -444,6 +450,8 @@
D0C7D42424F76169001EBDBB /* AddIdentityView.swift */,
D0CE9F86258B076900E3A6B6 /* AttachmentUploadView.swift */,
D065966025899E890096AC5D /* CompositionAttachmentCollectionViewCell.swift */,
D080413D258D904400AD6139 /* CompositionAttachmentContentConfiguration.swift */,
D0804132258D902900AD6139 /* CompositionAttachmentView.swift */,
D08E52E2257D747400FA2C5F /* CompositionContentConfiguration.swift */,
D0E9F9A9258450B300EF503D /* CompositionInputAccessoryView.swift */,
D08E52DB257D742B00FA2C5F /* CompositionListCell.swift */,
@ -767,6 +775,7 @@
D0C7D49C24F7616A001EBDBB /* RootView.swift in Sources */,
D0F0B126251A90F400942152 /* AccountListCell.swift in Sources */,
D0B32F50250B373600311912 /* RegistrationView.swift in Sources */,
D0804133258D902900AD6139 /* CompositionAttachmentView.swift in Sources */,
D08B8D612540DE3B00B1EBEF /* ZoomDismissalInteractionController.swift in Sources */,
D036AA07254B6118009094DF /* NotificationView.swift in Sources */,
D08E52EE257D757100FA2C5F /* CompositionView.swift in Sources */,
@ -791,6 +800,7 @@
D01F41D924F880C400D55A2D /* TouchFallthroughTextView.swift in Sources */,
D0C7D4D624F7616A001EBDBB /* NSMutableAttributedString+Extensions.swift in Sources */,
D0E9F9AA258450B300EF503D /* CompositionInputAccessoryView.swift in Sources */,
D080413E258D904400AD6139 /* CompositionAttachmentContentConfiguration.swift in Sources */,
D0625E59250F092900502611 /* StatusListCell.swift in Sources */,
D0E569DB2529319100FA1D72 /* LoadMoreView.swift in Sources */,
D0C7D49D24F7616A001EBDBB /* PostingReadingPreferencesView.swift in Sources */,
@ -867,11 +877,13 @@
D0E7AD4225870C79005F5E2D /* UIVIewController+Extensions.swift in Sources */,
D08E52EF257D757100FA2C5F /* CompositionView.swift in Sources */,
D0CE9F88258B076900E3A6B6 /* AttachmentUploadView.swift in Sources */,
D080413F258D904400AD6139 /* CompositionAttachmentContentConfiguration.swift in Sources */,
D0F2D5452581ABAB00986197 /* KingfisherOptionsInfo+Extensions.swift in Sources */,
D08E52C7257C7AEE00FA2C5F /* ShareErrorViewController.swift in Sources */,
D08E52F8257D78BE00FA2C5F /* ViewConstants.swift in Sources */,
D0F2D4D6257EED6100986197 /* NewStatusDataSource.swift in Sources */,
D065966725899E910096AC5D /* CompositionAttachmentsDataSource.swift in Sources */,
D0804134258D902900AD6139 /* CompositionAttachmentView.swift in Sources */,
D08E52FD257D78CB00FA2C5F /* UIColor+Extensions.swift in Sources */,
D08E52E4257D747400FA2C5F /* CompositionContentConfiguration.swift in Sources */,
D065966225899E890096AC5D /* CompositionAttachmentCollectionViewCell.swift in Sources */,

View file

@ -0,0 +1,5 @@
// Copyright © 2020 Metabolist. All rights reserved.
import MastodonAPI
public typealias StatusComponents = StatusEndpoint.Components

View file

@ -212,30 +212,9 @@ public extension IdentityService {
progress: progress)
}
// func post(compositions: [Composition]) -> AnyPublisher<Never, Error> {
// fatalError()
// guard let composition = compositions.first else { fatalError() }
// guard let attachment = composition.attachments.first else { fatalError() }
// return mastodonAPIClient.request(AttachmentEndpoint.create(
// data: attachment.data,
// mimeType: attachment.mimeType,
// description: attachment.description,
// focus: attachment.focus))
// .print()
// .ignoreOutput()
// .eraseToAnyPublisher()
// var components = StatusEndpoint.Components()
//
// if !composition.text.isEmpty {
// components.text = composition.text
// }
//
// return mastodonAPIClient.request(StatusEndpoint.post(components))
// .ignoreOutput()
// .eraseToAnyPublisher()
// }
func post(statusComponents: StatusComponents) -> AnyPublisher<Status.Id, Error> {
mastodonAPIClient.request(StatusEndpoint.post(statusComponents)).map(\.id).eraseToAnyPublisher()
}
func service(timeline: Timeline) -> TimelineService {
TimelineService(timeline: timeline, mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase)

View file

@ -6,7 +6,7 @@ import Mastodon
import ServiceLayer
public final class CompositionAttachmentViewModel: ObservableObject {
public var attachment: Attachment
public let attachment: Attachment
init(attachment: Attachment) {
self.attachment = attachment

View file

@ -7,8 +7,9 @@ import ServiceLayer
public final class CompositionViewModel: ObservableObject {
public let id = Id()
public var isPosted = false
@Published public var text = ""
@Published public private(set) var attachments = [Attachment]()
@Published public private(set) var attachmentViewModels = [CompositionAttachmentViewModel]()
@Published public private(set) var isPostable = false
@Published public private(set) var identification: Identification
@Published public private(set) var attachmentUpload: AttachmentUpload?
@ -35,10 +36,11 @@ public extension CompositionViewModel {
case error(Error)
}
struct AttachmentUpload {
public let progress: Progress
public let data: Data
public let mimeType: String
func components(inReplyToId: Status.Id?) -> StatusComponents {
StatusComponents(
inReplyToId: inReplyToId,
text: text,
mediaIds: attachmentViewModels.map(\.attachment.id))
}
func presentMediaPicker() {
@ -63,17 +65,20 @@ public extension CompositionViewModel {
return self.identification.service.uploadAttachment(data: data, mimeType: mimeType, progress: progress)
}
.print()
.receive(on: DispatchQueue.main)
.sink { [weak self] in
DispatchQueue.main.async {
self?.attachmentUpload = nil
}
self?.attachmentUpload = nil
if case let .failure(error) = $0 {
self?.eventsSubject.send(.error(error))
}
} receiveValue: { [weak self] in
self?.attachments.append($0)
self?.attachmentViewModels.append(CompositionAttachmentViewModel(attachment: $0))
}
.store(in: &cancellables)
}
func attachmentViewModel(indexPath: IndexPath) -> CompositionAttachmentViewModel {
attachmentViewModels[indexPath.item]
}
}

View file

@ -0,0 +1,9 @@
// Copyright © 2020 Metabolist. All rights reserved.
import Foundation
public struct AttachmentUpload: Hashable {
public let progress: Progress
public let data: Data
public let mimeType: String
}

View file

@ -67,14 +67,9 @@ public extension NewStatusViewModel {
}
func post() {
// identification.service.post(compositions: compositionViewModels.map(\.composition))
// .receive(on: DispatchQueue.main)
// .handleEvents(
// receiveSubscription: { [weak self] _ in self?.loading = true },
// receiveCompletion: { [weak self] _ in self?.loading = false })
// .assignErrorsToAlertItem(to: \.alertItem, on: self)
// .sink { _ in }
// .store(in: &cancellables)
guard let unposted = compositionViewModels.first(where: { !$0.isPosted }) else { return }
post(viewModel: unposted, inReplyToId: nil)
}
}
@ -104,4 +99,30 @@ private extension NewStatusViewModel {
eventsSubject.send(event)
}
}
func post(viewModel: CompositionViewModel, inReplyToId: Status.Id?) {
loading = true
identification.service.post(statusComponents: viewModel.components(inReplyToId: inReplyToId))
.receive(on: DispatchQueue.main)
.sink { [weak self] in
guard let self = self else { return }
switch $0 {
case .finished:
self.loading = self.compositionViewModels.allSatisfy(\.isPosted)
case let .failure(error):
self.alertItem = AlertItem(error: error)
self.loading = false
}
} receiveValue: { [weak self] in
guard let self = self else { return }
viewModel.isPosted = true
if let unposted = self.compositionViewModels.first(where: { !$0.isPosted }) {
self.post(viewModel: unposted, inReplyToId: $0)
}
}
.store(in: &cancellables)
}
}

View file

@ -8,7 +8,7 @@ final class AttachmentUploadView: UIView {
let progressView = UIProgressView(progressViewStyle: .default)
private var progressCancellable: AnyCancellable?
var attachmentUpload: CompositionViewModel.AttachmentUpload? {
var attachmentUpload: AttachmentUpload? {
didSet {
if let attachmentUpload = attachmentUpload {
progressCancellable = attachmentUpload.progress.publisher(for: \.fractionCompleted)

View file

@ -1,7 +1,15 @@
// Copyright © 2020 Metabolist. All rights reserved.
import UIKit
import ViewModels
class CompositionAttachmentCollectionViewCell: UICollectionViewCell {
var viewModel: CompositionAttachmentViewModel?
override func updateConfiguration(using state: UICellConfigurationState) {
guard let viewModel = viewModel else { return }
contentConfiguration = CompositionAttachmentContentConfiguration(viewModel: viewModel).updated(for: state)
backgroundConfiguration = UIBackgroundConfiguration.clear().updated(for: state)
}
}

View file

@ -0,0 +1,18 @@
// Copyright © 2020 Metabolist. All rights reserved.
import UIKit
import ViewModels
struct CompositionAttachmentContentConfiguration {
let viewModel: CompositionAttachmentViewModel
}
extension CompositionAttachmentContentConfiguration: UIContentConfiguration {
func makeContentView() -> UIView & UIContentView {
CompositionAttachmentView(configuration: self)
}
func updated(for state: UIConfigurationState) -> CompositionAttachmentContentConfiguration {
self
}
}

View file

@ -0,0 +1,59 @@
// Copyright © 2020 Metabolist. All rights reserved.
import Kingfisher
import UIKit
import ViewModels
class CompositionAttachmentView: UIView {
let imageView = UIImageView()
private var compositionAttachmentConfiguration: CompositionAttachmentContentConfiguration
init(configuration: CompositionAttachmentContentConfiguration) {
self.compositionAttachmentConfiguration = configuration
super.init(frame: .zero)
initialSetup()
applyCompositionAttachmentConfiguration()
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
extension CompositionAttachmentView: UIContentView {
var configuration: UIContentConfiguration {
get { compositionAttachmentConfiguration }
set {
guard let compositionAttachmentConfiguration = newValue as? CompositionAttachmentContentConfiguration
else { return }
self.compositionAttachmentConfiguration = compositionAttachmentConfiguration
applyCompositionAttachmentConfiguration()
}
}
}
private extension CompositionAttachmentView {
func initialSetup() {
addSubview(imageView)
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.contentMode = .scaleAspectFill
imageView.layer.cornerRadius = .defaultCornerRadius
imageView.clipsToBounds = true
NSLayoutConstraint.activate([
imageView.leadingAnchor.constraint(equalTo: leadingAnchor),
imageView.topAnchor.constraint(equalTo: topAnchor),
imageView.trailingAnchor.constraint(equalTo: trailingAnchor),
imageView.bottomAnchor.constraint(equalTo: bottomAnchor)
])
}
func applyCompositionAttachmentConfiguration() {
imageView.kf.setImage(with: compositionAttachmentConfiguration.viewModel.attachment.previewUrl)
}
}

View file

@ -4,18 +4,41 @@ import Combine
import Kingfisher
import UIKit
class CompositionView: UIView {
final class CompositionView: UIView {
let avatarImageView = UIImageView()
let textView = UITextView()
let attachmentUploadView = AttachmentUploadView()
// let attachmentsCollectionView = UICollectionView()
let attachmentsCollectionView: UICollectionView
private var compositionConfiguration: CompositionContentConfiguration
private var cancellables = Set<AnyCancellable>()
private lazy var attachmentsDataSource: CompositionAttachmentsDataSource = {
CompositionAttachmentsDataSource(
collectionView: attachmentsCollectionView,
viewModelProvider: compositionConfiguration.viewModel.attachmentViewModel(indexPath:))
}()
init(configuration: CompositionContentConfiguration) {
self.compositionConfiguration = configuration
let itemSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(0.2),
heightDimension: .fractionalHeight(1.0))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let groupSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension: .fractionalWidth(0.2))
let group = NSCollectionLayoutGroup.horizontal(
layoutSize: groupSize,
subitems: [item])
group.interItemSpacing = .fixed(.defaultSpacing)
let section = NSCollectionLayoutSection(group: group)
let attachmentsLayout = UICollectionViewCompositionalLayout(section: section)
attachmentsCollectionView = UICollectionView(frame: .zero, collectionViewLayout: attachmentsLayout)
super.init(frame: .zero)
initialSetup()
@ -48,7 +71,7 @@ extension CompositionView: UITextViewDelegate {
}
private extension CompositionView {
static let attachmentsCollectionViewHeight: CGFloat = 100
static let attachmentUploadViewHeight: CGFloat = 100
func initialSetup() {
addSubview(avatarImageView)
@ -71,7 +94,8 @@ private extension CompositionView {
textView.inputAccessoryView?.sizeToFit()
textView.delegate = self
// stackView.addArrangedSubview(attachmentsCollectionView)
stackView.addArrangedSubview(attachmentsCollectionView)
attachmentsCollectionView.dataSource = attachmentsDataSource
stackView.addArrangedSubview(attachmentUploadView)
@ -85,8 +109,10 @@ private extension CompositionView {
stackView.topAnchor.constraint(equalTo: readableContentGuide.topAnchor),
stackView.trailingAnchor.constraint(equalTo: readableContentGuide.trailingAnchor),
stackView.bottomAnchor.constraint(equalTo: readableContentGuide.bottomAnchor),
// attachmentsCollectionView.heightAnchor.constraint(equalToConstant: Self.attachmentsCollectionViewHeight)
attachmentUploadView.heightAnchor.constraint(equalToConstant: Self.attachmentsCollectionViewHeight)
attachmentsCollectionView.heightAnchor.constraint(
equalTo: attachmentsCollectionView.widthAnchor,
multiplier: 1 / 4),
attachmentUploadView.heightAnchor.constraint(equalToConstant: Self.attachmentUploadViewHeight)
]
for constraint in constraints {
@ -99,6 +125,13 @@ private extension CompositionView {
.sink { [weak self] in self?.avatarImageView.kf.setImage(with: $0) }
.store(in: &cancellables)
compositionConfiguration.viewModel.$attachmentViewModels
.sink { [weak self] in
self?.attachmentsDataSource.apply([$0.map(\.attachment)].snapshot())
self?.attachmentsCollectionView.isHidden = $0.isEmpty
}
.store(in: &cancellables)
compositionConfiguration.viewModel.$attachmentUpload
.sink { [weak self] in self?.attachmentUploadView.attachmentUpload = $0 }
.store(in: &cancellables)