diff --git a/src-tauri/src/commands/relay.rs b/src-tauri/src/commands/relay.rs index d7cc8528..d2fdb0f9 100644 --- a/src-tauri/src/commands/relay.rs +++ b/src-tauri/src/commands/relay.rs @@ -1,12 +1,8 @@ use nostr_sdk::prelude::*; use serde::Serialize; use specta::Type; -use std::{ - fs::OpenOptions, - io::{self, BufRead, Write}, - str::FromStr, -}; -use tauri::{path::BaseDirectory, Manager, State}; +use std::str::FromStr; +use tauri::State; use crate::{Nostr, FETCH_LIMIT}; @@ -20,82 +16,17 @@ pub struct Relays { #[tauri::command] #[specta::specta] -pub async fn get_relays(id: String, state: State<'_, Nostr>) -> Result { +pub async fn get_all_relays(state: State<'_, Nostr>) -> Result, String> { let client = &state.client; - let public_key = PublicKey::from_str(&id).map_err(|e| e.to_string())?; + let relays = client.pool().all_relays().await; + let v: Vec = relays.iter().map(|item| item.0.to_string()).collect(); - let connected_relays = client - .relays() - .await - .into_keys() - .map(|url| url.to_string()) - .collect::>(); - - let filter = Filter::new() - .author(public_key) - .kind(Kind::RelayList) - .limit(1); - - match client.database().query(vec![filter]).await { - Ok(events) => { - if let Some(event) = events.first() { - let nip65_list = nip65::extract_relay_list(event).collect::>(); - - let read = nip65_list - .iter() - .filter_map(|(url, meta)| { - if let Some(RelayMetadata::Read) = meta { - Some(url.to_string()) - } else { - None - } - }) - .collect(); - - let write = nip65_list - .iter() - .filter_map(|(url, meta)| { - if let Some(RelayMetadata::Write) = meta { - Some(url.to_string()) - } else { - None - } - }) - .collect(); - - let both = nip65_list - .iter() - .filter_map(|(url, meta)| { - if meta.is_none() { - Some(url.to_string()) - } else { - None - } - }) - .collect(); - - Ok(Relays { - connected: connected_relays, - read: Some(read), - write: Some(write), - both: Some(both), - }) - } else { - Ok(Relays { - connected: connected_relays, - read: None, - write: None, - both: None, - }) - } - } - Err(e) => Err(e.to_string()), - } + Ok(v) } #[tauri::command] #[specta::specta] -pub async fn get_all_relays( +pub async fn get_all_relay_lists( until: Option, state: State<'_, Nostr>, ) -> Result, String> { @@ -149,36 +80,3 @@ pub async fn remove_relay(relay: String, state: State<'_, Nostr>) -> Result<(), Ok(()) } - -#[tauri::command] -#[specta::specta] -pub fn get_bootstrap_relays(app: tauri::AppHandle) -> Result, String> { - let relays_path = app - .path() - .resolve("resources/relays.txt", 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::, 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", 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()) -} diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index dc69a429..4462d8b2 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -25,6 +25,7 @@ use tauri::{ }; use tauri_plugin_decorum::WebviewWindowExt; use tauri_plugin_notification::{NotificationExt, PermissionState}; +use tauri_plugin_store::StoreExt; use tauri_specta::{collect_commands, Builder}; use tokio::{sync::RwLock, time::sleep}; @@ -42,7 +43,7 @@ pub struct Payload { id: String, } -#[derive(Clone, Serialize, Deserialize, Type)] +#[derive(Debug, Clone, Serialize, Deserialize, Type)] pub struct Settings { resize_service: bool, content_warning: bool, @@ -74,13 +75,11 @@ fn main() { tracing_subscriber::fmt::init(); let builder = Builder::::new().commands(collect_commands![ - get_relays, get_all_relays, + get_all_relay_lists, is_relay_connected, connect_relay, remove_relay, - get_bootstrap_relays, - set_bootstrap_relays, get_accounts, watch_account, import_account, @@ -278,11 +277,29 @@ fn main() { client }); + // Load app settings + let store = app.store(".data")?; + + // Parse app settings if exist + let settings = if let Some(data) = store.get("tanstack-query-[\"settings\"]") { + if let Some(str) = data.as_str() { + let v: Value = serde_json::from_str(str).unwrap(); + let data = v["state"]["data"].clone(); + let parse: Settings = serde_json::from_value(data).unwrap(); + + RwLock::new(parse) + } else { + RwLock::new(Settings::default()) + } + } else { + RwLock::new(Settings::default()) + }; + // Create global state app.manage(Nostr { client, + settings, queue: RwLock::new(HashSet::new()), - settings: RwLock::new(Settings::default()), }); // Listen for request metadata diff --git a/src/commands.gen.ts b/src/commands.gen.ts index 401edaca..5911a16e 100644 --- a/src/commands.gen.ts +++ b/src/commands.gen.ts @@ -5,17 +5,17 @@ export const commands = { -async getRelays(id: string) : Promise> { +async getAllRelays() : Promise> { try { - return { status: "ok", data: await TAURI_INVOKE("get_relays", { id }) }; + return { status: "ok", data: await TAURI_INVOKE("get_all_relays") }; } catch (e) { if(e instanceof Error) throw e; else return { status: "error", error: e as any }; } }, -async getAllRelays(until: string | null) : Promise> { +async getAllRelayLists(until: string | null) : Promise> { try { - return { status: "ok", data: await TAURI_INVOKE("get_all_relays", { until }) }; + return { status: "ok", data: await TAURI_INVOKE("get_all_relay_lists", { until }) }; } catch (e) { if(e instanceof Error) throw e; else return { status: "error", error: e as any }; @@ -45,22 +45,6 @@ async removeRelay(relay: string) : Promise> { else return { status: "error", error: e as any }; } }, -async getBootstrapRelays() : Promise> { - 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> { - 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 getAccounts() : Promise { return await TAURI_INVOKE("get_accounts"); }, @@ -536,7 +520,6 @@ export type Column = { label: string; url: string; x: number; y: number; width: export type Mention = { pubkey: string; avatar: string; display_name: string; name: string } export type Meta = { content: string; images: string[]; events: string[]; mentions: string[]; hashtags: string[] } export type NewWindow = { label: string; title: string; url: string; width: number; height: number; maximizable: boolean; minimizable: boolean; hidden_title: boolean; closable: boolean } -export type Relays = { connected: string[]; read: string[] | null; write: string[] | null; both: string[] | null } export type RichEvent = { raw: string; parsed: Meta | null } export type Settings = { resize_service: boolean; content_warning: boolean; display_avatar: boolean; display_zap_button: boolean; display_repost_button: boolean; display_media: boolean } diff --git a/src/components/note/buttons/zap.tsx b/src/components/note/buttons/zap.tsx index 621b6ae8..8f648ea9 100644 --- a/src/components/note/buttons/zap.tsx +++ b/src/components/note/buttons/zap.tsx @@ -3,14 +3,12 @@ import { ZapIcon } from "@/components"; import { settingsQueryOptions } from "@/routes/__root"; import { LumeWindow } from "@/system"; import { useSuspenseQuery } from "@tanstack/react-query"; -import { useSearch } from "@tanstack/react-router"; import { useNoteContext } from "../provider"; export function NoteZap({ label = false, smol = false, }: { label?: boolean; smol?: boolean }) { - const search = useSearch({ strict: false }); const settings = useSuspenseQuery(settingsQueryOptions); const event = useNoteContext(); @@ -19,7 +17,7 @@ export function NoteZap({ return ( - - - ))} -
-
- setNewRelay(e.target.value)} - name="url" - placeholder="wss://..." - disabled={isPending} - spellCheck={false} - className="flex-1 px-3 bg-transparent rounded-lg h-9 border-neutral-300 placeholder:text-neutral-500 focus:border-blue-500 focus:ring-0 dark:border-neutral-700 dark:placeholder:text-neutral-400" - /> - -
-
- - -
-

