damus

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

commit d5ecc9bce40cfac30984e2070615f48591799aa5
parent d82b69aac5eb5b78997f2d5381408bad0064ba7d
Author: Swift <scoder1747@gmail.com>
Date:   Sat,  8 Apr 2023 16:48:14 -0400

Preview media uploads when posting

Changelog-Added: Preview media uploads when posting
Closes: #894

Diffstat:
Mdamus/Models/DraftsModel.swift | 1+
Mdamus/Models/ImageUploadModel.swift | 9+++++++++
Mdamus/Views/PostView.swift | 124+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------
3 files changed, 113 insertions(+), 21 deletions(-)

diff --git a/damus/Models/DraftsModel.swift b/damus/Models/DraftsModel.swift @@ -10,4 +10,5 @@ import Foundation class Drafts: ObservableObject { @Published var post: NSMutableAttributedString = NSMutableAttributedString(string: "") @Published var replies: [NostrEvent: NSMutableAttributedString] = [:] + @Published var medias: [UploadedMedia] = [] } diff --git a/damus/Models/ImageUploadModel.swift b/damus/Models/ImageUploadModel.swift @@ -25,6 +25,15 @@ enum MediaUpload { return url.pathExtension } } + + var localURL: URL { + switch self { + case .image(let url): + return url + case .video(let url): + return url + } + } var is_image: Bool { if case .image = self { diff --git a/damus/Views/PostView.swift b/damus/Views/PostView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import AVFoundation enum NostrPostResult { case post(NostrPost) @@ -21,7 +22,7 @@ struct PostView: View { @State var attach_media: Bool = false @State var attach_camera: Bool = false @State var error: String? = nil - + @State var uploadedMedias: [UploadedMedia] = [] @State var originalReferences: [ReferencedId] = [] @State var references: [ReferencedId] = [] @@ -57,7 +58,14 @@ struct PostView: View { } } - let content = self.post.string.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) + + + var content = self.post.string.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) + + let imagesString = uploadedMedias.map { $0.uploadedURL.absoluteString }.joined(separator: " ") + + content.append(" " + imagesString + " ") + let new_post = NostrPost(content: content, references: references, kind: kind) NotificationCenter.default.post(name: .post, object: NostrPostResult.post(new_post)) @@ -66,13 +74,15 @@ struct PostView: View { damus_state.drafts.replies.removeValue(forKey: replying_to) } else { damus_state.drafts.post = NSMutableAttributedString(string: "") + uploadedMedias = [] + damus_state.drafts.medias = [] } dismiss() } var is_post_empty: Bool { - return post.string.allSatisfy { $0.isWhitespace } + return post.string.allSatisfy { $0.isWhitespace } && uploadedMedias.isEmpty } var ImageButton: some View { @@ -168,29 +178,20 @@ struct PostView: View { .padding() } - func append_url(_ url: String) { - let uploadedImageURL = NSMutableAttributedString(string: url) - let combinedAttributedString = NSMutableAttributedString() - combinedAttributedString.append(post) - if !post.string.hasSuffix(" ") { - combinedAttributedString.append(NSAttributedString(string: " ")) - } - combinedAttributedString.append(uploadedImageURL) - - // make sure we have a space at the end - combinedAttributedString.append(NSAttributedString(string: " ")) - post = combinedAttributedString - } - func handle_upload(media: MediaUpload) { let uploader = get_media_uploader(damus_state.pubkey) - Task.init { + let img = getImage(media: media) let res = await image_upload.start(media: media, uploader: uploader) switch res { case .success(let url): - append_url(url) + guard let url = URL(string: url) else { + self.error = "Error uploading image :(" + return + } + let uploadedMedia = UploadedMedia(localURL: media.localURL, uploadedURL: url, representingImage: img) + uploadedMedias.append(uploadedMedia) case .failed(let error): if let error { @@ -206,7 +207,7 @@ struct PostView: View { var body: some View { GeometryReader { (deviceSize: GeometryProxy) in VStack(alignment: .leading, spacing: 0) { - + let searching = get_searching_string(post.string) TopBar @@ -222,8 +223,14 @@ struct PostView: View { TextEntry } - .frame(height: deviceSize.size.height*0.78) + .frame(height: uploadedMedias.isEmpty ? deviceSize.size.height*0.78 : deviceSize.size.height*0.2) .id("post") + + PVImageCarouselView(media: $uploadedMedias, deviceWidth: deviceSize.size.width) + .onChange(of: uploadedMedias) { _ in + damus_state.drafts.medias = uploadedMedias + } + } .padding(.horizontal) } @@ -272,6 +279,7 @@ struct PostView: View { } } else { post = damus_state.drafts.post + uploadedMedias = damus_state.drafts.medias } DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { @@ -283,6 +291,7 @@ struct PostView: View { damus_state.drafts.replies.removeValue(forKey: replying_to) } else if replying_to == nil && damus_state.drafts.post.string.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { damus_state.drafts.post = NSMutableAttributedString(string : "") + damus_state.drafts.medias = uploadedMedias } } .alert(NSLocalizedString("Note contains \"nsec1\" private key. Are you sure?", comment: "Alert user that they might be attempting to paste a private key and ask them to confirm."), isPresented: $showPrivateKeyWarning, actions: { @@ -323,3 +332,76 @@ struct PostView_Previews: PreviewProvider { PostView(replying_to: nil, damus_state: test_damus_state()) } } + +struct PVImageCarouselView: View { + @Binding var media: [UploadedMedia] + + let deviceWidth: CGFloat + + var body: some View { + ScrollView(.horizontal, showsIndicators: false) { + HStack { + ForEach(media.map({$0.representingImage}), id: \.self) { image in + ZStack(alignment: .topTrailing) { + Image(uiImage: image) + .resizable() + .aspectRatio(contentMode: .fill) + .frame(width: media.count == 1 ? deviceWidth*0.8 : 250, height: media.count == 1 ? 400 : 250) + .cornerRadius(10) + .padding() + Image(systemName: "xmark.circle.fill") + .foregroundColor(.white) + .padding(20) + .onTapGesture { + if let index = media.map({$0.representingImage}).firstIndex(of: image) { + media.remove(at: index) + } + } + } + } + } + .padding() + } + } +} + + +fileprivate func getImage(media: MediaUpload) -> UIImage { + var uiimage: UIImage = UIImage() + if media.is_image { + // fetch the image data + if let data = try? Data(contentsOf: media.localURL) { + uiimage = UIImage(data: data) ?? UIImage() + } + } else { + let asset = AVURLAsset(url: media.localURL) + let generator = AVAssetImageGenerator(asset: asset) + generator.appliesPreferredTrackTransform = true + let time = CMTimeMake(value: 1, timescale: 60) // get the thumbnail image at the 1st second + do { + let cgImage = try generator.copyCGImage(at: time, actualTime: nil) + uiimage = UIImage(cgImage: cgImage) + } catch { + print("No thumbnail: \(error)") + } + // create a play icon on the top to differentiate if media upload is image or a video, gif is an image + let playIcon = UIImage(systemName: "play.fill")?.withTintColor(.white, renderingMode: .alwaysOriginal) + let size = uiimage.size + let scale = UIScreen.main.scale + UIGraphicsBeginImageContextWithOptions(size, false, scale) + uiimage.draw(at: .zero) + let playIconSize = CGSize(width: 60, height: 60) + let playIconOrigin = CGPoint(x: (size.width - playIconSize.width) / 2, y: (size.height - playIconSize.height) / 2) + playIcon?.draw(in: CGRect(origin: playIconOrigin, size: playIconSize)) + let newImage = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + uiimage = newImage ?? UIImage() + } + return uiimage +} + +struct UploadedMedia: Equatable { + let localURL: URL + let uploadedURL: URL + let representingImage: UIImage +}