Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions damus.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -2448,6 +2451,7 @@
BA3759962ABCCF360018D73B /* CameraPreview.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CameraPreview.swift; sourceTree = "<group>"; };
BA693073295D649800ADDB87 /* UserSettingsStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSettingsStore.swift; sourceTree = "<group>"; };
BAB68BEC29543FA3007BA466 /* SelectWalletView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectWalletView.swift; sourceTree = "<group>"; };
C6391DB92DA542D100F5B388 /* ToastView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastView.swift; sourceTree = "<group>"; };
D2277EE92A089BD5006C3807 /* Router.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Router.swift; sourceTree = "<group>"; };
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; };
Expand Down Expand Up @@ -3608,6 +3612,7 @@
4C28595F2A12A2BE004746F7 /* SupporterBadge.swift */,
5C6E1DAC2A193EC2008FC15A /* GradientButtonStyle.swift */,
5CC868DC2AA29B3200FB22BA /* NeutralButtonStyle.swift */,
C6391DB92DA542D100F5B388 /* ToastView.swift */,
);
path = Components;
sourceTree = "<group>";
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -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;
};
Expand Down
111 changes: 111 additions & 0 deletions damus/Components/ToastView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
//
// ToastView.swift
// damus
//
// Created by Sanjay Siddharth on 08/04/25.
//

import SwiftUI

struct ToastView: View {
var style: ToastStyle
var message: String

var body: some View {
HStack{
Image(systemName: style.iconName)
.foregroundStyle(style.color)
Text(message)
.font(.caption)
.fontWeight(.semibold)
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can make the base ToastView more generic and allow other usages to make toasts with custom layouts, by using ViewBuilders:

.padding()
.background(
RoundedRectangle(cornerRadius: 24)
.stroke(.purple, lineWidth: 0.5)
.background(
RoundedRectangle(cornerRadius: 24)
.fill(.white)
)
.shadow(color: .purple.opacity(0.35), radius: 6)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can stick to more neutral colours for the toast itself (borders, shadows, and backgrounds) to better match the iOS look?

Two nice examples from iOS would be:

  • The "AirPods connected" notification (Example image)
  • The focus mode switch notification
    IMG_0E3E02361CB1-1

cc @robagreda, any thoughts?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the contentView is mostly white, I feel that the glass background effect will not look nice. Any thoughts ?

Simulator Screenshot - iPhone 16 - 2025-05-14 at 22 28 56

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one is a bit lighter

Simulator Screenshot - iPhone 16 - 2025-05-14 at 22 34 06

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@SanjaySiddharth, that is a good point! Perhaps we can try:

  1. Adding a subtle shadow (semi-transparent black, on the bottom of the notification)
  2. Adding a thin border (semi-transparent "adaptable black")

So it would look roughly like this:
Screenshot 2025-05-15 at 15 31 12
Screenshot 2025-05-15 at 15 36 01

What do you think?

)
.padding()
.transition(.opacity.combined(with: .move(edge: .top)))
.animation(.easeInOut(duration: 0.3), value: message)
}
}

#Preview {
ToastView(style: .success, message: "Your note has been posted to 10/14 relays")
ToastView(style: .error, message: "Could not post your note")

}

struct ToastModifier: ViewModifier {
@Binding var message: String?
@State var timer: Timer?
let style: ToastStyle

func body(content: Content) -> some View {
ZStack(alignment: .top){
content

if let message = message {
ToastView(style: style, message: message)
.padding(.top, 50)
.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
}
}
}

struct Toast: Equatable {
var style: ToastStyle
var message: String
var Duration: Double = 3
var width: Double = .infinity
}

enum ToastStyle{
case success
case error

}

extension ToastStyle{
var iconName: String {
switch self {
case .error:
return "xmark.circle.fill"
case .success:
return "checkmark"
}
}
var color: Color {
switch self {
case .error:
return Color.red
case .success:
return Color.green
}

}
}

extension View{
func postConfirmationToast(message: Binding<String?>, style: ToastStyle) -> some View {
self.modifier(ToastModifier(message: message, style: style))
}
}
27 changes: 26 additions & 1 deletion damus/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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"
// }
Copy link
Collaborator

Choose a reason for hiding this comment

The 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"
Copy link
Collaborator

Choose a reason for hiding this comment

The 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..."
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can use NSLocalizedString here:

Suggested change
postConfirmationToastMessage = "Your note is being posted..."
postConfirmationToastMessage = NSLocalizedString("Your note is being posted...", comment: "A label on a toast that indicates the user's post is being posted")

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)
Expand Down Expand Up @@ -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")
// }
// }
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused code

}
.onReceive(handle_notify(.new_mutes)) { _ in
home.filter_events()
Expand Down Expand Up @@ -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) {
Expand Down
15 changes: 14 additions & 1 deletion damus/Util/PostBox.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -44,6 +44,7 @@ class PostedEvent {
self.remaining = remaining.map {
Relayer(relay: $0, attempts: 0, retry_after: 10.0)
}
self.totalRelays = remaining.count
}
}

Expand All @@ -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]
Expand Down Expand Up @@ -137,6 +145,11 @@ class PostBox {
if ev.remaining.count == 0 {
self.events.removeValue(forKey: event_id)
}
print("Toatl Relays : \(ev.totalRelays)")
Copy link
Collaborator

@danieldaquino danieldaquino Jun 17, 2025

Choose a reason for hiding this comment

The 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{
Copy link
Collaborator

@danieldaquino danieldaquino Jun 17, 2025

Choose a reason for hiding this comment

The 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 Task and {

Suggested change
Task{
Task {

await relayNotification.add(item: .inProgress(ev, relay_id))
}
return prev_count != after_count
}

Expand Down