From 0264ee8bf65ff616549c4e7f307c0bd08f380d17 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Tue, 17 Jan 2023 14:57:08 -0300 Subject: [PATCH] insert and parse npub keys into and from text content. instead of inserting a "#[index]" string. includes a new file `tags.rs` with useful functions. --- src/main.rs | 1 + src/overlord/mod.rs | 59 ++++++++++++++++--------------- src/people.rs | 13 +++++-- src/tags.rs | 84 +++++++++++++++++++++++++++++++++++++++++++++ src/ui/feed.rs | 69 ++++++++++--------------------------- src/ui/mod.rs | 4 +-- 6 files changed, 145 insertions(+), 85 deletions(-) create mode 100644 src/tags.rs diff --git a/src/main.rs b/src/main.rs index 401e0e79..1bb19343 100644 --- a/src/main.rs +++ b/src/main.rs @@ -19,6 +19,7 @@ mod process; mod relationship; mod settings; mod signer; +mod tags; mod ui; use crate::comms::ToOverlordMessage; diff --git a/src/overlord/mod.rs b/src/overlord/mod.rs index 6fe83e1c..b0f540ec 100644 --- a/src/overlord/mod.rs +++ b/src/overlord/mod.rs @@ -6,6 +6,7 @@ use crate::db::{DbEvent, DbEventSeen, DbPersonRelay, DbRelay}; use crate::error::Error; use crate::globals::GLOBALS; use crate::people::People; +use crate::tags::{add_pubkey_to_tags, add_tag, keys_from_text}; use minion::Minion; use nostr_types::{ Event, EventKind, Id, IdHex, PreEvent, PrivateKey, PublicKey, PublicKeyHex, Tag, Unixtime, Url, @@ -602,7 +603,11 @@ impl Overlord { Ok(()) } - async fn post_textnote(&mut self, content: String, mut tags: Vec) -> Result<(), Error> { + async fn post_textnote( + &mut self, + mut content: String, + mut tags: Vec, + ) -> Result<(), Error> { let event = { let public_key = match GLOBALS.signer.read().await.public_key() { Some(pk) => pk, @@ -619,6 +624,13 @@ impl Overlord { }); } + // Add tags for keys that are in the post body as npub1... + for (npub, pubkey) in keys_from_text(&content) { + let idx = add_pubkey_to_tags(&mut tags, pubkey); + content = content.replace(&npub, &format!("#[{}]", idx)); + } + + // Finally build the event let pre_event = PreEvent { pubkey: public_key, created_at: Unixtime::now().unwrap(), @@ -669,7 +681,7 @@ impl Overlord { async fn post_reply( &mut self, - content: String, + mut content: String, mut tags: Vec, reply_to: Id, ) -> Result<(), Error> { @@ -692,35 +704,26 @@ impl Overlord { } }; - // Add all the 'p' tags from the note we are replying to - for parent_p_tag in event.tags.iter() { - if let Tag::Pubkey { - pubkey: parent_p_tag_pubkey, - .. - } = parent_p_tag - { - if parent_p_tag_pubkey.0 == public_key.as_hex_string() { - // do not tag ourselves - continue; - } - - if tags - .iter() - .any(|existing_tag| { - matches!( - existing_tag, - Tag::Pubkey { pubkey: existing_pubkey, .. } if existing_pubkey.0 == parent_p_tag_pubkey.0 - ) - }) { - // we already have this `p` tag, do not add again - continue; - } - - // add (FIXME: include relay hint it not exists) - tags.push(parent_p_tag.to_owned()) + // Add a 'p' tag for the author we are replying to (except if it is our own key) + if let Some(pubkey) = GLOBALS.signer.read().await.public_key() { + if pubkey != event.pubkey { + add_pubkey_to_tags(&mut tags, pubkey.into()); } } + // Add all the 'p' tags from the note we are replying to (except our own) + for tag in &event.tags { + if tag.tagname() == "p" { + add_tag(&mut tags, tag); + } + } + + // Add tags for keys that are in the post body as npub1... + for (npub, pubkey) in keys_from_text(&content) { + let idx = add_pubkey_to_tags(&mut tags, pubkey); + content = content.replace(&npub, &format!("#[{}]", idx)); + } + if let Some((root, _maybeurl)) = event.replies_to_root() { // Add an 'e' tag for the root tags.push(Tag::Event { diff --git a/src/people.rs b/src/people.rs index 547b0596..1d08a85c 100644 --- a/src/people.rs +++ b/src/people.rs @@ -6,7 +6,9 @@ use dashmap::{DashMap, DashSet}; use eframe::egui::ColorImage; use egui_extras::image::FitTo; use image::imageops::FilterType; -use nostr_types::{Event, EventKind, Metadata, PreEvent, PublicKeyHex, Tag, Unixtime, Url}; +use nostr_types::{ + Event, EventKind, Metadata, PreEvent, PublicKey, PublicKeyHex, Tag, Unixtime, Url, +}; use std::cmp::Ordering; use std::time::Duration; use tokio::task; @@ -351,7 +353,7 @@ impl People { /// This lets you start typing a name, and autocomplete the results for tagging /// someone in a post. It returns maximum 10 results. - pub fn search_people_to_tag(&self, mut text: &str) -> Vec<(String, PublicKeyHex)> { + pub fn search_people_to_tag(&self, mut text: &str) -> Vec<(String, PublicKey)> { // work with or without the @ symbol: if text.starts_with('@') { text = &text[1..] @@ -419,7 +421,12 @@ impl People { }; results[0..max] .iter() - .map(|r| (r.1.to_owned(), r.2.clone())) + .map(|r| { + ( + r.1.to_owned(), + PublicKey::try_from_hex_string(&r.2).unwrap(), + ) + }) .collect() } diff --git a/src/tags.rs b/src/tags.rs new file mode 100644 index 00000000..58dda14c --- /dev/null +++ b/src/tags.rs @@ -0,0 +1,84 @@ +use nostr_types::{PublicKey, Tag}; + +pub fn keys_from_text(text: &str) -> Vec<(String, PublicKey)> { + let mut pubkeys: Vec<(String, PublicKey)> = text + .split(&[' ', ',', '.', ':', ';', '?', '!', '/'][..]) + .filter_map(|npub| { + if !npub.starts_with("npub1") { + None + } else { + PublicKey::try_from_bech32_string(&npub) + .ok() + .map(|pubkey| (npub.to_string(), pubkey)) + } + }) + .collect(); + pubkeys.sort_unstable_by_key(|nk| nk.1.as_bytes()); + pubkeys.dedup(); + pubkeys +} + +pub fn add_pubkey_to_tags(existing_tags: &mut Vec, added: PublicKey) -> usize { + add_tag( + existing_tags, + &Tag::Pubkey { + pubkey: added.as_hex_string().into(), + recommended_relay_url: None, + petname: None, + }, + ) +} + +pub fn add_tag(existing_tags: &mut Vec, added: &Tag) -> usize { + match added { + Tag::Pubkey { pubkey, .. } => { + match existing_tags.iter().position(|existing_tag| { + matches!( + existing_tag, + Tag::Pubkey { pubkey: existing_p, .. } if existing_p.0 == pubkey.0 + ) + }) { + None => { + // add (FIXME: include relay hint it not exists) + existing_tags.push(added.to_owned()); + existing_tags.len() - 1 + } + Some(idx) => idx, + } + } + Tag::Event { id, .. } => { + match existing_tags.iter().position(|existing_tag| { + matches!( + existing_tag, + Tag::Event { id: existing_e, .. } if existing_e.0 == id.0 + ) + }) { + None => { + // add (FIXME: include relay hint it not exists) + existing_tags.push(added.to_owned()); + existing_tags.len() - 1 + } + Some(idx) => idx, + } + } + Tag::Hashtag(hashtag) => { + match existing_tags.iter().position(|existing_tag| { + matches!( + existing_tag, + Tag::Hashtag(existing_hashtag) if existing_hashtag == hashtag + ) + }) { + None => { + // add (FIXME: include relay hint it not exists) + existing_tags.push(added.to_owned()); + existing_tags.len() - 1 + } + Some(idx) => idx, + } + } + _ => { + existing_tags.push(added.to_owned()); + existing_tags.len() - 1 + } + } +} diff --git a/src/ui/feed.rs b/src/ui/feed.rs index 0956b1b3..0fc58cda 100644 --- a/src/ui/feed.rs +++ b/src/ui/feed.rs @@ -2,6 +2,7 @@ use super::{GossipUi, Page}; use crate::comms::ToOverlordMessage; use crate::feed::FeedKind; use crate::globals::{Globals, GLOBALS}; +use crate::tags::keys_from_text; use crate::ui::widgets::CopyButton; use crate::AVATAR_SIZE_F32; use eframe::egui; @@ -141,25 +142,22 @@ fn real_posting_area(app: &mut GossipUi, ctx: &Context, frame: &mut eframe::Fram Some(replying_to_id) => { let _ = GLOBALS.to_overlord.send(ToOverlordMessage::PostReply( app.draft.clone(), - app.draft_tags.clone(), + vec![], replying_to_id, )); } None => { - let _ = GLOBALS.to_overlord.send(ToOverlordMessage::PostTextNote( - app.draft.clone(), - app.draft_tags.clone(), - )); + let _ = GLOBALS + .to_overlord + .send(ToOverlordMessage::PostTextNote(app.draft.clone(), vec![])); } } app.draft = "".to_owned(); - app.draft_tags = vec![]; app.replying_to = None; } if ui.button("Cancel").clicked() { app.draft = "".to_owned(); - app.draft_tags = vec![]; app.replying_to = None; } @@ -174,24 +172,10 @@ fn real_posting_area(app: &mut GossipUi, ctx: &Context, frame: &mut eframe::Fram ui.menu_button("@", |ui| { for pair in pairs { if ui.button(pair.0).clicked() { - let idx = app - .draft_tags - .iter() - .position(|tag| { - matches!( - tag, - Tag::Pubkey { pubkey, .. } if pubkey.0 == *pair.1 - ) - }) - .unwrap_or_else(|| { - app.draft_tags.push(Tag::Pubkey { - pubkey: pair.1, - recommended_relay_url: None, // FIXME - petname: None, - }); - app.draft_tags.len() - 1 - }); - app.draft.push_str(&format!("#[{}]", idx)); + if !app.draft.ends_with(" ") { + app.draft.push_str(" "); + } + app.draft.push_str(&pair.1.try_as_bech32_string().unwrap()); app.tag_someone = "".to_owned(); } } @@ -209,21 +193,17 @@ fn real_posting_area(app: &mut GossipUi, ctx: &Context, frame: &mut eframe::Fram ); }); - // List of tags to be applied - for (i, tag) in app.draft_tags.iter().enumerate() { - let rendered = match tag { - Tag::Pubkey { pubkey, .. } => { - if let Some(person) = GLOBALS.people.get(pubkey) { - match person.name { - Some(name) => name, - None => pubkey.0.clone(), - } - } else { - pubkey.0.clone() - } + // List tags that will be applied (FIXME: list tags from parent event too in case of reply) + for (i, (npub, pubkey)) in keys_from_text(&app.draft).iter().enumerate() { + let rendered = if let Some(person) = GLOBALS.people.get(&pubkey.as_hex_string().into()) { + match person.name { + Some(name) => name, + None => npub.to_string(), } - _ => serde_json::to_string(tag).unwrap(), + } else { + npub.to_string() }; + ui.label(format!("{}: {}", i, rendered)); } } @@ -538,19 +518,6 @@ fn render_post_actual( .clicked() { app.replying_to = Some(event.id); - - // Cleanup tags - app.draft_tags = vec![]; - // Add a 'p' tag for the author we are replying to (except if it is our own key) - if let Some(pubkey) = GLOBALS.signer.blocking_read().public_key() { - if pubkey != event.pubkey { - app.draft_tags.push(Tag::Pubkey { - pubkey: event.pubkey.into(), - recommended_relay_url: None, // FIXME - petname: None, - }); - } - } } ui.add_space(24.0); diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 3a8ea0d9..fc181fd2 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -19,7 +19,7 @@ use egui::{ ColorImage, Context, ImageData, Label, RichText, SelectableLabel, Sense, TextStyle, TextureHandle, TextureOptions, Ui, }; -use nostr_types::{Id, IdHex, PublicKey, PublicKeyHex, Tag}; +use nostr_types::{Id, IdHex, PublicKey, PublicKeyHex}; use std::collections::HashMap; use std::sync::atomic::Ordering; use std::time::{Duration, Instant}; @@ -76,7 +76,6 @@ struct GossipUi { icon: TextureHandle, placeholder_avatar: TextureHandle, draft: String, - draft_tags: Vec, tag_someone: String, settings: Settings, nip05follow: String, @@ -174,7 +173,6 @@ impl GossipUi { icon: icon_texture_handle, placeholder_avatar: placeholder_avatar_texture_handle, draft: "".to_owned(), - draft_tags: Vec::new(), tag_someone: "".to_owned(), settings, nip05follow: "".to_owned(),