Make NdbBlock ~Copyable for better lifetime safety
Changelog-None Closes: https://github.com/damus-io/damus/issues/3127 Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
This commit is contained in:
@@ -1571,6 +1571,10 @@
|
||||
D74EA0932D2E77B9002290DD /* LoadableNostrEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74EA0922D2E77B9002290DD /* LoadableNostrEventView.swift */; };
|
||||
D74EA0942D2E77B9002290DD /* LoadableNostrEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74EA0922D2E77B9002290DD /* LoadableNostrEventView.swift */; };
|
||||
D74EA0952D2E77B9002290DD /* LoadableNostrEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74EA0922D2E77B9002290DD /* LoadableNostrEventView.swift */; };
|
||||
D74EC84F2E1856B70091DC51 /* NonCopyableLinkedList.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74EC84E2E1856AF0091DC51 /* NonCopyableLinkedList.swift */; };
|
||||
D74EC8502E1856B70091DC51 /* NonCopyableLinkedList.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74EC84E2E1856AF0091DC51 /* NonCopyableLinkedList.swift */; };
|
||||
D74EC8512E1856B70091DC51 /* NonCopyableLinkedList.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74EC84E2E1856AF0091DC51 /* NonCopyableLinkedList.swift */; };
|
||||
D74EC8522E1856B70091DC51 /* NonCopyableLinkedList.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74EC84E2E1856AF0091DC51 /* NonCopyableLinkedList.swift */; };
|
||||
D74F430A2B23F0BE00425B75 /* DamusPurple.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74F43092B23F0BE00425B75 /* DamusPurple.swift */; };
|
||||
D74F430C2B23FB9B00425B75 /* StoreObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74F430B2B23FB9B00425B75 /* StoreObserver.swift */; };
|
||||
D753CEAA2BE9DE04001C3A5D /* MutingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D753CEA92BE9DE04001C3A5D /* MutingTests.swift */; };
|
||||
@@ -2627,6 +2631,7 @@
|
||||
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>"; };
|
||||
D74EC84E2E1856AF0091DC51 /* NonCopyableLinkedList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NonCopyableLinkedList.swift; sourceTree = "<group>"; };
|
||||
D74F43092B23F0BE00425B75 /* DamusPurple.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusPurple.swift; sourceTree = "<group>"; };
|
||||
D74F430B2B23FB9B00425B75 /* StoreObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreObserver.swift; sourceTree = "<group>"; };
|
||||
D753CEA92BE9DE04001C3A5D /* MutingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MutingTests.swift; sourceTree = "<group>"; };
|
||||
@@ -3154,6 +3159,7 @@
|
||||
4C9054862A6AEB4500811EEC /* nostrdb */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D74EC84E2E1856AF0091DC51 /* NonCopyableLinkedList.swift */,
|
||||
D733F9E42D92C75C00317B11 /* UnownedNdbNote.swift */,
|
||||
D7F5630F2DEE71BB008509DE /* NdbFilter.swift */,
|
||||
D74DEC892DA0A19800E69FA6 /* Ndb+.swift */,
|
||||
@@ -5764,6 +5770,7 @@
|
||||
4C3BEFD6281D995700B3DE84 /* ActionBarModel.swift in Sources */,
|
||||
4C7D09762A0AF19E00943473 /* FillAndStroke.swift in Sources */,
|
||||
4CA927612A290E340098A105 /* EventShell.swift in Sources */,
|
||||
D74EC8502E1856B70091DC51 /* NonCopyableLinkedList.swift in Sources */,
|
||||
4C363AA428296DEE006E126D /* SearchModel.swift in Sources */,
|
||||
4C8D00CC29DF92DF0036AF10 /* Hashtags.swift in Sources */,
|
||||
D74EA0942D2E77B9002290DD /* LoadableNostrEventView.swift in Sources */,
|
||||
@@ -6268,6 +6275,7 @@
|
||||
82D6FBB02CD99F7900C925F4 /* RelayConnection.swift in Sources */,
|
||||
82D6FBB12CD99F7900C925F4 /* RelayLog.swift in Sources */,
|
||||
82D6FBB22CD99F7900C925F4 /* Nostr.swift in Sources */,
|
||||
D74EC8512E1856B70091DC51 /* NonCopyableLinkedList.swift in Sources */,
|
||||
82D6FBB32CD99F7900C925F4 /* NostrFilter.swift in Sources */,
|
||||
82D6FBB42CD99F7900C925F4 /* NostrResponse.swift in Sources */,
|
||||
82D6FBB52CD99F7900C925F4 /* NostrEvent.swift in Sources */,
|
||||
@@ -6570,6 +6578,7 @@
|
||||
D73E5E622C6A97F4007EB227 /* BlurHashEncode.swift in Sources */,
|
||||
5C09FD122DF283D700823661 /* FollowPackModel.swift in Sources */,
|
||||
D73E5E632C6A97F4007EB227 /* BlurHashDecode.swift in Sources */,
|
||||
D74EC8522E1856B70091DC51 /* NonCopyableLinkedList.swift in Sources */,
|
||||
D73E5F952C6AA753007EB227 /* FullScreenCarouselView.swift in Sources */,
|
||||
D76BE18E2E0CF3DA004AD0C6 /* Interests.swift in Sources */,
|
||||
D73E5E642C6A97F4007EB227 /* PostBox.swift in Sources */,
|
||||
@@ -7020,6 +7029,7 @@
|
||||
4CBB6F792B7311AA000477A4 /* error.c in Sources */,
|
||||
4CBB6F7A2B7311AA000477A4 /* bech32_util.c in Sources */,
|
||||
4CBB6F712B731184000477A4 /* bolt11.c in Sources */,
|
||||
D74EC84F2E1856B70091DC51 /* NonCopyableLinkedList.swift in Sources */,
|
||||
4CBB6F702B731179000477A4 /* invoice.c in Sources */,
|
||||
4CBB6F6F2B73116B000477A4 /* content_parser.c in Sources */,
|
||||
4CBB6F6E2B731113000477A4 /* block.c in Sources */,
|
||||
|
||||
@@ -784,48 +784,41 @@ func first_eref_mention(ndb: Ndb, ev: NostrEvent, keypair: Keypair) -> Mention<N
|
||||
return nil
|
||||
}
|
||||
|
||||
let blocks = blockGroup.blocks.filter { block in
|
||||
guard case .mention(let mention) = block else {
|
||||
return false
|
||||
}
|
||||
|
||||
switch mention.bech32_type {
|
||||
case .note, .nevent:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/// MARK: - Preview
|
||||
if let firstBlock = blocks.first,
|
||||
case .mention(let mention) = firstBlock {
|
||||
return try? blockGroup.forEachBlock({ index, block in
|
||||
// Step 1: Filter
|
||||
switch block {
|
||||
case .mention(let mention):
|
||||
switch mention.bech32_type {
|
||||
case .note:
|
||||
let data = mention.bech32.note.event_id.as_data(size: 32)
|
||||
return .note(NoteId(data))
|
||||
return .loopReturn(.note(NoteId(data)))
|
||||
case .nevent:
|
||||
let data = mention.bech32.nevent.event_id.as_data(size: 32)
|
||||
return .note(NoteId(data))
|
||||
return .loopReturn(.note(NoteId(data)))
|
||||
default:
|
||||
return nil
|
||||
return .loopBreak
|
||||
}
|
||||
default:
|
||||
return .loopContinue
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
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 invoiceBlocks: [Invoice] = blockGroup.blocks.reduce(into: []) { invoices, block in
|
||||
guard case .invoice(let invoice) = block,
|
||||
let invoice = invoice.as_invoice()
|
||||
else {
|
||||
return
|
||||
let invoiceBlocks: [Invoice] = (try? blockGroup.reduce(initialResult: [Invoice](), { index, invoices, block in
|
||||
switch block {
|
||||
case .invoice(let invoice):
|
||||
if let invoice = invoice.as_invoice() {
|
||||
return .loopReturn(invoices + [invoice])
|
||||
}
|
||||
invoices.append(invoice)
|
||||
default:
|
||||
break
|
||||
}
|
||||
return .loopContinue
|
||||
})) ?? []
|
||||
return invoiceBlocks.isEmpty ? nil : invoiceBlocks
|
||||
}
|
||||
|
||||
|
||||
@@ -105,22 +105,26 @@ func render_blocks(blocks: borrowing NdbBlockGroup, profiles: Profiles, can_hide
|
||||
var end_mention_count = 0
|
||||
var end_url_count = 0
|
||||
|
||||
let ndb_blocks = blocks.blocks
|
||||
let one_note_ref = ndb_blocks
|
||||
.filter({
|
||||
if case .mention(let mention) = $0,
|
||||
let typ = mention.bech32_type,
|
||||
let note_ref_count: Int? = try? blocks.reduce(initialResult: 0) { index, partialResult, item in
|
||||
switch item {
|
||||
case .mention(let mention):
|
||||
if let typ = mention.bech32_type,
|
||||
typ.is_notelike {
|
||||
return true
|
||||
return .loopReturn(partialResult + 1)
|
||||
}
|
||||
return false
|
||||
})
|
||||
.count == 1
|
||||
default:
|
||||
break
|
||||
}
|
||||
return .loopContinue
|
||||
}
|
||||
let one_note_ref = note_ref_count == 1
|
||||
|
||||
// Search backwards until we find the beginning index of the chain of previewables that reach the end of the content.
|
||||
var hide_text_index = ndb_blocks.endIndex
|
||||
var hide_text_index: Int = 0
|
||||
if can_hide_last_previewable_refs {
|
||||
outerLoop: for (i, block) in ndb_blocks.enumerated().reversed() {
|
||||
let _: ()? = blocks.withList({ blocksList in
|
||||
let endIndex = blocksList.count
|
||||
return blocksList.forEachItemReversed({ index, block in
|
||||
if block.is_previewable {
|
||||
switch block {
|
||||
case .mention:
|
||||
@@ -130,13 +134,13 @@ func render_blocks(blocks: borrowing NdbBlockGroup, profiles: Profiles, can_hide
|
||||
// do not hide anything because we allow rich rendering of only one mention currently.
|
||||
// This should be fixed in the future to show events inline instead.
|
||||
if end_mention_count > 1 {
|
||||
hide_text_index = ndb_blocks.endIndex
|
||||
break outerLoop
|
||||
hide_text_index = endIndex
|
||||
return .loopBreak
|
||||
}
|
||||
case .url(let url_block):
|
||||
guard let url_string = NdbBlock.convertToStringCopy(from: url_block),
|
||||
let url = URL(string: url_string) else {
|
||||
continue // We can't classify this, ignore and move on
|
||||
return .loopContinue // We can't classify this, ignore and move on
|
||||
}
|
||||
let url_type = classify_url(url)
|
||||
if case .link = url_type {
|
||||
@@ -145,31 +149,40 @@ func render_blocks(blocks: borrowing NdbBlockGroup, profiles: Profiles, can_hide
|
||||
// If there is more than one link, do not hide anything because we allow rich rendering of only
|
||||
// one link.
|
||||
if end_url_count > 1 {
|
||||
hide_text_index = ndb_blocks.endIndex
|
||||
break outerLoop
|
||||
hide_text_index = endIndex
|
||||
return .loopBreak
|
||||
}
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
hide_text_index = i
|
||||
} else if case .text(let txt_block) = block,
|
||||
let txt = NdbBlock.convertToStringCopy(from: txt_block),
|
||||
hide_text_index = index
|
||||
}
|
||||
else {
|
||||
switch block {
|
||||
case .text(let txt_block):
|
||||
if let txt = NdbBlock.convertToStringCopy(from: txt_block),
|
||||
txt.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||
// We should hide whitespace at the end sequence.
|
||||
hide_text_index = i
|
||||
} else if case .hashtag = block {
|
||||
hide_text_index = index
|
||||
}
|
||||
case .hashtag(_):
|
||||
// SPECIAL CASE:
|
||||
// We should keep hashtags at the end sequence but hide all the other previewables around it.
|
||||
hide_text_index = i
|
||||
} else {
|
||||
break
|
||||
hide_text_index = index
|
||||
default:
|
||||
return .loopBreak
|
||||
}
|
||||
}
|
||||
return .loopContinue
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
var ind: Int = -1
|
||||
let txt: CompatibleText = ndb_blocks.reduce(into: CompatibleText()) { str, block in
|
||||
let txt: CompatibleText? = try? blocks.withList({ blocksList in
|
||||
let endIndex = blocksList.count
|
||||
return try blocksList.reduce(initialResult: CompatibleText(), { index, str, block in
|
||||
ind = ind + 1
|
||||
|
||||
// Add the rendered previewable blocks to their type-specific lists.
|
||||
@@ -192,7 +205,7 @@ func render_blocks(blocks: borrowing NdbBlockGroup, profiles: Profiles, can_hide
|
||||
// If there are previewable blocks that occur before the consecutive sequence of them at the end of the content,
|
||||
// we should not hide the text representation of any previewable block to avoid altering the format of the note.
|
||||
if ind < hide_text_index && block.is_previewable {
|
||||
hide_text_index = ndb_blocks.endIndex
|
||||
hide_text_index = endIndex
|
||||
}
|
||||
|
||||
// No need to show the text representation of the block if the only previewables are the sequence of them
|
||||
@@ -201,56 +214,74 @@ func render_blocks(blocks: borrowing NdbBlockGroup, profiles: Profiles, can_hide
|
||||
// The only exception is that if there are hashtags embedded in the end sequence, which is not uncommon,
|
||||
// then we still want to show those hashtags but hide everything else that is previewable in the end sequence.
|
||||
if ind >= hide_text_index {
|
||||
if case .text(let txt_block) = block,
|
||||
let txt = NdbBlock.convertToStringCopy(from: txt_block),
|
||||
switch block {
|
||||
case .text(let txt_block):
|
||||
if let txt = NdbBlock.convertToStringCopy(from: txt_block),
|
||||
txt.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||
if case .hashtag = ndb_blocks[safe: ind+1] {
|
||||
str = str + CompatibleText(stringLiteral: reduce_text_block(ind: ind, hide_text_index: hide_text_index, txt: txt))
|
||||
let returnItem: CompatibleText? = blocksList.useItem(at: ind + 1, { matchingBlock in
|
||||
switch matchingBlock {
|
||||
case .hashtag(_):
|
||||
return str + CompatibleText(stringLiteral: reduce_text_block(ind: ind, hide_text_index: hide_text_index, txt: txt))
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
} else if case .hashtag(let htag) = block {
|
||||
str = str + hashtag_str(htag.as_str())
|
||||
}) ?? nil
|
||||
if let returnItem {
|
||||
return .loopReturn(returnItem)
|
||||
}
|
||||
}
|
||||
case .hashtag(let htag):
|
||||
return .loopReturn(str + hashtag_str(htag.as_str()))
|
||||
default:
|
||||
break
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
switch block {
|
||||
case .mention(let m):
|
||||
if let typ = m.bech32_type, typ.is_notelike, one_note_ref {
|
||||
return
|
||||
return .loopContinue
|
||||
}
|
||||
guard let mention = MentionRef(block: m) else { return }
|
||||
str = str + mention_str(.any(mention), profiles: profiles)
|
||||
guard let mention = MentionRef(block: m) else { return .loopContinue }
|
||||
return .loopReturn(str + mention_str(.any(mention), profiles: profiles))
|
||||
case .text(let txt):
|
||||
if case .hashtag = blocks[safe: ind+1] {
|
||||
var hide_text_index_argument = hide_text_index
|
||||
blocksList.useItem(at: ind+1, { block in
|
||||
switch block {
|
||||
case .hashtag(_):
|
||||
// SPECIAL CASE:
|
||||
// Do not trim whitespaces from suffix if the following block is a hashtag.
|
||||
// This is because of the code further up (see "SPECIAL CASE").
|
||||
str = str + CompatibleText(stringLiteral: reduce_text_block(ind: ind, hide_text_index: -1, txt: txt.as_str()))
|
||||
} else {
|
||||
str = str + CompatibleText(stringLiteral: reduce_text_block(ind: ind, hide_text_index: hide_text_index, txt: txt.as_str()))
|
||||
hide_text_index_argument = -1
|
||||
default:
|
||||
break
|
||||
}
|
||||
})
|
||||
return .loopReturn(str + CompatibleText(stringLiteral: reduce_text_block(ind: ind, hide_text_index: hide_text_index_argument, txt: txt.as_str())))
|
||||
case .hashtag(let htag):
|
||||
str = str + hashtag_str(htag.as_str())
|
||||
return .loopReturn(str + hashtag_str(htag.as_str()))
|
||||
case .invoice(let invoice):
|
||||
guard let inv = invoice.as_invoice() else { return }
|
||||
guard let inv = invoice.as_invoice() else { return .loopContinue }
|
||||
invoices.append(inv)
|
||||
case .url(let url):
|
||||
guard let url = URL(string: url.as_str()) else { return }
|
||||
guard let url = URL(string: url.as_str()) else { return .loopContinue }
|
||||
let url_type = classify_url(url)
|
||||
switch url_type {
|
||||
case .media:
|
||||
urls.append(url_type)
|
||||
case .link(let url):
|
||||
urls.append(url_type)
|
||||
str = str + url_str(url)
|
||||
return .loopReturn(str + url_str(url))
|
||||
}
|
||||
case .mention_index:
|
||||
return
|
||||
}
|
||||
return .loopContinue
|
||||
}
|
||||
return .loopContinue
|
||||
})
|
||||
})
|
||||
|
||||
return NoteArtifactsSeparated(content: txt, words: blocks.words, urls: urls, invoices: invoices)
|
||||
return NoteArtifactsSeparated(content: txt ?? CompatibleText(), words: blocks.words, urls: urls, invoices: invoices)
|
||||
}
|
||||
|
||||
func reduce_text_block(ind: Int, hide_text_index: Int, txt: String) -> String {
|
||||
|
||||
@@ -353,11 +353,11 @@ struct NoteContentView: View {
|
||||
guard let blockGroup = try? NdbBlockGroup.from(event: event, using: damus_state.ndb, and: damus_state.keypair) else {
|
||||
return
|
||||
}
|
||||
for block in blockGroup.blocks {
|
||||
let _: Int? = try? blockGroup.forEachBlock { index, block in
|
||||
switch block {
|
||||
case .mention(let m):
|
||||
guard let typ = m.bech32_type else {
|
||||
continue
|
||||
return .loopContinue
|
||||
}
|
||||
switch typ {
|
||||
case .nprofile:
|
||||
@@ -368,18 +368,19 @@ struct NoteContentView: View {
|
||||
if m.bech32.npub.matches_pubkey(pk: profile.pubkey) {
|
||||
load(force_artifacts: true)
|
||||
}
|
||||
case .nevent: continue
|
||||
case .nrelay: continue
|
||||
case .nsec: continue
|
||||
case .note: continue
|
||||
case .naddr: continue
|
||||
case .nevent: return .loopContinue
|
||||
case .nrelay: return .loopContinue
|
||||
case .nsec: return .loopContinue
|
||||
case .note: return .loopContinue
|
||||
case .naddr: return .loopContinue
|
||||
}
|
||||
case .text: return
|
||||
case .hashtag: return
|
||||
case .url: return
|
||||
case .invoice: return
|
||||
case .mention_index(_): return
|
||||
case .text: return .loopContinue
|
||||
case .hashtag: return .loopContinue
|
||||
case .url: return .loopContinue
|
||||
case .invoice: return .loopContinue
|
||||
case .mention_index(_): return .loopContinue
|
||||
}
|
||||
return .loopContinue
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
@@ -538,16 +539,21 @@ func separate_images(ndb: Ndb, ev: NostrEvent, keypair: Keypair) -> [MediaUrl]?
|
||||
guard let blockGroup = try? NdbBlockGroup.from(event: ev, using: ndb, and: keypair) else {
|
||||
return nil
|
||||
}
|
||||
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
|
||||
let urlBlocks: [URL] = (try? blockGroup.reduce(initialResult: Array<URL>()) { index, urls, block in
|
||||
switch block {
|
||||
case .url(let url):
|
||||
guard let parsed_url = URL(string: url.as_str()) else {
|
||||
return .loopContinue
|
||||
}
|
||||
|
||||
if classify_url(parsed_url).is_img != nil {
|
||||
urls.append(parsed_url)
|
||||
return .loopReturn(urls + [parsed_url])
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
return .loopContinue
|
||||
}) ?? []
|
||||
let mediaUrls = urlBlocks.map { MediaUrl.image($0) }
|
||||
return mediaUrls.isEmpty ? nil : mediaUrls
|
||||
}
|
||||
|
||||
@@ -74,13 +74,21 @@ func generate_local_notification_object(ndb: Ndb, from ev: NostrEvent, state: He
|
||||
state.settings.mention_notification,
|
||||
let blockGroup = try? NdbBlockGroup.from(event: ev, using: ndb, and: state.keypair)
|
||||
{
|
||||
for case .mention(let mention) in blockGroup.blocks {
|
||||
let notification: LocalNotification? = try? blockGroup.forEachBlock({ index, block in
|
||||
switch block {
|
||||
case .mention(let mention):
|
||||
guard case .npub = mention.bech32_type,
|
||||
(memcmp(state.keypair.pubkey.id.bytes, mention.bech32.npub.pubkey, 32) == 0) else {
|
||||
continue
|
||||
return .loopContinue
|
||||
}
|
||||
let content_preview = render_notification_content_preview(ndb: ndb, ev: ev, profiles: state.profiles, keypair: state.keypair)
|
||||
return LocalNotification(type: .mention, event: ev, target: .note(ev), content: content_preview)
|
||||
return .loopReturn(LocalNotification(type: .mention, event: ev, target: .note(ev), content: content_preview))
|
||||
default:
|
||||
return .loopContinue
|
||||
}
|
||||
})
|
||||
if let notification {
|
||||
return notification
|
||||
}
|
||||
|
||||
if ev.referenced_ids.contains(where: { note_id in
|
||||
|
||||
@@ -348,12 +348,14 @@ class NoteContentViewTests: XCTestCase {
|
||||
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 blockCount1 = try? blocks.withList({ $0.count })
|
||||
XCTAssertEqual(blockCount1, 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)
|
||||
let blockCount2 = try? blocks2.withList({ $0.count })
|
||||
XCTAssertEqual(blockCount2, 1)
|
||||
}
|
||||
|
||||
func testMentionStr_Pubkey_ContainsAbbreviated() throws {
|
||||
|
||||
@@ -51,7 +51,7 @@ extension ndb_invoice_block {
|
||||
}
|
||||
}
|
||||
|
||||
enum NdbBlock {
|
||||
enum NdbBlock: ~Copyable {
|
||||
case text(ndb_str_block)
|
||||
case mention(ndb_mention_bech32_block)
|
||||
case hashtag(ndb_str_block)
|
||||
@@ -107,10 +107,6 @@ struct NdbBlockGroup: ~Copyable {
|
||||
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 {
|
||||
return metadata.borrow { $0.words }
|
||||
}
|
||||
@@ -149,12 +145,12 @@ enum MaybeTxn<T: ~Copyable>: ~Copyable {
|
||||
case pure(T)
|
||||
case txn(SafeNdbTxn<T>)
|
||||
|
||||
func borrow<Y>(_ borrowFunction: (borrowing T) -> Y) -> Y {
|
||||
func borrow<Y>(_ borrowFunction: (borrowing T) throws -> Y) rethrows -> Y {
|
||||
switch self {
|
||||
case .pure(let item):
|
||||
return borrowFunction(item)
|
||||
return try borrowFunction(item)
|
||||
case .txn(let txn):
|
||||
return borrowFunction(txn.val)
|
||||
return try borrowFunction(txn.val)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -239,35 +235,60 @@ extension NdbBlockGroup {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Enumeration support
|
||||
|
||||
extension NdbBlockGroup {
|
||||
/// Collects all blocks in the group into an array without using Iterator/Sequence protocols
|
||||
typealias NdbBlockList = NonCopyableLinkedList<NdbBlock>
|
||||
|
||||
/// Borrows all blocks in the group one by one and runs a function defined by the caller.
|
||||
///
|
||||
/// **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.
|
||||
/// This is done as a function instead of using `Sequence` and `Iterator` protocols because it is currently not possible to conform to both `Sequence` and `~Copyable` at the same time, as Sequence requires elements to be `Copyable`
|
||||
///
|
||||
/// - Returns: An array of all blocks in the group
|
||||
fileprivate func collectBlocks() -> [NdbBlock] {
|
||||
var blocks = [NdbBlock]()
|
||||
/// - Parameter borrowingFunction: The function to be run on each iteration. Takes in two parameters: The index of the item in the list (zero-indexed), and the block itself.
|
||||
/// - Returns: The `Y` value returned by the provided function, when such function returns `.loopReturn(Y)`
|
||||
@discardableResult
|
||||
func forEachBlock<Y>(_ borrowingFunction: ((Int, borrowing NdbBlock) throws -> NdbBlockList.LoopCommand<Y>)) rethrows -> Y? {
|
||||
return try withList({ try $0.forEachItem(borrowingFunction) })
|
||||
}
|
||||
|
||||
// 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
|
||||
/// Borrows all blocks in the group one by one and runs a function defined by the caller, in reverse order
|
||||
///
|
||||
/// **Implementation note:**
|
||||
/// This is done as a function instead of using `Sequence` and `Iterator` protocols because it is currently not possible to conform to both `Sequence` and `~Copyable` at the same time, as Sequence requires elements to be `Copyable`
|
||||
///
|
||||
/// - Parameter borrowingFunction: The function to be run on each iteration. Takes in two parameters: The index of the item in the list (zero-indexed), and the block itself.
|
||||
/// - Returns: The `Y` value returned by the provided function, when such function returns `.loopReturn(Y)`
|
||||
@discardableResult
|
||||
func forEachBlockReversed<Y>(_ borrowingFunction: ((Int, borrowing NdbBlock) throws -> NdbBlockList.LoopCommand<Y>)) rethrows -> Y? {
|
||||
return try withList({ try $0.forEachItemReversed(borrowingFunction) })
|
||||
}
|
||||
|
||||
/// Iterates over each item of the list, updating a final value, and returns the final result at the end.
|
||||
func reduce<Y>(initialResult: Y, _ borrowingFunction: ((_ index: Int, _ partialResult: Y, _ item: borrowing NdbBlock) throws -> NdbBlockList.LoopCommand<Y>)) rethrows -> Y? {
|
||||
return try withList({ try $0.reduce(initialResult: initialResult, borrowingFunction) })
|
||||
}
|
||||
|
||||
/// Borrows the block list for processing
|
||||
func withList<Y>(_ borrowingFunction: (borrowing NdbBlockList) throws -> Y) rethrows -> Y {
|
||||
var linkedList: NdbBlockList = .init()
|
||||
|
||||
return try 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
|
||||
return try 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),
|
||||
outerLoop: while let ptr = ndb_blocks_iterate_next(&iter),
|
||||
let block = NdbBlock(ndb_block_ptr(ptr: ptr)) {
|
||||
blocks.append(block)
|
||||
}
|
||||
}
|
||||
linkedList.add(item: block)
|
||||
}
|
||||
|
||||
return blocks
|
||||
return try borrowingFunction(linkedList)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
160
nostrdb/NonCopyableLinkedList.swift
Normal file
160
nostrdb/NonCopyableLinkedList.swift
Normal file
@@ -0,0 +1,160 @@
|
||||
//
|
||||
// NonCopyableLinkedList.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Daniel D’Aquino on 2025-07-04.
|
||||
//
|
||||
|
||||
/// A linked list to help with iteration of non-copyable elements
|
||||
///
|
||||
/// This is needed to provide an array-like abstraction or iterators since swift arrays or iterator protocols require the element to be "copyable"
|
||||
struct NonCopyableLinkedList<T: ~Copyable>: ~Copyable {
|
||||
private var head: Node<T>? = nil
|
||||
private var tail: Node<T>? = nil
|
||||
private(set) var count: Int = 0
|
||||
|
||||
/// Iterates over each item of the list, with enumeration support.
|
||||
func forEachItem<Y>(_ borrowingFunction: ((_ index: Int, _ item: borrowing T) throws -> LoopCommand<Y>)) rethrows -> Y? {
|
||||
var indexCounter = 0
|
||||
|
||||
var cursor: Node? = self.head
|
||||
|
||||
outerLoop: while let nextItem = cursor {
|
||||
let loopIterationResult = try borrowingFunction(indexCounter, nextItem.value)
|
||||
indexCounter += 1
|
||||
cursor = nextItem.next
|
||||
switch loopIterationResult {
|
||||
case .loopBreak:
|
||||
break outerLoop
|
||||
case .loopContinue:
|
||||
continue outerLoop
|
||||
case .loopReturn(let result):
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
/// Iterates over each item of the list in reverse, with enumeration support.
|
||||
func forEachItemReversed<Y, E: Error>(_ borrowingFunction: ((_ index: Int, _ item: borrowing T) throws(E) -> LoopCommand<Y>)) throws(E) -> Y? {
|
||||
var indexCounter = count
|
||||
var cursor: Node? = self.tail
|
||||
|
||||
outerLoop: while let nextItem = cursor {
|
||||
let loopIterationResult = try borrowingFunction(indexCounter, nextItem.value)
|
||||
indexCounter -= 1
|
||||
cursor = nextItem.previous
|
||||
switch loopIterationResult {
|
||||
case .loopBreak:
|
||||
break outerLoop
|
||||
case .loopContinue:
|
||||
continue outerLoop
|
||||
case .loopReturn(let result):
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
/// Iterates over each item of the list, with enumeration support, updating some value in each iteration and returning the final value at the end.
|
||||
func reduce<Y>(initialResult: Y, _ borrowingFunction: ((_ index: Int, _ partialResult: Y, _ item: borrowing T) throws -> LoopCommand<Y>)) throws -> Y {
|
||||
var indexCounter = 0
|
||||
var currentResult = initialResult
|
||||
|
||||
var cursor: Node? = self.head
|
||||
|
||||
outerLoop: while let nextItem = cursor {
|
||||
let loopIterationResult = try borrowingFunction(indexCounter, currentResult, nextItem.value)
|
||||
indexCounter += 1
|
||||
cursor = nextItem.next
|
||||
switch loopIterationResult {
|
||||
case .loopBreak:
|
||||
break outerLoop
|
||||
case .loopContinue:
|
||||
continue outerLoop
|
||||
case .loopReturn(let result):
|
||||
currentResult = result
|
||||
continue outerLoop
|
||||
}
|
||||
}
|
||||
|
||||
return currentResult
|
||||
}
|
||||
|
||||
/// Uses a specific item of the list based on a provided index.
|
||||
///
|
||||
/// O(N/2) worst case scenario
|
||||
///
|
||||
/// Returns `nil` if nothing was found
|
||||
func useItem<Y>(at index: Int, _ borrowingFunction: ((_ item: borrowing T) throws -> Y)) rethrows -> Y? {
|
||||
if index < 0 || index >= self.count {
|
||||
return nil
|
||||
}
|
||||
else if index < self.count / 2 {
|
||||
return try self.forEachItem({ i, item in
|
||||
if i == index {
|
||||
return .loopReturn(try borrowingFunction(item))
|
||||
}
|
||||
return .loopContinue
|
||||
})
|
||||
}
|
||||
else {
|
||||
return try self.forEachItemReversed({ i, item in
|
||||
if i == index {
|
||||
return .loopReturn(try borrowingFunction(item))
|
||||
}
|
||||
return .loopContinue
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds an item to the tail end list
|
||||
mutating func add(item: consuming T) {
|
||||
guard self.head != nil, let currentTail = self.tail else {
|
||||
let firstNode = Node(value: item, next: nil, previous: nil)
|
||||
self.head = firstNode
|
||||
self.tail = firstNode
|
||||
self.count = 1
|
||||
return
|
||||
}
|
||||
let newTail = Node(value: item, next: nil, previous: currentTail)
|
||||
currentTail.next = newTail
|
||||
self.tail = newTail
|
||||
self.count += 1
|
||||
}
|
||||
|
||||
/// A node of the linked list
|
||||
///
|
||||
/// Should be `~Copyable` but that would require using a value type such as a struct or enum, and the Swift compiler does not support recursive enums with non-copyable objects for some reason. Example:
|
||||
/// ```swift
|
||||
/// enum List<Y: ~Copyable>: ~Copyable {
|
||||
/// indirect case node(value: Y, next: NewList<Y>) // <-- ERROR: Noncopyable enum 'List' cannot be marked indirect or have indirect cases yet
|
||||
/// case empty
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Therefore, we make it `private` to make sure we contain the exposure of this unsafe object to only this class. Outside users of the linked list can access objects via the iterator functions.
|
||||
private class Node<Item: ~Copyable> {
|
||||
let value: Item
|
||||
var next: Node?
|
||||
var previous: Node?
|
||||
|
||||
init(value: consuming Item, next: consuming Node?, previous: consuming Node?) {
|
||||
self.value = value
|
||||
self.next = next
|
||||
self.previous = previous
|
||||
}
|
||||
}
|
||||
|
||||
/// A loop command to allow closures to control the loop they are in.
|
||||
enum LoopCommand<Y> {
|
||||
/// Breaks out of the loop
|
||||
case loopBreak
|
||||
/// Continues to the next iteration of the loop
|
||||
case loopContinue
|
||||
/// Stops iterating and return a value
|
||||
case loopReturn(Y)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user