notedeck

One damus client to rule them all
git clone git://jb55.com/notedeck
Log | Files | Refs | README | LICENSE

commit 9b7033e208f7a8405fe56125a2f0867cacdb6839
parent 4014d122c9d9ee9c43962cbd669a4869adf28547
Author: kernelkind <kernelkind@gmail.com>
Date:   Sat,  5 Jul 2025 13:21:07 -0400

add `Contacts`

Signed-off-by: kernelkind <kernelkind@gmail.com>

Diffstat:
Acrates/notedeck/src/account/contacts.rs | 145+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/notedeck/src/account/mod.rs | 1+
Mcrates/notedeck/src/lib.rs | 1+
3 files changed, 147 insertions(+), 0 deletions(-)

diff --git a/crates/notedeck/src/account/contacts.rs b/crates/notedeck/src/account/contacts.rs @@ -0,0 +1,145 @@ +use std::collections::HashSet; + +use enostr::Pubkey; +use nostrdb::{Filter, Ndb, Note, NoteKey, Subscription, Transaction}; + +pub struct Contacts { + pub filter: Filter, + pub(super) state: ContactState, +} + +pub enum ContactState { + Unreceived, + Received { + contacts: HashSet<Pubkey>, + note_key: NoteKey, + }, +} + +#[derive(Eq, PartialEq, Debug, Clone, Copy)] +pub enum IsFollowing { + /// We don't have the contact list, so we don't know + Unknown, + + /// We are follow + Yes, + + No, +} + +impl Contacts { + pub fn new(pubkey: &[u8; 32]) -> Self { + let filter = Filter::new().authors([pubkey]).kinds([3]).limit(1).build(); + + Self { + filter, + state: ContactState::Unreceived, + } + } + + pub(super) fn query(&mut self, ndb: &Ndb, txn: &Transaction) { + let binding = ndb + .query(txn, &[self.filter.clone()], 1) + .expect("query user relays results"); + + let Some(res) = binding.first() else { + return; + }; + + update_state(&mut self.state, &res.note, res.note_key); + } + + pub fn is_following(&self, other: &Pubkey) -> IsFollowing { + match &self.state { + ContactState::Unreceived => IsFollowing::Unknown, + ContactState::Received { + contacts, + note_key: _, + } => { + if contacts.contains(other) { + IsFollowing::Yes + } else { + IsFollowing::No + } + } + } + } + + pub(super) fn poll_for_updates(&mut self, ndb: &Ndb, txn: &Transaction, sub: Subscription) { + let nks = ndb.poll_for_notes(sub, 1); + + let Some(key) = nks.first() else { + return; + }; + + let note = match ndb.get_note_by_key(txn, *key) { + Ok(note) => note, + Err(e) => { + tracing::error!("Could not find note at key {:?}: {e}", key); + return; + } + }; + + update_state(&mut self.state, &note, *key); + } + + pub fn get_state(&self) -> &ContactState { + &self.state + } +} + +fn update_state(state: &mut ContactState, note: &Note, key: NoteKey) { + match state { + ContactState::Unreceived => { + *state = ContactState::Received { + contacts: get_contacts_owned(note), + note_key: key, + }; + } + ContactState::Received { contacts, note_key } => { + update_contacts(contacts, note); + *note_key = key; + } + }; +} + +fn get_contacts<'a>(note: &Note<'a>) -> HashSet<&'a [u8; 32]> { + let mut contacts = HashSet::with_capacity(note.tags().count().into()); + + for tag in note.tags() { + if tag.count() < 2 { + continue; + } + + let Some("p") = tag.get_str(0) else { + continue; + }; + + let Some(cur_id) = tag.get_id(1) else { + continue; + }; + + contacts.insert(cur_id); + } + + contacts +} + +fn get_contacts_owned(note: &Note<'_>) -> HashSet<Pubkey> { + get_contacts(note) + .iter() + .map(|p| Pubkey::new(**p)) + .collect() +} + +fn update_contacts(cur: &mut HashSet<Pubkey>, new: &Note<'_>) { + let new_contacts = get_contacts(new); + + cur.retain(|pk| new_contacts.contains(pk.bytes())); + + new_contacts.iter().for_each(|c| { + if !cur.contains(*c) { + cur.insert(Pubkey::new(**c)); + } + }); +} diff --git a/crates/notedeck/src/account/mod.rs b/crates/notedeck/src/account/mod.rs @@ -1,5 +1,6 @@ pub mod accounts; pub mod cache; +pub mod contacts; pub mod mute; pub mod relay; diff --git a/crates/notedeck/src/lib.rs b/crates/notedeck/src/lib.rs @@ -34,6 +34,7 @@ mod wallet; mod zaps; pub use account::accounts::{AccountData, Accounts}; +pub use account::contacts::{ContactState, IsFollowing}; pub use account::relay::RelayAction; pub use account::FALLBACK_PUBKEY; pub use app::{App, AppAction, Notedeck};