Compare commits

..

No commits in common. "28332ef4487ae870852d5978f507640a6b73266b" and "c36a239f46da2a6bd72b56a9755bc506127de819" have entirely different histories.

4 changed files with 110 additions and 86 deletions

View File

@ -99,11 +99,6 @@
value = "1" value = "1"
isEnabled = "NO"> isEnabled = "NO">
</EnvironmentVariable> </EnvironmentVariable>
<EnvironmentVariable
key = "LIBDISPATCH_COOPERATIVE_POOL_STRICT"
value = "1"
isEnabled = "YES">
</EnvironmentVariable>
</EnvironmentVariables> </EnvironmentVariables>
</LaunchAction> </LaunchAction>
<ProfileAction <ProfileAction

View File

@ -46,8 +46,8 @@ class MastodonController: ObservableObject {
@Published private(set) var instance: Instance! @Published private(set) var instance: Instance!
private(set) var customEmojis: [Emoji]? private(set) var customEmojis: [Emoji]?
@MainActor private var pendingOwnInstanceRequestCallbacks = [(Instance) -> Void]() private var pendingOwnInstanceRequestCallbacks = [(Instance) -> Void]()
@MainActor private var ownInstanceRequest: URLSessionTask? private var ownInstanceRequest: URLSessionTask?
var loggedIn: Bool { var loggedIn: Bool {
accountInfo != nil accountInfo != nil
@ -118,13 +118,14 @@ class MastodonController: ObservableObject {
} }
} }
@MainActor
func getOwnInstance(completion: ((Instance) -> Void)? = nil) { func getOwnInstance(completion: ((Instance) -> Void)? = nil) {
getOwnInstanceInternal(retryAttempt: 0, completion: completion) getOwnInstanceInternal(retryAttempt: 0, completion: completion)
} }
@MainActor
private func getOwnInstanceInternal(retryAttempt: Int, completion: ((Instance) -> Void)?) { private func getOwnInstanceInternal(retryAttempt: Int, completion: ((Instance) -> Void)?) {
// this is main thread only to prevent concurrent access to ownInstanceRequest and pendingOwnInstanceRequestCallbacks
assert(Thread.isMainThread)
if let instance = self.instance { if let instance = self.instance {
completion?(instance) completion?(instance)
} else { } else {

View File

@ -104,14 +104,6 @@ enum CompositionAttachmentData {
} }
} }
func getData() async -> (Data, String) {
return await withCheckedContinuation { continuation in
getData { data, mimeType in
continuation.resume(returning: (data, mimeType))
}
}
}
private static func exportVideoData(session: AVAssetExportSession, completion: @escaping (Data, String) -> Void) { private static func exportVideoData(session: AVAssetExportSession, completion: @escaping (Data, String) -> Void) {
session.outputFileType = .mp4 session.outputFileType = .mp4
session.outputURL = FileManager.default.temporaryDirectory.appendingPathComponent("exported_video_\(UUID())").appendingPathExtension("mp4") session.outputURL = FileManager.default.temporaryDirectory.appendingPathComponent("exported_video_\(UUID())").appendingPathExtension("mp4")

View File

@ -19,7 +19,7 @@ struct ComposeView: View {
@State private var postProgress: Double = 0 @State private var postProgress: Double = 0
@State private var postTotalProgress: Double = 0 @State private var postTotalProgress: Double = 0
@State private var isShowingPostErrorAlert = false @State private var isShowingPostErrorAlert = false
@State private var postError: Error? @State private var postError: PostError?
private let stackPadding: CGFloat = 8 private let stackPadding: CGFloat = 8
@ -148,11 +148,7 @@ struct ComposeView: View {
} }
private var postButton: some View { private var postButton: some View {
Button { Button(action: self.postStatus) {
async {
await self.postStatus()
}
} label: {
Text("Post") Text("Post")
} }
.disabled(!postButtonEnabled) .disabled(!postButtonEnabled)
@ -194,7 +190,7 @@ struct ComposeView: View {
]) ])
} }
private func postStatus() async { private func postStatus() {
guard draft.hasContent else { return } guard draft.hasContent else { return }
isPosting = true isPosting = true
@ -209,57 +205,70 @@ struct ComposeView: View {
postTotalProgress = Double(2 + (draft.attachments.count * 2)) postTotalProgress = Double(2 + (draft.attachments.count * 2))
postProgress = 1 postProgress = 1
let uploadedAttachments: [Attachment] uploadAttachments { (result) in
do { switch result {
uploadedAttachments = try await uploadAttachments() case let .failure(error):
} catch { self.isShowingPostErrorAlert = true
self.isShowingPostErrorAlert = true self.postError = error
self.postError = error self.postProgress = 0
self.postProgress = 0 self.postTotalProgress = 0
self.postTotalProgress = 0 self.isPosting = false
self.isPosting = false
return
}
let request = Client.createStatus(text: draft.textForPosting, case let .success(uploadedAttachments):
contentType: Preferences.shared.statusContentType, let request = Client.createStatus(text: draft.textForPosting,
inReplyTo: draft.inReplyToID, contentType: Preferences.shared.statusContentType,
media: uploadedAttachments, inReplyTo: draft.inReplyToID,
sensitive: sensitive, media: uploadedAttachments,
spoilerText: contentWarning, sensitive: sensitive,
visibility: draft.visibility, spoilerText: contentWarning,
language: nil, visibility: draft.visibility,
pollOptions: draft.poll?.options.map(\.text), language: nil,
pollExpiresIn: draft.poll == nil ? nil : Int(draft.poll!.duration), pollOptions: draft.poll?.options.map(\.text),
pollMultiple: draft.poll?.multiple) pollExpiresIn: draft.poll == nil ? nil : Int(draft.poll!.duration),
do { pollMultiple: draft.poll?.multiple)
try await mastodonController.run(request) self.mastodonController.run(request) { (response) in
self.postProgress += 1 switch response {
case let .failure(error):
self.isShowingPostErrorAlert = true
self.postError = error
DraftsManager.shared.remove(self.draft) case .success(_, _):
self.postProgress += 1
// wait .25 seconds so the user can see the progress bar has completed DraftsManager.shared.remove(self.draft)
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(250)) {
self.uiState.delegate?.dismissCompose(mode: .post) // 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)
}
}
}
} }
} catch {
self.isShowingPostErrorAlert = true
self.postError = error
} }
} }
private func uploadAttachments() async throws -> [Attachment] { private func uploadAttachments(_ completion: @escaping (Result<[Attachment], AttachmentUploadError>) -> Void) {
guard !draft.attachments.isEmpty else { let group = DispatchGroup()
return []
var attachmentDatas = [(Data, String)?]()
for (index, compAttachment) in draft.attachments.enumerated() {
group.enter()
attachmentDatas.append(nil)
compAttachment.data.getData { (data, mimeType) in
postProgress += 1
attachmentDatas[index] = (data, mimeType)
group.leave()
}
} }
return try await withThrowingTaskGroup(of: (CompositionAttachment, (Data, String)).self) { taskGroup in group.notify(queue: .global(qos: .userInitiated)) {
for compAttachment in draft.attachments {
postProgress += 1 var anyFailed = false
taskGroup.async { var uploadedAttachments = [Result<Attachment, Error>?]()
return (compAttachment, await compAttachment.data.getData())
}
}
// Mastodon does not respect the order of the `media_ids` parameter in the create post request, // 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 // it determines attachment order by which was uploaded first. Since the upload attachment request
@ -268,40 +277,67 @@ struct ComposeView: View {
// posted status reflects order the user set. // posted status reflects order the user set.
// Pleroma does respect the order of the `media_ids` parameter. // Pleroma does respect the order of the `media_ids` parameter.
var anyFailed = false for (index, (data, mimeType)) in attachmentDatas.map(\.unsafelyUnwrapped).enumerated() {
var uploaded = [Result<Attachment, Error>]() group.enter()
for try await (compAttachment, (data, mimeType)) in taskGroup { let compAttachment = draft.attachments[index]
let formAttachment = FormAttachment(mimeType: mimeType, data: data, fileName: "file") let formAttachment = FormAttachment(mimeType: mimeType, data: data, fileName: "file")
let request = Client.upload(attachment: formAttachment, description: compAttachment.attachmentDescription) 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
do { case let .success(attachment, _):
let (uploadedAttachment, _) = try await mastodonController.run(request) self.postProgress += 1
uploaded.append(.success(uploadedAttachment)) uploadedAttachments.append(.success(attachment))
postProgress += 1 }
} catch {
uploaded.append(.failure(error)) group.leave()
anyFailed = true
} }
group.wait()
} }
if anyFailed { if anyFailed {
let errors = uploaded.map { result -> Error? in let errors = uploadedAttachments.map { (result) -> Error? in
if case let .failure(error) = result { if case let .failure(error) = result {
return error return error
} else { } else {
return nil return nil
} }
} }
throw AttachmentUploadError(errors: errors) completion(.failure(AttachmentUploadError(errors: errors)))
} else { } else {
return uploaded.map { try! $0.get() } let uploadedAttachments = uploadedAttachments.map {
try! $0!.get()
}
completion(.success(uploadedAttachments))
} }
} }
} }
} }
fileprivate struct AttachmentUploadError: LocalizedError { 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?] let errors: [Error?]
var localizedDescription: String { var localizedDescription: String {