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