Customize Bootstrap Relays (#205)
* feat: add bootstrap relays file * feat: add save bootstrap relays command * feat: add customize bootstrap relays screen
This commit is contained in:
132
apps/desktop2/src/routes/bootstrap-relays.tsx
Normal file
132
apps/desktop2/src/routes/bootstrap-relays.tsx
Normal file
@@ -0,0 +1,132 @@
|
||||
import { CancelIcon, PlusIcon } from "@lume/icons";
|
||||
import { NostrQuery } from "@lume/system";
|
||||
import { Relay } from "@lume/types";
|
||||
import { Spinner } from "@lume/ui";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export const Route = createFileRoute("/bootstrap-relays")({
|
||||
loader: async () => {
|
||||
const bootstrapRelays = await NostrQuery.getBootstrapRelays();
|
||||
return bootstrapRelays;
|
||||
},
|
||||
component: Screen,
|
||||
});
|
||||
|
||||
function Screen() {
|
||||
const bootstrapRelays = Route.useLoaderData();
|
||||
const { register, reset, handleSubmit } = useForm();
|
||||
|
||||
const [relays, setRelays] = useState<Relay[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const removeRelay = (url: string) => {
|
||||
setRelays((prev) => prev.filter((relay) => relay.url !== url));
|
||||
};
|
||||
|
||||
const onSubmit = async (data: { url: string; purpose: string }) => {
|
||||
try {
|
||||
const relay: Relay = { url: data.url, purpose: data.purpose };
|
||||
setRelays((prev) => [...prev, relay]);
|
||||
reset();
|
||||
} catch (e) {
|
||||
toast.error(String(e));
|
||||
}
|
||||
};
|
||||
|
||||
const save = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
await NostrQuery.saveBootstrapRelays(relays);
|
||||
} catch (e) {
|
||||
setIsLoading(false);
|
||||
toast.error(String(e));
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setRelays(bootstrapRelays);
|
||||
}, [bootstrapRelays]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col justify-center items-center h-screen w-screen">
|
||||
<div className="mx-auto max-w-sm lg:max-w-lg w-full">
|
||||
<div className="h-11 text-center">
|
||||
<h1 className="font-semibold">Customize Bootstrap Relays</h1>
|
||||
</div>
|
||||
<div className="flex w-full flex-col bg-white rounded-xl shadow-primary backdrop-blur-lg dark:bg-white/20 dark:ring-1 ring-neutral-800/50 px-2">
|
||||
{relays.map((relay) => (
|
||||
<div
|
||||
key={relay.url}
|
||||
className="flex justify-between items-center h-11"
|
||||
>
|
||||
<div className="inline-flex items-center gap-2 text-sm font-medium">
|
||||
{relay.url}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{relay.purpose?.length ? (
|
||||
<button
|
||||
type="button"
|
||||
className="h-7 w-max rounded-md inline-flex items-center justify-center px-2 uppercase text-xs font-medium hover:bg-black/10 dark:hover:bg-white/10"
|
||||
>
|
||||
{relay.purpose}
|
||||
</button>
|
||||
) : null}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeRelay(relay.url)}
|
||||
className="inline-flex items-center justify-center size-7 rounded-md text-neutral-700 dark:text-white/20 hover:bg-black/10 dark:hover:bg-white/10"
|
||||
>
|
||||
<CancelIcon className="size-3" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<div className="flex items-center h-14 border-t border-neutral-100 dark:border-white/5">
|
||||
<form
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
className="w-full flex items-center gap-2 mb-0"
|
||||
>
|
||||
<div className="flex-1 flex items-center gap-2 rounded-lg border border-neutral-300 dark:border-white/20">
|
||||
<input
|
||||
{...register("url", {
|
||||
required: true,
|
||||
minLength: 1,
|
||||
})}
|
||||
name="url"
|
||||
placeholder="wss://..."
|
||||
spellCheck={false}
|
||||
className="h-9 flex-1 bg-transparent border-none rounded-l-lg px-3 placeholder:text-neutral-500 dark:placeholder:text-neutral-400"
|
||||
/>
|
||||
<select
|
||||
{...register("purpose")}
|
||||
className="flex-1 h-9 p-0 m-0 text-sm bg-transparent border-none ring-0 outline-none focus:outline-none focus:ring-0"
|
||||
>
|
||||
<option value="read">Read</option>
|
||||
<option value="write">Write</option>
|
||||
<option value="">Both</option>
|
||||
</select>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
className="shrink-0 inline-flex h-9 w-14 px-2 items-center justify-center rounded-lg bg-black/20 dark:bg-white/20 font-medium text-sm text-white hover:bg-blue-500 disabled:opacity-50"
|
||||
>
|
||||
<PlusIcon className="size-7" />
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => save()}
|
||||
disabled={isLoading}
|
||||
className="mt-4 inline-flex h-10 w-full shrink-0 items-center justify-center rounded-lg bg-blue-500 text-sm font-semibold text-white hover:bg-blue-600 disabled:opacity-50"
|
||||
>
|
||||
{isLoading ? <Spinner /> : "Save & Relaunch"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { PlusIcon } from "@lume/icons";
|
||||
import { PlusIcon, RelayIcon } from "@lume/icons";
|
||||
import { Spinner } from "@lume/ui";
|
||||
import { User } from "@/components/user";
|
||||
import { checkForAppUpdates } from "@lume/utils";
|
||||
@@ -59,46 +59,57 @@ function Screen() {
|
||||
return (
|
||||
<div
|
||||
data-tauri-drag-region
|
||||
className="relative flex h-full w-full items-center justify-center"
|
||||
className="h-full w-full flex flex-col items-center justify-between"
|
||||
>
|
||||
<div className="relative z-20 flex flex-col items-center gap-16">
|
||||
<div className="w-full flex-1 flex items-end justify-center px-4">
|
||||
<div className="text-center">
|
||||
<h2 className="text-xl text-neutral-700 dark:text-neutral-300">
|
||||
<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 className="flex flex-wrap px-3 items-center justify-center gap-6">
|
||||
{loading ? (
|
||||
<div className="inline-flex size-6 items-center justify-center">
|
||||
<Spinner className="size-6" />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{context.accounts.map((account) => (
|
||||
<button
|
||||
key={account}
|
||||
type="button"
|
||||
onClick={() => select(account)}
|
||||
>
|
||||
<User.Provider pubkey={account}>
|
||||
<User.Root className="flex h-36 w-32 flex-col items-center justify-center gap-3 rounded-2xl p-2 hover:bg-black/10 dark:hover:bg-white/10">
|
||||
<User.Avatar className="size-20 rounded-full object-cover" />
|
||||
<User.Name className="max-w-[6rem] truncate font-medium leading-tight" />
|
||||
</User.Root>
|
||||
</User.Provider>
|
||||
</button>
|
||||
))}
|
||||
<Link to="/landing">
|
||||
<div className="flex h-36 w-32 flex-col items-center justify-center gap-3 rounded-2xl p-2 hover:bg-black/10 dark:hover:bg-white/10">
|
||||
<div className="flex size-20 items-center justify-center rounded-full bg-black/5 dark:bg-white/5">
|
||||
<PlusIcon className="size-8" />
|
||||
</div>
|
||||
<p className="font-medium leading-tight">Add</p>
|
||||
</div>
|
||||
<div className="w-full flex-1 flex flex-wrap items-center justify-center gap-6 px-3">
|
||||
{loading ? (
|
||||
<div className="inline-flex size-6 items-center justify-center">
|
||||
<Spinner className="size-6" />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{context.accounts.map((account) => (
|
||||
<button
|
||||
key={account}
|
||||
type="button"
|
||||
onClick={() => select(account)}
|
||||
>
|
||||
<User.Provider pubkey={account}>
|
||||
<User.Root className="flex h-36 w-32 flex-col items-center justify-center gap-3 rounded-2xl p-2 hover:bg-black/10 dark:hover:bg-white/10">
|
||||
<User.Avatar className="size-20 rounded-full object-cover" />
|
||||
<User.Name className="max-w-[6rem] truncate font-medium leading-tight" />
|
||||
</User.Root>
|
||||
</User.Provider>
|
||||
</button>
|
||||
))}
|
||||
<Link to="/landing">
|
||||
<div className="flex h-36 w-32 flex-col items-center justify-center gap-3 rounded-2xl p-2 hover:bg-black/10 dark:hover:bg-white/10">
|
||||
<div className="flex size-20 items-center justify-center rounded-full bg-black/5 dark:bg-white/5">
|
||||
<PlusIcon className="size-8" />
|
||||
</div>
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
<p className="font-medium leading-tight">Add</p>
|
||||
</div>
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="w-full flex-1 flex items-end justify-center pb-4 px-4">
|
||||
<div>
|
||||
<Link
|
||||
to="/bootstrap-relays"
|
||||
className="inline-flex items-center justify-center gap-2 bg-black/5 dark:bg-white/5 hover:bg-black/10 dark:hover:bg-white/10 rounded-full h-8 w-36 px-2 text-xs font-medium text-neutral-700 dark:text-white/40"
|
||||
>
|
||||
<RelayIcon className="size-4" />
|
||||
Bootstrap Relays
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { CancelIcon, PlusIcon } from "@lume/icons";
|
||||
import { NostrQuery } from "@lume/system";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
|
||||
@@ -15,22 +15,32 @@ export const Route = createFileRoute("/settings/relay")({
|
||||
|
||||
function Screen() {
|
||||
const relayList = Route.useLoaderData();
|
||||
const [relays, setRelays] = useState(relayList.connected);
|
||||
|
||||
const { register, reset, handleSubmit } = useForm();
|
||||
|
||||
const [relays, setRelays] = useState<string[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const onSubmit = async (data: { url: string }) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
const add = await NostrQuery.connectRelay(data.url);
|
||||
|
||||
if (add) {
|
||||
setRelays((prev) => [...prev, data.url]);
|
||||
setIsLoading(false);
|
||||
reset();
|
||||
}
|
||||
} catch (e) {
|
||||
setIsLoading(false);
|
||||
toast.error(String(e));
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setRelays(relayList.connected);
|
||||
}, [relayList]);
|
||||
|
||||
return (
|
||||
<div className="mx-auto w-full max-w-xl">
|
||||
<div className="flex flex-col gap-6">
|
||||
@@ -79,6 +89,7 @@ function Screen() {
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isLoading}
|
||||
className="shrink-0 inline-flex h-9 w-16 px-2 items-center justify-center rounded-lg bg-black/20 dark:bg-white/20 font-medium text-sm text-white hover:bg-blue-500 disabled:opacity-50"
|
||||
>
|
||||
<PlusIcon className="size-7" />
|
||||
|
||||
Reference in New Issue
Block a user