Compare commits

...

3 Commits

Author SHA1 Message Date
eb889a7591 Optimize classify_url function
Changelog-Fixed: Optimized classify_url function
Signed-off-by: Terry Yiu <git@tyiu.xyz>
2025-05-06 10:58:39 -04:00
c9696dd9c8 Add inline note rendering of invoices to pull up wallet selector sheet
Changelog-Added: Added inline note rendering of invoices to pull up wallet selector sheet
Signed-off-by: Terry Yiu <git@tyiu.xyz>
2025-05-06 10:56:39 -04:00
212b4785fb Fix note rendering for those that contain previewable items or leading and trailing whitespaces
Changelog-Fixed: Fixed note rendering for those that contain previewable items or leading and trailing whitespaces
Closes: https://github.com/damus-io/damus/issues/2187
Signed-off-by: Terry Yiu <git@tyiu.xyz>
2025-05-06 10:55:35 -04:00
9 changed files with 425 additions and 76 deletions

View File

@@ -160,7 +160,7 @@ func translate_note(profiles: Profiles, keypair: Keypair, event: NostrEvent, set
// Render translated note // Render translated note
let translated_blocks = parse_note_content(content: .content(translated_note, event.tags)) let translated_blocks = parse_note_content(content: .content(translated_note, event.tags))
let artifacts = render_blocks(blocks: translated_blocks, profiles: profiles) let artifacts = render_blocks(blocks: translated_blocks, profiles: profiles, can_hide_last_previewable_refs: true)
// and cache it // and cache it
return .translated(Translated(artifacts: artifacts, language: note_lang)) return .translated(Translated(artifacts: artifacts, language: note_lang))

View File

@@ -73,85 +73,130 @@ func render_note_content(ev: NostrEvent, profiles: Profiles, keypair: Keypair) -
return .longform(LongformContent(ev.content)) return .longform(LongformContent(ev.content))
} }
return .separated(render_blocks(blocks: blocks, profiles: profiles)) return .separated(render_blocks(blocks: blocks, profiles: profiles, can_hide_last_previewable_refs: true))
} }
func render_blocks(blocks bs: Blocks, profiles: Profiles) -> NoteArtifactsSeparated { func render_blocks(blocks bs: Blocks, profiles: Profiles, can_hide_last_previewable_refs: Bool = false) -> NoteArtifactsSeparated {
var invoices: [Invoice] = [] var invoices: [Invoice] = []
var urls: [UrlType] = [] var urls: [UrlType] = []
let blocks = bs.blocks let blocks = bs.blocks
let one_note_ref = blocks var end_mention_count = 0
.filter({ var end_url_count = 0
if case .mention(let mention) = $0,
case .note = mention.ref { // Search backwards until we find the beginning index of the chain of previewables that reach the end of the content.
return true var hide_text_index = blocks.endIndex
if can_hide_last_previewable_refs {
outerLoop: for (i, block) in 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 = blocks.endIndex
break outerLoop
}
case .url(let url):
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 = blocks.endIndex
break outerLoop
}
}
default:
break
}
hide_text_index = i
} else if case .text(let txt) = block, txt.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
hide_text_index = i
} else {
break
} }
else { }
return false }
}
})
.count == 1
var ind: Int = -1 var ind: Int = -1
let txt: CompatibleText = blocks.reduce(CompatibleText()) { str, block in let txt: CompatibleText = blocks.reduce(CompatibleText()) { str, block in
ind = ind + 1 ind = ind + 1
// Add the rendered previewable blocks to their type-specific lists.
switch block { switch block {
case .mention(let m): case .invoice(let invoice):
if case .note = m.ref, one_note_ref { invoices.append(invoice)
case .url(let url):
let url_type = classify_url(url)
urls.append(url_type)
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 = blocks.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.
if ind >= hide_text_index {
return str return str
} }
}
switch block {
case .mention(let m):
return str + mention_str(m, profiles: profiles) return str + mention_str(m, profiles: profiles)
case .text(let txt): case .text(let txt):
return str + CompatibleText(stringLiteral: reduce_text_block(blocks: blocks, ind: ind, txt: txt, one_note_ref: one_note_ref)) return str + CompatibleText(stringLiteral: reduce_text_block(ind: ind, hide_text_index: hide_text_index, txt: txt))
case .relay(let relay): case .relay(let relay):
return str + CompatibleText(stringLiteral: relay) return str + CompatibleText(stringLiteral: relay)
case .hashtag(let htag): case .hashtag(let htag):
return str + hashtag_str(htag) return str + hashtag_str(htag)
case .invoice(let invoice): case .invoice(let invoice):
invoices.append(invoice) return str + invoice_str(invoice)
return str
case .url(let url): case .url(let url):
let url_type = classify_url(url) return str + url_str(url)
switch url_type {
case .media:
urls.append(url_type)
return str
case .link(let url):
urls.append(url_type)
return str + url_str(url)
}
} }
} }
return NoteArtifactsSeparated(content: txt, words: bs.words, urls: urls, invoices: invoices) return NoteArtifactsSeparated(content: txt, words: bs.words, urls: urls, invoices: invoices)
} }
func reduce_text_block(blocks: [Block], ind: Int, txt: String, one_note_ref: Bool) -> String { func reduce_text_block(ind: Int, hide_text_index: Int, txt: String) -> String {
var trimmed = txt var trimmed = txt
if let prev = blocks[safe: ind-1], // Trim leading whitespaces.
case .url(let u) = prev, if ind == 0 {
classify_url(u).is_media != nil { trimmed = trim_prefix(trimmed)
trimmed = " " + trim_prefix(trimmed)
} }
if let next = blocks[safe: ind+1] { // Trim trailing whitespaces if the following blocks will be hidden or if this is the last block.
if case .url(let u) = next, classify_url(u).is_media != nil { if ind == hide_text_index - 1 {
trimmed = trim_suffix(trimmed) trimmed = trim_suffix(trimmed)
} else if case .mention(let m) = next,
case .note = m.ref,
one_note_ref {
trimmed = trim_suffix(trimmed)
}
} }
return trimmed return trimmed
} }
func invoice_str(_ invoice: Invoice) -> CompatibleText {
var attributedString = AttributedString(stringLiteral: abbrev_identifier(invoice.string))
attributedString.link = URL(string: "damus:lightning:\(invoice.string)")
attributedString.foregroundColor = DamusColors.purple
return CompatibleText(attributed: attributedString)
}
func url_str(_ url: URL) -> CompatibleText { func url_str(_ url: URL) -> CompatibleText {
var attributedString = AttributedString(stringLiteral: url.absoluteString) var attributedString = AttributedString(stringLiteral: url.absoluteString)
attributedString.link = url attributedString.link = url
@@ -161,17 +206,16 @@ func url_str(_ url: URL) -> CompatibleText {
} }
func classify_url(_ url: URL) -> UrlType { func classify_url(_ url: URL) -> UrlType {
let str = url.lastPathComponent.lowercased() let fileExtension = url.lastPathComponent.lowercased().components(separatedBy: ".").last
if str.hasSuffix(".png") || str.hasSuffix(".jpg") || str.hasSuffix(".jpeg") || str.hasSuffix(".gif") || str.hasSuffix(".webp") { switch fileExtension {
case "png", "jpg", "jpeg", "gif", "webp":
return .media(.image(url)) return .media(.image(url))
} case "mp4", "mov", "m3u8":
if str.hasSuffix(".mp4") || str.hasSuffix(".mov") || str.hasSuffix(".m3u8") {
return .media(.video(url)) return .media(.video(url))
default:
return .link(url)
} }
return .link(url)
} }
func attributed_string_attach_icon(_ astr: inout AttributedString, img: UIImage) { func attributed_string_attach_icon(_ astr: inout AttributedString, img: UIImage) {
@@ -194,11 +238,11 @@ func mention_str(_ m: Mention<MentionRef>, profiles: Profiles) -> CompatibleText
let display_str: String = { let display_str: String = {
switch m.ref { switch m.ref {
case .pubkey(let pk): return getDisplayName(pk: pk, profiles: profiles) case .pubkey(let pk): return getDisplayName(pk: pk, profiles: profiles)
case .note: return abbrev_pubkey(bech32String) case .note: return abbrev_identifier(bech32String)
case .nevent: return abbrev_pubkey(bech32String) case .nevent: return abbrev_identifier(bech32String)
case .nprofile(let nprofile): return getDisplayName(pk: nprofile.author, profiles: profiles) case .nprofile(let nprofile): return getDisplayName(pk: nprofile.author, profiles: profiles)
case .nrelay(let url): return url case .nrelay(let url): return url
case .naddr: return abbrev_pubkey(bech32String) case .naddr: return abbrev_identifier(bech32String)
} }
}() }()

View File

@@ -43,6 +43,18 @@ struct DamusURLHandler {
return .route(.Script(script: model)) return .route(.Script(script: model))
case .purple(let purple_url): case .purple(let purple_url):
return await damus_state.purple.handle(purple_url: purple_url) return await damus_state.purple.handle(purple_url: purple_url)
case .invoice(let invoice):
if damus_state.settings.show_wallet_selector {
return .sheet(.select_wallet(invoice: invoice.string))
} else {
do {
try open_with_wallet(wallet: damus_state.settings.default_wallet.model, invoice: invoice.string)
return .no_action
}
catch {
return .sheet(.select_wallet(invoice: invoice.string))
}
}
case nil: case nil:
break break
} }
@@ -91,6 +103,11 @@ struct DamusURLHandler {
return .filter(filt) return .filter(filt)
case .script(let script): case .script(let script):
return .script(script) return .script(script)
case .invoice(let bolt11):
if let invoice = decode_bolt11(bolt11) {
return .invoice(invoice)
}
return nil
} }
return nil return nil
} }
@@ -103,5 +120,6 @@ struct DamusURLHandler {
case wallet_connect(WalletConnectURL) case wallet_connect(WalletConnectURL)
case script([UInt8]) case script([UInt8])
case purple(DamusPurpleURL) case purple(DamusPurpleURL)
case invoice(Invoice)
} }
} }

