diff --git a/src-tauri/src/commands/account.rs b/src-tauri/src/commands/account.rs index 05b4309..ff1f62e 100644 --- a/src-tauri/src/commands/account.rs +++ b/src-tauri/src/commands/account.rs @@ -1,7 +1,8 @@ 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, str::FromStr, time::Duration}; use tauri::{Emitter, Manager, State}; use tauri_plugin_notification::NotificationExt; @@ -14,18 +15,6 @@ pub struct EventPayload { sender: String, } -#[tauri::command] -#[specta::specta] -pub fn get_accounts() -> Vec { - let search = Search::new().expect("Unexpected."); - let results = search.by_service("Coop 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() -} - #[tauri::command] #[specta::specta] pub async fn get_metadata(user_id: String, state: State<'_, Nostr>) -> Result { @@ -48,13 +37,22 @@ pub async fn get_metadata(user_id: String, state: State<'_, Nostr>) -> Result, +} + #[tauri::command] #[specta::specta] -pub fn delete_account(id: String) -> Result<(), String> { - let keyring = Entry::new("Coop Secret Storage", &id).map_err(|e| e.to_string())?; - let _ = keyring.delete_credential(); +pub fn get_accounts() -> Vec { + let search = Search::new().expect("Unexpected."); + let results = search.by_service("Coop 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(); - Ok(()) + accounts.into_iter().collect() } #[tauri::command] @@ -75,8 +73,10 @@ pub async fn create_account( let enc_bech32 = enc.to_bech32().map_err(|err| err.to_string())?; // Save account - let keyring = Entry::new("Coop Secret Storage", &npub).unwrap(); - let _ = keyring.set_password(&enc_bech32); + let keyring = Entry::new("Coop 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); @@ -98,44 +98,49 @@ pub async fn create_account( #[tauri::command] #[specta::specta] -pub async fn import_key( - key: String, - password: Option, - state: State<'_, Nostr>, -) -> Result { - 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(); +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())?; + let enc_bech32 = enc.to_bech32().map_err(|err| err.to_string())?; + let secret_key = enc.to_secret_key(password).map_err(|err| err.to_string())?; + let keys = Keys::new(secret_key); + let npub = keys.public_key().to_bech32().unwrap(); - let enc_bech32 = match password { - Some(pw) => { - let enc = EncryptedSecretKey::new(&secret_key, pw, 16, KeySecurity::Medium) + (npub, enc_bech32) + } + false => { + 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(); + + let enc = EncryptedSecretKey::new(&secret_key, password, 16, KeySecurity::Medium) .map_err(|err| err.to_string())?; - enc.to_bech32().map_err(|err| err.to_string())? + let enc_bech32 = enc.to_bech32().map_err(|err| err.to_string())?; + + (npub, enc_bech32) } - None => secret_key.to_bech32().map_err(|err| err.to_string())?, }; - let keyring = Entry::new("Coop Secret Storage", &npub).unwrap(); - let _ = keyring.set_password(&enc_bech32); + let keyring = Entry::new("Coop Secret Storage", &npub).map_err(|e| e.to_string())?; - let signer = NostrSigner::Keys(keys); + let account = Account { password: enc_bech32, nostr_connect: None }; - // Update client's signer - client.set_signer(Some(signer)).await; + let pwd = serde_json::to_string(&account).map_err(|e| e.to_string())?; + keyring.set_password(&pwd).map_err(|e| e.to_string())?; Ok(npub) } #[tauri::command] #[specta::specta] -pub async fn connect_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().to_string(); @@ -145,8 +150,20 @@ pub async fn connect_account(uri: &str, state: State<'_, Nostr>) -> Result { - let keyring = Entry::new("Coop Secret Storage", &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("Coop 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; @@ -160,6 +177,34 @@ pub async fn connect_account(uri: &str, state: State<'_, Nostr>) -> Result Result<(), String> { + 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(); + + 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())?; + + let keyring = Entry::new("Coop 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(()) +} + +#[tauri::command] +#[specta::specta] +pub fn delete_account(id: String) -> Result<(), String> { + let keyring = Entry::new("Coop Secret Storage", &id).map_err(|e| e.to_string())?; + let _ = keyring.delete_credential(); + + Ok(()) +} + #[tauri::command] #[specta::specta] pub async fn get_contact_list(state: State<'_, Nostr>) -> Result, String> { @@ -185,19 +230,43 @@ pub async fn login( let client = &state.client; let keyring = Entry::new("Coop Secret Storage", &account).map_err(|e| e.to_string())?; - let bech32 = match keyring.get_password() { - Ok(pw) => pw, - Err(_) => return Err("Action have been 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()), }; - let ncryptsec = EncryptedSecretKey::from_bech32(bech32).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(); - let signer = NostrSigner::Keys(keys); + let public_key = match account.nostr_connect { + None => { + 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(); + let signer = NostrSigner::Keys(keys); - // Update signer - client.set_signer(Some(signer)).await; + // 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(); + 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()), + } + } + }; let inbox = Filter::new().kind(Kind::Custom(10050)).author(public_key).limit(1); diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index bc17283..48cef7e 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -40,10 +40,11 @@ fn main() { connect_inbox_relays, disconnect_inbox_relays, login, - delete_account, create_account, - import_key, + import_account, connect_account, + delete_account, + reset_password, get_accounts, get_metadata, get_contact_list, diff --git a/src/commands.ts b/src/commands.ts index 4c2d738..f9052e5 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -61,14 +61,6 @@ async login(account: string, password: string) : Promise> else return { status: "error", error: e as any }; } }, -async deleteAccount(id: string) : Promise> { - try { - 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 createAccount(name: string, about: string, picture: string, password: string) : Promise> { try { return { status: "ok", data: await TAURI_INVOKE("create_account", { name, about, picture, password }) }; @@ -77,9 +69,9 @@ async createAccount(name: string, about: string, picture: string, password: stri else return { status: "error", error: e as any }; } }, -async importKey(key: string, password: string | null) : Promise> { +async importAccount(key: string, password: string) : Promise> { try { - return { status: "ok", data: await TAURI_INVOKE("import_key", { key, 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 }; @@ -93,6 +85,22 @@ async connectAccount(uri: string) : Promise> { else return { status: "error", error: e as any }; } }, +async deleteAccount(id: string) : Promise> { + try { + 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 resetPassword(key: string, password: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("reset_password", { key, password }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, async getAccounts() : Promise { return await TAURI_INVOKE("get_accounts"); }, diff --git a/src/components/index.ts b/src/components/index.ts new file mode 100644 index 0000000..35d5927 --- /dev/null +++ b/src/components/index.ts @@ -0,0 +1,5 @@ +export * from "./frame"; +export * from "./back"; +export * from "./spinner"; + +export * from "./user"; diff --git a/src/routes.gen.ts b/src/routes.gen.ts index 984950f..6f8c0de 100644 --- a/src/routes.gen.ts +++ b/src/routes.gen.ts @@ -15,43 +15,32 @@ import { createFileRoute } from '@tanstack/react-router' import { Route as rootRoute } from './routes/__root' import { Route as BootstrapRelaysImport } from './routes/bootstrap-relays' import { Route as IndexImport } from './routes/index' +import { Route as AuthNewImport } from './routes/auth/new' +import { Route as AuthImportImport } from './routes/auth/import' +import { Route as AuthConnectImport } from './routes/auth/connect' import { Route as AccountRelaysImport } from './routes/$account.relays' import { Route as AccountContactsImport } from './routes/$account.contacts' import { Route as AccountChatsIdImport } from './routes/$account.chats.$id' // Create Virtual Routes -const NostrConnectLazyImport = createFileRoute('/nostr-connect')() +const ResetLazyImport = createFileRoute('/reset')() const NewLazyImport = createFileRoute('/new')() -const ImportKeyLazyImport = createFileRoute('/import-key')() -const CreateAccountLazyImport = createFileRoute('/create-account')() const AccountChatsLazyImport = createFileRoute('/$account/chats')() const AccountChatsNewLazyImport = createFileRoute('/$account/chats/new')() // Create/Update Routes -const NostrConnectLazyRoute = NostrConnectLazyImport.update({ - path: '/nostr-connect', +const ResetLazyRoute = ResetLazyImport.update({ + path: '/reset', getParentRoute: () => rootRoute, -} as any).lazy(() => import('./routes/nostr-connect.lazy').then((d) => d.Route)) +} as any).lazy(() => import('./routes/reset.lazy').then((d) => d.Route)) const NewLazyRoute = NewLazyImport.update({ path: '/new', getParentRoute: () => rootRoute, } as any).lazy(() => import('./routes/new.lazy').then((d) => d.Route)) -const ImportKeyLazyRoute = ImportKeyLazyImport.update({ - path: '/import-key', - getParentRoute: () => rootRoute, -} as any).lazy(() => import('./routes/import-key.lazy').then((d) => d.Route)) - -const CreateAccountLazyRoute = CreateAccountLazyImport.update({ - path: '/create-account', - getParentRoute: () => rootRoute, -} as any).lazy(() => - import('./routes/create-account.lazy').then((d) => d.Route), -) - const BootstrapRelaysRoute = BootstrapRelaysImport.update({ path: '/bootstrap-relays', getParentRoute: () => rootRoute, @@ -71,6 +60,21 @@ const AccountChatsLazyRoute = AccountChatsLazyImport.update({ import('./routes/$account.chats.lazy').then((d) => d.Route), ) +const AuthNewRoute = AuthNewImport.update({ + path: '/auth/new', + getParentRoute: () => rootRoute, +} as any) + +const AuthImportRoute = AuthImportImport.update({ + path: '/auth/import', + getParentRoute: () => rootRoute, +} as any) + +const AuthConnectRoute = AuthConnectImport.update({ + path: '/auth/connect', + getParentRoute: () => rootRoute, +} as any) + const AccountRelaysRoute = AccountRelaysImport.update({ path: '/$account/relays', getParentRoute: () => rootRoute, @@ -117,20 +121,6 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof BootstrapRelaysImport parentRoute: typeof rootRoute } - '/create-account': { - id: '/create-account' - path: '/create-account' - fullPath: '/create-account' - preLoaderRoute: typeof CreateAccountLazyImport - parentRoute: typeof rootRoute - } - '/import-key': { - id: '/import-key' - path: '/import-key' - fullPath: '/import-key' - preLoaderRoute: typeof ImportKeyLazyImport - parentRoute: typeof rootRoute - } '/new': { id: '/new' path: '/new' @@ -138,11 +128,11 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof NewLazyImport parentRoute: typeof rootRoute } - '/nostr-connect': { - id: '/nostr-connect' - path: '/nostr-connect' - fullPath: '/nostr-connect' - preLoaderRoute: typeof NostrConnectLazyImport + '/reset': { + id: '/reset' + path: '/reset' + fullPath: '/reset' + preLoaderRoute: typeof ResetLazyImport parentRoute: typeof rootRoute } '/$account/contacts': { @@ -159,6 +149,27 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof AccountRelaysImport parentRoute: typeof rootRoute } + '/auth/connect': { + id: '/auth/connect' + path: '/auth/connect' + fullPath: '/auth/connect' + preLoaderRoute: typeof AuthConnectImport + parentRoute: typeof rootRoute + } + '/auth/import': { + id: '/auth/import' + path: '/auth/import' + fullPath: '/auth/import' + preLoaderRoute: typeof AuthImportImport + parentRoute: typeof rootRoute + } + '/auth/new': { + id: '/auth/new' + path: '/auth/new' + fullPath: '/auth/new' + preLoaderRoute: typeof AuthNewImport + parentRoute: typeof rootRoute + } '/$account/chats': { id: '/$account/chats' path: '/$account/chats' @@ -188,12 +199,13 @@ declare module '@tanstack/react-router' { export const routeTree = rootRoute.addChildren({ IndexRoute, BootstrapRelaysRoute, - CreateAccountLazyRoute, - ImportKeyLazyRoute, NewLazyRoute, - NostrConnectLazyRoute, + ResetLazyRoute, AccountContactsRoute, AccountRelaysRoute, + AuthConnectRoute, + AuthImportRoute, + AuthNewRoute, AccountChatsLazyRoute: AccountChatsLazyRoute.addChildren({ AccountChatsIdRoute, AccountChatsNewLazyRoute, @@ -210,12 +222,13 @@ export const routeTree = rootRoute.addChildren({ "children": [ "/", "/bootstrap-relays", - "/create-account", - "/import-key", "/new", - "/nostr-connect", + "/reset", "/$account/contacts", "/$account/relays", + "/auth/connect", + "/auth/import", + "/auth/new", "/$account/chats" ] }, @@ -225,17 +238,11 @@ export const routeTree = rootRoute.addChildren({ "/bootstrap-relays": { "filePath": "bootstrap-relays.tsx" }, - "/create-account": { - "filePath": "create-account.lazy.tsx" - }, - "/import-key": { - "filePath": "import-key.lazy.tsx" - }, "/new": { "filePath": "new.lazy.tsx" }, - "/nostr-connect": { - "filePath": "nostr-connect.lazy.tsx" + "/reset": { + "filePath": "reset.lazy.tsx" }, "/$account/contacts": { "filePath": "$account.contacts.tsx" @@ -243,6 +250,15 @@ export const routeTree = rootRoute.addChildren({ "/$account/relays": { "filePath": "$account.relays.tsx" }, + "/auth/connect": { + "filePath": "auth/connect.tsx" + }, + "/auth/import": { + "filePath": "auth/import.tsx" + }, + "/auth/new": { + "filePath": "auth/new.tsx" + }, "/$account/chats": { "filePath": "$account.chats.lazy.tsx", "children": [ diff --git a/src/routes/nostr-connect.lazy.tsx b/src/routes/auth/connect.tsx similarity index 92% rename from src/routes/nostr-connect.lazy.tsx rename to src/routes/auth/connect.tsx index 215e22a..0aab178 100644 --- a/src/routes/nostr-connect.lazy.tsx +++ b/src/routes/auth/connect.tsx @@ -2,12 +2,12 @@ import { commands } from "@/commands"; import { GoBack } from "@/components/back"; import { Frame } from "@/components/frame"; import { Spinner } from "@/components/spinner"; -import { createLazyFileRoute } from "@tanstack/react-router"; +import { createFileRoute } 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("/nostr-connect")({ +export const Route = createFileRoute("/auth/connect")({ component: Screen, }); @@ -70,12 +70,12 @@ 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" + 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" /> diff --git a/src/routes/import-key.lazy.tsx b/src/routes/auth/import.tsx similarity index 87% rename from src/routes/import-key.lazy.tsx rename to src/routes/auth/import.tsx index 26d0a8d..7cec25f 100644 --- a/src/routes/import-key.lazy.tsx +++ b/src/routes/auth/import.tsx @@ -2,12 +2,12 @@ import { commands } from "@/commands"; import { GoBack } from "@/components/back"; import { Frame } from "@/components/frame"; import { Spinner } from "@/components/spinner"; -import { createLazyFileRoute } from "@tanstack/react-router"; +import { createFileRoute } 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("/import-key")({ +export const Route = createFileRoute("/auth/import")({ component: Screen, }); @@ -41,7 +41,7 @@ function Screen() { return; } - const res = await commands.importKey(key, password); + const res = await commands.importAccount(key, password); if (res.status === "ok") { navigate({ to: "/", replace: true }); @@ -63,7 +63,7 @@ function Screen() {

