diff --git a/apps/desktop2/src/routes/app/home.lazy.tsx b/apps/desktop2/src/routes/app/home.lazy.tsx index e654d418..051b8212 100644 --- a/apps/desktop2/src/routes/app/home.lazy.tsx +++ b/apps/desktop2/src/routes/app/home.lazy.tsx @@ -18,12 +18,7 @@ function Home() { queryKey: ["timeline"], initialPageParam: 0, queryFn: async ({ pageParam }: { pageParam: number }) => { - const events = await ark.get_text_events( - FETCH_LIMIT, - pageParam, - undefined, - true, - ); + const events = await ark.get_text_events(FETCH_LIMIT, pageParam, true); return events; }, getNextPageParam: (lastPage) => { diff --git a/packages/ark/src/ark.ts b/packages/ark/src/ark.ts index bd33cb1e..36155402 100644 --- a/packages/ark/src/ark.ts +++ b/packages/ark/src/ark.ts @@ -109,7 +109,6 @@ export class Ark { public async get_text_events( limit: number, asOf?: number, - authors?: string[], dedup?: boolean, ) { try { @@ -118,12 +117,10 @@ export class Ark { const seenIds = new Set(); const dedupQueue = new Set(); - const contact_list = authors ?? this.account.contacts; - const nostrEvents: Event[] = await invoke("get_text_events", { + const nostrEvents: Event[] = await invoke("get_local_events", { limit, until, - contact_list, }); if (dedup) { diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index e057eee8..98f99956 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -13,7 +13,10 @@ use tauri::Manager; use tauri_plugin_autostart::MacosLauncher; use tokio::sync::Mutex; -pub struct NostrClient(Mutex); +pub struct Nostr { + client: Mutex, + contact_list: Mutex>>, +} fn main() { let mut ctx = tauri::generate_context!(); @@ -58,7 +61,10 @@ fn main() { client.connect().await; // Update global state - handle.manage(NostrClient(Mutex::new(client))) + handle.manage(Nostr { + client: Mutex::new(client), + contact_list: Mutex::new(None), + }) }); Ok(()) @@ -92,8 +98,11 @@ fn main() { nostr::metadata::get_profile, nostr::metadata::get_contact_list, nostr::metadata::create_profile, + nostr::metadata::follow, + nostr::metadata::unfollow, nostr::event::get_event, - nostr::event::get_text_events, + nostr::event::get_local_events, + nostr::event::get_global_events, nostr::event::get_event_thread, nostr::event::publish, nostr::event::reply_to, diff --git a/src-tauri/src/nostr/event.rs b/src-tauri/src/nostr/event.rs index f2cd675f..88aa850e 100644 --- a/src-tauri/src/nostr/event.rs +++ b/src-tauri/src/nostr/event.rs @@ -1,11 +1,11 @@ -use crate::NostrClient; +use crate::Nostr; use nostr_sdk::prelude::*; use std::{str::FromStr, time::Duration}; use tauri::State; #[tauri::command] -pub async fn get_event(id: &str, state: State<'_, NostrClient>) -> Result { - let client = state.0.lock().await; +pub async fn get_event(id: &str, state: State<'_, Nostr>) -> Result { + let client = state.client.lock().await; let event_id: Option = match Nip19::from_bech32(id) { Ok(val) => match val { Nip19::EventId(id) => Some(id), @@ -39,30 +39,25 @@ pub async fn get_event(id: &str, state: State<'_, NostrClient>) -> Result, - contact_list: Option>, - state: State<'_, NostrClient>, -) -> Result, ()> { - let client = state.0.lock().await; + state: State<'_, Nostr>, +) -> Result, String> { + let client = state.client.lock().await; + let f_until = match until { + Some(until) => Timestamp::from_str(until).unwrap(), + None => Timestamp::now(), + }; - if let Some(list) = contact_list { - let authors: Vec = list - .into_iter() - .map(|x| PublicKey::from_str(x).unwrap()) - .collect(); - let mut final_until = Timestamp::now(); - - if let Some(t) = until { - final_until = Timestamp::from_str(&t).unwrap(); - } + let contact_list = state.contact_list.lock().await; + if let Some(authors) = contact_list.clone() { let filter = Filter::new() .kinds(vec![Kind::TextNote, Kind::Repost]) .authors(authors) .limit(limit) - .until(final_until); + .until(f_until); if let Ok(events) = client .get_events_of(vec![filter], Some(Duration::from_secs(10))) @@ -70,16 +65,43 @@ pub async fn get_text_events( { Ok(events) } else { - Err(()) + Err("Get text event failed".into()) } } else { - Err(()) + Err("Contact list not found".into()) } } #[tauri::command] -pub async fn get_event_thread(id: &str, state: State<'_, NostrClient>) -> Result, ()> { - let client = state.0.lock().await; +pub async fn get_global_events( + limit: usize, + until: Option<&str>, + state: State<'_, Nostr>, +) -> Result, String> { + let client = state.client.lock().await; + let f_until = match until { + Some(until) => Timestamp::from_str(until).unwrap(), + None => Timestamp::now(), + }; + + let filter = Filter::new() + .kinds(vec![Kind::TextNote, Kind::Repost]) + .limit(limit) + .until(f_until); + + if let Ok(events) = client + .get_events_of(vec![filter], Some(Duration::from_secs(10))) + .await + { + Ok(events) + } else { + Err("Get text event failed".into()) + } +} + +#[tauri::command] +pub async fn get_event_thread(id: &str, state: State<'_, Nostr>) -> Result, ()> { + let client = state.client.lock().await; let event_id = EventId::from_hex(id).unwrap(); let filter = Filter::new().kinds(vec![Kind::TextNote]).event(event_id); @@ -94,8 +116,8 @@ pub async fn get_event_thread(id: &str, state: State<'_, NostrClient>) -> Result } #[tauri::command] -pub async fn publish(content: &str, state: State<'_, NostrClient>) -> Result { - let client = state.0.lock().await; +pub async fn publish(content: &str, state: State<'_, Nostr>) -> Result { + let client = state.client.lock().await; let event = client .publish_text_note(content, []) .await @@ -108,9 +130,9 @@ pub async fn publish(content: &str, state: State<'_, NostrClient>) -> Result, - state: State<'_, NostrClient>, + state: State<'_, Nostr>, ) -> Result { - let client = state.0.lock().await; + let client = state.client.lock().await; if let Ok(event_tags) = Tag::parse(tags) { let event = client .publish_text_note(content, vec![event_tags]) @@ -124,8 +146,8 @@ pub async fn reply_to( } #[tauri::command] -pub async fn repost(id: &str, pubkey: &str, state: State<'_, NostrClient>) -> Result { - let client = state.0.lock().await; +pub async fn repost(id: &str, pubkey: &str, state: State<'_, Nostr>) -> Result { + let client = state.client.lock().await; let public_key = PublicKey::from_str(pubkey).unwrap(); let event_id = EventId::from_hex(id).unwrap(); @@ -138,8 +160,8 @@ pub async fn repost(id: &str, pubkey: &str, state: State<'_, NostrClient>) -> Re } #[tauri::command] -pub async fn upvote(id: &str, pubkey: &str, state: State<'_, NostrClient>) -> Result { - let client = state.0.lock().await; +pub async fn upvote(id: &str, pubkey: &str, state: State<'_, Nostr>) -> Result { + let client = state.client.lock().await; let public_key = PublicKey::from_str(pubkey).unwrap(); let event_id = EventId::from_hex(id).unwrap(); @@ -152,12 +174,8 @@ pub async fn upvote(id: &str, pubkey: &str, state: State<'_, NostrClient>) -> Re } #[tauri::command] -pub async fn downvote( - id: &str, - pubkey: &str, - state: State<'_, NostrClient>, -) -> Result { - let client = state.0.lock().await; +pub async fn downvote(id: &str, pubkey: &str, state: State<'_, Nostr>) -> Result { + let client = state.client.lock().await; let public_key = PublicKey::from_str(pubkey).unwrap(); let event_id = EventId::from_hex(id).unwrap(); diff --git a/src-tauri/src/nostr/keys.rs b/src-tauri/src/nostr/keys.rs index 7b96e856..441ea446 100644 --- a/src-tauri/src/nostr/keys.rs +++ b/src-tauri/src/nostr/keys.rs @@ -1,4 +1,4 @@ -use crate::NostrClient; +use crate::Nostr; use keyring::Entry; use nostr_sdk::prelude::*; use std::io::{BufReader, Read}; @@ -31,7 +31,7 @@ pub fn create_keys() -> Result { pub async fn save_key( nsec: &str, app_handle: tauri::AppHandle, - state: State<'_, NostrClient>, + state: State<'_, Nostr>, ) -> Result { if let Ok(nostr_secret_key) = SecretKey::from_bech32(nsec) { let nostr_keys = Keys::new(nostr_secret_key); @@ -39,9 +39,19 @@ pub async fn save_key( let signer = NostrSigner::Keys(nostr_keys); // Update client's signer - let client = state.0.lock().await; + let client = state.client.lock().await; client.set_signer(Some(signer)).await; + // Update contact list + let mut contact_list = state.contact_list.lock().await; + if let Ok(list) = client + .get_contact_list_public_keys(Some(Duration::from_secs(10))) + .await + { + println!("total contacts: {}", list.len()); + *contact_list = Some(list); + } + let keyring_entry = Entry::new("Lume Secret Storage", "AppKey").unwrap(); let secret_key = keyring_entry.get_password().unwrap(); let app_key = age::x25519::Identity::from_str(&secret_key).unwrap(); @@ -81,8 +91,8 @@ pub fn get_public_key(nsec: &str) -> Result { } #[tauri::command] -pub async fn update_signer(nsec: &str, state: State<'_, NostrClient>) -> Result<(), ()> { - let client = state.0.lock().await; +pub async fn update_signer(nsec: &str, state: State<'_, Nostr>) -> Result<(), ()> { + let client = state.client.lock().await; let secret_key = SecretKey::from_bech32(nsec).unwrap(); let keys = Keys::new(secret_key); let signer = NostrSigner::Keys(keys); @@ -93,8 +103,8 @@ pub async fn update_signer(nsec: &str, state: State<'_, NostrClient>) -> Result< } #[tauri::command] -pub async fn verify_signer(state: State<'_, NostrClient>) -> Result { - let client = state.0.lock().await; +pub async fn verify_signer(state: State<'_, Nostr>) -> Result { + let client = state.client.lock().await; if let Ok(_) = client.signer().await { Ok(true) @@ -107,9 +117,9 @@ pub async fn verify_signer(state: State<'_, NostrClient>) -> Result { pub async fn load_selected_account( npub: &str, app_handle: tauri::AppHandle, - state: State<'_, NostrClient>, + state: State<'_, Nostr>, ) -> Result { - let client = state.0.lock().await; + let client = state.client.lock().await; let config_dir = app_handle.path().app_config_dir().unwrap(); let keyring_entry = Entry::new("Lume Secret Storage", "AppKey").unwrap(); @@ -146,12 +156,14 @@ pub async fn load_selected_account( client.set_signer(Some(signer)).await; // Update contact list - let _contact_list = Some( - client - .get_contact_list(Some(Duration::from_secs(10))) - .await - .unwrap(), - ); + let mut contact_list = state.contact_list.lock().await; + if let Ok(list) = client + .get_contact_list_public_keys(Some(Duration::from_secs(10))) + .await + { + println!("total contacts: {}", list.len()); + *contact_list = Some(list); + } Ok(true) } else { diff --git a/src-tauri/src/nostr/metadata.rs b/src-tauri/src/nostr/metadata.rs index ec032cc7..ce29547a 100644 --- a/src-tauri/src/nostr/metadata.rs +++ b/src-tauri/src/nostr/metadata.rs @@ -1,11 +1,11 @@ -use crate::NostrClient; +use crate::Nostr; use nostr_sdk::prelude::*; use std::{str::FromStr, time::Duration}; use tauri::State; #[tauri::command] -pub async fn get_profile(id: &str, state: State<'_, NostrClient>) -> Result { - let client = state.0.lock().await; +pub async fn get_profile(id: &str, state: State<'_, Nostr>) -> Result { + let client = state.client.lock().await; let public_key: Option = match Nip19::from_bech32(id) { Ok(val) => match val { Nip19::Pubkey(pubkey) => Some(pubkey), @@ -38,8 +38,8 @@ pub async fn get_profile(id: &str, state: State<'_, NostrClient>) -> Result) -> Result, String> { - let client = state.0.lock().await; +pub async fn get_contact_list(state: State<'_, Nostr>) -> Result, String> { + let client = state.client.lock().await; let contact_list = client.get_contact_list(Some(Duration::from_secs(10))).await; if let Ok(list) = contact_list { @@ -60,9 +60,9 @@ pub async fn create_profile( nip05: &str, lud16: &str, website: &str, - state: State<'_, NostrClient>, + state: State<'_, Nostr>, ) -> Result { - let client = state.0.lock().await; + let client = state.client.lock().await; let metadata = Metadata::new() .name(name) .display_name(display_name) @@ -79,3 +79,53 @@ pub async fn create_profile( Err(()) } } + +#[tauri::command] +pub async fn follow( + id: &str, + alias: Option<&str>, + state: State<'_, Nostr>, +) -> Result { + let client = state.client.lock().await; + let public_key = PublicKey::from_str(id).unwrap(); + let contact = Contact::new(public_key, None, alias); + let contact_list = client.get_contact_list(Some(Duration::from_secs(10))).await; + + if let Ok(mut old_list) = contact_list { + old_list.push(contact); + let new_list = old_list.into_iter(); + + if let Ok(event_id) = client.set_contact_list(new_list).await { + Ok(event_id) + } else { + Err("Follow failed".into()) + } + } else { + Err("Follow failed".into()) + } +} + +#[tauri::command] +pub async fn unfollow(id: &str, state: State<'_, Nostr>) -> Result { + let client = state.client.lock().await; + let public_key = PublicKey::from_str(id).unwrap(); + let contact_list = client.get_contact_list(Some(Duration::from_secs(10))).await; + + if let Ok(mut old_list) = contact_list { + let index = old_list + .iter() + .position(|x| x.public_key == public_key) + .unwrap(); + old_list.remove(index); + + let new_list = old_list.into_iter(); + + if let Ok(event_id) = client.set_contact_list(new_list).await { + Ok(event_id) + } else { + Err("Follow failed".into()) + } + } else { + Err("Follow failed".into()) + } +} diff --git a/src-tauri/src/nostr/relay.rs b/src-tauri/src/nostr/relay.rs index 2f035570..65c8da1e 100644 --- a/src-tauri/src/nostr/relay.rs +++ b/src-tauri/src/nostr/relay.rs @@ -1,10 +1,10 @@ -use crate::NostrClient; +use crate::Nostr; use nostr_sdk::prelude::*; use tauri::State; #[tauri::command] -pub async fn list_connected_relays(state: State<'_, NostrClient>) -> Result, ()> { - let client = state.0.lock().await; +pub async fn list_connected_relays(state: State<'_, Nostr>) -> Result, ()> { + let client = state.client.lock().await; let relays = client.relays().await; let list: Vec = relays.into_keys().collect(); @@ -12,8 +12,8 @@ pub async fn list_connected_relays(state: State<'_, NostrClient>) -> Result) -> Result { - let client = state.0.lock().await; +pub async fn connect_relay(relay: &str, state: State<'_, Nostr>) -> Result { + let client = state.client.lock().await; if let Ok(_) = client.add_relay(relay).await { Ok(true) } else { @@ -22,8 +22,8 @@ pub async fn connect_relay(relay: &str, state: State<'_, NostrClient>) -> Result } #[tauri::command] -pub async fn remove_relay(relay: &str, state: State<'_, NostrClient>) -> Result { - let client = state.0.lock().await; +pub async fn remove_relay(relay: &str, state: State<'_, Nostr>) -> Result { + let client = state.client.lock().await; if let Ok(_) = client.remove_relay(relay).await { Ok(true) } else {