notecrumbs

a nostr opengraph server build on nostrdb and egui
git clone git://jb55.com/notecrumbs
Log | Files | Refs | README | LICENSE

commit b7db84af605f112f326480ebfb6775a93226ecff
parent d4cb5a739d74a4fde1b4d42c5f54e31034173a65
Author: alltheseas <alltheseas@users.noreply.github.com>
Date:   Wed, 22 Oct 2025 10:41:10 -0500

Refine relay pool connections and profile experience

Diffstat:
Msrc/html.rs | 63++++++++++++++++++++++++++++++++++++++++++++++++++++-----------
Msrc/relay_pool.rs | 41+++++++++++++++++------------------------
2 files changed, 69 insertions(+), 35 deletions(-)

diff --git a/src/html.rs b/src/html.rs @@ -1,7 +1,9 @@ use crate::Error; use crate::{ abbrev::{abbrev_str, abbreviate}, - render::{NoteAndProfileRenderData, ProfileRenderData}, + render::{ + is_image_url, NoteAndProfileRenderData, ProfileRenderData, PROFILE_FEED_RECENT_LIMIT, + }, Notecrumbs, }; use ammonia::Builder as HtmlSanitizer; @@ -414,21 +416,57 @@ const COPY_NPUB_SCRIPT: &str = r#" (function() { 'use strict'; var buttons = document.querySelectorAll('[data-copy-npub]'); - if (!navigator.clipboard || buttons.length === 0) { + if (buttons.length === 0 || !document.body) { return; } + function copyWithExecCommand(value) { + var textarea = document.createElement('textarea'); + textarea.value = value; + textarea.setAttribute('readonly', ''); + textarea.style.position = 'fixed'; + textarea.style.top = '-9999px'; + document.body.appendChild(textarea); + textarea.select(); + var success = false; + try { + success = document.execCommand('copy'); + } catch (err) { + success = false; + } + document.body.removeChild(textarea); + return success; + } + function copyText(value) { + if (navigator.clipboard && navigator.clipboard.writeText) { + return navigator.clipboard.writeText(value); + } + return new Promise(function(resolve, reject) { + if (copyWithExecCommand(value)) { + resolve(); + } else { + reject(new Error('copy unsupported')); + } + }); + } Array.prototype.forEach.call(buttons, function(button) { button.addEventListener('click', function(event) { var value = event.currentTarget.getAttribute('data-copy-npub'); if (!value) { return; } - navigator.clipboard.writeText(value).then(function() { - event.currentTarget.textContent = 'Copied!'; - setTimeout(function() { - event.currentTarget.textContent = 'Copy npub'; - }, 1500); - }); + copyText(value) + .then(function() { + event.currentTarget.textContent = 'Copied!'; + setTimeout(function() { + event.currentTarget.textContent = 'Copy npub'; + }, 1500); + }) + .catch(function() { + event.currentTarget.textContent = 'Copy failed'; + setTimeout(function() { + event.currentTarget.textContent = 'Copy npub'; + }, 1500); + }); }); }); }()); @@ -791,10 +829,13 @@ pub fn serve_profile_html( let note_filter = nostrdb::Filter::new() .authors(author_ref) .kinds([1]) - .limit(20) + .limit(PROFILE_FEED_RECENT_LIMIT as u64) .build(); - if let Ok(results) = app.ndb.query(&txn, &[note_filter], 20) { + if let Ok(results) = app + .ndb + .query(&txn, &[note_filter], PROFILE_FEED_RECENT_LIMIT as i32) + { let mut entries = Vec::new(); for res in results { @@ -824,7 +865,7 @@ pub fn serve_profile_html( } entries.sort_by(|a, b| b.0.cmp(&a.0)); - entries.truncate(6); + entries.truncate(PROFILE_FEED_RECENT_LIMIT); for (timestamp_value, note_body_html, note_link_attr) in entries { let _ = write!( diff --git a/src/relay_pool.rs b/src/relay_pool.rs @@ -12,7 +12,7 @@ use tracing::{debug, warn}; pub struct RelayPool { client: Client, known_relays: Arc<Mutex<HashSet<String>>>, - default_relays: Arc<Vec<RelayUrl>>, + default_relays: Arc<[RelayUrl]>, connect_timeout: Duration, } @@ -34,21 +34,22 @@ impl RelayPool { }) .collect(); + let default_relays = Arc::<[RelayUrl]>::from(parsed_defaults); let pool = Self { client, known_relays: Arc::new(Mutex::new(HashSet::new())), - default_relays: Arc::new(parsed_defaults), + default_relays: default_relays.clone(), connect_timeout, }; - pool.ensure_relays(pool.default_relays()).await?; - pool.connect_known_relays().await?; + pool.ensure_relays(pool.default_relays().iter().cloned()) + .await?; Ok(pool) } - pub fn default_relays(&self) -> Vec<RelayUrl> { - self.default_relays.as_ref().clone() + pub fn default_relays(&self) -> &[RelayUrl] { + self.default_relays.as_ref() } pub async fn ensure_relays<I>(&self, relays: I) -> Result<(), Error> @@ -56,24 +57,33 @@ impl RelayPool { I: IntoIterator<Item = RelayUrl>, { let mut new_relays = Vec::new(); + let mut had_new = false; { let mut guard = self.known_relays.lock().await; for relay in relays { let key = relay.to_string(); if guard.insert(key) { new_relays.push(relay); + had_new = true; } } } for relay in new_relays { debug!("adding relay {}", relay); - self.client.add_relay(relay.clone()).await?; + self.client + .add_relay(relay.clone()) + .await + .map_err(|err| Error::Generic(format!("failed to add relay {relay}: {err}")))?; if let Err(err) = self.client.connect_relay(relay.clone()).await { warn!("failed to connect relay {}: {}", relay, err); } } + if had_new { + self.client.connect_with_timeout(self.connect_timeout).await; + } + Ok(()) } @@ -83,8 +93,6 @@ impl RelayPool { relays: &[RelayUrl], timeout: Duration, ) -> Result<ReceiverStream<Event>, Error> { - self.client.connect_with_timeout(self.connect_timeout).await; - if relays.is_empty() { Ok(self.client.stream_events(filters, Some(timeout)).await?) } else { @@ -95,19 +103,4 @@ impl RelayPool { .await?) } } - - async fn connect_known_relays(&self) -> Result<(), Error> { - let relays = { - let guard = self.known_relays.lock().await; - guard.iter().cloned().collect::<Vec<_>>() - }; - - if relays.is_empty() { - return Ok(()); - } - - self.client.connect_with_timeout(self.connect_timeout).await; - - Ok(()) - } }