From cc7de41bfd8c682d7baf753b6cd5c4b3cf29bfb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=A8=E5=AE=AE=E8=93=AE?= <123083837+reyamir@users.noreply.github.com> Date: Tue, 22 Oct 2024 16:00:06 +0700 Subject: [PATCH] feat: Multi Accounts (#237) * wip: new sync * wip: restructure routes * update * feat: improve sync * feat: repost with multi-account * feat: improve sync * feat: publish with multi account * fix: settings screen * feat: add zap for multi accounts --- package.json | 1 + pnpm-lock.yaml | 20 + src-tauri/resources/columns.json | 49 +- src-tauri/src/commands/account.rs | 312 ++----- src-tauri/src/commands/event.rs | 79 +- src-tauri/src/commands/metadata.rs | 245 +++--- src-tauri/src/commands/mod.rs | 1 + src-tauri/src/commands/relay.rs | 18 +- src-tauri/src/commands/sync.rs | 204 +++++ src-tauri/src/commands/window.rs | 31 +- src-tauri/src/common.rs | 62 +- src-tauri/src/main.rs | 265 +++--- src/commands.gen.ts | 106 +-- src/commons.ts | 2 +- src/components/column.tsx | 67 +- src/components/conversation.tsx | 47 -- src/components/frame.tsx | 9 +- src/components/icons/publish.tsx | 19 + src/components/icons/quote.tsx | 23 + src/components/index.ts | 6 +- src/components/note/buttons/open.tsx | 2 +- src/components/note/buttons/quote.tsx | 40 + src/components/note/buttons/reply.tsx | 6 +- src/components/note/buttons/repost.tsx | 208 +++-- src/components/note/buttons/zap.tsx | 6 +- src/components/note/index.ts | 2 + src/components/note/mentions/note.tsx | 4 +- src/components/quote.tsx | 39 - src/components/reply.tsx | 39 +- src/components/repost.tsx | 2 +- src/components/text.tsx | 2 +- .../user/{followButton.tsx => button.tsx} | 4 +- src/components/user/index.ts | 4 +- src/routes.gen.ts | 780 ++++++++---------- src/routes/$account/_app.lazy.tsx | 117 --- src/routes/$account/_app.tsx | 3 - .../_settings/bitcoin-connect.lazy.tsx | 40 - .../$account/_settings/general.lazy.tsx | 185 ----- src/routes/$account/_settings/general.tsx | 17 - src/routes/$account/_settings/relay.tsx | 15 - src/routes/$account/_settings/wallet.lazy.tsx | 55 -- src/routes/$account/_settings/wallet.tsx | 21 - src/routes/$account/backup.tsx | 177 ---- src/routes/__root.tsx | 7 +- src/routes/_layout.lazy.tsx | 195 +++++ src/routes/_layout.tsx | 14 + .../index.lazy.tsx} | 64 +- src/routes/_layout/index.tsx | 3 + src/routes/auth/connect.lazy.tsx | 16 +- src/routes/auth/import.lazy.tsx | 19 +- src/routes/auth/new.lazy.tsx | 166 ---- src/routes/auth/watch.lazy.tsx | 106 +++ src/routes/columns/_layout.tsx | 10 +- src/routes/columns/_layout/global.tsx | 4 +- .../columns/_layout/groups.$id.lazy.tsx | 2 +- .../columns/_layout/interests.$id.lazy.tsx | 2 +- src/routes/columns/_layout/launchpad.lazy.tsx | 271 +++--- ...ewsfeed.lazy.tsx => newsfeed.$id.lazy.tsx} | 140 +--- .../_layout/{stories.tsx => newsfeed.$id.tsx} | 6 +- ...ion.lazy.tsx => notification.$id.lazy.tsx} | 18 +- ...{stories.lazy.tsx => stories.$id.lazy.tsx} | 6 +- .../_layout/{newsfeed.tsx => stories.$id.tsx} | 6 +- src/routes/columns/_layout/trending.lazy.tsx | 2 +- src/routes/columns/_layout/users.$id.lazy.tsx | 2 +- src/routes/index.lazy.tsx | 236 ------ src/routes/index.tsx | 24 - src/routes/loading.tsx | 58 -- .../-components/media.tsx | 5 - .../{editor => new-post}/-components/pow.tsx | 0 .../-components/warning.tsx | 0 src/routes/{editor => new-post}/index.tsx | 241 ++++-- src/routes/new.lazy.tsx | 20 +- src/routes/set-signer.$id.lazy.tsx | 92 +++ ...ettings.lazy.tsx => settings.$id.lazy.tsx} | 17 +- src/routes/settings.$id/general.lazy.tsx | 189 +++++ src/routes/settings.$id/general.tsx | 17 + .../profile.lazy.tsx | 6 +- .../_settings => settings.$id}/profile.tsx | 4 +- .../_settings => settings.$id}/relay.lazy.tsx | 5 +- src/routes/settings.$id/relay.tsx | 14 + src/routes/settings.$id/wallet.lazy.tsx | 50 ++ .../wallet.tsx} | 4 +- src/routes/zap.$id.lazy.tsx | 145 +++- src/routes/zap.$id.tsx | 3 +- src/system/event.ts | 14 +- src/system/useEvent.ts | 2 +- src/system/useProfile.ts | 2 +- src/system/window.ts | 53 +- src/types.ts | 12 +- 89 files changed, 2695 insertions(+), 2911 deletions(-) create mode 100644 src-tauri/src/commands/sync.rs delete mode 100644 src/components/conversation.tsx create mode 100644 src/components/icons/publish.tsx create mode 100644 src/components/icons/quote.tsx create mode 100644 src/components/note/buttons/quote.tsx delete mode 100644 src/components/quote.tsx rename src/components/user/{followButton.tsx => button.tsx} (92%) delete mode 100644 src/routes/$account/_app.lazy.tsx delete mode 100644 src/routes/$account/_app.tsx delete mode 100644 src/routes/$account/_settings/bitcoin-connect.lazy.tsx delete mode 100644 src/routes/$account/_settings/general.lazy.tsx delete mode 100644 src/routes/$account/_settings/general.tsx delete mode 100644 src/routes/$account/_settings/relay.tsx delete mode 100644 src/routes/$account/_settings/wallet.lazy.tsx delete mode 100644 src/routes/$account/_settings/wallet.tsx delete mode 100644 src/routes/$account/backup.tsx create mode 100644 src/routes/_layout.lazy.tsx create mode 100644 src/routes/_layout.tsx rename src/routes/{$account/_app.home.lazy.tsx => _layout/index.lazy.tsx} (81%) create mode 100644 src/routes/_layout/index.tsx delete mode 100644 src/routes/auth/new.lazy.tsx create mode 100644 src/routes/auth/watch.lazy.tsx rename src/routes/columns/_layout/{newsfeed.lazy.tsx => newsfeed.$id.lazy.tsx} (63%) rename src/routes/columns/_layout/{stories.tsx => newsfeed.$id.tsx} (56%) rename src/routes/columns/_layout/{notification.lazy.tsx => notification.$id.lazy.tsx} (97%) rename src/routes/columns/_layout/{stories.lazy.tsx => stories.$id.lazy.tsx} (98%) rename src/routes/columns/_layout/{newsfeed.tsx => stories.$id.tsx} (56%) delete mode 100644 src/routes/index.lazy.tsx delete mode 100644 src/routes/index.tsx delete mode 100644 src/routes/loading.tsx rename src/routes/{editor => new-post}/-components/media.tsx (90%) rename src/routes/{editor => new-post}/-components/pow.tsx (100%) rename src/routes/{editor => new-post}/-components/warning.tsx (100%) rename src/routes/{editor => new-post}/index.tsx (64%) create mode 100644 src/routes/set-signer.$id.lazy.tsx rename src/routes/{$account/_settings.lazy.tsx => settings.$id.lazy.tsx} (88%) create mode 100644 src/routes/settings.$id/general.lazy.tsx create mode 100644 src/routes/settings.$id/general.tsx rename src/routes/{$account/_settings => settings.$id}/profile.lazy.tsx (97%) rename src/routes/{$account/_settings => settings.$id}/profile.tsx (70%) rename src/routes/{$account/_settings => settings.$id}/relay.lazy.tsx (97%) create mode 100644 src/routes/settings.$id/relay.tsx create mode 100644 src/routes/settings.$id/wallet.lazy.tsx rename src/routes/{$account/_settings/bitcoin-connect.tsx => settings.$id/wallet.tsx} (66%) diff --git a/package.json b/package.json index 404a3176..a5b062f2 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "@radix-ui/react-tooltip": "^1.1.3", "@tanstack/query-persist-client-core": "^5.59.0", "@tanstack/react-query": "^5.59.0", + "@tanstack/react-query-devtools": "^5.59.15", "@tanstack/react-router": "^1.63.5", "@tanstack/react-store": "^0.5.5", "@tanstack/store": "^0.5.5", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5f745b8f..8dcc1a9e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -41,6 +41,9 @@ importers: '@tanstack/react-query': specifier: ^5.59.0 version: 5.59.0(react@19.0.0-rc-d025ddd3-20240722) + '@tanstack/react-query-devtools': + specifier: ^5.59.15 + version: 5.59.15(@tanstack/react-query@5.59.0(react@19.0.0-rc-d025ddd3-20240722))(react@19.0.0-rc-d025ddd3-20240722) '@tanstack/react-router': specifier: ^1.63.5 version: 1.63.5(@tanstack/router-generator@1.63.5)(react-dom@19.0.0-rc-d025ddd3-20240722(react@19.0.0-rc-d025ddd3-20240722))(react@19.0.0-rc-d025ddd3-20240722) @@ -1230,9 +1233,18 @@ packages: '@tanstack/query-core@5.59.0': resolution: {integrity: sha512-WGD8uIhX6/deH/tkZqPNcRyAhDUqs729bWKoByYHSogcshXfFbppOdTER5+qY7mFvu8KEFJwT0nxr8RfPTVh0Q==} + '@tanstack/query-devtools@5.58.0': + resolution: {integrity: sha512-iFdQEFXaYYxqgrv63ots+65FGI+tNp5ZS5PdMU1DWisxk3fez5HG3FyVlbUva+RdYS5hSLbxZ9aw3yEs97GNTw==} + '@tanstack/query-persist-client-core@5.59.0': resolution: {integrity: sha512-uGXnTgck1AX2xXDVj417vtQD4Sz3J1D5iPxVhfUc7f/fkY9Qad2X7Id9mZUtll1/m9z55DfHoXMXlx5H1JK6fQ==} + '@tanstack/react-query-devtools@5.59.15': + resolution: {integrity: sha512-rX28KTivkA2XEn3Fj9ckDtnTPY8giWYgssySSAperpVol4+th+NCij/MhLylfB+Mfg2JfCxOcwnM/fwzS8iSog==} + peerDependencies: + '@tanstack/react-query': ^5.59.15 + react: ^18 || ^19 + '@tanstack/react-query@5.59.0': resolution: {integrity: sha512-YDXp3OORbYR+8HNQx+lf4F73NoiCmCcSvZvgxE29OifmQFk0sBlO26NWLHpcNERo92tVk3w+JQ53/vkcRUY1hA==} peerDependencies: @@ -3259,10 +3271,18 @@ snapshots: '@tanstack/query-core@5.59.0': {} + '@tanstack/query-devtools@5.58.0': {} + '@tanstack/query-persist-client-core@5.59.0': dependencies: '@tanstack/query-core': 5.59.0 + '@tanstack/react-query-devtools@5.59.15(@tanstack/react-query@5.59.0(react@19.0.0-rc-d025ddd3-20240722))(react@19.0.0-rc-d025ddd3-20240722)': + dependencies: + '@tanstack/query-devtools': 5.58.0 + '@tanstack/react-query': 5.59.0(react@19.0.0-rc-d025ddd3-20240722) + react: 19.0.0-rc-d025ddd3-20240722 + '@tanstack/react-query@5.59.0(react@19.0.0-rc-d025ddd3-20240722)': dependencies: '@tanstack/query-core': 5.59.0 diff --git a/src-tauri/resources/columns.json b/src-tauri/resources/columns.json index 8120758b..9f2e3503 100644 --- a/src-tauri/resources/columns.json +++ b/src-tauri/resources/columns.json @@ -1,74 +1,37 @@ [ { "default": true, - "official": true, "label": "onboarding", "name": "Onboarding", "description": "Tips for Mastering Lume.", - "url": "/columns/onboarding", - "picture": "" + "url": "/columns/onboarding" }, { "default": true, - "official": true, - "label": "Launchpad", + "label": "launchpad", "name": "Launchpad", "description": "Expand your experiences.", - "url": "/columns/launchpad", - "picture": "" + "url": "/columns/launchpad" }, { "default": false, - "official": true, - "label": "newsfeed", - "name": "Newsfeed", - "description": "All notes from you're following.", - "url": "/columns/newsfeed", - "picture": "" - }, - { - "default": false, - "official": true, - "label": "notification", - "name": "Notification", - "description": "All things around you.", - "url": "/columns/notification", - "picture": "" - }, - { - "default": false, - "official": true, "label": "search", "name": "Search", "description": "Find anything.", - "url": "/columns/search", - "picture": "" + "url": "/columns/search" }, { "default": false, - "official": true, - "label": "stories", - "name": "Stories", - "description": "Keep up to date with your follows.", - "url": "/columns/stories", - "picture": "" - }, - { - "default": false, - "official": true, "label": "global_feeds", "name": "Global Feeds", "description": "All global notes from all connected relays.", - "url": "/columns/global", - "picture": "" + "url": "/columns/global" }, { "default": false, - "official": true, "label": "trending", "name": "Trending", "description": "Discover all trending notes.", - "url": "/columns/trending", - "picture": "" + "url": "/columns/trending" } ] diff --git a/src-tauri/src/commands/account.rs b/src-tauri/src/commands/account.rs index b71d6e6f..54b6945a 100644 --- a/src-tauri/src/commands/account.rs +++ b/src-tauri/src/commands/account.rs @@ -1,17 +1,11 @@ use keyring::Entry; -use keyring_search::{Limit, List, Search}; use nostr_sdk::prelude::*; use serde::{Deserialize, Serialize}; use specta::Type; -use std::{ - collections::HashSet, - fs::{self, File}, - str::FromStr, - time::Duration, -}; -use tauri::{Emitter, Manager, State}; +use std::{str::FromStr, time::Duration}; +use tauri::{Emitter, State}; -use crate::{Nostr, NOTIFICATION_SUB_ID}; +use crate::{common::get_all_accounts, Nostr}; #[derive(Debug, Clone, Serialize, Deserialize, Type)] struct Account { @@ -22,67 +16,31 @@ struct Account { #[tauri::command] #[specta::specta] pub fn get_accounts() -> Vec { - let search = Search::new().expect("Unexpected."); - let results = search.by_service("Lume Secret Storage"); - let list = List::list_credentials(&results, Limit::All); - let accounts: HashSet = list - .split_whitespace() - .filter(|v| v.starts_with("npub1")) - .map(String::from) - .collect(); - - accounts.into_iter().collect() + get_all_accounts() } #[tauri::command] #[specta::specta] -pub async fn create_account( - name: String, - about: String, - picture: String, +pub async fn watch_account(key: String, state: State<'_, Nostr>) -> Result { + let public_key = PublicKey::from_str(&key).map_err(|e| e.to_string())?; + let bech32 = public_key.to_bech32().map_err(|e| e.to_string())?; + let keyring = Entry::new("Lume Secret Storage", &bech32).map_err(|e| e.to_string())?; + + keyring.set_password("").map_err(|e| e.to_string())?; + + // Update state + state.accounts.lock().unwrap().push(bech32.clone()); + + Ok(bech32) +} + +#[tauri::command] +#[specta::specta] +pub async fn import_account( + key: String, password: String, state: State<'_, Nostr>, ) -> Result { - let client = &state.client; - let keys = Keys::generate(); - let npub = keys.public_key().to_bech32().map_err(|e| e.to_string())?; - let secret_key = keys.secret_key(); - let enc = EncryptedSecretKey::new(secret_key, password, 16, KeySecurity::Medium) - .map_err(|err| err.to_string())?; - let enc_bech32 = enc.to_bech32().map_err(|err| err.to_string())?; - - // Save account - let keyring = Entry::new("Lume Secret Storage", &npub).map_err(|e| e.to_string())?; - let account = Account { - password: enc_bech32, - nostr_connect: None, - }; - let j = serde_json::to_string(&account).map_err(|e| e.to_string())?; - let _ = keyring.set_password(&j); - - let signer = NostrSigner::Keys(keys); - - // Update signer - client.set_signer(Some(signer)).await; - - let mut metadata = Metadata::new() - .display_name(name.clone()) - .name(name.to_lowercase()) - .about(about); - - if let Ok(url) = Url::parse(&picture) { - metadata = metadata.picture(url) - } - - match client.set_metadata(&metadata).await { - Ok(_) => Ok(npub), - Err(e) => Err(e.to_string()), - } -} - -#[tauri::command] -#[specta::specta] -pub async fn import_account(key: String, password: String) -> Result { let (npub, enc_bech32) = match key.starts_with("ncryptsec") { true => { let enc = EncryptedSecretKey::from_bech32(key).map_err(|err| err.to_string())?; @@ -117,6 +75,9 @@ pub async fn import_account(key: String, password: String) -> Result) -> Result Err(err.to_string()), @@ -208,34 +172,32 @@ pub fn delete_account(id: String) -> Result<(), String> { #[tauri::command] #[specta::specta] -pub fn is_account_sync(id: String, handle: tauri::AppHandle) -> bool { - let config_dir = handle - .path() - .app_config_dir() - .expect("Error: app config directory not found."); +pub async fn has_signer(id: String, state: State<'_, Nostr>) -> Result { + let client = &state.client; + let public_key = PublicKey::from_str(&id).map_err(|e| e.to_string())?; - fs::metadata(config_dir.join(id)).is_ok() + match client.signer().await { + Ok(signer) => { + // Emit reload in front-end + // handle.emit("signer", ()).unwrap(); + + let signer_key = signer.public_key().await.map_err(|e| e.to_string())?; + let is_match = signer_key == public_key; + + Ok(is_match) + } + Err(_) => Ok(false), + } } #[tauri::command] #[specta::specta] -pub fn create_sync_file(id: String, handle: tauri::AppHandle) -> bool { - let config_dir = handle - .path() - .app_config_dir() - .expect("Error: app config directory not found."); - - File::create(config_dir.join(id)).is_ok() -} - -#[tauri::command] -#[specta::specta] -pub async fn login( +pub async fn set_signer( account: String, password: String, state: State<'_, Nostr>, handle: tauri::AppHandle, -) -> Result { +) -> Result<(), String> { let client = &state.client; let keyring = Entry::new("Lume Secret Storage", &account).map_err(|e| e.to_string())?; @@ -247,7 +209,7 @@ pub async fn login( Err(e) => return Err(e.to_string()), }; - let public_key = match account.nostr_connect { + match account.nostr_connect { None => { let ncryptsec = EncryptedSecretKey::from_bech32(account.password).map_err(|e| e.to_string())?; @@ -255,196 +217,30 @@ pub async fn login( .to_secret_key(password) .map_err(|_| "Wrong password.")?; let keys = Keys::new(secret_key); - let public_key = keys.public_key().to_bech32().unwrap(); let signer = NostrSigner::Keys(keys); // Update signer client.set_signer(Some(signer)).await; + // Emit to front-end + handle.emit("signer-updated", ()).unwrap(); - public_key + Ok(()) } Some(bunker) => { let uri = NostrConnectURI::parse(bunker).map_err(|e| e.to_string())?; - let public_key = uri.signer_public_key().unwrap().to_bech32().unwrap(); let app_keys = Keys::from_str(&account.password).map_err(|e| e.to_string())?; match Nip46Signer::new(uri, app_keys, Duration::from_secs(120), None) { Ok(signer) => { // Update signer client.set_signer(Some(signer.into())).await; - public_key + // Emit to front-end + handle.emit("signer-updated", ()).unwrap(); + + Ok(()) } - Err(e) => return Err(e.to_string()), + Err(e) => Err(e.to_string()), } } - }; - - // NIP-65: Connect to user's relay list - // init_nip65(client, &public_key).await; - - // NIP-03: Get user's contact list - let contact_list = { - if let Ok(contacts) = client.get_contact_list(Some(Duration::from_secs(5))).await { - state.contact_list.lock().unwrap().clone_from(&contacts); - contacts - } else { - Vec::new() - } - }; - - let public_key_clone = public_key.clone(); - - // Run seperate thread for sync - tauri::async_runtime::spawn(async move { - let state = handle.state::(); - let client = &state.client; - let author = PublicKey::from_str(&public_key).unwrap(); - - // Subscribe for new notification - if let Ok(e) = client - .subscribe_with_id( - SubscriptionId::new(NOTIFICATION_SUB_ID), - vec![Filter::new().pubkey(author).since(Timestamp::now())], - None, - ) - .await - { - println!("Subscribed: {}", e.success.len()) - } - - // Get events from contact list - if !contact_list.is_empty() { - let authors: Vec = contact_list.iter().map(|f| f.public_key).collect(); - - // Syncing all metadata events from contact list - if let Ok(report) = client - .reconcile( - Filter::new() - .authors(authors.clone()) - .kinds(vec![Kind::Metadata, Kind::ContactList]) - .limit(authors.len() * 10), - NegentropyOptions::default(), - ) - .await - { - println!("Received: {}", report.received.len()); - } - - // Syncing all events from contact list - if let Ok(report) = client - .reconcile( - Filter::new() - .authors(authors.clone()) - .kinds(vec![Kind::TextNote, Kind::Repost, Kind::EventDeletion]) - .limit(authors.len() * 40), - NegentropyOptions::default(), - ) - .await - { - println!("Received: {}", report.received.len()); - } - - // Create the trusted public key list from contact list - // TODO: create a cached file - if let Ok(events) = client - .database() - .query(vec![Filter::new().kind(Kind::ContactList)]) - .await - { - let keys: Vec<&str> = events - .iter() - .flat_map(|event| { - event - .tags - .iter() - .filter(|t| t.kind() == TagKind::p()) - .filter_map(|t| t.content()) - .collect::>() - }) - .collect(); - - let trusted_list: HashSet = keys - .into_iter() - .filter_map(|item| { - if let Ok(pk) = PublicKey::from_str(item) { - Some(pk) - } else { - None - } - }) - .collect(); - - // Update app's state - state.trusted_list.lock().unwrap().clone_from(&trusted_list); - - let trusted_users: Vec = trusted_list.into_iter().collect(); - println!("Total trusted users: {}", trusted_users.len()); - - if let Ok(report) = client - .reconcile( - Filter::new() - .authors(trusted_users) - .kinds(vec![ - Kind::Metadata, - Kind::TextNote, - Kind::Repost, - Kind::EventDeletion, - ]) - .limit(5000), - NegentropyOptions::default(), - ) - .await - { - println!("Received: {}", report.received.len()) - } - }; - }; - - // Syncing all user's events - if let Ok(report) = client - .reconcile( - Filter::new().author(author).kinds(vec![ - Kind::TextNote, - Kind::Repost, - Kind::FollowSet, - Kind::InterestSet, - Kind::Interests, - Kind::EventDeletion, - Kind::MuteList, - Kind::BookmarkSet, - Kind::BlockedRelays, - Kind::EmojiSet, - Kind::RelaySet, - Kind::RelayList, - Kind::ApplicationSpecificData, - ]), - NegentropyOptions::default(), - ) - .await - { - println!("Received: {}", report.received.len()) - } - - // Syncing all tagged events for current user - if let Ok(report) = client - .reconcile( - Filter::new().pubkey(author).kinds(vec![ - Kind::TextNote, - Kind::Repost, - Kind::Reaction, - Kind::ZapReceipt, - ]), - NegentropyOptions::default(), - ) - .await - { - println!("Received: {}", report.received.len()) - }; - - handle - .emit("neg_synchronized", ()) - .expect("Something wrong!"); - }); - - Ok(public_key_clone) + } } diff --git a/src-tauri/src/commands/event.rs b/src-tauri/src/commands/event.rs index 467ac9e9..8682a532 100644 --- a/src-tauri/src/commands/event.rs +++ b/src-tauri/src/commands/event.rs @@ -410,16 +410,58 @@ pub async fn repost(raw: String, state: State<'_, Nostr>) -> Result) -> Result { +pub async fn is_reposted(id: String, state: State<'_, Nostr>) -> Result { let client = &state.client; + let accounts = state.accounts.lock().unwrap().clone(); + let event_id = EventId::parse(&id).map_err(|err| err.to_string())?; - match client.delete_event(event_id).await { - Ok(event_id) => Ok(event_id.to_string()), + let authors: Vec = accounts + .iter() + .map(|acc| PublicKey::from_str(acc).unwrap()) + .collect(); + + let filter = Filter::new() + .event(event_id) + .kind(Kind::Repost) + .authors(authors); + + match client.database().query(vec![filter]).await { + Ok(events) => Ok(!events.is_empty()), Err(err) => Err(err.to_string()), } } +#[tauri::command] +#[specta::specta] +pub async fn request_delete(id: String, state: State<'_, Nostr>) -> Result<(), String> { + let client = &state.client; + let event_id = EventId::from_str(&id).map_err(|err| err.to_string())?; + + match client.delete_event(event_id).await { + Ok(_) => Ok(()), + Err(e) => Err(e.to_string()), + } +} + +#[tauri::command] +#[specta::specta] +pub async fn is_deleted_event(id: String, state: State<'_, Nostr>) -> Result { + let client = &state.client; + let signer = client.signer().await.map_err(|err| err.to_string())?; + let public_key = signer.public_key().await.map_err(|err| err.to_string())?; + let event_id = EventId::from_str(&id).map_err(|err| err.to_string())?; + let filter = Filter::new() + .author(public_key) + .event(event_id) + .kind(Kind::EventDeletion); + + match client.database().query(vec![filter]).await { + Ok(events) => Ok(!events.is_empty()), + Err(e) => Err(e.to_string()), + } +} + #[tauri::command] #[specta::specta] pub async fn event_to_bech32(id: String, state: State<'_, Nostr>) -> Result { @@ -498,34 +540,3 @@ pub async fn search(query: String, state: State<'_, Nostr>) -> Result Err(e.to_string()), } } - -#[tauri::command] -#[specta::specta] -pub async fn is_deleted_event(id: String, state: State<'_, Nostr>) -> Result { - let client = &state.client; - let signer = client.signer().await.map_err(|err| err.to_string())?; - let public_key = signer.public_key().await.map_err(|err| err.to_string())?; - let event_id = EventId::from_str(&id).map_err(|err| err.to_string())?; - let filter = Filter::new() - .author(public_key) - .event(event_id) - .kind(Kind::EventDeletion); - - match client.database().query(vec![filter]).await { - Ok(events) => Ok(!events.is_empty()), - Err(e) => Err(e.to_string()), - } -} - -#[tauri::command] -#[specta::specta] -pub async fn request_delete(id: String, state: State<'_, Nostr>) -> Result<(), String> { - let client = &state.client; - let event_id = EventId::from_str(&id).map_err(|err| err.to_string())?; - let builder = EventBuilder::delete(vec![event_id]); - - match client.send_event_builder(builder).await { - Ok(_) => Ok(()), - Err(e) => Err(e.to_string()), - } -} diff --git a/src-tauri/src/commands/metadata.rs b/src-tauri/src/commands/metadata.rs index d29046bd..15b0c9ef 100644 --- a/src-tauri/src/commands/metadata.rs +++ b/src-tauri/src/commands/metadata.rs @@ -4,11 +4,10 @@ use serde::{Deserialize, Serialize}; use specta::Type; use std::{str::FromStr, time::Duration}; use tauri::{Emitter, Manager, State}; -use tauri_specta::Event; use crate::{ - common::{get_latest_event, process_event}, - NewSettings, Nostr, RichEvent, Settings, + common::{get_all_accounts, get_latest_event, get_tags_content, process_event}, + Nostr, RichEvent, Settings, }; #[derive(Clone, Serialize, Deserialize, Type)] @@ -104,14 +103,36 @@ pub async fn set_contact_list( #[tauri::command] #[specta::specta] -pub fn get_contact_list(state: State<'_, Nostr>) -> Result, String> { - let contact_list = state.contact_list.lock().unwrap().clone(); - let vec: Vec = contact_list - .into_iter() - .map(|f| f.public_key.to_hex()) - .collect(); +pub async fn get_contact_list(id: String, state: State<'_, Nostr>) -> Result, String> { + let client = &state.client; + let public_key = PublicKey::parse(&id).map_err(|e| e.to_string())?; - Ok(vec) + let filter = Filter::new() + .author(public_key) + .kind(Kind::ContactList) + .limit(1); + + let mut contact_list: Vec = Vec::new(); + + match client.database().query(vec![filter]).await { + Ok(events) => { + if let Some(event) = events.into_iter().next() { + for tag in event.tags.into_iter() { + if let Some(TagStandard::PublicKey { + public_key, + uppercase: false, + .. + }) = tag.to_standardized() + { + contact_list.push(public_key.to_hex()) + } + } + } + + Ok(contact_list) + } + Err(e) => Err(e.to_string()), + } } #[tauri::command] @@ -149,13 +170,27 @@ pub async fn set_profile(profile: Profile, state: State<'_, Nostr>) -> Result) -> Result { - let contact_list = &state.contact_list.lock().unwrap(); - let public_key = PublicKey::from_str(&id).map_err(|e| e.to_string())?; +pub async fn is_contact(id: String, state: State<'_, Nostr>) -> Result { + let client = &state.client; + let public_key = PublicKey::parse(&id).map_err(|e| e.to_string())?; - match contact_list.iter().position(|x| x.public_key == public_key) { - Some(_) => Ok(true), - None => Ok(false), + let filter = Filter::new() + .author(public_key) + .kind(Kind::ContactList) + .limit(1); + + match client.database().query(vec![filter]).await { + Ok(events) => { + if let Some(event) = events.into_iter().next() { + let hex = public_key.to_hex(); + let pubkeys = get_tags_content(&event, TagKind::p()); + + Ok(pubkeys.iter().any(|i| i == &hex)) + } else { + Ok(false) + } + } + Err(e) => Err(e.to_string()), } } @@ -267,9 +302,18 @@ pub async fn get_group(id: String, state: State<'_, Nostr>) -> Result) -> Result, String> { let client = &state.client; - let signer = client.signer().await.map_err(|e| e.to_string())?; - let public_key = signer.public_key().await.map_err(|e| e.to_string())?; - let filter = Filter::new().kind(Kind::FollowSet).author(public_key); + let accounts = get_all_accounts(); + let authors: Vec = accounts + .iter() + .filter_map(|acc| { + if let Ok(pk) = PublicKey::from_str(acc) { + Some(pk) + } else { + None + } + }) + .collect(); + let filter = Filter::new().kind(Kind::FollowSet).authors(authors); match client.database().query(vec![filter]).await { Ok(events) => Ok(process_event(client, events).await), @@ -347,11 +391,20 @@ pub async fn get_interest(id: String, state: State<'_, Nostr>) -> Result) -> Result, String> { let client = &state.client; - let signer = client.signer().await.map_err(|e| e.to_string())?; - let public_key = signer.public_key().await.map_err(|e| e.to_string())?; + let accounts = get_all_accounts(); + let authors: Vec = accounts + .iter() + .filter_map(|acc| { + if let Ok(pk) = PublicKey::from_str(acc) { + Some(pk) + } else { + None + } + }) + .collect(); let filter = Filter::new() .kinds(vec![Kind::InterestSet, Kind::Interests]) - .author(public_key); + .authors(authors); match client.database().query(vec![filter]).await { Ok(events) => Ok(process_event(client, events).await), @@ -361,7 +414,7 @@ pub async fn get_all_interests(state: State<'_, Nostr>) -> Result #[tauri::command] #[specta::specta] -pub async fn get_mention_list(state: State<'_, Nostr>) -> Result, String> { +pub async fn get_all_profiles(state: State<'_, Nostr>) -> Result, String> { let client = &state.client; let filter = Filter::new().kind(Kind::Metadata); @@ -396,7 +449,9 @@ pub async fn set_wallet(uri: &str, state: State<'_, Nostr>) -> Result) -> Result) -> Result { +pub async fn load_wallet(state: State<'_, Nostr>) -> Result<(), String> { let client = &state.client; - let keyring = - Entry::new("Lume Secret Storage", "Bitcoin Connect").map_err(|e| e.to_string())?; - match keyring.get_password() { - Ok(val) => { - let uri = NostrWalletConnectURI::from_str(&val).unwrap(); - let nwc = NWC::new(uri); + if client.zapper().await.is_err() { + let keyring = + Entry::new("Lume Secret Storage", "Bitcoin Connect").map_err(|e| e.to_string())?; - // Get current balance - let balance = nwc.get_balance().await; + match keyring.get_password() { + Ok(val) => { + let uri = NostrWalletConnectURI::from_str(&val).unwrap(); + let nwc = NWC::new(uri); - // Update zapper - client.set_zapper(nwc).await; - - match balance { - Ok(val) => Ok(val.to_string()), - Err(_) => Err("Get balance failed.".into()), + client.set_zapper(nwc).await; } + Err(_) => return Err("Wallet not found.".into()), } - Err(_) => Err("NWC not found.".into()), } + + Ok(()) } #[tauri::command] @@ -452,52 +503,40 @@ pub async fn remove_wallet(state: State<'_, Nostr>) -> Result<(), String> { #[tauri::command] #[specta::specta] pub async fn zap_profile( - id: &str, - amount: &str, - message: &str, + id: String, + amount: String, + message: Option, state: State<'_, Nostr>, -) -> Result { +) -> Result<(), String> { let client = &state.client; + let public_key: PublicKey = PublicKey::parse(id).map_err(|e| e.to_string())?; - - let details = ZapDetails::new(ZapType::Private).message(message); let num = amount.parse::().map_err(|e| e.to_string())?; + let details = message.map(|m| ZapDetails::new(ZapType::Public).message(m)); - if client.zap(public_key, num, Some(details)).await.is_ok() { - Ok(true) - } else { - Err("Zap profile failed".into()) + match client.zap(public_key, num, details).await { + Ok(()) => Ok(()), + Err(e) => Err(e.to_string()), } } #[tauri::command] #[specta::specta] pub async fn zap_event( - id: &str, - amount: &str, - message: &str, + id: String, + amount: String, + message: Option, state: State<'_, Nostr>, -) -> Result { +) -> Result<(), String> { let client = &state.client; - let event_id = match Nip19::from_bech32(id) { - Ok(val) => match val { - Nip19::EventId(id) => id, - Nip19::Event(event) => event.event_id, - _ => return Err("Event ID is invalid.".into()), - }, - Err(_) => match EventId::from_hex(id) { - Ok(val) => val, - Err(_) => return Err("Event ID is invalid.".into()), - }, - }; - let details = ZapDetails::new(ZapType::Private).message(message); + let event_id = EventId::from_str(&id).map_err(|e| e.to_string())?; let num = amount.parse::().map_err(|e| e.to_string())?; + let details = message.map(|m| ZapDetails::new(ZapType::Public).message(m)); - if client.zap(event_id, num, Some(details)).await.is_ok() { - Ok(true) - } else { - Err("Zap event failed".into()) + match client.zap(event_id, num, details).await { + Ok(()) => Ok(()), + Err(e) => Err(e.to_string()), } } @@ -544,27 +583,22 @@ pub async fn copy_friend(npub: &str, state: State<'_, Nostr>) -> Result) -> Result, String> { +pub async fn get_notifications(id: String, state: State<'_, Nostr>) -> Result, String> { let client = &state.client; + let public_key = PublicKey::from_str(&id).map_err(|e| e.to_string())?; - match client.signer().await { - Ok(signer) => { - let public_key = signer.public_key().await.unwrap(); - let filter = Filter::new() - .pubkey(public_key) - .kinds(vec![ - Kind::TextNote, - Kind::Repost, - Kind::Reaction, - Kind::ZapReceipt, - ]) - .limit(200); + let filter = Filter::new() + .pubkey(public_key) + .kinds(vec![ + Kind::TextNote, + Kind::Repost, + Kind::Reaction, + Kind::ZapReceipt, + ]) + .limit(200); - match client.database().query(vec![filter]).await { - Ok(events) => Ok(events.into_iter().map(|ev| ev.as_json()).collect()), - Err(err) => Err(err.to_string()), - } - } + match client.database().query(vec![filter]).await { + Ok(events) => Ok(events.into_iter().map(|ev| ev.as_json()).collect()), Err(err) => Err(err.to_string()), } } @@ -577,29 +611,11 @@ pub fn get_user_settings(state: State<'_, Nostr>) -> Result { #[tauri::command] #[specta::specta] -pub async fn set_user_settings( - settings: String, - state: State<'_, Nostr>, - handle: tauri::AppHandle, -) -> Result<(), String> { - let client = &state.client; - let tags = vec![Tag::identifier("lume_user_setting")]; - let builder = EventBuilder::new(Kind::ApplicationSpecificData, &settings, tags); +pub async fn set_user_settings(settings: String, state: State<'_, Nostr>) -> Result<(), String> { + let parsed: Settings = serde_json::from_str(&settings).map_err(|e| e.to_string())?; + state.settings.lock().unwrap().clone_from(&parsed); - match client.send_event_builder(builder).await { - Ok(_) => { - let parsed: Settings = serde_json::from_str(&settings).map_err(|e| e.to_string())?; - - // Update state - state.settings.lock().unwrap().clone_from(&parsed); - - // Emit new changes to frontend - NewSettings(parsed).emit(&handle).unwrap(); - - Ok(()) - } - Err(err) => Err(err.to_string()), - } + Ok(()) } #[tauri::command] @@ -613,12 +629,3 @@ pub async fn verify_nip05(id: String, nip05: &str) -> Result { Err(e) => Err(e.to_string()), } } - -#[tauri::command] -#[specta::specta] -pub fn is_trusted_user(id: String, state: State<'_, Nostr>) -> Result { - let trusted_list = &state.trusted_list.lock().unwrap(); - let public_key = PublicKey::from_str(&id).map_err(|e| e.to_string())?; - - Ok(trusted_list.contains(&public_key)) -} diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs index b768bfcc..a6543f97 100644 --- a/src-tauri/src/commands/mod.rs +++ b/src-tauri/src/commands/mod.rs @@ -2,4 +2,5 @@ pub mod account; pub mod event; pub mod metadata; pub mod relay; +pub mod sync; pub mod window; diff --git a/src-tauri/src/commands/relay.rs b/src-tauri/src/commands/relay.rs index bd49036d..eb86b83f 100644 --- a/src-tauri/src/commands/relay.rs +++ b/src-tauri/src/commands/relay.rs @@ -5,6 +5,7 @@ use specta::Type; use std::{ fs::OpenOptions, io::{self, BufRead, Write}, + str::FromStr, }; use tauri::{path::BaseDirectory, Manager, State}; @@ -18,8 +19,9 @@ pub struct Relays { #[tauri::command] #[specta::specta] -pub async fn get_relays(state: State<'_, Nostr>) -> Result { +pub async fn get_relays(id: String, state: State<'_, Nostr>) -> Result { let client = &state.client; + let public_key = PublicKey::from_str(&id).map_err(|e| e.to_string())?; let connected_relays = client .relays() @@ -28,9 +30,6 @@ pub async fn get_relays(state: State<'_, Nostr>) -> Result { .map(|url| url.to_string()) .collect::>(); - let signer = client.signer().await.map_err(|e| e.to_string())?; - let public_key = signer.public_key().await.map_err(|e| e.to_string())?; - let filter = Filter::new() .author(public_key) .kind(Kind::RelayList) @@ -98,13 +97,14 @@ pub async fn get_relays(state: State<'_, Nostr>) -> Result { pub async fn connect_relay(relay: &str, state: State<'_, Nostr>) -> Result { let client = &state.client; let status = client.add_relay(relay).await.map_err(|e| e.to_string())?; + if status { - println!("Connecting to relay: {}", relay); client .connect_relay(relay) .await .map_err(|e| e.to_string())?; } + Ok(status) } @@ -112,14 +112,12 @@ pub async fn connect_relay(relay: &str, state: State<'_, Nostr>) -> Result) -> Result { let client = &state.client; + client - .remove_relay(relay) - .await - .map_err(|e| e.to_string())?; - client - .disconnect_relay(relay) + .force_remove_relay(relay) .await .map_err(|e| e.to_string())?; + Ok(true) } diff --git a/src-tauri/src/commands/sync.rs b/src-tauri/src/commands/sync.rs new file mode 100644 index 00000000..17e35f7a --- /dev/null +++ b/src-tauri/src/commands/sync.rs @@ -0,0 +1,204 @@ +use nostr_sdk::prelude::*; +use serde::{Deserialize, Serialize}; +use specta::Type; +use std::str::FromStr; +use tauri::{AppHandle, Manager}; +use tauri_specta::Event as TauriEvent; + +use crate::{common::get_tags_content, Nostr}; + +#[derive(Clone, Serialize, Type, TauriEvent)] +pub struct NegentropyEvent { + kind: NegentropyKind, + total_event: i32, +} + +#[derive(Clone, Serialize, Deserialize, Type)] +pub enum NegentropyKind { + Profile, + Metadata, + Events, + EventIds, + Global, + Notification, + Others, +} + +pub fn run_fast_sync(accounts: Vec, app_handle: AppHandle) { + if accounts.is_empty() { + return; + }; + + let public_keys: Vec = accounts + .iter() + .filter_map(|acc| { + if let Ok(pk) = PublicKey::from_str(acc) { + Some(pk) + } else { + None + } + }) + .collect(); + + 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 profile + // + let profile = Filter::new() + .authors(public_keys.clone()) + .kind(Kind::Metadata) + .limit(4); + + if let Ok(report) = client + .reconcile_with(&bootstrap_relays, profile, NegentropyOptions::default()) + .await + { + NegentropyEvent { + kind: NegentropyKind::Profile, + total_event: report.received.len() as i32, + } + .emit(&app_handle) + .unwrap(); + } + + // NEG: Sync contact list + // + let contact_list = Filter::new() + .authors(public_keys.clone()) + .kind(Kind::ContactList) + .limit(4); + + if let Ok(report) = client + .reconcile_with( + &bootstrap_relays, + contact_list.clone(), + NegentropyOptions::default(), + ) + .await + { + NegentropyEvent { + kind: NegentropyKind::Metadata, + total_event: report.received.len() as i32, + } + .emit(&app_handle) + .unwrap(); + } + + // NEG: Sync events from contact list + // + if let Ok(events) = client.database().query(vec![contact_list]).await { + let pubkeys: Vec = events + .iter() + .flat_map(|ev| { + let tags = get_tags_content(ev, TagKind::p()); + tags.into_iter().filter_map(|p| { + if let Ok(pk) = PublicKey::from_hex(p) { + Some(pk) + } else { + None + } + }) + }) + .collect(); + + for chunk in pubkeys.chunks(500) { + if chunk.is_empty() { + break; + } + + let authors = chunk.to_owned(); + + // NEG: Sync event + // + let events = Filter::new() + .authors(authors.clone()) + .kinds(vec![Kind::TextNote, Kind::Repost]) + .limit(1000); + + if let Ok(report) = client + .reconcile_with(&bootstrap_relays, events, NegentropyOptions::default()) + .await + { + NegentropyEvent { + kind: NegentropyKind::Events, + total_event: report.received.len() as i32, + } + .emit(&app_handle) + .unwrap(); + } + + // NEG: Sync metadata + // + let metadata = Filter::new() + .authors(authors) + .kind(Kind::Metadata) + .limit(1000); + + if let Ok(report) = client + .reconcile_with(&bootstrap_relays, metadata, NegentropyOptions::default()) + .await + { + NegentropyEvent { + kind: NegentropyKind::Metadata, + total_event: report.received.len() as i32, + } + .emit(&app_handle) + .unwrap(); + } + } + } + + // NEG: Sync other metadata + // + let others = Filter::new().authors(public_keys.clone()).kinds(vec![ + Kind::Interests, + Kind::InterestSet, + Kind::FollowSet, + Kind::EventDeletion, + Kind::Custom(30315), + ]); + + if let Ok(report) = client + .reconcile_with(&bootstrap_relays, others, 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::Reaction, + Kind::TextNote, + Kind::Repost, + Kind::ZapReceipt, + ]) + .limit(10000); + + if let Ok(report) = client + .reconcile_with( + &bootstrap_relays, + notification, + NegentropyOptions::default(), + ) + .await + { + NegentropyEvent { + kind: NegentropyKind::Notification, + total_event: report.received.len() as i32, + } + .emit(&app_handle) + .unwrap(); + } + }); +} diff --git a/src-tauri/src/commands/window.rs b/src-tauri/src/commands/window.rs index 41259f35..4395513f 100644 --- a/src-tauri/src/commands/window.rs +++ b/src-tauri/src/commands/window.rs @@ -22,6 +22,7 @@ pub struct Window { maximizable: bool, minimizable: bool, hidden_title: bool, + closable: bool, } #[derive(Serialize, Deserialize, Type)] @@ -109,7 +110,7 @@ pub fn reload_column(label: String, app_handle: tauri::AppHandle) -> Result Result<(), String> { +pub fn open_window(window: Window, app_handle: tauri::AppHandle) -> 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(); @@ -117,6 +118,8 @@ pub fn open_window(window: Window, app_handle: tauri::AppHandle) -> Result<(), S let _ = current_window.show(); let _ = current_window.set_focus(); }; + + Ok(current_window.label().to_string()) } else { let new_window = WebviewWindowBuilder::new( &app_handle, @@ -131,6 +134,7 @@ pub fn open_window(window: Window, app_handle: tauri::AppHandle) -> Result<(), S .minimizable(window.minimizable) .maximizable(window.maximizable) .transparent(true) + .closable(window.closable) .effects(WindowEffectsConfig { state: None, effects: vec![Effect::UnderWindowBackground], @@ -142,24 +146,26 @@ pub fn open_window(window: Window, app_handle: tauri::AppHandle) -> Result<(), S // Restore native border new_window.add_border(None); - } - Ok(()) + Ok(new_window.label().to_string()) + } } #[tauri::command(async)] #[specta::specta] #[cfg(target_os = "windows")] -pub fn open_window(window: Window, app_handle: tauri::AppHandle) -> Result<(), String> { - if let Some(window) = app_handle.get_window(&window.label) { - if window.is_visible().unwrap_or_default() { - let _ = window.set_focus(); +pub fn open_window(window: Window, app_handle: tauri::AppHandle) -> 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 _ = window.show(); - let _ = window.set_focus(); + let _ = current_window.show(); + let _ = current_window.set_focus(); }; + + Ok(current_window.label().to_string()) } else { - let window = WebviewWindowBuilder::new( + let new_window = WebviewWindowBuilder::new( &app_handle, &window.label, WebviewUrl::App(PathBuf::from(window.url)), @@ -171,6 +177,7 @@ pub fn open_window(window: Window, app_handle: tauri::AppHandle) -> Result<(), S .maximizable(window.maximizable) .transparent(true) .decorations(false) + .closable(window.closable) .effects(WindowEffectsConfig { state: None, effects: vec![Effect::Mica], @@ -181,7 +188,9 @@ pub fn open_window(window: Window, app_handle: tauri::AppHandle) -> Result<(), S .unwrap(); // Set decoration - window.create_overlay_titlebar().unwrap(); + new_window.create_overlay_titlebar().unwrap(); + + Ok(new_window.label().to_string()) } Ok(()) diff --git a/src-tauri/src/common.rs b/src-tauri/src/common.rs index 292b342d..ae6d7c57 100644 --- a/src-tauri/src/common.rs +++ b/src-tauri/src/common.rs @@ -1,10 +1,11 @@ use futures::future::join_all; +use keyring_search::{Limit, List, Search}; use linkify::LinkFinder; use nostr_sdk::prelude::*; use reqwest::Client as ReqClient; use serde::Serialize; use specta::Type; -use std::{collections::HashSet, str::FromStr, time::Duration}; +use std::{collections::HashSet, str::FromStr}; use crate::RichEvent; @@ -18,6 +19,8 @@ pub struct Meta { } const IMAGES: [&str; 7] = ["jpg", "jpeg", "gif", "png", "webp", "avif", "tiff"]; +// const VIDEOS: [&str; 6] = ["mp4", "avi", "mov", "mkv", "wmv", "webm"]; + const NOSTR_EVENTS: [&str; 10] = [ "@nevent1", "@note1", @@ -30,6 +33,7 @@ const NOSTR_EVENTS: [&str; 10] = [ "Nostr:note1", "Nostr:nevent1", ]; + const NOSTR_MENTIONS: [&str; 10] = [ "@npub1", "nostr:npub1", @@ -47,6 +51,15 @@ pub fn get_latest_event(events: &Events) -> Option<&Event> { events.iter().next() } +pub fn get_tags_content(event: &Event, kind: TagKind) -> Vec { + event + .tags + .iter() + .filter(|t| t.kind() == kind) + .filter_map(|t| t.content().map(|content| content.to_string())) + .collect() +} + pub fn create_tags(content: &str) -> Vec { let mut tags: Vec = vec![]; let mut tag_set: HashSet = HashSet::new(); @@ -65,7 +78,7 @@ pub fn create_tags(content: &str) -> Vec { let hashtags = words .iter() .filter(|&&word| word.starts_with('#')) - .map(|&s| s.to_string()) + .map(|&s| s.to_string().replace("#", "").to_lowercase()) .collect::>(); for mention in mentions { @@ -128,6 +141,19 @@ pub fn create_tags(content: &str) -> Vec { tags } +pub fn get_all_accounts() -> Vec { + let search = Search::new().expect("Unexpected."); + let results = search.by_service("Lume Secret Storage"); + let list = List::list_credentials(&results, Limit::All); + let accounts: HashSet = list + .split_whitespace() + .filter(|v| v.starts_with("npub1") && !v.ends_with("Lume")) + .map(String::from) + .collect(); + + accounts.into_iter().collect() +} + pub async fn process_event(client: &Client, events: Events) -> Vec { // Remove event thread if event is TextNote let events: Vec = events @@ -201,38 +227,6 @@ pub async fn process_event(client: &Client, events: Events) -> Vec { join_all(futures).await } -pub async fn init_nip65(client: &Client, public_key: &str) { - let author = PublicKey::from_str(public_key).unwrap(); - let filter = Filter::new().author(author).kind(Kind::RelayList).limit(1); - - // client.add_relay("ws://127.0.0.1:1984").await.unwrap(); - // client.connect_relay("ws://127.0.0.1:1984").await.unwrap(); - - if let Ok(events) = client - .fetch_events(vec![filter], Some(Duration::from_secs(5))) - .await - { - if let Some(event) = events.first() { - let relay_list = nip65::extract_relay_list(event); - for (url, metadata) in relay_list { - let opts = match metadata { - Some(RelayMetadata::Read) => RelayOptions::new().read(true).write(false), - Some(_) => RelayOptions::new().write(true).read(false), - None => RelayOptions::default(), - }; - if let Err(e) = client.pool().add_relay(&url.to_string(), opts).await { - eprintln!("Failed to add relay {}: {:?}", url, e); - } - if let Err(e) = client.connect_relay(url.to_string()).await { - eprintln!("Failed to connect to relay {}: {:?}", url, e); - } else { - println!("Connecting to relay: {} - {:?}", url, metadata); - } - } - } - } -} - pub async fn parse_event(content: &str) -> Meta { let mut finder = LinkFinder::new(); finder.url_must_have_scheme(false); diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index d25b852a..484eca84 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -5,14 +5,20 @@ #[cfg(target_os = "macos")] use border::WebviewWindowExt as BorderWebviewWindowExt; -use commands::{account::*, event::*, metadata::*, relay::*, window::*}; -use common::parse_event; +use commands::{ + account::*, + event::*, + metadata::*, + relay::*, + sync::{run_fast_sync, NegentropyEvent}, + window::*, +}; +use common::{get_all_accounts, get_tags_content, parse_event}; use nostr_sdk::prelude::{Profile as DatabaseProfile, *}; use serde::{Deserialize, Serialize}; use specta::Type; use specta_typescript::Typescript; use std::{ - collections::HashSet, fs, io::{self, BufRead}, str::FromStr, @@ -30,8 +36,9 @@ pub mod common; pub struct Nostr { client: Client, settings: Mutex, - contact_list: Mutex>, - trusted_list: Mutex>, + accounts: Mutex>, + subscriptions: Mutex>, + bootstrap_relays: Mutex>, } #[derive(Clone, Serialize, Deserialize, Type)] @@ -76,14 +83,14 @@ struct Subscription { label: String, kind: SubKind, event_id: Option, + contacts: Option>, } #[derive(Serialize, Deserialize, Type, Clone, TauriEvent)] struct NewSettings(Settings); pub const DEFAULT_DIFFICULTY: u8 = 21; -pub const FETCH_LIMIT: usize = 100; -pub const NOTIFICATION_NEG_LIMIT: usize = 64; +pub const FETCH_LIMIT: usize = 50; pub const NOTIFICATION_SUB_ID: &str = "lume_notification"; fn main() { @@ -99,22 +106,21 @@ fn main() { get_bootstrap_relays, save_bootstrap_relays, get_accounts, - create_account, + watch_account, import_account, connect_account, get_private_key, delete_account, reset_password, - is_account_sync, - create_sync_file, - login, + has_signer, + set_signer, get_profile, set_profile, get_contact_list, set_contact_list, - check_contact, + is_contact, toggle_contact, - get_mention_list, + get_all_profiles, set_group, get_group, get_all_groups, @@ -131,7 +137,6 @@ fn main() { get_user_settings, set_user_settings, verify_nip05, - is_trusted_user, get_event_meta, get_event, get_event_from, @@ -142,12 +147,13 @@ fn main() { get_all_events_by_hashtags, get_local_events, get_global_events, - is_deleted_event, - request_delete, search, publish, reply, repost, + is_reposted, + request_delete, + is_deleted_event, event_to_bech32, user_to_bech32, create_column, @@ -158,7 +164,7 @@ fn main() { reopen_lume, quit ]) - .events(collect_events![Subscription, NewSettings]); + .events(collect_events![Subscription, NewSettings, NegentropyEvent]); #[cfg(debug_assertions)] builder @@ -179,6 +185,7 @@ fn main() { let handle = app.handle(); let handle_clone = handle.clone(); let handle_clone_child = handle_clone.clone(); + let handle_clone_sync = handle_clone_child.clone(); let main_window = app.get_webview_window("main").unwrap(); let config_dir = handle @@ -216,7 +223,7 @@ fn main() { } }); - let client = tauri::async_runtime::block_on(async move { + let (client, bootstrap_relays) = tauri::async_runtime::block_on(async move { // Setup database let database = NostrLMDB::open(config_dir.join("nostr-lmdb")) .expect("Error: cannot create database."); @@ -224,10 +231,10 @@ fn main() { // Config let opts = Options::new() .gossip(true) - .max_avg_latency(Duration::from_millis(500)) + .max_avg_latency(Duration::from_millis(800)) .automatic_authentication(false) .connection_timeout(Some(Duration::from_secs(20))) - .send_timeout(Some(Duration::from_secs(10))) + .send_timeout(Some(Duration::from_secs(20))) .timeout(Duration::from_secs(20)); // Setup nostr client @@ -279,17 +286,27 @@ fn main() { // Connect client.connect_with_timeout(Duration::from_secs(10)).await; - client + // Get all bootstrap relays + let bootstrap_relays: Vec = + client.pool().all_relays().await.into_keys().collect(); + + (client, bootstrap_relays) }); + let accounts = get_all_accounts(); + // Run fast sync for all accounts + run_fast_sync(accounts.clone(), handle_clone_sync); + // Create global state app.manage(Nostr { client, + accounts: Mutex::new(accounts), settings: Mutex::new(Settings::default()), - contact_list: Mutex::new(Vec::new()), - trusted_list: Mutex::new(HashSet::new()), + subscriptions: Mutex::new(Vec::new()), + bootstrap_relays: Mutex::new(bootstrap_relays), }); + // Handle subscription Subscription::listen_any(app, move |event| { let handle = handle_clone_child.to_owned(); let payload = event.payload; @@ -302,42 +319,84 @@ fn main() { SubKind::Subscribe => { let subscription_id = SubscriptionId::new(payload.label); - match payload.event_id { - Some(id) => { + if !client + .pool() + .subscriptions() + .await + .contains_key(&subscription_id) + { + // Update state + state + .subscriptions + .lock() + .unwrap() + .push(subscription_id.clone()); + + println!( + "Total subscriptions: {}", + state.subscriptions.lock().unwrap().len() + ); + + if let Some(id) = payload.event_id { let event_id = EventId::from_str(&id).unwrap(); let filter = Filter::new().event(event_id).since(Timestamp::now()); if let Err(e) = client - .subscribe_with_id(subscription_id, vec![filter], None) + .subscribe_with_id( + subscription_id.clone(), + vec![filter], + None, + ) .await { println!("Subscription error: {}", e) } } - None => { - let contact_list = state.contact_list.lock().unwrap().clone(); - if !contact_list.is_empty() { - let authors: Vec = - contact_list.iter().map(|f| f.public_key).collect(); - let filter = Filter::new() - .kinds(vec![Kind::TextNote, Kind::Repost]) - .authors(authors) - .since(Timestamp::now()); + if let Some(ids) = payload.contacts { + let authors: Vec = ids + .iter() + .filter_map(|item| { + if let Ok(pk) = PublicKey::from_str(item) { + Some(pk) + } else { + None + } + }) + .collect(); - if let Err(e) = client - .subscribe_with_id(subscription_id, vec![filter], None) - .await - { - println!("Subscription error: {}", e) - } + if let Err(e) = client + .subscribe_with_id( + subscription_id, + vec![Filter::new() + .kinds(vec![Kind::TextNote, Kind::Repost]) + .authors(authors) + .since(Timestamp::now())], + None, + ) + .await + { + println!("Subscription error: {}", e) } } - }; + } } SubKind::Unsubscribe => { let subscription_id = SubscriptionId::new(payload.label); + let mut sub_state = state.subscriptions.lock().unwrap().clone(); + + if let Some(pos) = sub_state.iter().position(|x| *x == subscription_id) + { + sub_state.remove(pos); + state.subscriptions.lock().unwrap().clone_from(&sub_state) + } + + println!( + "Total subscriptions: {}", + state.subscriptions.lock().unwrap().len() + ); + client.unsubscribe(subscription_id).await } } @@ -363,6 +422,30 @@ 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 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()) + } let allow_notification = match handle_clone.notification().request_permission() { Ok(_) => { @@ -377,6 +460,7 @@ 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 { @@ -394,6 +478,17 @@ 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() @@ -426,8 +521,12 @@ fn main() { event, } = message { + let tags = get_tags_content(&event, TagKind::p()); + // Handle events from notification subscription - if subscription_id == notification_id { + if subscription_id == notification_id + && tags.iter().any(|item| accounts.iter().any(|i| i == item)) + { // Send native notification if allow_notification { let author = client @@ -437,27 +536,46 @@ fn main() { .unwrap_or_else(|_| { DatabaseProfile::new(event.pubkey, Metadata::new()) }); - let metadata = author.metadata(); - send_event_notification(&event, metadata, &handle_clone); + send_event_notification( + &event, + author.metadata(), + &handle_clone, + ); } } - let label = subscription_id.to_string(); - let raw = event.as_json(); - let 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( - EventTarget::labeled(label), + EventTarget::labeled(subscription_id.to_string()), "event", - RichEvent { raw, parsed }, + 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(); + } + } }; } RelayPoolNotification::Shutdown => break, @@ -468,47 +586,6 @@ fn main() { Ok(()) }) - .on_window_event(|window, event| { - if let tauri::WindowEvent::Focused(focused) = event { - if !focused { - let handle = window.app_handle().to_owned(); - let config_dir = handle.path().app_config_dir().unwrap(); - - tauri::async_runtime::spawn(async move { - let state = handle.state::(); - let client = &state.client; - - if let Ok(signer) = client.signer().await { - let public_key = signer.public_key().await.unwrap(); - let bech32 = public_key.to_bech32().unwrap(); - - if fs::metadata(config_dir.join(bech32)).is_ok() { - if let Ok(contact_list) = - client.get_contact_list(Some(Duration::from_secs(5))).await - { - let authors: Vec = - contact_list.iter().map(|f| f.public_key).collect(); - - if client - .reconcile( - Filter::new() - .authors(authors) - .kinds(vec![Kind::TextNote, Kind::Repost]) - .limit(1000), - NegentropyOptions::default(), - ) - .await - .is_ok() - { - handle.emit("synchronized", ()).unwrap(); - } - } - } - } - }); - } - } - }) .plugin(prevent_default()) .plugin(tauri_plugin_decorum::init()) .plugin(tauri_plugin_store::Builder::default().build()) diff --git a/src/commands.gen.ts b/src/commands.gen.ts index 532c4c67..116a4bb1 100644 --- a/src/commands.gen.ts +++ b/src/commands.gen.ts @@ -5,9 +5,9 @@ export const commands = { -async getRelays() : Promise> { +async getRelays(id: string) : Promise> { try { - return { status: "ok", data: await TAURI_INVOKE("get_relays") }; + return { status: "ok", data: await TAURI_INVOKE("get_relays", { id }) }; } catch (e) { if(e instanceof Error) throw e; else return { status: "error", error: e as any }; @@ -48,9 +48,9 @@ async saveBootstrapRelays(relays: string) : Promise> { async getAccounts() : Promise { return await TAURI_INVOKE("get_accounts"); }, -async createAccount(name: string, about: string, picture: string, password: string) : Promise> { +async watchAccount(key: string) : Promise> { try { - return { status: "ok", data: await TAURI_INVOKE("create_account", { name, about, picture, password }) }; + return { status: "ok", data: await TAURI_INVOKE("watch_account", { key }) }; } catch (e) { if(e instanceof Error) throw e; else return { status: "error", error: e as any }; @@ -96,15 +96,17 @@ async resetPassword(key: string, password: string) : Promise { - return await TAURI_INVOKE("is_account_sync", { id }); -}, -async createSyncFile(id: string) : Promise { - return await TAURI_INVOKE("create_sync_file", { id }); -}, -async login(account: string, password: string) : Promise> { +async hasSigner(id: string) : Promise> { try { - return { status: "ok", data: await TAURI_INVOKE("login", { account, password }) }; + return { status: "ok", data: await TAURI_INVOKE("has_signer", { id }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async setSigner(account: string, password: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("set_signer", { account, password }) }; } catch (e) { if(e instanceof Error) throw e; else return { status: "error", error: e as any }; @@ -126,9 +128,9 @@ async setProfile(profile: Profile) : Promise> { else return { status: "error", error: e as any }; } }, -async getContactList() : Promise> { +async getContactList(id: string) : Promise> { try { - return { status: "ok", data: await TAURI_INVOKE("get_contact_list") }; + return { status: "ok", data: await TAURI_INVOKE("get_contact_list", { id }) }; } catch (e) { if(e instanceof Error) throw e; else return { status: "error", error: e as any }; @@ -142,9 +144,9 @@ async setContactList(publicKeys: string[]) : Promise> { else return { status: "error", error: e as any }; } }, -async checkContact(id: string) : Promise> { +async isContact(id: string) : Promise> { try { - return { status: "ok", data: await TAURI_INVOKE("check_contact", { id }) }; + return { status: "ok", data: await TAURI_INVOKE("is_contact", { id }) }; } catch (e) { if(e instanceof Error) throw e; else return { status: "error", error: e as any }; @@ -158,9 +160,9 @@ async toggleContact(id: string, alias: string | null) : Promise> { +async getAllProfiles() : Promise> { try { - return { status: "ok", data: await TAURI_INVOKE("get_mention_list") }; + return { status: "ok", data: await TAURI_INVOKE("get_all_profiles") }; } catch (e) { if(e instanceof Error) throw e; else return { status: "error", error: e as any }; @@ -222,7 +224,7 @@ async setWallet(uri: string) : Promise> { else return { status: "error", error: e as any }; } }, -async loadWallet() : Promise> { +async loadWallet() : Promise> { try { return { status: "ok", data: await TAURI_INVOKE("load_wallet") }; } catch (e) { @@ -238,7 +240,7 @@ async removeWallet() : Promise> { else return { status: "error", error: e as any }; } }, -async zapProfile(id: string, amount: string, message: string) : Promise> { +async zapProfile(id: string, amount: string, message: string | null) : Promise> { try { return { status: "ok", data: await TAURI_INVOKE("zap_profile", { id, amount, message }) }; } catch (e) { @@ -246,7 +248,7 @@ async zapProfile(id: string, amount: string, message: string) : Promise> { +async zapEvent(id: string, amount: string, message: string | null) : Promise> { try { return { status: "ok", data: await TAURI_INVOKE("zap_event", { id, amount, message }) }; } catch (e) { @@ -262,9 +264,9 @@ async copyFriend(npub: string) : Promise> { else return { status: "error", error: e as any }; } }, -async getNotifications() : Promise> { +async getNotifications(id: string) : Promise> { try { - return { status: "ok", data: await TAURI_INVOKE("get_notifications") }; + return { status: "ok", data: await TAURI_INVOKE("get_notifications", { id }) }; } catch (e) { if(e instanceof Error) throw e; else return { status: "error", error: e as any }; @@ -294,14 +296,6 @@ async verifyNip05(id: string, nip05: string) : Promise> else return { status: "error", error: e as any }; } }, -async isTrustedUser(id: string) : Promise> { - try { - return { status: "ok", data: await TAURI_INVOKE("is_trusted_user", { id }) }; -} catch (e) { - if(e instanceof Error) throw e; - else return { status: "error", error: e as any }; -} -}, async getEventMeta(content: string) : Promise> { try { return { status: "ok", data: await TAURI_INVOKE("get_event_meta", { content }) }; @@ -382,22 +376,6 @@ async getGlobalEvents(until: string | null) : Promise> { - try { - return { status: "ok", data: await TAURI_INVOKE("is_deleted_event", { id }) }; -} catch (e) { - if(e instanceof Error) throw e; - else return { status: "error", error: e as any }; -} -}, -async requestDelete(id: string) : Promise> { - try { - return { status: "ok", data: await TAURI_INVOKE("request_delete", { id }) }; -} catch (e) { - if(e instanceof Error) throw e; - else return { status: "error", error: e as any }; -} -}, async search(query: string) : Promise> { try { return { status: "ok", data: await TAURI_INVOKE("search", { query }) }; @@ -430,6 +408,30 @@ async repost(raw: string) : Promise> { else return { status: "error", error: e as any }; } }, +async isReposted(id: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("is_reposted", { id }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async requestDelete(id: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("request_delete", { id }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async isDeletedEvent(id: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("is_deleted_event", { id }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, async eventToBech32(id: string) : Promise> { try { return { status: "ok", data: await TAURI_INVOKE("event_to_bech32", { id }) }; @@ -478,7 +480,7 @@ async closeColumn(label: string) : Promise> { else return { status: "error", error: e as any }; } }, -async openWindow(window: Window) : Promise> { +async openWindow(window: Window) : Promise> { try { return { status: "ok", data: await TAURI_INVOKE("open_window", { window }) }; } catch (e) { @@ -498,9 +500,11 @@ async quit() : Promise { export const events = __makeEvents__<{ +negentropyEvent: NegentropyEvent, newSettings: NewSettings, subscription: Subscription }>({ +negentropyEvent: "negentropy-event", newSettings: "new-settings", subscription: "subscription" }) @@ -514,14 +518,16 @@ subscription: "subscription" export type Column = { label: string; url: string; x: number; y: number; width: number; height: number } export type Mention = { pubkey: string; avatar: string; display_name: string; name: string } export type Meta = { content: string; images: string[]; events: string[]; mentions: string[]; hashtags: string[] } +export type NegentropyEvent = { kind: NegentropyKind; total_event: number } +export type NegentropyKind = "Profile" | "Metadata" | "Events" | "EventIds" | "Global" | "Notification" | "Others" export type NewSettings = Settings export type Profile = { name: string; display_name: string; about: string | null; picture: string; banner: string | null; nip05: string | null; lud16: string | null; website: string | null } export type Relays = { connected: string[]; read: string[] | null; write: string[] | null; both: string[] | null } export type RichEvent = { raw: string; parsed: Meta | null } export type Settings = { proxy: string | null; image_resize_service: string | null; use_relay_hint: boolean; content_warning: boolean; trusted_only: boolean; display_avatar: boolean; display_zap_button: boolean; display_repost_button: boolean; display_media: boolean; transparent: boolean } export type SubKind = "Subscribe" | "Unsubscribe" -export type Subscription = { label: string; kind: SubKind; event_id: string | null } -export type Window = { label: string; title: string; url: string; width: number; height: number; maximizable: boolean; minimizable: boolean; hidden_title: boolean } +export type Subscription = { label: string; kind: SubKind; event_id: string | null; contacts: string[] | null } +export type Window = { label: string; title: string; url: string; width: number; height: number; maximizable: boolean; minimizable: boolean; hidden_title: boolean; closable: boolean } /** tauri-specta globals **/ diff --git a/src/commons.ts b/src/commons.ts index 163c5cf0..41100e0c 100644 --- a/src/commons.ts +++ b/src/commons.ts @@ -155,7 +155,7 @@ export function decodeZapInvoice(tags?: string[][]) { ); // @ts-ignore, its fine. - const amount = Number.parseInt(amountSection.value); + const amount = Number.parseInt(amountSection.value) / 1000; const displayValue = getBitcoinDisplayValues(amount); return displayValue; diff --git a/src/components/column.tsx b/src/components/column.tsx index 06d29ff3..83601e1f 100644 --- a/src/components/column.tsx +++ b/src/components/column.tsx @@ -1,23 +1,17 @@ import { commands } from "@/commands.gen"; -import { appColumns } from "@/commons"; import { useRect } from "@/system"; import type { LumeColumn } from "@/types"; import { CaretDown, Check } from "@phosphor-icons/react"; -import { useParams } from "@tanstack/react-router"; -import { useStore } from "@tanstack/react-store"; import { Menu, MenuItem, PredefinedMenuItem } from "@tauri-apps/api/menu"; import { getCurrentWindow } from "@tauri-apps/api/window"; import { useCallback, useEffect, useMemo, useState } from "react"; +import { User } from "./user"; export function Column({ column }: { column: LumeColumn }) { - const params = useParams({ strict: false }); - const webviewLabel = useMemo( - () => `column-${params.account}_${column.label}`, - [params.account, column.label], - ); + const webviewLabel = useMemo(() => `column-${column.label}`, [column.label]); const [rect, ref] = useRect(); - const [error, setError] = useState(null); + const [_error, setError] = useState(null); useEffect(() => { (async () => { @@ -52,7 +46,7 @@ export function Column({ column }: { column: LumeColumn }) { y: initialRect.y, width: initialRect.width, height: initialRect.height, - url: `${column.url}?account=${params.account}&label=${column.label}&name=${column.name}`, + url: `${column.url}?label=${column.label}&name=${column.name}`, }) .then((res) => { if (res.status === "ok") { @@ -73,42 +67,35 @@ export function Column({ column }: { column: LumeColumn }) { }); }; } - }, [params.account]); + }, []); return (
-
+
); } -function Header({ label }: { label: string }) { +function Header({ + label, + name, + account, +}: { label: string; name: string; account?: string }) { const [title, setTitle] = useState(""); const [isChanged, setIsChanged] = useState(false); - const column = useStore(appColumns, (state) => - state.find((col) => col.label === label), - ); - - const saveNewTitle = async () => { - const mainWindow = getCurrentWindow(); - await mainWindow.emit("columns", { type: "set_title", label, title }); - - // update search params - // @ts-ignore, hahaha - search.name = title; - - // reset state - setIsChanged(false); - }; - const showContextMenu = useCallback(async (e: React.MouseEvent) => { e.preventDefault(); const window = getCurrentWindow(); + const menuItems = await Promise.all([ MenuItem.new({ text: "Reload", @@ -116,10 +103,6 @@ function Header({ label }: { label: string }) { await commands.reloadColumn(label); }, }), - MenuItem.new({ - text: "Open in new window", - action: () => console.log("not implemented."), - }), PredefinedMenuItem.new({ item: "Separator" }), MenuItem.new({ text: "Move left", @@ -160,6 +143,15 @@ function Header({ label }: { label: string }) { await menu.popup().catch((e) => console.error(e)); }, []); + const saveNewTitle = async () => { + await getCurrentWindow().emit("columns", { + type: "set_title", + label, + title, + }); + setIsChanged(false); + }; + useEffect(() => { if (title.length > 0) setIsChanged(true); }, [title.length]); @@ -168,13 +160,20 @@ function Header({ label }: { label: string }) {
+ {account?.length ? ( + + + + + + ) : null}
setTitle(e.currentTarget.textContent)} className="text-[12px] font-semibold focus:outline-none" > - {column.name} + {name}
{isChanged ? ( + + + + Quote + + + + + + ); +} diff --git a/src/components/note/buttons/reply.tsx b/src/components/note/buttons/reply.tsx index 00e94ac2..8e091553 100644 --- a/src/components/note/buttons/reply.tsx +++ b/src/components/note/buttons/reply.tsx @@ -18,10 +18,8 @@ export function NoteReply({ type="button" onClick={() => LumeWindow.openEditor(event.id)} className={cn( - "inline-flex items-center justify-center text-neutral-800 dark:text-neutral-200", - label - ? "rounded-full h-7 gap-1.5 w-20 text-sm font-medium hover:bg-black/10 dark:hover:bg-white/10" - : "size-7", + "h-7 rounded-full inline-flex items-center justify-center text-neutral-800 hover:bg-black/5 dark:hover:bg-white/5 dark:text-neutral-200 text-sm font-medium", + label ? "w-24 gap-1.5" : "w-14", )} > diff --git a/src/components/note/buttons/repost.tsx b/src/components/note/buttons/repost.tsx index 233e474e..e73caccb 100644 --- a/src/components/note/buttons/repost.tsx +++ b/src/components/note/buttons/repost.tsx @@ -1,88 +1,174 @@ -import { appSettings, cn } from "@/commons"; +import { commands } from "@/commands.gen"; +import { appSettings, cn, displayNpub } from "@/commons"; import { RepostIcon, Spinner } from "@/components"; import { LumeWindow } from "@/system"; +import type { Metadata } from "@/types"; +import * as Tooltip from "@radix-ui/react-tooltip"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useStore } from "@tanstack/react-store"; import { Menu, MenuItem } from "@tauri-apps/api/menu"; -import { message } from "@tauri-apps/plugin-dialog"; -import { useCallback, useState } from "react"; +import type { Window } from "@tauri-apps/api/window"; +import { useCallback, useEffect, useState, useTransition } from "react"; import { useNoteContext } from "../provider"; export function NoteRepost({ label = false, smol = false, }: { label?: boolean; smol?: boolean }) { - const visible = useStore(appSettings, (state) => state.display_repost_button); const event = useNoteContext(); + const visible = useStore(appSettings, (state) => state.display_repost_button); + const queryClient = useQueryClient(); - const [loading, setLoading] = useState(false); - const [isRepost, setIsRepost] = useState(false); + const { isLoading, data: status } = useQuery({ + queryKey: ["is-reposted", event.id], + queryFn: async () => { + const res = await commands.isReposted(event.id); + if (res.status === "ok") { + return res.data; + } else { + return false; + } + }, + enabled: visible, + refetchOnMount: false, + refetchOnWindowFocus: false, + refetchOnReconnect: false, + staleTime: Number.POSITIVE_INFINITY, + retry: false, + }); - const repost = async () => { - if (isRepost) return; - - try { - setLoading(true); - - // repost - await event.repost(); - - // update state - setLoading(false); - setIsRepost(true); - } catch { - setLoading(false); - await message("Repost failed, try again later", { - title: "Lume", - kind: "info", - }); - } - }; + const [isPending, startTransition] = useTransition(); + const [popup, setPopup] = useState(null); const showContextMenu = useCallback(async (e: React.MouseEvent) => { e.preventDefault(); - const menuItems = await Promise.all([ - MenuItem.new({ - text: "Repost", - action: async () => repost(), - }), - MenuItem.new({ - text: "Quote", - action: () => LumeWindow.openEditor(null, event.id), - }), - ]); + const accounts = await commands.getAccounts(); + const list = []; - const menu = await Menu.new({ - items: menuItems, - }); + 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; + } + + list.push( + MenuItem.new({ + text: `Repost as ${name} (${displayNpub(account, 16)})`, + action: async () => submit(account), + }), + ); + } + + const items = await Promise.all(list); + const menu = await Menu.new({ items }); await menu.popup().catch((e) => console.error(e)); }, []); + const repost = useMutation({ + mutationFn: async () => { + // Cancel any outgoing refetches + await queryClient.cancelQueries({ queryKey: ["is-reposted", event.id] }); + + // Optimistically update to the new value + queryClient.setQueryData(["is-reposted", event.id], true); + + const res = await commands.repost(JSON.stringify(event.raw)); + + if (res.status === "ok") { + return; + } else { + throw new Error(res.error); + } + }, + onError: () => { + queryClient.setQueryData(["is-reposted", event.id], false); + }, + onSettled: async () => { + return await queryClient.invalidateQueries({ + queryKey: ["is-reposted", event.id], + }); + }, + }); + + const submit = (account: string) => { + startTransition(async () => { + if (!status) { + const signer = await commands.hasSigner(account); + + if (signer.status === "ok") { + if (!signer.data) { + const newPopup = await LumeWindow.openPopup( + `/set-signer/${account}`, + undefined, + false, + ); + + setPopup(newPopup); + return; + } + + repost.mutate(); + } else { + return; + } + } else { + return; + } + }); + }; + + useEffect(() => { + if (!visible) return; + if (!popup) return; + + const unlisten = popup.listen("signer-updated", async () => { + repost.mutate(); + }); + + return () => { + unlisten.then((f) => f()); + }; + }, [popup]); + if (!visible) return null; return ( - + + + + + + + + Repost + + + + + ); } diff --git a/src/components/note/buttons/zap.tsx b/src/components/note/buttons/zap.tsx index fb581da5..5d49ebc6 100644 --- a/src/components/note/buttons/zap.tsx +++ b/src/components/note/buttons/zap.tsx @@ -20,10 +20,8 @@ export function NoteZap({ type="button" onClick={() => LumeWindow.openZap(event.id, search.account)} className={cn( - "inline-flex items-center justify-center text-neutral-800 dark:text-neutral-200", - label - ? "rounded-full h-7 gap-1.5 w-20 text-sm font-medium hover:bg-black/10 dark:hover:bg-white/10" - : "size-7", + "h-7 rounded-full inline-flex items-center justify-center text-neutral-800 hover:bg-black/5 dark:hover:bg-white/5 dark:text-neutral-200 text-sm font-medium", + label ? "w-24 gap-1.5" : "w-14", )} > diff --git a/src/components/note/index.ts b/src/components/note/index.ts index 43069be3..158c407b 100644 --- a/src/components/note/index.ts +++ b/src/components/note/index.ts @@ -1,4 +1,5 @@ import { NoteOpenThread } from "./buttons/open"; +import { NoteQuote } from "./buttons/quote"; import { NoteReply } from "./buttons/reply"; import { NoteRepost } from "./buttons/repost"; import { NoteZap } from "./buttons/zap"; @@ -16,6 +17,7 @@ export const Note = { User: NoteUser, Menu: NoteMenu, Reply: NoteReply, + Quote: NoteQuote, Repost: NoteRepost, Content: NoteContent, ContentLarge: NoteContentLarge, diff --git a/src/components/note/mentions/note.tsx b/src/components/note/mentions/note.tsx index aa900d80..63e6df99 100644 --- a/src/components/note/mentions/note.tsx +++ b/src/components/note/mentions/note.tsx @@ -51,11 +51,11 @@ export const MentionNote = memo(function MentionNote({ {replyTime(event.created_at)} -
+
diff --git a/src/components/quote.tsx b/src/components/quote.tsx deleted file mode 100644 index f848fa8e..00000000 --- a/src/components/quote.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import { cn } from "@/commons"; -import { Note } from "@/components/note"; -import type { LumeEvent } from "@/system"; -import { Quotes } from "@phosphor-icons/react"; -import { memo } from "react"; - -export const Quote = memo(function Quote({ - event, - className, -}: { - event: LumeEvent; - className?: string; -}) { - return ( - - -
- -
-
- - Quote -
-
-
-
-
- -
- -
-
-
- -
- - - ); -}); diff --git a/src/components/reply.tsx b/src/components/reply.tsx index 2cd0a1c3..16ec2800 100644 --- a/src/components/reply.tsx +++ b/src/components/reply.tsx @@ -1,21 +1,12 @@ -import { commands } from "@/commands.gen"; -import { appSettings, cn, replyTime } from "@/commons"; +import { cn, replyTime } from "@/commons"; import { Note } from "@/components/note"; import { type LumeEvent, LumeWindow } from "@/system"; import { CaretDown } from "@phosphor-icons/react"; import { Link, useSearch } from "@tanstack/react-router"; -import { useStore } from "@tanstack/react-store"; import { Menu, MenuItem } from "@tauri-apps/api/menu"; import { writeText } from "@tauri-apps/plugin-clipboard-manager"; import { nip19 } from "nostr-tools"; -import { - type ReactNode, - memo, - useCallback, - useEffect, - useMemo, - useState, -} from "react"; +import { type ReactNode, memo, useCallback, useMemo } from "react"; import reactStringReplace from "react-string-replace"; import { Hashtag } from "./note/mentions/hashtag"; import { MentionUser } from "./note/mentions/user"; @@ -28,11 +19,7 @@ export const ReplyNote = memo(function ReplyNote({ event: LumeEvent; className?: string; }) { - const trustedOnly = useStore(appSettings, (state) => state.trusted_only); const search = useSearch({ strict: false }); - - const [isTrusted, setIsTrusted] = useState(null); - const showContextMenu = useCallback(async (e: React.MouseEvent) => { e.preventDefault(); @@ -57,24 +44,6 @@ export const ReplyNote = memo(function ReplyNote({ await menu.popup().catch((e) => console.error(e)); }, []); - useEffect(() => { - async function check() { - const res = await commands.isTrustedUser(event.pubkey); - - if (res.status === "ok") { - setIsTrusted(res.data); - } - } - - if (trustedOnly) { - check(); - } - }, []); - - if (isTrusted !== null && isTrusted === false) { - return null; - } - return ( @@ -99,7 +68,7 @@ export const ReplyNote = memo(function ReplyNote({ {replyTime(event.created_at)} -
+
@@ -180,7 +149,7 @@ function ChildReply({ event }: { event: LumeEvent }) { {replyTime(event.created_at)} -
+
diff --git a/src/components/repost.tsx b/src/components/repost.tsx index ad2c7522..bbac35f3 100644 --- a/src/components/repost.tsx +++ b/src/components/repost.tsx @@ -36,7 +36,7 @@ export const RepostNote = memo(function RepostNote({
-
+
diff --git a/src/components/text.tsx b/src/components/text.tsx index c491c57e..b1e83e11 100644 --- a/src/components/text.tsx +++ b/src/components/text.tsx @@ -18,7 +18,7 @@ export const TextNote = memo(function TextNote({
-
+
diff --git a/src/components/user/followButton.tsx b/src/components/user/button.tsx similarity index 92% rename from src/components/user/followButton.tsx rename to src/components/user/button.tsx index 3385a98b..88d802f5 100644 --- a/src/components/user/followButton.tsx +++ b/src/components/user/button.tsx @@ -7,7 +7,7 @@ import { message } from "@tauri-apps/plugin-dialog"; import { useTransition } from "react"; import { useUserContext } from "./provider"; -export function UserFollowButton({ className }: { className?: string }) { +export function UserButton({ className }: { className?: string }) { const user = useUserContext(); const { queryClient } = useRouteContext({ strict: false }); @@ -18,7 +18,7 @@ export function UserFollowButton({ className }: { className?: string }) { } = useQuery({ queryKey: ["status", user.pubkey], queryFn: async () => { - const res = await commands.checkContact(user.pubkey); + const res = await commands.isContact(user.pubkey); if (res.status === "ok") { return res.data; diff --git a/src/components/user/index.ts b/src/components/user/index.ts index 749a7e23..d967ae7b 100644 --- a/src/components/user/index.ts +++ b/src/components/user/index.ts @@ -1,7 +1,7 @@ import { UserAbout } from "./about"; import { UserAvatar } from "./avatar"; +import { UserButton } from "./button"; import { UserCover } from "./cover"; -import { UserFollowButton } from "./followButton"; import { UserName } from "./name"; import { UserNip05 } from "./nip05"; import { UserProvider } from "./provider"; @@ -17,5 +17,5 @@ export const User = { NIP05: UserNip05, Time: UserTime, About: UserAbout, - Button: UserFollowButton, + Button: UserButton, }; diff --git a/src/routes.gen.ts b/src/routes.gen.ts index 3f47bf65..9251512a 100644 --- a/src/routes.gen.ts +++ b/src/routes.gen.ts @@ -15,23 +15,20 @@ import { createFileRoute } from '@tanstack/react-router' import { Route as rootRoute } from './routes/__root' import { Route as SetInterestImport } from './routes/set-interest' import { Route as SetGroupImport } from './routes/set-group' -import { Route as LoadingImport } from './routes/loading' import { Route as BootstrapRelaysImport } from './routes/bootstrap-relays' -import { Route as IndexImport } from './routes/index' -import { Route as EditorIndexImport } from './routes/editor/index' +import { Route as LayoutImport } from './routes/_layout' +import { Route as NewPostIndexImport } from './routes/new-post/index' +import { Route as LayoutIndexImport } from './routes/_layout/index' import { Route as ZapIdImport } from './routes/zap.$id' import { Route as ColumnsLayoutImport } from './routes/columns/_layout' -import { Route as AccountBackupImport } from './routes/$account/backup' -import { Route as AccountAppImport } from './routes/$account/_app' -import { Route as ColumnsLayoutStoriesImport } from './routes/columns/_layout/stories' -import { Route as ColumnsLayoutNewsfeedImport } from './routes/columns/_layout/newsfeed' +import { Route as SettingsIdWalletImport } from './routes/settings.$id/wallet' +import { Route as SettingsIdRelayImport } from './routes/settings.$id/relay' +import { Route as SettingsIdProfileImport } from './routes/settings.$id/profile' +import { Route as SettingsIdGeneralImport } from './routes/settings.$id/general' import { Route as ColumnsLayoutGlobalImport } from './routes/columns/_layout/global' import { Route as ColumnsLayoutCreateNewsfeedImport } from './routes/columns/_layout/create-newsfeed' -import { Route as AccountSettingsWalletImport } from './routes/$account/_settings/wallet' -import { Route as AccountSettingsRelayImport } from './routes/$account/_settings/relay' -import { Route as AccountSettingsProfileImport } from './routes/$account/_settings/profile' -import { Route as AccountSettingsGeneralImport } from './routes/$account/_settings/general' -import { Route as AccountSettingsBitcoinConnectImport } from './routes/$account/_settings/bitcoin-connect' +import { Route as ColumnsLayoutStoriesIdImport } from './routes/columns/_layout/stories.$id' +import { Route as ColumnsLayoutNewsfeedIdImport } from './routes/columns/_layout/newsfeed.$id' import { Route as ColumnsLayoutInterestsIdImport } from './routes/columns/_layout/interests.$id' import { Route as ColumnsLayoutGroupsIdImport } from './routes/columns/_layout/groups.$id' import { Route as ColumnsLayoutCreateNewsfeedUsersImport } from './routes/columns/_layout/create-newsfeed.users' @@ -40,13 +37,13 @@ import { Route as ColumnsLayoutCreateNewsfeedF2fImport } from './routes/columns/ // Create Virtual Routes const ColumnsImport = createFileRoute('/columns')() -const AccountImport = createFileRoute('/$account')() const ResetLazyImport = createFileRoute('/reset')() const NewLazyImport = createFileRoute('/new')() -const AuthNewLazyImport = createFileRoute('/auth/new')() +const SettingsIdLazyImport = createFileRoute('/settings/$id')() +const SetSignerIdLazyImport = createFileRoute('/set-signer/$id')() +const AuthWatchLazyImport = createFileRoute('/auth/watch')() const AuthImportLazyImport = createFileRoute('/auth/import')() const AuthConnectLazyImport = createFileRoute('/auth/connect')() -const AccountSettingsLazyImport = createFileRoute('/$account/_settings')() const ColumnsLayoutTrendingLazyImport = createFileRoute( '/columns/_layout/trending', )() @@ -56,19 +53,18 @@ const ColumnsLayoutSearchLazyImport = createFileRoute( const ColumnsLayoutOnboardingLazyImport = createFileRoute( '/columns/_layout/onboarding', )() -const ColumnsLayoutNotificationLazyImport = createFileRoute( - '/columns/_layout/notification', -)() const ColumnsLayoutLaunchpadLazyImport = createFileRoute( '/columns/_layout/launchpad', )() -const AccountAppHomeLazyImport = createFileRoute('/$account/_app/home')() const ColumnsLayoutUsersIdLazyImport = createFileRoute( '/columns/_layout/users/$id', )() const ColumnsLayoutRepliesIdLazyImport = createFileRoute( '/columns/_layout/replies/$id', )() +const ColumnsLayoutNotificationIdLazyImport = createFileRoute( + '/columns/_layout/notification/$id', +)() const ColumnsLayoutEventsIdLazyImport = createFileRoute( '/columns/_layout/events/$id', )() @@ -80,11 +76,6 @@ const ColumnsRoute = ColumnsImport.update({ getParentRoute: () => rootRoute, } as any) -const AccountRoute = AccountImport.update({ - path: '/$account', - getParentRoute: () => rootRoute, -} as any) - const ResetLazyRoute = ResetLazyImport.update({ path: '/reset', getParentRoute: () => rootRoute, @@ -105,11 +96,6 @@ const SetGroupRoute = SetGroupImport.update({ getParentRoute: () => rootRoute, } as any).lazy(() => import('./routes/set-group.lazy').then((d) => d.Route)) -const LoadingRoute = LoadingImport.update({ - path: '/loading', - getParentRoute: () => rootRoute, -} as any) - const BootstrapRelaysRoute = BootstrapRelaysImport.update({ path: '/bootstrap-relays', getParentRoute: () => rootRoute, @@ -117,20 +103,37 @@ const BootstrapRelaysRoute = BootstrapRelaysImport.update({ import('./routes/bootstrap-relays.lazy').then((d) => d.Route), ) -const IndexRoute = IndexImport.update({ - path: '/', +const LayoutRoute = LayoutImport.update({ + id: '/_layout', getParentRoute: () => rootRoute, -} as any).lazy(() => import('./routes/index.lazy').then((d) => d.Route)) +} as any).lazy(() => import('./routes/_layout.lazy').then((d) => d.Route)) -const EditorIndexRoute = EditorIndexImport.update({ - path: '/editor/', +const NewPostIndexRoute = NewPostIndexImport.update({ + path: '/new-post/', getParentRoute: () => rootRoute, } as any) -const AuthNewLazyRoute = AuthNewLazyImport.update({ - path: '/auth/new', +const LayoutIndexRoute = LayoutIndexImport.update({ + path: '/', + getParentRoute: () => LayoutRoute, +} as any).lazy(() => import('./routes/_layout/index.lazy').then((d) => d.Route)) + +const SettingsIdLazyRoute = SettingsIdLazyImport.update({ + path: '/settings/$id', getParentRoute: () => rootRoute, -} as any).lazy(() => import('./routes/auth/new.lazy').then((d) => d.Route)) +} as any).lazy(() => import('./routes/settings.$id.lazy').then((d) => d.Route)) + +const SetSignerIdLazyRoute = SetSignerIdLazyImport.update({ + path: '/set-signer/$id', + getParentRoute: () => rootRoute, +} as any).lazy(() => + import('./routes/set-signer.$id.lazy').then((d) => d.Route), +) + +const AuthWatchLazyRoute = AuthWatchLazyImport.update({ + path: '/auth/watch', + getParentRoute: () => rootRoute, +} as any).lazy(() => import('./routes/auth/watch.lazy').then((d) => d.Route)) const AuthImportLazyRoute = AuthImportLazyImport.update({ path: '/auth/import', @@ -142,13 +145,6 @@ const AuthConnectLazyRoute = AuthConnectLazyImport.update({ getParentRoute: () => rootRoute, } as any).lazy(() => import('./routes/auth/connect.lazy').then((d) => d.Route)) -const AccountSettingsLazyRoute = AccountSettingsLazyImport.update({ - id: '/_settings', - getParentRoute: () => AccountRoute, -} as any).lazy(() => - import('./routes/$account/_settings.lazy').then((d) => d.Route), -) - const ZapIdRoute = ZapIdImport.update({ path: '/zap/$id', getParentRoute: () => rootRoute, @@ -159,16 +155,6 @@ const ColumnsLayoutRoute = ColumnsLayoutImport.update({ getParentRoute: () => ColumnsRoute, } as any) -const AccountBackupRoute = AccountBackupImport.update({ - path: '/backup', - getParentRoute: () => AccountRoute, -} as any) - -const AccountAppRoute = AccountAppImport.update({ - id: '/_app', - getParentRoute: () => AccountRoute, -} as any).lazy(() => import('./routes/$account/_app.lazy').then((d) => d.Route)) - const ColumnsLayoutTrendingLazyRoute = ColumnsLayoutTrendingLazyImport.update({ path: '/trending', getParentRoute: () => ColumnsLayoutRoute, @@ -191,14 +177,6 @@ const ColumnsLayoutOnboardingLazyRoute = import('./routes/columns/_layout/onboarding.lazy').then((d) => d.Route), ) -const ColumnsLayoutNotificationLazyRoute = - ColumnsLayoutNotificationLazyImport.update({ - path: '/notification', - getParentRoute: () => ColumnsLayoutRoute, - } as any).lazy(() => - import('./routes/columns/_layout/notification.lazy').then((d) => d.Route), - ) - const ColumnsLayoutLaunchpadLazyRoute = ColumnsLayoutLaunchpadLazyImport.update( { path: '/launchpad', @@ -208,25 +186,32 @@ const ColumnsLayoutLaunchpadLazyRoute = ColumnsLayoutLaunchpadLazyImport.update( import('./routes/columns/_layout/launchpad.lazy').then((d) => d.Route), ) -const AccountAppHomeLazyRoute = AccountAppHomeLazyImport.update({ - path: '/home', - getParentRoute: () => AccountAppRoute, +const SettingsIdWalletRoute = SettingsIdWalletImport.update({ + path: '/wallet', + getParentRoute: () => SettingsIdLazyRoute, } as any).lazy(() => - import('./routes/$account/_app.home.lazy').then((d) => d.Route), + import('./routes/settings.$id/wallet.lazy').then((d) => d.Route), ) -const ColumnsLayoutStoriesRoute = ColumnsLayoutStoriesImport.update({ - path: '/stories', - getParentRoute: () => ColumnsLayoutRoute, +const SettingsIdRelayRoute = SettingsIdRelayImport.update({ + path: '/relay', + getParentRoute: () => SettingsIdLazyRoute, } as any).lazy(() => - import('./routes/columns/_layout/stories.lazy').then((d) => d.Route), + import('./routes/settings.$id/relay.lazy').then((d) => d.Route), ) -const ColumnsLayoutNewsfeedRoute = ColumnsLayoutNewsfeedImport.update({ - path: '/newsfeed', - getParentRoute: () => ColumnsLayoutRoute, +const SettingsIdProfileRoute = SettingsIdProfileImport.update({ + path: '/profile', + getParentRoute: () => SettingsIdLazyRoute, } as any).lazy(() => - import('./routes/columns/_layout/newsfeed.lazy').then((d) => d.Route), + import('./routes/settings.$id/profile.lazy').then((d) => d.Route), +) + +const SettingsIdGeneralRoute = SettingsIdGeneralImport.update({ + path: '/general', + getParentRoute: () => SettingsIdLazyRoute, +} as any).lazy(() => + import('./routes/settings.$id/general.lazy').then((d) => d.Route), ) const ColumnsLayoutGlobalRoute = ColumnsLayoutGlobalImport.update({ @@ -240,44 +225,6 @@ const ColumnsLayoutCreateNewsfeedRoute = getParentRoute: () => ColumnsLayoutRoute, } as any) -const AccountSettingsWalletRoute = AccountSettingsWalletImport.update({ - path: '/wallet', - getParentRoute: () => AccountSettingsLazyRoute, -} as any).lazy(() => - import('./routes/$account/_settings/wallet.lazy').then((d) => d.Route), -) - -const AccountSettingsRelayRoute = AccountSettingsRelayImport.update({ - path: '/relay', - getParentRoute: () => AccountSettingsLazyRoute, -} as any).lazy(() => - import('./routes/$account/_settings/relay.lazy').then((d) => d.Route), -) - -const AccountSettingsProfileRoute = AccountSettingsProfileImport.update({ - path: '/profile', - getParentRoute: () => AccountSettingsLazyRoute, -} as any).lazy(() => - import('./routes/$account/_settings/profile.lazy').then((d) => d.Route), -) - -const AccountSettingsGeneralRoute = AccountSettingsGeneralImport.update({ - path: '/general', - getParentRoute: () => AccountSettingsLazyRoute, -} as any).lazy(() => - import('./routes/$account/_settings/general.lazy').then((d) => d.Route), -) - -const AccountSettingsBitcoinConnectRoute = - AccountSettingsBitcoinConnectImport.update({ - path: '/bitcoin-connect', - getParentRoute: () => AccountSettingsLazyRoute, - } as any).lazy(() => - import('./routes/$account/_settings/bitcoin-connect.lazy').then( - (d) => d.Route, - ), - ) - const ColumnsLayoutUsersIdLazyRoute = ColumnsLayoutUsersIdLazyImport.update({ path: '/users/$id', getParentRoute: () => ColumnsLayoutRoute, @@ -294,6 +241,16 @@ const ColumnsLayoutRepliesIdLazyRoute = ColumnsLayoutRepliesIdLazyImport.update( import('./routes/columns/_layout/replies.$id.lazy').then((d) => d.Route), ) +const ColumnsLayoutNotificationIdLazyRoute = + ColumnsLayoutNotificationIdLazyImport.update({ + path: '/notification/$id', + getParentRoute: () => ColumnsLayoutRoute, + } as any).lazy(() => + import('./routes/columns/_layout/notification.$id.lazy').then( + (d) => d.Route, + ), + ) + const ColumnsLayoutEventsIdLazyRoute = ColumnsLayoutEventsIdLazyImport.update({ path: '/events/$id', getParentRoute: () => ColumnsLayoutRoute, @@ -301,6 +258,20 @@ const ColumnsLayoutEventsIdLazyRoute = ColumnsLayoutEventsIdLazyImport.update({ import('./routes/columns/_layout/events.$id.lazy').then((d) => d.Route), ) +const ColumnsLayoutStoriesIdRoute = ColumnsLayoutStoriesIdImport.update({ + path: '/stories/$id', + getParentRoute: () => ColumnsLayoutRoute, +} as any).lazy(() => + import('./routes/columns/_layout/stories.$id.lazy').then((d) => d.Route), +) + +const ColumnsLayoutNewsfeedIdRoute = ColumnsLayoutNewsfeedIdImport.update({ + path: '/newsfeed/$id', + getParentRoute: () => ColumnsLayoutRoute, +} as any).lazy(() => + import('./routes/columns/_layout/newsfeed.$id.lazy').then((d) => d.Route), +) + const ColumnsLayoutInterestsIdRoute = ColumnsLayoutInterestsIdImport.update({ path: '/interests/$id', getParentRoute: () => ColumnsLayoutRoute, @@ -331,11 +302,11 @@ const ColumnsLayoutCreateNewsfeedF2fRoute = declare module '@tanstack/react-router' { interface FileRoutesByPath { - '/': { - id: '/' - path: '/' - fullPath: '/' - preLoaderRoute: typeof IndexImport + '/_layout': { + id: '/_layout' + path: '' + fullPath: '' + preLoaderRoute: typeof LayoutImport parentRoute: typeof rootRoute } '/bootstrap-relays': { @@ -345,13 +316,6 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof BootstrapRelaysImport parentRoute: typeof rootRoute } - '/loading': { - id: '/loading' - path: '/loading' - fullPath: '/loading' - preLoaderRoute: typeof LoadingImport - parentRoute: typeof rootRoute - } '/set-group': { id: '/set-group' path: '/set-group' @@ -380,27 +344,6 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof ResetLazyImport parentRoute: typeof rootRoute } - '/$account': { - id: '/$account' - path: '/$account' - fullPath: '/$account' - preLoaderRoute: typeof AccountImport - parentRoute: typeof rootRoute - } - '/$account/_app': { - id: '/$account/_app' - path: '/$account' - fullPath: '/$account' - preLoaderRoute: typeof AccountAppImport - parentRoute: typeof AccountRoute - } - '/$account/backup': { - id: '/$account/backup' - path: '/backup' - fullPath: '/$account/backup' - preLoaderRoute: typeof AccountBackupImport - parentRoute: typeof AccountImport - } '/columns': { id: '/columns' path: '/columns' @@ -422,13 +365,6 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof ZapIdImport parentRoute: typeof rootRoute } - '/$account/_settings': { - id: '/$account/_settings' - path: '' - fullPath: '/$account' - preLoaderRoute: typeof AccountSettingsLazyImport - parentRoute: typeof AccountImport - } '/auth/connect': { id: '/auth/connect' path: '/auth/connect' @@ -443,54 +379,40 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof AuthImportLazyImport parentRoute: typeof rootRoute } - '/auth/new': { - id: '/auth/new' - path: '/auth/new' - fullPath: '/auth/new' - preLoaderRoute: typeof AuthNewLazyImport + '/auth/watch': { + id: '/auth/watch' + path: '/auth/watch' + fullPath: '/auth/watch' + preLoaderRoute: typeof AuthWatchLazyImport parentRoute: typeof rootRoute } - '/editor/': { - id: '/editor/' - path: '/editor' - fullPath: '/editor' - preLoaderRoute: typeof EditorIndexImport + '/set-signer/$id': { + id: '/set-signer/$id' + path: '/set-signer/$id' + fullPath: '/set-signer/$id' + preLoaderRoute: typeof SetSignerIdLazyImport parentRoute: typeof rootRoute } - '/$account/_settings/bitcoin-connect': { - id: '/$account/_settings/bitcoin-connect' - path: '/bitcoin-connect' - fullPath: '/$account/bitcoin-connect' - preLoaderRoute: typeof AccountSettingsBitcoinConnectImport - parentRoute: typeof AccountSettingsLazyImport + '/settings/$id': { + id: '/settings/$id' + path: '/settings/$id' + fullPath: '/settings/$id' + preLoaderRoute: typeof SettingsIdLazyImport + parentRoute: typeof rootRoute } - '/$account/_settings/general': { - id: '/$account/_settings/general' - path: '/general' - fullPath: '/$account/general' - preLoaderRoute: typeof AccountSettingsGeneralImport - parentRoute: typeof AccountSettingsLazyImport + '/_layout/': { + id: '/_layout/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof LayoutIndexImport + parentRoute: typeof LayoutImport } - '/$account/_settings/profile': { - id: '/$account/_settings/profile' - path: '/profile' - fullPath: '/$account/profile' - preLoaderRoute: typeof AccountSettingsProfileImport - parentRoute: typeof AccountSettingsLazyImport - } - '/$account/_settings/relay': { - id: '/$account/_settings/relay' - path: '/relay' - fullPath: '/$account/relay' - preLoaderRoute: typeof AccountSettingsRelayImport - parentRoute: typeof AccountSettingsLazyImport - } - '/$account/_settings/wallet': { - id: '/$account/_settings/wallet' - path: '/wallet' - fullPath: '/$account/wallet' - preLoaderRoute: typeof AccountSettingsWalletImport - parentRoute: typeof AccountSettingsLazyImport + '/new-post/': { + id: '/new-post/' + path: '/new-post' + fullPath: '/new-post' + preLoaderRoute: typeof NewPostIndexImport + parentRoute: typeof rootRoute } '/columns/_layout/create-newsfeed': { id: '/columns/_layout/create-newsfeed' @@ -506,26 +428,33 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof ColumnsLayoutGlobalImport parentRoute: typeof ColumnsLayoutImport } - '/columns/_layout/newsfeed': { - id: '/columns/_layout/newsfeed' - path: '/newsfeed' - fullPath: '/columns/newsfeed' - preLoaderRoute: typeof ColumnsLayoutNewsfeedImport - parentRoute: typeof ColumnsLayoutImport + '/settings/$id/general': { + id: '/settings/$id/general' + path: '/general' + fullPath: '/settings/$id/general' + preLoaderRoute: typeof SettingsIdGeneralImport + parentRoute: typeof SettingsIdLazyImport } - '/columns/_layout/stories': { - id: '/columns/_layout/stories' - path: '/stories' - fullPath: '/columns/stories' - preLoaderRoute: typeof ColumnsLayoutStoriesImport - parentRoute: typeof ColumnsLayoutImport + '/settings/$id/profile': { + id: '/settings/$id/profile' + path: '/profile' + fullPath: '/settings/$id/profile' + preLoaderRoute: typeof SettingsIdProfileImport + parentRoute: typeof SettingsIdLazyImport } - '/$account/_app/home': { - id: '/$account/_app/home' - path: '/home' - fullPath: '/$account/home' - preLoaderRoute: typeof AccountAppHomeLazyImport - parentRoute: typeof AccountAppImport + '/settings/$id/relay': { + id: '/settings/$id/relay' + path: '/relay' + fullPath: '/settings/$id/relay' + preLoaderRoute: typeof SettingsIdRelayImport + parentRoute: typeof SettingsIdLazyImport + } + '/settings/$id/wallet': { + id: '/settings/$id/wallet' + path: '/wallet' + fullPath: '/settings/$id/wallet' + preLoaderRoute: typeof SettingsIdWalletImport + parentRoute: typeof SettingsIdLazyImport } '/columns/_layout/launchpad': { id: '/columns/_layout/launchpad' @@ -534,13 +463,6 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof ColumnsLayoutLaunchpadLazyImport parentRoute: typeof ColumnsLayoutImport } - '/columns/_layout/notification': { - id: '/columns/_layout/notification' - path: '/notification' - fullPath: '/columns/notification' - preLoaderRoute: typeof ColumnsLayoutNotificationLazyImport - parentRoute: typeof ColumnsLayoutImport - } '/columns/_layout/onboarding': { id: '/columns/_layout/onboarding' path: '/onboarding' @@ -590,6 +512,20 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof ColumnsLayoutInterestsIdImport parentRoute: typeof ColumnsLayoutImport } + '/columns/_layout/newsfeed/$id': { + id: '/columns/_layout/newsfeed/$id' + path: '/newsfeed/$id' + fullPath: '/columns/newsfeed/$id' + preLoaderRoute: typeof ColumnsLayoutNewsfeedIdImport + parentRoute: typeof ColumnsLayoutImport + } + '/columns/_layout/stories/$id': { + id: '/columns/_layout/stories/$id' + path: '/stories/$id' + fullPath: '/columns/stories/$id' + preLoaderRoute: typeof ColumnsLayoutStoriesIdImport + parentRoute: typeof ColumnsLayoutImport + } '/columns/_layout/events/$id': { id: '/columns/_layout/events/$id' path: '/events/$id' @@ -597,6 +533,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof ColumnsLayoutEventsIdLazyImport parentRoute: typeof ColumnsLayoutImport } + '/columns/_layout/notification/$id': { + id: '/columns/_layout/notification/$id' + path: '/notification/$id' + fullPath: '/columns/notification/$id' + preLoaderRoute: typeof ColumnsLayoutNotificationIdLazyImport + parentRoute: typeof ColumnsLayoutImport + } '/columns/_layout/replies/$id': { id: '/columns/_layout/replies/$id' path: '/replies/$id' @@ -616,51 +559,16 @@ declare module '@tanstack/react-router' { // Create and export the route tree -interface AccountAppRouteChildren { - AccountAppHomeLazyRoute: typeof AccountAppHomeLazyRoute +interface LayoutRouteChildren { + LayoutIndexRoute: typeof LayoutIndexRoute } -const AccountAppRouteChildren: AccountAppRouteChildren = { - AccountAppHomeLazyRoute: AccountAppHomeLazyRoute, +const LayoutRouteChildren: LayoutRouteChildren = { + LayoutIndexRoute: LayoutIndexRoute, } -const AccountAppRouteWithChildren = AccountAppRoute._addFileChildren( - AccountAppRouteChildren, -) - -interface AccountSettingsLazyRouteChildren { - AccountSettingsBitcoinConnectRoute: typeof AccountSettingsBitcoinConnectRoute - AccountSettingsGeneralRoute: typeof AccountSettingsGeneralRoute - AccountSettingsProfileRoute: typeof AccountSettingsProfileRoute - AccountSettingsRelayRoute: typeof AccountSettingsRelayRoute - AccountSettingsWalletRoute: typeof AccountSettingsWalletRoute -} - -const AccountSettingsLazyRouteChildren: AccountSettingsLazyRouteChildren = { - AccountSettingsBitcoinConnectRoute: AccountSettingsBitcoinConnectRoute, - AccountSettingsGeneralRoute: AccountSettingsGeneralRoute, - AccountSettingsProfileRoute: AccountSettingsProfileRoute, - AccountSettingsRelayRoute: AccountSettingsRelayRoute, - AccountSettingsWalletRoute: AccountSettingsWalletRoute, -} - -const AccountSettingsLazyRouteWithChildren = - AccountSettingsLazyRoute._addFileChildren(AccountSettingsLazyRouteChildren) - -interface AccountRouteChildren { - AccountAppRoute: typeof AccountAppRouteWithChildren - AccountBackupRoute: typeof AccountBackupRoute - AccountSettingsLazyRoute: typeof AccountSettingsLazyRouteWithChildren -} - -const AccountRouteChildren: AccountRouteChildren = { - AccountAppRoute: AccountAppRouteWithChildren, - AccountBackupRoute: AccountBackupRoute, - AccountSettingsLazyRoute: AccountSettingsLazyRouteWithChildren, -} - -const AccountRouteWithChildren = - AccountRoute._addFileChildren(AccountRouteChildren) +const LayoutRouteWithChildren = + LayoutRoute._addFileChildren(LayoutRouteChildren) interface ColumnsLayoutCreateNewsfeedRouteChildren { ColumnsLayoutCreateNewsfeedF2fRoute: typeof ColumnsLayoutCreateNewsfeedF2fRoute @@ -682,16 +590,16 @@ const ColumnsLayoutCreateNewsfeedRouteWithChildren = interface ColumnsLayoutRouteChildren { ColumnsLayoutCreateNewsfeedRoute: typeof ColumnsLayoutCreateNewsfeedRouteWithChildren ColumnsLayoutGlobalRoute: typeof ColumnsLayoutGlobalRoute - ColumnsLayoutNewsfeedRoute: typeof ColumnsLayoutNewsfeedRoute - ColumnsLayoutStoriesRoute: typeof ColumnsLayoutStoriesRoute ColumnsLayoutLaunchpadLazyRoute: typeof ColumnsLayoutLaunchpadLazyRoute - ColumnsLayoutNotificationLazyRoute: typeof ColumnsLayoutNotificationLazyRoute ColumnsLayoutOnboardingLazyRoute: typeof ColumnsLayoutOnboardingLazyRoute ColumnsLayoutSearchLazyRoute: typeof ColumnsLayoutSearchLazyRoute ColumnsLayoutTrendingLazyRoute: typeof ColumnsLayoutTrendingLazyRoute ColumnsLayoutGroupsIdRoute: typeof ColumnsLayoutGroupsIdRoute ColumnsLayoutInterestsIdRoute: typeof ColumnsLayoutInterestsIdRoute + ColumnsLayoutNewsfeedIdRoute: typeof ColumnsLayoutNewsfeedIdRoute + ColumnsLayoutStoriesIdRoute: typeof ColumnsLayoutStoriesIdRoute ColumnsLayoutEventsIdLazyRoute: typeof ColumnsLayoutEventsIdLazyRoute + ColumnsLayoutNotificationIdLazyRoute: typeof ColumnsLayoutNotificationIdLazyRoute ColumnsLayoutRepliesIdLazyRoute: typeof ColumnsLayoutRepliesIdLazyRoute ColumnsLayoutUsersIdLazyRoute: typeof ColumnsLayoutUsersIdLazyRoute } @@ -700,16 +608,16 @@ const ColumnsLayoutRouteChildren: ColumnsLayoutRouteChildren = { ColumnsLayoutCreateNewsfeedRoute: ColumnsLayoutCreateNewsfeedRouteWithChildren, ColumnsLayoutGlobalRoute: ColumnsLayoutGlobalRoute, - ColumnsLayoutNewsfeedRoute: ColumnsLayoutNewsfeedRoute, - ColumnsLayoutStoriesRoute: ColumnsLayoutStoriesRoute, ColumnsLayoutLaunchpadLazyRoute: ColumnsLayoutLaunchpadLazyRoute, - ColumnsLayoutNotificationLazyRoute: ColumnsLayoutNotificationLazyRoute, ColumnsLayoutOnboardingLazyRoute: ColumnsLayoutOnboardingLazyRoute, ColumnsLayoutSearchLazyRoute: ColumnsLayoutSearchLazyRoute, ColumnsLayoutTrendingLazyRoute: ColumnsLayoutTrendingLazyRoute, ColumnsLayoutGroupsIdRoute: ColumnsLayoutGroupsIdRoute, ColumnsLayoutInterestsIdRoute: ColumnsLayoutInterestsIdRoute, + ColumnsLayoutNewsfeedIdRoute: ColumnsLayoutNewsfeedIdRoute, + ColumnsLayoutStoriesIdRoute: ColumnsLayoutStoriesIdRoute, ColumnsLayoutEventsIdLazyRoute: ColumnsLayoutEventsIdLazyRoute, + ColumnsLayoutNotificationIdLazyRoute: ColumnsLayoutNotificationIdLazyRoute, ColumnsLayoutRepliesIdLazyRoute: ColumnsLayoutRepliesIdLazyRoute, ColumnsLayoutUsersIdLazyRoute: ColumnsLayoutUsersIdLazyRoute, } @@ -729,34 +637,47 @@ const ColumnsRouteChildren: ColumnsRouteChildren = { const ColumnsRouteWithChildren = ColumnsRoute._addFileChildren(ColumnsRouteChildren) +interface SettingsIdLazyRouteChildren { + SettingsIdGeneralRoute: typeof SettingsIdGeneralRoute + SettingsIdProfileRoute: typeof SettingsIdProfileRoute + SettingsIdRelayRoute: typeof SettingsIdRelayRoute + SettingsIdWalletRoute: typeof SettingsIdWalletRoute +} + +const SettingsIdLazyRouteChildren: SettingsIdLazyRouteChildren = { + SettingsIdGeneralRoute: SettingsIdGeneralRoute, + SettingsIdProfileRoute: SettingsIdProfileRoute, + SettingsIdRelayRoute: SettingsIdRelayRoute, + SettingsIdWalletRoute: SettingsIdWalletRoute, +} + +const SettingsIdLazyRouteWithChildren = SettingsIdLazyRoute._addFileChildren( + SettingsIdLazyRouteChildren, +) + export interface FileRoutesByFullPath { - '/': typeof IndexRoute + '': typeof LayoutRouteWithChildren '/bootstrap-relays': typeof BootstrapRelaysRoute - '/loading': typeof LoadingRoute '/set-group': typeof SetGroupRoute '/set-interest': typeof SetInterestRoute '/new': typeof NewLazyRoute '/reset': typeof ResetLazyRoute - '/$account': typeof AccountSettingsLazyRouteWithChildren - '/$account/backup': typeof AccountBackupRoute '/columns': typeof ColumnsLayoutRouteWithChildren '/zap/$id': typeof ZapIdRoute '/auth/connect': typeof AuthConnectLazyRoute '/auth/import': typeof AuthImportLazyRoute - '/auth/new': typeof AuthNewLazyRoute - '/editor': typeof EditorIndexRoute - '/$account/bitcoin-connect': typeof AccountSettingsBitcoinConnectRoute - '/$account/general': typeof AccountSettingsGeneralRoute - '/$account/profile': typeof AccountSettingsProfileRoute - '/$account/relay': typeof AccountSettingsRelayRoute - '/$account/wallet': typeof AccountSettingsWalletRoute + '/auth/watch': typeof AuthWatchLazyRoute + '/set-signer/$id': typeof SetSignerIdLazyRoute + '/settings/$id': typeof SettingsIdLazyRouteWithChildren + '/': typeof LayoutIndexRoute + '/new-post': typeof NewPostIndexRoute '/columns/create-newsfeed': typeof ColumnsLayoutCreateNewsfeedRouteWithChildren '/columns/global': typeof ColumnsLayoutGlobalRoute - '/columns/newsfeed': typeof ColumnsLayoutNewsfeedRoute - '/columns/stories': typeof ColumnsLayoutStoriesRoute - '/$account/home': typeof AccountAppHomeLazyRoute + '/settings/$id/general': typeof SettingsIdGeneralRoute + '/settings/$id/profile': typeof SettingsIdProfileRoute + '/settings/$id/relay': typeof SettingsIdRelayRoute + '/settings/$id/wallet': typeof SettingsIdWalletRoute '/columns/launchpad': typeof ColumnsLayoutLaunchpadLazyRoute - '/columns/notification': typeof ColumnsLayoutNotificationLazyRoute '/columns/onboarding': typeof ColumnsLayoutOnboardingLazyRoute '/columns/search': typeof ColumnsLayoutSearchLazyRoute '/columns/trending': typeof ColumnsLayoutTrendingLazyRoute @@ -764,39 +685,36 @@ export interface FileRoutesByFullPath { '/columns/create-newsfeed/users': typeof ColumnsLayoutCreateNewsfeedUsersRoute '/columns/groups/$id': typeof ColumnsLayoutGroupsIdRoute '/columns/interests/$id': typeof ColumnsLayoutInterestsIdRoute + '/columns/newsfeed/$id': typeof ColumnsLayoutNewsfeedIdRoute + '/columns/stories/$id': typeof ColumnsLayoutStoriesIdRoute '/columns/events/$id': typeof ColumnsLayoutEventsIdLazyRoute + '/columns/notification/$id': typeof ColumnsLayoutNotificationIdLazyRoute '/columns/replies/$id': typeof ColumnsLayoutRepliesIdLazyRoute '/columns/users/$id': typeof ColumnsLayoutUsersIdLazyRoute } export interface FileRoutesByTo { - '/': typeof IndexRoute '/bootstrap-relays': typeof BootstrapRelaysRoute - '/loading': typeof LoadingRoute '/set-group': typeof SetGroupRoute '/set-interest': typeof SetInterestRoute '/new': typeof NewLazyRoute '/reset': typeof ResetLazyRoute - '/$account': typeof AccountSettingsLazyRouteWithChildren - '/$account/backup': typeof AccountBackupRoute '/columns': typeof ColumnsLayoutRouteWithChildren '/zap/$id': typeof ZapIdRoute '/auth/connect': typeof AuthConnectLazyRoute '/auth/import': typeof AuthImportLazyRoute - '/auth/new': typeof AuthNewLazyRoute - '/editor': typeof EditorIndexRoute - '/$account/bitcoin-connect': typeof AccountSettingsBitcoinConnectRoute - '/$account/general': typeof AccountSettingsGeneralRoute - '/$account/profile': typeof AccountSettingsProfileRoute - '/$account/relay': typeof AccountSettingsRelayRoute - '/$account/wallet': typeof AccountSettingsWalletRoute + '/auth/watch': typeof AuthWatchLazyRoute + '/set-signer/$id': typeof SetSignerIdLazyRoute + '/settings/$id': typeof SettingsIdLazyRouteWithChildren + '/': typeof LayoutIndexRoute + '/new-post': typeof NewPostIndexRoute '/columns/create-newsfeed': typeof ColumnsLayoutCreateNewsfeedRouteWithChildren '/columns/global': typeof ColumnsLayoutGlobalRoute - '/columns/newsfeed': typeof ColumnsLayoutNewsfeedRoute - '/columns/stories': typeof ColumnsLayoutStoriesRoute - '/$account/home': typeof AccountAppHomeLazyRoute + '/settings/$id/general': typeof SettingsIdGeneralRoute + '/settings/$id/profile': typeof SettingsIdProfileRoute + '/settings/$id/relay': typeof SettingsIdRelayRoute + '/settings/$id/wallet': typeof SettingsIdWalletRoute '/columns/launchpad': typeof ColumnsLayoutLaunchpadLazyRoute - '/columns/notification': typeof ColumnsLayoutNotificationLazyRoute '/columns/onboarding': typeof ColumnsLayoutOnboardingLazyRoute '/columns/search': typeof ColumnsLayoutSearchLazyRoute '/columns/trending': typeof ColumnsLayoutTrendingLazyRoute @@ -804,43 +722,39 @@ export interface FileRoutesByTo { '/columns/create-newsfeed/users': typeof ColumnsLayoutCreateNewsfeedUsersRoute '/columns/groups/$id': typeof ColumnsLayoutGroupsIdRoute '/columns/interests/$id': typeof ColumnsLayoutInterestsIdRoute + '/columns/newsfeed/$id': typeof ColumnsLayoutNewsfeedIdRoute + '/columns/stories/$id': typeof ColumnsLayoutStoriesIdRoute '/columns/events/$id': typeof ColumnsLayoutEventsIdLazyRoute + '/columns/notification/$id': typeof ColumnsLayoutNotificationIdLazyRoute '/columns/replies/$id': typeof ColumnsLayoutRepliesIdLazyRoute '/columns/users/$id': typeof ColumnsLayoutUsersIdLazyRoute } export interface FileRoutesById { __root__: typeof rootRoute - '/': typeof IndexRoute + '/_layout': typeof LayoutRouteWithChildren '/bootstrap-relays': typeof BootstrapRelaysRoute - '/loading': typeof LoadingRoute '/set-group': typeof SetGroupRoute '/set-interest': typeof SetInterestRoute '/new': typeof NewLazyRoute '/reset': typeof ResetLazyRoute - '/$account': typeof AccountRouteWithChildren - '/$account/_app': typeof AccountAppRouteWithChildren - '/$account/backup': typeof AccountBackupRoute '/columns': typeof ColumnsRouteWithChildren '/columns/_layout': typeof ColumnsLayoutRouteWithChildren '/zap/$id': typeof ZapIdRoute - '/$account/_settings': typeof AccountSettingsLazyRouteWithChildren '/auth/connect': typeof AuthConnectLazyRoute '/auth/import': typeof AuthImportLazyRoute - '/auth/new': typeof AuthNewLazyRoute - '/editor/': typeof EditorIndexRoute - '/$account/_settings/bitcoin-connect': typeof AccountSettingsBitcoinConnectRoute - '/$account/_settings/general': typeof AccountSettingsGeneralRoute - '/$account/_settings/profile': typeof AccountSettingsProfileRoute - '/$account/_settings/relay': typeof AccountSettingsRelayRoute - '/$account/_settings/wallet': typeof AccountSettingsWalletRoute + '/auth/watch': typeof AuthWatchLazyRoute + '/set-signer/$id': typeof SetSignerIdLazyRoute + '/settings/$id': typeof SettingsIdLazyRouteWithChildren + '/_layout/': typeof LayoutIndexRoute + '/new-post/': typeof NewPostIndexRoute '/columns/_layout/create-newsfeed': typeof ColumnsLayoutCreateNewsfeedRouteWithChildren '/columns/_layout/global': typeof ColumnsLayoutGlobalRoute - '/columns/_layout/newsfeed': typeof ColumnsLayoutNewsfeedRoute - '/columns/_layout/stories': typeof ColumnsLayoutStoriesRoute - '/$account/_app/home': typeof AccountAppHomeLazyRoute + '/settings/$id/general': typeof SettingsIdGeneralRoute + '/settings/$id/profile': typeof SettingsIdProfileRoute + '/settings/$id/relay': typeof SettingsIdRelayRoute + '/settings/$id/wallet': typeof SettingsIdWalletRoute '/columns/_layout/launchpad': typeof ColumnsLayoutLaunchpadLazyRoute - '/columns/_layout/notification': typeof ColumnsLayoutNotificationLazyRoute '/columns/_layout/onboarding': typeof ColumnsLayoutOnboardingLazyRoute '/columns/_layout/search': typeof ColumnsLayoutSearchLazyRoute '/columns/_layout/trending': typeof ColumnsLayoutTrendingLazyRoute @@ -848,7 +762,10 @@ export interface FileRoutesById { '/columns/_layout/create-newsfeed/users': typeof ColumnsLayoutCreateNewsfeedUsersRoute '/columns/_layout/groups/$id': typeof ColumnsLayoutGroupsIdRoute '/columns/_layout/interests/$id': typeof ColumnsLayoutInterestsIdRoute + '/columns/_layout/newsfeed/$id': typeof ColumnsLayoutNewsfeedIdRoute + '/columns/_layout/stories/$id': typeof ColumnsLayoutStoriesIdRoute '/columns/_layout/events/$id': typeof ColumnsLayoutEventsIdLazyRoute + '/columns/_layout/notification/$id': typeof ColumnsLayoutNotificationIdLazyRoute '/columns/_layout/replies/$id': typeof ColumnsLayoutRepliesIdLazyRoute '/columns/_layout/users/$id': typeof ColumnsLayoutUsersIdLazyRoute } @@ -856,33 +773,28 @@ export interface FileRoutesById { export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath fullPaths: - | '/' + | '' | '/bootstrap-relays' - | '/loading' | '/set-group' | '/set-interest' | '/new' | '/reset' - | '/$account' - | '/$account/backup' | '/columns' | '/zap/$id' | '/auth/connect' | '/auth/import' - | '/auth/new' - | '/editor' - | '/$account/bitcoin-connect' - | '/$account/general' - | '/$account/profile' - | '/$account/relay' - | '/$account/wallet' + | '/auth/watch' + | '/set-signer/$id' + | '/settings/$id' + | '/' + | '/new-post' | '/columns/create-newsfeed' | '/columns/global' - | '/columns/newsfeed' - | '/columns/stories' - | '/$account/home' + | '/settings/$id/general' + | '/settings/$id/profile' + | '/settings/$id/relay' + | '/settings/$id/wallet' | '/columns/launchpad' - | '/columns/notification' | '/columns/onboarding' | '/columns/search' | '/columns/trending' @@ -890,38 +802,35 @@ export interface FileRouteTypes { | '/columns/create-newsfeed/users' | '/columns/groups/$id' | '/columns/interests/$id' + | '/columns/newsfeed/$id' + | '/columns/stories/$id' | '/columns/events/$id' + | '/columns/notification/$id' | '/columns/replies/$id' | '/columns/users/$id' fileRoutesByTo: FileRoutesByTo to: - | '/' | '/bootstrap-relays' - | '/loading' | '/set-group' | '/set-interest' | '/new' | '/reset' - | '/$account' - | '/$account/backup' | '/columns' | '/zap/$id' | '/auth/connect' | '/auth/import' - | '/auth/new' - | '/editor' - | '/$account/bitcoin-connect' - | '/$account/general' - | '/$account/profile' - | '/$account/relay' - | '/$account/wallet' + | '/auth/watch' + | '/set-signer/$id' + | '/settings/$id' + | '/' + | '/new-post' | '/columns/create-newsfeed' | '/columns/global' - | '/columns/newsfeed' - | '/columns/stories' - | '/$account/home' + | '/settings/$id/general' + | '/settings/$id/profile' + | '/settings/$id/relay' + | '/settings/$id/wallet' | '/columns/launchpad' - | '/columns/notification' | '/columns/onboarding' | '/columns/search' | '/columns/trending' @@ -929,41 +838,37 @@ export interface FileRouteTypes { | '/columns/create-newsfeed/users' | '/columns/groups/$id' | '/columns/interests/$id' + | '/columns/newsfeed/$id' + | '/columns/stories/$id' | '/columns/events/$id' + | '/columns/notification/$id' | '/columns/replies/$id' | '/columns/users/$id' id: | '__root__' - | '/' + | '/_layout' | '/bootstrap-relays' - | '/loading' | '/set-group' | '/set-interest' | '/new' | '/reset' - | '/$account' - | '/$account/_app' - | '/$account/backup' | '/columns' | '/columns/_layout' | '/zap/$id' - | '/$account/_settings' | '/auth/connect' | '/auth/import' - | '/auth/new' - | '/editor/' - | '/$account/_settings/bitcoin-connect' - | '/$account/_settings/general' - | '/$account/_settings/profile' - | '/$account/_settings/relay' - | '/$account/_settings/wallet' + | '/auth/watch' + | '/set-signer/$id' + | '/settings/$id' + | '/_layout/' + | '/new-post/' | '/columns/_layout/create-newsfeed' | '/columns/_layout/global' - | '/columns/_layout/newsfeed' - | '/columns/_layout/stories' - | '/$account/_app/home' + | '/settings/$id/general' + | '/settings/$id/profile' + | '/settings/$id/relay' + | '/settings/$id/wallet' | '/columns/_layout/launchpad' - | '/columns/_layout/notification' | '/columns/_layout/onboarding' | '/columns/_layout/search' | '/columns/_layout/trending' @@ -971,44 +876,47 @@ export interface FileRouteTypes { | '/columns/_layout/create-newsfeed/users' | '/columns/_layout/groups/$id' | '/columns/_layout/interests/$id' + | '/columns/_layout/newsfeed/$id' + | '/columns/_layout/stories/$id' | '/columns/_layout/events/$id' + | '/columns/_layout/notification/$id' | '/columns/_layout/replies/$id' | '/columns/_layout/users/$id' fileRoutesById: FileRoutesById } export interface RootRouteChildren { - IndexRoute: typeof IndexRoute + LayoutRoute: typeof LayoutRouteWithChildren BootstrapRelaysRoute: typeof BootstrapRelaysRoute - LoadingRoute: typeof LoadingRoute SetGroupRoute: typeof SetGroupRoute SetInterestRoute: typeof SetInterestRoute NewLazyRoute: typeof NewLazyRoute ResetLazyRoute: typeof ResetLazyRoute - AccountRoute: typeof AccountRouteWithChildren ColumnsRoute: typeof ColumnsRouteWithChildren ZapIdRoute: typeof ZapIdRoute AuthConnectLazyRoute: typeof AuthConnectLazyRoute AuthImportLazyRoute: typeof AuthImportLazyRoute - AuthNewLazyRoute: typeof AuthNewLazyRoute - EditorIndexRoute: typeof EditorIndexRoute + AuthWatchLazyRoute: typeof AuthWatchLazyRoute + SetSignerIdLazyRoute: typeof SetSignerIdLazyRoute + SettingsIdLazyRoute: typeof SettingsIdLazyRouteWithChildren + NewPostIndexRoute: typeof NewPostIndexRoute } const rootRouteChildren: RootRouteChildren = { - IndexRoute: IndexRoute, + LayoutRoute: LayoutRouteWithChildren, BootstrapRelaysRoute: BootstrapRelaysRoute, - LoadingRoute: LoadingRoute, SetGroupRoute: SetGroupRoute, SetInterestRoute: SetInterestRoute, NewLazyRoute: NewLazyRoute, ResetLazyRoute: ResetLazyRoute, - AccountRoute: AccountRouteWithChildren, ColumnsRoute: ColumnsRouteWithChildren, ZapIdRoute: ZapIdRoute, AuthConnectLazyRoute: AuthConnectLazyRoute, AuthImportLazyRoute: AuthImportLazyRoute, - AuthNewLazyRoute: AuthNewLazyRoute, - EditorIndexRoute: EditorIndexRoute, + AuthWatchLazyRoute: AuthWatchLazyRoute, + SetSignerIdLazyRoute: SetSignerIdLazyRoute, + SettingsIdLazyRoute: SettingsIdLazyRouteWithChildren, + NewPostIndexRoute: NewPostIndexRoute, } export const routeTree = rootRoute @@ -1023,31 +931,31 @@ export const routeTree = rootRoute "__root__": { "filePath": "__root.tsx", "children": [ - "/", + "/_layout", "/bootstrap-relays", - "/loading", "/set-group", "/set-interest", "/new", "/reset", - "/$account", "/columns", "/zap/$id", "/auth/connect", "/auth/import", - "/auth/new", - "/editor/" + "/auth/watch", + "/set-signer/$id", + "/settings/$id", + "/new-post/" ] }, - "/": { - "filePath": "index.tsx" + "/_layout": { + "filePath": "_layout.tsx", + "children": [ + "/_layout/" + ] }, "/bootstrap-relays": { "filePath": "bootstrap-relays.tsx" }, - "/loading": { - "filePath": "loading.tsx" - }, "/set-group": { "filePath": "set-group.tsx" }, @@ -1060,25 +968,6 @@ export const routeTree = rootRoute "/reset": { "filePath": "reset.lazy.tsx" }, - "/$account": { - "filePath": "$account", - "children": [ - "/$account/_app", - "/$account/backup", - "/$account/_settings" - ] - }, - "/$account/_app": { - "filePath": "$account/_app.tsx", - "parent": "/$account", - "children": [ - "/$account/_app/home" - ] - }, - "/$account/backup": { - "filePath": "$account/backup.tsx", - "parent": "/$account" - }, "/columns": { "filePath": "columns", "children": [ @@ -1091,16 +980,16 @@ export const routeTree = rootRoute "children": [ "/columns/_layout/create-newsfeed", "/columns/_layout/global", - "/columns/_layout/newsfeed", - "/columns/_layout/stories", "/columns/_layout/launchpad", - "/columns/_layout/notification", "/columns/_layout/onboarding", "/columns/_layout/search", "/columns/_layout/trending", "/columns/_layout/groups/$id", "/columns/_layout/interests/$id", + "/columns/_layout/newsfeed/$id", + "/columns/_layout/stories/$id", "/columns/_layout/events/$id", + "/columns/_layout/notification/$id", "/columns/_layout/replies/$id", "/columns/_layout/users/$id" ] @@ -1108,48 +997,33 @@ export const routeTree = rootRoute "/zap/$id": { "filePath": "zap.$id.tsx" }, - "/$account/_settings": { - "filePath": "$account/_settings.lazy.tsx", - "parent": "/$account", - "children": [ - "/$account/_settings/bitcoin-connect", - "/$account/_settings/general", - "/$account/_settings/profile", - "/$account/_settings/relay", - "/$account/_settings/wallet" - ] - }, "/auth/connect": { "filePath": "auth/connect.lazy.tsx" }, "/auth/import": { "filePath": "auth/import.lazy.tsx" }, - "/auth/new": { - "filePath": "auth/new.lazy.tsx" + "/auth/watch": { + "filePath": "auth/watch.lazy.tsx" }, - "/editor/": { - "filePath": "editor/index.tsx" + "/set-signer/$id": { + "filePath": "set-signer.$id.lazy.tsx" }, - "/$account/_settings/bitcoin-connect": { - "filePath": "$account/_settings/bitcoin-connect.tsx", - "parent": "/$account/_settings" + "/settings/$id": { + "filePath": "settings.$id.lazy.tsx", + "children": [ + "/settings/$id/general", + "/settings/$id/profile", + "/settings/$id/relay", + "/settings/$id/wallet" + ] }, - "/$account/_settings/general": { - "filePath": "$account/_settings/general.tsx", - "parent": "/$account/_settings" + "/_layout/": { + "filePath": "_layout/index.tsx", + "parent": "/_layout" }, - "/$account/_settings/profile": { - "filePath": "$account/_settings/profile.tsx", - "parent": "/$account/_settings" - }, - "/$account/_settings/relay": { - "filePath": "$account/_settings/relay.tsx", - "parent": "/$account/_settings" - }, - "/$account/_settings/wallet": { - "filePath": "$account/_settings/wallet.tsx", - "parent": "/$account/_settings" + "/new-post/": { + "filePath": "new-post/index.tsx" }, "/columns/_layout/create-newsfeed": { "filePath": "columns/_layout/create-newsfeed.tsx", @@ -1163,26 +1037,26 @@ export const routeTree = rootRoute "filePath": "columns/_layout/global.tsx", "parent": "/columns/_layout" }, - "/columns/_layout/newsfeed": { - "filePath": "columns/_layout/newsfeed.tsx", - "parent": "/columns/_layout" + "/settings/$id/general": { + "filePath": "settings.$id/general.tsx", + "parent": "/settings/$id" }, - "/columns/_layout/stories": { - "filePath": "columns/_layout/stories.tsx", - "parent": "/columns/_layout" + "/settings/$id/profile": { + "filePath": "settings.$id/profile.tsx", + "parent": "/settings/$id" }, - "/$account/_app/home": { - "filePath": "$account/_app.home.lazy.tsx", - "parent": "/$account/_app" + "/settings/$id/relay": { + "filePath": "settings.$id/relay.tsx", + "parent": "/settings/$id" + }, + "/settings/$id/wallet": { + "filePath": "settings.$id/wallet.tsx", + "parent": "/settings/$id" }, "/columns/_layout/launchpad": { "filePath": "columns/_layout/launchpad.lazy.tsx", "parent": "/columns/_layout" }, - "/columns/_layout/notification": { - "filePath": "columns/_layout/notification.lazy.tsx", - "parent": "/columns/_layout" - }, "/columns/_layout/onboarding": { "filePath": "columns/_layout/onboarding.lazy.tsx", "parent": "/columns/_layout" @@ -1211,10 +1085,22 @@ export const routeTree = rootRoute "filePath": "columns/_layout/interests.$id.tsx", "parent": "/columns/_layout" }, + "/columns/_layout/newsfeed/$id": { + "filePath": "columns/_layout/newsfeed.$id.tsx", + "parent": "/columns/_layout" + }, + "/columns/_layout/stories/$id": { + "filePath": "columns/_layout/stories.$id.tsx", + "parent": "/columns/_layout" + }, "/columns/_layout/events/$id": { "filePath": "columns/_layout/events.$id.lazy.tsx", "parent": "/columns/_layout" }, + "/columns/_layout/notification/$id": { + "filePath": "columns/_layout/notification.$id.lazy.tsx", + "parent": "/columns/_layout" + }, "/columns/_layout/replies/$id": { "filePath": "columns/_layout/replies.$id.lazy.tsx", "parent": "/columns/_layout" diff --git a/src/routes/$account/_app.lazy.tsx b/src/routes/$account/_app.lazy.tsx deleted file mode 100644 index 0498f8d4..00000000 --- a/src/routes/$account/_app.lazy.tsx +++ /dev/null @@ -1,117 +0,0 @@ -import { cn } from "@/commons"; -import { User } from "@/components/user"; -import { LumeWindow } from "@/system"; -import { CaretDown, Feather, MagnifyingGlass } from "@phosphor-icons/react"; -import { Outlet, createLazyFileRoute } from "@tanstack/react-router"; -import { Menu, MenuItem, PredefinedMenuItem } from "@tauri-apps/api/menu"; -import { writeText } from "@tauri-apps/plugin-clipboard-manager"; -import { memo, useCallback } from "react"; - -export const Route = createLazyFileRoute("/$account/_app")({ - component: Screen, -}); - -function Screen() { - const context = Route.useRouteContext(); - - return ( -
-
-
- -
- - -
-
-
-
-
- -
-
- ); -} - -const Account = memo(function Account() { - const params = Route.useParams(); - const navigate = Route.useNavigate(); - - const showContextMenu = useCallback( - async (e: React.MouseEvent) => { - e.preventDefault(); - - const menuItems = await Promise.all([ - MenuItem.new({ - text: "New Post", - action: () => LumeWindow.openEditor(), - }), - MenuItem.new({ - text: "Profile", - action: () => LumeWindow.openProfile(params.account), - }), - MenuItem.new({ - text: "Settings", - action: () => LumeWindow.openSettings(params.account), - }), - PredefinedMenuItem.new({ item: "Separator" }), - MenuItem.new({ - text: "Copy Public Key", - action: async () => await writeText(params.account), - }), - MenuItem.new({ - text: "Logout", - action: () => navigate({ to: "/" }), - }), - ]); - - const menu = await Menu.new({ - items: menuItems, - }); - - await menu.popup().catch((e) => console.error(e)); - }, - [params.account], - ); - - return ( - - ); -}); diff --git a/src/routes/$account/_app.tsx b/src/routes/$account/_app.tsx deleted file mode 100644 index b7b988e9..00000000 --- a/src/routes/$account/_app.tsx +++ /dev/null @@ -1,3 +0,0 @@ -import { createFileRoute } from "@tanstack/react-router"; - -export const Route = createFileRoute("/$account/_app")(); diff --git a/src/routes/$account/_settings/bitcoin-connect.lazy.tsx b/src/routes/$account/_settings/bitcoin-connect.lazy.tsx deleted file mode 100644 index aa073fd5..00000000 --- a/src/routes/$account/_settings/bitcoin-connect.lazy.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { commands } from "@/commands.gen"; -import { NostrAccount } from "@/system"; -import { Button } from "@getalby/bitcoin-connect-react"; -import { createLazyFileRoute } from "@tanstack/react-router"; -import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow"; - -export const Route = createLazyFileRoute("/$account/_settings/bitcoin-connect")( - { - component: Screen, - }, -); - -function Screen() { - const setNwcUri = async (uri: string) => { - const res = await commands.setWallet(uri); - - if (res.status === "ok") { - await getCurrentWebviewWindow().close(); - } else { - throw new Error(res.error); - } - }; - - return ( -
-
-
-

- Click to the button below to connect with your Bitcoin wallet. -

-
-
-
- ); -} diff --git a/src/routes/$account/_settings/general.lazy.tsx b/src/routes/$account/_settings/general.lazy.tsx deleted file mode 100644 index 8f8e033c..00000000 --- a/src/routes/$account/_settings/general.lazy.tsx +++ /dev/null @@ -1,185 +0,0 @@ -import { commands } from "@/commands.gen"; -import { appSettings } from "@/commons"; -import { Spinner } from "@/components"; -import * as Switch from "@radix-ui/react-switch"; -import { createLazyFileRoute } from "@tanstack/react-router"; -import { useStore } from "@tanstack/react-store"; -import { invoke } from "@tauri-apps/api/core"; -import { message } from "@tauri-apps/plugin-dialog"; -import { useCallback, useEffect, useState, useTransition } from "react"; - -type Theme = "auto" | "light" | "dark"; - -export const Route = createLazyFileRoute("/$account/_settings/general")({ - component: Screen, -}); - -function Screen() { - const [theme, setTheme] = useState(null); - const [isPending, startTransition] = useTransition(); - - const changeTheme = useCallback(async (theme: string) => { - if (theme === "auto" || theme === "light" || theme === "dark") { - invoke("plugin:theme|set_theme", { - theme: theme, - }).then(() => setTheme(theme)); - } - }, []); - - const updateSettings = () => { - startTransition(async () => { - const newSettings = JSON.stringify(appSettings.state); - const res = await commands.setUserSettings(newSettings); - - if (res.status === "error") { - await message(res.error, { kind: "error" }); - } - - return; - }); - }; - - useEffect(() => { - invoke("plugin:theme|get_theme").then((data) => setTheme(data as Theme)); - }, []); - - return ( -
-
-
-

- General -

-
- - - -
-
-
-

- Appearance -

-
-
-
-

Appearance

-

- Change app theme -

-
-
- -
-
- - - -
-
-
-

- Privacy & Performance -

-
- - - -
-
-
-
-
- -
-
- ); -} - -function Setting({ - label, - name, - description, -}: { label: string; name: string; description: string }) { - const state = useStore(appSettings, (state) => state[label]); - - const toggle = useCallback(() => { - appSettings.setState((state) => { - return { - ...state, - [label]: !state[label], - }; - }); - }, []); - - return ( -
-
-

{name}

-

- {description} -

-
-
- toggle()} - className="relative h-7 w-12 shrink-0 cursor-default rounded-full bg-black/10 outline-none data-[state=checked]:bg-blue-500 dark:bg-white/10" - > - - -
-
- ); -} diff --git a/src/routes/$account/_settings/general.tsx b/src/routes/$account/_settings/general.tsx deleted file mode 100644 index fbc2049f..00000000 --- a/src/routes/$account/_settings/general.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { commands } from "@/commands.gen"; -import { appSettings } from "@/commons"; -import { createFileRoute } from "@tanstack/react-router"; - -export const Route = createFileRoute("/$account/_settings/general")({ - beforeLoad: async () => { - const res = await commands.getUserSettings(); - - if (res.status === "ok") { - appSettings.setState((state) => { - return { ...state, ...res.data }; - }); - } else { - throw new Error(res.error); - } - }, -}); diff --git a/src/routes/$account/_settings/relay.tsx b/src/routes/$account/_settings/relay.tsx deleted file mode 100644 index 89754b4d..00000000 --- a/src/routes/$account/_settings/relay.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { commands } from "@/commands.gen"; -import { createFileRoute } from "@tanstack/react-router"; - -export const Route = createFileRoute("/$account/_settings/relay")({ - beforeLoad: async () => { - const res = await commands.getRelays(); - - if (res.status === "ok") { - const relayList = res.data; - return { relayList }; - } else { - throw new Error(res.error); - } - }, -}); diff --git a/src/routes/$account/_settings/wallet.lazy.tsx b/src/routes/$account/_settings/wallet.lazy.tsx deleted file mode 100644 index d991fed7..00000000 --- a/src/routes/$account/_settings/wallet.lazy.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { commands } from "@/commands.gen"; -import { createLazyFileRoute, redirect } from "@tanstack/react-router"; - -export const Route = createLazyFileRoute("/$account/_settings/wallet")({ - component: Screen, -}); - -function Screen() { - const { account } = Route.useParams(); - const { balance } = Route.useRouteContext(); - - const disconnect = async () => { - const res = await commands.removeWallet(); - - if (res.status === "ok") { - window.localStorage.removeItem("bc:config"); - return redirect({ to: "/$account/bitcoin-connect", params: { account } }); - } else { - throw new Error(res.error); - } - }; - - return ( -
-
-
-
-
-

Connection

-
-
- -
-
-
-
-
-
-

Current Balance

-
-
- â‚¿ {balance.bitcoinFormatted} -
-
-
-
-
- ); -} diff --git a/src/routes/$account/_settings/wallet.tsx b/src/routes/$account/_settings/wallet.tsx deleted file mode 100644 index adf36b71..00000000 --- a/src/routes/$account/_settings/wallet.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { commands } from "@/commands.gen"; -import { getBitcoinDisplayValues } from "@/commons"; -import { createFileRoute, redirect } from "@tanstack/react-router"; - -export const Route = createFileRoute("/$account/_settings/wallet")({ - beforeLoad: async ({ params }) => { - const query = await commands.loadWallet(); - - if (query.status === "ok") { - const wallet = Number.parseInt(query.data); - const balance = getBitcoinDisplayValues(wallet); - - return { balance }; - } else { - throw redirect({ - to: "/$account/bitcoin-connect", - params: { account: params.account }, - }); - } - }, -}); diff --git a/src/routes/$account/backup.tsx b/src/routes/$account/backup.tsx deleted file mode 100644 index 641c793d..00000000 --- a/src/routes/$account/backup.tsx +++ /dev/null @@ -1,177 +0,0 @@ -import { displayNsec } from "@/commons"; -import { Spinner } from "@/components"; -import { Check } from "@phosphor-icons/react"; -import * as Checkbox from "@radix-ui/react-checkbox"; -import { createFileRoute, useNavigate } from "@tanstack/react-router"; -import { invoke } from "@tauri-apps/api/core"; -import { writeText } from "@tauri-apps/plugin-clipboard-manager"; -import { message } from "@tauri-apps/plugin-dialog"; -import { useState } from "react"; - -export const Route = createFileRoute("/$account/backup")({ - component: Screen, -}); - -function Screen() { - const { account } = Route.useParams(); - const navigate = useNavigate(); - - const [key, setKey] = useState(null); - const [passphase, setPassphase] = useState(""); - const [copied, setCopied] = useState(false); - const [loading, setLoading] = useState(false); - const [confirm, setConfirm] = useState({ c1: false, c2: false }); - - const submit = async () => { - try { - if (key) { - if (!confirm.c1 || !confirm.c2) { - return await message("You need to confirm before continue", { - title: "Backup", - kind: "info", - }); - } - - navigate({ to: "/", replace: true }); - } - - // start loading - setLoading(true); - - invoke("get_encrypted_key", { - npub: account, - password: passphase, - }).then((encrypted: string) => { - // update state - setKey(encrypted); - setLoading(false); - }); - } catch (e) { - setLoading(false); - await message(String(e), { - title: "Backup", - kind: "error", - }); - } - }; - - const copyKey = async () => { - try { - await writeText(key); - setCopied(true); - } catch (e) { - await message(String(e), { - title: "Backup", - kind: "error", - }); - } - }; - - return ( -
-
-

Backup your sign in keys

-

- It's use for login to Lume or other Nostr clients. You will lost - access to your account if you lose this key. -

-
-
-
- -
- setPassphase(e.target.value)} - className="w-full px-3 border-transparent rounded-lg h-11 bg-neutral-100 placeholder:text-neutral-600 focus:border-blue-500 focus:ring-0 dark:bg-white/10 dark:placeholder:text-neutral-400" - /> -
-
- {key ? ( - <> -
- -
- - -
-
-
-
Before you continue:
-
-
- - setConfirm((state) => ({ ...state, c1: !state.c1 })) - } - className="flex items-center justify-center rounded-md outline-none appearance-none size-6 bg-neutral-100 dark:bg-white/10 dark:hover:bg-white/20" - id="confirm1" - > - - - - - -
-
- - setConfirm((state) => ({ ...state, c2: !state.c2 })) - } - className="flex items-center justify-center rounded-md outline-none appearance-none size-6 bg-neutral-100 dark:bg-white/10 dark:hover:bg-white/20" - id="confirm2" - > - - - - - -
-
-
- - ) : null} -
- -
-
-
- ); -} diff --git a/src/routes/__root.tsx b/src/routes/__root.tsx index 7882d0d9..09056444 100644 --- a/src/routes/__root.tsx +++ b/src/routes/__root.tsx @@ -3,13 +3,13 @@ import { appSettings } from "@/commons"; import { Spinner } from "@/components"; import type { QueryClient } from "@tanstack/react-query"; import { Outlet, createRootRouteWithContext } from "@tanstack/react-router"; -import { listen } from "@tauri-apps/api/event"; import type { OsType } from "@tauri-apps/plugin-os"; import { useEffect } from "react"; interface RouterContext { queryClient: QueryClient; platform: OsType; + accounts: string[]; } export const Route = createRootRouteWithContext()({ @@ -33,8 +33,9 @@ function Screen() { }, []); useEffect(() => { - const unlisten = listen("synchronized", async () => { - await queryClient.invalidateQueries(); + const unlisten = events.negentropyEvent.listen(async (data) => { + const queryKey = [data.payload.kind.toLowerCase()]; + await queryClient.invalidateQueries({ queryKey }); }); return () => { diff --git a/src/routes/_layout.lazy.tsx b/src/routes/_layout.lazy.tsx new file mode 100644 index 00000000..5e084db2 --- /dev/null +++ b/src/routes/_layout.lazy.tsx @@ -0,0 +1,195 @@ +import { commands } from "@/commands.gen"; +import { cn } from "@/commons"; +import { PublishIcon } from "@/components"; +import { User } from "@/components/user"; +import { LumeWindow } from "@/system"; +import { MagnifyingGlass, Plus } from "@phosphor-icons/react"; +import { useQuery } from "@tanstack/react-query"; +import { Link, Outlet, createLazyFileRoute } from "@tanstack/react-router"; +import { listen } from "@tauri-apps/api/event"; +import { Menu, MenuItem, PredefinedMenuItem } from "@tauri-apps/api/menu"; +import { writeText } from "@tauri-apps/plugin-clipboard-manager"; +import { memo, useCallback, useEffect, useState } from "react"; + +export const Route = createLazyFileRoute("/_layout")({ + component: Layout, +}); + +function Layout() { + return ( +
+ +
+ +
+
+ ); +} + +function Topbar() { + const { platform, accounts } = Route.useRouteContext(); + + return ( +
+
+ {accounts?.map((account) => ( + + ))} + + + +
+
+ {accounts?.length ? ( +
+ + +
+ ) : null} +
+
+
+ ); +} + +const NegentropyBadge = memo(function NegentropyBadge() { + const [process, setProcess] = useState(null); + + useEffect(() => { + const unlisten = listen("negentropy", async (data) => { + if (data.payload === "Ok") { + setProcess(null); + } else { + setProcess(data.payload); + } + }); + + return () => { + unlisten.then((f) => f()); + }; + }, []); + + if (!process) { + return null; + } + + return ( +
+ {process ? ( + + {process.message} + {process.total_event > 0 ? ` / ${process.total_event}` : null} + + ) : ( + "Syncing" + )} +
+ ); +}); + +function Account({ pubkey }: { pubkey: string }) { + const navigate = Route.useNavigate(); + const context = Route.useRouteContext(); + + const { data: isActive } = useQuery({ + queryKey: ["signer", pubkey], + queryFn: async () => { + const res = await commands.hasSigner(pubkey); + + if (res.status === "ok") { + return res.data; + } else { + return false; + } + }, + }); + + const showContextMenu = useCallback( + async (e: React.MouseEvent) => { + e.preventDefault(); + + const items = await Promise.all([ + MenuItem.new({ + text: "View Profile", + action: () => LumeWindow.openProfile(pubkey), + }), + MenuItem.new({ + text: "Copy Public Key", + action: async () => await writeText(pubkey), + }), + PredefinedMenuItem.new({ item: "Separator" }), + MenuItem.new({ + text: "Settings", + action: () => LumeWindow.openSettings(pubkey), + }), + PredefinedMenuItem.new({ item: "Separator" }), + MenuItem.new({ + text: "Logout", + action: async () => { + const res = await commands.deleteAccount(pubkey); + + if (res.status === "ok") { + const newAccounts = context.accounts.filter( + (account) => account !== pubkey, + ); + + if (newAccounts.length < 1) { + navigate({ to: "/", replace: true }); + } + } + }, + }), + ]); + + const menu = await Menu.new({ items }); + + await menu.popup().catch((e) => console.error(e)); + }, + [pubkey], + ); + + return ( + + ); +} diff --git a/src/routes/_layout.tsx b/src/routes/_layout.tsx new file mode 100644 index 00000000..3dcb8189 --- /dev/null +++ b/src/routes/_layout.tsx @@ -0,0 +1,14 @@ +import { commands } from "@/commands.gen"; +import { createFileRoute, redirect } from "@tanstack/react-router"; + +export const Route = createFileRoute("/_layout")({ + beforeLoad: async () => { + const accounts = await commands.getAccounts(); + + if (!accounts.length) { + throw redirect({ to: "/new", replace: true }); + } + + return { accounts }; + }, +}); diff --git a/src/routes/$account/_app.home.lazy.tsx b/src/routes/_layout/index.lazy.tsx similarity index 81% rename from src/routes/$account/_app.home.lazy.tsx rename to src/routes/_layout/index.lazy.tsx index c565718a..e061d82e 100644 --- a/src/routes/$account/_app.home.lazy.tsx +++ b/src/routes/_layout/index.lazy.tsx @@ -1,13 +1,11 @@ import { appColumns } from "@/commons"; -import { Spinner } from "@/components"; -import { Column } from "@/components/column"; +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 { createLazyFileRoute } from "@tanstack/react-router"; import { useStore } from "@tanstack/react-store"; import { listen } from "@tauri-apps/api/event"; -import { Menu, MenuItem, PredefinedMenuItem } from "@tauri-apps/api/menu"; import { resolveResource } from "@tauri-apps/api/path"; import { getCurrentWindow } from "@tauri-apps/api/window"; import { readTextFile } from "@tauri-apps/plugin-fs"; @@ -23,12 +21,11 @@ import { import { createPortal } from "react-dom"; import { useDebouncedCallback } from "use-debounce"; -export const Route = createLazyFileRoute("/$account/_app/home")({ +export const Route = createLazyFileRoute("/_layout/")({ component: Screen, }); function Screen() { - const params = Route.useParams(); const columns = useStore(appColumns, (state) => state); const [emblaRef, emblaApi] = useEmblaCarousel({ @@ -158,9 +155,7 @@ function Screen() { } if (!columns.length) { - const prevColumns = window.localStorage.getItem( - `${params.account}_columns`, - ); + const prevColumns = window.localStorage.getItem("columns"); if (!prevColumns) { getSystemColumns(); @@ -169,10 +164,7 @@ function Screen() { appColumns.setState(() => parsed); } } else { - window.localStorage.setItem( - `${params.account}_columns`, - JSON.stringify(columns), - ); + window.localStorage.setItem("columns", JSON.stringify(columns)); } }, [columns.length]); @@ -193,7 +185,7 @@ function Screen() {
- + - ); -} - function Toolbar({ children }: { children: ReactNode[] }) { const [domReady, setDomReady] = useState(false); diff --git a/src/routes/_layout/index.tsx b/src/routes/_layout/index.tsx new file mode 100644 index 00000000..c67745be --- /dev/null +++ b/src/routes/_layout/index.tsx @@ -0,0 +1,3 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/_layout/')() diff --git a/src/routes/auth/connect.lazy.tsx b/src/routes/auth/connect.lazy.tsx index 8ecd58a8..d25e0c67 100644 --- a/src/routes/auth/connect.lazy.tsx +++ b/src/routes/auth/connect.lazy.tsx @@ -44,20 +44,22 @@ function Screen() { return (
-
+
-

Nostr Connect

+

+ Continue with Nostr Connect +

-
+
@@ -68,7 +70,7 @@ function Screen() { placeholder="bunker://..." value={uri} onChange={(e) => setUri(e.target.value)} - className="pl-3 pr-12 rounded-lg w-full h-10 bg-transparent border border-neutral-200 dark:border-neutral-500 focus:border-blue-500 focus:outline-none placeholder:text-neutral-400" + className="pl-3 pr-12 rounded-lg w-full h-10 bg-transparent border border-neutral-200 dark:border-neutral-700 focus:border-blue-500 focus:outline-none placeholder:text-neutral-400" />
) : null} diff --git a/src/routes/auth/new.lazy.tsx b/src/routes/auth/new.lazy.tsx deleted file mode 100644 index 0a11af5d..00000000 --- a/src/routes/auth/new.lazy.tsx +++ /dev/null @@ -1,166 +0,0 @@ -import { commands } from "@/commands.gen"; -import { upload } from "@/commons"; -import { Frame, GoBack, Spinner } from "@/components"; -import { Plus } from "@phosphor-icons/react"; -import { createLazyFileRoute } from "@tanstack/react-router"; -import { message } from "@tauri-apps/plugin-dialog"; -import { useState, useTransition } from "react"; - -export const Route = createLazyFileRoute("/auth/new")({ - component: Screen, -}); - -function Screen() { - const navigate = Route.useNavigate(); - - const [password, setPassword] = useState(""); - const [picture, setPicture] = useState(""); - const [name, setName] = useState(""); - const [about, setAbout] = useState(""); - const [isPending, startTransition] = useTransition(); - - const uploadAvatar = async () => { - const file = await upload(); - - if (file) { - setPicture(file); - } else { - return; - } - }; - - const submit = () => { - startTransition(async () => { - if (!name.length) { - await message("Please add your name", { - title: "New Identity", - kind: "info", - }); - return; - } - - if (!password.length) { - await message("You must set password to secure your account", { - title: "New Identity", - kind: "info", - }); - return; - } - - const res = await commands.createAccount(name, picture, about, password); - - if (res.status === "ok") { - navigate({ - to: "/", - replace: true, - }); - } else { - await message(res.error, { - title: "New Identity", - kind: "error", - }); - return; - } - }); - }; - - return ( -
-
-
-

New Identity

-
-
- -
- {picture.length ? ( - avatar - ) : null} - -
-
- - setName(e.target.value)} - placeholder="e.g. Alice" - spellCheck={false} - className="px-3 rounded-lg h-10 bg-transparent border border-neutral-200 dark:border-neutral-500 focus:ring-0 focus:border-blue-500 focus:outline-none placeholder:text-neutral-400 dark:text-neutral-200" - /> -
-
- -