wip
This commit is contained in:
parent
d7c73ee06d
commit
0c5a3de66b
|
@ -19,20 +19,6 @@ public class Composition {
|
|||
|
||||
public extension Composition {
|
||||
typealias Id = UUID
|
||||
|
||||
struct Attachment {
|
||||
public let data: Data
|
||||
public let type: Mastodon.Attachment.AttachmentType
|
||||
public let mimeType: String
|
||||
public var description: String?
|
||||
public var focus: Mastodon.Attachment.Meta.Focus?
|
||||
|
||||
public init(data: Data, type: Mastodon.Attachment.AttachmentType, mimeType: String) {
|
||||
self.data = data
|
||||
self.type = type
|
||||
self.mimeType = mimeType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Composition {
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
// Copyright © 2020 Metabolist. All rights reserved.
|
||||
|
||||
import Mastodon
|
||||
import UIKit
|
||||
import ViewModels
|
||||
|
||||
final class CompositionAttachmentsDataSource: UICollectionViewDiffableDataSource<Int, Attachment> {
|
||||
// init(collectionView: UICollectionView, composition: Composition) {
|
||||
// super.init(collectionView: collectionView) { collectionView, indexPath, attachment in
|
||||
//
|
||||
// }
|
||||
// }
|
||||
}
|
|
@ -19,14 +19,14 @@ open class HTTPClient {
|
|||
}
|
||||
|
||||
open func dataTaskPublisher<T: DecodableTarget>(
|
||||
_ target: T) -> AnyPublisher<(data: Data, response: HTTPURLResponse), Error> {
|
||||
_ target: T, progress: Progress? = nil) -> AnyPublisher<(data: Data, response: HTTPURLResponse), Error> {
|
||||
if let protocolClasses = session.configuration.protocolClasses {
|
||||
for protocolClass in protocolClasses {
|
||||
(protocolClass as? TargetProcessing.Type)?.process(target: target)
|
||||
}
|
||||
}
|
||||
|
||||
return session.dataTaskPublisher(for: target.urlRequest())
|
||||
return session.dataTaskPublisher(for: target.urlRequest(), progress: progress)
|
||||
.tryMap { data, response in
|
||||
guard let httpResponse = response as? HTTPURLResponse else {
|
||||
throw HTTPError.nonHTTPURLResponse(data: data, response: response)
|
||||
|
@ -41,8 +41,8 @@ open class HTTPClient {
|
|||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
open func request<T: DecodableTarget>(_ target: T) -> AnyPublisher<T.ResultType, Error> {
|
||||
dataTaskPublisher(target)
|
||||
open func request<T: DecodableTarget>(_ target: T, progress: Progress? = nil) -> AnyPublisher<T.ResultType, Error> {
|
||||
dataTaskPublisher(target, progress: progress)
|
||||
.map(\.data)
|
||||
.decode(type: T.ResultType.self, decoder: decoder)
|
||||
.eraseToAnyPublisher()
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
// Copyright © 2020 Metabolist. All rights reserved.
|
||||
|
||||
import Combine
|
||||
import Foundation
|
||||
|
||||
extension URLSession {
|
||||
func dataTaskPublisher(for request: URLRequest, progress: Progress?)
|
||||
-> AnyPublisher<DataTaskPublisher.Output, Error> {
|
||||
if let progress = progress {
|
||||
return Deferred {
|
||||
Future<DataTaskPublisher.Output, Error> { promise in
|
||||
let dataTask = self.dataTask(with: request) { data, response, error in
|
||||
if let error = error {
|
||||
promise(.failure(error))
|
||||
} else if let data = data, let response = response {
|
||||
promise(.success((data, response)))
|
||||
}
|
||||
}
|
||||
|
||||
progress.addChild(dataTask.progress, withPendingUnitCount: 1)
|
||||
dataTask.resume()
|
||||
}
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
} else {
|
||||
return dataTaskPublisher(for: request).mapError { $0 as Error }.eraseToAnyPublisher()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -15,8 +15,8 @@ public final class MastodonAPIClient: HTTPClient {
|
|||
}
|
||||
|
||||
public override func dataTaskPublisher<T: DecodableTarget>(
|
||||
_ target: T) -> AnyPublisher<(data: Data, response: HTTPURLResponse), Error> {
|
||||
super.dataTaskPublisher(target)
|
||||
_ target: T, progress: Progress? = nil) -> AnyPublisher<(data: Data, response: HTTPURLResponse), Error> {
|
||||
super.dataTaskPublisher(target, progress: progress)
|
||||
.mapError { [weak self] error -> Error in
|
||||
if case let HTTPError.invalidStatusCode(data, _) = error,
|
||||
let apiError = try? self?.decoder.decode(APIError.self, from: data) {
|
||||
|
@ -30,8 +30,8 @@ public final class MastodonAPIClient: HTTPClient {
|
|||
}
|
||||
|
||||
extension MastodonAPIClient {
|
||||
public func request<E: Endpoint>(_ endpoint: E) -> AnyPublisher<E.ResultType, Error> {
|
||||
dataTaskPublisher(target(endpoint: endpoint))
|
||||
public func request<E: Endpoint>(_ endpoint: E, progress: Progress? = nil) -> AnyPublisher<E.ResultType, Error> {
|
||||
dataTaskPublisher(target(endpoint: endpoint), progress: progress)
|
||||
.map(\.data)
|
||||
.decode(type: E.ResultType.self, decoder: decoder)
|
||||
.eraseToAnyPublisher()
|
||||
|
@ -42,9 +42,10 @@ extension MastodonAPIClient {
|
|||
maxId: String? = nil,
|
||||
minId: String? = nil,
|
||||
sinceId: String? = nil,
|
||||
limit: Int? = nil) -> AnyPublisher<PagedResult<E.ResultType>, Error> {
|
||||
limit: Int? = nil,
|
||||
progress: Progress? = nil) -> AnyPublisher<PagedResult<E.ResultType>, Error> {
|
||||
let pagedTarget = target(endpoint: Paged(endpoint, maxId: maxId, minId: minId, sinceId: sinceId, limit: limit))
|
||||
let dataTask = dataTaskPublisher(pagedTarget).share()
|
||||
let dataTask = dataTaskPublisher(pagedTarget, progress: progress).share()
|
||||
let decoded = dataTask.map(\.data).decode(type: E.ResultType.self, decoder: decoder)
|
||||
let info = dataTask.map { _, response -> PagedResult<E.ResultType>.Info in
|
||||
var maxId: String?
|
||||
|
|
|
@ -27,6 +27,10 @@
|
|||
D04226FD2546AC0B000980A3 /* StartupAndSyncingPreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04226FC2546AC0B000980A3 /* StartupAndSyncingPreferencesView.swift */; };
|
||||
D0625E59250F092900502611 /* StatusListCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0625E58250F092900502611 /* StatusListCell.swift */; };
|
||||
D0625E5D250F0B5C00502611 /* StatusContentConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0625E5C250F0B5C00502611 /* StatusContentConfiguration.swift */; };
|
||||
D065965B25899DAE0096AC5D /* CompositionAttachmentsDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D065965A25899DAE0096AC5D /* CompositionAttachmentsDataSource.swift */; };
|
||||
D065966125899E890096AC5D /* CompositionAttachmentCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D065966025899E890096AC5D /* CompositionAttachmentCollectionViewCell.swift */; };
|
||||
D065966225899E890096AC5D /* CompositionAttachmentCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D065966025899E890096AC5D /* CompositionAttachmentCollectionViewCell.swift */; };
|
||||
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 */; };
|
||||
D08B8D3D253F929E00B1EBEF /* ImageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08B8D3C253F929E00B1EBEF /* ImageViewController.swift */; };
|
||||
|
@ -91,6 +95,8 @@
|
|||
D0C7D4D724F7616A001EBDBB /* UIColor+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C7D46C24F76169001EBDBB /* UIColor+Extensions.swift */; };
|
||||
D0C7D4D924F7616A001EBDBB /* KingfisherOptionsInfo+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C7D46E24F76169001EBDBB /* KingfisherOptionsInfo+Extensions.swift */; };
|
||||
D0C7D4DA24F7616A001EBDBB /* View+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C7D46F24F76169001EBDBB /* View+Extensions.swift */; };
|
||||
D0CE9F87258B076900E3A6B6 /* AttachmentUploadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0CE9F86258B076900E3A6B6 /* AttachmentUploadView.swift */; };
|
||||
D0CE9F88258B076900E3A6B6 /* AttachmentUploadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0CE9F86258B076900E3A6B6 /* AttachmentUploadView.swift */; };
|
||||
D0DD50CB256B1F24004A04F7 /* ReportView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DD50CA256B1F24004A04F7 /* ReportView.swift */; };
|
||||
D0E1F583251F13EC00D45315 /* WebfingerIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E1F582251F13EC00D45315 /* WebfingerIndicatorView.swift */; };
|
||||
D0E2C1D124FD97F000854680 /* ViewModels in Frameworks */ = {isa = PBXBuildFile; productRef = D0E2C1D024FD97F000854680 /* ViewModels */; };
|
||||
|
@ -180,6 +186,8 @@
|
|||
D047FA8C24C3E21200AF17C5 /* Metatext.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Metatext.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
D0625E58250F092900502611 /* StatusListCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusListCell.swift; sourceTree = "<group>"; };
|
||||
D0625E5C250F0B5C00502611 /* StatusContentConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusContentConfiguration.swift; sourceTree = "<group>"; };
|
||||
D065965A25899DAE0096AC5D /* CompositionAttachmentsDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompositionAttachmentsDataSource.swift; sourceTree = "<group>"; };
|
||||
D065966025899E890096AC5D /* CompositionAttachmentCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompositionAttachmentCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||
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>"; };
|
||||
|
@ -245,6 +253,7 @@
|
|||
D0C7D46C24F76169001EBDBB /* UIColor+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIColor+Extensions.swift"; sourceTree = "<group>"; };
|
||||
D0C7D46E24F76169001EBDBB /* KingfisherOptionsInfo+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "KingfisherOptionsInfo+Extensions.swift"; sourceTree = "<group>"; };
|
||||
D0C7D46F24F76169001EBDBB /* View+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "View+Extensions.swift"; sourceTree = "<group>"; };
|
||||
D0CE9F86258B076900E3A6B6 /* AttachmentUploadView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentUploadView.swift; sourceTree = "<group>"; };
|
||||
D0D7C013250440610039AD6F /* CodableBloomFilter */ = {isa = PBXFileReference; lastKnownFileType = folder; path = CodableBloomFilter; sourceTree = "<group>"; };
|
||||
D0DD50CA256B1F24004A04F7 /* ReportView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportView.swift; sourceTree = "<group>"; };
|
||||
D0E0F1E424FC49FC002C04BF /* Mastodon */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Mastodon; sourceTree = "<group>"; };
|
||||
|
@ -410,6 +419,7 @@
|
|||
children = (
|
||||
D0A1F4F6252E7D4B004435BF /* TableViewDataSource.swift */,
|
||||
D0F2D4D0257EE84400986197 /* NewStatusDataSource.swift */,
|
||||
D065965A25899DAE0096AC5D /* CompositionAttachmentsDataSource.swift */,
|
||||
);
|
||||
path = "Data Sources";
|
||||
sourceTree = "<group>";
|
||||
|
@ -432,6 +442,8 @@
|
|||
D0F0B125251A90F400942152 /* AccountListCell.swift */,
|
||||
D0F0B10D251A868200942152 /* AccountView.swift */,
|
||||
D0C7D42424F76169001EBDBB /* AddIdentityView.swift */,
|
||||
D0CE9F86258B076900E3A6B6 /* AttachmentUploadView.swift */,
|
||||
D065966025899E890096AC5D /* CompositionAttachmentCollectionViewCell.swift */,
|
||||
D08E52E2257D747400FA2C5F /* CompositionContentConfiguration.swift */,
|
||||
D0E9F9A9258450B300EF503D /* CompositionInputAccessoryView.swift */,
|
||||
D08E52DB257D742B00FA2C5F /* CompositionListCell.swift */,
|
||||
|
@ -772,6 +784,7 @@
|
|||
D0F0B12E251A97E400942152 /* TableViewController.swift in Sources */,
|
||||
D0DD50CB256B1F24004A04F7 /* ReportView.swift in Sources */,
|
||||
D0FE1C8F253686F9003EF1EB /* PlayerView.swift in Sources */,
|
||||
D0CE9F87258B076900E3A6B6 /* AttachmentUploadView.swift in Sources */,
|
||||
D0F0B113251A86A000942152 /* AccountContentConfiguration.swift in Sources */,
|
||||
D036AA02254B6101009094DF /* NotificationListCell.swift in Sources */,
|
||||
D08B8D42253F92B600B1EBEF /* ImagePageViewController.swift in Sources */,
|
||||
|
@ -806,6 +819,7 @@
|
|||
D036AA17254CA824009094DF /* StatusBodyView.swift in Sources */,
|
||||
D08E512125786A6600FA2C5F /* UIButton+Extensions.swift in Sources */,
|
||||
D0EA59482522B8B600804347 /* ViewConstants.swift in Sources */,
|
||||
D065965B25899DAE0096AC5D /* CompositionAttachmentsDataSource.swift in Sources */,
|
||||
D04226FD2546AC0B000980A3 /* StartupAndSyncingPreferencesView.swift in Sources */,
|
||||
D036AA0C254B612B009094DF /* NotificationContentConfiguration.swift in Sources */,
|
||||
D0C7D49824F7616A001EBDBB /* CustomEmojiText.swift in Sources */,
|
||||
|
@ -814,6 +828,7 @@
|
|||
D08E52612579D2E100FA2C5F /* DomainBlocksView.swift in Sources */,
|
||||
D01F41E424F8889700D55A2D /* StatusAttachmentsView.swift in Sources */,
|
||||
D00702312555F4AE00F38136 /* ConversationView.swift in Sources */,
|
||||
D065966125899E890096AC5D /* CompositionAttachmentCollectionViewCell.swift in Sources */,
|
||||
D0BEB21124FA2A91001B0F04 /* EditFilterView.swift in Sources */,
|
||||
D08B8D4A253FC36500B1EBEF /* ImageNavigationController.swift in Sources */,
|
||||
D0070252255921B100F38136 /* AccountFieldView.swift in Sources */,
|
||||
|
@ -851,12 +866,15 @@
|
|||
D08E52DD257D742B00FA2C5F /* CompositionListCell.swift in Sources */,
|
||||
D0E7AD4225870C79005F5E2D /* UIVIewController+Extensions.swift in Sources */,
|
||||
D08E52EF257D757100FA2C5F /* CompositionView.swift in Sources */,
|
||||
D0CE9F88258B076900E3A6B6 /* AttachmentUploadView.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 */,
|
||||
D08E52FD257D78CB00FA2C5F /* UIColor+Extensions.swift in Sources */,
|
||||
D08E52E4257D747400FA2C5F /* CompositionContentConfiguration.swift in Sources */,
|
||||
D065966225899E890096AC5D /* CompositionAttachmentCollectionViewCell.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
|
|
@ -206,17 +206,26 @@ public extension IdentityService {
|
|||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
func uploadAttachment(data: Data, mimeType: String, progress: Progress) -> AnyPublisher<Attachment, Error> {
|
||||
mastodonAPIClient.request(
|
||||
AttachmentEndpoint.create(data: data, mimeType: mimeType, description: nil, focus: nil),
|
||||
progress: progress)
|
||||
}
|
||||
|
||||
func post(compositions: [Composition]) -> AnyPublisher<Never, Error> {
|
||||
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()
|
||||
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 {
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
import Combine
|
||||
import Foundation
|
||||
import ImageIO
|
||||
import Mastodon
|
||||
import UniformTypeIdentifiers
|
||||
|
||||
enum MediaProcessingError: Error {
|
||||
|
@ -18,32 +17,18 @@ enum MediaProcessingError: Error {
|
|||
public struct MediaProcessingService {}
|
||||
|
||||
public extension MediaProcessingService {
|
||||
static func attachment(itemProvider: NSItemProvider) -> AnyPublisher<Composition.Attachment, Error> {
|
||||
static func dataAndMimeType(itemProvider: NSItemProvider) -> AnyPublisher<(data: Data, mimeType: String), Error> {
|
||||
let registeredTypes = itemProvider.registeredTypeIdentifiers.compactMap(UTType.init)
|
||||
|
||||
guard let uniformType = registeredTypes.first(where: {
|
||||
guard let mimeType = $0.preferredMIMEType else { return false }
|
||||
|
||||
return !Self.unuploadableMimeTypes.contains(mimeType)
|
||||
return Self.uploadableMimeTypes.contains(mimeType)
|
||||
}),
|
||||
let mimeType = uniformType.preferredMIMEType else {
|
||||
return Fail(error: MediaProcessingError.invalidMimeType).eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
let type: Attachment.AttachmentType
|
||||
|
||||
if uniformType.conforms(to: .image) {
|
||||
type = .image
|
||||
} else if uniformType.conforms(to: .movie) {
|
||||
type = .video
|
||||
} else if uniformType.conforms(to: .audio) {
|
||||
type = .audio
|
||||
} else if uniformType.conforms(to: .video), uniformType == .mpeg4Movie {
|
||||
type = .gifv
|
||||
} else {
|
||||
type = .unknown
|
||||
}
|
||||
|
||||
return Future<Data, Error> { promise in
|
||||
itemProvider.loadFileRepresentation(forTypeIdentifier: uniformType.identifier) { url, error in
|
||||
if let error = error {
|
||||
|
@ -63,13 +48,23 @@ public extension MediaProcessingService {
|
|||
}
|
||||
}
|
||||
}
|
||||
.map { Composition.Attachment(data: $0, type: type, mimeType: mimeType) }
|
||||
.map { (data: $0, mimeType: mimeType) }
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
}
|
||||
|
||||
private extension MediaProcessingService {
|
||||
static let unuploadableMimeTypes: Set<String> = [UTType.heic.preferredMIMEType!]
|
||||
|
||||
static let uploadableMimeTypes = Set(
|
||||
[UTType.png,
|
||||
UTType.jpeg,
|
||||
UTType.gif,
|
||||
UTType.webP,
|
||||
UTType.mpeg4Movie,
|
||||
UTType.quickTimeMovie,
|
||||
UTType.mp3,
|
||||
UTType.wav]
|
||||
.compactMap(\.preferredMIMEType))
|
||||
static let imageSourceOptions = [kCGImageSourceShouldCache: false] as CFDictionary
|
||||
static let thumbnailOptions = [
|
||||
kCGImageSourceCreateThumbnailFromImageAlways: true,
|
||||
|
|
|
@ -73,9 +73,14 @@ class NewStatusViewController: UICollectionViewController {
|
|||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
// Invalidate the collection view layout on anything that could change the height of a cell
|
||||
viewModel.$compositionViewModels
|
||||
.flatMap { Publishers.MergeMany($0.map(\.composition.$text)) }
|
||||
.sink { [weak self] _ in self?.collectionView.collectionViewLayout.invalidateLayout() }
|
||||
.map { _ in () }
|
||||
.merge(with: viewModel.$compositionViewModels
|
||||
.flatMap { Publishers.MergeMany($0.map(\.objectWillChange)) }
|
||||
.map { _ in () })
|
||||
.sink { [weak self] in self?.collectionView.collectionViewLayout.invalidateLayout() }
|
||||
.store(in: &cancellables)
|
||||
|
||||
viewModel.$canPost.sink { [weak self] in self?.postButton.isEnabled = $0 }.store(in: &cancellables)
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
// Copyright © 2020 Metabolist. All rights reserved.
|
||||
|
||||
import Combine
|
||||
import Foundation
|
||||
import Mastodon
|
||||
import ServiceLayer
|
||||
|
||||
public final class CompositionAttachmentViewModel: ObservableObject {
|
||||
public var attachment: Attachment
|
||||
|
||||
init(attachment: Attachment) {
|
||||
self.attachment = attachment
|
||||
}
|
||||
}
|
|
@ -9,8 +9,10 @@ public final class CompositionViewModel: ObservableObject {
|
|||
public let composition: Composition
|
||||
@Published public private(set) var isPostable = false
|
||||
@Published public private(set) var identification: Identification
|
||||
@Published public private(set) var attachmentUpload: AttachmentUpload?
|
||||
|
||||
private let eventsSubject: PassthroughSubject<Event, Never>
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
init(composition: Composition,
|
||||
identification: Identification,
|
||||
|
@ -28,7 +30,13 @@ public extension CompositionViewModel {
|
|||
enum Event {
|
||||
case insertAfter(CompositionViewModel)
|
||||
case presentMediaPicker(CompositionViewModel)
|
||||
case attach(itemProvider: NSItemProvider, viewModel: CompositionViewModel)
|
||||
case error(Error)
|
||||
}
|
||||
|
||||
struct AttachmentUpload {
|
||||
public let progress: Progress
|
||||
public let data: Data
|
||||
public let mimeType: String
|
||||
}
|
||||
|
||||
func presentMediaPicker() {
|
||||
|
@ -40,6 +48,30 @@ public extension CompositionViewModel {
|
|||
}
|
||||
|
||||
func attach(itemProvider: NSItemProvider) {
|
||||
eventsSubject.send(.attach(itemProvider: itemProvider, viewModel: self))
|
||||
let progress = Progress(totalUnitCount: 1)
|
||||
|
||||
MediaProcessingService.dataAndMimeType(itemProvider: itemProvider)
|
||||
.flatMap { [weak self] data, mimeType -> AnyPublisher<Attachment, Error> in
|
||||
guard let self = self else { return Empty().eraseToAnyPublisher() }
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.attachmentUpload = AttachmentUpload(progress: progress, data: data, mimeType: mimeType)
|
||||
}
|
||||
|
||||
return self.identification.service.uploadAttachment(data: data, mimeType: mimeType, progress: progress)
|
||||
}
|
||||
.print()
|
||||
.sink { [weak self] in
|
||||
DispatchQueue.main.async {
|
||||
self?.attachmentUpload = nil
|
||||
}
|
||||
|
||||
if case let .failure(error) = $0 {
|
||||
self?.eventsSubject.send(.error(error))
|
||||
}
|
||||
} receiveValue: { [weak self] in
|
||||
self?.composition.attachments.append($0)
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -99,11 +99,8 @@ private extension NewStatusViewModel {
|
|||
} else {
|
||||
compositionViewModels.insert(newViewModel, at: index + 1)
|
||||
}
|
||||
case let .attach(itemProvider, viewModel):
|
||||
MediaProcessingService.attachment(itemProvider: itemProvider)
|
||||
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
||||
.sink { viewModel.composition.attachments.append($0) }
|
||||
.store(in: &cancellables)
|
||||
case let .error(error):
|
||||
alertItem = AlertItem(error: error)
|
||||
default:
|
||||
eventsSubject.send(event)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
// Copyright © 2020 Metabolist. All rights reserved.
|
||||
|
||||
import Combine
|
||||
import UIKit
|
||||
import ViewModels
|
||||
|
||||
final class AttachmentUploadView: UIView {
|
||||
let progressView = UIProgressView(progressViewStyle: .default)
|
||||
private var progressCancellable: AnyCancellable?
|
||||
|
||||
var attachmentUpload: CompositionViewModel.AttachmentUpload? {
|
||||
didSet {
|
||||
if let attachmentUpload = attachmentUpload {
|
||||
progressCancellable = attachmentUpload.progress.publisher(for: \.fractionCompleted)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] in self?.progressView.progress = Float($0) }
|
||||
isHidden = false
|
||||
} else {
|
||||
isHidden = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init() {
|
||||
super.init(frame: .zero)
|
||||
|
||||
addSubview(progressView)
|
||||
progressView.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
progressView.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor),
|
||||
progressView.trailingAnchor.constraint(equalTo: layoutMarginsGuide.trailingAnchor),
|
||||
progressView.centerYAnchor.constraint(equalTo: centerYAnchor)
|
||||
])
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
// Copyright © 2020 Metabolist. All rights reserved.
|
||||
|
||||
import UIKit
|
||||
|
||||
class CompositionAttachmentCollectionViewCell: UICollectionViewCell {
|
||||
|
||||
}
|
|
@ -7,6 +7,8 @@ import UIKit
|
|||
class CompositionView: UIView {
|
||||
let avatarImageView = UIImageView()
|
||||
let textView = UITextView()
|
||||
let attachmentUploadView = AttachmentUploadView()
|
||||
// let attachmentsCollectionView = UICollectionView()
|
||||
|
||||
private var compositionConfiguration: CompositionContentConfiguration
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
@ -46,6 +48,8 @@ extension CompositionView: UITextViewDelegate {
|
|||
}
|
||||
|
||||
private extension CompositionView {
|
||||
static let attachmentsCollectionViewHeight: CGFloat = 100
|
||||
|
||||
func initialSetup() {
|
||||
addSubview(avatarImageView)
|
||||
avatarImageView.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
@ -67,6 +71,10 @@ private extension CompositionView {
|
|||
textView.inputAccessoryView?.sizeToFit()
|
||||
textView.delegate = self
|
||||
|
||||
// stackView.addArrangedSubview(attachmentsCollectionView)
|
||||
|
||||
stackView.addArrangedSubview(attachmentUploadView)
|
||||
|
||||
let constraints = [
|
||||
avatarImageView.heightAnchor.constraint(equalToConstant: .avatarDimension),
|
||||
avatarImageView.widthAnchor.constraint(equalToConstant: .avatarDimension),
|
||||
|
@ -76,7 +84,9 @@ private extension CompositionView {
|
|||
stackView.leadingAnchor.constraint(equalTo: avatarImageView.trailingAnchor, constant: .defaultSpacing),
|
||||
stackView.topAnchor.constraint(equalTo: readableContentGuide.topAnchor),
|
||||
stackView.trailingAnchor.constraint(equalTo: readableContentGuide.trailingAnchor),
|
||||
stackView.bottomAnchor.constraint(equalTo: readableContentGuide.bottomAnchor)
|
||||
stackView.bottomAnchor.constraint(equalTo: readableContentGuide.bottomAnchor),
|
||||
// attachmentsCollectionView.heightAnchor.constraint(equalToConstant: Self.attachmentsCollectionViewHeight)
|
||||
attachmentUploadView.heightAnchor.constraint(equalToConstant: Self.attachmentsCollectionViewHeight)
|
||||
]
|
||||
|
||||
for constraint in constraints {
|
||||
|
@ -88,6 +98,10 @@ private extension CompositionView {
|
|||
compositionConfiguration.viewModel.$identification.map(\.identity.image)
|
||||
.sink { [weak self] in self?.avatarImageView.kf.setImage(with: $0) }
|
||||
.store(in: &cancellables)
|
||||
|
||||
compositionConfiguration.viewModel.$attachmentUpload
|
||||
.sink { [weak self] in self?.attachmentUploadView.attachmentUpload = $0 }
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
func applyCompositionConfiguration() {
|
||||
|
|
Loading…
Reference in New Issue