View File

@@ -12,6 +12,7 @@ enum NostrLink: Equatable {
case ref(RefId) case ref(RefId)
case filter(NostrFilter) case filter(NostrFilter)
case script([UInt8]) case script([UInt8])
case invoice(String)
} }
func encode_pubkey_uri(_ pubkey: Pubkey) -> String { func encode_pubkey_uri(_ pubkey: Pubkey) -> String {
@@ -93,8 +94,15 @@ func decode_nostr_uri(_ s: String) -> NostrLink? {
return return
} }
if parts.count >= 2 && parts[0] == "t" { if parts.count >= 2 {
return .filter(NostrFilter(hashtag: [parts[1].lowercased()])) switch parts[0] {
case "t":
return .filter(NostrFilter(hashtag: [parts[1].lowercased()]))
case "lightning":
return .invoice(parts[1])
default:
break
}
} }
guard parts.count == 1 else { guard parts.count == 1 else {

View File

@@ -37,7 +37,23 @@ enum Block: Equatable {
return false return false
} }
} }
var is_previewable: Bool {
switch self {
case .mention(let m):
switch m.ref {
case .note, .nevent: return true
default: return false
}
case .invoice:
return true
case .url:
return true
default:
return false
}
}
case text(String) case text(String)
case mention(Mention<MentionRef>) case mention(Mention<MentionRef>)
case hashtag(String) case hashtag(String)

