mirror of
https://github.com/luminous-devs/lume.git
synced 2024-10-01 01:10:48 +00:00
feat: add basic relay management in rust
This commit is contained in:
parent
b46a5cf68f
commit
73f80f27fb
@ -17,13 +17,11 @@ export function Notification({
|
|||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex flex-col gap-3">
|
<div>
|
||||||
<div>
|
<div className="px-3 h-14 flex items-center justify-between">
|
||||||
<div className="px-3 h-14 flex items-center justify-between">
|
<Note.User />
|
||||||
<Note.User />
|
|
||||||
</div>
|
|
||||||
<Note.Content className="px-3" />
|
|
||||||
</div>
|
</div>
|
||||||
|
<Note.Content className="px-3" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center h-14 px-3">
|
<div className="flex items-center h-14 px-3">
|
||||||
<Note.Open />
|
<Note.Open />
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import type { Ark } from "@lume/ark";
|
import type { Ark } from "@lume/ark";
|
||||||
import type { Account, Interests, Metadata, Settings } from "@lume/types";
|
import type { Interests, Metadata, Settings } from "@lume/types";
|
||||||
import { Spinner } from "@lume/ui";
|
import { Spinner } from "@lume/ui";
|
||||||
import type { QueryClient } from "@tanstack/react-query";
|
import type { QueryClient } from "@tanstack/react-query";
|
||||||
import { Outlet, createRootRouteWithContext } from "@tanstack/react-router";
|
import { Outlet, createRootRouteWithContext } from "@tanstack/react-router";
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import type { EventWithReplies } from "@lume/types";
|
import type { EventWithReplies } from "@lume/types";
|
||||||
import { Note, User } from "@lume/ui";
|
import { Note } from "@lume/ui";
|
||||||
import { cn } from "@lume/utils";
|
import { cn } from "@lume/utils";
|
||||||
import { SubReply } from "./subReply";
|
import { SubReply } from "./subReply";
|
||||||
|
|
||||||
|
@ -1,4 +1,10 @@
|
|||||||
import { SecureIcon, SettingsIcon, UserIcon, ZapIcon } from "@lume/icons";
|
import {
|
||||||
|
RelayIcon,
|
||||||
|
SecureIcon,
|
||||||
|
SettingsIcon,
|
||||||
|
UserIcon,
|
||||||
|
ZapIcon,
|
||||||
|
} from "@lume/icons";
|
||||||
import { cn } from "@lume/utils";
|
import { cn } from "@lume/utils";
|
||||||
import { Link } from "@tanstack/react-router";
|
import { Link } from "@tanstack/react-router";
|
||||||
import { Outlet, createFileRoute } from "@tanstack/react-router";
|
import { Outlet, createFileRoute } from "@tanstack/react-router";
|
||||||
@ -12,10 +18,10 @@ function Screen() {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full w-full flex-col bg-neutral-100 dark:bg-neutral-950">
|
<div className="flex h-full w-full flex-col">
|
||||||
<div
|
<div
|
||||||
data-tauri-drag-region
|
data-tauri-drag-region
|
||||||
className="flex h-20 w-full shrink-0 items-center justify-center border-b border-neutral-200 dark:border-neutral-800"
|
className="flex h-20 w-full shrink-0 items-center justify-center border-b border-black/10 dark:border-white/10"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<Link to="/settings/general">
|
<Link to="/settings/general">
|
||||||
@ -25,8 +31,8 @@ function Screen() {
|
|||||||
className={cn(
|
className={cn(
|
||||||
"flex h-14 w-20 shrink-0 flex-col items-center justify-center rounded-lg p-2",
|
"flex h-14 w-20 shrink-0 flex-col items-center justify-center rounded-lg p-2",
|
||||||
isActive
|
isActive
|
||||||
? "bg-neutral-200 hover:bg-neutral-300 dark:bg-neutral-800 dark:text-neutral-100 dark:hover:bg-neutral-700"
|
? "bg-black/10 hover:bg-black/20 dark:bg-white/10 text-neutral-900 dark:text-neutral-100 dark:hover:bg-bg-white/20"
|
||||||
: "text-neutral-700 hover:bg-neutral-200 dark:text-neutral-300 dark:hover:bg-neutral-800",
|
: "text-neutral-700 hover:bg-black/10 dark:text-neutral-300 dark:hover:bg-white/10",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<SettingsIcon className="size-5 shrink-0" />
|
<SettingsIcon className="size-5 shrink-0" />
|
||||||
@ -44,8 +50,8 @@ function Screen() {
|
|||||||
className={cn(
|
className={cn(
|
||||||
"flex h-14 w-20 shrink-0 flex-col items-center justify-center rounded-lg p-2",
|
"flex h-14 w-20 shrink-0 flex-col items-center justify-center rounded-lg p-2",
|
||||||
isActive
|
isActive
|
||||||
? "bg-neutral-200 hover:bg-neutral-300 dark:bg-neutral-800 dark:text-neutral-100 dark:hover:bg-neutral-700"
|
? "bg-black/10 hover:bg-black/20 dark:bg-white/10 text-neutral-900 dark:text-neutral-100 dark:hover:bg-bg-white/20"
|
||||||
: "text-neutral-700 hover:bg-neutral-200 dark:text-neutral-300 dark:hover:bg-neutral-800",
|
: "text-neutral-700 hover:bg-black/10 dark:text-neutral-300 dark:hover:bg-white/10",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<UserIcon className="size-5 shrink-0" />
|
<UserIcon className="size-5 shrink-0" />
|
||||||
@ -56,6 +62,23 @@ function Screen() {
|
|||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
</Link>
|
</Link>
|
||||||
|
<Link to="/settings/relay">
|
||||||
|
{({ isActive }) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex h-14 w-20 shrink-0 flex-col items-center justify-center rounded-lg p-2",
|
||||||
|
isActive
|
||||||
|
? "bg-black/10 hover:bg-black/20 dark:bg-white/10 text-neutral-900 dark:text-neutral-100 dark:hover:bg-bg-white/20"
|
||||||
|
: "text-neutral-700 hover:bg-black/10 dark:text-neutral-300 dark:hover:bg-white/10",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<RelayIcon className="size-5 shrink-0" />
|
||||||
|
<p className="text-sm font-medium">Relay</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Link>
|
||||||
<Link to="/settings/zap">
|
<Link to="/settings/zap">
|
||||||
{({ isActive }) => {
|
{({ isActive }) => {
|
||||||
return (
|
return (
|
||||||
@ -63,8 +86,8 @@ function Screen() {
|
|||||||
className={cn(
|
className={cn(
|
||||||
"flex h-14 w-20 shrink-0 flex-col items-center justify-center rounded-lg p-2",
|
"flex h-14 w-20 shrink-0 flex-col items-center justify-center rounded-lg p-2",
|
||||||
isActive
|
isActive
|
||||||
? "bg-neutral-200 hover:bg-neutral-300 dark:bg-neutral-800 dark:text-neutral-100 dark:hover:bg-neutral-700"
|
? "bg-black/10 hover:bg-black/20 dark:bg-white/10 text-neutral-900 dark:text-neutral-100 dark:hover:bg-bg-white/20"
|
||||||
: "text-neutral-700 hover:bg-neutral-200 dark:text-neutral-300 dark:hover:bg-neutral-800",
|
: "text-neutral-700 hover:bg-black/10 dark:text-neutral-300 dark:hover:bg-white/10",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<ZapIcon className="size-5 shrink-0" />
|
<ZapIcon className="size-5 shrink-0" />
|
||||||
@ -82,8 +105,8 @@ function Screen() {
|
|||||||
className={cn(
|
className={cn(
|
||||||
"flex h-14 w-20 shrink-0 flex-col items-center justify-center rounded-lg p-2",
|
"flex h-14 w-20 shrink-0 flex-col items-center justify-center rounded-lg p-2",
|
||||||
isActive
|
isActive
|
||||||
? "bg-neutral-200 hover:bg-neutral-300 dark:bg-neutral-800 dark:text-neutral-100 dark:hover:bg-neutral-700"
|
? "bg-black/10 hover:bg-black/20 dark:bg-white/10 text-neutral-900 dark:text-neutral-100 dark:hover:bg-bg-white/20"
|
||||||
: "text-neutral-700 hover:bg-neutral-200 dark:text-neutral-300 dark:hover:bg-neutral-800",
|
: "text-neutral-700 hover:bg-black/10 dark:text-neutral-300 dark:hover:bg-white/10",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<SecureIcon className="size-5 shrink-0" />
|
<SecureIcon className="size-5 shrink-0" />
|
||||||
@ -96,7 +119,7 @@ function Screen() {
|
|||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full flex-1 overflow-y-auto px-5 py-4">
|
<div className="w-full flex-1 overflow-y-auto scrollbar-none px-5 py-4">
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -71,85 +71,109 @@ function Screen() {
|
|||||||
|
|
||||||
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-3 divide-y divide-neutral-300 dark:divide-neutral-700">
|
<div className="flex flex-col gap-6">
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col gap-2">
|
||||||
<div className="flex w-full items-start justify-between gap-4 rounded-lg bg-neutral-100 px-5 py-4 dark:bg-neutral-900">
|
<h2 className="font-semibold text-sm text-neutral-700 dark:text-neutral-300">
|
||||||
<Switch.Root
|
General
|
||||||
checked={newSettings.notification}
|
</h2>
|
||||||
onClick={() => toggleNofitication()}
|
<div className="flex flex-col divide-y divide-black/10 dark:divide-white/10 bg-black/5 dark:bg-white/5 rounded-xl px-3">
|
||||||
className="relative mt-1 h-7 w-12 shrink-0 cursor-default rounded-full bg-neutral-200 outline-none data-[state=checked]:bg-blue-500 dark:bg-neutral-800"
|
<div className="flex w-full items-start justify-between gap-4 py-3">
|
||||||
>
|
<div className="flex-1">
|
||||||
<Switch.Thumb className="block size-6 translate-x-0.5 rounded-full bg-white transition-transform duration-100 will-change-transform data-[state=checked]:translate-x-[19px]" />
|
<h3 className="font-medium">Notification</h3>
|
||||||
</Switch.Root>
|
<p className="text-sm text-neutral-700 dark:text-neutral-300">
|
||||||
<div className="flex-1">
|
By turning on push notifications, you'll start getting
|
||||||
<h3 className="font-semibold">Push Notification</h3>
|
notifications from Lume directly.
|
||||||
<p className="text-sm text-neutral-700 dark:text-neutral-300">
|
</p>
|
||||||
Enabling push notifications will allow you to receive
|
</div>
|
||||||
notifications from Lume.
|
<div className="w-36 flex justify-end shrink-0">
|
||||||
</p>
|
<Switch.Root
|
||||||
|
checked={newSettings.notification}
|
||||||
|
onClick={() => toggleNofitication()}
|
||||||
|
className="relative h-7 w-12 shrink-0 cursor-default rounded-full bg-black/10 outline-none data-[state=checked]:bg-blue-500 dark:bg-white/10"
|
||||||
|
>
|
||||||
|
<Switch.Thumb className="block size-6 translate-x-0.5 rounded-full bg-white transition-transform duration-100 will-change-transform data-[state=checked]:translate-x-[19px]" />
|
||||||
|
</Switch.Root>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex w-full items-start justify-between gap-4 py-3">
|
||||||
|
<div className="flex-1">
|
||||||
|
<h3 className="font-medium">Enhanced Privacy</h3>
|
||||||
|
<p className="text-sm text-neutral-700 dark:text-neutral-300">
|
||||||
|
Lume presents external resources like images, videos, or link
|
||||||
|
previews in plain text.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="w-36 flex justify-end shrink-0">
|
||||||
|
<Switch.Root
|
||||||
|
checked={newSettings.enhancedPrivacy}
|
||||||
|
onClick={() => toggleEnhancedPrivacy()}
|
||||||
|
className="relative h-7 w-12 shrink-0 cursor-default rounded-full bg-black/10 outline-none data-[state=checked]:bg-blue-500 dark:bg-white/10"
|
||||||
|
>
|
||||||
|
<Switch.Thumb className="block size-6 translate-x-0.5 rounded-full bg-white transition-transform duration-100 will-change-transform data-[state=checked]:translate-x-[19px]" />
|
||||||
|
</Switch.Root>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex w-full items-start justify-between gap-4 py-3">
|
||||||
|
<div className="flex-1">
|
||||||
|
<h3 className="font-medium">Auto Update</h3>
|
||||||
|
<p className="text-sm text-neutral-700 dark:text-neutral-300">
|
||||||
|
Automatically download and install new version.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="w-36 flex justify-end shrink-0">
|
||||||
|
<Switch.Root
|
||||||
|
checked={newSettings.autoUpdate}
|
||||||
|
onClick={() => toggleAutoUpdate()}
|
||||||
|
className="relative h-7 w-12 shrink-0 cursor-default rounded-full bg-black/10 outline-none data-[state=checked]:bg-blue-500 dark:bg-white/10"
|
||||||
|
>
|
||||||
|
<Switch.Thumb className="block size-6 translate-x-0.5 rounded-full bg-white transition-transform duration-100 will-change-transform data-[state=checked]:translate-x-[19px]" />
|
||||||
|
</Switch.Root>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex w-full items-start justify-between gap-4 py-3">
|
||||||
|
<div className="flex-1">
|
||||||
|
<h3 className="font-semibold">Filter sensitive content</h3>
|
||||||
|
<p className="text-sm text-neutral-700 dark:text-neutral-300">
|
||||||
|
By default, Lume will display all content which have Content
|
||||||
|
Warning tag, it's may include NSFW content.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="w-36 flex justify-end shrink-0">
|
||||||
|
<Switch.Root
|
||||||
|
checked={newSettings.nsfw}
|
||||||
|
onClick={() => toggleNsfw()}
|
||||||
|
className="relative h-7 w-12 shrink-0 cursor-default rounded-full bg-black/10 outline-none data-[state=checked]:bg-blue-500 dark:bg-white/10"
|
||||||
|
>
|
||||||
|
<Switch.Thumb className="block size-6 translate-x-0.5 rounded-full bg-white transition-transform duration-100 will-change-transform data-[state=checked]:translate-x-[19px]" />
|
||||||
|
</Switch.Root>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex w-full items-start justify-between gap-4 rounded-lg bg-neutral-100 px-5 py-4 dark:bg-neutral-900">
|
</div>
|
||||||
<Switch.Root
|
<div className="flex flex-col gap-2">
|
||||||
checked={newSettings.enhancedPrivacy}
|
<h2 className="font-semibold text-sm text-neutral-700 dark:text-neutral-300">
|
||||||
onClick={() => toggleEnhancedPrivacy()}
|
Interface
|
||||||
className="relative mt-1 h-7 w-12 shrink-0 cursor-default rounded-full bg-neutral-200 outline-none data-[state=checked]:bg-blue-500 dark:bg-neutral-800"
|
</h2>
|
||||||
>
|
<div className="flex flex-col divide-y divide-black/10 dark:divide-white/10 bg-black/5 dark:bg-white/5 rounded-xl px-3">
|
||||||
<Switch.Thumb className="block size-6 translate-x-0.5 rounded-full bg-white transition-transform duration-100 will-change-transform data-[state=checked]:translate-x-[19px]" />
|
<div className="flex flex-col gap-4">
|
||||||
</Switch.Root>
|
<div className="flex w-full items-start justify-between gap-4 py-3">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<h3 className="font-semibold">Enhanced Privacy</h3>
|
<h3 className="font-semibold">Zap</h3>
|
||||||
<p className="text-sm text-neutral-700 dark:text-neutral-300">
|
<p className="text-sm text-neutral-700 dark:text-neutral-300">
|
||||||
Lume will display external resources like image, video or link
|
Show the Zap button in each note and user's profile screen,
|
||||||
preview as plain text.
|
use for send bitcoin tip to other users.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className="w-36 flex justify-end shrink-0">
|
||||||
<div className="flex w-full items-start justify-between gap-4 rounded-lg bg-neutral-100 px-5 py-4 dark:bg-neutral-900">
|
<Switch.Root
|
||||||
<Switch.Root
|
checked={newSettings.zap}
|
||||||
checked={newSettings.autoUpdate}
|
onClick={() => toggleZap()}
|
||||||
onClick={() => toggleAutoUpdate()}
|
className="relative h-7 w-12 shrink-0 cursor-default rounded-full bg-black/10 outline-none data-[state=checked]:bg-blue-500 dark:bg-white/10"
|
||||||
className="relative mt-1 h-7 w-12 shrink-0 cursor-default rounded-full bg-neutral-200 outline-none data-[state=checked]:bg-blue-500 dark:bg-neutral-800"
|
>
|
||||||
>
|
<Switch.Thumb className="block size-6 translate-x-0.5 rounded-full bg-white transition-transform duration-100 will-change-transform data-[state=checked]:translate-x-[19px]" />
|
||||||
<Switch.Thumb className="block size-6 translate-x-0.5 rounded-full bg-white transition-transform duration-100 will-change-transform data-[state=checked]:translate-x-[19px]" />
|
</Switch.Root>
|
||||||
</Switch.Root>
|
</div>
|
||||||
<div className="flex-1">
|
</div>
|
||||||
<h3 className="font-semibold">Auto Update</h3>
|
|
||||||
<p className="text-sm text-neutral-700 dark:text-neutral-300">
|
|
||||||
Automatically download and install new version.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex w-full items-start justify-between gap-4 rounded-lg bg-neutral-100 px-5 py-4 dark:bg-neutral-900">
|
|
||||||
<Switch.Root
|
|
||||||
checked={newSettings.zap}
|
|
||||||
onClick={() => toggleZap()}
|
|
||||||
className="relative mt-1 h-7 w-12 shrink-0 cursor-default rounded-full bg-neutral-200 outline-none data-[state=checked]:bg-blue-500 dark:bg-neutral-800"
|
|
||||||
>
|
|
||||||
<Switch.Thumb className="block size-6 translate-x-0.5 rounded-full bg-white transition-transform duration-100 will-change-transform data-[state=checked]:translate-x-[19px]" />
|
|
||||||
</Switch.Root>
|
|
||||||
<div className="flex-1">
|
|
||||||
<h3 className="font-semibold">Zap</h3>
|
|
||||||
<p className="text-sm text-neutral-700 dark:text-neutral-300">
|
|
||||||
Show the Zap button in each note and user's profile screen, use
|
|
||||||
for send Bitcoin tip to other users.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex w-full items-start justify-between gap-4 rounded-lg bg-neutral-100 px-5 py-4 dark:bg-neutral-900">
|
|
||||||
<Switch.Root
|
|
||||||
checked={newSettings.nsfw}
|
|
||||||
onClick={() => toggleNsfw()}
|
|
||||||
className="relative mt-1 h-7 w-12 shrink-0 cursor-default rounded-full bg-neutral-200 outline-none data-[state=checked]:bg-blue-500 dark:bg-neutral-800"
|
|
||||||
>
|
|
||||||
<Switch.Thumb className="block size-6 translate-x-0.5 rounded-full bg-white transition-transform duration-100 will-change-transform data-[state=checked]:translate-x-[19px]" />
|
|
||||||
</Switch.Root>
|
|
||||||
<div className="flex-1">
|
|
||||||
<h3 className="font-semibold">Filter sensitive content</h3>
|
|
||||||
<p className="text-sm text-neutral-700 dark:text-neutral-300">
|
|
||||||
By default, Lume will display all content which have Content
|
|
||||||
Warning tag, it's may include NSFW content.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
135
apps/desktop2/src/routes/settings/relay.tsx
Normal file
135
apps/desktop2/src/routes/settings/relay.tsx
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
import { CancelIcon, PlusIcon } from "@lume/icons";
|
||||||
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/settings/relay")({
|
||||||
|
loader: async ({ context }) => {
|
||||||
|
const ark = context.ark;
|
||||||
|
const relays = await ark.get_relays();
|
||||||
|
|
||||||
|
return relays;
|
||||||
|
},
|
||||||
|
component: Screen,
|
||||||
|
});
|
||||||
|
|
||||||
|
function Screen() {
|
||||||
|
const relayList = Route.useLoaderData();
|
||||||
|
const [relays, setRelays] = useState(relayList.connected);
|
||||||
|
|
||||||
|
const { ark } = Route.useRouteContext();
|
||||||
|
const { register, reset, handleSubmit } = useForm();
|
||||||
|
|
||||||
|
const onSubmit = async (data: { url: string }) => {
|
||||||
|
try {
|
||||||
|
const add = await ark.add_relay(data.url);
|
||||||
|
if (add) {
|
||||||
|
setRelays((prev) => [...prev, data.url]);
|
||||||
|
reset();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
toast.error(String(e));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mx-auto w-full max-w-xl">
|
||||||
|
<div className="flex flex-col gap-6">
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<h2 className="font-semibold text-sm text-neutral-700 dark:text-neutral-300">
|
||||||
|
Connected Relays
|
||||||
|
</h2>
|
||||||
|
<div className="flex flex-col divide-y divide-black/10 dark:divide-white/10 bg-black/5 dark:bg-white/5 rounded-xl px-3">
|
||||||
|
{relays.map((relay) => (
|
||||||
|
<div
|
||||||
|
key={relay}
|
||||||
|
className="flex justify-between items-center h-11"
|
||||||
|
>
|
||||||
|
<div className="inline-flex items-center gap-2 text-sm font-medium">
|
||||||
|
<span className="relative flex size-2">
|
||||||
|
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-teal-400 opacity-75"></span>
|
||||||
|
<span className="relative inline-flex rounded-full size-2 bg-teal-500"></span>
|
||||||
|
</span>
|
||||||
|
{relay}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="inline-flex items-center justify-center size-7 rounded-md hover:bg-black/10 dark:hover:bg-white/10"
|
||||||
|
>
|
||||||
|
<CancelIcon className="size-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<div className="flex items-center h-14">
|
||||||
|
<form
|
||||||
|
onSubmit={handleSubmit(onSubmit)}
|
||||||
|
className="w-full flex items-center gap-2 mb-0"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
{...register("url", {
|
||||||
|
required: true,
|
||||||
|
minLength: 1,
|
||||||
|
})}
|
||||||
|
name="url"
|
||||||
|
placeholder="wss://..."
|
||||||
|
spellCheck={false}
|
||||||
|
className="h-9 flex-1 rounded-lg border-neutral-300 bg-transparent px-3 placeholder:text-neutral-500 focus:border-blue-500 focus:ring-0 dark:border-neutral-700 dark:placeholder:text-neutral-400"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
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" />
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<h2 className="font-semibold text-sm text-neutral-700 dark:text-neutral-300">
|
||||||
|
User Relays (NIP-65)
|
||||||
|
</h2>
|
||||||
|
<div className="flex flex-col py-2 bg-black/5 dark:bg-white/5 rounded-xl px-3">
|
||||||
|
<p className="text-sm text-yellow-500">
|
||||||
|
Lume will automatically connect to the user's relay list, but the
|
||||||
|
manager function (like adding, removing, changing relay purpose)
|
||||||
|
is not yet available.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col divide-y divide-black/10 dark:divide-white/10 bg-black/5 dark:bg-white/5 rounded-xl px-3">
|
||||||
|
{relayList.read?.map((relay) => (
|
||||||
|
<div
|
||||||
|
key={relay}
|
||||||
|
className="flex justify-between items-center h-11"
|
||||||
|
>
|
||||||
|
<div className="text-sm font-medium">{relay}</div>
|
||||||
|
<div className="text-xs font-semibold">READ</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{relayList.write?.map((relay) => (
|
||||||
|
<div
|
||||||
|
key={relay}
|
||||||
|
className="flex justify-between items-center h-11"
|
||||||
|
>
|
||||||
|
<div className="text-sm font-medium">{relay}</div>
|
||||||
|
<div className="text-xs font-semibold">WRITE</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{relayList.both?.map((relay) => (
|
||||||
|
<div
|
||||||
|
key={relay}
|
||||||
|
className="flex justify-between items-center h-11"
|
||||||
|
>
|
||||||
|
<div className="text-sm font-medium">{relay}</div>
|
||||||
|
<div className="text-xs font-semibold">READ + WRITE</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -28,7 +28,7 @@ function Screen() {
|
|||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
const profile = { ...data };
|
const profile = { ...data, picture };
|
||||||
await ark.create_profile(profile);
|
await ark.create_profile(profile);
|
||||||
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@ -44,7 +44,7 @@ function Screen() {
|
|||||||
<div className="relative size-24 rounded-full bg-gradient-to-tr from-orange-100 via-red-50 to-blue-200">
|
<div className="relative size-24 rounded-full bg-gradient-to-tr from-orange-100 via-red-50 to-blue-200">
|
||||||
{profile.picture ? (
|
{profile.picture ? (
|
||||||
<img
|
<img
|
||||||
src={profile.picture}
|
src={picture || profile.picture}
|
||||||
alt="avatar"
|
alt="avatar"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
decoding="async"
|
decoding="async"
|
||||||
|
@ -8,6 +8,7 @@ import {
|
|||||||
type LumeColumn,
|
type LumeColumn,
|
||||||
type Metadata,
|
type Metadata,
|
||||||
type Settings,
|
type Settings,
|
||||||
|
Relays,
|
||||||
} from "@lume/types";
|
} from "@lume/types";
|
||||||
import { generateContentTags } from "@lume/utils";
|
import { generateContentTags } from "@lume/utils";
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
@ -55,16 +56,6 @@ export class Ark {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async get_activities(account: string, kind: "1" | "6" | "9735" = "1") {
|
|
||||||
try {
|
|
||||||
const events: Event[] = await invoke("get_activities", { account, kind });
|
|
||||||
return events;
|
|
||||||
} catch (e) {
|
|
||||||
console.error(String(e));
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async nostr_connect(uri: string) {
|
public async nostr_connect(uri: string) {
|
||||||
try {
|
try {
|
||||||
const remoteKey = uri.replace("bunker://", "").split("?")[0];
|
const remoteKey = uri.replace("bunker://", "").split("?")[0];
|
||||||
@ -117,6 +108,52 @@ export class Ark {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async get_relays() {
|
||||||
|
try {
|
||||||
|
const cmd: Relays = await invoke("get_relays");
|
||||||
|
return cmd;
|
||||||
|
} catch (e) {
|
||||||
|
console.error(String(e));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async add_relay(url: string) {
|
||||||
|
try {
|
||||||
|
const relayUrl = new URL(url);
|
||||||
|
|
||||||
|
if (relayUrl.protocol === "wss:" || relayUrl.protocol === "ws:") {
|
||||||
|
const cmd: boolean = await invoke("connect_relay", { relay: relayUrl });
|
||||||
|
return cmd;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error(String(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async remove_relay(url: string) {
|
||||||
|
try {
|
||||||
|
const relayUrl = new URL(url);
|
||||||
|
|
||||||
|
if (relayUrl.protocol === "wss:" || relayUrl.protocol === "ws:") {
|
||||||
|
const cmd: boolean = await invoke("remove_relay", { relay: relayUrl });
|
||||||
|
return cmd;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error(String(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async get_activities(account: string, kind: "1" | "6" | "9735" = "1") {
|
||||||
|
try {
|
||||||
|
const events: Event[] = await invoke("get_activities", { account, kind });
|
||||||
|
return events;
|
||||||
|
} catch (e) {
|
||||||
|
console.error(String(e));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async get_event(id: string) {
|
public async get_event(id: string) {
|
||||||
try {
|
try {
|
||||||
const eventId: string = id.replace("nostr:", "").replace(/[^\w\s]/gi, "");
|
const eventId: string = id.replace("nostr:", "").replace(/[^\w\s]/gi, "");
|
||||||
@ -463,16 +500,6 @@ export class Ark {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async get_contact_metadata() {
|
|
||||||
try {
|
|
||||||
const cmd: Contact[] = await invoke("get_contact_metadata");
|
|
||||||
return cmd;
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async follow(id: string, alias?: string) {
|
public async follow(id: string, alias?: string) {
|
||||||
try {
|
try {
|
||||||
const cmd: string = await invoke("follow", { id, alias });
|
const cmd: string = await invoke("follow", { id, alias });
|
||||||
|
@ -1,19 +1,12 @@
|
|||||||
export function RelayIcon(props: JSX.IntrinsicElements["svg"]) {
|
export function RelayIcon(props: JSX.IntrinsicElements["svg"]) {
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" {...props}>
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="25"
|
|
||||||
height="24"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 25 24"
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<path
|
<path
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
strokeWidth="2"
|
strokeWidth="1.5"
|
||||||
d="M21.5 12a9.002 9.002 0 01-4.682 7.897 9 9 0 01-5.59 1.013c-1.203-.17-1.805-.255-1.964-.267-.257-.02-.165-.016-.423-.014-.159 0-.34.014-.702.04l-2.153.153c-.857.062-1.286.092-1.607-.06a1.348 1.348 0 01-.641-.64c-.152-.32-.122-.75-.06-1.608l.153-2.153c.026-.362.04-.542.04-.702.002-.258.006-.166-.014-.423-.012-.159-.098-.76-.268-1.964A9 9 0 1121.5 12z"
|
d="m7.5 3.25 4.5 3.5 4.5-3.5m-11.75 17h14.5a2 2 0 0 0 2-2v-9.5a2 2 0 0 0-2-2H4.75a2 2 0 0 0-2 2v9.5a2 2 0 0 0 2 2Z"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
|
7
packages/types/index.d.ts
vendored
7
packages/types/index.d.ts
vendored
@ -165,3 +165,10 @@ export interface NIP05 {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Relays {
|
||||||
|
connected: string[];
|
||||||
|
read: string[];
|
||||||
|
write: string[];
|
||||||
|
both: string[];
|
||||||
|
}
|
||||||
|
@ -15,7 +15,7 @@ export function NoteActivity({ className }: { className?: string }) {
|
|||||||
{mentions.splice(0, 4).map((mention) => (
|
{mentions.splice(0, 4).map((mention) => (
|
||||||
<User.Provider key={mention} pubkey={mention}>
|
<User.Provider key={mention} pubkey={mention}>
|
||||||
<User.Root>
|
<User.Root>
|
||||||
<User.Name className="text-sm font-medium" />
|
<User.Name className="text-sm font-medium" prefix="@" />
|
||||||
</User.Root>
|
</User.Root>
|
||||||
</User.Provider>
|
</User.Provider>
|
||||||
))}
|
))}
|
||||||
|
@ -3,19 +3,19 @@ import { useUserContext } from "./provider";
|
|||||||
|
|
||||||
export function UserName({
|
export function UserName({
|
||||||
className,
|
className,
|
||||||
suffix,
|
prefix,
|
||||||
}: {
|
}: {
|
||||||
className?: string;
|
className?: string;
|
||||||
suffix?: string;
|
prefix?: string;
|
||||||
}) {
|
}) {
|
||||||
const user = useUserContext();
|
const user = useUserContext();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn("max-w-[12rem] truncate", className)}>
|
<div className={cn("max-w-[12rem] truncate", className)}>
|
||||||
|
{prefix}
|
||||||
{user.profile?.display_name ||
|
{user.profile?.display_name ||
|
||||||
user.profile?.name ||
|
user.profile?.name ||
|
||||||
displayNpub(user.pubkey, 16)}
|
displayNpub(user.pubkey, 16)}
|
||||||
{suffix}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -60,6 +60,10 @@ fn main() {
|
|||||||
.add_relay("wss://bostr.nokotaro.work/")
|
.add_relay("wss://bostr.nokotaro.work/")
|
||||||
.await
|
.await
|
||||||
.expect("Cannot connect to bostr.nokotaro.work, please try again later.");
|
.expect("Cannot connect to bostr.nokotaro.work, please try again later.");
|
||||||
|
client
|
||||||
|
.add_relay("wss://purplepag.es/")
|
||||||
|
.await
|
||||||
|
.expect("Cannot connect to purplepag.es, please try again later.");
|
||||||
|
|
||||||
// Connect
|
// Connect
|
||||||
client.connect().await;
|
client.connect().await;
|
||||||
@ -92,6 +96,10 @@ fn main() {
|
|||||||
Some(vec![]),
|
Some(vec![]),
|
||||||
))
|
))
|
||||||
.invoke_handler(tauri::generate_handler![
|
.invoke_handler(tauri::generate_handler![
|
||||||
|
nostr::relay::get_relays,
|
||||||
|
nostr::relay::list_connected_relays,
|
||||||
|
nostr::relay::connect_relay,
|
||||||
|
nostr::relay::remove_relay,
|
||||||
nostr::keys::create_keys,
|
nostr::keys::create_keys,
|
||||||
nostr::keys::save_key,
|
nostr::keys::save_key,
|
||||||
nostr::keys::get_encrypted_key,
|
nostr::keys::get_encrypted_key,
|
||||||
@ -108,7 +116,6 @@ fn main() {
|
|||||||
nostr::metadata::get_current_user_profile,
|
nostr::metadata::get_current_user_profile,
|
||||||
nostr::metadata::get_profile,
|
nostr::metadata::get_profile,
|
||||||
nostr::metadata::get_contact_list,
|
nostr::metadata::get_contact_list,
|
||||||
nostr::metadata::get_contact_metadata,
|
|
||||||
nostr::metadata::create_profile,
|
nostr::metadata::create_profile,
|
||||||
nostr::metadata::follow,
|
nostr::metadata::follow,
|
||||||
nostr::metadata::unfollow,
|
nostr::metadata::unfollow,
|
||||||
|
@ -118,7 +118,7 @@ pub async fn verify_signer(state: State<'_, Nostr>) -> Result<bool, ()> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command(async)]
|
||||||
pub fn get_encrypted_key(npub: &str, password: &str) -> Result<String, String> {
|
pub fn get_encrypted_key(npub: &str, password: &str) -> Result<String, String> {
|
||||||
let keyring = Entry::new("Lume Secret Storage", npub).unwrap();
|
let keyring = Entry::new("Lume Secret Storage", npub).unwrap();
|
||||||
|
|
||||||
@ -190,12 +190,26 @@ pub async fn load_selected_account(npub: &str, state: State<'_, Nostr>) -> Resul
|
|||||||
if let Some(event) = events.first() {
|
if let Some(event) = events.first() {
|
||||||
let relay_list = nip65::extract_relay_list(event);
|
let relay_list = nip65::extract_relay_list(event);
|
||||||
for item in relay_list.into_iter() {
|
for item in relay_list.into_iter() {
|
||||||
println!("connecting to relay: {}", item.0);
|
println!("connecting to relay: {} - {:?}", item.0, item.1);
|
||||||
// Add relay to pool
|
|
||||||
|
let relay_url = item.0.to_string();
|
||||||
|
let opts = match item.1 {
|
||||||
|
Some(val) => {
|
||||||
|
if val == RelayMetadata::Read {
|
||||||
|
RelayOptions::new().read(true).write(false)
|
||||||
|
} else {
|
||||||
|
RelayOptions::new().write(true).read(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => RelayOptions::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add relay to relay pool
|
||||||
let _ = client
|
let _ = client
|
||||||
.add_relay(item.0.to_string())
|
.add_relay_with_opts(relay_url, opts)
|
||||||
.await
|
.await
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
// Connect relay
|
// Connect relay
|
||||||
client
|
client
|
||||||
.connect_relay(item.0.to_string())
|
.connect_relay(item.0.to_string())
|
||||||
|
@ -5,12 +5,6 @@ use std::{str::FromStr, time::Duration};
|
|||||||
use tauri::{Manager, State};
|
use tauri::{Manager, State};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
#[derive(serde::Serialize)]
|
|
||||||
pub struct CacheContact {
|
|
||||||
pubkey: String,
|
|
||||||
profile: Metadata,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn run_notification(accounts: Vec<String>, app: tauri::AppHandle) -> Result<(), ()> {
|
pub fn run_notification(accounts: Vec<String>, app: tauri::AppHandle) -> Result<(), ()> {
|
||||||
tauri::async_runtime::spawn(async move {
|
tauri::async_runtime::spawn(async move {
|
||||||
@ -206,28 +200,6 @@ pub async fn get_contact_list(state: State<'_, Nostr>) -> Result<Vec<String>, St
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub async fn get_contact_metadata(state: State<'_, Nostr>) -> Result<Vec<CacheContact>, String> {
|
|
||||||
let client = &state.client;
|
|
||||||
|
|
||||||
if let Ok(contact_list) = client
|
|
||||||
.get_contact_list_metadata(Some(Duration::from_secs(10)))
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
let list: Vec<CacheContact> = contact_list
|
|
||||||
.into_iter()
|
|
||||||
.map(|(id, metadata)| CacheContact {
|
|
||||||
pubkey: id.to_hex(),
|
|
||||||
profile: metadata,
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
Ok(list)
|
|
||||||
} else {
|
|
||||||
Err("Contact list not found".into())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn create_profile(
|
pub async fn create_profile(
|
||||||
name: &str,
|
name: &str,
|
||||||
|
@ -2,11 +2,81 @@ use crate::Nostr;
|
|||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use tauri::State;
|
use tauri::State;
|
||||||
|
|
||||||
|
#[derive(serde::Serialize)]
|
||||||
|
pub struct Relays {
|
||||||
|
connected: Vec<String>,
|
||||||
|
read: Option<Vec<String>>,
|
||||||
|
write: Option<Vec<String>>,
|
||||||
|
both: Option<Vec<String>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn get_relays(state: State<'_, Nostr>) -> Result<Relays, ()> {
|
||||||
|
let client = &state.client;
|
||||||
|
|
||||||
|
// Get connected relays
|
||||||
|
let list = client.relays().await;
|
||||||
|
let connected_relays: Vec<String> = list.into_iter().map(|(url, _)| url.to_string()).collect();
|
||||||
|
|
||||||
|
// Get NIP-65 relay list
|
||||||
|
let signer = client.signer().await.unwrap();
|
||||||
|
let public_key = signer.public_key().await.unwrap();
|
||||||
|
let filter = Filter::new()
|
||||||
|
.author(public_key)
|
||||||
|
.kind(Kind::RelayList)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
match client.get_events_of(vec![filter], None).await {
|
||||||
|
Ok(events) => {
|
||||||
|
if let Some(event) = events.first() {
|
||||||
|
let nip65_list = nip65::extract_relay_list(event);
|
||||||
|
let read: Vec<String> = nip65_list
|
||||||
|
.clone()
|
||||||
|
.into_iter()
|
||||||
|
.filter(|i| matches!(&i.1, Some(y) if *y == RelayMetadata::Read))
|
||||||
|
.map(|(url, _)| url.to_string())
|
||||||
|
.collect();
|
||||||
|
let write: Vec<String> = nip65_list
|
||||||
|
.clone()
|
||||||
|
.into_iter()
|
||||||
|
.filter(|i| matches!(&i.1, Some(y) if *y == RelayMetadata::Write))
|
||||||
|
.map(|(url, _)| url.to_string())
|
||||||
|
.collect();
|
||||||
|
let both: Vec<String> = nip65_list
|
||||||
|
.into_iter()
|
||||||
|
.filter(|i| i.1.is_none())
|
||||||
|
.map(|(url, _)| url.to_string())
|
||||||
|
.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(_) => Ok(Relays {
|
||||||
|
connected: connected_relays,
|
||||||
|
read: None,
|
||||||
|
write: None,
|
||||||
|
both: None,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn list_connected_relays(state: State<'_, Nostr>) -> Result<Vec<Url>, ()> {
|
pub async fn list_connected_relays(state: State<'_, Nostr>) -> Result<Vec<Url>, ()> {
|
||||||
let client = &state.client;
|
let client = &state.client;
|
||||||
let relays = client.relays().await;
|
let connected_relays = client.relays().await;
|
||||||
let list: Vec<Url> = relays.into_keys().collect();
|
let list = connected_relays.into_keys().collect();
|
||||||
|
|
||||||
Ok(list)
|
Ok(list)
|
||||||
}
|
}
|
||||||
@ -15,6 +85,7 @@ pub async fn list_connected_relays(state: State<'_, Nostr>) -> Result<Vec<Url>,
|
|||||||
pub async fn connect_relay(relay: &str, state: State<'_, Nostr>) -> Result<bool, ()> {
|
pub async fn connect_relay(relay: &str, state: State<'_, Nostr>) -> Result<bool, ()> {
|
||||||
let client = &state.client;
|
let client = &state.client;
|
||||||
if let Ok(_) = client.add_relay(relay).await {
|
if let Ok(_) = client.add_relay(relay).await {
|
||||||
|
let _ = client.connect_relay(relay);
|
||||||
Ok(true)
|
Ok(true)
|
||||||
} else {
|
} else {
|
||||||
Ok(false)
|
Ok(false)
|
||||||
@ -25,6 +96,7 @@ pub async fn connect_relay(relay: &str, state: State<'_, Nostr>) -> Result<bool,
|
|||||||
pub async fn remove_relay(relay: &str, state: State<'_, Nostr>) -> Result<bool, ()> {
|
pub async fn remove_relay(relay: &str, state: State<'_, Nostr>) -> Result<bool, ()> {
|
||||||
let client = &state.client;
|
let client = &state.client;
|
||||||
if let Ok(_) = client.remove_relay(relay).await {
|
if let Ok(_) = client.remove_relay(relay).await {
|
||||||
|
let _ = client.disconnect_relay(relay);
|
||||||
Ok(true)
|
Ok(true)
|
||||||
} else {
|
} else {
|
||||||
Ok(false)
|
Ok(false)
|
||||||
|
Loading…
Reference in New Issue
Block a user