Files
notedeck/crates/notedeck_columns/src/profile.rs
kernelkind fdef74c353 fix: sometimes most recent contacts list wasn't used
`ndb::poll_for_notes` appears to give notes as they arrive. We
need to make sure we only use the most recent for contacts

Signed-off-by: kernelkind <kernelkind@gmail.com>
2025-07-17 18:17:56 -04:00

236 lines
6.6 KiB
Rust

use enostr::{FilledKeypair, FullKeypair, ProfileState, Pubkey, RelayPool};
use nostrdb::{Ndb, Note, NoteBuildOptions, NoteBuilder, Transaction};
use notedeck::{Accounts, ContactState};
use tracing::info;
use crate::{nav::RouterAction, route::Route};
pub struct SaveProfileChanges {
pub kp: FullKeypair,
pub state: ProfileState,
}
impl SaveProfileChanges {
pub fn new(kp: FullKeypair, state: ProfileState) -> Self {
Self { kp, state }
}
pub fn to_note(&self) -> Note {
let sec = &self.kp.secret_key.to_secret_bytes();
add_client_tag(NoteBuilder::new())
.kind(0)
.content(&self.state.to_json())
.options(NoteBuildOptions::default().created_at(true).sign(sec))
.build()
.expect("should build")
}
}
fn add_client_tag(builder: NoteBuilder<'_>) -> NoteBuilder<'_> {
builder
.start_tag()
.tag_str("client")
.tag_str("Damus Notedeck")
}
pub enum ProfileAction {
Edit(FullKeypair),
SaveChanges(SaveProfileChanges),
Follow(Pubkey),
Unfollow(Pubkey),
}
impl ProfileAction {
pub fn process_profile_action(
&self,
ndb: &Ndb,
pool: &mut RelayPool,
accounts: &Accounts,
) -> Option<RouterAction> {
match self {
ProfileAction::Edit(kp) => Some(RouterAction::route_to(Route::EditProfile(kp.pubkey))),
ProfileAction::SaveChanges(changes) => {
let note = changes.to_note();
let Ok(event) = enostr::ClientMessage::event(&note) else {
tracing::error!("could not serialize profile note?");
return None;
};
let Ok(json) = event.to_json() else {
tracing::error!("could not serialize profile note?");
return None;
};
// TODO(jb55): do this in a more centralized place
let _ = ndb.process_event_with(&json, nostrdb::IngestMetadata::new().client(true));
info!("sending {}", &json);
pool.send(&event);
Some(RouterAction::GoBack)
}
ProfileAction::Follow(target_key) => {
Self::send_follow_user_event(ndb, pool, accounts, target_key);
None
}
ProfileAction::Unfollow(target_key) => {
Self::send_unfollow_user_event(ndb, pool, accounts, target_key);
None
}
}
}
fn send_follow_user_event(
ndb: &Ndb,
pool: &mut RelayPool,
accounts: &Accounts,
target_key: &Pubkey,
) {
send_kind_3_event(ndb, pool, accounts, FollowAction::Follow(target_key));
}
fn send_unfollow_user_event(
ndb: &Ndb,
pool: &mut RelayPool,
accounts: &Accounts,
target_key: &Pubkey,
) {
send_kind_3_event(ndb, pool, accounts, FollowAction::Unfollow(target_key));
}
}
pub fn builder_from_note<F>(note: Note<'_>, skip_tag: Option<F>) -> NoteBuilder<'_>
where
F: Fn(&nostrdb::Tag<'_>) -> bool,
{
let mut builder = NoteBuilder::new();
builder = builder.content(note.content());
builder = builder.options(NoteBuildOptions::default());
builder = builder.kind(note.kind());
builder = builder.pubkey(note.pubkey());
for tag in note.tags() {
if let Some(skip) = &skip_tag {
if skip(&tag) {
continue;
}
}
builder = builder.start_tag();
for tag_item in tag {
builder = match tag_item.variant() {
nostrdb::NdbStrVariant::Id(i) => builder.tag_id(i),
nostrdb::NdbStrVariant::Str(s) => builder.tag_str(s),
};
}
}
builder
}
enum FollowAction<'a> {
Follow(&'a Pubkey),
Unfollow(&'a Pubkey),
}
fn send_kind_3_event(ndb: &Ndb, pool: &mut RelayPool, accounts: &Accounts, action: FollowAction) {
let Some(kp) = accounts.get_selected_account().key.to_full() else {
return;
};
let txn = Transaction::new(ndb).expect("txn");
let ContactState::Received {
contacts: _,
note_key,
timestamp: _,
} = accounts.get_selected_account().data.contacts.get_state()
else {
return;
};
let contact_note = match ndb.get_note_by_key(&txn, *note_key).ok() {
Some(n) => n,
None => {
tracing::error!(
"Somehow we are in state ContactState::Received but the contact note key doesn't exist"
);
return;
}
};
if contact_note.kind() != 3 {
tracing::error!(
"Something very wrong just occured. The key for the supposed contact note yielded a note which was not a contact..."
);
return;
}
let builder = match action {
FollowAction::Follow(pubkey) => {
builder_from_note(contact_note, None::<fn(&nostrdb::Tag<'_>) -> bool>)
.start_tag()
.tag_str("p")
.tag_str(&pubkey.hex())
}
FollowAction::Unfollow(pubkey) => builder_from_note(
contact_note,
Some(|tag: &nostrdb::Tag<'_>| {
if tag.count() < 2 {
return false;
}
let Some("p") = tag.get_str(0) else {
return false;
};
let Some(cur_val) = tag.get_id(1) else {
return false;
};
cur_val == pubkey.bytes()
}),
),
};
send_note_builder(builder, ndb, pool, kp);
}
fn send_note_builder(builder: NoteBuilder, ndb: &Ndb, pool: &mut RelayPool, kp: FilledKeypair) {
let note = builder
.sign(&kp.secret_key.secret_bytes())
.build()
.expect("build note");
let Ok(event) = &enostr::ClientMessage::event(&note) else {
tracing::error!("send_note_builder: failed to build json");
return;
};
let Ok(json) = event.to_json() else {
tracing::error!("send_note_builder: failed to build json");
return;
};
let _ = ndb.process_event_with(&json, nostrdb::IngestMetadata::new().client(true));
info!("sending {}", &json);
pool.send(event);
}
pub fn send_new_contact_list(kp: FilledKeypair, ndb: &Ndb, pool: &mut RelayPool) {
let builder = construct_new_contact_list(kp.pubkey);
send_note_builder(builder, ndb, pool, kp);
}
fn construct_new_contact_list<'a>(pk: &'a Pubkey) -> NoteBuilder<'a> {
NoteBuilder::new()
.content("")
.kind(3)
.options(NoteBuildOptions::default())
.start_tag()
.tag_str("p")
.tag_str(&pk.hex())
}