From 5ab2b1ae318efaeea09eb4751981be9666747c04 Mon Sep 17 00:00:00 2001 From: reya Date: Fri, 25 Oct 2024 14:57:12 +0700 Subject: [PATCH] feat: negentropy progress --- package.json | 1 + pnpm-lock.yaml | 26 ++ src-tauri/Cargo.lock | 84 +++- src-tauri/Cargo.toml | 1 + src-tauri/resources/relays.txt | 2 +- src-tauri/src/commands/account.rs | 76 ++-- src-tauri/src/commands/event.rs | 85 +--- src-tauri/src/commands/metadata.rs | 19 +- src-tauri/src/commands/sync.rs | 389 ++++++++++-------- src-tauri/src/common.rs | 4 +- src-tauri/src/main.rs | 18 +- src/commands.gen.ts | 44 +- src/components/note/content.tsx | 87 ++-- src/components/note/mentions/note.tsx | 11 +- src/components/note/preview/image.tsx | 4 - src/components/note/preview/images.tsx | 20 +- src/components/user/avatar.tsx | 3 + src/routes.gen.ts | 63 ++- src/routes/_app.lazy.tsx | 38 +- src/routes/_app/index.lazy.tsx | 94 +++-- src/routes/_app/index.tsx | 55 ++- ...nchpad.lazy.tsx => launchpad.$id.lazy.tsx} | 261 +++++++----- .../columns/_layout/notification.$id.lazy.tsx | 12 +- src/system/event.ts | 2 +- src/system/useEvent.ts | 18 +- src/system/useProfile.ts | 10 +- src/system/window.ts | 5 +- 27 files changed, 769 insertions(+), 663 deletions(-) rename src/routes/columns/_layout/{launchpad.lazy.tsx => launchpad.$id.lazy.tsx} (62%) diff --git a/package.json b/package.json index a5b062f2..619d07b1 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "@radix-ui/react-avatar": "^1.1.1", "@radix-ui/react-checkbox": "^1.1.2", "@radix-ui/react-popover": "^1.1.2", + "@radix-ui/react-progress": "^1.1.0", "@radix-ui/react-scroll-area": "^1.2.0", "@radix-ui/react-switch": "^1.1.1", "@radix-ui/react-tabs": "^1.1.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8dcc1a9e..35bbd7d9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,6 +23,9 @@ importers: '@radix-ui/react-popover': specifier: ^1.1.2 version: 1.1.2(react-dom@19.0.0-rc-d025ddd3-20240722(react@19.0.0-rc-d025ddd3-20240722))(react@19.0.0-rc-d025ddd3-20240722)(types-react-dom@19.0.0-rc.1)(types-react@19.0.0-rc.1) + '@radix-ui/react-progress': + specifier: ^1.1.0 + version: 1.1.0(react-dom@19.0.0-rc-d025ddd3-20240722(react@19.0.0-rc-d025ddd3-20240722))(react@19.0.0-rc-d025ddd3-20240722)(types-react-dom@19.0.0-rc.1)(types-react@19.0.0-rc.1) '@radix-ui/react-scroll-area': specifier: ^1.2.0 version: 1.2.0(react-dom@19.0.0-rc-d025ddd3-20240722(react@19.0.0-rc-d025ddd3-20240722))(react@19.0.0-rc-d025ddd3-20240722)(types-react-dom@19.0.0-rc.1)(types-react@19.0.0-rc.1) @@ -974,6 +977,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-progress@1.1.0': + resolution: {integrity: sha512-aSzvnYpP725CROcxAOEBVZZSIQVQdHgBr2QQFKySsaD14u8dNT0batuXI+AAGDdAHfXH8rbnHmjYFqVJ21KkRg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-roving-focus@1.1.0': resolution: {integrity: sha512-EA6AMGeq9AEeQDeSH0aZgG198qkfHSbvWTf1HvoDmOB5bBG/qTxjYMWUKMnYiV6J/iP/J8MEFSuB2zRU2n7ODA==} peerDependencies: @@ -3044,6 +3060,16 @@ snapshots: '@types/react': types-react@19.0.0-rc.1 '@types/react-dom': types-react-dom@19.0.0-rc.1 + '@radix-ui/react-progress@1.1.0(react-dom@19.0.0-rc-d025ddd3-20240722(react@19.0.0-rc-d025ddd3-20240722))(react@19.0.0-rc-d025ddd3-20240722)(types-react-dom@19.0.0-rc.1)(types-react@19.0.0-rc.1)': + dependencies: + '@radix-ui/react-context': 1.1.0(react@19.0.0-rc-d025ddd3-20240722)(types-react@19.0.0-rc.1) + '@radix-ui/react-primitive': 2.0.0(react-dom@19.0.0-rc-d025ddd3-20240722(react@19.0.0-rc-d025ddd3-20240722))(react@19.0.0-rc-d025ddd3-20240722)(types-react-dom@19.0.0-rc.1)(types-react@19.0.0-rc.1) + react: 19.0.0-rc-d025ddd3-20240722 + react-dom: 19.0.0-rc-d025ddd3-20240722(react@19.0.0-rc-d025ddd3-20240722) + optionalDependencies: + '@types/react': types-react@19.0.0-rc.1 + '@types/react-dom': types-react-dom@19.0.0-rc.1 + '@radix-ui/react-roving-focus@1.1.0(react-dom@19.0.0-rc-d025ddd3-20240722(react@19.0.0-rc-d025ddd3-20240722))(react@19.0.0-rc-d025ddd3-20240722)(types-react-dom@19.0.0-rc.1)(types-react@19.0.0-rc.1)': dependencies: '@radix-ui/primitive': 1.1.0 diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 2af1f637..1efea5c9 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -44,6 +44,7 @@ dependencies = [ "tauri-plugin-window-state", "tauri-specta", "tokio", + "tracing-subscriber", "url", ] @@ -3479,7 +3480,7 @@ checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" [[package]] name = "nostr" version = "0.35.0" -source = "git+https://github.com/rust-nostr/nostr#a8114e090b333e6ca50fb80d8afc1ced4229ee33" +source = "git+https://github.com/rust-nostr/nostr#a398775dd1013482f785816e6b9fe99f7668416a" dependencies = [ "aes", "base64 0.22.1", @@ -3509,7 +3510,7 @@ dependencies = [ [[package]] name = "nostr-database" version = "0.35.0" -source = "git+https://github.com/rust-nostr/nostr#a8114e090b333e6ca50fb80d8afc1ced4229ee33" +source = "git+https://github.com/rust-nostr/nostr#a398775dd1013482f785816e6b9fe99f7668416a" dependencies = [ "async-trait", "flatbuffers", @@ -3523,7 +3524,7 @@ dependencies = [ [[package]] name = "nostr-lmdb" version = "0.35.0" -source = "git+https://github.com/rust-nostr/nostr#a8114e090b333e6ca50fb80d8afc1ced4229ee33" +source = "git+https://github.com/rust-nostr/nostr#a398775dd1013482f785816e6b9fe99f7668416a" dependencies = [ "heed", "nostr", @@ -3536,7 +3537,7 @@ dependencies = [ [[package]] name = "nostr-relay-pool" version = "0.35.0" -source = "git+https://github.com/rust-nostr/nostr#a8114e090b333e6ca50fb80d8afc1ced4229ee33" +source = "git+https://github.com/rust-nostr/nostr#a398775dd1013482f785816e6b9fe99f7668416a" dependencies = [ "async-utility", "async-wsocket", @@ -3554,7 +3555,7 @@ dependencies = [ [[package]] name = "nostr-sdk" version = "0.35.0" -source = "git+https://github.com/rust-nostr/nostr#a8114e090b333e6ca50fb80d8afc1ced4229ee33" +source = "git+https://github.com/rust-nostr/nostr#a398775dd1013482f785816e6b9fe99f7668416a" dependencies = [ "async-utility", "atomic-destructor", @@ -3574,7 +3575,7 @@ dependencies = [ [[package]] name = "nostr-signer" version = "0.35.0" -source = "git+https://github.com/rust-nostr/nostr#a8114e090b333e6ca50fb80d8afc1ced4229ee33" +source = "git+https://github.com/rust-nostr/nostr#a398775dd1013482f785816e6b9fe99f7668416a" dependencies = [ "async-utility", "nostr", @@ -3587,7 +3588,7 @@ dependencies = [ [[package]] name = "nostr-zapper" version = "0.35.0" -source = "git+https://github.com/rust-nostr/nostr#a8114e090b333e6ca50fb80d8afc1ced4229ee33" +source = "git+https://github.com/rust-nostr/nostr#a398775dd1013482f785816e6b9fe99f7668416a" dependencies = [ "async-trait", "nostr", @@ -3607,6 +3608,16 @@ dependencies = [ "zbus 4.4.0", ] +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + [[package]] name = "num" version = "0.4.3" @@ -3721,7 +3732,7 @@ dependencies = [ [[package]] name = "nwc" version = "0.35.0" -source = "git+https://github.com/rust-nostr/nostr#a8114e090b333e6ca50fb80d8afc1ced4229ee33" +source = "git+https://github.com/rust-nostr/nostr#a398775dd1013482f785816e6b9fe99f7668416a" dependencies = [ "async-utility", "nostr", @@ -3993,6 +4004,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + [[package]] name = "page_size" version = "0.6.0" @@ -5316,6 +5333,15 @@ dependencies = [ "digest", ] +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + [[package]] name = "share-picker" version = "0.1.0" @@ -6331,6 +6357,16 @@ dependencies = [ "syn 2.0.79", ] +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", +] + [[package]] name = "tiff" version = "0.9.1" @@ -6606,6 +6642,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" dependencies = [ "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" +dependencies = [ + "nu-ansi-term", + "sharded-slab", + "smallvec", + "thread_local", + "tracing-core", + "tracing-log", ] [[package]] @@ -6813,6 +6875,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + [[package]] name = "vcpkg" version = "0.2.15" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index a529d614..1f68de35 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -48,6 +48,7 @@ linkify = "0.10.0" regex = "1.10.4" keyring = { version = "3", features = ["apple-native", "windows-native"] } keyring-search = "1.2.0" +tracing-subscriber = { version = "0.3.18", features = ["fmt"] } [target.'cfg(target_os = "macos")'.dependencies] border = { git = "https://github.com/ahkohd/tauri-toolkit", branch = "v2" } diff --git a/src-tauri/resources/relays.txt b/src-tauri/resources/relays.txt index d2969fba..99163654 100644 --- a/src-tauri/resources/relays.txt +++ b/src-tauri/resources/relays.txt @@ -1,4 +1,4 @@ wss://relay.damus.io, wss://relay.nostr.net, +wss://relay.primal.net, wss://nostr.fmt.wiz.biz, -wss://offchain.pub, diff --git a/src-tauri/src/commands/account.rs b/src-tauri/src/commands/account.rs index ef9e8172..d6259c7b 100644 --- a/src-tauri/src/commands/account.rs +++ b/src-tauri/src/commands/account.rs @@ -1,15 +1,12 @@ -use async_utility::thread::sleep; use keyring::Entry; use nostr_sdk::prelude::*; use serde::{Deserialize, Serialize}; use specta::Type; -use std::{fs, str::FromStr, time::Duration}; -use tauri::{Emitter, Manager, State}; +use std::{str::FromStr, time::Duration}; +use tauri::{Emitter, State}; use crate::{common::get_all_accounts, Nostr}; -use super::sync::sync_account; - #[derive(Debug, Clone, Serialize, Deserialize, Type)] struct Account { secret_key: String, @@ -24,11 +21,8 @@ pub fn get_accounts() -> Vec { #[tauri::command] #[specta::specta] -pub async fn watch_account( - id: String, - state: State<'_, Nostr>, - app_handle: tauri::AppHandle, -) -> Result { +pub async fn watch_account(id: String, state: State<'_, Nostr>) -> Result { + let client = &state.client; let public_key = PublicKey::from_str(&id).map_err(|e| e.to_string())?; let npub = public_key.to_bech32().map_err(|e| e.to_string())?; let keyring = Entry::new("Lume Safe Storage", &npub).map_err(|e| e.to_string())?; @@ -36,14 +30,14 @@ pub async fn watch_account( // Set empty password keyring.set_password("").map_err(|e| e.to_string())?; - // Run sync for this account - sync_account(public_key, app_handle); - // Update state - state.accounts.lock().unwrap().push(npub.clone()); + let mut accounts = state.accounts.lock().unwrap().clone(); + accounts.push(npub.clone()); - // Fake loading - sleep(Duration::from_secs(4)).await; + // Get user's profile + let _ = client + .fetch_metadata(public_key, Some(Duration::from_secs(4))) + .await; Ok(npub) } @@ -54,7 +48,6 @@ pub async fn import_account( key: String, password: Option, state: State<'_, Nostr>, - app_handle: tauri::AppHandle, ) -> Result { let client = &state.client; @@ -87,25 +80,21 @@ pub async fn import_account( // Update signer client.set_signer(Some(signer)).await; - // Run sync for this account - sync_account(public_key, app_handle); - - // Fake loading - sleep(Duration::from_secs(4)).await; - // Update state - state.accounts.lock().unwrap().push(npub.clone()); + let mut accounts = state.accounts.lock().unwrap().clone(); + accounts.push(npub.clone()); + + // Get user's profile + let _ = client + .fetch_metadata(public_key, Some(Duration::from_secs(4))) + .await; Ok(npub) } #[tauri::command] #[specta::specta] -pub async fn connect_account( - uri: String, - state: State<'_, Nostr>, - app_handle: tauri::AppHandle, -) -> Result { +pub async fn connect_account(uri: String, state: State<'_, Nostr>) -> Result { let client = &state.client; match NostrConnectURI::parse(uri.clone()) { @@ -118,9 +107,6 @@ pub async fn connect_account( let remote_user = bunker_uri.signer_public_key().unwrap(); let remote_npub = remote_user.to_bech32().unwrap(); - // Run sync for this account - sync_account(remote_user, app_handle); - match Nip46Signer::new(bunker_uri, app_keys, Duration::from_secs(120), None) { Ok(signer) => { let mut url = Url::parse(&uri).unwrap(); @@ -146,7 +132,13 @@ pub async fn connect_account( let _ = client.set_signer(Some(signer.into())).await; // Update state - state.accounts.lock().unwrap().push(remote_npub.clone()); + let mut accounts = state.accounts.lock().unwrap().clone(); + accounts.push(remote_npub.clone()); + + // Get user's profile + let _ = client + .fetch_metadata(remote_user, Some(Duration::from_secs(4))) + .await; Ok(remote_npub) } @@ -198,24 +190,6 @@ pub fn delete_account(id: String) -> Result<(), String> { Ok(()) } -#[tauri::command] -#[specta::specta] -pub async fn is_new_account(id: String, app_handle: tauri::AppHandle) -> Result { - let config_dir = app_handle.path().config_dir().map_err(|e| e.to_string())?; - let exist = fs::metadata(config_dir.join(id)).is_ok(); - - Ok(!exist) -} - -#[tauri::command] -#[specta::specta] -pub async fn toggle_new_account(id: String, app_handle: tauri::AppHandle) -> Result<(), String> { - let config_dir = app_handle.path().config_dir().map_err(|e| e.to_string())?; - fs::File::create(config_dir.join(id)).unwrap(); - - Ok(()) -} - #[tauri::command] #[specta::specta] pub async fn has_signer(id: String, state: State<'_, Nostr>) -> Result { diff --git a/src-tauri/src/commands/event.rs b/src-tauri/src/commands/event.rs index 7a7db3ee..dbc28977 100644 --- a/src-tauri/src/commands/event.rs +++ b/src-tauri/src/commands/event.rs @@ -1,11 +1,10 @@ -use futures::future::join_all; use nostr_sdk::prelude::*; use serde::Serialize; use specta::Type; use std::{str::FromStr, time::Duration}; use tauri::State; -use crate::common::{create_tags, get_latest_event, parse_event, process_event, Meta}; +use crate::common::{create_tags, parse_event, process_event, Meta}; use crate::{Nostr, DEFAULT_DIFFICULTY, FETCH_LIMIT}; #[derive(Debug, Clone, Serialize, Type)] @@ -14,23 +13,15 @@ pub struct RichEvent { pub parsed: Option, } -#[tauri::command] -#[specta::specta] -pub async fn get_event_meta(content: String) -> Result { - let meta = parse_event(&content).await; - Ok(meta) -} - #[tauri::command] #[specta::specta] pub async fn get_event(id: String, state: State<'_, Nostr>) -> Result { let client = &state.client; - let event_id = EventId::parse(&id).map_err(|err| err.to_string())?; - let filter = Filter::new().id(event_id); + let event_id = EventId::from_str(&id).map_err(|err| err.to_string())?; - match client.database().query(vec![filter.clone()]).await { + match client.database().event_by_id(&event_id).await { Ok(events) => { - if let Some(event) = get_latest_event(&events) { + if let Some(event) = events { let raw = event.as_json(); let parsed = if event.kind == Kind::TextNote { Some(parse_event(&event.content).await) @@ -40,26 +31,7 @@ pub async fn get_event(id: String, state: State<'_, Nostr>) -> Result { - if let Some(event) = get_latest_event(&events) { - 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("Not found.".into()) - } - } - Err(err) => Err(err.to_string()), - } + Err("Event not found".to_string()) } } Err(err) => Err(err.to_string()), @@ -68,35 +40,8 @@ pub async fn get_event(id: String, state: State<'_, Nostr>) -> Result, -) -> Result { - let client = &state.client; - let event_id = EventId::parse(&id).map_err(|err| err.to_string())?; - let filter = Filter::new().id(event_id); - - match client - .fetch_events(vec![filter], Some(Duration::from_secs(5))) - .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()), - } +pub async fn get_meta_from_event(content: String) -> Result { + Ok(parse_event(&content).await) } #[tauri::command] @@ -110,21 +55,7 @@ pub async fn get_replies(id: String, state: State<'_, Nostr>) -> Result { - let futures = events.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) - } + Ok(events) => Ok(process_event(client, events).await), Err(err) => Err(err.to_string()), } } diff --git a/src-tauri/src/commands/metadata.rs b/src-tauri/src/commands/metadata.rs index 2585727c..e94171cf 100644 --- a/src-tauri/src/commands/metadata.rs +++ b/src-tauri/src/commands/metadata.rs @@ -237,7 +237,7 @@ pub async fn set_group( .authors(public_keys) .limit(500); - if let Ok(report) = client.sync(filter, NegentropyOptions::default()).await { + if let Ok(report) = client.sync(filter, SyncOptions::default()).await { println!("Received: {}", report.received.len()); handle.emit("synchronized", ()).unwrap(); }; @@ -331,7 +331,7 @@ pub async fn set_interest( .hashtags(hashtags) .limit(500); - if let Ok(report) = client.sync(filter, NegentropyOptions::default()).await { + if let Ok(report) = client.sync(filter, SyncOptions::default()).await { println!("Received: {}", report.received.len()); handle.emit("synchronized", ()).unwrap(); }; @@ -560,15 +560,12 @@ pub async fn get_notifications(id: String, state: State<'_, Nostr>) -> Result Ok(events.into_iter().map(|ev| ev.as_json()).collect()), diff --git a/src-tauri/src/commands/sync.rs b/src-tauri/src/commands/sync.rs index 9816ce90..2ed2d3f0 100644 --- a/src-tauri/src/commands/sync.rs +++ b/src-tauri/src/commands/sync.rs @@ -1,8 +1,11 @@ use nostr_sdk::prelude::*; use serde::{Deserialize, Serialize}; use specta::Type; -use std::str::FromStr; -use tauri::{AppHandle, Manager}; +use std::{ + fs::{self, File}, + str::FromStr, +}; +use tauri::{ipc::Channel, AppHandle, Manager, State}; use tauri_specta::Event as TauriEvent; use crate::Nostr; @@ -45,70 +48,19 @@ pub fn sync_all(accounts: Vec, app_handle: AppHandle) { let client = &state.client; let bootstrap_relays = state.bootstrap_relays.lock().unwrap().clone(); - // NEG: Sync metadata - // - let metadata = Filter::new().authors(public_keys.clone()).kinds(vec![ - Kind::Metadata, - Kind::ContactList, - Kind::Interests, - Kind::InterestSet, - Kind::FollowSet, - Kind::EventDeletion, - Kind::TextNote, - Kind::Repost, - Kind::Custom(30315), - ]); - - if let Ok(report) = client - .sync_with(&bootstrap_relays, metadata, NegentropyOptions::default()) - .await - { - NegentropyEvent { - kind: NegentropyKind::Others, - total_event: report.received.len() as i32, - } - .emit(&app_handle) - .unwrap(); - } - - // NEG: Sync notification - // - let notification = Filter::new() - .pubkeys(public_keys) - .kinds(vec![ - Kind::TextNote, - Kind::Repost, - Kind::Reaction, - Kind::ZapReceipt, - ]) - .limit(5000); - - if let Ok(report) = client - .sync_with( - &bootstrap_relays, - notification, - NegentropyOptions::default(), - ) - .await - { - NegentropyEvent { - kind: NegentropyKind::Notification, - total_event: report.received.len() as i32, - } - .emit(&app_handle) - .unwrap(); - } - // NEG: Sync events for all pubkeys in local database // - let pubkey_filter = Filter::new().kinds(vec![ - Kind::ContactList, - Kind::Repost, - Kind::TextNote, - Kind::FollowSet, - ]); - - if let Ok(events) = client.database().query(vec![pubkey_filter]).await { + if let Ok(events) = client + .database() + .query(vec![Filter::new().kinds(vec![ + Kind::ContactList, + Kind::FollowSet, + Kind::MuteList, + Kind::Repost, + Kind::TextNote, + ])]) + .await + { let pubkeys: Vec = events .iter() .flat_map(|ev| ev.tags.public_keys().copied()) @@ -126,15 +78,15 @@ pub fn sync_all(accounts: Vec, app_handle: AppHandle) { let events = Filter::new() .authors(authors.clone()) .kinds(vec![Kind::TextNote, Kind::Repost]) - .limit(5000); + .limit(1000); - if let Ok(report) = client - .sync_with(&bootstrap_relays, events, NegentropyOptions::default()) + if let Ok(output) = client + .sync_with(&bootstrap_relays, events, SyncOptions::default()) .await { NegentropyEvent { kind: NegentropyKind::Events, - total_event: report.received.len() as i32, + total_event: output.received.len() as i32, } .emit(&app_handle) .unwrap(); @@ -144,124 +96,35 @@ pub fn sync_all(accounts: Vec, app_handle: AppHandle) { // let metadata = Filter::new() .authors(authors) - .kinds(vec![Kind::Metadata, Kind::ContactList]); + .kinds(vec![ + Kind::Metadata, + Kind::ContactList, + Kind::Interests, + Kind::InterestSet, + Kind::FollowSet, + Kind::MuteList, + Kind::RelaySet, + ]) + .limit(1000); - if let Ok(report) = client - .sync_with(&bootstrap_relays, metadata, NegentropyOptions::default()) + if let Ok(output) = client + .sync_with(&bootstrap_relays, metadata, SyncOptions::default()) .await { NegentropyEvent { kind: NegentropyKind::Metadata, - total_event: report.received.len() as i32, + total_event: output.received.len() as i32, } .emit(&app_handle) .unwrap(); } } } - }); -} -pub fn sync_account(public_key: PublicKey, app_handle: AppHandle) { - tauri::async_runtime::spawn(async move { - let state = app_handle.state::(); - let client = &state.client; - let bootstrap_relays = state.bootstrap_relays.lock().unwrap().clone(); - - // NEG: Sync all user's metadata - // - let metadata = Filter::new().author(public_key).kinds(vec![ - Kind::Metadata, - Kind::ContactList, - Kind::Interests, - Kind::InterestSet, - Kind::FollowSet, - Kind::RelayList, - Kind::RelaySet, - Kind::EventDeletion, - Kind::Custom(30315), - ]); - - if let Ok(report) = client - .sync_with(&bootstrap_relays, metadata, NegentropyOptions::default()) - .await - { - NegentropyEvent { - kind: NegentropyKind::Metadata, - total_event: report.received.len() as i32, - } - .emit(&app_handle) - .unwrap(); - } - - if let Ok(contact_list) = client.database().contacts_public_keys(public_key).await { - // NEG: Sync all contact's metadata - // - let metadata = Filter::new() - .authors(contact_list.clone()) - .kinds(vec![Kind::Metadata, Kind::RelaySet, Kind::Custom(30315)]) - .limit(1000); - - if let Ok(report) = client - .sync_with(&bootstrap_relays, metadata, NegentropyOptions::default()) - .await - { - NegentropyEvent { - kind: NegentropyKind::Metadata, - total_event: report.received.len() as i32, - } - .emit(&app_handle) - .unwrap(); - } - - // NEG: Sync all contact's events - // - let metadata = Filter::new() - .authors(contact_list.clone()) - .kinds(vec![Kind::TextNote, Kind::Repost]) - .limit(1000); - - if let Ok(report) = client - .sync_with(&bootstrap_relays, metadata, NegentropyOptions::default()) - .await - { - NegentropyEvent { - kind: NegentropyKind::Events, - total_event: report.received.len() as i32, - } - .emit(&app_handle) - .unwrap(); - } - - // NEG: Sync all contact's other metadata - // - let metadata = Filter::new() - .authors(contact_list) - .kinds(vec![ - Kind::Interests, - Kind::InterestSet, - Kind::FollowSet, - Kind::EventDeletion, - ]) - .limit(1000); - - if let Ok(report) = client - .sync_with(&bootstrap_relays, metadata, NegentropyOptions::default()) - .await - { - NegentropyEvent { - kind: NegentropyKind::Metadata, - total_event: report.received.len() as i32, - } - .emit(&app_handle) - .unwrap(); - } - } - - // NEG: Sync all user's metadata + // NEG: Sync notification // let notification = Filter::new() - .pubkey(public_key) + .pubkeys(public_keys.clone()) .kinds(vec![ Kind::TextNote, Kind::Repost, @@ -270,20 +133,188 @@ pub fn sync_account(public_key: PublicKey, app_handle: AppHandle) { ]) .limit(500); - if let Ok(report) = client - .sync_with( - &bootstrap_relays, - notification, - NegentropyOptions::default(), - ) + if let Ok(output) = client + .sync_with(&bootstrap_relays, notification, SyncOptions::default()) .await { NegentropyEvent { kind: NegentropyKind::Notification, - total_event: report.received.len() as i32, + 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(); } }); } + +#[tauri::command] +#[specta::specta] +pub fn is_account_sync(id: String, app_handle: tauri::AppHandle) -> Result { + let config_dir = app_handle + .path() + .app_config_dir() + .map_err(|e| e.to_string())?; + let exist = fs::metadata(config_dir.join(id)).is_ok(); + + Ok(exist) +} + +#[tauri::command] +#[specta::specta] +pub async fn sync_account( + id: String, + state: State<'_, Nostr>, + reader: Channel, + app_handle: tauri::AppHandle, +) -> Result<(), String> { + let client = &state.client; + let bootstrap_relays = state.bootstrap_relays.lock().unwrap().clone(); + + let public_key = PublicKey::from_bech32(&id).map_err(|e| e.to_string())?; + + let filter = Filter::new().author(public_key).kinds(vec![ + Kind::Metadata, + Kind::ContactList, + Kind::Interests, + Kind::InterestSet, + Kind::FollowSet, + Kind::RelayList, + Kind::MuteList, + Kind::EventDeletion, + Kind::Bookmarks, + Kind::BookmarkSet, + Kind::TextNote, + Kind::Repost, + Kind::Custom(30315), + ]); + + let (tx, mut rx) = SyncProgress::channel(); + let opts = SyncOptions::default().progress(tx); + + tauri::async_runtime::spawn(async move { + while (rx.changed().await).is_ok() { + let SyncProgress { total, current } = *rx.borrow_and_update(); + + if total > 0 { + reader + .send((current as f64 / total as f64) * 100.0) + .unwrap() + } + } + }); + + if let Ok(output) = client + .sync_with(&bootstrap_relays, filter, opts.clone()) + .await + { + println!("Success: {:?}", output.success); + println!("Failed: {:?}", output.failed); + + let event_pubkeys = client + .database() + .query(vec![Filter::new().kinds(vec![ + Kind::ContactList, + Kind::FollowSet, + Kind::MuteList, + Kind::Repost, + Kind::TextNote, + ])]) + .await + .map_err(|e| e.to_string())?; + + if !event_pubkeys.is_empty() { + let pubkeys: Vec = event_pubkeys + .iter() + .flat_map(|ev| ev.tags.public_keys().copied()) + .collect(); + + let filter = Filter::new() + .authors(pubkeys) + .kinds(vec![ + Kind::Metadata, + Kind::TextNote, + Kind::Repost, + Kind::EventDeletion, + Kind::Interests, + Kind::InterestSet, + Kind::FollowSet, + Kind::RelayList, + Kind::MuteList, + Kind::EventDeletion, + Kind::Bookmarks, + Kind::BookmarkSet, + Kind::Custom(30315), + ]) + .limit(10000); + + if let Ok(output) = client + .sync_with(&bootstrap_relays, filter, opts.clone()) + .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() + .map_err(|e| e.to_string())?; + let _ = File::create(config_dir.join(id)); + + Ok(()) +} diff --git a/src-tauri/src/common.rs b/src-tauri/src/common.rs index fea4cb7e..c7e0f47f 100644 --- a/src-tauri/src/common.rs +++ b/src-tauri/src/common.rs @@ -34,14 +34,12 @@ const NOSTR_EVENTS: [&str; 10] = [ "Nostr:nevent1", ]; -const NOSTR_MENTIONS: [&str; 10] = [ +const NOSTR_MENTIONS: [&str; 8] = [ "@npub1", "nostr:npub1", "nostr:nprofile1", - "nostr:naddr1", "npub1", "nprofile1", - "naddr1", "Nostr:npub1", "Nostr:nprofile1", "Nostr:naddr1", diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index da505d96..85aee589 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -5,14 +5,7 @@ #[cfg(target_os = "macos")] use border::WebviewWindowExt as BorderWebviewWindowExt; -use commands::{ - account::*, - event::*, - metadata::*, - relay::*, - sync::{sync_all, NegentropyEvent}, - window::*, -}; +use commands::{account::*, event::*, metadata::*, relay::*, sync::*, window::*}; use common::{get_all_accounts, parse_event}; use nostr_sdk::prelude::{Profile as DatabaseProfile, *}; use serde::{Deserialize, Serialize}; @@ -97,8 +90,12 @@ pub const FETCH_LIMIT: usize = 50; pub const NOTIFICATION_SUB_ID: &str = "lume_notification"; fn main() { + tracing_subscriber::fmt::init(); + let builder = Builder::::new() .commands(collect_commands![ + sync_account, + is_account_sync, get_relays, connect_relay, remove_relay, @@ -111,8 +108,6 @@ fn main() { get_private_key, delete_account, reset_password, - is_new_account, - toggle_new_account, has_signer, set_signer, get_profile, @@ -138,9 +133,8 @@ fn main() { get_user_settings, set_user_settings, verify_nip05, - get_event_meta, + get_meta_from_event, get_event, - get_event_from, get_replies, subscribe_to, get_all_events_by_author, diff --git a/src/commands.gen.ts b/src/commands.gen.ts index a6c54ce5..d284a033 100644 --- a/src/commands.gen.ts +++ b/src/commands.gen.ts @@ -5,6 +5,22 @@ export const commands = { +async syncAccount(id: string, reader: TAURI_CHANNEL) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("sync_account", { id, reader }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async isAccountSync(id: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("is_account_sync", { id }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, async getRelays(id: string) : Promise> { try { return { status: "ok", data: await TAURI_INVOKE("get_relays", { id }) }; @@ -96,22 +112,6 @@ async resetPassword(key: string, password: string) : Promise> { - try { - return { status: "ok", data: await TAURI_INVOKE("is_new_account", { id }) }; -} catch (e) { - if(e instanceof Error) throw e; - else return { status: "error", error: e as any }; -} -}, -async toggleNewAccount(id: string) : Promise> { - try { - return { status: "ok", data: await TAURI_INVOKE("toggle_new_account", { id }) }; -} catch (e) { - if(e instanceof Error) throw e; - else return { status: "error", error: e as any }; -} -}, async hasSigner(id: string) : Promise> { try { return { status: "ok", data: await TAURI_INVOKE("has_signer", { id }) }; @@ -312,9 +312,9 @@ async verifyNip05(id: string, nip05: string) : Promise> else return { status: "error", error: e as any }; } }, -async getEventMeta(content: string) : Promise> { +async getMetaFromEvent(content: string) : Promise> { try { - return { status: "ok", data: await TAURI_INVOKE("get_event_meta", { content }) }; + return { status: "ok", data: await TAURI_INVOKE("get_meta_from_event", { content }) }; } catch (e) { if(e instanceof Error) throw e; else return { status: "error", error: e as any }; @@ -328,14 +328,6 @@ async getEvent(id: string) : Promise> { else return { status: "error", error: e as any }; } }, -async getEventFrom(id: string, relayHint: string) : Promise> { - try { - return { status: "ok", data: await TAURI_INVOKE("get_event_from", { id, relayHint }) }; -} catch (e) { - if(e instanceof Error) throw e; - else return { status: "error", error: e as any }; -} -}, async getReplies(id: string) : Promise> { try { return { status: "ok", data: await TAURI_INVOKE("get_replies", { id }) }; diff --git a/src/components/note/content.tsx b/src/components/note/content.tsx index 651c5e99..ada40d18 100644 --- a/src/components/note/content.tsx +++ b/src/components/note/content.tsx @@ -1,7 +1,7 @@ import { appSettings, cn } from "@/commons"; import { useStore } from "@tanstack/react-store"; import { nanoid } from "nanoid"; -import { type ReactNode, memo, useMemo, useState } from "react"; +import { type ReactNode, useMemo, useState } from "react"; import reactStringReplace from "react-string-replace"; import { Hashtag } from "./mentions/hashtag"; import { MentionNote } from "./mentions/note"; @@ -22,7 +22,6 @@ export function NoteContent({ }) { const event = useNoteContext(); const visible = useStore(appSettings, (state) => state.display_media); - const warning = useMemo(() => event.warning, [event]); const content = useMemo(() => { try { // Get parsed meta @@ -88,52 +87,48 @@ export function NoteContent({ } }, [event.content]); + const [blurred, setBlurred] = useState(() => + event.warning ? event.warning.length > 0 : false, + ); + return (
- -
- {content} -
- {visible ? ( - event.meta?.images.length ? ( - - ) : null - ) : null} + {!blurred ? ( + <> +
+ {content} +
+ {visible ? ( + event.meta?.images.length ? ( + + ) : null + ) : null} + + ) : ( +
+

+ The content is hidden because the author marked it with a warning + for a reason: {event.warning} +

+ +
+ )}
); } - -const ContentWarning = memo(function ContentWarning({ - warning, -}: { warning: string }) { - const [blurred, setBlurred] = useState(() => warning?.length > 0); - - if (!blurred) { - return null; - } - - return ( -
-
-

- The content is hidden because the author -
- marked it with a warning for a reason: -

-

{warning}

- -
-
- ); -}); diff --git a/src/components/note/mentions/note.tsx b/src/components/note/mentions/note.tsx index 63e6df99..6ca88a63 100644 --- a/src/components/note/mentions/note.tsx +++ b/src/components/note/mentions/note.tsx @@ -17,16 +17,19 @@ export const MentionNote = memo(function MentionNote({ return (
-
+
{isLoading ? ( -
+
+ Loadng note
) : isError || !event ? ( -
+

- {error.message || "Note can be found with your current relay set"} + {error?.message ?? + "Cannot found this note within your current relay set"}

+

{eventId}

) : ( diff --git a/src/components/note/preview/image.tsx b/src/components/note/preview/image.tsx index 265416a6..01722483 100644 --- a/src/components/note/preview/image.tsx +++ b/src/components/note/preview/image.tsx @@ -39,10 +39,6 @@ export function ImagePreview({ url }: { url: string }) { decoding="async" style={{ contentVisibility: "auto" }} className="max-h-[400px] w-full h-auto object-cover rounded-lg outline outline-1 -outline-offset-1 outline-black/15" - onError={({ currentTarget }) => { - currentTarget.onerror = null; - currentTarget.src = "/404.jpg"; - }} />
); diff --git a/src/components/note/preview/images.tsx b/src/components/note/preview/images.tsx index c22d5f05..1d6c00da 100644 --- a/src/components/note/preview/images.tsx +++ b/src/components/note/preview/images.tsx @@ -21,9 +21,15 @@ export function Images({ urls }: { urls: string[] }) { let newUrls: string[]; if (urls.length === 1) { - newUrls = urls.map( - (url) => `${service}?url=${url}&ll&af&default=1&n=-1`, - ); + newUrls = urls.map((url) => { + if (url.includes("_next/")) { + return url; + } + if (url.includes("bsky.network")) { + return url; + } + return `${service}?url=${url}&ll&af&default=1&n=-1`; + }); } else { newUrls = urls.map( (url) => `${service}?url=${url}&w=480&h=640&ll&af&default=1&n=-1`, @@ -83,10 +89,6 @@ export function Images({ urls }: { urls: string[] }) { className="max-h-[400px] w-auto object-cover rounded-lg outline outline-1 -outline-offset-1 outline-black/15" onClick={() => urls[0]} onKeyDown={() => urls[0]} - onError={({ currentTarget }) => { - currentTarget.onerror = null; - currentTarget.src = "/404.jpg"; - }} />
); @@ -162,10 +164,6 @@ function LazyImage({ url, inView }: { url: string; inView: boolean }) { onClick={() => open(url)} onKeyDown={() => open(url)} onLoad={setLoaded} - onError={({ currentTarget }) => { - currentTarget.onerror = null; - currentTarget.src = "/404.jpg"; - }} />
); diff --git a/src/components/user/avatar.tsx b/src/components/user/avatar.tsx index de602242..b50b7296 100644 --- a/src/components/user/avatar.tsx +++ b/src/components/user/avatar.tsx @@ -18,6 +18,9 @@ export function UserAvatar({ className }: { className?: string }) { if (user.profile?.picture.includes("_next/")) { return user.profile?.picture; } + if (user.profile?.picture.includes("bsky.network")) { + return user.profile?.picture; + } return `${service}?url=${user.profile?.picture}&w=100&h=100&n=-1&default=${user.profile?.picture}`; } else { return user.profile?.picture; diff --git a/src/routes.gen.ts b/src/routes.gen.ts index f88a48f5..17e5e314 100644 --- a/src/routes.gen.ts +++ b/src/routes.gen.ts @@ -51,9 +51,6 @@ const ColumnsLayoutSearchLazyImport = createFileRoute( const ColumnsLayoutOnboardingLazyImport = createFileRoute( '/columns/_layout/onboarding', )() -const ColumnsLayoutLaunchpadLazyImport = createFileRoute( - '/columns/_layout/launchpad', -)() const ColumnsLayoutUsersIdLazyImport = createFileRoute( '/columns/_layout/users/$id', )() @@ -63,6 +60,9 @@ const ColumnsLayoutRepliesIdLazyImport = createFileRoute( const ColumnsLayoutNotificationIdLazyImport = createFileRoute( '/columns/_layout/notification/$id', )() +const ColumnsLayoutLaunchpadIdLazyImport = createFileRoute( + '/columns/_layout/launchpad/$id', +)() const ColumnsLayoutEventsIdLazyImport = createFileRoute( '/columns/_layout/events/$id', )() @@ -171,15 +171,6 @@ const ColumnsLayoutOnboardingLazyRoute = import('./routes/columns/_layout/onboarding.lazy').then((d) => d.Route), ) -const ColumnsLayoutLaunchpadLazyRoute = ColumnsLayoutLaunchpadLazyImport.update( - { - path: '/launchpad', - getParentRoute: () => ColumnsLayoutRoute, - } as any, -).lazy(() => - import('./routes/columns/_layout/launchpad.lazy').then((d) => d.Route), -) - const SettingsIdWalletRoute = SettingsIdWalletImport.update({ path: '/wallet', getParentRoute: () => SettingsIdLazyRoute, @@ -245,6 +236,14 @@ const ColumnsLayoutNotificationIdLazyRoute = ), ) +const ColumnsLayoutLaunchpadIdLazyRoute = + ColumnsLayoutLaunchpadIdLazyImport.update({ + path: '/launchpad/$id', + getParentRoute: () => ColumnsLayoutRoute, + } as any).lazy(() => + import('./routes/columns/_layout/launchpad.$id.lazy').then((d) => d.Route), + ) + const ColumnsLayoutEventsIdLazyRoute = ColumnsLayoutEventsIdLazyImport.update({ path: '/events/$id', getParentRoute: () => ColumnsLayoutRoute, @@ -436,13 +435,6 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof SettingsIdWalletImport parentRoute: typeof SettingsIdLazyImport } - '/columns/_layout/launchpad': { - id: '/columns/_layout/launchpad' - path: '/launchpad' - fullPath: '/columns/launchpad' - preLoaderRoute: typeof ColumnsLayoutLaunchpadLazyImport - parentRoute: typeof ColumnsLayoutImport - } '/columns/_layout/onboarding': { id: '/columns/_layout/onboarding' path: '/onboarding' @@ -513,6 +505,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof ColumnsLayoutEventsIdLazyImport parentRoute: typeof ColumnsLayoutImport } + '/columns/_layout/launchpad/$id': { + id: '/columns/_layout/launchpad/$id' + path: '/launchpad/$id' + fullPath: '/columns/launchpad/$id' + preLoaderRoute: typeof ColumnsLayoutLaunchpadIdLazyImport + parentRoute: typeof ColumnsLayoutImport + } '/columns/_layout/notification/$id': { id: '/columns/_layout/notification/$id' path: '/notification/$id' @@ -569,7 +568,6 @@ const ColumnsLayoutCreateNewsfeedRouteWithChildren = interface ColumnsLayoutRouteChildren { ColumnsLayoutCreateNewsfeedRoute: typeof ColumnsLayoutCreateNewsfeedRouteWithChildren ColumnsLayoutGlobalRoute: typeof ColumnsLayoutGlobalRoute - ColumnsLayoutLaunchpadLazyRoute: typeof ColumnsLayoutLaunchpadLazyRoute ColumnsLayoutOnboardingLazyRoute: typeof ColumnsLayoutOnboardingLazyRoute ColumnsLayoutSearchLazyRoute: typeof ColumnsLayoutSearchLazyRoute ColumnsLayoutTrendingLazyRoute: typeof ColumnsLayoutTrendingLazyRoute @@ -578,6 +576,7 @@ interface ColumnsLayoutRouteChildren { ColumnsLayoutNewsfeedIdRoute: typeof ColumnsLayoutNewsfeedIdRoute ColumnsLayoutStoriesIdRoute: typeof ColumnsLayoutStoriesIdRoute ColumnsLayoutEventsIdLazyRoute: typeof ColumnsLayoutEventsIdLazyRoute + ColumnsLayoutLaunchpadIdLazyRoute: typeof ColumnsLayoutLaunchpadIdLazyRoute ColumnsLayoutNotificationIdLazyRoute: typeof ColumnsLayoutNotificationIdLazyRoute ColumnsLayoutRepliesIdLazyRoute: typeof ColumnsLayoutRepliesIdLazyRoute ColumnsLayoutUsersIdLazyRoute: typeof ColumnsLayoutUsersIdLazyRoute @@ -587,7 +586,6 @@ const ColumnsLayoutRouteChildren: ColumnsLayoutRouteChildren = { ColumnsLayoutCreateNewsfeedRoute: ColumnsLayoutCreateNewsfeedRouteWithChildren, ColumnsLayoutGlobalRoute: ColumnsLayoutGlobalRoute, - ColumnsLayoutLaunchpadLazyRoute: ColumnsLayoutLaunchpadLazyRoute, ColumnsLayoutOnboardingLazyRoute: ColumnsLayoutOnboardingLazyRoute, ColumnsLayoutSearchLazyRoute: ColumnsLayoutSearchLazyRoute, ColumnsLayoutTrendingLazyRoute: ColumnsLayoutTrendingLazyRoute, @@ -596,6 +594,7 @@ const ColumnsLayoutRouteChildren: ColumnsLayoutRouteChildren = { ColumnsLayoutNewsfeedIdRoute: ColumnsLayoutNewsfeedIdRoute, ColumnsLayoutStoriesIdRoute: ColumnsLayoutStoriesIdRoute, ColumnsLayoutEventsIdLazyRoute: ColumnsLayoutEventsIdLazyRoute, + ColumnsLayoutLaunchpadIdLazyRoute: ColumnsLayoutLaunchpadIdLazyRoute, ColumnsLayoutNotificationIdLazyRoute: ColumnsLayoutNotificationIdLazyRoute, ColumnsLayoutRepliesIdLazyRoute: ColumnsLayoutRepliesIdLazyRoute, ColumnsLayoutUsersIdLazyRoute: ColumnsLayoutUsersIdLazyRoute, @@ -654,7 +653,6 @@ export interface FileRoutesByFullPath { '/settings/$id/profile': typeof SettingsIdProfileRoute '/settings/$id/relay': typeof SettingsIdRelayRoute '/settings/$id/wallet': typeof SettingsIdWalletRoute - '/columns/launchpad': typeof ColumnsLayoutLaunchpadLazyRoute '/columns/onboarding': typeof ColumnsLayoutOnboardingLazyRoute '/columns/search': typeof ColumnsLayoutSearchLazyRoute '/columns/trending': typeof ColumnsLayoutTrendingLazyRoute @@ -665,6 +663,7 @@ export interface FileRoutesByFullPath { '/columns/newsfeed/$id': typeof ColumnsLayoutNewsfeedIdRoute '/columns/stories/$id': typeof ColumnsLayoutStoriesIdRoute '/columns/events/$id': typeof ColumnsLayoutEventsIdLazyRoute + '/columns/launchpad/$id': typeof ColumnsLayoutLaunchpadIdLazyRoute '/columns/notification/$id': typeof ColumnsLayoutNotificationIdLazyRoute '/columns/replies/$id': typeof ColumnsLayoutRepliesIdLazyRoute '/columns/users/$id': typeof ColumnsLayoutUsersIdLazyRoute @@ -689,7 +688,6 @@ export interface FileRoutesByTo { '/settings/$id/profile': typeof SettingsIdProfileRoute '/settings/$id/relay': typeof SettingsIdRelayRoute '/settings/$id/wallet': typeof SettingsIdWalletRoute - '/columns/launchpad': typeof ColumnsLayoutLaunchpadLazyRoute '/columns/onboarding': typeof ColumnsLayoutOnboardingLazyRoute '/columns/search': typeof ColumnsLayoutSearchLazyRoute '/columns/trending': typeof ColumnsLayoutTrendingLazyRoute @@ -700,6 +698,7 @@ export interface FileRoutesByTo { '/columns/newsfeed/$id': typeof ColumnsLayoutNewsfeedIdRoute '/columns/stories/$id': typeof ColumnsLayoutStoriesIdRoute '/columns/events/$id': typeof ColumnsLayoutEventsIdLazyRoute + '/columns/launchpad/$id': typeof ColumnsLayoutLaunchpadIdLazyRoute '/columns/notification/$id': typeof ColumnsLayoutNotificationIdLazyRoute '/columns/replies/$id': typeof ColumnsLayoutRepliesIdLazyRoute '/columns/users/$id': typeof ColumnsLayoutUsersIdLazyRoute @@ -727,7 +726,6 @@ export interface FileRoutesById { '/settings/$id/profile': typeof SettingsIdProfileRoute '/settings/$id/relay': typeof SettingsIdRelayRoute '/settings/$id/wallet': typeof SettingsIdWalletRoute - '/columns/_layout/launchpad': typeof ColumnsLayoutLaunchpadLazyRoute '/columns/_layout/onboarding': typeof ColumnsLayoutOnboardingLazyRoute '/columns/_layout/search': typeof ColumnsLayoutSearchLazyRoute '/columns/_layout/trending': typeof ColumnsLayoutTrendingLazyRoute @@ -738,6 +736,7 @@ export interface FileRoutesById { '/columns/_layout/newsfeed/$id': typeof ColumnsLayoutNewsfeedIdRoute '/columns/_layout/stories/$id': typeof ColumnsLayoutStoriesIdRoute '/columns/_layout/events/$id': typeof ColumnsLayoutEventsIdLazyRoute + '/columns/_layout/launchpad/$id': typeof ColumnsLayoutLaunchpadIdLazyRoute '/columns/_layout/notification/$id': typeof ColumnsLayoutNotificationIdLazyRoute '/columns/_layout/replies/$id': typeof ColumnsLayoutRepliesIdLazyRoute '/columns/_layout/users/$id': typeof ColumnsLayoutUsersIdLazyRoute @@ -765,7 +764,6 @@ export interface FileRouteTypes { | '/settings/$id/profile' | '/settings/$id/relay' | '/settings/$id/wallet' - | '/columns/launchpad' | '/columns/onboarding' | '/columns/search' | '/columns/trending' @@ -776,6 +774,7 @@ export interface FileRouteTypes { | '/columns/newsfeed/$id' | '/columns/stories/$id' | '/columns/events/$id' + | '/columns/launchpad/$id' | '/columns/notification/$id' | '/columns/replies/$id' | '/columns/users/$id' @@ -799,7 +798,6 @@ export interface FileRouteTypes { | '/settings/$id/profile' | '/settings/$id/relay' | '/settings/$id/wallet' - | '/columns/launchpad' | '/columns/onboarding' | '/columns/search' | '/columns/trending' @@ -810,6 +808,7 @@ export interface FileRouteTypes { | '/columns/newsfeed/$id' | '/columns/stories/$id' | '/columns/events/$id' + | '/columns/launchpad/$id' | '/columns/notification/$id' | '/columns/replies/$id' | '/columns/users/$id' @@ -835,7 +834,6 @@ export interface FileRouteTypes { | '/settings/$id/profile' | '/settings/$id/relay' | '/settings/$id/wallet' - | '/columns/_layout/launchpad' | '/columns/_layout/onboarding' | '/columns/_layout/search' | '/columns/_layout/trending' @@ -846,6 +844,7 @@ export interface FileRouteTypes { | '/columns/_layout/newsfeed/$id' | '/columns/_layout/stories/$id' | '/columns/_layout/events/$id' + | '/columns/_layout/launchpad/$id' | '/columns/_layout/notification/$id' | '/columns/_layout/replies/$id' | '/columns/_layout/users/$id' @@ -938,7 +937,6 @@ export const routeTree = rootRoute "children": [ "/columns/_layout/create-newsfeed", "/columns/_layout/global", - "/columns/_layout/launchpad", "/columns/_layout/onboarding", "/columns/_layout/search", "/columns/_layout/trending", @@ -947,6 +945,7 @@ export const routeTree = rootRoute "/columns/_layout/newsfeed/$id", "/columns/_layout/stories/$id", "/columns/_layout/events/$id", + "/columns/_layout/launchpad/$id", "/columns/_layout/notification/$id", "/columns/_layout/replies/$id", "/columns/_layout/users/$id" @@ -1008,10 +1007,6 @@ export const routeTree = rootRoute "filePath": "settings.$id/wallet.tsx", "parent": "/settings/$id" }, - "/columns/_layout/launchpad": { - "filePath": "columns/_layout/launchpad.lazy.tsx", - "parent": "/columns/_layout" - }, "/columns/_layout/onboarding": { "filePath": "columns/_layout/onboarding.lazy.tsx", "parent": "/columns/_layout" @@ -1052,6 +1047,10 @@ export const routeTree = rootRoute "filePath": "columns/_layout/events.$id.lazy.tsx", "parent": "/columns/_layout" }, + "/columns/_layout/launchpad/$id": { + "filePath": "columns/_layout/launchpad.$id.lazy.tsx", + "parent": "/columns/_layout" + }, "/columns/_layout/notification/$id": { "filePath": "columns/_layout/notification.$id.lazy.tsx", "parent": "/columns/_layout" diff --git a/src/routes/_app.lazy.tsx b/src/routes/_app.lazy.tsx index edbffebc..0c5a8b7d 100644 --- a/src/routes/_app.lazy.tsx +++ b/src/routes/_app.lazy.tsx @@ -60,26 +60,24 @@ function Topbar() { data-tauri-drag-region className="relative z-[200] flex-1 flex items-center justify-end gap-4" > - {accounts?.length ? ( -
- - -
- ) : null} -
+
+ + +
+
); diff --git a/src/routes/_app/index.lazy.tsx b/src/routes/_app/index.lazy.tsx index 61f6b2d6..4a667e81 100644 --- a/src/routes/_app/index.lazy.tsx +++ b/src/routes/_app/index.lazy.tsx @@ -1,14 +1,15 @@ -import { appColumns } from "@/commons"; +import { commands } from "@/commands.gen"; +import { appColumns, displayNpub } from "@/commons"; import { Column, Spinner } from "@/components"; import { LumeWindow } from "@/system"; -import type { ColumnEvent, LumeColumn } from "@/types"; -import { ArrowLeft, ArrowRight, Plus, StackPlus } from "@phosphor-icons/react"; +import type { ColumnEvent, LumeColumn, Metadata } from "@/types"; +import { ArrowLeft, ArrowRight, Plus } from "@phosphor-icons/react"; import { createLazyFileRoute } from "@tanstack/react-router"; import { useStore } from "@tanstack/react-store"; import { listen } from "@tauri-apps/api/event"; +import { Menu, MenuItem } from "@tauri-apps/api/menu"; import { getCurrentWindow } from "@tauri-apps/api/window"; import useEmblaCarousel from "embla-carousel-react"; -import { nanoid } from "nanoid"; import { type ReactNode, useCallback, @@ -45,11 +46,14 @@ function Screen() { }, []); const add = useDebouncedCallback((column: LumeColumn) => { - column.label = `${column.label}-${nanoid()}`; // update col label - appColumns.setState((prev) => [column, ...prev]); + const exist = columns.find((col) => col.label === column.label); - if (emblaApi) { - emblaApi.scrollTo(0, true); + if (!exist) { + appColumns.setState((prev) => [column, ...prev]); + + if (emblaApi) { + emblaApi.scrollTo(0, true); + } } }, 150); @@ -141,47 +145,79 @@ function Screen() { )) )} -
-
- -
-
+
- ); } +function OpenLaunchpad() { + const { accounts } = Route.useRouteContext(); + + const showContextMenu = useCallback( + async (e: React.MouseEvent) => { + e.preventDefault(); + + const list: Promise[] = []; + + for (const account of accounts) { + const res = await commands.getProfile(account); + let name = "unknown"; + + if (res.status === "ok") { + const profile: Metadata = JSON.parse(res.data); + name = profile.display_name ?? profile.name ?? "unknown"; + } + + list.push( + MenuItem.new({ + text: `Open Launchpad for ${name} (${displayNpub(account, 16)})`, + action: () => LumeWindow.openLaunchpad(account), + }), + ); + } + + const items = await Promise.all(list); + const menu = await Menu.new({ items }); + + await menu.popup().catch((e) => console.error(e)); + }, + [accounts], + ); + + return ( +
+
+ +
+
+ ); +} + function Toolbar({ children }: { children: ReactNode[] }) { const [domReady, setDomReady] = useState(false); diff --git a/src/routes/_app/index.tsx b/src/routes/_app/index.tsx index 80954636..1b3a0467 100644 --- a/src/routes/_app/index.tsx +++ b/src/routes/_app/index.tsx @@ -1,26 +1,37 @@ -import type { LumeColumn } from '@/types' -import { createFileRoute } from '@tanstack/react-router' -import { resolveResource } from '@tauri-apps/api/path' -import { readTextFile } from '@tauri-apps/plugin-fs' +import type { LumeColumn } from "@/types"; +import { createFileRoute } from "@tanstack/react-router"; +import { nanoid } from "nanoid"; -export const Route = createFileRoute('/_app/')({ - loader: async ({ context }) => { - const prevColumns = window.localStorage.getItem('columns') +export const Route = createFileRoute("/_app/")({ + loader: async ({ context }) => { + const accounts = context.accounts; + const prevColumns = window.localStorage.getItem("columns"); - if (!prevColumns) { - const resourcePath = await resolveResource('resources/columns.json') - const resourceFile = await readTextFile(resourcePath) - const content: LumeColumn[] = JSON.parse(resourceFile) - const initialAppColumns = content.filter((col) => col.default) + let initialAppColumns: LumeColumn[] = []; - return initialAppColumns - } else { - const parsed: LumeColumn[] = JSON.parse(prevColumns) - const initialAppColumns = parsed.filter((item) => - item.account ? context.accounts.includes(item.account) : item, - ) + if (!prevColumns || prevColumns.length < 1) { + initialAppColumns.push({ + label: "onboarding", + name: "Onboarding", + url: "/columns/onboarding", + }); - return initialAppColumns - } - }, -}) + for (const account of accounts) { + initialAppColumns.push({ + label: `launchpad-${nanoid()}`, + name: "Launchpad", + url: `/columns/launchpad/${account}`, + account, + }); + } + } else { + const parsed: LumeColumn[] = JSON.parse(prevColumns); + + initialAppColumns = parsed.filter((item) => + item.account ? context.accounts.includes(item.account) : item, + ); + } + + return initialAppColumns; + }, +}); diff --git a/src/routes/columns/_layout/launchpad.lazy.tsx b/src/routes/columns/_layout/launchpad.$id.lazy.tsx similarity index 62% rename from src/routes/columns/_layout/launchpad.lazy.tsx rename to src/routes/columns/_layout/launchpad.$id.lazy.tsx index 858b96da..45aa4767 100644 --- a/src/routes/columns/_layout/launchpad.lazy.tsx +++ b/src/routes/columns/_layout/launchpad.$id.lazy.tsx @@ -4,19 +4,35 @@ import { Spinner, User } from "@/components"; import { LumeWindow } from "@/system"; import type { LumeColumn, NostrEvent } from "@/types"; import { ArrowClockwise, Plus } from "@phosphor-icons/react"; +import * as Progress from "@radix-ui/react-progress"; import * as ScrollArea from "@radix-ui/react-scroll-area"; import { useQuery } from "@tanstack/react-query"; import { createLazyFileRoute } from "@tanstack/react-router"; +import { Channel } from "@tauri-apps/api/core"; import { resolveResource } from "@tauri-apps/api/path"; import { readTextFile } from "@tauri-apps/plugin-fs"; import { nanoid } from "nanoid"; -import { useCallback } from "react"; +import { useCallback, useEffect, useState } from "react"; -export const Route = createLazyFileRoute("/columns/_layout/launchpad")({ +export const Route = createLazyFileRoute("/columns/_layout/launchpad/$id")({ component: Screen, }); function Screen() { + const { id } = Route.useParams(); + const { data: isSync } = useQuery({ + queryKey: ["is-sync", id], + queryFn: async () => { + const res = await commands.isAccountSync(id); + + if (res.status === "ok") { + return res.data; + } else { + return false; + } + }, + }); + return ( - - - - + {!isSync ? ( + + ) : ( + <> + + + + + )} { + (async () => { + if (progress >= 100) { + await queryClient.invalidateQueries(); + } + })(); + }, [progress]); + + useEffect(() => { + const channel = new Channel(); + + channel.onmessage = (message) => { + setProgress(message); + }; + + (async () => { + const res = await commands.syncAccount(id, channel); + + if (res.status === "error") { + setError(res.error); + } + })(); + }, []); + + return ( +
+
+
+
+ + + + + {error ? error : "Syncing in Progress..."} + +
+
+ + Learn more about Negentropy + +
+
+ ); +} + function Groups() { - const { isLoading, data, refetch, isRefetching } = useQuery({ + const { isLoading, isError, error, data, refetch, isRefetching } = useQuery({ queryKey: ["others", "groups"], queryFn: async () => { const res = await commands.getAllGroups(); @@ -72,7 +159,7 @@ function Groups() { className="group flex flex-col rounded-xl overflow-hidden bg-white dark:bg-neutral-800/50 shadow-lg shadow-primary dark:ring-1 dark:ring-neutral-800" >
-
+
{item.tags .filter((tag) => tag[0] === "p") .map((tag) => ( @@ -148,12 +235,16 @@ function Groups() { Loading...
- ) : !data.length ? ( + ) : isError ? ( +
+

{error?.message ?? "Error"}

+
+ ) : !data?.length ? (

You don't have any groups yet.

) : ( - data.map((item) => renderItem(item)) + data?.map((item) => renderItem(item)) )}
@@ -161,7 +252,7 @@ function Groups() { } function Interests() { - const { isLoading, data, refetch, isRefetching } = useQuery({ + const { isLoading, isError, error, data, refetch, isRefetching } = useQuery({ queryKey: ["others", "interests"], queryFn: async () => { const res = await commands.getAllInterests(); @@ -193,7 +284,7 @@ function Interests() { className="group flex flex-col rounded-xl overflow-hidden bg-white dark:bg-neutral-800/50 shadow-lg shadow-primary dark:ring-1 dark:ring-neutral-800" >
-
+
{item.tags .filter((tag) => tag[0] === "t") .map((tag) => ( @@ -267,87 +358,16 @@ function Interests() { Loading...
- ) : !data.length ? ( + ) : isError ? ( +
+

{error?.message ?? "Error"}

+
+ ) : !data?.length ? (

You don't have any interests yet.

) : ( - data.map((item) => renderItem(item)) - )} -
-
- ); -} - -function Accounts() { - const { isLoading, data: accounts } = useQuery({ - queryKey: ["accounts"], - queryFn: async () => { - const res = await commands.getAccounts(); - return res; - }, - refetchOnWindowFocus: false, - }); - - return ( -
-
-

Accounts

-
-
- {isLoading ? ( -
- - Loading... -
- ) : ( - accounts.map((account) => ( -
-
- - - - - - -
-
-
-
Newsfeed
- -
-
-
Stories
- -
-
-
Notification
- -
-
-
- )) + data?.map((item) => renderItem(item)) )}
@@ -355,8 +375,9 @@ function Accounts() { } function Core() { - const { isLoading, data } = useQuery({ - queryKey: ["other-columns"], + const { id } = Route.useParams(); + const { data } = useQuery({ + queryKey: ["core-columns"], queryFn: async () => { const systemPath = "resources/columns.json"; const resourcePath = await resolveResource(systemPath); @@ -373,38 +394,56 @@ function Core() { return (
-

Others

+

Core

-
- {isLoading ? ( -
- - Loading... +
+
+
+
Newsfeed
+
- ) : ( - data.map((column) => ( +
+
Stories
+ +
+
+
Notification
+ +
+ {data?.map((column) => (
-
-
- {column.name} -
-
- {column.description} -
-
+
{column.name}
- )) - )} + ))} +
); diff --git a/src/routes/columns/_layout/notification.$id.lazy.tsx b/src/routes/columns/_layout/notification.$id.lazy.tsx index f2257b7a..84de223f 100644 --- a/src/routes/columns/_layout/notification.$id.lazy.tsx +++ b/src/routes/columns/_layout/notification.$id.lazy.tsx @@ -52,7 +52,11 @@ function Screen() { if (rootId) { if (reactions.has(rootId)) { - reactions.get(rootId).push(event); + const ev = reactions.get(rootId); + + if (ev) { + ev.push(event); + } } else { reactions.set(rootId, [event]); } @@ -64,7 +68,11 @@ function Screen() { if (rootId) { if (zaps.has(rootId)) { - zaps.get(rootId).push(event); + const ev = zaps.get(rootId); + + if (ev) { + ev.push(event); + } } else { zaps.set(rootId, [event]); } diff --git a/src/system/event.ts b/src/system/event.ts index ab280fd9..276354a8 100644 --- a/src/system/event.ts +++ b/src/system/event.ts @@ -157,7 +157,7 @@ export class LumeEvent { } static async build(event: NostrEvent) { - const query = await commands.getEventMeta(event.content); + const query = await commands.getMetaFromEvent(event.content); if (query.status === "ok") { event.meta = query.data; diff --git a/src/system/useEvent.ts b/src/system/useEvent.ts index 9160e31e..71947b39 100644 --- a/src/system/useEvent.ts +++ b/src/system/useEvent.ts @@ -1,6 +1,7 @@ import { commands } from "@/commands.gen"; import type { NostrEvent } from "@/types"; import { useQuery } from "@tanstack/react-query"; +import { nip19 } from "nostr-tools"; import { LumeEvent } from "./event"; export function useEvent(id: string, repost?: string) { @@ -10,7 +11,7 @@ export function useEvent(id: string, repost?: string) { try { if (repost?.length) { const nostrEvent: NostrEvent = JSON.parse(repost); - const res = await commands.getEventMeta(nostrEvent.content); + const res = await commands.getMetaFromEvent(nostrEvent.content); if (res.status === "ok") { nostrEvent.meta = res.data; @@ -19,12 +20,17 @@ export function useEvent(id: string, repost?: string) { return new LumeEvent(nostrEvent); } - // Validate ID - const normalizeId: string = id - .replace("nostr:", "") - .replace(/[^\w\s]/gi, ""); + let normalizedId = id.replace("nostr:", "").replace(/[^\w\s]/gi, ""); - const res = await commands.getEvent(normalizeId); + if (normalizedId.startsWith("nevent")) { + const decoded = nip19.decode(normalizedId); + + if (decoded.type === "nevent") { + normalizedId = decoded.data.id; + } + } + + const res = await commands.getEvent(normalizedId); if (res.status === "ok") { const data = res.data; diff --git a/src/system/useProfile.ts b/src/system/useProfile.ts index c4666867..ff95b79e 100644 --- a/src/system/useProfile.ts +++ b/src/system/useProfile.ts @@ -16,17 +16,17 @@ export function useProfile(pubkey: string, embed?: string) { return metadata; } - let normalizeId = pubkey.replace("nostr:", "").replace(/[^\w\s]/gi, ""); + let normalizedId = pubkey.replace("nostr:", "").replace(/[^\w\s]/gi, ""); - if (normalizeId.startsWith("nprofile")) { - const decoded = nip19.decode(normalizeId); + if (normalizedId.startsWith("nprofile")) { + const decoded = nip19.decode(normalizedId); if (decoded.type === "nprofile") { - normalizeId = decoded.data.pubkey; + normalizedId = decoded.data.pubkey; } } - const query = await commands.getProfile(normalizeId); + const query = await commands.getProfile(normalizedId); if (query.status === "ok") { return JSON.parse(query.data) as Metadata; diff --git a/src/system/window.ts b/src/system/window.ts index 83d84214..ef41ecdc 100644 --- a/src/system/window.ts +++ b/src/system/window.ts @@ -11,13 +11,14 @@ export const LumeWindow = { column, }); }, - openLaunchpad: async () => { + openLaunchpad: async (account: string) => { await getCurrentWindow().emit("columns", { type: "add", column: { label: "launchpad", name: "Launchpad", - url: "/columns/launchpad", + url: `/columns/launchpad/${account}`, + account, }, }); },