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 settings;
mod signer;
mod tags;
mod ui;
use crate::comms::ToOverlordMessage;

View File

@ -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<Tag>) -> Result<(), Error> {
async fn post_textnote(
&mut self,
mut content: String,
mut tags: Vec<Tag>,
) -> 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<Tag>,
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 {

View File

@ -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()
}

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::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);

View File

@ -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>,
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(),