commit e35bb71cc6e777a9c515f6c6e335a4f559bce2a4
parent 2ef85fbc1dd57e67a159698cc3ad5f1fafa85473
Author: William Casarin <jb55@jb55.com>
Date: Wed, 4 Feb 2026 22:52:34 -0800
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>
Diffstat:
3 files changed, 145 insertions(+), 34 deletions(-)
diff --git a/src/main.rs b/src/main.rs
@@ -188,16 +188,14 @@ async fn serve(
}
}
- // Always check for quote unknowns when we have a note
+ // Collect and fetch all unknowns from the note (author, mentions, quotes, replies)
if let RenderData::Note(note_rd) = &render_data {
- if let Some(unknowns) =
- render::collect_quote_unknowns(&app.ndb, ¬e_rd.note_rd)
- {
- tracing::debug!("fetching {} quote unknowns", unknowns.relay_hints().len());
+ if let Some(unknowns) = render::collect_note_unknowns(&app.ndb, ¬e_rd.note_rd) {
+ tracing::debug!("fetching {} unknowns", unknowns.ids_len());
if let Err(err) =
render::fetch_unknowns(&app.relay_pool, &app.ndb, unknowns).await
{
- tracing::warn!("failed to fetch quote unknowns: {err}");
+ tracing::warn!("failed to fetch unknowns: {err}");
}
}
}
diff --git a/src/render.rs b/src/render.rs
@@ -527,51 +527,39 @@ impl RenderData {
}
// Wait for primary fetch to complete
- let primary_result = match fetch_handle.await {
+ // Note: unknowns collection happens in main.rs after complete() returns
+ match fetch_handle.await {
Ok(Ok(())) => Ok(()),
Ok(Err(err)) => Err(err),
Err(join_err) => Err(Error::Generic(format!(
"relay fetch task failed: {}",
join_err
))),
- };
-
- // After primary note is fetched, collect and fetch quote unknowns
- if let RenderData::Note(note_rd) = self {
- debug!("checking for quote unknowns");
- if let Some(unknowns) = collect_quote_unknowns(&ndb, ¬e_rd.note_rd) {
- debug!("fetching {} quote unknowns", unknowns.relay_hints().len());
- if let Err(err) = fetch_unknowns(&relay_pool, &ndb, unknowns).await {
- warn!("failed to fetch quote unknowns: {err}");
- }
- } else {
- debug!("no quote unknowns to fetch");
- }
}
-
- primary_result
}
}
-/// Collect unknown IDs from a note's quote references.
-pub fn collect_quote_unknowns(
+/// Collect all unknown IDs from a note - author, mentions, quotes, reply chain.
+pub fn collect_note_unknowns(
ndb: &Ndb,
note_rd: &NoteRenderData,
) -> Option<crate::unknowns::UnknownIds> {
let txn = Transaction::new(ndb).ok()?;
let note = note_rd.lookup(&txn, ndb).ok()?;
- let quote_refs = crate::html::collect_all_quote_refs(ndb, &txn, ¬e);
- debug!("found {} quote refs in note", quote_refs.len());
+ let mut unknowns = crate::unknowns::UnknownIds::new();
- if quote_refs.is_empty() {
- return None;
- }
+ // Collect from note content, author, reply chain, mentioned profiles/events
+ unknowns.collect_from_note(ndb, &txn, ¬e);
- let mut unknowns = crate::unknowns::UnknownIds::new();
- unknowns.collect_from_quote_refs(ndb, &txn, "e_refs);
+ // Also collect from quote refs (q tags and inline nevent/naddr for embedded quotes)
+ let quote_refs = crate::html::collect_all_quote_refs(ndb, &txn, ¬e);
+ if !quote_refs.is_empty() {
+ debug!("found {} quote refs in note", quote_refs.len());
+ unknowns.collect_from_quote_refs(ndb, &txn, "e_refs);
+ }
- debug!("collected {} unknowns from quote refs", unknowns.ids_len());
+ debug!("collected {} total unknowns from note", unknowns.ids_len());
if unknowns.is_empty() {
None
diff --git a/src/unknowns.rs b/src/unknowns.rs
@@ -1,10 +1,15 @@
-//! Unknown ID collection with relay provenance for fetching quoted events.
+//! Unknown ID collection with relay provenance for fetching missing data.
//!
//! Adapted from notedeck's unknowns pattern for notecrumbs' one-shot HTTP context.
+//! Collects unknown note IDs and profile pubkeys from:
+//! - Quote references (q tags, inline nevent/note/naddr)
+//! - Mentioned profiles (npub/nprofile in content)
+//! - Reply chain (e tags with reply/root markers)
+//! - Author profile
use crate::html::QuoteRef;
use nostr::RelayUrl;
-use nostrdb::{Ndb, Transaction};
+use nostrdb::{BlockType, Mention, Ndb, Note, Transaction};
use std::collections::{HashMap, HashSet};
/// An unknown ID that needs to be fetched from relays.
@@ -139,4 +144,124 @@ impl UnknownIds {
}
}
}
+
+ /// Collect all unknown IDs from a note - author, mentioned profiles/events, reply chain.
+ ///
+ /// This is the comprehensive collection function adapted from notedeck's pattern.
+ pub fn collect_from_note(&mut self, ndb: &Ndb, txn: &Transaction, note: &Note) {
+ // 1. Author profile
+ self.add_profile_if_missing(ndb, txn, note.pubkey());
+
+ // 2. Reply chain - check e tags for root/reply markers
+ self.collect_reply_chain(ndb, txn, note);
+
+ // 3. Mentioned profiles and events from content blocks
+ self.collect_from_blocks(ndb, txn, note);
+ }
+
+ /// Collect reply chain unknowns using nostrdb's NoteReply (NIP-10 compliant).
+ fn collect_reply_chain(&mut self, ndb: &Ndb, txn: &Transaction, note: &Note) {
+ use nostrdb::NoteReply;
+
+ let reply = NoteReply::new(note.tags());
+
+ // Add root note if missing
+ if let Some(root_ref) = reply.root() {
+ let relay_hint: Vec<RelayUrl> = root_ref
+ .relay
+ .and_then(|s| RelayUrl::parse(s).ok())
+ .into_iter()
+ .collect();
+ self.add_note_if_missing(ndb, txn, root_ref.id, relay_hint);
+ }
+
+ // Add reply note if missing (and different from root)
+ if let Some(reply_ref) = reply.reply() {
+ let relay_hint: Vec<RelayUrl> = reply_ref
+ .relay
+ .and_then(|s| RelayUrl::parse(s).ok())
+ .into_iter()
+ .collect();
+ self.add_note_if_missing(ndb, txn, reply_ref.id, relay_hint);
+ }
+ }
+
+ /// Collect unknowns from content blocks (mentions).
+ fn collect_from_blocks(&mut self, ndb: &Ndb, txn: &Transaction, note: &Note) {
+ let Some(note_key) = note.key() else {
+ return;
+ };
+
+ let Ok(blocks) = ndb.get_blocks_by_key(txn, note_key) else {
+ return;
+ };
+
+ for block in blocks.iter(note) {
+ if block.blocktype() != BlockType::MentionBech32 {
+ continue;
+ }
+
+ let Some(mention) = block.as_mention() else {
+ continue;
+ };
+
+ match mention {
+ // npub - simple pubkey mention
+ Mention::Pubkey(npub) => {
+ self.add_profile_if_missing(ndb, txn, npub.pubkey());
+ }
+ // nprofile - pubkey with relay hints
+ Mention::Profile(nprofile) => {
+ if ndb.get_profile_by_pubkey(txn, nprofile.pubkey()).is_err() {
+ let relays: HashSet<RelayUrl> = nprofile
+ .relays_iter()
+ .filter_map(|s| RelayUrl::parse(s).ok())
+ .collect();
+ let unknown_id = UnknownId::Profile(*nprofile.pubkey());
+ self.ids.entry(unknown_id).or_default().extend(relays);
+ }
+ }
+ // nevent - event with relay hints
+ Mention::Event(ev) => {
+ let relays: HashSet<RelayUrl> = ev
+ .relays_iter()
+ .filter_map(|s| RelayUrl::parse(s).ok())
+ .collect();
+
+ match ndb.get_note_by_id(txn, ev.id()) {
+ Err(_) => {
+ // Event not found - add it and its author if specified
+ self.add_note_if_missing(ndb, txn, ev.id(), relays.clone());
+ if let Some(pk) = ev.pubkey() {
+ if ndb.get_profile_by_pubkey(txn, pk).is_err() {
+ let unknown_id = UnknownId::Profile(*pk);
+ self.ids.entry(unknown_id).or_default().extend(relays);
+ }
+ }
+ }
+ Ok(found_note) => {
+ // Event found but maybe we need the author profile
+ if ndb.get_profile_by_pubkey(txn, found_note.pubkey()).is_err() {
+ let unknown_id = UnknownId::Profile(*found_note.pubkey());
+ self.ids.entry(unknown_id).or_default().extend(relays);
+ }
+ }
+ }
+ }
+ // note1 - simple note mention
+ Mention::Note(note_mention) => {
+ match ndb.get_note_by_id(txn, note_mention.id()) {
+ Err(_) => {
+ self.add_note_if_missing(ndb, txn, note_mention.id(), std::iter::empty());
+ }
+ Ok(found_note) => {
+ // Note found but maybe we need the author profile
+ self.add_profile_if_missing(ndb, txn, found_note.pubkey());
+ }
+ }
+ }
+ _ => {}
+ }
+ }
+ }
}