commit caa7802bce2ff07a4bd579e586b94d0bd8f46b41
parent 9c47d2e0bde950038c4d581335d37134cdb573fe
Author: Daniel D’Aquino <daniel@daquino.me>
Date: Fri, 27 Jun 2025 19:27:42 -0700
Fix broken DM rendering
Currently NostrDB does not seem to handle encryption/decryption of DMs.
Since NostrDB now controls the block parsing process and fetches note
contents directly from the database, we have to add a specific condition
that injects decrypted content directly to the ndb content parser.
This is done in conjunction with some minor refactoring to `NdbBlocks`
and associated structs, as in C those are separated between the content
string and the offsets for each block, but in Swift this is more
ergonomically represented as a standalone/self-containing object.
No changelog entry is added because the previously broken version was
never released to the public, and therefore this fix produces no
user-facing changes compared to the last released version.
Changelog-None
Closes: https://github.com/damus-io/damus/issues/3106
Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
Diffstat:
11 files changed, 349 insertions(+), 128 deletions(-)
diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj
@@ -154,9 +154,7 @@
4C32B95F2A9AD44700DC3548 /* Enum.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C32B94A2A9AD44700DC3548 /* Enum.swift */; };
4C32B9602A9AD44700DC3548 /* Struct.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C32B94B2A9AD44700DC3548 /* Struct.swift */; };
4C36245B2D5E9B2F00DD066E /* NdbProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF47FFE2B631C0100F2B2C0 /* NdbProfile.swift */; };
- 4C36245C2D5E9B4400DD066E /* NdbBlocksIterator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF480562B633F2600F2B2C0 /* NdbBlocksIterator.swift */; };
4C36245D2D5E9B4400DD066E /* NdbBlock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF480582B633F3800F2B2C0 /* NdbBlock.swift */; };
- 4C36245E2D5E9B5F00DD066E /* NdbBlocksIterator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF480562B633F2600F2B2C0 /* NdbBlocksIterator.swift */; };
4C36245F2D5E9B5F00DD066E /* NdbBlock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF480582B633F3800F2B2C0 /* NdbBlock.swift */; };
4C3624602D5E9EB800DD066E /* NdbProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF47FFE2B631C0100F2B2C0 /* NdbProfile.swift */; };
4C3624612D5E9FFD00DD066E /* wasm.c in Sources */ = {isa = PBXBuildFile; fileRef = 4CF480532B631C4F00F2B2C0 /* wasm.c */; };
@@ -343,10 +341,8 @@
4CB883B6297730E400DC99E7 /* LNUrls.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB883B5297730E400DC99E7 /* LNUrls.swift */; };
4CB8FC232A41ABA800763C51 /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB8FC222A41ABA500763C51 /* AboutView.swift */; };
4CB9D4A72992D02B00A9A7E4 /* ProfileNameView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB9D4A62992D02B00A9A7E4 /* ProfileNameView.swift */; };
- 4CBB6F662B72B5DD000477A4 /* NdbBlocksIterator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF480562B633F2600F2B2C0 /* NdbBlocksIterator.swift */; };
4CBB6F672B72B5E8000477A4 /* NdbBlock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF480582B633F3800F2B2C0 /* NdbBlock.swift */; };
4CBB6F682B72B5F0000477A4 /* NdbProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF47FFE2B631C0100F2B2C0 /* NdbProfile.swift */; };
- 4CBB6F692B72C783000477A4 /* NdbBlocksIterator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF480562B633F2600F2B2C0 /* NdbBlocksIterator.swift */; };
4CBB6F6A2B730EF1000477A4 /* nostrdb.c in Sources */ = {isa = PBXBuildFile; fileRef = 4CF47FDE2B631C0100F2B2C0 /* nostrdb.c */; };
4CBB6F6E2B731113000477A4 /* block.c in Sources */ = {isa = PBXBuildFile; fileRef = 4CF47FDF2B631C0100F2B2C0 /* block.c */; };
4CBB6F6F2B73116B000477A4 /* content_parser.c in Sources */ = {isa = PBXBuildFile; fileRef = 4CF47FF62B631C0100F2B2C0 /* content_parser.c */; };
@@ -1558,15 +1554,15 @@
D74AAFD22B155E78006CF0F4 /* WalletConnect.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C7D09612A098D0E00943473 /* WalletConnect.swift */; };
D74AAFD42B155ECB006CF0F4 /* Zaps+.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74AAFD32B155ECB006CF0F4 /* Zaps+.swift */; };
D74AAFD62B155F0C006CF0F4 /* WalletConnect+.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74AAFD52B155F0C006CF0F4 /* WalletConnect+.swift */; };
- D74E64132DC95CC7004C7892 /* HumanReadableErrors.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74E64112DC95CBE004C7892 /* HumanReadableErrors.swift */; };
- D74E64142DC95CC7004C7892 /* HumanReadableErrors.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74E64112DC95CBE004C7892 /* HumanReadableErrors.swift */; };
- D74E64152DC95CC7004C7892 /* HumanReadableErrors.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74E64112DC95CBE004C7892 /* HumanReadableErrors.swift */; };
D74DEC8A2DA0A19B00E69FA6 /* Ndb+.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74DEC892DA0A19800E69FA6 /* Ndb+.swift */; };
D74DEC8B2DA0A19B00E69FA6 /* Ndb+.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74DEC892DA0A19800E69FA6 /* Ndb+.swift */; };
D74DEC8C2DA0A19B00E69FA6 /* Ndb+.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74DEC892DA0A19800E69FA6 /* Ndb+.swift */; };
D74DEC8F2DA0C65F00E69FA6 /* Ndb+.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74DEC892DA0A19800E69FA6 /* Ndb+.swift */; };
D74DEC902DA0C6B500E69FA6 /* NostrFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C75EFAE28049D340006080F /* NostrFilter.swift */; };
D74DEC912DA0CA2400E69FA6 /* Array.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72E12772BEED22400F4F781 /* Array.swift */; };
+ D74E64132DC95CC7004C7892 /* HumanReadableErrors.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74E64112DC95CBE004C7892 /* HumanReadableErrors.swift */; };
+ D74E64142DC95CC7004C7892 /* HumanReadableErrors.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74E64112DC95CBE004C7892 /* HumanReadableErrors.swift */; };
+ D74E64152DC95CC7004C7892 /* HumanReadableErrors.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74E64112DC95CBE004C7892 /* HumanReadableErrors.swift */; };
D74EA08A2D2BF2A7002290DD /* URLHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D767066E2C8BB3CE00F09726 /* URLHandler.swift */; };
D74EA08E2D2E271E002290DD /* ErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74EA08D2D2E271E002290DD /* ErrorView.swift */; };
D74EA08F2D2E271E002290DD /* ErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74EA08D2D2E271E002290DD /* ErrorView.swift */; };
@@ -1782,13 +1778,13 @@
D7F360262CEBBD8B009D34DA /* PresentFullScreenItemNotify.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7EB00AF2CD59C8300660C07 /* PresentFullScreenItemNotify.swift */; };
D7F360272CEBBDC0009D34DA /* DamusVideoControlsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7EFBA362CC322F300F45588 /* DamusVideoControlsView.swift */; };
D7F360292CEBBE34009D34DA /* CodeScanner in Frameworks */ = {isa = PBXBuildFile; productRef = D7F360282CEBBE34009D34DA /* CodeScanner */; };
- D7FA46E52DBDAA7E002C9BB0 /* ImageCacheMigrations.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7FA46E42DBDAA75002C9BB0 /* ImageCacheMigrations.swift */; };
- D7FA46E62DBDAA7E002C9BB0 /* ImageCacheMigrations.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7FA46E42DBDAA75002C9BB0 /* ImageCacheMigrations.swift */; };
- D7FA46E72DBDAA7E002C9BB0 /* ImageCacheMigrations.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7FA46E42DBDAA75002C9BB0 /* ImageCacheMigrations.swift */; };
D7F563102DEE71C0008509DE /* NdbFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7F5630F2DEE71BB008509DE /* NdbFilter.swift */; };
D7F563112DEE71C0008509DE /* NdbFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7F5630F2DEE71BB008509DE /* NdbFilter.swift */; };
D7F563122DEE71C0008509DE /* NdbFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7F5630F2DEE71BB008509DE /* NdbFilter.swift */; };
D7F563132DEE71C0008509DE /* NdbFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7F5630F2DEE71BB008509DE /* NdbFilter.swift */; };
+ D7FA46E52DBDAA7E002C9BB0 /* ImageCacheMigrations.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7FA46E42DBDAA75002C9BB0 /* ImageCacheMigrations.swift */; };
+ D7FA46E62DBDAA7E002C9BB0 /* ImageCacheMigrations.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7FA46E42DBDAA75002C9BB0 /* ImageCacheMigrations.swift */; };
+ D7FA46E72DBDAA7E002C9BB0 /* ImageCacheMigrations.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7FA46E42DBDAA75002C9BB0 /* ImageCacheMigrations.swift */; };
D7FB10A72B0C371A00FA8D42 /* Log.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C2B10272A7B0F5C008AA43E /* Log.swift */; };
D7FB14222BE5970000398331 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = D7FB14212BE5970000398331 /* PrivacyInfo.xcprivacy */; };
D7FB14252BE5A9A800398331 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = D7FB14242BE5A9A800398331 /* PrivacyInfo.xcprivacy */; };
@@ -2484,7 +2480,6 @@
4CF480372B631C0100F2B2C0 /* invoice.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = invoice.c; sourceTree = "<group>"; };
4CF480532B631C4F00F2B2C0 /* wasm.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = wasm.c; sourceTree = "<group>"; };
4CF480542B631C4F00F2B2C0 /* wasm.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = wasm.h; sourceTree = "<group>"; };
- 4CF480562B633F2600F2B2C0 /* NdbBlocksIterator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NdbBlocksIterator.swift; sourceTree = "<group>"; };
4CF480582B633F3800F2B2C0 /* NdbBlock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NdbBlock.swift; sourceTree = "<group>"; };
4CFD502E2A2DA45800A229DB /* MediaView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaView.swift; sourceTree = "<group>"; };
4CFF8F5829C9FD1E008DB934 /* DamusPurpleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusPurpleView.swift; sourceTree = "<group>"; };
@@ -2628,8 +2623,8 @@
D74AAFCE2B155D8C006CF0F4 /* ZapDataModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZapDataModel.swift; sourceTree = "<group>"; };
D74AAFD32B155ECB006CF0F4 /* Zaps+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Zaps+.swift"; sourceTree = "<group>"; };
D74AAFD52B155F0C006CF0F4 /* WalletConnect+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WalletConnect+.swift"; sourceTree = "<group>"; };
- D74E64112DC95CBE004C7892 /* HumanReadableErrors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HumanReadableErrors.swift; sourceTree = "<group>"; };
D74DEC892DA0A19800E69FA6 /* Ndb+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Ndb+.swift"; sourceTree = "<group>"; };
+ D74E64112DC95CBE004C7892 /* HumanReadableErrors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HumanReadableErrors.swift; sourceTree = "<group>"; };
D74EA08D2D2E271E002290DD /* ErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorView.swift; sourceTree = "<group>"; };
D74EA0922D2E77B9002290DD /* LoadableNostrEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadableNostrEventView.swift; sourceTree = "<group>"; };
D74F43092B23F0BE00425B75 /* DamusPurple.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusPurple.swift; sourceTree = "<group>"; };
@@ -2694,8 +2689,8 @@
D7EDED2D2B128E8A0018B19C /* CollectionExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionExtension.swift; sourceTree = "<group>"; };
D7EDED322B12ACAE0018B19C /* DamusUserDefaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusUserDefaults.swift; sourceTree = "<group>"; };
D7EFBA362CC322F300F45588 /* DamusVideoControlsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusVideoControlsView.swift; sourceTree = "<group>"; };
- D7FA46E42DBDAA75002C9BB0 /* ImageCacheMigrations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageCacheMigrations.swift; sourceTree = "<group>"; };
D7F5630F2DEE71BB008509DE /* NdbFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NdbFilter.swift; sourceTree = "<group>"; };
+ D7FA46E42DBDAA75002C9BB0 /* ImageCacheMigrations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageCacheMigrations.swift; sourceTree = "<group>"; };
D7FB14212BE5970000398331 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
D7FB14242BE5A9A800398331 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
D7FD12252BD345A700CF195B /* FirstAidSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirstAidSettingsView.swift; sourceTree = "<group>"; };
@@ -3183,7 +3178,6 @@
4C78EFDA2A707C67007E8197 /* secp256k1_extrakeys.h */,
4C78EFD92A707C4D007E8197 /* secp256k1.h */,
D798D2272B085CDA00234419 /* NdbNote+.swift */,
- 4CF480562B633F2600F2B2C0 /* NdbBlocksIterator.swift */,
4CF480582B633F3800F2B2C0 /* NdbBlock.swift */,
);
path = nostrdb;
@@ -5384,7 +5378,6 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
- 4CBB6F692B72C783000477A4 /* NdbBlocksIterator.swift in Sources */,
4C3DCC762A9FE9EC0091E592 /* NdbTxn.swift in Sources */,
4CEF958D2A9CE650000F901B /* verifier.c in Sources */,
D73BDB0E2D6FF5F600D69970 /* NostrNetworkManager.swift in Sources */,
@@ -5998,7 +5991,6 @@
4C3624632D5EA01100DD066E /* block.c in Sources */,
4C3624622D5EA00300DD066E /* nostrdb.c in Sources */,
4C3624612D5E9FFD00DD066E /* wasm.c in Sources */,
- 4C36245C2D5E9B4400DD066E /* NdbBlocksIterator.swift in Sources */,
4C36245D2D5E9B4400DD066E /* NdbBlock.swift in Sources */,
4C36245B2D5E9B2F00DD066E /* NdbProfile.swift in Sources */,
D7F360262CEBBD8B009D34DA /* PresentFullScreenItemNotify.swift in Sources */,
@@ -6508,7 +6500,6 @@
4C3624742D5EA1D700DD066E /* wasm.c in Sources */,
4C3624732D5EA1BE00DD066E /* nostrdb.c in Sources */,
4C3624602D5E9EB800DD066E /* NdbProfile.swift in Sources */,
- 4C36245E2D5E9B5F00DD066E /* NdbBlocksIterator.swift in Sources */,
4C36245F2D5E9B5F00DD066E /* NdbBlock.swift in Sources */,
D73E5E202C6A97F4007EB227 /* AttachedWalletNotify.swift in Sources */,
D73E5E212C6A97F4007EB227 /* DisplayTabBarNotify.swift in Sources */,
@@ -7035,7 +7026,6 @@
4CBB6F6A2B730EF1000477A4 /* nostrdb.c in Sources */,
4CBB6F682B72B5F0000477A4 /* NdbProfile.swift in Sources */,
4CBB6F672B72B5E8000477A4 /* NdbBlock.swift in Sources */,
- 4CBB6F662B72B5DD000477A4 /* NdbBlocksIterator.swift in Sources */,
D74DEC8F2DA0C65F00E69FA6 /* Ndb+.swift in Sources */,
D798D21F2B0858D600234419 /* MigratedTypes.swift in Sources */,
D7CE1B472B0BE719002EDAD4 /* NativeObject.swift in Sources */,
diff --git a/damus/Core/Nostr/NostrEvent.swift b/damus/Core/Nostr/NostrEvent.swift
@@ -780,12 +780,11 @@ func validate_event(ev: NostrEvent) -> ValidationResult {
}
func first_eref_mention(ndb: Ndb, ev: NostrEvent, keypair: Keypair) -> Mention<NoteId>? {
- guard let blocks_txn = ev.blocks(ndb: ndb) else {
+ guard let blockGroup = try? NdbBlockGroup.from(event: ev, using: ndb, and: keypair) else {
return nil
}
- let ndb_blocks = blocks_txn.unsafeUnownedValue
- let blocks = ndb_blocks.iter(note: ev).filter { block in
+ let blocks = blockGroup.blocks.filter { block in
guard case .mention(let mention) = block else {
return false
}
@@ -815,12 +814,11 @@ func first_eref_mention(ndb: Ndb, ev: NostrEvent, keypair: Keypair) -> Mention<N
return nil
}
-func separate_invoices(ndb: Ndb, ev: NostrEvent) -> [Invoice]? {
- guard let blocks_txn = ev.blocks(ndb: ndb) else {
+func separate_invoices(ndb: Ndb, ev: NostrEvent, keypair: Keypair) -> [Invoice]? {
+ guard let blockGroup = try? NdbBlockGroup.from(event: ev, using: ndb, and: keypair) else {
return nil
}
- let ndb_blocks = blocks_txn.unsafeUnownedValue
- let invoiceBlocks: [Invoice] = ndb_blocks.iter(note: ev).reduce(into: []) { invoices, block in
+ let invoiceBlocks: [Invoice] = blockGroup.blocks.reduce(into: []) { invoices, block in
guard case .invoice(let invoice) = block,
let invoice = invoice.as_invoice()
else {
diff --git a/damus/Features/Events/Models/NoteContent.swift b/damus/Features/Events/Models/NoteContent.swift
@@ -67,22 +67,28 @@ func note_artifact_is_separated(kind: NostrKind?) -> Bool {
}
func render_immediately_available_note_content(ndb: Ndb, ev: NostrEvent, profiles: Profiles, keypair: Keypair) -> NoteArtifacts {
- guard let blocks = ev.blocks(ndb: ndb) else {
- return .separated(.just_content(ev.get_content(keypair)))
- }
-
if ev.known_kind == .longform {
return .longform(LongformContent(ev.content))
}
- return .separated(render_blocks(blocks: blocks.unsafeUnownedValue, profiles: profiles, note: ev, can_hide_last_previewable_refs: true))
+ do {
+ let blocks = try NdbBlockGroup.from(event: ev, using: ndb, and: keypair)
+ return .separated(render_blocks(blocks: blocks, profiles: profiles, can_hide_last_previewable_refs: true))
+ }
+ 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.
+ return .separated(.just_content(ev.get_content(keypair)))
+ }
}
actor ContentRenderer {
func render_note_content(ndb: Ndb, ev: NostrEvent, profiles: Profiles, keypair: Keypair) async -> NoteArtifacts {
- guard let result = try? await ndb.waitFor(noteId: ev.id, timeout: 10) else {
- return .separated(.just_content(ev.get_content(keypair)))
+ if ev.known_kind == .dm {
+ // Use the enhanced render_immediately_available_note_content which now handles DMs properly
+ // by decrypting and parsing the content with ndb_parse_content
+ return render_immediately_available_note_content(ndb: ndb, ev: ev, profiles: profiles, keypair: keypair)
}
+ let result = try? await ndb.waitFor(noteId: ev.id, timeout: 3)
return render_immediately_available_note_content(ndb: ndb, ev: ev, profiles: profiles, keypair: keypair)
}
}
@@ -92,14 +98,14 @@ actor ContentRenderer {
// Block previews should actually be rendered in the position of the note content where it was found.
// Currently, we put some previews at the bottom of the note, which is incorrect as they take things out of
// the author's intended context.
-func render_blocks(blocks: NdbBlocks, profiles: Profiles, note: NdbNote, can_hide_last_previewable_refs: Bool = false) -> NoteArtifactsSeparated {
+func render_blocks(blocks: borrowing NdbBlockGroup, profiles: Profiles, can_hide_last_previewable_refs: Bool = false) -> NoteArtifactsSeparated {
var invoices: [Invoice] = []
var urls: [UrlType] = []
var end_mention_count = 0
var end_url_count = 0
- let ndb_blocks = blocks.iter(note: note).collect()
+ let ndb_blocks = blocks.blocks
let one_note_ref = ndb_blocks
.filter({
if case .mention(let mention) = $0,
diff --git a/damus/Features/Events/NoteContentView.swift b/damus/Features/Events/NoteContentView.swift
@@ -350,11 +350,10 @@ struct NoteContentView: View {
var body: some View {
ArtifactContent
.onReceive(handle_notify(.profile_updated)) { profile in
- guard let blocks_txn = event.blocks(ndb: damus_state.ndb) else {
+ guard let blockGroup = try? NdbBlockGroup.from(event: event, using: damus_state.ndb, and: damus_state.keypair) else {
return
}
- let blocks = blocks_txn.unsafeUnownedValue
- for block in blocks.iter(note: event) {
+ for block in blockGroup.blocks {
switch block {
case .mention(let m):
guard let typ = m.bech32_type else {
@@ -536,11 +535,10 @@ struct NoteContentView_Previews: PreviewProvider {
}
func separate_images(ndb: Ndb, ev: NostrEvent, keypair: Keypair) -> [MediaUrl]? {
- guard let blocks_txn = ev.blocks(ndb: ndb) else {
+ guard let blockGroup = try? NdbBlockGroup.from(event: ev, using: ndb, and: keypair) else {
return nil
}
- let blocks = blocks_txn.unsafeUnownedValue
- let urlBlocks: [URL] = blocks.iter(note: ev).reduce(into: []) { urls, block in
+ let urlBlocks: [URL] = blockGroup.blocks.reduce(into: []) { urls, block in
guard case .url(let url) = block,
let parsed_url = URL(string: url.as_str()) else {
return
diff --git a/damus/Features/Notifications/Models/NotificationsManager.swift b/damus/Features/Notifications/Models/NotificationsManager.swift
@@ -72,9 +72,9 @@ func generate_local_notification_object(ndb: Ndb, from ev: NostrEvent, state: He
if type == .text,
state.settings.mention_notification,
- let blocks = ev.blocks(ndb: ndb)?.unsafeUnownedValue
+ let blockGroup = try? NdbBlockGroup.from(event: ev, using: ndb, and: state.keypair)
{
- for case .mention(let mention) in blocks.iter(note: ev) {
+ for case .mention(let mention) in blockGroup.blocks {
guard case .npub = mention.bech32_type,
(memcmp(state.keypair.pubkey.id.bytes, mention.bech32.npub.pubkey, 32) == 0) else {
continue
diff --git a/damusTests/NoteContentViewTests.swift b/damusTests/NoteContentViewTests.swift
@@ -342,6 +342,20 @@ class NoteContentViewTests: XCTestCase {
XCTAssertTrue((parsed.blocks[0].asURL != nil), "NoteContentView does not correctly parse an image block when url in JSON content contains optional escaped slashes.")
}
+ /// Quick test that exercises the direct parsing methods (i.e. not fetching blocks from nostrdb) from `NdbBlockGroup`, and its bridging code with C.
+ /// The parsing logic itself already has test coverage at the nostrdb level.
+ func testDirectBlockParsing() {
+ let kp = test_keypair_full
+ 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())
+ XCTAssertEqual(blocks.blocks.count, 1)
+
+ let post = NostrPost(content: "Test", kind: .text)
+ let event = post.to_event(keypair: kp)!
+ let blocks2 = try! NdbBlockGroup.from(event: event, using: test_damus_state.ndb, and: kp.to_keypair())
+ XCTAssertEqual(blocks2.blocks.count, 1)
+ }
+
func testMentionStr_Pubkey_ContainsAbbreviated() throws {
let compatibleText = createCompatibleText(test_pubkey.npub)
diff --git a/nostrdb/Ndb.swift b/nostrdb/Ndb.swift
@@ -227,16 +227,16 @@ class Ndb {
return true
}
- func lookup_blocks_by_key_with_txn<Y>(_ key: NoteKey, txn: NdbTxn<Y>) -> NdbBlocks? {
+ 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 {
return nil
}
- return NdbBlocks(ptr: blocks)
+ return NdbBlockGroup.BlocksMetadata(ptr: blocks)
}
- func lookup_blocks_by_key(_ key: NoteKey) -> NdbTxn<NdbBlocks?>? {
- NdbTxn(ndb: self) { txn in
+ func lookup_blocks_by_key(_ key: NoteKey) -> SafeNdbTxn<NdbBlockGroup.BlocksMetadata?>? {
+ SafeNdbTxn<NdbBlockGroup.BlocksMetadata?>.new(on: self) { txn in
lookup_blocks_by_key_with_txn(key, txn: txn)
}
}
@@ -493,7 +493,7 @@ class Ndb {
}
}
- func lookup_note_key_with_txn<Y>(_ id: NoteId, txn: NdbTxn<Y>) -> NoteKey? {
+ func lookup_note_key_with_txn(_ id: NoteId, txn: some RawNdbTxnAccessible) -> NoteKey? {
guard !closed else { return nil }
return id.id.withUnsafeBytes { (ptr: UnsafeRawBufferPointer) -> NoteKey? in
guard let p = ptr.baseAddress else {
diff --git a/nostrdb/NdbBlock.swift b/nostrdb/NdbBlock.swift
@@ -93,31 +93,180 @@ enum NdbBlock {
guard let cString = block.str else {
return nil
}
+ // Copy byte-by-byte from the pointer into a new buffer
let byteBuffer = UnsafeBufferPointer(start: cString, count: Int(block.len)).map { UInt8(bitPattern: $0) }
- // Create a Swift String from the byte array
+ // Create an owned Swift String from the buffer we created
return String(bytes: byteBuffer, encoding: .utf8)
}
}
-
-struct NdbBlocks {
- private let blocks_ptr: ndb_blocks_ptr
-
- init(ptr: OpaquePointer?) {
- self.blocks_ptr = ndb_blocks_ptr(ptr: ptr)
+/// Represents a group of blocks
+struct NdbBlockGroup: ~Copyable {
+ /// The block offsets
+ fileprivate let metadata: MaybeTxn<BlocksMetadata>
+ /// The raw text content of the note
+ fileprivate let rawTextContent: String
+ /// An iterable list of blocks that make up this object
+ var blocks: [NdbBlock] {
+ return self.collectBlocks()
}
-
var words: Int {
- Int(ndb_blocks_word_count(blocks_ptr.ptr))
+ return metadata.borrow { $0.words }
}
+
+ /// Gets the parsed blocks from a specific note.
+ ///
+ /// This function will:
+ /// - fetch blocks information from NostrDB if possible _and_ available, or
+ /// - parse blocks on-demand.
+ static func from(event: NdbNote, using ndb: Ndb, and keypair: Keypair) throws(NdbBlocksError) -> Self {
+ if event.is_content_encrypted() {
+ return try parse(event: event, keypair: keypair)
+ }
+ else {
+ guard let offsets = event.block_offsets(ndb: ndb) else {
+ return try parse(event: event, keypair: keypair)
+ }
+ return .init(metadata: .txn(offsets), rawTextContent: event.content)
+ }
+ }
+
+ /// Parses the note contents on-demand from a specific note.
+ ///
+ /// Prioritize using `from(event: NdbNote, using ndb: Ndb, and keypair: Keypair)` when possible.
+ static func parse(event: NdbNote, keypair: Keypair) throws(NdbBlocksError) -> Self {
+ guard let content = event.maybe_get_content(keypair) else { throw NdbBlocksError.decryptionError }
+ guard let metadata = BlocksMetadata.parseContent(content: content) else { throw NdbBlocksError.parseError }
+ return self.init(
+ metadata: .pure(metadata),
+ rawTextContent: content
+ )
+ }
+}
- func iter(note: NdbNote) -> BlocksSequence {
- BlocksSequence(note: note, blocks: self)
+enum MaybeTxn<T: ~Copyable>: ~Copyable {
+ case pure(T)
+ case txn(SafeNdbTxn<T>)
+
+ func borrow<Y>(_ borrowFunction: (borrowing T) -> Y) -> Y {
+ switch self {
+ case .pure(let item):
+ return borrowFunction(item)
+ case .txn(let txn):
+ return borrowFunction(txn.val)
+ }
}
+}
+
- func as_ptr() -> OpaquePointer? {
- return self.blocks_ptr.ptr
+// MARK: - Helper structs
+
+extension NdbBlockGroup {
+ /// 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.
+ ///
+ /// **Implementation note:** This would be better as `~Copyable`, but `NdbTxn` does not support `~Copyable` yet.
+ struct BlocksMetadata: ~Copyable {
+ private let blocks_ptr: ndb_blocks_ptr
+ private let buffer: UnsafeMutableRawPointer?
+
+ init(ptr: OpaquePointer?, buffer: UnsafeMutableRawPointer? = nil) {
+ self.blocks_ptr = ndb_blocks_ptr(ptr: ptr)
+ self.buffer = buffer
+ }
+
+ var words: Int {
+ Int(ndb_blocks_word_count(blocks_ptr.ptr))
+ }
+
+ /// Gets the opaque pointer
+ ///
+ /// **Implementation note:** This is marked `fileprivate` because we want to minimize the exposure of raw pointers to Swift code outside these wrapper structs.
+ fileprivate func as_ptr() -> OpaquePointer? {
+ return self.blocks_ptr.ptr
+ }
+
+ /// Parses text content and returns the parsed block metadata if successful
+ ///
+ /// **Implementation notes:** This is `fileprivate` because it makes no sense for outside Swift code to use this directly. Use `NdbBlockGroup` instead.
+ fileprivate static func parseContent(content: String) -> Self? {
+ // Allocate scratch buffer with enough space
+ guard let buffer = malloc(MAX_NOTE_SIZE) else {
+ return nil
+ }
+
+ var blocks: OpaquePointer? = nil
+
+ // Call the C parsing function and check its success status
+ let success = content.withCString { contentPtr -> Bool in
+ let contentLen = content.utf8.count
+ return ndb_parse_content(
+ buffer.assumingMemoryBound(to: UInt8.self),
+ Int32(MAX_NOTE_SIZE),
+ contentPtr,
+ Int32(contentLen),
+ &blocks
+ ) == 1
+ }
+
+ if !success || blocks == nil {
+ // Something failed
+ free(buffer)
+ return nil
+ }
+
+ // TODO: We should set the owned flag as in the C code.
+ // However, There does not seem to be a way to set this from Swift code. The code shown below does not work.
+ // blocks!.pointee.flags |= NDB_BLOCK_FLAG_OWNED
+ // But perhaps this is not necessary because `NdbBlockGroup` is non-copyable
+
+ return BlocksMetadata(ptr: blocks, buffer: buffer)
+ }
+
+ deinit {
+ if let buffer {
+ free(buffer)
+ }
+ }
+ }
+
+ /// Models specific errors that may happen when parsing or constructing an `NdbBlocks` object
+ enum NdbBlocksError: Error {
+ case parseError
+ case decryptionError
+ }
+}
+
+extension NdbBlockGroup {
+ /// Collects all blocks in the group into an array without using Iterator/Sequence protocols
+ ///
+ /// **Implementation note:**
+ /// This is done as a function instead of using `Sequence` and `Iterator` protocols because it does seem to be possible to conform to both `Sequence` and `~Copyable` at the same time.
+ ///
+ /// - Returns: An array of all blocks in the group
+ fileprivate func collectBlocks() -> [NdbBlock] {
+ var blocks = [NdbBlock]()
+
+ // Ensure the C string remains valid for the entire operation by keeping
+ // all operations using it within the withCString closure
+ self.rawTextContent.withCString { cptr in
+ var iter = ndb_block_iterator(content: cptr, blocks: nil, block: ndb_block(), p: nil)
+
+ // Start the iteration
+ self.metadata.borrow { value in
+ ndb_blocks_iterate_start(cptr, value.as_ptr(), &iter)
+ }
+
+ // Collect blocks into array
+ while let ptr = ndb_blocks_iterate_next(&iter),
+ let block = NdbBlock(ndb_block_ptr(ptr: ptr)) {
+ blocks.append(block)
+ }
+ }
+
+ return blocks
}
}
diff --git a/nostrdb/NdbBlocksIterator.swift b/nostrdb/NdbBlocksIterator.swift
@@ -1,59 +0,0 @@
-//
-// NdbBlockIterator.swift
-// damus
-//
-// Created by William Casarin on 2024-01-25.
-//
-
-import Foundation
-
-
-struct BlocksIterator: IteratorProtocol {
- typealias Element = NdbBlock
-
- var done: Bool
- var iter: ndb_block_iterator
- var note: NdbNote
-
- mutating func next() -> NdbBlock? {
- guard iter.blocks != nil,
- let ptr = ndb_blocks_iterate_next(&iter) else {
- done = true
- return nil
- }
-
- let block_ptr = ndb_block_ptr(ptr: ptr)
- return NdbBlock(block_ptr)
- }
-
- init(note: NdbNote, blocks: NdbBlocks) {
- let content = ndb_note_content(note.note.ptr)
- self.iter = ndb_block_iterator(content: content, blocks: nil, block: ndb_block(), p: nil)
- ndb_blocks_iterate_start(content, blocks.as_ptr(), &self.iter)
- self.done = false
- self.note = note
- }
-}
-
-struct BlocksSequence: Sequence {
- let blocks: NdbBlocks
- let note: NdbNote
-
- init(note: NdbNote, blocks: NdbBlocks) {
- self.blocks = blocks
- self.note = note
- }
-
- func makeIterator() -> BlocksIterator {
- return .init(note: note, blocks: blocks)
- }
-
- func collect() -> [NdbBlock] {
- var xs = [NdbBlock]()
- for x in self {
- xs.append(x)
- }
- return xs
- }
-}
-
diff --git a/nostrdb/NdbNote.swift b/nostrdb/NdbNote.swift
@@ -457,23 +457,25 @@ extension NdbNote {
return ThreadReply(tags: self.tags)?.reply.note_id
}
- func blocks(ndb: Ndb) -> NdbTxn<NdbBlocks>? {
- let blocks_txn = NdbTxn<NdbBlocks?>(ndb: ndb) { txn in
+ func block_offsets(ndb: Ndb) -> SafeNdbTxn<NdbBlockGroup.BlocksMetadata>? {
+ let blocks_txn: SafeNdbTxn<NdbBlockGroup.BlocksMetadata>? = .new(on: ndb) { txn -> NdbBlockGroup.BlocksMetadata? in
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
- }
+ guard let blocks_txn else { return nil }
- return blocks_txn.collect()
+ return blocks_txn
+ }
+
+ func is_content_encrypted() -> Bool {
+ return known_kind == .dm // Probably other kinds should be listed here
}
func get_content(_ keypair: Keypair) -> String {
- if known_kind == .dm {
+ if is_content_encrypted() {
return decrypted(keypair: keypair) ?? "*failed to decrypt content*"
}
else if known_kind == .highlight {
@@ -484,7 +486,7 @@ extension NdbNote {
}
func maybe_get_content(_ keypair: Keypair) -> String? {
- if known_kind == .dm {
+ if is_content_encrypted() {
return decrypted(keypair: keypair)
}
diff --git a/nostrdb/NdbTxn.swift b/nostrdb/NdbTxn.swift
@@ -12,7 +12,7 @@ fileprivate var txn_count: Int = 0
#endif
// Would use struct and ~Copyable but generics aren't supported well
-class NdbTxn<T> {
+class NdbTxn<T>: RawNdbTxnAccessible {
var txn: ndb_txn
private var val: T!
var moved: Bool
@@ -117,6 +117,129 @@ class NdbTxn<T> {
}
}
+protocol RawNdbTxnAccessible: AnyObject {
+ var txn: ndb_txn { get set }
+}
+
+class PlaceholderNdbTxn: RawNdbTxnAccessible {
+ var txn: ndb_txn
+
+ init(txn: ndb_txn) {
+ self.txn = txn
+ }
+}
+
+class SafeNdbTxn<T: ~Copyable> {
+ var txn: ndb_txn
+ var val: T!
+ var moved: Bool
+ var inherited: Bool
+ var ndb: Ndb
+ var generation: Int
+ var name: String
+
+ static func pure(ndb: Ndb, val: consuming T) -> SafeNdbTxn<T> {
+ .init(ndb: ndb, txn: ndb_txn(), val: val, generation: ndb.generation, inherited: true, name: "pure_txn")
+ }
+
+ static func new(on ndb: Ndb, with valueGetter: (PlaceholderNdbTxn) -> T? = { _ in () }, name: String = "txn") -> SafeNdbTxn<T>? {
+ guard !ndb.is_closed else { return nil }
+ var generation = ndb.generation
+ var txn: ndb_txn
+ let inherited: Bool
+ if let active_txn = Thread.current.threadDictionary["ndb_txn"] as? ndb_txn {
+ // some parent thread is active, use that instead
+ print("txn: inherited txn")
+ txn = active_txn
+ inherited = true
+ generation = Thread.current.threadDictionary["txn_generation"] as! Int
+ } else {
+ txn = ndb_txn()
+ guard !ndb.is_closed else { return nil }
+ generation = ndb.generation
+ #if TXNDEBUG
+ txn_count += 1
+ #endif
+ let ok = ndb_begin_query(ndb.ndb.ndb, &txn) != 0
+ if !ok {
+ return nil
+ }
+ generation = ndb.generation
+ Thread.current.threadDictionary["ndb_txn"] = txn
+ Thread.current.threadDictionary["txn_generation"] = ndb.generation
+ inherited = false
+ }
+ #if TXNDEBUG
+ print("txn: open gen\(self.generation) '\(self.name)' \(txn_count)")
+ #endif
+ let moved = false
+ let placeholderTxn = PlaceholderNdbTxn(txn: txn)
+ guard let val = valueGetter(placeholderTxn) else { return nil }
+ return SafeNdbTxn<T>(ndb: ndb, txn: txn, val: val, generation: generation, inherited: inherited, name: name)
+ }
+
+ private init(ndb: Ndb, txn: ndb_txn, val: consuming T, generation: Int, inherited: Bool, name: String) {
+ self.txn = txn
+ self.val = consume val
+ self.moved = false
+ self.inherited = inherited
+ self.ndb = ndb
+ self.generation = generation
+ self.name = name
+ }
+
+ deinit {
+ if self.generation != ndb.generation {
+ print("txn: OLD GENERATION (\(self.generation) != \(ndb.generation)), IGNORING")
+ return
+ }
+ if inherited {
+ print("txn: not closing. inherited ")
+ return
+ }
+ if moved {
+ //print("txn: not closing. moved")
+ return
+ }
+ if ndb.is_closed {
+ print("txn: not closing. db closed")
+ return
+ }
+
+ #if TXNDEBUG
+ txn_count -= 1;
+ print("txn: close gen\(generation) '\(name)' \(txn_count)")
+ #endif
+ ndb_end_query(&self.txn)
+ //self.skip_close = true
+ Thread.current.threadDictionary.removeObject(forKey: "ndb_txn")
+ }
+
+ // functor
+ func map<Y>(_ transform: (borrowing T) -> Y) -> SafeNdbTxn<Y> {
+ self.moved = true
+ return .init(ndb: self.ndb, txn: self.txn, val: transform(val), generation: generation, inherited: inherited, name: self.name)
+ }
+
+ // comonad!?
+ // useful for moving ownership of a transaction to another value
+ func extend<Y>(_ with: (SafeNdbTxn<T>) -> Y) -> SafeNdbTxn<Y> {
+ self.moved = true
+ return .init(ndb: self.ndb, txn: self.txn, val: with(self), generation: generation, inherited: inherited, name: self.name)
+ }
+
+ consuming func maybeExtend<Y>(_ with: (consuming SafeNdbTxn<T>) -> Y?) -> SafeNdbTxn<Y>? where Y: ~Copyable {
+ self.moved = true
+ let ndb = self.ndb
+ let txn = self.txn
+ let generation = self.generation
+ let inherited = self.inherited
+ let name = self.name
+ guard let newVal = with(consume self) else { return nil }
+ return .init(ndb: ndb, txn: txn, val: newVal, generation: generation, inherited: inherited, name: name)
+ }
+}
+
protocol OptionalType {
associatedtype Wrapped
var optional: Wrapped? { get }