feat: improve account management
This commit is contained in:
@@ -11,6 +11,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@getalby/bitcoin-connect-react": "^3.6.1",
|
"@getalby/bitcoin-connect-react": "^3.6.1",
|
||||||
|
"@phosphor-icons/react": "^2.1.7",
|
||||||
"@radix-ui/react-avatar": "^1.1.0",
|
"@radix-ui/react-avatar": "^1.1.0",
|
||||||
"@radix-ui/react-checkbox": "^1.1.1",
|
"@radix-ui/react-checkbox": "^1.1.1",
|
||||||
"@radix-ui/react-popover": "^1.1.1",
|
"@radix-ui/react-popover": "^1.1.1",
|
||||||
@@ -42,8 +43,8 @@
|
|||||||
"nanoid": "^5.0.7",
|
"nanoid": "^5.0.7",
|
||||||
"nostr-tools": "^2.7.2",
|
"nostr-tools": "^2.7.2",
|
||||||
"react": "19.0.0-rc-d025ddd3-20240722",
|
"react": "19.0.0-rc-d025ddd3-20240722",
|
||||||
"react-dom": "19.0.0-rc-d025ddd3-20240722",
|
|
||||||
"react-currency-input-field": "^3.8.0",
|
"react-currency-input-field": "^3.8.0",
|
||||||
|
"react-dom": "19.0.0-rc-d025ddd3-20240722",
|
||||||
"react-hook-form": "^7.52.2",
|
"react-hook-form": "^7.52.2",
|
||||||
"react-i18next": "^15.0.1",
|
"react-i18next": "^15.0.1",
|
||||||
"react-string-replace": "^1.1.1",
|
"react-string-replace": "^1.1.1",
|
||||||
@@ -53,9 +54,6 @@
|
|||||||
"virtua": "^0.33.5"
|
"virtua": "^0.33.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"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",
|
"@biomejs/biome": "^1.8.3",
|
||||||
"@evilmartians/harmony": "^1.2.0",
|
"@evilmartians/harmony": "^1.2.0",
|
||||||
"@tailwindcss/forms": "^0.5.7",
|
"@tailwindcss/forms": "^0.5.7",
|
||||||
@@ -63,8 +61,11 @@
|
|||||||
"@tanstack/router-devtools": "^1.47.1",
|
"@tanstack/router-devtools": "^1.47.1",
|
||||||
"@tanstack/router-plugin": "^1.47.0",
|
"@tanstack/router-plugin": "^1.47.0",
|
||||||
"@tauri-apps/cli": "2.0.0-rc.1",
|
"@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",
|
"@vitejs/plugin-react": "^4.3.1",
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
|
"babel-plugin-react-compiler": "0.0.0-experimental-696af53-20240625",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"postcss": "^8.4.41",
|
"postcss": "^8.4.41",
|
||||||
"tailwind-gradient-mask-image": "^1.2.0",
|
"tailwind-gradient-mask-image": "^1.2.0",
|
||||||
|
|||||||
15
pnpm-lock.yaml
generated
15
pnpm-lock.yaml
generated
@@ -11,6 +11,9 @@ importers:
|
|||||||
'@getalby/bitcoin-connect-react':
|
'@getalby/bitcoin-connect-react':
|
||||||
specifier: ^3.6.1
|
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)
|
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':
|
'@radix-ui/react-avatar':
|
||||||
specifier: ^1.1.0
|
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)
|
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==}
|
resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
|
||||||
engines: {node: '>= 8'}
|
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':
|
'@pkgjs/parseargs@0.11.0':
|
||||||
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
|
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
|
||||||
engines: {node: '>=14'}
|
engines: {node: '>=14'}
|
||||||
@@ -2604,6 +2614,11 @@ snapshots:
|
|||||||
'@nodelib/fs.scandir': 2.1.5
|
'@nodelib/fs.scandir': 2.1.5
|
||||||
fastq: 1.17.1
|
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':
|
'@pkgjs/parseargs@0.11.0':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
|||||||
@@ -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::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 specta::Type;
|
||||||
use std::collections::HashSet;
|
use std::{collections::HashSet, str::FromStr, time::Duration};
|
||||||
use std::time::Duration;
|
|
||||||
use tauri::{Emitter, EventTarget, Manager, State};
|
use tauri::{Emitter, EventTarget, Manager, State};
|
||||||
use tauri_plugin_notification::NotificationExt;
|
use tauri_plugin_notification::NotificationExt;
|
||||||
|
|
||||||
#[derive(Serialize, Type)]
|
// #[cfg(target_os = "macos")]
|
||||||
pub struct Account {
|
// use crate::commands::tray::create_tray_panel;
|
||||||
npub: String,
|
use crate::{
|
||||||
nsec: String,
|
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<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
#[specta::specta]
|
#[specta::specta]
|
||||||
pub fn get_accounts() -> Vec<String> {
|
pub fn get_accounts() -> Vec<String> {
|
||||||
let search = Search::new().expect("Unexpected.");
|
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 list = List::list_credentials(&results, Limit::All);
|
||||||
let accounts: HashSet<String> = list
|
let accounts: HashSet<String> = list
|
||||||
.split_whitespace()
|
.split_whitespace()
|
||||||
@@ -36,76 +37,96 @@ pub fn get_accounts() -> Vec<String> {
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
#[specta::specta]
|
#[specta::specta]
|
||||||
pub fn create_account() -> Result<Account, String> {
|
pub async fn create_account(
|
||||||
let keys = Keys::generate();
|
name: String,
|
||||||
let public_key = keys.public_key();
|
about: String,
|
||||||
let secret_key = keys.secret_key().unwrap();
|
picture: String,
|
||||||
|
password: String,
|
||||||
let result = Account {
|
|
||||||
npub: public_key.to_bech32().unwrap(),
|
|
||||||
nsec: secret_key.to_bech32().unwrap(),
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
#[specta::specta]
|
|
||||||
pub fn get_private_key(npub: &str) -> Result<String, String> {
|
|
||||||
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())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
#[specta::specta]
|
|
||||||
pub async fn save_account(
|
|
||||||
nsec: &str,
|
|
||||||
password: &str,
|
|
||||||
state: State<'_, Nostr>,
|
state: State<'_, Nostr>,
|
||||||
) -> Result<String, String> {
|
) -> Result<String, String> {
|
||||||
let secret_key = if nsec.starts_with("ncryptsec") {
|
let client = &state.client;
|
||||||
let encrypted_key = EncryptedSecretKey::from_bech32(nsec).unwrap();
|
let keys = Keys::generate();
|
||||||
encrypted_key
|
let npub = keys.public_key().to_bech32().map_err(|e| e.to_string())?;
|
||||||
.to_secret_key(password)
|
let secret_key = keys.secret_key().map_err(|e| e.to_string())?;
|
||||||
.map_err(|err| err.to_string())
|
let enc = EncryptedSecretKey::new(secret_key, password, 16, KeySecurity::Medium)
|
||||||
} else {
|
.map_err(|err| err.to_string())?;
|
||||||
SecretKey::from_bech32(nsec).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);
|
||||||
|
|
||||||
match secret_key {
|
let signer = NostrSigner::Keys(keys);
|
||||||
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 keyring = Entry::new("Lume", &npub).unwrap();
|
// Update signer
|
||||||
let _ = keyring.set_password(&nsec);
|
client.set_signer(Some(signer)).await;
|
||||||
|
|
||||||
let signer = NostrSigner::Keys(nostr_keys);
|
let mut metadata = Metadata::new()
|
||||||
let client = &state.client;
|
.display_name(name.clone())
|
||||||
|
.name(name.to_lowercase())
|
||||||
|
.about(about);
|
||||||
|
|
||||||
// Update client's signer
|
if let Ok(url) = Url::parse(&picture) {
|
||||||
client.set_signer(Some(signer)).await;
|
metadata = metadata.picture(url)
|
||||||
|
}
|
||||||
|
|
||||||
Ok(npub)
|
match client.set_metadata(&metadata).await {
|
||||||
}
|
Ok(_) => Ok(npub),
|
||||||
Err(msg) => Err(msg),
|
Err(e) => Err(e.to_string()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
#[specta::specta]
|
#[specta::specta]
|
||||||
pub async fn connect_remote_account(uri: &str, state: State<'_, Nostr>) -> Result<String, String> {
|
pub async fn import_account(
|
||||||
|
key: String,
|
||||||
|
password: Option<String>,
|
||||||
|
state: State<'_, Nostr>,
|
||||||
|
) -> Result<String, 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();
|
||||||
|
|
||||||
|
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 Secret Storage", &npub).map_err(|e| e.to_string())?;
|
||||||
|
let account = Account {
|
||||||
|
password: enc_bech32,
|
||||||
|
nostr_connect: None,
|
||||||
|
};
|
||||||
|
let j = serde_json::to_string(&account).map_err(|e| e.to_string())?;
|
||||||
|
let _ = keyring.set_password(&j);
|
||||||
|
|
||||||
|
let signer = NostrSigner::Keys(keys);
|
||||||
|
|
||||||
|
// Update client's signer
|
||||||
|
client.set_signer(Some(signer)).await;
|
||||||
|
|
||||||
|
Ok(npub)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
#[specta::specta]
|
||||||
|
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().unwrap().to_string();
|
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 {
|
match Nip46Signer::new(bunker_uri, app_keys, Duration::from_secs(120), None).await {
|
||||||
Ok(signer) => {
|
Ok(signer) => {
|
||||||
let keyring = Entry::new("Lume", &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("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
|
// Update signer
|
||||||
let _ = client.set_signer(Some(signer.into())).await;
|
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]
|
#[tauri::command]
|
||||||
#[specta::specta]
|
#[specta::specta]
|
||||||
pub async fn get_encrypted_key(npub: &str, password: &str) -> Result<String, String> {
|
pub fn delete_account(id: String) -> Result<(), String> {
|
||||||
let keyring = Entry::new("Lume", npub).unwrap();
|
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() {
|
Ok(())
|
||||||
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())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
#[specta::specta]
|
#[specta::specta]
|
||||||
pub async fn load_account(
|
pub async fn login(
|
||||||
npub: &str,
|
account: String,
|
||||||
bunker: Option<&str>,
|
password: String,
|
||||||
state: State<'_, Nostr>,
|
state: State<'_, Nostr>,
|
||||||
app: tauri::AppHandle,
|
app: tauri::AppHandle,
|
||||||
) -> Result<bool, String> {
|
) -> Result<String, String> {
|
||||||
let handle = app.clone();
|
let handle = app.clone();
|
||||||
let client = &state.client;
|
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() {
|
let account = match keyring.get_password() {
|
||||||
Ok(pw) => pw,
|
Ok(pw) => {
|
||||||
Err(_) => return Err("Cancelled".into()),
|
let account: Account = serde_json::from_str(&pw).map_err(|e| e.to_string())?;
|
||||||
|
account
|
||||||
|
}
|
||||||
|
Err(e) => return Err(e.to_string()),
|
||||||
};
|
};
|
||||||
|
|
||||||
match bunker {
|
let public_key = match account.nostr_connect {
|
||||||
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()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => {
|
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);
|
let signer = NostrSigner::Keys(keys);
|
||||||
|
|
||||||
// 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().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)
|
// Connect to user's relay (NIP-65)
|
||||||
init_nip65(client).await;
|
init_nip65(client).await;
|
||||||
|
|
||||||
// Create tray (macOS)
|
// Create tray (macOS)
|
||||||
#[cfg(target_os = "macos")]
|
// #[cfg(target_os = "macos")]
|
||||||
create_tray_panel(npub, &handle);
|
// create_tray_panel(&public_key.to_bech32().unwrap(), &handle);
|
||||||
|
|
||||||
// Get user's contact list
|
// 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
|
*state.contact_list.lock().unwrap() = contacts
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -418,5 +452,5 @@ pub async fn load_account(
|
|||||||
.await
|
.await
|
||||||
});
|
});
|
||||||
|
|
||||||
Ok(true)
|
Ok(public_key)
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
|
pub mod account;
|
||||||
pub mod event;
|
pub mod event;
|
||||||
pub mod keys;
|
|
||||||
pub mod metadata;
|
pub mod metadata;
|
||||||
pub mod relay;
|
pub mod relay;
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ extern crate objc;
|
|||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
use border::WebviewWindowExt as BorderWebviewWindowExt;
|
use border::WebviewWindowExt as BorderWebviewWindowExt;
|
||||||
use commands::{event::*, keys::*, metadata::*, relay::*, window::*};
|
use commands::{account::*, event::*, metadata::*, relay::*, window::*};
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use specta::Type;
|
use specta::Type;
|
||||||
@@ -85,11 +85,10 @@ fn main() {
|
|||||||
save_bootstrap_relays,
|
save_bootstrap_relays,
|
||||||
get_accounts,
|
get_accounts,
|
||||||
create_account,
|
create_account,
|
||||||
save_account,
|
import_account,
|
||||||
get_encrypted_key,
|
connect_account,
|
||||||
get_private_key,
|
delete_account,
|
||||||
connect_remote_account,
|
login,
|
||||||
load_account,
|
|
||||||
get_current_profile,
|
get_current_profile,
|
||||||
get_profile,
|
get_profile,
|
||||||
get_contact_list,
|
get_contact_list,
|
||||||
|
|||||||
@@ -48,49 +48,41 @@ async saveBootstrapRelays(relays: string) : Promise<Result<null, string>> {
|
|||||||
async getAccounts() : Promise<string[]> {
|
async getAccounts() : Promise<string[]> {
|
||||||
return await TAURI_INVOKE("get_accounts");
|
return await TAURI_INVOKE("get_accounts");
|
||||||
},
|
},
|
||||||
async createAccount() : Promise<Result<Account, 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") };
|
return { status: "ok", data: await TAURI_INVOKE("create_account", { name, about, picture, 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 };
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async saveAccount(nsec: string, password: string) : Promise<Result<string, string>> {
|
async importAccount(key: string, password: string | null) : Promise<Result<string, string>> {
|
||||||
try {
|
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) {
|
} 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 };
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async getEncryptedKey(npub: string, password: string) : Promise<Result<string, string>> {
|
async connectAccount(uri: string) : Promise<Result<string, string>> {
|
||||||
try {
|
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) {
|
} 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 };
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async getPrivateKey(npub: string) : Promise<Result<string, string>> {
|
async deleteAccount(id: string) : Promise<Result<null, string>> {
|
||||||
try {
|
try {
|
||||||
return { status: "ok", data: await TAURI_INVOKE("get_private_key", { npub }) };
|
return { status: "ok", data: await TAURI_INVOKE("delete_account", { id }) };
|
||||||
} 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 };
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async connectRemoteAccount(uri: string) : Promise<Result<string, string>> {
|
async login(account: string, password: string) : Promise<Result<string, string>> {
|
||||||
try {
|
try {
|
||||||
return { status: "ok", data: await TAURI_INVOKE("connect_remote_account", { uri }) };
|
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 };
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async loadAccount(npub: string, bunker: string | null) : Promise<Result<boolean, string>> {
|
|
||||||
try {
|
|
||||||
return { status: "ok", data: await TAURI_INVOKE("load_account", { npub, bunker }) };
|
|
||||||
} 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 };
|
||||||
@@ -461,7 +453,6 @@ async setBadge(count: number) : Promise<void> {
|
|||||||
|
|
||||||
/** user-defined types **/
|
/** 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 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 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 }
|
export type Relays = { connected: string[]; read: string[] | null; write: string[] | null; both: string[] | null }
|
||||||
|
|||||||
20
src/components/back.tsx
Normal file
20
src/components/back.tsx
Normal file
@@ -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 (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => history.go(-1)}
|
||||||
|
className={cn(className)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
27
src/components/frame.tsx
Normal file
27
src/components/frame.tsx
Normal file
@@ -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 (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
className,
|
||||||
|
platform === "linux"
|
||||||
|
? "bg-white dark:bg-neutral-950"
|
||||||
|
: "bg-white dark:bg-white/10",
|
||||||
|
shadow
|
||||||
|
? "shadow-lg shadow-neutral-500/10 dark:shadow-none dark:ring-1 dark:ring-white/20"
|
||||||
|
: "",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
export * from "./container";
|
export * from "./container";
|
||||||
|
export * from "./frame";
|
||||||
|
export * from "./back";
|
||||||
export * from "./box";
|
export * from "./box";
|
||||||
export * from "./spinner";
|
export * from "./spinner";
|
||||||
export * from "./quote";
|
export * from "./quote";
|
||||||
|
|||||||
@@ -26,11 +26,11 @@ import { Route as CreateTopicImport } from './routes/create-topic'
|
|||||||
import { Route as CreateNewsfeedImport } from './routes/create-newsfeed'
|
import { Route as CreateNewsfeedImport } from './routes/create-newsfeed'
|
||||||
import { Route as CreateGroupImport } from './routes/create-group'
|
import { Route as CreateGroupImport } from './routes/create-group'
|
||||||
import { Route as BootstrapRelaysImport } from './routes/bootstrap-relays'
|
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 IndexImport } from './routes/index'
|
||||||
import { Route as EditorIndexImport } from './routes/editor/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 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 TrendingUsersImport } from './routes/trending.users'
|
||||||
import { Route as TrendingNotesImport } from './routes/trending.notes'
|
import { Route as TrendingNotesImport } from './routes/trending.notes'
|
||||||
import { Route as SettingsWalletImport } from './routes/settings/wallet'
|
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 EventsIdImport } from './routes/events/$id'
|
||||||
import { Route as CreateNewsfeedUsersImport } from './routes/create-newsfeed.users'
|
import { Route as CreateNewsfeedUsersImport } from './routes/create-newsfeed.users'
|
||||||
import { Route as CreateNewsfeedF2fImport } from './routes/create-newsfeed.f2f'
|
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 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
|
// Create Virtual Routes
|
||||||
|
|
||||||
const LandingLazyImport = createFileRoute('/landing')()
|
const NewLazyImport = createFileRoute('/new')()
|
||||||
const AuthLazyImport = createFileRoute('/auth')()
|
const AuthNewLazyImport = createFileRoute('/auth/new')()
|
||||||
const AuthRemoteLazyImport = createFileRoute('/auth/remote')()
|
|
||||||
const AuthImportLazyImport = createFileRoute('/auth/import')()
|
const AuthImportLazyImport = createFileRoute('/auth/import')()
|
||||||
|
const AuthConnectLazyImport = createFileRoute('/auth/connect')()
|
||||||
|
const AccountPanelLazyImport = createFileRoute('/$account/panel')()
|
||||||
|
|
||||||
// Create/Update Routes
|
// Create/Update Routes
|
||||||
|
|
||||||
const LandingLazyRoute = LandingLazyImport.update({
|
const NewLazyRoute = NewLazyImport.update({
|
||||||
path: '/landing',
|
path: '/new',
|
||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => rootRoute,
|
||||||
} as any).lazy(() => import('./routes/landing.lazy').then((d) => d.Route))
|
} as any).lazy(() => import('./routes/new.lazy').then((d) => d.Route))
|
||||||
|
|
||||||
const AuthLazyRoute = AuthLazyImport.update({
|
|
||||||
path: '/auth',
|
|
||||||
getParentRoute: () => rootRoute,
|
|
||||||
} as any).lazy(() => import('./routes/auth.lazy').then((d) => d.Route))
|
|
||||||
|
|
||||||
const TrendingRoute = TrendingImport.update({
|
const TrendingRoute = TrendingImport.update({
|
||||||
path: '/trending',
|
path: '/trending',
|
||||||
@@ -133,40 +127,50 @@ const BootstrapRelaysRoute = BootstrapRelaysImport.update({
|
|||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => rootRoute,
|
||||||
} as any)
|
} 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({
|
const IndexRoute = IndexImport.update({
|
||||||
path: '/',
|
path: '/',
|
||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => rootRoute,
|
||||||
} as any)
|
} as any).lazy(() => import('./routes/index.lazy').then((d) => d.Route))
|
||||||
|
|
||||||
const EditorIndexRoute = EditorIndexImport.update({
|
const EditorIndexRoute = EditorIndexImport.update({
|
||||||
path: '/editor/',
|
path: '/editor/',
|
||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => rootRoute,
|
||||||
} as any)
|
} as any)
|
||||||
|
|
||||||
const AccountIndexRoute = AccountIndexImport.update({
|
const AuthNewLazyRoute = AuthNewLazyImport.update({
|
||||||
path: '/$account/',
|
path: '/auth/new',
|
||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => rootRoute,
|
||||||
} as any).lazy(() =>
|
} as any).lazy(() => import('./routes/auth/new.lazy').then((d) => d.Route))
|
||||||
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))
|
|
||||||
|
|
||||||
const AuthImportLazyRoute = AuthImportLazyImport.update({
|
const AuthImportLazyRoute = AuthImportLazyImport.update({
|
||||||
path: '/import',
|
path: '/auth/import',
|
||||||
getParentRoute: () => AuthLazyRoute,
|
getParentRoute: () => rootRoute,
|
||||||
} as any).lazy(() => import('./routes/auth/import.lazy').then((d) => d.Route))
|
} 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({
|
const ZapIdRoute = ZapIdImport.update({
|
||||||
path: '/zap/$id',
|
path: '/zap/$id',
|
||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => rootRoute,
|
||||||
} as any)
|
} as any)
|
||||||
|
|
||||||
const UsersPubkeyRoute = UsersPubkeyImport.update({
|
const UsersIdRoute = UsersIdImport.update({
|
||||||
path: '/users/$pubkey',
|
path: '/users/$id',
|
||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => rootRoute,
|
||||||
} as any)
|
} as any)
|
||||||
|
|
||||||
@@ -235,26 +239,14 @@ const CreateNewsfeedF2fRoute = CreateNewsfeedF2fImport.update({
|
|||||||
getParentRoute: () => CreateNewsfeedRoute,
|
getParentRoute: () => CreateNewsfeedRoute,
|
||||||
} as any)
|
} 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({
|
const AccountHomeRoute = AccountHomeImport.update({
|
||||||
path: '/$account/home',
|
path: '/home',
|
||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => AccountRoute,
|
||||||
} as any).lazy(() => import('./routes/$account/home.lazy').then((d) => d.Route))
|
} as any).lazy(() => import('./routes/$account/home.lazy').then((d) => d.Route))
|
||||||
|
|
||||||
const AuthAccountBackupRoute = AuthAccountBackupImport.update({
|
const AccountBackupRoute = AccountBackupImport.update({
|
||||||
path: '/$account/backup',
|
path: '/backup',
|
||||||
getParentRoute: () => AuthLazyRoute,
|
getParentRoute: () => AccountRoute,
|
||||||
} as any)
|
} as any)
|
||||||
|
|
||||||
// Populate the FileRoutesByPath interface
|
// Populate the FileRoutesByPath interface
|
||||||
@@ -268,6 +260,13 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof IndexImport
|
preLoaderRoute: typeof IndexImport
|
||||||
parentRoute: typeof rootRoute
|
parentRoute: typeof rootRoute
|
||||||
}
|
}
|
||||||
|
'/$account': {
|
||||||
|
id: '/$account'
|
||||||
|
path: '/$account'
|
||||||
|
fullPath: '/$account'
|
||||||
|
preLoaderRoute: typeof AccountImport
|
||||||
|
parentRoute: typeof rootRoute
|
||||||
|
}
|
||||||
'/bootstrap-relays': {
|
'/bootstrap-relays': {
|
||||||
id: '/bootstrap-relays'
|
id: '/bootstrap-relays'
|
||||||
path: '/bootstrap-relays'
|
path: '/bootstrap-relays'
|
||||||
@@ -359,40 +358,26 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof TrendingImport
|
preLoaderRoute: typeof TrendingImport
|
||||||
parentRoute: typeof rootRoute
|
parentRoute: typeof rootRoute
|
||||||
}
|
}
|
||||||
'/auth': {
|
'/new': {
|
||||||
id: '/auth'
|
id: '/new'
|
||||||
path: '/auth'
|
path: '/new'
|
||||||
fullPath: '/auth'
|
fullPath: '/new'
|
||||||
preLoaderRoute: typeof AuthLazyImport
|
preLoaderRoute: typeof NewLazyImport
|
||||||
parentRoute: typeof rootRoute
|
parentRoute: typeof rootRoute
|
||||||
}
|
}
|
||||||
'/landing': {
|
'/$account/backup': {
|
||||||
id: '/landing'
|
id: '/$account/backup'
|
||||||
path: '/landing'
|
path: '/backup'
|
||||||
fullPath: '/landing'
|
fullPath: '/$account/backup'
|
||||||
preLoaderRoute: typeof LandingLazyImport
|
preLoaderRoute: typeof AccountBackupImport
|
||||||
parentRoute: typeof rootRoute
|
parentRoute: typeof AccountImport
|
||||||
}
|
}
|
||||||
'/$account/home': {
|
'/$account/home': {
|
||||||
id: '/$account/home'
|
id: '/$account/home'
|
||||||
path: '/$account/home'
|
path: '/home'
|
||||||
fullPath: '/$account/home'
|
fullPath: '/$account/home'
|
||||||
preLoaderRoute: typeof AccountHomeImport
|
preLoaderRoute: typeof AccountHomeImport
|
||||||
parentRoute: typeof rootRoute
|
parentRoute: typeof AccountImport
|
||||||
}
|
|
||||||
'/$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
|
|
||||||
}
|
}
|
||||||
'/create-newsfeed/f2f': {
|
'/create-newsfeed/f2f': {
|
||||||
id: '/create-newsfeed/f2f'
|
id: '/create-newsfeed/f2f'
|
||||||
@@ -485,11 +470,11 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof TrendingUsersImport
|
preLoaderRoute: typeof TrendingUsersImport
|
||||||
parentRoute: typeof TrendingImport
|
parentRoute: typeof TrendingImport
|
||||||
}
|
}
|
||||||
'/users/$pubkey': {
|
'/users/$id': {
|
||||||
id: '/users/$pubkey'
|
id: '/users/$id'
|
||||||
path: '/users/$pubkey'
|
path: '/users/$id'
|
||||||
fullPath: '/users/$pubkey'
|
fullPath: '/users/$id'
|
||||||
preLoaderRoute: typeof UsersPubkeyImport
|
preLoaderRoute: typeof UsersIdImport
|
||||||
parentRoute: typeof rootRoute
|
parentRoute: typeof rootRoute
|
||||||
}
|
}
|
||||||
'/zap/$id': {
|
'/zap/$id': {
|
||||||
@@ -499,25 +484,32 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof ZapIdImport
|
preLoaderRoute: typeof ZapIdImport
|
||||||
parentRoute: typeof rootRoute
|
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': {
|
'/auth/import': {
|
||||||
id: '/auth/import'
|
id: '/auth/import'
|
||||||
path: '/import'
|
path: '/auth/import'
|
||||||
fullPath: '/auth/import'
|
fullPath: '/auth/import'
|
||||||
preLoaderRoute: typeof AuthImportLazyImport
|
preLoaderRoute: typeof AuthImportLazyImport
|
||||||
parentRoute: typeof AuthLazyImport
|
parentRoute: typeof rootRoute
|
||||||
}
|
}
|
||||||
'/auth/remote': {
|
'/auth/new': {
|
||||||
id: '/auth/remote'
|
id: '/auth/new'
|
||||||
path: '/remote'
|
path: '/auth/new'
|
||||||
fullPath: '/auth/remote'
|
fullPath: '/auth/new'
|
||||||
preLoaderRoute: typeof AuthRemoteLazyImport
|
preLoaderRoute: typeof AuthNewLazyImport
|
||||||
parentRoute: typeof AuthLazyImport
|
|
||||||
}
|
|
||||||
'/$account/': {
|
|
||||||
id: '/$account/'
|
|
||||||
path: '/$account'
|
|
||||||
fullPath: '/$account'
|
|
||||||
preLoaderRoute: typeof AccountIndexImport
|
|
||||||
parentRoute: typeof rootRoute
|
parentRoute: typeof rootRoute
|
||||||
}
|
}
|
||||||
'/editor/': {
|
'/editor/': {
|
||||||
@@ -527,13 +519,6 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof EditorIndexImport
|
preLoaderRoute: typeof EditorIndexImport
|
||||||
parentRoute: typeof rootRoute
|
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({
|
export const routeTree = rootRoute.addChildren({
|
||||||
IndexRoute,
|
IndexRoute,
|
||||||
|
AccountRoute: AccountRoute.addChildren({
|
||||||
|
AccountBackupRoute,
|
||||||
|
AccountHomeRoute,
|
||||||
|
AccountPanelLazyRoute,
|
||||||
|
}),
|
||||||
BootstrapRelaysRoute,
|
BootstrapRelaysRoute,
|
||||||
CreateGroupRoute,
|
CreateGroupRoute,
|
||||||
CreateNewsfeedRoute: CreateNewsfeedRoute.addChildren({
|
CreateNewsfeedRoute: CreateNewsfeedRoute.addChildren({
|
||||||
@@ -567,19 +557,13 @@ export const routeTree = rootRoute.addChildren({
|
|||||||
TrendingNotesRoute,
|
TrendingNotesRoute,
|
||||||
TrendingUsersRoute,
|
TrendingUsersRoute,
|
||||||
}),
|
}),
|
||||||
AuthLazyRoute: AuthLazyRoute.addChildren({
|
NewLazyRoute,
|
||||||
AuthCreateProfileRoute,
|
|
||||||
AuthImportLazyRoute,
|
|
||||||
AuthRemoteLazyRoute,
|
|
||||||
AuthAccountBackupRoute,
|
|
||||||
}),
|
|
||||||
LandingLazyRoute,
|
|
||||||
AccountHomeRoute,
|
|
||||||
AccountPanelRoute,
|
|
||||||
EventsIdRoute,
|
EventsIdRoute,
|
||||||
UsersPubkeyRoute,
|
UsersIdRoute,
|
||||||
ZapIdRoute,
|
ZapIdRoute,
|
||||||
AccountIndexRoute,
|
AuthConnectLazyRoute,
|
||||||
|
AuthImportLazyRoute,
|
||||||
|
AuthNewLazyRoute,
|
||||||
EditorIndexRoute,
|
EditorIndexRoute,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -592,6 +576,7 @@ export const routeTree = rootRoute.addChildren({
|
|||||||
"filePath": "__root.tsx",
|
"filePath": "__root.tsx",
|
||||||
"children": [
|
"children": [
|
||||||
"/",
|
"/",
|
||||||
|
"/$account",
|
||||||
"/bootstrap-relays",
|
"/bootstrap-relays",
|
||||||
"/create-group",
|
"/create-group",
|
||||||
"/create-newsfeed",
|
"/create-newsfeed",
|
||||||
@@ -605,20 +590,27 @@ export const routeTree = rootRoute.addChildren({
|
|||||||
"/store",
|
"/store",
|
||||||
"/topic",
|
"/topic",
|
||||||
"/trending",
|
"/trending",
|
||||||
"/auth",
|
"/new",
|
||||||
"/landing",
|
|
||||||
"/$account/home",
|
|
||||||
"/$account/panel",
|
|
||||||
"/events/$id",
|
"/events/$id",
|
||||||
"/users/$pubkey",
|
"/users/$id",
|
||||||
"/zap/$id",
|
"/zap/$id",
|
||||||
"/$account/",
|
"/auth/connect",
|
||||||
|
"/auth/import",
|
||||||
|
"/auth/new",
|
||||||
"/editor/"
|
"/editor/"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"/": {
|
"/": {
|
||||||
"filePath": "index.tsx"
|
"filePath": "index.tsx"
|
||||||
},
|
},
|
||||||
|
"/$account": {
|
||||||
|
"filePath": "$account.tsx",
|
||||||
|
"children": [
|
||||||
|
"/$account/backup",
|
||||||
|
"/$account/home",
|
||||||
|
"/$account/panel"
|
||||||
|
]
|
||||||
|
},
|
||||||
"/bootstrap-relays": {
|
"/bootstrap-relays": {
|
||||||
"filePath": "bootstrap-relays.tsx"
|
"filePath": "bootstrap-relays.tsx"
|
||||||
},
|
},
|
||||||
@@ -678,27 +670,16 @@ export const routeTree = rootRoute.addChildren({
|
|||||||
"/trending/users"
|
"/trending/users"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"/auth": {
|
"/new": {
|
||||||
"filePath": "auth.lazy.tsx",
|
"filePath": "new.lazy.tsx"
|
||||||
"children": [
|
|
||||||
"/auth/create-profile",
|
|
||||||
"/auth/import",
|
|
||||||
"/auth/remote",
|
|
||||||
"/auth/$account/backup"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"/landing": {
|
"/$account/backup": {
|
||||||
"filePath": "landing.lazy.tsx"
|
"filePath": "$account/backup.tsx",
|
||||||
|
"parent": "/$account"
|
||||||
},
|
},
|
||||||
"/$account/home": {
|
"/$account/home": {
|
||||||
"filePath": "$account/home.tsx"
|
"filePath": "$account/home.tsx",
|
||||||
},
|
"parent": "/$account"
|
||||||
"/$account/panel": {
|
|
||||||
"filePath": "$account/panel.tsx"
|
|
||||||
},
|
|
||||||
"/auth/create-profile": {
|
|
||||||
"filePath": "auth/create-profile.tsx",
|
|
||||||
"parent": "/auth"
|
|
||||||
},
|
},
|
||||||
"/create-newsfeed/f2f": {
|
"/create-newsfeed/f2f": {
|
||||||
"filePath": "create-newsfeed.f2f.tsx",
|
"filePath": "create-newsfeed.f2f.tsx",
|
||||||
@@ -751,29 +732,27 @@ export const routeTree = rootRoute.addChildren({
|
|||||||
"filePath": "trending.users.tsx",
|
"filePath": "trending.users.tsx",
|
||||||
"parent": "/trending"
|
"parent": "/trending"
|
||||||
},
|
},
|
||||||
"/users/$pubkey": {
|
"/users/$id": {
|
||||||
"filePath": "users/$pubkey.tsx"
|
"filePath": "users.$id.tsx"
|
||||||
},
|
},
|
||||||
"/zap/$id": {
|
"/zap/$id": {
|
||||||
"filePath": "zap.$id.tsx"
|
"filePath": "zap.$id.tsx"
|
||||||
},
|
},
|
||||||
|
"/$account/panel": {
|
||||||
|
"filePath": "$account/panel.lazy.tsx",
|
||||||
|
"parent": "/$account"
|
||||||
|
},
|
||||||
|
"/auth/connect": {
|
||||||
|
"filePath": "auth/connect.lazy.tsx"
|
||||||
|
},
|
||||||
"/auth/import": {
|
"/auth/import": {
|
||||||
"filePath": "auth/import.lazy.tsx",
|
"filePath": "auth/import.lazy.tsx"
|
||||||
"parent": "/auth"
|
|
||||||
},
|
},
|
||||||
"/auth/remote": {
|
"/auth/new": {
|
||||||
"filePath": "auth/remote.lazy.tsx",
|
"filePath": "auth/new.lazy.tsx"
|
||||||
"parent": "/auth"
|
|
||||||
},
|
|
||||||
"/$account/": {
|
|
||||||
"filePath": "$account/index.tsx"
|
|
||||||
},
|
},
|
||||||
"/editor/": {
|
"/editor/": {
|
||||||
"filePath": "editor/index.tsx"
|
"filePath": "editor/index.tsx"
|
||||||
},
|
|
||||||
"/auth/$account/backup": {
|
|
||||||
"filePath": "auth/$account.backup.tsx",
|
|
||||||
"parent": "/auth"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { getCurrentWindow } from "@tauri-apps/api/window";
|
|||||||
import { message } from "@tauri-apps/plugin-dialog";
|
import { message } from "@tauri-apps/plugin-dialog";
|
||||||
import { memo, useCallback, useState } from "react";
|
import { memo, useCallback, useState } from "react";
|
||||||
|
|
||||||
export const Route = createLazyFileRoute("/$account/")({
|
export const Route = createLazyFileRoute("/$account")({
|
||||||
component: Screen,
|
component: Screen,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { NostrAccount, NostrQuery } from "@/system";
|
import { NostrAccount, NostrQuery } from "@/system";
|
||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
|
||||||
export const Route = createFileRoute("/$account/")({
|
export const Route = createFileRoute("/$account")({
|
||||||
beforeLoad: async ({ params }) => {
|
beforeLoad: async ({ params }) => {
|
||||||
const settings = await NostrQuery.getUserSettings();
|
const settings = await NostrQuery.getUserSettings();
|
||||||
const accounts = await NostrAccount.getAccounts();
|
const accounts = await NostrAccount.getAccounts();
|
||||||
@@ -8,7 +8,7 @@ import { writeText } from "@tauri-apps/plugin-clipboard-manager";
|
|||||||
import { message } from "@tauri-apps/plugin-dialog";
|
import { message } from "@tauri-apps/plugin-dialog";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
export const Route = createFileRoute("/auth/$account/backup")({
|
export const Route = createFileRoute("/$account/backup")({
|
||||||
component: Screen,
|
component: Screen,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -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 (
|
|
||||||
<Container withDrag>
|
|
||||||
<div className="max-w-sm mx-auto size-full">
|
|
||||||
<Outlet />
|
|
||||||
</div>
|
|
||||||
</Container>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
105
src/routes/auth/connect.lazy.tsx
Normal file
105
src/routes/auth/connect.lazy.tsx
Normal file
@@ -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 (
|
||||||
|
<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">Nostr Connect</h1>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<Frame
|
||||||
|
className="flex flex-col gap-1 p-3 rounded-xl overflow-hidden"
|
||||||
|
shadow
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
htmlFor="uri"
|
||||||
|
className="font-medium text-neutral-900 dark:text-neutral-100"
|
||||||
|
>
|
||||||
|
Connection String
|
||||||
|
</label>
|
||||||
|
<div className="relative">
|
||||||
|
<input
|
||||||
|
name="uri"
|
||||||
|
type="text"
|
||||||
|
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"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => pasteFromClipboard()}
|
||||||
|
className="absolute top-1/2 right-2 transform -translate-y-1/2 text-xs font-semibold text-blue-500"
|
||||||
|
>
|
||||||
|
Paste
|
||||||
|
</button>
|
||||||
|
</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>
|
||||||
|
{isPending ? (
|
||||||
|
<p className="mt-2 text-sm text-center text-neutral-600 dark:text-neutral-400">
|
||||||
|
Waiting confirmation...
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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<string>("");
|
|
||||||
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 (
|
|
||||||
<div className="flex flex-col items-center justify-center size-full gap-4">
|
|
||||||
<div className="text-center">
|
|
||||||
<h3 className="text-xl font-semibold">Let's set up your profile.</h3>
|
|
||||||
</div>
|
|
||||||
<form onSubmit={handleSubmit(onSubmit)} className="w-full mb-0">
|
|
||||||
<div className="flex flex-col gap-3 w-full p-3 overflow-hidden bg-white rounded-xl shadow-primary dark:bg-white/10 dark:ring-1 ring-white/15">
|
|
||||||
<div className="self-center relative rounded-full size-20 bg-neutral-200 dark:bg-white/70 my-3">
|
|
||||||
{picture ? (
|
|
||||||
<img
|
|
||||||
src={picture}
|
|
||||||
alt="avatar"
|
|
||||||
loading="lazy"
|
|
||||||
decoding="async"
|
|
||||||
className="absolute inset-0 z-10 object-cover w-full h-full rounded-full"
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
<AvatarUploader
|
|
||||||
setPicture={setPicture}
|
|
||||||
className="absolute inset-0 z-20 flex items-center justify-center w-full h-full text-white rounded-full dark:text-black bg-black/10 hover:bg-black/20 dark:bg-white/10 dark:hover:bg-white/20"
|
|
||||||
>
|
|
||||||
<PlusIcon className="size-8" />
|
|
||||||
</AvatarUploader>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
<label htmlFor="display_name" className="font-medium">
|
|
||||||
Display Name *
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type={"text"}
|
|
||||||
{...register("display_name", { required: true, minLength: 1 })}
|
|
||||||
placeholder="e.g. Alice in Nostrland"
|
|
||||||
spellCheck={false}
|
|
||||||
className="px-3 border-transparent rounded-lg h-11 bg-neutral-100 placeholder:text-neutral-600 focus:border-blue-500 focus:ring-0 dark:bg-white/10 dark:placeholder:text-neutral-400"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
<label htmlFor="name" className="font-medium">
|
|
||||||
Name
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type={"text"}
|
|
||||||
{...register("name")}
|
|
||||||
placeholder="e.g. alice"
|
|
||||||
spellCheck={false}
|
|
||||||
className="px-3 border-transparent rounded-lg h-11 bg-neutral-100 placeholder:text-neutral-600 focus:border-blue-500 focus:ring-0 dark:bg-white/10 dark:placeholder:text-neutral-400"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
<label htmlFor="about" className="font-medium">
|
|
||||||
Bio
|
|
||||||
</label>
|
|
||||||
<textarea
|
|
||||||
{...register("about")}
|
|
||||||
placeholder="e.g. Artist, anime-lover, and k-pop fan"
|
|
||||||
spellCheck={false}
|
|
||||||
className="relative h-24 w-full resize-none rounded-lg border-transparent bg-neutral-100 px-3 py-2 !outline-none placeholder:text-neutral-600 focus:border-blue-500 focus:ring-0 dark:bg-white/10 dark:placeholder:text-neutral-400"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
<label htmlFor="website" className="font-medium">
|
|
||||||
Website
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="url"
|
|
||||||
{...register("website")}
|
|
||||||
placeholder="e.g. https://alice.me"
|
|
||||||
spellCheck={false}
|
|
||||||
className="px-3 border-transparent rounded-lg h-11 bg-neutral-100 placeholder:text-neutral-500 focus:border-blue-500 focus:ring-0 dark:bg-white/10 dark:placeholder:text-neutral-400"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
disabled={loading}
|
|
||||||
className="inline-flex items-center justify-center w-full h-9 mt-4 text-sm font-semibold text-white bg-blue-500 rounded-lg shrink-0 hover:bg-blue-600 disabled:opacity-50"
|
|
||||||
>
|
|
||||||
{loading ? <Spinner /> : "Continue"}
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
import { Spinner } from "@/components";
|
import { commands } from "@/commands.gen";
|
||||||
import { NostrAccount } from "@/system";
|
import { Frame, GoBack } from "@/components";
|
||||||
|
import { Spinner } from "@/components/spinner";
|
||||||
import { createLazyFileRoute } from "@tanstack/react-router";
|
import { createLazyFileRoute } from "@tanstack/react-router";
|
||||||
|
import { readText } from "@tauri-apps/plugin-clipboard-manager";
|
||||||
import { message } from "@tauri-apps/plugin-dialog";
|
import { message } from "@tauri-apps/plugin-dialog";
|
||||||
import { useState } from "react";
|
import { useState, useTransition } from "react";
|
||||||
|
|
||||||
export const Route = createLazyFileRoute("/auth/import")({
|
export const Route = createLazyFileRoute("/auth/import")({
|
||||||
component: Screen,
|
component: Screen,
|
||||||
@@ -13,77 +15,118 @@ function Screen() {
|
|||||||
|
|
||||||
const [key, setKey] = useState("");
|
const [key, setKey] = useState("");
|
||||||
const [password, setPassword] = useState("");
|
const [password, setPassword] = useState("");
|
||||||
const [loading, setLoading] = useState(false);
|
const [isPending, startTransition] = useTransition();
|
||||||
|
|
||||||
const submit = async () => {
|
const pasteFromClipboard = async () => {
|
||||||
if (!key.startsWith("nsec1")) {
|
const val = await readText();
|
||||||
return await message(
|
setKey(val);
|
||||||
"You need to enter a valid private key starts with nsec or ncryptsec",
|
};
|
||||||
{ title: "Import Key", kind: "info" },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
const submit = () => {
|
||||||
setLoading(true);
|
startTransition(async () => {
|
||||||
|
if (!key.startsWith("nsec1") && !key.startsWith("ncryptsec")) {
|
||||||
const npub = await NostrAccount.saveAccount(key, password);
|
await message(
|
||||||
|
"You need to enter a valid private key starts with nsec or ncryptsec",
|
||||||
if (npub) {
|
{ title: "Login", kind: "info" },
|
||||||
navigate({ to: "/", replace: true });
|
);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
|
||||||
setLoading(false);
|
if (key.startsWith("nsec1") && !password.length) {
|
||||||
await message(String(e), { title: "Import Key", kind: "error" });
|
await message("You must set password to secure your key", {
|
||||||
}
|
title: "Login",
|
||||||
|
kind: "info",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await commands.importAccount(key, password);
|
||||||
|
|
||||||
|
if (res.status === "ok") {
|
||||||
|
navigate({ to: "/", replace: true });
|
||||||
|
} else {
|
||||||
|
await message(res.error, {
|
||||||
|
title: "Import Private Ket",
|
||||||
|
kind: "error",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center justify-center size-full gap-4">
|
<div
|
||||||
<div className="text-center">
|
data-tauri-drag-region
|
||||||
<h3 className="text-xl font-semibold">Continue with Private Key</h3>
|
className="size-full flex items-center justify-center"
|
||||||
</div>
|
>
|
||||||
<div className="flex flex-col w-full">
|
<div className="w-[320px] flex flex-col gap-8">
|
||||||
<div className="flex flex-col gap-3 w-full p-3 overflow-hidden bg-white rounded-xl shadow-primary dark:bg-white/10 dark:ring-1 ring-white/15">
|
<div className="flex flex-col gap-1 text-center">
|
||||||
<div className="flex flex-col gap-1">
|
<h1 className="leading-tight text-xl font-semibold">
|
||||||
<label
|
Import Private Key
|
||||||
htmlFor="key"
|
</h1>
|
||||||
className="font-medium text-neutral-900 dark:text-neutral-100"
|
</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 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"
|
||||||
|
/>
|
||||||
|
<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>
|
||||||
|
{key.length && !key.startsWith("ncryptsec") ? (
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<label
|
||||||
|
htmlFor="password"
|
||||||
|
className="text-sm font-medium text-neutral-800 dark:text-neutral-200"
|
||||||
|
>
|
||||||
|
Set password to secure your key
|
||||||
|
</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>
|
||||||
|
) : null}
|
||||||
|
</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"
|
||||||
>
|
>
|
||||||
Private Key
|
{isPending ? <Spinner /> : "Continue"}
|
||||||
</label>
|
</button>
|
||||||
<input
|
<GoBack className="mt-2 w-full text-sm text-neutral-600 dark:text-neutral-400 inline-flex items-center justify-center">
|
||||||
name="key"
|
Go back to previous screen
|
||||||
type="text"
|
</GoBack>
|
||||||
placeholder="nsec or ncryptsec..."
|
|
||||||
value={key}
|
|
||||||
onChange={(e) => setKey(e.target.value)}
|
|
||||||
className="px-3 border-transparent rounded-lg h-11 bg-neutral-100 placeholder:text-neutral-600 focus:border-blue-500 focus:ring-0 dark:bg-white/10 dark:placeholder:text-neutral-400"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
<label
|
|
||||||
htmlFor="password"
|
|
||||||
className="font-medium text-neutral-900 dark:text-neutral-100"
|
|
||||||
>
|
|
||||||
Password (Optional)
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
name="password"
|
|
||||||
type="password"
|
|
||||||
value={password}
|
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
|
||||||
className="px-3 border-transparent rounded-lg h-11 bg-neutral-100 placeholder:text-neutral-600 focus:border-blue-500 focus:ring-0 dark:bg-white/10 dark:placeholder:text-neutral-400"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => submit()}
|
|
||||||
disabled={loading}
|
|
||||||
className="inline-flex items-center justify-center w-full h-9 mt-4 text-sm font-semibold text-white bg-blue-500 rounded-lg shrink-0 hover:bg-blue-600 disabled:opacity-50"
|
|
||||||
>
|
|
||||||
{loading ? <Spinner /> : "Login"}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
166
src/routes/auth/new.lazy.tsx
Normal file
166
src/routes/auth/new.lazy.tsx
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
import { commands } from "@/commands.gen";
|
||||||
|
import { Frame, GoBack, Spinner } from "@/components";
|
||||||
|
import { NostrQuery } from "@/system";
|
||||||
|
import { Plus } from "@phosphor-icons/react";
|
||||||
|
import { createLazyFileRoute } from "@tanstack/react-router";
|
||||||
|
import { message } from "@tauri-apps/plugin-dialog";
|
||||||
|
import { useState, useTransition } from "react";
|
||||||
|
|
||||||
|
export const Route = createLazyFileRoute("/auth/new")({
|
||||||
|
component: Screen,
|
||||||
|
});
|
||||||
|
|
||||||
|
function Screen() {
|
||||||
|
const navigate = Route.useNavigate();
|
||||||
|
|
||||||
|
const [password, setPassword] = useState("");
|
||||||
|
const [picture, setPicture] = useState<string>("");
|
||||||
|
const [name, setName] = useState("");
|
||||||
|
const [about, setAbout] = useState("");
|
||||||
|
const [isPending, startTransition] = useTransition();
|
||||||
|
|
||||||
|
const uploadAvatar = async () => {
|
||||||
|
const file = await NostrQuery.upload();
|
||||||
|
|
||||||
|
if (file) {
|
||||||
|
setPicture(file);
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const submit = () => {
|
||||||
|
startTransition(async () => {
|
||||||
|
if (!name.length) {
|
||||||
|
await message("Please add your name", {
|
||||||
|
title: "New Identity",
|
||||||
|
kind: "info",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!password.length) {
|
||||||
|
await message("You must set password to secure your account", {
|
||||||
|
title: "New Identity",
|
||||||
|
kind: "info",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await commands.createAccount(name, picture, about, password);
|
||||||
|
|
||||||
|
if (res.status === "ok") {
|
||||||
|
navigate({
|
||||||
|
to: "/",
|
||||||
|
replace: true,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await message(res.error, {
|
||||||
|
title: "New Identity",
|
||||||
|
kind: "error",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<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">New Identity</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="self-center relative rounded-full size-20 bg-neutral-100 dark:bg-neutral-900 my-3">
|
||||||
|
{picture.length ? (
|
||||||
|
<img
|
||||||
|
src={picture}
|
||||||
|
alt="avatar"
|
||||||
|
loading="lazy"
|
||||||
|
decoding="async"
|
||||||
|
className="absolute inset-0 z-10 object-cover w-full h-full rounded-full"
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => uploadAvatar()}
|
||||||
|
className="absolute inset-0 z-20 flex items-center justify-center w-full h-full rounded-full bg-black/10 hover:bg-black/20 dark:bg-white/10 dark:hover:bg-white/20"
|
||||||
|
>
|
||||||
|
<Plus className="size-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<label
|
||||||
|
htmlFor="name"
|
||||||
|
className="text-sm font-medium text-neutral-800 dark:text-neutral-200"
|
||||||
|
>
|
||||||
|
Name *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
name="name"
|
||||||
|
type="text"
|
||||||
|
value={name}
|
||||||
|
onChange={(e) => 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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<label
|
||||||
|
htmlFor="about"
|
||||||
|
className="text-sm font-medium text-neutral-800 dark:text-neutral-200"
|
||||||
|
>
|
||||||
|
About
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
name="about"
|
||||||
|
value={about}
|
||||||
|
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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="h-px w-full mt-2 bg-neutral-100 dark:bg-neutral-900" />
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<label
|
||||||
|
htmlFor="password"
|
||||||
|
className="text-sm font-medium text-neutral-800 dark:text-neutral-200"
|
||||||
|
>
|
||||||
|
Set password to secure your account *
|
||||||
|
</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 placeholder:text-neutral-400 dark:text-neutral-600"
|
||||||
|
/>
|
||||||
|
</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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
import { Spinner } from "@/components";
|
|
||||||
import { NostrAccount } from "@/system";
|
|
||||||
import { createLazyFileRoute } from "@tanstack/react-router";
|
|
||||||
import { message } from "@tauri-apps/plugin-dialog";
|
|
||||||
import { useState } from "react";
|
|
||||||
|
|
||||||
export const Route = createLazyFileRoute("/auth/remote")({
|
|
||||||
component: Screen,
|
|
||||||
});
|
|
||||||
|
|
||||||
function Screen() {
|
|
||||||
const navigate = Route.useNavigate();
|
|
||||||
|
|
||||||
const [uri, setUri] = useState("");
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
|
|
||||||
const submit = async () => {
|
|
||||||
if (!uri.startsWith("bunker://")) {
|
|
||||||
return await message(
|
|
||||||
"You need to enter a valid Connect URI starts with bunker://",
|
|
||||||
{ title: "Nostr Connect", kind: "info" },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
setLoading(true);
|
|
||||||
|
|
||||||
const remoteAccount = await NostrAccount.connectRemoteAccount(uri);
|
|
||||||
|
|
||||||
if (remoteAccount?.length) {
|
|
||||||
navigate({ to: "/", replace: true });
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
setLoading(false);
|
|
||||||
await message(String(e), { title: "Nostr Connect", kind: "error" });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col items-center justify-center size-full gap-4">
|
|
||||||
<div className="text-center">
|
|
||||||
<h3 className="text-xl font-semibold">Continue with Nostr Connect</h3>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col w-full">
|
|
||||||
<div className="flex flex-col gap-1 w-full p-3 overflow-hidden bg-white rounded-xl shadow-primary dark:bg-white/10 dark:ring-1 ring-white/15">
|
|
||||||
<label
|
|
||||||
htmlFor="uri"
|
|
||||||
className="font-medium text-neutral-900 dark:text-neutral-100"
|
|
||||||
>
|
|
||||||
Connect URI
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
name="uri"
|
|
||||||
type="text"
|
|
||||||
placeholder="bunker://..."
|
|
||||||
value={uri}
|
|
||||||
onChange={(e) => setUri(e.target.value)}
|
|
||||||
className="px-3 border-transparent rounded-lg h-11 bg-neutral-100 placeholder:text-neutral-600 focus:border-blue-500 focus:ring-0 dark:bg-white/10 dark:placeholder:text-neutral-400"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col items-center gap-1">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => submit()}
|
|
||||||
disabled={loading}
|
|
||||||
className="inline-flex items-center justify-center w-full h-9 mt-4 text-sm font-semibold text-white bg-blue-500 rounded-lg shrink-0 hover:bg-blue-600 disabled:opacity-50"
|
|
||||||
>
|
|
||||||
{loading ? <Spinner /> : "Login"}
|
|
||||||
</button>
|
|
||||||
{loading ? (
|
|
||||||
<p className="text-sm text-center text-neutral-600 dark:text-neutral-400">
|
|
||||||
Waiting confirmation...
|
|
||||||
</p>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
211
src/routes/index.lazy.tsx
Normal file
211
src/routes/index.lazy.tsx
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
import { commands } from "@/commands.gen";
|
||||||
|
import { displayNpub } from "@/commons";
|
||||||
|
import { Frame, Spinner, User } 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";
|
||||||
|
import { message } from "@tauri-apps/plugin-dialog";
|
||||||
|
import {
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
|
useTransition,
|
||||||
|
} from "react";
|
||||||
|
|
||||||
|
export const Route = createLazyFileRoute("/")({
|
||||||
|
component: Screen,
|
||||||
|
});
|
||||||
|
|
||||||
|
function Screen() {
|
||||||
|
const context = Route.useRouteContext();
|
||||||
|
const navigate = Route.useNavigate();
|
||||||
|
|
||||||
|
const currentDate = useMemo(
|
||||||
|
() =>
|
||||||
|
new Date().toLocaleString("default", {
|
||||||
|
weekday: "long",
|
||||||
|
month: "long",
|
||||||
|
day: "numeric",
|
||||||
|
}),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const [accounts, setAccounts] = useState([]);
|
||||||
|
const [value, setValue] = useState("");
|
||||||
|
const [autoLogin, setAutoLogin] = useState(false);
|
||||||
|
const [password, setPassword] = useState("");
|
||||||
|
const [isPending, startTransition] = useTransition();
|
||||||
|
|
||||||
|
const deleteAccount = async (account: string) => {
|
||||||
|
const res = await commands.deleteAccount(account);
|
||||||
|
|
||||||
|
if (res.status === "ok") {
|
||||||
|
setAccounts((prev) => prev.filter((item) => item !== account));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectAccount = (account: string) => {
|
||||||
|
setValue(account);
|
||||||
|
|
||||||
|
if (account.includes("_nostrconnect")) {
|
||||||
|
setAutoLogin(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const loginWith = () => {
|
||||||
|
startTransition(async () => {
|
||||||
|
const res = await commands.login(value, password);
|
||||||
|
|
||||||
|
if (res.status === "ok") {
|
||||||
|
navigate({
|
||||||
|
to: "/$account/home",
|
||||||
|
params: { account: res.data },
|
||||||
|
replace: true,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await message(res.error, { title: "Login", kind: "error" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const showContextMenu = useCallback(
|
||||||
|
async (e: React.MouseEvent, account: string) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
const menuItems = await Promise.all([
|
||||||
|
MenuItem.new({
|
||||||
|
text: "Delete account",
|
||||||
|
action: async () => await deleteAccount(account),
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const menu = await Menu.new({
|
||||||
|
items: menuItems,
|
||||||
|
});
|
||||||
|
|
||||||
|
await menu.popup().catch((e) => console.error(e));
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (autoLogin) {
|
||||||
|
loginWith();
|
||||||
|
}
|
||||||
|
}, [autoLogin, value]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setAccounts(context.accounts);
|
||||||
|
}, [context.accounts]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<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="flex flex-col gap-1 text-center">
|
||||||
|
<h3 className="leading-tight text-neutral-700 dark:text-neutral-300">
|
||||||
|
{currentDate}
|
||||||
|
</h3>
|
||||||
|
<h1 className="leading-tight text-xl font-semibold">Welcome back!</h1>
|
||||||
|
</div>
|
||||||
|
<Frame
|
||||||
|
className="flex flex-col w-full divide-y divide-neutral-100 dark:divide-white/5 rounded-xl overflow-hidden"
|
||||||
|
shadow
|
||||||
|
>
|
||||||
|
{accounts.map((account) => (
|
||||||
|
<div
|
||||||
|
key={account}
|
||||||
|
onClick={() => selectAccount(account)}
|
||||||
|
onKeyDown={() => selectAccount(account)}
|
||||||
|
className="group flex items-center gap-2 hover:bg-black/5 dark:hover:bg-white/5 p-3"
|
||||||
|
>
|
||||||
|
<User.Provider pubkey={account.replace("_nostrconnect", "")}>
|
||||||
|
<User.Root className="flex-1 flex items-center gap-2.5">
|
||||||
|
<User.Avatar className="rounded-full size-10" />
|
||||||
|
{value === account && !value.includes("_nostrconnect") ? (
|
||||||
|
<div className="flex-1 flex items-center gap-2">
|
||||||
|
<input
|
||||||
|
name="password"
|
||||||
|
type="password"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter") loginWith();
|
||||||
|
}}
|
||||||
|
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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<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" />
|
||||||
|
{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">
|
||||||
|
{displayNpub(account.replace("_nostrconnect", ""), 16)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</User.Root>
|
||||||
|
</User.Provider>
|
||||||
|
<div className="inline-flex items-center justify-center size-8 shrink-0">
|
||||||
|
{value === account ? (
|
||||||
|
isPending ? (
|
||||||
|
<Spinner />
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => loginWith()}
|
||||||
|
className="rounded-full size-10 inline-flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<ArrowRight className="size-5" />
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={(e) => showContextMenu(e, account)}
|
||||||
|
className="rounded-full size-10 hidden group-hover:inline-flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<DotsThree className="size-5" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<Link
|
||||||
|
to="/new"
|
||||||
|
className="flex items-center justify-between hover:bg-black/5 dark:hover:bg-white/5"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2.5 p-3">
|
||||||
|
<div className="inline-flex items-center justify-center rounded-full size-10 bg-neutral-200 dark:bg-white/10">
|
||||||
|
<Plus className="size-5" />
|
||||||
|
</div>
|
||||||
|
<span className="truncate text-sm font-medium leading-tight">
|
||||||
|
New account
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
</Frame>
|
||||||
|
</div>
|
||||||
|
<div className="absolute bottom-2 right-2">
|
||||||
|
<Link
|
||||||
|
to="/bootstrap-relays"
|
||||||
|
className="h-8 w-max text-xs px-3 inline-flex items-center justify-center gap-1.5 bg-black/5 hover:bg-black/10 dark:bg-white/5 dark:hover:bg-white/10 rounded-full"
|
||||||
|
>
|
||||||
|
<GearSix className="size-4" />
|
||||||
|
Manage Relays
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,140 +1,22 @@
|
|||||||
import { checkForAppUpdates, displayNpub } from "@/commons";
|
import { checkForAppUpdates } from "@/commons";
|
||||||
import { Spinner } from "@/components";
|
|
||||||
import { PlusIcon, RelayIcon } from "@/components";
|
|
||||||
import { User } from "@/components/user";
|
|
||||||
import { NostrAccount } from "@/system";
|
import { NostrAccount } from "@/system";
|
||||||
import { Link } from "@tanstack/react-router";
|
|
||||||
import { createFileRoute, redirect } from "@tanstack/react-router";
|
import { createFileRoute, redirect } from "@tanstack/react-router";
|
||||||
import { message } from "@tauri-apps/plugin-dialog";
|
|
||||||
import { useState } from "react";
|
|
||||||
|
|
||||||
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);
|
||||||
|
|
||||||
// Get all accounts
|
// Get all accounts
|
||||||
// TODO: use emit & listen
|
|
||||||
const accounts = await NostrAccount.getAccounts();
|
const accounts = await NostrAccount.getAccounts();
|
||||||
|
|
||||||
if (accounts.length < 1) {
|
if (accounts.length < 1) {
|
||||||
throw redirect({
|
throw redirect({
|
||||||
to: "/landing",
|
to: "/new",
|
||||||
replace: true,
|
replace: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return { accounts };
|
return { accounts };
|
||||||
},
|
},
|
||||||
component: Screen,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
function Screen() {
|
|
||||||
const navigate = Route.useNavigate();
|
|
||||||
const context = Route.useRouteContext();
|
|
||||||
|
|
||||||
const [loading, setLoading] = useState({ npub: "", status: false });
|
|
||||||
|
|
||||||
const select = async (npub: string) => {
|
|
||||||
try {
|
|
||||||
setLoading({ npub, status: true });
|
|
||||||
|
|
||||||
const status = await NostrAccount.loadAccount(npub);
|
|
||||||
|
|
||||||
if (status) {
|
|
||||||
return navigate({
|
|
||||||
to: "/$account/home",
|
|
||||||
params: { account: npub },
|
|
||||||
replace: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
setLoading({ npub: "", status: false });
|
|
||||||
await message(String(e), {
|
|
||||||
title: "Account",
|
|
||||||
kind: "error",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const currentDate = new Date().toLocaleString("default", {
|
|
||||||
weekday: "long",
|
|
||||||
month: "long",
|
|
||||||
day: "numeric",
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-tauri-drag-region
|
|
||||||
className="relative flex flex-col items-center justify-between w-full h-full"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
data-tauri-drag-region
|
|
||||||
className="absolute top-0 left-0 h-14 w-full"
|
|
||||||
/>
|
|
||||||
<div className="flex items-end justify-center flex-1 w-full px-4 pb-10">
|
|
||||||
<div className="text-center">
|
|
||||||
<h2 className="mb-1 text-lg text-neutral-700 dark:text-neutral-300">
|
|
||||||
{currentDate}
|
|
||||||
</h2>
|
|
||||||
<h2 className="text-2xl font-semibold">Welcome back!</h2>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col items-center flex-1 w-full gap-3">
|
|
||||||
<div className="flex flex-col w-full max-w-sm mx-auto overflow-hidden bg-white divide-y divide-neutral-100 dark:divide-white/5 rounded-xl shadow-primary dark:bg-white/10 dark:ring-1 ring-white/15">
|
|
||||||
{context.accounts.map((account) => (
|
|
||||||
<div
|
|
||||||
key={account}
|
|
||||||
onClick={() => select(account)}
|
|
||||||
onKeyDown={() => select(account)}
|
|
||||||
className="flex items-center justify-between hover:bg-black/5 dark:hover:bg-white/5"
|
|
||||||
>
|
|
||||||
<User.Provider pubkey={account}>
|
|
||||||
<User.Root className="flex items-center gap-2.5 p-3">
|
|
||||||
<User.Avatar className="rounded-full size-10" />
|
|
||||||
<div className="inline-flex flex-col items-start">
|
|
||||||
<User.Name className="max-w-[6rem] truncate font-medium leading-tight" />
|
|
||||||
<span className="text-sm text-neutral-700 dark:text-neutral-300">
|
|
||||||
{displayNpub(account, 16)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</User.Root>
|
|
||||||
</User.Provider>
|
|
||||||
<div className="inline-flex items-center justify-center size-10">
|
|
||||||
{loading.npub === account ? (
|
|
||||||
loading.status ? (
|
|
||||||
<Spinner />
|
|
||||||
) : null
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
<Link
|
|
||||||
to="/landing"
|
|
||||||
className="flex items-center justify-between hover:bg-black/5 dark:hover:bg-white/5"
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-2.5 p-3">
|
|
||||||
<div className="inline-flex items-center justify-center rounded-full size-10 bg-neutral-200 dark:bg-white/10">
|
|
||||||
<PlusIcon className="size-5" />
|
|
||||||
</div>
|
|
||||||
<span className="max-w-[6rem] truncate text-sm font-medium leading-tight">
|
|
||||||
Add account
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
<div className="w-full max-w-sm mx-auto">
|
|
||||||
<Link
|
|
||||||
to="/bootstrap-relays"
|
|
||||||
className="inline-flex items-center justify-center w-full h-8 gap-2 px-2 text-xs font-medium rounded-lg bg-black/5 dark:bg-white/5 hover:bg-black/10 dark:hover:bg-white/10 text-neutral-700 dark:text-white/40"
|
|
||||||
>
|
|
||||||
<RelayIcon className="size-4" />
|
|
||||||
Custom Bootstrap Relays
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex-1" />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,62 +0,0 @@
|
|||||||
import { KeyIcon, RemoteIcon } from "@/components";
|
|
||||||
import { Link, createLazyFileRoute } from "@tanstack/react-router";
|
|
||||||
|
|
||||||
export const Route = createLazyFileRoute("/landing")({
|
|
||||||
component: Screen,
|
|
||||||
});
|
|
||||||
|
|
||||||
function Screen() {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-tauri-drag-region
|
|
||||||
className="flex flex-col items-center justify-center w-screen h-screen"
|
|
||||||
>
|
|
||||||
<div className="w-full max-w-xs mx-auto lg:max-w-md">
|
|
||||||
<div className="flex flex-col w-full gap-2 px-2 bg-white rounded-xl shadow-primary dark:bg-white/20 dark:ring-1 ring-neutral-800/50">
|
|
||||||
<div className="flex items-center h-20 border-b border-neutral-100 dark:border-white/5">
|
|
||||||
<Link
|
|
||||||
to="/auth/create-profile"
|
|
||||||
className="flex items-center justify-center w-full gap-2 px-2 rounded-lg h-14 hover:bg-neutral-100 dark:hover:bg-white/10"
|
|
||||||
>
|
|
||||||
<div className="inline-flex items-center justify-center rounded-full size-9 shrink-0">
|
|
||||||
<img
|
|
||||||
src="/icon.jpeg"
|
|
||||||
alt="App Icon"
|
|
||||||
className="object-cover rounded-full size-9"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="inline-flex flex-col flex-1">
|
|
||||||
<span className="font-semibold leading-tight">
|
|
||||||
Create new account
|
|
||||||
</span>
|
|
||||||
<span className="text-sm leading-tight text-neutral-500">
|
|
||||||
Use everywhere
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-1 pb-2.5">
|
|
||||||
<Link
|
|
||||||
to="/auth/import"
|
|
||||||
className="inline-flex items-center w-full gap-2 px-2 rounded-lg h-11 hover:bg-neutral-100 dark:hover:bg-white/10"
|
|
||||||
>
|
|
||||||
<div className="inline-flex items-center justify-center size-9">
|
|
||||||
<KeyIcon className="size-5 text-neutral-600 dark:text-neutral-400" />
|
|
||||||
</div>
|
|
||||||
Login with Private Key
|
|
||||||
</Link>
|
|
||||||
<Link
|
|
||||||
to="/auth/remote"
|
|
||||||
className="inline-flex items-center w-full gap-2 px-2 rounded-lg h-11 hover:bg-neutral-100 dark:hover:bg-white/10"
|
|
||||||
>
|
|
||||||
<div className="inline-flex items-center justify-center size-9">
|
|
||||||
<RemoteIcon className="size-5 text-neutral-600 dark:text-neutral-400" />
|
|
||||||
</div>
|
|
||||||
Nostr Connect
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
45
src/routes/new.lazy.tsx
Normal file
45
src/routes/new.lazy.tsx
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { Link, createLazyFileRoute } from "@tanstack/react-router";
|
||||||
|
|
||||||
|
export const Route = createLazyFileRoute("/new")({
|
||||||
|
component: Screen,
|
||||||
|
});
|
||||||
|
|
||||||
|
function Screen() {
|
||||||
|
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">
|
||||||
|
Welcome to Nostr.
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<Link
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
Create a new identity
|
||||||
|
</Link>
|
||||||
|
<div className="w-full h-px bg-black/5 dark:bg-white/5" />
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Link
|
||||||
|
to="/auth/connect"
|
||||||
|
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 Nostr Connect
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
Login with Private Key
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -11,19 +11,19 @@ import { Await } from "@tanstack/react-router";
|
|||||||
import { Suspense, useCallback } from "react";
|
import { Suspense, useCallback } from "react";
|
||||||
import { WindowVirtualizer } from "virtua";
|
import { WindowVirtualizer } from "virtua";
|
||||||
|
|
||||||
export const Route = createFileRoute("/users/$pubkey")({
|
export const Route = createFileRoute("/users/$id")({
|
||||||
beforeLoad: async () => {
|
beforeLoad: async () => {
|
||||||
const settings = await NostrQuery.getUserSettings();
|
const settings = await NostrQuery.getUserSettings();
|
||||||
return { settings };
|
return { settings };
|
||||||
},
|
},
|
||||||
loader: async ({ params }) => {
|
loader: async ({ params }) => {
|
||||||
return { data: defer(NostrQuery.getUserEvents(params.pubkey)) };
|
return { data: defer(NostrQuery.getUserEvents(params.id)) };
|
||||||
},
|
},
|
||||||
component: Screen,
|
component: Screen,
|
||||||
});
|
});
|
||||||
|
|
||||||
function Screen() {
|
function Screen() {
|
||||||
const { pubkey } = Route.useParams();
|
const { id } = Route.useParams();
|
||||||
const { data } = Route.useLoaderData();
|
const { data } = Route.useLoaderData();
|
||||||
|
|
||||||
const renderItem = useCallback(
|
const renderItem = useCallback(
|
||||||
@@ -52,7 +52,7 @@ function Screen() {
|
|||||||
<Container withDrag>
|
<Container withDrag>
|
||||||
<Box className="px-0 scrollbar-none bg-black/5 dark:bg-white/5">
|
<Box className="px-0 scrollbar-none bg-black/5 dark:bg-white/5">
|
||||||
<WindowVirtualizer>
|
<WindowVirtualizer>
|
||||||
<User.Provider pubkey={pubkey}>
|
<User.Provider pubkey={id}>
|
||||||
<User.Root>
|
<User.Root>
|
||||||
<User.Cover className="object-cover w-full h-44" />
|
<User.Cover className="object-cover w-full h-44" />
|
||||||
<div className="relative flex flex-col px-3 -mt-8">
|
<div className="relative flex flex-col px-3 -mt-8">
|
||||||
@@ -1,14 +1,11 @@
|
|||||||
/** @type {import('tailwindcss').Config} */
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
|
||||||
import harmonyPalette from "@evilmartians/harmony/tailwind";
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
content: [
|
content: [
|
||||||
"./src/**/*.{js,ts,jsx,tsx}",
|
"./src/**/*.{js,ts,jsx,tsx}",
|
||||||
"index.html",
|
"index.html",
|
||||||
],
|
],
|
||||||
theme: {
|
theme: {
|
||||||
colors: harmonyPalette,
|
|
||||||
extend: {
|
extend: {
|
||||||
keyframes: {
|
keyframes: {
|
||||||
slideDownAndFade: {
|
slideDownAndFade: {
|
||||||
|
|||||||
Reference in New Issue
Block a user