Skip to content

Commit

Permalink
Update
Browse files Browse the repository at this point in the history
  • Loading branch information
Sjmarf committed Sep 30, 2023
1 parent 2b324ef commit 9252b67
Show file tree
Hide file tree
Showing 7 changed files with 122 additions and 83 deletions.
44 changes: 16 additions & 28 deletions Mlem/API/APIClient/APIClient+Pictrs.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@ extension APIClient {
func uploadImage(
_ imageData: Data,
onProgress progressCallback: @escaping (_ progress: Double) -> Void,
onCompletion completionCallback: @escaping(_ response: ImageUploadResponse?) -> Void
) async throws -> URLSessionUploadTask {
onCompletion completionCallback: @escaping(_ response: ImageUploadResponse?) -> Void,
`catch`: @escaping (Error) -> Void
) async throws -> Task<(), any Error> {

let delegate = ImageUploadDelegate(callback: progressCallback)

// Modify the instance URL to remove "api/v3" and add "pictrs/image".
var components = URLComponents()
components.scheme = try self.session.instanceUrl.scheme
Expand All @@ -46,39 +46,27 @@ extension APIClient {
auth: session.token
)

let task = self.urlSession.uploadTask(
with: request,
from: multiPartForm.createField(boundary: boundary),
completionHandler: { data, response, _ in
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
if let data = data {
return Task { [request] in
do {
let (data, _) = try await self.urlSession.upload(
for: request,
from: multiPartForm.createField(boundary: boundary),
delegate: delegate)
do {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let response = try decoder.decode(ImageUploadResponse.self, from: data)
completionCallback(response)
} catch {
print("Upload failed: \(String(data: data, encoding: .utf8))")
completionCallback(nil)
} catch DecodingError.dataCorrupted {
throw APIClientError.decoding(data)
}
} catch {
`catch`(error)
}
})

task.delegate = delegate
task.resume()
return task
}
}
}

struct ImageUploadResponse: Codable {
public let msg: String
public let files: [PictrsFile]?
}

struct PictrsFile: Codable, Equatable {
public let file: String
public let deleteToken: String
}

private struct MultiPartForm: Codable {
var mimeType: String
var fileName: String
Expand Down
10 changes: 10 additions & 0 deletions Mlem/Models/PictrsImageModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@

import SwiftUI

struct ImageUploadResponse: Codable {
public let msg: String
public let files: [PictrsFile]?
}

struct PictrsFile: Codable, Equatable {
public let file: String
public let deleteToken: String
}

struct PictrsImageModel {
enum UploadState {
case uploading(progress: Double)
Expand Down
26 changes: 24 additions & 2 deletions Mlem/Repositories/PictrsRespository.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ class PictrsRespository {
imageModel: PictrsImageModel,
imageSelection: PhotosPickerItem,
onUpdate updateCallback: @escaping (_ imageModel: PictrsImageModel) -> Void
) async throws -> URLSessionUploadTask? {
) async throws -> Task<(), any Error>? {
var imageModel = imageModel
do {
let data = try await imageSelection.loadTransferable(type: Data.self)
Expand Down Expand Up @@ -44,13 +44,35 @@ class PictrsRespository {
imageModel.state = .failed(nil)
updateCallback(imageModel)
}
}, catch: { error in
print("Upload failed: \(error)")
switch error {
case APIClientError.decoding(let data):
imageModel.state = .failed(String(data: data, encoding: .utf8))
default:
imageModel.state = .failed(String(describing: error))
}

updateCallback(imageModel)
})
} else {
imageModel.state = .failed("No data to upload")
updateCallback(imageModel)
}
} catch {
print("Upload failed: \(error)")
imageModel.state = .failed(nil)
imageModel.state = .failed(String(describing: error))
updateCallback(imageModel)
}
return nil
}

func deleteImage(file: PictrsFile) async throws {
// A decoding error will always be throws because the delete request has no response... there's
// certainly a better way to handle this by making ImageDeleteRequest itself have no response
// object, possibly via an intermediate APIRequestWithResponse protocol
do {
try await apiClient.deleteImage(file: file)
} catch APIClientError.decoding(_) { }
}
}
17 changes: 11 additions & 6 deletions Mlem/Views/Shared/Components/ImageUploadView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@ import PhotosUI
import Dependencies

