ui: move note and profile rendering to notedeck_ui

We want to render notes in other apps like dave, so lets move
our note rendering to notedeck_ui. We rework NoteAction so it doesn't
have anything specific to notedeck_columns

Signed-off-by: William Casarin <jb55@jb55.com>
This commit is contained in:
William Casarin
2025-04-17 11:01:45 -07:00
parent e4bae57619
commit 8af80d7d10
53 changed files with 1436 additions and 1607 deletions

View File

@@ -0,0 +1,20 @@
#[inline]
pub fn floor_char_boundary(s: &str, index: usize) -> usize {
if index >= s.len() {
s.len()
} else {
let lower_bound = index.saturating_sub(3);
let new_index = s.as_bytes()[lower_bound..=index]
.iter()
.rposition(|b| is_utf8_char_boundary(*b));
// SAFETY: we know that the character boundary will be within four bytes
unsafe { lower_bound + new_index.unwrap_unchecked() }
}
}
#[inline]
fn is_utf8_char_boundary(c: u8) -> bool {
// This is bit magic equivalent to: b < 128 || b >= 192
(c as i8) >= -0x40
}

View File

@@ -1,3 +1,4 @@
pub mod abbrev;
mod accounts;
mod app;
mod args;
@@ -9,10 +10,12 @@ pub mod fonts;
mod frame_history;
mod imgcache;
mod muted;
pub mod name;
pub mod note;
mod notecache;
mod persist;
pub mod platform;
pub mod profile;
pub mod relay_debug;
pub mod relayspec;
mod result;
@@ -41,9 +44,14 @@ pub use imgcache::{
MediaCacheValue, TextureFrame, TexturedImage,
};
pub use muted::{MuteFun, Muted};
pub use note::{NoteRef, RootIdError, RootNoteId, RootNoteIdBuf};
pub use name::NostrName;
pub use note::{
BroadcastContext, ContextSelection, NoteAction, NoteContext, NoteContextSelection, NoteRef,
RootIdError, RootNoteId, RootNoteIdBuf, ZapAction,
};
pub use notecache::{CachedNote, NoteCache};
pub use persist::*;
pub use profile::get_profile_url;
pub use relay_debug::RelayDebugView;
pub use relayspec::RelaySpec;
pub use result::Result;

View File

@@ -0,0 +1,64 @@
use nostrdb::ProfileRecord;
pub struct NostrName<'a> {
pub username: Option<&'a str>,
pub display_name: Option<&'a str>,
pub nip05: Option<&'a str>,
}
impl<'a> NostrName<'a> {
pub fn name(&self) -> &'a str {
if let Some(name) = self.username {
name
} else if let Some(name) = self.display_name {
name
} else {
self.nip05.unwrap_or("??")
}
}
pub fn unknown() -> Self {
Self {
username: None,
display_name: None,
nip05: None,
}
}
}
fn is_empty(s: &str) -> bool {
s.chars().all(|c| c.is_whitespace())
}
pub fn get_display_name<'a>(record: Option<&ProfileRecord<'a>>) -> NostrName<'a> {
let Some(record) = record else {
return NostrName::unknown();
};
let Some(profile) = record.record().profile() else {
return NostrName::unknown();
};
let display_name = profile.display_name().filter(|n| !is_empty(n));
let username = profile.name().filter(|n| !is_empty(n));
let nip05 = if let Some(raw_nip05) = profile.nip05() {
if let Some(at_pos) = raw_nip05.find('@') {
if raw_nip05.starts_with('_') {
raw_nip05.get(at_pos + 1..)
} else {
Some(raw_nip05)
}
} else {
None
}
} else {
None
};
NostrName {
username,
display_name,
nip05,
}
}

View File

