forked from shadowfacts/Tusker
Extract status posting to separate class, convert to async/await
This commit is contained in:
parent
3f04d74dd6
commit
2f7c7bae5e
|
@ -287,6 +287,7 @@
|
||||||
D6E426AD25334DA500C02E1C /* FuzzyMatcherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E426AC25334DA500C02E1C /* FuzzyMatcherTests.swift */; };
|
D6E426AD25334DA500C02E1C /* FuzzyMatcherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E426AC25334DA500C02E1C /* FuzzyMatcherTests.swift */; };
|
||||||
D6E426B325337C7000C02E1C /* CustomEmojiImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E426B225337C7000C02E1C /* CustomEmojiImageView.swift */; };
|
D6E426B325337C7000C02E1C /* CustomEmojiImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E426B225337C7000C02E1C /* CustomEmojiImageView.swift */; };
|
||||||
D6E57FA325C26FAB00341037 /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = D6E57FA525C26FAB00341037 /* Localizable.stringsdict */; };
|
D6E57FA325C26FAB00341037 /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = D6E57FA525C26FAB00341037 /* Localizable.stringsdict */; };
|
||||||
|
D6E9CDA8281A427800BBC98E /* PostService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E9CDA7281A427800BBC98E /* PostService.swift */; };
|
||||||
D6EAE0DB2550CC8A002DB0AC /* FocusableTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6EAE0DA2550CC8A002DB0AC /* FocusableTextField.swift */; };
|
D6EAE0DB2550CC8A002DB0AC /* FocusableTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6EAE0DA2550CC8A002DB0AC /* FocusableTextField.swift */; };
|
||||||
D6EBF01523C55C0900AE061B /* UIApplication+Scenes.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6EBF01423C55C0900AE061B /* UIApplication+Scenes.swift */; };
|
D6EBF01523C55C0900AE061B /* UIApplication+Scenes.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6EBF01423C55C0900AE061B /* UIApplication+Scenes.swift */; };
|
||||||
D6EBF01723C55E0D00AE061B /* UISceneSession+MastodonController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6EBF01623C55E0D00AE061B /* UISceneSession+MastodonController.swift */; };
|
D6EBF01723C55E0D00AE061B /* UISceneSession+MastodonController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6EBF01623C55E0D00AE061B /* UISceneSession+MastodonController.swift */; };
|
||||||
|
@ -635,6 +636,7 @@
|
||||||
D6E426B225337C7000C02E1C /* CustomEmojiImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomEmojiImageView.swift; sourceTree = "<group>"; };
|
D6E426B225337C7000C02E1C /* CustomEmojiImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomEmojiImageView.swift; sourceTree = "<group>"; };
|
||||||
D6E4885C24A2890C0011C13E /* Tusker.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Tusker.entitlements; sourceTree = "<group>"; };
|
D6E4885C24A2890C0011C13E /* Tusker.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Tusker.entitlements; sourceTree = "<group>"; };
|
||||||
D6E57FA425C26FAB00341037 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = en; path = en.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
D6E57FA425C26FAB00341037 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = en; path = en.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||||
|
D6E9CDA7281A427800BBC98E /* PostService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostService.swift; sourceTree = "<group>"; };
|
||||||
D6EAE0DA2550CC8A002DB0AC /* FocusableTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FocusableTextField.swift; sourceTree = "<group>"; };
|
D6EAE0DA2550CC8A002DB0AC /* FocusableTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FocusableTextField.swift; sourceTree = "<group>"; };
|
||||||
D6EBF01423C55C0900AE061B /* UIApplication+Scenes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIApplication+Scenes.swift"; sourceTree = "<group>"; };
|
D6EBF01423C55C0900AE061B /* UIApplication+Scenes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIApplication+Scenes.swift"; sourceTree = "<group>"; };
|
||||||
D6EBF01623C55E0D00AE061B /* UISceneSession+MastodonController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UISceneSession+MastodonController.swift"; sourceTree = "<group>"; };
|
D6EBF01623C55E0D00AE061B /* UISceneSession+MastodonController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UISceneSession+MastodonController.swift"; sourceTree = "<group>"; };
|
||||||
|
@ -1366,6 +1368,7 @@
|
||||||
D61959D2241E846D00A37B8E /* Models */,
|
D61959D2241E846D00A37B8E /* Models */,
|
||||||
D663626021360A9600C9CBA2 /* Preferences */,
|
D663626021360A9600C9CBA2 /* Preferences */,
|
||||||
D641C780213DD7C4004B4513 /* Screens */,
|
D641C780213DD7C4004B4513 /* Screens */,
|
||||||
|
D6E9CDA6281A426700BBC98E /* Services */,
|
||||||
D62D241E217AA46B005076CC /* Shortcuts */,
|
D62D241E217AA46B005076CC /* Shortcuts */,
|
||||||
D67B506B250B28FF00FAECFB /* Vendor */,
|
D67B506B250B28FF00FAECFB /* Vendor */,
|
||||||
D6BED1722126661300F02DA0 /* Views */,
|
D6BED1722126661300F02DA0 /* Views */,
|
||||||
|
@ -1422,6 +1425,14 @@
|
||||||
path = OpenInTusker;
|
path = OpenInTusker;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
D6E9CDA6281A426700BBC98E /* Services */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
D6E9CDA7281A427800BBC98E /* PostService.swift */,
|
||||||
|
);
|
||||||
|
path = Services;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
D6F1F84E2193B9BE00F5FE67 /* Caching */ = {
|
D6F1F84E2193B9BE00F5FE67 /* Caching */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
@ -1827,6 +1838,7 @@
|
||||||
D64AAE9726C88DC400FC57FB /* ToastConfiguration.swift in Sources */,
|
D64AAE9726C88DC400FC57FB /* ToastConfiguration.swift in Sources */,
|
||||||
D6093FB025BE0B01004811E6 /* TrendingHashtagTableViewCell.swift in Sources */,
|
D6093FB025BE0B01004811E6 /* TrendingHashtagTableViewCell.swift in Sources */,
|
||||||
D63661C02381C144004B9E16 /* PreferencesNavigationController.swift in Sources */,
|
D63661C02381C144004B9E16 /* PreferencesNavigationController.swift in Sources */,
|
||||||
|
D6E9CDA8281A427800BBC98E /* PostService.swift in Sources */,
|
||||||
D6B053A223BD2C0600A066FA /* AssetPickerViewController.swift in Sources */,
|
D6B053A223BD2C0600A066FA /* AssetPickerViewController.swift in Sources */,
|
||||||
D6EE63FB2551F7F60065485C /* StatusCollapseButton.swift in Sources */,
|
D6EE63FB2551F7F60065485C /* StatusCollapseButton.swift in Sources */,
|
||||||
D627944A23A6AD6100D38C68 /* BookmarksTableViewController.swift in Sources */,
|
D627944A23A6AD6100D38C68 /* BookmarksTableViewController.swift in Sources */,
|
||||||
|
|
|
@ -10,16 +10,49 @@ import SwiftUI
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
import Combine
|
import Combine
|
||||||
|
|
||||||
|
@propertyWrapper struct OptionalStateObject<T: ObservableObject>: DynamicProperty {
|
||||||
|
private class Republisher: ObservableObject {
|
||||||
|
var cancellable: AnyCancellable?
|
||||||
|
var wrapped: T? {
|
||||||
|
didSet {
|
||||||
|
cancellable?.cancel()
|
||||||
|
cancellable = wrapped?.objectWillChange
|
||||||
|
.receive(on: RunLoop.main)
|
||||||
|
.sink { [unowned self] _ in
|
||||||
|
self.objectWillChange.send()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@StateObject private var republisher = Republisher()
|
||||||
|
@State private var object: T?
|
||||||
|
var wrappedValue: T? {
|
||||||
|
get {
|
||||||
|
object
|
||||||
|
}
|
||||||
|
nonmutating set {
|
||||||
|
object = newValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func update() {
|
||||||
|
republisher.wrapped = wrappedValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct ComposeView: View {
|
struct ComposeView: View {
|
||||||
@ObservedObject var draft: Draft
|
@ObservedObject var draft: Draft
|
||||||
@EnvironmentObject var mastodonController: MastodonController
|
@EnvironmentObject var mastodonController: MastodonController
|
||||||
@EnvironmentObject var uiState: ComposeUIState
|
@EnvironmentObject var uiState: ComposeUIState
|
||||||
|
|
||||||
@State private var isPosting = false
|
@OptionalStateObject private var poster: PostService?
|
||||||
@State private var postProgress: Double = 0
|
|
||||||
@State private var postTotalProgress: Double = 0
|
|
||||||
@State private var isShowingPostErrorAlert = false
|
@State private var isShowingPostErrorAlert = false
|
||||||
@State private var postError: PostError?
|
@State private var postError: PostService.Error?
|
||||||
|
|
||||||
|
private var isPosting: Bool {
|
||||||
|
poster != nil
|
||||||
|
}
|
||||||
|
|
||||||
private let stackPadding: CGFloat = 8
|
private let stackPadding: CGFloat = 8
|
||||||
|
|
||||||
|
@ -58,9 +91,9 @@ struct ComposeView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if postProgress > 0 {
|
if let poster = poster {
|
||||||
// can't use SwiftUI.ProgressView because there's no UIProgressView.Style.bar equivalent, see FB8587149
|
// can't use SwiftUI.ProgressView because there's no UIProgressView.Style.bar equivalent, see FB8587149
|
||||||
WrappedProgressView(value: postProgress, total: postTotalProgress)
|
WrappedProgressView(value: poster.currentStep, total: poster.totalSteps)
|
||||||
}
|
}
|
||||||
|
|
||||||
autocompleteSuggestions
|
autocompleteSuggestions
|
||||||
|
@ -123,6 +156,7 @@ struct ComposeView: View {
|
||||||
// the list rows provide their own padding, so we cancel out the extra spacing from the VStack
|
// the list rows provide their own padding, so we cancel out the extra spacing from the VStack
|
||||||
.padding([.top, .bottom], -8)
|
.padding([.top, .bottom], -8)
|
||||||
}
|
}
|
||||||
|
.disabled(isPosting)
|
||||||
.padding(stackPadding)
|
.padding(stackPadding)
|
||||||
.padding(.bottom, uiState.autocompleteState != nil ? 46 : nil)
|
.padding(.bottom, uiState.autocompleteState != nil ? 46 : nil)
|
||||||
}
|
}
|
||||||
|
@ -147,7 +181,11 @@ struct ComposeView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
private var postButton: some View {
|
private var postButton: some View {
|
||||||
Button(action: self.postStatus) {
|
Button {
|
||||||
|
Task {
|
||||||
|
await self.postStatus()
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
Text("Post")
|
Text("Post")
|
||||||
}
|
}
|
||||||
.disabled(!postButtonEnabled)
|
.disabled(!postButtonEnabled)
|
||||||
|
@ -184,179 +222,31 @@ struct ComposeView: View {
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
private func postStatus() {
|
private func postStatus() async {
|
||||||
guard draft.hasContent else { return }
|
guard !isPosting,
|
||||||
|
draft.hasContent else {
|
||||||
isPosting = true
|
|
||||||
|
|
||||||
// save before posting, so if a crash occurs during network request, the status won't be lost
|
|
||||||
DraftsManager.save()
|
|
||||||
|
|
||||||
let contentWarning = draft.contentWarningEnabled ? draft.contentWarning : nil
|
|
||||||
let sensitive = contentWarning != nil
|
|
||||||
|
|
||||||
// 2 steps (request data, then upload) for each attachment
|
|
||||||
postTotalProgress = Double(2 + (draft.attachments.count * 2))
|
|
||||||
postProgress = 1
|
|
||||||
|
|
||||||
uploadAttachments { (result) in
|
|
||||||
switch result {
|
|
||||||
case let .failure(error):
|
|
||||||
self.isShowingPostErrorAlert = true
|
|
||||||
self.postError = error
|
|
||||||
self.postProgress = 0
|
|
||||||
self.postTotalProgress = 0
|
|
||||||
self.isPosting = false
|
|
||||||
|
|
||||||
case let .success(uploadedAttachments):
|
|
||||||
let request = Client.createStatus(text: draft.textForPosting(on: mastodonController.instanceFeatures),
|
|
||||||
contentType: Preferences.shared.statusContentType,
|
|
||||||
inReplyTo: draft.inReplyToID,
|
|
||||||
media: uploadedAttachments,
|
|
||||||
sensitive: sensitive,
|
|
||||||
spoilerText: contentWarning,
|
|
||||||
visibility: draft.visibility,
|
|
||||||
language: nil,
|
|
||||||
pollOptions: draft.poll?.options.map(\.text),
|
|
||||||
pollExpiresIn: draft.poll == nil ? nil : Int(draft.poll!.duration),
|
|
||||||
pollMultiple: draft.poll?.multiple,
|
|
||||||
localOnly: mastodonController.instanceFeatures.instanceType == .hometown ? draft.localOnly : nil)
|
|
||||||
self.mastodonController.run(request) { (response) in
|
|
||||||
switch response {
|
|
||||||
case let .failure(error):
|
|
||||||
self.isShowingPostErrorAlert = true
|
|
||||||
self.postError = error
|
|
||||||
|
|
||||||
case .success(_, _):
|
|
||||||
self.postProgress += 1
|
|
||||||
|
|
||||||
DraftsManager.shared.remove(self.draft)
|
|
||||||
|
|
||||||
// wait .25 seconds so the user can see the progress bar has completed
|
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(250)) {
|
|
||||||
self.uiState.delegate?.dismissCompose(mode: .post)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func uploadAttachments(_ completion: @escaping (Result<[Attachment], AttachmentUploadError>) -> Void) {
|
|
||||||
let group = DispatchGroup()
|
|
||||||
|
|
||||||
var attachmentDataResults = [Result<(Data, String), CompositionAttachmentData.Error>?]()
|
|
||||||
|
|
||||||
for (index, compAttachment) in draft.attachments.enumerated() {
|
|
||||||
group.enter()
|
|
||||||
|
|
||||||
attachmentDataResults.append(nil)
|
|
||||||
|
|
||||||
compAttachment.data.getData { (result) in
|
|
||||||
postProgress += 1
|
|
||||||
|
|
||||||
attachmentDataResults[index] = result
|
|
||||||
group.leave()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
group.notify(queue: .global(qos: .userInitiated)) {
|
|
||||||
|
|
||||||
var anyFailed = false
|
|
||||||
var uploadedAttachments = [Result<Attachment, Error>?]()
|
|
||||||
|
|
||||||
// Mastodon does not respect the order of the `media_ids` parameter in the create post request,
|
|
||||||
// it determines attachment order by which was uploaded first. Since the upload attachment request
|
|
||||||
// does not include any timestamp data, and requests may arrive at the server out-of-order,
|
|
||||||
// attachments need to be uploaded serially in order to ensure the order of attachments in the
|
|
||||||
// posted status reflects order the user set.
|
|
||||||
// Pleroma does respect the order of the `media_ids` parameter.
|
|
||||||
|
|
||||||
let datas: [(Data, String)]
|
|
||||||
do {
|
|
||||||
datas = try attachmentDataResults.map { try $0!.get() }
|
|
||||||
} catch {
|
|
||||||
completion(.failure(AttachmentUploadError(errors: [error])))
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
for (index, (data, mimeType)) in datas.enumerated() {
|
let poster = PostService(mastodonController: mastodonController, draft: draft)
|
||||||
group.enter()
|
self.poster = poster
|
||||||
|
|
||||||
let compAttachment = draft.attachments[index]
|
do {
|
||||||
let formAttachment = FormAttachment(mimeType: mimeType, data: data, fileName: "file")
|
try await poster.post()
|
||||||
let request = Client.upload(attachment: formAttachment, description: compAttachment.attachmentDescription)
|
|
||||||
self.mastodonController.run(request) { (response) in
|
|
||||||
switch response {
|
|
||||||
case let .failure(error):
|
|
||||||
uploadedAttachments.append(.failure(error))
|
|
||||||
anyFailed = true
|
|
||||||
|
|
||||||
case let .success(attachment, _):
|
// wait .25 seconds so the user can see the progress bar has completed
|
||||||
self.postProgress += 1
|
try? await Task.sleep(nanoseconds: 250_000_000)
|
||||||
uploadedAttachments.append(.success(attachment))
|
|
||||||
|
uiState.delegate?.dismissCompose(mode: .post)
|
||||||
|
|
||||||
|
} catch let error as PostService.Error {
|
||||||
|
self.isShowingPostErrorAlert = true
|
||||||
|
self.postError = error
|
||||||
|
} catch {
|
||||||
|
fatalError("Unreachable")
|
||||||
}
|
}
|
||||||
|
|
||||||
group.leave()
|
self.poster = nil
|
||||||
}
|
|
||||||
|
|
||||||
group.wait()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if anyFailed {
|
|
||||||
let errors = uploadedAttachments.map { (result) -> Error? in
|
|
||||||
if case let .failure(error) = result {
|
|
||||||
return error
|
|
||||||
} else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
completion(.failure(AttachmentUploadError(errors: errors)))
|
|
||||||
} else {
|
|
||||||
let uploadedAttachments = uploadedAttachments.map {
|
|
||||||
try! $0!.get()
|
|
||||||
}
|
|
||||||
completion(.success(uploadedAttachments))
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fileprivate protocol PostError: LocalizedError {}
|
|
||||||
|
|
||||||
extension PostError {
|
|
||||||
var localizedDescription: String {
|
|
||||||
if let self = self as? Client.Error {
|
|
||||||
return self.localizedDescription
|
|
||||||
} else if let self = self as? AttachmentUploadError {
|
|
||||||
return self.localizedDescription
|
|
||||||
} else {
|
|
||||||
return "Unknown Error"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension Client.Error: PostError {}
|
|
||||||
|
|
||||||
fileprivate struct AttachmentUploadError: PostError {
|
|
||||||
let errors: [Error?]
|
|
||||||
|
|
||||||
var localizedDescription: String {
|
|
||||||
return errors.enumerated().compactMap { (index, error) -> String? in
|
|
||||||
guard let error = error else { return nil }
|
|
||||||
let description: String
|
|
||||||
// need to downcast to use more specific localizedDescription impl from Pachyderm
|
|
||||||
if let error = error as? Client.Error {
|
|
||||||
description = error.localizedDescription
|
|
||||||
} else if let error = error as? CompositionAttachmentData.Error {
|
|
||||||
description = error.localizedDescription
|
|
||||||
} else {
|
|
||||||
description = error.localizedDescription
|
|
||||||
}
|
|
||||||
return "Attachment \(index + 1): \(description)"
|
|
||||||
}.joined(separator: ",\n")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,122 @@
|
||||||
|
//
|
||||||
|
// PostService.swift
|
||||||
|
// Tusker
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 4/27/22.
|
||||||
|
// Copyright © 2022 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Pachyderm
|
||||||
|
|
||||||
|
class PostService: ObservableObject {
|
||||||
|
private let mastodonController: MastodonController
|
||||||
|
private let draft: Draft
|
||||||
|
let totalSteps: Int
|
||||||
|
|
||||||
|
@Published var currentStep = 1
|
||||||
|
|
||||||
|
init(mastodonController: MastodonController, draft: Draft) {
|
||||||
|
self.mastodonController = mastodonController
|
||||||
|
self.draft = draft
|
||||||
|
// 2 steps (request data, then upload) for each attachment
|
||||||
|
self.totalSteps = 2 + (draft.attachments.count * 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
func post() async throws {
|
||||||
|
guard draft.hasContent else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// save before posting, so if a crash occurs during network request, the status won't be lost
|
||||||
|
DraftsManager.save()
|
||||||
|
|
||||||
|
let uploadedAttachments = try await uploadAttachments()
|
||||||
|
|
||||||
|
let contentWarning = draft.contentWarningEnabled ? draft.contentWarning : nil
|
||||||
|
let sensitive = contentWarning != nil
|
||||||
|
|
||||||
|
let request = Client.createStatus(
|
||||||
|
text: draft.textForPosting(on: mastodonController.instanceFeatures),
|
||||||
|
contentType: Preferences.shared.statusContentType,
|
||||||
|
inReplyTo: draft.inReplyToID,
|
||||||
|
media: uploadedAttachments,
|
||||||
|
sensitive: sensitive,
|
||||||
|
spoilerText: contentWarning,
|
||||||
|
visibility: draft.visibility,
|
||||||
|
language: nil,
|
||||||
|
pollOptions: draft.poll?.options.map(\.text),
|
||||||
|
pollExpiresIn: draft.poll == nil ? nil : Int(draft.poll!.duration),
|
||||||
|
pollMultiple: draft.poll?.multiple,
|
||||||
|
localOnly: mastodonController.instanceFeatures.localOnlyPosts ? draft.localOnly : nil
|
||||||
|
)
|
||||||
|
do {
|
||||||
|
let (_, _) = try await mastodonController.run(request)
|
||||||
|
currentStep += 1
|
||||||
|
|
||||||
|
DraftsManager.shared.remove(self.draft)
|
||||||
|
} catch let error as Client.Error {
|
||||||
|
throw Error.posting(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func uploadAttachments() async throws -> [Attachment] {
|
||||||
|
var attachments: [Attachment] = []
|
||||||
|
attachments.reserveCapacity(draft.attachments.count)
|
||||||
|
for (index, attachment) in draft.attachments.enumerated() {
|
||||||
|
let data: Data
|
||||||
|
let mimeType: String
|
||||||
|
do {
|
||||||
|
(data, mimeType) = try await getData(for: attachment)
|
||||||
|
currentStep += 1
|
||||||
|
} catch let error as CompositionAttachmentData.Error {
|
||||||
|
throw Error.attachmentData(index: index, cause: error)
|
||||||
|
}
|
||||||
|
do {
|
||||||
|
let uploaded = try await uploadAttachment(data: data, mimeType: mimeType, description: attachment.description)
|
||||||
|
attachments.append(uploaded)
|
||||||
|
currentStep += 1
|
||||||
|
} catch let error as Client.Error {
|
||||||
|
throw Error.attachmentUpload(index: index, cause: error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return attachments
|
||||||
|
}
|
||||||
|
|
||||||
|
private func getData(for attachment: CompositionAttachment) async throws -> (Data, String) {
|
||||||
|
return try await withCheckedThrowingContinuation { continuation in
|
||||||
|
attachment.data.getData { result in
|
||||||
|
switch result {
|
||||||
|
case let .success(res):
|
||||||
|
continuation.resume(returning: res)
|
||||||
|
case let .failure(error):
|
||||||
|
continuation.resume(throwing: error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func uploadAttachment(data: Data, mimeType: String, description: String?) async throws -> Attachment {
|
||||||
|
let formAttachment = FormAttachment(mimeType: mimeType, data: data, fileName: "file")
|
||||||
|
let req = Client.upload(attachment: formAttachment, description: description)
|
||||||
|
return try await mastodonController.run(req).0
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Error: Swift.Error, LocalizedError {
|
||||||
|
case attachmentData(index: Int, cause: CompositionAttachmentData.Error)
|
||||||
|
case attachmentUpload(index: Int, cause: Client.Error)
|
||||||
|
case posting(Client.Error)
|
||||||
|
|
||||||
|
var localizedDescription: String {
|
||||||
|
switch self {
|
||||||
|
case let .attachmentData(index: index, cause: cause):
|
||||||
|
return "Attachment \(index + 1): \(cause.localizedDescription)"
|
||||||
|
case let .attachmentUpload(index: index, cause: cause):
|
||||||
|
return "Attachment \(index + 1): \(cause.localizedDescription)"
|
||||||
|
case let .posting(error):
|
||||||
|
return error.localizedDescription
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -11,8 +11,8 @@ import SwiftUI
|
||||||
struct WrappedProgressView: UIViewRepresentable {
|
struct WrappedProgressView: UIViewRepresentable {
|
||||||
typealias UIViewType = UIProgressView
|
typealias UIViewType = UIProgressView
|
||||||
|
|
||||||
let value: Double
|
let value: Int
|
||||||
let total: Double
|
let total: Int
|
||||||
|
|
||||||
func makeUIView(context: Context) -> UIProgressView {
|
func makeUIView(context: Context) -> UIProgressView {
|
||||||
return UIProgressView(progressViewStyle: .bar)
|
return UIProgressView(progressViewStyle: .bar)
|
||||||
|
@ -20,7 +20,9 @@ struct WrappedProgressView: UIViewRepresentable {
|
||||||
|
|
||||||
func updateUIView(_ uiView: UIProgressView, context: Context) {
|
func updateUIView(_ uiView: UIProgressView, context: Context) {
|
||||||
if total > 0 {
|
if total > 0 {
|
||||||
uiView.setProgress(Float(value / total), animated: true)
|
let progress = Float(value) / Float(total)
|
||||||
|
print(progress)
|
||||||
|
uiView.setProgress(progress, animated: true)
|
||||||
} else {
|
} else {
|
||||||
uiView.setProgress(0, animated: true)
|
uiView.setProgress(0, animated: true)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue