damus

nostr ios client
git clone git://jb55.com/damus
Log | Files | Refs | README | LICENSE

commit 7b1f4b770160b5b8aa0a0834adfd37d65543fdee
parent 7b6d3ef9dfb471e9030aa9e7792b4636127f8997
Author: William Casarin <jb55@jb55.com>
Date:   Thu, 16 Mar 2023 09:13:03 -0600

Show image upload progress

Diffstat:
Mdamus.xcodeproj/project.pbxproj | 8++++++--
Adamus/Models/ImageUploadModel.swift | 28++++++++++++++++++++++++++++
Mdamus/Views/AttachMediaUtility.swift | 93+++++++++++++++++++++++++++++++++++++++++--------------------------------------
Mdamus/Views/PostView.swift | 65++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------
4 files changed, 136 insertions(+), 58 deletions(-)

diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj @@ -175,6 +175,7 @@ 4CCEB7A929B29DD50078AA28 /* SearchResultsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CCEB7A829B29DD50078AA28 /* SearchResultsModel.swift */; }; 4CCEB7AE29B53D260078AA28 /* SearchingEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CCEB7AD29B53D260078AA28 /* SearchingEventView.swift */; }; 4CCEB7B029B5415A0078AA28 /* SearchingProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CCEB7AF29B5415A0078AA28 /* SearchingProfileView.swift */; }; + 4CD348EF29C3659D00497EB2 /* ImageUploadModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CD348EE29C3659D00497EB2 /* ImageUploadModel.swift */; }; 4CD7641B28A1641400B6928F /* EndBlock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CD7641A28A1641400B6928F /* EndBlock.swift */; }; 4CE0E2AF29A2E82100DB4CA2 /* EventHolder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE0E2AE29A2E82100DB4CA2 /* EventHolder.swift */; }; 4CE0E2B229A3DF6900DB4CA2 /* LoadMoreButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE0E2B129A3DF6900DB4CA2 /* LoadMoreButton.swift */; }; @@ -238,8 +239,8 @@ 7C95CAEE299DCEF1009DCB67 /* KFOptionSetter+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C95CAED299DCEF1009DCB67 /* KFOptionSetter+.swift */; }; 7CFF6317299FEFE5005D382A /* SelectableText.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CFF6316299FEFE5005D382A /* SelectableText.swift */; }; 9609F058296E220800069BF3 /* BannerImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9609F057296E220800069BF3 /* BannerImageView.swift */; }; - 9CA876E229A00CEA0003B9A3 /* AttachMediaUtility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CA876E129A00CE90003B9A3 /* AttachMediaUtility.swift */; }; 9C83F89329A937B900136C08 /* TextViewWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C83F89229A937B900136C08 /* TextViewWrapper.swift */; }; + 9CA876E229A00CEA0003B9A3 /* AttachMediaUtility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CA876E129A00CE90003B9A3 /* AttachMediaUtility.swift */; }; BA693074295D649800ADDB87 /* UserSettingsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA693073295D649800ADDB87 /* UserSettingsStore.swift */; }; BAB68BED29543FA3007BA466 /* SelectWalletView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAB68BEC29543FA3007BA466 /* SelectWalletView.swift */; }; DD597CBD2963D85A00C64D32 /* MarkdownTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD597CBC2963D85A00C64D32 /* MarkdownTests.swift */; }; @@ -542,6 +543,7 @@ 4CCEB7A829B29DD50078AA28 /* SearchResultsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultsModel.swift; sourceTree = "<group>"; }; 4CCEB7AD29B53D260078AA28 /* SearchingEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchingEventView.swift; sourceTree = "<group>"; }; 4CCEB7AF29B5415A0078AA28 /* SearchingProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchingProfileView.swift; sourceTree = "<group>"; }; + 4CD348EE29C3659D00497EB2 /* ImageUploadModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageUploadModel.swift; sourceTree = "<group>"; }; 4CD7641A28A1641400B6928F /* EndBlock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EndBlock.swift; sourceTree = "<group>"; }; 4CE0E2AE29A2E82100DB4CA2 /* EventHolder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventHolder.swift; sourceTree = "<group>"; }; 4CE0E2B129A3DF6900DB4CA2 /* LoadMoreButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadMoreButton.swift; sourceTree = "<group>"; }; @@ -607,8 +609,8 @@ 7C95CAED299DCEF1009DCB67 /* KFOptionSetter+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "KFOptionSetter+.swift"; sourceTree = "<group>"; }; 7CFF6316299FEFE5005D382A /* SelectableText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectableText.swift; sourceTree = "<group>"; }; 9609F057296E220800069BF3 /* BannerImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BannerImageView.swift; sourceTree = "<group>"; }; - 9CA876E129A00CE90003B9A3 /* AttachMediaUtility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachMediaUtility.swift; sourceTree = "<group>"; }; 9C83F89229A937B900136C08 /* TextViewWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextViewWrapper.swift; sourceTree = "<group>"; }; + 9CA876E129A00CE90003B9A3 /* AttachMediaUtility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachMediaUtility.swift; sourceTree = "<group>"; }; BA693073295D649800ADDB87 /* UserSettingsStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSettingsStore.swift; sourceTree = "<group>"; }; BAB68BEC29543FA3007BA466 /* SelectWalletView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectWalletView.swift; sourceTree = "<group>"; }; DD597CBC2963D85A00C64D32 /* MarkdownTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarkdownTests.swift; sourceTree = "<group>"; }; @@ -766,6 +768,7 @@ 4CE8795A2996C47A00F758CC /* ZapsModel.swift */, 3AA59D1C2999B0400061C48E /* DraftsModel.swift */, 4C54AA0629A540BA003E4487 /* NotificationsModel.swift */, + 4CD348EE29C3659D00497EB2 /* ImageUploadModel.swift */, ); path = Models; sourceTree = "<group>"; @@ -1434,6 +1437,7 @@ 4CB8838B296F6E1E00DC99E7 /* NIP05Badge.swift in Sources */, 4C3EA66828FF5F9900C48A62 /* hex.c in Sources */, E9E4ED0B295867B900DD7078 /* ThreadView.swift in Sources */, + 4CD348EF29C3659D00497EB2 /* ImageUploadModel.swift in Sources */, 4C3BEFDC281DCE6100B3DE84 /* Liked.swift in Sources */, 4CF0ABE7298444FD00D66079 /* MutedEventView.swift in Sources */, 9C83F89329A937B900136C08 /* TextViewWrapper.swift in Sources */, diff --git a/damus/Models/ImageUploadModel.swift b/damus/Models/ImageUploadModel.swift @@ -0,0 +1,28 @@ +// +// ImageUploadModel.swift +// damus +// +// Created by William Casarin on 2023-03-16. +// + +import Foundation +import UIKit + + +class ImageUploadModel: NSObject, URLSessionTaskDelegate, ObservableObject { + @Published var progress: Double? = nil + + func start(img: UIImage, uploader: ImageUploader) async -> ImageUploadResult { + return await create_image_upload_request(imageToUpload: img, imageUploader: uploader, progress: self) + } + + func urlSession(_ session: URLSession, task: URLSessionTask, didSendBodyData bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64) { + DispatchQueue.main.async { + self.progress = Double(totalBytesSent) / Double(totalBytesExpectedToSend) + + if self.progress! >= 1.0 { + self.progress = nil + } + } + } +} diff --git a/damus/Views/AttachMediaUtility.swift b/damus/Views/AttachMediaUtility.swift @@ -6,50 +6,16 @@ // import SwiftUI +import UIKit +import CoreGraphics +import UniformTypeIdentifiers -extension PostView { - func myImageUploadRequest(imageToUpload: UIImage, imageUploader: ImageUploader) { - let myUrl = NSURL(string: imageUploader.postAPI); - let request = NSMutableURLRequest(url:myUrl! as URL); - request.httpMethod = "POST"; - let boundary = generateBoundaryString() - request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type") - let imageData = imageToUpload.jpegData(compressionQuality: 1) - if imageData == nil { - return - } - request.httpBody = createBodyWithParameters(imageDataKey: imageData! as NSData, boundary: boundary, imageUploader: imageUploader) as Data - - let task = URLSession.shared.dataTask(with: request as URLRequest) { - data, response, error in - if let error { - print("error=\(error)") - return - } - - guard let data else { - return - } - - guard let responseString = String(data: data, encoding: String.Encoding(rawValue: String.Encoding.utf8.rawValue)) else { - return - } - print("response data = \(responseString)") - - guard let url = imageUploader.getImageURL(from: responseString) else { - return - } - - let uploadedImageURL = NSMutableAttributedString(string: url, attributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 18.0), NSAttributedString.Key.foregroundColor: UIColor.label]) - let combinedAttributedString = NSMutableAttributedString() - combinedAttributedString.append(post) - combinedAttributedString.append(uploadedImageURL) - post = combinedAttributedString - } - task.resume() - } +enum ImageUploadResult { + case success(String) + case failed(Error?) +} - func createBodyWithParameters(imageDataKey: NSData, boundary: String, imageUploader: ImageUploader) -> NSData { +fileprivate func create_upload_body(imageDataKey: Data, boundary: String, imageUploader: ImageUploader) -> Data { let body = NSMutableData(); let contentType = "image/jpg" body.appendString(string: "Content-Type: multipart/form-data; boundary=\(boundary)\r\n\r\n") @@ -59,14 +25,51 @@ extension PostView { body.append(imageDataKey as Data) body.appendString(string: "\r\n") body.appendString(string: "--\(boundary)--\r\n") - return body + return body as Data } - func generateBoundaryString() -> String { - return "Boundary-\(NSUUID().uuidString)" +func create_image_upload_request(imageToUpload: UIImage, imageUploader: ImageUploader, progress: URLSessionTaskDelegate) async -> ImageUploadResult { + + guard let url = URL(string: imageUploader.postAPI) else { + return .failed(nil) + } + + var request = URLRequest(url: url) + request.httpMethod = "POST"; + let boundary = "Boundary-\(UUID().description)" + request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type") + + // otherwise convert to jpg + guard let jpegData = imageToUpload.jpegData(compressionQuality: 0.8) else { + // somehow failed, just return original + return .failed(nil) } + + request.httpBody = create_upload_body(imageDataKey: jpegData, boundary: boundary, imageUploader: imageUploader) + + do { + let (data, _) = try await URLSession.shared.data(for: request, delegate: progress) + + guard let responseString = String(data: data, encoding: String.Encoding(rawValue: String.Encoding.utf8.rawValue)) else { + print("Upload failed getting response string") + return .failed(nil) + } + + guard let url = imageUploader.getImageURL(from: responseString) else { + print("Upload failed getting image url") + return .failed(nil) + } + + return .success(url) + + } catch { + return .failed(error) + } + +} +extension PostView { struct ImagePicker: UIViewControllerRepresentable { @Environment(\.presentationMode) diff --git a/damus/Views/PostView.swift b/damus/Views/PostView.swift @@ -19,6 +19,9 @@ struct PostView: View { @FocusState var focus: Bool @State var showPrivateKeyWarning: Bool = false @State var attach_media: Bool = false + @State var error: String? = nil + + @StateObject var image_upload: ImageUploadModel = ImageUploadModel() let replying_to: NostrEvent? let references: [ReferencedId] @@ -80,6 +83,7 @@ struct PostView: View { var AttachmentBar: some View { HStack(alignment: .center) { ImageButton + .disabled(image_upload.progress != nil) } } @@ -122,22 +126,62 @@ struct PostView: View { } var TopBar: some View { - HStack { - Button(NSLocalizedString("Cancel", comment: "Button to cancel out of posting a note.")) { - self.cancel() - } - .foregroundColor(.primary) + VStack { + HStack(spacing: 5.0) { + Button(NSLocalizedString("Cancel", comment: "Button to cancel out of posting a note.")) { + self.cancel() + } + .foregroundColor(.primary) + + if let error { + Text(error) + .foregroundColor(.red) + } - Spacer() + Spacer() - if !is_post_empty { - PostButton + if !is_post_empty { + PostButton + } + } + + if let progress = image_upload.progress { + ProgressView(value: progress, total: 1.0) + .progressViewStyle(.linear) } } .frame(height: 30) .padding([.top, .bottom], 4) } + func handle_upload(image: UIImage) { + let uploader = get_image_uploader(damus_state.pubkey) + + Task.init { + let res = await image_upload.start(img: image, uploader: uploader) + + switch res { + case .success(let url): + let uploadedImageURL = NSMutableAttributedString(string: url, attributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 18.0), NSAttributedString.Key.foregroundColor: UIColor.label]) + let combinedAttributedString = NSMutableAttributedString() + combinedAttributedString.append(post) + if !post.string.hasSuffix(" ") { + combinedAttributedString.append(NSAttributedString(string: " ")) + } + combinedAttributedString.append(uploadedImageURL) + post = combinedAttributedString + + case .failed(let error): + if let error { + self.error = error.localizedDescription + } else { + self.error = "Error uploading image :(" + } + } + + } + } + var body: some View { VStack(alignment: .leading) { TopBar @@ -162,9 +206,8 @@ struct PostView: View { AttachmentBar } .sheet(isPresented: $attach_media) { - ImagePicker(sourceType: .photoLibrary) { image in - let imageUploader = get_image_uploader(damus_state.pubkey) - myImageUploadRequest(imageToUpload: image, imageUploader: imageUploader) + ImagePicker(sourceType: .photoLibrary) { img in + handle_upload(image: img) } } .onAppear() {