Video Uploads
Changelog-Added: Add support for video uploads
This commit is contained in:
@@ -12,8 +12,8 @@ import UIKit
|
|||||||
class ImageUploadModel: NSObject, URLSessionTaskDelegate, ObservableObject {
|
class ImageUploadModel: NSObject, URLSessionTaskDelegate, ObservableObject {
|
||||||
@Published var progress: Double? = nil
|
@Published var progress: Double? = nil
|
||||||
|
|
||||||
func start(img: UIImage, uploader: ImageUploader) async -> ImageUploadResult {
|
func start(media: Any, uploader: MediaUploader) async -> ImageUploadResult {
|
||||||
let res = await create_image_upload_request(imageToUpload: img, imageUploader: uploader, progress: self)
|
let res = await create_upload_request(mediaToUpload: media, mediaUploader: uploader, progress: self)
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.progress = nil
|
self.progress = nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,10 +50,10 @@ func get_default_wallet(_ pubkey: String) -> Wallet {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func get_image_uploader(_ pubkey: String) -> ImageUploader {
|
func get_media_uploader(_ pubkey: String) -> MediaUploader {
|
||||||
if let defaultImageUploader = UserDefaults.standard.string(forKey: "default_image_uploader"),
|
if let defaultMediaUploader = UserDefaults.standard.string(forKey: "default_media_uploader"),
|
||||||
let defaultImageUploader = ImageUploader(rawValue: defaultImageUploader) {
|
let defaultMediaUploader = MediaUploader(rawValue: defaultMediaUploader) {
|
||||||
return defaultImageUploader
|
return defaultMediaUploader
|
||||||
} else {
|
} else {
|
||||||
return .nostrBuild
|
return .nostrBuild
|
||||||
}
|
}
|
||||||
@@ -98,9 +98,9 @@ class UserSettingsStore: ObservableObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Published var default_image_uploader: ImageUploader {
|
@Published var default_media_uploader: MediaUploader {
|
||||||
didSet {
|
didSet {
|
||||||
UserDefaults.standard.set(default_image_uploader.rawValue, forKey: "default_image_uploader")
|
UserDefaults.standard.set(default_media_uploader.rawValue, forKey: "default_media_uploader")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,7 +217,7 @@ class UserSettingsStore: ObservableObject {
|
|||||||
show_wallet_selector = should_show_wallet_selector(pubkey)
|
show_wallet_selector = should_show_wallet_selector(pubkey)
|
||||||
always_show_images = UserDefaults.standard.object(forKey: "always_show_images") as? Bool ?? false
|
always_show_images = UserDefaults.standard.object(forKey: "always_show_images") as? Bool ?? false
|
||||||
|
|
||||||
default_image_uploader = get_image_uploader(pubkey)
|
default_media_uploader = get_media_uploader(pubkey)
|
||||||
|
|
||||||
left_handed = UserDefaults.standard.object(forKey: "left_handed") as? Bool ?? false
|
left_handed = UserDefaults.standard.object(forKey: "left_handed") as? Bool ?? false
|
||||||
zap_vibration = UserDefaults.standard.object(forKey: "zap_vibration") as? Bool ?? false
|
zap_vibration = UserDefaults.standard.object(forKey: "zap_vibration") as? Bool ?? false
|
||||||
|
|||||||
@@ -15,23 +15,24 @@ enum ImageUploadResult {
|
|||||||
case failed(Error?)
|
case failed(Error?)
|
||||||
}
|
}
|
||||||
|
|
||||||
fileprivate func create_upload_body(imageDataKey: Data, boundary: String, imageUploader: ImageUploader) -> Data {
|
fileprivate func create_upload_body(mediaData: Data, boundary: String, mediaUploader: MediaUploader, mediaIsImage: Bool) -> Data {
|
||||||
let body = NSMutableData();
|
let body = NSMutableData();
|
||||||
let contentType = "image/jpg"
|
let contentType = mediaIsImage ? "image/jpg" : "video/mp4"
|
||||||
|
let genericFileName = mediaIsImage ? "damus_generic_filename.jpg" : "damus_generic_filename.mp4"
|
||||||
body.appendString(string: "Content-Type: multipart/form-data; boundary=\(boundary)\r\n\r\n")
|
body.appendString(string: "Content-Type: multipart/form-data; boundary=\(boundary)\r\n\r\n")
|
||||||
body.appendString(string: "--\(boundary)\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-Disposition: form-data; name=\(mediaUploader.nameParam); filename=\(genericFileName)\r\n")
|
||||||
body.appendString(string: "Content-Type: \(contentType)\r\n\r\n")
|
body.appendString(string: "Content-Type: \(contentType)\r\n\r\n")
|
||||||
body.append(imageDataKey as Data)
|
body.append(mediaData as Data)
|
||||||
body.appendString(string: "\r\n")
|
body.appendString(string: "\r\n")
|
||||||
body.appendString(string: "--\(boundary)--\r\n")
|
body.appendString(string: "--\(boundary)--\r\n")
|
||||||
return body as Data
|
return body as Data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func create_upload_request(mediaToUpload: Any, mediaUploader: MediaUploader, progress: URLSessionTaskDelegate) async -> ImageUploadResult {
|
||||||
func create_image_upload_request(imageToUpload: UIImage, imageUploader: ImageUploader, progress: URLSessionTaskDelegate) async -> ImageUploadResult {
|
var mediaIsImage: Bool = false
|
||||||
|
var mediaData: Data?
|
||||||
guard let url = URL(string: imageUploader.postAPI) else {
|
guard let url = URL(string: mediaUploader.postAPI) else {
|
||||||
return .failed(nil)
|
return .failed(nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,13 +41,18 @@ func create_image_upload_request(imageToUpload: UIImage, imageUploader: ImageUpl
|
|||||||
let boundary = "Boundary-\(UUID().description)"
|
let boundary = "Boundary-\(UUID().description)"
|
||||||
request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
|
request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
|
||||||
|
|
||||||
// otherwise convert to jpg
|
if let imageToUpload = mediaToUpload as? UIImage {
|
||||||
guard let jpegData = imageToUpload.jpegData(compressionQuality: 0.8) else {
|
mediaData = imageToUpload.jpegData(compressionQuality: 0.8)
|
||||||
// somehow failed, just return original
|
mediaIsImage = true
|
||||||
|
} else if let videoToUpload = mediaToUpload as? URL {
|
||||||
|
mediaData = try? Data(contentsOf: videoToUpload)
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let mediaData = mediaData else {
|
||||||
return .failed(nil)
|
return .failed(nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
request.httpBody = create_upload_body(imageDataKey: jpegData, boundary: boundary, imageUploader: imageUploader)
|
request.httpBody = create_upload_body(mediaData: mediaData, boundary: boundary, mediaUploader: mediaUploader, mediaIsImage: mediaIsImage)
|
||||||
|
|
||||||
do {
|
do {
|
||||||
let (data, _) = try await URLSession.shared.data(for: request, delegate: progress)
|
let (data, _) = try await URLSession.shared.data(for: request, delegate: progress)
|
||||||
@@ -56,8 +62,8 @@ func create_image_upload_request(imageToUpload: UIImage, imageUploader: ImageUpl
|
|||||||
return .failed(nil)
|
return .failed(nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let url = imageUploader.getImageURL(from: responseString) else {
|
guard let url = mediaUploader.getMediaURL(from: responseString, mediaIsImage: mediaIsImage) else {
|
||||||
print("Upload failed getting image url")
|
print("Upload failed getting media url")
|
||||||
return .failed(nil)
|
return .failed(nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,7 +72,6 @@ func create_image_upload_request(imageToUpload: UIImage, imageUploader: ImageUpl
|
|||||||
} catch {
|
} catch {
|
||||||
return .failed(error)
|
return .failed(error)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension PostView {
|
extension PostView {
|
||||||
@@ -76,7 +81,9 @@ extension PostView {
|
|||||||
private var presentationMode
|
private var presentationMode
|
||||||
|
|
||||||
let sourceType: UIImagePickerController.SourceType
|
let sourceType: UIImagePickerController.SourceType
|
||||||
|
let damusState: DamusState
|
||||||
let onImagePicked: (UIImage) -> Void
|
let onImagePicked: (UIImage) -> Void
|
||||||
|
let onVideoPicked: (URL) -> Void
|
||||||
|
|
||||||
final class Coordinator: NSObject,
|
final class Coordinator: NSObject,
|
||||||
UINavigationControllerDelegate,
|
UINavigationControllerDelegate,
|
||||||
@@ -86,19 +93,27 @@ extension PostView {
|
|||||||
private var presentationMode: PresentationMode
|
private var presentationMode: PresentationMode
|
||||||
private let sourceType: UIImagePickerController.SourceType
|
private let sourceType: UIImagePickerController.SourceType
|
||||||
private let onImagePicked: (UIImage) -> Void
|
private let onImagePicked: (UIImage) -> Void
|
||||||
|
private let onVideoPicked: (URL) -> Void
|
||||||
|
|
||||||
init(presentationMode: Binding<PresentationMode>,
|
init(presentationMode: Binding<PresentationMode>,
|
||||||
sourceType: UIImagePickerController.SourceType,
|
sourceType: UIImagePickerController.SourceType,
|
||||||
onImagePicked: @escaping (UIImage) -> Void) {
|
onImagePicked: @escaping (UIImage) -> Void,
|
||||||
|
onVideoPicked: @escaping (URL) -> Void) {
|
||||||
_presentationMode = presentationMode
|
_presentationMode = presentationMode
|
||||||
self.sourceType = sourceType
|
self.sourceType = sourceType
|
||||||
self.onImagePicked = onImagePicked
|
self.onImagePicked = onImagePicked
|
||||||
|
self.onVideoPicked = onVideoPicked
|
||||||
}
|
}
|
||||||
|
|
||||||
func imagePickerController(_ picker: UIImagePickerController,
|
func imagePickerController(_ picker: UIImagePickerController,
|
||||||
didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
|
didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
|
||||||
let uiImage = info[UIImagePickerController.InfoKey.originalImage] as! UIImage
|
if let videoURL = info[UIImagePickerController.InfoKey.mediaURL] as? URL {
|
||||||
onImagePicked(uiImage)
|
// Handle the selected video
|
||||||
|
onVideoPicked(videoURL)
|
||||||
|
} else if let uiImage = info[UIImagePickerController.InfoKey.originalImage] as? UIImage {
|
||||||
|
// Handle the selected image
|
||||||
|
onImagePicked(uiImage)
|
||||||
|
}
|
||||||
presentationMode.dismiss()
|
presentationMode.dismiss()
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -112,12 +127,17 @@ extension PostView {
|
|||||||
func makeCoordinator() -> Coordinator {
|
func makeCoordinator() -> Coordinator {
|
||||||
return Coordinator(presentationMode: presentationMode,
|
return Coordinator(presentationMode: presentationMode,
|
||||||
sourceType: sourceType,
|
sourceType: sourceType,
|
||||||
onImagePicked: onImagePicked)
|
onImagePicked: onImagePicked, onVideoPicked: onVideoPicked)
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeUIViewController(context: UIViewControllerRepresentableContext<ImagePicker>) -> UIImagePickerController {
|
func makeUIViewController(context: UIViewControllerRepresentableContext<ImagePicker>) -> UIImagePickerController {
|
||||||
let picker = UIImagePickerController()
|
let picker = UIImagePickerController()
|
||||||
picker.sourceType = sourceType
|
picker.sourceType = sourceType
|
||||||
|
let mediaUploader = get_media_uploader(damusState.keypair.pubkey)
|
||||||
|
picker.mediaTypes = ["public.image"]
|
||||||
|
if mediaUploader.supportsVideo {
|
||||||
|
picker.mediaTypes.append("public.movie")
|
||||||
|
}
|
||||||
picker.delegate = context.coordinator
|
picker.delegate = context.coordinator
|
||||||
return picker
|
return picker
|
||||||
}
|
}
|
||||||
@@ -138,7 +158,7 @@ extension NSMutableData {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ImageUploader: String, CaseIterable, Identifiable {
|
enum MediaUploader: String, CaseIterable, Identifiable {
|
||||||
var id: String { self.rawValue }
|
var id: String { self.rawValue }
|
||||||
case nostrBuild
|
case nostrBuild
|
||||||
case nostrImg
|
case nostrImg
|
||||||
@@ -152,12 +172,12 @@ enum ImageUploader: String, CaseIterable, Identifiable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var displayImageUploaderName: String {
|
var supportsVideo: Bool {
|
||||||
switch self {
|
switch self {
|
||||||
case .nostrBuild:
|
case .nostrBuild:
|
||||||
return "NostrBuild"
|
return true
|
||||||
case .nostrImg:
|
case .nostrImg:
|
||||||
return "NostrImg"
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,7 +207,7 @@ enum ImageUploader: String, CaseIterable, Identifiable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func getImageURL(from responseString: String) -> String? {
|
func getMediaURL(from responseString: String, mediaIsImage: Bool) -> String? {
|
||||||
switch self {
|
switch self {
|
||||||
case .nostrBuild:
|
case .nostrBuild:
|
||||||
guard let startIndex = responseString.range(of: "nostr.build_")?.lowerBound else {
|
guard let startIndex = responseString.range(of: "nostr.build_")?.lowerBound else {
|
||||||
@@ -199,7 +219,7 @@ enum ImageUploader: String, CaseIterable, Identifiable {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
let nostrBuildImageName = responseString[startIndex..<endIndex]
|
let nostrBuildImageName = responseString[startIndex..<endIndex]
|
||||||
let nostrBuildURL = "https://nostr.build/i/\(nostrBuildImageName)"
|
let nostrBuildURL = mediaIsImage ? "https://nostr.build/i/\(nostrBuildImageName)" : "https://nostr.build/av/\(nostrBuildImageName)"
|
||||||
return nostrBuildURL
|
return nostrBuildURL
|
||||||
|
|
||||||
case .nostrImg:
|
case .nostrImg:
|
||||||
|
|||||||
@@ -228,8 +228,8 @@ struct ConfigView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Picker(NSLocalizedString("Select image uploader", comment: "Prompt selection of user's image uploader"),
|
Picker(NSLocalizedString("Select image uploader", comment: "Prompt selection of user's image uploader"),
|
||||||
selection: $settings.default_image_uploader) {
|
selection: $settings.default_media_uploader) {
|
||||||
ForEach(ImageUploader.allCases, id: \.self) { uploader in
|
ForEach(MediaUploader.allCases, id: \.self) { uploader in
|
||||||
Text(uploader.model.displayName)
|
Text(uploader.model.displayName)
|
||||||
.tag(uploader.model.tag)
|
.tag(uploader.model.tag)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -168,11 +168,11 @@ struct PostView: View {
|
|||||||
post = combinedAttributedString
|
post = combinedAttributedString
|
||||||
}
|
}
|
||||||
|
|
||||||
func handle_upload(image: UIImage) {
|
func handle_upload(media: Any) {
|
||||||
let uploader = get_image_uploader(damus_state.pubkey)
|
let uploader = get_media_uploader(damus_state.pubkey)
|
||||||
|
|
||||||
Task.init {
|
Task.init {
|
||||||
let res = await image_upload.start(img: image, uploader: uploader)
|
let res = await image_upload.start(media: media, uploader: uploader)
|
||||||
|
|
||||||
switch res {
|
switch res {
|
||||||
case .success(let url):
|
case .success(let url):
|
||||||
@@ -215,8 +215,10 @@ struct PostView: View {
|
|||||||
}
|
}
|
||||||
.padding()
|
.padding()
|
||||||
.sheet(isPresented: $attach_media) {
|
.sheet(isPresented: $attach_media) {
|
||||||
ImagePicker(sourceType: .photoLibrary) { img in
|
ImagePicker(sourceType: .photoLibrary, damusState: damus_state) { img in
|
||||||
handle_upload(image: img)
|
handle_upload(media: img)
|
||||||
|
} onVideoPicked: { url in
|
||||||
|
handle_upload(media: url)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onAppear() {
|
.onAppear() {
|
||||||
|
|||||||
Reference in New Issue
Block a user