-
Notifications
You must be signed in to change notification settings - Fork 296
Posting confirmation Toast #3015
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
634f86e
8f7a5f6
32145c8
8fe1352
9d5e354
decddfb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -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<Content:View>: View { | ||||||
// var style: ToastStyle | ||||||
// var message: String | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can delete unused code. |
||||||
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) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nitpick: There is some inconsistency in the way the code is formatted. The convention is to add a space after the
Suggested change
|
||||||
) | ||||||
.padding() | ||||||
.transition(.opacity.combined(with: .move(edge: .top))) | ||||||
// .animation(.easeInOut(duration: 0.3)) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unused commented-out code. |
||||||
|
||||||
} | ||||||
|
||||||
} | ||||||
|
||||||
// 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<String?>, 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 | ||||||
} | ||||||
} | ||||||
} | ||||||
|
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -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" | ||||||
// } | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unused code |
||||||
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" | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should format this for localization to make sure it can be translated into different languages. This one is a bit trickier than usual, but here is a useful comment from @tyiu from another recent PR that had a similar situation: #3031 (comment) |
||||||
|
||||||
case .initial: | ||||||
postConfirmationToastMessage = "Your note is being posted..." | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can use
Suggested change
This will ensure that our translators can translate this label to other languages. |
||||||
|
||||||
} | ||||||
} | ||||||
} | ||||||
.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") | ||||||
// } | ||||||
// } | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unused code |
||||||
} | ||||||
.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) { | ||||||
|
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -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)") | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we need this debug log? It looks like we might not need it. |
||||||
|
||||||
Task{ | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nitpick/Minor: Please use code formatting conventions for styling consistency. In this case, the convention is generally to use a space between
Suggested change
|
||||||
await relayNotification.add(item: .inProgress(ev, relay_id)) | ||||||
} | ||||||
return prev_count != after_count | ||||||
} | ||||||
|
||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank for adding these documentation strings!
One tip (for next time, there is no need to fix it now), if you add this right on top of the
struct
without an empty line, and use 3 slashes (///
), you can get this documentation to show up on the quick help or automated documentation generators.A good article that talks about this: https://nshipster.com/swift-documentation/#documentation-is-my-new-bicycle
We are not super strict about code documentation though, so no need to worry — this is just for reference!