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:
1
src-tauri/Cargo.lock
generated
1
src-tauri/Cargo.lock
generated
@@ -2803,6 +2803,7 @@ dependencies = [
|
||||
"nostr-sdk",
|
||||
"objc",
|
||||
"rand 0.8.5",
|
||||
"regex",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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" }
|
||||
]
|
||||
|
||||
@@ -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(())
|
||||
|
||||
@@ -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()),
|
||||
}
|
||||
|
||||
@@ -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, ()> {
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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::*;
|
||||
|
||||
Reference in New Issue
Block a user