From 73f80f27fbfff38d7d53532cc9383031b15ba55f Mon Sep 17 00:00:00 2001 From: reya <123083837+reyamir@users.noreply.github.com> Date: Sat, 11 May 2024 12:28:07 +0700 Subject: [PATCH] feat: add basic relay management in rust --- apps/desktop2/src/components/notification.tsx | 10 +- apps/desktop2/src/routes/__root.tsx | 2 +- .../src/routes/events/-components/reply.tsx | 2 +- apps/desktop2/src/routes/settings.tsx | 47 +++-- apps/desktop2/src/routes/settings/general.tsx | 178 ++++++++++-------- apps/desktop2/src/routes/settings/relay.tsx | 135 +++++++++++++ apps/desktop2/src/routes/settings/user.tsx | 4 +- packages/ark/src/ark.ts | 67 +++++-- packages/icons/src/relay.tsx | 13 +- packages/types/index.d.ts | 7 + packages/ui/src/note/activity.tsx | 2 +- packages/ui/src/user/name.tsx | 6 +- src-tauri/src/main.rs | 9 +- src-tauri/src/nostr/keys.rs | 22 ++- src-tauri/src/nostr/metadata.rs | 28 --- src-tauri/src/nostr/relay.rs | 76 +++++++- 16 files changed, 440 insertions(+), 168 deletions(-) create mode 100644 apps/desktop2/src/routes/settings/relay.tsx diff --git a/apps/desktop2/src/components/notification.tsx b/apps/desktop2/src/components/notification.tsx index 2af26d55..9db6f92e 100644 --- a/apps/desktop2/src/components/notification.tsx +++ b/apps/desktop2/src/components/notification.tsx @@ -17,13 +17,11 @@ export function Notification({ className, )} > -
-
-
- -
- +
+
+
+
diff --git a/apps/desktop2/src/routes/__root.tsx b/apps/desktop2/src/routes/__root.tsx index 53312b17..0b1ea245 100644 --- a/apps/desktop2/src/routes/__root.tsx +++ b/apps/desktop2/src/routes/__root.tsx @@ -1,5 +1,5 @@ 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 type { QueryClient } from "@tanstack/react-query"; import { Outlet, createRootRouteWithContext } from "@tanstack/react-router"; diff --git a/apps/desktop2/src/routes/events/-components/reply.tsx b/apps/desktop2/src/routes/events/-components/reply.tsx index 8649bf85..8215854a 100644 --- a/apps/desktop2/src/routes/events/-components/reply.tsx +++ b/apps/desktop2/src/routes/events/-components/reply.tsx @@ -1,5 +1,5 @@ import type { EventWithReplies } from "@lume/types"; -import { Note, User } from "@lume/ui"; +import { Note } from "@lume/ui"; import { cn } from "@lume/utils"; import { SubReply } from "./subReply"; diff --git a/apps/desktop2/src/routes/settings.tsx b/apps/desktop2/src/routes/settings.tsx index 21e6c703..c4cf7fc9 100644 --- a/apps/desktop2/src/routes/settings.tsx +++ b/apps/desktop2/src/routes/settings.tsx @@ -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 { Link } from "@tanstack/react-router"; import { Outlet, createFileRoute } from "@tanstack/react-router"; @@ -12,10 +18,10 @@ function Screen() { const { t } = useTranslation(); return ( -
+
@@ -25,8 +31,8 @@ function Screen() { className={cn( "flex h-14 w-20 shrink-0 flex-col items-center justify-center rounded-lg p-2", isActive - ? "bg-neutral-200 hover:bg-neutral-300 dark:bg-neutral-800 dark:text-neutral-100 dark:hover:bg-neutral-700" - : "text-neutral-700 hover:bg-neutral-200 dark:text-neutral-300 dark:hover:bg-neutral-800", + ? "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", )} > @@ -44,8 +50,8 @@ function Screen() { className={cn( "flex h-14 w-20 shrink-0 flex-col items-center justify-center rounded-lg p-2", isActive - ? "bg-neutral-200 hover:bg-neutral-300 dark:bg-neutral-800 dark:text-neutral-100 dark:hover:bg-neutral-700" - : "text-neutral-700 hover:bg-neutral-200 dark:text-neutral-300 dark:hover:bg-neutral-800", + ? "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", )} > @@ -56,6 +62,23 @@ function Screen() { ); }} + + {({ isActive }) => { + return ( +
+ +

Relay

+
+ ); + }} + {({ isActive }) => { return ( @@ -63,8 +86,8 @@ function Screen() { className={cn( "flex h-14 w-20 shrink-0 flex-col items-center justify-center rounded-lg p-2", isActive - ? "bg-neutral-200 hover:bg-neutral-300 dark:bg-neutral-800 dark:text-neutral-100 dark:hover:bg-neutral-700" - : "text-neutral-700 hover:bg-neutral-200 dark:text-neutral-300 dark:hover:bg-neutral-800", + ? "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", )} > @@ -82,8 +105,8 @@ function Screen() { className={cn( "flex h-14 w-20 shrink-0 flex-col items-center justify-center rounded-lg p-2", isActive - ? "bg-neutral-200 hover:bg-neutral-300 dark:bg-neutral-800 dark:text-neutral-100 dark:hover:bg-neutral-700" - : "text-neutral-700 hover:bg-neutral-200 dark:text-neutral-300 dark:hover:bg-neutral-800", + ? "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", )} > @@ -96,7 +119,7 @@ function Screen() {
-
+
diff --git a/apps/desktop2/src/routes/settings/general.tsx b/apps/desktop2/src/routes/settings/general.tsx index 7501bb5c..8aeb1854 100644 --- a/apps/desktop2/src/routes/settings/general.tsx +++ b/apps/desktop2/src/routes/settings/general.tsx @@ -71,85 +71,109 @@ function Screen() { return (
-
-
-
- toggleNofitication()} - 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" - > - - -
-

Push Notification

-

- Enabling push notifications will allow you to receive - notifications from Lume. -

+
+
+

+ General +

+
+
+
+

Notification

+

+ By turning on push notifications, you'll start getting + notifications from Lume directly. +

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

Enhanced Privacy

+

+ Lume presents external resources like images, videos, or link + previews in plain text. +

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

Auto Update

+

+ Automatically download and install new version. +

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

Filter sensitive content

+

+ By default, Lume will display all content which have Content + Warning tag, it's may include NSFW content. +

+
+
+ 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" + > + + +
-
- toggleEnhancedPrivacy()} - 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" - > - - -
-

Enhanced Privacy

-

- Lume will display external resources like image, video or link - preview as plain text. -

-
-
-
- toggleAutoUpdate()} - 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" - > - - -
-

Auto Update

-

- Automatically download and install new version. -

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

Zap

-

- Show the Zap button in each note and user's profile screen, use - for send Bitcoin tip to other users. -

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

Filter sensitive content

-

- By default, Lume will display all content which have Content - Warning tag, it's may include NSFW content. -

+
+
+

+ Interface +

+
+
+
+
+

Zap

+

+ Show the Zap button in each note and user's profile screen, + use for send bitcoin tip to other users. +

+
+
+ toggleZap()} + 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" + > + + +
+
diff --git a/apps/desktop2/src/routes/settings/relay.tsx b/apps/desktop2/src/routes/settings/relay.tsx new file mode 100644 index 00000000..3781fb58 --- /dev/null +++ b/apps/desktop2/src/routes/settings/relay.tsx @@ -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 ( +
+
+
+

+ Connected Relays +

+
+ {relays.map((relay) => ( +
+
+ + + + + {relay} +
+
+ +
+
+ ))} +
+
+ + +
+
+
+
+
+

+ User Relays (NIP-65) +

+
+

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

+
+
+ {relayList.read?.map((relay) => ( +
+
{relay}
+
READ
+
+ ))} + {relayList.write?.map((relay) => ( +
+
{relay}
+
WRITE
+
+ ))} + {relayList.both?.map((relay) => ( +
+
{relay}
+
READ + WRITE
+
+ ))} +
+
+
+
+ ); +} diff --git a/apps/desktop2/src/routes/settings/user.tsx b/apps/desktop2/src/routes/settings/user.tsx index 7cd57324..4f626c39 100644 --- a/apps/desktop2/src/routes/settings/user.tsx +++ b/apps/desktop2/src/routes/settings/user.tsx @@ -28,7 +28,7 @@ function Screen() { try { setLoading(true); - const profile = { ...data }; + const profile = { ...data, picture }; await ark.create_profile(profile); setLoading(false); @@ -44,7 +44,7 @@ function Screen() {
{profile.picture ? ( avatar + ); diff --git a/packages/types/index.d.ts b/packages/types/index.d.ts index c0707797..53822653 100644 --- a/packages/types/index.d.ts +++ b/packages/types/index.d.ts @@ -165,3 +165,10 @@ export interface NIP05 { }; }; } + +export interface Relays { + connected: string[]; + read: string[]; + write: string[]; + both: string[]; +} diff --git a/packages/ui/src/note/activity.tsx b/packages/ui/src/note/activity.tsx index 3f6ec922..d6cb45e5 100644 --- a/packages/ui/src/note/activity.tsx +++ b/packages/ui/src/note/activity.tsx @@ -15,7 +15,7 @@ export function NoteActivity({ className }: { className?: string }) { {mentions.splice(0, 4).map((mention) => ( - + ))} diff --git a/packages/ui/src/user/name.tsx b/packages/ui/src/user/name.tsx index 28b08167..ea792d10 100644 --- a/packages/ui/src/user/name.tsx +++ b/packages/ui/src/user/name.tsx @@ -3,19 +3,19 @@ import { useUserContext } from "./provider"; export function UserName({ className, - suffix, + prefix, }: { className?: string; - suffix?: string; + prefix?: string; }) { const user = useUserContext(); return (
+ {prefix} {user.profile?.display_name || user.profile?.name || displayNpub(user.pubkey, 16)} - {suffix}
); } diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index d0d9e0b8..d916f70f 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -60,6 +60,10 @@ fn main() { .add_relay("wss://bostr.nokotaro.work/") .await .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 client.connect().await; @@ -92,6 +96,10 @@ fn main() { Some(vec![]), )) .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::save_key, nostr::keys::get_encrypted_key, @@ -108,7 +116,6 @@ fn main() { nostr::metadata::get_current_user_profile, nostr::metadata::get_profile, nostr::metadata::get_contact_list, - nostr::metadata::get_contact_metadata, nostr::metadata::create_profile, nostr::metadata::follow, nostr::metadata::unfollow, diff --git a/src-tauri/src/nostr/keys.rs b/src-tauri/src/nostr/keys.rs index 65981418..fceb89bf 100644 --- a/src-tauri/src/nostr/keys.rs +++ b/src-tauri/src/nostr/keys.rs @@ -118,7 +118,7 @@ pub async fn verify_signer(state: State<'_, Nostr>) -> Result { } } -#[tauri::command] +#[tauri::command(async)] pub fn get_encrypted_key(npub: &str, password: &str) -> Result { 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() { let relay_list = nip65::extract_relay_list(event); for item in relay_list.into_iter() { - println!("connecting to relay: {}", item.0); - // Add relay to pool + println!("connecting to relay: {} - {:?}", item.0, item.1); + + 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 - .add_relay(item.0.to_string()) + .add_relay_with_opts(relay_url, opts) .await .unwrap_or_default(); + // Connect relay client .connect_relay(item.0.to_string()) diff --git a/src-tauri/src/nostr/metadata.rs b/src-tauri/src/nostr/metadata.rs index eefeaf16..fa8c339b 100644 --- a/src-tauri/src/nostr/metadata.rs +++ b/src-tauri/src/nostr/metadata.rs @@ -5,12 +5,6 @@ use std::{str::FromStr, time::Duration}; use tauri::{Manager, State}; use url::Url; -#[derive(serde::Serialize)] -pub struct CacheContact { - pubkey: String, - profile: Metadata, -} - #[tauri::command] pub fn run_notification(accounts: Vec, app: tauri::AppHandle) -> Result<(), ()> { tauri::async_runtime::spawn(async move { @@ -206,28 +200,6 @@ pub async fn get_contact_list(state: State<'_, Nostr>) -> Result, St } } -#[tauri::command] -pub async fn get_contact_metadata(state: State<'_, Nostr>) -> Result, String> { - let client = &state.client; - - if let Ok(contact_list) = client - .get_contact_list_metadata(Some(Duration::from_secs(10))) - .await - { - let list: Vec = 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] pub async fn create_profile( name: &str, diff --git a/src-tauri/src/nostr/relay.rs b/src-tauri/src/nostr/relay.rs index 434feaaf..4cb6aed3 100644 --- a/src-tauri/src/nostr/relay.rs +++ b/src-tauri/src/nostr/relay.rs @@ -2,11 +2,81 @@ use crate::Nostr; use nostr_sdk::prelude::*; use tauri::State; +#[derive(serde::Serialize)] +pub struct Relays { + connected: Vec, + read: Option>, + write: Option>, + both: Option>, +} + +#[tauri::command] +pub async fn get_relays(state: State<'_, Nostr>) -> Result { + let client = &state.client; + + // Get connected relays + let list = client.relays().await; + let connected_relays: Vec = 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 = 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 = 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 = 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] pub async fn list_connected_relays(state: State<'_, Nostr>) -> Result, ()> { let client = &state.client; - let relays = client.relays().await; - let list: Vec = relays.into_keys().collect(); + let connected_relays = client.relays().await; + let list = connected_relays.into_keys().collect(); Ok(list) } @@ -15,6 +85,7 @@ pub async fn list_connected_relays(state: State<'_, Nostr>) -> Result, pub async fn connect_relay(relay: &str, state: State<'_, Nostr>) -> Result { let client = &state.client; if let Ok(_) = client.add_relay(relay).await { + let _ = client.connect_relay(relay); Ok(true) } else { Ok(false) @@ -25,6 +96,7 @@ pub async fn connect_relay(relay: &str, state: State<'_, Nostr>) -> Result) -> Result { let client = &state.client; if let Ok(_) = client.remove_relay(relay).await { + let _ = client.disconnect_relay(relay); Ok(true) } else { Ok(false)