commit dfa6a3547acc4133bf0511155d2a8e32cc45d1b4
parent e774c105060e73d927738d6cedd40fd96b21f96f
Author: kernelkind <kernelkind@gmail.com>
Date: Thu, 26 Feb 2026 17:21:51 -0500
feat(messages): prefetch participant DM relay lists and route outgoing messages
Prefetch kind 10050 relay lists when opening a conversation so
participant relay preferences are available before sending. Outgoing
gift-wrapped messages are now routed to each participant's preferred
DM relays when known, falling back to account write relays otherwise.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat:
4 files changed, 109 insertions(+), 5 deletions(-)
diff --git a/crates/notedeck_messages/src/lib.rs b/crates/notedeck_messages/src/lib.rs
@@ -4,6 +4,7 @@ pub mod loader;
pub mod nav;
pub mod nip17;
mod relay_ensure;
+mod relay_prefetch;
pub mod ui;
use enostr::Pubkey;
@@ -226,7 +227,12 @@ fn handle_loader_messages(
if cache.active.is_none() && !is_narrow {
if let Some(first) = cache.first_convo_id() {
- cache.active = Some(first);
+ open_conversation_with_prefetch(
+ &mut ctx.remote,
+ ctx.accounts,
+ cache,
+ first,
+ );
request_conversation_messages(
cache,
ctx.accounts.selected_account_pubkey(),
@@ -296,11 +302,19 @@ fn request_conversation_messages(
/// Scoped-sub owner namespace for messages DM relay-list lifecycles.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
enum RelayListOwner {
+ Prefetch,
Ensure,
}
const RELAY_LIST_KEY: &str = "dm_relay_list";
+/// Stable owner for DM relay-list prefetch subscriptions per selected account.
+fn list_prefetch_owner_key(account_pk: Pubkey) -> SubOwnerKey {
+ SubOwnerKey::builder(RelayListOwner::Prefetch)
+ .with(account_pk)
+ .finish()
+}
+
/// Stable owner for selected-account DM relay-list ensure subscriptions per selected account.
fn list_ensure_owner_key(account_pk: Pubkey) -> SubOwnerKey {
SubOwnerKey::builder(RelayListOwner::Ensure)
@@ -325,6 +339,18 @@ pub(crate) fn ensure_selected_account_dm_relay_list(
ensure_selected_account_dm_list(ndb, remote, accounts, cache.dm_relay_list_ensure_mut())
}
+/// Marks a conversation active and ensures participant relay-list prefetch.
+#[profiling::function]
+pub(crate) fn open_conversation_with_prefetch(
+ remote: &mut RemoteApi<'_>,
+ accounts: &Accounts,
+ cache: &mut ConversationCache,
+ conversation_id: cache::ConversationId,
+) {
+ cache.active = Some(conversation_id);
+ relay_prefetch::ensure_conversation_prefetch(remote, accounts, cache, conversation_id);
+}
+
/// Storage for conversations per account. Account management is performed by `Accounts`
#[derive(Default)]
struct ConversationsCtx {
diff --git a/crates/notedeck_messages/src/nav.rs b/crates/notedeck_messages/src/nav.rs
@@ -9,6 +9,7 @@ use crate::{
},
loader::MessagesLoader,
nip17::send_conversation_message,
+ open_conversation_with_prefetch,
};
#[derive(Clone, Debug)]
@@ -160,7 +161,7 @@ fn handle_messages_action(
));
cache.initialize_conversation(id, vec![recipient, *selected]);
- cache.active = Some(id);
+ open_conversation_with_prefetch(&mut ctx.remote, ctx.accounts, cache, id);
request_conversation_messages(
cache,
ctx.accounts.selected_account_pubkey(),
@@ -197,7 +198,7 @@ fn open_coversation_action(
loader: &MessagesLoader,
inflight_messages: &mut HashSet<ConversationId>,
) {
- cache.active = Some(id);
+ open_conversation_with_prefetch(&mut ctx.remote, ctx.accounts, cache, id);
request_conversation_messages(
cache,
ctx.accounts.selected_account_pubkey(),
diff --git a/crates/notedeck_messages/src/nip17/message.rs b/crates/notedeck_messages/src/nip17/message.rs
@@ -1,7 +1,9 @@
+use nostrdb::Transaction;
+use notedeck::enostr::RelayId;
use notedeck::{AppContext, RelayType};
use crate::cache::{ConversationCache, ConversationId};
-use crate::nip17::{build_rumor_json, giftwrap_message, OsRng};
+use crate::nip17::{build_rumor_json, giftwrap_message, query_participant_dm_relays, OsRng};
pub fn send_conversation_message(
conversation_id: ConversationId,
@@ -36,6 +38,7 @@ pub fn send_conversation_message(
return;
};
+ let txn = Transaction::new(ctx.ndb).expect("txn");
let mut rng = OsRng;
for participant in &conversation.metadata.participants {
let Some(gifrwrap_note) =
@@ -53,7 +56,19 @@ pub fn send_conversation_message(
}
}
+ let participant_relays = query_participant_dm_relays(ctx.ndb, &txn, participant);
+ let relay_type = if participant_relays.is_empty() {
+ RelayType::AccountsWrite
+ } else {
+ RelayType::Explicit(
+ participant_relays
+ .into_iter()
+ .map(RelayId::Websocket)
+ .collect(),
+ )
+ };
+
let mut publisher = ctx.remote.publisher(ctx.accounts);
- publisher.publish_note(&gifrwrap_note, RelayType::AccountsWrite);
+ publisher.publish_note(&gifrwrap_note, relay_type);
}
}
diff --git a/crates/notedeck_messages/src/relay_prefetch.rs b/crates/notedeck_messages/src/relay_prefetch.rs
@@ -0,0 +1,62 @@
+use enostr::Pubkey;
+use notedeck::{
+ Accounts, RelaySelection, RemoteApi, ScopedSubApi, ScopedSubIdentity, SubConfig, SubOwnerKey,
+};
+
+use crate::{
+ cache::{ConversationCache, ConversationId},
+ list_fetch_sub_key, list_prefetch_owner_key,
+ nip17::participant_dm_relay_list_filter,
+};
+
+/// Pure builder for the scoped-sub spec used to prefetch one participant relay list.
+fn participant_relay_prefetch_spec(participant: &Pubkey) -> SubConfig {
+ SubConfig {
+ relays: RelaySelection::AccountsRead,
+ filters: vec![participant_dm_relay_list_filter(participant)],
+ use_transparent: false,
+ }
+}
+
+/// Ensures remote prefetch subscriptions for one conversation's participants.
+pub(crate) fn ensure_conversation_prefetch(
+ remote: &mut RemoteApi<'_>,
+ accounts: &Accounts,
+ cache: &ConversationCache,
+ conversation_id: ConversationId,
+) {
+ let Some(conversation) = cache.get(conversation_id) else {
+ return;
+ };
+
+ ensure_participant_prefetch(remote, accounts, &conversation.metadata.participants);
+}
+
+/// Ensures remote prefetch subscriptions for all provided participants.
+pub(crate) fn ensure_participant_prefetch(
+ remote: &mut RemoteApi<'_>,
+ accounts: &Accounts,
+ participants: &[Pubkey],
+) {
+ if participants.is_empty() {
+ return;
+ }
+
+ let account_pk = *accounts.selected_account_pubkey();
+ let owner = list_prefetch_owner_key(account_pk);
+ let mut scoped_subs = remote.scoped_subs(accounts);
+ ensure_participant_subs(&mut scoped_subs, owner, participants);
+}
+
+fn ensure_participant_subs(
+ scoped_subs: &mut ScopedSubApi<'_, '_>,
+ owner: SubOwnerKey,
+ participants: &[Pubkey],
+) {
+ for participant in participants {
+ let key = list_fetch_sub_key(participant);
+ let spec = participant_relay_prefetch_spec(participant);
+ let identity = ScopedSubIdentity::account(owner, key);
+ let _ = scoped_subs.ensure_sub(identity, spec);
+ }
+}