Make Lume Faster (#208)

* chore: fix some lint issues

* feat: refactor contact list

* feat: refactor relay hint

* feat: add missing commands

* feat: use new cache layer for react query

* feat: refactor column

* feat: improve relay hint

* fix: replace break with continue in parser

* refactor: publish function

* feat: add reply command

* feat: improve editor

* fix: quote

* chore: update deps

* refactor: note component

* feat: improve repost

* feat: improve cache

* fix: backup screen

* refactor: column manager
This commit is contained in:
雨宮蓮
2024-06-17 13:52:06 +07:00
committed by GitHub
parent 7c99ed39e4
commit 843895d876
79 changed files with 1738 additions and 1975 deletions

1
src-tauri/Cargo.lock generated
View File

@@ -2803,6 +2803,7 @@ dependencies = [
"nostr-sdk",
"objc",
"rand 0.8.5",
"regex",
"reqwest",
"serde",
"serde_json",

View File

@@ -44,6 +44,7 @@ reqwest = "0.12.4"
url = "2.5.0"
futures = "0.3.30"
linkify = "0.10.0"
regex = "1.10.4"
[target.'cfg(target_os = "macos")'.dependencies]
cocoa = "0.25.0"

View File

@@ -1,7 +1,5 @@
[
{ "label": "onboarding", "name": "Onboarding", "content": "/onboarding" },
{ "label": "lume_newsfeed", "name": "Newsfeed", "content": "/newsfeed" },
{ "label": "lume_topic", "name": "Topic", "content": "/topic" },
{ "label": "lume_group", "name": "Group", "content": "/group" },
{ "label": "open", "name": "Open", "content": "/open" }
{ "label": "lume_topic", "name": "Topic", "content": "/topic" }
]

View File

@@ -3,24 +3,24 @@
windows_subsystem = "windows"
)]
pub mod commands;
pub mod fns;
pub mod nostr;
#[cfg(target_os = "macos")]
extern crate cocoa;
#[cfg(target_os = "macos")]
#[macro_use]
extern crate objc;
use nostr_sdk::prelude::*;
use std::{
fs,
io::{self, BufRead},
str::FromStr,
};
use tauri::{path::BaseDirectory, Manager};
use std::sync::Mutex;
use nostr_sdk::prelude::*;
use serde::Serialize;
use tauri::{Manager, path::BaseDirectory};
#[cfg(target_os = "macos")]
use tauri::tray::{MouseButtonState, TrayIconEvent};
use tauri_nspanel::ManagerExt;
use tauri_plugin_decorum::WebviewWindowExt;
@@ -30,11 +30,15 @@ use crate::fns::{
update_menubar_appearance,
};
#[cfg(target_os = "macos")]
use tauri::tray::{MouseButtonState, TrayIconEvent};
pub mod commands;
pub mod fns;
pub mod nostr;
#[derive(Serialize)]
pub struct Nostr {
#[serde(skip_serializing)]
client: Client,
contact_list: Mutex<Vec<Contact>>,
}
fn main() {
@@ -50,6 +54,7 @@ fn main() {
nostr::keys::create_account,
nostr::keys::save_account,
nostr::keys::get_encrypted_key,
nostr::keys::get_private_key,
nostr::keys::connect_remote_account,
nostr::keys::load_account,
nostr::keys::event_to_bech32,
@@ -60,8 +65,9 @@ fn main() {
nostr::metadata::get_contact_list,
nostr::metadata::set_contact_list,
nostr::metadata::create_profile,
nostr::metadata::follow,
nostr::metadata::unfollow,
nostr::metadata::is_contact_list_empty,
nostr::metadata::check_contact,
nostr::metadata::toggle_contact,
nostr::metadata::get_nstore,
nostr::metadata::set_nstore,
nostr::metadata::set_nwc,
@@ -71,13 +77,17 @@ fn main() {
nostr::metadata::zap_event,
nostr::metadata::friend_to_friend,
nostr::metadata::get_notifications,
nostr::event::get_event_meta,
nostr::event::get_event,
nostr::event::get_event_from,
nostr::event::get_replies,
nostr::event::get_events_by,
nostr::event::get_local_events,
nostr::event::get_group_events,
nostr::event::get_global_events,
nostr::event::get_hashtag_events,
nostr::event::publish,
nostr::event::reply,
nostr::event::repost,
commands::folder::show_in_folder,
commands::window::create_column,
@@ -184,7 +194,10 @@ fn main() {
client.connect().await;
// Update global state
app.handle().manage(Nostr { client })
app.handle().manage(Nostr {
client,
contact_list: Mutex::new(vec![]),
})
});
Ok(())

View File

@@ -7,7 +7,7 @@ use specta::Type;
use tauri::State;
use crate::Nostr;
use crate::nostr::utils::{dedup_event, Meta, parse_event};
use crate::nostr::utils::{create_event_tags, dedup_event, Meta, parse_event};
#[derive(Debug, Serialize, Type)]
pub struct RichEvent {
@@ -15,6 +15,13 @@ pub struct RichEvent {
pub parsed: Option<Meta>,
}
#[tauri::command]
#[specta::specta]
pub async fn get_event_meta(content: &str) -> Result<Meta, ()> {
let meta = parse_event(content).await;
Ok(meta)
}
#[tauri::command]
#[specta::specta]
pub async fn get_event(id: &str, state: State<'_, Nostr>) -> Result<RichEvent, String> {
@@ -22,14 +29,7 @@ pub async fn get_event(id: &str, state: State<'_, Nostr>) -> Result<RichEvent, S
let event_id: Option<EventId> = match Nip19::from_bech32(id) {
Ok(val) => match val {
Nip19::EventId(id) => Some(id),
Nip19::Event(event) => {
let relays = event.relays;
for relay in relays.into_iter() {
let _ = client.add_relay(&relay).await.unwrap_or_default();
client.connect_relay(&relay).await.unwrap_or_default();
}
Some(event.event_id)
}
Nip19::Event(event) => Some(event.event_id),
_ => None,
},
Err(_) => match EventId::from_hex(id) {
@@ -40,10 +40,8 @@ pub async fn get_event(id: &str, state: State<'_, Nostr>) -> Result<RichEvent, S
match event_id {
Some(id) => {
let filter = Filter::new().id(id);
match client
.get_events_of(vec![filter], Some(Duration::from_secs(10)))
.get_events_of(vec![Filter::new().id(id)], Some(Duration::from_secs(10)))
.await
{
Ok(events) => {
@@ -67,6 +65,63 @@ pub async fn get_event(id: &str, state: State<'_, Nostr>) -> Result<RichEvent, S
}
}
#[tauri::command]
#[specta::specta]
pub async fn get_event_from(
id: &str,
relay_hint: &str,
state: State<'_, Nostr>,
) -> Result<RichEvent, String> {
let client = &state.client;
let event_id: Option<EventId> = match Nip19::from_bech32(id) {
Ok(val) => match val {
Nip19::EventId(id) => Some(id),
Nip19::Event(event) => Some(event.event_id),
_ => None,
},
Err(_) => match EventId::from_hex(id) {
Ok(val) => Some(val),
Err(_) => None,
},
};
// Add relay hint to relay pool
if let Err(err) = client.add_relay(relay_hint).await {
return Err(err.to_string());
}
if (client.connect_relay(relay_hint).await).is_ok() {
match event_id {
Some(id) => {
match client
.get_events_from(vec![relay_hint], vec![Filter::new().id(id)], None)
.await
{
Ok(events) => {
if let Some(event) = events.first() {
let raw = event.as_json();
let parsed = if event.kind == Kind::TextNote {
Some(parse_event(&event.content).await)
} else {
None
};
Ok(RichEvent { raw, parsed })
} else {
Err("Cannot found this event with current relay list".into())
}
}
Err(err) => Err(err.to_string()),
}
}
None => Err("Event ID is not valid.".into()),
}
} else {
Err("Relay connection failed.".into())
}
}
#[tauri::command]
#[specta::specta]
pub async fn get_replies(id: &str, state: State<'_, Nostr>) -> Result<Vec<RichEvent>, String> {
@@ -146,25 +201,19 @@ pub async fn get_events_by(
#[tauri::command]
#[specta::specta]
pub async fn get_local_events(
pubkeys: Vec<String>,
until: Option<&str>,
state: State<'_, Nostr>,
) -> Result<Vec<RichEvent>, String> {
let client = &state.client;
let contact_list = state.contact_list.lock().unwrap().clone();
let as_of = match until {
Some(until) => Timestamp::from_str(until).unwrap(),
None => Timestamp::now(),
};
let authors: Vec<PublicKey> = pubkeys
.into_iter()
.map(|p| {
if p.starts_with("npub1") {
PublicKey::from_bech32(p).unwrap()
} else {
PublicKey::from_hex(p).unwrap()
}
})
.collect();
let authors: Vec<PublicKey> = contact_list.into_iter().map(|f| f.public_key).collect();
let filter = Filter::new()
.kinds(vec![Kind::TextNote, Kind::Repost])
.limit(20)
@@ -177,6 +226,7 @@ pub async fn get_local_events(
{
Ok(events) => {
let dedup = dedup_event(&events, false);
let futures = dedup.into_iter().map(|ev| async move {
let raw = ev.as_json();
let parsed = if ev.kind == Kind::TextNote {
@@ -187,6 +237,64 @@ pub async fn get_local_events(
RichEvent { raw, parsed }
});
let rich_events = join_all(futures).await;
Ok(rich_events)
}
Err(err) => Err(err.to_string()),
}
}
#[tauri::command]
#[specta::specta]
pub async fn get_group_events(
public_keys: Vec<&str>,
until: Option<&str>,
state: State<'_, Nostr>,
) -> Result<Vec<RichEvent>, String> {
let client = &state.client;
let as_of = match until {
Some(until) => Timestamp::from_str(until).unwrap(),
None => Timestamp::now(),
};
let authors: Vec<PublicKey> = public_keys
.into_iter()
.map(|p| {
if p.starts_with("npub1") {
PublicKey::from_bech32(p).unwrap()
} else {
PublicKey::from_hex(p).unwrap()
}
})
.collect();
let filter = Filter::new()
.kinds(vec![Kind::TextNote, Kind::Repost])
.limit(20)
.until(as_of)
.authors(authors);
match client
.get_events_of(vec![filter], Some(Duration::from_secs(10)))
.await
{
Ok(events) => {
let dedup = dedup_event(&events, false);
let futures = dedup.into_iter().map(|ev| async move {
let raw = ev.as_json();
let parsed = if ev.kind == Kind::TextNote {
Some(parse_event(&ev.content).await)
} else {
None
};
RichEvent { raw, parsed }
});
let rich_events = join_all(futures).await;
Ok(rich_events)
@@ -278,14 +386,126 @@ pub async fn get_hashtag_events(
#[tauri::command]
#[specta::specta]
pub async fn publish(
content: &str,
tags: Vec<Vec<&str>>,
content: String,
warning: Option<String>,
difficulty: Option<u8>,
state: State<'_, Nostr>,
) -> Result<String, String> {
let client = &state.client;
let event_tags = tags.into_iter().map(|val| Tag::parse(&val).unwrap());
match client.publish_text_note(content, event_tags).await {
// Create tags from content
let mut tags = create_event_tags(&content);
// Add content-warning tag if present
if let Some(reason) = warning {
let t = TagStandard::ContentWarning {
reason: Some(reason),
};
let tag = Tag::from(t);
tags.push(tag)
};
// Get signer
let signer = match client.signer().await {
Ok(signer) => signer,
Err(_) => return Err("Signer is required.".into()),
};
// Get public key
let public_key = signer.public_key().await.unwrap();
// Create unsigned event
let unsigned_event = match difficulty {
Some(num) => EventBuilder::text_note(content, tags).to_unsigned_pow_event(public_key, num),
None => EventBuilder::text_note(content, tags).to_unsigned_event(public_key),
};
// Publish
match signer.sign_event(unsigned_event).await {
Ok(event) => match client.send_event(event).await {
Ok(event_id) => Ok(event_id.to_bech32().unwrap()),
Err(err) => Err(err.to_string()),
},
Err(err) => Err(err.to_string()),
}
}
#[tauri::command]
#[specta::specta]
pub async fn reply(
content: String,
to: String,
root: Option<String>,
state: State<'_, Nostr>,
) -> Result<String, String> {
let client = &state.client;
let database = client.database();
// Create tags from content
let mut tags = create_event_tags(&content);
let reply_id = match EventId::from_hex(to) {
Ok(val) => val,
Err(_) => return Err("Event is not valid.".into()),
};
match database
.query(vec![Filter::new().id(reply_id)], Order::Desc)
.await
{
Ok(events) => {
if let Some(event) = events.into_iter().next() {
let relay_hint =
if let Some(relays) = database.event_seen_on_relays(event.id).await.unwrap() {
relays.into_iter().next().map(UncheckedUrl::new)
} else {
None
};
let t = TagStandard::Event {
event_id: event.id,
relay_url: relay_hint,
marker: Some(Marker::Reply),
public_key: Some(event.pubkey),
};
let tag = Tag::from(t);
tags.push(tag)
} else {
return Err("Reply event is not found.".into());
}
}
Err(err) => return Err(err.to_string()),
};
if let Some(id) = root {
let root_id = match EventId::from_hex(id) {
Ok(val) => val,
Err(_) => return Err("Event is not valid.".into()),
};
if let Ok(events) = database
.query(vec![Filter::new().id(root_id)], Order::Desc)
.await
{
if let Some(event) = events.into_iter().next() {
let relay_hint =
if let Some(relays) = database.event_seen_on_relays(event.id).await.unwrap() {
relays.into_iter().next().map(UncheckedUrl::new)
} else {
None
};
let t = TagStandard::Event {
event_id: event.id,
relay_url: relay_hint,
marker: Some(Marker::Root),
public_key: Some(event.pubkey),
};
let tag = Tag::from(t);
tags.push(tag)
}
}
};
match client.publish_text_note(content, tags).await {
Ok(event_id) => Ok(event_id.to_bech32().unwrap()),
Err(err) => Err(err.to_string()),
}

View File

@@ -1,14 +1,16 @@
use crate::Nostr;
use std::str::FromStr;
use std::time::Duration;
use keyring::Entry;
use keyring_search::{Limit, List, Search};
use nostr_sdk::prelude::*;
use serde::Serialize;
use specta::Type;
use std::str::FromStr;
use std::time::Duration;
use tauri::{EventTarget, Manager, State};
use tauri_plugin_notification::NotificationExt;
use crate::Nostr;
#[derive(Serialize, Type)]
pub struct Account {
npub: String,
@@ -139,22 +141,29 @@ pub async fn load_account(
let signer = client.signer().await.unwrap();
let public_key = signer.public_key().await.unwrap();
let filter = Filter::new()
.author(public_key)
.kind(Kind::RelayList)
.limit(1);
// Get user's contact list
let contacts = client
.get_contact_list(Some(Duration::from_secs(10)))
.await
.unwrap();
// Update state
*state.contact_list.lock().unwrap() = contacts;
// Connect to user's relay (NIP-65)
// #TODO: Let rust-nostr handle it
if let Ok(events) = client
.get_events_of(vec![filter], Some(Duration::from_secs(10)))
.get_events_of(
vec![Filter::new()
.author(public_key)
.kind(Kind::RelayList)
.limit(1)],
Some(Duration::from_secs(10)),
)
.await
{
if let Some(event) = events.first() {
let relay_list = nip65::extract_relay_list(event);
for item in relay_list.into_iter() {
println!("connecting to relay: {} - {:?}", item.0, item.1);
let relay_url = item.0.to_string();
let opts = match item.1 {
Some(val) => {
@@ -164,7 +173,7 @@ pub async fn load_account(
RelayOptions::new().write(true).read(false)
}
}
None => RelayOptions::new(),
None => RelayOptions::default(),
};
// Add relay to relay pool
@@ -175,6 +184,7 @@ pub async fn load_account(
// Connect relay
client.connect_relay(relay_url).await.unwrap_or_default();
println!("connecting to relay: {} - {:?}", item.0, item.1);
}
}
};
@@ -360,6 +370,19 @@ pub fn get_encrypted_key(npub: &str, password: &str) -> Result<String, String> {
}
}
#[tauri::command(async)]
#[specta::specta]
pub fn get_private_key(npub: &str) -> Result<String, String> {
let keyring = Entry::new(npub, "nostr_secret").unwrap();
if let Ok(nsec) = keyring.get_password() {
let secret_key = SecretKey::from_bech32(nsec).unwrap();
Ok(secret_key.to_bech32().unwrap())
} else {
Err("Key not found".into())
}
}
#[tauri::command]
#[specta::specta]
pub fn event_to_bech32(id: &str, relays: Vec<String>) -> Result<String, ()> {

View File

@@ -1,10 +1,13 @@
use super::get_latest_event;
use crate::Nostr;
use std::{str::FromStr, time::Duration};
use keyring::Entry;
use nostr_sdk::prelude::*;
use std::{str::FromStr, time::Duration};
use tauri::State;
use crate::Nostr;
use super::get_latest_event;
#[tauri::command]
#[specta::specta]
pub async fn get_current_user_profile(state: State<'_, Nostr>) -> Result<String, String> {
@@ -50,7 +53,7 @@ pub async fn get_profile(id: &str, state: State<'_, Nostr>) -> Result<String, St
let client = &state.client;
let public_key: Option<PublicKey> = match Nip19::from_bech32(id) {
Ok(val) => match val {
Nip19::Pubkey(pubkey) => Some(pubkey),
Nip19::Pubkey(key) => Some(key),
Nip19::Profile(profile) => {
let relays = profile.relays;
for relay in relays.into_iter() {
@@ -94,9 +97,12 @@ pub async fn get_profile(id: &str, state: State<'_, Nostr>) -> Result<String, St
#[tauri::command]
#[specta::specta]
pub async fn set_contact_list(pubkeys: Vec<&str>, state: State<'_, Nostr>) -> Result<bool, String> {
pub async fn set_contact_list(
public_keys: Vec<&str>,
state: State<'_, Nostr>,
) -> Result<bool, String> {
let client = &state.client;
let contact_list: Vec<Contact> = pubkeys
let contact_list: Vec<Contact> = public_keys
.into_iter()
.filter_map(|p| match PublicKey::from_hex(p) {
Ok(pk) => Some(Contact::new(pk, None, Some(""))),
@@ -174,52 +180,54 @@ pub async fn create_profile(
#[tauri::command]
#[specta::specta]
pub async fn follow(
id: &str,
alias: Option<&str>,
state: State<'_, Nostr>,
) -> Result<String, String> {
let client = &state.client;
let public_key = match PublicKey::from_str(id) {
Ok(pk) => pk,
Err(_) => return Err("Invalid public key.".into()),
};
let contact = Contact::new(public_key, None, alias); // TODO: Add relay_url
let contact_list_result = client.get_contact_list(Some(Duration::from_secs(10))).await;
pub async fn is_contact_list_empty(state: State<'_, Nostr>) -> Result<bool, ()> {
let contact_list = state.contact_list.lock().unwrap();
Ok(contact_list.is_empty())
}
match contact_list_result {
Ok(mut old_list) => {
old_list.push(contact);
let new_list = old_list.into_iter();
#[tauri::command]
#[specta::specta]
pub async fn check_contact(hex: &str, state: State<'_, Nostr>) -> Result<bool, ()> {
let contact_list = state.contact_list.lock().unwrap();
let public_key = PublicKey::from_str(hex).unwrap();
match client.set_contact_list(new_list).await {
Ok(event_id) => Ok(event_id.to_string()),
Err(err) => Err(err.to_string()),
}
}
Err(err) => Err(err.to_string()),
match contact_list.iter().position(|x| x.public_key == public_key) {
Some(_) => Ok(true),
None => Ok(false),
}
}
#[tauri::command]
#[specta::specta]
pub async fn unfollow(id: &str, state: State<'_, Nostr>) -> Result<String, String> {
pub async fn toggle_contact(
hex: &str,
alias: Option<&str>,
state: State<'_, Nostr>,
) -> Result<String, String> {
let client = &state.client;
let public_key = match PublicKey::from_str(id) {
Ok(pk) => pk,
Err(_) => return Err("Invalid public key.".into()),
};
let contact_list_result = client.get_contact_list(Some(Duration::from_secs(10))).await;
match client.get_contact_list(None).await {
Ok(mut contact_list) => {
let public_key = PublicKey::from_str(hex).unwrap();
match contact_list_result {
Ok(old_list) => {
let contacts: Vec<Contact> = old_list
.into_iter()
.filter(|contact| contact.public_key != public_key)
.collect();
match contact_list.iter().position(|x| x.public_key == public_key) {
Some(index) => {
// Remove contact
contact_list.remove(index);
}
None => {
// TODO: Add relay_url
let new_contact = Contact::new(public_key, None, alias);
// Add new contact
contact_list.push(new_contact);
}
}
match client.set_contact_list(contacts).await {
// Update local state
state.contact_list.lock().unwrap().clone_from(&contact_list);
// Publish
match client.set_contact_list(contact_list).await {
Ok(event_id) => Ok(event_id.to_string()),
Err(err) => Err(err.to_string()),
}
@@ -365,7 +373,7 @@ pub async fn zap_profile(
let client = &state.client;
let public_key: Option<PublicKey> = match Nip19::from_bech32(id) {
Ok(val) => match val {
Nip19::Pubkey(pubkey) => Some(pubkey),
Nip19::Pubkey(key) => Some(key),
Nip19::Profile(profile) => Some(profile.public_key),
_ => None,
},

View File

@@ -2,7 +2,8 @@ use std::collections::HashSet;
use std::str::FromStr;
use linkify::LinkFinder;
use nostr_sdk::{Alphabet, Event, SingleLetterTag, Tag, TagKind};
use nostr_sdk::{Alphabet, Event, EventId, FromBech32, PublicKey, SingleLetterTag, Tag, TagKind};
use nostr_sdk::prelude::Nip19Event;
use reqwest::Client;
use serde::Serialize;
use specta::Type;
@@ -79,21 +80,26 @@ pub fn dedup_event(events: &[Event], nsfw: bool) -> Vec<Event> {
}
pub async fn parse_event(content: &str) -> Meta {
let words: Vec<_> = content.split_whitespace().collect();
let mut finder = LinkFinder::new();
finder.url_must_have_scheme(false);
// Get urls
let urls: Vec<_> = finder.links(content).collect();
// Get words
let words: Vec<_> = content.split_whitespace().collect();
let hashtags = words
.iter()
.filter(|&&word| word.starts_with('#'))
.map(|&s| s.to_string())
.collect::<Vec<_>>();
let events = words
.iter()
.filter(|&&word| NOSTR_EVENTS.iter().any(|&el| word.starts_with(el)))
.map(|&s| s.to_string())
.collect::<Vec<_>>();
let mentions = words
.iter()
.filter(|&&word| NOSTR_MENTIONS.iter().any(|&el| word.starts_with(el)))
@@ -118,12 +124,14 @@ pub async fn parse_event(content: &str) -> Meta {
if IMAGES.contains(&ext) {
text = text.replace(url_str, "");
images.push(url_str.to_string());
break;
// Process the next item.
continue;
}
if VIDEOS.contains(&ext) {
text = text.replace(url_str, "");
videos.push(url_str.to_string());
break;
// Process the next item.
continue;
}
}
@@ -133,7 +141,8 @@ pub async fn parse_event(content: &str) -> Meta {
if content_type.to_str().unwrap_or("").starts_with("image") {
text = text.replace(url_str, "");
images.push(url_str.to_string());
break;
// Process the next item.
continue;
}
}
}
@@ -154,6 +163,87 @@ pub async fn parse_event(content: &str) -> Meta {
}
}
pub fn create_event_tags(content: &str) -> Vec<Tag> {
let mut tags: Vec<Tag> = vec![];
let mut tag_set: HashSet<String> = HashSet::new();
// Get words
let words: Vec<_> = content.split_whitespace().collect();
// Get mentions
let mentions = words
.iter()
.filter(|&&word| ["nostr:", "@"].iter().any(|&el| word.starts_with(el)))
.map(|&s| s.to_string())
.collect::<Vec<_>>();
// Get hashtags
let hashtags = words
.iter()
.filter(|&&word| word.starts_with('#'))
.map(|&s| s.to_string())
.collect::<Vec<_>>();
for mention in mentions {
let entity = mention.replace("nostr:", "").replace("@", "");
if !tag_set.contains(&entity) {
if entity.starts_with("npub") {
if let Ok(public_key) = PublicKey::from_bech32(&entity) {
let tag = Tag::public_key(public_key);
tags.push(tag);
} else {
continue;
}
}
if entity.starts_with("nprofile") {
if let Ok(public_key) = PublicKey::from_bech32(&entity) {
let tag = Tag::public_key(public_key);
tags.push(tag);
} else {
continue;
}
}
if entity.starts_with("note") {
if let Ok(event_id) = EventId::from_bech32(&entity) {
let hex = event_id.to_hex();
let tag = Tag::parse(&["e", &hex, "", "mention"]).unwrap();
tags.push(tag);
} else {
continue;
}
}
if entity.starts_with("nevent") {
if let Ok(event) = Nip19Event::from_bech32(&entity) {
let hex = event.event_id.to_hex();
let relay = event.clone().relays.into_iter().next().unwrap_or("".into());
let tag = Tag::parse(&["e", &hex, &relay, "mention"]).unwrap();
if let Some(author) = event.author {
let tag = Tag::public_key(author);
tags.push(tag);
}
tags.push(tag);
} else {
continue;
}
}
tag_set.insert(entity);
}
}
for hashtag in hashtags {
if !tag_set.contains(&hashtag) {
let tag = Tag::hashtag(hashtag.clone());
tags.push(tag);
tag_set.insert(hashtag);
}
}
tags
}
#[cfg(test)]
mod tests {
use super::*;