diff --git a/damus.xcodeproj/xcshareddata/xcschemes/damus.xcscheme b/damus.xcodeproj/xcshareddata/xcschemes/damus.xcscheme index b59a9803..5ff8e186 100644 --- a/damus.xcodeproj/xcshareddata/xcschemes/damus.xcscheme +++ b/damus.xcodeproj/xcshareddata/xcschemes/damus.xcscheme @@ -55,7 +55,6 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - enableAddressSanitizer = "YES" launchStyle = "0" useCustomWorkingDirectory = "NO" ignoresPersistentStateOnLaunch = "NO" diff --git a/damus/ContentView.swift b/damus/ContentView.swift index 46c468c1..7799fb83 100644 --- a/damus/ContentView.swift +++ b/damus/ContentView.swift @@ -300,16 +300,18 @@ struct ContentView: View { .ignoresSafeArea(.keyboard) .edgesIgnoringSafeArea(hide_bar ? [.bottom] : []) .onAppear() { - self.connect() - try? AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category.playback, mode: .default, options: .mixWithOthers) - setup_notifications() - if !hasSeenOnboardingSuggestions || damus_state!.settings.always_show_onboarding_suggestions { - active_sheet = .onboardingSuggestions - hasSeenOnboardingSuggestions = true - } - self.appDelegate?.state = damus_state - Task { // We probably don't need this to be a detached task. According to https://docs.swift.org/swift-book/documentation/the-swift-programming-language/concurrency/#Defining-and-Calling-Asynchronous-Functions, awaits are only suspension points that do not block the thread. - await self.listenAndHandleLocalNotifications() + Task { + await self.connect() + try? AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category.playback, mode: .default, options: .mixWithOthers) + setup_notifications() + if !hasSeenOnboardingSuggestions || damus_state!.settings.always_show_onboarding_suggestions { + active_sheet = .onboardingSuggestions + hasSeenOnboardingSuggestions = true + } + self.appDelegate?.state = damus_state + Task { // We probably don't need this to be a detached task. According to https://docs.swift.org/swift-book/documentation/the-swift-programming-language/concurrency/#Defining-and-Calling-Asynchronous-Functions, awaits are only suspension points that do not block the thread. + await self.listenAndHandleLocalNotifications() + } } } .sheet(item: $active_sheet) { item in @@ -371,7 +373,7 @@ struct ContentView: View { self.hide_bar = !show } .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() } .onReceive(handle_notify(.report)) { target in @@ -382,45 +384,47 @@ struct ContentView: View { self.confirm_mute = true } .onReceive(handle_notify(.attached_wallet)) { nwc in - try? damus_state.nostrNetwork.userRelayList.load() // Reload relay list to apply changes - - // update the lightning address on our profile when we attach a - // wallet with an associated - guard let ds = self.damus_state, - let lud16 = nwc.lud16, - let keypair = ds.keypair.to_full(), - let profile_txn = ds.profiles.lookup(id: ds.pubkey), - let profile = profile_txn.unsafeUnownedValue, - lud16 != profile.lud16 else { - return + 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 + // wallet with an associated + guard let ds = self.damus_state, + let lud16 = nwc.lud16, + let keypair = ds.keypair.to_full(), + let profile_txn = ds.profiles.lookup(id: ds.pubkey), + let profile = profile_txn.unsafeUnownedValue, + lud16 != profile.lud16 else { + return + } + + // clear zapper cache for old lud16 + if profile.lud16 != nil { + // TODO: should this be somewhere else, where we process profile events!? + invalidate_zapper_cache(pubkey: keypair.pubkey, profiles: ds.profiles, lnurl: ds.lnurls) + } + + 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 } + await ds.nostrNetwork.postbox.send(ev) } - - // clear zapper cache for old lud16 - if profile.lud16 != nil { - // TODO: should this be somewhere else, where we process profile events!? - invalidate_zapper_cache(pubkey: keypair.pubkey, profiles: ds.profiles, lnurl: ds.lnurls) - } - - 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 } - ds.nostrNetwork.postbox.send(ev) } .onReceive(handle_notify(.broadcast)) { ev in 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 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 home.resubscribe(.unfollowing(unfollow)) } .onReceive(handle_notify(.follow)) { target in 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 home.resubscribe(.following) @@ -431,8 +435,10 @@ struct ContentView: View { return } - if !handle_post_notification(keypair: keypair, postbox: state.nostrNetwork.postbox, events: state.events, post: post) { - self.active_sheet = nil + Task { + if await !handle_post_notification(keypair: keypair, postbox: state.nostrNetwork.postbox, events: state.events, post: post) { + self.active_sheet = nil + } } } .onReceive(handle_notify(.new_mutes)) { _ in @@ -475,7 +481,7 @@ struct ContentView: View { } } .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 print("txn: 📙 DAMUS ACTIVE NOTIFY") @@ -540,27 +546,29 @@ struct ContentView: View { damusClosingTask = nil damus_state.ndb.reopen() // Pinging the network will automatically reconnect any dead websocket connections - damus_state.nostrNetwork.ping() + await damus_state.nostrNetwork.ping() } @unknown default: break } } .onReceive(handle_notify(.onlyzaps_mode)) { hide in - home.filter_events() - - guard let ds = damus_state, - let profile_txn = ds.profiles.lookup(id: ds.pubkey), - let profile = profile_txn.unsafeUnownedValue, - let keypair = ds.keypair.to_full() - else { - return + Task { + home.filter_events() + + guard let ds = damus_state, + let profile_txn = ds.profiles.lookup(id: ds.pubkey), + let profile = profile_txn.unsafeUnownedValue, + let keypair = ds.keypair.to_full() + else { + return + } + + 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 } + await ds.nostrNetwork.postbox.send(profile_ev) } - - 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 } - 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: { Button(NSLocalizedString("Thanks!", comment: "Button to close out of alert that informs that the action to muted a user was successful.")) { @@ -583,20 +591,22 @@ struct ContentView: View { } Button(NSLocalizedString("Yes, Overwrite", comment: "Text of button that confirms to overwrite the existing mutelist.")) { - guard let ds = damus_state, - let keypair = ds.keypair.to_full(), - let muting, - let mutelist = create_or_update_mutelist(keypair: keypair, mprev: nil, to_add: muting) - else { - return + Task { + guard let ds = damus_state, + let keypair = ds.keypair.to_full(), + let muting, + let mutelist = create_or_update_mutelist(keypair: keypair, mprev: nil, to_add: muting) + else { + return + } + + ds.mutelist_manager.set_mutelist(mutelist) + await ds.nostrNetwork.postbox.send(mutelist) + + confirm_overwrite_mutelist = false + confirm_mute = false + user_muted_confirm = true } - - ds.mutelist_manager.set_mutelist(mutelist) - ds.nostrNetwork.postbox.send(mutelist) - - confirm_overwrite_mutelist = false - confirm_mute = false - user_muted_confirm = true } }, 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.") @@ -624,7 +634,7 @@ struct ContentView: View { } ds.mutelist_manager.set_mutelist(ev) - ds.nostrNetwork.postbox.send(ev) + Task { await ds.nostrNetwork.postbox.send(ev) } } } }, message: { @@ -676,7 +686,7 @@ struct ContentView: View { self.execute_open_action(openAction) } - func connect() { + func connect() async { // nostrdb var mndb = Ndb() if mndb == nil { @@ -698,7 +708,7 @@ struct ContentView: View { 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, likes: EventCounter(our_pubkey: pubkey), @@ -756,7 +766,7 @@ struct ContentView: View { 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 DispatchQueue.main.asyncAfter(deadline: .now() + 0.25, execute: { self.home.send_initial_filters() @@ -764,26 +774,28 @@ struct ContentView: View { } func music_changed(_ state: MusicState) { - guard let damus_state else { return } - switch state { - case .playback_state: - break - case .song(let song): - guard let song, let kp = damus_state.keypair.to_full() else { return } - - let pdata = damus_state.profiles.profile_data(damus_state.pubkey) - - let desc = "\(song.title ?? "Unknown") - \(song.artist ?? "Unknown")" - let encodedDesc = desc.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) - let url = encodedDesc.flatMap { enc in - URL(string: "spotify:search:\(enc)") + Task { + guard let damus_state else { return } + switch state { + case .playback_state: + break + case .song(let song): + guard let song, let kp = damus_state.keypair.to_full() else { return } + + let pdata = damus_state.profiles.profile_data(damus_state.pubkey) + + let desc = "\(song.title ?? "Unknown") - \(song.artist ?? "Unknown")" + let encodedDesc = desc.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) + let url = encodedDesc.flatMap { enc in + URL(string: "spotify:search:\(enc)") + } + let music = UserStatus(type: .music, expires_at: Date.now.addingTimeInterval(song.playbackDuration), content: desc, created_at: UInt32(Date.now.timeIntervalSince1970), url: url) + + pdata.status.music = music + + guard let ev = music.to_note(keypair: kp) else { return } + await damus_state.nostrNetwork.postbox.send(ev) } - let music = UserStatus(type: .music, expires_at: Date.now.addingTimeInterval(song.playbackDuration), content: desc, created_at: UInt32(Date.now.timeIntervalSince1970), url: url) - - pdata.status.music = music - - guard let ev = music.to_note(keypair: kp) else { return } - 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() { this_app.registerForRemoteNotifications() let center = UNUserNotificationCenter.current() @@ -992,14 +1004,14 @@ func timeline_name(_ timeline: Timeline?) -> String { } @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 { return false } 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 { return false } @@ -1020,12 +1032,12 @@ func handle_unfollow(state: DamusState, unfollow: FollowRef) -> Bool { } @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 { 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 { return false } @@ -1045,7 +1057,7 @@ func handle_follow(state: DamusState, follow: FollowRef) -> Bool { } @discardableResult -func handle_follow_notif(state: DamusState, target: FollowTarget) -> Bool { +func handle_follow_notif(state: DamusState, target: FollowTarget) async -> Bool { switch target { case .pubkey(let 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) } - 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 { case .post(let post): //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 { return false } - postbox.send(new_ev) + await postbox.send(new_ev) for eref in new_ev.referenced_ids.prefix(3) { // also broadcast at most 3 referenced events if let ev = events.lookup(eref) { - postbox.send(ev) + await postbox.send(ev) } } for qref in new_ev.referenced_quote_ids.prefix(3) { // also broadcast at most 3 referenced quoted events if let ev = events.lookup(qref.note_id) { - postbox.send(ev) + await postbox.send(ev) } } return true diff --git a/damus/Core/Networking/NostrNetworkManager/NostrNetworkManager.swift b/damus/Core/Networking/NostrNetworkManager/NostrNetworkManager.swift index c9b3a393..287693a2 100644 --- a/damus/Core/Networking/NostrNetworkManager/NostrNetworkManager.swift +++ b/damus/Core/Networking/NostrNetworkManager/NostrNetworkManager.swift @@ -50,18 +50,18 @@ class NostrNetworkManager { // MARK: - Control and lifecycle functions /// Connects the app to the Nostr network - func connect() { - self.userRelayList.connect() // Will load the user's list, apply it, and get RelayPool to connect to it. - Task { await self.profilesManager.load() } + func connect() async { + await self.userRelayList.connect() // Will load the user's list, apply it, and get RelayPool to connect to it. + await self.profilesManager.load() } - func disconnectRelays() { - self.pool.disconnect() + func disconnectRelays() async { + await self.pool.disconnect() } func handleAppBackgroundRequest() async { await self.reader.cancelAllTasks() - self.pool.cleanQueuedRequestForSessionEnd() + await self.pool.cleanQueuedRequestForSessionEnd() } func close() async { @@ -75,18 +75,19 @@ class NostrNetworkManager { } // But await on each one to prevent race conditions for await value in group { continue } - pool.close() + await pool.close() } } - func ping() { - self.pool.ping() + func ping() async { + 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 // 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) } @@ -103,30 +104,35 @@ class NostrNetworkManager { /// - 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. - func sendToNostrDB(event: NostrEvent) { - self.pool.send_raw_to_local_ndb(.typical(.event(event))) + func sendToNostrDB(event: NostrEvent) async { + await self.pool.send_raw_to_local_ndb(.typical(.event(event))) } - func send(event: NostrEvent, to targetRelays: [RelayURL]? = nil, skipEphemeralRelays: Bool = true) { - self.pool.send(.event(event), to: targetRelays, skip_ephemeral: skipEphemeralRelays) + func send(event: NostrEvent, to targetRelays: [RelayURL]? = nil, skipEphemeralRelays: Bool = true) async { + await self.pool.send(.event(event), to: targetRelays, skip_ephemeral: skipEphemeralRelays) } + @MainActor func getRelay(_ id: RelayURL) -> RelayPool.Relay? { pool.get_relay(id) } + @MainActor var connectedRelays: [RelayPool.Relay] { self.pool.relays } + @MainActor var ourRelayDescriptors: [RelayPool.RelayDescriptor] { self.pool.our_descriptors } - func relayURLsThatSawNote(id: NoteId) -> Set? { - return self.pool.seen[id] + @MainActor + func relayURLsThatSawNote(id: NoteId) async -> Set? { + return await self.pool.seen[id] } + @MainActor func determineToRelays(filters: RelayFilters) -> [RelayURL] { return self.pool.our_descriptors .map { $0.url } @@ -137,8 +143,8 @@ class NostrNetworkManager { // TODO: Move this to NWCManager @discardableResult - func nwcPay(url: WalletConnectURL, post: PostBox, invoice: String, delay: TimeInterval? = 5.0, on_flush: OnFlush? = nil, zap_request: NostrEvent? = nil) -> NostrEvent? { - WalletConnect.pay(url: url, pool: self.pool, post: post, invoice: invoice, zap_request: nil) + func nwcPay(url: WalletConnectURL, post: PostBox, invoice: String, delay: TimeInterval? = 5.0, on_flush: OnFlush? = nil, zap_request: NostrEvent? = nil) async -> NostrEvent? { + await WalletConnect.pay(url: url, pool: self.pool, post: post, invoice: invoice, zap_request: nil) } /// Send a donation zap to the Damus team @@ -154,7 +160,7 @@ class NostrNetworkManager { } 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) } } diff --git a/damus/Core/Networking/NostrNetworkManager/SubscriptionManager.swift b/damus/Core/Networking/NostrNetworkManager/SubscriptionManager.swift index 7ea4cfb6..d08de04f 100644 --- a/damus/Core/Networking/NostrNetworkManager/SubscriptionManager.swift +++ b/damus/Core/Networking/NostrNetworkManager/SubscriptionManager.swift @@ -192,14 +192,14 @@ extension NostrNetworkManager { Self.logger.debug("Network subscription \(id.uuidString, privacy: .public): Started") 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.") try await Task.sleep(nanoseconds: 1_000_000_000) continue } 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 // TODO: Improve efficiency of subscriptions? try Task.checkCancellation() @@ -333,7 +333,7 @@ extension NostrNetworkManager { } // 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 { case .event(let event): return NdbNoteLender(ownedNdbNote: event) diff --git a/damus/Core/Networking/NostrNetworkManager/UserRelayListManager.swift b/damus/Core/Networking/NostrNetworkManager/UserRelayListManager.swift index 104b2a92..83fc96ca 100644 --- a/damus/Core/Networking/NostrNetworkManager/UserRelayListManager.swift +++ b/damus/Core/Networking/NostrNetworkManager/UserRelayListManager.swift @@ -122,68 +122,68 @@ extension NostrNetworkManager { // MARK: - Listening to and handling relay updates from the network - func connect() { - self.load() + func connect() async { + await self.load() self.relayListObserverTask?.cancel() self.relayListObserverTask = Task { await self.listenAndHandleRelayUpdates() } 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 { let filter = NostrFilter(kinds: [.relay_list], authors: [delegate.keypair.pubkey]) for await noteLender in self.reader.streamIndefinitely(filters: [filter]) { 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.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 - 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 - 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 !currentUserRelayList.relays.keys.contains(relay.url) || overwriteExisting else { throw .relayAlreadyExists } var newList = currentUserRelayList.relays 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 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 currentUserRelayList.relays.keys.contains(relayURL) || force else { throw .noSuchRelay } var newList = currentUserRelayList.relays 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 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 } // MARK: - Syncing our saved user relay list with the active `RelayPool` /// Loads the current user relay list - func load() { - self.apply(newRelayList: self.relaysToConnectTo()) + func load() async { + await self.apply(newRelayList: self.relaysToConnectTo()) } /// 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, /// 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 }) var changed = false @@ -217,31 +218,37 @@ extension NostrNetworkManager { let relaysToRemove = currentRelayURLs.subtracting(newRelayURLs) let relaysToAdd = newRelayURLs.subtracting(currentRelayURLs) - // Remove relays not in the new list - relaysToRemove.forEach { url in - pool.remove_relay(url) - changed = true - } + await withTaskGroup { taskGroup in + // Remove relays not in the new list + relaysToRemove.forEach { url in + taskGroup.addTask(operation: { await self.pool.remove_relay(url) }) + changed = true + } - // Add new relays from the new list - relaysToAdd.forEach { url in - guard let descriptor = newRelayList.first(where: { $0.url == url }) else { return } - add_new_relay( - model_cache: delegate.relayModelCache, - relay_filters: delegate.relayFilters, - pool: pool, - descriptor: descriptor, - new_relay_filters: new_relay_filters, - logging_enabled: delegate.developerMode - ) - changed = true + // Add new relays from the new list + relaysToAdd.forEach { url in + guard let descriptor = newRelayList.first(where: { $0.url == url }) else { return } + taskGroup.addTask(operation: { + await add_new_relay( + model_cache: self.delegate.relayModelCache, + relay_filters: self.delegate.relayFilters, + pool: self.pool, + descriptor: descriptor, + new_relay_filters: new_relay_filters, + logging_enabled: self.delegate.developerMode + ) + }) + changed = true + } + + for await value in taskGroup { continue } } // Always tell RelayPool to connect whether or not we are already connected. // This is because: // 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 - pool.connect() + await pool.connect() if changed { notify(.relays_changed) @@ -281,8 +288,8 @@ fileprivate extension NIP65.RelayList { /// - descriptor: The description of the relay being added /// - new_relay_filters: Whether to insert new relay filters /// - 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) { - try? pool.add_relay(descriptor) +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? await pool.add_relay(descriptor) let url = descriptor.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) 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 diff --git a/damus/Core/Nostr/RelayConnection.swift b/damus/Core/Nostr/RelayConnection.swift index 987e1bd5..608c9ea7 100644 --- a/damus/Core/Nostr/RelayConnection.swift +++ b/damus/Core/Nostr/RelayConnection.swift @@ -48,13 +48,13 @@ final class RelayConnection: ObservableObject { private lazy var socket = WebSocket(relay_url.url) private var subscriptionToken: AnyCancellable? - private var handleEvent: (NostrConnectionEvent) -> () + private var handleEvent: (NostrConnectionEvent) async -> () private var processEvent: (WebSocketEvent) -> () private let relay_url: RelayURL var log: RelayLog? init(url: RelayURL, - handleEvent: @escaping (NostrConnectionEvent) -> (), + handleEvent: @escaping (NostrConnectionEvent) async -> (), processUnverifiedWSEvent: @escaping (WebSocketEvent) -> ()) { self.relay_url = url @@ -95,12 +95,12 @@ final class RelayConnection: ObservableObject { .sink { [weak self] completion in switch completion { case .failure(let error): - self?.receive(event: .error(error)) + Task { await self?.receive(event: .error(error)) } case .finished: - self?.receive(event: .disconnected(.normalClosure, nil)) + Task { await self?.receive(event: .disconnected(.normalClosure, nil)) } } } receiveValue: { [weak self] event in - self?.receive(event: event) + Task { await self?.receive(event: event) } } 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") processEvent(event) switch event { @@ -149,7 +149,7 @@ final class RelayConnection: ObservableObject { self.isConnecting = false } case .message(let message): - self.receive(message: message) + await self.receive(message: message) case .disconnected(let closeCode, let reason): if closeCode != .normalClosure { 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() } } - DispatchQueue.main.async { - guard let ws_connection_event = NostrConnectionEvent.WSConnectionEvent.from(full_ws_event: event) else { return } - self.handleEvent(.ws_connection_event(ws_connection_event)) - } + guard let ws_connection_event = NostrConnectionEvent.WSConnectionEvent.from(full_ws_event: event) else { return } + await self.handleEvent(.ws_connection_event(ws_connection_event)) if let description = event.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 { case .string(let messageString): // NOTE: Once we switch to the local relay model, // we will not need to verify nostr events at this point. if let ev = decode_and_verify_nostr_response(txt: messageString) { - DispatchQueue.main.async { - self.handleEvent(.nostr_event(ev)) - } + await self.handleEvent(.nostr_event(ev)) return } print("failed to decode event \(messageString)") case .data(let messageData): if let messageString = String(data: messageData, encoding: .utf8) { - receive(message: .string(messageString)) + await receive(message: .string(messageString)) } @unknown default: print("An unexpected URLSessionWebSocketTask.Message was received.") diff --git a/damus/Core/Nostr/RelayPool.swift b/damus/Core/Nostr/RelayPool.swift index 86705744..8cf8e434 100644 --- a/damus/Core/Nostr/RelayPool.swift +++ b/damus/Core/Nostr/RelayPool.swift @@ -12,7 +12,7 @@ struct RelayHandler { let sub_id: String let filters: [NostrFilter]? let to: [RelayURL]? - var callback: (RelayURL, NostrConnectionEvent) -> () + var handler: AsyncStream<(RelayURL, NostrConnectionEvent)>.Continuation } struct QueuedRequest { @@ -27,7 +27,8 @@ struct SeenEvent: Hashable { } /// Establishes and manages connections and subscriptions to a list of relays. -class RelayPool { +actor RelayPool { + @MainActor private(set) var relays: [Relay] = [] var open: Bool = false 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. static let MAX_CONCURRENT_SUBSCRIPTION_LIMIT = 14 // This number is only an educated guess based on some local experiments. - func close() { - disconnect() - relays = [] + func close() async { + await disconnect() + await clearRelays() open = false handlers = [] request_queue = [] - seen.removeAll() + await clearSeen() counts = [:] keypair = nil } + + @MainActor + private func clearRelays() { + relays = [] + } + + private func clearSeen() { + seen.removeAll() + } init(ndb: Ndb, keypair: Keypair? = nil) { self.ndb = ndb self.keypair = keypair network_monitor.pathUpdateHandler = { [weak self] path in - if (path.status == .satisfied || path.status == .requiresConnection) && self?.last_network_status != path.status { - 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 + Task { await self?.pathUpdateHandler(path: path) } } 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] { return all_descriptors.filter { d in !d.ephemeral } } + @MainActor var all_descriptors: [RelayDescriptor] { relays.map { r in r.descriptor } } + @MainActor var num_connected: Int { return relays.reduce(0) { n, r in n + (r.connection.isConnected ? 1 : 0) } } 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) } - func ping() { - Log.info("Pinging %d relays", for: .networking, relays.count) - for relay in relays { + func ping() async { + Log.info("Pinging %d relays", for: .networking, await relays.count) + for relay in await relays { relay.connection.ping() } } - @MainActor - func register_handler(sub_id: String, filters: [NostrFilter]?, to relays: [RelayURL]? = nil, handler: @escaping (RelayURL, NostrConnectionEvent) -> ()) async { + func register_handler(sub_id: String, filters: [NostrFilter]?, to relays: [RelayURL]? = nil, handler: AsyncStream<(RelayURL, NostrConnectionEvent)>.Continuation) async { while handlers.count > Self.MAX_CONCURRENT_SUBSCRIPTION_LIMIT { Log.debug("%s: Too many subscriptions, waiting for subscription pool to clear", for: .networking, sub_id) try? await Task.sleep(for: .seconds(1)) @@ -117,20 +139,22 @@ class RelayPool { handlers = handlers.filter({ handler in if handler.sub_id == sub_id { Log.error("Duplicate handler detected for the same subscription ID. Overriding.", for: .networking) + handler.handler.finish() return false } else { 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) } - func remove_relay(_ relay_id: RelayURL) { + @MainActor + func remove_relay(_ relay_id: RelayURL) async { var i: Int = 0 - self.disconnect(to: [relay_id]) + await self.disconnect(to: [relay_id]) for relay in relays { 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 - if get_relay(relay_id) != nil { + if await get_relay(relay_id) != nil { throw RelayError.RelayAlreadyExists } 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 guard case .message(let msg) = wsev, case .string(let str) = msg @@ -159,19 +183,24 @@ class RelayPool { self.message_received_function?((str, desc)) }) let relay = Relay(descriptor: desc, connection: conn) + await self.appendRelayToList(relay: relay) + } + + @MainActor + private func appendRelayToList(relay: 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 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 - func connect_to_disconnected() { - for relay in relays { + func connect_to_disconnected() async { + for relay in await relays { let c = relay.connection let is_connecting = c.isConnecting @@ -188,16 +217,16 @@ class RelayPool { } } - func reconnect(to: [RelayURL]? = nil) { - let relays = to.map{ get_relays($0) } ?? self.relays + func reconnect(to targetRelays: [RelayURL]? = nil) async { + let relays = await getRelays(targetRelays: targetRelays) for relay in relays { // don't try to reconnect to broken relays relay.connection.reconnect() } } - func connect(to: [RelayURL]? = nil) { - let relays = to.map{ get_relays($0) } ?? self.relays + func connect(to targetRelays: [RelayURL]? = nil) async { + let relays = await getRelays(targetRelays: targetRelays) for relay in relays { relay.connection.connect() } @@ -205,15 +234,20 @@ class RelayPool { 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 open = false - let relays = to.map{ get_relays($0) } ?? self.relays + let relays = await getRelays(targetRelays: targetRelays) for relay in relays { 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) func cleanQueuedRequestForSessionEnd() { 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 { 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 { 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. 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 /// - 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 - func subscribe(filters: [NostrFilter], to desiredRelays: [RelayURL]? = nil, eoseTimeout: Duration? = nil, id: UUID? = nil) -> AsyncStream { + func subscribe(filters: [NostrFilter], to desiredRelays: [RelayURL]? = nil, eoseTimeout: Duration? = nil, id: UUID? = nil) async -> AsyncStream { let eoseTimeout = eoseTimeout ?? .seconds(5) - let desiredRelays = desiredRelays ?? self.relays.map({ $0.descriptor.url }) + let desiredRelays = await getRelays(targetRelays: desiredRelays) let startTime = CFAbsoluteTimeGetCurrent() return AsyncStream { continuation in let id = id ?? UUID() @@ -267,34 +301,40 @@ class RelayPool { var seenEvents: Set = [] var relaysWhoFinishedInitialResults: Set = [] var eoseSent = false - self.subscribe(sub_id: sub_id, filters: filters, handler: { (relayUrl, connectionEvent) in - switch connectionEvent { - case .ws_connection_event(let ev): - // Websocket events such as connect/disconnect/error are already handled in `RelayConnection`. Do not perform any handling here. - // For the future, perhaps we should abstract away `.ws_connection_event` in `RelayPool`? Seems like something to be handled on the `RelayConnection` layer. - break - case .nostr_event(let nostrResponse): - guard nostrResponse.subid == sub_id else { return } // Do not stream items that do not belong in this subscription - switch nostrResponse { - case .event(_, let nostrEvent): - if seenEvents.contains(nostrEvent.id) { break } // Don't send two of the same events. - continuation.yield(with: .success(.event(nostrEvent))) - seenEvents.insert(nostrEvent.id) - case .notice(let note): - break // We do not support handling these yet - case .eose(_): - relaysWhoFinishedInitialResults.insert(relayUrl) - let desiredAndConnectedRelays = desiredRelays ?? self.relays.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) - if relaysWhoFinishedInitialResults == Set(desiredAndConnectedRelays) { - continuation.yield(with: .success(.eose)) - eoseSent = true + 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 { + case .ws_connection_event(let ev): + // Websocket events such as connect/disconnect/error are already handled in `RelayConnection`. Do not perform any handling here. + // For the future, perhaps we should abstract away `.ws_connection_event` in `RelayPool`? Seems like something to be handled on the `RelayConnection` layer. + break + case .nostr_event(let nostrResponse): + guard nostrResponse.subid == sub_id else { return } // Do not stream items that do not belong in this subscription + switch nostrResponse { + case .event(_, let nostrEvent): + if seenEvents.contains(nostrEvent.id) { break } // Don't send two of the same events. + continuation.yield(with: .success(.event(nostrEvent))) + seenEvents.insert(nostrEvent.id) + case .notice(let note): + break // We do not support handling these yet + case .eose(_): + relaysWhoFinishedInitialResults.insert(relayUrl) + 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) + if relaysWhoFinishedInitialResults == Set(desiredAndConnectedRelays) { + continuation.yield(with: .success(.eose)) + eoseSent = true + } + case .ok(_): break // No need to handle this, we are not sending an event to the relay + case .auth(_): break // Handled in a separate function in RelayPool } - case .ok(_): break // No need to handle this, we are not sending an event to the relay - case .auth(_): break // Handled in a separate function in RelayPool } } - }, to: desiredRelays) + } let timeoutTask = Task { try? await Task.sleep(for: eoseTimeout) if !eoseSent { continuation.yield(with: .success(.eose)) } @@ -308,9 +348,12 @@ class RelayPool { @unknown default: break } - self.unsubscribe(sub_id: sub_id, to: desiredRelays) - self.remove_handler(sub_id: sub_id) + Task { + await self.unsubscribe(sub_id: sub_id, to: desiredRelays.map({ $0.descriptor.url })) + await self.remove_handler(sub_id: sub_id) + } timeoutTask.cancel() + upstreamStreamingTask.cancel() } } } @@ -322,11 +365,11 @@ class RelayPool { 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 { 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 } - @MainActor func queue_req(r: NostrRequestType, relay: RelayURL, skip_ephemeral: Bool) { let count = count_queued(relay: relay) guard count <= 10 else { @@ -365,8 +407,8 @@ class RelayPool { } } - func send_raw(_ req: NostrRequestType, to: [RelayURL]? = nil, skip_ephemeral: Bool = true) { - let relays = to.map{ get_relays($0) } ?? self.relays + func send_raw(_ req: NostrRequestType, to: [RelayURL]? = nil, skip_ephemeral: Bool = true) async { + 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 @@ -394,15 +436,17 @@ class RelayPool { } } - func send(_ req: NostrRequest, to: [RelayURL]? = nil, skip_ephemeral: Bool = true) { - send_raw(.typical(req), to: to, skip_ephemeral: skip_ephemeral) + func send(_ req: NostrRequest, to: [RelayURL]? = nil, skip_ephemeral: Bool = true) async { + await send_raw(.typical(req), to: to, skip_ephemeral: skip_ephemeral) } + @MainActor func get_relays(_ ids: [RelayURL]) -> [Relay] { // don't include ephemeral relays in the default list to query relays.filter { ids.contains($0.id) } } + @MainActor func get_relay(_ id: RelayURL) -> Relay? { relays.first(where: { $0.id == id }) } @@ -415,7 +459,7 @@ class RelayPool { } 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 { 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. @@ -446,11 +490,11 @@ class RelayPool { } 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) // When we reconnect, do two things @@ -459,20 +503,20 @@ class RelayPool { if case .ws_connection_event(let ws) = event { if case .connected = ws { run_queue(relay_id) - self.resubscribeAll(relayId: relay_id) + await self.resubscribeAll(relayId: relay_id) } } // Handle auth if case let .nostr_event(nostrResponse) = event, 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)") relay.authentication_state = .pending if let keypair { if let fullKeypair = keypair.to_full() { 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 } else { print("failed to make auth request") @@ -491,13 +535,13 @@ class RelayPool { } for handler in handlers { - handler.callback(relay_id, event) + handler.handler.yield((relay_id, event)) } } } -func add_rw_relay(_ pool: RelayPool, _ url: RelayURL) { - try? pool.add_relay(RelayPool.RelayDescriptor(url: url, info: .readWrite)) +func add_rw_relay(_ pool: RelayPool, _ url: RelayURL) async { + try? await pool.add_relay(RelayPool.RelayDescriptor(url: url, info: .readWrite)) } diff --git a/damus/Features/Actions/ActionBar/Models/ActionBarModel.swift b/damus/Features/Actions/ActionBar/Models/ActionBarModel.swift index 7c9b06d9..a6c4cfc7 100644 --- a/damus/Features/Actions/ActionBar/Models/ActionBarModel.swift +++ b/damus/Features/Actions/ActionBar/Models/ActionBarModel.swift @@ -46,7 +46,8 @@ class ActionBarModel: ObservableObject { 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.boosts = damus.boosts.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_reply = damus.replies.our_reply(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() } diff --git a/damus/Features/Actions/ActionBar/Views/EventActionBar.swift b/damus/Features/Actions/ActionBar/Views/EventActionBar.swift index 7973b67d..7bbe2a6a 100644 --- a/damus/Features/Actions/ActionBar/Views/EventActionBar.swift +++ b/damus/Features/Actions/ActionBar/Views/EventActionBar.swift @@ -89,8 +89,10 @@ struct EventActionBar: View { var like_swipe_button: some View { SwipeAction(image: "shaka", backgroundColor: DamusColors.adaptableGrey) { - send_like(emoji: damus_state.settings.default_emoji_reaction) - self.swipe_context?.state.wrappedValue = .closed + Task { + await send_like(emoji: damus_state.settings.default_emoji_reaction) + self.swipe_context?.state.wrappedValue = .closed + } } .swipeButtonStyle() .accessibilityLabel(NSLocalizedString("React with default reaction emoji", comment: "Accessibility label for react button")) @@ -138,7 +140,7 @@ struct EventActionBar: View { if bar.liked { //notify(.delete, bar.our_like) } else { - send_like(emoji: emoji) + Task { await send_like(emoji: emoji) } } } @@ -225,8 +227,15 @@ struct EventActionBar: View { } } - var event_relay_url_strings: [RelayURL] { - let relays = damus_state.nostrNetwork.relaysForEvent(event: event) + @State var event_relay_url_strings: [RelayURL] = [] + + 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 { return relays.prefix(Constants.MAX_SHARE_RELAYS).map { $0 } } @@ -237,9 +246,10 @@ struct EventActionBar: View { var body: some View { self.content .onAppear { - self.bar.update(damus: damus_state, evid: self.event.id) Task.detached(priority: .background, operation: { + await self.bar.update(damus: damus_state, evid: self.event.id) self.fetchLNURL() + await self.updateEventRelayURLStrings() }) } .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 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 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(), - 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 } @@ -291,7 +304,7 @@ struct EventActionBar: View { generator.impactOccurred() - damus_state.nostrNetwork.postbox.send(like_ev) + await damus_state.nostrNetwork.postbox.send(like_ev) } // MARK: Helper structures diff --git a/damus/Features/Actions/ActionBar/Views/EventDetailBar.swift b/damus/Features/Actions/ActionBar/Views/EventDetailBar.swift index 604c4e30..f3f5a3af 100644 --- a/damus/Features/Actions/ActionBar/Views/EventDetailBar.swift +++ b/damus/Features/Actions/ActionBar/Views/EventDetailBar.swift @@ -13,6 +13,7 @@ struct EventDetailBar: View { let target_pk: Pubkey @ObservedObject var bar: ActionBarModel + @State var relays: [RelayURL] = [] init(state: DamusState, target: NoteId, target_pk: Pubkey) { self.state = state @@ -61,7 +62,6 @@ struct EventDetailBar: View { } if bar.relays > 0 { - let relays = Array(state.nostrNetwork.relayURLsThatSawNote(id: target) ?? []) NavigationLink(value: Route.UserRelays(relays: relays)) { let nounString = pluralizedString(key: "relays_count", count: bar.relays) let noun = Text(nounString).foregroundColor(.gray) @@ -70,6 +70,18 @@ struct EventDetailBar: View { .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 } } diff --git a/damus/Features/Actions/ActionBar/Views/ShareAction.swift b/damus/Features/Actions/ActionBar/Views/ShareAction.swift index c3a1309a..2e040469 100644 --- a/damus/Features/Actions/ActionBar/Views/ShareAction.swift +++ b/damus/Features/Actions/ActionBar/Views/ShareAction.swift @@ -27,8 +27,15 @@ struct ShareAction: View { self._show_share = show_share } - var event_relay_url_strings: [RelayURL] { - let relays = userProfile.damus.nostrNetwork.relaysForEvent(event: event) + @State var event_relay_url_strings: [RelayURL] = [] + + 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 { 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() { userProfile.subscribeToFindRelays() + Task { await self.updateEventRelayURLStrings() } } .onDisappear() { userProfile.unsubscribeFindRelays() diff --git a/damus/Features/Actions/Reports/Views/ReportView.swift b/damus/Features/Actions/Reports/Views/ReportView.swift index 6d5fcc78..33c622ec 100644 --- a/damus/Features/Actions/Reports/Views/ReportView.swift +++ b/damus/Features/Actions/Reports/Views/ReportView.swift @@ -57,13 +57,13 @@ struct ReportView: View { .padding() } - func do_send_report() { + func do_send_report() async { 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 { return } - postbox.send(ev) + await postbox.send(ev) report_sent = true report_id = bech32_note_id(ev.id) @@ -116,7 +116,7 @@ struct ReportView: View { Section(content: { Button(send_report_button_text) { - do_send_report() + Task { await do_send_report() } } .disabled(selected_report_type == nil) }, footer: { diff --git a/damus/Features/Actions/Reposts/Views/RepostAction.swift b/damus/Features/Actions/Reposts/Views/RepostAction.swift index 4f7ac4dc..8669ce2e 100644 --- a/damus/Features/Actions/Reposts/Views/RepostAction.swift +++ b/damus/Features/Actions/Reposts/Views/RepostAction.swift @@ -19,13 +19,15 @@ struct RepostAction: View { Button { dismiss() - - 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 { - return + + Task { + guard let keypair = self.damus_state.keypair.to_full(), + let boost = await make_boost_event(keypair: keypair, boosted: self.event, relayURL: damus_state.nostrNetwork.relaysForEvent(event: self.event).first) else { + return + } + + await damus_state.nostrNetwork.postbox.send(boost) } - - damus_state.nostrNetwork.postbox.send(boost) } label: { Label(NSLocalizedString("Repost", comment: "Button to repost a note"), image: "repost") .frame(maxWidth: .infinity, minHeight: 50, maxHeight: 50, alignment: .leading) diff --git a/damus/Features/Chat/ChatEventView.swift b/damus/Features/Chat/ChatEventView.swift index 530e1b6c..c7df2fbf 100644 --- a/damus/Features/Chat/ChatEventView.swift +++ b/damus/Features/Chat/ChatEventView.swift @@ -197,8 +197,10 @@ struct ChatEventView: View { } .onChange(of: selected_emoji) { newSelectedEmoji in if let newSelectedEmoji { - send_like(emoji: newSelectedEmoji.value) - popover_state = .closed + Task { + await send_like(emoji: newSelectedEmoji.value) + popover_state = .closed + } } } } @@ -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(), - 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 } @@ -244,7 +246,7 @@ struct ChatEventView: View { let generator = UIImpactFeedbackGenerator(style: .medium) generator.impactOccurred() - damus_state.nostrNetwork.postbox.send(like_ev) + await damus_state.nostrNetwork.postbox.send(like_ev) } var action_bar: some View { diff --git a/damus/Features/DMs/Views/DMChatView.swift b/damus/Features/DMs/Views/DMChatView.swift index 39a74936..50d13ccc 100644 --- a/damus/Features/DMs/Views/DMChatView.swift +++ b/damus/Features/DMs/Views/DMChatView.swift @@ -108,7 +108,7 @@ struct DMChatView: View, KeyboardReadable { Button( role: .none, action: { - send_message() + Task { await send_message() } } ) { Label("", image: "send") @@ -124,7 +124,7 @@ struct DMChatView: View, KeyboardReadable { */ } - func send_message() { + func send_message() async { let tags = [["p", pubkey.hex()]] guard let post_blocks = parse_post_blocks(content: dms.draft)?.blocks else { return @@ -138,7 +138,7 @@ struct DMChatView: View, KeyboardReadable { 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()) diff --git a/damus/Features/Events/EventMenu.swift b/damus/Features/Events/EventMenu.swift index 28691fdd..2e2bd6b8 100644 --- a/damus/Features/Events/EventMenu.swift +++ b/damus/Features/Events/EventMenu.swift @@ -64,8 +64,8 @@ struct MenuItems: View { self.profileModel = profileModel } - var event_relay_url_strings: [RelayURL] { - let relays = damus_state.nostrNetwork.relaysForEvent(event: event) + func event_relay_url_strings() async -> [RelayURL] { + let relays = await damus_state.nostrNetwork.relaysForEvent(event: event) if !relays.isEmpty { return relays.prefix(Constants.MAX_SHARE_RELAYS).map { $0 } } @@ -88,7 +88,7 @@ struct MenuItems: View { } 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(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(), 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.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) isMutedThread = muted diff --git a/damus/Features/Events/EventView.swift b/damus/Features/Events/EventView.swift index cb4d8727..9885fe60 100644 --- a/damus/Features/Events/EventView.swift +++ b/damus/Features/Events/EventView.swift @@ -106,7 +106,7 @@ func format_date(date: Date, time_style: DateFormatter.Style = .short) -> String func make_actionbar_model(ev: NoteId, damus: DamusState) -> ActionBarModel { let model = ActionBarModel.empty() - model.update(damus: damus, evid: ev) + Task { await model.update(damus: damus, evid: ev) } return model } diff --git a/damus/Features/Events/SelectedEventView.swift b/damus/Features/Events/SelectedEventView.swift index b107f103..5cff475d 100644 --- a/damus/Features/Events/SelectedEventView.swift +++ b/damus/Features/Events/SelectedEventView.swift @@ -74,7 +74,7 @@ struct SelectedEventView: View { } .onReceive(handle_notify(.update_stats)) { target in 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() } diff --git a/damus/Features/FollowPack/Models/FollowPackModel.swift b/damus/Features/FollowPack/Models/FollowPackModel.swift index a66e669b..87c6bf90 100644 --- a/damus/Features/FollowPack/Models/FollowPackModel.swift +++ b/damus/Features/FollowPack/Models/FollowPackModel.swift @@ -37,7 +37,7 @@ class FollowPackModel: ObservableObject { } 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]) filter.until = UInt32(Date.now.timeIntervalSince1970) filter.authors = follow_pack_users diff --git a/damus/Features/Follows/Models/Contacts+.swift b/damus/Features/Follows/Models/Contacts+.swift index 81dd9803..e52e24c7 100644 --- a/damus/Features/Follows/Models/Contacts+.swift +++ b/damus/Features/Follows/Models/Contacts+.swift @@ -9,17 +9,17 @@ 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 { return nil } - box.send(ev) + await box.send(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 { return nil } @@ -28,7 +28,7 @@ func unfollow_reference(postbox: PostBox, our_contacts: NostrEvent?, keypair: Fu return nil } - postbox.send(ev) + await postbox.send(ev) return ev } diff --git a/damus/Features/Muting/Models/MutedThreadsManager.swift b/damus/Features/Muting/Models/MutedThreadsManager.swift index 7b463971..7f766a22 100644 --- a/damus/Features/Muting/Models/MutedThreadsManager.swift +++ b/damus/Features/Muting/Models/MutedThreadsManager.swift @@ -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 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.nostrNetwork.postbox.send(new_mutelist_event) + Task { await damus_state.nostrNetwork.postbox.send(new_mutelist_event) } // Set existing muted threads to an empty array UserDefaults.standard.set([], forKey: getMutedThreadsKey(pubkey: keypair.pubkey)) } diff --git a/damus/Features/Muting/Views/AddMuteItemView.swift b/damus/Features/Muting/Views/AddMuteItemView.swift index 4a464395..bc5fb150 100644 --- a/damus/Features/Muting/Views/AddMuteItemView.swift +++ b/damus/Features/Muting/Views/AddMuteItemView.swift @@ -87,7 +87,7 @@ struct AddMuteItemView: View { } state.mutelist_manager.set_mutelist(mutelist) - state.nostrNetwork.postbox.send(mutelist) + Task { await state.nostrNetwork.postbox.send(mutelist) } } new_text = "" diff --git a/damus/Features/Muting/Views/MutelistView.swift b/damus/Features/Muting/Views/MutelistView.swift index dcc25500..d152dd0c 100644 --- a/damus/Features/Muting/Views/MutelistView.swift +++ b/damus/Features/Muting/Views/MutelistView.swift @@ -30,8 +30,10 @@ struct MutelistView: View { } damus_state.mutelist_manager.set_mutelist(new_ev) - damus_state.nostrNetwork.postbox.send(new_ev) - updateMuteItems() + Task { + await damus_state.nostrNetwork.postbox.send(new_ev) + updateMuteItems() + } } label: { Label(NSLocalizedString("Delete", comment: "Button to remove a user from their mutelist."), image: "delete") } diff --git a/damus/Features/Onboarding/Views/OnboardingSuggestionsView.swift b/damus/Features/Onboarding/Views/OnboardingSuggestionsView.swift index 264ecff4..dbfdcbba 100644 --- a/damus/Features/Onboarding/Views/OnboardingSuggestionsView.swift +++ b/damus/Features/Onboarding/Views/OnboardingSuggestionsView.swift @@ -56,7 +56,7 @@ struct OnboardingSuggestionsView: View { // - 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 - model.damus_state.nostrNetwork.sendToNostrDB(event: event) + Task { await model.damus_state.nostrNetwork.sendToNostrDB(event: event) } } var body: some View { diff --git a/damus/Features/Onboarding/Views/SaveKeysView.swift b/damus/Features/Onboarding/Views/SaveKeysView.swift index 9939d778..6ec5dc3d 100644 --- a/damus/Features/Onboarding/Views/SaveKeysView.swift +++ b/damus/Features/Onboarding/Views/SaveKeysView.swift @@ -75,7 +75,7 @@ struct SaveKeysView: View { .foregroundColor(.red) Button(action: { - complete_account_creation(account) + Task { await complete_account_creation(account) } }) { HStack { Text("Retry", comment: "Button to retry completing account creation after an error occurred.") @@ -89,7 +89,7 @@ struct SaveKeysView: View { Button(action: { save_key(account) - complete_account_creation(account) + Task { await complete_account_creation(account) } }) { HStack { 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) Button(action: { - complete_account_creation(account) + Task { await complete_account_creation(account) } }) { HStack { 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) } - func complete_account_creation(_ account: CreateAccountModel) { + func complete_account_creation(_ account: CreateAccountModel) async { 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.") return @@ -139,14 +139,21 @@ struct SaveKeysView: View { let bootstrap_relays = load_bootstrap_relays(pubkey: account.pubkey) for relay in bootstrap_relays { - add_rw_relay(self.pool, relay) + await add_rw_relay(self.pool, relay) + } + + 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) + } } - - Task { await self.pool.register_handler(sub_id: "signup", filters: nil, handler: handle_event) } 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) { @@ -160,7 +167,7 @@ struct SaveKeysView: View { 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 { case .ws_connection_event(let wsev): switch wsev { @@ -169,15 +176,15 @@ struct SaveKeysView: View { if let keypair = account.keypair.to_full(), 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 { - self.pool.send(.event(first_contact_event)) + await self.pool.send(.event(first_contact_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 { diff --git a/damus/Features/Posting/Models/DraftsModel.swift b/damus/Features/Posting/Models/DraftsModel.swift index 882e7b5e..de00ac1b 100644 --- a/damus/Features/Posting/Models/DraftsModel.swift +++ b/damus/Features/Posting/Models/DraftsModel.swift @@ -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 /// - references: references in the post? /// - 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 } - 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 } 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 { var draft_events: [NdbNote] = [] 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 } draft_events.append(wrapped_note) } 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 } - 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 } draft_events.append(wrapped_note) } 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 } - 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 } draft_events.append(wrapped_note) } 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 } 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: // - 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) - damus_state.nostrNetwork.sendToNostrDB(event: draft_event) + await damus_state.nostrNetwork.sendToNostrDB(event: draft_event) } DispatchQueue.main.async { diff --git a/damus/Features/Posting/Models/PostBox.swift b/damus/Features/Posting/Models/PostBox.swift index db5bb3b8..b34e7b60 100644 --- a/damus/Features/Posting/Models/PostBox.swift +++ b/damus/Features/Posting/Models/PostBox.swift @@ -60,7 +60,14 @@ class PostBox { init(pool: RelayPool) { self.pool = pool 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 @@ -81,7 +88,7 @@ class PostBox { return nil } - func try_flushing_events() { + func try_flushing_events() async { let now = Int64(Date().timeIntervalSince1970) for kv in events { let event = kv.value @@ -95,7 +102,7 @@ class PostBox { if relayer.last_attempt == nil || (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") - flush_event(event, to_relay: relayer) + await flush_event(event, to_relay: relayer) } } } @@ -140,7 +147,7 @@ class PostBox { 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 if let to_relay { relayers = [to_relay] @@ -150,29 +157,35 @@ class PostBox { relayer.attempts += 1 relayer.last_attempt = Int64(Date().timeIntervalSince1970) 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)") } else { 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 if events[event.id] != nil { 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 posted_ev = PostedEvent(event: event, remaining: remaining, skip_ephemeral: skip_ephemeral, flush_after: after, on_flush: on_flush) events[event.id] = posted_ev if after == nil { - flush_event(posted_ev) + await flush_event(posted_ev) } } } diff --git a/damus/Features/Posting/Views/PostView.swift b/damus/Features/Posting/Views/PostView.swift index 1f9672a4..c9c501bc 100644 --- a/damus/Features/Posting/Views/PostView.swift +++ b/damus/Features/Posting/Views/PostView.swift @@ -121,8 +121,8 @@ struct PostView: View { uploadTasks.removeAll() } - func send_post() { - let new_post = build_post(state: self.damus_state, post: self.post, action: action, uploadedMedias: uploadedMedias, references: self.references, filtered_pubkeys: filtered_pubkeys) + func send_post() async { + 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))) @@ -190,7 +190,7 @@ struct PostView: View { var PostButton: some View { Button(NSLocalizedString("Post", comment: "Button to post a note.")) { - self.send_post() + Task { await self.send_post() } } .disabled(posting_disabled) .opacity(posting_disabled ? 0.5 : 1.0) @@ -829,8 +829,8 @@ func nip10_reply_tags(replying_to: NostrEvent, keypair: Keypair, relayURL: Relay return tags } -func build_post(state: DamusState, action: PostAction, draft: DraftArtifacts) -> NostrPost { - return build_post( +func build_post(state: DamusState, action: PostAction, draft: DraftArtifacts) async -> NostrPost { + return await build_post( state: state, post: draft.content, 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) -> NostrPost { +func build_post(state: DamusState, post: NSAttributedString, action: PostAction, uploadedMedias: [UploadedMedia], references: [RefId], filtered_pubkeys: Set) async -> NostrPost { // don't add duplicate pubkeys but retain order var pkset = Set() @@ -858,7 +858,7 @@ func build_post(state: DamusState, post: NSAttributedString, action: PostAction, 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 @@ -874,7 +874,7 @@ func build_post(state: DamusState, post: NSAttributedString, action: PostAction, /// - uploadedMedias: The medias attached to this post /// - pubkeys: The referenced pubkeys /// - 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) post.enumerateAttributes(in: NSRange(location: 0, length: post.length), options: []) { attributes, range, stop in let linkValue = attributes[.link] @@ -916,10 +916,10 @@ func build_post(state: DamusState, post: NSAttributedString, action: PostAction, switch action { case .replying_to(let replying_to): // 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): - 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 }))) content.append("\n\nnostr:\(nevent)") diff --git a/damus/Features/Profile/Views/EditMetadataView.swift b/damus/Features/Profile/Views/EditMetadataView.swift index 99c4214d..6dc65b5b 100644 --- a/damus/Features/Profile/Views/EditMetadataView.swift +++ b/damus/Features/Profile/Views/EditMetadataView.swift @@ -58,7 +58,7 @@ struct EditMetadataView: View { return profile } - func save() { + func save() async { let profile = to_profile() guard let keypair = damus_state.keypair.to_full(), let metadata_ev = make_metadata_event(keypair: keypair, metadata: profile) @@ -66,7 +66,7 @@ struct EditMetadataView: View { return } - damus_state.nostrNetwork.postbox.send(metadata_ev) + await damus_state.nostrNetwork.postbox.send(metadata_ev) } func is_ln_valid(ln: String) -> Bool { @@ -211,8 +211,10 @@ struct EditMetadataView: View { if !ln.isEmpty && !is_ln_valid(ln: ln) { confirm_ln_address = true } else { - save() - dismiss() + Task { + await save() + dismiss() + } } }, label: { Text(NSLocalizedString("Save", comment: "Button for saving profile.")) diff --git a/damus/Features/Profile/Views/ProfileView.swift b/damus/Features/Profile/Views/ProfileView.swift index 5e1d247b..b9350ab6 100644 --- a/damus/Features/Profile/Views/ProfileView.swift +++ b/damus/Features/Profile/Views/ProfileView.swift @@ -219,7 +219,7 @@ struct ProfileView: View { } 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 { Button(NSLocalizedString("Mute", comment: "Button to mute a profile"), role: .destructive) { diff --git a/damus/Features/Relays/Views/AddRelayView.swift b/damus/Features/Relays/Views/AddRelayView.swift index f4e4b7f6..dc11d8d0 100644 --- a/damus/Features/Relays/Views/AddRelayView.swift +++ b/damus/Features/Relays/Views/AddRelayView.swift @@ -80,30 +80,32 @@ struct AddRelayView: View { } Button(action: { - if new_relay.starts(with: "wss://") == false && new_relay.starts(with: "ws://") == false { - new_relay = "wss://" + new_relay + Task { + if new_relay.starts(with: "wss://") == false && new_relay.starts(with: "ws://") == false { + new_relay = "wss://" + new_relay + } + + guard let url = RelayURL(new_relay) else { + relayAddErrorTitle = NSLocalizedString("Invalid relay address", comment: "Heading for an error when adding a relay") + relayAddErrorMessage = NSLocalizedString("Please check the address and try again", comment: "Tip for an error where the relay address being added is invalid") + return + } + + do { + try await state.nostrNetwork.userRelayList.insert(relay: NIP65.RelayList.RelayItem(url: url, rwConfiguration: .readWrite)) + relayAddErrorTitle = nil // Clear error title + relayAddErrorMessage = nil // Clear error message + } + catch { + present_sheet(.error(self.humanReadableError(for: error))) + } + + new_relay = "" + + this_app.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) + + dismiss() } - - guard let url = RelayURL(new_relay) else { - relayAddErrorTitle = NSLocalizedString("Invalid relay address", comment: "Heading for an error when adding a relay") - relayAddErrorMessage = NSLocalizedString("Please check the address and try again", comment: "Tip for an error where the relay address being added is invalid") - return - } - - do { - try state.nostrNetwork.userRelayList.insert(relay: NIP65.RelayList.RelayItem(url: url, rwConfiguration: .readWrite)) - relayAddErrorTitle = nil // Clear error title - relayAddErrorMessage = nil // Clear error message - } - catch { - present_sheet(.error(self.humanReadableError(for: error))) - } - - new_relay = "" - - this_app.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) - - dismiss() }) { HStack { Text("Add relay", comment: "Button to add a relay.") diff --git a/damus/Features/Search/Models/SearchHomeModel.swift b/damus/Features/Search/Models/SearchHomeModel.swift index 4c99fbca..4d34eba3 100644 --- a/damus/Features/Search/Models/SearchHomeModel.swift +++ b/damus/Features/Search/Models/SearchHomeModel.swift @@ -55,7 +55,7 @@ class SearchHomeModel: ObservableObject { DispatchQueue.main.async { self.loading = true } - let to_relays = damus_state.nostrNetwork.ourRelayDescriptors + let to_relays = await damus_state.nostrNetwork.ourRelayDescriptors .map { $0.url } .filter { !damus_state.relay_filters.is_filtered(timeline: .search, relay_id: $0) } diff --git a/damus/Features/Search/Views/SearchHeaderView.swift b/damus/Features/Search/Views/SearchHeaderView.swift index 7f9449dc..7b19a62f 100644 --- a/damus/Features/Search/Views/SearchHeaderView.swift +++ b/damus/Features/Search/Views/SearchHeaderView.swift @@ -125,7 +125,7 @@ struct HashtagUnfollowButton: View { func unfollow(_ hashtag: String) { 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) { is_following = true - handle_follow(state: damus_state, follow: .hashtag(hashtag)) + Task { await handle_follow(state: damus_state, follow: .hashtag(hashtag)) } } } diff --git a/damus/Features/Search/Views/SearchView.swift b/damus/Features/Search/Views/SearchView.swift index dd0e233a..b43b5149 100644 --- a/damus/Features/Search/Views/SearchView.swift +++ b/damus/Features/Search/Views/SearchView.swift @@ -69,7 +69,7 @@ struct SearchView: View { } appstate.mutelist_manager.set_mutelist(mutelist) - appstate.nostrNetwork.postbox.send(mutelist) + Task { await appstate.nostrNetwork.postbox.send(mutelist) } } 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.") } @@ -104,7 +104,7 @@ struct SearchView: View { } appstate.mutelist_manager.set_mutelist(mutelist) - appstate.nostrNetwork.postbox.send(mutelist) + Task { await appstate.nostrNetwork.postbox.send(mutelist) } } var described_search: DescribedSearch { diff --git a/damus/Features/Settings/Views/ConfigView.swift b/damus/Features/Settings/Views/ConfigView.swift index b16ef68d..d3f5b5e8 100644 --- a/damus/Features/Settings/Views/ConfigView.swift +++ b/damus/Features/Settings/Views/ConfigView.swift @@ -182,8 +182,10 @@ struct ConfigView: View { let ev = created_deleted_account_profile(keypair: keypair) else { return } - state.nostrNetwork.postbox.send(ev) - logout(state) + Task { + await state.nostrNetwork.postbox.send(ev) + logout(state) + } } } .alert(NSLocalizedString("Logout", comment: "Alert for logging out the user."), isPresented: $confirm_logout) { diff --git a/damus/Features/Settings/Views/FirstAidSettingsView.swift b/damus/Features/Settings/Views/FirstAidSettingsView.swift index 84fbe48e..354b94e6 100644 --- a/damus/Features/Settings/Views/FirstAidSettingsView.swift +++ b/damus/Features/Settings/Views/FirstAidSettingsView.swift @@ -68,13 +68,13 @@ struct FirstAidSettingsView: View { guard let new_contact_list_event = make_first_contact_event(keypair: damus_state.keypair) else { 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() } func resetRelayList() async throws { 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 { diff --git a/damus/Features/Status/Views/UserStatusSheet.swift b/damus/Features/Status/Views/UserStatusSheet.swift index dbff37bf..0ce18d41 100644 --- a/damus/Features/Status/Views/UserStatusSheet.swift +++ b/damus/Features/Status/Views/UserStatusSheet.swift @@ -109,16 +109,18 @@ struct UserStatusSheet: View { Spacer() Button(action: { - guard let status = self.status.general, - let kp = keypair.to_full(), - let ev = make_user_status_note(status: status, keypair: kp, expiry: duration.expiration) - else { - return + Task { + guard let status = self.status.general, + let kp = keypair.to_full(), + let ev = make_user_status_note(status: status, keypair: kp, expiry: duration.expiration) + else { + return + } + + await postbox.send(ev) + + dismiss() } - - postbox.send(ev) - - dismiss() }, label: { Text("Share", comment: "Save button text for saving profile status settings.") }) diff --git a/damus/Features/Timeline/Models/HomeModel.swift b/damus/Features/Timeline/Models/HomeModel.swift index 913cd8a6..0280ab57 100644 --- a/damus/Features/Timeline/Models/HomeModel.swift +++ b/damus/Features/Timeline/Models/HomeModel.swift @@ -812,13 +812,15 @@ class HomeModel: ContactsDelegate, ObservableObject { } -func update_signal_from_pool(signal: SignalModel, pool: RelayPool) { - if signal.max_signal != pool.relays.count { - signal.max_signal = pool.relays.count +func update_signal_from_pool(signal: SignalModel, pool: RelayPool) async { + let relayCount = await pool.relays.count + if signal.max_signal != relayCount { + signal.max_signal = relayCount } - if signal.signal != pool.num_connected { - signal.signal = pool.num_connected + let numberOfConnectedRelays = await pool.num_connected + if signal.signal != numberOfConnectedRelays { + signal.signal = numberOfConnectedRelays } } diff --git a/damus/Features/Wallet/Models/WalletConnect/WalletConnect+.swift b/damus/Features/Wallet/Models/WalletConnect/WalletConnect+.swift index b7f3a2c1..530395e4 100644 --- a/damus/Features/Wallet/Models/WalletConnect/WalletConnect+.swift +++ b/damus/Features/Wallet/Models/WalletConnect/WalletConnect+.swift @@ -17,14 +17,14 @@ extension WalletConnect { /// - Parameters: /// - url: The Nostr Wallet Connect URL containing connection info to the NWC wallet /// - 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]) filter.authors = [url.pubkey] filter.pubkeys = [url.keypair.pubkey] filter.limit = 0 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: @@ -41,16 +41,16 @@ extension WalletConnect { /// - 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 @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) guard let ev = req.to_nostr_event(to_pk: url.pubkey, keypair: url.keypair) else { return nil } - try? 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 - post.send(ev, to: [url.relay], skip_ephemeral: false, delay: delay, on_flush: on_flush) + try? await pool.add_relay(.nwc(url: url.relay)) // Ensure the NWC relay is connected + await WalletConnect.subscribe(url: url, pool: pool) // Ensure we are listening to NWC updates from the relay + await post.send(ev, to: [url.relay], skip_ephemeral: false, delay: delay, on_flush: on_flush) return ev } diff --git a/damus/Features/Wallet/Models/WalletModel.swift b/damus/Features/Wallet/Models/WalletModel.swift index ecde5121..240b8bcc 100644 --- a/damus/Features/Wallet/Models/WalletModel.swift +++ b/damus/Features/Wallet/Models/WalletModel.swift @@ -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) { guard let responseEvent = try? event.getCopy() else { throw .internalError } diff --git a/damus/Features/Wallet/Views/NWCSettings.swift b/damus/Features/Wallet/Views/NWCSettings.swift index c0240ae5..c72d8762 100644 --- a/damus/Features/Wallet/Views/NWCSettings.swift +++ b/damus/Features/Wallet/Views/NWCSettings.swift @@ -268,7 +268,7 @@ struct NWCSettings: View { guard let meta = make_metadata_event(keypair: keypair, metadata: prof) else { return } - damus_state.nostrNetwork.postbox.send(meta) + Task { await damus_state.nostrNetwork.postbox.send(meta) } } } diff --git a/damus/Features/Wallet/Views/SendPaymentView.swift b/damus/Features/Wallet/Views/SendPaymentView.swift index 19f3d93a..9d180485 100644 --- a/damus/Features/Wallet/Views/SendPaymentView.swift +++ b/damus/Features/Wallet/Views/SendPaymentView.swift @@ -182,18 +182,18 @@ struct SendPaymentView: View { .buttonStyle(NeutralButtonStyle()) Button(action: { - sendState = .processing - - // Process payment - guard let payRequestEv = damus_state.nostrNetwork.nwcPay(url: nwc, post: damus_state.nostrNetwork.postbox, invoice: invoice.string, zap_request: nil) else { - 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"), - 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."), - technical_info: "Cannot form Nostr Event to send to the NWC provider when calling `pay` from the \"send payment\" feature. Wallet provider relay: \"\(nwc.relay)\"" - )) - return - } Task { + sendState = .processing + + // Process payment + 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( + 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."), + technical_info: "Cannot form Nostr Event to send to the NWC provider when calling `pay` from the \"send payment\" feature. Wallet provider relay: \"\(nwc.relay)\"" + )) + return + } do { let result = try await model.waitForResponse(for: payRequestEv.id, timeout: SEND_PAYMENT_TIMEOUT) guard case .pay_invoice(_) = result else { diff --git a/damus/Features/Zaps/Models/Zaps.swift b/damus/Features/Zaps/Models/Zaps.swift index c16e0d0e..3c370e41 100644 --- a/damus/Features/Zaps/Models/Zaps.swift +++ b/damus/Features/Zaps/Models/Zaps.swift @@ -95,7 +95,7 @@ class Zaps { event_counts[note_id] = event_counts[note_id]! + 1 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)) } } } } diff --git a/damus/Features/Zaps/Views/NoteZapButton.swift b/damus/Features/Zaps/Views/NoteZapButton.swift index d7a1c9ef..fe15f396 100644 --- a/damus/Features/Zaps/Views/NoteZapButton.swift +++ b/damus/Features/Zaps/Views/NoteZapButton.swift @@ -179,7 +179,7 @@ func send_zap(damus_state: DamusState, target: ZapTarget, lnurl: String, is_cust } // 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 ?? "" 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) 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 { print("nwc: failed to send nwc request for zapreq \(reqid.reqid)") diff --git a/damus/Notify/Notify.swift b/damus/Notify/Notify.swift index 0557c2d8..bcc01d96 100644 --- a/damus/Notify/Notify.swift +++ b/damus/Notify/Notify.swift @@ -33,7 +33,9 @@ struct NotifyHandler { } func notify(_ notify: Notifications) { let notify = notify.notify - NotificationCenter.default.post(name: T.name, object: notify.payload) + DispatchQueue.main.async { + NotificationCenter.default.post(name: T.name, object: notify.payload) + } } func handle_notify(_ handler: NotifyHandler) -> AnyPublisher { diff --git a/damus/Notify/PresentFullScreenItemNotify.swift b/damus/Notify/PresentFullScreenItemNotify.swift index afc88b97..0cd5d990 100644 --- a/damus/Notify/PresentFullScreenItemNotify.swift +++ b/damus/Notify/PresentFullScreenItemNotify.swift @@ -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`. /// func present(full_screen_item: FullScreenItem) { - notify(.present_full_screen_item(full_screen_item)) + Task { await notify(.present_full_screen_item(full_screen_item)) } } diff --git a/highlighter action extension/ActionViewController.swift b/highlighter action extension/ActionViewController.swift index 0ee8bbc3..d886cbf4 100644 --- a/highlighter action extension/ActionViewController.swift +++ b/highlighter action extension/ActionViewController.swift @@ -135,7 +135,7 @@ struct ShareExtensionView: View { return } self.state = DamusState(keypair: keypair) - self.state?.nostrNetwork.connect() + Task { await self.state?.nostrNetwork.connect() } }) .onChange(of: self.highlighter_state) { if case .cancelled = highlighter_state { @@ -144,10 +144,10 @@ struct ShareExtensionView: View { } .onReceive(handle_notify(.post)) { post_notification in switch post_notification { - case .post(let post): - self.post(post) - case .cancel: - self.highlighter_state = .cancelled + case .post(let post): + Task { await self.post(post) } + case .cancel: + self.highlighter_state = .cancelled } } .onChange(of: scenePhase) { (phase: ScenePhase) in @@ -164,7 +164,7 @@ struct ShareExtensionView: View { break case .active: print("txn: 📙 HIGHLIGHTER ACTIVE") - state.nostrNetwork.ping() + Task { await state.nostrNetwork.ping() } @unknown default: break } @@ -225,7 +225,7 @@ struct ShareExtensionView: View { } } - func post(_ post: NostrPost) { + func post(_ post: NostrPost) async { self.highlighter_state = .posting guard let state else { 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") 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 { DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: { // Offset labor perception bias self.highlighter_state = .posted(event: flushed_event.event) diff --git a/nostrdb/UnownedNdbNote.swift b/nostrdb/UnownedNdbNote.swift index 79ef237b..2c971659 100644 --- a/nostrdb/UnownedNdbNote.swift +++ b/nostrdb/UnownedNdbNote.swift @@ -64,7 +64,19 @@ enum NdbNoteLender: Sendable { case .owned(let note): return try lendingFunction(UnownedNdbNote(note)) } - + } + + /// Borrows the note temporarily (asynchronously) + func borrow(_ 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 diff --git a/nostrscript/NostrScript.swift b/nostrscript/NostrScript.swift index 34f0e102..917ccb78 100644 --- a/nostrscript/NostrScript.swift +++ b/nostrscript/NostrScript.swift @@ -310,7 +310,10 @@ public func nscript_nostr_cmd(interp: UnsafeMutablePointer?, cmd: I func nscript_add_relay(script: NostrScript, relay: String) -> Bool { guard let url = RelayURL(relay) else { return false } 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?, pre return 0 } - DispatchQueue.main.async { - script.pool.send_raw(.custom(req_str), to: [to_relay_url], skip_ephemeral: false) - } + Task { await script.pool.send_raw(.custom(req_str), to: [to_relay_url], skip_ephemeral: false) } return 1; } @@ -354,9 +355,7 @@ public func nscript_pool_send_to(interp: UnsafeMutablePointer?, pre func nscript_pool_send(script: NostrScript, req req_str: String) -> Int32 { //script.test("pool_send: '\(req_str)'") - DispatchQueue.main.sync { - script.pool.send_raw(.custom(req_str), skip_ephemeral: false) - } + Task { await script.pool.send_raw(.custom(req_str), skip_ephemeral: false) } return 1; } diff --git a/share extension/ShareViewController.swift b/share extension/ShareViewController.swift index 67c38f76..6b17d894 100644 --- a/share extension/ShareViewController.swift +++ b/share extension/ShareViewController.swift @@ -173,7 +173,7 @@ struct ShareExtensionView: View { .onReceive(handle_notify(.post)) { post_notification in switch post_notification { case .post(let post): - self.post(post) + Task { await self.post(post) } case .cancel: self.share_state = .cancelled dismissParent?() @@ -193,7 +193,7 @@ struct ShareExtensionView: View { break case .active: print("txn: 📙 SHARE ACTIVE") - state.nostrNetwork.ping() + Task { await state.nostrNetwork.ping() } @unknown default: break } @@ -216,7 +216,7 @@ struct ShareExtensionView: View { } } - func post(_ post: NostrPost) { + func post(_ post: NostrPost) async { self.share_state = .posting guard let state else { 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") 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 { DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: { // Offset labor perception bias self.share_state = .posted(event: flushed_event.event) @@ -250,7 +250,7 @@ struct ShareExtensionView: View { return false } state = DamusState(keypair: keypair) - state?.nostrNetwork.connect() + Task { await state?.nostrNetwork.connect() } return true }