struct ImageUploadView: View {
@Dependency(\.apiClient) var apiClient

var imageModel: PictrsImageModel
let onCancel: () -> Void

Expand All @@ -33,10 +31,17 @@ struct ImageUploadView: View {
HStack {
switch imageModel.state {
case .uploading(let progress):
Text("Uploading...")
ProgressView(value: progress)
.progressViewStyle(LinearProgressViewStyle())
.frame(width: 100, height: 10)
if progress == 1 {
Text("Processing...")
ProgressView()
.controlSize(.small)
.padding(.horizontal, 6)
} else {
Text("Uploading...")
ProgressView(value: progress)
.progressViewStyle(LinearProgressViewStyle())
.frame(width: 100, height: 10)
}
case .uploaded:
Text("Uploaded")
case .failed(let msg):
Expand Down
5 changes: 0 additions & 5 deletions Mlem/Views/Shared/Composer/PostComposerView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,6 @@ struct PostComposerView: View {
@State var postBody: String
@State var isNSFW: Bool

private var hasPostContent: Bool {
!postTitle.isEmpty || !postURL.isEmpty || !postBody.isEmpty
}

init(editModel: PostEditorModel) {
self.postTracker = editModel.postTracker
self.editModel = editModel
Expand Down Expand Up @@ -71,7 +67,6 @@ struct PostComposerView: View {

dismiss()
}
.interactiveDismissDisabled(hasPostContent)
}
}

Expand Down
56 changes: 56 additions & 0 deletions Mlem/Views/Shared/Composer/PostDetailEditorView+Logic.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ import SwiftUI
import PhotosUI

extension PostDetailEditorView {
var hasPostContent: Bool {
!postTitle.isEmpty || !postURL.isEmpty || !postBody.isEmpty || imageModel != nil
}

var isReadyToPost: Bool {
switch imageModel?.state {
case nil, .uploaded:
Expand Down Expand Up @@ -55,4 +59,56 @@ extension PostDetailEditorView {
errorHandler.handle(error)
}
}

func uploadImage(imageSelection: PhotosPickerItem) {
self.imageModel = .init()
Task {
self.uploadTask = try await pictrsRepository.uploadImage(
imageModel: .init(),
imageSelection: imageSelection,
onUpdate: { newValue in
self.imageModel = newValue
switch newValue.state {
case .uploaded(let file):
if let file = file {
do {
var components = URLComponents()
components.scheme = try apiClient.session.instanceUrl.scheme
components.host = try apiClient.session.instanceUrl.host
components.path = "/pictrs/image/\(file.file)"
postURL = components.url?.absoluteString ?? ""
} catch {
self.imageModel?.state = .failed(nil)
}
} else {

}
default:
postURL = ""
}
}
)
}
}

func cancelUpload() {
if let task = self.uploadTask {
task.cancel()
}
switch imageModel?.state {
case .uploaded(file: let file):
if let file = file {
Task {
do {
try await pictrsRepository.deleteImage(file: file)
} catch {
errorHandler.handle(error)
}
print("Deleted from pictrs")
}
}
default:
break
}
}
}
47 changes: 5 additions & 42 deletions Mlem/Views/Shared/Composer/PostDetailEditorView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ struct PostDetailEditorView: View {
@State var imageSelection: PhotosPickerItem?
@State var imageModel: PictrsImageModel?

@State var uploadTask: URLSessionUploadTask?
@State var uploadTask: Task<(), any Error>?

@FocusState private var focusedField: Field?

Expand Down Expand Up @@ -111,19 +111,7 @@ struct PostDetailEditorView: View {
// URL Row
if let imageModel = imageModel {
ImageUploadView(imageModel: imageModel, onCancel: {
if let task = self.uploadTask {
task.cancel()
}
switch imageModel.state {
case .uploaded(file: let file):
if let file = file {
Task {
try await apiClient.deleteImage(file: file)
}
}
default:
break
}
cancelUpload()
imageSelection = nil
self.imageModel = nil
postURL = ""
Expand Down Expand Up @@ -188,6 +176,7 @@ struct PostDetailEditorView: View {
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("Cancel", role: .destructive) {
cancelUpload()
dismiss()
}
.tint(.red)
Expand All @@ -204,6 +193,7 @@ struct PostDetailEditorView: View {
}.disabled(isSubmitting || !isReadyToPost)
}
}
.interactiveDismissDisabled(hasPostContent)
.alert("Submit Failed", isPresented: $isShowingErrorDialog) {
Button("OK", role: .cancel) {}
} message: {
Expand All @@ -214,34 +204,7 @@ struct PostDetailEditorView: View {
.photosPicker(isPresented: $showingPhotosPicker, selection: $imageSelection, matching: .images)
.onChange(of: imageSelection) { newValue in
if let selection = newValue {
self.imageModel = .init()
Task {
self.uploadTask = try await pictrsRepository.uploadImage(
imageModel: .init(),
imageSelection: selection,
onUpdate: { newValue in
self.imageModel = newValue
switch newValue.state {
case .uploaded(let file):
if let file = file {
do {
var components = URLComponents()
components.scheme = try apiClient.session.instanceUrl.scheme
components.host = try apiClient.session.instanceUrl.host
components.path = "/pictrs/image/\(file.file)"
postURL = components.url?.absoluteString ?? ""
} catch {
self.imageModel?.state = .failed(nil)
}
} else {

}
default:
postURL = ""
}
}
)
}
uploadImage(imageSelection: selection)
}
}
}
Expand Down

0 comments on commit 9252b67

Please sign in to comment.