diff --git a/damus/Core/Networking/NostrNetworkManager/ProfilesManager.swift b/damus/Core/Networking/NostrNetworkManager/ProfilesManager.swift index b59f7b12..71e9166a 100644 --- a/damus/Core/Networking/NostrNetworkManager/ProfilesManager.swift +++ b/damus/Core/Networking/NostrNetworkManager/ProfilesManager.swift @@ -101,10 +101,17 @@ extension NostrNetworkManager { relevantStream.continuation.yield(profile) } } - - // Notify the rest of the app so views that rely on rendered text (like mention strings) - // can reload and pick up the freshly fetched profile metadata. - notify(.profile_updated(.remote(pubkey: metadataEvent.pubkey))) + } + + /// Manually trigger profile updates for a given pubkey + /// This is useful for local profile changes (e.g., nip05 validation, donation percentage updates) + func notifyProfileUpdate(pubkey: Pubkey) { + if let relevantStreams = streams[pubkey] { + guard let profile = ndb.lookup_profile_and_copy(pubkey) else { return } + for relevantStream in relevantStreams.values { + relevantStream.continuation.yield(profile) + } + } } @@ -121,6 +128,29 @@ extension NostrNetworkManager { } } + func streamProfiles(pubkeys: Set) -> AsyncStream { + guard !pubkeys.isEmpty else { + return AsyncStream { continuation in + continuation.finish() + } + } + + return AsyncStream { continuation in + let stream = ProfileStreamInfo(continuation: continuation) + for pubkey in pubkeys { + self.add(pubkey: pubkey, stream: stream) + } + + continuation.onTermination = { @Sendable _ in + Task { + for pubkey in pubkeys { + await self.removeStream(pubkey: pubkey, id: stream.id) + } + } + } + } + } + // MARK: - Stream management diff --git a/damus/Features/Events/NoteContentView.swift b/damus/Features/Events/NoteContentView.swift index 9085f023..50b7b45b 100644 --- a/damus/Features/Events/NoteContentView.swift +++ b/damus/Features/Events/NoteContentView.swift @@ -46,7 +46,6 @@ struct NoteContentView: View { let event: NostrEvent @State var blur_images: Bool @State var load_media: Bool = false - @State private var requestedMentionProfiles: Set = [] let size: EventViewKind let preview_height: CGFloat? let options: EventViewOptions @@ -279,10 +278,12 @@ struct NoteContentView: View { .padding(.horizontal) } - func ensureMentionProfilesAreFetchingIfNeeded() { + @concurrent + func streamProfiles() async throws { var mentionPubkeys: Set = [] - try? NdbBlockGroup.borrowBlockGroup(event: event, using: damus_state.ndb, and: damus_state.keypair, borrow: { blockGroup in - let _: ()? = try? blockGroup.forEachBlock({ _, block in + let event = await self.event.clone() + try await NdbBlockGroup.borrowBlockGroup(event: event, using: damus_state.ndb, and: damus_state.keypair, borrow: { blockGroup in + blockGroup.forEachBlock({ _, block in guard let pubkey = block.mentionPubkey(tags: event.tags) else { return .loopContinue } @@ -290,33 +291,13 @@ struct NoteContentView: View { return .loopContinue }) }) - - guard !mentionPubkeys.isEmpty else { return } - - var toFetch: [Pubkey] = [] - for pubkey in mentionPubkeys { - if requestedMentionProfiles.contains(pubkey) { - continue - } - requestedMentionProfiles.insert(pubkey) - - if damus_state.profiles.has_fresh_profile(id: pubkey) { - continue - } - - toFetch.append(pubkey) + + if mentionPubkeys.isEmpty { + return } - guard !toFetch.isEmpty else { return } - - // Kick off metadata fetches for any missing mention profiles so their names can render once loaded. - for pubkey in toFetch { - Task { - for await _ in await damus_state.nostrNetwork.profilesManager.streamProfile(pubkey: pubkey) { - // NO-OP, we will receive the update via `notify` - break - } - } + for await profile in await damus_state.nostrNetwork.profilesManager.streamProfiles(pubkeys: mentionPubkeys) { + await load(force_artifacts: true) } } @@ -324,8 +305,6 @@ struct NoteContentView: View { if case .loading = damus_state.events.get_cache_data(event.id).artifacts_model.state { return } - - ensureMentionProfilesAreFetchingIfNeeded() // always reload artifacts on load let plan = get_preload_plan(evcache: damus_state.events, ev: event, our_keypair: damus_state.keypair, settings: damus_state.settings) @@ -442,44 +421,13 @@ struct NoteContentView: View { var body: some View { ArtifactContent - .onReceive(handle_notify(.profile_updated)) { profile in - try? NdbBlockGroup.borrowBlockGroup(event: event, using: damus_state.ndb, and: damus_state.keypair, borrow: { blockGroup in - let _: Int? = blockGroup.forEachBlock { index, block in - switch block { - case .mention(let m): - guard let typ = m.bech32_type else { - return .loopContinue - } - switch typ { - case .nprofile: - if m.bech32.nprofile.matches_pubkey(pk: profile.pubkey) { - load(force_artifacts: true) - } - case .npub: - if m.bech32.npub.matches_pubkey(pk: profile.pubkey) { - load(force_artifacts: true) - } - case .nevent: return .loopContinue - case .nrelay: return .loopContinue - case .nsec: return .loopContinue - case .note: return .loopContinue - case .naddr: return .loopContinue - } - case .text: return .loopContinue - case .hashtag: return .loopContinue - case .url: return .loopContinue - case .invoice: return .loopContinue - case .mention_index(_): return .loopContinue - } - return .loopContinue - } - }) + .task { + try? await streamProfiles() } .onAppear { load() } } - } class NoteArtifactsParts { diff --git a/damus/Features/NIP05/Views/NIP05DomainTimelineView.swift b/damus/Features/NIP05/Views/NIP05DomainTimelineView.swift index 20369bb7..03c94840 100644 --- a/damus/Features/NIP05/Views/NIP05DomainTimelineView.swift +++ b/damus/Features/NIP05/Views/NIP05DomainTimelineView.swift @@ -46,7 +46,7 @@ struct NIP05DomainTimelineView: View { if let pubkeys = model.filter.authors { for pubkey in pubkeys { - check_nip05_validity(pubkey: pubkey, profiles: damus_state.profiles) + check_nip05_validity(pubkey: pubkey, damus_state: damus_state) } } } diff --git a/damus/Features/Profile/Views/EventProfileName.swift b/damus/Features/Profile/Views/EventProfileName.swift index c6dce13e..645ad0c9 100644 --- a/damus/Features/Profile/Views/EventProfileName.swift +++ b/damus/Features/Profile/Views/EventProfileName.swift @@ -17,7 +17,6 @@ struct EventProfileName: View { @State var nip05: NIP05? @State var donation: Int? @State var purple_account: DamusPurple.Account? - @StateObject private var profileObserver: ProfileObserver let size: EventViewKind @@ -28,7 +27,6 @@ struct EventProfileName: View { let donation = damus.profiles.lookup(id: pubkey)?.damus_donation self._donation = State(wrappedValue: donation) self.purple_account = nil - self._profileObserver = StateObject.init(wrappedValue: ProfileObserver(pubkey: pubkey, damusState: damus)) } var friend_type: FriendType? { @@ -102,26 +100,22 @@ struct EventProfileName: View { SupporterBadge(percent: self.supporter_percentage(), purple_account: self.purple_account, style: .compact) } } - .onReceive(handle_notify(.profile_updated)) { update in - if update.pubkey != pubkey { - return - } + .task { + for await profile in await damus_state.nostrNetwork.profilesManager.streamProfile(pubkey: pubkey) { + let display_name = Profile.displayName(profile: profile, pubkey: pubkey) + if display_name != self.display_name { + self.display_name = display_name + } - guard let profile = damus_state.profiles.lookup(id: update.pubkey) else { return } + let nip05 = damus_state.profiles.is_validated(pubkey) - let display_name = Profile.displayName(profile: profile, pubkey: pubkey) - if display_name != self.display_name { - self.display_name = display_name - } + if self.nip05 != nip05 { + self.nip05 = nip05 + } - let nip05 = damus_state.profiles.is_validated(pubkey) - - if self.nip05 != nip05 { - self.nip05 = nip05 - } - - if self.donation != profile.damus_donation { - donation = profile.damus_donation + if self.donation != profile.damus_donation { + donation = profile.damus_donation + } } } .task { diff --git a/damus/Features/Profile/Views/ProfileName.swift b/damus/Features/Profile/Views/ProfileName.swift index a5ef4eda..788f307f 100644 --- a/damus/Features/Profile/Views/ProfileName.swift +++ b/damus/Features/Profile/Views/ProfileName.swift @@ -45,7 +45,6 @@ struct ProfileName: View { @State var donation: Int? @State var purple_account: DamusPurple.Account? @State var nip05_domain_favicon: FaviconURL? - @StateObject var profileObserver: ProfileObserver init(pubkey: Pubkey, prefix: String = "", damus: DamusState, show_nip5_domain: Bool = true, supporterBadgeStyle: SupporterBadge.Style = .compact) { self.pubkey = pubkey @@ -54,7 +53,6 @@ struct ProfileName: View { self.show_nip5_domain = show_nip5_domain self.supporterBadgeStyle = supporterBadgeStyle self.purple_account = nil - self._profileObserver = StateObject.init(wrappedValue: ProfileObserver(pubkey: pubkey, damusState: damus)) } var friend_type: FriendType? { @@ -131,21 +129,10 @@ struct ProfileName: View { .largest() } } - .onReceive(handle_notify(.profile_updated)) { update in - if update.pubkey != pubkey { - return + .task { + for await profile in await damus_state.nostrNetwork.profilesManager.streamProfile(pubkey: pubkey) { + handle_profile_update(profile: profile) } - - switch update { - case .remote(let pubkey): - guard let prof = damus_state.profiles.lookup(id: pubkey) else { - return - } - handle_profile_update(profile: prof) - case .manual(_, let prof): - handle_profile_update(profile: prof) - } - } } diff --git a/damus/Features/Profile/Views/ProfilePicView.swift b/damus/Features/Profile/Views/ProfilePicView.swift index cfc17406..04fe8080 100644 --- a/damus/Features/Profile/Views/ProfilePicView.swift +++ b/damus/Features/Profile/Views/ProfilePicView.swift @@ -75,8 +75,7 @@ struct ProfilePicView: View { let privacy_sensitive: Bool @State var picture: String? - @StateObject private var profileObserver: ProfileObserver - @EnvironmentObject var damusState: DamusState + let damusState: DamusState init(pubkey: Pubkey, size: CGFloat, highlight: Highlight, profiles: Profiles, disable_animation: Bool, picture: String? = nil, show_zappability: Bool? = nil, privacy_sensitive: Bool = false, damusState: DamusState) { self.pubkey = pubkey @@ -87,7 +86,7 @@ struct ProfilePicView: View { self.disable_animation = disable_animation self.zappability_indicator = show_zappability ?? false self.privacy_sensitive = privacy_sensitive - self._profileObserver = StateObject.init(wrappedValue: ProfileObserver(pubkey: pubkey, damusState: damusState)) + self.damusState = damusState } var privacy_sensitive_pubkey: Pubkey { @@ -110,23 +109,6 @@ struct ProfilePicView: View { var body: some View { ZStack (alignment: Alignment(horizontal: .trailing, vertical: .bottom)) { InnerProfilePicView(url: get_profile_url(picture: picture, pubkey: privacy_sensitive_pubkey, profiles: profiles), fallbackUrl: URL(string: robohash(privacy_sensitive_pubkey)), size: size, highlight: highlight, disable_animation: disable_animation) - .onReceive(handle_notify(.profile_updated)) { updated in - guard updated.pubkey == self.pubkey else { - return - } - - switch updated { - case .manual(_, let profile): - if let pic = profile.picture { - self.picture = pic - } - case .remote(pubkey: let pk): - let profile = profiles.lookup(id: pk) - if let pic = profile?.picture { - self.picture = pic - } - } - } if self.zappability_indicator, let lnurl = self.get_lnurl(), lnurl != "" { Image("zap.fill") @@ -141,6 +123,13 @@ struct ProfilePicView: View { .clipShape(Circle()) } } + .task { + for await profile in await damusState.nostrNetwork.profilesManager.streamProfile(pubkey: pubkey) { + if let pic = profile.picture { + self.picture = pic + } + } + } } } diff --git a/damus/Features/Profile/Views/ProfileView.swift b/damus/Features/Profile/Views/ProfileView.swift index e766de0c..c5780113 100644 --- a/damus/Features/Profile/Views/ProfileView.swift +++ b/damus/Features/Profile/Views/ProfileView.swift @@ -144,7 +144,7 @@ struct ProfileView: View { return AnyView( VStack(spacing: 0) { ZStack { - BannerImageView(pubkey: profile.pubkey, profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation) + BannerImageView(pubkey: profile.pubkey, profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation, damusState: damus_state) .aspectRatio(contentMode: .fill) .frame(width: proxy.size.width, height: minY > 0 ? bannerHeight + minY : bannerHeight) .clipped() @@ -525,7 +525,7 @@ struct ProfileView: View { dismiss() } .onAppear() { - check_nip05_validity(pubkey: self.profile.pubkey, profiles: self.damus_state.profiles) + check_nip05_validity(pubkey: self.profile.pubkey, damus_state: self.damus_state) profile.subscribe() //followers.subscribe() } @@ -568,7 +568,8 @@ extension View { } @MainActor -func check_nip05_validity(pubkey: Pubkey, profiles: Profiles) { +func check_nip05_validity(pubkey: Pubkey, damus_state: DamusState) { + let profiles = damus_state.profiles let profile = profiles.lookup(id: pubkey) guard let nip05 = profile?.nip05, @@ -586,7 +587,7 @@ func check_nip05_validity(pubkey: Pubkey, profiles: Profiles) { Task { @MainActor in profiles.set_validated(pubkey, nip05: validated) profiles.nip05_pubkey[nip05] = pubkey - notify(.profile_updated(.remote(pubkey: pubkey))) + await damus_state.nostrNetwork.profilesManager.notifyProfileUpdate(pubkey: pubkey) } } } diff --git a/damus/Features/Wallet/Views/NWCSettings.swift b/damus/Features/Wallet/Views/NWCSettings.swift index e2251161..12f82573 100644 --- a/damus/Features/Wallet/Views/NWCSettings.swift +++ b/damus/Features/Wallet/Views/NWCSettings.swift @@ -250,7 +250,9 @@ struct NWCSettings: 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: p, reactions: profile.reactions) - notify(.profile_updated(.manual(pubkey: self.damus_state.pubkey, profile: prof))) + Task { + await damus_state.nostrNetwork.profilesManager.notifyProfileUpdate(pubkey: self.damus_state.pubkey) + } } .onDisappear { diff --git a/damus/Shared/Media/Images/BannerImageView.swift b/damus/Shared/Media/Images/BannerImageView.swift index 04d29f9b..a85d4eaa 100644 --- a/damus/Shared/Media/Images/BannerImageView.swift +++ b/damus/Shared/Media/Images/BannerImageView.swift @@ -90,27 +90,25 @@ struct BannerImageView: View { let disable_animation: Bool let pubkey: Pubkey let profiles: Profiles + let damusState: DamusState @State var banner: String? - init(pubkey: Pubkey, profiles: Profiles, disable_animation: Bool, banner: String? = nil) { + init(pubkey: Pubkey, profiles: Profiles, disable_animation: Bool, banner: String? = nil, damusState: DamusState) { self.pubkey = pubkey self.profiles = profiles self._banner = State(initialValue: banner) self.disable_animation = disable_animation + self.damusState = damusState } var body: some View { InnerBannerImageView(disable_animation: disable_animation, url: get_banner_url(banner: banner, pubkey: pubkey, profiles: profiles)) - .onReceive(handle_notify(.profile_updated)) { updated in - guard updated.pubkey == self.pubkey, - let profile = profiles.lookup(id: updated.pubkey) - else { - return - } - - if let bannerImage = profile.banner, bannerImage != self.banner { - self.banner = bannerImage + .task { + for await profile in await damusState.nostrNetwork.profilesManager.streamProfile(pubkey: pubkey) { + if let bannerImage = profile.banner, bannerImage != self.banner { + self.banner = bannerImage + } } } } @@ -129,7 +127,8 @@ struct BannerImageView_Previews: PreviewProvider { BannerImageView( pubkey: test_pubkey, profiles: make_preview_profiles(test_pubkey), - disable_animation: false + disable_animation: false, + damusState: test_damus_state ) } }