diff --git a/package.json b/package.json index 14817e46..9d9b9bde 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ }, "dependencies": { "@getalby/bitcoin-connect-react": "^3.6.1", + "@phosphor-icons/react": "^2.1.7", "@radix-ui/react-avatar": "^1.1.0", "@radix-ui/react-checkbox": "^1.1.1", "@radix-ui/react-popover": "^1.1.1", @@ -42,8 +43,8 @@ "nanoid": "^5.0.7", "nostr-tools": "^2.7.2", "react": "19.0.0-rc-d025ddd3-20240722", - "react-dom": "19.0.0-rc-d025ddd3-20240722", "react-currency-input-field": "^3.8.0", + "react-dom": "19.0.0-rc-d025ddd3-20240722", "react-hook-form": "^7.52.2", "react-i18next": "^15.0.1", "react-string-replace": "^1.1.1", @@ -53,9 +54,6 @@ "virtua": "^0.33.5" }, "devDependencies": { - "@types/react": "npm:types-react@19.0.0-rc.1", - "@types/react-dom": "npm:types-react-dom@19.0.0-rc.1", - "babel-plugin-react-compiler": "0.0.0-experimental-696af53-20240625", "@biomejs/biome": "^1.8.3", "@evilmartians/harmony": "^1.2.0", "@tailwindcss/forms": "^0.5.7", @@ -63,8 +61,11 @@ "@tanstack/router-devtools": "^1.47.1", "@tanstack/router-plugin": "^1.47.0", "@tauri-apps/cli": "2.0.0-rc.1", + "@types/react": "npm:types-react@19.0.0-rc.1", + "@types/react-dom": "npm:types-react-dom@19.0.0-rc.1", "@vitejs/plugin-react": "^4.3.1", "autoprefixer": "^10.4.20", + "babel-plugin-react-compiler": "0.0.0-experimental-696af53-20240625", "clsx": "^2.1.1", "postcss": "^8.4.41", "tailwind-gradient-mask-image": "^1.2.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8f30fb25..3f560b36 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: '@getalby/bitcoin-connect-react': specifier: ^3.6.1 version: 3.6.1(immer@10.1.1)(react@19.0.0-rc-d025ddd3-20240722)(types-react@19.0.0-rc.1)(typescript@5.5.4) + '@phosphor-icons/react': + specifier: ^2.1.7 + version: 2.1.7(react-dom@19.0.0-rc-d025ddd3-20240722(react@19.0.0-rc-d025ddd3-20240722))(react@19.0.0-rc-d025ddd3-20240722) '@radix-ui/react-avatar': specifier: ^1.1.0 version: 1.1.0(react-dom@19.0.0-rc-d025ddd3-20240722(react@19.0.0-rc-d025ddd3-20240722))(react@19.0.0-rc-d025ddd3-20240722)(types-react-dom@19.0.0-rc.1)(types-react@19.0.0-rc.1) @@ -607,6 +610,13 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} + '@phosphor-icons/react@2.1.7': + resolution: {integrity: sha512-g2e2eVAn1XG2a+LI09QU3IORLhnFNAFkNbo2iwbX6NOKSLOwvEMmTa7CgOzEbgNWR47z8i8kwjdvYZ5fkGx1mQ==} + engines: {node: '>=10'} + peerDependencies: + react: '>= 16.8' + react-dom: '>= 16.8' + '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} @@ -2604,6 +2614,11 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.17.1 + '@phosphor-icons/react@2.1.7(react-dom@19.0.0-rc-d025ddd3-20240722(react@19.0.0-rc-d025ddd3-20240722))(react@19.0.0-rc-d025ddd3-20240722)': + dependencies: + react: 19.0.0-rc-d025ddd3-20240722 + react-dom: 19.0.0-rc-d025ddd3-20240722(react@19.0.0-rc-d025ddd3-20240722) + '@pkgjs/parseargs@0.11.0': optional: true diff --git a/src-tauri/src/commands/keys.rs b/src-tauri/src/commands/account.rs similarity index 66% rename from src-tauri/src/commands/keys.rs rename to src-tauri/src/commands/account.rs index 91a15587..d33fb4a9 100644 --- a/src-tauri/src/commands/keys.rs +++ b/src-tauri/src/commands/account.rs @@ -1,29 +1,30 @@ -#[cfg(target_os = "macos")] -use crate::commands::tray::create_tray_panel; -use crate::common::{get_user_settings, init_nip65, parse_event}; -use crate::{Nostr, RichEvent, NEWSFEED_NEG_LIMIT, NOTIFICATION_NEG_LIMIT}; - use keyring::Entry; use keyring_search::{Limit, List, Search}; use nostr_sdk::prelude::*; -use serde::Serialize; +use serde::{Deserialize, Serialize}; use specta::Type; -use std::collections::HashSet; -use std::time::Duration; +use std::{collections::HashSet, str::FromStr, time::Duration}; use tauri::{Emitter, EventTarget, Manager, State}; use tauri_plugin_notification::NotificationExt; -#[derive(Serialize, Type)] -pub struct Account { - npub: String, - nsec: String, +// #[cfg(target_os = "macos")] +// use crate::commands::tray::create_tray_panel; +use crate::{ + common::{get_user_settings, init_nip65, parse_event}, + Nostr, RichEvent, NEWSFEED_NEG_LIMIT, NOTIFICATION_NEG_LIMIT, +}; + +#[derive(Debug, Clone, Serialize, Deserialize, Type)] +struct Account { + password: String, + nostr_connect: Option, } #[tauri::command] #[specta::specta] pub fn get_accounts() -> Vec { let search = Search::new().expect("Unexpected."); - let results = search.by_service("Lume"); + let results = search.by_service("Lume Secret Storage"); let list = List::list_credentials(&results, Limit::All); let accounts: HashSet = list .split_whitespace() @@ -36,76 +37,96 @@ pub fn get_accounts() -> Vec { #[tauri::command] #[specta::specta] -pub fn create_account() -> Result { +pub async fn create_account( + name: String, + about: String, + picture: String, + password: String, + state: State<'_, Nostr>, +) -> Result { + let client = &state.client; let keys = Keys::generate(); - let public_key = keys.public_key(); - let secret_key = keys.secret_key().unwrap(); - - let result = Account { - npub: public_key.to_bech32().unwrap(), - nsec: secret_key.to_bech32().unwrap(), + let npub = keys.public_key().to_bech32().map_err(|e| e.to_string())?; + let secret_key = keys.secret_key().map_err(|e| e.to_string())?; + 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); - Ok(result) -} + let signer = NostrSigner::Keys(keys); -#[tauri::command] -#[specta::specta] -pub fn get_private_key(npub: &str) -> Result { - let keyring = Entry::new("Lume", npub).unwrap(); - - if let Ok(nsec) = keyring.get_password() { - let secret_key = SecretKey::from_bech32(nsec).unwrap(); - Ok(secret_key.to_bech32().unwrap()) - } else { - Err("Key not found".into()) + // 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 save_account( - nsec: &str, - password: &str, +pub async fn import_account( + key: String, + password: Option, state: State<'_, Nostr>, ) -> Result { - let secret_key = if nsec.starts_with("ncryptsec") { - let encrypted_key = EncryptedSecretKey::from_bech32(nsec).unwrap(); - encrypted_key - .to_secret_key(password) - .map_err(|err| err.to_string()) - } else { - SecretKey::from_bech32(nsec).map_err(|err| err.to_string()) - }; + let client = &state.client; + let secret_key = SecretKey::from_bech32(key).map_err(|err| err.to_string())?; + let keys = Keys::new(secret_key.clone()); + let npub = keys.public_key().to_bech32().unwrap(); - match secret_key { - Ok(val) => { - let nostr_keys = Keys::new(val); - let npub = nostr_keys.public_key().to_bech32().unwrap(); - let nsec = nostr_keys.secret_key().unwrap().to_bech32().unwrap(); + let enc_bech32 = match password { + Some(pw) => { + let enc = EncryptedSecretKey::new(&secret_key, pw, 16, KeySecurity::Medium) + .map_err(|err| err.to_string())?; + + enc.to_bech32().map_err(|err| err.to_string())? + } + None => secret_key.to_bech32().map_err(|err| err.to_string())?, + }; - let keyring = Entry::new("Lume", &npub).unwrap(); - let _ = keyring.set_password(&nsec); + 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(nostr_keys); - let client = &state.client; + let signer = NostrSigner::Keys(keys); - // Update client's signer - client.set_signer(Some(signer)).await; + // Update client's signer + client.set_signer(Some(signer)).await; - Ok(npub) - } - Err(msg) => Err(msg), - } + Ok(npub) } #[tauri::command] #[specta::specta] -pub async fn connect_remote_account(uri: &str, state: State<'_, Nostr>) -> Result { +pub async fn connect_account(uri: String, state: State<'_, Nostr>) -> Result { let client = &state.client; - match NostrConnectURI::parse(uri) { + match NostrConnectURI::parse(uri.clone()) { Ok(bunker_uri) => { + // Local user let app_keys = Keys::generate(); let app_secret = app_keys.secret_key().unwrap().to_string(); @@ -115,8 +136,22 @@ pub async fn connect_remote_account(uri: &str, state: State<'_, Nostr>) -> Resul match Nip46Signer::new(bunker_uri, app_keys, Duration::from_secs(120), None).await { Ok(signer) => { - let keyring = Entry::new("Lume", &remote_npub).unwrap(); - let _ = keyring.set_password(&app_secret); + let mut url = Url::parse(&uri).unwrap(); + let query: Vec<(String, String)> = url + .query_pairs() + .filter(|(name, _)| name != "secret") + .map(|(name, value)| (name.into_owned(), value.into_owned())) + .collect(); + url.query_pairs_mut().clear().extend_pairs(&query); + + let key = format!("{}_nostrconnect", remote_npub); + let keyring = Entry::new("Lume Secret Storage", &key).unwrap(); + let account = Account { + password: app_secret, + nostr_connect: Some(url.to_string()), + }; + let j = serde_json::to_string(&account).map_err(|e| e.to_string())?; + let _ = keyring.set_password(&j); // Update signer let _ = client.set_signer(Some(signer.into())).await; @@ -132,75 +167,74 @@ pub async fn connect_remote_account(uri: &str, state: State<'_, Nostr>) -> Resul #[tauri::command] #[specta::specta] -pub async fn get_encrypted_key(npub: &str, password: &str) -> Result { - let keyring = Entry::new("Lume", npub).unwrap(); +pub fn delete_account(id: String) -> Result<(), String> { + let keyring = Entry::new("Lume Secret Storage", &id).map_err(|e| e.to_string())?; + let _ = keyring.delete_credential(); - if let Ok(nsec) = keyring.get_password() { - let secret_key = SecretKey::from_bech32(nsec).unwrap(); - let new_key = EncryptedSecretKey::new(&secret_key, password, 16, KeySecurity::Medium); - - if let Ok(key) = new_key { - Ok(key.to_bech32().unwrap()) - } else { - Err("Encrypt key failed".into()) - } - } else { - Err("Key not found".into()) - } + Ok(()) } #[tauri::command] #[specta::specta] -pub async fn load_account( - npub: &str, - bunker: Option<&str>, +pub async fn login( + account: String, + password: String, state: State<'_, Nostr>, app: tauri::AppHandle, -) -> Result { +) -> Result { let handle = app.clone(); let client = &state.client; - let keyring = Entry::new("Lume", npub).unwrap(); + let keyring = Entry::new("Lume Secret Storage", &account).map_err(|e| e.to_string())?; - let password = match keyring.get_password() { - Ok(pw) => pw, - Err(_) => return Err("Cancelled".into()), + let account = match keyring.get_password() { + Ok(pw) => { + let account: Account = serde_json::from_str(&pw).map_err(|e| e.to_string())?; + account + } + Err(e) => return Err(e.to_string()), }; - match bunker { - Some(uri) => { - let app_keys = - Keys::parse(password).expect("Secret Key is modified, please check again."); - - match NostrConnectURI::parse(uri) { - Ok(bunker_uri) => { - match Nip46Signer::new(bunker_uri, app_keys, Duration::from_secs(30), None) - .await - { - Ok(signer) => client.set_signer(Some(signer.into())).await, - Err(err) => return Err(err.to_string()), - } - } - Err(err) => return Err(err.to_string()), - } - } + let public_key = match account.nostr_connect { None => { - let keys = Keys::parse(password).expect("Secret Key is modified, please check again."); + let ncryptsec = + EncryptedSecretKey::from_bech32(account.password).map_err(|e| e.to_string())?; + let secret_key = ncryptsec + .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; + + public_key } - } + 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).await { + Ok(signer) => { + // Update signer + client.set_signer(Some(signer.into())).await; + public_key + } + Err(e) => return Err(e.to_string()), + } + } + }; // Connect to user's relay (NIP-65) init_nip65(client).await; // Create tray (macOS) - #[cfg(target_os = "macos")] - create_tray_panel(npub, &handle); + // #[cfg(target_os = "macos")] + // create_tray_panel(&public_key.to_bech32().unwrap(), &handle); // Get user's contact list - if let Ok(contacts) = client.get_contact_list(None).await { + if let Ok(contacts) = client.get_contact_list(Some(Duration::from_secs(5))).await { *state.contact_list.lock().unwrap() = contacts }; @@ -418,5 +452,5 @@ pub async fn load_account( .await }); - Ok(true) + Ok(public_key) } diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs index 082d5980..bacd0a99 100644 --- a/src-tauri/src/commands/mod.rs +++ b/src-tauri/src/commands/mod.rs @@ -1,5 +1,5 @@ +pub mod account; pub mod event; -pub mod keys; pub mod metadata; pub mod relay; #[cfg(target_os = "macos")] diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 3abe5164..2280b6bc 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -11,7 +11,7 @@ extern crate objc; #[cfg(target_os = "macos")] use border::WebviewWindowExt as BorderWebviewWindowExt; -use commands::{event::*, keys::*, metadata::*, relay::*, window::*}; +use commands::{account::*, event::*, metadata::*, relay::*, window::*}; use nostr_sdk::prelude::*; use serde::{Deserialize, Serialize}; use specta::Type; @@ -85,11 +85,10 @@ fn main() { save_bootstrap_relays, get_accounts, create_account, - save_account, - get_encrypted_key, - get_private_key, - connect_remote_account, - load_account, + import_account, + connect_account, + delete_account, + login, get_current_profile, get_profile, get_contact_list, diff --git a/src/commands.gen.ts b/src/commands.gen.ts index e63cfe3c..80de1a4f 100644 --- a/src/commands.gen.ts +++ b/src/commands.gen.ts @@ -48,49 +48,41 @@ async saveBootstrapRelays(relays: string) : Promise> { async getAccounts() : Promise { return await TAURI_INVOKE("get_accounts"); }, -async createAccount() : Promise> { +async createAccount(name: string, about: string, picture: string, password: string) : Promise> { try { - return { status: "ok", data: await TAURI_INVOKE("create_account") }; + return { status: "ok", data: await TAURI_INVOKE("create_account", { name, about, picture, password }) }; } catch (e) { if(e instanceof Error) throw e; else return { status: "error", error: e as any }; } }, -async saveAccount(nsec: string, password: string) : Promise> { +async importAccount(key: string, password: string | null) : Promise> { try { - return { status: "ok", data: await TAURI_INVOKE("save_account", { nsec, password }) }; + return { status: "ok", data: await TAURI_INVOKE("import_account", { key, password }) }; } catch (e) { if(e instanceof Error) throw e; else return { status: "error", error: e as any }; } }, -async getEncryptedKey(npub: string, password: string) : Promise> { +async connectAccount(uri: string) : Promise> { try { - return { status: "ok", data: await TAURI_INVOKE("get_encrypted_key", { npub, password }) }; + return { status: "ok", data: await TAURI_INVOKE("connect_account", { uri }) }; } catch (e) { if(e instanceof Error) throw e; else return { status: "error", error: e as any }; } }, -async getPrivateKey(npub: string) : Promise> { +async deleteAccount(id: string) : Promise> { try { - return { status: "ok", data: await TAURI_INVOKE("get_private_key", { npub }) }; + return { status: "ok", data: await TAURI_INVOKE("delete_account", { id }) }; } catch (e) { if(e instanceof Error) throw e; else return { status: "error", error: e as any }; } }, -async connectRemoteAccount(uri: string) : Promise> { +async login(account: string, password: string) : Promise> { try { - return { status: "ok", data: await TAURI_INVOKE("connect_remote_account", { uri }) }; -} catch (e) { - if(e instanceof Error) throw e; - else return { status: "error", error: e as any }; -} -}, -async loadAccount(npub: string, bunker: string | null) : Promise> { - try { - return { status: "ok", data: await TAURI_INVOKE("load_account", { npub, bunker }) }; + return { status: "ok", data: await TAURI_INVOKE("login", { account, password }) }; } catch (e) { if(e instanceof Error) throw e; else return { status: "error", error: e as any }; @@ -461,7 +453,6 @@ async setBadge(count: number) : Promise { /** user-defined types **/ -export type Account = { npub: string; nsec: string } export type Column = { label: string; url: string; x: number; y: number; width: number; height: number } export type Meta = { content: string; images: string[]; videos: string[]; events: string[]; mentions: string[]; hashtags: string[] } export type Relays = { connected: string[]; read: string[] | null; write: string[] | null; both: string[] | null } diff --git a/src/components/back.tsx b/src/components/back.tsx new file mode 100644 index 00000000..b15c5c73 --- /dev/null +++ b/src/components/back.tsx @@ -0,0 +1,20 @@ +import { cn } from "@/commons"; +import { useRouter } from "@tanstack/react-router"; +import type { ReactNode } from "react"; + +export function GoBack({ + children, + className, +}: { children: ReactNode | ReactNode[]; className?: string }) { + const { history } = useRouter(); + + return ( + + ); +} diff --git a/src/components/frame.tsx b/src/components/frame.tsx new file mode 100644 index 00000000..d5b7b109 --- /dev/null +++ b/src/components/frame.tsx @@ -0,0 +1,27 @@ +import { cn } from "@/commons"; +import { useRouteContext } from "@tanstack/react-router"; +import type { ReactNode } from "react"; + +export function Frame({ + children, + shadow, + className, +}: { children: ReactNode; shadow?: boolean; className?: string }) { + const { platform } = useRouteContext({ strict: false }); + + return ( +
+ {children} +
+ ); +} diff --git a/src/components/index.ts b/src/components/index.ts index 7b848f41..7eb84fe9 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -1,4 +1,6 @@ export * from "./container"; +export * from "./frame"; +export * from "./back"; export * from "./box"; export * from "./spinner"; export * from "./quote"; diff --git a/src/routes.gen.ts b/src/routes.gen.ts index abb454e0..e6e84157 100644 --- a/src/routes.gen.ts +++ b/src/routes.gen.ts @@ -26,11 +26,11 @@ import { Route as CreateTopicImport } from './routes/create-topic' import { Route as CreateNewsfeedImport } from './routes/create-newsfeed' import { Route as CreateGroupImport } from './routes/create-group' import { Route as BootstrapRelaysImport } from './routes/bootstrap-relays' +import { Route as AccountImport } from './routes/$account' import { Route as IndexImport } from './routes/index' import { Route as EditorIndexImport } from './routes/editor/index' -import { Route as AccountIndexImport } from './routes/$account/index' import { Route as ZapIdImport } from './routes/zap.$id' -import { Route as UsersPubkeyImport } from './routes/users/$pubkey' +import { Route as UsersIdImport } from './routes/users.$id' import { Route as TrendingUsersImport } from './routes/trending.users' import { Route as TrendingNotesImport } from './routes/trending.notes' import { Route as SettingsWalletImport } from './routes/settings/wallet' @@ -44,29 +44,23 @@ import { Route as SearchNotesImport } from './routes/search.notes' import { Route as EventsIdImport } from './routes/events/$id' import { Route as CreateNewsfeedUsersImport } from './routes/create-newsfeed.users' import { Route as CreateNewsfeedF2fImport } from './routes/create-newsfeed.f2f' -import { Route as AuthCreateProfileImport } from './routes/auth/create-profile' -import { Route as AccountPanelImport } from './routes/$account/panel' import { Route as AccountHomeImport } from './routes/$account/home' -import { Route as AuthAccountBackupImport } from './routes/auth/$account.backup' +import { Route as AccountBackupImport } from './routes/$account/backup' // Create Virtual Routes -const LandingLazyImport = createFileRoute('/landing')() -const AuthLazyImport = createFileRoute('/auth')() -const AuthRemoteLazyImport = createFileRoute('/auth/remote')() +const NewLazyImport = createFileRoute('/new')() +const AuthNewLazyImport = createFileRoute('/auth/new')() const AuthImportLazyImport = createFileRoute('/auth/import')() +const AuthConnectLazyImport = createFileRoute('/auth/connect')() +const AccountPanelLazyImport = createFileRoute('/$account/panel')() // Create/Update Routes -const LandingLazyRoute = LandingLazyImport.update({ - path: '/landing', +const NewLazyRoute = NewLazyImport.update({ + path: '/new', getParentRoute: () => rootRoute, -} as any).lazy(() => import('./routes/landing.lazy').then((d) => d.Route)) - -const AuthLazyRoute = AuthLazyImport.update({ - path: '/auth', - getParentRoute: () => rootRoute, -} as any).lazy(() => import('./routes/auth.lazy').then((d) => d.Route)) +} as any).lazy(() => import('./routes/new.lazy').then((d) => d.Route)) const TrendingRoute = TrendingImport.update({ path: '/trending', @@ -133,40 +127,50 @@ const BootstrapRelaysRoute = BootstrapRelaysImport.update({ getParentRoute: () => rootRoute, } as any) +const AccountRoute = AccountImport.update({ + path: '/$account', + getParentRoute: () => rootRoute, +} as any).lazy(() => import('./routes/$account.lazy').then((d) => d.Route)) + const IndexRoute = IndexImport.update({ path: '/', getParentRoute: () => rootRoute, -} as any) +} as any).lazy(() => import('./routes/index.lazy').then((d) => d.Route)) const EditorIndexRoute = EditorIndexImport.update({ path: '/editor/', getParentRoute: () => rootRoute, } as any) -const AccountIndexRoute = AccountIndexImport.update({ - path: '/$account/', +const AuthNewLazyRoute = AuthNewLazyImport.update({ + path: '/auth/new', getParentRoute: () => rootRoute, -} as any).lazy(() => - import('./routes/$account/index.lazy').then((d) => d.Route), -) - -const AuthRemoteLazyRoute = AuthRemoteLazyImport.update({ - path: '/remote', - getParentRoute: () => AuthLazyRoute, -} as any).lazy(() => import('./routes/auth/remote.lazy').then((d) => d.Route)) +} as any).lazy(() => import('./routes/auth/new.lazy').then((d) => d.Route)) const AuthImportLazyRoute = AuthImportLazyImport.update({ - path: '/import', - getParentRoute: () => AuthLazyRoute, + path: '/auth/import', + getParentRoute: () => rootRoute, } as any).lazy(() => import('./routes/auth/import.lazy').then((d) => d.Route)) +const AuthConnectLazyRoute = AuthConnectLazyImport.update({ + path: '/auth/connect', + getParentRoute: () => rootRoute, +} as any).lazy(() => import('./routes/auth/connect.lazy').then((d) => d.Route)) + +const AccountPanelLazyRoute = AccountPanelLazyImport.update({ + path: '/panel', + getParentRoute: () => AccountRoute, +} as any).lazy(() => + import('./routes/$account/panel.lazy').then((d) => d.Route), +) + const ZapIdRoute = ZapIdImport.update({ path: '/zap/$id', getParentRoute: () => rootRoute, } as any) -const UsersPubkeyRoute = UsersPubkeyImport.update({ - path: '/users/$pubkey', +const UsersIdRoute = UsersIdImport.update({ + path: '/users/$id', getParentRoute: () => rootRoute, } as any) @@ -235,26 +239,14 @@ const CreateNewsfeedF2fRoute = CreateNewsfeedF2fImport.update({ getParentRoute: () => CreateNewsfeedRoute, } as any) -const AuthCreateProfileRoute = AuthCreateProfileImport.update({ - path: '/create-profile', - getParentRoute: () => AuthLazyRoute, -} as any) - -const AccountPanelRoute = AccountPanelImport.update({ - path: '/$account/panel', - getParentRoute: () => rootRoute, -} as any).lazy(() => - import('./routes/$account/panel.lazy').then((d) => d.Route), -) - const AccountHomeRoute = AccountHomeImport.update({ - path: '/$account/home', - getParentRoute: () => rootRoute, + path: '/home', + getParentRoute: () => AccountRoute, } as any).lazy(() => import('./routes/$account/home.lazy').then((d) => d.Route)) -const AuthAccountBackupRoute = AuthAccountBackupImport.update({ - path: '/$account/backup', - getParentRoute: () => AuthLazyRoute, +const AccountBackupRoute = AccountBackupImport.update({ + path: '/backup', + getParentRoute: () => AccountRoute, } as any) // Populate the FileRoutesByPath interface @@ -268,6 +260,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof IndexImport parentRoute: typeof rootRoute } + '/$account': { + id: '/$account' + path: '/$account' + fullPath: '/$account' + preLoaderRoute: typeof AccountImport + parentRoute: typeof rootRoute + } '/bootstrap-relays': { id: '/bootstrap-relays' path: '/bootstrap-relays' @@ -359,40 +358,26 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof TrendingImport parentRoute: typeof rootRoute } - '/auth': { - id: '/auth' - path: '/auth' - fullPath: '/auth' - preLoaderRoute: typeof AuthLazyImport + '/new': { + id: '/new' + path: '/new' + fullPath: '/new' + preLoaderRoute: typeof NewLazyImport parentRoute: typeof rootRoute } - '/landing': { - id: '/landing' - path: '/landing' - fullPath: '/landing' - preLoaderRoute: typeof LandingLazyImport - parentRoute: typeof rootRoute + '/$account/backup': { + id: '/$account/backup' + path: '/backup' + fullPath: '/$account/backup' + preLoaderRoute: typeof AccountBackupImport + parentRoute: typeof AccountImport } '/$account/home': { id: '/$account/home' - path: '/$account/home' + path: '/home' fullPath: '/$account/home' preLoaderRoute: typeof AccountHomeImport - parentRoute: typeof rootRoute - } - '/$account/panel': { - id: '/$account/panel' - path: '/$account/panel' - fullPath: '/$account/panel' - preLoaderRoute: typeof AccountPanelImport - parentRoute: typeof rootRoute - } - '/auth/create-profile': { - id: '/auth/create-profile' - path: '/create-profile' - fullPath: '/auth/create-profile' - preLoaderRoute: typeof AuthCreateProfileImport - parentRoute: typeof AuthLazyImport + parentRoute: typeof AccountImport } '/create-newsfeed/f2f': { id: '/create-newsfeed/f2f' @@ -485,11 +470,11 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof TrendingUsersImport parentRoute: typeof TrendingImport } - '/users/$pubkey': { - id: '/users/$pubkey' - path: '/users/$pubkey' - fullPath: '/users/$pubkey' - preLoaderRoute: typeof UsersPubkeyImport + '/users/$id': { + id: '/users/$id' + path: '/users/$id' + fullPath: '/users/$id' + preLoaderRoute: typeof UsersIdImport parentRoute: typeof rootRoute } '/zap/$id': { @@ -499,25 +484,32 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof ZapIdImport parentRoute: typeof rootRoute } + '/$account/panel': { + id: '/$account/panel' + path: '/panel' + fullPath: '/$account/panel' + preLoaderRoute: typeof AccountPanelLazyImport + parentRoute: typeof AccountImport + } + '/auth/connect': { + id: '/auth/connect' + path: '/auth/connect' + fullPath: '/auth/connect' + preLoaderRoute: typeof AuthConnectLazyImport + parentRoute: typeof rootRoute + } '/auth/import': { id: '/auth/import' - path: '/import' + path: '/auth/import' fullPath: '/auth/import' preLoaderRoute: typeof AuthImportLazyImport - parentRoute: typeof AuthLazyImport - } - '/auth/remote': { - id: '/auth/remote' - path: '/remote' - fullPath: '/auth/remote' - preLoaderRoute: typeof AuthRemoteLazyImport - parentRoute: typeof AuthLazyImport + parentRoute: typeof rootRoute } - '/$account/': { - id: '/$account/' - path: '/$account' - fullPath: '/$account' - preLoaderRoute: typeof AccountIndexImport + '/auth/new': { + id: '/auth/new' + path: '/auth/new' + fullPath: '/auth/new' + preLoaderRoute: typeof AuthNewLazyImport parentRoute: typeof rootRoute } '/editor/': { @@ -527,13 +519,6 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof EditorIndexImport parentRoute: typeof rootRoute } - '/auth/$account/backup': { - id: '/auth/$account/backup' - path: '/$account/backup' - fullPath: '/auth/$account/backup' - preLoaderRoute: typeof AuthAccountBackupImport - parentRoute: typeof AuthLazyImport - } } } @@ -541,6 +526,11 @@ declare module '@tanstack/react-router' { export const routeTree = rootRoute.addChildren({ IndexRoute, + AccountRoute: AccountRoute.addChildren({ + AccountBackupRoute, + AccountHomeRoute, + AccountPanelLazyRoute, + }), BootstrapRelaysRoute, CreateGroupRoute, CreateNewsfeedRoute: CreateNewsfeedRoute.addChildren({ @@ -567,19 +557,13 @@ export const routeTree = rootRoute.addChildren({ TrendingNotesRoute, TrendingUsersRoute, }), - AuthLazyRoute: AuthLazyRoute.addChildren({ - AuthCreateProfileRoute, - AuthImportLazyRoute, - AuthRemoteLazyRoute, - AuthAccountBackupRoute, - }), - LandingLazyRoute, - AccountHomeRoute, - AccountPanelRoute, + NewLazyRoute, EventsIdRoute, - UsersPubkeyRoute, + UsersIdRoute, ZapIdRoute, - AccountIndexRoute, + AuthConnectLazyRoute, + AuthImportLazyRoute, + AuthNewLazyRoute, EditorIndexRoute, }) @@ -592,6 +576,7 @@ export const routeTree = rootRoute.addChildren({ "filePath": "__root.tsx", "children": [ "/", + "/$account", "/bootstrap-relays", "/create-group", "/create-newsfeed", @@ -605,20 +590,27 @@ export const routeTree = rootRoute.addChildren({ "/store", "/topic", "/trending", - "/auth", - "/landing", - "/$account/home", - "/$account/panel", + "/new", "/events/$id", - "/users/$pubkey", + "/users/$id", "/zap/$id", - "/$account/", + "/auth/connect", + "/auth/import", + "/auth/new", "/editor/" ] }, "/": { "filePath": "index.tsx" }, + "/$account": { + "filePath": "$account.tsx", + "children": [ + "/$account/backup", + "/$account/home", + "/$account/panel" + ] + }, "/bootstrap-relays": { "filePath": "bootstrap-relays.tsx" }, @@ -678,27 +670,16 @@ export const routeTree = rootRoute.addChildren({ "/trending/users" ] }, - "/auth": { - "filePath": "auth.lazy.tsx", - "children": [ - "/auth/create-profile", - "/auth/import", - "/auth/remote", - "/auth/$account/backup" - ] + "/new": { + "filePath": "new.lazy.tsx" }, - "/landing": { - "filePath": "landing.lazy.tsx" + "/$account/backup": { + "filePath": "$account/backup.tsx", + "parent": "/$account" }, "/$account/home": { - "filePath": "$account/home.tsx" - }, - "/$account/panel": { - "filePath": "$account/panel.tsx" - }, - "/auth/create-profile": { - "filePath": "auth/create-profile.tsx", - "parent": "/auth" + "filePath": "$account/home.tsx", + "parent": "/$account" }, "/create-newsfeed/f2f": { "filePath": "create-newsfeed.f2f.tsx", @@ -751,29 +732,27 @@ export const routeTree = rootRoute.addChildren({ "filePath": "trending.users.tsx", "parent": "/trending" }, - "/users/$pubkey": { - "filePath": "users/$pubkey.tsx" + "/users/$id": { + "filePath": "users.$id.tsx" }, "/zap/$id": { "filePath": "zap.$id.tsx" }, - "/auth/import": { - "filePath": "auth/import.lazy.tsx", - "parent": "/auth" + "/$account/panel": { + "filePath": "$account/panel.lazy.tsx", + "parent": "/$account" + }, + "/auth/connect": { + "filePath": "auth/connect.lazy.tsx" }, - "/auth/remote": { - "filePath": "auth/remote.lazy.tsx", - "parent": "/auth" + "/auth/import": { + "filePath": "auth/import.lazy.tsx" }, - "/$account/": { - "filePath": "$account/index.tsx" + "/auth/new": { + "filePath": "auth/new.lazy.tsx" }, "/editor/": { "filePath": "editor/index.tsx" - }, - "/auth/$account/backup": { - "filePath": "auth/$account.backup.tsx", - "parent": "/auth" } } } diff --git a/src/routes/$account/index.lazy.tsx b/src/routes/$account.lazy.tsx similarity index 99% rename from src/routes/$account/index.lazy.tsx rename to src/routes/$account.lazy.tsx index 45222688..b72b0fa6 100644 --- a/src/routes/$account/index.lazy.tsx +++ b/src/routes/$account.lazy.tsx @@ -13,7 +13,7 @@ import { getCurrentWindow } from "@tauri-apps/api/window"; import { message } from "@tauri-apps/plugin-dialog"; import { memo, useCallback, useState } from "react"; -export const Route = createLazyFileRoute("/$account/")({ +export const Route = createLazyFileRoute("/$account")({ component: Screen, }); diff --git a/src/routes/$account/index.tsx b/src/routes/$account.tsx similarity index 88% rename from src/routes/$account/index.tsx rename to src/routes/$account.tsx index 603c2809..762425fd 100644 --- a/src/routes/$account/index.tsx +++ b/src/routes/$account.tsx @@ -1,7 +1,7 @@ import { NostrAccount, NostrQuery } from "@/system"; import { createFileRoute } from "@tanstack/react-router"; -export const Route = createFileRoute("/$account/")({ +export const Route = createFileRoute("/$account")({ beforeLoad: async ({ params }) => { const settings = await NostrQuery.getUserSettings(); const accounts = await NostrAccount.getAccounts(); diff --git a/src/routes/auth/$account.backup.tsx b/src/routes/$account/backup.tsx similarity index 98% rename from src/routes/auth/$account.backup.tsx rename to src/routes/$account/backup.tsx index de0aa6a7..0fef53c7 100644 --- a/src/routes/auth/$account.backup.tsx +++ b/src/routes/$account/backup.tsx @@ -8,7 +8,7 @@ import { writeText } from "@tauri-apps/plugin-clipboard-manager"; import { message } from "@tauri-apps/plugin-dialog"; import { useState } from "react"; -export const Route = createFileRoute("/auth/$account/backup")({ +export const Route = createFileRoute("/$account/backup")({ component: Screen, }); diff --git a/src/routes/auth.lazy.tsx b/src/routes/auth.lazy.tsx deleted file mode 100644 index 7e0ecdb6..00000000 --- a/src/routes/auth.lazy.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { Container } from "@/components"; -import { Outlet, createLazyFileRoute } from "@tanstack/react-router"; - -export const Route = createLazyFileRoute("/auth")({ - component: Screen, -}); - -function Screen() { - return ( - -
- -
-
- ); -} diff --git a/src/routes/auth/connect.lazy.tsx b/src/routes/auth/connect.lazy.tsx new file mode 100644 index 00000000..58d8465b --- /dev/null +++ b/src/routes/auth/connect.lazy.tsx @@ -0,0 +1,105 @@ +import { commands } from "@/commands.gen"; +import { Frame, GoBack, Spinner } from "@/components"; +import { createLazyFileRoute } from "@tanstack/react-router"; +import { readText } from "@tauri-apps/plugin-clipboard-manager"; +import { message } from "@tauri-apps/plugin-dialog"; +import { useState, useTransition } from "react"; + +export const Route = createLazyFileRoute("/auth/connect")({ + component: Screen, +}); + +function Screen() { + const navigate = Route.useNavigate(); + + const [uri, setUri] = useState(""); + const [isPending, startTransition] = useTransition(); + + const pasteFromClipboard = async () => { + const val = await readText(); + setUri(val); + }; + + const submit = () => { + startTransition(async () => { + if (!uri.startsWith("bunker://")) { + await message( + "You need to enter a valid Connect URI starts with bunker://", + { title: "Nostr Connect", kind: "info" }, + ); + return; + } + + const res = await commands.connectAccount(uri); + + if (res.status === "ok") { + navigate({ to: "/", replace: true }); + } else { + await message(res.error, { title: "Nostr Connect", kind: "error" }); + return; + } + }); + }; + + return ( +
+
+
+

Nostr Connect

+
+
+ + +
+ 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" + /> + +
+ +
+ + {isPending ? ( +

+ Waiting confirmation... +

+ ) : ( + + Go back to previous screen + + )} +
+
+
+
+ ); +} diff --git a/src/routes/auth/create-profile.tsx b/src/routes/auth/create-profile.tsx deleted file mode 100644 index 72b50a6e..00000000 --- a/src/routes/auth/create-profile.tsx +++ /dev/null @@ -1,139 +0,0 @@ -import { Spinner } from "@/components"; -import { PlusIcon } from "@/components"; -import { AvatarUploader } from "@/components/avatarUploader"; -import { NostrAccount } from "@/system"; -import type { Metadata } from "@/types"; -import { createFileRoute, useNavigate } from "@tanstack/react-router"; -import { message } from "@tauri-apps/plugin-dialog"; -import { useState } from "react"; -import { useForm } from "react-hook-form"; - -export const Route = createFileRoute("/auth/create-profile")({ - loader: async () => { - const account = await NostrAccount.createAccount(); - return account; - }, - component: Screen, -}); - -function Screen() { - const account = Route.useLoaderData(); - const navigate = useNavigate(); - const { register, handleSubmit } = useForm(); - - const [picture, setPicture] = useState(""); - const [loading, setLoading] = useState(false); - - const onSubmit = async (data: { - name: string; - about: string; - website: string; - }) => { - setLoading(true); - - try { - // Save account keys - const save = await NostrAccount.saveAccount(account.nsec); - - // Then create profile - if (save) { - const profile: Metadata = { ...data, picture }; - const eventId = await NostrAccount.createProfile(profile); - - if (eventId) { - navigate({ - to: "/auth/$account/backup", - params: { account: account.npub }, - replace: true, - }); - } - } - } catch (e) { - setLoading(false); - await message(String(e), { title: "Create Profile", kind: "error" }); - } - }; - - return ( -
-
-

Let's set up your profile.

-
-
-
-
- {picture ? ( - avatar - ) : null} - - - -
-
- - -
-
- - -
-
- -