- User Relays (NIP-65) -

-
-

- Lume will automatically connect to the user's relay list, but the - manager function (like adding, removing, changing relay purpose) - is not yet available. -

-
-
- {relayList.read?.map((relay) => ( -
-
{relay}
-
READ
-
- ))} - {relayList.write?.map((relay) => ( -
-
{relay}
-
WRITE
-
- ))} - {relayList.both?.map((relay) => ( -
-
{relay}
-
READ + WRITE
-
- ))} -
-
- - - ); -} diff --git a/src/routes/settings.lazy.tsx b/src/routes/settings.lazy.tsx new file mode 100644 index 00000000..78599e86 --- /dev/null +++ b/src/routes/settings.lazy.tsx @@ -0,0 +1,96 @@ +import { cn } from '@/commons' +import { CurrencyBtc, GearSix, HardDrives } from '@phosphor-icons/react' +import * as ScrollArea from '@radix-ui/react-scroll-area' +import { Link } from '@tanstack/react-router' +import { Outlet, createLazyFileRoute } from '@tanstack/react-router' + +export const Route = createLazyFileRoute('/settings')({ + component: Screen, +}) + +function Screen() { + const { platform } = Route.useRouteContext() + + return ( +
+
+
+

Settings

+
+ + {({ isActive }) => { + return ( +
+ +

General

+
+ ) + }} + + + {({ isActive }) => { + return ( +
+ +

Relays

+
+ ) + }} + + + {({ isActive }) => { + return ( +
+ +

Wallet

+
+ ) + }} + +
+ + + + + + + + + +
+ ) +} diff --git a/src/routes/settings.$id/general.lazy.tsx b/src/routes/settings/general.lazy.tsx similarity index 98% rename from src/routes/settings.$id/general.lazy.tsx rename to src/routes/settings/general.lazy.tsx index 01b0f60e..bf73ff1e 100644 --- a/src/routes/settings.$id/general.lazy.tsx +++ b/src/routes/settings/general.lazy.tsx @@ -17,7 +17,7 @@ import { settingsQueryOptions } from "../__root"; type Theme = "auto" | "light" | "dark"; -export const Route = createLazyFileRoute("/settings/$id/general")({ +export const Route = createLazyFileRoute("/settings/general")({ component: Screen, }); @@ -46,6 +46,7 @@ function Screen() { return; } else { await message(res.error, { kind: "error" }); + return; } }); }; diff --git a/src/routes/settings/general.tsx b/src/routes/settings/general.tsx new file mode 100644 index 00000000..51d57f23 --- /dev/null +++ b/src/routes/settings/general.tsx @@ -0,0 +1,3 @@ +import { createFileRoute } from "@tanstack/react-router"; + +export const Route = createFileRoute("/settings/general")(); diff --git a/src/routes/settings/relays.lazy.tsx b/src/routes/settings/relays.lazy.tsx new file mode 100644 index 00000000..fcd9f32f --- /dev/null +++ b/src/routes/settings/relays.lazy.tsx @@ -0,0 +1,107 @@ +import { commands } from "@/commands.gen"; +import { isValidRelayUrl } from "@/commons"; +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("/settings/relays")({ + component: Screen, +}); + +function Screen() { + const { allRelays } = Route.useRouteContext(); + + const [relays, setRelays] = useState([]); + const [newRelay, setNewRelay] = useState(""); + const [isPending, startTransition] = useTransition(); + + const removeRelay = async (relay: string) => { + const res = await commands.removeRelay(relay); + + if (res.status === "ok") { + return res.data; + } else { + throw new Error(res.error); + } + }; + + const addNewRelay = () => { + startTransition(async () => { + if (!isValidRelayUrl(newRelay)) { + await message("Relay URL is not valid", { kind: "error" }); + return; + } + + const res = await commands.connectRelay(newRelay); + + if (res.status === "ok") { + setRelays((prev) => [...prev, newRelay]); + setNewRelay(""); + } else { + await message(res.error, { title: "Relay", kind: "error" }); + return; + } + }); + }; + + useEffect(() => { + if (allRelays) setRelays(allRelays); + }, [allRelays]); + + return ( +
+
+
+

+ Connected Relays +

+
+
+
+ setNewRelay(e.target.value)} + name="url" + placeholder="wss://..." + disabled={isPending} + spellCheck={false} + className="flex-1 px-3 bg-transparent rounded-lg h-9 border-neutral-400/50 placeholder:text-neutral-500 focus:border-blue-500 focus:ring-0 dark:border-neutral-800/50 dark:placeholder:text-neutral-400" + /> + +
+
+ {relays.map((relay) => ( +
+
+ + + + + {relay} +
+ +
+ ))} +
+
+
+
+ ); +} diff --git a/src/routes/settings.$id/relay.tsx b/src/routes/settings/relays.tsx similarity index 50% rename from src/routes/settings.$id/relay.tsx rename to src/routes/settings/relays.tsx index 1ce9c65d..80301703 100644 --- a/src/routes/settings.$id/relay.tsx +++ b/src/routes/settings/relays.tsx @@ -1,12 +1,12 @@ import { commands } from "@/commands.gen"; import { createFileRoute } from "@tanstack/react-router"; -export const Route = createFileRoute("/settings/$id/relay")({ - beforeLoad: async ({ params }) => { - const res = await commands.getRelays(params.id); +export const Route = createFileRoute("/settings/relays")({ + beforeLoad: async () => { + const res = await commands.getAllRelays(); if (res.status === "ok") { - return { relayList: res.data }; + return { allRelays: res.data }; } else { throw new Error(res.error); } diff --git a/src/routes/settings.$id/wallet.lazy.tsx b/src/routes/settings/wallet.lazy.tsx similarity index 94% rename from src/routes/settings.$id/wallet.lazy.tsx rename to src/routes/settings/wallet.lazy.tsx index 54543bd4..c17bff76 100644 --- a/src/routes/settings.$id/wallet.lazy.tsx +++ b/src/routes/settings/wallet.lazy.tsx @@ -3,7 +3,7 @@ import { Button } from "@getalby/bitcoin-connect-react"; import { createLazyFileRoute } from "@tanstack/react-router"; import { useState } from "react"; -export const Route = createLazyFileRoute("/settings/$id/wallet")({ +export const Route = createLazyFileRoute("/settings/wallet")({ component: Screen, }); diff --git a/src/routes/settings.$id/wallet.tsx b/src/routes/settings/wallet.tsx similarity index 78% rename from src/routes/settings.$id/wallet.tsx rename to src/routes/settings/wallet.tsx index 2973318c..b7616ca1 100644 --- a/src/routes/settings.$id/wallet.tsx +++ b/src/routes/settings/wallet.tsx @@ -1,7 +1,7 @@ import { init } from "@getalby/bitcoin-connect-react"; import { createFileRoute } from "@tanstack/react-router"; -export const Route = createFileRoute("/settings/$id/wallet")({ +export const Route = createFileRoute("/settings/wallet")({ beforeLoad: async () => { init({ appName: "Lume", diff --git a/src/system/window.ts b/src/system/window.ts index 1ce8c5a3..778bc7b1 100644 --- a/src/system/window.ts +++ b/src/system/window.ts @@ -121,7 +121,7 @@ export const LumeWindow = { throw new Error(query.error); } }, - openZap: async (id: string, account?: string) => { + openZap: async (id: string) => { const wallet = await commands.loadWallet(); if (wallet.status === "ok") { @@ -136,16 +136,14 @@ export const LumeWindow = { hidden_title: true, closable: true, }); - } else if (account) { - await LumeWindow.openSettings(account, "wallet"); + } else { + await LumeWindow.openSettings("wallet"); } }, - openSettings: async (account: string, path?: string) => { + openSettings: async (path?: string) => { const query = await commands.openWindow({ label: "settings", - url: path - ? `/settings/${account}/${path}` - : `/settings/${account}/general`, + url: path ? `/settings/${path}` : "/settings/general", title: "Settings", width: 700, height: 500,