feat: add some improvements from lume
This commit is contained in:
@@ -1,7 +1,8 @@
|
|||||||
use keyring::Entry;
|
use keyring::Entry;
|
||||||
use keyring_search::{Limit, List, Search};
|
use keyring_search::{Limit, List, Search};
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use serde::Serialize;
|
use serde::{Deserialize, Serialize};
|
||||||
|
use specta::Type;
|
||||||
use std::{collections::HashSet, str::FromStr, time::Duration};
|
use std::{collections::HashSet, str::FromStr, time::Duration};
|
||||||
use tauri::{Emitter, Manager, State};
|
use tauri::{Emitter, Manager, State};
|
||||||
use tauri_plugin_notification::NotificationExt;
|
use tauri_plugin_notification::NotificationExt;
|
||||||
@@ -14,18 +15,6 @@ pub struct EventPayload {
|
|||||||
sender: String,
|
sender: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
#[specta::specta]
|
|
||||||
pub fn get_accounts() -> Vec<String> {
|
|
||||||
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<String> =
|
|
||||||
list.split_whitespace().filter(|v| v.starts_with("npub1")).map(String::from).collect();
|
|
||||||
|
|
||||||
accounts.into_iter().collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
#[specta::specta]
|
#[specta::specta]
|
||||||
pub async fn get_metadata(user_id: String, state: State<'_, Nostr>) -> Result<String, String> {
|
pub async fn get_metadata(user_id: String, state: State<'_, Nostr>) -> Result<String, String> {
|
||||||
@@ -48,13 +37,22 @@ pub async fn get_metadata(user_id: String, state: State<'_, Nostr>) -> Result<St
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
|
||||||
|
struct Account {
|
||||||
|
password: String,
|
||||||
|
nostr_connect: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
#[specta::specta]
|
#[specta::specta]
|
||||||
pub fn delete_account(id: String) -> Result<(), String> {
|
pub fn get_accounts() -> Vec<String> {
|
||||||
let keyring = Entry::new("Coop Secret Storage", &id).map_err(|e| e.to_string())?;
|
let search = Search::new().expect("Unexpected.");
|
||||||
let _ = keyring.delete_credential();
|
let results = search.by_service("Coop Secret Storage");
|
||||||
|
let list = List::list_credentials(&results, Limit::All);
|
||||||
|
let accounts: HashSet<String> =
|
||||||
|
list.split_whitespace().filter(|v| v.starts_with("npub1")).map(String::from).collect();
|
||||||
|
|
||||||
Ok(())
|
accounts.into_iter().collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
@@ -75,8 +73,10 @@ pub async fn create_account(
|
|||||||
let enc_bech32 = enc.to_bech32().map_err(|err| err.to_string())?;
|
let enc_bech32 = enc.to_bech32().map_err(|err| err.to_string())?;
|
||||||
|
|
||||||
// Save account
|
// Save account
|
||||||
let keyring = Entry::new("Coop Secret Storage", &npub).unwrap();
|
let keyring = Entry::new("Coop Secret Storage", &npub).map_err(|e| e.to_string())?;
|
||||||
let _ = keyring.set_password(&enc_bech32);
|
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);
|
let signer = NostrSigner::Keys(keys);
|
||||||
|
|
||||||
@@ -98,44 +98,49 @@ pub async fn create_account(
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
#[specta::specta]
|
#[specta::specta]
|
||||||
pub async fn import_key(
|
pub async fn import_account(key: String, password: String) -> Result<String, String> {
|
||||||
key: String,
|
let (npub, enc_bech32) = match key.starts_with("ncryptsec") {
|
||||||
password: Option<String>,
|
true => {
|
||||||
state: State<'_, Nostr>,
|
let enc = EncryptedSecretKey::from_bech32(key).map_err(|err| err.to_string())?;
|
||||||
) -> Result<String, String> {
|
let enc_bech32 = enc.to_bech32().map_err(|err| err.to_string())?;
|
||||||
let client = &state.client;
|
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();
|
||||||
|
|
||||||
|
(npub, enc_bech32)
|
||||||
|
}
|
||||||
|
false => {
|
||||||
let secret_key = SecretKey::from_bech32(key).map_err(|err| err.to_string())?;
|
let secret_key = SecretKey::from_bech32(key).map_err(|err| err.to_string())?;
|
||||||
let keys = Keys::new(secret_key.clone());
|
let keys = Keys::new(secret_key.clone());
|
||||||
let npub = keys.public_key().to_bech32().unwrap();
|
let npub = keys.public_key().to_bech32().unwrap();
|
||||||
|
|
||||||
let enc_bech32 = match password {
|
let enc = EncryptedSecretKey::new(&secret_key, password, 16, KeySecurity::Medium)
|
||||||
Some(pw) => {
|
|
||||||
let enc = EncryptedSecretKey::new(&secret_key, pw, 16, KeySecurity::Medium)
|
|
||||||
.map_err(|err| err.to_string())?;
|
.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 = Entry::new("Coop Secret Storage", &npub).map_err(|e| e.to_string())?;
|
||||||
let _ = keyring.set_password(&enc_bech32);
|
|
||||||
|
|
||||||
let signer = NostrSigner::Keys(keys);
|
let account = Account { password: enc_bech32, nostr_connect: None };
|
||||||
|
|
||||||
// Update client's signer
|
let pwd = serde_json::to_string(&account).map_err(|e| e.to_string())?;
|
||||||
client.set_signer(Some(signer)).await;
|
keyring.set_password(&pwd).map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
Ok(npub)
|
Ok(npub)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
#[specta::specta]
|
#[specta::specta]
|
||||||
pub async fn connect_account(uri: &str, state: State<'_, Nostr>) -> Result<String, String> {
|
pub async fn connect_account(uri: String, state: State<'_, Nostr>) -> Result<String, String> {
|
||||||
let client = &state.client;
|
let client = &state.client;
|
||||||
|
|
||||||
match NostrConnectURI::parse(uri) {
|
match NostrConnectURI::parse(uri.clone()) {
|
||||||
Ok(bunker_uri) => {
|
Ok(bunker_uri) => {
|
||||||
|
// Local user
|
||||||
let app_keys = Keys::generate();
|
let app_keys = Keys::generate();
|
||||||
let app_secret = app_keys.secret_key().to_string();
|
let app_secret = app_keys.secret_key().to_string();
|
||||||
|
|
||||||
@@ -145,8 +150,20 @@ pub async fn connect_account(uri: &str, state: State<'_, Nostr>) -> Result<Strin
|
|||||||
|
|
||||||
match Nip46Signer::new(bunker_uri, app_keys, Duration::from_secs(120), None).await {
|
match Nip46Signer::new(bunker_uri, app_keys, Duration::from_secs(120), None).await {
|
||||||
Ok(signer) => {
|
Ok(signer) => {
|
||||||
let keyring = Entry::new("Coop Secret Storage", &remote_npub).unwrap();
|
let mut url = Url::parse(&uri).unwrap();
|
||||||
let _ = keyring.set_password(&app_secret);
|
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
|
// Update signer
|
||||||
let _ = client.set_signer(Some(signer.into())).await;
|
let _ = client.set_signer(Some(signer.into())).await;
|
||||||
@@ -160,6 +177,34 @@ pub async fn connect_account(uri: &str, state: State<'_, Nostr>) -> Result<Strin
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
#[specta::specta]
|
||||||
|
pub async fn reset_password(key: String, password: String) -> 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]
|
#[tauri::command]
|
||||||
#[specta::specta]
|
#[specta::specta]
|
||||||
pub async fn get_contact_list(state: State<'_, Nostr>) -> Result<Vec<String>, String> {
|
pub async fn get_contact_list(state: State<'_, Nostr>) -> Result<Vec<String>, String> {
|
||||||
@@ -185,12 +230,18 @@ pub async fn login(
|
|||||||
let client = &state.client;
|
let client = &state.client;
|
||||||
let keyring = Entry::new("Coop Secret Storage", &account).map_err(|e| e.to_string())?;
|
let keyring = Entry::new("Coop Secret Storage", &account).map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
let bech32 = match keyring.get_password() {
|
let account = match keyring.get_password() {
|
||||||
Ok(pw) => pw,
|
Ok(pw) => {
|
||||||
Err(_) => return Err("Action have been cancelled".into()),
|
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 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 secret_key = ncryptsec.to_secret_key(password).map_err(|_| "Wrong password.")?;
|
||||||
let keys = Keys::new(secret_key);
|
let keys = Keys::new(secret_key);
|
||||||
let public_key = keys.public_key();
|
let public_key = keys.public_key();
|
||||||
@@ -199,6 +250,24 @@ pub async fn login(
|
|||||||
// Update signer
|
// Update signer
|
||||||
client.set_signer(Some(signer)).await;
|
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);
|
let inbox = Filter::new().kind(Kind::Custom(10050)).author(public_key).limit(1);
|
||||||
|
|
||||||
if let Ok(events) =
|
if let Ok(events) =
|
||||||
|
|||||||
@@ -40,10 +40,11 @@ fn main() {
|
|||||||
connect_inbox_relays,
|
connect_inbox_relays,
|
||||||
disconnect_inbox_relays,
|
disconnect_inbox_relays,
|
||||||
login,
|
login,
|
||||||
delete_account,
|
|
||||||
create_account,
|
create_account,
|
||||||
import_key,
|
import_account,
|
||||||
connect_account,
|
connect_account,
|
||||||
|
delete_account,
|
||||||
|
reset_password,
|
||||||
get_accounts,
|
get_accounts,
|
||||||
get_metadata,
|
get_metadata,
|
||||||
get_contact_list,
|
get_contact_list,
|
||||||
|
|||||||
@@ -61,14 +61,6 @@ async login(account: string, password: string) : Promise<Result<string, string>>
|
|||||||
else return { status: "error", error: e as any };
|
else return { status: "error", error: e as any };
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async deleteAccount(id: string) : Promise<Result<null, string>> {
|
|
||||||
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<Result<string, string>> {
|
async createAccount(name: string, about: string, picture: string, password: string) : Promise<Result<string, string>> {
|
||||||
try {
|
try {
|
||||||
return { status: "ok", data: await TAURI_INVOKE("create_account", { name, about, picture, password }) };
|
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 };
|
else return { status: "error", error: e as any };
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async importKey(key: string, password: string | null) : Promise<Result<string, string>> {
|
async importAccount(key: string, password: string) : Promise<Result<string, string>> {
|
||||||
try {
|
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) {
|
} catch (e) {
|
||||||
if(e instanceof Error) throw e;
|
if(e instanceof Error) throw e;
|
||||||
else return { status: "error", error: e as any };
|
else return { status: "error", error: e as any };
|
||||||
@@ -93,6 +85,22 @@ async connectAccount(uri: string) : Promise<Result<string, string>> {
|
|||||||
else return { status: "error", error: e as any };
|
else return { status: "error", error: e as any };
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
async deleteAccount(id: string) : Promise<Result<null, string>> {
|
||||||
|
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<Result<null, string>> {
|
||||||
|
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<string[]> {
|
async getAccounts() : Promise<string[]> {
|
||||||
return await TAURI_INVOKE("get_accounts");
|
return await TAURI_INVOKE("get_accounts");
|
||||||
},
|
},
|
||||||
|
|||||||
5
src/components/index.ts
Normal file
5
src/components/index.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export * from "./frame";
|
||||||
|
export * from "./back";
|
||||||
|
export * from "./spinner";
|
||||||
|
|
||||||
|
export * from "./user";
|
||||||
@@ -15,43 +15,32 @@ import { createFileRoute } from '@tanstack/react-router'
|
|||||||
import { Route as rootRoute } from './routes/__root'
|
import { Route as rootRoute } from './routes/__root'
|
||||||
import { Route as BootstrapRelaysImport } from './routes/bootstrap-relays'
|
import { Route as BootstrapRelaysImport } from './routes/bootstrap-relays'
|
||||||
import { Route as IndexImport } from './routes/index'
|
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 AccountRelaysImport } from './routes/$account.relays'
|
||||||
import { Route as AccountContactsImport } from './routes/$account.contacts'
|
import { Route as AccountContactsImport } from './routes/$account.contacts'
|
||||||
import { Route as AccountChatsIdImport } from './routes/$account.chats.$id'
|
import { Route as AccountChatsIdImport } from './routes/$account.chats.$id'
|
||||||
|
|
||||||
// Create Virtual Routes
|
// Create Virtual Routes
|
||||||
|
|
||||||
const NostrConnectLazyImport = createFileRoute('/nostr-connect')()
|
const ResetLazyImport = createFileRoute('/reset')()
|
||||||
const NewLazyImport = createFileRoute('/new')()
|
const NewLazyImport = createFileRoute('/new')()
|
||||||
const ImportKeyLazyImport = createFileRoute('/import-key')()
|
|
||||||
const CreateAccountLazyImport = createFileRoute('/create-account')()
|
|
||||||
const AccountChatsLazyImport = createFileRoute('/$account/chats')()
|
const AccountChatsLazyImport = createFileRoute('/$account/chats')()
|
||||||
const AccountChatsNewLazyImport = createFileRoute('/$account/chats/new')()
|
const AccountChatsNewLazyImport = createFileRoute('/$account/chats/new')()
|
||||||
|
|
||||||
// Create/Update Routes
|
// Create/Update Routes
|
||||||
|
|
||||||
const NostrConnectLazyRoute = NostrConnectLazyImport.update({
|
const ResetLazyRoute = ResetLazyImport.update({
|
||||||
path: '/nostr-connect',
|
path: '/reset',
|
||||||
getParentRoute: () => rootRoute,
|
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({
|
const NewLazyRoute = NewLazyImport.update({
|
||||||
path: '/new',
|
path: '/new',
|
||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => rootRoute,
|
||||||
} as any).lazy(() => import('./routes/new.lazy').then((d) => d.Route))
|
} 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({
|
const BootstrapRelaysRoute = BootstrapRelaysImport.update({
|
||||||
path: '/bootstrap-relays',
|
path: '/bootstrap-relays',
|
||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => rootRoute,
|
||||||
@@ -71,6 +60,21 @@ const AccountChatsLazyRoute = AccountChatsLazyImport.update({
|
|||||||
import('./routes/$account.chats.lazy').then((d) => d.Route),
|
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({
|
const AccountRelaysRoute = AccountRelaysImport.update({
|
||||||
path: '/$account/relays',
|
path: '/$account/relays',
|
||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => rootRoute,
|
||||||
@@ -117,20 +121,6 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof BootstrapRelaysImport
|
preLoaderRoute: typeof BootstrapRelaysImport
|
||||||
parentRoute: typeof rootRoute
|
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': {
|
'/new': {
|
||||||
id: '/new'
|
id: '/new'
|
||||||
path: '/new'
|
path: '/new'
|
||||||
@@ -138,11 +128,11 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof NewLazyImport
|
preLoaderRoute: typeof NewLazyImport
|
||||||
parentRoute: typeof rootRoute
|
parentRoute: typeof rootRoute
|
||||||
}
|
}
|
||||||
'/nostr-connect': {
|
'/reset': {
|
||||||
id: '/nostr-connect'
|
id: '/reset'
|
||||||
path: '/nostr-connect'
|
path: '/reset'
|
||||||
fullPath: '/nostr-connect'
|
fullPath: '/reset'
|
||||||
preLoaderRoute: typeof NostrConnectLazyImport
|
preLoaderRoute: typeof ResetLazyImport
|
||||||
parentRoute: typeof rootRoute
|
parentRoute: typeof rootRoute
|
||||||
}
|
}
|
||||||
'/$account/contacts': {
|
'/$account/contacts': {
|
||||||
@@ -159,6 +149,27 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof AccountRelaysImport
|
preLoaderRoute: typeof AccountRelaysImport
|
||||||
parentRoute: typeof rootRoute
|
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': {
|
'/$account/chats': {
|
||||||
id: '/$account/chats'
|
id: '/$account/chats'
|
||||||
path: '/$account/chats'
|
path: '/$account/chats'
|
||||||
@@ -188,12 +199,13 @@ declare module '@tanstack/react-router' {
|
|||||||
export const routeTree = rootRoute.addChildren({
|
export const routeTree = rootRoute.addChildren({
|
||||||
IndexRoute,
|
IndexRoute,
|
||||||
BootstrapRelaysRoute,
|
BootstrapRelaysRoute,
|
||||||
CreateAccountLazyRoute,
|
|
||||||
ImportKeyLazyRoute,
|
|
||||||
NewLazyRoute,
|
NewLazyRoute,
|
||||||
NostrConnectLazyRoute,
|
ResetLazyRoute,
|
||||||
AccountContactsRoute,
|
AccountContactsRoute,
|
||||||
AccountRelaysRoute,
|
AccountRelaysRoute,
|
||||||
|
AuthConnectRoute,
|
||||||
|
AuthImportRoute,
|
||||||
|
AuthNewRoute,
|
||||||
AccountChatsLazyRoute: AccountChatsLazyRoute.addChildren({
|
AccountChatsLazyRoute: AccountChatsLazyRoute.addChildren({
|
||||||
AccountChatsIdRoute,
|
AccountChatsIdRoute,
|
||||||
AccountChatsNewLazyRoute,
|
AccountChatsNewLazyRoute,
|
||||||
@@ -210,12 +222,13 @@ export const routeTree = rootRoute.addChildren({
|
|||||||
"children": [
|
"children": [
|
||||||
"/",
|
"/",
|
||||||
"/bootstrap-relays",
|
"/bootstrap-relays",
|
||||||
"/create-account",
|
|
||||||
"/import-key",
|
|
||||||
"/new",
|
"/new",
|
||||||
"/nostr-connect",
|
"/reset",
|
||||||
"/$account/contacts",
|
"/$account/contacts",
|
||||||
"/$account/relays",
|
"/$account/relays",
|
||||||
|
"/auth/connect",
|
||||||
|
"/auth/import",
|
||||||
|
"/auth/new",
|
||||||
"/$account/chats"
|
"/$account/chats"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -225,17 +238,11 @@ export const routeTree = rootRoute.addChildren({
|
|||||||
"/bootstrap-relays": {
|
"/bootstrap-relays": {
|
||||||
"filePath": "bootstrap-relays.tsx"
|
"filePath": "bootstrap-relays.tsx"
|
||||||
},
|
},
|
||||||
"/create-account": {
|
|
||||||
"filePath": "create-account.lazy.tsx"
|
|
||||||
},
|
|
||||||
"/import-key": {
|
|
||||||
"filePath": "import-key.lazy.tsx"
|
|
||||||
},
|
|
||||||
"/new": {
|
"/new": {
|
||||||
"filePath": "new.lazy.tsx"
|
"filePath": "new.lazy.tsx"
|
||||||
},
|
},
|
||||||
"/nostr-connect": {
|
"/reset": {
|
||||||
"filePath": "nostr-connect.lazy.tsx"
|
"filePath": "reset.lazy.tsx"
|
||||||
},
|
},
|
||||||
"/$account/contacts": {
|
"/$account/contacts": {
|
||||||
"filePath": "$account.contacts.tsx"
|
"filePath": "$account.contacts.tsx"
|
||||||
@@ -243,6 +250,15 @@ export const routeTree = rootRoute.addChildren({
|
|||||||
"/$account/relays": {
|
"/$account/relays": {
|
||||||
"filePath": "$account.relays.tsx"
|
"filePath": "$account.relays.tsx"
|
||||||
},
|
},
|
||||||
|
"/auth/connect": {
|
||||||
|
"filePath": "auth/connect.tsx"
|
||||||
|
},
|
||||||
|
"/auth/import": {
|
||||||
|
"filePath": "auth/import.tsx"
|
||||||
|
},
|
||||||
|
"/auth/new": {
|
||||||
|
"filePath": "auth/new.tsx"
|
||||||
|
},
|
||||||
"/$account/chats": {
|
"/$account/chats": {
|
||||||
"filePath": "$account.chats.lazy.tsx",
|
"filePath": "$account.chats.lazy.tsx",
|
||||||
"children": [
|
"children": [
|
||||||
|
|||||||
@@ -2,12 +2,12 @@ import { commands } from "@/commands";
|
|||||||
import { GoBack } from "@/components/back";
|
import { GoBack } from "@/components/back";
|
||||||
import { Frame } from "@/components/frame";
|
import { Frame } from "@/components/frame";
|
||||||
import { Spinner } from "@/components/spinner";
|
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 { readText } from "@tauri-apps/plugin-clipboard-manager";
|
||||||
import { message } from "@tauri-apps/plugin-dialog";
|
import { message } from "@tauri-apps/plugin-dialog";
|
||||||
import { useState, useTransition } from "react";
|
import { useState, useTransition } from "react";
|
||||||
|
|
||||||
export const Route = createLazyFileRoute("/nostr-connect")({
|
export const Route = createFileRoute("/auth/connect")({
|
||||||
component: Screen,
|
component: Screen,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -70,12 +70,12 @@ function Screen() {
|
|||||||
placeholder="bunker://..."
|
placeholder="bunker://..."
|
||||||
value={uri}
|
value={uri}
|
||||||
onChange={(e) => setUri(e.target.value)}
|
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"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => pasteFromClipboard()}
|
onClick={() => pasteFromClipboard()}
|
||||||
className="absolute top-1/2 right-2 transform -translate-y-1/2 text-xs font-semibold text-blue-500"
|
className="absolute top-1/2 right-2 transform -translate-y-1/2 text-xs font-semibold text-blue-500 dark:text-blue-300"
|
||||||
>
|
>
|
||||||
Paste
|
Paste
|
||||||
</button>
|
</button>
|
||||||
@@ -2,12 +2,12 @@ import { commands } from "@/commands";
|
|||||||
import { GoBack } from "@/components/back";
|
import { GoBack } from "@/components/back";
|
||||||
import { Frame } from "@/components/frame";
|
import { Frame } from "@/components/frame";
|
||||||
import { Spinner } from "@/components/spinner";
|
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 { readText } from "@tauri-apps/plugin-clipboard-manager";
|
||||||
import { message } from "@tauri-apps/plugin-dialog";
|
import { message } from "@tauri-apps/plugin-dialog";
|
||||||
import { useState, useTransition } from "react";
|
import { useState, useTransition } from "react";
|
||||||
|
|
||||||
export const Route = createLazyFileRoute("/import-key")({
|
export const Route = createFileRoute("/auth/import")({
|
||||||
component: Screen,
|
component: Screen,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -41,7 +41,7 @@ function Screen() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await commands.importKey(key, password);
|
const res = await commands.importAccount(key, password);
|
||||||
|
|
||||||
if (res.status === "ok") {
|
if (res.status === "ok") {
|
||||||
navigate({ to: "/", replace: true });
|
navigate({ to: "/", replace: true });
|
||||||
@@ -63,7 +63,7 @@ function Screen() {
|
|||||||
<div className="w-[320px] flex flex-col gap-8">
|
<div className="w-[320px] flex flex-col gap-8">
|
||||||
<div className="flex flex-col gap-1 text-center">
|
<div className="flex flex-col gap-1 text-center">
|
||||||
<h1 className="leading-tight text-xl font-semibold">
|
<h1 className="leading-tight text-xl font-semibold">
|
||||||
Import Private Key
|
Import Account
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
@@ -85,24 +85,26 @@ function Screen() {
|
|||||||
placeholder="nsec or ncryptsec..."
|
placeholder="nsec or ncryptsec..."
|
||||||
value={key}
|
value={key}
|
||||||
onChange={(e) => setKey(e.target.value)}
|
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"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => pasteFromClipboard()}
|
onClick={() => pasteFromClipboard()}
|
||||||
className="absolute uppercase top-1/2 right-2 transform -translate-y-1/2 text-xs font-semibold text-blue-500"
|
className="absolute top-1/2 right-2 transform -translate-y-1/2 text-xs font-semibold text-blue-500 dark:text-blue-300"
|
||||||
>
|
>
|
||||||
Paste
|
Paste
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{key.length && !key.startsWith("ncryptsec") ? (
|
{key.length ? (
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<label
|
<label
|
||||||
htmlFor="password"
|
htmlFor="password"
|
||||||
className="text-sm font-medium text-neutral-800 dark:text-neutral-200"
|
className="text-sm font-medium text-neutral-800 dark:text-neutral-200"
|
||||||
>
|
>
|
||||||
Set password to secure your key
|
{!key.startsWith("ncryptsec")
|
||||||
|
? "Set password to secure your key"
|
||||||
|
: "Enter password to decrypt your key"}
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
name="password"
|
name="password"
|
||||||
@@ -4,11 +4,11 @@ import { GoBack } from "@/components/back";
|
|||||||
import { Frame } from "@/components/frame";
|
import { Frame } from "@/components/frame";
|
||||||
import { Spinner } from "@/components/spinner";
|
import { Spinner } from "@/components/spinner";
|
||||||
import { Plus } from "@phosphor-icons/react";
|
import { Plus } from "@phosphor-icons/react";
|
||||||
import { createLazyFileRoute } from "@tanstack/react-router";
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
import { message } from "@tauri-apps/plugin-dialog";
|
import { message } from "@tauri-apps/plugin-dialog";
|
||||||
import { useState, useTransition } from "react";
|
import { useState, useTransition } from "react";
|
||||||
|
|
||||||
export const Route = createLazyFileRoute("/create-account")({
|
export const Route = createFileRoute("/auth/new")({
|
||||||
component: Screen,
|
component: Screen,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -53,8 +53,7 @@ function Screen() {
|
|||||||
|
|
||||||
if (res.status === "ok") {
|
if (res.status === "ok") {
|
||||||
navigate({
|
navigate({
|
||||||
to: "/$account/relays",
|
to: "/",
|
||||||
params: { account: res.data },
|
|
||||||
replace: true,
|
replace: true,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -81,7 +80,7 @@ function Screen() {
|
|||||||
className="flex flex-col gap-3 p-3 rounded-xl overflow-hidden"
|
className="flex flex-col gap-3 p-3 rounded-xl overflow-hidden"
|
||||||
shadow
|
shadow
|
||||||
>
|
>
|
||||||
<div className="self-center relative rounded-full size-20 bg-neutral-100 dark:bg-neutral-900 my-3">
|
<div className="self-center relative rounded-full size-20 bg-neutral-100 dark:bg-white/10 my-3">
|
||||||
{picture.length ? (
|
{picture.length ? (
|
||||||
<img
|
<img
|
||||||
src={picture}
|
src={picture}
|
||||||
@@ -113,7 +112,7 @@ function Screen() {
|
|||||||
onChange={(e) => setName(e.target.value)}
|
onChange={(e) => setName(e.target.value)}
|
||||||
placeholder="e.g. Alice"
|
placeholder="e.g. Alice"
|
||||||
spellCheck={false}
|
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"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
@@ -129,7 +128,7 @@ function Screen() {
|
|||||||
onChange={(e) => setAbout(e.target.value)}
|
onChange={(e) => setAbout(e.target.value)}
|
||||||
placeholder="e.g. Artist, anime-lover, and k-pop fan"
|
placeholder="e.g. Artist, anime-lover, and k-pop fan"
|
||||||
spellCheck={false}
|
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"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="h-px w-full mt-2 bg-neutral-100 dark:bg-neutral-900" />
|
<div className="h-px w-full mt-2 bg-neutral-100 dark:bg-neutral-900" />
|
||||||
@@ -145,7 +144,7 @@ function Screen() {
|
|||||||
type="password"
|
type="password"
|
||||||
value={password}
|
value={password}
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
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"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Frame>
|
</Frame>
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
import { commands } from "@/commands";
|
import { commands } from "@/commands";
|
||||||
|
import { GoBack } from "@/components";
|
||||||
import { Frame } from "@/components/frame";
|
import { Frame } from "@/components/frame";
|
||||||
import { Spinner } from "@/components/spinner";
|
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 { createLazyFileRoute } from "@tanstack/react-router";
|
||||||
import { message } from "@tauri-apps/plugin-dialog";
|
import { message } from "@tauri-apps/plugin-dialog";
|
||||||
|
import { relaunch } from "@tauri-apps/plugin-process";
|
||||||
import { useEffect, useState, useTransition } from "react";
|
import { useEffect, useState, useTransition } from "react";
|
||||||
|
|
||||||
export const Route = createLazyFileRoute("/bootstrap-relays")({
|
export const Route = createLazyFileRoute("/bootstrap-relays")({
|
||||||
@@ -50,13 +52,11 @@ function Screen() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const merged = relays
|
const merged = relays.join("\r\n");
|
||||||
.map((relay) => Object.values(relay).join(","))
|
|
||||||
.join("\n");
|
|
||||||
const res = await commands.setBootstrapRelays(merged);
|
const res = await commands.setBootstrapRelays(merged);
|
||||||
|
|
||||||
if (res.status === "ok") {
|
if (res.status === "ok") {
|
||||||
// TODO: restart app
|
return await relaunch();
|
||||||
} else {
|
} else {
|
||||||
await message(res.error, {
|
await message(res.error, {
|
||||||
title: "Manage Relays",
|
title: "Manage Relays",
|
||||||
@@ -72,7 +72,10 @@ function Screen() {
|
|||||||
}, [bootstrapRelays]);
|
}, [bootstrapRelays]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="size-full flex items-center justify-center">
|
<div
|
||||||
|
data-tauri-drag-region
|
||||||
|
className="relative size-full flex items-center justify-center"
|
||||||
|
>
|
||||||
<div className="w-[320px] flex flex-col gap-8">
|
<div className="w-[320px] flex flex-col gap-8">
|
||||||
<div className="flex flex-col gap-1 text-center">
|
<div className="flex flex-col gap-1 text-center">
|
||||||
<h1 className="leading-tight text-xl font-semibold">Manage Relays</h1>
|
<h1 className="leading-tight text-xl font-semibold">Manage Relays</h1>
|
||||||
@@ -134,9 +137,16 @@ function Screen() {
|
|||||||
>
|
>
|
||||||
{isPending ? <Spinner /> : "Save & Restart"}
|
{isPending ? <Spinner /> : "Save & Restart"}
|
||||||
</button>
|
</button>
|
||||||
|
<span className="mt-2 w-full text-sm text-neutral-600 dark:text-neutral-400 inline-flex items-center justify-center">
|
||||||
|
Lume will relaunch after saving.
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<GoBack className="fixed top-11 left-2 flex items-center gap-1.5 text-sm font-medium">
|
||||||
|
<ArrowLeft className="size-5" />
|
||||||
|
Back
|
||||||
|
</GoBack>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import { commands } from "@/commands";
|
import { commands } from "@/commands";
|
||||||
import { npub } from "@/commons";
|
import { npub } from "@/commons";
|
||||||
import { Frame } from "@/components/frame";
|
import { Frame, User, Spinner } from "@/components";
|
||||||
import { Spinner } from "@/components/spinner";
|
|
||||||
import { User } from "@/components/user";
|
|
||||||
import { ArrowRight, DotsThree, GearSix, Plus } from "@phosphor-icons/react";
|
import { ArrowRight, DotsThree, GearSix, Plus } from "@phosphor-icons/react";
|
||||||
import { Link, createLazyFileRoute } from "@tanstack/react-router";
|
import { Link, createLazyFileRoute } from "@tanstack/react-router";
|
||||||
import { Menu, MenuItem } from "@tauri-apps/api/menu";
|
import { Menu, MenuItem } from "@tauri-apps/api/menu";
|
||||||
@@ -35,6 +33,7 @@ function Screen() {
|
|||||||
|
|
||||||
const [accounts, setAccounts] = useState([]);
|
const [accounts, setAccounts] = useState([]);
|
||||||
const [value, setValue] = useState("");
|
const [value, setValue] = useState("");
|
||||||
|
const [autoLogin, setAutoLogin] = useState(false);
|
||||||
const [password, setPassword] = useState("");
|
const [password, setPassword] = useState("");
|
||||||
const [isPending, startTransition] = useTransition();
|
const [isPending, startTransition] = useTransition();
|
||||||
|
|
||||||
@@ -48,30 +47,25 @@ function Screen() {
|
|||||||
|
|
||||||
const selectAccount = (account: string) => {
|
const selectAccount = (account: string) => {
|
||||||
setValue(account);
|
setValue(account);
|
||||||
|
|
||||||
|
if (account.includes("_nostrconnect")) {
|
||||||
|
setAutoLogin(true);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const loginWith = () => {
|
const loginWith = () => {
|
||||||
startTransition(async () => {
|
startTransition(async () => {
|
||||||
if (!value || !password) return;
|
|
||||||
|
|
||||||
const res = await commands.login(value, password);
|
const res = await commands.login(value, password);
|
||||||
|
|
||||||
if (res.status === "ok") {
|
if (res.status === "ok") {
|
||||||
navigate({
|
navigate({
|
||||||
to: "/$account/chats/new",
|
to: "/$account/chats",
|
||||||
params: { account: res.data },
|
params: { account: res.data },
|
||||||
replace: true,
|
replace: true,
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
if (res.error === "404") {
|
|
||||||
navigate({
|
|
||||||
to: "/$account/relays",
|
|
||||||
params: { account: value },
|
|
||||||
replace: true,
|
|
||||||
});
|
|
||||||
} else {
|
} 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();
|
e.stopPropagation();
|
||||||
|
|
||||||
const menuItems = await Promise.all([
|
const menuItems = await Promise.all([
|
||||||
|
MenuItem.new({
|
||||||
|
text: "Reset password",
|
||||||
|
enabled: !account.includes("_nostrconnect"),
|
||||||
|
action: () => navigate({ to: "/reset", search: { account } }),
|
||||||
|
}),
|
||||||
MenuItem.new({
|
MenuItem.new({
|
||||||
text: "Delete account",
|
text: "Delete account",
|
||||||
action: async () => await deleteAccount(account),
|
action: async () => await deleteAccount(account),
|
||||||
@@ -96,6 +95,12 @@ function Screen() {
|
|||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (autoLogin) {
|
||||||
|
loginWith();
|
||||||
|
}
|
||||||
|
}, [autoLogin, value]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setAccounts(context.accounts);
|
setAccounts(context.accounts);
|
||||||
}, [context.accounts]);
|
}, [context.accounts]);
|
||||||
@@ -123,10 +128,10 @@ function Screen() {
|
|||||||
onKeyDown={() => selectAccount(account)}
|
onKeyDown={() => selectAccount(account)}
|
||||||
className="group flex items-center gap-2 hover:bg-black/5 dark:hover:bg-white/5 p-3"
|
className="group flex items-center gap-2 hover:bg-black/5 dark:hover:bg-white/5 p-3"
|
||||||
>
|
>
|
||||||
<User.Provider pubkey={account}>
|
<User.Provider pubkey={account.replace("_nostrconnect", "")}>
|
||||||
<User.Root className="flex-1 flex items-center gap-2.5">
|
<User.Root className="flex-1 flex items-center gap-2.5">
|
||||||
<User.Avatar className="rounded-full size-10" />
|
<User.Avatar className="rounded-full size-10" />
|
||||||
{value === account ? (
|
{value === account && !value.includes("_nostrconnect") ? (
|
||||||
<div className="flex-1 flex items-center gap-2">
|
<div className="flex-1 flex items-center gap-2">
|
||||||
<input
|
<input
|
||||||
name="password"
|
name="password"
|
||||||
@@ -137,14 +142,21 @@ function Screen() {
|
|||||||
if (e.key === "Enter") loginWith();
|
if (e.key === "Enter") loginWith();
|
||||||
}}
|
}}
|
||||||
placeholder="Password"
|
placeholder="Password"
|
||||||
className="px-3 rounded-full 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="px-3 rounded-full 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"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="inline-flex flex-col items-start">
|
<div className="inline-flex flex-col items-start">
|
||||||
|
<div className="inline-flex items-center gap-1.5">
|
||||||
<User.Name className="max-w-[6rem] truncate font-medium leading-tight" />
|
<User.Name className="max-w-[6rem] truncate font-medium leading-tight" />
|
||||||
|
{account.includes("_nostrconnect") ? (
|
||||||
|
<div className="text-[8px] border border-blue-500 text-blue-500 px-1.5 rounded-full">
|
||||||
|
Nostr Connect
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
<span className="text-sm text-neutral-700 dark:text-neutral-300">
|
<span className="text-sm text-neutral-700 dark:text-neutral-300">
|
||||||
{npub(account, 16)}
|
{npub(account.replace("_nostrconnect", ""), 16)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,16 +1,12 @@
|
|||||||
import { commands } from "@/commands";
|
import { commands } from "@/commands";
|
||||||
import { checkForAppUpdates, checkPermission } from "@/commons";
|
import { checkForAppUpdates } from "@/commons";
|
||||||
import { createFileRoute, redirect } from "@tanstack/react-router";
|
import { createFileRoute, redirect } from "@tanstack/react-router";
|
||||||
|
|
||||||
export const Route = createFileRoute("/")({
|
export const Route = createFileRoute("/")({
|
||||||
beforeLoad: async () => {
|
beforeLoad: async () => {
|
||||||
// Check for app updates
|
// Check for app updates
|
||||||
// TODO: move this function to rust
|
|
||||||
await checkForAppUpdates(true);
|
await checkForAppUpdates(true);
|
||||||
|
|
||||||
// Request notification permission
|
|
||||||
await checkPermission();
|
|
||||||
|
|
||||||
// Get all accounts from system
|
// Get all accounts from system
|
||||||
const accounts = await commands.getAccounts();
|
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 };
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ function Screen() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<Link
|
<Link
|
||||||
to="/create-account"
|
to="/auth/new"
|
||||||
className="w-full h-10 bg-blue-500 font-medium hover:bg-blue-600 text-white rounded-lg inline-flex items-center justify-center shadow"
|
className="w-full h-10 bg-blue-500 font-medium hover:bg-blue-600 text-white rounded-lg inline-flex items-center justify-center shadow"
|
||||||
>
|
>
|
||||||
Create a new identity
|
Create a new identity
|
||||||
@@ -32,7 +32,7 @@ function Screen() {
|
|||||||
Login with Nostr Connect
|
Login with Nostr Connect
|
||||||
</Link>*/}
|
</Link>*/}
|
||||||
<Link
|
<Link
|
||||||
to="/import-key"
|
to="/auth/import"
|
||||||
className="w-full h-10 bg-white hover:bg-neutral-100 dark:hover:bg-neutral-100 dark:bg-white dark:text-black rounded-lg inline-flex items-center justify-center"
|
className="w-full h-10 bg-white hover:bg-neutral-100 dark:hover:bg-neutral-100 dark:bg-white dark:text-black rounded-lg inline-flex items-center justify-center"
|
||||||
>
|
>
|
||||||
Login with Private Key
|
Login with Private Key
|
||||||
|
|||||||
130
src/routes/reset.lazy.tsx
Normal file
130
src/routes/reset.lazy.tsx
Normal file
@@ -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 (
|
||||||
|
<div
|
||||||
|
data-tauri-drag-region
|
||||||
|
className="size-full flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<div className="w-[320px] flex flex-col gap-8">
|
||||||
|
<div className="flex flex-col gap-1 text-center">
|
||||||
|
<h1 className="leading-tight text-xl font-semibold">
|
||||||
|
Reset password
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<Frame
|
||||||
|
className="flex flex-col gap-3 p-3 rounded-xl overflow-hidden"
|
||||||
|
shadow
|
||||||
|
>
|
||||||
|
<div className="flex flex-col gap-1.5">
|
||||||
|
<label
|
||||||
|
htmlFor="key"
|
||||||
|
className="text-sm font-medium text-neutral-800 dark:text-neutral-200"
|
||||||
|
>
|
||||||
|
Private Key
|
||||||
|
</label>
|
||||||
|
<div className="relative">
|
||||||
|
<input
|
||||||
|
name="key"
|
||||||
|
type="password"
|
||||||
|
placeholder="nsec..."
|
||||||
|
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"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => pasteFromClipboard()}
|
||||||
|
className="absolute uppercase top-1/2 right-2 transform -translate-y-1/2 text-xs font-semibold text-blue-500"
|
||||||
|
>
|
||||||
|
Paste
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<label
|
||||||
|
htmlFor="password"
|
||||||
|
className="text-sm font-medium text-neutral-800 dark:text-neutral-200"
|
||||||
|
>
|
||||||
|
Set a new password
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
name="password"
|
||||||
|
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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Frame>
|
||||||
|
<div className="flex flex-col items-center gap-1">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => submit()}
|
||||||
|
disabled={isPending}
|
||||||
|
className="inline-flex items-center justify-center w-full h-9 text-sm font-semibold text-white bg-blue-500 rounded-lg shrink-0 hover:bg-blue-600 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{isPending ? <Spinner /> : "Continue"}
|
||||||
|
</button>
|
||||||
|
<GoBack className="mt-2 w-full text-sm text-neutral-600 dark:text-neutral-400 inline-flex items-center justify-center">
|
||||||
|
Go back to previous screen
|
||||||
|
</GoBack>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user