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:
雨宮蓮 2024-06-10 10:48:39 +07:00 committed by GitHub
parent b396c8a695
commit 90342c552f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 902 additions and 448 deletions

View 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>
);
}

View File

@ -1,4 +1,4 @@
import { PlusIcon } from "@lume/icons"; import { PlusIcon, RelayIcon } from "@lume/icons";
import { Spinner } from "@lume/ui"; import { Spinner } from "@lume/ui";
import { User } from "@/components/user"; import { User } from "@/components/user";
import { checkForAppUpdates } from "@lume/utils"; import { checkForAppUpdates } from "@lume/utils";
@ -59,16 +59,17 @@ function Screen() {
return ( return (
<div <div
data-tauri-drag-region 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"> <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} {currentDate}
</h2> </h2>
<h2 className="text-2xl font-semibold">Welcome back!</h2> <h2 className="text-2xl font-semibold">Welcome back!</h2>
</div> </div>
<div className="flex flex-wrap px-3 items-center justify-center gap-6"> </div>
<div className="w-full flex-1 flex flex-wrap items-center justify-center gap-6 px-3">
{loading ? ( {loading ? (
<div className="inline-flex size-6 items-center justify-center"> <div className="inline-flex size-6 items-center justify-center">
<Spinner className="size-6" /> <Spinner className="size-6" />
@ -100,6 +101,16 @@ function Screen() {
</> </>
)} )}
</div> </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>
</div> </div>
); );

View File

@ -1,7 +1,7 @@
import { CancelIcon, PlusIcon } from "@lume/icons"; import { CancelIcon, PlusIcon } from "@lume/icons";
import { NostrQuery } from "@lume/system"; import { NostrQuery } from "@lume/system";
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute } from "@tanstack/react-router";
import { useState } from "react"; import { useEffect, useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { toast } from "sonner"; import { toast } from "sonner";
@ -15,22 +15,32 @@ export const Route = createFileRoute("/settings/relay")({
function Screen() { function Screen() {
const relayList = Route.useLoaderData(); const relayList = Route.useLoaderData();
const [relays, setRelays] = useState(relayList.connected);
const { register, reset, handleSubmit } = useForm(); const { register, reset, handleSubmit } = useForm();
const [relays, setRelays] = useState<string[]>([]);
const [isLoading, setIsLoading] = useState(false);
const onSubmit = async (data: { url: string }) => { const onSubmit = async (data: { url: string }) => {
try { try {
setIsLoading(true);
const add = await NostrQuery.connectRelay(data.url); const add = await NostrQuery.connectRelay(data.url);
if (add) { if (add) {
setRelays((prev) => [...prev, data.url]); setRelays((prev) => [...prev, data.url]);
setIsLoading(false);
reset(); reset();
} }
} catch (e) { } catch (e) {
setIsLoading(false);
toast.error(String(e)); toast.error(String(e));
} }
}; };
useEffect(() => {
setRelays(relayList.connected);
}, [relayList]);
return ( return (
<div className="mx-auto w-full max-w-xl"> <div className="mx-auto w-full max-w-xl">
<div className="flex flex-col gap-6"> <div className="flex flex-col gap-6">
@ -79,6 +89,7 @@ function Screen() {
/> />
<button <button
type="submit" 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" 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" /> <PlusIcon className="size-7" />

View File

@ -1,370 +1,568 @@
// This file was generated by [tauri-specta](https://github.com/oscartbeaumont/tauri-specta). Do not edit this file manually.
// This file was generated by [tauri-specta](https://github.com/oscartbeaumont/tauri-specta). Do not edit this file manually. /** user-defined commands **/
/** user-defined commands **/ export const commands = {
async getRelays(): Promise<Result<Relays, null>> {
export const commands = { try {
async getRelays() : Promise<Result<Relays, null>> {
try {
return { status: "ok", data: await TAURI_INVOKE("get_relays") }; return { status: "ok", data: await TAURI_INVOKE("get_relays") };
} 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 connectRelay(relay: string) : Promise<Result<boolean, null>> { async connectRelay(relay: string): Promise<Result<boolean, null>> {
try { try {
return { status: "ok", data: await TAURI_INVOKE("connect_relay", { relay }) }; return {
} catch (e) { status: "ok",
if(e instanceof Error) throw e; data: await TAURI_INVOKE("connect_relay", { relay }),
};
} catch (e) {
if (e instanceof Error) throw e;
else return { status: "error", error: e as any }; else return { status: "error", error: e as any };
} }
}, },
async removeRelay(relay: string) : Promise<Result<boolean, null>> { async removeRelay(relay: string): Promise<Result<boolean, null>> {
try { try {
return { status: "ok", data: await TAURI_INVOKE("remove_relay", { relay }) }; return {
} catch (e) { status: "ok",
if(e instanceof Error) throw e; data: await TAURI_INVOKE("remove_relay", { relay }),
};
} catch (e) {
if (e instanceof Error) throw e;
else return { status: "error", error: e as any }; else return { status: "error", error: e as any };
} }
}, },
async getAccounts() : Promise<Result<string[], string>> { async getBootstrapRelays(): Promise<Result<string[], null>> {
try { 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 saveBootstrapRelays(relays: string): Promise<Result<null, string>> {
try {
return {
status: "ok",
data: await TAURI_INVOKE("save_bootstrap_relays", { relays }),
};
} catch (e) {
if (e instanceof Error) throw e;
else return { status: "error", error: e as any };
}
},
async getAccounts(): Promise<Result<string[], string>> {
try {
return { status: "ok", data: await TAURI_INVOKE("get_accounts") }; return { status: "ok", data: await TAURI_INVOKE("get_accounts") };
} 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 createAccount() : Promise<Result<Account, null>> { async createAccount(): Promise<Result<Account, null>> {
try { try {
return { status: "ok", data: await TAURI_INVOKE("create_account") }; return { status: "ok", data: await TAURI_INVOKE("create_account") };
} 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 saveAccount(
try { nsec: string,
return { status: "ok", data: await TAURI_INVOKE("save_account", { nsec, password }) }; password: string,
} catch (e) { ): Promise<Result<string, string>> {
if(e instanceof Error) throw e; try {
return {
status: "ok",
data: await TAURI_INVOKE("save_account", { nsec, password }),
};
} catch (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 getEncryptedKey(
try { npub: string,
return { status: "ok", data: await TAURI_INVOKE("get_encrypted_key", { npub, password }) }; password: string,
} catch (e) { ): Promise<Result<string, string>> {
if(e instanceof Error) throw e; try {
return {
status: "ok",
data: await TAURI_INVOKE("get_encrypted_key", { npub, password }),
};
} catch (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 connectRemoteAccount(uri: string): Promise<Result<string, string>> {
try { try {
return { status: "ok", data: await TAURI_INVOKE("connect_remote_account", { uri }) }; return {
} catch (e) { status: "ok",
if(e instanceof Error) throw e; data: await TAURI_INVOKE("connect_remote_account", { uri }),
};
} catch (e) {
if (e instanceof Error) throw e;
else return { status: "error", error: e as any }; else return { status: "error", error: e as any };
} }
}, },
async loadAccount(npub: string, bunker: string | null) : Promise<Result<boolean, string>> { async loadAccount(
try { npub: string,
return { status: "ok", data: await TAURI_INVOKE("load_account", { npub, bunker }) }; bunker: string | null,
} catch (e) { ): Promise<Result<boolean, string>> {
if(e instanceof Error) throw e; try {
return {
status: "ok",
data: await TAURI_INVOKE("load_account", { npub, bunker }),
};
} catch (e) {
if (e instanceof Error) throw e;
else return { status: "error", error: e as any }; else return { status: "error", error: e as any };
} }
}, },
async eventToBech32(id: string, relays: string[]) : Promise<Result<string, null>> { async eventToBech32(
try { id: string,
return { status: "ok", data: await TAURI_INVOKE("event_to_bech32", { id, relays }) }; relays: string[],
} catch (e) { ): Promise<Result<string, null>> {
if(e instanceof Error) throw e; try {
return {
status: "ok",
data: await TAURI_INVOKE("event_to_bech32", { id, relays }),
};
} catch (e) {
if (e instanceof Error) throw e;
else return { status: "error", error: e as any }; else return { status: "error", error: e as any };
} }
}, },
async userToBech32(key: string, relays: string[]) : Promise<Result<string, null>> { async userToBech32(
try { key: string,
return { status: "ok", data: await TAURI_INVOKE("user_to_bech32", { key, relays }) }; relays: string[],
} catch (e) { ): Promise<Result<string, null>> {
if(e instanceof Error) throw e; try {
return {
status: "ok",
data: await TAURI_INVOKE("user_to_bech32", { key, relays }),
};
} catch (e) {
if (e instanceof Error) throw e;
else return { status: "error", error: e as any }; else return { status: "error", error: e as any };
} }
}, },
async verifyNip05(key: string, nip05: string) : Promise<Result<boolean, string>> { async verifyNip05(
try { key: string,
return { status: "ok", data: await TAURI_INVOKE("verify_nip05", { key, nip05 }) }; nip05: string,
} catch (e) { ): Promise<Result<boolean, string>> {
if(e instanceof Error) throw e; try {
return {
status: "ok",
data: await TAURI_INVOKE("verify_nip05", { key, nip05 }),
};
} catch (e) {
if (e instanceof Error) throw e;
else return { status: "error", error: e as any }; else return { status: "error", error: e as any };
} }
}, },
async getCurrentUserProfile() : Promise<Result<string, string>> { async getCurrentUserProfile(): Promise<Result<string, string>> {
try { try {
return { status: "ok", data: await TAURI_INVOKE("get_current_user_profile") }; return {
} catch (e) { status: "ok",
if(e instanceof Error) throw e; data: await TAURI_INVOKE("get_current_user_profile"),
};
} catch (e) {
if (e instanceof Error) throw e;
else return { status: "error", error: e as any }; else return { status: "error", error: e as any };
} }
}, },
async getProfile(id: string) : Promise<Result<string, string>> { async getProfile(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_profile", { 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 getContactList() : Promise<Result<string[], string>> { async getContactList(): Promise<Result<string[], string>> {
try { try {
return { status: "ok", data: await TAURI_INVOKE("get_contact_list") }; return { status: "ok", data: await TAURI_INVOKE("get_contact_list") };
} 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 setContactList(pubkeys: string[]) : Promise<Result<boolean, string>> { async setContactList(pubkeys: string[]): Promise<Result<boolean, string>> {
try { try {
return { status: "ok", data: await TAURI_INVOKE("set_contact_list", { pubkeys }) }; return {
} catch (e) { status: "ok",
if(e instanceof Error) throw e; data: await TAURI_INVOKE("set_contact_list", { pubkeys }),
};
} catch (e) {
if (e instanceof Error) throw e;
else return { status: "error", error: e as any }; else return { status: "error", error: e as any };
} }
}, },
async createProfile(name: string, displayName: string, about: string, picture: string, banner: string, nip05: string, lud16: string, website: string) : Promise<Result<string, string>> { async createProfile(
try { name: string,
return { status: "ok", data: await TAURI_INVOKE("create_profile", { name, displayName, about, picture, banner, nip05, lud16, website }) }; displayName: string,
} catch (e) { about: string,
if(e instanceof Error) throw e; picture: string,
banner: string,
nip05: string,
lud16: string,
website: string,
): Promise<Result<string, string>> {
try {
return {
status: "ok",
data: await TAURI_INVOKE("create_profile", {
name,
displayName,
about,
picture,
banner,
nip05,
lud16,
website,
}),
};
} catch (e) {
if (e instanceof Error) throw e;
else return { status: "error", error: e as any }; else return { status: "error", error: e as any };
} }
}, },
async follow(id: string, alias: string | null) : Promise<Result<string, string>> { async follow(
try { id: string,
return { status: "ok", data: await TAURI_INVOKE("follow", { id, alias }) }; alias: string | null,
} catch (e) { ): Promise<Result<string, string>> {
if(e instanceof Error) throw e; try {
return {
status: "ok",
data: await TAURI_INVOKE("follow", { id, alias }),
};
} catch (e) {
if (e instanceof Error) throw e;
else return { status: "error", error: e as any }; else return { status: "error", error: e as any };
} }
}, },
async unfollow(id: string) : Promise<Result<string, string>> { async unfollow(id: string): Promise<Result<string, string>> {
try { try {
return { status: "ok", data: await TAURI_INVOKE("unfollow", { id }) }; return { status: "ok", data: await TAURI_INVOKE("unfollow", { 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 getNstore(key: string) : Promise<Result<string, string>> { async getNstore(key: string): Promise<Result<string, string>> {
try { try {
return { status: "ok", data: await TAURI_INVOKE("get_nstore", { key }) }; return { status: "ok", data: await TAURI_INVOKE("get_nstore", { key }) };
} 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 setNstore(key: string, content: string) : Promise<Result<string, string>> { async setNstore(
try { key: string,
return { status: "ok", data: await TAURI_INVOKE("set_nstore", { key, content }) }; content: string,
} catch (e) { ): Promise<Result<string, string>> {
if(e instanceof Error) throw e; try {
return {
status: "ok",
data: await TAURI_INVOKE("set_nstore", { key, content }),
};
} catch (e) {
if (e instanceof Error) throw e;
else return { status: "error", error: e as any }; else return { status: "error", error: e as any };
} }
}, },
async setNwc(uri: string) : Promise<Result<boolean, string>> { async setNwc(uri: string): Promise<Result<boolean, string>> {
try { try {
return { status: "ok", data: await TAURI_INVOKE("set_nwc", { uri }) }; return { status: "ok", data: await TAURI_INVOKE("set_nwc", { 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 loadNwc() : Promise<Result<boolean, string>> { async loadNwc(): Promise<Result<boolean, string>> {
try { try {
return { status: "ok", data: await TAURI_INVOKE("load_nwc") }; return { status: "ok", data: await TAURI_INVOKE("load_nwc") };
} 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 getBalance() : Promise<Result<string, string>> { async getBalance(): Promise<Result<string, string>> {
try { try {
return { status: "ok", data: await TAURI_INVOKE("get_balance") }; return { status: "ok", data: await TAURI_INVOKE("get_balance") };
} 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 zapProfile(id: string, amount: string, message: string) : Promise<Result<boolean, string>> { async zapProfile(
try { id: string,
return { status: "ok", data: await TAURI_INVOKE("zap_profile", { id, amount, message }) }; amount: string,
} catch (e) { message: string,
if(e instanceof Error) throw e; ): Promise<Result<boolean, string>> {
try {
return {
status: "ok",
data: await TAURI_INVOKE("zap_profile", { id, amount, message }),
};
} catch (e) {
if (e instanceof Error) throw e;
else return { status: "error", error: e as any }; else return { status: "error", error: e as any };
} }
}, },
async zapEvent(id: string, amount: string, message: string) : Promise<Result<boolean, string>> { async zapEvent(
try { id: string,
return { status: "ok", data: await TAURI_INVOKE("zap_event", { id, amount, message }) }; amount: string,
} catch (e) { message: string,
if(e instanceof Error) throw e; ): Promise<Result<boolean, string>> {
try {
return {
status: "ok",
data: await TAURI_INVOKE("zap_event", { id, amount, message }),
};
} catch (e) {
if (e instanceof Error) throw e;
else return { status: "error", error: e as any }; else return { status: "error", error: e as any };
} }
}, },
async friendToFriend(npub: string) : Promise<Result<boolean, string>> { async friendToFriend(npub: string): Promise<Result<boolean, string>> {
try { try {
return { status: "ok", data: await TAURI_INVOKE("friend_to_friend", { npub }) }; return {
} catch (e) { status: "ok",
if(e instanceof Error) throw e; data: await TAURI_INVOKE("friend_to_friend", { npub }),
};
} catch (e) {
if (e instanceof Error) throw e;
else return { status: "error", error: e as any }; else return { status: "error", error: e as any };
} }
}, },
async getNotifications() : Promise<Result<string[], string>> { async getNotifications(): Promise<Result<string[], string>> {
try { try {
return { status: "ok", data: await TAURI_INVOKE("get_notifications") }; return { status: "ok", data: await TAURI_INVOKE("get_notifications") };
} 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 getEvent(id: string) : Promise<Result<string, string>> { async getEvent(id: string): Promise<Result<string, string>> {
try { try {
return { status: "ok", data: await TAURI_INVOKE("get_event", { id }) }; return { status: "ok", data: await TAURI_INVOKE("get_event", { 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 getReplies(id: string) : Promise<Result<string[], string>> { async getReplies(id: string): Promise<Result<string[], string>> {
try { try {
return { status: "ok", data: await TAURI_INVOKE("get_replies", { id }) }; return { status: "ok", data: await TAURI_INVOKE("get_replies", { 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 getEventsBy(publicKey: string, asOf: string | null) : Promise<Result<string[], string>> { async getEventsBy(
try { publicKey: string,
return { status: "ok", data: await TAURI_INVOKE("get_events_by", { publicKey, asOf }) }; asOf: string | null,
} catch (e) { ): Promise<Result<string[], string>> {
if(e instanceof Error) throw e; try {
return {
status: "ok",
data: await TAURI_INVOKE("get_events_by", { publicKey, asOf }),
};
} catch (e) {
if (e instanceof Error) throw e;
else return { status: "error", error: e as any }; else return { status: "error", error: e as any };
} }
}, },
async getLocalEvents(pubkeys: string[], until: string | null) : Promise<Result<string[], string>> { async getLocalEvents(
try { pubkeys: string[],
return { status: "ok", data: await TAURI_INVOKE("get_local_events", { pubkeys, until }) }; until: string | null,
} catch (e) { ): Promise<Result<string[], string>> {
if(e instanceof Error) throw e; try {
return {
status: "ok",
data: await TAURI_INVOKE("get_local_events", { pubkeys, until }),
};
} catch (e) {
if (e instanceof Error) throw e;
else return { status: "error", error: e as any }; else return { status: "error", error: e as any };
} }
}, },
async getGlobalEvents(until: string | null) : Promise<Result<string[], string>> { async getGlobalEvents(
try { until: string | null,
return { status: "ok", data: await TAURI_INVOKE("get_global_events", { until }) }; ): Promise<Result<string[], string>> {
} catch (e) { try {
if(e instanceof Error) throw e; return {
status: "ok",
data: await TAURI_INVOKE("get_global_events", { until }),
};
} catch (e) {
if (e instanceof Error) throw e;
else return { status: "error", error: e as any }; else return { status: "error", error: e as any };
} }
}, },
async getHashtagEvents(hashtags: string[], until: string | null) : Promise<Result<string[], string>> { async getHashtagEvents(
try { hashtags: string[],
return { status: "ok", data: await TAURI_INVOKE("get_hashtag_events", { hashtags, until }) }; until: string | null,
} catch (e) { ): Promise<Result<string[], string>> {
if(e instanceof Error) throw e; try {
return {
status: "ok",
data: await TAURI_INVOKE("get_hashtag_events", { hashtags, until }),
};
} catch (e) {
if (e instanceof Error) throw e;
else return { status: "error", error: e as any }; else return { status: "error", error: e as any };
} }
}, },
async publish(content: string, tags: string[][]) : Promise<Result<string, string>> { async publish(
try { content: string,
return { status: "ok", data: await TAURI_INVOKE("publish", { content, tags }) }; tags: string[][],
} catch (e) { ): Promise<Result<string, string>> {
if(e instanceof Error) throw e; try {
return {
status: "ok",
data: await TAURI_INVOKE("publish", { content, tags }),
};
} catch (e) {
if (e instanceof Error) throw e;
else return { status: "error", error: e as any }; else return { status: "error", error: e as any };
} }
}, },
async repost(raw: string) : Promise<Result<string, string>> { async repost(raw: string): Promise<Result<string, string>> {
try { try {
return { status: "ok", data: await TAURI_INVOKE("repost", { raw }) }; return { status: "ok", data: await TAURI_INVOKE("repost", { raw }) };
} 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 showInFolder(path: string) : Promise<void> { async showInFolder(path: string): Promise<void> {
await TAURI_INVOKE("show_in_folder", { path }); await TAURI_INVOKE("show_in_folder", { path });
}, },
async createColumn(label: string, x: number, y: number, width: number, height: number, url: string) : Promise<Result<string, string>> { async createColumn(
try { label: string,
return { status: "ok", data: await TAURI_INVOKE("create_column", { label, x, y, width, height, url }) }; x: number,
} catch (e) { y: number,
if(e instanceof Error) throw e; width: number,
height: number,
url: string,
): Promise<Result<string, string>> {
try {
return {
status: "ok",
data: await TAURI_INVOKE("create_column", {
label,
x,
y,
width,
height,
url,
}),
};
} catch (e) {
if (e instanceof Error) throw e;
else return { status: "error", error: e as any }; else return { status: "error", error: e as any };
} }
}, },
async closeColumn(label: string) : Promise<Result<boolean, null>> { async closeColumn(label: string): Promise<Result<boolean, null>> {
try { try {
return { status: "ok", data: await TAURI_INVOKE("close_column", { label }) }; return {
} catch (e) { status: "ok",
if(e instanceof Error) throw e; data: await TAURI_INVOKE("close_column", { label }),
};
} catch (e) {
if (e instanceof Error) throw e;
else return { status: "error", error: e as any }; else return { status: "error", error: e as any };
} }
}, },
async repositionColumn(label: string, x: number, y: number) : Promise<Result<null, string>> { async repositionColumn(
try { label: string,
return { status: "ok", data: await TAURI_INVOKE("reposition_column", { label, x, y }) }; x: number,
} catch (e) { y: number,
if(e instanceof Error) throw e; ): Promise<Result<null, string>> {
try {
return {
status: "ok",
data: await TAURI_INVOKE("reposition_column", { label, x, y }),
};
} catch (e) {
if (e instanceof Error) throw e;
else return { status: "error", error: e as any }; else return { status: "error", error: e as any };
} }
}, },
async resizeColumn(label: string, width: number, height: number) : Promise<Result<null, string>> { async resizeColumn(
try { label: string,
return { status: "ok", data: await TAURI_INVOKE("resize_column", { label, width, height }) }; width: number,
} catch (e) { height: number,
if(e instanceof Error) throw e; ): Promise<Result<null, string>> {
try {
return {
status: "ok",
data: await TAURI_INVOKE("resize_column", { label, width, height }),
};
} catch (e) {
if (e instanceof Error) throw e;
else return { status: "error", error: e as any }; else return { status: "error", error: e as any };
} }
}, },
async openWindow(label: string, title: string, url: string, width: number, height: number) : Promise<Result<null, string>> { async openWindow(
try { label: string,
return { status: "ok", data: await TAURI_INVOKE("open_window", { label, title, url, width, height }) }; title: string,
} catch (e) { url: string,
if(e instanceof Error) throw e; width: number,
height: number,
): Promise<Result<null, string>> {
try {
return {
status: "ok",
data: await TAURI_INVOKE("open_window", {
label,
title,
url,
width,
height,
}),
};
} catch (e) {
if (e instanceof Error) throw e;
else return { status: "error", error: e as any }; else return { status: "error", error: e as any };
} }
}, },
async setBadge(count: number) : Promise<void> { async setBadge(count: number): Promise<void> {
await TAURI_INVOKE("set_badge", { count }); await TAURI_INVOKE("set_badge", { count });
} },
} };
/** user-defined events **/
/** user-defined statics **/
/** user-defined events **/
/** user-defined statics **/
/** user-defined types **/ /** user-defined types **/
export type Account = { npub: string; nsec: string } export type Account = { npub: string; nsec: 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;
};
/** tauri-specta globals **/ /** tauri-specta globals **/
import { invoke as TAURI_INVOKE } from "@tauri-apps/api/core"; import { invoke as TAURI_INVOKE } from "@tauri-apps/api/core";
import * as TAURI_API_EVENT from "@tauri-apps/api/event"; import * as TAURI_API_EVENT from "@tauri-apps/api/event";
import { type WebviewWindow as __WebviewWindow__ } from "@tauri-apps/api/webviewWindow"; import { type WebviewWindow as __WebviewWindow__ } from "@tauri-apps/api/webviewWindow";
type __EventObj__<T> = { type __EventObj__<T> = {
listen: ( listen: (
cb: TAURI_API_EVENT.EventCallback<T> cb: TAURI_API_EVENT.EventCallback<T>,
) => ReturnType<typeof TAURI_API_EVENT.listen<T>>; ) => ReturnType<typeof TAURI_API_EVENT.listen<T>>;
once: ( once: (
cb: TAURI_API_EVENT.EventCallback<T> cb: TAURI_API_EVENT.EventCallback<T>,
) => ReturnType<typeof TAURI_API_EVENT.once<T>>; ) => ReturnType<typeof TAURI_API_EVENT.once<T>>;
emit: T extends null emit: T extends null
? (payload?: T) => ReturnType<typeof TAURI_API_EVENT.emit> ? (payload?: T) => ReturnType<typeof TAURI_API_EVENT.emit>
@ -376,7 +574,7 @@ export type Result<T, E> =
| { status: "error"; error: E }; | { status: "error"; error: E };
function __makeEvents__<T extends Record<string, any>>( function __makeEvents__<T extends Record<string, any>>(
mappings: Record<keyof T, string> mappings: Record<keyof T, string>,
) { ) {
return new Proxy( return new Proxy(
{} as unknown as { {} as unknown as {
@ -406,8 +604,6 @@ function __makeEvents__<T extends Record<string, any>>(
}, },
}); });
}, },
} },
); );
} }

View File

@ -1,4 +1,4 @@
import { LumeColumn, Metadata, NostrEvent, Settings } from "@lume/types"; import { LumeColumn, Metadata, NostrEvent, Relay, Settings } from "@lume/types";
import { commands } from "./commands"; import { commands } from "./commands";
import { resolveResource } from "@tauri-apps/api/path"; import { resolveResource } from "@tauri-apps/api/path";
import { readFile, readTextFile } from "@tauri-apps/plugin-fs"; import { readFile, readTextFile } from "@tauri-apps/plugin-fs";
@ -6,6 +6,7 @@ import { isPermissionGranted } from "@tauri-apps/plugin-notification";
import { open } from "@tauri-apps/plugin-dialog"; import { open } from "@tauri-apps/plugin-dialog";
import { dedupEvents } from "./dedup"; import { dedupEvents } from "./dedup";
import { invoke } from "@tauri-apps/api/core"; import { invoke } from "@tauri-apps/api/core";
import { relaunch } from "@tauri-apps/plugin-process";
enum NSTORE_KEYS { enum NSTORE_KEYS {
settings = "lume_user_settings", settings = "lume_user_settings",
@ -305,4 +306,38 @@ export class NostrQuery {
} }
} }
} }
static async getBootstrapRelays() {
const query = await commands.getBootstrapRelays();
if (query.status === "ok") {
let relays: Relay[] = [];
console.log(query.data);
for (const item of query.data) {
const line = item.split(",");
const url = line[0];
const purpose = line[1] ?? "";
relays.push({ url, purpose });
}
return relays;
} else {
return [];
}
}
static async saveBootstrapRelays(relays: Relay[]) {
const text = relays
.map((relay) => Object.values(relay).join(","))
.join("\n");
const query = await commands.saveBootstrapRelays(text);
if (query.status === "ok") {
return await relaunch();
} else {
throw new Error(query.error);
}
}
} }

View File

@ -179,3 +179,8 @@ export interface Relays {
write: string[]; write: string[];
both: string[]; both: string[];
} }
export interface Relay {
url: string;
purpose: "read" | "write" | string;
}

View File

@ -0,0 +1,2 @@
wss://nostr.wine,
wss://relay.nostr.net,

View File

@ -15,8 +15,12 @@ extern crate cocoa;
extern crate objc; extern crate objc;
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use std::fs; use std::{
use tauri::Manager; fs,
io::{self, BufRead},
str::FromStr,
};
use tauri::{path::BaseDirectory, Manager};
use tauri_nspanel::ManagerExt; use tauri_nspanel::ManagerExt;
use tauri_plugin_decorum::WebviewWindowExt; use tauri_plugin_decorum::WebviewWindowExt;
@ -40,6 +44,8 @@ fn main() {
nostr::relay::get_relays, nostr::relay::get_relays,
nostr::relay::connect_relay, nostr::relay::connect_relay,
nostr::relay::remove_relay, nostr::relay::remove_relay,
nostr::relay::get_bootstrap_relays,
nostr::relay::save_bootstrap_relays,
nostr::keys::get_accounts, nostr::keys::get_accounts,
nostr::keys::create_account, nostr::keys::create_account,
nostr::keys::save_account, nostr::keys::save_account,
@ -145,22 +151,34 @@ fn main() {
Err(_) => ClientBuilder::default().opts(opts).build(), Err(_) => ClientBuilder::default().opts(opts).build(),
}; };
// Add bootstrap relays // Get bootstrap relays
client let relays_path = app
.add_relay("wss://relay.nostr.net") .path()
.await .resolve("resources/relays.txt", BaseDirectory::Resource)
.expect("Cannot connect to relay.nostr.net, please try again later."); .expect("Bootstrap relays not found.");
client let file = std::fs::File::open(&relays_path).unwrap();
.add_relay("wss://relay.damus.io") let lines = io::BufReader::new(file).lines();
.await
.expect("Cannot connect to relay.damus.io, please try again later."); // Add bootstrap relays to relay pool
client for line in lines.flatten() {
.add_relay_with_opts( if let Some((relay, option)) = line.split_once(',') {
"wss://directory.yabu.me/", match RelayMetadata::from_str(option) {
RelayOptions::new().read(true).write(false), Ok(meta) => {
) println!("connecting to bootstrap relay...: {} - {}", relay, meta);
.await let opts = if meta == RelayMetadata::Read {
.expect("Cannot connect to directory.yabu.me, please try again later."); 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 bootstrap relay...: {}", relay);
let _ = client.add_relay(relay).await;
}
}
}
}
// Connect // Connect
client.connect().await; client.connect().await;

View File

@ -1,8 +1,13 @@
use std::{
fs,
io::{self, BufRead, Write},
};
use crate::Nostr; use crate::Nostr;
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use serde::Serialize; use serde::Serialize;
use specta::Type; use specta::Type;
use tauri::State; use tauri::{path::BaseDirectory, Manager, State};
#[derive(Serialize, Type)] #[derive(Serialize, Type)]
pub struct Relays { pub struct Relays {
@ -103,3 +108,42 @@ pub async fn remove_relay(relay: &str, state: State<'_, Nostr>) -> Result<bool,
Ok(false) Ok(false)
} }
} }
#[tauri::command]
#[specta::specta]
pub fn get_bootstrap_relays(app: tauri::AppHandle) -> Result<Vec<String>, ()> {
let relays_path = app
.path()
.resolve("resources/relays.txt", BaseDirectory::Resource)
.expect("Bootstrap relays not found.");
let file = std::fs::File::open(&relays_path).unwrap();
let lines = io::BufReader::new(file).lines();
let mut relays = Vec::new();
for line in lines.flatten() {
relays.push(line.to_string())
}
Ok(relays)
}
#[tauri::command]
#[specta::specta]
pub fn save_bootstrap_relays(relays: &str, app: tauri::AppHandle) -> Result<(), String> {
let relays_path = app
.path()
.resolve("resources/relays.txt", BaseDirectory::Resource)
.expect("Bootstrap relays not found.");
let mut file = fs::OpenOptions::new()
.write(true)
.open(&relays_path)
.unwrap();
match file.write_all(relays.as_bytes()) {
Ok(_) => Ok(()),
Err(_) => Err("Cannot save bootstrap relays, please try again later.".into()),
}
}