@@ -0,0 +1,33 @@
use super::context::ContextSelection;
use crate::zaps::NoteZapTargetOwned;
use enostr::{NoteId, Pubkey};
#[derive(Debug, Eq, PartialEq, Clone)]
pub enum NoteAction {
/// User has clicked the quote reply action
Reply(NoteId),
/// User has clicked the quote repost action
Quote(NoteId),
/// User has clicked a hashtag
Hashtag(String),
/// User has clicked a profile
Profile(Pubkey),
/// User has clicked a note link
Note(NoteId),
/// User has selected some context option
Context(ContextSelection),
/// User has clicked the zap action
Zap(ZapAction),
}
#[derive(Debug, Eq, PartialEq, Clone)]
pub enum ZapAction {
Send(NoteZapTargetOwned),
ClearError(NoteZapTargetOwned),
}

View File

@@ -0,0 +1,63 @@
use enostr::{ClientMessage, NoteId, Pubkey, RelayPool};
use nostrdb::{Note, NoteKey};
use tracing::error;
/// When broadcasting notes, this determines whether to broadcast
/// over the local network via multicast, or globally
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum BroadcastContext {
LocalNetwork,
Everywhere,
}
#[derive(Debug, Clone, Eq, PartialEq)]
#[allow(clippy::enum_variant_names)]
pub enum NoteContextSelection {
CopyText,
CopyPubkey,
CopyNoteId,
CopyNoteJSON,
Broadcast(BroadcastContext),
}
#[derive(Debug, Eq, PartialEq, Clone)]
pub struct ContextSelection {
pub note_key: NoteKey,
pub action: NoteContextSelection,
}
impl NoteContextSelection {
pub fn process(&self, ui: &mut egui::Ui, note: &Note<'_>, pool: &mut RelayPool) {
match self {
NoteContextSelection::Broadcast(context) => {
tracing::info!("Broadcasting note {}", hex::encode(note.id()));
match context {
BroadcastContext::LocalNetwork => {
pool.send_to(&ClientMessage::event(note).unwrap(), "multicast");
}
BroadcastContext::Everywhere => {
pool.send(&ClientMessage::event(note).unwrap());
}
}
}
NoteContextSelection::CopyText => {
ui.ctx().copy_text(note.content().to_string());
}
NoteContextSelection::CopyPubkey => {
if let Some(bech) = Pubkey::new(*note.pubkey()).to_bech() {
ui.ctx().copy_text(bech);
}
}
NoteContextSelection::CopyNoteId => {
if let Some(bech) = NoteId::new(*note.id()).to_bech() {
ui.ctx().copy_text(bech);
}
}
NoteContextSelection::CopyNoteJSON => match note.json() {
Ok(json) => ui.ctx().copy_text(json),
Err(err) => error!("error copying note json: {err}"),
},
}
}
}

View File

@@ -1,10 +1,26 @@
use crate::notecache::NoteCache;
use enostr::NoteId;
mod action;
mod context;
pub use action::{NoteAction, ZapAction};
pub use context::{BroadcastContext, ContextSelection, NoteContextSelection};
use crate::{notecache::NoteCache, zaps::Zaps, Images};
use enostr::{NoteId, RelayPool};
use nostrdb::{Ndb, Note, NoteKey, QueryResult, Transaction};
use std::borrow::Borrow;
use std::cmp::Ordering;
use std::fmt;
/// Aggregates dependencies to reduce the number of parameters
/// passed to inner UI elements, minimizing prop drilling.
pub struct NoteContext<'d> {
pub ndb: &'d Ndb,
pub img_cache: &'d mut Images,
pub note_cache: &'d mut NoteCache,
pub zaps: &'d mut Zaps,
pub pool: &'d mut RelayPool,
}
#[derive(Debug, Eq, PartialEq, Copy, Clone, Hash)]
pub struct NoteRef {
pub key: NoteKey,

View File

@@ -0,0 +1,18 @@
use nostrdb::ProfileRecord;
pub fn get_profile_url<'a>(profile: Option<&ProfileRecord<'a>>) -> &'a str {
unwrap_profile_url(profile.and_then(|pr| pr.record().profile().and_then(|p| p.picture())))
}
pub fn unwrap_profile_url(maybe_url: Option<&str>) -> &str {
if let Some(url) = maybe_url {
url
} else {
no_pfp_url()
}
}
#[inline]
pub fn no_pfp_url() -> &'static str {
"https://damus.io/img/no-profile.svg"
}