commit fc9b9f2940feb199c3ec14035f8bbe1c097a34d2 parent 622a436589704bab7fda4452013275a131d3bdde Author: William Casarin <jb55@jb55.com> Date: Sun, 10 Sep 2023 14:51:55 -0700 ndb: switch profile queries to use transactions this should ensure no crashing occurs when querying profiles Diffstat:
51 files changed, 430 insertions(+), 247 deletions(-)
diff --git a/damus/Components/NIP05Badge.swift b/damus/Components/NIP05Badge.swift @@ -45,8 +45,7 @@ struct NIP05Badge: View { } var username_matches_nip05: Bool { - guard let profile = profiles.lookup(id: pubkey), - let name = profile.name + guard let name = profiles.lookup(id: pubkey).map({ p in p?.name }).value else { return false } diff --git a/damus/Components/Reposted.swift b/damus/Components/Reposted.swift @@ -10,13 +10,12 @@ import SwiftUI struct Reposted: View { let damus: DamusState let pubkey: Pubkey - let profile: Profile? - + var body: some View { HStack(alignment: .center) { Image("repost") .foregroundColor(Color.gray) - ProfileName(pubkey: pubkey, profile: profile, damus: damus, show_nip5_domain: false) + ProfileName(pubkey: pubkey, damus: damus, show_nip5_domain: false) .foregroundColor(Color.gray) Text("Reposted", comment: "Text indicating that the note was reposted (i.e. re-shared).") .foregroundColor(Color.gray) @@ -27,6 +26,6 @@ struct Reposted: View { struct Reposted_Previews: PreviewProvider { static var previews: some View { let test_state = test_damus_state() - Reposted(damus: test_state, pubkey: test_state.pubkey, profile: make_test_profile()) + Reposted(damus: test_state, pubkey: test_state.pubkey) } } diff --git a/damus/Components/UserView.swift b/damus/Components/UserView.swift @@ -37,8 +37,7 @@ struct UserView: View { ProfilePicView(pubkey: pubkey, size: PFP_SIZE, highlight: .none, profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation) VStack(alignment: .leading) { - let profile = damus_state.profiles.lookup(id: pubkey) - ProfileName(pubkey: pubkey, profile: profile, damus: damus_state, show_nip5_domain: false) + ProfileName(pubkey: pubkey, damus: damus_state, show_nip5_domain: false) if let about_text { about_text .lineLimit(3) diff --git a/damus/ContentView.swift b/damus/ContentView.swift @@ -359,13 +359,18 @@ struct ContentView: View { // wallet with an associated guard let ds = self.damus_state, let lud16 = nwc.lud16, - let keypair = ds.keypair.to_full(), - let profile = ds.profiles.lookup(id: ds.pubkey), - lud16 != profile.lud16 + let keypair = ds.keypair.to_full() else { return } - + + let profile_txn = ds.profiles.lookup(id: ds.pubkey) + + guard let profile = profile_txn.unsafeUnownedValue, + lud16 != profile.lud16 else { + return + } + // clear zapper cache for old lud16 if profile.lud16 != nil { // TODO: should this be somewhere else, where we process profile events!? @@ -378,15 +383,9 @@ struct ContentView: View { ds.postbox.send(ev) } .onReceive(handle_notify(.broadcast)) { ev in - guard let ds = self.damus_state else { - return - } + guard let ds = self.damus_state else { return } + ds.postbox.send(ev) - if let record = ds.profiles.lookup_with_timestamp(ev.pubkey), - let event = ds.events.lookup_by_key(record.noteKey) - { - ds.postbox.send(event) - } } .onReceive(handle_notify(.unfollow)) { target in guard let state = self.damus_state else { return } @@ -488,10 +487,12 @@ struct ContentView: View { } .onReceive(handle_notify(.onlyzaps_mode)) { hide in home.filter_events() - - guard let damus_state, - let profile = damus_state.profiles.lookup(id: damus_state.pubkey), - let keypair = damus_state.keypair.to_full() + + guard let ds = damus_state else { return } + let profile_txn = ds.profiles.lookup(id: ds.pubkey) + + guard let profile = profile_txn.unsafeUnownedValue, + let keypair = ds.keypair.to_full() else { return } @@ -499,7 +500,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 } - damus_state.postbox.send(profile_ev) + ds.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.")) { @@ -507,11 +508,12 @@ struct ContentView: View { } }, message: { if let pubkey = self.muting { - let profile = damus_state!.profiles.lookup(id: pubkey) - let name = Profile.displayName(profile: profile, pubkey: pubkey).username.truncate(maxLength: 50) + let name = damus_state!.profiles.lookup(id: pubkey).map { profile in + Profile.displayName(profile: profile, pubkey: pubkey).username.truncate(maxLength: 50) + }.value Text("\(name) has been muted", comment: "Alert message that informs a user was muted.") } else { - Text("User has been muted", comment: "Alert message that informs a user was d.") + Text("User has been muted", comment: "Alert message that informs a user was muted.") } }) .alert(NSLocalizedString("Create new mutelist", comment: "Title of alert prompting the user to create a new mutelist."), isPresented: $confirm_overwrite_mutelist, actions: { @@ -567,8 +569,9 @@ struct ContentView: View { } }, message: { if let pubkey = muting { - let profile = damus_state?.profiles.lookup(id: pubkey) - let name = Profile.displayName(profile: profile, pubkey: pubkey).username.truncate(maxLength: 50) + let name = damus_state?.profiles.lookup(id: pubkey).map({ profile in + Profile.displayName(profile: profile, pubkey: pubkey).username.truncate(maxLength: 50) + }).value ?? "unknown" Text("Mute \(name)?", comment: "Alert message prompt to ask if a user should be muted.") } else { Text("Could not find user to mute...", comment: "Alert message to indicate that the muted user could not be found.") @@ -799,7 +802,7 @@ enum FindEventType { } enum FoundEvent { - case profile(Profile, NostrEvent) + case profile(Pubkey) case invalid_profile(NostrEvent) case event(NostrEvent) } @@ -816,11 +819,10 @@ func find_event_with_subid(state: DamusState, query query_: FindEvent, subid: St switch query { case .profile(let pubkey): - if let record = state.profiles.lookup_with_timestamp(pubkey), - let profile = record.profile, - let event = state.events.lookup_by_key(record.noteKey) + if let record = state.ndb.lookup_profile(pubkey).unsafeUnownedValue, + record.profile != nil { - callback(.profile(profile, event)) + callback(.profile(pubkey)) return } filter = NostrFilter(kinds: [.metadata], limit: 1, authors: [pubkey]) @@ -857,11 +859,11 @@ func find_event_with_subid(state: DamusState, query query_: FindEvent, subid: St switch query { case .profile: if ev.known_kind == .metadata { - guard let profile = state.profiles.lookup(id: ev.pubkey) else { + guard state.ndb.lookup_profile_key(ev.pubkey) != nil else { callback(.invalid_profile(ev)) return } - callback(.profile(profile, ev)) + callback(.profile(ev.pubkey)) } case .event: callback(.event(ev)) diff --git a/damus/Models/HomeModel.swift b/damus/Models/HomeModel.swift @@ -1108,10 +1108,13 @@ func zap_notification_title(_ zap: Zap) -> String { func zap_notification_body(profiles: Profiles, zap: Zap, locale: Locale = Locale.current) -> String { let src = zap.request.ev let pk = zap.is_anon ? ANON_PUBKEY : src.pubkey - let profile = profiles.lookup(id: pk) + + let name = profiles.lookup(id: pk).map { profile in + Profile.displayName(profile: profile, pubkey: pk).displayName.truncate(maxLength: 50) + }.value + let sats = NSNumber(value: (Double(zap.invoice.amount) / 1000.0)) let formattedSats = format_msats_abbrev(zap.invoice.amount) - let name = Profile.displayName(profile: profile, pubkey: pk).displayName.truncate(maxLength: 50) if src.content.isEmpty { let format = localizedStringFormat(key: "zap_notification_no_message", locale: locale) @@ -1361,8 +1364,8 @@ func process_zap_event(damus_state: DamusState, ev: NostrEvent, completion: @esc return } - guard let record = damus_state.profiles.lookup_with_timestamp(ptag), - let lnurl = record.lnurl else { + guard let lnurl = damus_state.profiles.lookup_with_timestamp(ptag) + .map({ pr in pr?.lnurl }).value else { completion(.failed) return } diff --git a/damus/Models/ProfileUpdate.swift b/damus/Models/ProfileUpdate.swift @@ -8,7 +8,16 @@ import Foundation -struct ProfileUpdate { - let pubkey: Pubkey - let profile: Profile +enum ProfileUpdate { + case manual(pubkey: Pubkey, profile: Profile) + case remote(pubkey: Pubkey) + + var pubkey: Pubkey { + switch self { + case .manual(let pubkey, _): + return pubkey + case .remote(let pubkey): + return pubkey + } + } } diff --git a/damus/Nostr/Nostr.swift b/damus/Nostr/Nostr.swift @@ -8,15 +8,18 @@ import Foundation typealias Profile = NdbProfile +typealias ProfileKey = UInt64 //typealias ProfileRecord = NdbProfileRecord class ProfileRecord { let data: NdbProfileRecord - init(data: NdbProfileRecord) { + init(data: NdbProfileRecord, key: ProfileKey) { self.data = data + self.profileKey = key } + let profileKey: ProfileKey var profile: Profile? { return data.profile } var receivedAt: UInt64 { data.receivedAt } var noteKey: UInt64 { data.noteKey } diff --git a/damus/Nostr/NostrResponse.swift b/damus/Nostr/NostrResponse.swift @@ -84,7 +84,7 @@ enum NostrResponse { return nil } let new_note = note_data.assumingMemoryBound(to: ndb_note.self) - let note = NdbNote(note: new_note, owned_size: Int(len)) + let note = NdbNote(note: new_note, owned_size: Int(len), key: nil) guard let subid = sized_cstr(cstr: tce.subid, len: tce.subid_len) else { free(data) diff --git a/damus/Nostr/Profiles.swift b/damus/Nostr/Profiles.swift @@ -76,18 +76,25 @@ class Profiles { profile_data(pubkey).zapper } - func lookup_with_timestamp(_ pubkey: Pubkey) -> ProfileRecord? { + func lookup_with_timestamp(_ pubkey: Pubkey) -> NdbTxn<ProfileRecord?> { return ndb.lookup_profile(pubkey) } - func lookup(id: Pubkey) -> Profile? { - return ndb.lookup_profile(id)?.profile + func lookup_by_key(key: ProfileKey) -> NdbTxn<ProfileRecord?> { + return ndb.lookup_profile_by_key(key: key) + } + + func lookup(id: Pubkey) -> NdbTxn<Profile?> { + return ndb.lookup_profile(id).map({ pr in pr?.profile }) + } + + func lookup_key_by_pubkey(_ pubkey: Pubkey) -> ProfileKey? { + return ndb.lookup_profile_key(pubkey) } func has_fresh_profile(id: Pubkey) -> Bool { - var profile: Profile? - guard let profile = lookup_with_timestamp(id) else { return false } - return Date.now.timeIntervalSince(Date(timeIntervalSince1970: Double(profile.receivedAt))) < Profiles.db_freshness_threshold + guard let recv = lookup_with_timestamp(id).unsafeUnownedValue?.receivedAt else { return false } + return Date.now.timeIntervalSince(Date(timeIntervalSince1970: Double(recv))) < Profiles.db_freshness_threshold } } diff --git a/damus/Notify/ProfileUpdatedNotify.swift b/damus/Notify/ProfileUpdatedNotify.swift @@ -19,7 +19,7 @@ extension NotifyHandler { } extension Notifications { - static func profile_updated(pubkey: Pubkey, profile: Profile) -> Notifications<ProfileUpdatedNotify> { - .init(.init(payload: ProfileUpdate(pubkey: pubkey, profile: profile))) + static func profile_updated(_ update: ProfileUpdate) -> Notifications<ProfileUpdatedNotify> { + .init(.init(payload: update)) } } diff --git a/damus/Util/DisplayName.swift b/damus/Util/DisplayName.swift @@ -7,7 +7,7 @@ import Foundation -enum DisplayName { +enum DisplayName: Equatable { case both(username: String, displayName: String) case one(String) diff --git a/damus/Util/EventCache.swift b/damus/Util/EventCache.swift @@ -137,6 +137,7 @@ class EventData { } class EventCache { + // TODO: remove me and change code to use ndb directly private let ndb: Ndb private var events: [NoteId: NostrEvent] = [:] private var replies = ReplyMap() @@ -253,9 +254,11 @@ class EventCache { return ev } + /* func lookup_by_key(_ key: UInt64) -> NostrEvent? { ndb.lookup_note_by_key(key) } + */ func lookup(_ evid: NoteId) -> NostrEvent? { return events[evid] diff --git a/damus/Util/NIP05.swift b/damus/Util/NIP05.swift @@ -7,7 +7,7 @@ import Foundation -struct NIP05 { +struct NIP05: Equatable { let username: String let host: String diff --git a/damus/Views/ActionBar/EventActionBar.swift b/damus/Views/ActionBar/EventActionBar.swift @@ -28,7 +28,7 @@ struct EventActionBar: View { } var lnurl: String? { - damus_state.profiles.lookup_with_timestamp(event.pubkey)?.lnurl + damus_state.profiles.lookup_with_timestamp(event.pubkey).map({ pr in pr?.lnurl }).value } var show_like: Bool { diff --git a/damus/Views/BannerImageView.swift b/damus/Views/BannerImageView.swift @@ -81,8 +81,10 @@ struct BannerImageView: View { guard updated.pubkey == self.pubkey else { return } - - if let bannerImage = updated.profile.banner { + + let profile_txn = profiles.lookup(id: updated.pubkey) + let profile = profile_txn.unsafeUnownedValue + if let bannerImage = profile?.banner, bannerImage != self.banner { self.banner = bannerImage } } @@ -90,7 +92,7 @@ struct BannerImageView: View { } func get_banner_url(banner: String?, pubkey: Pubkey, profiles: Profiles) -> URL? { - let bannerUrlString = banner ?? profiles.lookup(id: pubkey)?.banner ?? "" + let bannerUrlString = banner ?? profiles.lookup(id: pubkey).map({ p in p?.banner }).value ?? "" if let url = URL(string: bannerUrlString) { return url } diff --git a/damus/Views/DMChatView.swift b/damus/Views/DMChatView.swift @@ -60,12 +60,11 @@ struct DMChatView: View, KeyboardReadable { } var Header: some View { - let profile = damus_state.profiles.lookup(id: pubkey) return NavigationLink(value: Route.ProfileByKey(pubkey: pubkey)) { HStack { ProfilePicView(pubkey: pubkey, size: 24, highlight: .none, profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation) - ProfileName(pubkey: pubkey, profile: profile, damus: damus_state) + ProfileName(pubkey: pubkey, damus: damus_state) } } .buttonStyle(PlainButtonStyle()) diff --git a/damus/Views/Events/Components/EventTop.swift b/damus/Views/Events/Components/EventTop.swift @@ -22,9 +22,8 @@ struct EventTop: View { } func ProfileName(is_anon: Bool) -> some View { - let profile = state.profiles.lookup(id: self.pubkey) let pk = is_anon ? ANON_PUBKEY : self.pubkey - return EventProfileName(pubkey: pk, profile: profile, damus: state, size: .normal) + return EventProfileName(pubkey: pk, damus: state, size: .normal) } var body: some View { diff --git a/damus/Views/Events/Components/ReplyDescription.swift b/damus/Views/Events/Components/ReplyDescription.swift @@ -11,10 +11,10 @@ import SwiftUI struct ReplyDescription: View { let event: NostrEvent let replying_to: NostrEvent? - let profiles: Profiles - + let ndb: Ndb + var body: some View { - Text(verbatim: "\(reply_desc(profiles: profiles, event: event, replying_to: replying_to))") + Text(verbatim: "\(reply_desc(ndb: ndb, event: event, replying_to: replying_to))") .font(.footnote) .foregroundColor(.gray) .frame(maxWidth: .infinity, alignment: .leading) @@ -23,11 +23,11 @@ struct ReplyDescription: View { struct ReplyDescription_Previews: PreviewProvider { static var previews: some View { - ReplyDescription(event: test_note, replying_to: test_note, profiles: test_damus_state().profiles) + ReplyDescription(event: test_note, replying_to: test_note, ndb: test_damus_state().ndb) } } -func reply_desc(profiles: Profiles, event: NostrEvent, replying_to: NostrEvent?, locale: Locale = Locale.current) -> String { +func reply_desc(ndb: Ndb, event: NostrEvent, replying_to: NostrEvent?, locale: Locale = Locale.current) -> String { let desc = make_reply_description(event, replying_to: replying_to) let pubkeys = desc.pubkeys let n = desc.others @@ -38,9 +38,12 @@ func reply_desc(profiles: Profiles, event: NostrEvent, replying_to: NostrEvent?, return NSLocalizedString("Replying to self", bundle: bundle, comment: "Label to indicate that the user is replying to themself.") } + let profile_txn = NdbTxn(ndb: ndb) + let names: [String] = pubkeys.map { pk in - let prof = profiles.lookup(id: pk) - return Profile.displayName(profile: prof, pubkey: pk).username.truncate(maxLength: 50) + let prof = ndb.lookup_profile_with_txn(pk, txn: profile_txn) + + return Profile.displayName(profile: prof?.profile, pubkey: pk).username.truncate(maxLength: 50) } let uniqueNames = NSOrderedSet(array: names).array as! [String] diff --git a/damus/Views/Events/Components/ReplyPart.swift b/damus/Views/Events/Components/ReplyPart.swift @@ -11,7 +11,7 @@ struct ReplyPart: View { let events: EventCache let event: NostrEvent let keypair: Keypair - let profiles: Profiles + let ndb: Ndb var replying_to: NostrEvent? { guard let note_ref = event.event_refs(keypair).first(where: { evref in evref.is_direct_reply != nil })?.is_direct_reply else { @@ -24,7 +24,7 @@ struct ReplyPart: View { var body: some View { Group { if event_is_reply(event.event_refs(keypair)) { - ReplyDescription(event: event, replying_to: replying_to, profiles: profiles) + ReplyDescription(event: event, replying_to: replying_to, ndb: ndb) } else { EmptyView() } @@ -34,6 +34,6 @@ struct ReplyPart: View { struct ReplyPart_Previews: PreviewProvider { static var previews: some View { - ReplyPart(events: test_damus_state().events, event: test_note, keypair: Keypair(pubkey: .empty, privkey: nil), profiles: test_damus_state().profiles) + ReplyPart(events: test_damus_state().events, event: test_note, keypair: Keypair(pubkey: .empty, privkey: nil), ndb: test_damus_state().ndb) } } diff --git a/damus/Views/Events/EventMenu.swift b/damus/Views/Events/EventMenu.swift @@ -52,8 +52,9 @@ struct MenuItems: View { let target_pubkey: Pubkey let bookmarks: BookmarksManager let muted_threads: MutedThreadsManager + @ObservedObject var settings: UserSettingsStore - + @State private var isBookmarked: Bool = false @State private var isMutedThread: Bool = false diff --git a/damus/Views/Events/EventProfile.swift b/damus/Views/Events/EventProfile.swift @@ -25,7 +25,6 @@ func eventview_pfp_size(_ size: EventViewKind) -> CGFloat { struct EventProfile: View { let damus_state: DamusState let pubkey: Pubkey - let profile: Profile? let size: EventViewKind var pfp_size: CGFloat { @@ -44,7 +43,7 @@ struct EventProfile: View { } VStack(alignment: .leading, spacing: 0) { - EventProfileName(pubkey: pubkey, profile: profile, damus: damus_state, size: size) + EventProfileName(pubkey: pubkey, damus: damus_state, size: size) UserStatusView(status: damus_state.profiles.profile_data(pubkey).status, show_general: damus_state.settings.show_general_statuses, show_music: damus_state.settings.show_music_statuses) } @@ -54,6 +53,6 @@ struct EventProfile: View { struct EventProfile_Previews: PreviewProvider { static var previews: some View { - EventProfile(damus_state: test_damus_state(), pubkey: test_note.pubkey, profile: nil, size: .normal) + EventProfile(damus_state: test_damus_state(), pubkey: test_note.pubkey, size: .normal) } } diff --git a/damus/Views/Events/EventShell.swift b/damus/Views/Events/EventShell.swift @@ -72,7 +72,7 @@ struct EventShell<Content: View>: View { UserStatusView(status: state.profiles.profile_data(pubkey).status, show_general: state.settings.show_general_statuses, show_music: state.settings.show_music_statuses) if !options.contains(.no_replying_to) { - ReplyPart(events: state.events, event: event, keypair: state.keypair, profiles: state.profiles) + ReplyPart(events: state.events, event: event, keypair: state.keypair, ndb: state.ndb) } content @@ -99,7 +99,7 @@ struct EventShell<Content: View>: View { VStack(alignment: .leading, spacing: 2) { EventTop(state: state, event: event, pubkey: pubkey, is_anon: is_anon) UserStatusView(status: state.profiles.profile_data(pubkey).status, show_general: state.settings.show_general_statuses, show_music: state.settings.show_music_statuses) - ReplyPart(events: state.events, event: event, keypair: state.keypair, profiles: state.profiles) + ReplyPart(events: state.events, event: event, keypair: state.keypair, ndb: state.ndb) } } .padding(.horizontal) diff --git a/damus/Views/Events/SelectedEventView.swift b/damus/Views/Events/SelectedEventView.swift @@ -35,11 +35,9 @@ struct SelectedEventView: View { var body: some View { HStack(alignment: .top) { - let profile = damus.profiles.lookup(id: pubkey) - VStack(alignment: .leading) { HStack { - EventProfile(damus_state: damus, pubkey: pubkey, profile: profile, size: .normal) + EventProfile(damus_state: damus, pubkey: pubkey, size: .normal) Spacer() @@ -51,7 +49,7 @@ struct SelectedEventView: View { .lineLimit(1) if event_is_reply(event.event_refs(damus.keypair)) { - ReplyDescription(event: event, replying_to: replying_to, profiles: damus.profiles) + ReplyDescription(event: event, replying_to: replying_to, ndb: damus.ndb) .padding(.horizontal) } diff --git a/damus/Views/NoteContentView.swift b/damus/Views/NoteContentView.swift @@ -273,7 +273,8 @@ func mention_str(_ m: Mention<MentionRef>, profiles: Profiles) -> CompatibleText switch m.ref { case .pubkey(let pk): let npub = bech32_pubkey(pk) - let profile = profiles.lookup(id: pk) + let profile_txn = profiles.lookup(id: pk) + let profile = profile_txn.unsafeUnownedValue let disp = Profile.displayName(profile: profile, pubkey: pk).username.truncate(maxLength: 50) var attributedString = AttributedString(stringLiteral: "@\(disp)") attributedString.link = URL(string: "damus:nostr:\(npub)") diff --git a/damus/Views/Notifications/EventGroupView.swift b/damus/Views/Notifications/EventGroupView.swift @@ -69,8 +69,8 @@ func determine_reacting_to(our_pubkey: Pubkey, ev: NostrEvent?) -> ReactingTo { } func event_author_name(profiles: Profiles, pubkey: Pubkey) -> String { - let alice_prof = profiles.lookup(id: pubkey) - return Profile.displayName(profile: alice_prof, pubkey: pubkey).username.truncate(maxLength: 50) + let alice_prof_txn = profiles.lookup(id: pubkey).unsafeUnownedValue + return Profile.displayName(profile: alice_prof_txn, pubkey: pubkey).username.truncate(maxLength: 50) } func event_group_unique_pubkeys(profiles: Profiles, group: EventGroupType) -> [Pubkey] { diff --git a/damus/Views/Notifications/NotificationsView.swift b/damus/Views/Notifications/NotificationsView.swift @@ -86,8 +86,10 @@ struct NotificationsView: View { @Environment(\.colorScheme) var colorScheme var mystery: some View { - VStack(spacing: 20) { - Text("Wake up, \(Profile.displayName(profile: state.profiles.lookup(id: state.pubkey), pubkey: state.pubkey).displayName.truncate(maxLength: 50))", comment: "Text telling the user to wake up, where the argument is their display name.") + let profile_txn = state.profiles.lookup(id: state.pubkey) + let profile = profile_txn.unsafeUnownedValue + return VStack(spacing: 20) { + Text("Wake up, \(Profile.displayName(profile: profile, pubkey: state.pubkey).displayName.truncate(maxLength: 50))", comment: "Text telling the user to wake up, where the argument is their display name.") Text("You are dreaming...", comment: "Text telling the user that they are dreaming.") } .id("what") diff --git a/damus/Views/Onboarding/SuggestedUserView.swift b/damus/Views/Onboarding/SuggestedUserView.swift @@ -12,14 +12,10 @@ struct SuggestedUser { let name: String let about: String let pfp: URL - let profile: Profile - init?(profile: Profile, pubkey: Pubkey) { - - guard let name = profile.name, - let about = profile.about, - let picture = profile.picture, - let pfpURL = URL(string: picture) else { + init?(name: String?, about: String?, picture: String?, pubkey: Pubkey) { + guard let name, let about, let picture, + let pfpURL = URL(string: picture) else { return nil } @@ -27,12 +23,10 @@ struct SuggestedUser { self.name = name self.about = about self.pfp = pfpURL - self.profile = profile } } struct SuggestedUserView: View { - let user: SuggestedUser let damus_state: DamusState @@ -47,7 +41,7 @@ struct SuggestedUserView: View { disable_animation: false) VStack(alignment: .leading, spacing: 4) { HStack { - ProfileName(pubkey: user.pubkey, profile: user.profile, damus: damus_state) + ProfileName(pubkey: user.pubkey, damus: damus_state) } Text(user.about) .lineLimit(3) @@ -62,9 +56,10 @@ struct SuggestedUserView: View { struct SuggestedUserView_Previews: PreviewProvider { static var previews: some View { - let profile = Profile(name: "klabo", about: "A person who likes nostr a lot and I like to tell people about myself in very long-winded ways that push the limits of UI and almost break things", picture: "https://primal.b-cdn.net/media-cache?s=m&a=1&u=https%3A%2F%2Fpbs.twimg.com%2Fprofile_images%2F1599994711430742017%2F33zLk9Wi_400x400.jpg") + let pfp = "https://primal.b-cdn.net/media-cache?s=m&a=1&u=https%3A%2F%2Fpbs.twimg.com%2Fprofile_images%2F1599994711430742017%2F33zLk9Wi_400x400.jpg" + let profile = Profile(name: "klabo", about: "A person who likes nostr a lot and I like to tell people about myself in very long-winded ways that push the limits of UI and almost break things", picture: pfp) - let user = SuggestedUser(profile: profile, pubkey: test_pubkey)! + let user = SuggestedUser(name: "klabo", about: "name", picture: "about", pubkey: test_pubkey)! List { SuggestedUserView(user: user, damus_state: test_damus_state()) } diff --git a/damus/Views/Onboarding/SuggestedUsersViewModel.swift b/damus/Views/Onboarding/SuggestedUsersViewModel.swift @@ -35,8 +35,9 @@ class SuggestedUsersViewModel: ObservableObject { } func suggestedUser(pubkey: Pubkey) -> SuggestedUser? { - if let profile = damus_state.profiles.lookup(id: pubkey), - let user = SuggestedUser(profile: profile, pubkey: pubkey) { + let profile_txn = damus_state.profiles.lookup(id: pubkey) + if let profile = profile_txn.unsafeUnownedValue, + let user = SuggestedUser(name: profile.name, about: profile.about, picture: profile.picture, pubkey: pubkey) { return user } return nil diff --git a/damus/Views/PostView.swift b/damus/Views/PostView.swift @@ -173,7 +173,8 @@ struct PostView: View { return .init(string: "") } - let profile = damus_state.profiles.lookup(id: pubkey) + let profile_txn = damus_state.profiles.lookup(id: pubkey) + let profile = profile_txn.unsafeUnownedValue return user_tag_attr_string(profile: profile, pubkey: pubkey) } diff --git a/damus/Views/Posting/UserSearch.swift b/damus/Views/Posting/UserSearch.swift @@ -7,15 +7,6 @@ import SwiftUI -struct SearchedUser: Identifiable { - let profile: Profile? - let pubkey: Pubkey - - var id: Pubkey { - return pubkey - } -} - struct UserSearch: View { let damus_state: DamusState let search: String @@ -25,13 +16,14 @@ struct UserSearch: View { @Binding var post: NSMutableAttributedString @EnvironmentObject var tagModel: TagModel - var users: [SearchedUser] { + var users: [Pubkey] { return search_profiles(profiles: damus_state.profiles, search: search) } - func on_user_tapped(user: SearchedUser) { - let pk = user.pubkey - let user_tag = user_tag_attr_string(profile: user.profile, pubkey: pk) + func on_user_tapped(pk: Pubkey) { + let profile_txn = damus_state.profiles.lookup(id: pk) + let profile = profile_txn.unsafeUnownedValue + let user_tag = user_tag_attr_string(profile: profile, pubkey: pk) appendUserTag(withTag: user_tag) } @@ -57,11 +49,11 @@ struct UserSearch: View { if users.count == 0 { EmptyUserSearchView() } else { - ForEach(users) { user in - UserView(damus_state: damus_state, pubkey: user.pubkey) + ForEach(users) { pk in + UserView(damus_state: damus_state, pubkey: pk) .contentShape(Rectangle()) .onTapGesture { - on_user_tapped(user: user) + on_user_tapped(pk: pk) } } } diff --git a/damus/Views/Profile/EditMetadataView.swift b/damus/Views/Profile/EditMetadataView.swift @@ -20,8 +20,7 @@ struct EditMetadataView: View { @State var name: String @State var ln: String @State var website: String - let profile: Profile? - + @Environment(\.dismiss) var dismiss @State var confirm_ln_address: Bool = false @@ -31,9 +30,8 @@ struct EditMetadataView: View { init(damus_state: DamusState) { self.damus_state = damus_state - let data = damus_state.profiles.lookup(id: damus_state.pubkey) - self.profile = data - + let data = damus_state.profiles.lookup(id: damus_state.pubkey).unsafeUnownedValue + _name = State(initialValue: data?.name ?? "") _display_name = State(initialValue: data?.display_name ?? "") _about = State(initialValue: data?.about ?? "") diff --git a/damus/Views/Profile/EventProfileName.swift b/damus/Views/Profile/EventProfileName.swift @@ -12,20 +12,19 @@ import SwiftUI struct EventProfileName: View { let damus_state: DamusState let pubkey: Pubkey - let profile: Profile? - + @State var display_name: DisplayName? @State var nip05: NIP05? @State var donation: Int? let size: EventViewKind - init(pubkey: Pubkey, profile: Profile?, damus: DamusState, size: EventViewKind = .normal) { + init(pubkey: Pubkey, damus: DamusState, size: EventViewKind = .normal) { self.damus_state = damus self.pubkey = pubkey - self.profile = profile self.size = size - self._donation = State(wrappedValue: profile?.damus_donation) + let donation = damus.ndb.lookup_profile(pubkey).map({ p in p?.profile?.damus_donation }).value + self._donation = State(wrappedValue: donation) } var friend_type: FriendType? { @@ -36,11 +35,11 @@ struct EventProfileName: View { nip05 ?? damus_state.profiles.is_validated(pubkey) } - var current_display_name: DisplayName { + func current_display_name(_ profile: Profile?) -> DisplayName { return display_name ?? Profile.displayName(profile: profile, pubkey: pubkey) } - var onlyzapper: Bool { + func onlyzapper(_ profile: Profile?) -> Bool { guard let profile else { return false } @@ -58,8 +57,10 @@ struct EventProfileName: View { } var body: some View { + let profile_txn = damus_state.profiles.lookup(id: pubkey) + let profile = profile_txn.unsafeUnownedValue HStack(spacing: 2) { - switch current_display_name { + switch current_display_name(profile) { case .one(let one): Text(one) .font(.body.weight(.bold)) @@ -84,7 +85,7 @@ struct EventProfileName: View { FriendIcon(friend: frend) } - if onlyzapper { + if onlyzapper(profile) { Image("zap-hashtag") .frame(width: 14, height: 14) } @@ -97,9 +98,24 @@ struct EventProfileName: View { if update.pubkey != pubkey { return } - display_name = Profile.displayName(profile: update.profile, pubkey: pubkey) - nip05 = damus_state.profiles.is_validated(pubkey) - donation = update.profile.damus_donation + + let profile_txn = damus_state.profiles.lookup(id: update.pubkey) + guard let profile = profile_txn.unsafeUnownedValue else { return } + + let display_name = Profile.displayName(profile: profile, pubkey: pubkey) + if display_name != self.display_name { + self.display_name = display_name + } + + 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 + } } } } @@ -107,6 +123,6 @@ struct EventProfileName: View { struct EventProfileName_Previews: PreviewProvider { static var previews: some View { - EventProfileName(pubkey: test_note.pubkey, profile: nil, damus: test_damus_state()) + EventProfileName(pubkey: test_note.pubkey, damus: test_damus_state()) } } diff --git a/damus/Views/Profile/ProfileName.swift b/damus/Views/Profile/ProfileName.swift @@ -27,7 +27,6 @@ func get_friend_type(contacts: Contacts, pubkey: Pubkey) -> FriendType? { struct ProfileName: View { let damus_state: DamusState let pubkey: Pubkey - let profile: Profile? let prefix: String let show_nip5_domain: Bool @@ -36,9 +35,8 @@ struct ProfileName: View { @State var nip05: NIP05? @State var donation: Int? - init(pubkey: Pubkey, profile: Profile?, prefix: String = "", damus: DamusState, show_nip5_domain: Bool = true) { + init(pubkey: Pubkey, prefix: String = "", damus: DamusState, show_nip5_domain: Bool = true) { self.pubkey = pubkey - self.profile = profile self.prefix = prefix self.damus_state = damus self.show_nip5_domain = show_nip5_domain @@ -53,15 +51,15 @@ struct ProfileName: View { nip05 ?? damus_state.profiles.is_validated(pubkey) } - var current_display_name: DisplayName { + func current_display_name(profile: Profile?) -> DisplayName { return display_name ?? Profile.displayName(profile: profile, pubkey: pubkey) } - var name_choice: String { - return prefix == "@" ? current_display_name.username.truncate(maxLength: 50) : current_display_name.displayName.truncate(maxLength: 50) + func name_choice(profile: Profile?) -> String { + return prefix == "@" ? current_display_name(profile: profile).username.truncate(maxLength: 50) : current_display_name(profile: profile).displayName.truncate(maxLength: 50) } - var onlyzapper: Bool { + func onlyzapper(profile: Profile?) -> Bool { guard let profile else { return false } @@ -69,7 +67,7 @@ struct ProfileName: View { return profile.reactions == false } - var supporter: Int? { + func supporter(profile: Profile?) -> Int? { guard let profile, let donation = profile.damus_donation, donation > 0 @@ -81,21 +79,28 @@ struct ProfileName: View { } var body: some View { + let profile_txn = damus_state.profiles.lookup(id: pubkey) + let profile = profile_txn.unsafeUnownedValue + HStack(spacing: 2) { - Text(verbatim: "\(prefix)\(name_choice)") + Text(verbatim: "\(prefix)\(name_choice(profile: profile))") .font(.body) .fontWeight(prefix == "@" ? .none : .bold) + if let nip05 = current_nip05 { NIP05Badge(nip05: nip05, pubkey: pubkey, contacts: damus_state.contacts, show_domain: show_nip5_domain, profiles: damus_state.profiles) } + if let friend = friend_type, current_nip05 == nil { FriendIcon(friend: friend) } - if onlyzapper { + + if onlyzapper(profile: profile) { Image("zap-hashtag") .frame(width: 14, height: 14) } - if let supporter { + + if let supporter = supporter(profile: profile) { SupporterBadge(percent: supporter) } } @@ -103,16 +108,38 @@ struct ProfileName: View { if update.pubkey != pubkey { return } - display_name = Profile.displayName(profile: update.profile, pubkey: pubkey) - nip05 = damus_state.profiles.is_validated(pubkey) - donation = profile?.damus_donation + + var profile: Profile! + var profile_txn: NdbTxn<Profile?>! + + switch update { + case .remote(let pubkey): + profile_txn = damus_state.profiles.lookup(id: pubkey) + guard let prof = profile_txn.unsafeUnownedValue else { return } + profile = prof + case .manual(_, let prof): + profile = prof + } + + let display_name = Profile.displayName(profile: profile, pubkey: pubkey) + if self.display_name != display_name { + self.display_name = display_name + } + + let nip05 = damus_state.profiles.is_validated(pubkey) + if nip05 != self.nip05 { + self.nip05 = nip05 + } + + if donation != profile.damus_donation { + donation = profile.damus_donation + } } } } struct ProfileName_Previews: PreviewProvider { static var previews: some View { - ProfileName(pubkey: - test_damus_state().pubkey, profile: make_test_profile(), damus: test_damus_state()) + ProfileName(pubkey: test_damus_state().pubkey, damus: test_damus_state()) } } diff --git a/damus/Views/Profile/ProfileNameView.swift b/damus/Views/Profile/ProfileNameView.swift @@ -87,7 +87,6 @@ fileprivate struct KeyView: View { struct ProfileNameView: View { let pubkey: Pubkey - let profile: Profile? let damus: DamusState var spacing: CGFloat { 10.0 } @@ -95,10 +94,13 @@ struct ProfileNameView: View { var body: some View { Group { VStack(alignment: .leading) { + let profile_txn = self.damus.profiles.lookup(id: pubkey) + let profile = profile_txn.unsafeUnownedValue + switch Profile.displayName(profile: profile, pubkey: pubkey) { case .one: HStack(alignment: .center, spacing: spacing) { - ProfileName(pubkey: pubkey, profile: profile, damus: damus) + ProfileName(pubkey: pubkey, damus: damus) .font(.title3.weight(.bold)) } case .both(username: _, displayName: let displayName): @@ -106,7 +108,7 @@ struct ProfileNameView: View { .font(.title3.weight(.bold)) HStack(alignment: .center, spacing: spacing) { - ProfileName(pubkey: pubkey, profile: profile, prefix: "@", damus: damus) + ProfileName(pubkey: pubkey, prefix: "@", damus: damus) .font(.callout) .foregroundColor(.gray) } @@ -124,9 +126,9 @@ struct ProfileNameView: View { struct ProfileNameView_Previews: PreviewProvider { static var previews: some View { VStack { - ProfileNameView(pubkey: test_note.pubkey, profile: nil, damus: test_damus_state()) + ProfileNameView(pubkey: test_note.pubkey, damus: test_damus_state()) - ProfileNameView(pubkey: test_note.pubkey, profile: nil, damus: test_damus_state()) + ProfileNameView(pubkey: test_note.pubkey, damus: test_damus_state()) } } } diff --git a/damus/Views/Profile/ProfilePicView.swift b/damus/Views/Profile/ProfilePicView.swift @@ -87,16 +87,25 @@ struct ProfilePicView: View { guard updated.pubkey == self.pubkey else { return } - - if let pic = updated.profile.picture { - self.picture = pic + + switch updated { + case .manual(_, let profile): + if let pic = profile.picture { + self.picture = pic + } + case .remote(pubkey: let pk): + let profile_txn = profiles.lookup(id: pk) + let profile = profile_txn.unsafeUnownedValue + if let pic = profile?.picture { + self.picture = pic + } } } } } func get_profile_url(picture: String?, pubkey: Pubkey, profiles: Profiles) -> URL { - let pic = picture ?? profiles.lookup(id: pubkey)?.picture ?? robohash(pubkey) + let pic = picture ?? profiles.lookup(id: pubkey).map({ $0?.picture }).value ?? robohash(pubkey) if let url = URL(string: pic) { return url } diff --git a/damus/Views/Profile/ProfilePictureSelector.swift b/damus/Views/Profile/ProfilePictureSelector.swift @@ -43,7 +43,7 @@ struct EditProfilePictureView: View { if let profile_url { return profile_url } else if let state = damus_state, - let picture = state.profiles.lookup(id: pubkey)?.picture { + let picture = state.profiles.lookup(id: pubkey).map({ pr in pr?.picture }).value { return URL(string: picture) } else { return profile_url ?? URL(string: robohash(pubkey)) diff --git a/damus/Views/Profile/ProfileView.swift b/damus/Views/Profile/ProfileView.swift @@ -31,11 +31,11 @@ func follow_btn_txt(_ fs: FollowState, follows_you: Bool) -> String { } } -func followedByString(_ friend_intersection: [Pubkey], profiles: Profiles, locale: Locale = Locale.current) -> String { +func followedByString<Y>(txn: NdbTxn<Y>, _ friend_intersection: [Pubkey], ndb: Ndb, locale: Locale = Locale.current) -> String { let bundle = bundleForLocale(locale: locale) - let names: [String] = friend_intersection.prefix(3).map { - let profile = profiles.lookup(id: $0) - return Profile.displayName(profile: profile, pubkey: $0).username.truncate(maxLength: 20) + let names: [String] = friend_intersection.prefix(3).map { pk in + let profile = ndb.lookup_profile_with_txn(pk, txn: txn)?.profile + return Profile.displayName(profile: profile, pubkey: pk).username.truncate(maxLength: 20) } switch friend_intersection.count { @@ -216,27 +216,29 @@ struct ProfileView: View { .accentColor(DamusColors.white) } - func lnButton(lnurl: String, record: ProfileRecord, profile: Profile) -> some View { - let profile = record.profile! - let button_img = profile.reactions == false ? "zap.fill" : "zap" - return Button(action: { + func lnButton(lnurl: String, unownedProfile: Profile?, pubkey: Pubkey) -> some View { + let reactions = unownedProfile?.reactions ?? true + let button_img = reactions ? "zap.fill" : "zap" + let lud16 = unownedProfile?.lud16 + + return Button(action: { [lnurl] in present_sheet(.zap(target: .profile(self.profile.pubkey), lnurl: lnurl)) }) { Image(button_img) .foregroundColor(button_img == "zap.fill" ? .orange : Color.primary) .profile_button_style(scheme: colorScheme) - .contextMenu { - if profile.reactions == false { + .contextMenu { [lud16, reactions, lnurl] in + if reactions == false { Text("OnlyZaps Enabled", comment: "Non-tappable text in context menu that shows up when the zap button on profile is long pressed to indicate that the user has enabled OnlyZaps, meaning that they would like to be only zapped and not accept reactions to their notes.") } - if let addr = profile.lud16 { + if let lud16 { Button { - UIPasteboard.general.string = addr + UIPasteboard.general.string = lud16 } label: { - Label(addr, image: "copy2") + Label(lud16, image: "copy2") } - } else if let lnurl = record.lnurl { + } else { Button { UIPasteboard.general.string = lnurl } label: { @@ -269,14 +271,14 @@ struct ProfileView: View { .font(.footnote) } - func actionSection(record: ProfileRecord?) -> some View { + func actionSection(record: ProfileRecord?, pubkey: Pubkey) -> some View { return Group { if let record, let profile = record.profile, let lnurl = record.lnurl, lnurl != "" { - lnButton(lnurl: lnurl, record: record, profile: profile) + lnButton(lnurl: lnurl, unownedProfile: profile, pubkey: pubkey) } dmButton @@ -311,6 +313,7 @@ struct ProfileView: View { func nameSection(profile_data: ProfileRecord?) -> some View { return Group { let follows_you = profile.pubkey != damus_state.pubkey && profile.follows(pubkey: damus_state.pubkey) + HStack(alignment: .center) { ProfilePicView(pubkey: profile.pubkey, size: pfp_size, highlight: .custom(imageBorderColor(), 4.0), profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation) .padding(.top, -(pfp_size / 2.0)) @@ -329,10 +332,10 @@ struct ProfileView: View { followsYouBadge } - actionSection(record: profile_data) + actionSection(record: profile_data, pubkey: profile.pubkey) } - ProfileNameView(pubkey: profile.pubkey, profile: profile_data?.profile, damus: damus_state) + ProfileNameView(pubkey: profile.pubkey, damus: damus_state) } } @@ -355,7 +358,8 @@ struct ProfileView: View { var aboutSection: some View { VStack(alignment: .leading, spacing: 8.0) { - let profile_data = damus_state.profiles.lookup_with_timestamp(profile.pubkey) + let profile_txn = damus_state.profiles.lookup_with_timestamp(profile.pubkey) + let profile_data = profile_txn.unsafeUnownedValue nameSection(profile_data: profile_data) @@ -422,7 +426,7 @@ struct ProfileView: View { NavigationLink(value: Route.FollowersYouKnow(friendedFollowers: friended_followers, followers: followers)) { HStack { CondensedProfilePicturesView(state: damus_state, pubkeys: friended_followers, maxPictures: 3) - let followedByString = followedByString(friended_followers, profiles: damus_state.profiles) + let followedByString = followedByString(txn: profile_txn, friended_followers, ndb: damus_state.ndb) Text(followedByString) .font(.subheadline).foregroundColor(.gray) .multilineTextAlignment(.leading) @@ -516,7 +520,9 @@ extension View { @MainActor func check_nip05_validity(pubkey: Pubkey, profiles: Profiles) { - guard let profile = profiles.lookup(id: pubkey), + let profile_txn = profiles.lookup(id: pubkey) + + guard let profile = profile_txn.unsafeUnownedValue, let nip05 = profile.nip05, profiles.is_validated(pubkey) == nil else { @@ -532,7 +538,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(pubkey: pubkey, profile: profile)) + notify(.profile_updated(.remote(pubkey: pubkey))) } } } diff --git a/damus/Views/QRCodeView.swift b/damus/Views/QRCodeView.swift @@ -118,9 +118,11 @@ struct QRCodeView: View { var QRView: some View { VStack(alignment: .center) { - let profile = damus_state.profiles.lookup(id: pubkey) - - if (damus_state.profiles.lookup(id: damus_state.pubkey)?.picture) != nil { + let profile_txn = damus_state.profiles.lookup(id: pubkey) + let profile = profile_txn.unsafeUnownedValue + let our_profile = damus_state.ndb.lookup_profile_with_txn(damus_state.pubkey, txn: profile_txn) + + if our_profile?.profile?.picture != nil { ProfilePicView(pubkey: pubkey, size: 90.0, highlight: .custom(DamusColors.white, 3.0), profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation) .padding(.top, 50) } else { diff --git a/damus/Views/ReplyView.swift b/damus/Views/ReplyView.swift @@ -24,10 +24,11 @@ struct ReplyView: View { var ReplyingToSection: some View { HStack { Group { + let txn = NdbTxn(ndb: damus.ndb) let names = references .map { pubkey in let pk = pubkey - let prof = damus.profiles.lookup(id: pk) + let prof = damus.ndb.lookup_profile_with_txn(pk, txn: txn)?.profile return "@" + Profile.displayName(profile: prof, pubkey: pk).username.truncate(maxLength: 50) } .joined(separator: " ") diff --git a/damus/Views/Reposts/RepostedEvent.swift b/damus/Views/Reposts/RepostedEvent.swift @@ -15,10 +15,8 @@ struct RepostedEvent: View { var body: some View { VStack(alignment: .leading) { - let prof = damus.profiles.lookup(id: event.pubkey) - NavigationLink(value: Route.ProfileByKey(pubkey: event.pubkey)) { - Reposted(damus: damus, pubkey: event.pubkey, profile: prof) + Reposted(damus: damus, pubkey: event.pubkey) .padding(.horizontal) } .buttonStyle(PlainButtonStyle()) diff --git a/damus/Views/Search/SearchingEventView.swift b/damus/Views/Search/SearchingEventView.swift @@ -44,23 +44,25 @@ struct SearchingEventView: View { switch search { case .nip05(let nip05): if let pk = state.profiles.nip05_pubkey[nip05] { - if state.profiles.lookup(id: pk) != nil { + if state.profiles.lookup_key_by_pubkey(pk) != nil { self.search_state = .found_profile(pk) } } else { Task { guard let nip05 = NIP05.parse(nip05) else { - self.search_state = .not_found + Task { @MainActor in + self.search_state = .not_found + } return } guard let nip05_resp = await fetch_nip05(nip05: nip05) else { - DispatchQueue.main.async { + Task { @MainActor in self.search_state = .not_found } return } - DispatchQueue.main.async { + Task { @MainActor in guard let pk = nip05_resp.names[nip05.username] else { self.search_state = .not_found return @@ -81,11 +83,11 @@ struct SearchingEventView: View { } case .profile(let pubkey): find_event(state: state, query: .profile(pubkey: pubkey)) { res in - guard case .profile(_, let ev) = res else { + guard case .profile(let pubkey) = res else { self.search_state = .not_found return } - self.search_state = .found_profile(ev.pubkey) + self.search_state = .found_profile(pubkey) } } } diff --git a/damus/Views/SearchResultsView.swift b/damus/Views/SearchResultsView.swift @@ -9,11 +9,11 @@ import SwiftUI struct MultiSearch { let hashtag: String - let profiles: [SearchedUser] + let profiles: [Pubkey] } enum Search: Identifiable { - case profiles([SearchedUser]) + case profiles([Pubkey]) case hashtag(String) case profile(Pubkey) case note(NoteId) @@ -49,10 +49,10 @@ struct InnerSearchResults: View { } } - func ProfilesSearch(_ results: [SearchedUser]) -> some View { + func ProfilesSearch(_ results: [Pubkey]) -> some View { return LazyVStack { - ForEach(results) { prof in - ProfileSearchResult(pk: prof.pubkey) + ForEach(results, id: \.id) { pk in + ProfileSearchResult(pk: pk) } } } @@ -171,27 +171,23 @@ func make_hashtagable(_ str: String) -> String { return String(new.filter{$0 != " "}) } -func search_profiles(profiles: Profiles, search: String) -> [SearchedUser] { +func search_profiles(profiles: Profiles, search: String) -> [Pubkey] { // Search by hex pubkey. if let pubkey = hex_decode_pubkey(search), - let profile = profiles.lookup(id: pubkey) + profiles.lookup_key_by_pubkey(pubkey) != nil { - return [SearchedUser(profile: profile, pubkey: pubkey)] + return [pubkey] } // Search by npub pubkey. if search.starts(with: "npub"), let bech32_key = decode_bech32_key(search), case Bech32Key.pub(let pk) = bech32_key, - let profile = profiles.lookup(id: pk) + profiles.lookup_key_by_pubkey(pk) != nil { - return [SearchedUser(profile: profile, pubkey: pk)] + return [pk] } let new = search.lowercased() - let matched_pubkeys = profiles.user_search_cache.search(key: new) - - return matched_pubkeys - .map { SearchedUser(profile: profiles.lookup(id: $0), pubkey: $0) } - .filter { $0.profile != nil } + return profiles.user_search_cache.search(key: new) } diff --git a/damus/Views/SideMenuView.swift b/damus/Views/SideMenuView.swift @@ -83,7 +83,8 @@ struct SideMenuView: View { } var TopProfile: some View { - let profile = damus_state.profiles.lookup(id: damus_state.pubkey) + let profile_txn = damus_state.profiles.lookup(id: damus_state.pubkey) + let profile = profile_txn.unsafeUnownedValue return VStack(alignment: .leading, spacing: verticalSpacing) { HStack { ProfilePicView(pubkey: damus_state.pubkey, size: 60, highlight: .none, profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation) diff --git a/damus/Views/Wallet/WalletView.swift b/damus/Views/Wallet/WalletView.swift @@ -145,7 +145,7 @@ struct WalletView: View { Spacer() } - EventProfile(damus_state: damus_state, pubkey: damus_state.pubkey, profile: damus_state.profiles.lookup(id: damus_state.pubkey), size: .small) + EventProfile(damus_state: damus_state, pubkey: damus_state.pubkey, size: .small) } .padding(25) } @@ -164,17 +164,20 @@ struct WalletView: View { model.initial_percent = settings.donation_percent } .onChange(of: settings.donation_percent) { p in - guard let profile = damus_state.profiles.lookup(id: damus_state.pubkey) else { + let profile_txn = damus_state.profiles.lookup(id: damus_state.pubkey) + guard let profile = profile_txn.unsafeUnownedValue else { return } let prof = Profile(name: profile.name, display_name: profile.display_name, about: profile.about, picture: profile.picture, banner: profile.banner, website: profile.website, lud06: profile.lud06, lud16: profile.lud16, nip05: profile.nip05, damus_donation: p, reactions: profile.reactions) - notify(.profile_updated(pubkey: damus_state.pubkey, profile: prof)) + notify(.profile_updated(.manual(pubkey: self.damus_state.pubkey, profile: prof))) } .onDisappear { + let profile_txn = damus_state.profiles.lookup(id: damus_state.pubkey) + guard let keypair = damus_state.keypair.to_full(), - let profile = damus_state.profiles.lookup(id: damus_state.pubkey), + let profile = profile_txn.unsafeUnownedValue, model.initial_percent != profile.damus_donation else { return diff --git a/damus/Views/Zaps/ZapTypePicker.swift b/damus/Views/Zaps/ZapTypePicker.swift @@ -117,7 +117,8 @@ func zap_type_desc(type: ZapType, profiles: Profiles, pubkey: Pubkey) -> String case .anon: return NSLocalizedString("No one will see that you zapped", comment: "Description of anonymous zap type where the zap is sent anonymously and does not identify the user who sent it.") case .priv: - let prof = profiles.lookup(id: pubkey) + let prof_txn = profiles.lookup(id: pubkey) + let prof = prof_txn.unsafeUnownedValue let name = Profile.displayName(profile: prof, pubkey: pubkey).username.truncate(maxLength: 50) return String.localizedStringWithFormat(NSLocalizedString("private_zap_description", value: "Only '%@' will see that you zapped them", comment: "Description of private zap type where the zap is sent privately and does not identify the user to the public."), name) case .non_zap: diff --git a/nostrdb/Ndb.swift b/nostrdb/Ndb.swift @@ -49,44 +49,130 @@ class Ndb { self.ndb = ndb } - func lookup_note_by_key(_ key: UInt64) -> NdbNote? { - guard let note_p = ndb_get_note_by_key(ndb.ndb, key, nil) else { + func lookup_note_by_key_with_txn<Y>(_ key: NoteKey, txn: NdbTxn<Y>) -> NdbNote? { + guard let note_p = ndb_get_note_by_key(&txn.txn, key, nil) else { return nil } - return NdbNote(note: note_p, owned_size: nil) + return NdbNote(note: note_p, owned_size: nil, key: key) } - func lookup_note(_ id: NoteId) -> NdbNote? { - id.id.withUnsafeBytes { (ptr: UnsafeRawBufferPointer) -> NdbNote? in + func lookup_note_by_key(_ key: NoteKey) -> NdbTxn<NdbNote?> { + return NdbTxn(ndb: self) { txn in + lookup_note_by_key_with_txn(key, txn: txn) + } + } + + private func lookup_profile_by_key_inner<Y>(_ key: ProfileKey, txn: NdbTxn<Y>) -> ProfileRecord? { + var size: Int = 0 + guard let profile_p = ndb_get_profile_by_key(&txn.txn, key, &size) else { + return nil + } + + return profile_flatbuf_to_record(ptr: profile_p, size: size, key: key) + } + + private func profile_flatbuf_to_record(ptr: UnsafeMutableRawPointer, size: Int, key: UInt64) -> ProfileRecord? { + do { + var buf = ByteBuffer(assumingMemoryBound: ptr, capacity: size) + let rec: NdbProfileRecord = try getDebugCheckedRoot(byteBuffer: &buf) + return ProfileRecord(data: rec, key: key) + } catch { + // Handle error appropriately + print("UNUSUAL: \(error)") + return nil + } + } + + private func lookup_note_with_txn_inner<Y>(id: NoteId, txn: NdbTxn<Y>) -> NdbNote? { + return id.id.withUnsafeBytes { (ptr: UnsafeRawBufferPointer) -> NdbNote? in + var key: UInt64 = 0 guard let baseAddress = ptr.baseAddress, - let note_p = ndb_get_note_by_id(ndb.ndb, baseAddress, nil) else { + let note_p = ndb_get_note_by_id(&txn.txn, baseAddress, nil, &key) else { return nil } - return NdbNote(note: note_p, owned_size: nil) + return NdbNote(note: note_p, owned_size: nil, key: key) } } - func lookup_profile(_ pubkey: Pubkey) -> ProfileRecord? { + private func lookup_profile_with_txn_inner<Y>(pubkey: Pubkey, txn: NdbTxn<Y>) -> ProfileRecord? { return pubkey.id.withUnsafeBytes { (ptr: UnsafeRawBufferPointer) -> ProfileRecord? in var size: Int = 0 + var key: UInt64 = 0 guard let baseAddress = ptr.baseAddress, - let profile_p = ndb_get_profile_by_pubkey(ndb.ndb, baseAddress, &size) + let profile_p = ndb_get_profile_by_pubkey(&txn.txn, baseAddress, &size, &key) else { return nil } - do { - var buf = ByteBuffer(assumingMemoryBound: profile_p, capacity: size) - let rec: NdbProfileRecord = try getDebugCheckedRoot(byteBuffer: &buf) - return ProfileRecord(data: rec) - } catch { - // Handle error appropriately - print("UNUSUAL: \(error)") + return profile_flatbuf_to_record(ptr: profile_p, size: size, key: key) + } + } + + func lookup_profile_by_key_with_txn<Y>(key: ProfileKey, txn: NdbTxn<Y>) -> ProfileRecord? { + lookup_profile_by_key_inner(key, txn: txn) + } + + func lookup_profile_by_key(key: ProfileKey) -> NdbTxn<ProfileRecord?> { + return NdbTxn(ndb: self) { txn in + lookup_profile_by_key_inner(key, txn: txn) + } + } + + func lookup_note_with_txn<Y>(id: NoteId, txn: NdbTxn<Y>) -> NdbNote? { + lookup_note_with_txn_inner(id: id, txn: txn) + } + + func lookup_profile_key(_ pubkey: Pubkey) -> ProfileKey? { + return NdbTxn(ndb: self) { txn in + lookup_profile_key_with_txn(pubkey, txn: txn) + }.value + } + + func lookup_profile_key_with_txn<Y>(_ pubkey: Pubkey, txn: NdbTxn<Y>) -> ProfileKey? { + return pubkey.id.withUnsafeBytes { (ptr: UnsafeRawBufferPointer) -> NoteKey? in + guard let p = ptr.baseAddress else { return nil } + let r = ndb_get_profilekey_by_pubkey(&txn.txn, p) + if r == 0 { return nil } + return r } } + + func lookup_note_key_with_txn<Y>(_ id: NoteId, txn: NdbTxn<Y>) -> NoteKey? { + return id.id.withUnsafeBytes { (ptr: UnsafeRawBufferPointer) -> NoteKey? in + guard let p = ptr.baseAddress else { + return nil + } + let r = ndb_get_notekey_by_id(&txn.txn, p) + if r == 0 { + return nil + } + return r + } + } + + func lookup_note_key(_ id: NoteId) -> NoteKey? { + NdbTxn(ndb: self, with: { txn in lookup_note_key_with_txn(id, txn: txn) }).value + } + + func lookup_note(_ id: NoteId) -> NdbTxn<NdbNote?> { + return NdbTxn(ndb: self) { txn in + lookup_note_with_txn_inner(id: id, txn: txn) + } + } + + func lookup_profile(_ pubkey: Pubkey) -> NdbTxn<ProfileRecord?> { + return NdbTxn(ndb: self) { txn in + lookup_profile_with_txn_inner(pubkey: pubkey, txn: txn) + } + } + + func lookup_profile_with_txn<Y>(_ pubkey: Pubkey, txn: NdbTxn<Y>) -> ProfileRecord? { + lookup_profile_with_txn_inner(pubkey: pubkey, txn: txn) + } + func process_event(_ str: String) -> Bool { return str.withCString { cstr in return ndb_process_event(ndb.ndb, cstr, Int32(str.utf8.count)) != 0 @@ -106,7 +192,7 @@ class Ndb { #if DEBUG func getDebugCheckedRoot<T: FlatBufferObject & Verifiable>(byteBuffer: inout ByteBuffer) throws -> T { - return try getCheckedRoot(byteBuffer: &byteBuffer) + return try getRoot(byteBuffer: &byteBuffer) } #else func getDebugCheckedRoot<T: FlatBufferObject>(byteBuffer: inout ByteBuffer) throws -> T { diff --git a/nostrdb/NdbNote.swift b/nostrdb/NdbNote.swift @@ -38,6 +38,7 @@ class NdbNote: Encodable, Equatable, Hashable { // we can have owned notes, but we can also have lmdb virtual-memory mapped notes so its optional private let owned: Bool let count: Int + let key: NoteKey? let note: UnsafeMutablePointer<ndb_note> // cached stuff (TODO: remove these) @@ -48,10 +49,11 @@ class NdbNote: Encodable, Equatable, Hashable { return NdbNote.owned_from_json_cstr(json: content_raw, json_len: content_len) }() - init(note: UnsafeMutablePointer<ndb_note>, owned_size: Int?) { + init(note: UnsafeMutablePointer<ndb_note>, owned_size: Int?, key: NoteKey?) { self.note = note self.owned = owned_size != nil self.count = owned_size ?? 0 + self.key = key #if DEBUG_NOTE_SIZE if let owned_size { @@ -218,6 +220,7 @@ class NdbNote: Encodable, Equatable, Hashable { } self.note = r.assumingMemoryBound(to: ndb_note.self) + self.key = nil } static func owned_from_json(json: String, bufsize: Int = 2 << 18) -> NdbNote? { @@ -245,7 +248,7 @@ class NdbNote: Encodable, Equatable, Hashable { guard let note_data = realloc(data, Int(len)) else { return nil } let new_note = note_data.assumingMemoryBound(to: ndb_note.self) - return NdbNote(note: new_note, owned_size: Int(len)) + return NdbNote(note: new_note, owned_size: Int(len), key: nil) } } diff --git a/nostrdb/NdbTagsIterator.swift b/nostrdb/NdbTagsIterator.swift @@ -70,7 +70,7 @@ struct TagsSequence: Encodable, Sequence { } precondition(false, "sequence subscript oob") // it seems like the compiler needs this or it gets bitchy - return .init(note: .init(note: .allocate(capacity: 1), owned_size: nil), tag: .allocate(capacity: 1)) + return .init(note: .init(note: .allocate(capacity: 1), owned_size: nil, key: nil), tag: .allocate(capacity: 1)) } func makeIterator() -> TagsIterator { diff --git a/nostrdb/NdbTxn.swift b/nostrdb/NdbTxn.swift @@ -17,7 +17,7 @@ class NdbTxn<T> { private var val: T! var moved: Bool - init(ndb: Ndb, with: (NdbTxn<T>) -> T) { + init(ndb: Ndb, with: (NdbTxn<T>) -> T = { _ in () }) { self.txn = ndb_txn() #if TXNDEBUG txn_count += 1 diff --git a/nostrdb/nostrdb.c b/nostrdb/nostrdb.c @@ -264,7 +264,9 @@ int ndb_get_tsid(MDB_txn *txn, struct ndb_lmdb *lmdb, enum ndb_dbs db, int success = 0; struct ndb_tsid tsid; + // position at the most recent ndb_tsid_high(&tsid, id); + k.mv_data = &tsid; k.mv_size = sizeof(tsid); @@ -341,32 +343,44 @@ static void *ndb_lookup_tsid(struct ndb_txn *txn, enum ndb_dbs ind, return res; } -void *ndb_get_profile_by_pubkey(struct ndb_txn *txn, const unsigned char *pk, size_t *len, uint32_t *key) +void *ndb_get_profile_by_pubkey(struct ndb_txn *txn, const unsigned char *pk, size_t *len, uint64_t *key) { return ndb_lookup_tsid(txn, NDB_DB_PROFILE_PK, NDB_DB_PROFILE, pk, len, key); } -struct ndb_note *ndb_get_note_by_id(struct ndb_txn *txn, const unsigned char *id, size_t *len, uint32_t *key) +struct ndb_note *ndb_get_note_by_id(struct ndb_txn *txn, const unsigned char *id, size_t *len, uint64_t *key) { return ndb_lookup_tsid(txn, NDB_DB_NOTE_ID, NDB_DB_NOTE, id, len, key); } -uint32_t ndb_get_notekey_by_id(struct ndb_txn *txn, const unsigned char *id) +static inline uint64_t ndb_get_indexkey_by_id(struct ndb_txn *txn, + enum ndb_dbs db, + const unsigned char *id) { MDB_val k; - if (!ndb_get_tsid(txn->mdb_txn, &txn->ndb->lmdb, NDB_DB_NOTE_ID, id, &k)) + if (!ndb_get_tsid(txn->mdb_txn, &txn->ndb->lmdb, db, id, &k)) return 0; return *(uint32_t*)k.mv_data; } -struct ndb_note *ndb_get_note_by_key(struct ndb_txn *txn, uint32_t key, size_t *len) +uint64_t ndb_get_notekey_by_id(struct ndb_txn *txn, const unsigned char *id) +{ + return ndb_get_indexkey_by_id(txn, NDB_DB_NOTE_ID, id); +} + +uint64_t ndb_get_profilekey_by_pubkey(struct ndb_txn *txn, const unsigned char *id) +{ + return ndb_get_indexkey_by_id(txn, NDB_DB_PROFILE_PK, id); +} + +struct ndb_note *ndb_get_note_by_key(struct ndb_txn *txn, uint64_t key, size_t *len) { return ndb_lookup_by_key(txn, key, NDB_DB_NOTE, len); } -void *ndb_get_profile_by_key(struct ndb_txn *txn, uint32_t key, size_t *len) +void *ndb_get_profile_by_key(struct ndb_txn *txn, uint64_t key, size_t *len) { return ndb_lookup_by_key(txn, key, NDB_DB_PROFILE, len); } diff --git a/nostrdb/nostrdb.h b/nostrdb/nostrdb.h @@ -166,6 +166,7 @@ void ndb_end_query(struct ndb_txn *); void *ndb_get_profile_by_pubkey(struct ndb_txn *txn, const unsigned char *pubkey, size_t *len, uint64_t *primkey); void *ndb_get_profile_by_key(struct ndb_txn *txn, uint64_t key, size_t *len); uint64_t ndb_get_notekey_by_id(struct ndb_txn *txn, const unsigned char *id); +uint64_t ndb_get_profilekey_by_pubkey(struct ndb_txn *txn, const unsigned char *id); struct ndb_note *ndb_get_note_by_id(struct ndb_txn *txn, const unsigned char *id, size_t *len, uint64_t *primkey); struct ndb_note *ndb_get_note_by_key(struct ndb_txn *txn, uint64_t key, size_t *len); void ndb_destroy(struct ndb *);