diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj index 3519f7c6..6f452ea8 100644 --- a/damus.xcodeproj/project.pbxproj +++ b/damus.xcodeproj/project.pbxproj @@ -1090,6 +1090,9 @@ D72E127A2BEEEED000F4F781 /* NostrFilterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72E12792BEEEED000F4F781 /* NostrFilterTests.swift */; }; D7315A2A2ACDF3B70036E30A /* DamusCacheManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7315A292ACDF3B70036E30A /* DamusCacheManager.swift */; }; D7315A2C2ACDF4DA0036E30A /* DamusCacheManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7315A2B2ACDF4DA0036E30A /* DamusCacheManagerTests.swift */; }; + D733F9E12D92C1D900317B11 /* SubscriptionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D733F9E02D92C1AA00317B11 /* SubscriptionManager.swift */; }; + D733F9E22D92C1D900317B11 /* SubscriptionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D733F9E02D92C1AA00317B11 /* SubscriptionManager.swift */; }; + D733F9E32D92C1D900317B11 /* SubscriptionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D733F9E02D92C1AA00317B11 /* SubscriptionManager.swift */; }; D733F9E52D92C76100317B11 /* UnownedNdbNote.swift in Sources */ = {isa = PBXBuildFile; fileRef = D733F9E42D92C75C00317B11 /* UnownedNdbNote.swift */; }; D733F9E62D92C76100317B11 /* UnownedNdbNote.swift in Sources */ = {isa = PBXBuildFile; fileRef = D733F9E42D92C75C00317B11 /* UnownedNdbNote.swift */; }; D733F9E72D92C76100317B11 /* UnownedNdbNote.swift in Sources */ = {isa = PBXBuildFile; fileRef = D733F9E42D92C75C00317B11 /* UnownedNdbNote.swift */; }; @@ -1102,6 +1105,15 @@ D73B74E12D8365BA0067BDBC /* ExtraFonts.swift in Sources */ = {isa = PBXBuildFile; fileRef = D73B74E02D8365B40067BDBC /* ExtraFonts.swift */; }; D73B74E22D8365BA0067BDBC /* ExtraFonts.swift in Sources */ = {isa = PBXBuildFile; fileRef = D73B74E02D8365B40067BDBC /* ExtraFonts.swift */; }; D73B74E32D8365BA0067BDBC /* ExtraFonts.swift in Sources */ = {isa = PBXBuildFile; fileRef = D73B74E02D8365B40067BDBC /* ExtraFonts.swift */; }; + D73BDB0D2D6FF5F600D69970 /* NostrNetworkManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D73BDB0C2D6FF58600D69970 /* NostrNetworkManager.swift */; }; + D73BDB0E2D6FF5F600D69970 /* NostrNetworkManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D73BDB0C2D6FF58600D69970 /* NostrNetworkManager.swift */; }; + D73BDB102D6FF5F600D69970 /* NostrNetworkManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D73BDB0C2D6FF58600D69970 /* NostrNetworkManager.swift */; }; + D73BDB142D71216500D69970 /* UserRelayListManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D73BDB132D71215F00D69970 /* UserRelayListManager.swift */; }; + D73BDB152D71216500D69970 /* UserRelayListManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D73BDB132D71215F00D69970 /* UserRelayListManager.swift */; }; + D73BDB162D71216500D69970 /* UserRelayListManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D73BDB132D71215F00D69970 /* UserRelayListManager.swift */; }; + D73BDB182D71311900D69970 /* UserRelayListErrors.swift in Sources */ = {isa = PBXBuildFile; fileRef = D73BDB172D71310C00D69970 /* UserRelayListErrors.swift */; }; + D73BDB192D71311900D69970 /* UserRelayListErrors.swift in Sources */ = {isa = PBXBuildFile; fileRef = D73BDB172D71310C00D69970 /* UserRelayListErrors.swift */; }; + D73BDB1A2D71311900D69970 /* UserRelayListErrors.swift in Sources */ = {isa = PBXBuildFile; fileRef = D73BDB172D71310C00D69970 /* UserRelayListErrors.swift */; }; D73E5E162C6A9619007EB227 /* PostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C75EFA327FA577B0006080F /* PostView.swift */; }; D73E5E172C6A962A007EB227 /* ImageUploadModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CD348EE29C3659D00497EB2 /* ImageUploadModel.swift */; }; D73E5E182C6A963D007EB227 /* AttachMediaUtility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CA876E129A00CE90003B9A3 /* AttachMediaUtility.swift */; }; @@ -2481,12 +2493,16 @@ D72E12792BEEEED000F4F781 /* NostrFilterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NostrFilterTests.swift; sourceTree = ""; }; D7315A292ACDF3B70036E30A /* DamusCacheManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusCacheManager.swift; sourceTree = ""; }; D7315A2B2ACDF4DA0036E30A /* DamusCacheManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusCacheManagerTests.swift; sourceTree = ""; }; + D733F9E02D92C1AA00317B11 /* SubscriptionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionManager.swift; sourceTree = ""; }; D733F9E42D92C75C00317B11 /* UnownedNdbNote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnownedNdbNote.swift; sourceTree = ""; }; D734B1442CCC19B1000B5C97 /* DamusFullScreenCover.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusFullScreenCover.swift; sourceTree = ""; }; D7373BA52B688EA200F7783D /* DamusPurpleTranslationSetupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusPurpleTranslationSetupView.swift; sourceTree = ""; }; D7373BA72B68974500F7783D /* DamusPurpleNewUserOnboardingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusPurpleNewUserOnboardingView.swift; sourceTree = ""; }; D7373BA92B68A65A00F7783D /* PurpleAccountUpdateNotify.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurpleAccountUpdateNotify.swift; sourceTree = ""; }; D73B74E02D8365B40067BDBC /* ExtraFonts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtraFonts.swift; sourceTree = ""; }; + D73BDB0C2D6FF58600D69970 /* NostrNetworkManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NostrNetworkManager.swift; sourceTree = ""; }; + D73BDB132D71215F00D69970 /* UserRelayListManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserRelayListManager.swift; sourceTree = ""; }; + D73BDB172D71310C00D69970 /* UserRelayListErrors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserRelayListErrors.swift; sourceTree = ""; }; D73E5F7E2C6AA066007EB227 /* DamusAliases.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusAliases.swift; sourceTree = ""; }; D73E5F802C6AA07A007EB227 /* HighlighterExtensionAliases.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighlighterExtensionAliases.swift; sourceTree = ""; }; D74AAFC12B153395006CF0F4 /* HeadlessDamusState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeadlessDamusState.swift; sourceTree = ""; }; @@ -2750,6 +2766,7 @@ 4C0A3F8D280F63FF000448DE /* Models */ = { isa = PBXGroup; children = ( + D73BDB122D71212600D69970 /* NostrNetworkManager */, D74F43082B23F09300425B75 /* Purple */, BA3759882ABCCDE30018D73B /* Camera */, 4C190F1E2A535FC200027FD5 /* Zaps */, @@ -3959,6 +3976,17 @@ path = Mocking; sourceTree = ""; }; + D73BDB122D71212600D69970 /* NostrNetworkManager */ = { + isa = PBXGroup; + children = ( + D733F9E02D92C1AA00317B11 /* SubscriptionManager.swift */, + D73BDB172D71310C00D69970 /* UserRelayListErrors.swift */, + D73BDB132D71215F00D69970 /* UserRelayListManager.swift */, + D73BDB0C2D6FF58600D69970 /* NostrNetworkManager.swift */, + ); + path = NostrNetworkManager; + sourceTree = ""; + }; D74EA08C2D2E26E6002290DD /* ErrorHandling */ = { isa = PBXGroup; children = ( @@ -4452,6 +4480,7 @@ 4C3DCC762A9FE9EC0091E592 /* NdbTxn.swift in Sources */, 4CEF958D2A9CE650000F901B /* verifier.c in Sources */, 4C32B9342A9AD01A00DC3548 /* NdbProfile.swift in Sources */, + D73BDB0E2D6FF5F600D69970 /* NostrNetworkManager.swift in Sources */, 4C32B9332A99845B00DC3548 /* Ndb.swift in Sources */, D7ADD3E22B538E3500F104C4 /* DamusPurpleVerifyNpubView.swift in Sources */, 4C4793082A993E8900489948 /* refmap.c in Sources */, @@ -4586,6 +4615,7 @@ 4C32B9582A9AD44700DC3548 /* VeriferOptions.swift in Sources */, D74AAFC22B153395006CF0F4 /* HeadlessDamusState.swift in Sources */, 4CA2EFA0280E37AC0044ACD8 /* TimelineView.swift in Sources */, + D73BDB1A2D71311900D69970 /* UserRelayListErrors.swift in Sources */, 4C30AC7629A5770900E2BD5A /* NotificationItemView.swift in Sources */, 4C86F7C42A76C44C00EC0817 /* ZappingNotify.swift in Sources */, 4C363A8428233689006E126D /* Parser.swift in Sources */, @@ -4622,6 +4652,7 @@ D78DB85B2C20FE5000F0AB12 /* VectorMath.swift in Sources */, D7CB5D3E2B116DAD00AD4105 /* NotificationsManager.swift in Sources */, 50A16FFF2AA76A0900DFEC1F /* DamusVideoCoordinator.swift in Sources */, + D733F9E32D92C1D900317B11 /* SubscriptionManager.swift in Sources */, F7908E97298B1FDF00AB113A /* NIPURLBuilder.swift in Sources */, 4C285C8228385570008A31F1 /* CarouselView.swift in Sources */, 3A3040F129A8FF97008A0F29 /* LocalizationUtil.swift in Sources */, @@ -4730,6 +4761,7 @@ 4C2859602A12A2BE004746F7 /* SupporterBadge.swift in Sources */, D73E5F7F2C6AA066007EB227 /* DamusAliases.swift in Sources */, 4C1A9A2A29DDF54400516EAC /* DamusVideoPlayerView.swift in Sources */, + D73BDB152D71216500D69970 /* UserRelayListManager.swift in Sources */, 4CA352A22A76AEC5003BB08B /* LikedNotify.swift in Sources */, 5CC8529F2BD744F60039FFC5 /* HighlightView.swift in Sources */, BA37598D2ABCCE500018D73B /* PhotoCaptureProcessor.swift in Sources */, @@ -5142,6 +5174,7 @@ 82D6FB212CD99F7900C925F4 /* SelectableText.swift in Sources */, 82D6FB222CD99F7900C925F4 /* DamusColors.swift in Sources */, 82D6FB232CD99F7900C925F4 /* ThiccDivider.swift in Sources */, + D733F9E22D92C1D900317B11 /* SubscriptionManager.swift in Sources */, 82D6FB242CD99F7900C925F4 /* IconLabel.swift in Sources */, 82D6FB252CD99F7900C925F4 /* TruncatedText.swift in Sources */, 82D6FB262CD99F7900C925F4 /* SupporterBadge.swift in Sources */, @@ -5156,6 +5189,7 @@ 82D6FB2F2CD99F7900C925F4 /* BlurHashDecode.swift in Sources */, 82D6FB302CD99F7900C925F4 /* PostBox.swift in Sources */, 82D6FB312CD99F7900C925F4 /* KFOptionSetter+.swift in Sources */, + D73BDB162D71216500D69970 /* UserRelayListManager.swift in Sources */, 82D6FB322CD99F7900C925F4 /* FillAndStroke.swift in Sources */, 82D6FB332CD99F7900C925F4 /* Array.swift in Sources */, 82D6FB342CD99F7900C925F4 /* VectorMath.swift in Sources */, @@ -5388,6 +5422,7 @@ 82D6FC132CD99F7900C925F4 /* FriendIcon.swift in Sources */, 82D6FC142CD99F7900C925F4 /* CondensedProfilePicturesView.swift in Sources */, 82D6FC152CD99F7900C925F4 /* ProfileEditButton.swift in Sources */, + D73BDB102D6FF5F600D69970 /* NostrNetworkManager.swift in Sources */, 82D6FC162CD99F7900C925F4 /* RelayPaidDetail.swift in Sources */, 82D6FC172CD99F7900C925F4 /* RelayAuthenticationDetail.swift in Sources */, 82D6FC182CD99F7900C925F4 /* RelaySoftwareDetail.swift in Sources */, @@ -5441,6 +5476,7 @@ 82D6FC472CD99F7900C925F4 /* RepostAction.swift in Sources */, 82D6FC482CD99F7900C925F4 /* ShareActionButton.swift in Sources */, 82D6FC492CD99F7900C925F4 /* BigButton.swift in Sources */, + D73BDB182D71311900D69970 /* UserRelayListErrors.swift in Sources */, 82D6FC4A2CD99F7900C925F4 /* AddRelayView.swift in Sources */, 82D6FC4B2CD99F7900C925F4 /* BlocksView.swift in Sources */, D74EA0912D2E3464002290DD /* URLHandler.swift in Sources */, @@ -5780,6 +5816,7 @@ D73E5F272C6A97F4007EB227 /* TimeDot.swift in Sources */, D73E5F282C6A97F4007EB227 /* EventTop.swift in Sources */, D73E5F292C6A97F4007EB227 /* ReplyDescription.swift in Sources */, + D73BDB0D2D6FF5F600D69970 /* NostrNetworkManager.swift in Sources */, D73E5F2A2C6A97F4007EB227 /* RelativeTime.swift in Sources */, D73E5F732C6A9885007EB227 /* TestData.swift in Sources */, D78F08192D7F7F7500FC6C75 /* NIP04.swift in Sources */, @@ -5818,6 +5855,7 @@ D73E5F472C6A97F5007EB227 /* BookmarksView.swift in Sources */, D73E5F482C6A97F5007EB227 /* CarouselView.swift in Sources */, D73E5F492C6A97F5007EB227 /* ConfigView.swift in Sources */, + D733F9E12D92C1D900317B11 /* SubscriptionManager.swift in Sources */, D73E5F4A2C6A97F5007EB227 /* CreateAccountView.swift in Sources */, D73E5F7A2C6A9C55007EB227 /* NotificationFormatter.swift in Sources */, D73E5F4B2C6A97F5007EB227 /* DirectMessagesView.swift in Sources */, @@ -5853,6 +5891,7 @@ D73E5F6A2C6A97F5007EB227 /* ReportView.swift in Sources */, D73E5F6C2C6A97F5007EB227 /* RepostsView.swift in Sources */, D734B1462CCC19B1000B5C97 /* DamusFullScreenCover.swift in Sources */, + D73BDB142D71216500D69970 /* UserRelayListManager.swift in Sources */, D73E5F6D2C6A97F5007EB227 /* Launch.storyboard in Sources */, D73E5F6F2C6A97F5007EB227 /* RelayFilterView.swift in Sources */, D703D78A2C670C8A00A400EA /* LibreTranslateServer.swift in Sources */, @@ -5904,6 +5943,7 @@ D703D7A52C670E3E00A400EA /* mdb.c in Sources */, D703D76B2C670B3100A400EA /* Referenced.swift in Sources */, D703D7952C670DE600A400EA /* hash_u5.c in Sources */, + D73BDB192D71311900D69970 /* UserRelayListErrors.swift in Sources */, D703D7582C670A6000A400EA /* Id.swift in Sources */, 5C05675A2C8FBDE70073F23A /* NDBSearchView.swift in Sources */, D703D76E2C670B4900A400EA /* NdbTagsIterator.swift in Sources */, diff --git a/damus/Components/NoteZapButton.swift b/damus/Components/NoteZapButton.swift index 137598e9..715d5336 100644 --- a/damus/Components/NoteZapButton.swift +++ b/damus/Components/NoteZapButton.swift @@ -84,7 +84,7 @@ struct NoteZapButton: View { print("cancel_zap: we already have a real zap, can't cancel") break case .pending(let pzap): - guard let res = cancel_zap(zap: pzap, box: damus_state.postbox, zapcache: damus_state.zaps, evcache: damus_state.events) else { + guard let res = cancel_zap(zap: pzap, box: damus_state.nostrNetwork.postbox, zapcache: damus_state.zaps, evcache: damus_state.events) else { UIImpactFeedbackGenerator(style: .soft).impactOccurred() return @@ -179,7 +179,7 @@ func send_zap(damus_state: DamusState, target: ZapTarget, lnurl: String, is_cust } // Only take the first 10 because reasons - let relays = Array(damus_state.pool.our_descriptors.prefix(10)) + let relays = Array(damus_state.nostrNetwork.pool.our_descriptors.prefix(10)) let content = comment ?? "" guard let mzapreq = make_zap_request_event(keypair: keypair, content: content, relays: relays, target: target, zap_type: zap_type) else { @@ -232,7 +232,7 @@ func send_zap(damus_state: DamusState, target: ZapTarget, lnurl: String, is_cust flusher = .once({ pe in // send donation zap when the pending zap is flushed, this allows user to cancel and not send a donation Task { @MainActor in - await WalletConnect.send_donation_zap(pool: damus_state.pool, postbox: damus_state.postbox, nwc: nwc_state.url, percent: damus_state.settings.donation_percent, base_msats: amount_msat) + await WalletConnect.send_donation_zap(pool: damus_state.nostrNetwork.pool, postbox: damus_state.nostrNetwork.postbox, nwc: nwc_state.url, percent: damus_state.settings.donation_percent, base_msats: amount_msat) } }) } @@ -240,7 +240,7 @@ func send_zap(damus_state: DamusState, target: ZapTarget, lnurl: String, is_cust // we don't have a delay on one-tap nozaps (since this will be from customize zap view) let delay = damus_state.settings.nozaps ? nil : 5.0 - let nwc_req = WalletConnect.pay(url: nwc_state.url, pool: damus_state.pool, post: damus_state.postbox, invoice: inv, delay: delay, on_flush: flusher) + let nwc_req = WalletConnect.pay(url: nwc_state.url, pool: damus_state.nostrNetwork.pool, post: damus_state.nostrNetwork.postbox, invoice: inv, delay: delay, on_flush: flusher) guard let nwc_req, case .nwc(let pzap_state) = pending_zap_state else { print("nwc: failed to send nwc request for zapreq \(reqid.reqid)") diff --git a/damus/Components/Status/UserStatusSheet.swift b/damus/Components/Status/UserStatusSheet.swift index 00f56ff1..f2c2dd88 100644 --- a/damus/Components/Status/UserStatusSheet.swift +++ b/damus/Components/Status/UserStatusSheet.swift @@ -213,6 +213,6 @@ struct UserStatusSheet: View { struct UserStatusSheet_Previews: PreviewProvider { static var previews: some View { - UserStatusSheet(damus_state: test_damus_state, postbox: test_damus_state.postbox, keypair: test_keypair, status: .init()) + UserStatusSheet(damus_state: test_damus_state, postbox: test_damus_state.nostrNetwork.postbox, keypair: test_keypair, status: .init()) } } diff --git a/damus/ContentView.swift b/damus/ContentView.swift index 8b3a08b8..d69ea659 100644 --- a/damus/ContentView.swift +++ b/damus/ContentView.swift @@ -199,7 +199,7 @@ struct ContentView: View { func MaybeReportView(target: ReportTarget) -> some View { Group { if let keypair = damus_state.keypair.to_full() { - ReportView(postbox: damus_state.postbox, target: target, keypair: keypair) + ReportView(postbox: damus_state.nostrNetwork.postbox, target: target, keypair: keypair) } else { EmptyView() } @@ -317,7 +317,7 @@ struct ContentView: View { case .post(let action): PostView(action: action, damus_state: damus_state!) case .user_status: - UserStatusSheet(damus_state: damus_state!, postbox: damus_state!.postbox, keypair: damus_state!.keypair, status: damus_state!.profiles.profile_data(damus_state!.pubkey).status) + UserStatusSheet(damus_state: damus_state!, postbox: damus_state!.nostrNetwork.postbox, keypair: damus_state!.keypair, status: damus_state!.profiles.profile_data(damus_state!.pubkey).status) .presentationDragIndicator(.visible) case .event: EventDetailView() @@ -356,7 +356,7 @@ struct ContentView: View { self.hide_bar = !show } .onReceive(timer) { n in - self.damus_state?.postbox.try_flushing_events() + self.damus_state?.nostrNetwork.postbox.try_flushing_events() self.damus_state!.profiles.profile_data(self.damus_state!.pubkey).status.try_expire() } .onReceive(handle_notify(.report)) { target in @@ -367,10 +367,6 @@ struct ContentView: View { self.confirm_mute = true } .onReceive(handle_notify(.attached_wallet)) { nwc in - // Ensure to add NWC relay to the pool and connect it. - try? damus_state.pool.add_relay(.nwc(url: nwc.relay)) - damus_state.pool.connect(to: [nwc.relay]) - // update the lightning address on our profile when we attach a // wallet with an associated guard let ds = self.damus_state, @@ -391,12 +387,12 @@ 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) guard let ev = make_metadata_event(keypair: keypair, metadata: prof) else { return } - ds.postbox.send(ev) + ds.nostrNetwork.postbox.send(ev) } .onReceive(handle_notify(.broadcast)) { ev in guard let ds = self.damus_state else { return } - ds.postbox.send(ev) + ds.nostrNetwork.postbox.send(ev) } .onReceive(handle_notify(.unfollow)) { target in guard let state = self.damus_state else { return } @@ -418,7 +414,7 @@ struct ContentView: View { return } - if !handle_post_notification(keypair: keypair, postbox: state.postbox, events: state.events, post: post) { + if !handle_post_notification(keypair: keypair, postbox: state.nostrNetwork.postbox, events: state.events, post: post) { self.active_sheet = nil } } @@ -462,7 +458,7 @@ struct ContentView: View { } } .onReceive(handle_notify(.disconnect_relays)) { () in - damus_state.pool.disconnect() + damus_state.nostrNetwork.pool.disconnect() } .onReceive(NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)) { obj in print("txn: 📙 DAMUS ACTIVE NOTIFY") @@ -508,7 +504,7 @@ struct ContentView: View { break case .active: print("txn: 📙 DAMUS ACTIVE") - damus_state.pool.ping() + damus_state.nostrNetwork.pool.ping() @unknown default: break } @@ -527,7 +523,7 @@ 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) guard let profile_ev = make_metadata_event(keypair: keypair, metadata: prof) else { return } - ds.postbox.send(profile_ev) + ds.nostrNetwork.postbox.send(profile_ev) } .alert(NSLocalizedString("User muted", comment: "Alert message to indicate the user has been muted"), isPresented: $user_muted_confirm, actions: { Button(NSLocalizedString("Thanks!", comment: "Button to close out of alert that informs that the action to muted a user was successful.")) { @@ -559,7 +555,7 @@ struct ContentView: View { } ds.mutelist_manager.set_mutelist(mutelist) - ds.postbox.send(mutelist) + ds.nostrNetwork.postbox.send(mutelist) confirm_overwrite_mutelist = false confirm_mute = false @@ -591,7 +587,7 @@ struct ContentView: View { } ds.mutelist_manager.set_mutelist(ev) - ds.postbox.send(ev) + ds.nostrNetwork.postbox.send(ev) } } }, message: { @@ -660,28 +656,14 @@ struct ContentView: View { guard let ndb = mndb else { return } - let pool = RelayPool(ndb: ndb, keypair: keypair) let model_cache = RelayModelCache() let relay_filters = RelayFilters(our_pubkey: pubkey) - let bootstrap_relays = load_bootstrap_relays(pubkey: pubkey) let settings = UserSettingsStore.globally_load_for(pubkey: pubkey) let new_relay_filters = load_relay_filters(pubkey) == nil - for relay in bootstrap_relays { - let descriptor = RelayPool.RelayDescriptor(url: relay, info: .rw) - add_new_relay(model_cache: model_cache, relay_filters: relay_filters, pool: pool, descriptor: descriptor, new_relay_filters: new_relay_filters, logging_enabled: settings.developer_mode) - } - pool.register_handler(sub_id: sub_id, handler: home.handle_event) - - if let nwc_str = settings.nostr_wallet_connect, - let nwc = WalletConnectURL(str: nwc_str) { - try? pool.add_relay(.nwc(url: nwc.relay)) - } - - self.damus_state = DamusState(pool: pool, - keypair: keypair, + self.damus_state = DamusState(keypair: keypair, likes: EventCounter(our_pubkey: pubkey), boosts: EventCounter(our_pubkey: pubkey), contacts: Contacts(our_pubkey: pubkey), @@ -697,8 +679,6 @@ struct ContentView: View { drafts: Drafts(), events: EventCache(ndb: ndb), bookmarks: BookmarksManager(pubkey: pubkey), - postbox: PostBox(pool: pool), - bootstrap_relays: bootstrap_relays, replies: ReplyCounter(our_pubkey: pubkey), wallet: WalletModel(settings: settings), nav: self.navigationCoordinator, @@ -722,7 +702,8 @@ struct ContentView: View { // Purple API is an experimental feature. If not enabled, do not connect `StoreObserver` with Purple API to avoid leaking receipts } - pool.connect() + damus_state.nostrNetwork.pool.register_handler(sub_id: sub_id, handler: home.handle_event) + damus_state.nostrNetwork.connect() } func music_changed(_ state: MusicState) { @@ -745,7 +726,7 @@ struct ContentView: View { pdata.status.music = music guard let ev = music.to_note(keypair: kp) else { return } - damus_state.postbox.send(ev) + damus_state.nostrNetwork.postbox.send(ev) } } @@ -994,7 +975,7 @@ func find_event_with_subid(state: DamusState, query query_: FindEvent, subid: St var has_event = false guard let filter else { return } - state.pool.subscribe_to(sub_id: subid, filters: [filter], to: find_from) { relay_id, res in + state.nostrNetwork.pool.subscribe_to(sub_id: subid, filters: [filter], to: find_from) { relay_id, res in guard case .nostr_event(let ev) = res else { return } @@ -1008,7 +989,7 @@ func find_event_with_subid(state: DamusState, query query_: FindEvent, subid: St break case .event(_, let ev): has_event = true - state.pool.unsubscribe(sub_id: subid) + state.nostrNetwork.pool.unsubscribe(sub_id: subid) switch query { case .profile: @@ -1021,11 +1002,11 @@ func find_event_with_subid(state: DamusState, query query_: FindEvent, subid: St case .eose: if !has_event { attempts += 1 - if attempts >= state.pool.our_descriptors.count { + if attempts >= state.nostrNetwork.pool.our_descriptors.count { callback(nil) // If we could not find any events in any of the relays we are connected to, send back nil } } - state.pool.unsubscribe(sub_id: subid, to: [relay_id]) // We are only finding an event once, so close subscription on eose + state.nostrNetwork.pool.unsubscribe(sub_id: subid, to: [relay_id]) // We are only finding an event once, so close subscription on eose case .notice: break case .auth: @@ -1050,9 +1031,9 @@ func naddrLookup(damus_state: DamusState, naddr: NAddr, callback: @escaping (Nos let subid = UUID().description - damus_state.pool.subscribe_to(sub_id: subid, filters: [filter], to: nil) { relay_id, res in + damus_state.nostrNetwork.pool.subscribe_to(sub_id: subid, filters: [filter], to: nil) { relay_id, res in guard case .nostr_event(let ev) = res else { - damus_state.pool.unsubscribe(sub_id: subid, to: [relay_id]) + damus_state.nostrNetwork.pool.unsubscribe(sub_id: subid, to: [relay_id]) return } @@ -1060,14 +1041,14 @@ func naddrLookup(damus_state: DamusState, naddr: NAddr, callback: @escaping (Nos for tag in ev.tags { if(tag.count >= 2 && tag[0].string() == "d"){ if (tag[1].string() == naddr.identifier){ - damus_state.pool.unsubscribe(sub_id: subid, to: [relay_id]) + damus_state.nostrNetwork.pool.unsubscribe(sub_id: subid, to: [relay_id]) callback(ev) return } } } } - damus_state.pool.unsubscribe(sub_id: subid, to: [relay_id]) + damus_state.nostrNetwork.pool.unsubscribe(sub_id: subid, to: [relay_id]) } } @@ -1115,7 +1096,7 @@ func handle_unfollow(state: DamusState, unfollow: FollowRef) -> Bool { let old_contacts = state.contacts.event - guard let ev = unfollow_reference(postbox: state.postbox, our_contacts: old_contacts, keypair: keypair, unfollow: unfollow) + guard let ev = unfollow_reference(postbox: state.nostrNetwork.postbox, our_contacts: old_contacts, keypair: keypair, unfollow: unfollow) else { return false } @@ -1141,7 +1122,7 @@ func handle_follow(state: DamusState, follow: FollowRef) -> Bool { return false } - guard let ev = follow_reference(box: state.postbox, our_contacts: state.contacts.event, keypair: keypair, follow: follow) + guard let ev = follow_reference(box: state.nostrNetwork.postbox, our_contacts: state.contacts.event, keypair: keypair, follow: follow) else { return false } diff --git a/damus/Models/Contacts+.swift b/damus/Models/Contacts+.swift index 3c712db7..81dd9803 100644 --- a/damus/Models/Contacts+.swift +++ b/damus/Models/Contacts+.swift @@ -67,41 +67,6 @@ func decode_json_relays(_ content: String) -> [RelayURL: LegacyKind3RelayRWConfi return decode_json(content) } -func remove_relay(ev: NostrEvent, current_relays: [RelayPool.RelayDescriptor], keypair: FullKeypair, relay: RelayURL) -> NostrEvent?{ - var relays = ensure_relay_info(relays: current_relays, content: ev.content) - - relays.removeValue(forKey: relay) - - guard let content = encode_json(relays) else { - return nil - } - - return NostrEvent(content: content, keypair: keypair.to_keypair(), kind: 3, tags: ev.tags.strings()) -} - -/// Handles the creation of a new `kind:3` contact list based on a previous contact list, with the specified relays -func add_relay(ev: NostrEvent, keypair: FullKeypair, current_relays: [RelayPool.RelayDescriptor], relay: RelayURL, info: LegacyKind3RelayRWConfiguration) -> NostrEvent? { - var relays = ensure_relay_info(relays: current_relays, content: ev.content) - - // If kind:3 content is empty, or if the relay doesn't exist in the list, - // we want to create a kind:3 event with the new relay - guard ev.content.isEmpty || relays.index(forKey: relay) == nil else { - return nil - } - - relays[relay] = info - - guard let content = encode_json(relays) else { - return nil - } - - return NostrEvent(content: content, keypair: keypair.to_keypair(), kind: 3, tags: ev.tags.strings()) -} - -func ensure_relay_info(relays: [RelayPool.RelayDescriptor], content: String) -> [RelayURL: LegacyKind3RelayRWConfiguration] { - return decode_json_relays(content) ?? make_contact_relays(relays) -} - func is_already_following(contacts: NostrEvent, follow: FollowRef) -> Bool { return contacts.references.contains { ref in switch (ref, follow) { @@ -129,22 +94,3 @@ func follow_with_existing_contacts(keypair: FullKeypair, our_contacts: NostrEven return NostrEvent(content: our_contacts.content, keypair: keypair.to_keypair(), kind: kind, tags: tags) } -func make_contact_relays(_ relays: [RelayPool.RelayDescriptor]) -> [RelayURL: LegacyKind3RelayRWConfiguration] { - return relays.reduce(into: [:]) { acc, relay in - acc[relay.url] = relay.info - } -} - -func make_relay_metadata(relays: [RelayPool.RelayDescriptor], keypair: FullKeypair) -> NostrEvent? { - let tags = relays.compactMap { r -> [String]? in - var tag = ["r", r.url.absoluteString] - if (r.info.read ?? true) != (r.info.write ?? true) { - tag += r.info.read == true ? ["read"] : ["write"] - } - if ((r.info.read ?? true) || (r.info.write ?? true)) && r.variant == .regular { - return tag; - } - return nil - } - return NostrEvent(content: "", keypair: keypair.to_keypair(), kind: 10_002, tags: tags) -} diff --git a/damus/Models/CreateAccountModel.swift b/damus/Models/CreateAccountModel.swift index b0f52ca0..c5da5b90 100644 --- a/damus/Models/CreateAccountModel.swift +++ b/damus/Models/CreateAccountModel.swift @@ -27,6 +27,10 @@ class CreateAccountModel: ObservableObject { return Keypair(pubkey: self.pubkey, privkey: self.privkey) } + var full_keypair: FullKeypair { + return FullKeypair(pubkey: self.pubkey, privkey: self.privkey) + } + init(display_name: String = "", name: String = "", about: String = "") { let keypair = generate_new_keypair() self.pubkey = keypair.pubkey diff --git a/damus/Models/DamusState.swift b/damus/Models/DamusState.swift index c816ef94..26b8b693 100644 --- a/damus/Models/DamusState.swift +++ b/damus/Models/DamusState.swift @@ -10,7 +10,6 @@ import LinkPresentation import EmojiPicker class DamusState: HeadlessDamusState { - let pool: RelayPool let keypair: Keypair let likes: EventCounter let boosts: EventCounter @@ -28,8 +27,6 @@ class DamusState: HeadlessDamusState { let drafts: Drafts let events: EventCache let bookmarks: BookmarksManager - let postbox: PostBox - let bootstrap_relays: [RelayURL] let replies: ReplyCounter let wallet: WalletModel let nav: NavigationCoordinator @@ -39,9 +36,9 @@ class DamusState: HeadlessDamusState { var purple: DamusPurple var push_notification_client: PushNotificationClient let emoji_provider: EmojiProvider + private(set) var nostrNetwork: NostrNetworkManager - init(pool: RelayPool, keypair: Keypair, likes: EventCounter, boosts: EventCounter, contacts: Contacts, mutelist_manager: MutelistManager, profiles: Profiles, dms: DirectMessagesModel, previews: PreviewCache, zaps: Zaps, lnurls: LNUrls, settings: UserSettingsStore, relay_filters: RelayFilters, relay_model_cache: RelayModelCache, drafts: Drafts, events: EventCache, bookmarks: BookmarksManager, postbox: PostBox, bootstrap_relays: [RelayURL], replies: ReplyCounter, wallet: WalletModel, nav: NavigationCoordinator, music: MusicController?, video: DamusVideoCoordinator, ndb: Ndb, purple: DamusPurple? = nil, quote_reposts: EventCounter, emoji_provider: EmojiProvider) { - self.pool = pool + init(keypair: Keypair, likes: EventCounter, boosts: EventCounter, contacts: Contacts, mutelist_manager: MutelistManager, profiles: Profiles, dms: DirectMessagesModel, previews: PreviewCache, zaps: Zaps, lnurls: LNUrls, settings: UserSettingsStore, relay_filters: RelayFilters, relay_model_cache: RelayModelCache, drafts: Drafts, events: EventCache, bookmarks: BookmarksManager, replies: ReplyCounter, wallet: WalletModel, nav: NavigationCoordinator, music: MusicController?, video: DamusVideoCoordinator, ndb: Ndb, purple: DamusPurple? = nil, quote_reposts: EventCounter, emoji_provider: EmojiProvider) { self.keypair = keypair self.likes = likes self.boosts = boosts @@ -58,8 +55,6 @@ class DamusState: HeadlessDamusState { self.drafts = drafts self.events = events self.bookmarks = bookmarks - self.postbox = postbox - self.bootstrap_relays = bootstrap_relays self.replies = replies self.wallet = wallet self.nav = nav @@ -73,6 +68,9 @@ class DamusState: HeadlessDamusState { self.quote_reposts = quote_reposts self.push_notification_client = PushNotificationClient(keypair: keypair, settings: settings) self.emoji_provider = emoji_provider + + let networkManagerDelegate = NostrNetworkManagerDelegate(settings: settings, contacts: contacts, ndb: ndb, keypair: keypair, relayModelCache: relay_model_cache, relayFilters: relay_filters) + self.nostrNetwork = NostrNetworkManager(delegate: networkManagerDelegate) } @MainActor @@ -98,27 +96,13 @@ class DamusState: HeadlessDamusState { guard let ndb = mndb else { return nil } let pubkey = keypair.pubkey - let pool = RelayPool(ndb: ndb, keypair: keypair) let model_cache = RelayModelCache() let relay_filters = RelayFilters(our_pubkey: pubkey) let bootstrap_relays = load_bootstrap_relays(pubkey: pubkey) let settings = UserSettingsStore.globally_load_for(pubkey: pubkey) - let new_relay_filters = load_relay_filters(pubkey) == nil - for relay in bootstrap_relays { - let descriptor = RelayPool.RelayDescriptor(url: relay, info: .rw) - add_new_relay(model_cache: model_cache, relay_filters: relay_filters, pool: pool, descriptor: descriptor, new_relay_filters: new_relay_filters, logging_enabled: settings.developer_mode) - } - - pool.register_handler(sub_id: sub_id, handler: home.handle_event) - - if let nwc_str = settings.nostr_wallet_connect, - let nwc = WalletConnectURL(str: nwc_str) { - try? pool.add_relay(.nwc(url: nwc.relay)) - } self.init( - pool: pool, keypair: keypair, likes: EventCounter(our_pubkey: pubkey), boosts: EventCounter(our_pubkey: pubkey), @@ -135,8 +119,6 @@ class DamusState: HeadlessDamusState { drafts: Drafts(), events: EventCache(ndb: ndb), bookmarks: BookmarksManager(pubkey: pubkey), - postbox: PostBox(pool: pool), - bootstrap_relays: bootstrap_relays, replies: ReplyCounter(our_pubkey: pubkey), wallet: WalletModel(settings: settings), nav: navigationCoordinator, @@ -179,7 +161,7 @@ class DamusState: HeadlessDamusState { try await self.push_notification_client.revoke_token() } wallet.disconnect() - pool.close() + nostrNetwork.pool.close() ndb.close() } @@ -189,7 +171,6 @@ class DamusState: HeadlessDamusState { let kp = Keypair(pubkey: empty_pub, privkey: nil) return DamusState.init( - pool: RelayPool(ndb: .empty), keypair: Keypair(pubkey: empty_pub, privkey: empty_sec), likes: EventCounter(our_pubkey: empty_pub), boosts: EventCounter(our_pubkey: empty_pub), @@ -206,8 +187,6 @@ class DamusState: HeadlessDamusState { drafts: Drafts(), events: EventCache(ndb: .empty), bookmarks: BookmarksManager(pubkey: empty_pub), - postbox: PostBox(pool: RelayPool(ndb: .empty)), - bootstrap_relays: [], replies: ReplyCounter(our_pubkey: empty_pub), wallet: WalletModel(settings: UserSettingsStore()), nav: NavigationCoordinator(), @@ -219,3 +198,29 @@ class DamusState: HeadlessDamusState { ) } } + +fileprivate extension DamusState { + struct NostrNetworkManagerDelegate: NostrNetworkManager.Delegate { + let settings: UserSettingsStore + let contacts: Contacts + + var ndb: Ndb + var keypair: Keypair + + var latestRelayListEventIdHex: String? { + get { self.settings.latestRelayListEventIdHex } + set { self.settings.latestRelayListEventIdHex = newValue } + } + + var latestContactListEvent: NostrEvent? { self.contacts.event } + var bootstrapRelays: [RelayURL] { get_default_bootstrap_relays() } + var developerMode: Bool { self.settings.developer_mode } + var relayModelCache: RelayModelCache + var relayFilters: RelayFilters + + var nwcWallet: WalletConnectURL? { + guard let nwcString = self.settings.nostr_wallet_connect else { return nil } + return WalletConnectURL(str: nwcString) + } + } +} diff --git a/damus/Models/DraftsModel.swift b/damus/Models/DraftsModel.swift index 1ae5b772..d7aad12d 100644 --- a/damus/Models/DraftsModel.swift +++ b/damus/Models/DraftsModel.swift @@ -251,7 +251,7 @@ class Drafts: ObservableObject { // TODO: Once it is time to implement draft syncing with relays, please consider the following: // - Privacy: Sending drafts to the network leaks metadata about app activity, and may break user expectations // - Down-sync conflict resolution: Consider how to solve conflicts for different draft versions holding the same ID (e.g. edited in Damus, then another client, then Damus again) - damus_state.pool.send_raw_to_local_ndb(.typical(.event(draft_event))) + damus_state.nostrNetwork.pool.send_raw_to_local_ndb(.typical(.event(draft_event))) } damus_state.settings.draft_event_ids = draft_events.map({ $0.id.hex() }) diff --git a/damus/Models/EventsModel.swift b/damus/Models/EventsModel.swift index aac2faa9..e2d5fef1 100644 --- a/damus/Models/EventsModel.swift +++ b/damus/Models/EventsModel.swift @@ -68,13 +68,13 @@ class EventsModel: ObservableObject { } func subscribe() { - state.pool.subscribe(sub_id: sub_id, + state.nostrNetwork.pool.subscribe(sub_id: sub_id, filters: [get_filter()], handler: handle_nostr_event) } func unsubscribe() { - state.pool.unsubscribe(sub_id: sub_id) + state.nostrNetwork.pool.unsubscribe(sub_id: sub_id) } private func handle_event(relay_id: RelayURL, ev: NostrEvent) { diff --git a/damus/Models/FollowersModel.swift b/damus/Models/FollowersModel.swift index 56e83215..24f052a1 100644 --- a/damus/Models/FollowersModel.swift +++ b/damus/Models/FollowersModel.swift @@ -37,11 +37,11 @@ class FollowersModel: ObservableObject { let filter = get_filter() let filters = [filter] //print_filters(relay_id: "following", filters: [filters]) - self.damus_state.pool.subscribe(sub_id: sub_id, filters: filters, handler: handle_event) + self.damus_state.nostrNetwork.pool.subscribe(sub_id: sub_id, filters: filters, handler: handle_event) } func unsubscribe() { - self.damus_state.pool.unsubscribe(sub_id: sub_id) + self.damus_state.nostrNetwork.pool.unsubscribe(sub_id: sub_id) } func handle_contact_event(_ ev: NostrEvent) { @@ -61,7 +61,7 @@ class FollowersModel: ObservableObject { let filter = NostrFilter(kinds: [.metadata], authors: authors) - damus_state.pool.subscribe_to(sub_id: profiles_id, filters: [filter], to: [relay_id], handler: handle_event) + damus_state.nostrNetwork.pool.subscribe_to(sub_id: profiles_id, filters: [filter], to: [relay_id], handler: handle_event) } func handle_event(relay_id: RelayURL, ev: NostrConnectionEvent) { @@ -86,7 +86,7 @@ class FollowersModel: ObservableObject { guard let txn = NdbTxn(ndb: self.damus_state.ndb) else { return } load_profiles(relay_id: relay_id, txn: txn) } else if sub_id == self.profiles_id { - damus_state.pool.unsubscribe(sub_id: profiles_id, to: [relay_id]) + damus_state.nostrNetwork.pool.unsubscribe(sub_id: profiles_id, to: [relay_id]) } case .ok: diff --git a/damus/Models/FollowingModel.swift b/damus/Models/FollowingModel.swift index 9a6e2e7a..db7c1275 100644 --- a/damus/Models/FollowingModel.swift +++ b/damus/Models/FollowingModel.swift @@ -42,7 +42,7 @@ class FollowingModel { } let filters = [filter] //print_filters(relay_id: "following", filters: [filters]) - self.damus_state.pool.subscribe(sub_id: sub_id, filters: filters, handler: handle_event) + self.damus_state.nostrNetwork.pool.subscribe(sub_id: sub_id, filters: filters, handler: handle_event) } func unsubscribe() { @@ -50,7 +50,7 @@ class FollowingModel { return } print("unsubscribing from following \(sub_id)") - self.damus_state.pool.unsubscribe(sub_id: sub_id) + self.damus_state.nostrNetwork.pool.unsubscribe(sub_id: sub_id) } func handle_event(relay_id: RelayURL, ev: NostrConnectionEvent) { diff --git a/damus/Models/HomeModel.swift b/damus/Models/HomeModel.swift index 06973484..14853dbf 100644 --- a/damus/Models/HomeModel.swift +++ b/damus/Models/HomeModel.swift @@ -95,13 +95,13 @@ class HomeModel: ContactsDelegate { } var pool: RelayPool { - return damus_state.pool + self.damus_state.nostrNetwork.pool } var dms: DirectMessagesModel { return damus_state.dms } - + func has_sub_id_event(sub_id: String, ev_id: NoteId) -> Bool { if !has_event.keys.contains(sub_id) { has_event[sub_id] = Set() @@ -268,7 +268,7 @@ class HomeModel: ContactsDelegate { // since command results are not returned for ephemeral events, // remove the request from the postbox which is likely failing over and over - if damus_state.postbox.remove_relayer(relay_id: nwc.relay, event_id: resp.req_id) { + if damus_state.nostrNetwork.postbox.remove_relayer(relay_id: nwc.relay, event_id: resp.req_id) { print("nwc: got response, removed \(resp.req_id) from the postbox [\(relay)]") } else { print("nwc: \(resp.req_id) not found in the postbox, nothing to remove [\(relay)]") @@ -480,7 +480,7 @@ class HomeModel: ContactsDelegate { break } - update_signal_from_pool(signal: self.signal, pool: damus_state.pool) + update_signal_from_pool(signal: self.signal, pool: damus_state.nostrNetwork.pool) case .nostr_event(let ev): switch ev { case .event(let sub_id, let ev): @@ -950,7 +950,6 @@ func load_our_stuff(state: DamusState, ev: NostrEvent) { state.contacts.event = ev load_our_contacts(state: state, m_old_ev: m_old_ev, ev: ev) - load_our_relays(state: state, m_old_ev: m_old_ev, ev: ev) } func process_contact_event(state: DamusState, ev: NostrEvent) { @@ -958,78 +957,6 @@ func process_contact_event(state: DamusState, ev: NostrEvent) { add_contact_if_friend(contacts: state.contacts, ev: ev) } -func load_our_relays(state: DamusState, m_old_ev: NostrEvent?, ev: NostrEvent) { - let bootstrap_dict: [RelayURL: LegacyKind3RelayRWConfiguration] = [:] - let old_decoded = m_old_ev.flatMap { decode_json_relays($0.content) } ?? state.bootstrap_relays.reduce(into: bootstrap_dict) { (d, r) in - d[r] = .rw - } - - guard let decoded: [RelayURL: LegacyKind3RelayRWConfiguration] = decode_json_relays(ev.content) else { - return - } - - var changed = false - - var new = Set() - for key in decoded.keys { - new.insert(key) - } - - var old = Set() - for key in old_decoded.keys { - old.insert(key) - } - - let diff = old.symmetricDifference(new) - - let new_relay_filters = load_relay_filters(state.pubkey) == nil - for d in diff { - changed = true - if new.contains(d) { - let descriptor = RelayPool.RelayDescriptor(url: d, info: decoded[d] ?? .rw) - add_new_relay(model_cache: state.relay_model_cache, relay_filters: state.relay_filters, pool: state.pool, descriptor: descriptor, new_relay_filters: new_relay_filters, logging_enabled: state.settings.developer_mode) - } else { - state.pool.remove_relay(d) - } - } - - if changed { - save_bootstrap_relays(pubkey: state.pubkey, relays: Array(new)) - state.pool.connect() - notify(.relays_changed) - } -} - -func add_new_relay(model_cache: RelayModelCache, relay_filters: RelayFilters, pool: RelayPool, descriptor: RelayPool.RelayDescriptor, new_relay_filters: Bool, logging_enabled: Bool) { - try? pool.add_relay(descriptor) - let url = descriptor.url - - let relay_id = url - guard model_cache.model(withURL: url) == nil else { - return - } - - Task.detached(priority: .background) { - guard let meta = try? await fetch_relay_metadata(relay_id: relay_id) else { - return - } - - await MainActor.run { - let model = RelayModel(url, metadata: meta) - model_cache.insert(model: model) - - if logging_enabled { - pool.setLog(model.log, for: relay_id) - } - - // if this is the first time adding filters, we should filter non-paid relays - if new_relay_filters && !meta.is_paid { - relay_filters.insert(timeline: .search, relay_id: relay_id) - } - } - } -} - func fetch_relay_metadata(relay_id: RelayURL) async throws -> RelayMetadata? { var urlString = relay_id.absoluteString.replacingOccurrences(of: "wss://", with: "https://") urlString = urlString.replacingOccurrences(of: "ws://", with: "http://") @@ -1252,3 +1179,4 @@ func create_in_app_event_zap_notification(profiles: Profiles, zap: Zap, locale: } } } + diff --git a/damus/Models/MutedThreadsManager.swift b/damus/Models/MutedThreadsManager.swift index 7a4e2c25..e4c93c75 100644 --- a/damus/Models/MutedThreadsManager.swift +++ b/damus/Models/MutedThreadsManager.swift @@ -33,7 +33,7 @@ func migrate_old_muted_threads_to_new_mutelist(keypair: Keypair, damus_state: Da let previous_mute_list_event = damus_state.mutelist_manager.event guard let new_mutelist_event = create_or_update_mutelist(keypair: fullKeypair, mprev: previous_mute_list_event, to_add: Set(mutedThreads.map { MuteItem.thread($0, nil) })) else { return } damus_state.mutelist_manager.set_mutelist(new_mutelist_event) - damus_state.postbox.send(new_mutelist_event) + damus_state.nostrNetwork.postbox.send(new_mutelist_event) // Set existing muted threads to an empty array UserDefaults.standard.set([], forKey: getMutedThreadsKey(pubkey: keypair.pubkey)) } diff --git a/damus/Models/NostrNetworkManager/NostrNetworkManager.swift b/damus/Models/NostrNetworkManager/NostrNetworkManager.swift new file mode 100644 index 00000000..3c3798e2 --- /dev/null +++ b/damus/Models/NostrNetworkManager/NostrNetworkManager.swift @@ -0,0 +1,95 @@ +// +// NostrNetworkManager.swift +// damus +// +// Created by Daniel D’Aquino on 2025-02-26. +// +import Foundation + +/// Manages interactions with the Nostr Network. +/// +/// This delineates a layer that is responsible for doing mid-level management of interactions with the Nostr network, controlling lower-level classes that perform more network/DB specific code, and providing an easier to use and more semantic interfaces for the rest of the app. +/// +/// This is responsible for: +/// - Managing the user's relay list +/// - Establishing a `RelayPool` and maintaining it in sync with the user's relay list as it changes +/// - Abstracting away complexities of interacting with the nostr network, providing an easier-to-use interface to fetch and send content related to the Nostr network +/// +/// This is **NOT** responsible for: +/// - Doing actual storage of relay list (delegated via the delegate +/// - Handling low-level relay logic (this will be delegated to lower level classes used in RelayPool/RelayConnection) +class NostrNetworkManager { + /// The relay pool that we manage + /// + /// ## Implementation notes + /// + /// - This will be marked `private` in the future to prevent other code from accessing the relay pool directly. Code outside this layer should use a higher level interface + let pool: RelayPool // TODO: Make this private and make higher level interface for classes outside the NostrNetworkManager + /// A delegate that allows us to interact with the rest of app without introducing hard or circular dependencies + private var delegate: Delegate + /// Manages the user's relay list, controls RelayPool's connected relays + let userRelayList: UserRelayListManager + /// Handles sending out notes to the network + let postbox: PostBox + /// Handles subscriptions and functions to read or consume data from the Nostr network + let reader: SubscriptionManager + + init(delegate: Delegate) { + self.delegate = delegate + let pool = RelayPool(ndb: delegate.ndb, keypair: delegate.keypair) + self.pool = pool + let reader = SubscriptionManager(pool: pool, ndb: delegate.ndb) + let userRelayList = UserRelayListManager(delegate: delegate, pool: pool, reader: reader) + self.reader = reader + self.userRelayList = userRelayList + self.postbox = PostBox(pool: pool) + } + + // MARK: - Control functions + + /// Connects the app to the Nostr network + func connect() { + self.userRelayList.connect() + } +} + + +// MARK: - Helper types + +extension NostrNetworkManager { + /// The delegate that provides information and structure for the `NostrNetworkManager` to function. + /// + /// ## Implementation notes + /// + /// This is needed to prevent a circular reference between `DamusState` and `NostrNetworkManager`, and reduce coupling. + protocol Delegate: Sendable { + /// NostrDB instance, used with `RelayPool` to send events for ingestion. + var ndb: Ndb { get } + + /// The keypair to use for relay authentication and updating relay lists + var keypair: Keypair { get } + + /// The latest relay list event id hex + var latestRelayListEventIdHex: String? { get set } // TODO: Update this once we have full NostrDB query support + + /// The latest contact list `NostrEvent` + /// + /// Note: Read-only access, because `NostrNetworkManager` does not manage contact lists. + var latestContactListEvent: NostrEvent? { get } + + /// Default bootstrap relays to start with when a user relay list is not present + var bootstrapRelays: [RelayURL] { get } + + /// Whether the app is in developer mode + var developerMode: Bool { get } + + /// The cache of relay model information + var relayModelCache: RelayModelCache { get } + + /// Relay filters + var relayFilters: RelayFilters { get } + + /// The user's connected NWC wallet + var nwcWallet: WalletConnectURL? { get } + } +} diff --git a/damus/Models/NostrNetworkManager/SubscriptionManager.swift b/damus/Models/NostrNetworkManager/SubscriptionManager.swift new file mode 100644 index 00000000..536c897f --- /dev/null +++ b/damus/Models/NostrNetworkManager/SubscriptionManager.swift @@ -0,0 +1,70 @@ +// +// SubscriptionManager.swift +// damus +// +// Created by Daniel D’Aquino on 2025-03-25. +// + +extension NostrNetworkManager { + /// Reads or fetches information from RelayPool and NostrDB, and provides an easier and unified higher-level interface. + /// + /// ## Implementation notes + /// + /// - This class will be a key part of the local relay model migration. Most higher-level code should fetch content from this class, which will properly setup the correct relay pool subscriptions, and provide a stream from NostrDB for higher performance and reliability. + class SubscriptionManager { + private let pool: RelayPool + private var ndb: Ndb + + init(pool: RelayPool, ndb: Ndb) { + self.pool = pool + self.ndb = ndb + } + + // MARK: - Reading data from Nostr + + /// Subscribes to data from the user's relays + /// + /// ## Implementation notes + /// + /// - When we migrate to the local relay model, we should modify this function to stream directly from NostrDB + /// + /// - Parameter filters: The nostr filters to specify what kind of data to subscribe to + /// - Returns: An async stream of nostr data + func subscribe(filters: [NostrFilter]) -> AsyncStream { + return AsyncStream { continuation in + let streamTask = Task { + for await item in self.pool.subscribe(filters: filters) { + switch item { + case .eose: continuation.yield(.eose) + case .event(let nostrEvent): + // At this point of the pipeline, if the note is valid it should have been processed and verified by NostrDB, + // in which case we should pull the note from NostrDB to ensure validity. + // However, NdbNotes are unowned, so we return a function where our callers can temporarily borrow the NostrDB note + let noteId = nostrEvent.id + let lender: NdbNoteLender = { lend in + guard let ndbNoteTxn = self.ndb.lookup_note(noteId) else { + throw NdbNoteLenderError.errorLoadingNote + } + guard let unownedNote = UnownedNdbNote(ndbNoteTxn) else { + throw NdbNoteLenderError.errorLoadingNote + } + lend(unownedNote) + } + continuation.yield(.event(borrow: lender)) + } + } + } + continuation.onTermination = { @Sendable _ in + streamTask.cancel() // Close the RelayPool stream when caller stops streaming + } + } + } + } + + enum StreamItem { + /// An event which can be borrowed from NostrDB + case event(borrow: NdbNoteLender) + /// The end of stored events + case eose + } +} diff --git a/damus/Models/NostrNetworkManager/UserRelayListErrors.swift b/damus/Models/NostrNetworkManager/UserRelayListErrors.swift new file mode 100644 index 00000000..a3292767 --- /dev/null +++ b/damus/Models/NostrNetworkManager/UserRelayListErrors.swift @@ -0,0 +1,85 @@ +// +// UserRelayListErrors.swift +// damus +// +// Created by Daniel D’Aquino on 2025-02-27. +// + +import Foundation + +extension NostrNetworkManager.UserRelayListManager { + /// Models an error that may occur when performing operations that change the user's relay list. + /// + /// Callers to functions that throw this error SHOULD handle them in order to provide a better user experience. + enum UpdateError: Error { + /// The user is not authorized to change relay list, usually because the private key is missing. + case notAuthorizedToChangeRelayList + /// An error occurred when forming the relay list Nostr event. + case cannotFormRelayListEvent + /// Cannot add item to the relay list because the relay is already present in the list. + case relayAlreadyExists + /// Cannot update the relay list because we do not have the user's previous relay list. + /// + /// Implementers must be careful not to overwrite the user's existing relay list if it exists somewhere else. + case noInitialRelayList + /// Cannot remove or update a specific relay because it is not on the relay list + case noSuchRelay + + /// Convert `RelayPool.RelayError` into `UserRelayListUpdateError` + static func from(_ relayPoolError: RelayPool.RelayError) -> Self { + switch relayPoolError { + case .RelayAlreadyExists: return .relayAlreadyExists + } + } + + var humanReadableError: ErrorView.UserPresentableError { + switch self { + case .notAuthorizedToChangeRelayList: + ErrorView.UserPresentableError( + user_visible_description: NSLocalizedString("You do not have permission to alter this relay list.", comment: "Human readable error description"), + tip: NSLocalizedString("Please make sure you have logged-in with your private key.", comment: "Human readable tip for error"), + technical_info: nil + ) + case .cannotFormRelayListEvent: + ErrorView.UserPresentableError( + user_visible_description: NSLocalizedString("There was a problem creating the relay list event.", comment: "Human readable error description"), + tip: NSLocalizedString("Please try again later or contact support if the issue persists.", comment: "Human readable tip for error"), + technical_info: "Failed forming Nostr event for the relay list update." + ) + case .relayAlreadyExists: + ErrorView.UserPresentableError( + user_visible_description: NSLocalizedString("This relay is already in your list.", comment: "Human readable tip for error"), + tip: NSLocalizedString("Check the address and/or the relay list.", comment: "Human readable tip for error"), + technical_info: nil + ) + case .noInitialRelayList: + ErrorView.UserPresentableError( + user_visible_description: NSLocalizedString("No initial relay list available to update.", comment: "Human readable error description"), + tip: NSLocalizedString("Please go to Settings > First Aid > Repair relay list, or contact support.", comment: "Human readable tip for error"), + technical_info: "Missing initial relay list data for reference during update." + ) + case .noSuchRelay: + ErrorView.UserPresentableError( + user_visible_description: NSLocalizedString("The specified relay that you are trying to udpate was not found in your relay list.", comment: "Human readable error description"), + tip: NSLocalizedString("This is an unexpected error, please contact support.", comment: "Human readable tip for error"), + technical_info: nil + ) + } + } + } + + enum LoadingError: Error { + case relayListParseError + + var humanReadableError: ErrorView.UserPresentableError { + switch self { + case .relayListParseError: + return ErrorView.UserPresentableError( + user_visible_description: NSLocalizedString("Your relay list appears to be broken, so we cannot connect you to your Nostr network.", comment: "Human readable error description for a failure to parse the relay list due to a bad relay list"), + tip: NSLocalizedString("Please contact support for further help.", comment: "Human readable tips for what to do for a failure to find the relay list"), + technical_info: "Relay list could not be parsed." + ) + } + } + } +} diff --git a/damus/Models/NostrNetworkManager/UserRelayListManager.swift b/damus/Models/NostrNetworkManager/UserRelayListManager.swift new file mode 100644 index 00000000..8f773378 --- /dev/null +++ b/damus/Models/NostrNetworkManager/UserRelayListManager.swift @@ -0,0 +1,311 @@ +// +// UserRelayListManager.swift +// damus +// +// Created by Daniel D’Aquino on 2025-02-27. +// + +import Foundation +import Combine + +extension NostrNetworkManager { + /// Manages the user's relay list + /// + /// - It can compute the user's current relay list + /// - It can compute the best relay list to connect to + /// - It can edit the user's relay list + class UserRelayListManager { + private var delegate: Delegate + private let pool: RelayPool + private let reader: SubscriptionManager + + private var relayListObserverTask: Task? = nil + private var walletUpdatesObserverTask: AnyCancellable? = nil + + init(delegate: Delegate, pool: RelayPool, reader: SubscriptionManager) { + self.delegate = delegate + self.pool = pool + self.reader = reader + } + + // MARK: - Computing the relays to connect to + + private func relaysToConnectTo() -> [RelayPool.RelayDescriptor] { + return self.computeRelaysToConnectTo(with: self.getBestEffortRelayList()) + } + + private func computeRelaysToConnectTo(with relayList: NIP65.RelayList) -> [RelayPool.RelayDescriptor] { + let regularRelayDescriptorList = relayList.toRelayDescriptors() + if let nwcWallet = delegate.nwcWallet { + return regularRelayDescriptorList + [.nwc(url: nwcWallet.relay)] + } + return regularRelayDescriptorList + } + + // MARK: - Getting the user's relay list + + /// Gets the "best effort" relay list. + /// + /// It attempts to get a relay list from the user. If one is not available, it uses the default bootstrap list. + /// + /// This is always guaranteed to return a relay list. + func getBestEffortRelayList() -> NIP65.RelayList { + guard let userCurrentRelayList = self.getUserCurrentRelayList() else { + return NIP65.RelayList(relays: delegate.bootstrapRelays) + } + return userCurrentRelayList + } + + /// Gets the user's current relay list. + /// + /// It attempts to get a NIP-65 relay list from the local database, or falls back to a legacy list. + func getUserCurrentRelayList() -> NIP65.RelayList? { + if let latestRelayListEvent = try? self.getLatestNIP65RelayList() { return latestRelayListEvent } + if let latestRelayListEvent = try? self.getLatestKind3RelayList() { return latestRelayListEvent } + if let latestRelayListEvent = try? self.getLatestUserDefaultsRelayList() { return latestRelayListEvent } + return nil + } + + /// Gets the latest NIP-65 relay list from NostrDB. + /// + /// This is `private` because it is part of internal logic. Callers should use the higher level functions. + /// + /// - Returns: The latest NIP-65 relay list object + private func getLatestNIP65RelayList() throws(LoadingError) -> NIP65.RelayList? { + guard let latestRelayListEvent = self.getLatestNIP65RelayListEvent() else { return nil } + guard let list = try? NIP65.RelayList(event: latestRelayListEvent) else { throw .relayListParseError } + return list + } + + /// Gets the latest NIP-65 relay list event from NostrDB. + /// + /// This is `private` because it is part of internal logic. Callers should use the higher level functions. + /// + /// It is recommended to use this function only if the NostrEvent metadata is needed. For cases where only the relay list info is needed, use `getLatestNIP65RelayList` instead. + /// + /// - Returns: The latest NIP-65 relay list NdbNote + private func getLatestNIP65RelayListEvent() -> NdbNote? { + guard let latestRelayListEventId = delegate.latestRelayListEventIdHex else { return nil } + guard let latestRelayListEventId = NoteId(hex: latestRelayListEventId) else { return nil } + return delegate.ndb.lookup_note(latestRelayListEventId)?.unsafeUnownedValue?.to_owned() + } + + /// Gets the latest `kind:3` relay list from NostrDB. + /// + /// This is `private` because it is part of internal logic. Callers should use the higher level functions. + private func getLatestKind3RelayList() throws(LoadingError) -> NIP65.RelayList? { + guard let latestContactListEvent = delegate.latestContactListEvent else { return nil } + guard let legacyContactList = try? NIP65.RelayList.fromLegacyContactList(latestContactListEvent) else { throw .relayListParseError } + return legacyContactList + } + + /// Gets the latest relay list from `UserDefaults` + /// + /// This is `private` because it is part of internal logic. Callers should use the higher level functions. + private func getLatestUserDefaultsRelayList() throws(LoadingError) -> NIP65.RelayList? { + let key = bootstrap_relays_setting_key(pubkey: delegate.keypair.pubkey) + guard let relays = UserDefaults.standard.stringArray(forKey: key) else { return nil } + let relayUrls = relays.compactMap({ RelayURL($0) }) + if relayUrls.count == 0 { return nil } + return NIP65.RelayList(relays: relayUrls) + } + + // MARK: - Getting metadata from the user's relay list + + /// Gets the creation date of the user's current relay list, with preference to NIP-65 relay lists + /// - Returns: The current relay list's creation date + private func getUserCurrentRelayListCreationDate() -> UInt32? { + if let latestNIP65RelayListEvent = self.getLatestNIP65RelayListEvent() { return latestNIP65RelayListEvent.created_at } + if let latestKind3RelayListEvent = delegate.latestContactListEvent { return latestKind3RelayListEvent.created_at } + return nil + } + + // MARK: - Listening to and handling relay updates from the network + + func connect() { + self.load() + + self.relayListObserverTask?.cancel() + self.relayListObserverTask = Task { await self.listenAndHandleRelayUpdates() } + self.walletUpdatesObserverTask?.cancel() + self.walletUpdatesObserverTask = handle_notify(.attached_wallet).sink { _ in self.load() } + } + + func listenAndHandleRelayUpdates() async { + let filter = NostrFilter(kinds: [.relay_list], authors: [delegate.keypair.pubkey]) + for await item in self.reader.subscribe(filters: [filter]) { + switch item { + case .event(borrow: let borrow): // Signature validity already ensured at this point + let currentRelayListCreationDate = self.getUserCurrentRelayListCreationDate() + try? borrow { note in + guard note.pubkey == self.delegate.keypair.pubkey else { return } // Ensure this new list was ours + guard note.createdAt > (currentRelayListCreationDate ?? 0) else { return } // Ensure this is a newer list + guard let relayList = try? NIP65.RelayList(event: note) else { return } // Ensure it is a valid NIP-65 list + + try? self.set(userRelayList: relayList) // Set the validated list + } + case .eose: continue + } + } + } + + // MARK: - Editing the user's relay list + + func upsert(relay: NIP65.RelayList.RelayItem, force: Bool = false, overwriteExisting: Bool = false) throws(UpdateError) { + guard let currentUserRelayList = force ? self.getBestEffortRelayList() : self.getUserCurrentRelayList() else { throw .noInitialRelayList } + guard !currentUserRelayList.relays.keys.contains(relay.url) || overwriteExisting else { throw .relayAlreadyExists } + var newList = currentUserRelayList.relays + newList[relay.url] = relay + try self.set(userRelayList: NIP65.RelayList(relays: Array(newList.values))) + } + + func insert(relay: NIP65.RelayList.RelayItem, force: Bool = false) throws(UpdateError) { + guard let currentUserRelayList = force ? self.getBestEffortRelayList() : self.getUserCurrentRelayList() else { throw .noInitialRelayList } + guard currentUserRelayList.relays[relay.url] == nil else { throw .relayAlreadyExists } + try self.upsert(relay: relay, force: force) + } + + func remove(relayURL: RelayURL, force: Bool = false) throws(UpdateError) { + guard let currentUserRelayList = force ? self.getBestEffortRelayList() : self.getUserCurrentRelayList() else { throw .noInitialRelayList } + guard currentUserRelayList.relays.keys.contains(relayURL) || force else { throw .noSuchRelay } + var newList = currentUserRelayList.relays + newList[relayURL] = nil + try self.set(userRelayList: NIP65.RelayList(relays: Array(newList.values))) + } + + func set(userRelayList: NIP65.RelayList) throws(UpdateError) { + guard let fullKeypair = delegate.keypair.to_full() else { throw .notAuthorizedToChangeRelayList } + guard let relayListEvent = userRelayList.toNostrEvent(keypair: fullKeypair) else { throw .cannotFormRelayListEvent } + + self.apply(newRelayList: self.computeRelaysToConnectTo(with: userRelayList)) + + self.pool.send(.event(relayListEvent)) // This will send to NostrDB as well, which will locally save that NIP-65 event + self.delegate.latestRelayListEventIdHex = relayListEvent.id.hex() // Make sure we are able to recall this event from NostrDB + } + + // MARK: - Syncing our saved user relay list with the active `RelayPool` + + /// Loads the current user relay list + func load() { + self.apply(newRelayList: self.relaysToConnectTo()) + } + + /// Loads a new relay list into the active relay pool, making sure it matches the specified relay list. + /// + /// - Parameters: + /// - state: The state of the app + /// - newRelayList: The new relay list to be applied + /// + /// + /// ## Implementation notes + /// + /// - This is `private` because syncing the user's saved relay list with the relay pool is `NostrNetworkManager`'s responsibility, + /// so we do not want other classes to forcibly load this. + private func apply(newRelayList: [RelayPool.RelayDescriptor]) { + let currentRelayList = self.pool.relays.map({ $0.descriptor }) + + var changed = false + let new_relay_filters = load_relay_filters(delegate.keypair.pubkey) == nil + + for index in self.pool.relays.indices { + guard let newDescriptor = newRelayList.first(where: { $0.url == self.pool.relays[index].descriptor.url }) else { continue } + self.pool.relays[index].descriptor.info = newDescriptor.info + // Relay read-write configuration change does not need reconnection to the relay, so we do not set the `changed` flag. + } + + // Working with URL Sets for difference analysis + let currentRelayURLs = Set(currentRelayList.map { $0.url }) + let newRelayURLs = Set(newRelayList.map { $0.url }) + + // Analyzing which relays to add or remove + let relaysToRemove = currentRelayURLs.subtracting(newRelayURLs) + let relaysToAdd = newRelayURLs.subtracting(currentRelayURLs) + + // Remove relays not in the new list + relaysToRemove.forEach { url in + pool.remove_relay(url) + changed = true + } + + // Add new relays from the new list + relaysToAdd.forEach { url in + guard let descriptor = newRelayList.first(where: { $0.url == url }) else { return } + add_new_relay( + model_cache: delegate.relayModelCache, + relay_filters: delegate.relayFilters, + pool: pool, + descriptor: descriptor, + new_relay_filters: new_relay_filters, + logging_enabled: delegate.developerMode + ) + changed = true + } + + if changed { + pool.connect() + notify(.relays_changed) + } + } + } +} + +// MARK: - Helper extensions + +fileprivate extension NIP65.RelayList.RelayItem { + func toRelayDescriptor() -> RelayPool.RelayDescriptor { + return RelayPool.RelayDescriptor(url: self.url, info: self.rwConfiguration, variant: .regular) // NIP-65 relays are regular by definition. + } +} + +fileprivate extension NIP65.RelayList { + func toRelayDescriptors() -> [RelayPool.RelayDescriptor] { + return self.relays.values.map({ $0.toRelayDescriptor() }) + } +} + +// MARK: - Helper functions + + +/// Adds a new relay, taking care of other tangential concerns, such as updating the relay model cache, configuring logging, etc +/// +/// ## Implementation notes +/// +/// 1. This function used to be in `HomeModel.swift` and moved here when `UserRelayListManager` was first implemented +/// 2. This is `fileprivate` because only `UserRelayListManager` should be able to manage the user's relay list and apply them to the `RelayPool` +/// +/// - Parameters: +/// - model_cache: The relay model cache, that keeps metadata cached +/// - relay_filters: Relay filters +/// - pool: The relay pool to add this in +/// - descriptor: The description of the relay being added +/// - new_relay_filters: Whether to insert new relay filters +/// - logging_enabled: Whether logging is enabled +fileprivate func add_new_relay(model_cache: RelayModelCache, relay_filters: RelayFilters, pool: RelayPool, descriptor: RelayPool.RelayDescriptor, new_relay_filters: Bool, logging_enabled: Bool) { + try? pool.add_relay(descriptor) + let url = descriptor.url + + let relay_id = url + guard model_cache.model(withURL: url) == nil else { + return + } + + Task.detached(priority: .background) { + guard let meta = try? await fetch_relay_metadata(relay_id: relay_id) else { + return + } + + await MainActor.run { + let model = RelayModel(url, metadata: meta) + model_cache.insert(model: model) + + if logging_enabled { + pool.setLog(model.log, for: relay_id) + } + + // if this is the first time adding filters, we should filter non-paid relays + if new_relay_filters && !meta.is_paid { + relay_filters.insert(timeline: .search, relay_id: relay_id) + } + } + } +} diff --git a/damus/Models/ProfileModel.swift b/damus/Models/ProfileModel.swift index 29c14e2d..ab9d0454 100644 --- a/damus/Models/ProfileModel.swift +++ b/damus/Models/ProfileModel.swift @@ -59,10 +59,10 @@ class ProfileModel: ObservableObject, Equatable { func unsubscribe() { print("unsubscribing from profile \(pubkey) with sub_id \(sub_id)") - damus.pool.unsubscribe(sub_id: sub_id) - damus.pool.unsubscribe(sub_id: prof_subid) + damus.nostrNetwork.pool.unsubscribe(sub_id: sub_id) + damus.nostrNetwork.pool.unsubscribe(sub_id: prof_subid) if pubkey != damus.pubkey { - damus.pool.unsubscribe(sub_id: conversations_subid) + damus.nostrNetwork.pool.unsubscribe(sub_id: conversations_subid) } } @@ -77,8 +77,8 @@ class ProfileModel: ObservableObject, Equatable { print("subscribing to textlike events from profile \(pubkey) with sub_id \(sub_id)") //print_filters(relay_id: "profile", filters: [[text_filter], [profile_filter]]) - damus.pool.subscribe(sub_id: sub_id, filters: [text_filter], handler: handle_event) - damus.pool.subscribe(sub_id: prof_subid, filters: [profile_filter], handler: handle_event) + damus.nostrNetwork.pool.subscribe(sub_id: sub_id, filters: [text_filter], handler: handle_event) + damus.nostrNetwork.pool.subscribe(sub_id: prof_subid, filters: [profile_filter], handler: handle_event) subscribe_to_conversations() } @@ -94,7 +94,7 @@ class ProfileModel: ObservableObject, Equatable { let conversations_filter_them = NostrFilter(kinds: conversation_kinds, pubkeys: [damus.pubkey], limit: limit, authors: [pubkey]) let conversations_filter_us = NostrFilter(kinds: conversation_kinds, pubkeys: [pubkey], limit: limit, authors: [damus.pubkey]) print("subscribing to conversation events from and to profile \(pubkey) with sub_id \(conversations_subid)") - damus.pool.subscribe(sub_id: conversations_subid, filters: [conversations_filter_them, conversations_filter_us], handler: handle_event) + damus.nostrNetwork.pool.subscribe(sub_id: conversations_subid, filters: [conversations_filter_them, conversations_filter_us], handler: handle_event) } func handle_profile_contact_event(_ ev: NostrEvent) { @@ -200,11 +200,11 @@ class ProfileModel: ObservableObject, Equatable { var profile_filter = NostrFilter(kinds: [.contacts]) profile_filter.authors = [pubkey] - damus.pool.subscribe(sub_id: findRelay_subid, filters: [profile_filter], handler: findRelaysHandler) + damus.nostrNetwork.pool.subscribe(sub_id: findRelay_subid, filters: [profile_filter], handler: findRelaysHandler) } func unsubscribeFindRelays() { - damus.pool.unsubscribe(sub_id: findRelay_subid) + damus.nostrNetwork.pool.unsubscribe(sub_id: findRelay_subid) } func getCappedRelayStrings() -> [String] { diff --git a/damus/Models/SearchHomeModel.swift b/damus/Models/SearchHomeModel.swift index 9d9fe6fd..1ad8fb47 100644 --- a/damus/Models/SearchHomeModel.swift +++ b/damus/Models/SearchHomeModel.swift @@ -41,13 +41,13 @@ class SearchHomeModel: ObservableObject { func subscribe() { loading = true - let to_relays = determine_to_relays(pool: damus_state.pool, filters: damus_state.relay_filters) - damus_state.pool.subscribe(sub_id: base_subid, filters: [get_base_filter()], handler: handle_event, to: to_relays) + let to_relays = determine_to_relays(pool: damus_state.nostrNetwork.pool, filters: damus_state.relay_filters) + damus_state.nostrNetwork.pool.subscribe(sub_id: base_subid, filters: [get_base_filter()], handler: handle_event, to: to_relays) } func unsubscribe(to: RelayURL? = nil) { loading = false - damus_state.pool.unsubscribe(sub_id: base_subid, to: to.map { [$0] }) + damus_state.nostrNetwork.pool.unsubscribe(sub_id: base_subid, to: to.map { [$0] }) } func handle_event(relay_id: RelayURL, conn_ev: NostrConnectionEvent) { @@ -140,7 +140,7 @@ func load_profiles(context: String, profiles_subid: String, relay_id: RelayUR let filter = NostrFilter(kinds: [.metadata], authors: authors) - damus_state.pool.subscribe_to(sub_id: profiles_subid, filters: [filter], to: [relay_id]) { rid, conn_ev in + damus_state.nostrNetwork.pool.subscribe_to(sub_id: profiles_subid, filters: [filter], to: [relay_id]) { rid, conn_ev in let now = UInt64(Date.now.timeIntervalSince1970) switch conn_ev { @@ -156,7 +156,7 @@ func load_profiles(context: String, profiles_subid: String, relay_id: RelayUR } case .eose: print("load_profiles[\(context)]: done loading \(authors.count) profiles from \(relay_id)") - damus_state.pool.unsubscribe(sub_id: profiles_subid, to: [relay_id]) + damus_state.nostrNetwork.pool.unsubscribe(sub_id: profiles_subid, to: [relay_id]) case .ok: break case .notice: diff --git a/damus/Models/SearchModel.swift b/damus/Models/SearchModel.swift index ab971bff..305e9307 100644 --- a/damus/Models/SearchModel.swift +++ b/damus/Models/SearchModel.swift @@ -41,13 +41,13 @@ class SearchModel: ObservableObject { //likes_filter.ids = ref_events.referenced_ids! print("subscribing to search '\(search)' with sub_id \(sub_id)") - state.pool.register_handler(sub_id: sub_id, handler: handle_event) + state.nostrNetwork.pool.register_handler(sub_id: sub_id, handler: handle_event) loading = true - state.pool.send(.subscribe(.init(filters: [search], sub_id: sub_id))) + state.nostrNetwork.pool.send(.subscribe(.init(filters: [search], sub_id: sub_id))) } func unsubscribe() { - state.pool.unsubscribe(sub_id: sub_id) + state.nostrNetwork.pool.unsubscribe(sub_id: sub_id) loading = false print("unsubscribing from search '\(search)' with sub_id \(sub_id)") } @@ -67,7 +67,7 @@ class SearchModel: ObservableObject { } func handle_event(relay_id: RelayURL, ev: NostrConnectionEvent) { - let (sub_id, done) = handle_subid_event(pool: state.pool, relay_id: relay_id, ev: ev) { sub_id, ev in + let (sub_id, done) = handle_subid_event(pool: state.nostrNetwork.pool, relay_id: relay_id, ev: ev) { sub_id, ev in if ev.is_textlike && ev.should_show_event { self.add_event(ev) } diff --git a/damus/Models/ThreadModel.swift b/damus/Models/ThreadModel.swift index 90be492a..0e21e586 100644 --- a/damus/Models/ThreadModel.swift +++ b/damus/Models/ThreadModel.swift @@ -88,12 +88,12 @@ class ThreadModel: ObservableObject { /// Unsubscribe from events in the relay pool. Call this when unloading the view func unsubscribe() { - self.damus_state.pool.remove_handler(sub_id: base_subid) - self.damus_state.pool.remove_handler(sub_id: meta_subid) - self.damus_state.pool.remove_handler(sub_id: profiles_subid) - self.damus_state.pool.unsubscribe(sub_id: base_subid) - self.damus_state.pool.unsubscribe(sub_id: meta_subid) - self.damus_state.pool.unsubscribe(sub_id: profiles_subid) + self.damus_state.nostrNetwork.pool.remove_handler(sub_id: base_subid) + self.damus_state.nostrNetwork.pool.remove_handler(sub_id: meta_subid) + self.damus_state.nostrNetwork.pool.remove_handler(sub_id: profiles_subid) + self.damus_state.nostrNetwork.pool.unsubscribe(sub_id: base_subid) + self.damus_state.nostrNetwork.pool.unsubscribe(sub_id: meta_subid) + self.damus_state.nostrNetwork.pool.unsubscribe(sub_id: profiles_subid) Log.info("unsubscribing to thread %s with sub_id %s", for: .render, original_event.id.hex(), base_subid) } @@ -129,8 +129,8 @@ class ThreadModel: ObservableObject { let meta_filters = [meta_events, quote_events] Log.info("subscribing to thread %s with sub_id %s", for: .render, original_event.id.hex(), base_subid) - damus_state.pool.subscribe(sub_id: base_subid, filters: base_filters, handler: handle_event) - damus_state.pool.subscribe(sub_id: meta_subid, filters: meta_filters, handler: handle_event) + damus_state.nostrNetwork.pool.subscribe(sub_id: base_subid, filters: base_filters, handler: handle_event) + damus_state.nostrNetwork.pool.subscribe(sub_id: meta_subid, filters: meta_filters, handler: handle_event) } /// Adds an event to this thread. @@ -176,7 +176,7 @@ class ThreadModel: ObservableObject { /// Marked as private because it is this class' responsibility to load events, not the view's. Simplify the interface @MainActor private func handle_event(relay_id: RelayURL, ev: NostrConnectionEvent) { - let (sub_id, done) = handle_subid_event(pool: damus_state.pool, relay_id: relay_id, ev: ev) { sid, ev in + let (sub_id, done) = handle_subid_event(pool: damus_state.nostrNetwork.pool, relay_id: relay_id, ev: ev) { sid, ev in guard subids.contains(sid) else { return } diff --git a/damus/Models/ZapsModel.swift b/damus/Models/ZapsModel.swift index ff2b8252..9e1e5694 100644 --- a/damus/Models/ZapsModel.swift +++ b/damus/Models/ZapsModel.swift @@ -31,11 +31,11 @@ class ZapsModel: ObservableObject { case .note(let note_target): filter.referenced_ids = [note_target.note_id] } - state.pool.subscribe(sub_id: zaps_subid, filters: [filter], handler: handle_event) + state.nostrNetwork.pool.subscribe(sub_id: zaps_subid, filters: [filter], handler: handle_event) } func unsubscribe() { - state.pool.unsubscribe(sub_id: zaps_subid) + state.nostrNetwork.pool.unsubscribe(sub_id: zaps_subid) } @MainActor diff --git a/damus/Nostr/Relay.swift b/damus/Nostr/Relay.swift index c5e6587e..f33d9c5e 100644 --- a/damus/Nostr/Relay.swift +++ b/damus/Nostr/Relay.swift @@ -38,10 +38,10 @@ extension RelayPool { /// Describes a relay for use in `RelayPool` public struct RelayDescriptor { let url: RelayURL - var info: LegacyKind3RelayRWConfiguration + var info: NIP65.RelayList.RelayItem.RWConfiguration let variant: RelayVariant - init(url: RelayURL, info: LegacyKind3RelayRWConfiguration, variant: RelayVariant = .regular) { + init(url: RelayURL, info: NIP65.RelayList.RelayItem.RWConfiguration, variant: RelayVariant = .regular) { self.url = url self.info = info self.variant = variant @@ -59,7 +59,7 @@ extension RelayPool { } static func nwc(url: RelayURL) -> RelayDescriptor { - return RelayDescriptor(url: url, info: .rw, variant: .nwc) + return RelayDescriptor(url: url, info: .readWrite, variant: .nwc) } } } diff --git a/damus/Nostr/RelayPool.swift b/damus/Nostr/RelayPool.swift index e74cdb83..e39e236e 100644 --- a/damus/Nostr/RelayPool.swift +++ b/damus/Nostr/RelayPool.swift @@ -26,7 +26,7 @@ struct SeenEvent: Hashable { /// Establishes and manages connections and subscriptions to a list of relays. class RelayPool { - var relays: [Relay] = [] + private(set) var relays: [Relay] = [] var handlers: [RelayHandler] = [] var request_queue: [QueuedRequest] = [] var seen: Set = Set() @@ -124,7 +124,7 @@ class RelayPool { } } - func add_relay(_ desc: RelayDescriptor) throws { + func add_relay(_ desc: RelayDescriptor) throws(RelayError) { let relay_id = desc.url if get_relay(relay_id) != nil { throw RelayError.RelayAlreadyExists @@ -306,11 +306,11 @@ class RelayPool { self.send_raw_to_local_ndb(req) // Always send Nostr events and data to NostrDB for a local copy for relay in relays { - if req.is_read && !(relay.descriptor.info.read ?? true) { + if req.is_read && !(relay.descriptor.info.canRead) { continue // Do not send read requests to relays that are not READ relays } - if req.is_write && !(relay.descriptor.info.write ?? true) { + if req.is_write && !(relay.descriptor.info.canWrite) { continue // Do not send write requests to relays that are not WRITE relays } @@ -414,7 +414,7 @@ class RelayPool { } func add_rw_relay(_ pool: RelayPool, _ url: RelayURL) { - try? pool.add_relay(RelayPool.RelayDescriptor(url: url, info: .rw)) + try? pool.add_relay(RelayPool.RelayDescriptor(url: url, info: .readWrite)) } diff --git a/damus/TestData.swift b/damus/TestData.swift index 8dcc5410..93c13ba6 100644 --- a/damus/TestData.swift +++ b/damus/TestData.swift @@ -83,8 +83,7 @@ var test_damus_state: DamusState = ({ let our_pubkey = test_pubkey let pool = RelayPool(ndb: ndb) let settings = UserSettingsStore() - let damus = DamusState(pool: pool, - keypair: test_keypair, + let damus = DamusState(keypair: test_keypair, likes: .init(our_pubkey: our_pubkey), boosts: .init(our_pubkey: our_pubkey), contacts: .init(our_pubkey: our_pubkey), @@ -100,8 +99,6 @@ var test_damus_state: DamusState = ({ drafts: .init(), events: .init(ndb: ndb), bookmarks: .init(pubkey: our_pubkey), - postbox: .init(pool: pool), - bootstrap_relays: .init(), replies: .init(our_pubkey: our_pubkey), wallet: .init(settings: settings), nav: .init(), diff --git a/damus/Util/PostBox.swift b/damus/Util/PostBox.swift index 6ae7304e..061b1ec0 100644 --- a/damus/Util/PostBox.swift +++ b/damus/Util/PostBox.swift @@ -54,7 +54,7 @@ enum CancelSendErr { } class PostBox { - let pool: RelayPool + private let pool: RelayPool var events: [NoteId: PostedEvent] init(pool: RelayPool) { diff --git a/damus/Util/Router.swift b/damus/Util/Router.swift index 2a2fc89c..86bb34f2 100644 --- a/damus/Util/Router.swift +++ b/damus/Util/Router.swift @@ -126,7 +126,7 @@ enum Route: Hashable { case .FollowersYouKnow(let friendedFollowers, let followers): FollowersYouKnowView(damus_state: damusState, friended_followers: friendedFollowers, followers: followers) case .Script(let load_model): - LoadScript(pool: damusState.pool, model: load_model) + LoadScript(pool: damusState.nostrNetwork.pool, model: load_model) } } diff --git a/damus/Views/ActionBar/EventActionBar.swift b/damus/Views/ActionBar/EventActionBar.swift index abde87f0..606b22e3 100644 --- a/damus/Views/ActionBar/EventActionBar.swift +++ b/damus/Views/ActionBar/EventActionBar.swift @@ -270,7 +270,7 @@ struct EventActionBar: View { generator.impactOccurred() - damus_state.postbox.send(like_ev) + damus_state.nostrNetwork.postbox.send(like_ev) } // MARK: Helper structures diff --git a/damus/Views/ActionBar/RepostAction.swift b/damus/Views/ActionBar/RepostAction.swift index 2ab7a5b2..6df575b3 100644 --- a/damus/Views/ActionBar/RepostAction.swift +++ b/damus/Views/ActionBar/RepostAction.swift @@ -25,7 +25,7 @@ struct RepostAction: View { return } - damus_state.postbox.send(boost) + damus_state.nostrNetwork.postbox.send(boost) } label: { Label(NSLocalizedString("Repost", comment: "Button to repost a note"), image: "repost") .frame(maxWidth: .infinity, minHeight: 50, maxHeight: 50, alignment: .leading) diff --git a/damus/Views/AddRelayView.swift b/damus/Views/AddRelayView.swift index 80cbf347..f4e4b7f6 100644 --- a/damus/Views/AddRelayView.swift +++ b/damus/Views/AddRelayView.swift @@ -15,6 +15,8 @@ struct AddRelayView: View { @Environment(\.dismiss) var dismiss + typealias UpdateError = NostrNetworkManager.UserRelayListManager.UpdateError + var body: some View { VStack { Text("Add relay", comment: "Title text to indicate user to an add a relay.") @@ -82,38 +84,21 @@ struct AddRelayView: View { new_relay = "wss://" + new_relay } - guard let url = RelayURL(new_relay), - let ev = state.contacts.event, - let keypair = state.keypair.to_full() else { + guard let url = RelayURL(new_relay) else { + relayAddErrorTitle = NSLocalizedString("Invalid relay address", comment: "Heading for an error when adding a relay") + relayAddErrorMessage = NSLocalizedString("Please check the address and try again", comment: "Tip for an error where the relay address being added is invalid") return } - let info = LegacyKind3RelayRWConfiguration.rw - let descriptor = RelayPool.RelayDescriptor(url: url, info: info) - do { - try state.pool.add_relay(descriptor) + try state.nostrNetwork.userRelayList.insert(relay: NIP65.RelayList.RelayItem(url: url, rwConfiguration: .readWrite)) relayAddErrorTitle = nil // Clear error title relayAddErrorMessage = nil // Clear error message - } catch RelayPool.RelayError.RelayAlreadyExists { - relayAddErrorTitle = NSLocalizedString("Duplicate relay", comment: "Title of the duplicate relay error message.") - relayAddErrorMessage = NSLocalizedString("The relay you are trying to add is already added.\nYou're all set!", comment: "An error message that appears when the user attempts to add a relay that has already been added.") - return - } catch { - return + } + catch { + present_sheet(.error(self.humanReadableError(for: error))) } - state.pool.connect(to: [url]) - - if let new_ev = add_relay(ev: ev, keypair: keypair, current_relays: state.pool.our_descriptors, relay: url, info: info) { - process_contact_event(state: state, ev: ev) - - state.pool.send(.event(new_ev)) - } - - if let relay_metadata = make_relay_metadata(relays: state.pool.our_descriptors, keypair: keypair) { - state.postbox.send(relay_metadata) - } new_relay = "" this_app.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) @@ -134,6 +119,17 @@ struct AddRelayView: View { } .padding() } + + func humanReadableError(for error: any Error) -> ErrorView.UserPresentableError { + guard let error = error as? UpdateError else { + return .init( + user_visible_description: NSLocalizedString("An unknown error occurred while adding a relay.", comment: "Title of an unknown relay error message."), + tip: NSLocalizedString("Please contact support.", comment: "Tip for an unknown relay error message."), + technical_info: error.localizedDescription + ) + } + return error.humanReadableError + } } // TODO diff --git a/damus/Views/Chat/ChatEventView.swift b/damus/Views/Chat/ChatEventView.swift index 386c4eb2..6157625a 100644 --- a/damus/Views/Chat/ChatEventView.swift +++ b/damus/Views/Chat/ChatEventView.swift @@ -244,7 +244,7 @@ struct ChatEventView: View { let generator = UIImpactFeedbackGenerator(style: .medium) generator.impactOccurred() - damus_state.postbox.send(like_ev) + damus_state.nostrNetwork.postbox.send(like_ev) } var action_bar: some View { diff --git a/damus/Views/ConfigView.swift b/damus/Views/ConfigView.swift index df849e6f..b16ef68d 100644 --- a/damus/Views/ConfigView.swift +++ b/damus/Views/ConfigView.swift @@ -182,7 +182,7 @@ struct ConfigView: View { let ev = created_deleted_account_profile(keypair: keypair) else { return } - state.postbox.send(ev) + state.nostrNetwork.postbox.send(ev) logout(state) } } diff --git a/damus/Views/DMChatView.swift b/damus/Views/DMChatView.swift index a15f2640..1fb0979a 100644 --- a/damus/Views/DMChatView.swift +++ b/damus/Views/DMChatView.swift @@ -138,7 +138,7 @@ struct DMChatView: View, KeyboardReadable { dms.draft = "" - damus_state.postbox.send(dm) + damus_state.nostrNetwork.postbox.send(dm) handle_incoming_dm(ev: dm, our_pubkey: damus_state.pubkey, dms: damus_state.dms, prev_events: NewEventsBits()) diff --git a/damus/Views/Events/EventLoaderView.swift b/damus/Views/Events/EventLoaderView.swift index 57d3f5ed..6028895b 100644 --- a/damus/Views/Events/EventLoaderView.swift +++ b/damus/Views/Events/EventLoaderView.swift @@ -24,12 +24,12 @@ struct EventLoaderView: View { } func unsubscribe() { - damus_state.pool.unsubscribe(sub_id: subscription_uuid) + damus_state.nostrNetwork.pool.unsubscribe(sub_id: subscription_uuid) } func subscribe(filters: [NostrFilter]) { - damus_state.pool.register_handler(sub_id: subscription_uuid, handler: handle_event) - damus_state.pool.send(.subscribe(.init(filters: filters, sub_id: subscription_uuid))) + damus_state.nostrNetwork.pool.register_handler(sub_id: subscription_uuid, handler: handle_event) + damus_state.nostrNetwork.pool.send(.subscribe(.init(filters: filters, sub_id: subscription_uuid))) } func handle_event(relay_id: RelayURL, ev: NostrConnectionEvent) { diff --git a/damus/Views/Events/EventMenu.swift b/damus/Views/Events/EventMenu.swift index e0c3e3a7..d66b55a2 100644 --- a/damus/Views/Events/EventMenu.swift +++ b/damus/Views/Events/EventMenu.swift @@ -113,7 +113,7 @@ struct MenuItems: View { if let full_keypair = self.damus_state.keypair.to_full(), let new_mutelist_ev = toggle_from_mutelist(keypair: full_keypair, prev: damus_state.mutelist_manager.event, to_toggle: .thread(event.thread_id(), duration?.date_from_now)) { damus_state.mutelist_manager.set_mutelist(new_mutelist_ev) - damus_state.postbox.send(new_mutelist_ev) + damus_state.nostrNetwork.postbox.send(new_mutelist_ev) } let muted = damus_state.mutelist_manager.is_event_muted(event) isMutedThread = muted diff --git a/damus/Views/Muting/AddMuteItemView.swift b/damus/Views/Muting/AddMuteItemView.swift index e86d4b7a..4a464395 100644 --- a/damus/Views/Muting/AddMuteItemView.swift +++ b/damus/Views/Muting/AddMuteItemView.swift @@ -87,7 +87,7 @@ struct AddMuteItemView: View { } state.mutelist_manager.set_mutelist(mutelist) - state.postbox.send(mutelist) + state.nostrNetwork.postbox.send(mutelist) } new_text = "" diff --git a/damus/Views/Muting/MutelistView.swift b/damus/Views/Muting/MutelistView.swift index b9ef3371..322023ec 100644 --- a/damus/Views/Muting/MutelistView.swift +++ b/damus/Views/Muting/MutelistView.swift @@ -30,7 +30,7 @@ struct MutelistView: View { } damus_state.mutelist_manager.set_mutelist(new_ev) - damus_state.postbox.send(new_ev) + damus_state.nostrNetwork.postbox.send(new_ev) updateMuteItems() } label: { Label(NSLocalizedString("Delete", comment: "Button to remove a user from their mutelist."), image: "delete") diff --git a/damus/Views/Onboarding/SuggestedUsersViewModel.swift b/damus/Views/Onboarding/SuggestedUsersViewModel.swift index 29bc2088..be877158 100644 --- a/damus/Views/Onboarding/SuggestedUsersViewModel.swift +++ b/damus/Views/Onboarding/SuggestedUsersViewModel.swift @@ -77,7 +77,7 @@ class SuggestedUsersViewModel: ObservableObject { private func subscribeToSuggestedProfiles(pubkeys: [Pubkey]) { let filter = NostrFilter(kinds: [.metadata], authors: pubkeys) - damus_state.pool.subscribe(sub_id: sub_id, filters: [filter], handler: handle_event) + damus_state.nostrNetwork.pool.subscribe(sub_id: sub_id, filters: [filter], handler: handle_event) } func handle_event(relay_id: RelayURL, ev: NostrConnectionEvent) { diff --git a/damus/Views/Profile/EditMetadataView.swift b/damus/Views/Profile/EditMetadataView.swift index 15abfb8d..d0ec3508 100644 --- a/damus/Views/Profile/EditMetadataView.swift +++ b/damus/Views/Profile/EditMetadataView.swift @@ -65,7 +65,7 @@ struct EditMetadataView: View { return } - damus_state.postbox.send(metadata_ev) + damus_state.nostrNetwork.postbox.send(metadata_ev) } func is_ln_valid(ln: String) -> Bool { diff --git a/damus/Views/Profile/ProfileView.swift b/damus/Views/Profile/ProfileView.swift index 4c15fc68..673bc0f7 100644 --- a/damus/Views/Profile/ProfileView.swift +++ b/damus/Views/Profile/ProfileView.swift @@ -219,7 +219,7 @@ struct ProfileView: View { } damus_state.mutelist_manager.set_mutelist(new_ev) - damus_state.postbox.send(new_ev) + damus_state.nostrNetwork.postbox.send(new_ev) } } else { Button(NSLocalizedString("Mute", comment: "Button to mute a profile"), role: .destructive) { diff --git a/damus/Views/RelayFilterView.swift b/damus/Views/RelayFilterView.swift index 47493e04..77c2a608 100644 --- a/damus/Views/RelayFilterView.swift +++ b/damus/Views/RelayFilterView.swift @@ -15,11 +15,11 @@ struct RelayFilterView: View { self.state = state self.timeline = timeline - //_relays = State(initialValue: state.pool.descriptors) + //_relays = State(initialValue: state.networkManager.pool.descriptors) } var relays: [RelayPool.RelayDescriptor] { - return state.pool.our_descriptors + return state.nostrNetwork.pool.our_descriptors } var body: some View { diff --git a/damus/Views/Relays/RelayConfigView.swift b/damus/Views/Relays/RelayConfigView.swift index ed59bdb5..0a6b46b3 100644 --- a/damus/Views/Relays/RelayConfigView.swift +++ b/damus/Views/Relays/RelayConfigView.swift @@ -32,7 +32,7 @@ struct RelayConfigView: View { init(state: DamusState) { self.state = state - _relays = State(initialValue: state.pool.our_descriptors) + _relays = State(initialValue: state.nostrNetwork.pool.our_descriptors) UITabBar.appearance().isHidden = true } @@ -40,7 +40,7 @@ struct RelayConfigView: View { let rs: [RelayPool.RelayDescriptor] = [] let recommended_relay_addresses = get_default_bootstrap_relays() return recommended_relay_addresses.reduce(into: rs) { xs, x in - xs.append(RelayPool.RelayDescriptor(url: x, info: .rw)) + xs.append(RelayPool.RelayDescriptor(url: x, info: .readWrite)) } } @@ -98,7 +98,7 @@ struct RelayConfigView: View { } } .onReceive(handle_notify(.relays_changed)) { _ in - self.relays = state.pool.our_descriptors + self.relays = state.nostrNetwork.pool.our_descriptors } .onAppear { notify(.display_tabbar(false)) diff --git a/damus/Views/Relays/RelayDetailView.swift b/damus/Views/Relays/RelayDetailView.swift index c69966e6..d2474ed7 100644 --- a/damus/Views/Relays/RelayDetailView.swift +++ b/damus/Views/Relays/RelayDetailView.swift @@ -25,32 +25,12 @@ struct RelayDetailView: View { } func check_connection() -> Bool { - for relay in state.pool.relays { - if relay.id == self.relay { - return true - } - } - return false + return state.nostrNetwork.userRelayList.getUserCurrentRelayList()?.relays.keys.contains(self.relay) == true } func RemoveRelayButton(_ keypair: FullKeypair) -> some View { Button(action: { - guard let ev = state.contacts.event else { - return - } - - let descriptors = state.pool.our_descriptors - guard let new_ev = remove_relay( ev: ev, current_relays: descriptors, keypair: keypair, relay: relay) else { - return - } - - process_contact_event(state: state, ev: new_ev) - state.postbox.send(new_ev) - - if let relay_metadata = make_relay_metadata(relays: state.pool.our_descriptors, keypair: keypair) { - state.postbox.send(relay_metadata) - } - dismiss() + self.removeRelay() }) { HStack { Text("Disconnect", comment: "Button to disconnect from the relay.") @@ -63,19 +43,7 @@ struct RelayDetailView: View { func ConnectRelayButton(_ keypair: FullKeypair) -> some View { Button(action: { - guard let ev_before_add = state.contacts.event else { - return - } - guard let ev_after_add = add_relay(ev: ev_before_add, keypair: keypair, current_relays: state.pool.our_descriptors, relay: relay, info: .rw) else { - return - } - process_contact_event(state: state, ev: ev_after_add) - state.postbox.send(ev_after_add) - - if let relay_metadata = make_relay_metadata(relays: state.pool.our_descriptors, keypair: keypair) { - state.postbox.send(relay_metadata) - } - dismiss() + self.connectRelay() }) { HStack { Text("Connect", comment: "Button to connect to the relay.") @@ -209,12 +177,32 @@ struct RelayDetailView: View { } private var relay_object: RelayPool.Relay? { - state.pool.get_relay(relay) + state.nostrNetwork.pool.get_relay(relay) } private var relay_connection: RelayConnection? { relay_object?.connection } + + func removeRelay() { + do { + try state.nostrNetwork.userRelayList.remove(relayURL: self.relay) + dismiss() + } + catch { + present_sheet(.error(error.humanReadableError)) + } + } + + func connectRelay() { + do { + try state.nostrNetwork.userRelayList.insert(relay: NIP65.RelayList.RelayItem(url: relay, rwConfiguration: .readWrite)) + dismiss() + } + catch { + present_sheet(.error(error.humanReadableError)) + } + } } struct RelayDetailView_Previews: PreviewProvider { diff --git a/damus/Views/Relays/RelayStatusView.swift b/damus/Views/Relays/RelayStatusView.swift index 3ec9c29c..d3f6ed61 100644 --- a/damus/Views/Relays/RelayStatusView.swift +++ b/damus/Views/Relays/RelayStatusView.swift @@ -56,7 +56,7 @@ struct RelayStatusView: View { struct RelayStatusView_Previews: PreviewProvider { static var previews: some View { - let connection = test_damus_state.pool.get_relay(RelayURL("wss://relay.damus.io")!)!.connection + let connection = test_damus_state.nostrNetwork.pool.get_relay(RelayURL("wss://relay.damus.io")!)!.connection RelayStatusView(connection: connection) } } diff --git a/damus/Views/Relays/RelayToggle.swift b/damus/Views/Relays/RelayToggle.swift index a5ab24c9..41e0b77c 100644 --- a/damus/Views/Relays/RelayToggle.swift +++ b/damus/Views/Relays/RelayToggle.swift @@ -36,7 +36,7 @@ struct RelayToggle: View { } private var relay_connection: RelayConnection? { - state.pool.get_relay(relay_id)?.connection + state.nostrNetwork.pool.get_relay(relay_id)?.connection } } diff --git a/damus/Views/Relays/RelayView.swift b/damus/Views/Relays/RelayView.swift index 428e8b73..f22cdcce 100644 --- a/damus/Views/Relays/RelayView.swift +++ b/damus/Views/Relays/RelayView.swift @@ -22,7 +22,7 @@ struct RelayView: View { self.recommended = recommended self.model_cache = state.relay_model_cache _showActionButtons = showActionButtons - let relay_state = RelayView.get_relay_state(pool: state.pool, relay: relay) + let relay_state = RelayView.get_relay_state(pool: state.nostrNetwork.pool, relay: relay) self._relay_state = State(initialValue: relay_state) } @@ -80,7 +80,7 @@ struct RelayView: View { AddButton(keypair: keypair) } else { Button(action: { - remove_action(privkey: keypair.privkey) + Task { await remove_action(privkey: keypair.privkey) } }) { Text("Added", comment: "Button to show relay server is already added to list.") .font(.caption) @@ -105,7 +105,7 @@ struct RelayView: View { .contentShape(Rectangle()) } .onReceive(handle_notify(.relays_changed)) { _ in - self.relay_state = RelayView.get_relay_state(pool: state.pool, relay: self.relay) + self.relay_state = RelayView.get_relay_state(pool: state.nostrNetwork.pool, relay: self.relay) } .onTapGesture { state.nav.push(route: Route.RelayDetail(relay: relay, metadata: model_cache.model(with_relay_id: relay)?.metadata)) @@ -113,46 +113,30 @@ struct RelayView: View { } private var relay_connection: RelayConnection? { - state.pool.get_relay(relay)?.connection + state.nostrNetwork.pool.get_relay(relay)?.connection } - func add_action(keypair: FullKeypair) { - guard let ev_before_add = state.contacts.event else { - return + func add_action(keypair: FullKeypair) async { + do { + try await state.nostrNetwork.userRelayList.insert(relay: NIP65.RelayList.RelayItem(url: relay, rwConfiguration: .readWrite)) } - guard let ev_after_add = add_relay(ev: ev_before_add, keypair: keypair, current_relays: state.pool.our_descriptors, relay: relay, info: .rw) else { - return - } - process_contact_event(state: state, ev: ev_after_add) - state.postbox.send(ev_after_add) - - if let relay_metadata = make_relay_metadata(relays: state.pool.our_descriptors, keypair: keypair) { - state.postbox.send(relay_metadata) + catch { + present_sheet(.error(error.humanReadableError)) } } - func remove_action(privkey: Privkey) { - guard let ev = state.contacts.event else { - return + func remove_action(privkey: Privkey) async { + do { + try await state.nostrNetwork.userRelayList.remove(relayURL: relay) } - - let descriptors = state.pool.our_descriptors - guard let keypair = state.keypair.to_full(), - let new_ev = remove_relay(ev: ev, current_relays: descriptors, keypair: keypair, relay: relay) else { - return - } - - process_contact_event(state: state, ev: new_ev) - state.postbox.send(new_ev) - - if let relay_metadata = make_relay_metadata(relays: state.pool.our_descriptors, keypair: keypair) { - state.postbox.send(relay_metadata) + catch { + present_sheet(.error(error.humanReadableError)) } } func AddButton(keypair: FullKeypair) -> some View { Button(action: { - add_action(keypair: keypair) + Task { await add_action(keypair: keypair) } }) { Text("Add", comment: "Button to add relay server to list.") .font(.caption) @@ -170,7 +154,7 @@ struct RelayView: View { func RemoveButton(privkey: Privkey, showText: Bool) -> some View { Button(action: { - remove_action(privkey: privkey) + Task { await remove_action(privkey: privkey) } }) { if showText { Text("Disconnect", comment: "Button to disconnect from a relay server.") diff --git a/damus/Views/ReportView.swift b/damus/Views/ReportView.swift index 49a970ac..6d5fcc78 100644 --- a/damus/Views/ReportView.swift +++ b/damus/Views/ReportView.swift @@ -132,9 +132,9 @@ struct ReportView_Previews: PreviewProvider { let ds = test_damus_state VStack { - ReportView(postbox: ds.postbox, target: ReportTarget.user(test_pubkey), keypair: test_keypair.to_full()!) + ReportView(postbox: ds.nostrNetwork.postbox, target: ReportTarget.user(test_pubkey), keypair: test_keypair.to_full()!) - ReportView(postbox: ds.postbox, target: ReportTarget.user(test_pubkey), keypair: test_keypair.to_full()!, report_sent: true, report_id: "report_id") + ReportView(postbox: ds.nostrNetwork.postbox, target: ReportTarget.user(test_pubkey), keypair: test_keypair.to_full()!, report_sent: true, report_id: "report_id") } } diff --git a/damus/Views/SaveKeysView.swift b/damus/Views/SaveKeysView.swift index 9f5f5be1..7ceb5765 100644 --- a/damus/Views/SaveKeysView.swift +++ b/damus/Views/SaveKeysView.swift @@ -20,10 +20,12 @@ struct SaveKeysView: View { @FocusState var privkey_focused: Bool let first_contact_event: NdbNote? + let first_relay_list_event: NdbNote? init(account: CreateAccountModel) { self.account = account self.first_contact_event = make_first_contact_event(keypair: account.keypair) + self.first_relay_list_event = NIP65.RelayList(relays: get_default_bootstrap_relays()).toNostrEvent(keypair: account.full_keypair) } var body: some View { @@ -128,8 +130,12 @@ struct SaveKeysView: View { 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 } + guard let first_relay_list_event else { + error = NSLocalizedString("Could not create your initial relay list. 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 relay list failed to be created.") + return + } // Save contact list to storage right away so that we don't need to depend on the network to complete this important step - self.save_to_storage(first_contact_event: first_contact_event, for: account) + self.save_to_storage(first_contact_event: first_contact_event, first_relay_list_event: first_relay_list_event, for: account) let bootstrap_relays = load_bootstrap_relays(pubkey: account.pubkey) for relay in bootstrap_relays { @@ -143,13 +149,15 @@ struct SaveKeysView: View { self.pool.connect() } - func save_to_storage(first_contact_event: NdbNote, for account: CreateAccountModel) { + func save_to_storage(first_contact_event: NdbNote, first_relay_list_event: NdbNote, for account: CreateAccountModel) { // Send to NostrDB so that we have a local copy in storage self.pool.send_raw_to_local_ndb(.typical(.event(first_contact_event))) + self.pool.send_raw_to_local_ndb(.typical(.event(first_relay_list_event))) // Save the ID to user settings so that we can easily find it later. let settings = UserSettingsStore.globally_load_for(pubkey: account.pubkey) settings.latest_contact_event_id_hex = first_contact_event.id.hex() + settings.latestRelayListEventIdHex = first_relay_list_event.id.hex() } func handle_event(relay: RelayURL, ev: NostrConnectionEvent) { @@ -168,6 +176,10 @@ struct SaveKeysView: View { self.pool.send(.event(first_contact_event)) } + if let first_relay_list_event { + self.pool.send(.event(first_relay_list_event)) + } + do { try save_keypair(pubkey: account.pubkey, privkey: account.privkey) notify(.login(account.keypair)) diff --git a/damus/Views/SearchView.swift b/damus/Views/SearchView.swift index b99c1bb4..dd0e233a 100644 --- a/damus/Views/SearchView.swift +++ b/damus/Views/SearchView.swift @@ -69,7 +69,7 @@ struct SearchView: View { } appstate.mutelist_manager.set_mutelist(mutelist) - appstate.postbox.send(mutelist) + appstate.nostrNetwork.postbox.send(mutelist) } label: { Text("Unmute Hashtag", comment: "Label represnting a button that the user can tap to unmute a given hashtag so they start seeing it in their feed again.") } @@ -104,7 +104,7 @@ struct SearchView: View { } appstate.mutelist_manager.set_mutelist(mutelist) - appstate.postbox.send(mutelist) + appstate.nostrNetwork.postbox.send(mutelist) } var described_search: DescribedSearch { diff --git a/damus/Views/Settings/FirstAidSettingsView.swift b/damus/Views/Settings/FirstAidSettingsView.swift index 6ea447b6..e875a805 100644 --- a/damus/Views/Settings/FirstAidSettingsView.swift +++ b/damus/Views/Settings/FirstAidSettingsView.swift @@ -65,7 +65,7 @@ struct FirstAidSettingsView: View { reset_contact_list_state = .error(NSLocalizedString("An unexpected error happened while trying to create the new contact list. Please contact support.", comment: "Error message for a failed contact list reset operation")) return } - damus_state.pool.send(.event(new_contact_list_event)) + damus_state.nostrNetwork.pool.send(.event(new_contact_list_event)) reset_contact_list_state = .completed } } diff --git a/damus/Views/UserRelaysView.swift b/damus/Views/UserRelaysView.swift index 8e3d8231..20f7c44e 100644 --- a/damus/Views/UserRelaysView.swift +++ b/damus/Views/UserRelaysView.swift @@ -16,13 +16,13 @@ struct UserRelaysView: View { init(state: DamusState, relays: [RelayURL]) { self.state = state self.relays = relays - let relay_state = UserRelaysView.make_relay_state(pool: state.pool, relays: relays) + let relay_state = UserRelaysView.make_relay_state(state: state, relays: relays) self._relay_state = State(initialValue: relay_state) } - static func make_relay_state(pool: RelayPool, relays: [RelayURL]) -> [(RelayURL, Bool)] { + static func make_relay_state(state: DamusState, relays: [RelayURL]) -> [(RelayURL, Bool)] { return relays.map({ r in - return (r, pool.get_relay(r) == nil) + return (r, state.nostrNetwork.pool.get_relay(r) == nil) }).sorted { (a, b) in a.0 < b.0 } } diff --git a/damus/Views/Wallet/NWCSettings.swift b/damus/Views/Wallet/NWCSettings.swift index 95461efe..c4e801b3 100644 --- a/damus/Views/Wallet/NWCSettings.swift +++ b/damus/Views/Wallet/NWCSettings.swift @@ -173,7 +173,7 @@ struct NWCSettings: View { guard let meta = make_metadata_event(keypair: keypair, metadata: prof) else { return } - damus_state.postbox.send(meta) + damus_state.nostrNetwork.postbox.send(meta) } } diff --git a/damus/Views/Wallet/WalletView.swift b/damus/Views/Wallet/WalletView.swift index 21683c2b..6d78e342 100644 --- a/damus/Views/Wallet/WalletView.swift +++ b/damus/Views/Wallet/WalletView.swift @@ -84,8 +84,8 @@ struct WalletView: View { let delay = 0.0 // We don't need a delay when fetching a transaction list or balance - WalletConnect.request_transaction_list(url: nwc, pool: damus_state.pool, post: damus_state.postbox, delay: delay, on_flush: flusher) - WalletConnect.request_balance_information(url: nwc, pool: damus_state.pool, post: damus_state.postbox, delay: delay, on_flush: flusher) + WalletConnect.request_transaction_list(url: nwc, pool: damus_state.nostrNetwork.pool, post: damus_state.nostrNetwork.postbox, delay: delay, on_flush: flusher) + WalletConnect.request_balance_information(url: nwc, pool: damus_state.nostrNetwork.pool, post: damus_state.nostrNetwork.postbox, delay: delay, on_flush: flusher) return } } diff --git a/damusTests/AuthIntegrationTests.swift b/damusTests/AuthIntegrationTests.swift index 83c850f2..cc28b810 100644 --- a/damusTests/AuthIntegrationTests.swift +++ b/damusTests/AuthIntegrationTests.swift @@ -98,7 +98,7 @@ final class AuthIntegrationTests: XCTestCase { sent_messages.append(str) } XCTAssertEqual(pool.relays.count, 0) - let relay_descriptor = RelayPool.RelayDescriptor.init(url: relay_url, info: .rw) + let relay_descriptor = RelayPool.RelayDescriptor.init(url: relay_url, info: .readWrite) try! pool.add_relay(relay_descriptor) XCTAssertEqual(pool.relays.count, 1) let connection_expectation = XCTestExpectation(description: "Waiting for connection") @@ -142,7 +142,7 @@ final class AuthIntegrationTests: XCTestCase { sent_messages.append(str) } XCTAssertEqual(pool.relays.count, 0) - let relay_descriptor = RelayPool.RelayDescriptor.init(url: relay_url, info: .rw) + let relay_descriptor = RelayPool.RelayDescriptor.init(url: relay_url, info: .readWrite) try! pool.add_relay(relay_descriptor) XCTAssertEqual(pool.relays.count, 1) let connection_expectation = XCTestExpectation(description: "Waiting for connection") diff --git a/damusTests/Mocking/MockDamusState.swift b/damusTests/Mocking/MockDamusState.swift index 890a0a08..99959ec0 100644 --- a/damusTests/Mocking/MockDamusState.swift +++ b/damusTests/Mocking/MockDamusState.swift @@ -27,8 +27,7 @@ func generate_test_damus_state( }() let mutelist_manager = MutelistManager(user_keypair: test_keypair) - let damus = DamusState(pool: pool, - keypair: test_keypair, + let damus = DamusState(keypair: test_keypair, likes: .init(our_pubkey: our_pubkey), boosts: .init(our_pubkey: our_pubkey), contacts: .init(our_pubkey: our_pubkey), mutelist_manager: mutelist_manager, @@ -43,8 +42,6 @@ func generate_test_damus_state( drafts: .init(), events: .init(ndb: ndb), bookmarks: .init(pubkey: our_pubkey), - postbox: .init(pool: pool), - bootstrap_relays: .init(), replies: .init(our_pubkey: our_pubkey), wallet: .init(settings: settings), nav: .init(), diff --git a/damusTests/MutingTests.swift b/damusTests/MutingTests.swift index 3d01d6af..804d5986 100644 --- a/damusTests/MutingTests.swift +++ b/damusTests/MutingTests.swift @@ -35,7 +35,7 @@ final class MutingTests: XCTestCase { } test_damus_state.mutelist_manager.set_mutelist(mutelist) - test_damus_state.postbox.send(mutelist) + test_damus_state.nostrNetwork.postbox.send(mutelist) XCTAssert(test_damus_state.mutelist_manager.is_event_muted(spammy_test_note)) XCTAssertFalse(test_damus_state.mutelist_manager.is_event_muted(test_note)) diff --git a/damusTests/RequestTests.swift b/damusTests/RequestTests.swift index ee85c26a..aa5348b6 100644 --- a/damusTests/RequestTests.swift +++ b/damusTests/RequestTests.swift @@ -20,7 +20,7 @@ final class RequestTests: XCTestCase { func testMakeAuthRequest() { let challenge_string = "8bc847dd-f2f6-4b3a-9c8a-71776ad9b071" let url = RelayURL("wss://example.com")! - let relayDescriptor = RelayPool.RelayDescriptor(url: url, info: .rw) + let relayDescriptor = RelayPool.RelayDescriptor(url: url, info: .readWrite) let relayConnection = RelayConnection(url: url) { _ in } processEvent: { _ in } diff --git a/highlighter action extension/ActionViewController.swift b/highlighter action extension/ActionViewController.swift index 72aa1ea9..7fbab04f 100644 --- a/highlighter action extension/ActionViewController.swift +++ b/highlighter action extension/ActionViewController.swift @@ -163,7 +163,7 @@ struct ShareExtensionView: View { break case .active: print("txn: 📙 HIGHLIGHTER ACTIVE") - state.pool.ping() + state.nostrNetwork.pool.ping() @unknown default: break } @@ -238,7 +238,7 @@ struct ShareExtensionView: View { self.highlighter_state = .failed(error: "Cannot convert post data into a nostr event") return } - state.postbox.send(posted_event, on_flush: .once({ flushed_event in + state.nostrNetwork.postbox.send(posted_event, on_flush: .once({ flushed_event in if flushed_event.event.id == posted_event.id { DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: { // Offset labor perception bias self.highlighter_state = .posted(event: flushed_event.event) diff --git a/nostrscript/NostrScript.swift b/nostrscript/NostrScript.swift index 60ba856f..34f0e102 100644 --- a/nostrscript/NostrScript.swift +++ b/nostrscript/NostrScript.swift @@ -309,7 +309,7 @@ public func nscript_nostr_cmd(interp: UnsafeMutablePointer?, cmd: I func nscript_add_relay(script: NostrScript, relay: String) -> Bool { guard let url = RelayURL(relay) else { return false } - let desc = RelayPool.RelayDescriptor(url: url, info: .rw, variant: .ephemeral) + let desc = RelayPool.RelayDescriptor(url: url, info: .readWrite, variant: .ephemeral) return (try? script.pool.add_relay(desc)) != nil } diff --git a/share extension/ShareViewController.swift b/share extension/ShareViewController.swift index 068c7578..fa68dfb2 100644 --- a/share extension/ShareViewController.swift +++ b/share extension/ShareViewController.swift @@ -193,7 +193,7 @@ struct ShareExtensionView: View { break case .active: print("txn: 📙 SHARE ACTIVE") - state.pool.ping() + state.nostrNetwork.pool.ping() @unknown default: break } @@ -230,7 +230,7 @@ struct ShareExtensionView: View { self.share_state = .failed(error: "Cannot convert post data into a nostr event") return } - state.postbox.send(posted_event, on_flush: .once({ flushed_event in + state.nostrNetwork.postbox.send(posted_event, on_flush: .once({ flushed_event in if flushed_event.event.id == posted_event.id { DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: { // Offset labor perception bias self.share_state = .posted(event: flushed_event.event)