This commit is contained in:
Justin Mazzocchi 2020-12-16 22:48:06 -08:00
parent d7c73ee06d
commit 0c5a3de66b
No known key found for this signature in database
GPG key ID: E223E6937AAFB01C
15 changed files with 223 additions and 62 deletions

View file

@ -19,20 +19,6 @@ public class Composition {
public extension Composition { public extension Composition {
typealias Id = UUID 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 { extension Composition {

View file

@ -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
//
// }
// }
}

View file

@ -19,14 +19,14 @@ open class HTTPClient {
} }
open func dataTaskPublisher<T: DecodableTarget>( 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 { if let protocolClasses = session.configuration.protocolClasses {
for protocolClass in protocolClasses { for protocolClass in protocolClasses {
(protocolClass as? TargetProcessing.Type)?.process(target: target) (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 .tryMap { data, response in
guard let httpResponse = response as? HTTPURLResponse else { guard let httpResponse = response as? HTTPURLResponse else {
throw HTTPError.nonHTTPURLResponse(data: data, response: response) throw HTTPError.nonHTTPURLResponse(data: data, response: response)
@ -41,8 +41,8 @@ open class HTTPClient {
.eraseToAnyPublisher() .eraseToAnyPublisher()
} }
open func request<T: DecodableTarget>(_ target: T) -> AnyPublisher<T.ResultType, Error> { open func request<T: DecodableTarget>(_ target: T, progress: Progress? = nil) -> AnyPublisher<T.ResultType, Error> {
dataTaskPublisher(target) dataTaskPublisher(target, progress: progress)
.map(\.data) .map(\.data)
.decode(type: T.ResultType.self, decoder: decoder) .decode(type: T.ResultType.self, decoder: decoder)
.eraseToAnyPublisher() .eraseToAnyPublisher()

View file

@ -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()
}
}
}

View file

@ -15,8 +15,8 @@ public final class MastodonAPIClient: HTTPClient {
} }
public override func dataTaskPublisher<T: DecodableTarget>( public override func dataTaskPublisher<T: DecodableTarget>(
_ target: T) -> AnyPublisher<(data: Data, response: HTTPURLResponse), Error> { _ target: T, progress: Progress? = nil) -> AnyPublisher<(data: Data, response: HTTPURLResponse), Error> {
super.dataTaskPublisher(target) super.dataTaskPublisher(target, progress: progress)
.mapError { [weak self] error -> Error in .mapError { [weak self] error -> Error in
if case let HTTPError.invalidStatusCode(data, _) = error, if case let HTTPError.invalidStatusCode(data, _) = error,
let apiError = try? self?.decoder.decode(APIError.self, from: data) { let apiError = try? self?.decoder.decode(APIError.self, from: data) {
@ -30,8 +30,8 @@ public final class MastodonAPIClient: HTTPClient {
} }
extension MastodonAPIClient { extension MastodonAPIClient {
public func request<E: Endpoint>(_ endpoint: E) -> AnyPublisher<E.ResultType, Error> { public func request<E: Endpoint>(_ endpoint: E, progress: Progress? = nil) -> AnyPublisher<E.ResultType, Error> {
dataTaskPublisher(target(endpoint: endpoint)) dataTaskPublisher(target(endpoint: endpoint), progress: progress)
.map(\.data) .map(\.data)
.decode(type: E.ResultType.self, decoder: decoder) .decode(type: E.ResultType.self, decoder: decoder)
.eraseToAnyPublisher() .eraseToAnyPublisher()
@ -42,9 +42,10 @@ extension MastodonAPIClient {
maxId: String? = nil, maxId: String? = nil,
minId: String? = nil, minId: String? = nil,
sinceId: 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 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 decoded = dataTask.map(\.data).decode(type: E.ResultType.self, decoder: decoder)
let info = dataTask.map { _, response -> PagedResult<E.ResultType>.Info in let info = dataTask.map { _, response -> PagedResult<E.ResultType>.Info in
var maxId: String? var maxId: String?

View file

@ -27,6 +27,10 @@
D04226FD2546AC0B000980A3 /* StartupAndSyncingPreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04226FC2546AC0B000980A3 /* StartupAndSyncingPreferencesView.swift */; }; D04226FD2546AC0B000980A3 /* StartupAndSyncingPreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04226FC2546AC0B000980A3 /* StartupAndSyncingPreferencesView.swift */; };
D0625E59250F092900502611 /* StatusListCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0625E58250F092900502611 /* StatusListCell.swift */; }; D0625E59250F092900502611 /* StatusListCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0625E58250F092900502611 /* StatusListCell.swift */; };
D0625E5D250F0B5C00502611 /* StatusContentConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0625E5C250F0B5C00502611 /* StatusContentConfiguration.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 */; }; D06B492324D4611300642749 /* KingfisherSwiftUI in Frameworks */ = {isa = PBXBuildFile; productRef = D06B492224D4611300642749 /* KingfisherSwiftUI */; };
D06BC5E625202AD90079541D /* ProfileViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D06BC5E525202AD90079541D /* ProfileViewController.swift */; }; D06BC5E625202AD90079541D /* ProfileViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D06BC5E525202AD90079541D /* ProfileViewController.swift */; };
D08B8D3D253F929E00B1EBEF /* ImageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08B8D3C253F929E00B1EBEF /* ImageViewController.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 */; }; 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 */; }; 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 */; }; 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 */; }; D0DD50CB256B1F24004A04F7 /* ReportView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DD50CA256B1F24004A04F7 /* ReportView.swift */; };
D0E1F583251F13EC00D45315 /* WebfingerIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E1F582251F13EC00D45315 /* WebfingerIndicatorView.swift */; }; D0E1F583251F13EC00D45315 /* WebfingerIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E1F582251F13EC00D45315 /* WebfingerIndicatorView.swift */; };
D0E2C1D124FD97F000854680 /* ViewModels in Frameworks */ = {isa = PBXBuildFile; productRef = D0E2C1D024FD97F000854680 /* ViewModels */; }; 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; }; 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>"; }; 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>"; }; 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; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; D0D7C013250440610039AD6F /* CodableBloomFilter */ = {isa = PBXFileReference; lastKnownFileType = folder; path = CodableBloomFilter; sourceTree = "<group>"; };
D0DD50CA256B1F24004A04F7 /* ReportView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportView.swift; 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>"; }; D0E0F1E424FC49FC002C04BF /* Mastodon */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Mastodon; sourceTree = "<group>"; };
@ -410,6 +419,7 @@
children = ( children = (
D0A1F4F6252E7D4B004435BF /* TableViewDataSource.swift */, D0A1F4F6252E7D4B004435BF /* TableViewDataSource.swift */,
D0F2D4D0257EE84400986197 /* NewStatusDataSource.swift */, D0F2D4D0257EE84400986197 /* NewStatusDataSource.swift */,
D065965A25899DAE0096AC5D /* CompositionAttachmentsDataSource.swift */,
); );
path = "Data Sources"; path = "Data Sources";
sourceTree = "<group>"; sourceTree = "<group>";
@ -432,6 +442,8 @@
D0F0B125251A90F400942152 /* AccountListCell.swift */, D0F0B125251A90F400942152 /* AccountListCell.swift */,
D0F0B10D251A868200942152 /* AccountView.swift */, D0F0B10D251A868200942152 /* AccountView.swift */,
D0C7D42424F76169001EBDBB /* AddIdentityView.swift */, D0C7D42424F76169001EBDBB /* AddIdentityView.swift */,
D0CE9F86258B076900E3A6B6 /* AttachmentUploadView.swift */,
D065966025899E890096AC5D /* CompositionAttachmentCollectionViewCell.swift */,
D08E52E2257D747400FA2C5F /* CompositionContentConfiguration.swift */, D08E52E2257D747400FA2C5F /* CompositionContentConfiguration.swift */,
D0E9F9A9258450B300EF503D /* CompositionInputAccessoryView.swift */, D0E9F9A9258450B300EF503D /* CompositionInputAccessoryView.swift */,
D08E52DB257D742B00FA2C5F /* CompositionListCell.swift */, D08E52DB257D742B00FA2C5F /* CompositionListCell.swift */,
@ -772,6 +784,7 @@
D0F0B12E251A97E400942152 /* TableViewController.swift in Sources */, D0F0B12E251A97E400942152 /* TableViewController.swift in Sources */,
D0DD50CB256B1F24004A04F7 /* ReportView.swift in Sources */, D0DD50CB256B1F24004A04F7 /* ReportView.swift in Sources */,
D0FE1C8F253686F9003EF1EB /* PlayerView.swift in Sources */, D0FE1C8F253686F9003EF1EB /* PlayerView.swift in Sources */,
D0CE9F87258B076900E3A6B6 /* AttachmentUploadView.swift in Sources */,
D0F0B113251A86A000942152 /* AccountContentConfiguration.swift in Sources */, D0F0B113251A86A000942152 /* AccountContentConfiguration.swift in Sources */,
D036AA02254B6101009094DF /* NotificationListCell.swift in Sources */, D036AA02254B6101009094DF /* NotificationListCell.swift in Sources */,
D08B8D42253F92B600B1EBEF /* ImagePageViewController.swift in Sources */, D08B8D42253F92B600B1EBEF /* ImagePageViewController.swift in Sources */,
@ -806,6 +819,7 @@
D036AA17254CA824009094DF /* StatusBodyView.swift in Sources */, D036AA17254CA824009094DF /* StatusBodyView.swift in Sources */,
D08E512125786A6600FA2C5F /* UIButton+Extensions.swift in Sources */, D08E512125786A6600FA2C5F /* UIButton+Extensions.swift in Sources */,
D0EA59482522B8B600804347 /* ViewConstants.swift in Sources */, D0EA59482522B8B600804347 /* ViewConstants.swift in Sources */,
D065965B25899DAE0096AC5D /* CompositionAttachmentsDataSource.swift in Sources */,
D04226FD2546AC0B000980A3 /* StartupAndSyncingPreferencesView.swift in Sources */, D04226FD2546AC0B000980A3 /* StartupAndSyncingPreferencesView.swift in Sources */,
D036AA0C254B612B009094DF /* NotificationContentConfiguration.swift in Sources */, D036AA0C254B612B009094DF /* NotificationContentConfiguration.swift in Sources */,
D0C7D49824F7616A001EBDBB /* CustomEmojiText.swift in Sources */, D0C7D49824F7616A001EBDBB /* CustomEmojiText.swift in Sources */,
@ -814,6 +828,7 @@
D08E52612579D2E100FA2C5F /* DomainBlocksView.swift in Sources */, D08E52612579D2E100FA2C5F /* DomainBlocksView.swift in Sources */,
D01F41E424F8889700D55A2D /* StatusAttachmentsView.swift in Sources */, D01F41E424F8889700D55A2D /* StatusAttachmentsView.swift in Sources */,
D00702312555F4AE00F38136 /* ConversationView.swift in Sources */, D00702312555F4AE00F38136 /* ConversationView.swift in Sources */,
D065966125899E890096AC5D /* CompositionAttachmentCollectionViewCell.swift in Sources */,
D0BEB21124FA2A91001B0F04 /* EditFilterView.swift in Sources */, D0BEB21124FA2A91001B0F04 /* EditFilterView.swift in Sources */,
D08B8D4A253FC36500B1EBEF /* ImageNavigationController.swift in Sources */, D08B8D4A253FC36500B1EBEF /* ImageNavigationController.swift in Sources */,
D0070252255921B100F38136 /* AccountFieldView.swift in Sources */, D0070252255921B100F38136 /* AccountFieldView.swift in Sources */,
@ -851,12 +866,15 @@
D08E52DD257D742B00FA2C5F /* CompositionListCell.swift in Sources */, D08E52DD257D742B00FA2C5F /* CompositionListCell.swift in Sources */,
D0E7AD4225870C79005F5E2D /* UIVIewController+Extensions.swift in Sources */, D0E7AD4225870C79005F5E2D /* UIVIewController+Extensions.swift in Sources */,
D08E52EF257D757100FA2C5F /* CompositionView.swift in Sources */, D08E52EF257D757100FA2C5F /* CompositionView.swift in Sources */,
D0CE9F88258B076900E3A6B6 /* AttachmentUploadView.swift in Sources */,
D0F2D5452581ABAB00986197 /* KingfisherOptionsInfo+Extensions.swift in Sources */, D0F2D5452581ABAB00986197 /* KingfisherOptionsInfo+Extensions.swift in Sources */,
D08E52C7257C7AEE00FA2C5F /* ShareErrorViewController.swift in Sources */, D08E52C7257C7AEE00FA2C5F /* ShareErrorViewController.swift in Sources */,
D08E52F8257D78BE00FA2C5F /* ViewConstants.swift in Sources */, D08E52F8257D78BE00FA2C5F /* ViewConstants.swift in Sources */,
D0F2D4D6257EED6100986197 /* NewStatusDataSource.swift in Sources */, D0F2D4D6257EED6100986197 /* NewStatusDataSource.swift in Sources */,
D065966725899E910096AC5D /* CompositionAttachmentsDataSource.swift in Sources */,
D08E52FD257D78CB00FA2C5F /* UIColor+Extensions.swift in Sources */, D08E52FD257D78CB00FA2C5F /* UIColor+Extensions.swift in Sources */,
D08E52E4257D747400FA2C5F /* CompositionContentConfiguration.swift in Sources */, D08E52E4257D747400FA2C5F /* CompositionContentConfiguration.swift in Sources */,
D065966225899E890096AC5D /* CompositionAttachmentCollectionViewCell.swift in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };

View file

@ -206,17 +206,26 @@ public extension IdentityService {
.eraseToAnyPublisher() .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> { func post(compositions: [Composition]) -> AnyPublisher<Never, Error> {
guard let composition = compositions.first else { fatalError() } fatalError()
guard let attachment = composition.attachments.first else { fatalError() } // guard let composition = compositions.first else { fatalError() }
return mastodonAPIClient.request(AttachmentEndpoint.create(
data: attachment.data, // guard let attachment = composition.attachments.first else { fatalError() }
mimeType: attachment.mimeType, // return mastodonAPIClient.request(AttachmentEndpoint.create(
description: attachment.description, // data: attachment.data,
focus: attachment.focus)) // mimeType: attachment.mimeType,
.print() // description: attachment.description,
.ignoreOutput() // focus: attachment.focus))
.eraseToAnyPublisher() // .print()
// .ignoreOutput()
// .eraseToAnyPublisher()
// var components = StatusEndpoint.Components() // var components = StatusEndpoint.Components()
// //
// if !composition.text.isEmpty { // if !composition.text.isEmpty {

View file

@ -3,7 +3,6 @@
import Combine import Combine
import Foundation import Foundation
import ImageIO import ImageIO
import Mastodon
import UniformTypeIdentifiers import UniformTypeIdentifiers
enum MediaProcessingError: Error { enum MediaProcessingError: Error {
@ -18,32 +17,18 @@ enum MediaProcessingError: Error {
public struct MediaProcessingService {} public struct MediaProcessingService {}
public extension 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) let registeredTypes = itemProvider.registeredTypeIdentifiers.compactMap(UTType.init)
guard let uniformType = registeredTypes.first(where: { guard let uniformType = registeredTypes.first(where: {
guard let mimeType = $0.preferredMIMEType else { return false } guard let mimeType = $0.preferredMIMEType else { return false }
return !Self.unuploadableMimeTypes.contains(mimeType) return Self.uploadableMimeTypes.contains(mimeType)
}), }),
let mimeType = uniformType.preferredMIMEType else { let mimeType = uniformType.preferredMIMEType else {
return Fail(error: MediaProcessingError.invalidMimeType).eraseToAnyPublisher() 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 return Future<Data, Error> { promise in
itemProvider.loadFileRepresentation(forTypeIdentifier: uniformType.identifier) { url, error in itemProvider.loadFileRepresentation(forTypeIdentifier: uniformType.identifier) { url, error in
if let error = error { 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() .eraseToAnyPublisher()
} }
} }
private extension MediaProcessingService { 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 imageSourceOptions = [kCGImageSourceShouldCache: false] as CFDictionary
static let thumbnailOptions = [ static let thumbnailOptions = [
kCGImageSourceCreateThumbnailFromImageAlways: true, kCGImageSourceCreateThumbnailFromImageAlways: true,

View file

@ -73,9 +73,14 @@ class NewStatusViewController: UICollectionViewController {
} }
.store(in: &cancellables) .store(in: &cancellables)
// Invalidate the collection view layout on anything that could change the height of a cell
viewModel.$compositionViewModels viewModel.$compositionViewModels
.flatMap { Publishers.MergeMany($0.map(\.composition.$text)) } .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) .store(in: &cancellables)
viewModel.$canPost.sink { [weak self] in self?.postButton.isEnabled = $0 }.store(in: &cancellables) viewModel.$canPost.sink { [weak self] in self?.postButton.isEnabled = $0 }.store(in: &cancellables)

View file

@ -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
}
}

View file

@ -9,8 +9,10 @@ public final class CompositionViewModel: ObservableObject {
public let composition: Composition public let composition: Composition
@Published public private(set) var isPostable = false @Published public private(set) var isPostable = false
@Published public private(set) var identification: Identification @Published public private(set) var identification: Identification
@Published public private(set) var attachmentUpload: AttachmentUpload?
private let eventsSubject: PassthroughSubject<Event, Never> private let eventsSubject: PassthroughSubject<Event, Never>
private var cancellables = Set<AnyCancellable>()
init(composition: Composition, init(composition: Composition,
identification: Identification, identification: Identification,
@ -28,7 +30,13 @@ public extension CompositionViewModel {
enum Event { enum Event {
case insertAfter(CompositionViewModel) case insertAfter(CompositionViewModel)
case presentMediaPicker(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() { func presentMediaPicker() {
@ -40,6 +48,30 @@ public extension CompositionViewModel {
} }
func attach(itemProvider: NSItemProvider) { 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)
} }
} }

View file

@ -99,11 +99,8 @@ private extension NewStatusViewModel {
} else { } else {
compositionViewModels.insert(newViewModel, at: index + 1) compositionViewModels.insert(newViewModel, at: index + 1)
} }
case let .attach(itemProvider, viewModel): case let .error(error):
MediaProcessingService.attachment(itemProvider: itemProvider) alertItem = AlertItem(error: error)
.assignErrorsToAlertItem(to: \.alertItem, on: self)
.sink { viewModel.composition.attachments.append($0) }
.store(in: &cancellables)
default: default:
eventsSubject.send(event) eventsSubject.send(event)
} }

View file

@ -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")
}
}

View file

@ -0,0 +1,7 @@
// Copyright © 2020 Metabolist. All rights reserved.
import UIKit
class CompositionAttachmentCollectionViewCell: UICollectionViewCell {
}

View file

@ -7,6 +7,8 @@ import UIKit
class CompositionView: UIView { class CompositionView: UIView {
let avatarImageView = UIImageView() let avatarImageView = UIImageView()
let textView = UITextView() let textView = UITextView()
let attachmentUploadView = AttachmentUploadView()
// let attachmentsCollectionView = UICollectionView()
private var compositionConfiguration: CompositionContentConfiguration private var compositionConfiguration: CompositionContentConfiguration
private var cancellables = Set<AnyCancellable>() private var cancellables = Set<AnyCancellable>()
@ -46,6 +48,8 @@ extension CompositionView: UITextViewDelegate {
} }
private extension CompositionView { private extension CompositionView {
static let attachmentsCollectionViewHeight: CGFloat = 100
func initialSetup() { func initialSetup() {
addSubview(avatarImageView) addSubview(avatarImageView)
avatarImageView.translatesAutoresizingMaskIntoConstraints = false avatarImageView.translatesAutoresizingMaskIntoConstraints = false
@ -67,6 +71,10 @@ private extension CompositionView {
textView.inputAccessoryView?.sizeToFit() textView.inputAccessoryView?.sizeToFit()
textView.delegate = self textView.delegate = self
// stackView.addArrangedSubview(attachmentsCollectionView)
stackView.addArrangedSubview(attachmentUploadView)
let constraints = [ let constraints = [
avatarImageView.heightAnchor.constraint(equalToConstant: .avatarDimension), avatarImageView.heightAnchor.constraint(equalToConstant: .avatarDimension),
avatarImageView.widthAnchor.constraint(equalToConstant: .avatarDimension), avatarImageView.widthAnchor.constraint(equalToConstant: .avatarDimension),
@ -76,7 +84,9 @@ private extension CompositionView {
stackView.leadingAnchor.constraint(equalTo: avatarImageView.trailingAnchor, constant: .defaultSpacing), stackView.leadingAnchor.constraint(equalTo: avatarImageView.trailingAnchor, constant: .defaultSpacing),
stackView.topAnchor.constraint(equalTo: readableContentGuide.topAnchor), stackView.topAnchor.constraint(equalTo: readableContentGuide.topAnchor),
stackView.trailingAnchor.constraint(equalTo: readableContentGuide.trailingAnchor), 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 { for constraint in constraints {
@ -88,6 +98,10 @@ private extension CompositionView {
compositionConfiguration.viewModel.$identification.map(\.identity.image) compositionConfiguration.viewModel.$identification.map(\.identity.image)
.sink { [weak self] in self?.avatarImageView.kf.setImage(with: $0) } .sink { [weak self] in self?.avatarImageView.kf.setImage(with: $0) }
.store(in: &cancellables) .store(in: &cancellables)
compositionConfiguration.viewModel.$attachmentUpload
.sink { [weak self] in self?.attachmentUploadView.attachmentUpload = $0 }
.store(in: &cancellables)
} }
func applyCompositionConfiguration() { func applyCompositionConfiguration() {