From d355450f640d9825529560e40e735f2413abe1d0 Mon Sep 17 00:00:00 2001 From: Terry Yiu Date: Sat, 1 Mar 2025 16:30:25 -0500 Subject: [PATCH 1/2] 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 --- damus/Components/TranslateView.swift | 2 +- damus/Models/NoteContent.swift | 115 +++++++++++++++------------ damus/Types/Block.swift | 18 ++++- damus/Util/DisplayName.swift | 4 +- damus/Views/PubkeyView.swift | 2 +- damusTests/damusTests.swift | 3 +- 6 files changed, 87 insertions(+), 57 deletions(-) diff --git a/damus/Components/TranslateView.swift b/damus/Components/TranslateView.swift index d7d530ed8..d60d5bad0 100644 --- 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 index 81adde7a6..0cbf18e25 100644 --- a/damus/Models/NoteContent.swift +++ b/damus/Models/NoteContent.swift @@ -73,85 +73,100 @@ 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 + + // 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 { + for (i, block) in blocks.enumerated().reversed() { + if block.is_previewable { + 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 block.is_previewable && ind < hide_text_index { + 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 +209,11 @@ func mention_str(_ m: Mention, 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 index a5cb99369..7ee9cdeeb 100644 --- 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) case hashtag(String) diff --git a/damus/Util/DisplayName.swift b/damus/Util/DisplayName.swift index 3a57eb65a..6ae5074ee 100644 --- 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 index 496bcc213..79ed753cc 100644 --- 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/damusTests.swift b/damusTests/damusTests.swift index 71fd1df47..93aaa23cf 100644 --- 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") } From ab58a9457d9d8be66b317470276033dd81a056b6 Mon Sep 17 00:00:00 2001 From: Terry Yiu Date: Sat, 1 Mar 2025 20:38:36 -0500 Subject: [PATCH 2/2] 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 --- damus/Models/NoteContent.swift | 1 + damus/Models/URLHandler.swift | 18 ++++++++++++++++++ damus/Nostr/NostrLink.swift | 12 ++++++++++-- 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/damus/Models/NoteContent.swift b/damus/Models/NoteContent.swift index 0cbf18e25..ffe9aab8b 100644 --- a/damus/Models/NoteContent.swift +++ b/damus/Models/NoteContent.swift @@ -162,6 +162,7 @@ func reduce_text_block(ind: Int, hide_text_index: Int, txt: String) -> String { 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) diff --git a/damus/Models/URLHandler.swift b/damus/Models/URLHandler.swift index 03198d8f1..4feec53d9 100644 --- a/damus/Models/URLHandler.swift +++ b/damus/Models/URLHandler.swift @@ -43,6 +43,18 @@ struct DamusURLHandler { return .route(.Script(script: model)) case .purple(let 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: break } @@ -91,6 +103,11 @@ struct DamusURLHandler { return .filter(filt) case .script(let script): return .script(script) + case .invoice(let bolt11): + if let invoice = decode_bolt11(bolt11) { + return .invoice(invoice) + } + return nil } return nil } @@ -103,5 +120,6 @@ struct DamusURLHandler { case wallet_connect(WalletConnectURL) case script([UInt8]) case purple(DamusPurpleURL) + case invoice(Invoice) } } diff --git a/damus/Nostr/NostrLink.swift b/damus/Nostr/NostrLink.swift index 160744290..6fd74e0e7 100644 --- a/damus/Nostr/NostrLink.swift +++ b/damus/Nostr/NostrLink.swift @@ -12,6 +12,7 @@ enum NostrLink: Equatable { case ref(RefId) case filter(NostrFilter) case script([UInt8]) + case invoice(String) } func encode_pubkey_uri(_ pubkey: Pubkey) -> String { @@ -93,8 +94,15 @@ func decode_nostr_uri(_ s: String) -> NostrLink? { return } - if parts.count >= 2 && parts[0] == "t" { - return .filter(NostrFilter(hashtag: [parts[1].lowercased()])) + if parts.count >= 2 { + 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 {