View File

@@ -80,9 +80,9 @@ func parse_display_name(name: String?, display_name: String?, pubkey: Pubkey) ->
} }
func abbrev_bech32_pubkey(pubkey: Pubkey) -> String { func abbrev_bech32_pubkey(pubkey: Pubkey) -> String {
return abbrev_pubkey(String(pubkey.npub.dropFirst(4))) return abbrev_identifier(String(pubkey.npub.dropFirst(4)))
} }
func abbrev_pubkey(_ pubkey: String, amount: Int = 8) -> String { func abbrev_identifier(_ pubkey: String, amount: Int = 8) -> String {
return pubkey.prefix(amount) + ":" + pubkey.suffix(amount) return pubkey.prefix(amount) + ":" + pubkey.suffix(amount)
} }

View File

@@ -46,7 +46,7 @@ struct PubkeyView: View {
let bech32 = pubkey.npub let bech32 = pubkey.npub
HStack { HStack {
Text(verbatim: "\(abbrev_pubkey(bech32, amount: sidemenu ? 12 : 16))") Text(verbatim: "\(abbrev_identifier(bech32, amount: sidemenu ? 12 : 16))")
.font(sidemenu ? .system(size: 10) : .footnote) .font(sidemenu ? .system(size: 10) : .footnote)
.foregroundColor(keyColor()) .foregroundColor(keyColor())
.padding(5) .padding(5)

View File

@@ -10,28 +10,292 @@ import SwiftUI
@testable import damus @testable import damus
class NoteContentViewTests: XCTestCase { class NoteContentViewTests: XCTestCase {
func testRenderBlocksWithNonLatinHashtags() { func testRenderBlocksWithNonLatinHashtags() throws {
let content = "Damusはかっこいいです #cool #かっこいい" let content = "Damusはかっこいいです #cool #かっこいい"
let note = NostrEvent(content: content, keypair: test_keypair, tags: [["t", "かっこいい"]])! let note = try XCTUnwrap(NostrEvent(content: content, keypair: test_keypair, tags: [["t", "かっこいい"]]))
let parsed: Blocks = parse_note_content(content: .init(note: note, keypair: test_keypair)) let parsed: Blocks = parse_note_content(content: .init(note: note, keypair: test_keypair))
let testState = test_damus_state let testState = test_damus_state
let text: NoteArtifactsSeparated = render_blocks(blocks: parsed, profiles: testState.profiles) let text: NoteArtifactsSeparated = render_blocks(blocks: parsed, profiles: testState.profiles, can_hide_last_previewable_refs: true)
let attributedText: AttributedString = text.content.attributed let attributedText: AttributedString = text.content.attributed
let runs: AttributedString.Runs = attributedText.runs let runs: AttributedString.Runs = attributedText.runs
let runArray: [AttributedString.Runs.Run] = Array(runs) let runArray: [AttributedString.Runs.Run] = Array(runs)
print(runArray.description) print(runArray.description)
XCTAssertEqual(runArray[1].link?.absoluteString, "damus:t:cool", "Latin-character hashtag is missing. Runs description :\(runArray.description)") XCTAssertEqual(runArray[1].link?.absoluteString, "damus:t:cool", "Latin-character hashtag is missing. Runs description :\(runArray.description)")
XCTAssertEqual(runArray[3].link?.absoluteString.removingPercentEncoding!, "damus:t:かっこいい", "Non-latin-character hashtag is missing. Runs description :\(runArray.description)") XCTAssertEqual(runArray[3].link?.absoluteString.removingPercentEncoding, "damus:t:かっこいい", "Non-latin-character hashtag is missing. Runs description :\(runArray.description)")
} }
func testRenderBlocksWithLeadingAndTrailingWhitespacesTrimmed() throws {
let content = " \n\n Hello, \nworld! \n\n "
let note = try XCTUnwrap(NostrEvent(content: content, keypair: test_keypair))
let parsed: Blocks = parse_note_content(content: .init(note: note, keypair: test_keypair))
let testState = test_damus_state
let noteArtifactsSeparated: NoteArtifactsSeparated = render_blocks(blocks: parsed, profiles: testState.profiles, can_hide_last_previewable_refs: true)
let attributedText: AttributedString = noteArtifactsSeparated.content.attributed
let text = attributedText.description
let runs: AttributedString.Runs = attributedText.runs
let runArray: [AttributedString.Runs.Run] = Array(runs)
XCTAssertEqual(runArray.count, 1)
XCTAssertTrue(text.contains("Hello, \nworld!"))
XCTAssertFalse(text.contains(content))
}
func testRenderBlocksWithMediaBlockInMiddleRendered() throws {
let content = " Check this out: https://damus.io/image.png Isn't this cool? "
let note = try XCTUnwrap(NostrEvent(content: content, keypair: test_keypair))
let parsed: Blocks = parse_note_content(content: .init(note: note, keypair: test_keypair))
let testState = test_damus_state
let noteArtifactsSeparated: NoteArtifactsSeparated = render_blocks(blocks: parsed, profiles: testState.profiles)
let attributedText: AttributedString = noteArtifactsSeparated.content.attributed
let runs: AttributedString.Runs = attributedText.runs
let runArray: [AttributedString.Runs.Run] = Array(runs)
XCTAssertEqual(runArray.count, 3)
XCTAssertTrue(runArray[0].description.contains("Check this out: "))
XCTAssertTrue(runArray[1].description.contains("https://damus.io/image.png "))
XCTAssertEqual(runArray[1].link?.absoluteString, "https://damus.io/image.png")
XCTAssertTrue(runArray[2].description.contains(" Isn't this cool?"))
XCTAssertEqual(noteArtifactsSeparated.images.count, 1)
XCTAssertEqual(noteArtifactsSeparated.images[0].absoluteString, "https://damus.io/image.png")
}
func testRenderBlocksWithInvoiceInMiddleAbbreviated() throws {
let invoiceString = "lnbc100n1p357sl0sp5t9n56wdztun39lgdqlr30xqwksg3k69q4q2rkr52aplujw0esn0qpp5mrqgljk62z20q4nvgr6lzcyn6fhylzccwdvu4k77apg3zmrkujjqdpzw35xjueqd9ejqcfqv3jhxcmjd9c8g6t0dcxqyjw5qcqpjrzjqt56h4gvp5yx36u2uzqa6qwcsk3e2duunfxppzj9vhypc3wfe2wswz607uqq3xqqqsqqqqqqqqqqqlqqyg9qyysgqagx5h20aeulj3gdwx3kxs8u9f4mcakdkwuakasamm9562ffyr9en8yg20lg0ygnr9zpwp68524kmda0t5xp2wytex35pu8hapyjajxqpsql29r"
let content = " Donations appreciated: \(invoiceString) Pura Vida "
let note = try XCTUnwrap(NostrEvent(content: content, keypair: test_keypair))
let parsed: Blocks = parse_note_content(content: .init(note: note, keypair: test_keypair))
let testState = test_damus_state
let noteArtifactsSeparated: NoteArtifactsSeparated = render_blocks(blocks: parsed, profiles: testState.profiles)
let attributedText: AttributedString = noteArtifactsSeparated.content.attributed
let runs: AttributedString.Runs = attributedText.runs
let runArray: [AttributedString.Runs.Run] = Array(runs)
XCTAssertEqual(runArray.count, 3)
XCTAssertTrue(runArray[0].description.contains("Donations appreciated: "))
XCTAssertTrue(runArray[1].description.contains("lnbc100n:qpsql29r"))
XCTAssertEqual(runArray[1].link?.absoluteString, "damus:lightning:\(invoiceString)")
XCTAssertTrue(runArray[2].description.contains(" Pura Vida"))
}
func testRenderBlocksWithNoteIdInMiddleAreRendered() throws {
let noteId = test_note.id.bech32
let content = " Check this out: nostr:\(noteId) Pura Vida "
let note = try XCTUnwrap(NostrEvent(content: content, keypair: test_keypair))
let parsed: Blocks = parse_note_content(content: .init(note: note, keypair: test_keypair))
let testState = test_damus_state
let noteArtifactsSeparated: NoteArtifactsSeparated = render_blocks(blocks: parsed, profiles: testState.profiles)
let attributedText: AttributedString = noteArtifactsSeparated.content.attributed
let runs: AttributedString.Runs = attributedText.runs
let runArray: [AttributedString.Runs.Run] = Array(runs)
XCTAssertEqual(runArray.count, 3)
XCTAssertTrue(runArray[0].description.contains("Check this out: "))
XCTAssertTrue(runArray[1].description.contains("note1qqq:qqn2l0z3"))
XCTAssertEqual(runArray[1].link?.absoluteString, "damus:nostr:\(noteId)")
XCTAssertTrue(runArray[2].description.contains(" Pura Vida"))
}
func testRenderBlocksWithNeventInMiddleAreRendered() throws {
let nevent = "nevent1qqstna2yrezu5wghjvswqqculvvwxsrcvu7uc0f78gan4xqhvz49d9spr3mhxue69uhkummnw3ez6un9d3shjtn4de6x2argwghx6egpr4mhxue69uhkummnw3ez6ur4vgh8wetvd3hhyer9wghxuet5nxnepm"
let content = " Check this out: nostr:\(nevent) Pura Vida "
let note = try XCTUnwrap(NostrEvent(content: content, keypair: test_keypair))
let parsed: Blocks = parse_note_content(content: .init(note: note, keypair: test_keypair))
let testState = test_damus_state
let noteArtifactsSeparated: NoteArtifactsSeparated = render_blocks(blocks: parsed, profiles: testState.profiles)
let attributedText: AttributedString = noteArtifactsSeparated.content.attributed
let runs: AttributedString.Runs = attributedText.runs
let runArray: [AttributedString.Runs.Run] = Array(runs)
XCTAssertEqual(runArray.count, 3)
XCTAssertTrue(runArray[0].description.contains("Check this out: "))
XCTAssertTrue(runArray[1].description.contains("nevent1q:t5nxnepm"))
XCTAssertEqual(runArray[1].link?.absoluteString, "damus:nostr:\(nevent)")
XCTAssertTrue(runArray[2].description.contains(" Pura Vida"))
}
func testRenderBlocksWithPreviewableBlocksAtEndAreHidden() throws {
let noteId = test_note.id.bech32
let invoiceString = "lnbc100n1p357sl0sp5t9n56wdztun39lgdqlr30xqwksg3k69q4q2rkr52aplujw0esn0qpp5mrqgljk62z20q4nvgr6lzcyn6fhylzccwdvu4k77apg3zmrkujjqdpzw35xjueqd9ejqcfqv3jhxcmjd9c8g6t0dcxqyjw5qcqpjrzjqt56h4gvp5yx36u2uzqa6qwcsk3e2duunfxppzj9vhypc3wfe2wswz607uqq3xqqqsqqqqqqqqqqqlqqyg9qyysgqagx5h20aeulj3gdwx3kxs8u9f4mcakdkwuakasamm9562ffyr9en8yg20lg0ygnr9zpwp68524kmda0t5xp2wytex35pu8hapyjajxqpsql29r"
let content = " Check this out. \nhttps://hidden.tld/\nhttps://damus.io/hidden1.png\n\(invoiceString)\nhttps://damus.io/hidden2.png\nnostr:\(noteId) "
let note = try XCTUnwrap(NostrEvent(content: content, keypair: test_keypair))
let parsed: Blocks = parse_note_content(content: .init(note: note, keypair: test_keypair))
let testState = test_damus_state
let noteArtifactsSeparated: NoteArtifactsSeparated = render_blocks(blocks: parsed, profiles: testState.profiles, can_hide_last_previewable_refs: true)
let attributedText: AttributedString = noteArtifactsSeparated.content.attributed
let runs: AttributedString.Runs = attributedText.runs
let runArray: [AttributedString.Runs.Run] = Array(runs)
XCTAssertEqual(runArray.count, 1)
XCTAssertTrue(runArray[0].description.contains("Check this out."))
XCTAssertFalse(runArray[0].description.contains("https://hidden.tld/"))
XCTAssertFalse(runArray[0].description.contains("https://damus.io/hidden1.png"))
XCTAssertFalse(runArray[0].description.contains("lnbc100n:qpsql29r"))
XCTAssertFalse(runArray[0].description.contains("https://damus.io/hidden2.png"))
XCTAssertFalse(runArray[0].description.contains("note1qqq:qqn2l0z3"))
XCTAssertEqual(noteArtifactsSeparated.images.count, 2)
XCTAssertEqual(noteArtifactsSeparated.images[0].absoluteString, "https://damus.io/hidden1.png")
XCTAssertEqual(noteArtifactsSeparated.images[1].absoluteString, "https://damus.io/hidden2.png")
XCTAssertEqual(noteArtifactsSeparated.media.count, 2)
XCTAssertEqual(noteArtifactsSeparated.media[0].url.absoluteString, "https://damus.io/hidden1.png")
XCTAssertEqual(noteArtifactsSeparated.media[1].url.absoluteString, "https://damus.io/hidden2.png")
XCTAssertEqual(noteArtifactsSeparated.links.count, 1)
XCTAssertEqual(noteArtifactsSeparated.links[0].absoluteString, "https://hidden.tld/")
XCTAssertEqual(noteArtifactsSeparated.invoices.count, 1)
XCTAssertEqual(noteArtifactsSeparated.invoices[0].string, invoiceString)
}
func testRenderBlocksWithMultipleLinksAtEndAreNotHidden() throws {
let noteId = test_note.id.bech32
let invoiceString = "lnbc100n1p357sl0sp5t9n56wdztun39lgdqlr30xqwksg3k69q4q2rkr52aplujw0esn0qpp5mrqgljk62z20q4nvgr6lzcyn6fhylzccwdvu4k77apg3zmrkujjqdpzw35xjueqd9ejqcfqv3jhxcmjd9c8g6t0dcxqyjw5qcqpjrzjqt56h4gvp5yx36u2uzqa6qwcsk3e2duunfxppzj9vhypc3wfe2wswz607uqq3xqqqsqqqqqqqqqqqlqqyg9qyysgqagx5h20aeulj3gdwx3kxs8u9f4mcakdkwuakasamm9562ffyr9en8yg20lg0ygnr9zpwp68524kmda0t5xp2wytex35pu8hapyjajxqpsql29r"
let content = " Check this out. \nhttps://nothidden1.tld/\nhttps://nothidden2.tld/\nhttps://damus.io/nothidden1.png\n\(invoiceString)\nhttps://damus.io/nothidden2.png\nnostr:\(noteId) "
let note = try XCTUnwrap(NostrEvent(content: content, keypair: test_keypair))
let parsed: Blocks = parse_note_content(content: .init(note: note, keypair: test_keypair))
let testState = test_damus_state
let noteArtifactsSeparated: NoteArtifactsSeparated = render_blocks(blocks: parsed, profiles: testState.profiles, can_hide_last_previewable_refs: true)
let attributedText: AttributedString = noteArtifactsSeparated.content.attributed
let runs: AttributedString.Runs = attributedText.runs
let runArray: [AttributedString.Runs.Run] = Array(runs)
XCTAssertEqual(runArray.count, 12)
XCTAssertTrue(runArray[0].description.contains("Check this out."))
XCTAssertTrue(runArray[1].description.contains("https://nothidden1.tld/"))
XCTAssertTrue(runArray[3].description.contains("https://nothidden2.tld/"))
XCTAssertTrue(runArray[5].description.contains("https://damus.io/nothidden1.png"))
XCTAssertTrue(runArray[7].description.contains("lnbc100n:qpsql29r"))
XCTAssertTrue(runArray[9].description.contains("https://damus.io/nothidden2.png"))
XCTAssertTrue(runArray[11].description.contains("note1qqq:qqn2l0z3"))
XCTAssertEqual(noteArtifactsSeparated.images.count, 2)
XCTAssertEqual(noteArtifactsSeparated.images[0].absoluteString, "https://damus.io/nothidden1.png")
XCTAssertEqual(noteArtifactsSeparated.images[1].absoluteString, "https://damus.io/nothidden2.png")
XCTAssertEqual(noteArtifactsSeparated.media.count, 2)
XCTAssertEqual(noteArtifactsSeparated.media[0].url.absoluteString, "https://damus.io/nothidden1.png")
XCTAssertEqual(noteArtifactsSeparated.media[1].url.absoluteString, "https://damus.io/nothidden2.png")
XCTAssertEqual(noteArtifactsSeparated.links.count, 2)
XCTAssertEqual(noteArtifactsSeparated.links[0].absoluteString, "https://nothidden1.tld/")
XCTAssertEqual(noteArtifactsSeparated.links[1].absoluteString, "https://nothidden2.tld/")
XCTAssertEqual(noteArtifactsSeparated.invoices.count, 1)
XCTAssertEqual(noteArtifactsSeparated.invoices[0].string, invoiceString)
}
func testRenderBlocksWithMultipleEventsAtEndAreNotHidden() throws {
let noteId = test_note.id.bech32
let nevent = "nevent1qqstna2yrezu5wghjvswqqculvvwxsrcvu7uc0f78gan4xqhvz49d9spr3mhxue69uhkummnw3ez6un9d3shjtn4de6x2argwghx6egpr4mhxue69uhkummnw3ez6ur4vgh8wetvd3hhyer9wghxuet5nxnepm"
let invoiceString = "lnbc100n1p357sl0sp5t9n56wdztun39lgdqlr30xqwksg3k69q4q2rkr52aplujw0esn0qpp5mrqgljk62z20q4nvgr6lzcyn6fhylzccwdvu4k77apg3zmrkujjqdpzw35xjueqd9ejqcfqv3jhxcmjd9c8g6t0dcxqyjw5qcqpjrzjqt56h4gvp5yx36u2uzqa6qwcsk3e2duunfxppzj9vhypc3wfe2wswz607uqq3xqqqsqqqqqqqqqqqlqqyg9qyysgqagx5h20aeulj3gdwx3kxs8u9f4mcakdkwuakasamm9562ffyr9en8yg20lg0ygnr9zpwp68524kmda0t5xp2wytex35pu8hapyjajxqpsql29r"
let content = " Check this out. \nnostr:\(noteId)\nnostr:\(nevent)\nhttps://damus.io/nothidden1.png\n\(invoiceString)\nhttps://damus.io/nothidden2.png "
let note = try XCTUnwrap(NostrEvent(content: content, keypair: test_keypair))
let parsed: Blocks = parse_note_content(content: .init(note: note, keypair: test_keypair))
let testState = test_damus_state
let noteArtifactsSeparated: NoteArtifactsSeparated = render_blocks(blocks: parsed, profiles: testState.profiles, can_hide_last_previewable_refs: true)
let attributedText: AttributedString = noteArtifactsSeparated.content.attributed
let runs: AttributedString.Runs = attributedText.runs
let runArray: [AttributedString.Runs.Run] = Array(runs)
XCTAssertEqual(runArray.count, 10)
XCTAssertTrue(runArray[0].description.contains("Check this out."))
XCTAssertTrue(runArray[1].description.contains("note1qqq:qqn2l0z3"))
XCTAssertTrue(runArray[3].description.contains("nevent1q:t5nxnepm"))
XCTAssertTrue(runArray[5].description.contains("https://damus.io/nothidden1.png"))
XCTAssertTrue(runArray[7].description.contains("lnbc100n:qpsql29r"))
XCTAssertTrue(runArray[9].description.contains("https://damus.io/nothidden2.png"))
XCTAssertEqual(noteArtifactsSeparated.images.count, 2)
XCTAssertEqual(noteArtifactsSeparated.images[0].absoluteString, "https://damus.io/nothidden1.png")
XCTAssertEqual(noteArtifactsSeparated.images[1].absoluteString, "https://damus.io/nothidden2.png")
XCTAssertEqual(noteArtifactsSeparated.media.count, 2)
XCTAssertEqual(noteArtifactsSeparated.media[0].url.absoluteString, "https://damus.io/nothidden1.png")
XCTAssertEqual(noteArtifactsSeparated.media[1].url.absoluteString, "https://damus.io/nothidden2.png")
XCTAssertEqual(noteArtifactsSeparated.links.count, 0)
XCTAssertEqual(noteArtifactsSeparated.invoices.count, 1)
XCTAssertEqual(noteArtifactsSeparated.invoices[0].string, invoiceString)
}
func testRenderBlocksWithPreviewableBlocksAtEndAreNotHiddenWhenMediaBlockPrecedesThem() throws {
let content = " Check this out: https://damus.io/image.png Isn't this cool? \nhttps://damus.io/nothidden.png "
let note = try XCTUnwrap(NostrEvent(content: content, keypair: test_keypair))
let parsed: Blocks = parse_note_content(content: .init(note: note, keypair: test_keypair))
let testState = test_damus_state
let noteArtifactsSeparated: NoteArtifactsSeparated = render_blocks(blocks: parsed, profiles: testState.profiles, can_hide_last_previewable_refs: true)
let attributedText: AttributedString = noteArtifactsSeparated.content.attributed
let runs: AttributedString.Runs = attributedText.runs
let runArray: [AttributedString.Runs.Run] = Array(runs)
XCTAssertEqual(runArray.count, 4)
XCTAssertTrue(runArray[0].description.contains("Check this out: "))
XCTAssertTrue(runArray[1].description.contains("https://damus.io/image.png "))
XCTAssertEqual(runArray[1].link?.absoluteString, "https://damus.io/image.png")
XCTAssertTrue(runArray[2].description.contains(" Isn't this cool?"))
XCTAssertTrue(runArray[3].description.contains("https://damus.io/nothidden.png"))
XCTAssertEqual(runArray[3].link?.absoluteString, "https://damus.io/nothidden.png")
XCTAssertEqual(noteArtifactsSeparated.images.count, 2)
XCTAssertEqual(noteArtifactsSeparated.images[0].absoluteString, "https://damus.io/image.png")
XCTAssertEqual(noteArtifactsSeparated.images[1].absoluteString, "https://damus.io/nothidden.png")
}
func testRenderBlocksWithPreviewableBlocksAtEndAreNotHiddenWhenInvoicePrecedesThem() throws {
let invoiceString = "lnbc100n1p357sl0sp5t9n56wdztun39lgdqlr30xqwksg3k69q4q2rkr52aplujw0esn0qpp5mrqgljk62z20q4nvgr6lzcyn6fhylzccwdvu4k77apg3zmrkujjqdpzw35xjueqd9ejqcfqv3jhxcmjd9c8g6t0dcxqyjw5qcqpjrzjqt56h4gvp5yx36u2uzqa6qwcsk3e2duunfxppzj9vhypc3wfe2wswz607uqq3xqqqsqqqqqqqqqqqlqqyg9qyysgqagx5h20aeulj3gdwx3kxs8u9f4mcakdkwuakasamm9562ffyr9en8yg20lg0ygnr9zpwp68524kmda0t5xp2wytex35pu8hapyjajxqpsql29r"
let content = " Donations appreciated: \(invoiceString) Pura Vida \nhttps://damus.io/nothidden.png "
let note = try XCTUnwrap(NostrEvent(content: content, keypair: test_keypair))
let parsed: Blocks = parse_note_content(content: .init(note: note, keypair: test_keypair))
let testState = test_damus_state
let noteArtifactsSeparated: NoteArtifactsSeparated = render_blocks(blocks: parsed, profiles: testState.profiles, can_hide_last_previewable_refs: true)
let attributedText: AttributedString = noteArtifactsSeparated.content.attributed
let runs: AttributedString.Runs = attributedText.runs
let runArray: [AttributedString.Runs.Run] = Array(runs)
XCTAssertEqual(runArray.count, 4)
XCTAssertTrue(runArray[0].description.contains("Donations appreciated: "))
XCTAssertTrue(runArray[1].description.contains("lnbc100n:qpsql29r"))
XCTAssertEqual(runArray[1].link?.absoluteString, "damus:lightning:\(invoiceString)")
XCTAssertTrue(runArray[2].description.contains(" Pura Vida"))
XCTAssertTrue(runArray[3].description.contains("https://damus.io/nothidden.png"))
XCTAssertEqual(runArray[3].link?.absoluteString, "https://damus.io/nothidden.png")
XCTAssertEqual(noteArtifactsSeparated.images.count, 1)
XCTAssertEqual(noteArtifactsSeparated.images[0].absoluteString, "https://damus.io/nothidden.png")
}
/// Based on https://github.com/damus-io/damus/issues/1468 /// Based on https://github.com/damus-io/damus/issues/1468
/// Tests whether a note content view correctly parses an image block when url in JSON content contains optional escaped slashes /// Tests whether a note content view correctly parses an image block when url in JSON content contains optional escaped slashes
func testParseImageBlockInContentWithEscapedSlashes() { func testParseImageBlockInContentWithEscapedSlashes() throws {
let testJSONWithEscapedSlashes = "{\"tags\":[],\"pubkey\":\"f8e6c64342f1e052480630e27e1016dce35fc3a614e60434fef4aa2503328ca9\",\"content\":\"https:\\/\\/cdn.nostr.build\\/i\\/5c1d3296f66c2630131bf123106486aeaf051ed8466031c0e0532d70b33cddb2.jpg\",\"created_at\":1691864981,\"kind\":1,\"sig\":\"fc0033aa3d4df50b692a5b346fa816fdded698de2045e36e0642a021391468c44ca69c2471adc7e92088131872d4aaa1e90ea6e1ad97f3cc748f4aed96dfae18\",\"id\":\"e8f6eca3b161abba034dac9a02bb6930ecde9fd2fb5d6c5f22a05526e11382cb\"}" let testJSONWithEscapedSlashes = "{\"tags\":[],\"pubkey\":\"f8e6c64342f1e052480630e27e1016dce35fc3a614e60434fef4aa2503328ca9\",\"content\":\"https:\\/\\/cdn.nostr.build\\/i\\/5c1d3296f66c2630131bf123106486aeaf051ed8466031c0e0532d70b33cddb2.jpg\",\"created_at\":1691864981,\"kind\":1,\"sig\":\"fc0033aa3d4df50b692a5b346fa816fdded698de2045e36e0642a021391468c44ca69c2471adc7e92088131872d4aaa1e90ea6e1ad97f3cc748f4aed96dfae18\",\"id\":\"e8f6eca3b161abba034dac9a02bb6930ecde9fd2fb5d6c5f22a05526e11382cb\"}"
let testNote = NostrEvent.owned_from_json(json: testJSONWithEscapedSlashes)! let testNote = try XCTUnwrap(NostrEvent.owned_from_json(json: testJSONWithEscapedSlashes))
let parsed = parse_note_content(content: .init(note: testNote, keypair: test_keypair)) let parsed = parse_note_content(content: .init(note: testNote, keypair: test_keypair))
XCTAssertTrue((parsed.blocks[0].asURL != nil), "NoteContentView does not correctly parse an image block when url in JSON content contains optional escaped slashes.") XCTAssertTrue((parsed.blocks[0].asURL != nil), "NoteContentView does not correctly parse an image block when url in JSON content contains optional escaped slashes.")
@@ -69,9 +333,9 @@ class NoteContentViewTests: XCTestCase {
} }
func testMentionStr_Note_ContainsFullBech32() { func testMentionStr_Note_ContainsFullBech32() {
let compatableText = createCompatibleText(test_note.id.bech32) let compatibleText = createCompatibleText(test_note.id.bech32)
assertCompatibleTextHasExpectedString(compatibleText: compatableText, expected: test_note.id.bech32) assertCompatibleTextHasExpectedString(compatibleText: compatibleText, expected: test_note.id.bech32)
} }
func testMentionStr_Nevent_ContainsAbbreviated() { func testMentionStr_Nevent_ContainsAbbreviated() {

View File

@@ -36,10 +36,9 @@ class damusTests: XCTestCase {
XCTAssertEqual(bytes.count, 32) XCTAssertEqual(bytes.count, 32)
} }
func testTrimmingFunctions() { func testTrimSuffix() {
let txt = " bobs " let txt = " bobs "
XCTAssertEqual(trim_prefix(txt), "bobs ")
XCTAssertEqual(trim_suffix(txt), " bobs") XCTAssertEqual(trim_suffix(txt), " bobs")
} }