- Import Private Key + Import Account

@@ -85,24 +85,26 @@ function Screen() { placeholder="nsec or ncryptsec..." value={key} onChange={(e) => setKey(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 dark:placeholder:text-neutral-600" + 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" />
- {key.length && !key.startsWith("ncryptsec") ? ( + {key.length ? (
-
+
{picture.length ? ( 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:border-blue-500 focus:outline-none placeholder:text-neutral-400 dark:text-neutral-600" + 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" />
@@ -129,7 +128,7 @@ function Screen() { onChange={(e) => setAbout(e.target.value)} placeholder="e.g. Artist, anime-lover, and k-pop fan" spellCheck={false} - className="px-3 py-1.5 rounded-lg min-h-16 bg-transparent border border-neutral-200 dark:border-neutral-500 focus:border-blue-500 focus:outline-none placeholder:text-neutral-400 dark:text-neutral-600" + className="px-3 py-1.5 rounded-lg min-h-16 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" />
@@ -145,7 +144,7 @@ function Screen() { type="password" value={password} onChange={(e) => setPassword(e.target.value)} - className="px-3 rounded-lg h-10 bg-transparent border border-neutral-200 dark:border-neutral-500 focus:border-blue-500 focus:outline-none placeholder:text-neutral-400 dark:text-neutral-600" + 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" />
diff --git a/src/routes/bootstrap-relays.lazy.tsx b/src/routes/bootstrap-relays.lazy.tsx index d104007..25e7151 100644 --- a/src/routes/bootstrap-relays.lazy.tsx +++ b/src/routes/bootstrap-relays.lazy.tsx @@ -1,9 +1,11 @@ import { commands } from "@/commands"; +import { GoBack } from "@/components"; import { Frame } from "@/components/frame"; import { Spinner } from "@/components/spinner"; -import { Plus, X } from "@phosphor-icons/react"; +import { ArrowLeft, Plus, X } from "@phosphor-icons/react"; import { createLazyFileRoute } from "@tanstack/react-router"; import { message } from "@tauri-apps/plugin-dialog"; +import { relaunch } from "@tauri-apps/plugin-process"; import { useEffect, useState, useTransition } from "react"; export const Route = createLazyFileRoute("/bootstrap-relays")({ @@ -50,13 +52,11 @@ function Screen() { return; } - const merged = relays - .map((relay) => Object.values(relay).join(",")) - .join("\n"); + const merged = relays.join("\r\n"); const res = await commands.setBootstrapRelays(merged); if (res.status === "ok") { - // TODO: restart app + return await relaunch(); } else { await message(res.error, { title: "Manage Relays", @@ -72,7 +72,10 @@ function Screen() { }, [bootstrapRelays]); return ( -
+

Manage Relays

@@ -134,9 +137,16 @@ function Screen() { > {isPending ? : "Save & Restart"} + + Lume will relaunch after saving. +
+ + + Back +
); } diff --git a/src/routes/index.lazy.tsx b/src/routes/index.lazy.tsx index aab90b8..cb11308 100644 --- a/src/routes/index.lazy.tsx +++ b/src/routes/index.lazy.tsx @@ -1,8 +1,6 @@ import { commands } from "@/commands"; import { npub } from "@/commons"; -import { Frame } from "@/components/frame"; -import { Spinner } from "@/components/spinner"; -import { User } from "@/components/user"; +import { Frame, User, Spinner } from "@/components"; import { ArrowRight, DotsThree, GearSix, Plus } from "@phosphor-icons/react"; import { Link, createLazyFileRoute } from "@tanstack/react-router"; import { Menu, MenuItem } from "@tauri-apps/api/menu"; @@ -35,6 +33,7 @@ function Screen() { const [accounts, setAccounts] = useState([]); const [value, setValue] = useState(""); + const [autoLogin, setAutoLogin] = useState(false); const [password, setPassword] = useState(""); const [isPending, startTransition] = useTransition(); @@ -48,30 +47,25 @@ function Screen() { const selectAccount = (account: string) => { setValue(account); + + if (account.includes("_nostrconnect")) { + setAutoLogin(true); + } }; const loginWith = () => { startTransition(async () => { - if (!value || !password) return; - const res = await commands.login(value, password); if (res.status === "ok") { navigate({ - to: "/$account/chats/new", + to: "/$account/chats", params: { account: res.data }, replace: true, }); } else { - if (res.error === "404") { - navigate({ - to: "/$account/relays", - params: { account: value }, - replace: true, - }); - } else { - await message(res.error, { title: "Login", kind: "error" }); - } + await message(res.error, { title: "Login", kind: "error" }); + return; } }); }; @@ -81,6 +75,11 @@ function Screen() { e.stopPropagation(); const menuItems = await Promise.all([ + MenuItem.new({ + text: "Reset password", + enabled: !account.includes("_nostrconnect"), + action: () => navigate({ to: "/reset", search: { account } }), + }), MenuItem.new({ text: "Delete account", action: async () => await deleteAccount(account), @@ -96,6 +95,12 @@ function Screen() { [], ); + useEffect(() => { + if (autoLogin) { + loginWith(); + } + }, [autoLogin, value]); + useEffect(() => { setAccounts(context.accounts); }, [context.accounts]); @@ -123,10 +128,10 @@ function Screen() { onKeyDown={() => selectAccount(account)} className="group flex items-center gap-2 hover:bg-black/5 dark:hover:bg-white/5 p-3" > - + - {value === account ? ( + {value === account && !value.includes("_nostrconnect") ? (
) : (
- +
+ + {account.includes("_nostrconnect") ? ( +
+ Nostr Connect +
+ ) : null} +
- {npub(account, 16)} + {npub(account.replace("_nostrconnect", ""), 16)}
)} diff --git a/src/routes/index.tsx b/src/routes/index.tsx index 9493b16..f6c7564 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -1,16 +1,12 @@ import { commands } from "@/commands"; -import { checkForAppUpdates, checkPermission } from "@/commons"; +import { checkForAppUpdates } from "@/commons"; import { createFileRoute, redirect } from "@tanstack/react-router"; export const Route = createFileRoute("/")({ beforeLoad: async () => { // Check for app updates - // TODO: move this function to rust await checkForAppUpdates(true); - // Request notification permission - await checkPermission(); - // Get all accounts from system const accounts = await commands.getAccounts(); @@ -21,6 +17,9 @@ export const Route = createFileRoute("/")({ }); } - return { accounts }; + // Workaround for keyring bug on Windows + const fil = accounts.filter((item) => !item.includes("Coop")); + + return { accounts: fil }; }, }); diff --git a/src/routes/new.lazy.tsx b/src/routes/new.lazy.tsx index a018878..2ea413a 100644 --- a/src/routes/new.lazy.tsx +++ b/src/routes/new.lazy.tsx @@ -18,7 +18,7 @@ function Screen() {
Create a new identity @@ -32,7 +32,7 @@ function Screen() { Login with Nostr Connect */} Login with Private Key diff --git a/src/routes/reset.lazy.tsx b/src/routes/reset.lazy.tsx new file mode 100644 index 0000000..b764bff --- /dev/null +++ b/src/routes/reset.lazy.tsx @@ -0,0 +1,130 @@ +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"; +import { Frame, GoBack, Spinner } from "@/components"; +import { commands } from "@/commands"; + +export const Route = createLazyFileRoute("/reset")({ + component: Screen, +}); + +function Screen() { + const navigate = Route.useNavigate(); + + const [key, setKey] = useState(""); + const [password, setPassword] = useState(""); + const [isPending, startTransition] = useTransition(); + + const pasteFromClipboard = async () => { + const val = await readText(); + setKey(val); + }; + + const submit = () => { + startTransition(async () => { + if (!key.startsWith("nsec1")) { + await message( + "You need to enter a valid private key starts with nsec", + { title: "Reset Password", kind: "info" }, + ); + return; + } + + if (!password.length) { + await message("You must set password to secure your key", { + title: "Reset Password", + kind: "info", + }); + return; + } + + const res = await commands.resetPassword(key, password); + + if (res.status === "ok") { + navigate({ to: "/", replace: true }); + } else { + await message(res.error, { + title: "Reset Password", + kind: "error", + }); + return; + } + }); + }; + + return ( +
+
+
+

+ Reset password +

+
+
+ +
+ +
+ setKey(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 dark:placeholder:text-neutral-600" + /> + +
+
+
+ + setPassword(e.target.value)} + className="px-3 rounded-lg h-10 bg-transparent border border-neutral-200 dark:border-neutral-500 focus:border-blue-500 focus:outline-none" + /> +
+ +
+ + + Go back to previous screen + +
+
+
+
+ ); +}