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 */; };
|
D74EA0932D2E77B9002290DD /* LoadableNostrEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74EA0922D2E77B9002290DD /* LoadableNostrEventView.swift */; };
|
||||||
D74EA0942D2E77B9002290DD /* 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 */; };
|
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 */; };
|
D74F430A2B23F0BE00425B75 /* DamusPurple.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74F43092B23F0BE00425B75 /* DamusPurple.swift */; };
|
||||||
D74F430C2B23FB9B00425B75 /* StoreObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74F430B2B23FB9B00425B75 /* StoreObserver.swift */; };
|
D74F430C2B23FB9B00425B75 /* StoreObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74F430B2B23FB9B00425B75 /* StoreObserver.swift */; };
|
||||||
D753CEAA2BE9DE04001C3A5D /* MutingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D753CEA92BE9DE04001C3A5D /* MutingTests.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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
D753CEA92BE9DE04001C3A5D /* MutingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MutingTests.swift; sourceTree = "<group>"; };
|
||||||
@@ -3154,6 +3159,7 @@
|
|||||||
4C9054862A6AEB4500811EEC /* nostrdb */ = {
|
4C9054862A6AEB4500811EEC /* nostrdb */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
D74EC84E2E1856AF0091DC51 /* NonCopyableLinkedList.swift */,
|
||||||
D733F9E42D92C75C00317B11 /* UnownedNdbNote.swift */,
|
D733F9E42D92C75C00317B11 /* UnownedNdbNote.swift */,
|
||||||
D7F5630F2DEE71BB008509DE /* NdbFilter.swift */,
|
D7F5630F2DEE71BB008509DE /* NdbFilter.swift */,
|
||||||
D74DEC892DA0A19800E69FA6 /* Ndb+.swift */,
|
D74DEC892DA0A19800E69FA6 /* Ndb+.swift */,
|
||||||
@@ -5764,6 +5770,7 @@
|
|||||||
4C3BEFD6281D995700B3DE84 /* ActionBarModel.swift in Sources */,
|
4C3BEFD6281D995700B3DE84 /* ActionBarModel.swift in Sources */,
|
||||||
4C7D09762A0AF19E00943473 /* FillAndStroke.swift in Sources */,
|
4C7D09762A0AF19E00943473 /* FillAndStroke.swift in Sources */,
|
||||||
4CA927612A290E340098A105 /* EventShell.swift in Sources */,
|
4CA927612A290E340098A105 /* EventShell.swift in Sources */,
|
||||||
|
D74EC8502E1856B70091DC51 /* NonCopyableLinkedList.swift in Sources */,
|
||||||
4C363AA428296DEE006E126D /* SearchModel.swift in Sources */,
|
4C363AA428296DEE006E126D /* SearchModel.swift in Sources */,
|
||||||
4C8D00CC29DF92DF0036AF10 /* Hashtags.swift in Sources */,
|
4C8D00CC29DF92DF0036AF10 /* Hashtags.swift in Sources */,
|
||||||
D74EA0942D2E77B9002290DD /* LoadableNostrEventView.swift in Sources */,
|
D74EA0942D2E77B9002290DD /* LoadableNostrEventView.swift in Sources */,
|
||||||
@@ -6268,6 +6275,7 @@
|
|||||||
82D6FBB02CD99F7900C925F4 /* RelayConnection.swift in Sources */,
|
82D6FBB02CD99F7900C925F4 /* RelayConnection.swift in Sources */,
|
||||||
82D6FBB12CD99F7900C925F4 /* RelayLog.swift in Sources */,
|
82D6FBB12CD99F7900C925F4 /* RelayLog.swift in Sources */,
|
||||||
82D6FBB22CD99F7900C925F4 /* Nostr.swift in Sources */,
|
82D6FBB22CD99F7900C925F4 /* Nostr.swift in Sources */,
|
||||||
|
D74EC8512E1856B70091DC51 /* NonCopyableLinkedList.swift in Sources */,
|
||||||
82D6FBB32CD99F7900C925F4 /* NostrFilter.swift in Sources */,
|
82D6FBB32CD99F7900C925F4 /* NostrFilter.swift in Sources */,
|
||||||
82D6FBB42CD99F7900C925F4 /* NostrResponse.swift in Sources */,
|
82D6FBB42CD99F7900C925F4 /* NostrResponse.swift in Sources */,
|
||||||
82D6FBB52CD99F7900C925F4 /* NostrEvent.swift in Sources */,
|
82D6FBB52CD99F7900C925F4 /* NostrEvent.swift in Sources */,
|
||||||
@@ -6570,6 +6578,7 @@
|
|||||||
D73E5E622C6A97F4007EB227 /* BlurHashEncode.swift in Sources */,
|
D73E5E622C6A97F4007EB227 /* BlurHashEncode.swift in Sources */,
|
||||||
5C09FD122DF283D700823661 /* FollowPackModel.swift in Sources */,
|
5C09FD122DF283D700823661 /* FollowPackModel.swift in Sources */,
|
||||||
D73E5E632C6A97F4007EB227 /* BlurHashDecode.swift in Sources */,
|
D73E5E632C6A97F4007EB227 /* BlurHashDecode.swift in Sources */,
|
||||||
|
D74EC8522E1856B70091DC51 /* NonCopyableLinkedList.swift in Sources */,
|
||||||
D73E5F952C6AA753007EB227 /* FullScreenCarouselView.swift in Sources */,
|
D73E5F952C6AA753007EB227 /* FullScreenCarouselView.swift in Sources */,
|
||||||
D76BE18E2E0CF3DA004AD0C6 /* Interests.swift in Sources */,
|
D76BE18E2E0CF3DA004AD0C6 /* Interests.swift in Sources */,
|
||||||
D73E5E642C6A97F4007EB227 /* PostBox.swift in Sources */,
|
D73E5E642C6A97F4007EB227 /* PostBox.swift in Sources */,
|
||||||
@@ -7020,6 +7029,7 @@
|
|||||||
4CBB6F792B7311AA000477A4 /* error.c in Sources */,
|
4CBB6F792B7311AA000477A4 /* error.c in Sources */,
|
||||||
4CBB6F7A2B7311AA000477A4 /* bech32_util.c in Sources */,
|
4CBB6F7A2B7311AA000477A4 /* bech32_util.c in Sources */,
|
||||||
4CBB6F712B731184000477A4 /* bolt11.c in Sources */,
|
4CBB6F712B731184000477A4 /* bolt11.c in Sources */,
|
||||||
|
D74EC84F2E1856B70091DC51 /* NonCopyableLinkedList.swift in Sources */,
|
||||||
4CBB6F702B731179000477A4 /* invoice.c in Sources */,
|
4CBB6F702B731179000477A4 /* invoice.c in Sources */,
|
||||||
4CBB6F6F2B73116B000477A4 /* content_parser.c in Sources */,
|
4CBB6F6F2B73116B000477A4 /* content_parser.c in Sources */,
|
||||||
4CBB6F6E2B731113000477A4 /* block.c in Sources */,
|
4CBB6F6E2B731113000477A4 /* block.c in Sources */,
|
||||||
|
|||||||
@@ -783,49 +783,42 @@ func first_eref_mention(ndb: Ndb, ev: NostrEvent, keypair: Keypair) -> Mention<N
|
|||||||
guard let blockGroup = try? NdbBlockGroup.from(event: ev, using: ndb, and: keypair) else {
|
guard let blockGroup = try? NdbBlockGroup.from(event: ev, using: ndb, and: keypair) else {
|
||||||
return nil
|
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
|
return try? blockGroup.forEachBlock({ index, block in
|
||||||
if let firstBlock = blocks.first,
|
// Step 1: Filter
|
||||||
case .mention(let mention) = firstBlock {
|
switch block {
|
||||||
switch mention.bech32_type {
|
case .mention(let mention):
|
||||||
case .note:
|
switch mention.bech32_type {
|
||||||
let data = mention.bech32.note.event_id.as_data(size: 32)
|
case .note:
|
||||||
return .note(NoteId(data))
|
let data = mention.bech32.note.event_id.as_data(size: 32)
|
||||||
case .nevent:
|
return .loopReturn(.note(NoteId(data)))
|
||||||
let data = mention.bech32.nevent.event_id.as_data(size: 32)
|
case .nevent:
|
||||||
return .note(NoteId(data))
|
let data = mention.bech32.nevent.event_id.as_data(size: 32)
|
||||||
|
return .loopReturn(.note(NoteId(data)))
|
||||||
|
default:
|
||||||
|
return .loopBreak
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return nil
|
return .loopContinue
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
guard let blockGroup = try? NdbBlockGroup.from(event: ev, using: ndb, and: keypair) else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
let invoiceBlocks: [Invoice] = blockGroup.blocks.reduce(into: []) { invoices, block in
|
let invoiceBlocks: [Invoice] = (try? blockGroup.reduce(initialResult: [Invoice](), { index, invoices, block in
|
||||||
guard case .invoice(let invoice) = block,
|
switch block {
|
||||||
let invoice = invoice.as_invoice()
|
case .invoice(let invoice):
|
||||||
else {
|
if let invoice = invoice.as_invoice() {
|
||||||
return
|
return .loopReturn(invoices + [invoice])
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break
|
||||||
}
|
}
|
||||||
invoices.append(invoice)
|
return .loopContinue
|
||||||
}
|
})) ?? []
|
||||||
return invoiceBlocks.isEmpty ? nil : invoiceBlocks
|
return invoiceBlocks.isEmpty ? nil : invoiceBlocks
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -105,152 +105,183 @@ func render_blocks(blocks: borrowing NdbBlockGroup, profiles: Profiles, can_hide
|
|||||||
var end_mention_count = 0
|
var end_mention_count = 0
|
||||||
var end_url_count = 0
|
var end_url_count = 0
|
||||||
|
|
||||||
let ndb_blocks = blocks.blocks
|
let note_ref_count: Int? = try? blocks.reduce(initialResult: 0) { index, partialResult, item in
|
||||||
let one_note_ref = ndb_blocks
|
switch item {
|
||||||
.filter({
|
case .mention(let mention):
|
||||||
if case .mention(let mention) = $0,
|
if let typ = mention.bech32_type,
|
||||||
let typ = mention.bech32_type,
|
|
||||||
typ.is_notelike {
|
typ.is_notelike {
|
||||||
return true
|
return .loopReturn(partialResult + 1)
|
||||||
}
|
}
|
||||||
return false
|
|
||||||
})
|
|
||||||
.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
|
|
||||||
if can_hide_last_previewable_refs {
|
|
||||||
outerLoop: for (i, block) in ndb_blocks.enumerated().reversed() {
|
|
||||||
if block.is_previewable {
|
|
||||||
switch block {
|
|
||||||
case .mention:
|
|
||||||
end_mention_count += 1
|
|
||||||
|
|
||||||
// If there is more than one previewable mention,
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
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
|
|
||||||
}
|
|
||||||
let url_type = classify_url(url)
|
|
||||||
if case .link = url_type {
|
|
||||||
end_url_count += 1
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
break
|
|
||||||
}
|
|
||||||
hide_text_index = i
|
|
||||||
} else if case .text(let txt_block) = block,
|
|
||||||
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 {
|
|
||||||
// SPECIAL CASE:
|
|
||||||
// We should keep hashtags at the end sequence but hide all the other previewables around it.
|
|
||||||
hide_text_index = i
|
|
||||||
} else {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var ind: Int = -1
|
|
||||||
let txt: CompatibleText = ndb_blocks.reduce(into: CompatibleText()) { str, block in
|
|
||||||
ind = ind + 1
|
|
||||||
|
|
||||||
// Add the rendered previewable blocks to their type-specific lists.
|
|
||||||
switch block {
|
|
||||||
case .url(let url_block):
|
|
||||||
guard let url_string = NdbBlock.convertToStringCopy(from: url_block),
|
|
||||||
let url = URL(string: url_string) else {
|
|
||||||
break // We can't classify this, ignore and move on
|
|
||||||
}
|
|
||||||
let url_type = classify_url(url)
|
|
||||||
urls.append(url_type)
|
|
||||||
case .invoice(let invoice_block):
|
|
||||||
guard let invoice = invoice_block.as_invoice() else { break }
|
|
||||||
invoices.append(invoice)
|
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
return .loopContinue
|
||||||
|
}
|
||||||
|
let one_note_ref = note_ref_count == 1
|
||||||
|
|
||||||
if can_hide_last_previewable_refs {
|
// Search backwards until we find the beginning index of the chain of previewables that reach the end of the content.
|
||||||
// If there are previewable blocks that occur before the consecutive sequence of them at the end of the content,
|
var hide_text_index: Int = 0
|
||||||
// we should not hide the text representation of any previewable block to avoid altering the format of the note.
|
if can_hide_last_previewable_refs {
|
||||||
if ind < hide_text_index && block.is_previewable {
|
let _: ()? = blocks.withList({ blocksList in
|
||||||
hide_text_index = ndb_blocks.endIndex
|
let endIndex = blocksList.count
|
||||||
}
|
return blocksList.forEachItemReversed({ index, block in
|
||||||
|
if block.is_previewable {
|
||||||
// No need to show the text representation of the block if the only previewables are the sequence of them
|
switch block {
|
||||||
// found at the end of the content.
|
case .mention:
|
||||||
// This is to save unnecessary use of screen space.
|
end_mention_count += 1
|
||||||
// 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 there is more than one previewable mention,
|
||||||
if ind >= hide_text_index {
|
// do not hide anything because we allow rich rendering of only one mention currently.
|
||||||
if case .text(let txt_block) = block,
|
// This should be fixed in the future to show events inline instead.
|
||||||
let txt = NdbBlock.convertToStringCopy(from: txt_block),
|
if end_mention_count > 1 {
|
||||||
txt.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
hide_text_index = endIndex
|
||||||
if case .hashtag = ndb_blocks[safe: ind+1] {
|
return .loopBreak
|
||||||
str = str + CompatibleText(stringLiteral: reduce_text_block(ind: ind, hide_text_index: hide_text_index, txt: txt))
|
}
|
||||||
|
case .url(let url_block):
|
||||||
|
guard let url_string = NdbBlock.convertToStringCopy(from: url_block),
|
||||||
|
let url = URL(string: url_string) else {
|
||||||
|
return .loopContinue // We can't classify this, ignore and move on
|
||||||
|
}
|
||||||
|
let url_type = classify_url(url)
|
||||||
|
if case .link = url_type {
|
||||||
|
end_url_count += 1
|
||||||
|
|
||||||
|
// 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 = endIndex
|
||||||
|
return .loopBreak
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break
|
||||||
}
|
}
|
||||||
} else if case .hashtag(let htag) = block {
|
hide_text_index = index
|
||||||
str = str + hashtag_str(htag.as_str())
|
|
||||||
}
|
}
|
||||||
return
|
else {
|
||||||
}
|
switch block {
|
||||||
}
|
case .text(let txt_block):
|
||||||
|
if let txt = NdbBlock.convertToStringCopy(from: txt_block),
|
||||||
switch block {
|
txt.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||||
case .mention(let m):
|
// We should hide whitespace at the end sequence.
|
||||||
if let typ = m.bech32_type, typ.is_notelike, one_note_ref {
|
hide_text_index = index
|
||||||
return
|
}
|
||||||
}
|
case .hashtag(_):
|
||||||
guard let mention = MentionRef(block: m) else { return }
|
// SPECIAL CASE:
|
||||||
str = str + mention_str(.any(mention), profiles: profiles)
|
// We should keep hashtags at the end sequence but hide all the other previewables around it.
|
||||||
case .text(let txt):
|
hide_text_index = index
|
||||||
if case .hashtag = blocks[safe: ind+1] {
|
default:
|
||||||
// SPECIAL CASE:
|
return .loopBreak
|
||||||
// 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()))
|
return .loopContinue
|
||||||
} else {
|
})
|
||||||
str = str + CompatibleText(stringLiteral: reduce_text_block(ind: ind, hide_text_index: hide_text_index, txt: txt.as_str()))
|
})
|
||||||
}
|
|
||||||
case .hashtag(let htag):
|
|
||||||
str = str + hashtag_str(htag.as_str())
|
|
||||||
case .invoice(let invoice):
|
|
||||||
guard let inv = invoice.as_invoice() else { return }
|
|
||||||
invoices.append(inv)
|
|
||||||
case .url(let url):
|
|
||||||
guard let url = URL(string: url.as_str()) else { return }
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
case .mention_index:
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return NoteArtifactsSeparated(content: txt, words: blocks.words, urls: urls, invoices: invoices)
|
var ind: Int = -1
|
||||||
|
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.
|
||||||
|
switch block {
|
||||||
|
case .url(let url_block):
|
||||||
|
guard let url_string = NdbBlock.convertToStringCopy(from: url_block),
|
||||||
|
let url = URL(string: url_string) else {
|
||||||
|
break // We can't classify this, ignore and move on
|
||||||
|
}
|
||||||
|
let url_type = classify_url(url)
|
||||||
|
urls.append(url_type)
|
||||||
|
case .invoice(let invoice_block):
|
||||||
|
guard let invoice = invoice_block.as_invoice() else { break }
|
||||||
|
invoices.append(invoice)
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if can_hide_last_previewable_refs {
|
||||||
|
// 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 = endIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
// No need to show the text representation of the block if the only previewables are the sequence of them
|
||||||
|
// found at the end of the content.
|
||||||
|
// This is to save unnecessary use of screen space.
|
||||||
|
// 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 {
|
||||||
|
switch block {
|
||||||
|
case .text(let txt_block):
|
||||||
|
if let txt = NdbBlock.convertToStringCopy(from: txt_block),
|
||||||
|
txt.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}) ?? nil
|
||||||
|
if let returnItem {
|
||||||
|
return .loopReturn(returnItem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case .hashtag(let htag):
|
||||||
|
return .loopReturn(str + hashtag_str(htag.as_str()))
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch block {
|
||||||
|
case .mention(let m):
|
||||||
|
if let typ = m.bech32_type, typ.is_notelike, one_note_ref {
|
||||||
|
return .loopContinue
|
||||||
|
}
|
||||||
|
guard let mention = MentionRef(block: m) else { return .loopContinue }
|
||||||
|
return .loopReturn(str + mention_str(.any(mention), profiles: profiles))
|
||||||
|
case .text(let txt):
|
||||||
|
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").
|
||||||
|
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):
|
||||||
|
return .loopReturn(str + hashtag_str(htag.as_str()))
|
||||||
|
case .invoice(let invoice):
|
||||||
|
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 .loopContinue }
|
||||||
|
let url_type = classify_url(url)
|
||||||
|
switch url_type {
|
||||||
|
case .media:
|
||||||
|
urls.append(url_type)
|
||||||
|
case .link(let url):
|
||||||
|
urls.append(url_type)
|
||||||
|
return .loopReturn(str + url_str(url))
|
||||||
|
}
|
||||||
|
case .mention_index:
|
||||||
|
return .loopContinue
|
||||||
|
}
|
||||||
|
return .loopContinue
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
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 {
|
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 {
|
guard let blockGroup = try? NdbBlockGroup.from(event: event, using: damus_state.ndb, and: damus_state.keypair) else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
for block in blockGroup.blocks {
|
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 {
|
||||||
continue
|
return .loopContinue
|
||||||
}
|
}
|
||||||
switch typ {
|
switch typ {
|
||||||
case .nprofile:
|
case .nprofile:
|
||||||
@@ -368,18 +368,19 @@ struct NoteContentView: View {
|
|||||||
if m.bech32.npub.matches_pubkey(pk: profile.pubkey) {
|
if m.bech32.npub.matches_pubkey(pk: profile.pubkey) {
|
||||||
load(force_artifacts: true)
|
load(force_artifacts: true)
|
||||||
}
|
}
|
||||||
case .nevent: continue
|
case .nevent: return .loopContinue
|
||||||
case .nrelay: continue
|
case .nrelay: return .loopContinue
|
||||||
case .nsec: continue
|
case .nsec: return .loopContinue
|
||||||
case .note: continue
|
case .note: return .loopContinue
|
||||||
case .naddr: continue
|
case .naddr: return .loopContinue
|
||||||
}
|
}
|
||||||
case .text: return
|
case .text: return .loopContinue
|
||||||
case .hashtag: return
|
case .hashtag: return .loopContinue
|
||||||
case .url: return
|
case .url: return .loopContinue
|
||||||
case .invoice: return
|
case .invoice: return .loopContinue
|
||||||
case .mention_index(_): return
|
case .mention_index(_): return .loopContinue
|
||||||
}
|
}
|
||||||
|
return .loopContinue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onAppear {
|
.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 {
|
guard let blockGroup = try? NdbBlockGroup.from(event: ev, using: ndb, and: keypair) else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
let urlBlocks: [URL] = blockGroup.blocks.reduce(into: []) { urls, block in
|
let urlBlocks: [URL] = (try? blockGroup.reduce(initialResult: Array<URL>()) { index, urls, block in
|
||||||
guard case .url(let url) = block,
|
switch block {
|
||||||
let parsed_url = URL(string: url.as_str()) else {
|
case .url(let url):
|
||||||
return
|
guard let parsed_url = URL(string: url.as_str()) else {
|
||||||
|
return .loopContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
if classify_url(parsed_url).is_img != nil {
|
||||||
|
return .loopReturn(urls + [parsed_url])
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break
|
||||||
}
|
}
|
||||||
|
return .loopContinue
|
||||||
if classify_url(parsed_url).is_img != nil {
|
}) ?? []
|
||||||
urls.append(parsed_url)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let mediaUrls = urlBlocks.map { MediaUrl.image($0) }
|
let mediaUrls = urlBlocks.map { MediaUrl.image($0) }
|
||||||
return mediaUrls.isEmpty ? nil : mediaUrls
|
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,
|
state.settings.mention_notification,
|
||||||
let blockGroup = try? NdbBlockGroup.from(event: ev, using: ndb, and: state.keypair)
|
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
|
||||||
guard case .npub = mention.bech32_type,
|
switch block {
|
||||||
(memcmp(state.keypair.pubkey.id.bytes, mention.bech32.npub.pubkey, 32) == 0) else {
|
case .mention(let mention):
|
||||||
continue
|
guard case .npub = mention.bech32_type,
|
||||||
|
(memcmp(state.keypair.pubkey.id.bytes, mention.bech32.npub.pubkey, 32) == 0) else {
|
||||||
|
return .loopContinue
|
||||||
|
}
|
||||||
|
let content_preview = render_notification_content_preview(ndb: ndb, ev: ev, profiles: state.profiles, keypair: state.keypair)
|
||||||
|
return .loopReturn(LocalNotification(type: .mention, event: ev, target: .note(ev), content: content_preview))
|
||||||
|
default:
|
||||||
|
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)
|
if let notification {
|
||||||
|
return notification
|
||||||
}
|
}
|
||||||
|
|
||||||
if ev.referenced_ids.contains(where: { note_id in
|
if ev.referenced_ids.contains(where: { note_id in
|
||||||
|
|||||||
@@ -348,12 +348,14 @@ class NoteContentViewTests: XCTestCase {
|
|||||||
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())
|
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 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())
|
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 {
|
func testMentionStr_Pubkey_ContainsAbbreviated() throws {
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ extension ndb_invoice_block {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum NdbBlock {
|
enum NdbBlock: ~Copyable {
|
||||||
case text(ndb_str_block)
|
case text(ndb_str_block)
|
||||||
case mention(ndb_mention_bech32_block)
|
case mention(ndb_mention_bech32_block)
|
||||||
case hashtag(ndb_str_block)
|
case hashtag(ndb_str_block)
|
||||||
@@ -107,10 +107,6 @@ struct NdbBlockGroup: ~Copyable {
|
|||||||
fileprivate let metadata: MaybeTxn<BlocksMetadata>
|
fileprivate let metadata: MaybeTxn<BlocksMetadata>
|
||||||
/// The raw text content of the note
|
/// The raw text content of the note
|
||||||
fileprivate let rawTextContent: String
|
fileprivate let rawTextContent: String
|
||||||
/// An iterable list of blocks that make up this object
|
|
||||||
var blocks: [NdbBlock] {
|
|
||||||
return self.collectBlocks()
|
|
||||||
}
|
|
||||||
var words: Int {
|
var words: Int {
|
||||||
return metadata.borrow { $0.words }
|
return metadata.borrow { $0.words }
|
||||||
}
|
}
|
||||||
@@ -149,12 +145,12 @@ enum MaybeTxn<T: ~Copyable>: ~Copyable {
|
|||||||
case pure(T)
|
case pure(T)
|
||||||
case txn(SafeNdbTxn<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 {
|
switch self {
|
||||||
case .pure(let item):
|
case .pure(let item):
|
||||||
return borrowFunction(item)
|
return try borrowFunction(item)
|
||||||
case .txn(let txn):
|
case .txn(let txn):
|
||||||
return borrowFunction(txn.val)
|
return try borrowFunction(txn.val)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -239,35 +235,60 @@ extension NdbBlockGroup {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: - Enumeration support
|
||||||
|
|
||||||
extension NdbBlockGroup {
|
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:**
|
/// **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
|
/// - 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.
|
||||||
fileprivate func collectBlocks() -> [NdbBlock] {
|
/// - Returns: The `Y` value returned by the provided function, when such function returns `.loopReturn(Y)`
|
||||||
var blocks = [NdbBlock]()
|
@discardableResult
|
||||||
|
func forEachBlock<Y>(_ borrowingFunction: ((Int, borrowing NdbBlock) throws -> NdbBlockList.LoopCommand<Y>)) rethrows -> Y? {
|
||||||
|
return try withList({ try $0.forEachItem(borrowingFunction) })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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()
|
||||||
|
|
||||||
// Ensure the C string remains valid for the entire operation by keeping
|
return try self.rawTextContent.withCString { cptr in
|
||||||
// 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)
|
var iter = ndb_block_iterator(content: cptr, blocks: nil, block: ndb_block(), p: nil)
|
||||||
|
|
||||||
// Start the iteration
|
// Start the iteration
|
||||||
self.metadata.borrow { value in
|
return try self.metadata.borrow { value in
|
||||||
ndb_blocks_iterate_start(cptr, value.as_ptr(), &iter)
|
ndb_blocks_iterate_start(cptr, value.as_ptr(), &iter)
|
||||||
|
|
||||||
// Collect blocks into array
|
// 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)) {
|
let block = NdbBlock(ndb_block_ptr(ptr: ptr)) {
|
||||||
blocks.append(block)
|
linkedList.add(item: block)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return try borrowingFunction(linkedList)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return blocks
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
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