Move most of RelayPool away from the Main Thread

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

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

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

Changelog-Fixed: Added performance improvements to timeline scrolling
Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
This commit is contained in:
Daniel D’Aquino
2025-10-10 14:12:30 -07:00
parent 7c1594107f
commit 991a4a86e6
50 changed files with 602 additions and 451 deletions

View File

@@ -55,7 +55,6 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
enableAddressSanitizer = "YES"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"

View File

@@ -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

View File

@@ -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<RelayURL>? {
return self.pool.seen[id]
@MainActor
func relayURLsThatSawNote(id: NoteId) async -> Set<RelayURL>? {
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)
}
}

View File

@@ -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)

View File

@@ -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

View File

@@ -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.")

View File

@@ -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<StreamItem> {
func subscribe(filters: [NostrFilter], to desiredRelays: [RelayURL]? = nil, eoseTimeout: Duration? = nil, id: UUID? = nil) async -> AsyncStream<StreamItem> {
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<StreamItem> { continuation in
let id = id ?? UUID()
@@ -267,34 +301,40 @@ class RelayPool {
var seenEvents: Set<NoteId> = []
var relaysWhoFinishedInitialResults: Set<RelayURL> = []
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))
}

View File

@@ -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()
}

View File

@@ -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

View File

@@ -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
}
}

View File

@@ -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()

View File

@@ -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: {

View File

@@ -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)

View File

@@ -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 {

View File

@@ -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())

View File

@@ -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

View File

@@ -106,7 +106,7 @@ func format_date(date: Date, time_style: DateFormatter.Style = .short) -> String
func make_actionbar_model(ev: NoteId, damus: DamusState) -> ActionBarModel {
let model = ActionBarModel.empty()
model.update(damus: damus, evid: ev)
Task { await model.update(damus: damus, evid: ev) }
return model
}

View File

@@ -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()
}

View File

@@ -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

View File

@@ -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
}

View File

@@ -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))
}

View File

@@ -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 = ""

View File

@@ -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")
}

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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)
}
}
}

View File

@@ -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<Pubkey>) -> NostrPost {
func build_post(state: DamusState, post: NSAttributedString, action: PostAction, uploadedMedias: [UploadedMedia], references: [RefId], filtered_pubkeys: Set<Pubkey>) async -> NostrPost {
// don't add duplicate pubkeys but retain order
var pkset = Set<Pubkey>()
@@ -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)")

View File

@@ -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."))

View File

@@ -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) {

View File

@@ -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.")

View File

@@ -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) }

View File

@@ -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)) }
}
}

View File

@@ -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 {

View File

@@ -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) {

View File

@@ -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 {

View File

@@ -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.")
})

View File

@@ -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
}
}

View File

@@ -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
}

View File

@@ -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 }

View File

@@ -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) }
}
}

View File

@@ -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 {

View File

@@ -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)) }
}
}
}

View File

@@ -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)")

View File

@@ -33,7 +33,9 @@ struct NotifyHandler<T> { }
func notify<T: Notify>(_ notify: Notifications<T>) {
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<T: Notify>(_ handler: NotifyHandler<T>) -> AnyPublisher<T.Payload, Never> {

View File

@@ -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)) }
}

View File

@@ -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)

View File

@@ -64,7 +64,19 @@ enum NdbNoteLender: Sendable {
case .owned(let note):
return try lendingFunction(UnownedNdbNote(note))
}
}
/// Borrows the note temporarily (asynchronously)
func borrow<T>(_ lendingFunction: (_: borrowing UnownedNdbNote) async throws -> T) async throws -> T {
switch self {
case .ndbNoteKey(let ndb, let noteKey):
guard !ndb.is_closed else { throw LendingError.ndbClosed }
guard let ndbNoteTxn = ndb.lookup_note_by_key(noteKey) else { throw LendingError.errorLoadingNote }
guard let unownedNote = UnownedNdbNote(ndbNoteTxn) else { throw LendingError.errorLoadingNote }
return try await lendingFunction(unownedNote)
case .owned(let note):
return try await lendingFunction(UnownedNdbNote(note))
}
}
/// Gets an owned copy of the note

View File

@@ -310,7 +310,10 @@ public func nscript_nostr_cmd(interp: UnsafeMutablePointer<wasm_interp>?, cmd: I
func nscript_add_relay(script: NostrScript, relay: String) -> Bool {
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<wasm_interp>?, 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<wasm_interp>?, 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;
}

View File

@@ -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
}