feat: add relay manager
This commit is contained in:
4
src-tauri/resources/relays.txt
Normal file
4
src-tauri/resources/relays.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
wss://purplepag.es/,
|
||||
wss://directory.yabu.me/,
|
||||
wss://user.kindpag.es/,
|
||||
wss://relay.nos.social/,
|
||||
@@ -5,7 +5,7 @@ use serde::Serialize;
|
||||
use std::{collections::HashSet, str::FromStr, time::Duration};
|
||||
use tauri::{Emitter, Manager, State};
|
||||
|
||||
use crate::{Nostr, BOOTSTRAP_RELAYS};
|
||||
use crate::Nostr;
|
||||
|
||||
#[derive(Clone, Serialize)]
|
||||
pub struct EventPayload {
|
||||
@@ -32,8 +32,7 @@ pub async fn get_metadata(id: String, state: State<'_, Nostr>) -> Result<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);
|
||||
|
||||
match client.get_events_from(BOOTSTRAP_RELAYS, vec![filter], Some(Duration::from_secs(3))).await
|
||||
{
|
||||
match client.get_events_of(vec![filter], Some(Duration::from_secs(3))).await {
|
||||
Ok(events) => {
|
||||
if let Some(event) = events.first() {
|
||||
Ok(Metadata::from_json(&event.content).unwrap_or(Metadata::new()).as_json())
|
||||
@@ -171,51 +170,6 @@ pub async fn get_contact_list(state: State<'_, Nostr>) -> Result<Vec<String>, St
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn get_inbox(id: String, state: State<'_, Nostr>) -> Result<Vec<String>, String> {
|
||||
let client = &state.client;
|
||||
let public_key = PublicKey::parse(id).map_err(|e| e.to_string())?;
|
||||
let inbox = Filter::new().kind(Kind::Custom(10050)).author(public_key).limit(1);
|
||||
|
||||
match client.get_events_from(BOOTSTRAP_RELAYS, vec![inbox], None).await {
|
||||
Ok(events) => {
|
||||
if let Some(event) = events.into_iter().next() {
|
||||
let urls = event
|
||||
.tags()
|
||||
.iter()
|
||||
.filter_map(|tag| {
|
||||
if let Some(TagStandard::Relay(relay)) = tag.as_standardized() {
|
||||
Some(relay.to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Ok(urls)
|
||||
} else {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
}
|
||||
Err(e) => Err(e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn set_inbox(relays: Vec<String>, state: State<'_, Nostr>) -> Result<(), String> {
|
||||
let client = &state.client;
|
||||
|
||||
let tags = relays.into_iter().map(|t| Tag::custom(TagKind::Relay, vec![t])).collect::<Vec<_>>();
|
||||
let event = EventBuilder::new(Kind::Custom(10050), "", tags);
|
||||
|
||||
match client.send_event_builder(event).await {
|
||||
Ok(_) => Ok(()),
|
||||
Err(e) => Err(e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn login(
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
pub mod account;
|
||||
pub mod chat;
|
||||
pub mod relay;
|
||||
|
||||
82
src-tauri/src/commands/relay.rs
Normal file
82
src-tauri/src/commands/relay.rs
Normal file
@@ -0,0 +1,82 @@
|
||||
use nostr_sdk::prelude::*;
|
||||
use std::{
|
||||
fs::OpenOptions,
|
||||
io::{self, BufRead, Write},
|
||||
};
|
||||
use tauri::{Manager, State};
|
||||
|
||||
use crate::Nostr;
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn get_bootstrap_relays(app: tauri::AppHandle) -> Result<Vec<String>, String> {
|
||||
let relays_path = app
|
||||
.path()
|
||||
.resolve("resources/relays.txt", tauri::path::BaseDirectory::Resource)
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let file = std::fs::File::open(relays_path).map_err(|e| e.to_string())?;
|
||||
let reader = io::BufReader::new(file);
|
||||
|
||||
reader.lines().collect::<Result<Vec<String>, io::Error>>().map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn set_bootstrap_relays(relays: String, app: tauri::AppHandle) -> Result<(), String> {
|
||||
let relays_path = app
|
||||
.path()
|
||||
.resolve("resources/relays.txt", tauri::path::BaseDirectory::Resource)
|
||||
.map_err(|e| e.to_string())?;
|
||||
let mut file = OpenOptions::new().write(true).open(relays_path).map_err(|e| e.to_string())?;
|
||||
|
||||
file.write_all(relays.as_bytes()).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn get_inbox_relays(
|
||||
user_id: String,
|
||||
state: State<'_, Nostr>,
|
||||
) -> Result<Vec<String>, String> {
|
||||
let client = &state.client;
|
||||
let public_key = PublicKey::parse(user_id).map_err(|e| e.to_string())?;
|
||||
let inbox = Filter::new().kind(Kind::Custom(10050)).author(public_key).limit(1);
|
||||
|
||||
match client.get_events_of(vec![inbox], None).await {
|
||||
Ok(events) => {
|
||||
if let Some(event) = events.into_iter().next() {
|
||||
let urls = event
|
||||
.tags()
|
||||
.iter()
|
||||
.filter_map(|tag| {
|
||||
if let Some(TagStandard::Relay(relay)) = tag.as_standardized() {
|
||||
Some(relay.to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Ok(urls)
|
||||
} else {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
}
|
||||
Err(e) => Err(e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn set_inbox_relays(relays: Vec<String>, state: State<'_, Nostr>) -> Result<(), String> {
|
||||
let client = &state.client;
|
||||
|
||||
let tags = relays.into_iter().map(|t| Tag::custom(TagKind::Relay, vec![t])).collect::<Vec<_>>();
|
||||
let event = EventBuilder::new(Kind::Custom(10050), "", tags);
|
||||
|
||||
match client.send_event_builder(event).await {
|
||||
Ok(_) => Ok(()),
|
||||
Err(e) => Err(e.to_string()),
|
||||
}
|
||||
}
|
||||
@@ -4,12 +4,18 @@
|
||||
#[cfg(target_os = "macos")]
|
||||
use border::WebviewWindowExt as WebviewWindowExtAlt;
|
||||
use nostr_sdk::prelude::*;
|
||||
use std::{collections::HashMap, fs, time::Duration};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fs,
|
||||
io::{self, BufRead},
|
||||
str::FromStr,
|
||||
time::Duration,
|
||||
};
|
||||
use tauri::{async_runtime::Mutex, Manager};
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
use tauri_plugin_decorum::WebviewWindowExt;
|
||||
|
||||
use commands::{account::*, chat::*};
|
||||
use commands::{account::*, chat::*, relay::*};
|
||||
|
||||
mod commands;
|
||||
|
||||
@@ -18,12 +24,13 @@ pub struct Nostr {
|
||||
inbox_relays: Mutex<HashMap<PublicKey, Vec<String>>>,
|
||||
}
|
||||
|
||||
// TODO: Allow user config bootstrap relays.
|
||||
pub const BOOTSTRAP_RELAYS: [&str; 2] = ["wss://relay.damus.io/", "wss://relay.nostr.net/"];
|
||||
|
||||
fn main() {
|
||||
let invoke_handler = {
|
||||
let builder = tauri_specta::ts::builder().commands(tauri_specta::collect_commands![
|
||||
get_bootstrap_relays,
|
||||
set_bootstrap_relays,
|
||||
get_inbox_relays,
|
||||
set_inbox_relays,
|
||||
login,
|
||||
delete_account,
|
||||
create_account,
|
||||
@@ -34,8 +41,6 @@ fn main() {
|
||||
get_contact_list,
|
||||
get_chats,
|
||||
get_chat_messages,
|
||||
get_inbox,
|
||||
set_inbox,
|
||||
connect_inbox,
|
||||
disconnect_inbox,
|
||||
send_message,
|
||||
@@ -102,8 +107,35 @@ fn main() {
|
||||
|
||||
let client = ClientBuilder::default().opts(opts).database(database).build();
|
||||
|
||||
// Add bootstrap relay
|
||||
let _ = client.add_relays(BOOTSTRAP_RELAYS).await;
|
||||
// Add bootstrap relays
|
||||
if let Ok(path) = handle
|
||||
.path()
|
||||
.resolve("resources/relays.txt", tauri::path::BaseDirectory::Resource)
|
||||
{
|
||||
let file = std::fs::File::open(&path).unwrap();
|
||||
let lines = io::BufReader::new(file).lines();
|
||||
|
||||
// Add bootstrap relays to relay pool
|
||||
for line in lines.map_while(Result::ok) {
|
||||
if let Some((relay, option)) = line.split_once(',') {
|
||||
match RelayMetadata::from_str(option) {
|
||||
Ok(meta) => {
|
||||
println!("Connecting to relay...: {} - {}", relay, meta);
|
||||
let opts = if meta == RelayMetadata::Read {
|
||||
RelayOptions::new().read(true).write(false)
|
||||
} else {
|
||||
RelayOptions::new().write(true).read(false)
|
||||
};
|
||||
let _ = client.add_relay_with_opts(relay, opts).await;
|
||||
}
|
||||
Err(_) => {
|
||||
println!("Connecting to relay...: {}", relay);
|
||||
let _ = client.add_relay(relay).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Connect
|
||||
client.connect().await;
|
||||
|
||||
@@ -38,6 +38,9 @@
|
||||
"targets": "all",
|
||||
"active": true,
|
||||
"category": "SocialNetworking",
|
||||
"resources": [
|
||||
"resources/*"
|
||||
],
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
|
||||
@@ -4,6 +4,38 @@
|
||||
/** user-defined commands **/
|
||||
|
||||
export const commands = {
|
||||
async getBootstrapRelays() : Promise<Result<string[], string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("get_bootstrap_relays") };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async setBootstrapRelays(relays: string) : Promise<Result<null, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("set_bootstrap_relays", { relays }) };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async getInboxRelays(userId: string) : Promise<Result<string[], string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("get_inbox_relays", { userId }) };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async setInboxRelays(relays: string[]) : Promise<Result<null, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("set_inbox_relays", { relays }) };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async login(account: string, password: string) : Promise<Result<string, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("login", { account, password }) };
|
||||
@@ -79,22 +111,6 @@ try {
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async getInbox(id: string) : Promise<Result<string[], string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("get_inbox", { id }) };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async setInbox(relays: string[]) : Promise<Result<null, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("set_inbox", { relays }) };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async connectInbox(id: string) : Promise<Result<string[], string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("connect_inbox", { id }) };
|
||||
|
||||
@@ -13,7 +13,9 @@ import { createFileRoute } from '@tanstack/react-router'
|
||||
// Import Routes
|
||||
|
||||
import { Route as rootRoute } from './routes/__root'
|
||||
import { Route as BootstrapRelaysImport } from './routes/bootstrap-relays'
|
||||
import { Route as IndexImport } from './routes/index'
|
||||
import { Route as AccountRelaysImport } from './routes/$account.relays'
|
||||
import { Route as AccountContactsImport } from './routes/$account.contacts'
|
||||
import { Route as AccountChatsIdImport } from './routes/$account.chats.$id'
|
||||
|
||||
@@ -23,7 +25,6 @@ const NostrConnectLazyImport = createFileRoute('/nostr-connect')()
|
||||
const NewLazyImport = createFileRoute('/new')()
|
||||
const ImportKeyLazyImport = createFileRoute('/import-key')()
|
||||
const CreateAccountLazyImport = createFileRoute('/create-account')()
|
||||
const AccountRelaysLazyImport = createFileRoute('/$account/relays')()
|
||||
const AccountChatsLazyImport = createFileRoute('/$account/chats')()
|
||||
const AccountChatsNewLazyImport = createFileRoute('/$account/chats/new')()
|
||||
|
||||
@@ -51,18 +52,18 @@ const CreateAccountLazyRoute = CreateAccountLazyImport.update({
|
||||
import('./routes/create-account.lazy').then((d) => d.Route),
|
||||
)
|
||||
|
||||
const BootstrapRelaysRoute = BootstrapRelaysImport.update({
|
||||
path: '/bootstrap-relays',
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any).lazy(() =>
|
||||
import('./routes/bootstrap-relays.lazy').then((d) => d.Route),
|
||||
)
|
||||
|
||||
const IndexRoute = IndexImport.update({
|
||||
path: '/',
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any).lazy(() => import('./routes/index.lazy').then((d) => d.Route))
|
||||
|
||||
const AccountRelaysLazyRoute = AccountRelaysLazyImport.update({
|
||||
path: '/$account/relays',
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any).lazy(() =>
|
||||
import('./routes/$account.relays.lazy').then((d) => d.Route),
|
||||
)
|
||||
|
||||
const AccountChatsLazyRoute = AccountChatsLazyImport.update({
|
||||
path: '/$account/chats',
|
||||
getParentRoute: () => rootRoute,
|
||||
@@ -70,6 +71,13 @@ const AccountChatsLazyRoute = AccountChatsLazyImport.update({
|
||||
import('./routes/$account.chats.lazy').then((d) => d.Route),
|
||||
)
|
||||
|
||||
const AccountRelaysRoute = AccountRelaysImport.update({
|
||||
path: '/$account/relays',
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any).lazy(() =>
|
||||
import('./routes/$account.relays.lazy').then((d) => d.Route),
|
||||
)
|
||||
|
||||
const AccountContactsRoute = AccountContactsImport.update({
|
||||
path: '/$account/contacts',
|
||||
getParentRoute: () => rootRoute,
|
||||
@@ -102,6 +110,13 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof IndexImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
'/bootstrap-relays': {
|
||||
id: '/bootstrap-relays'
|
||||
path: '/bootstrap-relays'
|
||||
fullPath: '/bootstrap-relays'
|
||||
preLoaderRoute: typeof BootstrapRelaysImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
'/create-account': {
|
||||
id: '/create-account'
|
||||
path: '/create-account'
|
||||
@@ -137,6 +152,13 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof AccountContactsImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
'/$account/relays': {
|
||||
id: '/$account/relays'
|
||||
path: '/$account/relays'
|
||||
fullPath: '/$account/relays'
|
||||
preLoaderRoute: typeof AccountRelaysImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
'/$account/chats': {
|
||||
id: '/$account/chats'
|
||||
path: '/$account/chats'
|
||||
@@ -144,13 +166,6 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof AccountChatsLazyImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
'/$account/relays': {
|
||||
id: '/$account/relays'
|
||||
path: '/$account/relays'
|
||||
fullPath: '/$account/relays'
|
||||
preLoaderRoute: typeof AccountRelaysLazyImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
'/$account/chats/$id': {
|
||||
id: '/$account/chats/$id'
|
||||
path: '/$id'
|
||||
@@ -172,16 +187,17 @@ declare module '@tanstack/react-router' {
|
||||
|
||||
export const routeTree = rootRoute.addChildren({
|
||||
IndexRoute,
|
||||
BootstrapRelaysRoute,
|
||||
CreateAccountLazyRoute,
|
||||
ImportKeyLazyRoute,
|
||||
NewLazyRoute,
|
||||
NostrConnectLazyRoute,
|
||||
AccountContactsRoute,
|
||||
AccountRelaysRoute,
|
||||
AccountChatsLazyRoute: AccountChatsLazyRoute.addChildren({
|
||||
AccountChatsIdRoute,
|
||||
AccountChatsNewLazyRoute,
|
||||
}),
|
||||
AccountRelaysLazyRoute,
|
||||
})
|
||||
|
||||
/* prettier-ignore-end */
|
||||
@@ -193,18 +209,22 @@ export const routeTree = rootRoute.addChildren({
|
||||
"filePath": "__root.tsx",
|
||||
"children": [
|
||||
"/",
|
||||
"/bootstrap-relays",
|
||||
"/create-account",
|
||||
"/import-key",
|
||||
"/new",
|
||||
"/nostr-connect",
|
||||
"/$account/contacts",
|
||||
"/$account/chats",
|
||||
"/$account/relays"
|
||||
"/$account/relays",
|
||||
"/$account/chats"
|
||||
]
|
||||
},
|
||||
"/": {
|
||||
"filePath": "index.tsx"
|
||||
},
|
||||
"/bootstrap-relays": {
|
||||
"filePath": "bootstrap-relays.tsx"
|
||||
},
|
||||
"/create-account": {
|
||||
"filePath": "create-account.lazy.tsx"
|
||||
},
|
||||
@@ -220,6 +240,9 @@ export const routeTree = rootRoute.addChildren({
|
||||
"/$account/contacts": {
|
||||
"filePath": "$account.contacts.tsx"
|
||||
},
|
||||
"/$account/relays": {
|
||||
"filePath": "$account.relays.tsx"
|
||||
},
|
||||
"/$account/chats": {
|
||||
"filePath": "$account.chats.lazy.tsx",
|
||||
"children": [
|
||||
@@ -227,9 +250,6 @@ export const routeTree = rootRoute.addChildren({
|
||||
"/$account/chats/new"
|
||||
]
|
||||
},
|
||||
"/$account/relays": {
|
||||
"filePath": "$account.relays.lazy.tsx"
|
||||
},
|
||||
"/$account/chats/$id": {
|
||||
"filePath": "$account.chats.$id.tsx",
|
||||
"parent": "/$account/chats"
|
||||
|
||||
@@ -12,6 +12,7 @@ export const Route = createLazyFileRoute("/$account/relays")({
|
||||
|
||||
function Screen() {
|
||||
const navigate = Route.useNavigate();
|
||||
const inboxRelays = Route.useLoaderData();
|
||||
const { account } = Route.useParams();
|
||||
|
||||
const [newRelay, setNewRelay] = useState("");
|
||||
@@ -43,6 +44,10 @@ function Screen() {
|
||||
}
|
||||
};
|
||||
|
||||
const remove = (relay: string) => {
|
||||
setRelays((prev) => prev.filter((item) => item !== relay));
|
||||
};
|
||||
|
||||
const submit = async () => {
|
||||
startTransition(async () => {
|
||||
if (!relays.length) {
|
||||
@@ -50,7 +55,7 @@ function Screen() {
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await commands.setInbox(relays);
|
||||
const res = await commands.setInboxRelays(relays);
|
||||
|
||||
if (res.status === "ok") {
|
||||
navigate({
|
||||
@@ -69,16 +74,8 @@ function Screen() {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
async function getRelays() {
|
||||
const res = await commands.getInbox(account);
|
||||
|
||||
if (res.status === "ok") {
|
||||
setRelays((prev) => [...prev, ...res.data]);
|
||||
}
|
||||
}
|
||||
|
||||
getRelays();
|
||||
}, []);
|
||||
setRelays(inboxRelays);
|
||||
}, [inboxRelays]);
|
||||
|
||||
return (
|
||||
<div className="size-full flex items-center justify-center">
|
||||
@@ -140,6 +137,7 @@ function Screen() {
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => remove(relay)}
|
||||
className="inline-flex items-center justify-center rounded-md size-7 text-neutral-700 dark:text-white/20 hover:bg-black/10 dark:hover:bg-white/10"
|
||||
>
|
||||
<X className="size-3" />
|
||||
|
||||
14
src/routes/$account.relays.tsx
Normal file
14
src/routes/$account.relays.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import { commands } from "@/commands";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
|
||||
export const Route = createFileRoute("/$account/relays")({
|
||||
loader: async ({ params }) => {
|
||||
const res = await commands.getInboxRelays(params.account);
|
||||
|
||||
if (res.status === "ok") {
|
||||
return res.data;
|
||||
} else {
|
||||
throw new Error(res.error);
|
||||
}
|
||||
},
|
||||
});
|
||||
142
src/routes/bootstrap-relays.lazy.tsx
Normal file
142
src/routes/bootstrap-relays.lazy.tsx
Normal file
@@ -0,0 +1,142 @@
|
||||
import { commands } from "@/commands";
|
||||
import { Frame } from "@/components/frame";
|
||||
import { Spinner } from "@/components/spinner";
|
||||
import { Plus, X } from "@phosphor-icons/react";
|
||||
import { createLazyFileRoute } from "@tanstack/react-router";
|
||||
import { message } from "@tauri-apps/plugin-dialog";
|
||||
import { useEffect, useState, useTransition } from "react";
|
||||
|
||||
export const Route = createLazyFileRoute("/bootstrap-relays")({
|
||||
component: Screen,
|
||||
});
|
||||
|
||||
function Screen() {
|
||||
const bootstrapRelays = Route.useLoaderData();
|
||||
|
||||
const [relays, setRelays] = useState<string[]>([]);
|
||||
const [newRelay, setNewRelay] = useState("");
|
||||
const [isPending, startTransition] = useTransition();
|
||||
|
||||
const add = () => {
|
||||
try {
|
||||
let url = newRelay;
|
||||
|
||||
if (!url.startsWith("wss://")) {
|
||||
url = `wss://${url}`;
|
||||
}
|
||||
|
||||
// Validate URL
|
||||
const relay = new URL(url);
|
||||
|
||||
// Update
|
||||
setRelays((prev) => [...prev, relay.toString()]);
|
||||
setNewRelay("");
|
||||
} catch {
|
||||
message("URL is not valid.", { kind: "error" });
|
||||
}
|
||||
};
|
||||
|
||||
const remove = (relay: string) => {
|
||||
setRelays((prev) => prev.filter((item) => item !== relay));
|
||||
};
|
||||
|
||||
const submit = async () => {
|
||||
startTransition(async () => {
|
||||
if (!relays.length) {
|
||||
await message("You need to add at least 1 relay", {
|
||||
title: "Manage Relays",
|
||||
kind: "info",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const merged = relays
|
||||
.map((relay) => Object.values(relay).join(","))
|
||||
.join("\n");
|
||||
const res = await commands.setBootstrapRelays(merged);
|
||||
|
||||
if (res.status === "ok") {
|
||||
// TODO: restart app
|
||||
} else {
|
||||
await message(res.error, {
|
||||
title: "Manage Relays",
|
||||
kind: "error",
|
||||
});
|
||||
return;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setRelays(bootstrapRelays);
|
||||
}, [bootstrapRelays]);
|
||||
|
||||
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">Manage Relays</h1>
|
||||
<p className="text-sm text-neutral-700 dark:text-neutral-300">
|
||||
This relays will be only use to get user's metadata.
|
||||
</p>
|
||||
</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 gap-2">
|
||||
<input
|
||||
name="relay"
|
||||
type="text"
|
||||
placeholder="ex: relay.nostr.net, ..."
|
||||
value={newRelay}
|
||||
onChange={(e) => setNewRelay(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") add();
|
||||
}}
|
||||
className="flex-1 px-3 rounded-lg h-9 bg-transparent border border-neutral-200 dark:border-neutral-800 focus:border-blue-500 focus:outline-none placeholder:text-neutral-400 dark:placeholder:text-neutral-600"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
onClick={() => add()}
|
||||
className="inline-flex items-center justify-center size-9 rounded-lg bg-neutral-100 hover:bg-neutral-200 dark:bg-neutral-900 dark:hover:bg-neutral-800"
|
||||
>
|
||||
<Plus className="size-5" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
{relays.map((relay) => (
|
||||
<div
|
||||
key={relay}
|
||||
className="flex items-center justify-between h-9 px-2 rounded-lg bg-neutral-100 dark:bg-neutral-900"
|
||||
>
|
||||
<div className="text-sm font-medium">{relay}</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => remove(relay)}
|
||||
className="inline-flex items-center justify-center rounded-md size-7 text-neutral-700 dark:text-white/20 hover:bg-black/10 dark:hover:bg-white/10"
|
||||
>
|
||||
<X className="size-3" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Frame>
|
||||
<div className="flex flex-col items-center gap-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => submit()}
|
||||
disabled={isPending || !relays.length}
|
||||
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 /> : "Save & Restart"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
14
src/routes/bootstrap-relays.tsx
Normal file
14
src/routes/bootstrap-relays.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import { commands } from "@/commands";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
|
||||
export const Route = createFileRoute("/bootstrap-relays")({
|
||||
loader: async () => {
|
||||
const res = await commands.getBootstrapRelays();
|
||||
|
||||
if (res.status === "ok") {
|
||||
return res.data.map((item) => item.replace(",", ""));
|
||||
} else {
|
||||
throw new Error(res.error);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -3,7 +3,7 @@ import { npub } from "@/commons";
|
||||
import { Frame } from "@/components/frame";
|
||||
import { Spinner } from "@/components/spinner";
|
||||
import { User } from "@/components/user";
|
||||
import { ArrowRight, DotsThree, Plus } from "@phosphor-icons/react";
|
||||
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";
|
||||
@@ -103,7 +103,7 @@ function Screen() {
|
||||
return (
|
||||
<div
|
||||
data-tauri-drag-region
|
||||
className="size-full flex items-center justify-center"
|
||||
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">
|
||||
@@ -190,6 +190,15 @@ function Screen() {
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user