diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj index da9c1b684..c207d6bd0 100644 --- a/damus.xcodeproj/project.pbxproj +++ b/damus.xcodeproj/project.pbxproj @@ -939,6 +939,9 @@ BA3759972ABCCF360018D73B /* CameraPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA3759962ABCCF360018D73B /* CameraPreview.swift */; }; BA693074295D649800ADDB87 /* UserSettingsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA693073295D649800ADDB87 /* UserSettingsStore.swift */; }; BAB68BED29543FA3007BA466 /* SelectWalletView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAB68BEC29543FA3007BA466 /* SelectWalletView.swift */; }; + C6391DBA2DA542D100F5B388 /* ToastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6391DB92DA542D100F5B388 /* ToastView.swift */; }; + C6391DBB2DA542D100F5B388 /* ToastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6391DB92DA542D100F5B388 /* ToastView.swift */; }; + C6391DBC2DA542D100F5B388 /* ToastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6391DB92DA542D100F5B388 /* ToastView.swift */; }; D2277EEA2A089BD5006C3807 /* Router.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2277EE92A089BD5006C3807 /* Router.swift */; }; D703D7192C66E47100A400EA /* UniformTypeIdentifiers.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D703D7182C66E47100A400EA /* UniformTypeIdentifiers.framework */; }; D703D71C2C66E47100A400EA /* Media.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D703D71B2C66E47100A400EA /* Media.xcassets */; }; @@ -2448,6 +2451,7 @@ BA3759962ABCCF360018D73B /* CameraPreview.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CameraPreview.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 = ""; }; + C6391DB92DA542D100F5B388 /* ToastView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastView.swift; sourceTree = ""; }; D2277EE92A089BD5006C3807 /* Router.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Router.swift; sourceTree = ""; }; D703D7172C66E47100A400EA /* HighlighterActionExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = HighlighterActionExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; D703D7182C66E47100A400EA /* UniformTypeIdentifiers.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UniformTypeIdentifiers.framework; path = System/Library/Frameworks/UniformTypeIdentifiers.framework; sourceTree = SDKROOT; }; @@ -3608,6 +3612,7 @@ 4C28595F2A12A2BE004746F7 /* SupporterBadge.swift */, 5C6E1DAC2A193EC2008FC15A /* GradientButtonStyle.swift */, 5CC868DC2AA29B3200FB22BA /* NeutralButtonStyle.swift */, + C6391DB92DA542D100F5B388 /* ToastView.swift */, ); path = Components; sourceTree = ""; @@ -4579,6 +4584,7 @@ BA3759922ABCCEBA0018D73B /* CameraService+Extensions.swift in Sources */, D74F430C2B23FB9B00425B75 /* StoreObserver.swift in Sources */, 4C363A9A28283854006E126D /* Reply.swift in Sources */, + C6391DBC2DA542D100F5B388 /* ToastView.swift in Sources */, BA693074295D649800ADDB87 /* UserSettingsStore.swift in Sources */, D7ADD3E02B538D4200F104C4 /* DamusPurpleURLSheetView.swift in Sources */, 4CFF8F6729CC9E3A008DB934 /* FullScreenCarouselView.swift in Sources */, @@ -5205,6 +5211,7 @@ 82D6FB6C2CD99F7900C925F4 /* DamusPurpleURL.swift in Sources */, 82D6FB6D2CD99F7900C925F4 /* DamusPurpleEnvironment.swift in Sources */, 82D6FB6E2CD99F7900C925F4 /* PurpleStoreKitManager.swift in Sources */, + C6391DBB2DA542D100F5B388 /* ToastView.swift in Sources */, 82D6FB6F2CD99F7900C925F4 /* CameraService+Extensions.swift in Sources */, 82D6FB702CD99F7900C925F4 /* ImageResizer.swift in Sources */, 82D6FB712CD99F7900C925F4 /* PhotoCaptureProcessor.swift in Sources */, @@ -5973,6 +5980,7 @@ D703D75B2C670A7F00A400EA /* Contacts.swift in Sources */, D703D7812C670C2B00A400EA /* Bech32.swift in Sources */, D73E5E1E2C6A9694007EB227 /* RelayFilters.swift in Sources */, + C6391DBA2DA542D100F5B388 /* ToastView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/damus/Components/ToastView.swift b/damus/Components/ToastView.swift new file mode 100644 index 000000000..cd5654de0 --- /dev/null +++ b/damus/Components/ToastView.swift @@ -0,0 +1,151 @@ +// +// ToastView.swift +// damus +// +// Created by Sanjay Siddharth on 08/04/25. +// + +import SwiftUI + +// Generic Toast View UI using which we build other custom Toasts + +struct GenericToastView: View { +// var style: ToastStyle +// var message: String + let content: Content + + init(@ViewBuilder content: () -> Content) { + self.content = content() + } + + var body: some View { + content + .padding() + .background( + RoundedRectangle(cornerRadius: 24) + .fill(.ultraThinMaterial) + .background(.white.opacity(0.05)) + .overlay( + RoundedRectangle(cornerRadius: 24) + .stroke(Color.damusAdaptableBlack.opacity(0.2),lineWidth: 1) + ) + .clipShape(RoundedRectangle(cornerRadius: 24)) + .shadow(radius: 5.0, x:0 , y: 5) + ) + .padding() + .transition(.opacity.combined(with: .move(edge: .top))) +// .animation(.easeInOut(duration: 0.3)) + + } + +} + +// Example implementation of a toast which shows a user on how many relays a post has been sucessfully posted to . + +struct PostConfirmationToastView: View { + var message : String + let style: ToastStyle + var body: some View { + GenericToastView{ + HStack{ + if let iconName = style.iconName{ + Image(systemName: iconName) + .foregroundStyle(style.color) + } + Text(message) + .font(.caption) + .fontWeight(.semibold) + } + } + } +} +// Current implementation uses separate ViewModifiers for each type of Toast. +// FUTURE WORK: Can be improved to use one ViewModifier for all kinds of toasts . + +struct PostConfirmationToastModifier: ViewModifier { + @Binding var message: String? + @State var timer: Timer? + let style: ToastStyle + @State private var offset = CGSize.zero + + func body(content: Content) -> some View { + ZStack(alignment: .top){ + + content + + if let message = message { + PostConfirmationToastView(message: message, style: style) + .padding(.top, 50) + .offset(x:offset.width) + .gesture( + DragGesture() + .onChanged{gesture in + offset = gesture.translation + } + .onEnded{_ in + if abs(offset.width)>100 { + withAnimation{ + self.message=nil + offset = CGSize.zero + } + } + else{ + offset = CGSize.zero + } + + } + ) + .animation(.easeInOut, value: message) + .onChange(of: message){ _ in + restartTimer() + } + .onAppear{ + restartTimer() + } + } + } + } + private func restartTimer(){ + timer?.invalidate() + timer = Timer.scheduledTimer(withTimeInterval: 3.0, repeats: false) { _ in + message = nil + } + } +} + +extension View{ + func postConfirmationToast(message: Binding, style: ToastStyle) -> some View { + self.modifier(PostConfirmationToastModifier(message: message, style: style)) + } +} + +enum ToastStyle{ + case success + case error + case initial + +} + +extension ToastStyle{ + var iconName: String? { + switch self { + case .error: + return "xmark.circle.fill" + case .success: + return "checkmark" + case .initial: + return nil + } + } + var color: Color { + switch self { + case .error: + return Color.red + case .success: + return Color.green + case .initial: + return Color.black + } + } +} + diff --git a/damus/ContentView.swift b/damus/ContentView.swift index d5e0d69d5..14b730c9b 100644 --- a/damus/ContentView.swift +++ b/damus/ContentView.swift @@ -134,6 +134,7 @@ struct ContentView: View { @StateObject var navigationCoordinator: NavigationCoordinator = NavigationCoordinator() @AppStorage("has_seen_suggested_users") private var hasSeenOnboardingSuggestions = false let sub_id = UUID().description + @State var postConfirmationToastMessage: String? // connect retry timer let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect() @@ -297,6 +298,22 @@ struct ContentView: View { } .ignoresSafeArea(.keyboard) .edgesIgnoringSafeArea(hide_bar ? [.bottom] : []) + .postConfirmationToast(message: $postConfirmationToastMessage, style: .success) + .task { + // for await (event, relayID) in relayNotification.stream { + // postConfirmationToastMessage = "Your note has been posted to \(event.totalRelays-event.remaining.count) out of \(event.totalRelays) relays" + // } + for await notification in relayNotification.stream { + switch notification { + case .inProgress(let event, let relayID): + postConfirmationToastMessage = "Your note has been posted to \(event.totalRelays-event.remaining.count) out of \(event.totalRelays) relays" + + case .initial: + postConfirmationToastMessage = "Your note is being posted..." + + } + } + } .onAppear() { self.connect() try? AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category.playback, mode: .default, options: .mixWithOthers) @@ -421,6 +438,11 @@ struct ContentView: View { if !handle_post_notification(keypair: keypair, postbox: state.postbox, events: state.events, post: post) { self.active_sheet = nil } +// Task { +// for await (event, relayID) in relayNotification.stream { +// print("Rithi Event: Posted to \(event.totalRelays-event.remaining.count) out of \(event.totalRelays) relays") +// } +// } } .onReceive(handle_notify(.new_mutes)) { _ in home.filter_events() @@ -1181,7 +1203,10 @@ func handle_post_notification(keypair: FullKeypair, postbox: PostBox, events: Ev guard let new_ev = post.to_event(keypair: keypair) else { return false } - postbox.send(new_ev) + Task{ + await relayNotification.add(item: .initial) + postbox.send(new_ev) + } for eref in new_ev.referenced_ids.prefix(3) { // also broadcast at most 3 referenced events if let ev = events.lookup(eref) { diff --git a/damus/Util/PostBox.swift b/damus/Util/PostBox.swift index 6ae7304ea..db3bbcbb2 100644 --- a/damus/Util/PostBox.swift +++ b/damus/Util/PostBox.swift @@ -34,7 +34,7 @@ class PostedEvent { let flush_after: Date? var flushed_once: Bool let on_flush: OnFlush? - + let totalRelays: Int init(event: NostrEvent, remaining: [RelayURL], skip_ephemeral: Bool, flush_after: Date?, on_flush: OnFlush?) { self.event = event self.skip_ephemeral = skip_ephemeral @@ -44,6 +44,7 @@ class PostedEvent { self.remaining = remaining.map { Relayer(relay: $0, attempts: 0, retry_after: 10.0) } + self.totalRelays = remaining.count } } @@ -53,6 +54,13 @@ enum CancelSendErr { case too_late } +enum RelayNotification { + case initial + case inProgress(PostedEvent,RelayURL) +} + +let relayNotification = QueueableNotify<(RelayNotification)>(maxQueueItems: 100) + class PostBox { let pool: RelayPool var events: [NoteId: PostedEvent] @@ -137,6 +145,11 @@ class PostBox { if ev.remaining.count == 0 { self.events.removeValue(forKey: event_id) } + print("Toatl Relays : \(ev.totalRelays)") + + Task{ + await relayNotification.add(item: .inProgress(ev, relay_id)) + } return prev_count != after_count }