diff --git a/src-tauri/src/commands/account.rs b/src-tauri/src/commands/account.rs index 72fa1da..c799aca 100644 --- a/src-tauri/src/commands/account.rs +++ b/src-tauri/src/commands/account.rs @@ -27,12 +27,12 @@ pub fn get_accounts() -> Vec { #[tauri::command] #[specta::specta] -pub async fn get_metadata(id: String, state: State<'_, Nostr>) -> Result { +pub async fn get_metadata(user_id: String, state: State<'_, Nostr>) -> Result { let client = &state.client; - let public_key = PublicKey::parse(&id).map_err(|e| e.to_string())?; + let public_key = PublicKey::parse(&user_id).map_err(|e| e.to_string())?; let filter = Filter::new().author(public_key).kind(Kind::Metadata).limit(1); - match client.get_events_of(vec![filter], Some(Duration::from_secs(3))).await { + match client.get_events_of(vec![filter], Some(Duration::from_secs(2))).await { Ok(events) => { if let Some(event) = events.first() { Ok(Metadata::from_json(&event.content).unwrap_or(Metadata::new()).as_json()) diff --git a/src-tauri/src/commands/chat.rs b/src-tauri/src/commands/chat.rs index 5bd5c5d..245cab1 100644 --- a/src-tauri/src/commands/chat.rs +++ b/src-tauri/src/commands/chat.rs @@ -1,7 +1,6 @@ use itertools::Itertools; use nostr_sdk::prelude::*; use std::cmp::Reverse; -use std::time::Duration; use tauri::State; use crate::Nostr; @@ -54,62 +53,6 @@ pub async fn get_chat_messages(id: String, state: State<'_, Nostr>) -> Result) -> Result, String> { - let client = &state.client; - let public_key = PublicKey::parse(&id).map_err(|e| e.to_string())?; - let mut inbox_relays = state.inbox_relays.lock().await; - - if let Some(relays) = inbox_relays.get(&public_key) { - for relay in relays { - let _ = client.connect_relay(relay).await; - } - return Ok(relays.to_owned()); - } - - let inbox = Filter::new().kind(Kind::Custom(10050)).author(public_key).limit(1); - - match client.get_events_of(vec![inbox], Some(Duration::from_secs(2))).await { - Ok(events) => { - let mut relays = Vec::new(); - - if let Some(event) = events.into_iter().next() { - for tag in &event.tags { - if let Some(TagStandard::Relay(relay)) = tag.as_standardized() { - let url = relay.to_string(); - let _ = client.add_relay(&url).await; - let _ = client.connect_relay(&url).await; - - relays.push(url) - } - } - - inbox_relays.insert(public_key, relays.clone()); - } - - Ok(relays) - } - Err(e) => Err(e.to_string()), - } -} - -#[tauri::command] -#[specta::specta] -pub async fn disconnect_inbox(id: String, state: State<'_, Nostr>) -> Result<(), String> { - let client = &state.client; - let public_key = PublicKey::parse(&id).map_err(|e| e.to_string())?; - let inbox_relays = state.inbox_relays.lock().await; - - if let Some(relays) = inbox_relays.get(&public_key) { - for relay in relays { - let _ = client.disconnect_relay(relay).await; - } - } - - Ok(()) -} - #[tauri::command] #[specta::specta] pub async fn send_message( @@ -126,24 +69,27 @@ pub async fn send_message( // TODO: Add support reply_to let rumor = EventBuilder::private_msg_rumor(receiver, message, None); - // Get inbox relays + // Get inbox state let relays = state.inbox_relays.lock().await; + // Get inbox relays per member let outbox = relays.get(&receiver); let inbox = relays.get(&public_key); let outbox_urls = match outbox { Some(relays) => relays, - None => return Err("User's didn't have inbox relays to receive message.".into()), + None => return Err("Receiver didn't have inbox relays to receive message.".into()), }; let inbox_urls = match inbox { Some(relays) => relays, - None => return Err("User's didn't have inbox relays to receive message.".into()), + None => return Err("Please config inbox relays to backup your message.".into()), }; + // Send message to [receiver] match client.gift_wrap_to(outbox_urls, receiver, rumor.clone(), None).await { Ok(_) => { + // Send message to [yourself] if let Err(e) = client.gift_wrap_to(inbox_urls, public_key, rumor, None).await { return Err(e.to_string()); } diff --git a/src-tauri/src/commands/relay.rs b/src-tauri/src/commands/relay.rs index 816a480..1008c1d 100644 --- a/src-tauri/src/commands/relay.rs +++ b/src-tauri/src/commands/relay.rs @@ -2,11 +2,27 @@ use nostr_sdk::prelude::*; use std::{ fs::OpenOptions, io::{self, BufRead, Write}, + time::Duration, }; use tauri::{Manager, State}; use crate::Nostr; +async fn get_nip65_list(public_key: PublicKey, client: &Client) -> Vec { + let filter = Filter::new().author(public_key).kind(Kind::RelayList).limit(1); + let mut relay_list: Vec = Vec::new(); + + if let Ok(events) = client.get_events_of(vec![filter], Some(Duration::from_secs(10))).await { + if let Some(event) = events.first() { + for (url, ..) in nip65::extract_relay_list(event) { + relay_list.push(url.to_string()) + } + } + }; + + relay_list +} + #[tauri::command] #[specta::specta] pub fn get_bootstrap_relays(app: tauri::AppHandle) -> Result, String> { @@ -80,3 +96,85 @@ pub async fn set_inbox_relays(relays: Vec, state: State<'_, Nostr>) -> R Err(e) => Err(e.to_string()), } } + +#[tauri::command] +#[specta::specta] +pub async fn connect_inbox_relays( + user_id: String, + ignore_cache: bool, + state: State<'_, Nostr>, +) -> Result, String> { + let client = &state.client; + let public_key = PublicKey::parse(&user_id).map_err(|e| e.to_string())?; + let mut inbox_relays = state.inbox_relays.lock().await; + + if !ignore_cache { + if let Some(relays) = inbox_relays.get(&public_key) { + for relay in relays { + let _ = client.connect_relay(relay).await; + } + return Ok(relays.to_owned()); + }; + }; + + let inbox = Filter::new().kind(Kind::Custom(10050)).author(public_key).limit(1); + + match client.get_events_of(vec![inbox], Some(Duration::from_secs(2))).await { + Ok(events) => { + let mut relays = Vec::new(); + + if let Some(event) = events.into_iter().next() { + for tag in &event.tags { + if let Some(TagStandard::Relay(relay)) = tag.as_standardized() { + let url = relay.to_string(); + let _ = client.add_relay(&url).await; + let _ = client.connect_relay(&url).await; + + relays.push(url) + } + } + + inbox_relays.insert(public_key, relays.clone()); + } + + // Workaround for https://github.com/rust-nostr/nostr/issues/509 + // TODO: remove this + // let relays_clone = relays.clone(); + /*tauri::async_runtime::spawn(async move { + let state = handle.state::(); + let client = &state.client; + + client + .get_events_from( + relays_clone, + vec![Filter::new().kind(Kind::TextNote).limit(0)], + Some(Duration::from_secs(5)), + ) + .await + }); + */ + + Ok(relays) + } + Err(e) => Err(e.to_string()), + } +} + +#[tauri::command] +#[specta::specta] +pub async fn disconnect_inbox_relays( + user_id: String, + state: State<'_, Nostr>, +) -> Result<(), String> { + let client = &state.client; + let public_key = PublicKey::parse(&user_id).map_err(|e| e.to_string())?; + let inbox_relays = state.inbox_relays.lock().await; + + if let Some(relays) = inbox_relays.get(&public_key) { + for relay in relays { + let _ = client.disconnect_relay(relay).await; + } + } + + Ok(()) +} diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index c367742..c8d530e 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -31,6 +31,8 @@ fn main() { set_bootstrap_relays, get_inbox_relays, set_inbox_relays, + connect_inbox_relays, + disconnect_inbox_relays, login, delete_account, create_account, @@ -41,8 +43,6 @@ fn main() { get_contact_list, get_chats, get_chat_messages, - connect_inbox, - disconnect_inbox, send_message, ]); diff --git a/src/commands.ts b/src/commands.ts index 6a8e742..79c3956 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -36,6 +36,22 @@ try { else return { status: "error", error: e as any }; } }, +async connectInboxRelays(userId: string, ignoreCache: boolean) : Promise> { +try { + return { status: "ok", data: await TAURI_INVOKE("connect_inbox_relays", { userId, ignoreCache }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async disconnectInboxRelays(userId: string) : Promise> { +try { + return { status: "ok", data: await TAURI_INVOKE("disconnect_inbox_relays", { userId }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, async login(account: string, password: string) : Promise> { try { return { status: "ok", data: await TAURI_INVOKE("login", { account, password }) }; @@ -111,22 +127,6 @@ try { else return { status: "error", error: e as any }; } }, -async connectInbox(id: string) : Promise> { -try { - return { status: "ok", data: await TAURI_INVOKE("connect_inbox", { id }) }; -} catch (e) { - if(e instanceof Error) throw e; - else return { status: "error", error: e as any }; -} -}, -async disconnectInbox(id: string) : Promise> { -try { - return { status: "ok", data: await TAURI_INVOKE("disconnect_inbox", { id }) }; -} catch (e) { - if(e instanceof Error) throw e; - else return { status: "error", error: e as any }; -} -}, async sendMessage(to: string, message: string) : Promise> { try { return { status: "ok", data: await TAURI_INVOKE("send_message", { to, message }) }; diff --git a/src/routes/$account.chats.$id.lazy.tsx b/src/routes/$account.chats.$id.lazy.tsx index 7871a58..ee48f8f 100644 --- a/src/routes/$account.chats.$id.lazy.tsx +++ b/src/routes/$account.chats.$id.lazy.tsx @@ -296,7 +296,7 @@ function Form() { const [newMessage, setNewMessage] = useState(""); const [isPending, startTransition] = useTransition(); - const submit = async () => { + const submit = () => { startTransition(async () => { if (!newMessage.length) return; @@ -351,7 +351,7 @@ function AttachMedia({ }: { callback: Dispatch> }) { const [isPending, startTransition] = useTransition(); - const attach = async () => { + const attach = () => { startTransition(async () => { const file = await upload(); diff --git a/src/routes/$account.chats.$id.tsx b/src/routes/$account.chats.$id.tsx index b3a652f..132033a 100644 --- a/src/routes/$account.chats.$id.tsx +++ b/src/routes/$account.chats.$id.tsx @@ -1,11 +1,14 @@ +import { commands } from "@/commands"; import { createFileRoute } from "@tanstack/react-router"; -import { invoke } from "@tauri-apps/api/core"; export const Route = createFileRoute("/$account/chats/$id")({ loader: async ({ params }) => { - const inboxRelays: string[] = await invoke("connect_inbox", { - id: params.id, - }); - return inboxRelays; + const res = await commands.connectInboxRelays(params.id, false); + + if (res.status === "ok") { + return res.data; + } else { + return []; + } }, }); diff --git a/src/routes/$account.chats.lazy.tsx b/src/routes/$account.chats.lazy.tsx index daf51d4..3e4bb86 100644 --- a/src/routes/$account.chats.lazy.tsx +++ b/src/routes/$account.chats.lazy.tsx @@ -16,6 +16,7 @@ import { useQuery } from "@tanstack/react-query"; import { Link, Outlet, createLazyFileRoute } from "@tanstack/react-router"; import { listen } from "@tauri-apps/api/event"; import { Menu, MenuItem, PredefinedMenuItem } from "@tauri-apps/api/menu"; +import { readText } from "@tauri-apps/plugin-clipboard-manager"; import { message } from "@tauri-apps/plugin-dialog"; import { open } from "@tauri-apps/plugin-shell"; import type { NostrEvent } from "nostr-tools"; @@ -273,20 +274,41 @@ function Compose() { const navigate = Route.useNavigate(); - const sendMessage = async () => { + const pasteFromClipboard = async () => { + const val = await readText(); + setTarget(val); + }; + + const sendMessage = () => { startTransition(async () => { if (!newMessage.length) return; if (!target.length) return; - const res = await commands.sendMessage(target, newMessage); + // Connect to user's inbox relays + const connect = await commands.connectInboxRelays(target, false); - if (res.status === "ok") { - navigate({ - to: "/$account/chats/$id", - params: { account, id: target }, - }); + // Send message + if (connect.status === "ok") { + const res = await commands.sendMessage(target, newMessage); + + if (res.status === "ok") { + setTarget(""); + setNewMessage(""); + setIsOpen(false); + + navigate({ + to: "/$account/chats/$id", + params: { account, id: target }, + }); + } else { + await message(res.error, { title: "Send Message", kind: "error" }); + return; + } } else { - await message(res.error, { title: "Coop", kind: "error" }); + await message(connect.error, { + title: "Connect Inbox Relays", + kind: "error", + }); return; } }); @@ -307,7 +329,7 @@ function Compose() {
- Send to + Send to
To: - setTarget(e.target.value)} - disabled={isPending || isLoading} - className="flex-1 h-9 bg-transparent focus:outline-none placeholder:text-neutral-400 dark:placeholder:text-neutral-600" - /> +
+ setTarget(e.target.value)} + disabled={isPending || isLoading} + className="w-full pr-14 h-9 bg-transparent focus:outline-none placeholder:text-neutral-400 dark:placeholder:text-neutral-600" + /> + +
Message: @@ -339,7 +370,11 @@ function Compose() { onClick={() => sendMessage()} className="rounded-full size-7 inline-flex items-center justify-center bg-blue-300 hover:bg-blue-500 dark:bg-blue-700 dark:hover:bg-blue-800 text-white" > - + {isPending ? ( + + ) : ( + + )}
diff --git a/src/routes/$account.relays.lazy.tsx b/src/routes/$account.relays.lazy.tsx index 84f475f..e16906e 100644 --- a/src/routes/$account.relays.lazy.tsx +++ b/src/routes/$account.relays.lazy.tsx @@ -48,7 +48,7 @@ function Screen() { setRelays((prev) => prev.filter((item) => item !== relay)); }; - const submit = async () => { + const submit = () => { startTransition(async () => { if (!relays.length) { await message("You need to add at least 1 relay", { kind: "info" }); diff --git a/src/routes/bootstrap-relays.lazy.tsx b/src/routes/bootstrap-relays.lazy.tsx index cf45023..d104007 100644 --- a/src/routes/bootstrap-relays.lazy.tsx +++ b/src/routes/bootstrap-relays.lazy.tsx @@ -40,7 +40,7 @@ function Screen() { setRelays((prev) => prev.filter((item) => item !== relay)); }; - const submit = async () => { + const submit = () => { startTransition(async () => { if (!relays.length) { await message("You need to add at least 1 relay", { diff --git a/src/routes/create-account.lazy.tsx b/src/routes/create-account.lazy.tsx index 669daed..326428a 100644 --- a/src/routes/create-account.lazy.tsx +++ b/src/routes/create-account.lazy.tsx @@ -31,7 +31,7 @@ function Screen() { } }; - const submit = async () => { + const submit = () => { startTransition(async () => { if (!name.length) { await message("Please add your name", { diff --git a/src/routes/import-key.lazy.tsx b/src/routes/import-key.lazy.tsx index a9ea084..26d0a8d 100644 --- a/src/routes/import-key.lazy.tsx +++ b/src/routes/import-key.lazy.tsx @@ -23,7 +23,7 @@ function Screen() { setKey(val); }; - const submit = async () => { + const submit = () => { startTransition(async () => { if (!key.startsWith("nsec1") && !key.startsWith("ncryptsec")) { await message( diff --git a/src/routes/index.lazy.tsx b/src/routes/index.lazy.tsx index bb478f5..aab90b8 100644 --- a/src/routes/index.lazy.tsx +++ b/src/routes/index.lazy.tsx @@ -50,7 +50,7 @@ function Screen() { setValue(account); }; - const loginWith = async () => { + const loginWith = () => { startTransition(async () => { if (!value || !password) return; diff --git a/src/routes/nostr-connect.lazy.tsx b/src/routes/nostr-connect.lazy.tsx index ef0f486..215e22a 100644 --- a/src/routes/nostr-connect.lazy.tsx +++ b/src/routes/nostr-connect.lazy.tsx @@ -22,7 +22,7 @@ function Screen() { setUri(val); }; - const submit = async () => { + const submit = () => { startTransition(async () => { if (!uri.startsWith("bunker://")) { await message(