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:
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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))
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user