diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 1efea5c9..20e8fdf3 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -3081,7 +3081,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" dependencies = [ "cfg-if", - "windows-targets 0.52.6", + "windows-targets 0.48.5", ] [[package]] @@ -3480,7 +3480,7 @@ checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" [[package]] name = "nostr" version = "0.35.0" -source = "git+https://github.com/rust-nostr/nostr#a398775dd1013482f785816e6b9fe99f7668416a" +source = "git+https://github.com/rust-nostr/nostr#370d044e5b0005b534dc82b48fe69acebac02d0f" dependencies = [ "aes", "base64 0.22.1", @@ -3510,7 +3510,7 @@ dependencies = [ [[package]] name = "nostr-database" version = "0.35.0" -source = "git+https://github.com/rust-nostr/nostr#a398775dd1013482f785816e6b9fe99f7668416a" +source = "git+https://github.com/rust-nostr/nostr#370d044e5b0005b534dc82b48fe69acebac02d0f" dependencies = [ "async-trait", "flatbuffers", @@ -3524,7 +3524,7 @@ dependencies = [ [[package]] name = "nostr-lmdb" version = "0.35.0" -source = "git+https://github.com/rust-nostr/nostr#a398775dd1013482f785816e6b9fe99f7668416a" +source = "git+https://github.com/rust-nostr/nostr#370d044e5b0005b534dc82b48fe69acebac02d0f" dependencies = [ "heed", "nostr", @@ -3537,7 +3537,7 @@ dependencies = [ [[package]] name = "nostr-relay-pool" version = "0.35.0" -source = "git+https://github.com/rust-nostr/nostr#a398775dd1013482f785816e6b9fe99f7668416a" +source = "git+https://github.com/rust-nostr/nostr#370d044e5b0005b534dc82b48fe69acebac02d0f" dependencies = [ "async-utility", "async-wsocket", @@ -3555,7 +3555,7 @@ dependencies = [ [[package]] name = "nostr-sdk" version = "0.35.0" -source = "git+https://github.com/rust-nostr/nostr#a398775dd1013482f785816e6b9fe99f7668416a" +source = "git+https://github.com/rust-nostr/nostr#370d044e5b0005b534dc82b48fe69acebac02d0f" dependencies = [ "async-utility", "atomic-destructor", @@ -3575,7 +3575,7 @@ dependencies = [ [[package]] name = "nostr-signer" version = "0.35.0" -source = "git+https://github.com/rust-nostr/nostr#a398775dd1013482f785816e6b9fe99f7668416a" +source = "git+https://github.com/rust-nostr/nostr#370d044e5b0005b534dc82b48fe69acebac02d0f" dependencies = [ "async-utility", "nostr", @@ -3588,7 +3588,7 @@ dependencies = [ [[package]] name = "nostr-zapper" version = "0.35.0" -source = "git+https://github.com/rust-nostr/nostr#a398775dd1013482f785816e6b9fe99f7668416a" +source = "git+https://github.com/rust-nostr/nostr#370d044e5b0005b534dc82b48fe69acebac02d0f" dependencies = [ "async-trait", "nostr", @@ -3732,7 +3732,7 @@ dependencies = [ [[package]] name = "nwc" version = "0.35.0" -source = "git+https://github.com/rust-nostr/nostr#a398775dd1013482f785816e6b9fe99f7668416a" +source = "git+https://github.com/rust-nostr/nostr#370d044e5b0005b534dc82b48fe69acebac02d0f" dependencies = [ "async-utility", "nostr", @@ -7223,7 +7223,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.48.0", ] [[package]] diff --git a/src-tauri/resources/relays.txt b/src-tauri/resources/relays.txt index 99163654..11c3ec13 100644 --- a/src-tauri/resources/relays.txt +++ b/src-tauri/resources/relays.txt @@ -1,4 +1,3 @@ wss://relay.damus.io, -wss://relay.nostr.net, wss://relay.primal.net, wss://nostr.fmt.wiz.biz, diff --git a/src-tauri/src/commands/event.rs b/src-tauri/src/commands/event.rs index dbc28977..c1328ff3 100644 --- a/src-tauri/src/commands/event.rs +++ b/src-tauri/src/commands/event.rs @@ -31,7 +31,28 @@ pub async fn get_event(id: String, state: State<'_, Nostr>) -> Result { + if let Some(event) = events.iter().next() { + 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("Event not found.".into()) + } + } + Err(err) => Err(err.to_string()), + } } } Err(err) => Err(err.to_string()), @@ -49,13 +70,16 @@ pub async fn get_meta_from_event(content: String) -> Result { pub async fn get_replies(id: String, state: State<'_, Nostr>) -> Result, String> { let client = &state.client; let event_id = EventId::parse(&id).map_err(|err| err.to_string())?; - let filter = Filter::new().kinds(vec![Kind::TextNote]).event(event_id); + + let filter = Filter::new() + .kinds(vec![Kind::TextNote, Kind::Custom(1111)]) + .event(event_id); match client .fetch_events(vec![filter], Some(Duration::from_secs(5))) .await { - Ok(events) => Ok(process_event(client, events).await), + Ok(events) => Ok(process_event(client, events, true).await), Err(err) => Err(err.to_string()), } } @@ -98,7 +122,7 @@ pub async fn get_all_events_by_author( .limit(limit as usize); match client.database().query(vec![filter]).await { - Ok(events) => Ok(process_event(client, events).await), + Ok(events) => Ok(process_event(client, events, false).await), Err(err) => Err(err.to_string()), } } @@ -129,7 +153,7 @@ pub async fn get_all_events_by_authors( .authors(authors); match client.database().query(vec![filter]).await { - Ok(events) => Ok(process_event(client, events).await), + Ok(events) => Ok(process_event(client, events, false).await), Err(err) => Err(err.to_string()), } } @@ -158,7 +182,7 @@ pub async fn get_all_events_by_hashtags( .fetch_events(vec![filter], Some(Duration::from_secs(5))) .await { - Ok(events) => Ok(process_event(client, events).await), + Ok(events) => Ok(process_event(client, events, false).await), Err(err) => Err(err.to_string()), } } @@ -182,7 +206,7 @@ pub async fn get_local_events( .until(as_of); match client.database().query(vec![filter]).await { - Ok(events) => Ok(process_event(client, events).await), + Ok(events) => Ok(process_event(client, events, false).await), Err(err) => Err(err.to_string()), } } @@ -205,11 +229,8 @@ pub async fn get_global_events( .limit(FETCH_LIMIT) .until(as_of); - match client - .fetch_events(vec![filter], Some(Duration::from_secs(5))) - .await - { - Ok(events) => Ok(process_event(client, events).await), + match client.database().query(vec![filter]).await { + Ok(events) => Ok(process_event(client, events, false).await), Err(err) => Err(err.to_string()), } } @@ -334,7 +355,13 @@ pub async fn is_reposted(id: String, state: State<'_, Nostr>) -> Result = accounts .iter() - .map(|acc| PublicKey::from_str(acc).unwrap()) + .filter_map(|acc| { + if let Ok(pk) = PublicKey::from_str(acc) { + Some(pk) + } else { + None + } + }) .collect(); let filter = Filter::new() @@ -452,7 +479,7 @@ pub async fn search(query: String, state: State<'_, Nostr>) -> Result Ok(process_event(client, events).await), + Ok(events) => Ok(process_event(client, events, false).await), Err(e) => Err(e.to_string()), } } diff --git a/src-tauri/src/commands/metadata.rs b/src-tauri/src/commands/metadata.rs index e94171cf..de8f89e2 100644 --- a/src-tauri/src/commands/metadata.rs +++ b/src-tauri/src/commands/metadata.rs @@ -36,8 +36,34 @@ pub async fn get_profile(id: String, state: State<'_, Nostr>) -> Result Ok(profile.metadata().as_json()), + let filter = Filter::new() + .author(public_key) + .kind(Kind::Metadata) + .limit(1); + + match client.database().query(vec![filter.clone()]).await { + Ok(events) => { + if let Some(event) = events.iter().next() { + let metadata = Metadata::from_json(&event.content).map_err(|e| e.to_string())?; + Ok(metadata.as_json()) + } else { + match client + .fetch_events(vec![filter], Some(Duration::from_secs(5))) + .await + { + Ok(events) => { + if let Some(event) = events.iter().next() { + let metadata = + Metadata::from_json(&event.content).map_err(|e| e.to_string())?; + Ok(metadata.as_json()) + } else { + Err("Profile not found.".into()) + } + } + Err(err) => Err(err.to_string()), + } + } + } Err(e) => Err(e.to_string()), } } @@ -203,7 +229,13 @@ pub async fn set_group( let client = &state.client; let public_keys: Vec = users .iter() - .map(|u| PublicKey::from_str(u).unwrap()) + .filter_map(|u| { + if let Ok(pk) = PublicKey::from_str(u) { + Some(pk) + } else { + None + } + }) .collect(); let label = title.to_lowercase().replace(" ", "-"); let mut tags: Vec = vec![Tag::title(title)]; @@ -237,7 +269,7 @@ pub async fn set_group( .authors(public_keys) .limit(500); - if let Ok(report) = client.sync(filter, SyncOptions::default()).await { + if let Ok(report) = client.sync(filter, &SyncOptions::default()).await { println!("Received: {}", report.received.len()); handle.emit("synchronized", ()).unwrap(); }; @@ -283,7 +315,7 @@ pub async fn get_all_groups(state: State<'_, Nostr>) -> Result, S let filter = Filter::new().kind(Kind::FollowSet).authors(authors); match client.database().query(vec![filter]).await { - Ok(events) => Ok(process_event(client, events).await), + Ok(events) => Ok(process_event(client, events, false).await), Err(e) => Err(e.to_string()), } } @@ -331,7 +363,7 @@ pub async fn set_interest( .hashtags(hashtags) .limit(500); - if let Ok(report) = client.sync(filter, SyncOptions::default()).await { + if let Ok(report) = client.sync(filter, &SyncOptions::default()).await { println!("Received: {}", report.received.len()); handle.emit("synchronized", ()).unwrap(); }; @@ -381,7 +413,7 @@ pub async fn get_all_interests(state: State<'_, Nostr>) -> Result .authors(authors); match client.database().query(vec![filter]).await { - Ok(events) => Ok(process_event(client, events).await), + Ok(events) => Ok(process_event(client, events, false).await), Err(e) => Err(e.to_string()), } } @@ -560,14 +592,20 @@ pub async fn get_notifications(id: String, state: State<'_, Nostr>) -> Result Ok(events.into_iter().map(|ev| ev.as_json()).collect()), Err(err) => Err(err.to_string()), } diff --git a/src-tauri/src/commands/sync.rs b/src-tauri/src/commands/sync.rs index 2ed2d3f0..bfbb02a2 100644 --- a/src-tauri/src/commands/sync.rs +++ b/src-tauri/src/commands/sync.rs @@ -2,6 +2,7 @@ use nostr_sdk::prelude::*; use serde::{Deserialize, Serialize}; use specta::Type; use std::{ + collections::HashSet, fs::{self, File}, str::FromStr, }; @@ -52,20 +53,18 @@ pub fn sync_all(accounts: Vec, app_handle: AppHandle) { // if let Ok(events) = client .database() - .query(vec![Filter::new().kinds(vec![ - Kind::ContactList, - Kind::FollowSet, - Kind::MuteList, - Kind::Repost, - Kind::TextNote, - ])]) + .query(vec![Filter::new() + .authors(public_keys) + .kinds(vec![Kind::ContactList, Kind::FollowSet])]) .await { - let pubkeys: Vec = events + let set: HashSet = events .iter() .flat_map(|ev| ev.tags.public_keys().copied()) .collect(); + let pubkeys: Vec = set.into_iter().collect(); + for chunk in pubkeys.chunks(500) { if chunk.is_empty() { break; @@ -78,10 +77,10 @@ pub fn sync_all(accounts: Vec, app_handle: AppHandle) { let events = Filter::new() .authors(authors.clone()) .kinds(vec![Kind::TextNote, Kind::Repost]) - .limit(1000); + .limit(500); if let Ok(output) = client - .sync_with(&bootstrap_relays, events, SyncOptions::default()) + .sync_with(&bootstrap_relays, events, &SyncOptions::default()) .await { NegentropyEvent { @@ -94,21 +93,20 @@ pub fn sync_all(accounts: Vec, app_handle: AppHandle) { // NEG: Sync metadata // - let metadata = Filter::new() - .authors(authors) + let events = Filter::new() + .authors(authors.clone()) .kinds(vec![ Kind::Metadata, - Kind::ContactList, - Kind::Interests, Kind::InterestSet, + Kind::Interests, Kind::FollowSet, - Kind::MuteList, - Kind::RelaySet, + Kind::EventDeletion, + Kind::Custom(30315), ]) - .limit(1000); + .limit(500); if let Ok(output) = client - .sync_with(&bootstrap_relays, metadata, SyncOptions::default()) + .sync_with(&bootstrap_relays, events, &SyncOptions::default()) .await { NegentropyEvent { @@ -120,62 +118,6 @@ pub fn sync_all(accounts: Vec, app_handle: AppHandle) { } } } - - // NEG: Sync notification - // - let notification = Filter::new() - .pubkeys(public_keys.clone()) - .kinds(vec![ - Kind::TextNote, - Kind::Repost, - Kind::Reaction, - Kind::ZapReceipt, - ]) - .limit(500); - - if let Ok(output) = client - .sync_with(&bootstrap_relays, notification, SyncOptions::default()) - .await - { - NegentropyEvent { - kind: NegentropyKind::Notification, - total_event: output.received.len() as i32, - } - .emit(&app_handle) - .unwrap(); - } - - // NEG: Sync metadata - // - let metadata = Filter::new().authors(public_keys.clone()).kinds(vec![ - Kind::Metadata, - Kind::ContactList, - Kind::Interests, - Kind::InterestSet, - Kind::FollowSet, - Kind::RelayList, - Kind::MuteList, - Kind::EventDeletion, - Kind::Bookmarks, - Kind::BookmarkSet, - Kind::Emojis, - Kind::EmojiSet, - Kind::TextNote, - Kind::Repost, - Kind::Custom(30315), - ]); - - if let Ok(output) = client - .sync_with(&bootstrap_relays, metadata, SyncOptions::default()) - .await - { - NegentropyEvent { - kind: NegentropyKind::Others, - total_event: output.received.len() as i32, - } - .emit(&app_handle) - .unwrap(); - } }); } @@ -235,10 +177,7 @@ pub async fn sync_account( } }); - if let Ok(output) = client - .sync_with(&bootstrap_relays, filter, opts.clone()) - .await - { + if let Ok(output) = client.sync_with(&bootstrap_relays, filter, &opts).await { println!("Success: {:?}", output.success); println!("Failed: {:?}", output.failed); @@ -279,37 +218,13 @@ pub async fn sync_account( ]) .limit(10000); - if let Ok(output) = client - .sync_with(&bootstrap_relays, filter, opts.clone()) - .await - { + if let Ok(output) = client.sync_with(&bootstrap_relays, filter, &opts).await { println!("Success: {:?}", output.success); println!("Failed: {:?}", output.failed); } }; } - let event_ids = client - .database() - .query(vec![Filter::new().kinds(vec![ - Kind::TextNote, - Kind::Repost, - Kind::Bookmarks, - Kind::BookmarkSet, - ])]) - .await - .map_err(|e| e.to_string())?; - - if !event_ids.is_empty() { - let ids: Vec = event_ids.iter().map(|ev| ev.id).collect(); - let filter = Filter::new().events(ids); - - if let Ok(output) = client.sync_with(&bootstrap_relays, filter, opts).await { - println!("Success: {:?}", output.success); - println!("Failed: {:?}", output.failed); - } - } - let config_dir = app_handle .path() .app_config_dir() diff --git a/src-tauri/src/commands/window.rs b/src-tauri/src/commands/window.rs index 92c5b3bc..3ea7e866 100644 --- a/src-tauri/src/commands/window.rs +++ b/src-tauri/src/commands/window.rs @@ -10,6 +10,7 @@ use tauri::window::Effect; use tauri::TitleBarStyle; use tauri::{LogicalPosition, LogicalSize, Manager, WebviewUrl}; use tauri::{WebviewBuilder, WebviewWindowBuilder}; +#[cfg(target_os = "windows")] use tauri_plugin_decorum::WebviewWindowExt; #[derive(Serialize, Deserialize, Type)] @@ -35,9 +36,9 @@ pub struct Column { height: f32, } -#[tauri::command(async)] +#[tauri::command] #[specta::specta] -pub fn create_column(column: Column, app_handle: tauri::AppHandle) -> Result { +pub async fn create_column(column: Column, app_handle: tauri::AppHandle) -> Result { match app_handle.get_window("main") { Some(main_window) => match app_handle.get_webview(&column.label) { Some(_) => Ok(column.label), @@ -63,18 +64,18 @@ pub fn create_column(column: Column, app_handle: tauri::AppHandle) -> Result Result { +pub async fn close_column(label: String, app_handle: tauri::AppHandle) -> Result { match app_handle.get_webview(&label) { Some(webview) => Ok(webview.close().is_ok()), None => Err("Cannot close, column not found.".into()), } } -#[tauri::command(async)] +#[tauri::command] #[specta::specta] -pub fn update_column( +pub async fn update_column( label: String, width: f32, height: f32, @@ -98,9 +99,9 @@ pub fn update_column( } } -#[tauri::command(async)] +#[tauri::command] #[specta::specta] -pub fn reload_column(label: String, app_handle: tauri::AppHandle) -> Result<(), String> { +pub async fn reload_column(label: String, app_handle: tauri::AppHandle) -> Result<(), String> { match app_handle.get_webview(&label) { Some(webview) => { webview.eval("location.reload(true)").unwrap(); @@ -124,6 +125,7 @@ pub fn open_window(window: Window, app_handle: tauri::AppHandle) -> Result Result Result { - if let Some(current_window) = app_handle.get_window(&window.label) { - if current_window.is_visible().unwrap_or_default() { - let _ = current_window.set_focus(); - } else { - let _ = current_window.show(); - let _ = current_window.set_focus(); - }; - - Ok(current_window.label().to_string()) - } else { + #[cfg(target_os = "windows")] let new_window = WebviewWindowBuilder::new( &app_handle, &window.label, @@ -190,58 +172,14 @@ pub fn open_window(window: Window, app_handle: tauri::AppHandle) -> Result Vec { accounts.into_iter().collect() } -pub async fn process_event(client: &Client, events: Events) -> Vec { +pub async fn process_event(client: &Client, events: Events, is_reply: bool) -> Vec { // Remove event thread if event is TextNote - let events: Vec = events - .into_iter() - .filter_map(|ev| { - if ev.kind == Kind::TextNote { - let tags = ev - .tags - .iter() - .filter(|t| t.is_reply() || t.is_root()) - .filter_map(|t| t.content()) - .collect::>(); + let events: Vec = if !is_reply { + events + .into_iter() + .filter_map(|ev| { + if ev.kind == Kind::TextNote { + let tags = ev + .tags + .iter() + .filter(|t| t.is_reply() || t.is_root()) + .filter_map(|t| t.content()) + .collect::>(); - if tags.is_empty() { - Some(ev) + if tags.is_empty() { + Some(ev) + } else { + None + } } else { - None + Some(ev) } - } else { - Some(ev) - } - }) - .collect(); + }) + .collect() + } else { + events.into_iter().collect() + }; // Get deletion request by event's authors let ids: Vec = events.iter().map(|ev| ev.id).collect(); diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 85aee589..047ef217 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -156,8 +156,6 @@ fn main() { reload_column, close_column, open_window, - reopen_lume, - quit ]) .events(collect_events![Subscription, NegentropyEvent]); @@ -207,12 +205,12 @@ fn main() { // Config let opts = Options::new() .gossip(true) - .max_avg_latency(Duration::from_millis(800)) + .max_avg_latency(Duration::from_millis(500)) .automatic_authentication(false) .connection_timeout(Some(Duration::from_secs(20))) .send_timeout(Some(Duration::from_secs(10))) .wait_for_send(false) - .timeout(Duration::from_secs(20)); + .timeout(Duration::from_secs(12)); // Setup nostr client let client = ClientBuilder::default() @@ -370,29 +368,31 @@ fn main() { tauri::async_runtime::spawn(async move { let state = handle_clone.state::(); let client = &state.client; - let accounts = state.accounts.lock().unwrap().clone(); + let accounts = get_all_accounts(); - let public_keys: Vec = accounts - .iter() - .filter_map(|acc| { - if let Ok(pk) = PublicKey::from_str(acc) { - Some(pk) - } else { - None - } - }) - .collect(); + if !accounts.is_empty() { + let public_keys: Vec = accounts + .iter() + .filter_map(|acc| { + if let Ok(pk) = PublicKey::from_str(acc) { + Some(pk) + } else { + None + } + }) + .collect(); - // Subscribe for new notification - if let Ok(e) = client - .subscribe_with_id( - SubscriptionId::new(NOTIFICATION_SUB_ID), - vec![Filter::new().pubkeys(public_keys).since(Timestamp::now())], - None, - ) - .await - { - println!("Subscribed for notification on {} relays", e.success.len()) + // Subscribe for new notification + if let Ok(e) = client + .subscribe_with_id( + SubscriptionId::new(NOTIFICATION_SUB_ID), + vec![Filter::new().pubkeys(public_keys).since(Timestamp::now())], + None, + ) + .await + { + println!("Subscribed for notification on {} relays", e.success.len()) + } } let allow_notification = match handle_clone.notification().request_permission() { @@ -408,7 +408,6 @@ fn main() { let notification_id = SubscriptionId::new(NOTIFICATION_SUB_ID); let mut notifications = client.pool().notifications(); - let mut new_events: Vec = Vec::new(); while let Ok(notification) = notifications.recv().await { match notification { @@ -426,17 +425,6 @@ fn main() { println!("Error: {}", e); } - // Workaround for https://github.com/rust-nostr/nostr/issues/509 - // TODO: remove - let _ = client - .fetch_events( - vec![Filter::new() - .kind(Kind::TextNote) - .limit(0)], - Some(Duration::from_secs(5)), - ) - .await; - if allow_notification { if let Err(e) = &handle_clone .notification() @@ -469,22 +457,8 @@ fn main() { event, } = message { - let tags: Vec = event - .tags - .public_keys() - .filter_map(|pk| { - if let Ok(bech32) = pk.to_bech32() { - Some(bech32) - } else { - None - } - }) - .collect(); - // Handle events from notification subscription - if subscription_id == notification_id - && tags.iter().any(|item| accounts.iter().any(|i| i == item)) - { + if subscription_id == notification_id { // Send native notification if allow_notification { let author = client @@ -501,37 +475,22 @@ fn main() { &handle_clone, ); } - } + } else { + let payload = RichEvent { + raw: event.as_json(), + parsed: if event.kind == Kind::TextNote { + Some(parse_event(&event.content).await) + } else { + None + }, + }; - let payload = RichEvent { - raw: event.as_json(), - parsed: if event.kind == Kind::TextNote { - Some(parse_event(&event.content).await) - } else { - None - }, - }; - - handle_clone - .emit_to( + if let Err(e) = handle_clone.emit_to( EventTarget::labeled(subscription_id.to_string()), "event", payload, - ) - .unwrap(); - - if state - .subscriptions - .lock() - .unwrap() - .iter() - .any(|i| i == &subscription_id) - { - new_events.push(event.id); - - if new_events.len() > 5 { - handle_clone.emit("synchronized", ()).unwrap(); - new_events.clear(); + ) { + println!("Emitter error: {}", e) } } }; diff --git a/src/app.tsx b/src/app.tsx index 71a25a2f..afb03f84 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -1,31 +1,24 @@ -import { experimental_createPersister } from "@tanstack/query-persist-client-core"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { RouterProvider, createRouter } from "@tanstack/react-router"; import { type } from "@tauri-apps/plugin-os"; -import { createStore } from "@tauri-apps/plugin-store"; import { StrictMode } from "react"; import ReactDOM from "react-dom/client"; -import { newQueryStorage } from "./commons"; import type { LumeEvent } from "./system"; import { routeTree } from "./routes.gen"; // auto generated file import "./app.css"; // global styles -const platform = type(); -// @ts-expect-error, required: https://github.com/tauri-apps/plugins-workspace/pull/1860 -const store = await createStore(".cache", { autoSave: 250 }); -const queryClient = new QueryClient({ - defaultOptions: { - queries: { - gcTime: 1000 * 30, - persister: experimental_createPersister({ - storage: newQueryStorage(store), - maxAge: 1000 * 60 * 60 * 12, - }), - }, - }, -}); +declare module "@tanstack/react-router" { + interface Register { + router: typeof router; + } + interface HistoryState { + events?: LumeEvent[]; + } +} +const platform = type(); +const queryClient = new QueryClient(); const router = createRouter({ routeTree, context: { queryClient, platform }, @@ -36,28 +29,11 @@ const router = createRouter({ }, }); -// Register things for typesafety -declare module "@tanstack/react-router" { - interface Register { - router: typeof router; - } - interface HistoryState { - events?: LumeEvent[]; - } -} +const rootElement = document.getElementById("root"); +const root = ReactDOM.createRoot(rootElement as unknown as HTMLElement); -function App() { - return ; -} - -// biome-ignore lint/style/noNonNullAssertion: idk -const rootElement = document.getElementById("root")!; - -if (!rootElement.innerHTML) { - const root = ReactDOM.createRoot(rootElement); - root.render( - - - , - ); -} +root.render( + + + , +); diff --git a/src/commands.gen.ts b/src/commands.gen.ts index d284a033..7e79b36f 100644 --- a/src/commands.gen.ts +++ b/src/commands.gen.ts @@ -495,12 +495,6 @@ async openWindow(window: Window) : Promise> { if(e instanceof Error) throw e; else return { status: "error", error: e as any }; } -}, -async reopenLume() : Promise { - await TAURI_INVOKE("reopen_lume"); -}, -async quit() : Promise { - await TAURI_INVOKE("quit"); } } diff --git a/src/components/column.tsx b/src/components/column.tsx index d37e4c8e..5fcbc3e4 100644 --- a/src/components/column.tsx +++ b/src/components/column.tsx @@ -4,14 +4,20 @@ import type { LumeColumn } from "@/types"; import { CaretDown, Check } from "@phosphor-icons/react"; import { Menu, MenuItem, PredefinedMenuItem } from "@tauri-apps/api/menu"; import { getCurrentWindow } from "@tauri-apps/api/window"; -import { useCallback, useEffect, useMemo, useState } from "react"; +import { + useCallback, + useEffect, + useLayoutEffect, + useMemo, + useState, +} from "react"; import { User } from "./user"; export function Column({ column }: { column: LumeColumn }) { const webviewLabel = useMemo(() => `column-${column.label}`, [column.label]); const [rect, ref] = useRect(); - const [_error, setError] = useState(null); + const [_error, setError] = useState(""); useEffect(() => { (async () => { @@ -33,10 +39,8 @@ export function Column({ column }: { column: LumeColumn }) { })(); }, [rect]); - useEffect(() => { - const isCreated = window.sessionStorage.getItem(webviewLabel); - - if (!isCreated) { + useLayoutEffect(() => { + if (ref.current) { const initialRect = ref.current.getBoundingClientRect(); commands @@ -51,7 +55,6 @@ export function Column({ column }: { column: LumeColumn }) { .then((res) => { if (res.status === "ok") { console.log("webview is created: ", webviewLabel); - window.sessionStorage.setItem(webviewLabel, ""); } else { setError(res.error); } @@ -172,7 +175,11 @@ function Header({
setTitle(e.currentTarget.textContent)} + onBlur={(e) => { + if (e.currentTarget.textContent) { + setTitle(e.currentTarget.textContent); + } + }} className="text-[12px] font-semibold focus:outline-none" > {name} diff --git a/src/components/note/mentions/hashtag.tsx b/src/components/note/mentions/hashtag.tsx index f6516423..6df11861 100644 --- a/src/components/note/mentions/hashtag.tsx +++ b/src/components/note/mentions/hashtag.tsx @@ -1,7 +1,7 @@ export function Hashtag({ tag }: { tag: string }) { return ( - {tag} + {tag.includes("#") ? tag : `#${tag}`} ); } diff --git a/src/routes/columns/_layout/events.$id.lazy.tsx b/src/routes/columns/_layout/events.$id.lazy.tsx index 24ea9296..60682dfd 100644 --- a/src/routes/columns/_layout/events.$id.lazy.tsx +++ b/src/routes/columns/_layout/events.$id.lazy.tsx @@ -6,7 +6,7 @@ import * as ScrollArea from "@radix-ui/react-scroll-area"; import { useQuery } from "@tanstack/react-query"; import { createLazyFileRoute } from "@tanstack/react-router"; import { getCurrentWindow } from "@tauri-apps/api/window"; -import { useEffect, useRef } from "react"; +import { type RefObject, useEffect, useRef } from "react"; import { Virtualizer } from "virtua"; export const Route = createLazyFileRoute("/columns/_layout/events/$id")({ @@ -35,7 +35,7 @@ function Screen() { ref={ref} className="relative h-full bg-white dark:bg-black rounded-t-xl shadow shadow-neutral-300/50 dark:shadow-none border-[.5px] border-neutral-300 dark:border-neutral-700" > - + }> @@ -64,10 +64,10 @@ function RootEvent() { ); } - if (isError) { + if (isError || !event) { return (
- {error.message} + {error?.message || "Event not found within your current relay set"}
); } @@ -181,15 +181,21 @@ function ReplyList() { useEffect(() => { events.subscription - .emit({ label, kind: "Subscribe", event_id: id }) + .emit({ + label: label || id, + kind: "Subscribe", + event_id: id, + contacts: null, + }) .then(() => console.log("Subscribe: ", label)); return () => { events.subscription .emit({ - label, + label: label || id, kind: "Unsubscribe", event_id: id, + contacts: null, }) .then(() => console.log("Unsubscribe: ", label)); }; @@ -229,7 +235,7 @@ function ReplyList() {
) : (
- {!data.length ? ( + {!data?.length ? (

👋

diff --git a/src/routes/columns/_layout/global.tsx b/src/routes/columns/_layout/global.tsx index aefa6070..b1dd11ee 100644 --- a/src/routes/columns/_layout/global.tsx +++ b/src/routes/columns/_layout/global.tsx @@ -7,7 +7,7 @@ import { ArrowDown } from "@phosphor-icons/react"; import * as ScrollArea from "@radix-ui/react-scroll-area"; import { useInfiniteQuery } from "@tanstack/react-query"; import { createFileRoute } from "@tanstack/react-router"; -import { useCallback, useRef } from "react"; +import { type RefObject, useCallback, useRef } from "react"; import { Virtualizer } from "virtua"; export const Route = createFileRoute("/columns/_layout/global")({ @@ -27,7 +27,7 @@ export function Screen() { queryKey: ["events", "global", label], initialPageParam: 0, queryFn: async ({ pageParam }: { pageParam: number }) => { - const until = pageParam > 0 ? pageParam.toString() : undefined; + const until = pageParam > 0 ? pageParam.toString() : null; const res = await commands.getGlobalEvents(until); if (res.status === "error") { @@ -36,7 +36,13 @@ export function Screen() { return toLumeEvents(res.data); }, - getNextPageParam: (lastPage) => lastPage?.at(-1)?.created_at - 1, + getNextPageParam: (lastPage) => { + const lastEvent = lastPage.at(-1); + + if (lastEvent) { + return lastEvent.created_at - 1; + } + }, select: (data) => data?.pages.flat(), refetchOnWindowFocus: false, }); @@ -47,14 +53,17 @@ export function Screen() { (event: LumeEvent) => { if (!event) return; switch (event.kind) { - case Kind.Repost: + case Kind.Repost: { + const repostId = event.repostId; + return ( ); + } default: return ( - + }> {isFetching && !isLoading && !isFetchingNextPage ? (
@@ -92,7 +101,7 @@ export function Screen() { Loading...
- ) : !data.length ? ( + ) : !data?.length ? (
🎉 Yo. You're catching up on all latest notes.
diff --git a/src/routes/columns/_layout/groups.$id.lazy.tsx b/src/routes/columns/_layout/groups.$id.lazy.tsx index 83b627bb..16b8a2b2 100644 --- a/src/routes/columns/_layout/groups.$id.lazy.tsx +++ b/src/routes/columns/_layout/groups.$id.lazy.tsx @@ -7,7 +7,7 @@ import { ArrowDown } from "@phosphor-icons/react"; import * as ScrollArea from "@radix-ui/react-scroll-area"; import { useInfiniteQuery } from "@tanstack/react-query"; import { createLazyFileRoute } from "@tanstack/react-router"; -import { useCallback, useRef } from "react"; +import { type RefObject, useCallback, useRef } from "react"; import { Virtualizer } from "virtua"; export const Route = createLazyFileRoute("/columns/_layout/groups/$id")({ @@ -29,7 +29,7 @@ export function Screen() { queryKey: ["events", "groups", params.id], initialPageParam: 0, queryFn: async ({ pageParam }: { pageParam: number }) => { - const until = pageParam > 0 ? pageParam.toString() : undefined; + const until = pageParam > 0 ? pageParam.toString() : null; const res = await commands.getAllEventsByAuthors(group, until); if (res.status === "error") { @@ -38,7 +38,13 @@ export function Screen() { return toLumeEvents(res.data); }, - getNextPageParam: (lastPage) => lastPage?.at(-1)?.created_at - 1, + getNextPageParam: (lastPage) => { + const lastEvent = lastPage.at(-1); + + if (lastEvent) { + return lastEvent.created_at - 1; + } + }, select: (data) => data?.pages.flat(), enabled: group?.length > 0, refetchOnWindowFocus: false, @@ -50,14 +56,17 @@ export function Screen() { (event: LumeEvent) => { if (!event) return; switch (event.kind) { - case Kind.Repost: + case Kind.Repost: { + const repostId = event.repostId; + return ( ); + } default: return ( - + }> {isFetching && !isLoading && !isFetchingNextPage ? (
@@ -95,7 +104,7 @@ export function Screen() { Loading...
- ) : !data.length ? ( + ) : !data?.length ? (
🎉 Yo. You're catching up on all latest notes.
diff --git a/src/routes/columns/_layout/interests.$id.lazy.tsx b/src/routes/columns/_layout/interests.$id.lazy.tsx index 558d3e4d..03593320 100644 --- a/src/routes/columns/_layout/interests.$id.lazy.tsx +++ b/src/routes/columns/_layout/interests.$id.lazy.tsx @@ -7,7 +7,7 @@ import { ArrowDown } from "@phosphor-icons/react"; import * as ScrollArea from "@radix-ui/react-scroll-area"; import { useInfiniteQuery } from "@tanstack/react-query"; import { createLazyFileRoute } from "@tanstack/react-router"; -import { useCallback, useRef } from "react"; +import { type RefObject, useCallback, useRef } from "react"; import { Virtualizer } from "virtua"; export const Route = createLazyFileRoute("/columns/_layout/interests/$id")({ @@ -30,7 +30,7 @@ export function Screen() { initialPageParam: 0, queryFn: async ({ pageParam }: { pageParam: number }) => { const tags = hashtags.map((tag) => tag.toLowerCase().replace("#", "")); - const until = pageParam > 0 ? pageParam.toString() : undefined; + const until = pageParam > 0 ? pageParam.toString() : null; const res = await commands.getAllEventsByHashtags(tags, until); if (res.status === "error") { @@ -39,7 +39,13 @@ export function Screen() { return toLumeEvents(res.data); }, - getNextPageParam: (lastPage) => lastPage?.at(-1)?.created_at - 1, + getNextPageParam: (lastPage) => { + const lastEvent = lastPage.at(-1); + + if (lastEvent) { + return lastEvent.created_at - 1; + } + }, select: (data) => data?.pages.flat(), refetchOnWindowFocus: false, }); @@ -50,14 +56,17 @@ export function Screen() { (event: LumeEvent) => { if (!event) return; switch (event.kind) { - case Kind.Repost: + case Kind.Repost: { + const repostId = event.repostId; + return ( ); + } default: return ( - + }> {isFetching && !isLoading && !isFetchingNextPage ? (
@@ -95,7 +104,7 @@ export function Screen() { Loading...
- ) : !data.length ? ( + ) : !data?.length ? (
🎉 Yo. You're catching up on all latest notes.
diff --git a/src/routes/columns/_layout/launchpad.$id.lazy.tsx b/src/routes/columns/_layout/launchpad.$id.lazy.tsx index 45aa4767..15ab3dde 100644 --- a/src/routes/columns/_layout/launchpad.$id.lazy.tsx +++ b/src/routes/columns/_layout/launchpad.$id.lazy.tsx @@ -20,7 +20,7 @@ export const Route = createLazyFileRoute("/columns/_layout/launchpad/$id")({ function Screen() { const { id } = Route.useParams(); - const { data: isSync } = useQuery({ + const { isLoading, data: isSync } = useQuery({ queryKey: ["is-sync", id], queryFn: async () => { const res = await commands.isAccountSync(id); @@ -40,7 +40,9 @@ function Screen() { className="overflow-hidden size-full" > - {!isSync ? ( + {isLoading ? ( + + ) : !isSync ? ( ) : ( <> diff --git a/src/routes/columns/_layout/newsfeed.$id.lazy.tsx b/src/routes/columns/_layout/newsfeed.$id.lazy.tsx index 5e174c9f..cafb19ea 100644 --- a/src/routes/columns/_layout/newsfeed.$id.lazy.tsx +++ b/src/routes/columns/_layout/newsfeed.$id.lazy.tsx @@ -7,7 +7,8 @@ import { ArrowDown } from "@phosphor-icons/react"; import * as ScrollArea from "@radix-ui/react-scroll-area"; import { useInfiniteQuery } from "@tanstack/react-query"; import { createLazyFileRoute } from "@tanstack/react-router"; -import { useCallback, useEffect, useRef } from "react"; +import { listen } from "@tauri-apps/api/event"; +import { type RefObject, useCallback, useEffect, useRef } from "react"; import { Virtualizer } from "virtua"; export const Route = createLazyFileRoute("/columns/_layout/newsfeed/$id")({ @@ -17,7 +18,7 @@ export const Route = createLazyFileRoute("/columns/_layout/newsfeed/$id")({ export function Screen() { const contacts = Route.useLoaderData(); const search = Route.useSearch(); - + const { queryClient } = Route.useRouteContext(); const { data, isLoading, @@ -38,7 +39,13 @@ export function Screen() { return toLumeEvents(res.data); }, - getNextPageParam: (lastPage) => lastPage?.at?.(-1)?.created_at - 1, + getNextPageParam: (lastPage) => { + const lastEvent = lastPage.at(-1); + + if (lastEvent) { + return lastEvent.created_at - 1; + } + }, select: (data) => data?.pages.flat(), enabled: contacts?.length > 0, }); @@ -52,14 +59,17 @@ export function Screen() { } switch (event.kind) { - case Kind.Repost: + case Kind.Repost: { + const repostId = event.repostId; + return ( ); + } default: return ( { + const unlisten = listen("synchronized", async () => { + await queryClient.refetchQueries({ + queryKey: ["events", "newsfeed", search.label], + }); + }); + + return () => { + unlisten.then((f) => f()); + }; + }, []); + return ( - + }> {isFetching && !isLoading && !isFetchingNextPage ? (
@@ -119,7 +141,7 @@ export function Screen() { Loading...
- ) : !data.length ? ( + ) : !data?.length ? (
🎉 Yo. You're catching up on all latest notes.
diff --git a/src/routes/columns/_layout/notification.$id.lazy.tsx b/src/routes/columns/_layout/notification.$id.lazy.tsx index 84de223f..f78fa42a 100644 --- a/src/routes/columns/_layout/notification.$id.lazy.tsx +++ b/src/routes/columns/_layout/notification.$id.lazy.tsx @@ -10,7 +10,13 @@ import { useQuery } from "@tanstack/react-query"; import { createLazyFileRoute } from "@tanstack/react-router"; import { getCurrentWindow } from "@tauri-apps/api/window"; import { nip19 } from "nostr-tools"; -import { type ReactNode, useEffect, useMemo, useRef } from "react"; +import { + type ReactNode, + type RefObject, + useEffect, + useMemo, + useRef, +} from "react"; import { Virtualizer } from "virtua"; export const Route = createLazyFileRoute("/columns/_layout/notification/$id")({ @@ -110,7 +116,7 @@ function Screen() {
- {data.texts.map((event) => ( - - ))} + {!data?.texts ? ( +
Empty
+ ) : ( + data.texts.map((event) => ( + + )) + )}
- {[...data.reactions.entries()].map(([root, events]) => ( -
-
-
- -
-
- {events.map((event) => ( - - - -
- {event.kind === Kind.Reaction ? ( - event.content === "+" ? ( - "👍" + {!data?.reactions ? ( +
Empty
+ ) : ( + [...data.reactions.entries()].map(([root, events]) => ( +
+
+
+ +
+
+ {events.map((event) => ( + + + +
+ {event.kind === Kind.Reaction ? ( + event.content === "+" ? ( + "👍" + ) : ( + event.content + ) ) : ( - event.content - ) - ) : ( - - )} -
-
-
- ))} + + )} +
+ + + ))} +
-
- ))} + )) + )} - {[...data.zaps.entries()].map(([root, events]) => ( -
-
-
- -
-
- {events.map((event) => ( - - ))} + {!data?.zaps ? ( +
Empty
+ ) : ( + [...data.zaps.entries()].map(([root, events]) => ( +
+
+
+ +
+
+ {events.map((event) => ( + + ))} +
-
- ))} + )) + )} (null); return ( - {children} + }> + {children} + ); @@ -311,10 +331,16 @@ function TextNote({ event }: { event: LumeEvent }) { } function ZapReceipt({ event }: { event: LumeEvent }) { - const amount = useMemo( - () => decodeZapInvoice(event.tags).bitcoinFormatted ?? "0", - [event.id], - ); + const amount = useMemo(() => { + const decoded = decodeZapInvoice(event.tags); + + if (decoded) { + return decoded.bitcoinFormatted; + } else { + return "0"; + } + }, [event.id]); + const sender = useMemo( () => event.tags.find((tag) => tag[0] === "P")?.[1], [event.id], diff --git a/src/routes/columns/_layout/replies.$id.lazy.tsx b/src/routes/columns/_layout/replies.$id.lazy.tsx index a0029d24..441e0fa0 100644 --- a/src/routes/columns/_layout/replies.$id.lazy.tsx +++ b/src/routes/columns/_layout/replies.$id.lazy.tsx @@ -6,7 +6,7 @@ import { useRouter, useRouterState, } from "@tanstack/react-router"; -import { useRef } from "react"; +import { type RefObject, useRef } from "react"; import { Virtualizer } from "virtua"; export const Route = createLazyFileRoute("/columns/_layout/replies/$id")({ @@ -20,7 +20,7 @@ function Screen() { return (
-
+