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.
This commit is contained in:
fiatjaf 2023-01-17 14:57:08 -03:00
parent 876e9e1516
commit 0264ee8bf6
No known key found for this signature in database
GPG Key ID: BAD43C4BE5C1A3A1
6 changed files with 145 additions and 85 deletions

View File

@ -19,6 +19,7 @@ mod process;
mod relationship; mod relationship;
mod settings; mod settings;
mod signer; mod signer;
mod tags;
mod ui; mod ui;
use crate::comms::ToOverlordMessage; use crate::comms::ToOverlordMessage;

View File

@ -6,6 +6,7 @@ use crate::db::{DbEvent, DbEventSeen, DbPersonRelay, DbRelay};
use crate::error::Error; use crate::error::Error;
use crate::globals::GLOBALS; use crate::globals::GLOBALS;
use crate::people::People; use crate::people::People;
use crate::tags::{add_pubkey_to_tags, add_tag, keys_from_text};
use minion::Minion; use minion::Minion;
use nostr_types::{ use nostr_types::{
Event, EventKind, Id, IdHex, PreEvent, PrivateKey, PublicKey, PublicKeyHex, Tag, Unixtime, Url, Event, EventKind, Id, IdHex, PreEvent, PrivateKey, PublicKey, PublicKeyHex, Tag, Unixtime, Url,
@ -602,7 +603,11 @@ impl Overlord {
Ok(()) Ok(())
} }
async fn post_textnote(&mut self, content: String, mut tags: Vec<Tag>) -> Result<(), Error> { async fn post_textnote(
&mut self,
mut content: String,
mut tags: Vec<Tag>,
) -> Result<(), Error> {
let event = { let event = {
let public_key = match GLOBALS.signer.read().await.public_key() { let public_key = match GLOBALS.signer.read().await.public_key() {
Some(pk) => pk, 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 { let pre_event = PreEvent {
pubkey: public_key, pubkey: public_key,
created_at: Unixtime::now().unwrap(), created_at: Unixtime::now().unwrap(),
@ -669,7 +681,7 @@ impl Overlord {
async fn post_reply( async fn post_reply(
&mut self, &mut self,
content: String, mut content: String,
mut tags: Vec<Tag>, mut tags: Vec<Tag>,
reply_to: Id, reply_to: Id,
) -> Result<(), Error> { ) -> Result<(), Error> {
@ -692,33 +704,24 @@ impl Overlord {
} }
}; };
// Add all the 'p' tags from the note we are replying to // Add a 'p' tag for the author we are replying to (except if it is our own key)
for parent_p_tag in event.tags.iter() { if let Some(pubkey) = GLOBALS.signer.read().await.public_key() {
if let Tag::Pubkey { if pubkey != event.pubkey {
pubkey: parent_p_tag_pubkey, add_pubkey_to_tags(&mut tags, pubkey.into());
.. }
} = parent_p_tag
{
if parent_p_tag_pubkey.0 == public_key.as_hex_string() {
// do not tag ourselves
continue;
} }
if tags // Add all the 'p' tags from the note we are replying to (except our own)
.iter() for tag in &event.tags {
.any(|existing_tag| { if tag.tagname() == "p" {
matches!( add_tag(&mut tags, tag);
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) // Add tags for keys that are in the post body as npub1...
tags.push(parent_p_tag.to_owned()) 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() { if let Some((root, _maybeurl)) = event.replies_to_root() {

View File

@ -6,7 +6,9 @@ use dashmap::{DashMap, DashSet};
use eframe::egui::ColorImage; use eframe::egui::ColorImage;
use egui_extras::image::FitTo; use egui_extras::image::FitTo;
use image::imageops::FilterType; 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::cmp::Ordering;
use std::time::Duration; use std::time::Duration;
use tokio::task; use tokio::task;
@ -351,7 +353,7 @@ impl People {
/// This lets you start typing a name, and autocomplete the results for tagging /// This lets you start typing a name, and autocomplete the results for tagging
/// someone in a post. It returns maximum 10 results. /// 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: // work with or without the @ symbol:
if text.starts_with('@') { if text.starts_with('@') {
text = &text[1..] text = &text[1..]
@ -419,7 +421,12 @@ impl People {
}; };
results[0..max] results[0..max]
.iter() .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() .collect()
} }

84
src/tags.rs Normal file
View File

@ -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<Tag>, 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<Tag>, 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
}
}
}

View File

@ -2,6 +2,7 @@ use super::{GossipUi, Page};
use crate::comms::ToOverlordMessage; use crate::comms::ToOverlordMessage;
use crate::feed::FeedKind; use crate::feed::FeedKind;
use crate::globals::{Globals, GLOBALS}; use crate::globals::{Globals, GLOBALS};
use crate::tags::keys_from_text;
use crate::ui::widgets::CopyButton; use crate::ui::widgets::CopyButton;
use crate::AVATAR_SIZE_F32; use crate::AVATAR_SIZE_F32;
use eframe::egui; use eframe::egui;
@ -141,25 +142,22 @@ fn real_posting_area(app: &mut GossipUi, ctx: &Context, frame: &mut eframe::Fram
Some(replying_to_id) => { Some(replying_to_id) => {
let _ = GLOBALS.to_overlord.send(ToOverlordMessage::PostReply( let _ = GLOBALS.to_overlord.send(ToOverlordMessage::PostReply(
app.draft.clone(), app.draft.clone(),
app.draft_tags.clone(), vec![],
replying_to_id, replying_to_id,
)); ));
} }
None => { None => {
let _ = GLOBALS.to_overlord.send(ToOverlordMessage::PostTextNote( let _ = GLOBALS
app.draft.clone(), .to_overlord
app.draft_tags.clone(), .send(ToOverlordMessage::PostTextNote(app.draft.clone(), vec![]));
));
} }
} }
app.draft = "".to_owned(); app.draft = "".to_owned();
app.draft_tags = vec![];
app.replying_to = None; app.replying_to = None;
} }
if ui.button("Cancel").clicked() { if ui.button("Cancel").clicked() {
app.draft = "".to_owned(); app.draft = "".to_owned();
app.draft_tags = vec![];
app.replying_to = None; 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| { ui.menu_button("@", |ui| {
for pair in pairs { for pair in pairs {
if ui.button(pair.0).clicked() { if ui.button(pair.0).clicked() {
let idx = app if !app.draft.ends_with(" ") {
.draft_tags app.draft.push_str(" ");
.iter() }
.position(|tag| { app.draft.push_str(&pair.1.try_as_bech32_string().unwrap());
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));
app.tag_someone = "".to_owned(); 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 // List tags that will be applied (FIXME: list tags from parent event too in case of reply)
for (i, tag) in app.draft_tags.iter().enumerate() { for (i, (npub, pubkey)) in keys_from_text(&app.draft).iter().enumerate() {
let rendered = match tag { let rendered = if let Some(person) = GLOBALS.people.get(&pubkey.as_hex_string().into()) {
Tag::Pubkey { pubkey, .. } => {
if let Some(person) = GLOBALS.people.get(pubkey) {
match person.name { match person.name {
Some(name) => name, Some(name) => name,
None => pubkey.0.clone(), None => npub.to_string(),
} }
} else { } else {
pubkey.0.clone() npub.to_string()
}
}
_ => serde_json::to_string(tag).unwrap(),
}; };
ui.label(format!("{}: {}", i, rendered)); ui.label(format!("{}: {}", i, rendered));
} }
} }
@ -538,19 +518,6 @@ fn render_post_actual(
.clicked() .clicked()
{ {
app.replying_to = Some(event.id); 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); ui.add_space(24.0);

View File

@ -19,7 +19,7 @@ use egui::{
ColorImage, Context, ImageData, Label, RichText, SelectableLabel, Sense, TextStyle, ColorImage, Context, ImageData, Label, RichText, SelectableLabel, Sense, TextStyle,
TextureHandle, TextureOptions, Ui, TextureHandle, TextureOptions, Ui,
}; };
use nostr_types::{Id, IdHex, PublicKey, PublicKeyHex, Tag}; use nostr_types::{Id, IdHex, PublicKey, PublicKeyHex};
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::atomic::Ordering; use std::sync::atomic::Ordering;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
@ -76,7 +76,6 @@ struct GossipUi {
icon: TextureHandle, icon: TextureHandle,
placeholder_avatar: TextureHandle, placeholder_avatar: TextureHandle,
draft: String, draft: String,
draft_tags: Vec<Tag>,
tag_someone: String, tag_someone: String,
settings: Settings, settings: Settings,
nip05follow: String, nip05follow: String,
@ -174,7 +173,6 @@ impl GossipUi {
icon: icon_texture_handle, icon: icon_texture_handle,
placeholder_avatar: placeholder_avatar_texture_handle, placeholder_avatar: placeholder_avatar_texture_handle,
draft: "".to_owned(), draft: "".to_owned(),
draft_tags: Vec::new(),
tag_someone: "".to_owned(), tag_someone: "".to_owned(),
settings, settings,
nip05follow: "".to_owned(), nip05follow: "".to_owned(),