feat: render NIP-84 highlights, NIP-18 quotes with rich embeds#51
feat: render NIP-84 highlights, NIP-18 quotes with rich embeds#51alltheseas wants to merge 7 commits intodamus-io:masterfrom
Conversation
Adds comprehensive rendering for additional nostr event types: NIP-84 Highlights (kind:9802): - Extract highlight metadata (context, comment, source references) - Render highlighted text with blockquote styling - Support source attribution for web URLs, notes, and articles - Preserve relay hints via bech32 parsing for nevent/naddr NIP-18 Quote Embeds (q tags and inline mentions): - Parse q tags and inline nostr:nevent/note mentions for quote references - Rich embedded quotes with author avatar, name, @username - Relative timestamps (e.g., "7h", "2d") with full date on hover - "Show more" link for truncated content - NIP-10 reply detection via e-tag "reply" marker NIP-23 Draft Badge: - Visual indicator for unpublished articles (kind:30024) Human-readable mentions: - Resolve @npub profile mentions to display names when available - Show abbreviated bech32 as fallback when profile not cached Helper functions for clean code organization: - parse_hex_id(): hex string to 32-byte array - detect_reply_author(): NIP-10 compliant reply detection - lookup_profile_handle(): profile name resolution 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Fixes 404 errors when Ubuntu package versions change on mirrors. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add @username handle below author name in note/article/highlight headers - Fix profile name lookup to filter out empty strings (prevents showing just "@" when profile has empty display_name/name fields) - Add CSS styling for .damus-note-handle 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Show "Article" badge for kind 30023 embedded quotes - Show "Draft" badge (orange) for kind 30024 embedded quotes - Show "Highlight" badge for kind 9802 embedded quotes - Fix empty string filtering in embedded quote profile extraction 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add support for highlight (kind 9802) references in naddr mentions - Style embedded highlights with left purple border instead of tag - Remove quotation marks from highlight content - Filter empty strings in embedded quote profile extraction 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Embedded article quotes now display as cards matching iOS Damus: - Hero image (if available) - Bold article title - Summary text (if available) - Word count - DRAFT badge via CSS for kind 30024 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Integrate relay provenance tracking from rust-nostr PR #1172 to enable proper NIP-19 bech32 links with relay hints for better content discoverability. Changes: - Update nostr-sdk to alltheseas/rust-nostr relay-provenance-tracking branch - RelayPool::stream_events returns BoxedStream<RelayEvent> with source relay URL - NoteAndProfileRenderData stores source_relays captured during fetch - Generate bech32 links with relay hints for all event types (notes, articles, highlights) - Filter profile (kind 0) relays from note hints - Prioritize default relays in source_relays for reliability - Preserve author/kind fields when rebuilding nevent bech32 - Graceful fallback to original nip19 on encoding failure with metric - Add bech32_with_relays() helper with 8 unit tests 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
|
@coderabbitai review please |
|
✅ Actions performedReview triggered.
|
📝 WalkthroughWalkthroughThis pull request modifies a Nostr relay client to support relay source tracking, enhanced HTML rendering for NIP-84 highlights and embedded quotes with database context, CSS styling for new UI components, and refactored relay pool APIs. Dependencies are updated to Git-based versions for relay provenance tracking. Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In `@Cargo.toml`:
- Around line 20-24: The Cargo.toml currently depends on branch names for
nostr-sdk and nostr which is non-deterministic; change both entries to use
explicit git commit hashes instead (replace the branch =
"relay-provenance-tracking" with rev = "<commit-hash>") for the nostr-sdk and
nostr dependencies in Cargo.toml, then run cargo update -p nostr-sdk -p nostr
(or cargo build) to regenerate and commit the updated Cargo.lock so the exact
resolved revisions are recorded for reproducible builds.
In `@src/html.rs`:
- Around line 831-1048: The loop in build_embedded_quotes_html currently
continues when quoted_note is None, dropping references; instead emit a minimal
fallback card linking to build_quote_link(quote_ref) so missing events still
render a placeholder link. Modify the for loop where quoted_note is computed:
when None, call build_quote_link(quote_ref) and append a small HTML snippet
(e.g. an <a> with class "damus-embedded-quote" containing a placeholder avatar,
author/name like "nostrich", a time placeholder or empty, and a short
"Referenced event" label) using the same write! pattern that appends to
quotes_html; keep all existing behavior for cases where quoted_note is present.
Ensure the fallback uses the same content_class or a distinct fallback class to
allow styling.
🧹 Nitpick comments (2)
src/render.rs (1)
71-96: Keep source relay dedup consistent in the builder helper.
with_source_relaybypasses the duplicate guard used byadd_source_relay, so chained calls can emit duplicate relay hints. Consider reusing the same check.♻️ Suggested tweak
- pub fn with_source_relay(mut self, relay: RelayUrl) -> Self { - self.source_relays.push(relay); - self - } + pub fn with_source_relay(mut self, relay: RelayUrl) -> Self { + if !self.source_relays.contains(&relay) { + self.source_relays.push(relay); + } + self + }src/html.rs (1)
538-587: Consider adding defensive fallback for markers when relay URLs are omitted.While NIP-10 specifies markers at index 3, real-world Nostr implementations may omit empty relay URLs entirely, shifting markers to index 2. The suggested fix adds a safe fallback that validates the value matches known marker types, preventing false positives:
Suggested improvement
- let tag_marker = tag_vec.get(3).and_then(|n| n.variant().str()); + let tag_marker = tag_vec + .get(3) + .and_then(|n| n.variant().str()) + .or_else(|| { + tag_vec + .get(2) + .and_then(|n| n.variant().str()) + .filter(|value| matches!(*value, "root" | "reply" | "mention")) + });
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (14)
.DS_Storeis excluded by!**/.DS_StoreCargo.lockis excluded by!**/*.lockPM.pngis excluded by!**/*.pngQL.pngis excluded by!**/*.pngborked.pngis excluded by!**/*.pngbrokenlongformquote.pngis excluded by!**/*.pngbrokenquote.pngis excluded by!**/*.pngdraft.pngis excluded by!**/*.pngiosquote.jpgis excluded by!**/*.jpgmissingname.pngis excluded by!**/*.pngmissingqh.pngis excluded by!**/*.pngmissingqh2.pngis excluded by!**/*.pngnpubnotname.pngis excluded by!**/*.pngquote1.pngis excluded by!**/*.png
📒 Files selected for processing (8)
.github/workflows/rust.ymlCargo.tomlassets/damus.csssrc/html.rssrc/main.rssrc/nip19.rssrc/relay_pool.rssrc/render.rs
💤 Files with no reviewable changes (1)
- src/main.rs
🧰 Additional context used
🧬 Code graph analysis (3)
src/nip19.rs (2)
src/relay_pool.rs (1)
new(28-53)src/render.rs (1)
new(80-86)
src/render.rs (1)
src/relay_pool.rs (2)
new(28-53)default_relays(55-57)
src/html.rs (2)
src/abbrev.rs (2)
abbrev_str(24-31)abbreviate(33-36)src/nip19.rs (2)
bech32_with_relays(19-46)nip19_relays(6-13)
🔇 Additional comments (28)
.github/workflows/rust.yml (1)
18-21: LGTM. The apt update step improves package install reliability in CI.assets/damus.css (3)
261-266: LGTM. Handle styling is consistent with existing note typography.
358-408: LGTM. Draft badge and highlight styles look consistent with the theme.
503-659: LGTM. Embedded quote and article card styles are cohesive and readable.src/nip19.rs (2)
1-45: LGTM. Relay override logic is clear and preserves author/kind with a safe fallback.
48-208: LGTM. Tests cover add/replace/empty relay cases and metadata preservation.src/relay_pool.rs (3)
28-53: LGTM. Constructor aligns with the simplified relay pool API.
144-162: LGTM. The empty‑relays vs explicit‑relays branching is clean and predictable.
106-108: Remove this comment —Client::connect()returns unit, notResult.The method signature is
pub async fn connect(&self), which returns(), not aResulttype. There are no errors to handle or drop, making the error handling concern invalid. The suggested fix would not compile against the current nostr-sdk implementation.Likely an incorrect or invalid review comment.
src/render.rs (6)
328-398: Relay source collection and prioritization look solid.The flow gathers responding relays, prefers defaults, and caps to three—nice balance between reliability and payload size.
566-575: Source relay propagation from the fetch task is clear.
664-691: Per-filter relay discovery loop is straightforward.
719-733: Relay hint application during profile feed fetch looks good.
742-842: Pubkey byte conversion updates look correct.Please confirm these conversions align with the current
nostr-sdkPublicKey::to_bytes()expectations.
1214-1218: Test updated forTag::coordinatesignature.Please verify the tag API signature matches the current
nostrcrate.src/html.rs (13)
10-11: Bech32 parsing import aligns with new helpers.Please confirm the
FromBech32trait path matches the currentnostr-sdkAPI.
80-101: Highlight metadata model is clear and well scoped.
177-293: Highlight tag parsing covers context/comment/source variants cleanly.
400-505: Mention rendering enhancements are clean.Profile mention resolution and escaping look good.
507-535: Profile lookup helpers are tidy and reusable.
589-673: Relative time formatting and URL pill utility look good.
675-829: Quote reference extraction is thorough.
1050-1122: Article lookup and quote-link fallback look reasonable.
1143-1238: Note HTML now includes@handleand embedded quote cards.
1242-1326: Draft badge integration for 30024 articles is clear.
1329-1386: Highlight card composition is clean and readable.
1388-1637: Source attribution helpers are nicely layered.
2304-2436: Routing for articles/highlights and relay-aware canonical URLs looks good.Please confirm the relay-augmented bech32 behavior aligns with current NIP-19/
nostr-sdkexpectations.
✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.
| #nostr-sdk = "0.37.0" | ||
| #nostr = "0.37.0" | ||
| # PR #1172: relay provenance tracking - https://github.com/rust-nostr/nostr/pull/1172 | ||
| nostr-sdk = { git = "https://github.com/alltheseas/rust-nostr.git", branch = "relay-provenance-tracking" } | ||
| nostr = { git = "https://github.com/alltheseas/rust-nostr.git", branch = "relay-provenance-tracking" } |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
Cargo.toml git dependencies branch vs commit hash reproducibility best practices
💡 Result:
Short answer — use rev (full commit SHA) or an annotated/tagged release for reproducibility; avoid depending only on a branch.
Best-practices (concise):
- Pin to a commit (rev = "") in Cargo.toml for maximal reproducibility — it fixes the exact tree. [1][2]
- If you want human-readable stability, pin to a tag (tag = "v1.2.3") that you control/publish. [1][3]
- Do NOT rely on branch = "…" for reproducible builds (branches move; Cargo only locks the commit at the time you add it, so later pulls/CI might differ unless you pin). Use branch only for tracking a moving target intentionally. [1]
- Always check-in Cargo.lock for applications (ensures everyone uses the same git commit resolved at add-time). Cargo.lock records the git rev Cargo used. [1][2]
- For library crates published to crates.io: avoid git deps (crates.io disallows publishing crates with external git deps). Use registry releases or vendor/patch instead. [1]
- Use [patch.crates-io] / workspace overrides to override indirect deps or point to a specific git rev when needed. [2]
- When pinning by rev, prefer the full SHA (not a short prefix) and update intentionally (cargo update or change rev). Keep notes/CHANGELOG for why you pinned. [3]
Sources: Cargo reference (specifying git deps, role of rev/branch/tag and Cargo.lock). [1] Stack Overflow (pinning to commit + [patch] override examples). [2] Practical guide on pinning to commit/tag. [3]
References
[1] Rust Cargo book — Specifying dependencies (git/branch/tag/rev, Cargo.lock).
[2] Stack Overflow — How to specify a certain commit / pin indirect deps (rev, [patch]).
[3] Guide — Pinning dependencies to specific git revisions in Cargo.
🏁 Script executed:
# Check if the branch exists and get its current state
# First, let's try to fetch information about the remote repository
git ls-remote https://github.com/alltheseas/rust-nostr.git relay-provenance-tracking 2>/dev/null || echo "Could not fetch remote refs"Repository: damus-io/notecrumbs
Length of output: 140
🌐 Web query:
rust-nostr relay provenance tracking feature upstream release
💡 Result:
I couldn't find any mention of a "provenance tracking" feature being released upstream in the rust‑nostr relay codebase or release notes. The rust‑nostr repository and recent release changelogs (e.g. v0.36–v0.41) list many relay and relay‑pool improvements but do not mention provenance tracking. [1][2]
If you want, I can:
- search open issues/PRs for a provenance-related discussion in the rust‑nostr org, or
- look for provenance-tracking implementations in other Rust relays (rnostr, nostr-rs-relay, gn…etc.) and report back.
Which would you prefer?
Sources:
- rust‑nostr GitHub repo (projects, supported NIPs, releases). [1]
- rust‑nostr release summaries (v0.36–v0.41). [2]
Pin git dependencies to a commit for reproducible builds.
Using a branch makes builds non-deterministic and increases supply-chain risk. Pin to a specific commit hash and ensure Cargo.lock is committed to lock the exact resolved revision.
🔧 Suggested change
-nostr-sdk = { git = "https://github.com/alltheseas/rust-nostr.git", branch = "relay-provenance-tracking" }
-nostr = { git = "https://github.com/alltheseas/rust-nostr.git", branch = "relay-provenance-tracking" }
+nostr-sdk = { git = "https://github.com/alltheseas/rust-nostr.git", rev = "2ea3dbea4d89697e1c2f2bd4dea1bb68432df6f5" }
+nostr = { git = "https://github.com/alltheseas/rust-nostr.git", rev = "2ea3dbea4d89697e1c2f2bd4dea1bb68432df6f5" }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| #nostr-sdk = "0.37.0" | |
| #nostr = "0.37.0" | |
| # PR #1172: relay provenance tracking - https://github.com/rust-nostr/nostr/pull/1172 | |
| nostr-sdk = { git = "https://github.com/alltheseas/rust-nostr.git", branch = "relay-provenance-tracking" } | |
| nostr = { git = "https://github.com/alltheseas/rust-nostr.git", branch = "relay-provenance-tracking" } | |
| `#nostr-sdk` = "0.37.0" | |
| `#nostr` = "0.37.0" | |
| # PR `#1172`: relay provenance tracking - https://github.com/rust-nostr/nostr/pull/1172 | |
| nostr-sdk = { git = "https://github.com/alltheseas/rust-nostr.git", rev = "2ea3dbea4d89697e1c2f2bd4dea1bb68432df6f5" } | |
| nostr = { git = "https://github.com/alltheseas/rust-nostr.git", rev = "2ea3dbea4d89697e1c2f2bd4dea1bb68432df6f5" } |
🤖 Prompt for AI Agents
In `@Cargo.toml` around lines 20 - 24, The Cargo.toml currently depends on branch
names for nostr-sdk and nostr which is non-deterministic; change both entries to
use explicit git commit hashes instead (replace the branch =
"relay-provenance-tracking" with rev = "<commit-hash>") for the nostr-sdk and
nostr dependencies in Cargo.toml, then run cargo update -p nostr-sdk -p nostr
(or cargo build) to regenerate and commit the updated Cargo.lock so the exact
resolved revisions are recorded for reproducible builds.
| /// Builds embedded quote HTML for referenced events. | ||
| /// Returns empty string if no quotes or quoted events not found. | ||
| fn build_embedded_quotes_html( | ||
| ndb: &Ndb, | ||
| txn: &Transaction, | ||
| quote_refs: &[QuoteRef], | ||
| ) -> String { | ||
| if quote_refs.is_empty() { | ||
| return String::new(); | ||
| } | ||
|
|
||
| let mut quotes_html = String::new(); | ||
|
|
||
| for quote_ref in quote_refs { | ||
| // Try to find the quoted note in nostrdb | ||
| let quoted_note = if let Some(event_id) = "e_ref.event_id { | ||
| // Look up by event ID | ||
| ndb.get_note_by_id(txn, event_id).ok() | ||
| } else if let Some(addr) = "e_ref.article_addr { | ||
| // Look up article by address | ||
| lookup_article_by_addr(ndb, txn, addr) | ||
| } else { | ||
| None | ||
| }; | ||
|
|
||
| let Some(quoted_note) = quoted_note else { | ||
| continue; | ||
| }; | ||
|
|
||
| // Get author profile for the quoted note (name, username, pfp) | ||
| // Filter out empty strings to ensure proper fallback behavior | ||
| let (display_name, username, pfp_url) = ndb | ||
| .get_profile_by_pubkey(txn, quoted_note.pubkey()) | ||
| .ok() | ||
| .and_then(|profile_rec| { | ||
| profile_rec.record().profile().map(|p| { | ||
| let name = p.display_name() | ||
| .filter(|s| !s.is_empty()) | ||
| .or_else(|| p.name().filter(|s| !s.is_empty())) | ||
| .map(|n| n.to_owned()); | ||
| let handle = p.name() | ||
| .filter(|s| !s.is_empty()) | ||
| .map(|n| format!("@{}", n)); | ||
| let picture = p.picture() | ||
| .filter(|s| !s.is_empty()) | ||
| .map(|s| s.to_owned()); | ||
| (name, handle, picture) | ||
| }) | ||
| }) | ||
| .unwrap_or((None, None, None)); | ||
|
|
||
| let display_name = display_name.unwrap_or_else(|| "nostrich".to_string()); | ||
| let display_name_html = html_escape::encode_text(&display_name); | ||
| let username_html = username | ||
| .map(|u| format!(r#" <span class="damus-embedded-quote-username">{}</span>"#, | ||
| html_escape::encode_text(&u))) | ||
| .unwrap_or_default(); | ||
|
|
||
| // Build profile picture HTML - use placeholder if not available | ||
| let pfp_html = pfp_url | ||
| .filter(|url| !url.trim().is_empty()) | ||
| .map(|url| { | ||
| let pfp_attr = html_escape::encode_double_quoted_attribute(&url); | ||
| format!(r#"<img src="{}" class="damus-embedded-quote-avatar" alt="" />"#, pfp_attr) | ||
| }) | ||
| .unwrap_or_else(|| { | ||
| r#"<img src="/img/no-profile.svg" class="damus-embedded-quote-avatar" alt="" />"#.to_string() | ||
| }); | ||
|
|
||
| // Get relative timestamp | ||
| let timestamp = quoted_note.created_at(); | ||
| let relative_time = format_relative_time(timestamp); | ||
| let time_html = html_escape::encode_text(&relative_time); | ||
|
|
||
| // Detect reply context per NIP-10: | ||
| // 1. Find e-tag with "reply" marker → look up that event → get author | ||
| // 2. Fallback: use first p-tag not marked "mention" | ||
| let reply_to = detect_reply_author(ndb, txn, "ed_note); | ||
|
|
||
| let reply_html = reply_to | ||
| .map(|name| format!( | ||
| r#"<div class="damus-embedded-quote-reply">Replying to {}</div>"#, | ||
| html_escape::encode_text(&name) | ||
| )) | ||
| .unwrap_or_default(); | ||
|
|
||
| // Build content preview, type indicator, and content class based on note kind | ||
| // For articles, we use a special card layout with image, title, summary, word count | ||
| let (content_preview, is_truncated, type_indicator, content_class, article_card) = match quoted_note.kind() { | ||
| // For articles, extract metadata and build card layout | ||
| 30023 | 30024 => { | ||
| let mut title: Option<&str> = None; | ||
| let mut image: Option<&str> = None; | ||
| let mut summary: Option<&str> = None; | ||
|
|
||
| for tag in quoted_note.tags() { | ||
| let mut iter = tag.into_iter(); | ||
| let Some(tag_name) = iter.next().and_then(|n| n.variant().str()) else { | ||
| continue; | ||
| }; | ||
| let tag_value = iter.next().and_then(|n| n.variant().str()); | ||
| match tag_name { | ||
| "title" => title = tag_value, | ||
| "image" => image = tag_value.filter(|s| !s.is_empty()), | ||
| "summary" => summary = tag_value.filter(|s| !s.is_empty()), | ||
| _ => {} | ||
| } | ||
| } | ||
|
|
||
| // Calculate word count | ||
| let word_count = quoted_note.content().split_whitespace().count(); | ||
| let word_count_text = format!("{} Words", word_count); | ||
|
|
||
| // Build article card HTML | ||
| let title_text = title.unwrap_or("Untitled article"); | ||
| let title_html = html_escape::encode_text(title_text); | ||
|
|
||
| let image_html = image | ||
| .map(|url| { | ||
| let url_attr = html_escape::encode_double_quoted_attribute(url); | ||
| format!(r#"<img src="{}" class="damus-embedded-article-image" alt="" />"#, url_attr) | ||
| }) | ||
| .unwrap_or_default(); | ||
|
|
||
| let summary_html = summary | ||
| .map(|s| { | ||
| let text = html_escape::encode_text(abbreviate(s, 150)); | ||
| format!(r#"<div class="damus-embedded-article-summary">{}</div>"#, text) | ||
| }) | ||
| .unwrap_or_default(); | ||
|
|
||
| let draft_class = if quoted_note.kind() == 30024 { " damus-embedded-article-draft" } else { "" }; | ||
|
|
||
| let card_html = format!( | ||
| r#"{image}<div class="damus-embedded-article-title{draft}">{title}</div>{summary}<div class="damus-embedded-article-wordcount">{words}</div>"#, | ||
| image = image_html, | ||
| draft = draft_class, | ||
| title = title_html, | ||
| summary = summary_html, | ||
| words = word_count_text | ||
| ); | ||
|
|
||
| (String::new(), false, "", " damus-embedded-quote-article", Some(card_html)) | ||
| } | ||
| // For highlights, show the highlighted text with left border styling (no tag needed) | ||
| 9802 => { | ||
| let full_content = quoted_note.content(); | ||
| let content = abbreviate(full_content, 200); | ||
| let truncated = content.len() < full_content.len(); | ||
| (content.to_string(), truncated, "", " damus-embedded-quote-highlight", None) | ||
| } | ||
| // For regular notes, show abbreviated content | ||
| _ => { | ||
| let full_content = quoted_note.content(); | ||
| let content = abbreviate(full_content, 280); | ||
| let truncated = content.len() < full_content.len(); | ||
| (content.to_string(), truncated, "", "", None) | ||
| } | ||
| }; | ||
| let content_html = html_escape::encode_text(&content_preview).replace("\n", " "); | ||
|
|
||
| // Build "Show more" link if content was truncated | ||
| let show_more_html = if is_truncated { | ||
| r#"<span class="damus-embedded-quote-showmore">Show more</span>"# | ||
| } else { | ||
| "" | ||
| }; | ||
|
|
||
| // Build link to quoted note | ||
| let link = build_quote_link(quote_ref); | ||
|
|
||
| // For articles, use card layout; for other types, use regular content layout | ||
| let body_html = if let Some(card) = article_card { | ||
| card | ||
| } else { | ||
| let show_more = if is_truncated { | ||
| r#" <span class="damus-embedded-quote-showmore">Show more</span>"# | ||
| } else { | ||
| "" | ||
| }; | ||
| format!( | ||
| r#"<div class="damus-embedded-quote-content{class}">{content}{showmore}</div>"#, | ||
| class = content_class, | ||
| content = content_html, | ||
| showmore = show_more | ||
| ) | ||
| }; | ||
|
|
||
| let _ = write!( | ||
| quotes_html, | ||
| r#"<a href="{link}" class="damus-embedded-quote{content_class}"> | ||
| <div class="damus-embedded-quote-header"> | ||
| {pfp} | ||
| <span class="damus-embedded-quote-author">{name}</span>{username} | ||
| <span class="damus-embedded-quote-time">· {time}</span> | ||
| {type_indicator} | ||
| </div> | ||
| {reply} | ||
| {body} | ||
| </a>"#, | ||
| link = link, | ||
| content_class = content_class, | ||
| pfp = pfp_html, | ||
| name = display_name_html, | ||
| username = username_html, | ||
| time = time_html, | ||
| type_indicator = type_indicator, | ||
| reply = reply_html, | ||
| body = body_html | ||
| ); | ||
| } | ||
|
|
||
| if quotes_html.is_empty() { | ||
| return String::new(); | ||
| } | ||
|
|
||
| format!(r#"<div class="damus-embedded-quotes">{}</div>"#, quotes_html) | ||
| } |
There was a problem hiding this comment.
Provide a fallback when quoted events aren’t found.
If the quoted note isn’t in nostrdb, the loop skips it; combined with suppressed inline mention rendering, this drops the reference entirely. Consider emitting a minimal link/card for missing quotes.
🐛 Suggested fallback
- let Some(quoted_note) = quoted_note else {
- continue;
- };
+ let Some(quoted_note) = quoted_note else {
+ let link = build_quote_link(quote_ref);
+ let label = quote_ref
+ .original_bech32
+ .as_deref()
+ .map(abbrev_str)
+ .unwrap_or_else(|| "Quoted note".to_string());
+ let label_html = html_escape::encode_text(&label);
+ let _ = write!(
+ quotes_html,
+ r#"<a href="{link}" class="damus-embedded-quote damus-embedded-quote--missing">{label}</a>"#,
+ link = link,
+ label = label_html
+ );
+ continue;
+ };🤖 Prompt for AI Agents
In `@src/html.rs` around lines 831 - 1048, The loop in build_embedded_quotes_html
currently continues when quoted_note is None, dropping references; instead emit
a minimal fallback card linking to build_quote_link(quote_ref) so missing events
still render a placeholder link. Modify the for loop where quoted_note is
computed: when None, call build_quote_link(quote_ref) and append a small HTML
snippet (e.g. an <a> with class "damus-embedded-quote" containing a placeholder
avatar, author/name like "nostrich", a time placeholder or empty, and a short
"Referenced event" label) using the same write! pattern that appends to
quotes_html; keep all existing behavior for cases where quoted_note is present.
Ensure the fallback uses the same content_class or a distinct fallback class to
allow styling.
|
Closing — this work has already been incorporated into master via squash-merged commits. |
NIP-84 Highlights (kind:9802): - Extract and render highlight metadata (context, comment) - Source attribution for web URLs, notes, and articles - Blockquote styling with left border accent NIP-18 Embedded Quotes: - Parse q tags and inline nevent/note/naddr mentions - Rich quote cards with avatar, name, @handle, relative time - Reply detection using nostrdb's NoteReply - Type indicators for articles/highlights/drafts Other improvements: - Draft badge for unpublished articles (kind:30024) - @username handles displayed under profile names - Human-readable @mentions (resolve npub to display names) Closes: damus-io#51 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> Co-Authored-By: alltheseas <alltheseas@users.noreply.github.com>
* ci: add apt-get update before installing deps Fixes 404 errors when Ubuntu package versions change on mirrors. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * upgrade nostrdb to 0.9.0 Update to nostrdb 0.9.0 and fix breaking API changes: - FilterBuilder.tags() now takes &str instead of String - Handle new FilterField variants (Search, Relays, Custom) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat: add NIP-84 highlights and NIP-18 embedded quotes NIP-84 Highlights (kind:9802): - Extract and render highlight metadata (context, comment) - Source attribution for web URLs, notes, and articles - Blockquote styling with left border accent NIP-18 Embedded Quotes: - Parse q tags and inline nevent/note/naddr mentions - Rich quote cards with avatar, name, @handle, relative time - Reply detection using nostrdb's NoteReply - Type indicators for articles/highlights/drafts Other improvements: - Draft badge for unpublished articles (kind:30024) - @username handles displayed under profile names - Human-readable @mentions (resolve npub to display names) Closes: #51 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> Co-Authored-By: alltheseas <alltheseas@users.noreply.github.com> * feat: iOS-style article card for embedded longform quotes Embedded article quotes now display as cards matching iOS Damus: - Hero image (if available) - Bold article title - Summary text (if available) - Word count - DRAFT badge via CSS for kind 30024 Co-Authored-By: alltheseas <alltheseas@users.noreply.github.com> Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat: fetch quoted events from relays with relay provenance Add an UnknownIds pattern (adapted from notedeck) to fetch quoted events referenced in q tags and inline mentions. Events are fetched using relay hints from nevent/naddr bech32 and q tag relay fields. - Add src/unknowns.rs with UnknownId enum and UnknownIds collection - Update QuoteRef to include relay hints (Vec<RelayUrl>) - Extract relay hints from nevent/naddr bech32 and q tag third element - Add collect_quote_unknowns() and fetch_unknowns() to render.rs - Fetch quote unknowns in main.rs after primary note is loaded Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor: generalize unknowns to fetch all missing data Expand the unknowns pattern beyond just quoted events to collect: - Author profiles - Reply chain (root/reply) using nostrdb's NoteReply (NIP-10) - Mentioned profiles (npub/nprofile with relay hints) - Mentioned events (nevent/note1 with relay hints) - Quoted events (q tags, inline mentions) Move unknowns collection to main.rs for consistent handling regardless of whether primary note was cached. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * chore: fix unused argument warnings Signed-off-by: William Casarin <jb55@jb55.com> * fix: refresh profile metadata during background updates Previously, background profile refreshes only fetched kind 1 (notes), never updating kind 0 (profile metadata). This caused profiles to remain stale indefinitely after initial cache. Now fetch_profile_feed also fetches the latest profile metadata from relays, allowing nostrdb to update cached profiles with newer versions. Fixes: #52 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * rustfmt Signed-off-by: William Casarin <jb55@jb55.com> * refactor: replace tuple with QuoteProfileInfo struct in build_embedded_quotes_html Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat: integrate rust-nostr PR #1172 relay provenance tracking Integrate relay provenance tracking from rust-nostr PR #1172 to enable proper NIP-19 bech32 links with relay hints for better content discoverability. Changes: - Update nostr-sdk to alltheseas/rust-nostr relay-provenance-tracking branch - RelayPool::stream_events returns BoxedStream<RelayEvent> with source relay URL - NoteAndProfileRenderData stores source_relays captured during fetch - Generate bech32 links with relay hints for all event types (notes, articles, highlights) - Filter profile (kind 0) relays from note hints - Prioritize default relays in source_relays for reliability - Preserve author/kind fields when rebuilding nevent bech32 - Graceful fallback to original nip19 on encoding failure with metric - Add bech32_with_relays() helper with 8 unit tests 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat: store relay provenance in nostrdb via IngestMetadata Use process_event_with instead of process_event to pass the source relay URL to nostrdb when ingesting events from relay streams. Fixes: fffaa9e ("feat: integrate rust-nostr PR #1172 relay provenance tracking") Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat: add sitemap.xml and robots.txt for SEO Add dynamic sitemap generation from nostrdb cache to improve search engine discoverability of Nostr content. New routes: - GET /robots.txt - crawler directives with sitemap reference - GET /sitemap.xml - dynamic sitemap from cached notes/profiles/articles The sitemap queries local nostrdb for: - Notes (kind:1) → note1xxx URLs - Long-form articles (kind:30023) → naddr1xxx URLs - Profiles (kind:0) → npub1xxx URLs Ref: #26 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat: add prometheus metrics for sitemap generation Track aggregate stats (privacy-preserving, no user tracking): - sitemap_generations_total: counter for generation requests - sitemap_generation_duration_seconds: time to generate - sitemap_urls_total: total URLs in sitemap - sitemap_notes_count: notes included - sitemap_articles_count: articles included - sitemap_profiles_count: profiles included Metrics available at /metrics endpoint. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: address sitemap code review findings - Skip kind:30023 entries with missing/empty d-tag to avoid ambiguous URLs and potential collisions across authors - Add since filter (90 days) to notes and articles queries to prioritize recent content for SEO freshness - Log warning when NOTECRUMBS_BASE_URL is not set, to surface potential misconfiguration in production 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: cache base URL and use longer lookback for articles - Cache base URL with OnceLock to avoid logging warning on every request - Use separate lookback periods: 90 days for notes, 365 days for evergreen article content (kind:30023) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: normalize base url and allow nostr.json * refactor: flatten nested conditionals with guard clauses Use early returns and let-else patterns to reduce nesting depth in generate_sitemap loops. Improves readability by making the happy path linear instead of deeply indented. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Signed-off-by: William Casarin <jb55@jb55.com> Co-authored-by: William Casarin <jb55@jb55.com> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> Co-authored-by: alltheseas <alltheseas@users.noreply.github.com>







Summary
Test plan
🤖 Generated with Claude Code
Summary by CodeRabbit
Release Notes
New Features
Style
✏️ Tip: You can customize this high-level summary in your review settings.