diff --git a/src/comms.rs b/src/comms.rs index 2985ce00..178bdce2 100644 --- a/src/comms.rs +++ b/src/comms.rs @@ -1,4 +1,4 @@ -use nostr_types::{Event, Id, IdHex, PublicKey, PublicKeyHex}; +use nostr_types::{Event, Id, IdHex, PublicKey, PublicKeyHex, Tag}; /// This is a message sent to the Overlord #[derive(Debug, Clone)] @@ -15,8 +15,8 @@ pub enum ToOverlordMessage { Like(Id, PublicKey), MinionIsReady, ProcessIncomingEvents, - PostReply(String, Id), - PostTextNote(String), + PostReply(String, Vec, Id), + PostTextNote(String, Vec), SaveRelays, SaveSettings, Shutdown, diff --git a/src/overlord/mod.rs b/src/overlord/mod.rs index 961e09c1..1564e8c8 100644 --- a/src/overlord/mod.rs +++ b/src/overlord/mod.rs @@ -436,11 +436,11 @@ impl Overlord { } }); } - ToOverlordMessage::PostReply(content, reply_to) => { - self.post_reply(content, reply_to).await?; + ToOverlordMessage::PostReply(content, tags, reply_to) => { + self.post_reply(content, tags, reply_to).await?; } - ToOverlordMessage::PostTextNote(content) => { - self.post_textnote(content).await?; + ToOverlordMessage::PostTextNote(content, tags) => { + self.post_textnote(content, tags).await?; } ToOverlordMessage::SaveRelays => { let dirty_relays: Vec = GLOBALS @@ -619,7 +619,7 @@ impl Overlord { Ok(()) } - async fn post_textnote(&mut self, content: String) -> Result<(), Error> { + async fn post_textnote(&mut self, content: String, tags: Vec) -> Result<(), Error> { let event = { let public_key = match GLOBALS.signer.read().await.public_key() { Some(pk) => pk, @@ -633,7 +633,7 @@ impl Overlord { pubkey: public_key, created_at: Unixtime::now().unwrap(), kind: EventKind::TextNote, - tags: vec![], + tags, content, ots: None, }; @@ -672,9 +672,12 @@ impl Overlord { Ok(()) } - async fn post_reply(&mut self, content: String, reply_to: Id) -> Result<(), Error> { - let mut tags: Vec = Vec::new(); - + async fn post_reply( + &mut self, + content: String, + mut tags: Vec, + reply_to: Id, + ) -> Result<(), Error> { let event = { let public_key = match GLOBALS.signer.read().await.public_key() { Some(pk) => pk, @@ -720,6 +723,8 @@ impl Overlord { .collect(); tags.extend(parent_p_tags); + // FIXME deduplicate 'p' tags + let pre_event = PreEvent { pubkey: public_key, created_at: Unixtime::now().unwrap(), diff --git a/src/people.rs b/src/people.rs index 0846b047..29d78e29 100644 --- a/src/people.rs +++ b/src/people.rs @@ -2,7 +2,7 @@ use crate::db::DbPerson; use crate::error::Error; use crate::globals::GLOBALS; use image::RgbaImage; -use nostr_types::{Metadata, PublicKeyHex, Unixtime, Url}; +use nostr_types::{Metadata, PublicKey, PublicKeyHex, Unixtime, Url}; use std::cmp::Ordering; use std::collections::{HashMap, HashSet}; use std::time::Duration; @@ -333,6 +333,28 @@ 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 get_ids_from_prefix(&self, mut prefix: &str) -> Vec<(String, PublicKey)> { + // work with or without the @ symbol: + if prefix.starts_with('@') { + prefix = &prefix[1..] + } + self.people + .iter() + .filter_map(|(_, person)| { + if let Some(name) = &person.name { + if name.starts_with(prefix) { + let pubkey = PublicKey::try_from_hex_string(&person.pubkey).unwrap(); // FIXME + return Some((name.clone(), pubkey)); + } + } + None + }) + .take(10) + .collect() + } + /// This is a 'just in case' the main code isn't keeping them in sync. pub async fn populate_new_people() -> Result<(), Error> { let sql = "INSERT or IGNORE INTO person (pubkey) SELECT DISTINCT pubkey FROM EVENT"; diff --git a/src/ui/feed.rs b/src/ui/feed.rs index 8e8f9f29..48d3fe56 100644 --- a/src/ui/feed.rs +++ b/src/ui/feed.rs @@ -145,28 +145,60 @@ pub(super) fn update(app: &mut GossipUi, ctx: &Context, frame: &mut eframe::Fram } ui.with_layout(Layout::right_to_left(Align::TOP), |ui| { - if ui.button("Send").clicked() && !app.draft.is_empty() { - match app.replying_to { - Some(replying_to_id) => { - let _ = GLOBALS.to_overlord.send(ToOverlordMessage::PostReply( - app.draft.clone(), - replying_to_id, - )); + ui.with_layout(Layout::top_down(Align::RIGHT), |ui| { + if ui.button("Send").clicked() && !app.draft.is_empty() { + match app.replying_to { + Some(replying_to_id) => { + let _ = GLOBALS.to_overlord.send(ToOverlordMessage::PostReply( + app.draft.clone(), + app.draft_tags.clone(), + replying_to_id, + )); + } + None => { + let _ = GLOBALS.to_overlord.send(ToOverlordMessage::PostTextNote( + app.draft.clone(), + app.draft_tags.clone(), + )); + } } - None => { - let _ = GLOBALS - .to_overlord - .send(ToOverlordMessage::PostTextNote(app.draft.clone())); + app.draft = "".to_owned(); + app.replying_to = None; + } + + if ui.button("Cancel").clicked() { + app.draft = "".to_owned(); + app.replying_to = None; + } + + ui.add( + TextEdit::singleline(&mut app.tag_someone) + .desired_width(100.0) + .hint_text("@username"), + ); + if !app.tag_someone.is_empty() { + let pairs = GLOBALS + .people + .blocking_read() + .get_ids_from_prefix(&app.tag_someone); + if !pairs.is_empty() { + ui.menu_button("@", |ui| { + for pair in pairs { + if ui.button(pair.0).clicked() { + app.draft_tags.push(Tag::Pubkey { + pubkey: pair.1, + recommended_relay_url: None, // FIXME + petname: None, + }); + app.draft + .push_str(&format!("#[{}]", app.draft_tags.len() - 1)); + app.tag_someone = "".to_owned(); + } + } + }); } } - app.draft = "".to_owned(); - app.replying_to = None; - } - if ui.button("Cancel").clicked() { - app.draft = "".to_owned(); - app.replying_to = None; - } - + }); ui.add( TextEdit::multiline(&mut app.draft) .hint_text("Type your message here") diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 120c2494..eeac1200 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -18,7 +18,7 @@ use egui::{ ColorImage, Context, ImageData, Label, RichText, SelectableLabel, Sense, TextStyle, TextureHandle, TextureOptions, Ui, }; -use nostr_types::{Id, IdHex, PublicKey, PublicKeyHex}; +use nostr_types::{Id, IdHex, PublicKey, PublicKeyHex, Tag}; use std::collections::HashMap; use std::time::{Duration, Instant}; use zeroize::Zeroize; @@ -76,6 +76,8 @@ struct GossipUi { icon: TextureHandle, placeholder_avatar: TextureHandle, draft: String, + draft_tags: Vec, + tag_someone: String, settings: Settings, nip05follow: String, follow_bech32_pubkey: String, @@ -148,6 +150,8 @@ 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(), follow_bech32_pubkey: "".to_owned(),