feat: add basic relay management in rust

This commit is contained in:
reya 2024-05-11 12:28:07 +07:00
parent b46a5cf68f
commit 73f80f27fb
16 changed files with 440 additions and 168 deletions

View File

@ -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 />

View File

@ -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";

View File

@ -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";

View File

@ -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>

View File

@ -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>

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

View File

@ -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"

View File

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

View File

@ -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>
); );

View File

@ -165,3 +165,10 @@ export interface NIP05 {
}; };
}; };
} }
export interface Relays {
connected: string[];
read: string[];
write: string[];
both: string[];
}

View File

@ -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>
))} ))}

View File

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

View File

@ -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,

View File

@ -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())

View File

@ -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,

View File

@ -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)