Compare commits
No commits in common. "28332ef4487ae870852d5978f507640a6b73266b" and "c36a239f46da2a6bd72b56a9755bc506127de819" have entirely different histories.
28332ef448
...
c36a239f46
|
@ -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
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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,58 +205,71 @@ 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,
|
|
||||||
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)
|
|
||||||
do {
|
|
||||||
try await mastodonController.run(request)
|
|
||||||
self.postProgress += 1
|
|
||||||
|
|
||||||
DraftsManager.shared.remove(self.draft)
|
case let .success(uploadedAttachments):
|
||||||
|
let request = Client.createStatus(text: draft.textForPosting,
|
||||||
// wait .25 seconds so the user can see the progress bar has completed
|
contentType: Preferences.shared.statusContentType,
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(250)) {
|
inReplyTo: draft.inReplyToID,
|
||||||
self.uiState.delegate?.dismissCompose(mode: .post)
|
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)
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} 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
|
||||||
// does not include any timestamp data, and requests may arrive at the server out-of-order,
|
// does not include any timestamp data, and requests may arrive at the server out-of-order,
|
||||||
|
@ -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
|
||||||
do {
|
switch response {
|
||||||
let (uploadedAttachment, _) = try await mastodonController.run(request)
|
case let .failure(error):
|
||||||
uploaded.append(.success(uploadedAttachment))
|
uploadedAttachments.append(.failure(error))
|
||||||
postProgress += 1
|
anyFailed = true
|
||||||
} catch {
|
|
||||||
uploaded.append(.failure(error))
|
case let .success(attachment, _):
|
||||||
anyFailed = true
|
self.postProgress += 1
|
||||||
|
uploadedAttachments.append(.success(attachment))
|
||||||
|
}
|
||||||
|
|
||||||
|
group.leave()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
|
Loading…
Reference in New Issue