feat: add auth screens
This commit is contained in:
@@ -1,9 +1,8 @@
|
|||||||
use itertools::Itertools;
|
|
||||||
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::Serialize;
|
||||||
use std::collections::HashSet;
|
use std::{collections::HashSet, time::Duration};
|
||||||
use tauri::{Emitter, Manager, State};
|
use tauri::{Emitter, Manager, State};
|
||||||
|
|
||||||
use crate::Nostr;
|
use crate::Nostr;
|
||||||
@@ -28,7 +27,7 @@ pub fn get_accounts() -> Vec<String> {
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
#[specta::specta]
|
#[specta::specta]
|
||||||
pub async fn get_profile(id: String, state: State<'_, Nostr>) -> Result<String, String> {
|
pub async fn get_metadata(id: String, state: State<'_, Nostr>) -> Result<String, String> {
|
||||||
let client = &state.client;
|
let client = &state.client;
|
||||||
let public_key = PublicKey::parse(&id).map_err(|e| e.to_string())?;
|
let public_key = PublicKey::parse(&id).map_err(|e| e.to_string())?;
|
||||||
let filter = Filter::new().author(public_key).kind(Kind::Metadata).limit(1);
|
let filter = Filter::new().author(public_key).kind(Kind::Metadata).limit(1);
|
||||||
@@ -45,10 +44,108 @@ pub async fn get_profile(id: String, state: State<'_, Nostr>) -> Result<String,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
#[specta::specta]
|
||||||
|
pub async fn create_account(
|
||||||
|
name: String,
|
||||||
|
picture: String,
|
||||||
|
state: State<'_, Nostr>,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let client = &state.client;
|
||||||
|
let keys = Keys::generate();
|
||||||
|
let npub = keys.public_key().to_bech32().map_err(|e| e.to_string())?;
|
||||||
|
let nsec = keys.secret_key().unwrap().to_bech32().map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
// Save account
|
||||||
|
let keyring = Entry::new(&npub, "nostr_secret").unwrap();
|
||||||
|
let _ = keyring.set_password(&nsec);
|
||||||
|
|
||||||
|
let signer = NostrSigner::Keys(keys);
|
||||||
|
|
||||||
|
// Update signer
|
||||||
|
client.set_signer(Some(signer)).await;
|
||||||
|
|
||||||
|
// Update metadata
|
||||||
|
let url = Url::parse(&picture).map_err(|e| e.to_string())?;
|
||||||
|
let metadata = Metadata::new().display_name(name).picture(url);
|
||||||
|
|
||||||
|
match client.set_metadata(&metadata).await {
|
||||||
|
Ok(_) => Ok(()),
|
||||||
|
Err(e) => Err(e.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
#[specta::specta]
|
||||||
|
pub async fn import_key(
|
||||||
|
nsec: &str,
|
||||||
|
password: &str,
|
||||||
|
state: State<'_, Nostr>,
|
||||||
|
) -> Result<String, String> {
|
||||||
|
let secret_key = if nsec.starts_with("ncryptsec") {
|
||||||
|
let encrypted_key = EncryptedSecretKey::from_bech32(nsec).unwrap();
|
||||||
|
encrypted_key.to_secret_key(password).map_err(|err| err.to_string())
|
||||||
|
} else {
|
||||||
|
SecretKey::from_bech32(nsec).map_err(|err| err.to_string())
|
||||||
|
};
|
||||||
|
|
||||||
|
match secret_key {
|
||||||
|
Ok(val) => {
|
||||||
|
let nostr_keys = Keys::new(val);
|
||||||
|
let npub = nostr_keys.public_key().to_bech32().unwrap();
|
||||||
|
let nsec = nostr_keys.secret_key().unwrap().to_bech32().unwrap();
|
||||||
|
|
||||||
|
let keyring = Entry::new(&npub, "nostr_secret").unwrap();
|
||||||
|
let _ = keyring.set_password(&nsec);
|
||||||
|
|
||||||
|
let signer = NostrSigner::Keys(nostr_keys);
|
||||||
|
let client = &state.client;
|
||||||
|
|
||||||
|
// Update client's signer
|
||||||
|
client.set_signer(Some(signer)).await;
|
||||||
|
|
||||||
|
Ok(npub)
|
||||||
|
}
|
||||||
|
Err(msg) => Err(msg),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
#[specta::specta]
|
||||||
|
pub async fn connect_account(uri: &str, state: State<'_, Nostr>) -> Result<String, String> {
|
||||||
|
let client = &state.client;
|
||||||
|
|
||||||
|
match NostrConnectURI::parse(uri) {
|
||||||
|
Ok(bunker_uri) => {
|
||||||
|
let app_keys = Keys::generate();
|
||||||
|
let app_secret = app_keys.secret_key().unwrap().to_string();
|
||||||
|
|
||||||
|
// Get remote user
|
||||||
|
let remote_user = bunker_uri.signer_public_key().unwrap();
|
||||||
|
let remote_npub = remote_user.to_bech32().unwrap();
|
||||||
|
|
||||||
|
match Nip46Signer::new(bunker_uri, app_keys, Duration::from_secs(120), None).await {
|
||||||
|
Ok(signer) => {
|
||||||
|
let keyring = Entry::new(&remote_npub, "nostr_secret").unwrap();
|
||||||
|
let _ = keyring.set_password(&app_secret);
|
||||||
|
|
||||||
|
// Update signer
|
||||||
|
let _ = client.set_signer(Some(signer.into())).await;
|
||||||
|
|
||||||
|
Ok(remote_npub)
|
||||||
|
}
|
||||||
|
Err(err) => Err(err.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => Err(err.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
#[specta::specta]
|
#[specta::specta]
|
||||||
pub async fn login(
|
pub async fn login(
|
||||||
id: String,
|
id: String,
|
||||||
|
bunker: Option<String>,
|
||||||
state: State<'_, Nostr>,
|
state: State<'_, Nostr>,
|
||||||
handle: tauri::AppHandle,
|
handle: tauri::AppHandle,
|
||||||
) -> Result<String, String> {
|
) -> Result<String, String> {
|
||||||
@@ -61,12 +158,33 @@ pub async fn login(
|
|||||||
Err(_) => return Err("Cancelled".into()),
|
Err(_) => return Err("Cancelled".into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let keys = Keys::parse(password).expect("Secret Key is modified, please check again.");
|
match bunker {
|
||||||
let signer = NostrSigner::Keys(keys);
|
Some(uri) => {
|
||||||
|
let app_keys =
|
||||||
|
Keys::parse(password).expect("Secret Key is modified, please check again.");
|
||||||
|
|
||||||
// Set signer
|
match NostrConnectURI::parse(uri) {
|
||||||
client.set_signer(Some(signer)).await;
|
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 => {
|
||||||
|
let keys = Keys::parse(password).expect("Secret Key is modified, please check again.");
|
||||||
|
let signer = NostrSigner::Keys(keys);
|
||||||
|
|
||||||
|
// Update signer
|
||||||
|
client.set_signer(Some(signer)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let hex = public_key.to_hex();
|
||||||
let inbox = Filter::new().kind(Kind::Custom(10050)).author(public_key).limit(1);
|
let inbox = Filter::new().kind(Kind::Custom(10050)).author(public_key).limit(1);
|
||||||
|
|
||||||
if let Ok(events) = client.get_events_of(vec![inbox], None).await {
|
if let Ok(events) = client.get_events_of(vec![inbox], None).await {
|
||||||
@@ -92,33 +210,11 @@ pub async fn login(
|
|||||||
let state = window.state::<Nostr>();
|
let state = window.state::<Nostr>();
|
||||||
let client = &state.client;
|
let client = &state.client;
|
||||||
|
|
||||||
let old = Filter::new().kind(Kind::GiftWrap).pubkey(public_key).until(Timestamp::now());
|
let old = Filter::new().kind(Kind::GiftWrap).pubkey(public_key);
|
||||||
let new = Filter::new().kind(Kind::GiftWrap).pubkey(public_key).limit(0);
|
let new = Filter::new().kind(Kind::GiftWrap).pubkey(public_key).limit(0);
|
||||||
|
|
||||||
if let Ok(report) = client.reconcile(old, NegentropyOptions::default()).await {
|
if client.reconcile(old, NegentropyOptions::default()).await.is_ok() {
|
||||||
let receives = report.received.clone();
|
println!("Sync done.")
|
||||||
let ids = receives.into_iter().collect::<Vec<_>>();
|
|
||||||
|
|
||||||
if let Ok(events) =
|
|
||||||
client.database().query(vec![Filter::new().ids(ids)], Order::Desc).await
|
|
||||||
{
|
|
||||||
let pubkeys = events
|
|
||||||
.into_iter()
|
|
||||||
.unique_by(|ev| ev.pubkey)
|
|
||||||
.map(|ev| ev.pubkey)
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
if client
|
|
||||||
.reconcile(
|
|
||||||
Filter::new().kind(Kind::GiftWrap).pubkeys(pubkeys),
|
|
||||||
NegentropyOptions::default(),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.is_ok()
|
|
||||||
{
|
|
||||||
println!("Sync done.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if client.subscribe(vec![new], None).await.is_ok() {
|
if client.subscribe(vec![new], None).await.is_ok() {
|
||||||
@@ -169,7 +265,5 @@ pub async fn login(
|
|||||||
.await
|
.await
|
||||||
});
|
});
|
||||||
|
|
||||||
let hex = public_key.to_hex();
|
|
||||||
|
|
||||||
Ok(hex)
|
Ok(hex)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,13 +8,7 @@ use std::{fs, sync::Mutex, time::Duration};
|
|||||||
use tauri::Manager;
|
use tauri::Manager;
|
||||||
use tauri_plugin_decorum::WebviewWindowExt;
|
use tauri_plugin_decorum::WebviewWindowExt;
|
||||||
|
|
||||||
use commands::{
|
use commands::{account::*, chat::*};
|
||||||
account::{get_accounts, get_profile, login},
|
|
||||||
chat::{
|
|
||||||
drop_inbox, get_chat_messages, get_chats, get_inboxes, send_message, subscribe_to,
|
|
||||||
unsubscribe,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
mod commands;
|
mod commands;
|
||||||
mod common;
|
mod common;
|
||||||
@@ -30,8 +24,11 @@ fn main() {
|
|||||||
let invoke_handler = {
|
let invoke_handler = {
|
||||||
let builder = tauri_specta::ts::builder().commands(tauri_specta::collect_commands![
|
let builder = tauri_specta::ts::builder().commands(tauri_specta::collect_commands![
|
||||||
login,
|
login,
|
||||||
|
create_account,
|
||||||
|
import_key,
|
||||||
|
connect_account,
|
||||||
get_accounts,
|
get_accounts,
|
||||||
get_profile,
|
get_metadata,
|
||||||
get_inboxes,
|
get_inboxes,
|
||||||
get_chats,
|
get_chats,
|
||||||
get_chat_messages,
|
get_chat_messages,
|
||||||
|
|||||||
@@ -4,9 +4,33 @@
|
|||||||
/** user-defined commands **/
|
/** user-defined commands **/
|
||||||
|
|
||||||
export const commands = {
|
export const commands = {
|
||||||
async login(id: string) : Promise<Result<string, string>> {
|
async login(id: string, bunker: string | null) : Promise<Result<string, string>> {
|
||||||
try {
|
try {
|
||||||
return { status: "ok", data: await TAURI_INVOKE("login", { id }) };
|
return { status: "ok", data: await TAURI_INVOKE("login", { id, bunker }) };
|
||||||
|
} catch (e) {
|
||||||
|
if(e instanceof Error) throw e;
|
||||||
|
else return { status: "error", error: e as any };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async createAccount(name: string, picture: string) : Promise<Result<null, string>> {
|
||||||
|
try {
|
||||||
|
return { status: "ok", data: await TAURI_INVOKE("create_account", { name, picture }) };
|
||||||
|
} catch (e) {
|
||||||
|
if(e instanceof Error) throw e;
|
||||||
|
else return { status: "error", error: e as any };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async importKey(nsec: string, password: string) : Promise<Result<string, string>> {
|
||||||
|
try {
|
||||||
|
return { status: "ok", data: await TAURI_INVOKE("import_key", { nsec, password }) };
|
||||||
|
} catch (e) {
|
||||||
|
if(e instanceof Error) throw e;
|
||||||
|
else return { status: "error", error: e as any };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async connectAccount(uri: string) : Promise<Result<string, string>> {
|
||||||
|
try {
|
||||||
|
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 };
|
||||||
@@ -15,9 +39,9 @@ try {
|
|||||||
async getAccounts() : Promise<string[]> {
|
async getAccounts() : Promise<string[]> {
|
||||||
return await TAURI_INVOKE("get_accounts");
|
return await TAURI_INVOKE("get_accounts");
|
||||||
},
|
},
|
||||||
async getProfile(id: string) : Promise<Result<string, string>> {
|
async getMetadata(id: string) : Promise<Result<string, string>> {
|
||||||
try {
|
try {
|
||||||
return { status: "ok", data: await TAURI_INVOKE("get_profile", { id }) };
|
return { status: "ok", data: await TAURI_INVOKE("get_metadata", { 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 };
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export function UserProvider({
|
|||||||
.replace("nostr:", "")
|
.replace("nostr:", "")
|
||||||
.replace(/[^\w\s]/gi, "");
|
.replace(/[^\w\s]/gi, "");
|
||||||
|
|
||||||
const query: string = await invoke("get_profile", {
|
const query: string = await invoke("get_metadata", {
|
||||||
id: normalizePubkey,
|
id: normalizePubkey,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -14,34 +14,58 @@ import { createFileRoute } from '@tanstack/react-router'
|
|||||||
|
|
||||||
import { Route as rootRoute } from './routes/__root'
|
import { Route as rootRoute } from './routes/__root'
|
||||||
import { Route as IndexImport } from './routes/index'
|
import { Route as IndexImport } from './routes/index'
|
||||||
import { Route as AccountChatsImport } from './routes/$account.chats'
|
|
||||||
import { Route as AccountChatsIdImport } from './routes/$account.chats.$id'
|
|
||||||
|
|
||||||
// Create Virtual Routes
|
// Create Virtual Routes
|
||||||
|
|
||||||
|
const NostrConnectLazyImport = createFileRoute('/nostr-connect')()
|
||||||
const NewLazyImport = createFileRoute('/new')()
|
const NewLazyImport = createFileRoute('/new')()
|
||||||
|
const ImportKeyLazyImport = createFileRoute('/import-key')()
|
||||||
|
const CreateAccountLazyImport = createFileRoute('/create-account')()
|
||||||
|
const AccountChatsLazyImport = createFileRoute('/$account/chats')()
|
||||||
|
const AccountChatsIdLazyImport = createFileRoute('/$account/chats/$id')()
|
||||||
|
|
||||||
// Create/Update Routes
|
// Create/Update Routes
|
||||||
|
|
||||||
|
const NostrConnectLazyRoute = NostrConnectLazyImport.update({
|
||||||
|
path: '/nostr-connect',
|
||||||
|
getParentRoute: () => rootRoute,
|
||||||
|
} as any).lazy(() => import('./routes/nostr-connect.lazy').then((d) => d.Route))
|
||||||
|
|
||||||
const NewLazyRoute = NewLazyImport.update({
|
const NewLazyRoute = NewLazyImport.update({
|
||||||
path: '/new',
|
path: '/new',
|
||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => rootRoute,
|
||||||
} as any).lazy(() => import('./routes/new.lazy').then((d) => d.Route))
|
} as any).lazy(() => import('./routes/new.lazy').then((d) => d.Route))
|
||||||
|
|
||||||
|
const ImportKeyLazyRoute = ImportKeyLazyImport.update({
|
||||||
|
path: '/import-key',
|
||||||
|
getParentRoute: () => rootRoute,
|
||||||
|
} as any).lazy(() => import('./routes/import-key.lazy').then((d) => d.Route))
|
||||||
|
|
||||||
|
const CreateAccountLazyRoute = CreateAccountLazyImport.update({
|
||||||
|
path: '/create-account',
|
||||||
|
getParentRoute: () => rootRoute,
|
||||||
|
} as any).lazy(() =>
|
||||||
|
import('./routes/create-account.lazy').then((d) => d.Route),
|
||||||
|
)
|
||||||
|
|
||||||
const IndexRoute = IndexImport.update({
|
const IndexRoute = IndexImport.update({
|
||||||
path: '/',
|
path: '/',
|
||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => rootRoute,
|
||||||
} as any)
|
} as any)
|
||||||
|
|
||||||
const AccountChatsRoute = AccountChatsImport.update({
|
const AccountChatsLazyRoute = AccountChatsLazyImport.update({
|
||||||
path: '/$account/chats',
|
path: '/$account/chats',
|
||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => rootRoute,
|
||||||
} as any)
|
} as any).lazy(() =>
|
||||||
|
import('./routes/$account.chats.lazy').then((d) => d.Route),
|
||||||
|
)
|
||||||
|
|
||||||
const AccountChatsIdRoute = AccountChatsIdImport.update({
|
const AccountChatsIdLazyRoute = AccountChatsIdLazyImport.update({
|
||||||
path: '/$id',
|
path: '/$id',
|
||||||
getParentRoute: () => AccountChatsRoute,
|
getParentRoute: () => AccountChatsLazyRoute,
|
||||||
} as any)
|
} as any).lazy(() =>
|
||||||
|
import('./routes/$account.chats.$id.lazy').then((d) => d.Route),
|
||||||
|
)
|
||||||
|
|
||||||
// Populate the FileRoutesByPath interface
|
// Populate the FileRoutesByPath interface
|
||||||
|
|
||||||
@@ -54,6 +78,20 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof IndexImport
|
preLoaderRoute: typeof IndexImport
|
||||||
parentRoute: typeof rootRoute
|
parentRoute: typeof rootRoute
|
||||||
}
|
}
|
||||||
|
'/create-account': {
|
||||||
|
id: '/create-account'
|
||||||
|
path: '/create-account'
|
||||||
|
fullPath: '/create-account'
|
||||||
|
preLoaderRoute: typeof CreateAccountLazyImport
|
||||||
|
parentRoute: typeof rootRoute
|
||||||
|
}
|
||||||
|
'/import-key': {
|
||||||
|
id: '/import-key'
|
||||||
|
path: '/import-key'
|
||||||
|
fullPath: '/import-key'
|
||||||
|
preLoaderRoute: typeof ImportKeyLazyImport
|
||||||
|
parentRoute: typeof rootRoute
|
||||||
|
}
|
||||||
'/new': {
|
'/new': {
|
||||||
id: '/new'
|
id: '/new'
|
||||||
path: '/new'
|
path: '/new'
|
||||||
@@ -61,19 +99,26 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof NewLazyImport
|
preLoaderRoute: typeof NewLazyImport
|
||||||
parentRoute: typeof rootRoute
|
parentRoute: typeof rootRoute
|
||||||
}
|
}
|
||||||
|
'/nostr-connect': {
|
||||||
|
id: '/nostr-connect'
|
||||||
|
path: '/nostr-connect'
|
||||||
|
fullPath: '/nostr-connect'
|
||||||
|
preLoaderRoute: typeof NostrConnectLazyImport
|
||||||
|
parentRoute: typeof rootRoute
|
||||||
|
}
|
||||||
'/$account/chats': {
|
'/$account/chats': {
|
||||||
id: '/$account/chats'
|
id: '/$account/chats'
|
||||||
path: '/$account/chats'
|
path: '/$account/chats'
|
||||||
fullPath: '/$account/chats'
|
fullPath: '/$account/chats'
|
||||||
preLoaderRoute: typeof AccountChatsImport
|
preLoaderRoute: typeof AccountChatsLazyImport
|
||||||
parentRoute: typeof rootRoute
|
parentRoute: typeof rootRoute
|
||||||
}
|
}
|
||||||
'/$account/chats/$id': {
|
'/$account/chats/$id': {
|
||||||
id: '/$account/chats/$id'
|
id: '/$account/chats/$id'
|
||||||
path: '/$id'
|
path: '/$id'
|
||||||
fullPath: '/$account/chats/$id'
|
fullPath: '/$account/chats/$id'
|
||||||
preLoaderRoute: typeof AccountChatsIdImport
|
preLoaderRoute: typeof AccountChatsIdLazyImport
|
||||||
parentRoute: typeof AccountChatsImport
|
parentRoute: typeof AccountChatsLazyImport
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -82,8 +127,13 @@ declare module '@tanstack/react-router' {
|
|||||||
|
|
||||||
export const routeTree = rootRoute.addChildren({
|
export const routeTree = rootRoute.addChildren({
|
||||||
IndexRoute,
|
IndexRoute,
|
||||||
|
CreateAccountLazyRoute,
|
||||||
|
ImportKeyLazyRoute,
|
||||||
NewLazyRoute,
|
NewLazyRoute,
|
||||||
AccountChatsRoute: AccountChatsRoute.addChildren({ AccountChatsIdRoute }),
|
NostrConnectLazyRoute,
|
||||||
|
AccountChatsLazyRoute: AccountChatsLazyRoute.addChildren({
|
||||||
|
AccountChatsIdLazyRoute,
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
/* prettier-ignore-end */
|
/* prettier-ignore-end */
|
||||||
@@ -95,24 +145,36 @@ export const routeTree = rootRoute.addChildren({
|
|||||||
"filePath": "__root.tsx",
|
"filePath": "__root.tsx",
|
||||||
"children": [
|
"children": [
|
||||||
"/",
|
"/",
|
||||||
|
"/create-account",
|
||||||
|
"/import-key",
|
||||||
"/new",
|
"/new",
|
||||||
|
"/nostr-connect",
|
||||||
"/$account/chats"
|
"/$account/chats"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"/": {
|
"/": {
|
||||||
"filePath": "index.tsx"
|
"filePath": "index.tsx"
|
||||||
},
|
},
|
||||||
|
"/create-account": {
|
||||||
|
"filePath": "create-account.lazy.tsx"
|
||||||
|
},
|
||||||
|
"/import-key": {
|
||||||
|
"filePath": "import-key.lazy.tsx"
|
||||||
|
},
|
||||||
"/new": {
|
"/new": {
|
||||||
"filePath": "new.lazy.tsx"
|
"filePath": "new.lazy.tsx"
|
||||||
},
|
},
|
||||||
|
"/nostr-connect": {
|
||||||
|
"filePath": "nostr-connect.lazy.tsx"
|
||||||
|
},
|
||||||
"/$account/chats": {
|
"/$account/chats": {
|
||||||
"filePath": "$account.chats.tsx",
|
"filePath": "$account.chats.lazy.tsx",
|
||||||
"children": [
|
"children": [
|
||||||
"/$account/chats/$id"
|
"/$account/chats/$id"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"/$account/chats/$id": {
|
"/$account/chats/$id": {
|
||||||
"filePath": "$account.chats.$id.tsx",
|
"filePath": "$account.chats.$id.lazy.tsx",
|
||||||
"parent": "/$account/chats"
|
"parent": "/$account/chats"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { Spinner } from "@/components/spinner";
|
|||||||
import { ArrowUp, CloudArrowUp, Paperclip } from "@phosphor-icons/react";
|
import { ArrowUp, CloudArrowUp, Paperclip } from "@phosphor-icons/react";
|
||||||
import * as ScrollArea from "@radix-ui/react-scroll-area";
|
import * as ScrollArea from "@radix-ui/react-scroll-area";
|
||||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createLazyFileRoute } from "@tanstack/react-router";
|
||||||
import { listen } from "@tauri-apps/api/event";
|
import { listen } from "@tauri-apps/api/event";
|
||||||
import { message } from "@tauri-apps/plugin-dialog";
|
import { message } from "@tauri-apps/plugin-dialog";
|
||||||
import type { NostrEvent } from "nostr-tools";
|
import type { NostrEvent } from "nostr-tools";
|
||||||
@@ -17,7 +17,7 @@ type Payload = {
|
|||||||
sender: string;
|
sender: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Route = createFileRoute("/$account/chats/$id")({
|
export const Route = createLazyFileRoute("/$account/chats/$id")({
|
||||||
component: Screen,
|
component: Screen,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -4,7 +4,7 @@ import { User } from "@/components/user";
|
|||||||
import { Plus, UsersThree } from "@phosphor-icons/react";
|
import { Plus, UsersThree } from "@phosphor-icons/react";
|
||||||
import * as ScrollArea from "@radix-ui/react-scroll-area";
|
import * as ScrollArea from "@radix-ui/react-scroll-area";
|
||||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { Link, Outlet, createFileRoute } from "@tanstack/react-router";
|
import { Link, Outlet, createLazyFileRoute } from "@tanstack/react-router";
|
||||||
import { listen } from "@tauri-apps/api/event";
|
import { listen } from "@tauri-apps/api/event";
|
||||||
import type { NostrEvent } from "nostr-tools";
|
import type { NostrEvent } from "nostr-tools";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
@@ -14,7 +14,7 @@ type Payload = {
|
|||||||
sender: string;
|
sender: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Route = createFileRoute("/$account/chats")({
|
export const Route = createLazyFileRoute("/$account/chats")({
|
||||||
component: Screen,
|
component: Screen,
|
||||||
});
|
});
|
||||||
|
|
||||||
94
src/routes/create-account.lazy.tsx
Normal file
94
src/routes/create-account.lazy.tsx
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
import { commands } from "@/commands";
|
||||||
|
import { Frame } from "@/components/frame";
|
||||||
|
import { Spinner } from "@/components/spinner";
|
||||||
|
import { createLazyFileRoute } from "@tanstack/react-router";
|
||||||
|
import { message } from "@tauri-apps/plugin-dialog";
|
||||||
|
import { useState, useTransition } from "react";
|
||||||
|
|
||||||
|
export const Route = createLazyFileRoute("/create-account")({
|
||||||
|
component: Screen,
|
||||||
|
});
|
||||||
|
|
||||||
|
function Screen() {
|
||||||
|
const navigate = Route.useNavigate();
|
||||||
|
|
||||||
|
const [picture, setPicture] = useState("");
|
||||||
|
const [name, setName] = useState("");
|
||||||
|
const [isPending, startTransition] = useTransition();
|
||||||
|
|
||||||
|
const submit = async () => {
|
||||||
|
startTransition(async () => {
|
||||||
|
const res = await commands.createAccount(name, picture);
|
||||||
|
|
||||||
|
if (res.status === "ok") {
|
||||||
|
navigate({ to: "/", replace: true });
|
||||||
|
} else {
|
||||||
|
await message(res.error, {
|
||||||
|
title: "New Identity",
|
||||||
|
kind: "error",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div 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">
|
||||||
|
Import Private Key
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<Frame
|
||||||
|
className="flex flex-col gap-3 p-3 rounded-xl overflow-hidden"
|
||||||
|
shadow
|
||||||
|
>
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<label
|
||||||
|
htmlFor="avatar"
|
||||||
|
className="font-medium text-neutral-900 dark:text-neutral-100"
|
||||||
|
>
|
||||||
|
Avatar
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
name="avatar"
|
||||||
|
type="text"
|
||||||
|
placeholder="https://"
|
||||||
|
value={picture}
|
||||||
|
onChange={(e) => setPicture(e.target.value)}
|
||||||
|
className="px-3 rounded-lg h-10 bg-transparent border border-neutral-200 dark:border-neutral-800 focus:border-blue-500 focus:outline-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<label
|
||||||
|
htmlFor="name"
|
||||||
|
className="font-medium text-neutral-900 dark:text-neutral-100"
|
||||||
|
>
|
||||||
|
Name
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
name="name"
|
||||||
|
type="text"
|
||||||
|
value={name}
|
||||||
|
onChange={(e) => setName(e.target.value)}
|
||||||
|
className="px-3 rounded-lg h-10 bg-transparent border border-neutral-200 dark:border-neutral-800 focus:border-blue-500 focus:outline-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Frame>
|
||||||
|
<div className="flex flex-col items-center gap-1">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => submit()}
|
||||||
|
disabled={isPending}
|
||||||
|
className="inline-flex items-center justify-center w-full h-10 text-sm font-semibold text-white bg-blue-500 rounded-lg shrink-0 hover:bg-blue-600 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{isPending ? <Spinner /> : "Continue"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
102
src/routes/import-key.lazy.tsx
Normal file
102
src/routes/import-key.lazy.tsx
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
import { commands } from "@/commands";
|
||||||
|
import { Frame } from "@/components/frame";
|
||||||
|
import { Spinner } from "@/components/spinner";
|
||||||
|
import { createLazyFileRoute } from "@tanstack/react-router";
|
||||||
|
import { message } from "@tauri-apps/plugin-dialog";
|
||||||
|
import { useState, useTransition } from "react";
|
||||||
|
|
||||||
|
export const Route = createLazyFileRoute("/import-key")({
|
||||||
|
component: Screen,
|
||||||
|
});
|
||||||
|
|
||||||
|
function Screen() {
|
||||||
|
const navigate = Route.useNavigate();
|
||||||
|
|
||||||
|
const [key, setKey] = useState("");
|
||||||
|
const [password, setPassword] = useState("");
|
||||||
|
const [isPending, startTransition] = useTransition();
|
||||||
|
|
||||||
|
const submit = async () => {
|
||||||
|
startTransition(async () => {
|
||||||
|
if (!key.startsWith("nsec1")) {
|
||||||
|
await message(
|
||||||
|
"You need to enter a valid private key starts with nsec or ncryptsec",
|
||||||
|
{ title: "Import Key", kind: "info" },
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await commands.importKey(key, password);
|
||||||
|
|
||||||
|
if (res.status === "ok") {
|
||||||
|
navigate({ to: "/", replace: true });
|
||||||
|
} else {
|
||||||
|
await message(res.error, {
|
||||||
|
title: "Import Private Ket",
|
||||||
|
kind: "error",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div 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">
|
||||||
|
Import Private Key
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<Frame
|
||||||
|
className="flex flex-col gap-3 p-3 rounded-xl overflow-hidden"
|
||||||
|
shadow
|
||||||
|
>
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<label
|
||||||
|
htmlFor="key"
|
||||||
|
className="font-medium text-neutral-900 dark:text-neutral-100"
|
||||||
|
>
|
||||||
|
Private Key
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
name="key"
|
||||||
|
type="text"
|
||||||
|
placeholder="nsec or ncryptsec..."
|
||||||
|
value={key}
|
||||||
|
onChange={(e) => setKey(e.target.value)}
|
||||||
|
className="px-3 rounded-lg h-10 bg-transparent border border-neutral-200 dark:border-neutral-800 focus:border-blue-500 focus:outline-none"
|
||||||
|
/>
|
||||||
|
</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 rounded-lg h-10 bg-transparent border border-neutral-200 dark:border-neutral-800 focus:border-blue-500 focus:outline-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Frame>
|
||||||
|
<div className="flex flex-col items-center gap-1">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => submit()}
|
||||||
|
disabled={isPending}
|
||||||
|
className="inline-flex items-center justify-center w-full h-10 text-sm font-semibold text-white bg-blue-500 rounded-lg shrink-0 hover:bg-blue-600 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{isPending ? <Spinner /> : "Continue"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -43,7 +43,9 @@ function Screen() {
|
|||||||
const loginWith = async (npub: string) => {
|
const loginWith = async (npub: string) => {
|
||||||
setValue(npub);
|
setValue(npub);
|
||||||
startTransition(async () => {
|
startTransition(async () => {
|
||||||
const res = await commands.login(npub);
|
const bunker: string = localStorage.getItem(`${npub}_bunker`);
|
||||||
|
const verifyBunker = bunker?.length && bunker?.startsWith("bunker://");
|
||||||
|
const res = await commands.login(npub, verifyBunker ? bunker : null);
|
||||||
|
|
||||||
if (res.status === "ok") {
|
if (res.status === "ok") {
|
||||||
navigate({
|
navigate({
|
||||||
|
|||||||
@@ -1,5 +1,39 @@
|
|||||||
import { createLazyFileRoute } from '@tanstack/react-router'
|
import { createLazyFileRoute, Link } from "@tanstack/react-router";
|
||||||
|
|
||||||
export const Route = createLazyFileRoute('/new')({
|
export const Route = createLazyFileRoute("/new")({
|
||||||
component: () => <div>Hello /new!</div>
|
component: Screen,
|
||||||
})
|
});
|
||||||
|
|
||||||
|
function Screen() {
|
||||||
|
return (
|
||||||
|
<div 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">
|
||||||
|
Direct Message client for Nostr.
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<Link
|
||||||
|
to="/create-account"
|
||||||
|
className="w-full h-10 bg-blue-500 hover:bg-blue-600 text-white rounded-lg inline-flex items-center justify-center shadow"
|
||||||
|
>
|
||||||
|
Create a new identity
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
to="/nostr-connect"
|
||||||
|
className="w-full h-10 bg-white hover:bg-neutral-100 dark:hover:bg-neutral-950 dark:bg-neutral-900 rounded-lg inline-flex items-center justify-center"
|
||||||
|
>
|
||||||
|
Login with Nostr Connect
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
to="/import-key"
|
||||||
|
className="w-full text-sm text-neutral-600 dark:text-neutral-400 inline-flex items-center justify-center"
|
||||||
|
>
|
||||||
|
Login with Private Key (not recommended)
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
93
src/routes/nostr-connect.lazy.tsx
Normal file
93
src/routes/nostr-connect.lazy.tsx
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
import { commands } from "@/commands";
|
||||||
|
import { Frame } from "@/components/frame";
|
||||||
|
import { Spinner } from "@/components/spinner";
|
||||||
|
import { createLazyFileRoute } from "@tanstack/react-router";
|
||||||
|
import { message } from "@tauri-apps/plugin-dialog";
|
||||||
|
import { useState, useTransition } from "react";
|
||||||
|
|
||||||
|
export const Route = createLazyFileRoute("/nostr-connect")({
|
||||||
|
component: Screen,
|
||||||
|
});
|
||||||
|
|
||||||
|
function Screen() {
|
||||||
|
const navigate = Route.useNavigate();
|
||||||
|
|
||||||
|
const [uri, setUri] = useState("");
|
||||||
|
const [isPending, startTransition] = useTransition();
|
||||||
|
|
||||||
|
const submit = async () => {
|
||||||
|
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") {
|
||||||
|
const npub = res.data;
|
||||||
|
const parsed = new URL(uri);
|
||||||
|
parsed.searchParams.delete("secret");
|
||||||
|
|
||||||
|
// save connection string
|
||||||
|
localStorage.setItem(`${npub}_bunker`, parsed.toString());
|
||||||
|
|
||||||
|
navigate({ to: "/", replace: true });
|
||||||
|
} else {
|
||||||
|
await message(res.error, { title: "Nostr Connect", kind: "error" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div 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>
|
||||||
|
<input
|
||||||
|
name="uri"
|
||||||
|
type="text"
|
||||||
|
placeholder="bunker://..."
|
||||||
|
value={uri}
|
||||||
|
onChange={(e) => setUri(e.target.value)}
|
||||||
|
className="px-3 rounded-lg h-10 bg-transparent border border-neutral-200 dark:border-neutral-800 focus:border-blue-500 focus:outline-none"
|
||||||
|
/>
|
||||||
|
</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-10 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="text-sm text-center text-neutral-600 dark:text-neutral-400">
|
||||||
|
Waiting confirmation...
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user