damus

nostr ios client
git clone git://jb55.com/damus
Log | Files | Refs | README | LICENSE

commit f1fdae59573f6961b3c1f241c3f8949b88918e7c
parent f96647fa407b3df2842dac0632b9368eba4f131b
Author: Terry Yiu <git@tyiu.xyz>
Date:   Sat,  1 Mar 2025 16:30:25 -0500

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>

Diffstat:
Mdamus/Components/TranslateView.swift | 2+-
Mdamus/Models/NoteContent.swift | 144+++++++++++++++++++++++++++++++++++++++++++++++++++----------------------------
Mdamus/Types/Block.swift | 18+++++++++++++++++-
Mdamus/Util/DisplayName.swift | 4++--
Mdamus/Views/PubkeyView.swift | 2+-
MdamusTests/NoteContentViewTests.swift | 280++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
MdamusTests/damusTests.swift | 3+--
7 files changed, 387 insertions(+), 66 deletions(-)

diff --git a/damus/Components/TranslateView.swift b/damus/Components/TranslateView.swift @@ -160,7 +160,7 @@ func translate_note(profiles: Profiles, keypair: Keypair, event: NostrEvent, set // Render translated note 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 return .translated(Translated(artifacts: artifacts, language: note_lang)) diff --git a/damus/Models/NoteContent.swift b/damus/Models/NoteContent.swift @@ -73,85 +73,129 @@ func render_note_content(ev: NostrEvent, profiles: Profiles, keypair: Keypair) - 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 urls: [UrlType] = [] let blocks = bs.blocks - - let one_note_ref = blocks - .filter({ - if case .mention(let mention) = $0, - case .note = mention.ref { - return true - } - else { - return false + + var end_mention_count = 0 + var end_url_count = 0 + + // Search backwards until we find the beginning index of the chain of previewables that reach the end of the content. + 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 } - }) - .count == 1 - + } + } + var ind: Int = -1 let txt: CompatibleText = blocks.reduce(CompatibleText()) { str, block in ind = ind + 1 - + + // Add the rendered previewable blocks to their type-specific lists. switch block { - case .mention(let m): - if case .note = m.ref, one_note_ref { + case .invoice(let invoice): + 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 } + } + + switch block { + case .mention(let m): return str + mention_str(m, profiles: profiles) 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): return str + CompatibleText(stringLiteral: relay) - case .hashtag(let htag): return str + hashtag_str(htag) case .invoice(let invoice): - invoices.append(invoice) - return str + return str + invoice_str(invoice) case .url(let url): - let url_type = classify_url(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 str + url_str(url) } } 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 - - if let prev = blocks[safe: ind-1], - case .url(let u) = prev, - classify_url(u).is_media != nil { - trimmed = " " + trim_prefix(trimmed) + + // Trim leading whitespaces. + if ind == 0 { + trimmed = trim_prefix(trimmed) } - - if let next = blocks[safe: ind+1] { - if case .url(let u) = next, classify_url(u).is_media != nil { - trimmed = trim_suffix(trimmed) - } else if case .mention(let m) = next, - case .note = m.ref, - one_note_ref { - trimmed = trim_suffix(trimmed) - } + + // Trim trailing whitespaces if the following blocks will be hidden or if this is the last block. + if ind == hide_text_index - 1 { + trimmed = trim_suffix(trimmed) } - + return trimmed } +func invoice_str(_ invoice: Invoice) -> CompatibleText { + var attributedString = AttributedString(stringLiteral: abbrev_identifier(invoice.string)) + attributedString.foregroundColor = DamusColors.purple + + return CompatibleText(attributed: attributedString) +} + func url_str(_ url: URL) -> CompatibleText { var attributedString = AttributedString(stringLiteral: url.absoluteString) attributedString.link = url @@ -194,11 +238,11 @@ func mention_str(_ m: Mention<MentionRef>, profiles: Profiles) -> CompatibleText let display_str: String = { switch m.ref { case .pubkey(let pk): return getDisplayName(pk: pk, profiles: profiles) - case .note: return abbrev_pubkey(bech32String) - case .nevent: return abbrev_pubkey(bech32String) + case .note: return abbrev_identifier(bech32String) + case .nevent: return abbrev_identifier(bech32String) case .nprofile(let nprofile): return getDisplayName(pk: nprofile.author, profiles: profiles) case .nrelay(let url): return url - case .naddr: return abbrev_pubkey(bech32String) + case .naddr: return abbrev_identifier(bech32String) } }() diff --git a/damus/Types/Block.swift b/damus/Types/Block.swift @@ -37,7 +37,23 @@ enum Block: Equatable { 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 mention(Mention<MentionRef>) case hashtag(String) diff --git a/damus/Util/DisplayName.swift b/damus/Util/DisplayName.swift @@ -80,9 +80,9 @@ func parse_display_name(name: String?, display_name: String?, pubkey: Pubkey) -> } 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) } diff --git a/damus/Views/PubkeyView.swift b/damus/Views/PubkeyView.swift @@ -46,7 +46,7 @@ struct PubkeyView: View { let bech32 = pubkey.npub 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) .foregroundColor(keyColor()) .padding(5) diff --git a/damusTests/NoteContentViewTests.swift b/damusTests/NoteContentViewTests.swift @@ -10,28 +10,290 @@ import SwiftUI @testable import damus class NoteContentViewTests: XCTestCase { - func testRenderBlocksWithNonLatinHashtags() { + func testRenderBlocksWithNonLatinHashtags() throws { 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 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 runs: AttributedString.Runs = attributedText.runs let runArray: [AttributedString.Runs.Run] = Array(runs) print(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")) + 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")) + 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 /// 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 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)) 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 +331,9 @@ class NoteContentViewTests: XCTestCase { } 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() { diff --git a/damusTests/damusTests.swift b/damusTests/damusTests.swift @@ -36,10 +36,9 @@ class damusTests: XCTestCase { XCTAssertEqual(bytes.count, 32) } - func testTrimmingFunctions() { + func testTrimSuffix() { let txt = " bobs " - XCTAssertEqual(trim_prefix(txt), "bobs ") XCTAssertEqual(trim_suffix(txt), " bobs") }