Move most of RelayPool away from the Main Thread

This is a large refactor that aims to improve performance by offloading
RelayPool computations into a separate actor outside the main thread.

This should reduce congestion on the main thread and thus improve UI
performance.

Also, the internal subscription callback mechanism was changed to use
AsyncStreams to prevent race conditions newly found in that area of the
code.

Changelog-Fixed: Added performance improvements to timeline scrolling
Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
This commit is contained in:
Daniel D’Aquino
2025-10-10 14:12:30 -07:00
parent 7c1594107f
commit 991a4a86e6
50 changed files with 602 additions and 451 deletions
@@ -55,7 +55,6 @@
buildConfiguration = "Debug" buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
enableAddressSanitizer = "YES"
launchStyle = "0" launchStyle = "0"
useCustomWorkingDirectory = "NO" useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO" ignoresPersistentStateOnLaunch = "NO"
+40 -28
View File
@@ -300,7 +300,8 @@ struct ContentView: View {
.ignoresSafeArea(.keyboard) .ignoresSafeArea(.keyboard)
.edgesIgnoringSafeArea(hide_bar ? [.bottom] : []) .edgesIgnoringSafeArea(hide_bar ? [.bottom] : [])
.onAppear() { .onAppear() {
self.connect() Task {
await self.connect()
try? AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category.playback, mode: .default, options: .mixWithOthers) try? AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category.playback, mode: .default, options: .mixWithOthers)
setup_notifications() setup_notifications()
if !hasSeenOnboardingSuggestions || damus_state!.settings.always_show_onboarding_suggestions { if !hasSeenOnboardingSuggestions || damus_state!.settings.always_show_onboarding_suggestions {
@@ -312,6 +313,7 @@ struct ContentView: View {
await self.listenAndHandleLocalNotifications() await self.listenAndHandleLocalNotifications()
} }
} }
}
.sheet(item: $active_sheet) { item in .sheet(item: $active_sheet) { item in
switch item { switch item {
case .report(let target): case .report(let target):
@@ -371,7 +373,7 @@ struct ContentView: View {
self.hide_bar = !show self.hide_bar = !show
} }
.onReceive(timer) { n in .onReceive(timer) { n in
self.damus_state?.nostrNetwork.postbox.try_flushing_events() Task{ await self.damus_state?.nostrNetwork.postbox.try_flushing_events() }
self.damus_state!.profiles.profile_data(self.damus_state!.pubkey).status.try_expire() self.damus_state!.profiles.profile_data(self.damus_state!.pubkey).status.try_expire()
} }
.onReceive(handle_notify(.report)) { target in .onReceive(handle_notify(.report)) { target in
@@ -382,7 +384,8 @@ struct ContentView: View {
self.confirm_mute = true self.confirm_mute = true
} }
.onReceive(handle_notify(.attached_wallet)) { nwc in .onReceive(handle_notify(.attached_wallet)) { nwc in
try? damus_state.nostrNetwork.userRelayList.load() // Reload relay list to apply changes Task {
try? await damus_state.nostrNetwork.userRelayList.load() // Reload relay list to apply changes
// update the lightning address on our profile when we attach a // update the lightning address on our profile when we attach a
// wallet with an associated // wallet with an associated
@@ -404,23 +407,24 @@ struct ContentView: View {
let prof = Profile(name: profile.name, display_name: profile.display_name, about: profile.about, picture: profile.picture, banner: profile.banner, website: profile.website, lud06: profile.lud06, lud16: lud16, nip05: profile.nip05, damus_donation: profile.damus_donation, reactions: profile.reactions) let prof = Profile(name: profile.name, display_name: profile.display_name, about: profile.about, picture: profile.picture, banner: profile.banner, website: profile.website, lud06: profile.lud06, lud16: lud16, nip05: profile.nip05, damus_donation: profile.damus_donation, reactions: profile.reactions)
guard let ev = make_metadata_event(keypair: keypair, metadata: prof) else { return } guard let ev = make_metadata_event(keypair: keypair, metadata: prof) else { return }
ds.nostrNetwork.postbox.send(ev) await ds.nostrNetwork.postbox.send(ev)
}
} }
.onReceive(handle_notify(.broadcast)) { ev in .onReceive(handle_notify(.broadcast)) { ev in
guard let ds = self.damus_state else { return } guard let ds = self.damus_state else { return }
ds.nostrNetwork.postbox.send(ev) Task { await ds.nostrNetwork.postbox.send(ev) }
} }
.onReceive(handle_notify(.unfollow)) { target in .onReceive(handle_notify(.unfollow)) { target in
guard let state = self.damus_state else { return } guard let state = self.damus_state else { return }
_ = handle_unfollow(state: state, unfollow: target.follow_ref) Task { _ = await handle_unfollow(state: state, unfollow: target.follow_ref) }
} }
.onReceive(handle_notify(.unfollowed)) { unfollow in .onReceive(handle_notify(.unfollowed)) { unfollow in
home.resubscribe(.unfollowing(unfollow)) home.resubscribe(.unfollowing(unfollow))
} }
.onReceive(handle_notify(.follow)) { target in .onReceive(handle_notify(.follow)) { target in
guard let state = self.damus_state else { return } guard let state = self.damus_state else { return }
handle_follow_notif(state: state, target: target) Task { await handle_follow_notif(state: state, target: target) }
} }
.onReceive(handle_notify(.followed)) { _ in .onReceive(handle_notify(.followed)) { _ in
home.resubscribe(.following) home.resubscribe(.following)
@@ -431,10 +435,12 @@ struct ContentView: View {
return return
} }
if !handle_post_notification(keypair: keypair, postbox: state.nostrNetwork.postbox, events: state.events, post: post) { Task {
if await !handle_post_notification(keypair: keypair, postbox: state.nostrNetwork.postbox, events: state.events, post: post) {
self.active_sheet = nil self.active_sheet = nil
} }
} }
}
.onReceive(handle_notify(.new_mutes)) { _ in .onReceive(handle_notify(.new_mutes)) { _ in
home.filter_events() home.filter_events()
} }
@@ -475,7 +481,7 @@ struct ContentView: View {
} }
} }
.onReceive(handle_notify(.disconnect_relays)) { () in .onReceive(handle_notify(.disconnect_relays)) { () in
damus_state.nostrNetwork.disconnectRelays() Task { await damus_state.nostrNetwork.disconnectRelays() }
} }
.onReceive(NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)) { obj in .onReceive(NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)) { obj in
print("txn: 📙 DAMUS ACTIVE NOTIFY") print("txn: 📙 DAMUS ACTIVE NOTIFY")
@@ -540,13 +546,14 @@ struct ContentView: View {
damusClosingTask = nil damusClosingTask = nil
damus_state.ndb.reopen() damus_state.ndb.reopen()
// Pinging the network will automatically reconnect any dead websocket connections // Pinging the network will automatically reconnect any dead websocket connections
damus_state.nostrNetwork.ping() await damus_state.nostrNetwork.ping()
} }
@unknown default: @unknown default:
break break
} }
} }
.onReceive(handle_notify(.onlyzaps_mode)) { hide in .onReceive(handle_notify(.onlyzaps_mode)) { hide in
Task {
home.filter_events() home.filter_events()
guard let ds = damus_state, guard let ds = damus_state,
@@ -560,7 +567,8 @@ struct ContentView: View {
let prof = Profile(name: profile.name, display_name: profile.display_name, about: profile.about, picture: profile.picture, banner: profile.banner, website: profile.website, lud06: profile.lud06, lud16: profile.lud16, nip05: profile.nip05, damus_donation: profile.damus_donation, reactions: !hide) let prof = Profile(name: profile.name, display_name: profile.display_name, about: profile.about, picture: profile.picture, banner: profile.banner, website: profile.website, lud06: profile.lud06, lud16: profile.lud16, nip05: profile.nip05, damus_donation: profile.damus_donation, reactions: !hide)
guard let profile_ev = make_metadata_event(keypair: keypair, metadata: prof) else { return } guard let profile_ev = make_metadata_event(keypair: keypair, metadata: prof) else { return }
ds.nostrNetwork.postbox.send(profile_ev) await ds.nostrNetwork.postbox.send(profile_ev)
}
} }
.alert(NSLocalizedString("User muted", comment: "Alert message to indicate the user has been muted"), isPresented: $user_muted_confirm, actions: { .alert(NSLocalizedString("User muted", comment: "Alert message to indicate the user has been muted"), isPresented: $user_muted_confirm, actions: {
Button(NSLocalizedString("Thanks!", comment: "Button to close out of alert that informs that the action to muted a user was successful.")) { Button(NSLocalizedString("Thanks!", comment: "Button to close out of alert that informs that the action to muted a user was successful.")) {
@@ -583,6 +591,7 @@ struct ContentView: View {
} }
Button(NSLocalizedString("Yes, Overwrite", comment: "Text of button that confirms to overwrite the existing mutelist.")) { Button(NSLocalizedString("Yes, Overwrite", comment: "Text of button that confirms to overwrite the existing mutelist.")) {
Task {
guard let ds = damus_state, guard let ds = damus_state,
let keypair = ds.keypair.to_full(), let keypair = ds.keypair.to_full(),
let muting, let muting,
@@ -592,12 +601,13 @@ struct ContentView: View {
} }
ds.mutelist_manager.set_mutelist(mutelist) ds.mutelist_manager.set_mutelist(mutelist)
ds.nostrNetwork.postbox.send(mutelist) await ds.nostrNetwork.postbox.send(mutelist)
confirm_overwrite_mutelist = false confirm_overwrite_mutelist = false
confirm_mute = false confirm_mute = false
user_muted_confirm = true user_muted_confirm = true
} }
}
}, message: { }, message: {
Text("No mute list found, create a new one? This will overwrite any previous mute lists.", comment: "Alert message prompt that asks if the user wants to create a new mute list, overwriting previous mute lists.") Text("No mute list found, create a new one? This will overwrite any previous mute lists.", comment: "Alert message prompt that asks if the user wants to create a new mute list, overwriting previous mute lists.")
}) })
@@ -624,7 +634,7 @@ struct ContentView: View {
} }
ds.mutelist_manager.set_mutelist(ev) ds.mutelist_manager.set_mutelist(ev)
ds.nostrNetwork.postbox.send(ev) Task { await ds.nostrNetwork.postbox.send(ev) }
} }
} }
}, message: { }, message: {
@@ -676,7 +686,7 @@ struct ContentView: View {
self.execute_open_action(openAction) self.execute_open_action(openAction)
} }
func connect() { func connect() async {
// nostrdb // nostrdb
var mndb = Ndb() var mndb = Ndb()
if mndb == nil { if mndb == nil {
@@ -698,7 +708,7 @@ struct ContentView: View {
let settings = UserSettingsStore.globally_load_for(pubkey: pubkey) let settings = UserSettingsStore.globally_load_for(pubkey: pubkey)
let new_relay_filters = load_relay_filters(pubkey) == nil let new_relay_filters = await load_relay_filters(pubkey) == nil
self.damus_state = DamusState(keypair: keypair, self.damus_state = DamusState(keypair: keypair,
likes: EventCounter(our_pubkey: pubkey), likes: EventCounter(our_pubkey: pubkey),
@@ -756,7 +766,7 @@ struct ContentView: View {
Log.error("Failed to configure tips: %s", for: .tips, error.localizedDescription) Log.error("Failed to configure tips: %s", for: .tips, error.localizedDescription)
} }
} }
damus_state.nostrNetwork.connect() await damus_state.nostrNetwork.connect()
// TODO: Move this to a better spot. Not sure what is the best signal to listen to for sending initial filters // TODO: Move this to a better spot. Not sure what is the best signal to listen to for sending initial filters
DispatchQueue.main.asyncAfter(deadline: .now() + 0.25, execute: { DispatchQueue.main.asyncAfter(deadline: .now() + 0.25, execute: {
self.home.send_initial_filters() self.home.send_initial_filters()
@@ -764,6 +774,7 @@ struct ContentView: View {
} }
func music_changed(_ state: MusicState) { func music_changed(_ state: MusicState) {
Task {
guard let damus_state else { return } guard let damus_state else { return }
switch state { switch state {
case .playback_state: case .playback_state:
@@ -783,7 +794,8 @@ struct ContentView: View {
pdata.status.music = music pdata.status.music = music
guard let ev = music.to_note(keypair: kp) else { return } guard let ev = music.to_note(keypair: kp) else { return }
damus_state.nostrNetwork.postbox.send(ev) await damus_state.nostrNetwork.postbox.send(ev)
}
} }
} }
@@ -935,7 +947,7 @@ func update_filters_with_since(last_of_kind: [UInt32: NostrEvent], filters: [Nos
} }
} }
@MainActor
func setup_notifications() { func setup_notifications() {
this_app.registerForRemoteNotifications() this_app.registerForRemoteNotifications()
let center = UNUserNotificationCenter.current() let center = UNUserNotificationCenter.current()
@@ -992,14 +1004,14 @@ func timeline_name(_ timeline: Timeline?) -> String {
} }
@discardableResult @discardableResult
func handle_unfollow(state: DamusState, unfollow: FollowRef) -> Bool { func handle_unfollow(state: DamusState, unfollow: FollowRef) async -> Bool {
guard let keypair = state.keypair.to_full() else { guard let keypair = state.keypair.to_full() else {
return false return false
} }
let old_contacts = state.contacts.event let old_contacts = state.contacts.event
guard let ev = unfollow_reference(postbox: state.nostrNetwork.postbox, our_contacts: old_contacts, keypair: keypair, unfollow: unfollow) guard let ev = await unfollow_reference(postbox: state.nostrNetwork.postbox, our_contacts: old_contacts, keypair: keypair, unfollow: unfollow)
else { else {
return false return false
} }
@@ -1020,12 +1032,12 @@ func handle_unfollow(state: DamusState, unfollow: FollowRef) -> Bool {
} }
@discardableResult @discardableResult
func handle_follow(state: DamusState, follow: FollowRef) -> Bool { func handle_follow(state: DamusState, follow: FollowRef) async -> Bool {
guard let keypair = state.keypair.to_full() else { guard let keypair = state.keypair.to_full() else {
return false return false
} }
guard let ev = follow_reference(box: state.nostrNetwork.postbox, our_contacts: state.contacts.event, keypair: keypair, follow: follow) guard let ev = await follow_reference(box: state.nostrNetwork.postbox, our_contacts: state.contacts.event, keypair: keypair, follow: follow)
else { else {
return false return false
} }
@@ -1045,7 +1057,7 @@ func handle_follow(state: DamusState, follow: FollowRef) -> Bool {
} }
@discardableResult @discardableResult
func handle_follow_notif(state: DamusState, target: FollowTarget) -> Bool { func handle_follow_notif(state: DamusState, target: FollowTarget) async -> Bool {
switch target { switch target {
case .pubkey(let pk): case .pubkey(let pk):
state.contacts.add_friend_pubkey(pk) state.contacts.add_friend_pubkey(pk)
@@ -1053,10 +1065,10 @@ func handle_follow_notif(state: DamusState, target: FollowTarget) -> Bool {
state.contacts.add_friend_contact(ev) state.contacts.add_friend_contact(ev)
} }
return handle_follow(state: state, follow: target.follow_ref) return await handle_follow(state: state, follow: target.follow_ref)
} }
func handle_post_notification(keypair: FullKeypair, postbox: PostBox, events: EventCache, post: NostrPostResult) -> Bool { func handle_post_notification(keypair: FullKeypair, postbox: PostBox, events: EventCache, post: NostrPostResult) async -> Bool {
switch post { switch post {
case .post(let post): case .post(let post):
//let post = tup.0 //let post = tup.0
@@ -1065,17 +1077,17 @@ func handle_post_notification(keypair: FullKeypair, postbox: PostBox, events: Ev
guard let new_ev = post.to_event(keypair: keypair) else { guard let new_ev = post.to_event(keypair: keypair) else {
return false return false
} }
postbox.send(new_ev) await postbox.send(new_ev)
for eref in new_ev.referenced_ids.prefix(3) { for eref in new_ev.referenced_ids.prefix(3) {
// also broadcast at most 3 referenced events // also broadcast at most 3 referenced events
if let ev = events.lookup(eref) { if let ev = events.lookup(eref) {
postbox.send(ev) await postbox.send(ev)
} }
} }
for qref in new_ev.referenced_quote_ids.prefix(3) { for qref in new_ev.referenced_quote_ids.prefix(3) {
// also broadcast at most 3 referenced quoted events // also broadcast at most 3 referenced quoted events
if let ev = events.lookup(qref.note_id) { if let ev = events.lookup(qref.note_id) {
postbox.send(ev) await postbox.send(ev)
} }
} }
return true return true
@@ -50,18 +50,18 @@ class NostrNetworkManager {
// MARK: - Control and lifecycle functions // MARK: - Control and lifecycle functions
/// Connects the app to the Nostr network /// Connects the app to the Nostr network
func connect() { func connect() async {
self.userRelayList.connect() // Will load the user's list, apply it, and get RelayPool to connect to it. await self.userRelayList.connect() // Will load the user's list, apply it, and get RelayPool to connect to it.
Task { await self.profilesManager.load() } await self.profilesManager.load()
} }
func disconnectRelays() { func disconnectRelays() async {
self.pool.disconnect() await self.pool.disconnect()
} }
func handleAppBackgroundRequest() async { func handleAppBackgroundRequest() async {
await self.reader.cancelAllTasks() await self.reader.cancelAllTasks()
self.pool.cleanQueuedRequestForSessionEnd() await self.pool.cleanQueuedRequestForSessionEnd()
} }
func close() async { func close() async {
@@ -75,18 +75,19 @@ class NostrNetworkManager {
} }
// But await on each one to prevent race conditions // But await on each one to prevent race conditions
for await value in group { continue } for await value in group { continue }
pool.close() await pool.close()
} }
} }
func ping() { func ping() async {
self.pool.ping() await self.pool.ping()
} }
func relaysForEvent(event: NostrEvent) -> [RelayURL] { @MainActor
func relaysForEvent(event: NostrEvent) async -> [RelayURL] {
// TODO(tyiu) Ideally this list would be sorted by the event author's outbox relay preferences // TODO(tyiu) Ideally this list would be sorted by the event author's outbox relay preferences
// and reliability of relays to maximize chances of others finding this event. // and reliability of relays to maximize chances of others finding this event.
if let relays = pool.seen[event.id] { if let relays = await pool.seen[event.id] {
return Array(relays) return Array(relays)
} }
@@ -103,30 +104,35 @@ class NostrNetworkManager {
/// - This is also to help us migrate to the relay model. /// - This is also to help us migrate to the relay model.
// TODO: Define a better interface. This is a temporary scaffold to replace direct relay pool access. After that is done, we can refactor this interface to be cleaner and reduce non-sense. // TODO: Define a better interface. This is a temporary scaffold to replace direct relay pool access. After that is done, we can refactor this interface to be cleaner and reduce non-sense.
func sendToNostrDB(event: NostrEvent) { func sendToNostrDB(event: NostrEvent) async {
self.pool.send_raw_to_local_ndb(.typical(.event(event))) await self.pool.send_raw_to_local_ndb(.typical(.event(event)))
} }
func send(event: NostrEvent, to targetRelays: [RelayURL]? = nil, skipEphemeralRelays: Bool = true) { func send(event: NostrEvent, to targetRelays: [RelayURL]? = nil, skipEphemeralRelays: Bool = true) async {
self.pool.send(.event(event), to: targetRelays, skip_ephemeral: skipEphemeralRelays) await self.pool.send(.event(event), to: targetRelays, skip_ephemeral: skipEphemeralRelays)
} }
@MainActor
func getRelay(_ id: RelayURL) -> RelayPool.Relay? { func getRelay(_ id: RelayURL) -> RelayPool.Relay? {
pool.get_relay(id) pool.get_relay(id)
} }
@MainActor
var connectedRelays: [RelayPool.Relay] { var connectedRelays: [RelayPool.Relay] {
self.pool.relays self.pool.relays
} }
@MainActor
var ourRelayDescriptors: [RelayPool.RelayDescriptor] { var ourRelayDescriptors: [RelayPool.RelayDescriptor] {
self.pool.our_descriptors self.pool.our_descriptors
} }
func relayURLsThatSawNote(id: NoteId) -> Set<RelayURL>? { @MainActor
return self.pool.seen[id] func relayURLsThatSawNote(id: NoteId) async -> Set<RelayURL>? {
return await self.pool.seen[id]
} }
@MainActor
func determineToRelays(filters: RelayFilters) -> [RelayURL] { func determineToRelays(filters: RelayFilters) -> [RelayURL] {
return self.pool.our_descriptors return self.pool.our_descriptors
.map { $0.url } .map { $0.url }
@@ -137,8 +143,8 @@ class NostrNetworkManager {
// TODO: Move this to NWCManager // TODO: Move this to NWCManager
@discardableResult @discardableResult
func nwcPay(url: WalletConnectURL, post: PostBox, invoice: String, delay: TimeInterval? = 5.0, on_flush: OnFlush? = nil, zap_request: NostrEvent? = nil) -> NostrEvent? { func nwcPay(url: WalletConnectURL, post: PostBox, invoice: String, delay: TimeInterval? = 5.0, on_flush: OnFlush? = nil, zap_request: NostrEvent? = nil) async -> NostrEvent? {
WalletConnect.pay(url: url, pool: self.pool, post: post, invoice: invoice, zap_request: nil) await WalletConnect.pay(url: url, pool: self.pool, post: post, invoice: invoice, zap_request: nil)
} }
/// Send a donation zap to the Damus team /// Send a donation zap to the Damus team
@@ -154,7 +160,7 @@ class NostrNetworkManager {
} }
print("damus-donation donating...") print("damus-donation donating...")
WalletConnect.pay(url: nwc, pool: self.pool, post: self.postbox, invoice: invoice, zap_request: nil, delay: nil) await WalletConnect.pay(url: nwc, pool: self.pool, post: self.postbox, invoice: invoice, zap_request: nil, delay: nil)
} }
} }
@@ -192,14 +192,14 @@ extension NostrNetworkManager {
Self.logger.debug("Network subscription \(id.uuidString, privacy: .public): Started") Self.logger.debug("Network subscription \(id.uuidString, privacy: .public): Started")
let streamTask = Task { let streamTask = Task {
while !self.pool.open { while await !self.pool.open {
Self.logger.info("\(id.uuidString, privacy: .public): RelayPool closed. Sleeping for 1 second before resuming.") Self.logger.info("\(id.uuidString, privacy: .public): RelayPool closed. Sleeping for 1 second before resuming.")
try await Task.sleep(nanoseconds: 1_000_000_000) try await Task.sleep(nanoseconds: 1_000_000_000)
continue continue
} }
do { do {
for await item in self.pool.subscribe(filters: filters, to: desiredRelays, id: id) { for await item in await self.pool.subscribe(filters: filters, to: desiredRelays, id: id) {
// NO-OP. Notes will be automatically ingested by NostrDB // NO-OP. Notes will be automatically ingested by NostrDB
// TODO: Improve efficiency of subscriptions? // TODO: Improve efficiency of subscriptions?
try Task.checkCancellation() try Task.checkCancellation()
@@ -333,7 +333,7 @@ extension NostrNetworkManager {
} }
// Not available in local ndb, stream from network // Not available in local ndb, stream from network
outerLoop: for await item in self.pool.subscribe(filters: [NostrFilter(ids: [noteId], limit: 1)], to: targetRelays, eoseTimeout: timeout) { outerLoop: for await item in await self.pool.subscribe(filters: [NostrFilter(ids: [noteId], limit: 1)], to: targetRelays, eoseTimeout: timeout) {
switch item { switch item {
case .event(let event): case .event(let event):
return NdbNoteLender(ownedNdbNote: event) return NdbNoteLender(ownedNdbNote: event)
@@ -122,68 +122,68 @@ extension NostrNetworkManager {
// MARK: - Listening to and handling relay updates from the network // MARK: - Listening to and handling relay updates from the network
func connect() { func connect() async {
self.load() await self.load()
self.relayListObserverTask?.cancel() self.relayListObserverTask?.cancel()
self.relayListObserverTask = Task { await self.listenAndHandleRelayUpdates() } self.relayListObserverTask = Task { await self.listenAndHandleRelayUpdates() }
self.walletUpdatesObserverTask?.cancel() self.walletUpdatesObserverTask?.cancel()
self.walletUpdatesObserverTask = handle_notify(.attached_wallet).sink { _ in self.load() } self.walletUpdatesObserverTask = handle_notify(.attached_wallet).sink { _ in Task { await self.load() } }
} }
func listenAndHandleRelayUpdates() async { func listenAndHandleRelayUpdates() async {
let filter = NostrFilter(kinds: [.relay_list], authors: [delegate.keypair.pubkey]) let filter = NostrFilter(kinds: [.relay_list], authors: [delegate.keypair.pubkey])
for await noteLender in self.reader.streamIndefinitely(filters: [filter]) { for await noteLender in self.reader.streamIndefinitely(filters: [filter]) {
let currentRelayListCreationDate = self.getUserCurrentRelayListCreationDate() let currentRelayListCreationDate = self.getUserCurrentRelayListCreationDate()
try? noteLender.borrow({ note in try? await noteLender.borrow({ note in
guard note.pubkey == self.delegate.keypair.pubkey else { return } // Ensure this new list was ours guard note.pubkey == self.delegate.keypair.pubkey else { return } // Ensure this new list was ours
guard note.createdAt > (currentRelayListCreationDate ?? 0) else { return } // Ensure this is a newer list guard note.createdAt > (currentRelayListCreationDate ?? 0) else { return } // Ensure this is a newer list
guard let relayList = try? NIP65.RelayList(event: note) else { return } // Ensure it is a valid NIP-65 list guard let relayList = try? NIP65.RelayList(event: note) else { return } // Ensure it is a valid NIP-65 list
try? self.set(userRelayList: relayList) // Set the validated list try? await self.set(userRelayList: relayList) // Set the validated list
}) })
} }
} }
// MARK: - Editing the user's relay list // MARK: - Editing the user's relay list
func upsert(relay: NIP65.RelayList.RelayItem, force: Bool = false, overwriteExisting: Bool = false) throws(UpdateError) { func upsert(relay: NIP65.RelayList.RelayItem, force: Bool = false, overwriteExisting: Bool = false) async throws(UpdateError) {
guard let currentUserRelayList = force ? self.getBestEffortRelayList() : self.getUserCurrentRelayList() else { throw .noInitialRelayList } guard let currentUserRelayList = force ? self.getBestEffortRelayList() : self.getUserCurrentRelayList() else { throw .noInitialRelayList }
guard !currentUserRelayList.relays.keys.contains(relay.url) || overwriteExisting else { throw .relayAlreadyExists } guard !currentUserRelayList.relays.keys.contains(relay.url) || overwriteExisting else { throw .relayAlreadyExists }
var newList = currentUserRelayList.relays var newList = currentUserRelayList.relays
newList[relay.url] = relay newList[relay.url] = relay
try self.set(userRelayList: NIP65.RelayList(relays: Array(newList.values))) try await self.set(userRelayList: NIP65.RelayList(relays: Array(newList.values)))
} }
func insert(relay: NIP65.RelayList.RelayItem, force: Bool = false) throws(UpdateError) { func insert(relay: NIP65.RelayList.RelayItem, force: Bool = false) async throws(UpdateError) {
guard let currentUserRelayList = force ? self.getBestEffortRelayList() : self.getUserCurrentRelayList() else { throw .noInitialRelayList } guard let currentUserRelayList = force ? self.getBestEffortRelayList() : self.getUserCurrentRelayList() else { throw .noInitialRelayList }
guard currentUserRelayList.relays[relay.url] == nil else { throw .relayAlreadyExists } guard currentUserRelayList.relays[relay.url] == nil else { throw .relayAlreadyExists }
try self.upsert(relay: relay, force: force) try await self.upsert(relay: relay, force: force)
} }
func remove(relayURL: RelayURL, force: Bool = false) throws(UpdateError) { func remove(relayURL: RelayURL, force: Bool = false) async throws(UpdateError) {
guard let currentUserRelayList = force ? self.getBestEffortRelayList() : self.getUserCurrentRelayList() else { throw .noInitialRelayList } guard let currentUserRelayList = force ? self.getBestEffortRelayList() : self.getUserCurrentRelayList() else { throw .noInitialRelayList }
guard currentUserRelayList.relays.keys.contains(relayURL) || force else { throw .noSuchRelay } guard currentUserRelayList.relays.keys.contains(relayURL) || force else { throw .noSuchRelay }
var newList = currentUserRelayList.relays var newList = currentUserRelayList.relays
newList[relayURL] = nil newList[relayURL] = nil
try self.set(userRelayList: NIP65.RelayList(relays: Array(newList.values))) try await self.set(userRelayList: NIP65.RelayList(relays: Array(newList.values)))
} }
func set(userRelayList: NIP65.RelayList) throws(UpdateError) { func set(userRelayList: NIP65.RelayList) async throws(UpdateError) {
guard let fullKeypair = delegate.keypair.to_full() else { throw .notAuthorizedToChangeRelayList } guard let fullKeypair = delegate.keypair.to_full() else { throw .notAuthorizedToChangeRelayList }
guard let relayListEvent = userRelayList.toNostrEvent(keypair: fullKeypair) else { throw .cannotFormRelayListEvent } guard let relayListEvent = userRelayList.toNostrEvent(keypair: fullKeypair) else { throw .cannotFormRelayListEvent }
self.apply(newRelayList: self.computeRelaysToConnectTo(with: userRelayList)) await self.apply(newRelayList: self.computeRelaysToConnectTo(with: userRelayList))
self.pool.send(.event(relayListEvent)) // This will send to NostrDB as well, which will locally save that NIP-65 event await self.pool.send(.event(relayListEvent)) // This will send to NostrDB as well, which will locally save that NIP-65 event
self.delegate.latestRelayListEventIdHex = relayListEvent.id.hex() // Make sure we are able to recall this event from NostrDB self.delegate.latestRelayListEventIdHex = relayListEvent.id.hex() // Make sure we are able to recall this event from NostrDB
} }
// MARK: - Syncing our saved user relay list with the active `RelayPool` // MARK: - Syncing our saved user relay list with the active `RelayPool`
/// Loads the current user relay list /// Loads the current user relay list
func load() { func load() async {
self.apply(newRelayList: self.relaysToConnectTo()) await self.apply(newRelayList: self.relaysToConnectTo())
} }
/// Loads a new relay list into the active relay pool, making sure it matches the specified relay list. /// Loads a new relay list into the active relay pool, making sure it matches the specified relay list.
@@ -197,7 +197,8 @@ extension NostrNetworkManager {
/// ///
/// - This is `private` because syncing the user's saved relay list with the relay pool is `NostrNetworkManager`'s responsibility, /// - This is `private` because syncing the user's saved relay list with the relay pool is `NostrNetworkManager`'s responsibility,
/// so we do not want other classes to forcibly load this. /// so we do not want other classes to forcibly load this.
private func apply(newRelayList: [RelayPool.RelayDescriptor]) { @MainActor
private func apply(newRelayList: [RelayPool.RelayDescriptor]) async {
let currentRelayList = self.pool.relays.map({ $0.descriptor }) let currentRelayList = self.pool.relays.map({ $0.descriptor })
var changed = false var changed = false
@@ -217,31 +218,37 @@ extension NostrNetworkManager {
let relaysToRemove = currentRelayURLs.subtracting(newRelayURLs) let relaysToRemove = currentRelayURLs.subtracting(newRelayURLs)
let relaysToAdd = newRelayURLs.subtracting(currentRelayURLs) let relaysToAdd = newRelayURLs.subtracting(currentRelayURLs)
await withTaskGroup { taskGroup in
// Remove relays not in the new list // Remove relays not in the new list
relaysToRemove.forEach { url in relaysToRemove.forEach { url in
pool.remove_relay(url) taskGroup.addTask(operation: { await self.pool.remove_relay(url) })
changed = true changed = true
} }
// Add new relays from the new list // Add new relays from the new list
relaysToAdd.forEach { url in relaysToAdd.forEach { url in
guard let descriptor = newRelayList.first(where: { $0.url == url }) else { return } guard let descriptor = newRelayList.first(where: { $0.url == url }) else { return }
add_new_relay( taskGroup.addTask(operation: {
model_cache: delegate.relayModelCache, await add_new_relay(
relay_filters: delegate.relayFilters, model_cache: self.delegate.relayModelCache,
pool: pool, relay_filters: self.delegate.relayFilters,
pool: self.pool,
descriptor: descriptor, descriptor: descriptor,
new_relay_filters: new_relay_filters, new_relay_filters: new_relay_filters,
logging_enabled: delegate.developerMode logging_enabled: self.delegate.developerMode
) )
})
changed = true changed = true
} }
for await value in taskGroup { continue }
}
// Always tell RelayPool to connect whether or not we are already connected. // Always tell RelayPool to connect whether or not we are already connected.
// This is because: // This is because:
// 1. Internally it won't redo the connection because of internal checks // 1. Internally it won't redo the connection because of internal checks
// 2. Even if the relay list has not changed, relays may have been disconnected from app lifecycle or other events // 2. Even if the relay list has not changed, relays may have been disconnected from app lifecycle or other events
pool.connect() await pool.connect()
if changed { if changed {
notify(.relays_changed) notify(.relays_changed)
@@ -281,8 +288,8 @@ fileprivate extension NIP65.RelayList {
/// - descriptor: The description of the relay being added /// - descriptor: The description of the relay being added
/// - new_relay_filters: Whether to insert new relay filters /// - new_relay_filters: Whether to insert new relay filters
/// - logging_enabled: Whether logging is enabled /// - logging_enabled: Whether logging is enabled
fileprivate func add_new_relay(model_cache: RelayModelCache, relay_filters: RelayFilters, pool: RelayPool, descriptor: RelayPool.RelayDescriptor, new_relay_filters: Bool, logging_enabled: Bool) { fileprivate func add_new_relay(model_cache: RelayModelCache, relay_filters: RelayFilters, pool: RelayPool, descriptor: RelayPool.RelayDescriptor, new_relay_filters: Bool, logging_enabled: Bool) async {
try? pool.add_relay(descriptor) try? await pool.add_relay(descriptor)
let url = descriptor.url let url = descriptor.url
let relay_id = url let relay_id = url
@@ -300,7 +307,7 @@ fileprivate func add_new_relay(model_cache: RelayModelCache, relay_filters: Rela
model_cache.insert(model: model) model_cache.insert(model: model)
if logging_enabled { if logging_enabled {
pool.setLog(model.log, for: relay_id) Task { await pool.setLog(model.log, for: relay_id) }
} }
// if this is the first time adding filters, we should filter non-paid relays // if this is the first time adding filters, we should filter non-paid relays
+11 -15
View File
@@ -48,13 +48,13 @@ final class RelayConnection: ObservableObject {
private lazy var socket = WebSocket(relay_url.url) private lazy var socket = WebSocket(relay_url.url)
private var subscriptionToken: AnyCancellable? private var subscriptionToken: AnyCancellable?
private var handleEvent: (NostrConnectionEvent) -> () private var handleEvent: (NostrConnectionEvent) async -> ()
private var processEvent: (WebSocketEvent) -> () private var processEvent: (WebSocketEvent) -> ()
private let relay_url: RelayURL private let relay_url: RelayURL
var log: RelayLog? var log: RelayLog?
init(url: RelayURL, init(url: RelayURL,
handleEvent: @escaping (NostrConnectionEvent) -> (), handleEvent: @escaping (NostrConnectionEvent) async -> (),
processUnverifiedWSEvent: @escaping (WebSocketEvent) -> ()) processUnverifiedWSEvent: @escaping (WebSocketEvent) -> ())
{ {
self.relay_url = url self.relay_url = url
@@ -95,12 +95,12 @@ final class RelayConnection: ObservableObject {
.sink { [weak self] completion in .sink { [weak self] completion in
switch completion { switch completion {
case .failure(let error): case .failure(let error):
self?.receive(event: .error(error)) Task { await self?.receive(event: .error(error)) }
case .finished: case .finished:
self?.receive(event: .disconnected(.normalClosure, nil)) Task { await self?.receive(event: .disconnected(.normalClosure, nil)) }
} }
} receiveValue: { [weak self] event in } receiveValue: { [weak self] event in
self?.receive(event: event) Task { await self?.receive(event: event) }
} }
socket.connect() socket.connect()
@@ -138,7 +138,7 @@ final class RelayConnection: ObservableObject {
} }
} }
private func receive(event: WebSocketEvent) { private func receive(event: WebSocketEvent) async {
assert(!Thread.isMainThread, "This code must not be executed on the main thread") assert(!Thread.isMainThread, "This code must not be executed on the main thread")
processEvent(event) processEvent(event)
switch event { switch event {
@@ -149,7 +149,7 @@ final class RelayConnection: ObservableObject {
self.isConnecting = false self.isConnecting = false
} }
case .message(let message): case .message(let message):
self.receive(message: message) await self.receive(message: message)
case .disconnected(let closeCode, let reason): case .disconnected(let closeCode, let reason):
if closeCode != .normalClosure { if closeCode != .normalClosure {
Log.error("⚠️ Warning: RelayConnection (%d) closed with code: %s", for: .networking, String(describing: closeCode), String(describing: reason)) Log.error("⚠️ Warning: RelayConnection (%d) closed with code: %s", for: .networking, String(describing: closeCode), String(describing: reason))
@@ -176,10 +176,8 @@ final class RelayConnection: ObservableObject {
self.reconnect_with_backoff() self.reconnect_with_backoff()
} }
} }
DispatchQueue.main.async {
guard let ws_connection_event = NostrConnectionEvent.WSConnectionEvent.from(full_ws_event: event) else { return } guard let ws_connection_event = NostrConnectionEvent.WSConnectionEvent.from(full_ws_event: event) else { return }
self.handleEvent(.ws_connection_event(ws_connection_event)) await self.handleEvent(.ws_connection_event(ws_connection_event))
}
if let description = event.description { if let description = event.description {
log?.add(description) log?.add(description)
@@ -213,21 +211,19 @@ final class RelayConnection: ObservableObject {
} }
} }
private func receive(message: URLSessionWebSocketTask.Message) { private func receive(message: URLSessionWebSocketTask.Message) async {
switch message { switch message {
case .string(let messageString): case .string(let messageString):
// NOTE: Once we switch to the local relay model, // NOTE: Once we switch to the local relay model,
// we will not need to verify nostr events at this point. // we will not need to verify nostr events at this point.
if let ev = decode_and_verify_nostr_response(txt: messageString) { if let ev = decode_and_verify_nostr_response(txt: messageString) {
DispatchQueue.main.async { await self.handleEvent(.nostr_event(ev))
self.handleEvent(.nostr_event(ev))
}
return return
} }
print("failed to decode event \(messageString)") print("failed to decode event \(messageString)")
case .data(let messageData): case .data(let messageData):
if let messageString = String(data: messageData, encoding: .utf8) { if let messageString = String(data: messageData, encoding: .utf8) {
receive(message: .string(messageString)) await receive(message: .string(messageString))
} }
@unknown default: @unknown default:
print("An unexpected URLSessionWebSocketTask.Message was received.") print("An unexpected URLSessionWebSocketTask.Message was received.")
+113 -69
View File
@@ -12,7 +12,7 @@ struct RelayHandler {
let sub_id: String let sub_id: String
let filters: [NostrFilter]? let filters: [NostrFilter]?
let to: [RelayURL]? let to: [RelayURL]?
var callback: (RelayURL, NostrConnectionEvent) -> () var handler: AsyncStream<(RelayURL, NostrConnectionEvent)>.Continuation
} }
struct QueuedRequest { struct QueuedRequest {
@@ -27,7 +27,8 @@ struct SeenEvent: Hashable {
} }
/// Establishes and manages connections and subscriptions to a list of relays. /// Establishes and manages connections and subscriptions to a list of relays.
class RelayPool { actor RelayPool {
@MainActor
private(set) var relays: [Relay] = [] private(set) var relays: [Relay] = []
var open: Bool = false var open: Bool = false
var handlers: [RelayHandler] = [] var handlers: [RelayHandler] = []
@@ -50,65 +51,86 @@ class RelayPool {
/// This is to avoid error states and undefined behaviour related to hitting subscription limits on the relays, by letting those wait instead with the principle that although slower is not ideal, it is better than completely broken. /// This is to avoid error states and undefined behaviour related to hitting subscription limits on the relays, by letting those wait instead with the principle that although slower is not ideal, it is better than completely broken.
static let MAX_CONCURRENT_SUBSCRIPTION_LIMIT = 14 // This number is only an educated guess based on some local experiments. static let MAX_CONCURRENT_SUBSCRIPTION_LIMIT = 14 // This number is only an educated guess based on some local experiments.
func close() { func close() async {
disconnect() await disconnect()
relays = [] await clearRelays()
open = false open = false
handlers = [] handlers = []
request_queue = [] request_queue = []
seen.removeAll() await clearSeen()
counts = [:] counts = [:]
keypair = nil keypair = nil
} }
@MainActor
private func clearRelays() {
relays = []
}
private func clearSeen() {
seen.removeAll()
}
init(ndb: Ndb, keypair: Keypair? = nil) { init(ndb: Ndb, keypair: Keypair? = nil) {
self.ndb = ndb self.ndb = ndb
self.keypair = keypair self.keypair = keypair
network_monitor.pathUpdateHandler = { [weak self] path in network_monitor.pathUpdateHandler = { [weak self] path in
if (path.status == .satisfied || path.status == .requiresConnection) && self?.last_network_status != path.status { Task { await self?.pathUpdateHandler(path: path) }
DispatchQueue.main.async {
self?.connect_to_disconnected()
}
}
if let self, path.status != self.last_network_status {
for relay in self.relays {
relay.connection.log?.add("Network state: \(path.status)")
}
}
self?.last_network_status = path.status
} }
network_monitor.start(queue: network_monitor_queue) network_monitor.start(queue: network_monitor_queue)
} }
private func pathUpdateHandler(path: NWPath) async {
if (path.status == .satisfied || path.status == .requiresConnection) && self.last_network_status != path.status {
await self.connect_to_disconnected()
}
if path.status != self.last_network_status {
for relay in await self.relays {
relay.connection.log?.add("Network state: \(path.status)")
}
}
self.last_network_status = path.status
}
@MainActor
var our_descriptors: [RelayDescriptor] { var our_descriptors: [RelayDescriptor] {
return all_descriptors.filter { d in !d.ephemeral } return all_descriptors.filter { d in !d.ephemeral }
} }
@MainActor
var all_descriptors: [RelayDescriptor] { var all_descriptors: [RelayDescriptor] {
relays.map { r in r.descriptor } relays.map { r in r.descriptor }
} }
@MainActor
var num_connected: Int { var num_connected: Int {
return relays.reduce(0) { n, r in n + (r.connection.isConnected ? 1 : 0) } return relays.reduce(0) { n, r in n + (r.connection.isConnected ? 1 : 0) }
} }
func remove_handler(sub_id: String) { func remove_handler(sub_id: String) {
self.handlers = handlers.filter { $0.sub_id != sub_id } self.handlers = handlers.filter {
if $0.sub_id != sub_id {
return true
}
else {
$0.handler.finish()
return false
}
}
Log.debug("Removing %s handler, current: %d", for: .networking, sub_id, handlers.count) Log.debug("Removing %s handler, current: %d", for: .networking, sub_id, handlers.count)
} }
func ping() { func ping() async {
Log.info("Pinging %d relays", for: .networking, relays.count) Log.info("Pinging %d relays", for: .networking, await relays.count)
for relay in relays { for relay in await relays {
relay.connection.ping() relay.connection.ping()
} }
} }
@MainActor func register_handler(sub_id: String, filters: [NostrFilter]?, to relays: [RelayURL]? = nil, handler: AsyncStream<(RelayURL, NostrConnectionEvent)>.Continuation) async {
func register_handler(sub_id: String, filters: [NostrFilter]?, to relays: [RelayURL]? = nil, handler: @escaping (RelayURL, NostrConnectionEvent) -> ()) async {
while handlers.count > Self.MAX_CONCURRENT_SUBSCRIPTION_LIMIT { while handlers.count > Self.MAX_CONCURRENT_SUBSCRIPTION_LIMIT {
Log.debug("%s: Too many subscriptions, waiting for subscription pool to clear", for: .networking, sub_id) Log.debug("%s: Too many subscriptions, waiting for subscription pool to clear", for: .networking, sub_id)
try? await Task.sleep(for: .seconds(1)) try? await Task.sleep(for: .seconds(1))
@@ -117,20 +139,22 @@ class RelayPool {
handlers = handlers.filter({ handler in handlers = handlers.filter({ handler in
if handler.sub_id == sub_id { if handler.sub_id == sub_id {
Log.error("Duplicate handler detected for the same subscription ID. Overriding.", for: .networking) Log.error("Duplicate handler detected for the same subscription ID. Overriding.", for: .networking)
handler.handler.finish()
return false return false
} }
else { else {
return true return true
} }
}) })
self.handlers.append(RelayHandler(sub_id: sub_id, filters: filters, to: relays, callback: handler)) self.handlers.append(RelayHandler(sub_id: sub_id, filters: filters, to: relays, handler: handler))
Log.debug("Registering %s handler, current: %d", for: .networking, sub_id, self.handlers.count) Log.debug("Registering %s handler, current: %d", for: .networking, sub_id, self.handlers.count)
} }
func remove_relay(_ relay_id: RelayURL) { @MainActor
func remove_relay(_ relay_id: RelayURL) async {
var i: Int = 0 var i: Int = 0
self.disconnect(to: [relay_id]) await self.disconnect(to: [relay_id])
for relay in relays { for relay in relays {
if relay.id == relay_id { if relay.id == relay_id {
@@ -143,13 +167,13 @@ class RelayPool {
} }
} }
func add_relay(_ desc: RelayDescriptor) throws(RelayError) { func add_relay(_ desc: RelayDescriptor) async throws(RelayError) {
let relay_id = desc.url let relay_id = desc.url
if get_relay(relay_id) != nil { if await get_relay(relay_id) != nil {
throw RelayError.RelayAlreadyExists throw RelayError.RelayAlreadyExists
} }
let conn = RelayConnection(url: desc.url, handleEvent: { event in let conn = RelayConnection(url: desc.url, handleEvent: { event in
self.handle_event(relay_id: relay_id, event: event) await self.handle_event(relay_id: relay_id, event: event)
}, processUnverifiedWSEvent: { wsev in }, processUnverifiedWSEvent: { wsev in
guard case .message(let msg) = wsev, guard case .message(let msg) = wsev,
case .string(let str) = msg case .string(let str) = msg
@@ -159,19 +183,24 @@ class RelayPool {
self.message_received_function?((str, desc)) self.message_received_function?((str, desc))
}) })
let relay = Relay(descriptor: desc, connection: conn) let relay = Relay(descriptor: desc, connection: conn)
await self.appendRelayToList(relay: relay)
}
@MainActor
private func appendRelayToList(relay: Relay) {
self.relays.append(relay) self.relays.append(relay)
} }
func setLog(_ log: RelayLog, for relay_id: RelayURL) { func setLog(_ log: RelayLog, for relay_id: RelayURL) async {
// add the current network state to the log // add the current network state to the log
log.add("Network state: \(network_monitor.currentPath.status)") log.add("Network state: \(network_monitor.currentPath.status)")
get_relay(relay_id)?.connection.log = log await get_relay(relay_id)?.connection.log = log
} }
/// This is used to retry dead connections /// This is used to retry dead connections
func connect_to_disconnected() { func connect_to_disconnected() async {
for relay in relays { for relay in await relays {
let c = relay.connection let c = relay.connection
let is_connecting = c.isConnecting let is_connecting = c.isConnecting
@@ -188,16 +217,16 @@ class RelayPool {
} }
} }
func reconnect(to: [RelayURL]? = nil) { func reconnect(to targetRelays: [RelayURL]? = nil) async {
let relays = to.map{ get_relays($0) } ?? self.relays let relays = await getRelays(targetRelays: targetRelays)
for relay in relays { for relay in relays {
// don't try to reconnect to broken relays // don't try to reconnect to broken relays
relay.connection.reconnect() relay.connection.reconnect()
} }
} }
func connect(to: [RelayURL]? = nil) { func connect(to targetRelays: [RelayURL]? = nil) async {
let relays = to.map{ get_relays($0) } ?? self.relays let relays = await getRelays(targetRelays: targetRelays)
for relay in relays { for relay in relays {
relay.connection.connect() relay.connection.connect()
} }
@@ -205,15 +234,20 @@ class RelayPool {
open = true open = true
} }
func disconnect(to: [RelayURL]? = nil) { func disconnect(to targetRelays: [RelayURL]? = nil) async {
// Mark as closed first, to prevent other classes from pulling data while the relays are being disconnected // Mark as closed first, to prevent other classes from pulling data while the relays are being disconnected
open = false open = false
let relays = to.map{ get_relays($0) } ?? self.relays let relays = await getRelays(targetRelays: targetRelays)
for relay in relays { for relay in relays {
relay.connection.disconnect() relay.connection.disconnect()
} }
} }
@MainActor
func getRelays(targetRelays: [RelayURL]? = nil) -> [Relay] {
targetRelays.map{ get_relays($0) } ?? self.relays
}
/// Deletes queued up requests that should not persist between app sessions (i.e. when the app goes to background then back to foreground) /// Deletes queued up requests that should not persist between app sessions (i.e. when the app goes to background then back to foreground)
func cleanQueuedRequestForSessionEnd() { func cleanQueuedRequestForSessionEnd() {
request_queue = request_queue.filter { request in request_queue = request_queue.filter { request in
@@ -231,14 +265,14 @@ class RelayPool {
} }
} }
func unsubscribe(sub_id: String, to: [RelayURL]? = nil) { func unsubscribe(sub_id: String, to: [RelayURL]? = nil) async {
if to == nil { if to == nil {
self.remove_handler(sub_id: sub_id) self.remove_handler(sub_id: sub_id)
} }
self.send(.unsubscribe(sub_id), to: to) await self.send(.unsubscribe(sub_id), to: to)
} }
func subscribe(sub_id: String, filters: [NostrFilter], handler: @escaping (RelayURL, NostrConnectionEvent) -> (), to: [RelayURL]? = nil) { func subscribe(sub_id: String, filters: [NostrFilter], handler: AsyncStream<(RelayURL, NostrConnectionEvent)>.Continuation, to: [RelayURL]? = nil) {
Task { Task {
await register_handler(sub_id: sub_id, filters: filters, to: to, handler: handler) await register_handler(sub_id: sub_id, filters: filters, to: to, handler: handler)
@@ -246,7 +280,7 @@ class RelayPool {
// When the caller specifies specific relays, do not skip ephemeral relays to respect the exact list given by the caller. // When the caller specifies specific relays, do not skip ephemeral relays to respect the exact list given by the caller.
let shouldSkipEphemeralRelays = to == nil ? true : false let shouldSkipEphemeralRelays = to == nil ? true : false
send(.subscribe(.init(filters: filters, sub_id: sub_id)), to: to, skip_ephemeral: shouldSkipEphemeralRelays) await send(.subscribe(.init(filters: filters, sub_id: sub_id)), to: to, skip_ephemeral: shouldSkipEphemeralRelays)
} }
} }
@@ -257,9 +291,9 @@ class RelayPool {
/// - desiredRelays: The desired relays which to subsctibe to. If `nil`, it defaults to the `RelayPool`'s default list /// - desiredRelays: The desired relays which to subsctibe to. If `nil`, it defaults to the `RelayPool`'s default list
/// - eoseTimeout: The maximum timeout which to give up waiting for the eoseSignal /// - eoseTimeout: The maximum timeout which to give up waiting for the eoseSignal
/// - Returns: Returns an async stream that callers can easily consume via a for-loop /// - Returns: Returns an async stream that callers can easily consume via a for-loop
func subscribe(filters: [NostrFilter], to desiredRelays: [RelayURL]? = nil, eoseTimeout: Duration? = nil, id: UUID? = nil) -> AsyncStream<StreamItem> { func subscribe(filters: [NostrFilter], to desiredRelays: [RelayURL]? = nil, eoseTimeout: Duration? = nil, id: UUID? = nil) async -> AsyncStream<StreamItem> {
let eoseTimeout = eoseTimeout ?? .seconds(5) let eoseTimeout = eoseTimeout ?? .seconds(5)
let desiredRelays = desiredRelays ?? self.relays.map({ $0.descriptor.url }) let desiredRelays = await getRelays(targetRelays: desiredRelays)
let startTime = CFAbsoluteTimeGetCurrent() let startTime = CFAbsoluteTimeGetCurrent()
return AsyncStream<StreamItem> { continuation in return AsyncStream<StreamItem> { continuation in
let id = id ?? UUID() let id = id ?? UUID()
@@ -267,7 +301,12 @@ class RelayPool {
var seenEvents: Set<NoteId> = [] var seenEvents: Set<NoteId> = []
var relaysWhoFinishedInitialResults: Set<RelayURL> = [] var relaysWhoFinishedInitialResults: Set<RelayURL> = []
var eoseSent = false var eoseSent = false
self.subscribe(sub_id: sub_id, filters: filters, handler: { (relayUrl, connectionEvent) in let upstreamStream = AsyncStream<(RelayURL, NostrConnectionEvent)> { upstreamContinuation in
self.subscribe(sub_id: sub_id, filters: filters, handler: upstreamContinuation, to: desiredRelays.map({ $0.descriptor.url }))
}
let upstreamStreamingTask = Task {
for await (relayUrl, connectionEvent) in upstreamStream {
try Task.checkCancellation()
switch connectionEvent { switch connectionEvent {
case .ws_connection_event(let ev): case .ws_connection_event(let ev):
// Websocket events such as connect/disconnect/error are already handled in `RelayConnection`. Do not perform any handling here. // Websocket events such as connect/disconnect/error are already handled in `RelayConnection`. Do not perform any handling here.
@@ -284,7 +323,7 @@ class RelayPool {
break // We do not support handling these yet break // We do not support handling these yet
case .eose(_): case .eose(_):
relaysWhoFinishedInitialResults.insert(relayUrl) relaysWhoFinishedInitialResults.insert(relayUrl)
let desiredAndConnectedRelays = desiredRelays ?? self.relays.filter({ $0.connection.isConnected }).map({ $0.descriptor.url }) let desiredAndConnectedRelays = desiredRelays.filter({ $0.connection.isConnected }).map({ $0.descriptor.url })
Log.debug("RelayPool subscription %s: EOSE from %s. EOSE count: %d/%d. Elapsed: %.2f seconds.", for: .networking, id.uuidString, relayUrl.absoluteString, relaysWhoFinishedInitialResults.count, Set(desiredAndConnectedRelays).count, CFAbsoluteTimeGetCurrent() - startTime) Log.debug("RelayPool subscription %s: EOSE from %s. EOSE count: %d/%d. Elapsed: %.2f seconds.", for: .networking, id.uuidString, relayUrl.absoluteString, relaysWhoFinishedInitialResults.count, Set(desiredAndConnectedRelays).count, CFAbsoluteTimeGetCurrent() - startTime)
if relaysWhoFinishedInitialResults == Set(desiredAndConnectedRelays) { if relaysWhoFinishedInitialResults == Set(desiredAndConnectedRelays) {
continuation.yield(with: .success(.eose)) continuation.yield(with: .success(.eose))
@@ -294,7 +333,8 @@ class RelayPool {
case .auth(_): break // Handled in a separate function in RelayPool case .auth(_): break // Handled in a separate function in RelayPool
} }
} }
}, to: desiredRelays) }
}
let timeoutTask = Task { let timeoutTask = Task {
try? await Task.sleep(for: eoseTimeout) try? await Task.sleep(for: eoseTimeout)
if !eoseSent { continuation.yield(with: .success(.eose)) } if !eoseSent { continuation.yield(with: .success(.eose)) }
@@ -308,9 +348,12 @@ class RelayPool {
@unknown default: @unknown default:
break break
} }
self.unsubscribe(sub_id: sub_id, to: desiredRelays) Task {
self.remove_handler(sub_id: sub_id) await self.unsubscribe(sub_id: sub_id, to: desiredRelays.map({ $0.descriptor.url }))
await self.remove_handler(sub_id: sub_id)
}
timeoutTask.cancel() timeoutTask.cancel()
upstreamStreamingTask.cancel()
} }
} }
} }
@@ -322,11 +365,11 @@ class RelayPool {
case eose case eose
} }
func subscribe_to(sub_id: String, filters: [NostrFilter], to: [RelayURL]?, handler: @escaping (RelayURL, NostrConnectionEvent) -> ()) { func subscribe_to(sub_id: String, filters: [NostrFilter], to: [RelayURL]?, handler: AsyncStream<(RelayURL, NostrConnectionEvent)>.Continuation) {
Task { Task {
await register_handler(sub_id: sub_id, filters: filters, to: to, handler: handler) await register_handler(sub_id: sub_id, filters: filters, to: to, handler: handler)
send(.subscribe(.init(filters: filters, sub_id: sub_id)), to: to) await send(.subscribe(.init(filters: filters, sub_id: sub_id)), to: to)
} }
} }
@@ -341,7 +384,6 @@ class RelayPool {
return c return c
} }
@MainActor
func queue_req(r: NostrRequestType, relay: RelayURL, skip_ephemeral: Bool) { func queue_req(r: NostrRequestType, relay: RelayURL, skip_ephemeral: Bool) {
let count = count_queued(relay: relay) let count = count_queued(relay: relay)
guard count <= 10 else { guard count <= 10 else {
@@ -365,8 +407,8 @@ class RelayPool {
} }
} }
func send_raw(_ req: NostrRequestType, to: [RelayURL]? = nil, skip_ephemeral: Bool = true) { func send_raw(_ req: NostrRequestType, to: [RelayURL]? = nil, skip_ephemeral: Bool = true) async {
let relays = to.map{ get_relays($0) } ?? self.relays let relays = await getRelays(targetRelays: to)
self.send_raw_to_local_ndb(req) // Always send Nostr events and data to NostrDB for a local copy self.send_raw_to_local_ndb(req) // Always send Nostr events and data to NostrDB for a local copy
@@ -394,15 +436,17 @@ class RelayPool {
} }
} }
func send(_ req: NostrRequest, to: [RelayURL]? = nil, skip_ephemeral: Bool = true) { func send(_ req: NostrRequest, to: [RelayURL]? = nil, skip_ephemeral: Bool = true) async {
send_raw(.typical(req), to: to, skip_ephemeral: skip_ephemeral) await send_raw(.typical(req), to: to, skip_ephemeral: skip_ephemeral)
} }
@MainActor
func get_relays(_ ids: [RelayURL]) -> [Relay] { func get_relays(_ ids: [RelayURL]) -> [Relay] {
// don't include ephemeral relays in the default list to query // don't include ephemeral relays in the default list to query
relays.filter { ids.contains($0.id) } relays.filter { ids.contains($0.id) }
} }
@MainActor
func get_relay(_ id: RelayURL) -> Relay? { func get_relay(_ id: RelayURL) -> Relay? {
relays.first(where: { $0.id == id }) relays.first(where: { $0.id == id })
} }
@@ -415,7 +459,7 @@ class RelayPool {
} }
print("running queueing request: \(req.req) for \(relay_id)") print("running queueing request: \(req.req) for \(relay_id)")
self.send_raw(req.req, to: [relay_id], skip_ephemeral: false) Task { await self.send_raw(req.req, to: [relay_id], skip_ephemeral: false) }
} }
} }
@@ -432,7 +476,7 @@ class RelayPool {
} }
} }
func resubscribeAll(relayId: RelayURL) { func resubscribeAll(relayId: RelayURL) async {
for handler in self.handlers { for handler in self.handlers {
guard let filters = handler.filters else { continue } guard let filters = handler.filters else { continue }
// When the caller specifies no relays, it is implied that the user wants to use the ones in the user relay list. Skip ephemeral relays in that case. // When the caller specifies no relays, it is implied that the user wants to use the ones in the user relay list. Skip ephemeral relays in that case.
@@ -446,11 +490,11 @@ class RelayPool {
} }
Log.debug("%s: Sending resubscribe request to %s", for: .networking, handler.sub_id, relayId.absoluteString) Log.debug("%s: Sending resubscribe request to %s", for: .networking, handler.sub_id, relayId.absoluteString)
send(.subscribe(.init(filters: filters, sub_id: handler.sub_id)), to: [relayId], skip_ephemeral: shouldSkipEphemeralRelays) await send(.subscribe(.init(filters: filters, sub_id: handler.sub_id)), to: [relayId], skip_ephemeral: shouldSkipEphemeralRelays)
} }
} }
func handle_event(relay_id: RelayURL, event: NostrConnectionEvent) { func handle_event(relay_id: RelayURL, event: NostrConnectionEvent) async {
record_seen(relay_id: relay_id, event: event) record_seen(relay_id: relay_id, event: event)
// When we reconnect, do two things // When we reconnect, do two things
@@ -459,20 +503,20 @@ class RelayPool {
if case .ws_connection_event(let ws) = event { if case .ws_connection_event(let ws) = event {
if case .connected = ws { if case .connected = ws {
run_queue(relay_id) run_queue(relay_id)
self.resubscribeAll(relayId: relay_id) await self.resubscribeAll(relayId: relay_id)
} }
} }
// Handle auth // Handle auth
if case let .nostr_event(nostrResponse) = event, if case let .nostr_event(nostrResponse) = event,
case let .auth(challenge_string) = nostrResponse { case let .auth(challenge_string) = nostrResponse {
if let relay = get_relay(relay_id) { if let relay = await get_relay(relay_id) {
print("received auth request from \(relay.descriptor.url.id)") print("received auth request from \(relay.descriptor.url.id)")
relay.authentication_state = .pending relay.authentication_state = .pending
if let keypair { if let keypair {
if let fullKeypair = keypair.to_full() { if let fullKeypair = keypair.to_full() {
if let authRequest = make_auth_request(keypair: fullKeypair, challenge_string: challenge_string, relay: relay) { if let authRequest = make_auth_request(keypair: fullKeypair, challenge_string: challenge_string, relay: relay) {
send(.auth(authRequest), to: [relay_id], skip_ephemeral: false) await send(.auth(authRequest), to: [relay_id], skip_ephemeral: false)
relay.authentication_state = .verified relay.authentication_state = .verified
} else { } else {
print("failed to make auth request") print("failed to make auth request")
@@ -491,13 +535,13 @@ class RelayPool {
} }
for handler in handlers { for handler in handlers {
handler.callback(relay_id, event) handler.handler.yield((relay_id, event))
} }
} }
} }
func add_rw_relay(_ pool: RelayPool, _ url: RelayURL) { func add_rw_relay(_ pool: RelayPool, _ url: RelayURL) async {
try? pool.add_relay(RelayPool.RelayDescriptor(url: url, info: .readWrite)) try? await pool.add_relay(RelayPool.RelayDescriptor(url: url, info: .readWrite))
} }
@@ -46,7 +46,8 @@ class ActionBarModel: ObservableObject {
self.relays = relays self.relays = relays
} }
func update(damus: DamusState, evid: NoteId) { @MainActor
func update(damus: DamusState, evid: NoteId) async {
self.likes = damus.likes.counts[evid] ?? 0 self.likes = damus.likes.counts[evid] ?? 0
self.boosts = damus.boosts.counts[evid] ?? 0 self.boosts = damus.boosts.counts[evid] ?? 0
self.zaps = damus.zaps.event_counts[evid] ?? 0 self.zaps = damus.zaps.event_counts[evid] ?? 0
@@ -58,7 +59,7 @@ class ActionBarModel: ObservableObject {
self.our_zap = damus.zaps.our_zaps[evid]?.first self.our_zap = damus.zaps.our_zaps[evid]?.first
self.our_reply = damus.replies.our_reply(evid) self.our_reply = damus.replies.our_reply(evid)
self.our_quote_repost = damus.quote_reposts.our_events[evid] self.our_quote_repost = damus.quote_reposts.our_events[evid]
self.relays = (damus.nostrNetwork.relayURLsThatSawNote(id: evid) ?? []).count self.relays = (await damus.nostrNetwork.relayURLsThatSawNote(id: evid) ?? []).count
self.objectWillChange.send() self.objectWillChange.send()
} }
@@ -89,9 +89,11 @@ struct EventActionBar: View {
var like_swipe_button: some View { var like_swipe_button: some View {
SwipeAction(image: "shaka", backgroundColor: DamusColors.adaptableGrey) { SwipeAction(image: "shaka", backgroundColor: DamusColors.adaptableGrey) {
send_like(emoji: damus_state.settings.default_emoji_reaction) Task {
await send_like(emoji: damus_state.settings.default_emoji_reaction)
self.swipe_context?.state.wrappedValue = .closed self.swipe_context?.state.wrappedValue = .closed
} }
}
.swipeButtonStyle() .swipeButtonStyle()
.accessibilityLabel(NSLocalizedString("React with default reaction emoji", comment: "Accessibility label for react button")) .accessibilityLabel(NSLocalizedString("React with default reaction emoji", comment: "Accessibility label for react button"))
} }
@@ -138,7 +140,7 @@ struct EventActionBar: View {
if bar.liked { if bar.liked {
//notify(.delete, bar.our_like) //notify(.delete, bar.our_like)
} else { } else {
send_like(emoji: emoji) Task { await send_like(emoji: emoji) }
} }
} }
@@ -225,8 +227,15 @@ struct EventActionBar: View {
} }
} }
var event_relay_url_strings: [RelayURL] { @State var event_relay_url_strings: [RelayURL] = []
let relays = damus_state.nostrNetwork.relaysForEvent(event: event)
func updateEventRelayURLStrings() async {
let newValue = await fetchEventRelayURLStrings()
self.event_relay_url_strings = newValue
}
func fetchEventRelayURLStrings() async -> [RelayURL] {
let relays = await damus_state.nostrNetwork.relaysForEvent(event: event)
if !relays.isEmpty { if !relays.isEmpty {
return relays.prefix(Constants.MAX_SHARE_RELAYS).map { $0 } return relays.prefix(Constants.MAX_SHARE_RELAYS).map { $0 }
} }
@@ -237,9 +246,10 @@ struct EventActionBar: View {
var body: some View { var body: some View {
self.content self.content
.onAppear { .onAppear {
self.bar.update(damus: damus_state, evid: self.event.id)
Task.detached(priority: .background, operation: { Task.detached(priority: .background, operation: {
await self.bar.update(damus: damus_state, evid: self.event.id)
self.fetchLNURL() self.fetchLNURL()
await self.updateEventRelayURLStrings()
}) })
} }
.sheet(isPresented: $show_share_action, onDismiss: { self.show_share_action = false }) { .sheet(isPresented: $show_share_action, onDismiss: { self.show_share_action = false }) {
@@ -268,7 +278,10 @@ struct EventActionBar: View {
} }
.onReceive(handle_notify(.update_stats)) { target in .onReceive(handle_notify(.update_stats)) { target in
guard target == self.event.id else { return } guard target == self.event.id else { return }
self.bar.update(damus: self.damus_state, evid: target) Task {
await self.bar.update(damus: self.damus_state, evid: target)
await self.updateEventRelayURLStrings()
}
} }
.onReceive(handle_notify(.liked)) { liked in .onReceive(handle_notify(.liked)) { liked in
if liked.id != event.id { if liked.id != event.id {
@@ -281,9 +294,9 @@ struct EventActionBar: View {
} }
} }
func send_like(emoji: String) { func send_like(emoji: String) async {
guard let keypair = damus_state.keypair.to_full(), guard let keypair = damus_state.keypair.to_full(),
let like_ev = make_like_event(keypair: keypair, liked: event, content: emoji, relayURL: damus_state.nostrNetwork.relaysForEvent(event: event).first) else { let like_ev = await make_like_event(keypair: keypair, liked: event, content: emoji, relayURL: damus_state.nostrNetwork.relaysForEvent(event: event).first) else {
return return
} }
@@ -291,7 +304,7 @@ struct EventActionBar: View {
generator.impactOccurred() generator.impactOccurred()
damus_state.nostrNetwork.postbox.send(like_ev) await damus_state.nostrNetwork.postbox.send(like_ev)
} }
// MARK: Helper structures // MARK: Helper structures
@@ -13,6 +13,7 @@ struct EventDetailBar: View {
let target_pk: Pubkey let target_pk: Pubkey
@ObservedObject var bar: ActionBarModel @ObservedObject var bar: ActionBarModel
@State var relays: [RelayURL] = []
init(state: DamusState, target: NoteId, target_pk: Pubkey) { init(state: DamusState, target: NoteId, target_pk: Pubkey) {
self.state = state self.state = state
@@ -61,7 +62,6 @@ struct EventDetailBar: View {
} }
if bar.relays > 0 { if bar.relays > 0 {
let relays = Array(state.nostrNetwork.relayURLsThatSawNote(id: target) ?? [])
NavigationLink(value: Route.UserRelays(relays: relays)) { NavigationLink(value: Route.UserRelays(relays: relays)) {
let nounString = pluralizedString(key: "relays_count", count: bar.relays) let nounString = pluralizedString(key: "relays_count", count: bar.relays)
let noun = Text(nounString).foregroundColor(.gray) let noun = Text(nounString).foregroundColor(.gray)
@@ -70,6 +70,18 @@ struct EventDetailBar: View {
.buttonStyle(PlainButtonStyle()) .buttonStyle(PlainButtonStyle())
} }
} }
.onAppear {
Task { await self.updateSeenRelays() }
}
.onReceive(handle_notify(.update_stats)) { noteId in
guard noteId == target else { return }
Task { await self.updateSeenRelays() }
}
}
func updateSeenRelays() async {
let relays = await Array(state.nostrNetwork.relayURLsThatSawNote(id: target) ?? [])
self.relays = relays
} }
} }
@@ -27,8 +27,15 @@ struct ShareAction: View {
self._show_share = show_share self._show_share = show_share
} }
var event_relay_url_strings: [RelayURL] { @State var event_relay_url_strings: [RelayURL] = []
let relays = userProfile.damus.nostrNetwork.relaysForEvent(event: event)
func updateEventRelayURLStrings() async {
let newValue = await fetchEventRelayURLStrings()
self.event_relay_url_strings = newValue
}
func fetchEventRelayURLStrings() async -> [RelayURL] {
let relays = await userProfile.damus.nostrNetwork.relaysForEvent(event: event)
if !relays.isEmpty { if !relays.isEmpty {
return relays.prefix(Constants.MAX_SHARE_RELAYS).map { $0 } return relays.prefix(Constants.MAX_SHARE_RELAYS).map { $0 }
} }
@@ -80,8 +87,13 @@ struct ShareAction: View {
} }
} }
} }
.onReceive(handle_notify(.update_stats), perform: { noteId in
guard noteId == event.id else { return }
Task { await self.updateEventRelayURLStrings() }
})
.onAppear() { .onAppear() {
userProfile.subscribeToFindRelays() userProfile.subscribeToFindRelays()
Task { await self.updateEventRelayURLStrings() }
} }
.onDisappear() { .onDisappear() {
userProfile.unsubscribeFindRelays() userProfile.unsubscribeFindRelays()
@@ -57,13 +57,13 @@ struct ReportView: View {
.padding() .padding()
} }
func do_send_report() { func do_send_report() async {
guard let selected_report_type, guard let selected_report_type,
let ev = NostrEvent(content: report_message, keypair: keypair.to_keypair(), kind: 1984, tags: target.reportTags(type: selected_report_type)) else { let ev = NostrEvent(content: report_message, keypair: keypair.to_keypair(), kind: 1984, tags: target.reportTags(type: selected_report_type)) else {
return return
} }
postbox.send(ev) await postbox.send(ev)
report_sent = true report_sent = true
report_id = bech32_note_id(ev.id) report_id = bech32_note_id(ev.id)
@@ -116,7 +116,7 @@ struct ReportView: View {
Section(content: { Section(content: {
Button(send_report_button_text) { Button(send_report_button_text) {
do_send_report() Task { await do_send_report() }
} }
.disabled(selected_report_type == nil) .disabled(selected_report_type == nil)
}, footer: { }, footer: {
@@ -20,12 +20,14 @@ struct RepostAction: View {
Button { Button {
dismiss() dismiss()
Task {
guard let keypair = self.damus_state.keypair.to_full(), guard let keypair = self.damus_state.keypair.to_full(),
let boost = make_boost_event(keypair: keypair, boosted: self.event, relayURL: damus_state.nostrNetwork.relaysForEvent(event: self.event).first) else { let boost = await make_boost_event(keypair: keypair, boosted: self.event, relayURL: damus_state.nostrNetwork.relaysForEvent(event: self.event).first) else {
return return
} }
damus_state.nostrNetwork.postbox.send(boost) await damus_state.nostrNetwork.postbox.send(boost)
}
} label: { } label: {
Label(NSLocalizedString("Repost", comment: "Button to repost a note"), image: "repost") Label(NSLocalizedString("Repost", comment: "Button to repost a note"), image: "repost")
.frame(maxWidth: .infinity, minHeight: 50, maxHeight: 50, alignment: .leading) .frame(maxWidth: .infinity, minHeight: 50, maxHeight: 50, alignment: .leading)
+6 -4
View File
@@ -197,11 +197,13 @@ struct ChatEventView: View {
} }
.onChange(of: selected_emoji) { newSelectedEmoji in .onChange(of: selected_emoji) { newSelectedEmoji in
if let newSelectedEmoji { if let newSelectedEmoji {
send_like(emoji: newSelectedEmoji.value) Task {
await send_like(emoji: newSelectedEmoji.value)
popover_state = .closed popover_state = .closed
} }
} }
} }
}
.scaleEffect(self.popover_state.some_sheet_open() ? 1.08 : is_pressing ? 1.02 : 1) .scaleEffect(self.popover_state.some_sheet_open() ? 1.08 : is_pressing ? 1.02 : 1)
.shadow(color: (is_pressing || self.popover_state.some_sheet_open()) ? .black.opacity(0.1) : .black.opacity(0.3), radius: (is_pressing || self.popover_state.some_sheet_open()) ? 8 : 0, y: (is_pressing || self.popover_state.some_sheet_open()) ? 15 : 0) .shadow(color: (is_pressing || self.popover_state.some_sheet_open()) ? .black.opacity(0.1) : .black.opacity(0.3), radius: (is_pressing || self.popover_state.some_sheet_open()) ? 8 : 0, y: (is_pressing || self.popover_state.some_sheet_open()) ? 15 : 0)
.onLongPressGesture(minimumDuration: 0.5, maximumDistance: 10, perform: { .onLongPressGesture(minimumDuration: 0.5, maximumDistance: 10, perform: {
@@ -233,9 +235,9 @@ struct ChatEventView: View {
) )
} }
func send_like(emoji: String) { func send_like(emoji: String) async {
guard let keypair = damus_state.keypair.to_full(), guard let keypair = damus_state.keypair.to_full(),
let like_ev = make_like_event(keypair: keypair, liked: event, content: emoji, relayURL: damus_state.nostrNetwork.relaysForEvent(event: event).first) else { let like_ev = make_like_event(keypair: keypair, liked: event, content: emoji, relayURL: await damus_state.nostrNetwork.relaysForEvent(event: event).first) else {
return return
} }
@@ -244,7 +246,7 @@ struct ChatEventView: View {
let generator = UIImpactFeedbackGenerator(style: .medium) let generator = UIImpactFeedbackGenerator(style: .medium)
generator.impactOccurred() generator.impactOccurred()
damus_state.nostrNetwork.postbox.send(like_ev) await damus_state.nostrNetwork.postbox.send(like_ev)
} }
var action_bar: some View { var action_bar: some View {
+3 -3
View File
@@ -108,7 +108,7 @@ struct DMChatView: View, KeyboardReadable {
Button( Button(
role: .none, role: .none,
action: { action: {
send_message() Task { await send_message() }
} }
) { ) {
Label("", image: "send") Label("", image: "send")
@@ -124,7 +124,7 @@ struct DMChatView: View, KeyboardReadable {
*/ */
} }
func send_message() { func send_message() async {
let tags = [["p", pubkey.hex()]] let tags = [["p", pubkey.hex()]]
guard let post_blocks = parse_post_blocks(content: dms.draft)?.blocks else { guard let post_blocks = parse_post_blocks(content: dms.draft)?.blocks else {
return return
@@ -138,7 +138,7 @@ struct DMChatView: View, KeyboardReadable {
dms.draft = "" dms.draft = ""
damus_state.nostrNetwork.postbox.send(dm) await damus_state.nostrNetwork.postbox.send(dm)
handle_incoming_dm(ev: dm, our_pubkey: damus_state.pubkey, dms: damus_state.dms, prev_events: NewEventsBits()) handle_incoming_dm(ev: dm, our_pubkey: damus_state.pubkey, dms: damus_state.dms, prev_events: NewEventsBits())
+4 -4
View File
@@ -64,8 +64,8 @@ struct MenuItems: View {
self.profileModel = profileModel self.profileModel = profileModel
} }
var event_relay_url_strings: [RelayURL] { func event_relay_url_strings() async -> [RelayURL] {
let relays = damus_state.nostrNetwork.relaysForEvent(event: event) let relays = await damus_state.nostrNetwork.relaysForEvent(event: event)
if !relays.isEmpty { if !relays.isEmpty {
return relays.prefix(Constants.MAX_SHARE_RELAYS).map { $0 } return relays.prefix(Constants.MAX_SHARE_RELAYS).map { $0 }
} }
@@ -88,7 +88,7 @@ struct MenuItems: View {
} }
Button { Button {
UIPasteboard.general.string = Bech32Object.encode(.nevent(NEvent(event: event, relays: event_relay_url_strings))) Task { UIPasteboard.general.string = Bech32Object.encode(.nevent(NEvent(event: event, relays: await event_relay_url_strings()))) }
} label: { } label: {
Label(NSLocalizedString("Copy note ID", comment: "Context menu option for copying the ID of the note."), image: "note-book") Label(NSLocalizedString("Copy note ID", comment: "Context menu option for copying the ID of the note."), image: "note-book")
} }
@@ -122,7 +122,7 @@ struct MenuItems: View {
if let full_keypair = self.damus_state.keypair.to_full(), if let full_keypair = self.damus_state.keypair.to_full(),
let new_mutelist_ev = toggle_from_mutelist(keypair: full_keypair, prev: damus_state.mutelist_manager.event, to_toggle: .thread(event.thread_id(), duration?.date_from_now)) { let new_mutelist_ev = toggle_from_mutelist(keypair: full_keypair, prev: damus_state.mutelist_manager.event, to_toggle: .thread(event.thread_id(), duration?.date_from_now)) {
damus_state.mutelist_manager.set_mutelist(new_mutelist_ev) damus_state.mutelist_manager.set_mutelist(new_mutelist_ev)
damus_state.nostrNetwork.postbox.send(new_mutelist_ev) Task { await damus_state.nostrNetwork.postbox.send(new_mutelist_ev) }
} }
let muted = damus_state.mutelist_manager.is_event_muted(event) let muted = damus_state.mutelist_manager.is_event_muted(event)
isMutedThread = muted isMutedThread = muted
+1 -1
View File
@@ -106,7 +106,7 @@ func format_date(date: Date, time_style: DateFormatter.Style = .short) -> String
func make_actionbar_model(ev: NoteId, damus: DamusState) -> ActionBarModel { func make_actionbar_model(ev: NoteId, damus: DamusState) -> ActionBarModel {
let model = ActionBarModel.empty() let model = ActionBarModel.empty()
model.update(damus: damus, evid: ev) Task { await model.update(damus: damus, evid: ev) }
return model return model
} }
@@ -74,7 +74,7 @@ struct SelectedEventView: View {
} }
.onReceive(handle_notify(.update_stats)) { target in .onReceive(handle_notify(.update_stats)) { target in
guard target == self.event.id else { return } guard target == self.event.id else { return }
self.bar.update(damus: self.damus, evid: target) Task { await self.bar.update(damus: self.damus, evid: target) }
} }
.compositingGroup() .compositingGroup()
} }
@@ -37,7 +37,7 @@ class FollowPackModel: ObservableObject {
} }
func listenForUpdates(follow_pack_users: [Pubkey]) async { func listenForUpdates(follow_pack_users: [Pubkey]) async {
let to_relays = damus_state.nostrNetwork.determineToRelays(filters: damus_state.relay_filters) let to_relays = await damus_state.nostrNetwork.determineToRelays(filters: damus_state.relay_filters)
var filter = NostrFilter(kinds: [.text, .chat]) var filter = NostrFilter(kinds: [.text, .chat])
filter.until = UInt32(Date.now.timeIntervalSince1970) filter.until = UInt32(Date.now.timeIntervalSince1970)
filter.authors = follow_pack_users filter.authors = follow_pack_users
@@ -9,17 +9,17 @@
import Foundation import Foundation
func follow_reference(box: PostBox, our_contacts: NostrEvent?, keypair: FullKeypair, follow: FollowRef) -> NostrEvent? { func follow_reference(box: PostBox, our_contacts: NostrEvent?, keypair: FullKeypair, follow: FollowRef) async -> NostrEvent? {
guard let ev = follow_user_event(our_contacts: our_contacts, keypair: keypair, follow: follow) else { guard let ev = follow_user_event(our_contacts: our_contacts, keypair: keypair, follow: follow) else {
return nil return nil
} }
box.send(ev) await box.send(ev)
return ev return ev
} }
func unfollow_reference(postbox: PostBox, our_contacts: NostrEvent?, keypair: FullKeypair, unfollow: FollowRef) -> NostrEvent? { func unfollow_reference(postbox: PostBox, our_contacts: NostrEvent?, keypair: FullKeypair, unfollow: FollowRef) async -> NostrEvent? {
guard let cs = our_contacts else { guard let cs = our_contacts else {
return nil return nil
} }
@@ -28,7 +28,7 @@ func unfollow_reference(postbox: PostBox, our_contacts: NostrEvent?, keypair: Fu
return nil return nil
} }
postbox.send(ev) await postbox.send(ev)
return ev return ev
} }
@@ -34,7 +34,7 @@ func migrate_old_muted_threads_to_new_mutelist(keypair: Keypair, damus_state: Da
let previous_mute_list_event = damus_state.mutelist_manager.event let previous_mute_list_event = damus_state.mutelist_manager.event
guard let new_mutelist_event = create_or_update_mutelist(keypair: fullKeypair, mprev: previous_mute_list_event, to_add: Set(mutedThreads.map { MuteItem.thread($0, nil) })) else { return } guard let new_mutelist_event = create_or_update_mutelist(keypair: fullKeypair, mprev: previous_mute_list_event, to_add: Set(mutedThreads.map { MuteItem.thread($0, nil) })) else { return }
damus_state.mutelist_manager.set_mutelist(new_mutelist_event) damus_state.mutelist_manager.set_mutelist(new_mutelist_event)
damus_state.nostrNetwork.postbox.send(new_mutelist_event) Task { await damus_state.nostrNetwork.postbox.send(new_mutelist_event) }
// Set existing muted threads to an empty array // Set existing muted threads to an empty array
UserDefaults.standard.set([], forKey: getMutedThreadsKey(pubkey: keypair.pubkey)) UserDefaults.standard.set([], forKey: getMutedThreadsKey(pubkey: keypair.pubkey))
} }
@@ -87,7 +87,7 @@ struct AddMuteItemView: View {
} }
state.mutelist_manager.set_mutelist(mutelist) state.mutelist_manager.set_mutelist(mutelist)
state.nostrNetwork.postbox.send(mutelist) Task { await state.nostrNetwork.postbox.send(mutelist) }
} }
new_text = "" new_text = ""
@@ -30,8 +30,10 @@ struct MutelistView: View {
} }
damus_state.mutelist_manager.set_mutelist(new_ev) damus_state.mutelist_manager.set_mutelist(new_ev)
damus_state.nostrNetwork.postbox.send(new_ev) Task {
await damus_state.nostrNetwork.postbox.send(new_ev)
updateMuteItems() updateMuteItems()
}
} label: { } label: {
Label(NSLocalizedString("Delete", comment: "Button to remove a user from their mutelist."), image: "delete") Label(NSLocalizedString("Delete", comment: "Button to remove a user from their mutelist."), image: "delete")
} }
@@ -56,7 +56,7 @@ struct OnboardingSuggestionsView: View {
// - We don't have other mechanisms to allow the user to edit this yet // - We don't have other mechanisms to allow the user to edit this yet
// //
// Therefore, it is better to just save it locally, and retrieve this once we build out https://github.com/damus-io/damus/issues/3042 // Therefore, it is better to just save it locally, and retrieve this once we build out https://github.com/damus-io/damus/issues/3042
model.damus_state.nostrNetwork.sendToNostrDB(event: event) Task { await model.damus_state.nostrNetwork.sendToNostrDB(event: event) }
} }
var body: some View { var body: some View {
@@ -75,7 +75,7 @@ struct SaveKeysView: View {
.foregroundColor(.red) .foregroundColor(.red)
Button(action: { Button(action: {
complete_account_creation(account) Task { await complete_account_creation(account) }
}) { }) {
HStack { HStack {
Text("Retry", comment: "Button to retry completing account creation after an error occurred.") Text("Retry", comment: "Button to retry completing account creation after an error occurred.")
@@ -89,7 +89,7 @@ struct SaveKeysView: View {
Button(action: { Button(action: {
save_key(account) save_key(account)
complete_account_creation(account) Task { await complete_account_creation(account) }
}) { }) {
HStack { HStack {
Text("Save", comment: "Button to save key, complete account creation, and start using the app.") Text("Save", comment: "Button to save key, complete account creation, and start using the app.")
@@ -101,7 +101,7 @@ struct SaveKeysView: View {
.padding(.top, 20) .padding(.top, 20)
Button(action: { Button(action: {
complete_account_creation(account) Task { await complete_account_creation(account) }
}) { }) {
HStack { HStack {
Text("Not now", comment: "Button to not save key, complete account creation, and start using the app.") Text("Not now", comment: "Button to not save key, complete account creation, and start using the app.")
@@ -125,7 +125,7 @@ struct SaveKeysView: View {
credential_handler.save_credential(pubkey: account.pubkey, privkey: account.privkey) credential_handler.save_credential(pubkey: account.pubkey, privkey: account.privkey)
} }
func complete_account_creation(_ account: CreateAccountModel) { func complete_account_creation(_ account: CreateAccountModel) async {
guard let first_contact_event else { guard let first_contact_event else {
error = NSLocalizedString("Could not create your initial contact list event. This is a software bug, please contact Damus support via support@damus.io or through our Nostr account for help.", comment: "Error message to the user indicating that the initial contact list failed to be created.") error = NSLocalizedString("Could not create your initial contact list event. This is a software bug, please contact Damus support via support@damus.io or through our Nostr account for help.", comment: "Error message to the user indicating that the initial contact list failed to be created.")
return return
@@ -139,14 +139,21 @@ struct SaveKeysView: View {
let bootstrap_relays = load_bootstrap_relays(pubkey: account.pubkey) let bootstrap_relays = load_bootstrap_relays(pubkey: account.pubkey)
for relay in bootstrap_relays { for relay in bootstrap_relays {
add_rw_relay(self.pool, relay) await add_rw_relay(self.pool, relay)
} }
Task { await self.pool.register_handler(sub_id: "signup", filters: nil, handler: handle_event) } Task {
let stream = AsyncStream<(RelayURL, NostrConnectionEvent)> { streamContinuation in
Task { await self.pool.register_handler(sub_id: "signup", filters: nil, handler: streamContinuation) }
}
for await (relayUrl, connectionEvent) in stream {
await handle_event(relay: relayUrl, ev: connectionEvent)
}
}
self.loading = true self.loading = true
self.pool.connect() await self.pool.connect()
} }
func save_to_storage(first_contact_event: NdbNote, first_relay_list_event: NdbNote, for account: CreateAccountModel) { func save_to_storage(first_contact_event: NdbNote, first_relay_list_event: NdbNote, for account: CreateAccountModel) {
@@ -160,7 +167,7 @@ struct SaveKeysView: View {
settings.latestRelayListEventIdHex = first_relay_list_event.id.hex() settings.latestRelayListEventIdHex = first_relay_list_event.id.hex()
} }
func handle_event(relay: RelayURL, ev: NostrConnectionEvent) { func handle_event(relay: RelayURL, ev: NostrConnectionEvent) async {
switch ev { switch ev {
case .ws_connection_event(let wsev): case .ws_connection_event(let wsev):
switch wsev { switch wsev {
@@ -169,15 +176,15 @@ struct SaveKeysView: View {
if let keypair = account.keypair.to_full(), if let keypair = account.keypair.to_full(),
let metadata_ev = make_metadata_event(keypair: keypair, metadata: metadata) { let metadata_ev = make_metadata_event(keypair: keypair, metadata: metadata) {
self.pool.send(.event(metadata_ev)) await self.pool.send(.event(metadata_ev))
} }
if let first_contact_event { if let first_contact_event {
self.pool.send(.event(first_contact_event)) await self.pool.send(.event(first_contact_event))
} }
if let first_relay_list_event { if let first_relay_list_event {
self.pool.send(.event(first_relay_list_event)) await self.pool.send(.event(first_relay_list_event))
} }
do { do {
@@ -64,9 +64,9 @@ class DraftArtifacts: Equatable {
/// - damus_state: The damus state, needed for encrypting, fetching Nostr data depedencies, and forming the NIP-37 draft /// - damus_state: The damus state, needed for encrypting, fetching Nostr data depedencies, and forming the NIP-37 draft
/// - references: references in the post? /// - references: references in the post?
/// - Returns: The NIP-37 draft packaged in a way that can be easily wrapped/unwrapped. /// - Returns: The NIP-37 draft packaged in a way that can be easily wrapped/unwrapped.
func to_nip37_draft(action: PostAction, damus_state: DamusState) throws -> NIP37Draft? { func to_nip37_draft(action: PostAction, damus_state: DamusState) async throws -> NIP37Draft? {
guard let keypair = damus_state.keypair.to_full() else { return nil } guard let keypair = damus_state.keypair.to_full() else { return nil }
let post = build_post(state: damus_state, action: action, draft: self) let post = await build_post(state: damus_state, action: action, draft: self)
guard let note = post.to_event(keypair: keypair) else { return nil } guard let note = post.to_event(keypair: keypair) else { return nil }
return try NIP37Draft(unwrapped_note: note, draft_id: self.id, keypair: keypair) return try NIP37Draft(unwrapped_note: note, draft_id: self.id, keypair: keypair)
} }
@@ -227,24 +227,24 @@ class Drafts: ObservableObject {
func save(damus_state: DamusState) async { func save(damus_state: DamusState) async {
var draft_events: [NdbNote] = [] var draft_events: [NdbNote] = []
post_artifact_block: if let post_artifacts = self.post { post_artifact_block: if let post_artifacts = self.post {
let nip37_draft = try? post_artifacts.to_nip37_draft(action: .posting(.user(damus_state.pubkey)), damus_state: damus_state) let nip37_draft = try? await post_artifacts.to_nip37_draft(action: .posting(.user(damus_state.pubkey)), damus_state: damus_state)
guard let wrapped_note = nip37_draft?.wrapped_note else { break post_artifact_block } guard let wrapped_note = nip37_draft?.wrapped_note else { break post_artifact_block }
draft_events.append(wrapped_note) draft_events.append(wrapped_note)
} }
for (replied_to_note_id, reply_artifacts) in self.replies { for (replied_to_note_id, reply_artifacts) in self.replies {
guard let replied_to_note = damus_state.ndb.lookup_note(replied_to_note_id)?.unsafeUnownedValue?.to_owned() else { continue } guard let replied_to_note = damus_state.ndb.lookup_note(replied_to_note_id)?.unsafeUnownedValue?.to_owned() else { continue }
let nip37_draft = try? reply_artifacts.to_nip37_draft(action: .replying_to(replied_to_note), damus_state: damus_state) let nip37_draft = try? await reply_artifacts.to_nip37_draft(action: .replying_to(replied_to_note), damus_state: damus_state)
guard let wrapped_note = nip37_draft?.wrapped_note else { continue } guard let wrapped_note = nip37_draft?.wrapped_note else { continue }
draft_events.append(wrapped_note) draft_events.append(wrapped_note)
} }
for (quoted_note_id, quote_note_artifacts) in self.quotes { for (quoted_note_id, quote_note_artifacts) in self.quotes {
guard let quoted_note = damus_state.ndb.lookup_note(quoted_note_id)?.unsafeUnownedValue?.to_owned() else { continue } guard let quoted_note = damus_state.ndb.lookup_note(quoted_note_id)?.unsafeUnownedValue?.to_owned() else { continue }
let nip37_draft = try? quote_note_artifacts.to_nip37_draft(action: .quoting(quoted_note), damus_state: damus_state) let nip37_draft = try? await quote_note_artifacts.to_nip37_draft(action: .quoting(quoted_note), damus_state: damus_state)
guard let wrapped_note = nip37_draft?.wrapped_note else { continue } guard let wrapped_note = nip37_draft?.wrapped_note else { continue }
draft_events.append(wrapped_note) draft_events.append(wrapped_note)
} }
for (highlight, highlight_note_artifacts) in self.highlights { for (highlight, highlight_note_artifacts) in self.highlights {
let nip37_draft = try? highlight_note_artifacts.to_nip37_draft(action: .highlighting(highlight), damus_state: damus_state) let nip37_draft = try? await highlight_note_artifacts.to_nip37_draft(action: .highlighting(highlight), damus_state: damus_state)
guard let wrapped_note = nip37_draft?.wrapped_note else { continue } guard let wrapped_note = nip37_draft?.wrapped_note else { continue }
draft_events.append(wrapped_note) draft_events.append(wrapped_note)
} }
@@ -254,7 +254,7 @@ class Drafts: ObservableObject {
// TODO: Once it is time to implement draft syncing with relays, please consider the following: // TODO: Once it is time to implement draft syncing with relays, please consider the following:
// - Privacy: Sending drafts to the network leaks metadata about app activity, and may break user expectations // - Privacy: Sending drafts to the network leaks metadata about app activity, and may break user expectations
// - Down-sync conflict resolution: Consider how to solve conflicts for different draft versions holding the same ID (e.g. edited in Damus, then another client, then Damus again) // - Down-sync conflict resolution: Consider how to solve conflicts for different draft versions holding the same ID (e.g. edited in Damus, then another client, then Damus again)
damus_state.nostrNetwork.sendToNostrDB(event: draft_event) await damus_state.nostrNetwork.sendToNostrDB(event: draft_event)
} }
DispatchQueue.main.async { DispatchQueue.main.async {
+22 -9
View File
@@ -60,7 +60,14 @@ class PostBox {
init(pool: RelayPool) { init(pool: RelayPool) {
self.pool = pool self.pool = pool
self.events = [:] self.events = [:]
Task { await pool.register_handler(sub_id: "postbox", filters: nil, to: nil, handler: handle_event) } Task {
let stream = AsyncStream<(RelayURL, NostrConnectionEvent)> { streamContinuation in
Task { await self.pool.register_handler(sub_id: "postbox", filters: nil, to: nil, handler: streamContinuation) }
}
for await (relayUrl, connectionEvent) in stream {
handle_event(relay_id: relayUrl, connectionEvent)
}
}
} }
// only works reliably on delay-sent events // only works reliably on delay-sent events
@@ -81,7 +88,7 @@ class PostBox {
return nil return nil
} }
func try_flushing_events() { func try_flushing_events() async {
let now = Int64(Date().timeIntervalSince1970) let now = Int64(Date().timeIntervalSince1970)
for kv in events { for kv in events {
let event = kv.value let event = kv.value
@@ -95,7 +102,7 @@ class PostBox {
if relayer.last_attempt == nil || if relayer.last_attempt == nil ||
(now >= (relayer.last_attempt! + Int64(relayer.retry_after))) { (now >= (relayer.last_attempt! + Int64(relayer.retry_after))) {
print("attempt #\(relayer.attempts) to flush event '\(event.event.content)' to \(relayer.relay) after \(relayer.retry_after) seconds") print("attempt #\(relayer.attempts) to flush event '\(event.event.content)' to \(relayer.relay) after \(relayer.retry_after) seconds")
flush_event(event, to_relay: relayer) await flush_event(event, to_relay: relayer)
} }
} }
} }
@@ -140,7 +147,7 @@ class PostBox {
return prev_count != after_count return prev_count != after_count
} }
private func flush_event(_ event: PostedEvent, to_relay: Relayer? = nil) { private func flush_event(_ event: PostedEvent, to_relay: Relayer? = nil) async {
var relayers = event.remaining var relayers = event.remaining
if let to_relay { if let to_relay {
relayers = [to_relay] relayers = [to_relay]
@@ -150,29 +157,35 @@ class PostBox {
relayer.attempts += 1 relayer.attempts += 1
relayer.last_attempt = Int64(Date().timeIntervalSince1970) relayer.last_attempt = Int64(Date().timeIntervalSince1970)
relayer.retry_after *= 1.5 relayer.retry_after *= 1.5
if pool.get_relay(relayer.relay) != nil { if await pool.get_relay(relayer.relay) != nil {
print("flushing event \(event.event.id) to \(relayer.relay)") print("flushing event \(event.event.id) to \(relayer.relay)")
} else { } else {
print("could not find relay when flushing: \(relayer.relay)") print("could not find relay when flushing: \(relayer.relay)")
} }
pool.send(.event(event.event), to: [relayer.relay], skip_ephemeral: event.skip_ephemeral) await pool.send(.event(event.event), to: [relayer.relay], skip_ephemeral: event.skip_ephemeral)
} }
} }
func send(_ event: NostrEvent, to: [RelayURL]? = nil, skip_ephemeral: Bool = true, delay: TimeInterval? = nil, on_flush: OnFlush? = nil) { func send(_ event: NostrEvent, to: [RelayURL]? = nil, skip_ephemeral: Bool = true, delay: TimeInterval? = nil, on_flush: OnFlush? = nil) async {
// Don't add event if we already have it // Don't add event if we already have it
if events[event.id] != nil { if events[event.id] != nil {
return return
} }
let remaining = to ?? pool.our_descriptors.map { $0.url } let remaining: [RelayURL]
if let to {
remaining = to
}
else {
remaining = await pool.our_descriptors.map { $0.url }
}
let after = delay.map { d in Date.now.addingTimeInterval(d) } let after = delay.map { d in Date.now.addingTimeInterval(d) }
let posted_ev = PostedEvent(event: event, remaining: remaining, skip_ephemeral: skip_ephemeral, flush_after: after, on_flush: on_flush) let posted_ev = PostedEvent(event: event, remaining: remaining, skip_ephemeral: skip_ephemeral, flush_after: after, on_flush: on_flush)
events[event.id] = posted_ev events[event.id] = posted_ev
if after == nil { if after == nil {
flush_event(posted_ev) await flush_event(posted_ev)
} }
} }
} }
+10 -10
View File
@@ -121,8 +121,8 @@ struct PostView: View {
uploadTasks.removeAll() uploadTasks.removeAll()
} }
func send_post() { func send_post() async {
let new_post = build_post(state: self.damus_state, post: self.post, action: action, uploadedMedias: uploadedMedias, references: self.references, filtered_pubkeys: filtered_pubkeys) let new_post = await build_post(state: self.damus_state, post: self.post, action: action, uploadedMedias: uploadedMedias, references: self.references, filtered_pubkeys: filtered_pubkeys)
notify(.post(.post(new_post))) notify(.post(.post(new_post)))
@@ -190,7 +190,7 @@ struct PostView: View {
var PostButton: some View { var PostButton: some View {
Button(NSLocalizedString("Post", comment: "Button to post a note.")) { Button(NSLocalizedString("Post", comment: "Button to post a note.")) {
self.send_post() Task { await self.send_post() }
} }
.disabled(posting_disabled) .disabled(posting_disabled)
.opacity(posting_disabled ? 0.5 : 1.0) .opacity(posting_disabled ? 0.5 : 1.0)
@@ -829,8 +829,8 @@ func nip10_reply_tags(replying_to: NostrEvent, keypair: Keypair, relayURL: Relay
return tags return tags
} }
func build_post(state: DamusState, action: PostAction, draft: DraftArtifacts) -> NostrPost { func build_post(state: DamusState, action: PostAction, draft: DraftArtifacts) async -> NostrPost {
return build_post( return await build_post(
state: state, state: state,
post: draft.content, post: draft.content,
action: action, action: action,
@@ -840,7 +840,7 @@ func build_post(state: DamusState, action: PostAction, draft: DraftArtifacts) ->
) )
} }
func build_post(state: DamusState, post: NSAttributedString, action: PostAction, uploadedMedias: [UploadedMedia], references: [RefId], filtered_pubkeys: Set<Pubkey>) -> NostrPost { func build_post(state: DamusState, post: NSAttributedString, action: PostAction, uploadedMedias: [UploadedMedia], references: [RefId], filtered_pubkeys: Set<Pubkey>) async -> NostrPost {
// don't add duplicate pubkeys but retain order // don't add duplicate pubkeys but retain order
var pkset = Set<Pubkey>() var pkset = Set<Pubkey>()
@@ -858,7 +858,7 @@ func build_post(state: DamusState, post: NSAttributedString, action: PostAction,
acc.append(pk) acc.append(pk)
} }
return build_post(state: state, post: post, action: action, uploadedMedias: uploadedMedias, pubkeys: pks) return await build_post(state: state, post: post, action: action, uploadedMedias: uploadedMedias, pubkeys: pks)
} }
/// This builds a Nostr post from draft data from `PostView` or other draft-related classes /// This builds a Nostr post from draft data from `PostView` or other draft-related classes
@@ -874,7 +874,7 @@ func build_post(state: DamusState, post: NSAttributedString, action: PostAction,
/// - uploadedMedias: The medias attached to this post /// - uploadedMedias: The medias attached to this post
/// - pubkeys: The referenced pubkeys /// - pubkeys: The referenced pubkeys
/// - Returns: A NostrPost, which can then be signed into an event. /// - Returns: A NostrPost, which can then be signed into an event.
func build_post(state: DamusState, post: NSAttributedString, action: PostAction, uploadedMedias: [UploadedMedia], pubkeys: [Pubkey]) -> NostrPost { func build_post(state: DamusState, post: NSAttributedString, action: PostAction, uploadedMedias: [UploadedMedia], pubkeys: [Pubkey]) async -> NostrPost {
let post = NSMutableAttributedString(attributedString: post) let post = NSMutableAttributedString(attributedString: post)
post.enumerateAttributes(in: NSRange(location: 0, length: post.length), options: []) { attributes, range, stop in post.enumerateAttributes(in: NSRange(location: 0, length: post.length), options: []) { attributes, range, stop in
let linkValue = attributes[.link] let linkValue = attributes[.link]
@@ -916,10 +916,10 @@ func build_post(state: DamusState, post: NSAttributedString, action: PostAction,
switch action { switch action {
case .replying_to(let replying_to): case .replying_to(let replying_to):
// start off with the reply tags // start off with the reply tags
tags = nip10_reply_tags(replying_to: replying_to, keypair: state.keypair, relayURL: state.nostrNetwork.relaysForEvent(event: replying_to).first) tags = nip10_reply_tags(replying_to: replying_to, keypair: state.keypair, relayURL: await state.nostrNetwork.relaysForEvent(event: replying_to).first)
case .quoting(let ev): case .quoting(let ev):
let relay_urls = state.nostrNetwork.relaysForEvent(event: ev) let relay_urls = await state.nostrNetwork.relaysForEvent(event: ev)
let nevent = Bech32Object.encode(.nevent(NEvent(event: ev, relays: relay_urls.prefix(4).map { $0 }))) let nevent = Bech32Object.encode(.nevent(NEvent(event: ev, relays: relay_urls.prefix(4).map { $0 })))
content.append("\n\nnostr:\(nevent)") content.append("\n\nnostr:\(nevent)")
@@ -58,7 +58,7 @@ struct EditMetadataView: View {
return profile return profile
} }
func save() { func save() async {
let profile = to_profile() let profile = to_profile()
guard let keypair = damus_state.keypair.to_full(), guard let keypair = damus_state.keypair.to_full(),
let metadata_ev = make_metadata_event(keypair: keypair, metadata: profile) let metadata_ev = make_metadata_event(keypair: keypair, metadata: profile)
@@ -66,7 +66,7 @@ struct EditMetadataView: View {
return return
} }
damus_state.nostrNetwork.postbox.send(metadata_ev) await damus_state.nostrNetwork.postbox.send(metadata_ev)
} }
func is_ln_valid(ln: String) -> Bool { func is_ln_valid(ln: String) -> Bool {
@@ -211,9 +211,11 @@ struct EditMetadataView: View {
if !ln.isEmpty && !is_ln_valid(ln: ln) { if !ln.isEmpty && !is_ln_valid(ln: ln) {
confirm_ln_address = true confirm_ln_address = true
} else { } else {
save() Task {
await save()
dismiss() dismiss()
} }
}
}, label: { }, label: {
Text(NSLocalizedString("Save", comment: "Button for saving profile.")) Text(NSLocalizedString("Save", comment: "Button for saving profile."))
.frame(minWidth: 300, maxWidth: .infinity, alignment: .center) .frame(minWidth: 300, maxWidth: .infinity, alignment: .center)
@@ -219,7 +219,7 @@ struct ProfileView: View {
} }
damus_state.mutelist_manager.set_mutelist(new_ev) damus_state.mutelist_manager.set_mutelist(new_ev)
damus_state.nostrNetwork.postbox.send(new_ev) Task { await damus_state.nostrNetwork.postbox.send(new_ev) }
} }
} else { } else {
Button(NSLocalizedString("Mute", comment: "Button to mute a profile"), role: .destructive) { Button(NSLocalizedString("Mute", comment: "Button to mute a profile"), role: .destructive) {
@@ -80,6 +80,7 @@ struct AddRelayView: View {
} }
Button(action: { Button(action: {
Task {
if new_relay.starts(with: "wss://") == false && new_relay.starts(with: "ws://") == false { if new_relay.starts(with: "wss://") == false && new_relay.starts(with: "ws://") == false {
new_relay = "wss://" + new_relay new_relay = "wss://" + new_relay
} }
@@ -91,7 +92,7 @@ struct AddRelayView: View {
} }
do { do {
try state.nostrNetwork.userRelayList.insert(relay: NIP65.RelayList.RelayItem(url: url, rwConfiguration: .readWrite)) try await state.nostrNetwork.userRelayList.insert(relay: NIP65.RelayList.RelayItem(url: url, rwConfiguration: .readWrite))
relayAddErrorTitle = nil // Clear error title relayAddErrorTitle = nil // Clear error title
relayAddErrorMessage = nil // Clear error message relayAddErrorMessage = nil // Clear error message
} }
@@ -104,6 +105,7 @@ struct AddRelayView: View {
this_app.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) this_app.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
dismiss() dismiss()
}
}) { }) {
HStack { HStack {
Text("Add relay", comment: "Button to add a relay.") Text("Add relay", comment: "Button to add a relay.")
@@ -55,7 +55,7 @@ class SearchHomeModel: ObservableObject {
DispatchQueue.main.async { DispatchQueue.main.async {
self.loading = true self.loading = true
} }
let to_relays = damus_state.nostrNetwork.ourRelayDescriptors let to_relays = await damus_state.nostrNetwork.ourRelayDescriptors
.map { $0.url } .map { $0.url }
.filter { !damus_state.relay_filters.is_filtered(timeline: .search, relay_id: $0) } .filter { !damus_state.relay_filters.is_filtered(timeline: .search, relay_id: $0) }
@@ -125,7 +125,7 @@ struct HashtagUnfollowButton: View {
func unfollow(_ hashtag: String) { func unfollow(_ hashtag: String) {
is_following = false is_following = false
handle_unfollow(state: damus_state, unfollow: FollowRef.hashtag(hashtag)) Task { await handle_unfollow(state: damus_state, unfollow: FollowRef.hashtag(hashtag)) }
} }
} }
@@ -144,7 +144,7 @@ struct HashtagFollowButton: View {
func follow(_ hashtag: String) { func follow(_ hashtag: String) {
is_following = true is_following = true
handle_follow(state: damus_state, follow: .hashtag(hashtag)) Task { await handle_follow(state: damus_state, follow: .hashtag(hashtag)) }
} }
} }
+2 -2
View File
@@ -69,7 +69,7 @@ struct SearchView: View {
} }
appstate.mutelist_manager.set_mutelist(mutelist) appstate.mutelist_manager.set_mutelist(mutelist)
appstate.nostrNetwork.postbox.send(mutelist) Task { await appstate.nostrNetwork.postbox.send(mutelist) }
} label: { } label: {
Text("Unmute Hashtag", comment: "Label represnting a button that the user can tap to unmute a given hashtag so they start seeing it in their feed again.") Text("Unmute Hashtag", comment: "Label represnting a button that the user can tap to unmute a given hashtag so they start seeing it in their feed again.")
} }
@@ -104,7 +104,7 @@ struct SearchView: View {
} }
appstate.mutelist_manager.set_mutelist(mutelist) appstate.mutelist_manager.set_mutelist(mutelist)
appstate.nostrNetwork.postbox.send(mutelist) Task { await appstate.nostrNetwork.postbox.send(mutelist) }
} }
var described_search: DescribedSearch { var described_search: DescribedSearch {
@@ -182,10 +182,12 @@ struct ConfigView: View {
let ev = created_deleted_account_profile(keypair: keypair) else { let ev = created_deleted_account_profile(keypair: keypair) else {
return return
} }
state.nostrNetwork.postbox.send(ev) Task {
await state.nostrNetwork.postbox.send(ev)
logout(state) logout(state)
} }
} }
}
.alert(NSLocalizedString("Logout", comment: "Alert for logging out the user."), isPresented: $confirm_logout) { .alert(NSLocalizedString("Logout", comment: "Alert for logging out the user."), isPresented: $confirm_logout) {
Button(NSLocalizedString("Cancel", comment: "Cancel out of logging out the user."), role: .cancel) { Button(NSLocalizedString("Cancel", comment: "Cancel out of logging out the user."), role: .cancel) {
confirm_logout = false confirm_logout = false
@@ -68,13 +68,13 @@ struct FirstAidSettingsView: View {
guard let new_contact_list_event = make_first_contact_event(keypair: damus_state.keypair) else { guard let new_contact_list_event = make_first_contact_event(keypair: damus_state.keypair) else {
throw FirstAidError.cannotMakeFirstContactEvent throw FirstAidError.cannotMakeFirstContactEvent
} }
damus_state.nostrNetwork.send(event: new_contact_list_event) await damus_state.nostrNetwork.send(event: new_contact_list_event)
damus_state.settings.latest_contact_event_id_hex = new_contact_list_event.id.hex() damus_state.settings.latest_contact_event_id_hex = new_contact_list_event.id.hex()
} }
func resetRelayList() async throws { func resetRelayList() async throws {
let bestEffortRelayList = damus_state.nostrNetwork.userRelayList.getBestEffortRelayList() let bestEffortRelayList = damus_state.nostrNetwork.userRelayList.getBestEffortRelayList()
try damus_state.nostrNetwork.userRelayList.set(userRelayList: bestEffortRelayList) try await damus_state.nostrNetwork.userRelayList.set(userRelayList: bestEffortRelayList)
} }
enum FirstAidError: Error { enum FirstAidError: Error {
@@ -109,6 +109,7 @@ struct UserStatusSheet: View {
Spacer() Spacer()
Button(action: { Button(action: {
Task {
guard let status = self.status.general, guard let status = self.status.general,
let kp = keypair.to_full(), let kp = keypair.to_full(),
let ev = make_user_status_note(status: status, keypair: kp, expiry: duration.expiration) let ev = make_user_status_note(status: status, keypair: kp, expiry: duration.expiration)
@@ -116,9 +117,10 @@ struct UserStatusSheet: View {
return return
} }
postbox.send(ev) await postbox.send(ev)
dismiss() dismiss()
}
}, label: { }, label: {
Text("Share", comment: "Save button text for saving profile status settings.") Text("Share", comment: "Save button text for saving profile status settings.")
}) })
@@ -812,13 +812,15 @@ class HomeModel: ContactsDelegate, ObservableObject {
} }
func update_signal_from_pool(signal: SignalModel, pool: RelayPool) { func update_signal_from_pool(signal: SignalModel, pool: RelayPool) async {
if signal.max_signal != pool.relays.count { let relayCount = await pool.relays.count
signal.max_signal = pool.relays.count if signal.max_signal != relayCount {
signal.max_signal = relayCount
} }
if signal.signal != pool.num_connected { let numberOfConnectedRelays = await pool.num_connected
signal.signal = pool.num_connected if signal.signal != numberOfConnectedRelays {
signal.signal = numberOfConnectedRelays
} }
} }
@@ -17,14 +17,14 @@ extension WalletConnect {
/// - Parameters: /// - Parameters:
/// - url: The Nostr Wallet Connect URL containing connection info to the NWC wallet /// - url: The Nostr Wallet Connect URL containing connection info to the NWC wallet
/// - pool: The RelayPool to send the subscription request through /// - pool: The RelayPool to send the subscription request through
static func subscribe(url: WalletConnectURL, pool: RelayPool) { static func subscribe(url: WalletConnectURL, pool: RelayPool) async {
var filter = NostrFilter(kinds: [.nwc_response]) var filter = NostrFilter(kinds: [.nwc_response])
filter.authors = [url.pubkey] filter.authors = [url.pubkey]
filter.pubkeys = [url.keypair.pubkey] filter.pubkeys = [url.keypair.pubkey]
filter.limit = 0 filter.limit = 0
let sub = NostrSubscribe(filters: [filter], sub_id: "nwc") let sub = NostrSubscribe(filters: [filter], sub_id: "nwc")
pool.send(.subscribe(sub), to: [url.relay], skip_ephemeral: false) await pool.send(.subscribe(sub), to: [url.relay], skip_ephemeral: false)
} }
/// Sends out a request to pay an invoice to the NWC relay, and ensures that: /// Sends out a request to pay an invoice to the NWC relay, and ensures that:
@@ -41,16 +41,16 @@ extension WalletConnect {
/// - on_flush: A callback to call after the event has been flushed to the network /// - on_flush: A callback to call after the event has been flushed to the network
/// - Returns: The Nostr Event that was sent to the network, representing the request that was made /// - Returns: The Nostr Event that was sent to the network, representing the request that was made
@discardableResult @discardableResult
static func pay(url: WalletConnectURL, pool: RelayPool, post: PostBox, invoice: String, zap_request: NostrEvent?, delay: TimeInterval? = 5.0, on_flush: OnFlush? = nil) -> NostrEvent? { static func pay(url: WalletConnectURL, pool: RelayPool, post: PostBox, invoice: String, zap_request: NostrEvent?, delay: TimeInterval? = 5.0, on_flush: OnFlush? = nil) async -> NostrEvent? {
let req = WalletConnect.Request.payZapRequest(invoice: invoice, zapRequest: zap_request) let req = WalletConnect.Request.payZapRequest(invoice: invoice, zapRequest: zap_request)
guard let ev = req.to_nostr_event(to_pk: url.pubkey, keypair: url.keypair) else { guard let ev = req.to_nostr_event(to_pk: url.pubkey, keypair: url.keypair) else {
return nil return nil
} }
try? pool.add_relay(.nwc(url: url.relay)) // Ensure the NWC relay is connected try? await pool.add_relay(.nwc(url: url.relay)) // Ensure the NWC relay is connected
WalletConnect.subscribe(url: url, pool: pool) // Ensure we are listening to NWC updates from the relay await WalletConnect.subscribe(url: url, pool: pool) // Ensure we are listening to NWC updates from the relay
post.send(ev, to: [url.relay], skip_ephemeral: false, delay: delay, on_flush: on_flush) await post.send(ev, to: [url.relay], skip_ephemeral: false, delay: delay, on_flush: on_flush)
return ev return ev
} }
@@ -181,7 +181,7 @@ class WalletModel: ObservableObject {
) )
] ]
nostrNetwork.send(event: requestEvent, to: [currentNwcUrl.relay], skipEphemeralRelays: false) await nostrNetwork.send(event: requestEvent, to: [currentNwcUrl.relay], skipEphemeralRelays: false)
for await event in nostrNetwork.reader.timedStream(filters: responseFilters, to: [currentNwcUrl.relay], timeout: timeout) { for await event in nostrNetwork.reader.timedStream(filters: responseFilters, to: [currentNwcUrl.relay], timeout: timeout) {
guard let responseEvent = try? event.getCopy() else { throw .internalError } guard let responseEvent = try? event.getCopy() else { throw .internalError }
@@ -268,7 +268,7 @@ struct NWCSettings: View {
guard let meta = make_metadata_event(keypair: keypair, metadata: prof) else { guard let meta = make_metadata_event(keypair: keypair, metadata: prof) else {
return return
} }
damus_state.nostrNetwork.postbox.send(meta) Task { await damus_state.nostrNetwork.postbox.send(meta) }
} }
} }
@@ -182,10 +182,11 @@ struct SendPaymentView: View {
.buttonStyle(NeutralButtonStyle()) .buttonStyle(NeutralButtonStyle())
Button(action: { Button(action: {
Task {
sendState = .processing sendState = .processing
// Process payment // Process payment
guard let payRequestEv = damus_state.nostrNetwork.nwcPay(url: nwc, post: damus_state.nostrNetwork.postbox, invoice: invoice.string, zap_request: nil) else { guard let payRequestEv = await damus_state.nostrNetwork.nwcPay(url: nwc, post: damus_state.nostrNetwork.postbox, invoice: invoice.string, zap_request: nil) else {
sendState = .failed(error: .init( sendState = .failed(error: .init(
user_visible_description: NSLocalizedString("The payment request could not be made to your wallet provider.", comment: "A human-readable error message"), user_visible_description: NSLocalizedString("The payment request could not be made to your wallet provider.", comment: "A human-readable error message"),
tip: NSLocalizedString("Check if your wallet looks configured correctly and try again. If the error persists, please contact support.", comment: "A human-readable tip for an error when a payment request cannot be made to a wallet."), tip: NSLocalizedString("Check if your wallet looks configured correctly and try again. If the error persists, please contact support.", comment: "A human-readable tip for an error when a payment request cannot be made to a wallet."),
@@ -193,7 +194,6 @@ struct SendPaymentView: View {
)) ))
return return
} }
Task {
do { do {
let result = try await model.waitForResponse(for: payRequestEv.id, timeout: SEND_PAYMENT_TIMEOUT) let result = try await model.waitForResponse(for: payRequestEv.id, timeout: SEND_PAYMENT_TIMEOUT)
guard case .pay_invoice(_) = result else { guard case .pay_invoice(_) = result else {
+1 -1
View File
@@ -95,7 +95,7 @@ class Zaps {
event_counts[note_id] = event_counts[note_id]! + 1 event_counts[note_id] = event_counts[note_id]! + 1
event_totals[note_id] = event_totals[note_id]! + zap.amount event_totals[note_id] = event_totals[note_id]! + zap.amount
notify(.update_stats(note_id: note_id)) Task { await notify(.update_stats(note_id: note_id)) }
} }
} }
} }
@@ -179,7 +179,7 @@ func send_zap(damus_state: DamusState, target: ZapTarget, lnurl: String, is_cust
} }
// Only take the first 10 because reasons // Only take the first 10 because reasons
let relays = Array(damus_state.nostrNetwork.ourRelayDescriptors.prefix(10)) let relays = Array(await damus_state.nostrNetwork.ourRelayDescriptors.prefix(10))
let content = comment ?? "" let content = comment ?? ""
guard let mzapreq = make_zap_request_event(keypair: keypair, content: content, relays: relays, target: target, zap_type: zap_type) else { guard let mzapreq = make_zap_request_event(keypair: keypair, content: content, relays: relays, target: target, zap_type: zap_type) else {
@@ -240,7 +240,7 @@ func send_zap(damus_state: DamusState, target: ZapTarget, lnurl: String, is_cust
// we don't have a delay on one-tap nozaps (since this will be from customize zap view) // we don't have a delay on one-tap nozaps (since this will be from customize zap view)
let delay = damus_state.settings.nozaps ? nil : 5.0 let delay = damus_state.settings.nozaps ? nil : 5.0
let nwc_req = damus_state.nostrNetwork.nwcPay(url: nwc_state.url, post: damus_state.nostrNetwork.postbox, invoice: inv, delay: delay, on_flush: flusher) let nwc_req = await damus_state.nostrNetwork.nwcPay(url: nwc_state.url, post: damus_state.nostrNetwork.postbox, invoice: inv, delay: delay, on_flush: flusher)
guard let nwc_req, case .nwc(let pzap_state) = pending_zap_state else { guard let nwc_req, case .nwc(let pzap_state) = pending_zap_state else {
print("nwc: failed to send nwc request for zapreq \(reqid.reqid)") print("nwc: failed to send nwc request for zapreq \(reqid.reqid)")
+2
View File
@@ -33,8 +33,10 @@ struct NotifyHandler<T> { }
func notify<T: Notify>(_ notify: Notifications<T>) { func notify<T: Notify>(_ notify: Notifications<T>) {
let notify = notify.notify let notify = notify.notify
DispatchQueue.main.async {
NotificationCenter.default.post(name: T.name, object: notify.payload) NotificationCenter.default.post(name: T.name, object: notify.payload)
} }
}
func handle_notify<T: Notify>(_ handler: NotifyHandler<T>) -> AnyPublisher<T.Payload, Never> { func handle_notify<T: Notify>(_ handler: NotifyHandler<T>) -> AnyPublisher<T.Payload, Never> {
return NotificationCenter.default.publisher(for: T.name) return NotificationCenter.default.publisher(for: T.name)
@@ -37,6 +37,6 @@ extension Notifications {
/// The requests from this function will be received and handled at the top level app view (`ContentView`), which contains a `.damus_full_screen_cover`. /// The requests from this function will be received and handled at the top level app view (`ContentView`), which contains a `.damus_full_screen_cover`.
/// ///
func present(full_screen_item: FullScreenItem) { func present(full_screen_item: FullScreenItem) {
notify(.present_full_screen_item(full_screen_item)) Task { await notify(.present_full_screen_item(full_screen_item)) }
} }
@@ -135,7 +135,7 @@ struct ShareExtensionView: View {
return return
} }
self.state = DamusState(keypair: keypair) self.state = DamusState(keypair: keypair)
self.state?.nostrNetwork.connect() Task { await self.state?.nostrNetwork.connect() }
}) })
.onChange(of: self.highlighter_state) { .onChange(of: self.highlighter_state) {
if case .cancelled = highlighter_state { if case .cancelled = highlighter_state {
@@ -145,7 +145,7 @@ struct ShareExtensionView: View {
.onReceive(handle_notify(.post)) { post_notification in .onReceive(handle_notify(.post)) { post_notification in
switch post_notification { switch post_notification {
case .post(let post): case .post(let post):
self.post(post) Task { await self.post(post) }
case .cancel: case .cancel:
self.highlighter_state = .cancelled self.highlighter_state = .cancelled
} }
@@ -164,7 +164,7 @@ struct ShareExtensionView: View {
break break
case .active: case .active:
print("txn: 📙 HIGHLIGHTER ACTIVE") print("txn: 📙 HIGHLIGHTER ACTIVE")
state.nostrNetwork.ping() Task { await state.nostrNetwork.ping() }
@unknown default: @unknown default:
break break
} }
@@ -225,7 +225,7 @@ struct ShareExtensionView: View {
} }
} }
func post(_ post: NostrPost) { func post(_ post: NostrPost) async {
self.highlighter_state = .posting self.highlighter_state = .posting
guard let state else { guard let state else {
self.highlighter_state = .failed(error: "Damus state not initialized") self.highlighter_state = .failed(error: "Damus state not initialized")
@@ -239,7 +239,7 @@ struct ShareExtensionView: View {
self.highlighter_state = .failed(error: "Cannot convert post data into a nostr event") self.highlighter_state = .failed(error: "Cannot convert post data into a nostr event")
return return
} }
state.nostrNetwork.postbox.send(posted_event, on_flush: .once({ flushed_event in await state.nostrNetwork.postbox.send(posted_event, on_flush: .once({ flushed_event in
if flushed_event.event.id == posted_event.id { if flushed_event.event.id == posted_event.id {
DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: { // Offset labor perception bias DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: { // Offset labor perception bias
self.highlighter_state = .posted(event: flushed_event.event) self.highlighter_state = .posted(event: flushed_event.event)
+12
View File
@@ -64,7 +64,19 @@ enum NdbNoteLender: Sendable {
case .owned(let note): case .owned(let note):
return try lendingFunction(UnownedNdbNote(note)) return try lendingFunction(UnownedNdbNote(note))
} }
}
/// Borrows the note temporarily (asynchronously)
func borrow<T>(_ lendingFunction: (_: borrowing UnownedNdbNote) async throws -> T) async throws -> T {
switch self {
case .ndbNoteKey(let ndb, let noteKey):
guard !ndb.is_closed else { throw LendingError.ndbClosed }
guard let ndbNoteTxn = ndb.lookup_note_by_key(noteKey) else { throw LendingError.errorLoadingNote }
guard let unownedNote = UnownedNdbNote(ndbNoteTxn) else { throw LendingError.errorLoadingNote }
return try await lendingFunction(unownedNote)
case .owned(let note):
return try await lendingFunction(UnownedNdbNote(note))
}
} }
/// Gets an owned copy of the note /// Gets an owned copy of the note
+6 -7
View File
@@ -310,7 +310,10 @@ public func nscript_nostr_cmd(interp: UnsafeMutablePointer<wasm_interp>?, cmd: I
func nscript_add_relay(script: NostrScript, relay: String) -> Bool { func nscript_add_relay(script: NostrScript, relay: String) -> Bool {
guard let url = RelayURL(relay) else { return false } guard let url = RelayURL(relay) else { return false }
let desc = RelayPool.RelayDescriptor(url: url, info: .readWrite, variant: .ephemeral) let desc = RelayPool.RelayDescriptor(url: url, info: .readWrite, variant: .ephemeral)
return (try? script.pool.add_relay(desc)) != nil // Interacting with RelayPool needs to be done asynchronously, thus we cannot return the answer synchronously
// return (try? await script.pool.add_relay(desc)) != nil
Task { try await script.pool.add_relay(desc) }
return true
} }
@@ -344,9 +347,7 @@ public func nscript_pool_send_to(interp: UnsafeMutablePointer<wasm_interp>?, pre
return 0 return 0
} }
DispatchQueue.main.async { Task { await script.pool.send_raw(.custom(req_str), to: [to_relay_url], skip_ephemeral: false) }
script.pool.send_raw(.custom(req_str), to: [to_relay_url], skip_ephemeral: false)
}
return 1; return 1;
} }
@@ -354,9 +355,7 @@ public func nscript_pool_send_to(interp: UnsafeMutablePointer<wasm_interp>?, pre
func nscript_pool_send(script: NostrScript, req req_str: String) -> Int32 { func nscript_pool_send(script: NostrScript, req req_str: String) -> Int32 {
//script.test("pool_send: '\(req_str)'") //script.test("pool_send: '\(req_str)'")
DispatchQueue.main.sync { Task { await script.pool.send_raw(.custom(req_str), skip_ephemeral: false) }
script.pool.send_raw(.custom(req_str), skip_ephemeral: false)
}
return 1; return 1;
} }
+5 -5
View File
@@ -173,7 +173,7 @@ struct ShareExtensionView: View {
.onReceive(handle_notify(.post)) { post_notification in .onReceive(handle_notify(.post)) { post_notification in
switch post_notification { switch post_notification {
case .post(let post): case .post(let post):
self.post(post) Task { await self.post(post) }
case .cancel: case .cancel:
self.share_state = .cancelled self.share_state = .cancelled
dismissParent?() dismissParent?()
@@ -193,7 +193,7 @@ struct ShareExtensionView: View {
break break
case .active: case .active:
print("txn: 📙 SHARE ACTIVE") print("txn: 📙 SHARE ACTIVE")
state.nostrNetwork.ping() Task { await state.nostrNetwork.ping() }
@unknown default: @unknown default:
break break
} }
@@ -216,7 +216,7 @@ struct ShareExtensionView: View {
} }
} }
func post(_ post: NostrPost) { func post(_ post: NostrPost) async {
self.share_state = .posting self.share_state = .posting
guard let state else { guard let state else {
self.share_state = .failed(error: "Damus state not initialized") self.share_state = .failed(error: "Damus state not initialized")
@@ -230,7 +230,7 @@ struct ShareExtensionView: View {
self.share_state = .failed(error: "Cannot convert post data into a nostr event") self.share_state = .failed(error: "Cannot convert post data into a nostr event")
return return
} }
state.nostrNetwork.postbox.send(posted_event, on_flush: .once({ flushed_event in await state.nostrNetwork.postbox.send(posted_event, on_flush: .once({ flushed_event in
if flushed_event.event.id == posted_event.id { if flushed_event.event.id == posted_event.id {
DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: { // Offset labor perception bias DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: { // Offset labor perception bias
self.share_state = .posted(event: flushed_event.event) self.share_state = .posted(event: flushed_event.event)
@@ -250,7 +250,7 @@ struct ShareExtensionView: View {
return false return false
} }
state = DamusState(keypair: keypair) state = DamusState(keypair: keypair)
state?.nostrNetwork.connect() Task { await state?.nostrNetwork.connect() }
return true return true
} }