Redesign Ndb.swift interface with build safety

This commit redesigns the Ndb.swift interface with a focus on build-time
safety against crashes.

It removes the external usage of NdbTxn and SafeNdbTxn, restricting it
to be used only in NostrDB internal code.

This prevents dangerous and crash prone usages throughout the app, such
as holding transactions in a variable in an async function (which can
cause thread-based reference counting to incorrectly deinit inherited
transactions in use by separate callers), as well as holding unsafe
unowned values longer than the lifetime of their corresponding
transactions.

Closes: https://github.com/damus-io/damus/issues/3364
Changelog-Fixed: Fixed several crashes throughout the app
Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
This commit is contained in:
Daniel D’Aquino
2025-11-28 19:17:35 -08:00
parent b562b930cc
commit f844ed9931
60 changed files with 611 additions and 497 deletions
@@ -125,8 +125,7 @@ struct NotificationFormatter {
let src = zap.request.ev let src = zap.request.ev
let pk = zap.is_anon ? ANON_PUBKEY : src.pubkey let pk = zap.is_anon ? ANON_PUBKEY : src.pubkey
let profile_txn = profiles.lookup(id: pk) let profile = profiles.lookup(id: pk)
let profile = profile_txn?.unsafeUnownedValue
let name = Profile.displayName(profile: profile, pubkey: pk).displayName.truncate(maxLength: 50) let name = Profile.displayName(profile: profile, pubkey: pk).displayName.truncate(maxLength: 50)
let sats = NSNumber(value: (Double(zap.invoice.amount) / 1000.0)) let sats = NSNumber(value: (Double(zap.invoice.amount) / 1000.0))
@@ -58,8 +58,7 @@ class NotificationService: UNNotificationServiceExtension {
} }
let sender_profile = { let sender_profile = {
let txn = state.ndb.lookup_profile(nostr_event.pubkey) let profile = state.profiles.lookup(id: nostr_event.pubkey)
let profile = txn?.unsafeUnownedValue?.profile
let picture = ((profile?.picture.map { URL(string: $0) }) ?? URL(string: robohash(nostr_event.pubkey)))! let picture = ((profile?.picture.map { URL(string: $0) }) ?? URL(string: robohash(nostr_event.pubkey)))!
return ProfileBuf(picture: picture, return ProfileBuf(picture: picture,
name: profile?.name, name: profile?.name,
@@ -186,8 +185,13 @@ func message_intent_from_note(ndb: Ndb, sender_profile: ProfileBuf, content: Str
// gather recipients // gather recipients
if let recipient_note_id = note.direct_replies() { if let recipient_note_id = note.direct_replies() {
let replying_to = ndb.lookup_note(recipient_note_id) let replying_to_pk = ndb.lookup_note(recipient_note_id, borrow: { replying_to_note -> Pubkey? in
if let replying_to_pk = replying_to?.unsafeUnownedValue?.pubkey { switch replying_to_note {
case .none: return nil
case .some(let note): return note.pubkey
}
})
if let replying_to_pk {
meta.isReplyToCurrentUser = replying_to_pk == our_pubkey meta.isReplyToCurrentUser = replying_to_pk == our_pubkey
if replying_to_pk != sender_pk { if replying_to_pk != sender_pk {
@@ -247,8 +251,12 @@ func message_intent_from_note(ndb: Ndb, sender_profile: ProfileBuf, content: Str
} }
func pubkey_to_inperson(ndb: Ndb, pubkey: Pubkey, our_pubkey: Pubkey) async -> INPerson { func pubkey_to_inperson(ndb: Ndb, pubkey: Pubkey, our_pubkey: Pubkey) async -> INPerson {
let profile_txn = ndb.lookup_profile(pubkey) let profile = ndb.lookup_profile(pubkey, borrow: { profileRecord in
let profile = profile_txn?.unsafeUnownedValue?.profile switch profileRecord {
case .some(let pr): return pr.profile
case .none: return nil
}
})
let name = profile?.name let name = profile?.name
let display_name = profile?.display_name let display_name = profile?.display_name
let nip05 = profile?.nip05 let nip05 = profile?.nip05
+4 -8
View File
@@ -397,8 +397,7 @@ struct ContentView: View {
guard let ds = self.damus_state, guard let ds = self.damus_state,
let lud16 = nwc.lud16, let lud16 = nwc.lud16,
let keypair = ds.keypair.to_full(), let keypair = ds.keypair.to_full(),
let profile_txn = ds.profiles.lookup(id: ds.pubkey), let profile = ds.profiles.lookup(id: ds.pubkey),
let profile = profile_txn.unsafeUnownedValue,
lud16 != profile.lud16 else { lud16 != profile.lud16 else {
return return
} }
@@ -561,8 +560,7 @@ struct ContentView: View {
home.filter_events() home.filter_events()
guard let ds = damus_state, guard let ds = damus_state,
let profile_txn = ds.profiles.lookup(id: ds.pubkey), let profile = ds.profiles.lookup(id: ds.pubkey),
let profile = profile_txn.unsafeUnownedValue,
let keypair = ds.keypair.to_full() let keypair = ds.keypair.to_full()
else { else {
return return
@@ -580,8 +578,7 @@ struct ContentView: View {
} }
}, message: { }, message: {
if case let .user(pubkey, _) = self.muting { if case let .user(pubkey, _) = self.muting {
let profile_txn = damus_state!.profiles.lookup(id: pubkey) let profile = damus_state!.profiles.lookup(id: pubkey)
let profile = profile_txn?.unsafeUnownedValue
let name = Profile.displayName(profile: profile, pubkey: pubkey).username.truncate(maxLength: 50) let name = Profile.displayName(profile: profile, pubkey: pubkey).username.truncate(maxLength: 50)
Text("\(name) has been muted", comment: "Alert message that informs a user was muted.") Text("\(name) has been muted", comment: "Alert message that informs a user was muted.")
} else { } else {
@@ -643,8 +640,7 @@ struct ContentView: View {
} }
}, message: { }, message: {
if case let .user(pubkey, _) = muting { if case let .user(pubkey, _) = muting {
let profile_txn = damus_state?.profiles.lookup(id: pubkey) let profile = damus_state?.profiles.lookup(id: pubkey)
let profile = profile_txn?.unsafeUnownedValue
let name = Profile.displayName(profile: profile, pubkey: pubkey).username.truncate(maxLength: 50) let name = Profile.displayName(profile: profile, pubkey: pubkey).username.truncate(maxLength: 50)
Text("Mute \(name)?", comment: "Alert message prompt to ask if a user should be muted.") Text("Mute \(name)?", comment: "Alert message prompt to ask if a user should be muted.")
} else { } else {
@@ -96,7 +96,7 @@ extension NostrNetworkManager {
if let relevantStreams = streams[metadataEvent.pubkey] { if let relevantStreams = streams[metadataEvent.pubkey] {
// If we have the user metadata event in ndb, then we should have the profile record as well. // If we have the user metadata event in ndb, then we should have the profile record as well.
guard let profile = ndb.lookup_profile(metadataEvent.pubkey) else { return } guard let profile = ndb.lookup_profile_and_copy(metadataEvent.pubkey) else { return }
for relevantStream in relevantStreams.values { for relevantStream in relevantStreams.values {
relevantStream.continuation.yield(profile) relevantStream.continuation.yield(profile)
} }
@@ -144,7 +144,7 @@ extension NostrNetworkManager {
// MARK: - Helper types // MARK: - Helper types
typealias ProfileStreamItem = NdbTxn<ProfileRecord?> typealias ProfileStreamItem = Profile
struct ProfileStreamInfo { struct ProfileStreamInfo {
let id: UUID = UUID() let id: UUID = UUID()
@@ -413,15 +413,18 @@ extension NostrNetworkManager {
switch query { switch query {
case .profile(let pubkey): case .profile(let pubkey):
if let profile_txn = self.ndb.lookup_profile(pubkey), let profileNotNil = self.ndb.lookup_profile(pubkey, borrow: { pr in
let record = profile_txn.unsafeUnownedValue, switch pr {
record.profile != nil case .some(let pr): return pr.profile != nil
{ case .none: return true
}
})
if profileNotNil {
return .profile(pubkey) return .profile(pubkey)
} }
filter = NostrFilter(kinds: [.metadata], limit: 1, authors: [pubkey]) filter = NostrFilter(kinds: [.metadata], limit: 1, authors: [pubkey])
case .event(let evid): case .event(let evid):
if let event = self.ndb.lookup_note(evid)?.unsafeUnownedValue?.to_owned() { if let event = self.ndb.lookup_note_and_copy(evid) {
return .event(event) return .event(event)
} }
filter = NostrFilter(ids: [evid], limit: 1) filter = NostrFilter(ids: [evid], limit: 1)
@@ -87,7 +87,7 @@ extension NostrNetworkManager {
private func getLatestNIP65RelayListEvent() -> NdbNote? { private func getLatestNIP65RelayListEvent() -> NdbNote? {
guard let latestRelayListEventId = delegate.latestRelayListEventIdHex else { return nil } guard let latestRelayListEventId = delegate.latestRelayListEventIdHex else { return nil }
guard let latestRelayListEventId = NoteId(hex: latestRelayListEventId) else { return nil } guard let latestRelayListEventId = NoteId(hex: latestRelayListEventId) else { return nil }
return delegate.ndb.lookup_note(latestRelayListEventId)?.unsafeUnownedValue?.to_owned() return delegate.ndb.lookup_note_and_copy(latestRelayListEventId)
} }
/// Gets the latest `kind:3` relay list from NostrDB. /// Gets the latest `kind:3` relay list from NostrDB.
+59 -8
View File
@@ -11,8 +11,8 @@ typealias Profile = NdbProfile
typealias ProfileKey = UInt64 typealias ProfileKey = UInt64
//typealias ProfileRecord = NdbProfileRecord //typealias ProfileRecord = NdbProfileRecord
class ProfileRecord { struct ProfileRecord: ~Copyable {
let data: NdbProfileRecord private let data: NdbProfileRecord // Marked as private to make users access the safer `profile` property
init(data: NdbProfileRecord, key: ProfileKey) { init(data: NdbProfileRecord, key: ProfileKey) {
self.data = data self.data = data
@@ -20,7 +20,11 @@ class ProfileRecord {
} }
let profileKey: ProfileKey let profileKey: ProfileKey
var profile: Profile? { return data.profile } var profile: Profile? {
// Clone the data since `NdbProfile` can be unowned, but does not `~Copyable` semantics.
// This helps ensure the memory safety of this property
return data.profile?.clone()
}
var receivedAt: UInt64 { data.receivedAt } var receivedAt: UInt64 { data.receivedAt }
var noteKey: UInt64 { data.noteKey } var noteKey: UInt64 { data.noteKey }
@@ -37,10 +41,7 @@ class ProfileRecord {
} }
if addr.contains("@") { if addr.contains("@") {
// this is a heavy op and is used a lot in views, cache it! return lnaddress_to_lnurl(addr)
let addr = lnaddress_to_lnurl(addr);
self._lnurl = addr
return addr
} }
if !addr.lowercased().hasPrefix("lnurl") { if !addr.lowercased().hasPrefix("lnurl") {
@@ -82,6 +83,24 @@ extension NdbProfile {
} }
} }
/// Clones this object. Useful for creating an owned copy from an unowned profile
func clone() -> Self {
return NdbProfile(
name: self.name,
display_name: self.display_name,
about: self.about,
picture: self.picture,
banner: self.banner,
website: self.website,
lud06: self.lud06,
lud16: self.lud16,
nip05: self.nip05,
damus_donation: self.damus_donation,
reactions: self.reactions
)
}
init(name: String? = nil, display_name: String? = nil, about: String? = nil, picture: String? = nil, banner: String? = nil, website: String? = nil, lud06: String? = nil, lud16: String? = nil, nip05: String? = nil, damus_donation: Int? = nil, reactions: Bool = true) { init(name: String? = nil, display_name: String? = nil, about: String? = nil, picture: String? = nil, banner: String? = nil, website: String? = nil, lud06: String? = nil, lud16: String? = nil, nip05: String? = nil, damus_donation: Int? = nil, reactions: Bool = true) {
var fbb = FlatBufferBuilder() var fbb = FlatBufferBuilder()
@@ -309,7 +328,40 @@ func make_ln_url(_ str: String?) -> URL? {
return str.flatMap { URL(string: "lightning:" + $0) } return str.flatMap { URL(string: "lightning:" + $0) }
} }
import Synchronization
@available(iOS 18.0, *)
class CachedLNAddressConverter {
static let shared: CachedLNAddressConverter = .init()
private let cache: Mutex<[String: String?]> = .init([:]) // Using a mutex here to avoid race conditions without imposing actor isolation requirements.
func lnaddress_to_lnurl(_ lnaddr: String) -> String? {
if let cachedValue = cache.withLock({ $0[lnaddr] }) {
return cachedValue
}
let lnurl: String? = compute_lnaddress_to_lnurl(lnaddr)
cache.withLock({ cache in
cache[lnaddr] = .some(lnurl)
})
return lnurl
}
}
func lnaddress_to_lnurl(_ lnaddr: String) -> String? { func lnaddress_to_lnurl(_ lnaddr: String) -> String? {
if #available(iOS 18.0, *) {
// This is a heavy op, use a cache if available!
return CachedLNAddressConverter.shared.lnaddress_to_lnurl(lnaddr)
} else {
// Fallback on earlier versions
return compute_lnaddress_to_lnurl(lnaddr)
}
}
func compute_lnaddress_to_lnurl(_ lnaddr: String) -> String? {
let parts = lnaddr.split(separator: "@") let parts = lnaddr.split(separator: "@")
guard parts.count == 2 else { guard parts.count == 2 else {
return nil return nil
@@ -322,4 +374,3 @@ func lnaddress_to_lnurl(_ lnaddr: String) -> String? {
return bech32_encode(hrp: "lnurl", Array(dat)) return bech32_encode(hrp: "lnurl", Array(dat))
} }
+4 -5
View File
@@ -810,8 +810,7 @@ func validate_event(ev: NostrEvent) -> ValidationResult {
} }
func first_eref_mention(ndb: Ndb, ev: NostrEvent, keypair: Keypair) -> Mention<NoteId>? { func first_eref_mention(ndb: Ndb, ev: NostrEvent, keypair: Keypair) -> Mention<NoteId>? {
guard let blockGroup = try? NdbBlockGroup.from(event: ev, using: ndb, and: keypair) else { return nil } return try? NdbBlockGroup.borrowBlockGroup(event: ev, using: ndb, and: keypair, borrow: { blockGroup in
return blockGroup.forEachBlock({ index, block in return blockGroup.forEachBlock({ index, block in
switch block { switch block {
case .mention(let mention): case .mention(let mention):
@@ -828,12 +827,11 @@ func first_eref_mention(ndb: Ndb, ev: NostrEvent, keypair: Keypair) -> Mention<N
return .loopContinue return .loopContinue
} }
}) })
})
} }
func separate_invoices(ndb: Ndb, ev: NostrEvent, keypair: Keypair) -> [Invoice]? { func separate_invoices(ndb: Ndb, ev: NostrEvent, keypair: Keypair) -> [Invoice]? {
guard let blockGroup = try? NdbBlockGroup.from(event: ev, using: ndb, and: keypair) else { return try? NdbBlockGroup.borrowBlockGroup(event: ev, using: ndb, and: keypair, borrow: { blockGroup in
return nil
}
let invoiceBlocks: [Invoice] = (try? blockGroup.reduce(initialResult: [Invoice](), { index, invoices, block in let invoiceBlocks: [Invoice] = (try? blockGroup.reduce(initialResult: [Invoice](), { index, invoices, block in
switch block { switch block {
case .invoice(let invoice): case .invoice(let invoice):
@@ -846,6 +844,7 @@ func separate_invoices(ndb: Ndb, ev: NostrEvent, keypair: Keypair) -> [Invoice]?
return .loopContinue return .loopContinue
})) ?? [] })) ?? []
return invoiceBlocks.isEmpty ? nil : invoiceBlocks return invoiceBlocks.isEmpty ? nil : invoiceBlocks
})
} }
/** /**
+25 -11
View File
@@ -74,31 +74,45 @@ class Profiles {
profile_data(pubkey).zapper profile_data(pubkey).zapper
} }
func lookup_with_timestamp(_ pubkey: Pubkey) -> NdbTxn<ProfileRecord?>? { func lookup_with_timestamp<T>(_ pubkey: Pubkey, borrow lendingFunction: (_: borrowing ProfileRecord?) throws -> T) rethrows -> T {
ndb.lookup_profile(pubkey) return try ndb.lookup_profile(pubkey, borrow: lendingFunction)
} }
func lookup_by_key(key: ProfileKey) -> NdbTxn<ProfileRecord?>? { func lookup_lnurl(_ pubkey: Pubkey) -> String? {
ndb.lookup_profile_by_key(key: key) return lookup_with_timestamp(pubkey, borrow: { pr in
switch pr {
case .some(let pr): return pr.lnurl
case .none: return nil
}
})
} }
func search<Y>(_ query: String, limit: Int, txn: NdbTxn<Y>) -> [Pubkey] { func lookup_by_key<T>(key: ProfileKey, borrow lendingFunction: (_: borrowing ProfileRecord?) throws -> T) rethrows -> T {
ndb.search_profile(query, limit: limit, txn: txn) return try ndb.lookup_profile_by_key(key: key, borrow: lendingFunction)
} }
func lookup(id: Pubkey, txn_name: String? = nil) -> NdbTxn<Profile?>? { func search(_ query: String, limit: Int) -> [Pubkey] {
guard let txn = ndb.lookup_profile(id, txn_name: txn_name) else { ndb.search_profile(query, limit: limit)
}
func lookup(id: Pubkey) -> Profile? {
return ndb.lookup_profile(id, borrow: { pr in
switch pr {
case .none:
return nil return nil
case .some(let profileRecord):
// This will clone the value to make it owned and safe to return.
return profileRecord.profile
} }
return txn.map({ pr in pr?.profile }) })
} }
func lookup_key_by_pubkey(_ pubkey: Pubkey) -> ProfileKey? { func lookup_key_by_pubkey(_ pubkey: Pubkey) -> ProfileKey? {
ndb.lookup_profile_key(pubkey) ndb.lookup_profile_key(pubkey)
} }
func has_fresh_profile<Y>(id: Pubkey, txn: NdbTxn<Y>) -> Bool { func has_fresh_profile(id: Pubkey) -> Bool {
guard let fetched_at = ndb.read_profile_last_fetched(txn: txn, pubkey: id) guard let fetched_at = ndb.read_profile_last_fetched(pubkey: id)
else { else {
return false return false
} }
@@ -41,9 +41,7 @@ struct EventActionBar: View {
// Fetching an LNURL is expensive enough that it can cause a hitch. Use a special backgroundable function to fetch the value. // Fetching an LNURL is expensive enough that it can cause a hitch. Use a special backgroundable function to fetch the value.
// Fetch on `.onAppear` // Fetch on `.onAppear`
nonisolated func fetchLNURL() { nonisolated func fetchLNURL() {
let lnurl = damus_state.profiles.lookup_with_timestamp(event.pubkey)?.map({ pr in let lnurl = damus_state.profiles.lookup_lnurl(event.pubkey)
pr?.lnurl
}).value
DispatchQueue.main.async { DispatchQueue.main.async {
self.lnurl = lnurl self.lnurl = lnurl
} }
+1 -3
View File
@@ -115,9 +115,7 @@ struct ChatEventView: View {
// MARK: Zapping properties // MARK: Zapping properties
var lnurl: String? { var lnurl: String? {
damus_state.profiles.lookup_with_timestamp(event.pubkey)?.map({ pr in damus_state.profiles.lookup_lnurl(event.pubkey)
pr?.lnurl
}).value
} }
var zap_target: ZapTarget { var zap_target: ZapTarget {
ZapTarget.note(id: event.id, author: event.pubkey) ZapTarget.note(id: event.id, author: event.pubkey)
@@ -38,14 +38,10 @@ func reply_desc(ndb: Ndb, event: NostrEvent, replying_to: NostrEvent?, locale: L
return NSLocalizedString("Replying to self", bundle: bundle, comment: "Label to indicate that the user is replying to themself.") return NSLocalizedString("Replying to self", bundle: bundle, comment: "Label to indicate that the user is replying to themself.")
} }
guard let profile_txn = NdbTxn(ndb: ndb) else {
return ""
}
let names: [String] = pubkeys.map { pk in let names: [String] = pubkeys.map { pk in
let prof = ndb.lookup_profile_with_txn(pk, txn: profile_txn) let profile = ndb.lookup_profile_and_copy(pk)
return Profile.displayName(profile: prof?.profile, pubkey: pk).username.truncate(maxLength: 50) return Profile.displayName(profile: profile, pubkey: pk).username.truncate(maxLength: 50)
} }
let uniqueNames = NSOrderedSet(array: names).array as! [String] let uniqueNames = NSOrderedSet(array: names).array as! [String]
@@ -72,8 +72,9 @@ func render_immediately_available_note_content(ndb: Ndb, ev: NostrEvent, profile
} }
do { do {
let blocks = try NdbBlockGroup.from(event: ev, using: ndb, and: keypair) return try NdbBlockGroup.borrowBlockGroup(event: ev, using: ndb, and: keypair, borrow: { blocks in
return .separated(render_blocks(blocks: blocks, profiles: profiles, can_hide_last_previewable_refs: true)) return .separated(render_blocks(blocks: blocks, profiles: profiles, can_hide_last_previewable_refs: true))
})
} }
catch { catch {
// TODO: Improve error handling in the future, bubbling it up so that the view can decide how display errors. Keep legacy behavior for now. // TODO: Improve error handling in the future, bubbling it up so that the view can decide how display errors. Keep legacy behavior for now.
@@ -327,8 +328,7 @@ func attributed_string_attach_icon(_ astr: inout AttributedString, img: UIImage)
} }
func getDisplayName(pk: Pubkey, profiles: Profiles) -> String { func getDisplayName(pk: Pubkey, profiles: Profiles) -> String {
let profile_txn = profiles.lookup(id: pk, txn_name: "getDisplayName") let profile = profiles.lookup(id: pk)
let profile = profile_txn?.unsafeUnownedValue
return Profile.displayName(profile: profile, pubkey: pk).username.truncate(maxLength: 50) return Profile.displayName(profile: profile, pubkey: pk).username.truncate(maxLength: 50)
} }
+9 -14
View File
@@ -275,11 +275,8 @@ struct NoteContentView: View {
} }
func ensureMentionProfilesAreFetchingIfNeeded() { func ensureMentionProfilesAreFetchingIfNeeded() {
guard let blockGroup = try? NdbBlockGroup.from(event: event, using: damus_state.ndb, and: damus_state.keypair) else {
return
}
var mentionPubkeys: Set<Pubkey> = [] var mentionPubkeys: Set<Pubkey> = []
try? NdbBlockGroup.borrowBlockGroup(event: event, using: damus_state.ndb, and: damus_state.keypair, borrow: { blockGroup in
let _: ()? = try? blockGroup.forEachBlock({ _, block in let _: ()? = try? blockGroup.forEachBlock({ _, block in
guard let pubkey = block.mentionPubkey(tags: event.tags) else { guard let pubkey = block.mentionPubkey(tags: event.tags) else {
return .loopContinue return .loopContinue
@@ -287,6 +284,7 @@ struct NoteContentView: View {
mentionPubkeys.insert(pubkey) mentionPubkeys.insert(pubkey)
return .loopContinue return .loopContinue
}) })
})
guard !mentionPubkeys.isEmpty else { return } guard !mentionPubkeys.isEmpty else { return }
@@ -297,8 +295,7 @@ struct NoteContentView: View {
} }
requestedMentionProfiles.insert(pubkey) requestedMentionProfiles.insert(pubkey)
if let txn = damus_state.ndb.lookup_profile(pubkey), if damus_state.profiles.has_fresh_profile(id: pubkey) {
damus_state.profiles.has_fresh_profile(id: pubkey, txn: txn) {
continue continue
} }
@@ -397,10 +394,8 @@ struct NoteContentView: View {
var body: some View { var body: some View {
ArtifactContent ArtifactContent
.onReceive(handle_notify(.profile_updated)) { profile in .onReceive(handle_notify(.profile_updated)) { profile in
guard let blockGroup = try? NdbBlockGroup.from(event: event, using: damus_state.ndb, and: damus_state.keypair) else { try? NdbBlockGroup.borrowBlockGroup(event: event, using: damus_state.ndb, and: damus_state.keypair, borrow: { blockGroup in
return let _: Int? = blockGroup.forEachBlock { index, block in
}
let _: Int? = try? blockGroup.forEachBlock { index, block in
switch block { switch block {
case .mention(let m): case .mention(let m):
guard let typ = m.bech32_type else { guard let typ = m.bech32_type else {
@@ -429,6 +424,7 @@ struct NoteContentView: View {
} }
return .loopContinue return .loopContinue
} }
})
} }
.onAppear { .onAppear {
load() load()
@@ -583,10 +579,8 @@ struct NoteContentView_Previews: PreviewProvider {
} }
func separate_images(ndb: Ndb, ev: NostrEvent, keypair: Keypair) -> [MediaUrl]? { func separate_images(ndb: Ndb, ev: NostrEvent, keypair: Keypair) -> [MediaUrl]? {
guard let blockGroup = try? NdbBlockGroup.from(event: ev, using: ndb, and: keypair) else { return try? NdbBlockGroup.borrowBlockGroup(event: ev, using: ndb, and: keypair, borrow: { blockGroup in
return nil let urlBlocks: [URL] = (blockGroup.reduce(initialResult: Array<URL>()) { index, urls, block in
}
let urlBlocks: [URL] = (try? blockGroup.reduce(initialResult: Array<URL>()) { index, urls, block in
switch block { switch block {
case .url(let url): case .url(let url):
guard let parsed_url = URL(string: url.as_str()) else { guard let parsed_url = URL(string: url.as_str()) else {
@@ -603,6 +597,7 @@ func separate_images(ndb: Ndb, ev: NostrEvent, keypair: Keypair) -> [MediaUrl]?
}) ?? [] }) ?? []
let mediaUrls = urlBlocks.map { MediaUrl.image($0) } let mediaUrls = urlBlocks.map { MediaUrl.image($0) }
return mediaUrls.isEmpty ? nil : mediaUrls return mediaUrls.isEmpty ? nil : mediaUrls
})
} }
extension NdbBlock { extension NdbBlock {
@@ -157,8 +157,7 @@ struct FollowPackPreviewBody: View {
.onTapGesture { .onTapGesture {
state.nav.push(route: Route.ProfileByKey(pubkey: event.event.pubkey)) state.nav.push(route: Route.ProfileByKey(pubkey: event.event.pubkey))
} }
let profile_txn = state.profiles.lookup(id: event.event.pubkey) let profile = state.profiles.lookup(id: event.event.pubkey)
let profile = profile_txn?.unsafeUnownedValue
let displayName = Profile.displayName(profile: profile, pubkey: event.event.pubkey) let displayName = Profile.displayName(profile: profile, pubkey: event.event.pubkey)
switch displayName { switch displayName {
case .one(let one): case .one(let one):
@@ -135,8 +135,7 @@ struct FollowPackView: View {
.onTapGesture { .onTapGesture {
state.nav.push(route: Route.ProfileByKey(pubkey: event.event.pubkey)) state.nav.push(route: Route.ProfileByKey(pubkey: event.event.pubkey))
} }
let profile_txn = state.profiles.lookup(id: event.event.pubkey) let profile = state.profiles.lookup(id: event.event.pubkey)
let profile = profile_txn?.unsafeUnownedValue
let displayName = Profile.displayName(profile: profile, pubkey: event.event.pubkey) let displayName = Profile.displayName(profile: profile, pubkey: event.event.pubkey)
switch displayName { switch displayName {
case .one(let one): case .one(let one):
@@ -22,11 +22,11 @@ class FollowingModel {
self.hashtags = hashtags self.hashtags = hashtags
} }
func get_filter<Y>(txn: NdbTxn<Y>) -> NostrFilter { func get_filter() -> NostrFilter {
var f = NostrFilter(kinds: [.metadata]) var f = NostrFilter(kinds: [.metadata])
f.authors = self.contacts.reduce(into: Array<Pubkey>()) { acc, pk in f.authors = self.contacts.reduce(into: Array<Pubkey>()) { acc, pk in
// don't fetch profiles we already have // don't fetch profiles we already have
if damus_state.profiles.has_fresh_profile(id: pk, txn: txn) { if damus_state.profiles.has_fresh_profile(id: pk) {
return return
} }
acc.append(pk) acc.append(pk)
@@ -34,8 +34,8 @@ class FollowingModel {
return f return f
} }
func subscribe<Y>(txn: NdbTxn<Y>) { func subscribe() {
let filter = get_filter(txn: txn) let filter = get_filter()
if (filter.authors?.count ?? 0) == 0 { if (filter.authors?.count ?? 0) == 0 {
needs_sub = false needs_sub = false
return return
@@ -151,8 +151,7 @@ struct FollowingView: View {
} }
.tabViewStyle(.page(indexDisplayMode: .never)) .tabViewStyle(.page(indexDisplayMode: .never))
.onAppear { .onAppear {
guard let txn = NdbTxn(ndb: self.damus_state.ndb) else { return } following.subscribe()
following.subscribe(txn: txn)
} }
.onDisappear { .onDisappear {
following.unsubscribe() following.unsubscribe()
@@ -100,14 +100,10 @@ struct HighlightEvent {
return NSLocalizedString("Highlighted", bundle: bundle, comment: "Label to indicate that the user is highlighting their own post.") return NSLocalizedString("Highlighted", bundle: bundle, comment: "Label to indicate that the user is highlighting their own post.")
} }
guard let profile_txn = NdbTxn(ndb: ndb) else {
return ""
}
let names: [String] = pubkeys.map { pk in let names: [String] = pubkeys.map { pk in
let prof = ndb.lookup_profile_with_txn(pk, txn: profile_txn) let profile = ndb.lookup_profile_and_copy(pk)
return Profile.displayName(profile: prof?.profile, pubkey: pk).username.truncate(maxLength: 50) return Profile.displayName(profile: profile, pubkey: pk).username.truncate(maxLength: 50)
} }
let uniqueNames: [String] = Array(Set(names)) let uniqueNames: [String] = Array(Set(names))
@@ -63,8 +63,7 @@ struct HighlightEventRef: View {
.font(.system(size: 14, weight: .bold)) .font(.system(size: 14, weight: .bold))
.lineLimit(1) .lineLimit(1)
let profile_txn = damus_state.profiles.lookup(id: longform_event.event.pubkey, txn_name: "highlight-profile") let profile = damus_state.profiles.lookup(id: longform_event.event.pubkey)
let profile = profile_txn?.unsafeUnownedValue
if let display_name = profile?.display_name { if let display_name = profile?.display_name {
Text(display_name) Text(display_name)
@@ -18,8 +18,7 @@ struct LiveStreamProfile: View {
.onTapGesture { .onTapGesture {
state.nav.push(route: Route.ProfileByKey(pubkey: pubkey)) state.nav.push(route: Route.ProfileByKey(pubkey: pubkey))
} }
let profile_txn = state.profiles.lookup(id: pubkey) let profile = state.profiles.lookup(id: pubkey)
let profile = profile_txn?.unsafeUnownedValue
let displayName = Profile.displayName(profile: profile, pubkey: pubkey) let displayName = Profile.displayName(profile: profile, pubkey: pubkey)
switch displayName { switch displayName {
case .one(let one): case .one(let one):
@@ -47,9 +47,7 @@ class NIP05DomainEventsModel: ObservableObject {
var authors = Set<Pubkey>() var authors = Set<Pubkey>()
for pubkey in state.contacts.get_friend_of_friends_list() { for pubkey in state.contacts.get_friend_of_friends_list() {
let profile_txn = state.profiles.lookup(id: pubkey) guard let profile = state.profiles.lookup(id: pubkey),
guard let profile = profile_txn?.unsafeUnownedValue,
let nip05_str = profile.nip05, let nip05_str = profile.nip05,
let nip05 = NIP05.parse(nip05_str), let nip05 = NIP05.parse(nip05_str),
nip05.host.caseInsensitiveCompare(domain) == .orderedSame else { nip05.host.caseInsensitiveCompare(domain) == .orderedSame else {
@@ -82,7 +82,12 @@ struct NIP05DomainTimelineHeaderView: View {
func friendsOfFriendsString(_ friendsOfFriends: [Pubkey], ndb: Ndb, locale: Locale = Locale.current) -> String { func friendsOfFriendsString(_ friendsOfFriends: [Pubkey], ndb: Ndb, locale: Locale = Locale.current) -> String {
let bundle = bundleForLocale(locale: locale) let bundle = bundleForLocale(locale: locale)
let names: [String] = friendsOfFriends.prefix(3).map { pk in let names: [String] = friendsOfFriends.prefix(3).map { pk in
let profile = ndb.lookup_profile(pk)?.unsafeUnownedValue?.profile let profile = ndb.lookup_profile(pk, borrow: { pr in
switch pr {
case .some(let pr): return pr.profile
case .none: return nil
}
})
return Profile.displayName(profile: profile, pubkey: pk).username.truncate(maxLength: 20) return Profile.displayName(profile: profile, pubkey: pk).username.truncate(maxLength: 20)
} }
@@ -65,16 +65,8 @@ func should_display_notification(state: HeadlessDamusState, event ev: NostrEvent
return true return true
} }
func generate_local_notification_object(ndb: Ndb, from ev: NostrEvent, state: HeadlessDamusState) -> LocalNotification? { func generate_text_mention_notification(ndb: Ndb, from ev: NostrEvent, state: HeadlessDamusState, blockGroup: borrowing NdbBlockGroup) -> LocalNotification? {
guard let type = ev.known_kind else { let notification: LocalNotification? = blockGroup.forEachBlock({ index, block in
return nil
}
if type == .text,
state.settings.mention_notification,
let blockGroup = try? NdbBlockGroup.from(event: ev, using: ndb, and: state.keypair)
{
let notification: LocalNotification? = try? blockGroup.forEachBlock({ index, block in
switch block { switch block {
case .mention(let mention): case .mention(let mention):
guard case .npub = mention.bech32_type, guard case .npub = mention.bech32_type,
@@ -92,7 +84,12 @@ func generate_local_notification_object(ndb: Ndb, from ev: NostrEvent, state: He
} }
if ev.referenced_ids.contains(where: { note_id in if ev.referenced_ids.contains(where: { note_id in
guard let note_author: Pubkey = state.ndb.lookup_note(note_id)?.unsafeUnownedValue?.pubkey else { return false } guard let note_author: Pubkey = state.ndb.lookup_note(note_id, borrow: { note in
switch note {
case .some(let note): return note.pubkey
case .none: return nil
}
}) else { return false }
guard note_author == state.keypair.pubkey else { return false } guard note_author == state.keypair.pubkey else { return false }
return true return true
}) { }) {
@@ -107,21 +104,38 @@ func generate_local_notification_object(ndb: Ndb, from ev: NostrEvent, state: He
return LocalNotification(type: .tagged, event: ev, target: .note(ev), content: content_preview) return LocalNotification(type: .tagged, event: ev, target: .note(ev), content: content_preview)
} }
return nil
}
func generate_local_notification_object(ndb: Ndb, from ev: NostrEvent, state: HeadlessDamusState) -> LocalNotification? {
guard let type = ev.known_kind else {
return nil
}
if type == .text,
state.settings.mention_notification
{
return try? NdbBlockGroup.borrowBlockGroup(event: ev, using: ndb, and: state.keypair, borrow: { blockGroup in
return generate_text_mention_notification(ndb: ndb, from: ev, state: state, blockGroup: blockGroup)
})
} else if type == .boost, } else if type == .boost,
state.settings.repost_notification, state.settings.repost_notification,
let inner_ev = ev.get_inner_event() let inner_ev = ev.get_inner_event()
{ {
let content_preview = render_notification_content_preview(ndb: ndb, ev: inner_ev, profiles: state.profiles, keypair: state.keypair) let content_preview = render_notification_content_preview(ndb: ndb, ev: inner_ev, profiles: state.profiles, keypair: state.keypair)
return LocalNotification(type: .repost, event: ev, target: .note(inner_ev), content: content_preview) return LocalNotification(type: .repost, event: ev, target: .note(inner_ev), content: content_preview)
} else if type == .like, state.settings.like_notification, let evid = ev.referenced_ids.last { } else if type == .like, state.settings.like_notification, let evid = ev.referenced_ids.last {
if let txn = state.ndb.lookup_note(evid, txn_name: "local_notification_like"), return state.ndb.lookup_note(evid, borrow: { liked_event in
let liked_event = txn.unsafeUnownedValue switch liked_event {
{ case .none:
let content_preview = render_notification_content_preview(ndb: ndb, ev: liked_event, profiles: state.profiles, keypair: state.keypair)
return LocalNotification(type: .like, event: ev, target: .note(liked_event), content: content_preview)
} else {
return LocalNotification(type: .like, event: ev, target: .note_id(evid), content: "") return LocalNotification(type: .like, event: ev, target: .note_id(evid), content: "")
case .some(let liked_event):
let owned_liked_event = liked_event.toOwned()
let content_preview = render_notification_content_preview(ndb: ndb, ev: owned_liked_event, profiles: state.profiles, keypair: state.keypair)
return LocalNotification(type: .like, event: ev, target: .note(owned_liked_event), content: content_preview)
} }
})
} }
else if type == .dm, else if type == .dm,
state.settings.dm_notification { state.settings.dm_notification {
@@ -177,8 +191,7 @@ func render_notification_content_preview(ndb: Ndb, ev: NostrEvent, profiles: Pro
} }
func event_author_name(profiles: Profiles, pubkey: Pubkey) -> String { func event_author_name(profiles: Profiles, pubkey: Pubkey) -> String {
let profile_txn = profiles.lookup(id: pubkey) let profile = profiles.lookup(id: pubkey)
let profile = profile_txn?.unsafeUnownedValue
return Profile.displayName(profile: profile, pubkey: pubkey).username.truncate(maxLength: 50) return Profile.displayName(profile: profile, pubkey: pubkey).username.truncate(maxLength: 50)
} }
@@ -215,8 +228,12 @@ func process_zap_event(state: HeadlessDamusState, ev: NostrEvent, completion: @e
return return
} }
guard let txn = state.profiles.lookup_with_timestamp(ptag), guard let lnurl = state.profiles.lookup_with_timestamp(ptag, borrow: { record -> String? in
let lnurl = txn.map({ pr in pr?.lnurl }).value else { switch record {
case .none: return nil
case .some(let record): return record.lnurl
}
}) else {
completion(.failed) completion(.failed)
return return
} }
@@ -263,18 +280,19 @@ func get_zap_target_pubkey(ev: NostrEvent, ndb: Ndb) -> Pubkey? {
} }
// we can't trust the p tag on note zaps because they can be faked // we can't trust the p tag on note zaps because they can be faked
guard let txn = ndb.lookup_note(etag), return ndb.lookup_note(etag, borrow: { note in
let pk = txn.unsafeUnownedValue?.pubkey else { switch note {
case .none:
// We don't have the event in cache so we can't check the pubkey. // We don't have the event in cache so we can't check the pubkey.
// We could return this as an invalid zap but that wouldn't be correct // We could return this as an invalid zap but that wouldn't be correct
// all of the time, and may reject valid zaps. What we need is a new // all of the time, and may reject valid zaps. What we need is a new
// unvalidated zap state, but for now we simply leak a bit of correctness... // unvalidated zap state, but for now we simply leak a bit of correctness...
return ev.referenced_pubkeys.just_one() return ev.referenced_pubkeys.just_one()
case .some(let note):
return note.pubkey
} }
})
return pk
} }
fileprivate func process_zap_event_with_zapper(state: HeadlessDamusState, ev: NostrEvent, zapper: Pubkey) -> Zap? { fileprivate func process_zap_event_with_zapper(state: HeadlessDamusState, ev: NostrEvent, zapper: Pubkey) -> Zap? {
@@ -79,8 +79,7 @@ class SuggestedUsersViewModel: ObservableObject {
/// Gets suggested user information from a provided pubkey /// Gets suggested user information from a provided pubkey
func suggestedUser(pubkey: Pubkey) -> SuggestedUser? { func suggestedUser(pubkey: Pubkey) -> SuggestedUser? {
let profile_txn = damus_state.profiles.lookup(id: pubkey) if let profile = 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) { let user = SuggestedUser(name: profile.name, about: profile.about, picture: profile.picture, pubkey: pubkey) {
return user return user
} }
@@ -116,7 +116,7 @@ class DraftArtifacts: Equatable {
case .mention(let mention): case .mention(let mention):
if let pubkey = mention.ref.nip19.pubkey() { if let pubkey = mention.ref.nip19.pubkey() {
// A profile reference, format things properly. // A profile reference, format things properly.
let profile = damus_state.ndb.lookup_profile(pubkey)?.unsafeUnownedValue?.profile let profile = damus_state.profiles.lookup(id: pubkey)
let profile_name = DisplayName(profile: profile, pubkey: pubkey).username let profile_name = DisplayName(profile: profile, pubkey: pubkey).username
guard let url_address = URL(string: block.asString) else { guard let url_address = URL(string: block.asString) else {
rich_text_content.append(.init(string: block.asString)) rich_text_content.append(.init(string: block.asString))
@@ -173,8 +173,10 @@ class Drafts: ObservableObject {
func load(from damus_state: DamusState) { func load(from damus_state: DamusState) {
guard let note_ids = damus_state.settings.draft_event_ids?.compactMap({ NoteId(hex: $0) }) else { return } guard let note_ids = damus_state.settings.draft_event_ids?.compactMap({ NoteId(hex: $0) }) else { return }
for note_id in note_ids { for note_id in note_ids {
let txn = damus_state.ndb.lookup_note(note_id) let note = damus_state.ndb.lookup_note(note_id, borrow: { event in
guard let note = txn?.unsafeUnownedValue else { continue } return event?.toOwned()
})
guard let note else { continue }
// Implementation note: This currently fails silently, because: // Implementation note: This currently fails silently, because:
// 1. Errors are unlikely and not expected // 1. Errors are unlikely and not expected
// 2. It is not mission critical to recover from this error // 2. It is not mission critical to recover from this error
@@ -232,13 +234,13 @@ class Drafts: ObservableObject {
draft_events.append(wrapped_note) draft_events.append(wrapped_note)
} }
for (replied_to_note_id, reply_artifacts) in self.replies { for (replied_to_note_id, reply_artifacts) in self.replies {
guard let replied_to_note = damus_state.ndb.lookup_note(replied_to_note_id)?.unsafeUnownedValue?.to_owned() else { continue } guard let replied_to_note = damus_state.ndb.lookup_note_and_copy(replied_to_note_id) else { continue }
let nip37_draft = try? await reply_artifacts.to_nip37_draft(action: .replying_to(replied_to_note), damus_state: damus_state) let nip37_draft = try? await reply_artifacts.to_nip37_draft(action: .replying_to(replied_to_note), damus_state: damus_state)
guard let wrapped_note = nip37_draft?.wrapped_note else { continue } guard let wrapped_note = nip37_draft?.wrapped_note else { continue }
draft_events.append(wrapped_note) draft_events.append(wrapped_note)
} }
for (quoted_note_id, quote_note_artifacts) in self.quotes { for (quoted_note_id, quote_note_artifacts) in self.quotes {
guard let quoted_note = damus_state.ndb.lookup_note(quoted_note_id)?.unsafeUnownedValue?.to_owned() else { continue } guard let quoted_note = damus_state.ndb.lookup_note_and_copy(quoted_note_id) else { continue }
let nip37_draft = try? await quote_note_artifacts.to_nip37_draft(action: .quoting(quoted_note), damus_state: damus_state) let nip37_draft = try? await quote_note_artifacts.to_nip37_draft(action: .quoting(quoted_note), damus_state: damus_state)
guard let wrapped_note = nip37_draft?.wrapped_note else { continue } guard let wrapped_note = nip37_draft?.wrapped_note else { continue }
draft_events.append(wrapped_note) draft_events.append(wrapped_note)
+1 -2
View File
@@ -212,8 +212,7 @@ struct PostView: View {
return .init(string: "") return .init(string: "")
} }
let profile_txn = damus_state.profiles.lookup(id: pubkey) let profile = damus_state.profiles.lookup(id: pubkey)
let profile = profile_txn?.unsafeUnownedValue
return user_tag_attr_string(profile: profile, pubkey: pubkey) return user_tag_attr_string(profile: profile, pubkey: pubkey)
} }
+1 -1
View File
@@ -27,7 +27,7 @@ struct ReplyView: View {
let names = references let names = references
.map { pubkey in .map { pubkey in
let pk = pubkey let pk = pubkey
let prof = damus.ndb.lookup_profile(pk)?.unsafeUnownedValue?.profile let prof = damus.profiles.lookup(id: pk)
return "@" + Profile.displayName(profile: prof, pubkey: pk).username.truncate(maxLength: 50) return "@" + Profile.displayName(profile: prof, pubkey: pk).username.truncate(maxLength: 50)
} }
.joined(separator: " ") .joined(separator: " ")
@@ -17,13 +17,11 @@ struct UserSearch: View {
@EnvironmentObject var tagModel: TagModel @EnvironmentObject var tagModel: TagModel
var users: [Pubkey] { var users: [Pubkey] {
guard let txn = NdbTxn(ndb: damus_state.ndb) else { return [] } return search_profiles(profiles: damus_state.profiles, contacts: damus_state.contacts, search: search)
return search_profiles(profiles: damus_state.profiles, contacts: damus_state.contacts, search: search, txn: txn)
} }
func on_user_tapped(pk: Pubkey) { func on_user_tapped(pk: Pubkey) {
let profile_txn = damus_state.profiles.lookup(id: pk) let profile = damus_state.profiles.lookup(id: pk)
let profile = profile_txn?.unsafeUnownedValue
let user_tag = user_tag_attr_string(profile: profile, pubkey: pk) let user_tag = user_tag_attr_string(profile: profile, pubkey: pk)
appendUserTag(withTag: user_tag) appendUserTag(withTag: user_tag)
@@ -33,8 +33,7 @@ struct EditMetadataView: View {
init(damus_state: DamusState) { init(damus_state: DamusState) {
self.damus_state = damus_state self.damus_state = damus_state
let profile_txn = damus_state.profiles.lookup(id: damus_state.pubkey) let data = damus_state.profiles.lookup(id: damus_state.pubkey)
let data = profile_txn?.unsafeUnownedValue
_name = State(initialValue: data?.name ?? "") _name = State(initialValue: data?.name ?? "")
_display_name = State(initialValue: data?.display_name ?? "") _display_name = State(initialValue: data?.display_name ?? "")
@@ -259,8 +258,7 @@ struct EditMetadataView: View {
} }
func didChange() -> Bool { func didChange() -> Bool {
let profile_txn = damus_state.profiles.lookup(id: damus_state.pubkey) let data = damus_state.profiles.lookup(id: damus_state.pubkey)
let data = profile_txn?.unsafeUnownedValue
if data?.name ?? "" != name { if data?.name ?? "" != name {
return true return true
@@ -25,7 +25,7 @@ struct EventProfileName: View {
self.damus_state = damus self.damus_state = damus
self.pubkey = pubkey self.pubkey = pubkey
self.size = size self.size = size
let donation = damus.ndb.lookup_profile(pubkey)?.map({ p in p?.profile?.damus_donation }).value let donation = damus.profiles.lookup(id: pubkey)?.damus_donation
self._donation = State(wrappedValue: donation) self._donation = State(wrappedValue: donation)
self.purple_account = nil self.purple_account = nil
self._profileObserver = StateObject.init(wrappedValue: ProfileObserver(pubkey: pubkey, damusState: damus)) self._profileObserver = StateObject.init(wrappedValue: ProfileObserver(pubkey: pubkey, damusState: damus))
@@ -61,8 +61,7 @@ struct EventProfileName: View {
} }
var body: some View { var body: some View {
let profile_txn = damus_state.profiles.lookup(id: pubkey) let profile = damus_state.profiles.lookup(id: pubkey)
let profile = profile_txn?.unsafeUnownedValue
HStack(spacing: 2) { HStack(spacing: 2) {
switch current_display_name(profile) { switch current_display_name(profile) {
case .one(let one): case .one(let one):
@@ -108,8 +107,7 @@ struct EventProfileName: View {
return return
} }
let profile_txn = damus_state.profiles.lookup(id: update.pubkey) guard let profile = damus_state.profiles.lookup(id: update.pubkey) else { return }
guard let profile = profile_txn?.unsafeUnownedValue else { return }
let display_name = Profile.displayName(profile: profile, pubkey: pubkey) let display_name = Profile.displayName(profile: profile, pubkey: pubkey)
if display_name != self.display_name { if display_name != self.display_name {
@@ -33,13 +33,16 @@ struct ProfileActionSheetView: View {
colorScheme == .light ? DamusColors.white : DamusColors.black colorScheme == .light ? DamusColors.white : DamusColors.black
} }
func profile_data() -> ProfileRecord? { func profile_data<T>(borrow lendingFunction: (_: borrowing ProfileRecord?) throws -> T) rethrows -> T {
let profile_txn = damus_state.profiles.lookup_with_timestamp(profile.pubkey) return try damus_state.profiles.lookup_with_timestamp(profile.pubkey, borrow: lendingFunction)
return profile_txn?.unsafeUnownedValue
} }
func get_profile() -> Profile? { func get_profile() -> Profile? {
return self.profile_data()?.profile return damus_state.profiles.lookup(id: profile.pubkey)
}
func get_lnurl() -> String? {
return damus_state.profiles.lookup_lnurl(profile.pubkey)
} }
func navigate(route: Route) { func navigate(route: Route) {
@@ -115,7 +118,7 @@ struct ProfileActionSheetView: View {
} }
var zapButton: some View { var zapButton: some View {
if let lnurl = self.profile_data()?.lnurl, lnurl != "" { if let lnurl = self.get_lnurl(), lnurl != "" {
return AnyView(ProfileActionSheetZapButton(damus_state: damus_state, profile: profile, lnurl: lnurl)) return AnyView(ProfileActionSheetZapButton(damus_state: damus_state, profile: profile, lnurl: lnurl))
} }
else { else {
@@ -134,7 +137,7 @@ struct ProfileActionSheetView: View {
var body: some View { var body: some View {
VStack(alignment: .center) { VStack(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, damusState: damus_state) ProfilePicView(pubkey: profile.pubkey, size: pfp_size, highlight: .custom(imageBorderColor(), 4.0), profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation, damusState: damus_state)
if let url = self.profile_data()?.profile?.website_url { if let url = self.get_profile()?.website_url {
WebsiteLink(url: url, style: .accent) WebsiteLink(url: url, style: .accent)
.padding(.top, -15) .padding(.top, -15)
} }
@@ -143,7 +146,7 @@ struct ProfileActionSheetView: View {
PubkeyView(pubkey: profile.pubkey) PubkeyView(pubkey: profile.pubkey)
if let about = self.profile_data()?.profile?.about { if let about = self.get_profile()?.about {
AboutView(state: damus_state, about: about, max_about_length: 140, text_alignment: .center) AboutView(state: damus_state, about: about, max_about_length: 140, text_alignment: .center)
.padding(.top) .padding(.top)
} }
@@ -96,8 +96,7 @@ struct ProfileName: View {
} }
var body: some View { var body: some View {
let profile_txn = damus_state.profiles.lookup(id: pubkey) let profile = damus_state.profiles.lookup(id: pubkey)
let profile = profile_txn?.unsafeUnownedValue
HStack(spacing: 2) { HStack(spacing: 2) {
Text(verbatim: "\(prefix)\(name_choice(profile: profile))") Text(verbatim: "\(prefix)\(name_choice(profile: profile))")
@@ -139,8 +138,7 @@ struct ProfileName: View {
switch update { switch update {
case .remote(let pubkey): case .remote(let pubkey):
guard let profile_txn = damus_state.profiles.lookup(id: pubkey), guard let prof = damus_state.profiles.lookup(id: pubkey) else {
let prof = profile_txn.unsafeUnownedValue else {
return return
} }
handle_profile_update(profile: prof) handle_profile_update(profile: prof)
@@ -16,8 +16,7 @@ struct ProfileNameView: View {
var body: some View { var body: some View {
Group { Group {
VStack(alignment: .leading) { VStack(alignment: .leading) {
let profile_txn = self.damus.profiles.lookup(id: pubkey) let profile = self.damus.profiles.lookup(id: pubkey)
let profile = profile_txn?.unsafeUnownedValue
switch Profile.displayName(profile: profile, pubkey: pubkey) { switch Profile.displayName(profile: profile, pubkey: pubkey) {
case .one: case .one:
@@ -99,7 +99,12 @@ struct ProfilePicView: View {
} }
func get_lnurl() -> String? { func get_lnurl() -> String? {
return profiles.lookup_with_timestamp(pubkey)?.unsafeUnownedValue?.lnurl return profiles.lookup_with_timestamp(pubkey, borrow: { pr in
switch pr {
case .some(let pr): return pr.lnurl
case .none: return nil
}
})
} }
var body: some View { var body: some View {
@@ -116,8 +121,7 @@ struct ProfilePicView: View {
self.picture = pic self.picture = pic
} }
case .remote(pubkey: let pk): case .remote(pubkey: let pk):
let profile_txn = profiles.lookup(id: pk) let profile = profiles.lookup(id: pk)
let profile = profile_txn?.unsafeUnownedValue
if let pic = profile?.picture { if let pic = profile?.picture {
self.picture = pic self.picture = pic
} }
@@ -141,7 +145,7 @@ struct ProfilePicView: View {
} }
func get_profile_url(picture: String?, pubkey: Pubkey, profiles: Profiles) -> URL { func get_profile_url(picture: String?, pubkey: Pubkey, profiles: Profiles) -> URL {
let pic = picture ?? profiles.lookup(id: pubkey, txn_name: "get_profile_url")?.map({ $0?.picture }).value ?? robohash(pubkey) let pic = picture ?? profiles.lookup(id: pubkey)?.picture ?? robohash(pubkey)
if let url = URL(string: pic) { if let url = URL(string: pic) {
return url return url
} }
@@ -52,7 +52,7 @@ struct EditProfilePictureView: View {
if let profile_url { if let profile_url {
return profile_url return profile_url
} else if let state = damus_state, } else if let state = damus_state,
let picture = state.profiles.lookup(id: pubkey)?.map({ pr in pr?.picture }).value { let picture = state.profiles.lookup(id: pubkey)?.picture {
return URL(string: picture) return URL(string: picture)
} else { } else {
return profile_url ?? URL(string: robohash(pubkey)) return profile_url ?? URL(string: robohash(pubkey))
+17 -20
View File
@@ -27,7 +27,7 @@ func follow_btn_txt(_ fs: FollowState, follows_you: Bool) -> String {
func followedByString(_ friend_intersection: [Pubkey], ndb: Ndb, locale: Locale = Locale.current) -> String { func followedByString(_ friend_intersection: [Pubkey], ndb: Ndb, locale: Locale = Locale.current) -> String {
let bundle = bundleForLocale(locale: locale) let bundle = bundleForLocale(locale: locale)
let names: [String] = friend_intersection.prefix(3).map { pk in let names: [String] = friend_intersection.prefix(3).map { pk in
let profile = ndb.lookup_profile(pk)?.unsafeUnownedValue?.profile let profile = ndb.lookup_profile_and_copy(pk)
return Profile.displayName(profile: profile, pubkey: pk).username.truncate(maxLength: 20) return Profile.displayName(profile: profile, pubkey: pk).username.truncate(maxLength: 20)
} }
@@ -108,8 +108,7 @@ struct ProfileView: View {
} }
func getProfileInfo() -> (String, String) { func getProfileInfo() -> (String, String) {
let profile_txn = self.damus_state.profiles.lookup(id: profile.pubkey) let ndbprofile = self.damus_state.profiles.lookup(id: profile.pubkey)
let ndbprofile = profile_txn?.unsafeUnownedValue
let displayName = Profile.displayName(profile: ndbprofile, pubkey: profile.pubkey).displayName.truncate(maxLength: 25) let displayName = Profile.displayName(profile: ndbprofile, pubkey: profile.pubkey).displayName.truncate(maxLength: 25)
let userName = Profile.displayName(profile: ndbprofile, pubkey: profile.pubkey).username.truncate(maxLength: 25) let userName = Profile.displayName(profile: ndbprofile, pubkey: profile.pubkey).username.truncate(maxLength: 25)
return (displayName, "@\(userName)") return (displayName, "@\(userName)")
@@ -241,8 +240,8 @@ struct ProfileView: View {
} }
} }
func lnButton(unownedProfile: Profile?, record: ProfileRecord?) -> some View { func lnButton(profile: Profile?, lnurl: String?) -> some View {
return ProfileZapLinkView(unownedProfileRecord: record, profileModel: self.profile) { reactions_enabled, lud16, lnurl in return ProfileZapLinkView(profile: profile, lnurl: lnurl, profileModel: self.profile) { reactions_enabled, lud16, lnurl in
Image(reactions_enabled ? "zap.fill" : "zap") Image(reactions_enabled ? "zap.fill" : "zap")
.foregroundColor(reactions_enabled ? .orange : Color.primary) .foregroundColor(reactions_enabled ? .orange : Color.primary)
.profile_button_style(scheme: colorScheme) .profile_button_style(scheme: colorScheme)
@@ -270,17 +269,16 @@ struct ProfileView: View {
.font(.footnote) .font(.footnote)
} }
func actionSection(record: ProfileRecord?, pubkey: Pubkey) -> some View { func actionSection(ndbprofile: Profile?, lnurl: String?, pubkey: Pubkey) -> some View {
return Group { return Group {
if damus_state.settings.enable_favourites_feature { if damus_state.settings.enable_favourites_feature {
FavoriteButtonView(pubkey: profile.pubkey, damus_state: damus_state) FavoriteButtonView(pubkey: profile.pubkey, damus_state: damus_state)
} }
if let record, if let profile = ndbprofile,
let profile = record.profile, let lnurl,
let lnurl = record.lnurl,
lnurl != "" lnurl != ""
{ {
lnButton(unownedProfile: profile, record: record) lnButton(profile: ndbprofile, lnurl: lnurl)
} }
dmButton dmButton
@@ -312,7 +310,7 @@ struct ProfileView: View {
return scale < 1 ? scale : 1 return scale < 1 ? scale : 1
} }
func nameSection(profile_data: ProfileRecord?) -> some View { func nameSection(ndbprofile: Profile?, lnurl: String?) -> some View {
return Group { return Group {
let follows_you = profile.pubkey != damus_state.pubkey && profile.follows(pubkey: damus_state.pubkey) let follows_you = profile.pubkey != damus_state.pubkey && profile.follows(pubkey: damus_state.pubkey)
@@ -334,7 +332,7 @@ struct ProfileView: View {
followsYouBadge followsYouBadge
} }
actionSection(record: profile_data, pubkey: profile.pubkey) actionSection(ndbprofile: ndbprofile, lnurl: lnurl, pubkey: profile.pubkey)
} }
ProfileNameView(pubkey: profile.pubkey, damus: damus_state) ProfileNameView(pubkey: profile.pubkey, damus: damus_state)
@@ -360,16 +358,16 @@ struct ProfileView: View {
var aboutSection: some View { var aboutSection: some View {
VStack(alignment: .leading, spacing: 8.0) { VStack(alignment: .leading, spacing: 8.0) {
let profile_txn = damus_state.profiles.lookup_with_timestamp(profile.pubkey) let lnurl = damus_state.profiles.lookup_lnurl(profile.pubkey)
let profile_data = profile_txn?.unsafeUnownedValue let ndbprofile = damus_state.profiles.lookup(id: profile.pubkey)
nameSection(profile_data: profile_data) nameSection(ndbprofile: ndbprofile, lnurl: lnurl)
if let about = profile_data?.profile?.about { if let about = ndbprofile?.about {
AboutView(state: damus_state, about: about) AboutView(state: damus_state, about: about)
} }
if let url = profile_data?.profile?.website_url { if let url = ndbprofile?.website_url {
WebsiteLink(url: url) WebsiteLink(url: url)
} }
@@ -571,10 +569,9 @@ extension View {
@MainActor @MainActor
func check_nip05_validity(pubkey: Pubkey, profiles: Profiles) { func check_nip05_validity(pubkey: Pubkey, profiles: Profiles) {
let profile_txn = profiles.lookup(id: pubkey) let profile = profiles.lookup(id: pubkey)
guard let profile = profile_txn?.unsafeUnownedValue, guard let nip05 = profile?.nip05,
let nip05 = profile.nip05,
profiles.is_validated(pubkey) == nil profiles.is_validated(pubkey) == nil
else { else {
return return
@@ -121,8 +121,7 @@ struct DamusPurpleAccountView: View {
} }
func profile_display_name() -> String { func profile_display_name() -> String {
let profile_txn: NdbTxn<ProfileRecord?>? = damus_state.profiles.lookup_with_timestamp(account.pubkey) let profile = damus_state.profiles.lookup(id: account.pubkey)
let profile: NdbProfile? = profile_txn?.unsafeUnownedValue?.profile
let display_name = DisplayName(profile: profile, pubkey: account.pubkey).displayName let display_name = DisplayName(profile: profile, pubkey: account.pubkey).displayName
return display_name return display_name
} }
@@ -110,29 +110,29 @@ class SearchHomeModel: ObservableObject {
} }
} }
func find_profiles_to_fetch<Y>(profiles: Profiles, load: PubkeysToLoad, cache: EventCache, txn: NdbTxn<Y>) -> [Pubkey] { func find_profiles_to_fetch(profiles: Profiles, load: PubkeysToLoad, cache: EventCache) -> [Pubkey] {
switch load { switch load {
case .from_events(let events): case .from_events(let events):
return find_profiles_to_fetch_from_events(profiles: profiles, events: events, cache: cache, txn: txn) return find_profiles_to_fetch_from_events(profiles: profiles, events: events, cache: cache)
case .from_keys(let pks): case .from_keys(let pks):
return find_profiles_to_fetch_from_keys(profiles: profiles, pks: pks, txn: txn) return find_profiles_to_fetch_from_keys(profiles: profiles, pks: pks)
} }
} }
func find_profiles_to_fetch_from_keys<Y>(profiles: Profiles, pks: [Pubkey], txn: NdbTxn<Y>) -> [Pubkey] { func find_profiles_to_fetch_from_keys(profiles: Profiles, pks: [Pubkey]) -> [Pubkey] {
Array(Set(pks.filter { pk in !profiles.has_fresh_profile(id: pk, txn: txn) })) Array(Set(pks.filter { pk in !profiles.has_fresh_profile(id: pk) }))
} }
func find_profiles_to_fetch_from_events<Y>(profiles: Profiles, events: [NostrEvent], cache: EventCache, txn: NdbTxn<Y>) -> [Pubkey] { func find_profiles_to_fetch_from_events(profiles: Profiles, events: [NostrEvent], cache: EventCache) -> [Pubkey] {
var pubkeys = Set<Pubkey>() var pubkeys = Set<Pubkey>()
for ev in events { for ev in events {
// lookup profiles from boosted events // lookup profiles from boosted events
if ev.known_kind == .boost, let bev = ev.get_inner_event(cache: cache), !profiles.has_fresh_profile(id: bev.pubkey, txn: txn) { if ev.known_kind == .boost, let bev = ev.get_inner_event(cache: cache), !profiles.has_fresh_profile(id: bev.pubkey) {
pubkeys.insert(bev.pubkey) pubkeys.insert(bev.pubkey)
} }
if !profiles.has_fresh_profile(id: ev.pubkey, txn: txn) { if !profiles.has_fresh_profile(id: ev.pubkey) {
pubkeys.insert(ev.pubkey) pubkeys.insert(ev.pubkey)
} }
} }
@@ -31,18 +31,19 @@ struct PullDownSearchView: View {
} }
do { do {
guard let txn = NdbTxn(ndb: state.ndb) else { return }
for note_key in note_keys { for note_key in note_keys {
guard let note = state.ndb.lookup_note_by_key_with_txn(note_key, txn: txn) else { state.ndb.lookup_note_by_key(note_key, borrow: { maybeUnownedNote in
continue switch maybeUnownedNote {
} case .none: return // Skip this
case .some(let unownedNote):
if !keyset.contains(note_key) { if !keyset.contains(note_key) {
let owned_note = note.to_owned() let owned_note = unownedNote.toOwned()
res.append(owned_note) res.append(owned_note)
keyset.insert(note_key) keyset.insert(note_key)
} }
} }
})
}
} }
let res_ = res let res_ = res
@@ -154,18 +154,19 @@ struct SearchResultsView: View {
} }
do { do {
guard let txn = NdbTxn(ndb: damus_state.ndb) else { return }
for note_key in note_keys { for note_key in note_keys {
guard let note = damus_state.ndb.lookup_note_by_key_with_txn(note_key, txn: txn) else { damus_state.ndb.lookup_note_by_key(note_key, borrow: { maybeUnownedNote in
continue switch maybeUnownedNote {
} case .none: return
case .some(let unownedNote):
if !keyset.contains(note_key) { if !keyset.contains(note_key) {
let owned_note = note.to_owned() let owned_note = unownedNote.toOwned()
res.append(owned_note) res.append(owned_note)
keyset.insert(note_key) keyset.insert(note_key)
} }
} }
})
}
} }
let res_ = res let res_ = res
@@ -182,12 +183,10 @@ struct SearchResultsView: View {
} }
.frame(maxHeight: .infinity) .frame(maxHeight: .infinity)
.onAppear { .onAppear {
guard let txn = NdbTxn.init(ndb: damus_state.ndb) else { return } self.result = search_for_string(profiles: damus_state.profiles, contacts: damus_state.contacts, search: search)
self.result = search_for_string(profiles: damus_state.profiles, contacts: damus_state.contacts, search: search, txn: txn)
} }
.onChange(of: search) { new in .onChange(of: search) { new in
guard let txn = NdbTxn.init(ndb: damus_state.ndb) else { return } self.result = search_for_string(profiles: damus_state.profiles, contacts: damus_state.contacts, search: search)
self.result = search_for_string(profiles: damus_state.profiles, contacts: damus_state.contacts, search: search, txn: txn)
} }
.onChange(of: search) { query in .onChange(of: search) { query in
debouncer.debounce { debouncer.debounce {
@@ -208,7 +207,7 @@ struct SearchResultsView_Previews: PreviewProvider {
*/ */
func search_for_string<Y>(profiles: Profiles, contacts: Contacts, search new: String, txn: NdbTxn<Y>) -> Search? { func search_for_string(profiles: Profiles, contacts: Contacts, search new: String) -> Search? {
guard new.count != 0 else { guard new.count != 0 else {
return nil return nil
} }
@@ -251,7 +250,7 @@ func search_for_string<Y>(profiles: Profiles, contacts: Contacts, search new: St
return .naddr(naddr) return .naddr(naddr)
} }
let multisearch = MultiSearch(text: new, hashtag: make_hashtagable(searchQuery), profiles: search_profiles(profiles: profiles, contacts: contacts, search: new, txn: txn)) let multisearch = MultiSearch(text: new, hashtag: make_hashtagable(searchQuery), profiles: search_profiles(profiles: profiles, contacts: contacts, search: new))
return .multi(multisearch) return .multi(multisearch)
} }
@@ -268,7 +267,7 @@ func make_hashtagable(_ str: String) -> String {
return String(new.filter{$0 != " "}) return String(new.filter{$0 != " "})
} }
func search_profiles<Y>(profiles: Profiles, contacts: Contacts, search: String, txn: NdbTxn<Y>) -> [Pubkey] { func search_profiles(profiles: Profiles, contacts: Contacts, search: String) -> [Pubkey] {
// Search by hex pubkey. // Search by hex pubkey.
if let pubkey = hex_decode_pubkey(search), if let pubkey = hex_decode_pubkey(search),
profiles.lookup_key_by_pubkey(pubkey) != nil profiles.lookup_key_by_pubkey(pubkey) != nil
@@ -285,7 +284,7 @@ func search_profiles<Y>(profiles: Profiles, contacts: Contacts, search: String,
return [pk] return [pk]
} }
return profiles.search(search, limit: 128, txn: txn).sorted { a, b in return profiles.search(search, limit: 128).sorted { a, b in
let aFriendTypePriority = get_friend_type(contacts: contacts, pubkey: a)?.priority ?? 0 let aFriendTypePriority = get_friend_type(contacts: contacts, pubkey: a)?.priority ?? 0
let bFriendTypePriority = get_friend_type(contacts: contacts, pubkey: b)?.priority ?? 0 let bFriendTypePriority = get_friend_type(contacts: contacts, pubkey: b)?.priority ?? 0
@@ -120,7 +120,7 @@ class HomeModel: ContactsDelegate, ObservableObject {
damus_state.contacts.delegate = self damus_state.contacts.delegate = self
guard let latest_contact_event_id_hex = damus_state.settings.latest_contact_event_id_hex else { return } guard let latest_contact_event_id_hex = damus_state.settings.latest_contact_event_id_hex else { return }
guard let latest_contact_event_id = NoteId(hex: latest_contact_event_id_hex) else { return } guard let latest_contact_event_id = NoteId(hex: latest_contact_event_id_hex) else { return }
guard let latest_contact_event: NdbNote = damus_state.ndb.lookup_note( latest_contact_event_id)?.unsafeUnownedValue?.to_owned() else { return } guard let latest_contact_event: NdbNote = damus_state.ndb.lookup_note_and_copy(latest_contact_event_id) else { return }
process_contact_event(state: damus_state, ev: latest_contact_event) process_contact_event(state: damus_state, ev: latest_contact_event)
} }
@@ -136,8 +136,7 @@ struct SideMenuView: View {
var display_name: String? = nil var display_name: String? = nil
do { do {
let profile_txn = damus_state.ndb.lookup_profile(damus_state.pubkey, txn_name: "top_profile") let profile = damus_state.profiles.lookup(id: damus_state.pubkey)
let profile = profile_txn?.unsafeUnownedValue?.profile
name = profile?.name name = profile?.name
display_name = profile?.display_name display_name = profile?.display_name
} }
@@ -244,8 +244,7 @@ struct NWCSettings: View {
} }
} }
.onChange(of: model.settings.donation_percent) { p in .onChange(of: model.settings.donation_percent) { p in
let profile_txn = damus_state.profiles.lookup(id: damus_state.pubkey) guard let profile = damus_state.profiles.lookup(id: damus_state.pubkey) else {
guard let profile = profile_txn?.unsafeUnownedValue else {
return return
} }
@@ -254,10 +253,9 @@ struct NWCSettings: View {
notify(.profile_updated(.manual(pubkey: self.damus_state.pubkey, profile: prof))) notify(.profile_updated(.manual(pubkey: self.damus_state.pubkey, profile: prof)))
} }
.onDisappear { .onDisappear {
let profile_txn = damus_state.profiles.lookup(id: damus_state.pubkey)
guard let keypair = damus_state.keypair.to_full(), guard let keypair = damus_state.keypair.to_full(),
let profile = profile_txn?.unsafeUnownedValue, let profile = damus_state.profiles.lookup(id: damus_state.pubkey),
model.initial_percent != profile.damus_donation model.initial_percent != profile.damus_donation
else { else {
return return
@@ -104,8 +104,7 @@ struct TransactionView: View {
return NSLocalizedString("Unknown", comment: "A name label for an unknown user") return NSLocalizedString("Unknown", comment: "A name label for an unknown user")
} }
let profile_txn = damus_state.profiles.lookup(id: pubkey, txn_name: "txview-profile") let profile = damus_state.profiles.lookup(id: pubkey)
let profile = profile_txn?.unsafeUnownedValue
return Profile.displayName(profile: profile, pubkey: pubkey).displayName return Profile.displayName(profile: profile, pubkey: pubkey).displayName
} }
@@ -33,21 +33,26 @@ struct ProfileZapLinkView<Content: View>: View {
self.label = label self.label = label
self.action = action self.action = action
let profile_txn = damus_state.profiles.lookup_with_timestamp(pubkey) let profile = damus_state.profiles.lookup(id: pubkey)
let record = profile_txn?.unsafeUnownedValue let lnurl = damus_state.profiles.lookup_with_timestamp(pubkey, borrow: { pr -> String? in
self.reactions_enabled = record?.profile?.reactions ?? true switch pr {
self.lud16 = record?.profile?.lud06?.trimmingCharacters(in: .whitespaces) case .some(let pr): return pr.lnurl
self.lnurl = record?.lnurl?.trimmingCharacters(in: .whitespaces) case .none: return nil
}
})
self.reactions_enabled = profile?.reactions ?? true
self.lud16 = profile?.lud06?.trimmingCharacters(in: .whitespaces)
self.lnurl = lnurl?.trimmingCharacters(in: .whitespaces)
} }
init(unownedProfileRecord: ProfileRecord?, profileModel: ProfileModel, action: ActionFunction? = nil, @ViewBuilder label: @escaping ContentViewFunction) { init(profile: Profile?, lnurl: String?, profileModel: ProfileModel, action: ActionFunction? = nil, @ViewBuilder label: @escaping ContentViewFunction) {
self.pubkey = profileModel.pubkey self.pubkey = profileModel.pubkey
self.label = label self.label = label
self.action = action self.action = action
self.reactions_enabled = unownedProfileRecord?.profile?.reactions ?? true self.reactions_enabled = profile?.reactions ?? true
self.lud16 = unownedProfileRecord?.profile?.lud16?.trimmingCharacters(in: .whitespaces) self.lud16 = profile?.lud16?.trimmingCharacters(in: .whitespaces)
self.lnurl = unownedProfileRecord?.lnurl?.trimmingCharacters(in: .whitespaces) self.lnurl = lnurl?.trimmingCharacters(in: .whitespaces)
} }
var body: some View { var body: some View {
@@ -97,8 +97,7 @@ func zap_type_desc(type: ZapType, profiles: Profiles, pubkey: Pubkey) -> String
case .anon: 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.") 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: case .priv:
let prof_txn = profiles.lookup(id: pubkey) let prof = profiles.lookup(id: pubkey)
let prof = prof_txn?.unsafeUnownedValue
let name = Profile.displayName(profile: prof, pubkey: pubkey).username.truncate(maxLength: 50) 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) 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: case .non_zap:
+1 -1
View File
@@ -60,7 +60,7 @@ struct NIP05Badge: View {
} }
var username_matches_nip05: Bool { var username_matches_nip05: Bool {
guard let name = damus_state.profiles.lookup(id: pubkey)?.map({ p in p?.name }).value guard let name = damus_state.profiles.lookup(id: pubkey)?.name
else { else {
return false return false
} }
+1 -2
View File
@@ -73,8 +73,7 @@ struct QRCodeView: View {
var QRView: some View { var QRView: some View {
VStack(alignment: .center) { VStack(alignment: .center) {
let profile_txn = damus_state.profiles.lookup(id: pubkey, txn_name: "qrview-profile") let profile = damus_state.profiles.lookup(id: pubkey)
let profile = profile_txn?.unsafeUnownedValue
ProfilePicView(pubkey: pubkey, size: 90.0, highlight: .custom(DamusColors.white, 3.0), profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation, damusState: damus_state) ProfilePicView(pubkey: pubkey, size: 90.0, highlight: .custom(DamusColors.white, 3.0), profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation, damusState: damus_state)
.padding(.top, 20) .padding(.top, 20)
@@ -104,13 +104,12 @@ struct BannerImageView: View {
InnerBannerImageView(disable_animation: disable_animation, url: get_banner_url(banner: banner, pubkey: pubkey, profiles: profiles)) InnerBannerImageView(disable_animation: disable_animation, url: get_banner_url(banner: banner, pubkey: pubkey, profiles: profiles))
.onReceive(handle_notify(.profile_updated)) { updated in .onReceive(handle_notify(.profile_updated)) { updated in
guard updated.pubkey == self.pubkey, guard updated.pubkey == self.pubkey,
let profile_txn = profiles.lookup(id: updated.pubkey) let profile = profiles.lookup(id: updated.pubkey)
else { else {
return return
} }
let profile = profile_txn.unsafeUnownedValue if let bannerImage = profile.banner, bannerImage != self.banner {
if let bannerImage = profile?.banner, bannerImage != self.banner {
self.banner = bannerImage self.banner = bannerImage
} }
} }
@@ -118,7 +117,7 @@ struct BannerImageView: View {
} }
func get_banner_url(banner: String?, pubkey: Pubkey, profiles: Profiles) -> URL? { func get_banner_url(banner: String?, pubkey: Pubkey, profiles: Profiles) -> URL? {
let bannerUrlString = banner ?? profiles.lookup(id: pubkey)?.map({ p in p?.banner }).value ?? "" let bannerUrlString = banner ?? profiles.lookup(id: pubkey)?.banner ?? ""
if let url = URL(string: bannerUrlString) { if let url = URL(string: bannerUrlString) {
return url return url
} }
+1 -1
View File
@@ -222,7 +222,7 @@ class EventCache {
return ev return ev
} }
if let ev = self.ndb.lookup_note(evid)?.unsafeUnownedValue?.to_owned() { if let ev = self.ndb.lookup_note_and_copy(evid) {
events[ev.id] = ev events[ev.id] = ev
return ev return ev
} }
@@ -137,12 +137,10 @@ class NostrNetworkManagerTests: XCTestCase {
switch item { switch item {
case .event(let noteKey): case .event(let noteKey):
// Lookup the note to verify it exists // Lookup the note to verify it exists
if let txn = NdbTxn(ndb: ndb) { if let note = ndb.lookup_note_by_key_and_copy(noteKey) {
if let note = ndb.lookup_note_by_key_with_txn(noteKey, txn: txn) {
count += 1 count += 1
receivedIds.insert(note.id) receivedIds.insert(note.id)
} }
}
if count >= expectedCount { if count >= expectedCount {
atLeastXNotes.fulfill() atLeastXNotes.fulfill()
} }
+8 -6
View File
@@ -347,15 +347,17 @@ class NoteContentViewTests: XCTestCase {
func testDirectBlockParsing() { func testDirectBlockParsing() {
let kp = test_keypair_full let kp = test_keypair_full
let dm: NdbNote = NIP04.create_dm("Test", to_pk: kp.pubkey, tags: [], keypair: kp.to_keypair())! let dm: NdbNote = NIP04.create_dm("Test", to_pk: kp.pubkey, tags: [], keypair: kp.to_keypair())!
let blocks = try! NdbBlockGroup.from(event: dm, using: test_damus_state.ndb, and: kp.to_keypair()) try! NdbBlockGroup.borrowBlockGroup(event: dm, using: test_damus_state.ndb, and: kp.to_keypair(), borrow: { blocks in
let blockCount1 = try? blocks.withList({ $0.count }) let blockCount = blocks.withList({ $0.count })
XCTAssertEqual(blockCount1, 1) XCTAssertEqual(blockCount, 1)
})
let post = NostrPost(content: "Test", kind: .text) let post = NostrPost(content: "Test", kind: .text)
let event = post.to_event(keypair: kp)! let event = post.to_event(keypair: kp)!
let blocks2 = try! NdbBlockGroup.from(event: event, using: test_damus_state.ndb, and: kp.to_keypair()) try! NdbBlockGroup.borrowBlockGroup(event: event, using: test_damus_state.ndb, and: kp.to_keypair(), borrow: { blocks in
let blockCount2 = try? blocks2.withList({ $0.count }) let blockCount = blocks.withList({ $0.count })
XCTAssertEqual(blockCount2, 1) XCTAssertEqual(blockCount, 1)
})
} }
func testMentionStr_Pubkey_ContainsAbbreviated() throws { func testMentionStr_Pubkey_ContainsAbbreviated() throws {
+2 -2
View File
@@ -29,8 +29,8 @@ extension Ndb {
} }
/// Determines if a given note was seen on any of the listed relay URLs /// Determines if a given note was seen on any of the listed relay URLs
func was(noteKey: NoteKey, seenOnAnyOf relayUrls: [RelayURL], txn: SafeNdbTxn<()>? = nil) throws -> Bool { func was(noteKey: NoteKey, seenOnAnyOf relayUrls: [RelayURL]) throws -> Bool {
return try self.was(noteKey: noteKey, seenOnAnyOf: relayUrls.map({ $0.absoluteString }), txn: txn) return try self.was(noteKey: noteKey, seenOnAnyOf: relayUrls.map({ $0.absoluteString }))
} }
func processEvent(_ str: String, originRelayURL: RelayURL? = nil) -> Bool { func processEvent(_ str: String, originRelayURL: RelayURL? = nil) -> Bool {
+105 -72
View File
@@ -235,7 +235,8 @@ class Ndb {
return true return true
} }
func lookup_blocks_by_key_with_txn(_ key: NoteKey, txn: RawNdbTxnAccessible) -> NdbBlockGroup.BlocksMetadata? { // GH_3245 TODO: This is a low level call, make it hidden from outside Ndb
internal func lookup_blocks_by_key_with_txn(_ key: NoteKey, txn: RawNdbTxnAccessible) -> NdbBlockGroup.BlocksMetadata? {
guard let blocks = ndb_get_blocks_by_key(self.ndb.ndb, &txn.txn, key) else { guard let blocks = ndb_get_blocks_by_key(self.ndb.ndb, &txn.txn, key) else {
return nil return nil
} }
@@ -243,13 +244,17 @@ class Ndb {
return NdbBlockGroup.BlocksMetadata(ptr: blocks) return NdbBlockGroup.BlocksMetadata(ptr: blocks)
} }
func lookup_blocks_by_key(_ key: NoteKey) -> SafeNdbTxn<NdbBlockGroup.BlocksMetadata?>? { func lookup_blocks_by_key<T>(_ key: NoteKey, borrow lendingFunction: (_: borrowing NdbBlockGroup.BlocksMetadata?) throws -> T) rethrows -> T {
SafeNdbTxn<NdbBlockGroup.BlocksMetadata?>.new(on: self) { txn in let txn = SafeNdbTxn<NdbBlockGroup.BlocksMetadata?>.new(on: self) { txn in
lookup_blocks_by_key_with_txn(key, txn: txn) lookup_blocks_by_key_with_txn(key, txn: txn)
} }
guard let txn else {
return try lendingFunction(nil)
}
return try lendingFunction(txn.val)
} }
func lookup_note_by_key_with_txn<Y>(_ key: NoteKey, txn: NdbTxn<Y>) -> NdbNote? { private func lookup_note_by_key_with_txn<Y>(_ key: NoteKey, txn: NdbTxn<Y>) -> NdbNote? {
var size: Int = 0 var size: Int = 0
guard let note_p = ndb_get_note_by_key(&txn.txn, key, &size) else { guard let note_p = ndb_get_note_by_key(&txn.txn, key, &size) else {
return nil return nil
@@ -411,13 +416,25 @@ class Ndb {
return note_ids return note_ids
} }
func lookup_note_by_key(_ key: NoteKey) -> NdbTxn<NdbNote?>? { func lookup_note_by_key<T>(_ key: NoteKey, borrow lendingFunction: (_: borrowing UnownedNdbNote?) throws -> T) rethrows -> T {
return NdbTxn(ndb: self) { txn in let txn = NdbTxn(ndb: self) { txn in
lookup_note_by_key_with_txn(key, txn: txn) lookup_note_by_key_with_txn(key, txn: txn)
} }
guard let rawNote = txn?.unsafeUnownedValue else { return try lendingFunction(nil) }
let unownedNote = UnownedNdbNote(rawNote)
return try lendingFunction(.some(unownedNote))
} }
private func lookup_profile_by_key_inner<Y>(_ key: ProfileKey, txn: NdbTxn<Y>) -> ProfileRecord? { func lookup_note_by_key_and_copy(_ key: NoteKey) -> NdbNote? {
return lookup_note_by_key(key, borrow: { maybeUnownedNote -> NdbNote? in
switch maybeUnownedNote {
case .none: return nil
case .some(let unownedNote): return unownedNote.toOwned()
}
})
}
private func lookup_profile_by_key_inner(_ key: ProfileKey, txn: RawNdbTxnAccessible) -> ProfileRecord? {
var size: Int = 0 var size: Int = 0
guard let profile_p = ndb_get_profile_by_key(&txn.txn, key, &size) else { guard let profile_p = ndb_get_profile_by_key(&txn.txn, key, &size) else {
return nil return nil
@@ -451,32 +468,36 @@ class Ndb {
} }
} }
private func lookup_profile_with_txn_inner<Y>(pubkey: Pubkey, txn: NdbTxn<Y>) -> ProfileRecord? { private func lookup_profile_with_txn_inner(pubkey: Pubkey, txn: some RawNdbTxnAccessible) -> ProfileRecord? {
return pubkey.id.withUnsafeBytes { (ptr: UnsafeRawBufferPointer) -> ProfileRecord? in var record: ProfileRecord? = nil
pubkey.id.withUnsafeBytes { (ptr: UnsafeRawBufferPointer) in
var size: Int = 0 var size: Int = 0
var key: UInt64 = 0 var key: UInt64 = 0
guard let baseAddress = ptr.baseAddress, guard let baseAddress = ptr.baseAddress,
let profile_p = ndb_get_profile_by_pubkey(&txn.txn, baseAddress, &size, &key) let profile_p = ndb_get_profile_by_pubkey(&txn.txn, baseAddress, &size, &key)
else { else {
return nil return
} }
return profile_flatbuf_to_record(ptr: profile_p, size: size, key: key) record = profile_flatbuf_to_record(ptr: profile_p, size: size, key: key)
} }
return record
} }
func lookup_profile_by_key_with_txn<Y>(key: ProfileKey, txn: NdbTxn<Y>) -> ProfileRecord? { private func lookup_profile_by_key_with_txn(key: ProfileKey, txn: RawNdbTxnAccessible) -> ProfileRecord? {
lookup_profile_by_key_inner(key, txn: txn) lookup_profile_by_key_inner(key, txn: txn)
} }
func lookup_profile_by_key(key: ProfileKey) -> NdbTxn<ProfileRecord?>? { func lookup_profile_by_key<T>(key: ProfileKey, borrow lendingFunction: (_: borrowing ProfileRecord?) throws -> T) rethrows -> T {
return NdbTxn(ndb: self) { txn in let txn = SafeNdbTxn<ProfileRecord?>.new(on: self) { txn in
lookup_profile_by_key_inner(key, txn: txn) return lookup_profile_by_key_inner(key, txn: txn)
} }
guard let txn else { return try lendingFunction(nil) }
return try lendingFunction(txn.val)
} }
func lookup_note_with_txn<Y>(id: NoteId, txn: NdbTxn<Y>) -> NdbNote? { private func lookup_note_with_txn<Y>(id: NoteId, txn: NdbTxn<Y>) -> NdbNote? {
lookup_note_with_txn_inner(id: id, txn: txn) lookup_note_with_txn_inner(id: id, txn: txn)
} }
@@ -490,7 +511,7 @@ class Ndb {
return txn.value return txn.value
} }
func lookup_profile_key_with_txn<Y>(_ pubkey: Pubkey, txn: NdbTxn<Y>) -> ProfileKey? { private func lookup_profile_key_with_txn<Y>(_ pubkey: Pubkey, txn: NdbTxn<Y>) -> ProfileKey? {
return pubkey.id.withUnsafeBytes { (ptr: UnsafeRawBufferPointer) -> NoteKey? in return pubkey.id.withUnsafeBytes { (ptr: UnsafeRawBufferPointer) -> NoteKey? in
guard let p = ptr.baseAddress else { return nil } guard let p = ptr.baseAddress else { return nil }
let r = ndb_get_profilekey_by_pubkey(&txn.txn, p) let r = ndb_get_profilekey_by_pubkey(&txn.txn, p)
@@ -501,7 +522,8 @@ class Ndb {
} }
} }
func lookup_note_key_with_txn(_ id: NoteId, txn: some RawNdbTxnAccessible) -> NoteKey? { // GH_3245 TODO: This is a low level call, make it hidden from outside Ndb
internal func lookup_note_key_with_txn(_ id: NoteId, txn: some RawNdbTxnAccessible) -> NoteKey? {
guard !closed else { return nil } guard !closed else { return nil }
return id.id.withUnsafeBytes { (ptr: UnsafeRawBufferPointer) -> NoteKey? in return id.id.withUnsafeBytes { (ptr: UnsafeRawBufferPointer) -> NoteKey? in
guard let p = ptr.baseAddress else { guard let p = ptr.baseAddress else {
@@ -525,19 +547,47 @@ class Ndb {
return txn.value return txn.value
} }
func lookup_note(_ id: NoteId, txn_name: String? = nil) -> NdbTxn<NdbNote?>? { func lookup_note<T>(_ id: NoteId, borrow lendingFunction: (_: borrowing UnownedNdbNote?) throws -> T) rethrows -> T {
NdbTxn(ndb: self, name: txn_name) { txn in let txn = NdbTxn(ndb: self) { txn in
lookup_note_with_txn_inner(id: id, txn: txn) lookup_note_with_txn_inner(id: id, txn: txn)
} }
guard let rawNote = txn?.unsafeUnownedValue else { return try lendingFunction(nil) }
return try lendingFunction(UnownedNdbNote(rawNote))
} }
func lookup_profile(_ pubkey: Pubkey, txn_name: String? = nil) -> NdbTxn<ProfileRecord?>? { func lookup_note_and_copy(_ id: NoteId) -> NdbNote? {
NdbTxn(ndb: self, name: txn_name) { txn in return self.lookup_note(id, borrow: { unownedNote in
return unownedNote?.toOwned()
})
}
func lookup_profile<T>(_ pubkey: Pubkey, borrow lendingFunction: (_: borrowing ProfileRecord?) throws -> T) rethrows -> T {
let txn = SafeNdbTxn<ProfileRecord?>.new(on: self) { txn in
lookup_profile_with_txn_inner(pubkey: pubkey, txn: txn) lookup_profile_with_txn_inner(pubkey: pubkey, txn: txn)
} }
guard let txn else { return try lendingFunction(nil) }
return try lendingFunction(txn.val)
} }
func lookup_profile_with_txn<Y>(_ pubkey: Pubkey, txn: NdbTxn<Y>) -> ProfileRecord? { func lookup_profile_lnurl(_ pubkey: Pubkey) -> String? {
return lookup_profile(pubkey, borrow: { pr in
switch pr {
case .none: return nil
case .some(let pr): return pr.lnurl
}
})
}
func lookup_profile_and_copy(_ pubkey: Pubkey) -> Profile? {
return self.lookup_profile(pubkey, borrow: { pr in
switch pr {
case .some(let pr): return pr.profile
case .none: return nil
}
})
}
private func lookup_profile_with_txn<Y>(_ pubkey: Pubkey, txn: NdbTxn<Y>) -> ProfileRecord? {
lookup_profile_with_txn_inner(pubkey: pubkey, txn: txn) lookup_profile_with_txn_inner(pubkey: pubkey, txn: txn)
} }
@@ -556,7 +606,7 @@ class Ndb {
} }
} }
func read_profile_last_fetched<Y>(txn: NdbTxn<Y>, pubkey: Pubkey) -> UInt64? { private func read_profile_last_fetched<Y>(txn: NdbTxn<Y>, pubkey: Pubkey) -> UInt64? {
guard !closed else { return nil } guard !closed else { return nil }
return pubkey.id.withUnsafeBytes { (ptr: UnsafeRawBufferPointer) -> UInt64? in return pubkey.id.withUnsafeBytes { (ptr: UnsafeRawBufferPointer) -> UInt64? in
guard let p = ptr.baseAddress else { return nil } guard let p = ptr.baseAddress else { return nil }
@@ -569,6 +619,14 @@ class Ndb {
} }
} }
func read_profile_last_fetched(pubkey: Pubkey) -> UInt64? {
var last_fetched: UInt64? = nil
let _ = NdbTxn(ndb: self) { txn in
last_fetched = read_profile_last_fetched(txn: txn, pubkey: pubkey)
}
return last_fetched
}
func process_event(_ str: String, originRelayURL: String? = nil) -> Bool { func process_event(_ str: String, originRelayURL: String? = nil) -> Bool {
guard !is_closed else { return false } guard !is_closed else { return false }
guard let originRelayURL else { guard let originRelayURL else {
@@ -593,7 +651,12 @@ class Ndb {
} }
} }
func search_profile<Y>(_ search: String, limit: Int, txn: NdbTxn<Y>) -> [Pubkey] { func search_profile(_ search: String, limit: Int) -> [Pubkey] {
guard let txn = NdbTxn<()>.init(ndb: self) else { return [] }
return search_profile(search, limit: limit, txn: txn)
}
private func search_profile<Y>(_ search: String, limit: Int, txn: NdbTxn<Y>) -> [Pubkey] {
var pks = Array<Pubkey>() var pks = Array<Pubkey>()
return search.withCString { q in return search.withCString { q in
@@ -621,6 +684,11 @@ class Ndb {
// MARK: NdbFilter queries and subscriptions // MARK: NdbFilter queries and subscriptions
func query(filters: [NdbFilter], maxResults: Int) throws(NdbStreamError) -> [NoteKey] {
guard let txn = NdbTxn(ndb: self) else { return [] }
return try query(with: txn, filters: filters, maxResults: maxResults)
}
/// Safe wrapper around the `ndb_query` C function /// Safe wrapper around the `ndb_query` C function
/// - Parameters: /// - Parameters:
/// - txn: Database transaction /// - txn: Database transaction
@@ -628,7 +696,7 @@ class Ndb {
/// - maxResults: Maximum number of results to return /// - maxResults: Maximum number of results to return
/// - Returns: Array of note keys matching the filters /// - Returns: Array of note keys matching the filters
/// - Throws: NdbStreamError if the query fails /// - Throws: NdbStreamError if the query fails
func query<Y>(with txn: NdbTxn<Y>, filters: [NdbFilter], maxResults: Int) throws(NdbStreamError) -> [NoteKey] { private func query<Y>(with txn: NdbTxn<Y>, filters: [NdbFilter], maxResults: Int) throws(NdbStreamError) -> [NoteKey] {
guard !self.is_closed else { throw .ndbClosed } guard !self.is_closed else { throw .ndbClosed }
let filtersPointer = UnsafeMutablePointer<ndb_filter>.allocate(capacity: filters.count) let filtersPointer = UnsafeMutablePointer<ndb_filter>.allocate(capacity: filters.count)
defer { filtersPointer.deallocate() } defer { filtersPointer.deallocate() }
@@ -784,60 +852,20 @@ class Ndb {
return nil return nil
} }
func waitFor(noteId: NoteId, timeout: TimeInterval = 10) async throws(NdbLookupError) -> NdbTxn<NdbNote>? {
do {
return try await withCheckedThrowingContinuation({ continuation in
var done = false
let waitTask = Task {
do {
Log.debug("ndb_wait: Waiting for %s", for: .ndb, noteId.hex())
let result = try await self.waitWithoutTimeout(for: noteId)
if !done {
Log.debug("ndb_wait: Found %s", for: .ndb, noteId.hex())
continuation.resume(returning: result)
done = true
}
}
catch {
if Task.isCancelled {
return // the timeout task will handle throwing the timeout error
}
if !done {
Log.debug("ndb_wait: Error on %s: %s", for: .ndb, noteId.hex(), error.localizedDescription)
continuation.resume(throwing: error)
done = true
}
}
}
let timeoutTask = Task {
try await Task.sleep(for: .seconds(Int(timeout)))
if !done {
Log.debug("ndb_wait: Timeout on %s. Cancelling wait task…", for: .ndb, noteId.hex())
done = true
print("ndb_wait: throwing timeout error")
continuation.resume(throwing: NdbLookupError.timeout)
}
waitTask.cancel()
}
})
}
catch {
if let error = error as? NdbLookupError { throw error }
else { throw .internalInconsistency }
}
}
/// Determines if a given note was seen on a specific relay URL /// Determines if a given note was seen on a specific relay URL
func was(noteKey: NoteKey, seenOn relayUrl: String, txn: SafeNdbTxn<()>? = nil) throws -> Bool { private func was(noteKey: NoteKey, seenOn relayUrl: String, txn: SafeNdbTxn<()>?) throws -> Bool {
guard let txn = txn ?? SafeNdbTxn.new(on: self) else { throw NdbLookupError.cannotOpenTransaction } guard let txn = txn ?? SafeNdbTxn.new(on: self) else { throw NdbLookupError.cannotOpenTransaction }
return relayUrl.withCString({ relayCString in return relayUrl.withCString({ relayCString in
return ndb_note_seen_on_relay(&txn.txn, noteKey, relayCString) == 1 return ndb_note_seen_on_relay(&txn.txn, noteKey, relayCString) == 1
}) })
} }
func was(noteKey: NoteKey, seenOn relayUrl: String) throws -> Bool {
return try self.was(noteKey: noteKey, seenOn: relayUrl, txn: nil)
}
/// Determines if a given note was seen on any of the listed relay URLs /// Determines if a given note was seen on any of the listed relay URLs
func was(noteKey: NoteKey, seenOnAnyOf relayUrls: [String], txn: SafeNdbTxn<()>? = nil) throws -> Bool { private func was(noteKey: NoteKey, seenOnAnyOf relayUrls: [String], txn: SafeNdbTxn<()>? = nil) throws -> Bool {
guard let txn = txn ?? SafeNdbTxn.new(on: self) else { throw NdbLookupError.cannotOpenTransaction } guard let txn = txn ?? SafeNdbTxn.new(on: self) else { throw NdbLookupError.cannotOpenTransaction }
for relayUrl in relayUrls { for relayUrl in relayUrls {
if try self.was(noteKey: noteKey, seenOn: relayUrl, txn: txn) { if try self.was(noteKey: noteKey, seenOn: relayUrl, txn: txn) {
@@ -847,6 +875,11 @@ class Ndb {
return false return false
} }
/// Determines if a given note was seen on any of the listed relay URLs
func was(noteKey: NoteKey, seenOnAnyOf relayUrls: [String]) throws -> Bool {
return try self.was(noteKey: noteKey, seenOnAnyOf: relayUrls, txn: nil)
}
// MARK: Internal ndb callback interfaces // MARK: Internal ndb callback interfaces
internal func setContinuation(for subscriptionId: UInt64, continuation: AsyncStream<NoteKey>.Continuation) async { internal func setContinuation(for subscriptionId: UInt64, continuation: AsyncStream<NoteKey>.Continuation) async {
+39 -27
View File
@@ -104,11 +104,16 @@ enum NdbBlock: ~Copyable {
/// Represents a group of blocks /// Represents a group of blocks
struct NdbBlockGroup: ~Copyable { struct NdbBlockGroup: ~Copyable {
/// The block offsets /// The block offsets
fileprivate let metadata: MaybeTxn<BlocksMetadata> fileprivate let metadata: BlocksMetadata
/// The raw text content of the note /// The raw text content of the note
fileprivate let rawTextContent: String fileprivate let rawTextContent: String
var words: Int { var words: Int {
return metadata.borrow { $0.words } return metadata.words
}
init(metadata: consuming BlocksMetadata, rawTextContent: String) {
self.metadata = metadata
self.rawTextContent = rawTextContent
} }
/// Gets the parsed blocks from a specific note. /// Gets the parsed blocks from a specific note.
@@ -116,18 +121,20 @@ struct NdbBlockGroup: ~Copyable {
/// This function will: /// This function will:
/// - fetch blocks information from NostrDB if possible _and_ available, or /// - fetch blocks information from NostrDB if possible _and_ available, or
/// - parse blocks on-demand. /// - parse blocks on-demand.
static func from(event: NdbNote, using ndb: Ndb, and keypair: Keypair) throws(NdbBlocksError) -> Self { static func borrowBlockGroup<T>(event: NdbNote, using ndb: Ndb, and keypair: Keypair, borrow lendingFunction: (_: borrowing Self) throws -> T) throws -> T {
if event.is_content_encrypted() { if event.is_content_encrypted() {
return try parse(event: event, keypair: keypair) return try lendingFunction(parse(event: event, keypair: keypair))
} }
else if event.known_kind == .highlight { else if event.known_kind == .highlight {
return try parse(event: event, keypair: keypair) return try lendingFunction(parse(event: event, keypair: keypair))
} }
else { else {
guard let offsets = event.block_offsets(ndb: ndb) else { return try ndb.lookup_block_group_by_key(event: event, borrow: { group in
return try parse(event: event, keypair: keypair) switch group {
case .none: return try lendingFunction(parse(event: event, keypair: keypair))
case .some(let group): return try lendingFunction(group)
} }
return .init(metadata: .txn(offsets), rawTextContent: event.content) })
} }
} }
@@ -136,34 +143,44 @@ struct NdbBlockGroup: ~Copyable {
/// Prioritize using `from(event: NdbNote, using ndb: Ndb, and keypair: Keypair)` when possible. /// Prioritize using `from(event: NdbNote, using ndb: Ndb, and keypair: Keypair)` when possible.
static func parse(event: NdbNote, keypair: Keypair) throws(NdbBlocksError) -> Self { static func parse(event: NdbNote, keypair: Keypair) throws(NdbBlocksError) -> Self {
guard let content = event.maybe_get_content(keypair) else { throw NdbBlocksError.decryptionError } guard let content = event.maybe_get_content(keypair) else { throw NdbBlocksError.decryptionError }
guard let metadata = BlocksMetadata.parseContent(content: content) else { throw NdbBlocksError.parseError } guard var metadata = BlocksMetadata.parseContent(content: content) else { throw NdbBlocksError.parseError }
return self.init( return self.init(
metadata: .pure(metadata), metadata: metadata,
rawTextContent: content rawTextContent: content
) )
} }
/// Parses the note contents on-demand from a specific text. /// Parses the note contents on-demand from a specific text.
static func parse(content: String) throws(NdbBlocksError) -> Self { static func parse(content: String) throws(NdbBlocksError) -> Self {
guard let metadata = BlocksMetadata.parseContent(content: content) else { throw NdbBlocksError.parseError } guard var metadata = BlocksMetadata.parseContent(content: content) else { throw NdbBlocksError.parseError }
return self.init( return self.init(
metadata: .pure(metadata), metadata: metadata,
rawTextContent: content rawTextContent: content
) )
} }
} }
enum MaybeTxn<T: ~Copyable>: ~Copyable { // MARK: - Extensions enabling low-level control
case pure(T)
case txn(SafeNdbTxn<T>)
func borrow<Y>(_ borrowFunction: (borrowing T) throws -> Y) rethrows -> Y { fileprivate extension Ndb {
switch self { func lookup_block_group_by_key<T>(event: NdbNote, borrow lendingFunction: sending (_: borrowing NdbBlockGroup?) throws -> T) rethrows -> T {
case .pure(let item): let txn = SafeNdbTxn<NdbBlockGroup?>.new(on: self) { txn in
return try borrowFunction(item) guard let key = lookup_note_key_with_txn(event.id, txn: txn) else { return nil }
case .txn(let txn): return lookup_block_group_by_key_with_txn(key, event: event, txn: txn)
return try borrowFunction(txn.val)
} }
guard let txn else {
return try lendingFunction(nil)
}
return try lendingFunction(txn.val)
}
func lookup_block_group_by_key_with_txn(_ key: NoteKey, event: NdbNote, txn: RawNdbTxnAccessible) -> NdbBlockGroup? {
guard let blocks = ndb_get_blocks_by_key(self.ndb.ndb, &txn.txn, key) else {
return nil
}
let metadata = NdbBlockGroup.BlocksMetadata(ptr: blocks)
return NdbBlockGroup(metadata: metadata, rawTextContent: event.content)
} }
} }
@@ -174,8 +191,6 @@ extension NdbBlockGroup {
/// Wrapper for the `ndb_blocks` C struct /// Wrapper for the `ndb_blocks` C struct
/// ///
/// This does not store the actual block contents, only the offsets on the content string and block metadata. /// This does not store the actual block contents, only the offsets on the content string and block metadata.
///
/// **Implementation note:** This would be better as `~Copyable`, but `NdbTxn` does not support `~Copyable` yet.
struct BlocksMetadata: ~Copyable { struct BlocksMetadata: ~Copyable {
private let blocks_ptr: ndb_blocks_ptr private let blocks_ptr: ndb_blocks_ptr
private let buffer: UnsafeMutableRawPointer? private let buffer: UnsafeMutableRawPointer?
@@ -290,17 +305,14 @@ extension NdbBlockGroup {
var iter = ndb_block_iterator(content: cptr, blocks: nil, block: ndb_block(), p: nil) var iter = ndb_block_iterator(content: cptr, blocks: nil, block: ndb_block(), p: nil)
// Start the iteration // Start the iteration
return try self.metadata.borrow { value in ndb_blocks_iterate_start(cptr, self.metadata.as_ptr(), &iter)
ndb_blocks_iterate_start(cptr, value.as_ptr(), &iter)
// Collect blocks into array // Collect blocks into array
outerLoop: while let ptr = ndb_blocks_iterate_next(&iter), outerLoop: while let ptr = ndb_blocks_iterate_next(&iter),
let block = NdbBlock(ndb_block_ptr(ptr: ptr)) { let block = NdbBlock(ndb_block_ptr(ptr: ptr)) {
linkedList.add(item: block) linkedList.add(item: block)
} }
return try borrowingFunction(linkedList) return try borrowingFunction(linkedList)
} }
} }
}
} }
+9 -10
View File
@@ -74,6 +74,10 @@ class NdbNote: Codable, Equatable, Hashable {
#endif #endif
} }
func clone() -> NdbNote {
return self.to_owned()
}
func to_owned() -> NdbNote { func to_owned() -> NdbNote {
if self.owned { if self.owned {
return self return self
@@ -474,17 +478,12 @@ extension NdbNote {
return ThreadReply(tags: self.tags)?.reply.note_id return ThreadReply(tags: self.tags)?.reply.note_id
} }
func block_offsets(ndb: Ndb) -> SafeNdbTxn<NdbBlockGroup.BlocksMetadata>? { func block_offsets<T>(ndb: Ndb, borrow lendingFunction: (_: borrowing NdbBlockGroup.BlocksMetadata?) throws -> T) rethrows -> T {
let blocks_txn: SafeNdbTxn<NdbBlockGroup.BlocksMetadata>? = .new(on: ndb) { txn -> NdbBlockGroup.BlocksMetadata? in guard let key = ndb.lookup_note_key(self.id) else { return try lendingFunction(nil) }
guard let key = ndb.lookup_note_key_with_txn(self.id, txn: txn) else {
return nil
}
return ndb.lookup_blocks_by_key_with_txn(key, txn: txn)
}
guard let blocks_txn else { return nil } return try ndb.lookup_blocks_by_key(key, borrow: { blocks in
return try lendingFunction(blocks)
return blocks_txn })
} }
func is_content_encrypted() -> Bool { func is_content_encrypted() -> Bool {
+1 -1
View File
@@ -78,7 +78,7 @@ class NdbTxn<T>: RawNdbTxnAccessible {
/// Only access temporarily! Do not store database references for longterm use. If it's a primitive type you /// Only access temporarily! Do not store database references for longterm use. If it's a primitive type you
/// can retrieve this value with `.value` /// can retrieve this value with `.value`
var unsafeUnownedValue: T { internal var unsafeUnownedValue: T {
precondition(!moved) precondition(!moved)
return val return val
} }
+11 -5
View File
@@ -64,18 +64,19 @@ final class NdbTests: XCTestCase {
let ndb = Ndb(path: db_dir)! let ndb = Ndb(path: db_dir)!
let id = NoteId(hex: "d12c17bde3094ad32f4ab862a6cc6f5c289cfe7d5802270bdf34904df585f349")! let id = NoteId(hex: "d12c17bde3094ad32f4ab862a6cc6f5c289cfe7d5802270bdf34904df585f349")!
guard let txn = NdbTxn(ndb: ndb) else { return XCTAssert(false) } guard let txn = NdbTxn(ndb: ndb) else { return XCTAssert(false) }
let note = ndb.lookup_note_with_txn(id: id, txn: txn) let note = ndb.lookup_note_and_copy(id)
XCTAssertNotNil(note) XCTAssertNotNil(note)
guard let note else { return } guard let note else { return }
let pk = Pubkey(hex: "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245")! let pk = Pubkey(hex: "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245")!
XCTAssertEqual(note.pubkey, pk) XCTAssertEqual(note.pubkey, pk)
let profile = ndb.lookup_profile_with_txn(pk, txn: txn) let profile = ndb.lookup_profile_and_copy(pk)
let lnurl = ndb.lookup_profile_lnurl(pk)
XCTAssertNotNil(profile) XCTAssertNotNil(profile)
guard let profile else { return } guard let profile else { return }
XCTAssertEqual(profile.profile?.name, "jb55") XCTAssertEqual(profile.name, "jb55")
XCTAssertEqual(profile.lnurl, nil) XCTAssertEqual(lnurl, nil)
} }
@@ -97,7 +98,12 @@ final class NdbTests: XCTestCase {
XCTFail("Expected at least one note to be found") XCTFail("Expected at least one note to be found")
return return
} }
let note_id = ndb.lookup_note_by_key(note_ids[0])?.map({ n in n?.id }).value let note_id = ndb.lookup_note_by_key(note_ids[0], borrow: { maybeUnownedNote -> NoteId? in
switch maybeUnownedNote {
case .none: return nil
case .some(let unownedNote): return unownedNote.id
}
})
XCTAssertEqual(note_id, .some(expected_note_id)) XCTAssertEqual(note_id, .some(expected_note_id))
} }
} }
+6 -3
View File
@@ -58,9 +58,12 @@ enum NdbNoteLender: Sendable {
switch self { switch self {
case .ndbNoteKey(let ndb, let noteKey): case .ndbNoteKey(let ndb, let noteKey):
guard !ndb.is_closed else { throw LendingError.ndbClosed } guard !ndb.is_closed else { throw LendingError.ndbClosed }
guard let ndbNoteTxn = ndb.lookup_note_by_key(noteKey) else { throw LendingError.errorLoadingNote } return try ndb.lookup_note_by_key(noteKey, borrow: { maybeUnownedNote in
guard let unownedNote = UnownedNdbNote(ndbNoteTxn) else { throw LendingError.errorLoadingNote } switch maybeUnownedNote {
return try lendingFunction(unownedNote) case .none: throw LendingError.errorLoadingNote
case .some(let unownedNote): return try lendingFunction(unownedNote)
}
})
case .owned(let note): case .owned(let note):
return try lendingFunction(UnownedNdbNote(note)) return try lendingFunction(UnownedNdbNote(note))
} }