diff --git a/src/overlord/mod.rs b/src/overlord/mod.rs index 1037f007..3dd6230d 100644 --- a/src/overlord/mod.rs +++ b/src/overlord/mod.rs @@ -6,7 +6,9 @@ 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 crate::tags::{ + add_event_to_tags, add_pubkey_hex_to_tags, add_pubkey_to_tags, keys_from_text, notes_from_text, +}; use minion::Minion; use nostr_types::{ Event, EventKind, Id, IdHex, PreEvent, PrivateKey, PublicKey, PublicKeyHex, Tag, Unixtime, Url, @@ -626,7 +628,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); + let idx = add_pubkey_to_tags(&mut tags, pubkey).await; + content = content.replace(&npub, &format!("#[{}]", idx)); + } + + // Do the same as above, but now with note1... + for (npub, pubkey) in notes_from_text(&content) { + let idx = add_event_to_tags(&mut tags, pubkey, "mention").await; content = content.replace(&npub, &format!("#[{}]", idx)); } @@ -707,46 +715,43 @@ impl Overlord { // 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); + add_pubkey_to_tags(&mut tags, pubkey).await; } } // 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); + match tag { + Tag::Pubkey { pubkey, .. } => { + add_pubkey_hex_to_tags(&mut tags, pubkey).await; + } + _ => {} } } // 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); + let idx = add_pubkey_to_tags(&mut tags, pubkey).await; + content = content.replace(&npub, &format!("#[{}]", idx)); + } + + // Do the same as above, but now with note1... + for (npub, pubkey) in notes_from_text(&content) { + // NIP-10: "Those marked with "mention" denote a quoted or reposted event id." + let idx = add_event_to_tags(&mut tags, pubkey, "mention").await; 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 { - id: root, - recommended_relay_url: DbRelay::recommended_relay_for_reply(root).await?, - marker: Some("root".to_string()), - }); + add_event_to_tags(&mut tags, root, "root").await; // Add an 'e' tag for the note we are replying to - tags.push(Tag::Event { - id: reply_to, - recommended_relay_url: DbRelay::recommended_relay_for_reply(reply_to).await?, - marker: Some("reply".to_string()), - }); + add_event_to_tags(&mut tags, reply_to, "reply").await; } else { // We are replying to the root. // NIP-10: "A direct reply to the root of a thread should have a single marked "e" tag of type "root"." - - tags.push(Tag::Event { - id: reply_to, - recommended_relay_url: DbRelay::recommended_relay_for_reply(reply_to).await?, - marker: Some("root".to_string()), - }); + add_event_to_tags(&mut tags, reply_to, "root").await; } if GLOBALS.settings.read().await.set_client_tag { diff --git a/src/tags.rs b/src/tags.rs index fc45d55f..415de28b 100644 --- a/src/tags.rs +++ b/src/tags.rs @@ -1,8 +1,9 @@ -use nostr_types::{PublicKey, Tag}; +use crate::db::DbRelay; +use nostr_types::{Id, PublicKey, PublicKeyHex, Tag}; pub fn keys_from_text(text: &str) -> Vec<(String, PublicKey)> { let mut pubkeys: Vec<(String, PublicKey)> = text - .split(&[' ', ',', '.', ':', ';', '?', '!', '/'][..]) + .split(|c: char| !c.is_alphanumeric()) .filter_map(|npub| { if !npub.starts_with("npub1") { None @@ -18,67 +19,95 @@ pub fn keys_from_text(text: &str) -> Vec<(String, PublicKey)> { 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 notes_from_text(text: &str) -> Vec<(String, Id)> { + let mut noteids: Vec<(String, Id)> = text + .split(|c: char| !c.is_alphanumeric()) + .filter_map(|note| { + if !note.starts_with("note1") { + None + } else { + Id::try_from_bech32_string(¬e) + .ok() + .map(|id| (note.to_string(), id)) + } + }) + .collect(); + noteids.sort_unstable_by_key(|ni| ni.1); + noteids.dedup(); + noteids } -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()); +pub async fn add_pubkey_hex_to_tags(existing_tags: &mut Vec, hex: &PublicKeyHex) -> usize { + let newtag = Tag::Pubkey { + pubkey: hex.to_owned(), + recommended_relay_url: None, + petname: None, + }; + + match existing_tags.iter().position(|existing_tag| { + matches!( + existing_tag, + Tag::Pubkey { pubkey: existing_p, .. } if existing_p.0 == hex.0 + ) + }) { + None => { + // FIXME: include relay hint + existing_tags.push(newtag); existing_tags.len() - 1 } + Some(idx) => idx, + } +} + +pub async fn add_pubkey_to_tags(existing_tags: &mut Vec, added: PublicKey) -> usize { + add_pubkey_hex_to_tags(existing_tags, &added.as_hex_string().into()).await +} + +pub async fn add_event_to_tags(existing_tags: &mut Vec, added: Id, marker: &str) -> usize { + let newtag = Tag::Event { + id: added, + recommended_relay_url: DbRelay::recommended_relay_for_reply(added) + .await + .ok() + .flatten(), + marker: Some(marker.to_string()), + }; + + match existing_tags.iter().position(|existing_tag| { + matches!( + existing_tag, + Tag::Event { id: existing_e, .. } if existing_e.0 == added.0 + ) + }) { + None => { + existing_tags.push(newtag); + existing_tags.len() - 1 + } + Some(idx) => idx, + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_parse_pubkeys() { + let pubkeys = keys_from_text("hello npub180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsyjh6w6 and npub180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsyjh6w6... actually npub1melv683fw6n2mvhl5h6dhqd8mqfv3wmxnz4qph83ua4dk4006ezsrt5c24"); + assert_eq!(pubkeys.len(), 2); + assert_eq!( + pubkeys[0].1.as_hex_string(), + "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d" + ); + } + + #[test] + fn test_parse_notes() { + let ids = notes_from_text( + "note1pm88wxjcqfh886gf5tvzjwe6k0crmxzdwtfnmn7ww93dh8dcrkhq82j67f + +Another naïve person falling for the scam of deletes.", + ); + assert_eq!(ids.len(), 1); } } diff --git a/src/ui/feed.rs b/src/ui/feed.rs index e81d2c59..5c1980da 100644 --- a/src/ui/feed.rs +++ b/src/ui/feed.rs @@ -172,7 +172,7 @@ 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() { - if !app.draft.ends_with(' ') { + if !app.draft.ends_with(' ') && app.draft != "" { app.draft.push(' '); } app.draft.push_str(&pair.1.try_as_bech32_string().unwrap()); @@ -513,6 +513,21 @@ fn render_post_actual( ui.add_space(24.0); + // Button to quote note + if ui + .add(Label::new(RichText::new("»").size(18.0)).sense(Sense::click())) + .clicked() + { + if !app.draft.ends_with(" ") && app.draft != "" { + app.draft.push_str(" "); + } + app.draft + .push_str(&event.id.try_as_bech32_string().unwrap()); + } + + ui.add_space(24.0); + + // Button to reply if ui .add(Label::new(RichText::new("💬").size(18.0)).sense(Sense::click())) .clicked() @@ -522,6 +537,7 @@ fn render_post_actual( ui.add_space(24.0); + // Buttons to react and reaction counts if app.settings.reactions { if ui .add(