From a574dcb27c3b80bdd65a369d28aad6e440e2fd76 Mon Sep 17 00:00:00 2001 From: Swift Date: Fri, 17 Feb 2023 15:20:35 -0500 Subject: [PATCH] Add image uploader Changelog-Added: Add image uploader --- damus.xcodeproj/project.pbxproj | 4 + damus/Models/UserSettingsStore.swift | 17 +++ damus/Views/AttachMediaUtility.swift | 200 +++++++++++++++++++++++++ damus/Views/ConfigView.swift | 9 +- damus/Views/PostView.swift | 17 ++- damusTests/ReplyDescriptionTests.swift | 2 + 6 files changed, 246 insertions(+), 3 deletions(-) create mode 100644 damus/Views/AttachMediaUtility.swift diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj index 3f27e4c3..094c4de4 100644 --- a/damus.xcodeproj/project.pbxproj +++ b/damus.xcodeproj/project.pbxproj @@ -238,6 +238,7 @@ 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 */; }; BA693074295D649800ADDB87 /* UserSettingsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA693073295D649800ADDB87 /* UserSettingsStore.swift */; }; BAB68BED29543FA3007BA466 /* SelectWalletView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAB68BEC29543FA3007BA466 /* SelectWalletView.swift */; }; @@ -606,6 +607,7 @@ 7C95CAED299DCEF1009DCB67 /* KFOptionSetter+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "KFOptionSetter+.swift"; sourceTree = ""; }; 7CFF6316299FEFE5005D382A /* SelectableText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectableText.swift; sourceTree = ""; }; 9609F057296E220800069BF3 /* BannerImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BannerImageView.swift; sourceTree = ""; }; + 9CA876E129A00CE90003B9A3 /* AttachMediaUtility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachMediaUtility.swift; sourceTree = ""; }; 9C83F89229A937B900136C08 /* TextViewWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextViewWrapper.swift; sourceTree = ""; }; BA693073295D649800ADDB87 /* UserSettingsStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSettingsStore.swift; sourceTree = ""; }; BAB68BEC29543FA3007BA466 /* SelectWalletView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectWalletView.swift; sourceTree = ""; }; @@ -825,6 +827,7 @@ 4C363A8D28236FE4006E126D /* NoteContentView.swift */, 4C75EFAC28049CFB0006080F /* PostButton.swift */, 4C75EFA327FA577B0006080F /* PostView.swift */, + 9CA876E129A00CE90003B9A3 /* AttachMediaUtility.swift */, 9C83F89229A937B900136C08 /* TextViewWrapper.swift */, 4CEE2AF8280B2EAC00AB5EEF /* PowView.swift */, 4CEE2AF6280B2DEA00AB5EEF /* ProfileName.swift */, @@ -1483,6 +1486,7 @@ 4C54AA0A29A55429003E4487 /* EventGroup.swift in Sources */, 4C3EA67928FF7ABF00C48A62 /* list.c in Sources */, 4C64987E286D082C00EAE2B3 /* DirectMessagesModel.swift in Sources */, + 9CA876E229A00CEA0003B9A3 /* AttachMediaUtility.swift in Sources */, 4CE0E2B629A3ED5500DB4CA2 /* InnerTimelineView.swift in Sources */, 4C363A8828236948006E126D /* BlocksView.swift in Sources */, 4C06670628FCB08600038D2A /* ImageCarousel.swift in Sources */, diff --git a/damus/Models/UserSettingsStore.swift b/damus/Models/UserSettingsStore.swift index eb2b1b3c..618fe04f 100644 --- a/damus/Models/UserSettingsStore.swift +++ b/damus/Models/UserSettingsStore.swift @@ -50,6 +50,15 @@ func get_default_wallet(_ pubkey: String) -> Wallet { } } +func get_image_uploader(_ pubkey: String) -> ImageUploader { + if let defaultImageUploader = UserDefaults.standard.string(forKey: "default_image_uploader"), + let defaultImageUploader = ImageUploader(rawValue: defaultImageUploader) { + return defaultImageUploader + } else { + return .nostrBuild + } +} + private func get_translation_service(_ pubkey: String) -> TranslationService? { guard let translation_service = UserDefaults.standard.string(forKey: "translation_service") else { return nil @@ -88,6 +97,12 @@ class UserSettingsStore: ObservableObject { UserDefaults.standard.set(default_wallet.rawValue, forKey: "default_wallet") } } + + @Published var default_image_uploader: ImageUploader { + didSet { + UserDefaults.standard.set(default_image_uploader.rawValue, forKey: "default_image_uploader") + } + } @Published var show_wallet_selector: Bool { didSet { @@ -190,6 +205,8 @@ class UserSettingsStore: ObservableObject { show_wallet_selector = should_show_wallet_selector(pubkey) always_show_images = UserDefaults.standard.object(forKey: "always_show_images") as? Bool ?? false + default_image_uploader = get_image_uploader(pubkey) + left_handed = UserDefaults.standard.object(forKey: "left_handed") as? Bool ?? false zap_vibration = UserDefaults.standard.object(forKey: "zap_vibration") as? Bool ?? false disable_animation = should_disable_image_animation() diff --git a/damus/Views/AttachMediaUtility.swift b/damus/Views/AttachMediaUtility.swift new file mode 100644 index 00000000..7d0a763a --- /dev/null +++ b/damus/Views/AttachMediaUtility.swift @@ -0,0 +1,200 @@ +// +// AttachMediaUtility.swift +// damus +// +// Created by Swift on 2/17/23. +// + +import SwiftUI + +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 error != nil { + print("error=\(error!)") + return + } + + let responseString = String(data: data!, encoding: String.Encoding(rawValue: String.Encoding.utf8.rawValue)) + print("response data = \(responseString!)") + + let uploadedImageURL = NSMutableAttributedString(string: imageUploader.getImageURL(from: responseString), 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() + } + + func createBodyWithParameters(imageDataKey: NSData, boundary: String, imageUploader: ImageUploader) -> NSData { + let body = NSMutableData(); + let contentType = "image/jpg" + body.appendString(string: "Content-Type: multipart/form-data; boundary=\(boundary)\r\n\r\n") + body.appendString(string: "--\(boundary)\r\n") + body.appendString(string: "Content-Disposition: form-data; name=\(imageUploader.nameParam); filename=\"damus_generic_filename.jpg\"\r\n") + body.appendString(string: "Content-Type: \(contentType)\r\n\r\n") + body.append(imageDataKey as Data) + body.appendString(string: "\r\n") + body.appendString(string: "--\(boundary)--\r\n") + return body + } + + func generateBoundaryString() -> String { + return "Boundary-\(NSUUID().uuidString)" + + } + + struct ImagePicker: UIViewControllerRepresentable { + + @Environment(\.presentationMode) + private var presentationMode + + let sourceType: UIImagePickerController.SourceType + let onImagePicked: (UIImage) -> Void + + final class Coordinator: NSObject, + UINavigationControllerDelegate, + UIImagePickerControllerDelegate { + + @Binding + private var presentationMode: PresentationMode + private let sourceType: UIImagePickerController.SourceType + private let onImagePicked: (UIImage) -> Void + + init(presentationMode: Binding, + sourceType: UIImagePickerController.SourceType, + onImagePicked: @escaping (UIImage) -> Void) { + _presentationMode = presentationMode + self.sourceType = sourceType + self.onImagePicked = onImagePicked + } + + func imagePickerController(_ picker: UIImagePickerController, + didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) { + let uiImage = info[UIImagePickerController.InfoKey.originalImage] as! UIImage + onImagePicked(uiImage) + presentationMode.dismiss() + + } + + func imagePickerControllerDidCancel(_ picker: UIImagePickerController) { + presentationMode.dismiss() + } + + } + + func makeCoordinator() -> Coordinator { + return Coordinator(presentationMode: presentationMode, + sourceType: sourceType, + onImagePicked: onImagePicked) + } + + func makeUIViewController(context: UIViewControllerRepresentableContext) -> UIImagePickerController { + let picker = UIImagePickerController() + picker.sourceType = sourceType + picker.delegate = context.coordinator + return picker + } + + func updateUIViewController(_ uiViewController: UIImagePickerController, + context: UIViewControllerRepresentableContext) { + + } + } +} + +extension NSMutableData { + func appendString(string: String) { + let data = string.data(using: String.Encoding.utf8, allowLossyConversion: true) + append(data!) + } +} + +enum ImageUploader: String, CaseIterable, Identifiable { + var id: String { self.rawValue } + case nostrBuild + case nostrImg + + var nameParam: String { + switch self { + case .nostrBuild: + return "\"fileToUpload\"" + case .nostrImg: + return "\"image\"" + } + } + + var displayImageUploaderName: String { + switch self { + case .nostrBuild: + return "NostrBuild" + case .nostrImg: + return "NostrImg" + } + } + + struct Model: Identifiable, Hashable { + var id: String { self.tag } + var index: Int + var tag: String + var displayName : String + } + + var model: Model { + switch self { + case .nostrBuild: + return .init(index: -1, tag: "nostrBuild", displayName: NSLocalizedString("NostrBuild", comment: "Dropdown option label for system default for NostrBuild image uploader.")) + case .nostrImg: + return .init(index: 0, tag: "nostrImg", displayName: NSLocalizedString("NostrImg", comment: "Dropdown option label for system default for NostrImg image uploader.")) + } + } + + + var postAPI: String { + switch self { + case .nostrBuild: + return "https://nostr.build/upload.php" + case .nostrImg: + return "https://nostrimg.com/api/upload" + } + } + + func getImageURL(from responseString: String?) -> String { + switch self { + case .nostrBuild: + if let startIndex = responseString?.range(of: "nostr.build_")?.lowerBound, + let stringContainingName = responseString?[startIndex..