From 35620fb1a97b9048651bb14068ff04503dd603ee Mon Sep 17 00:00:00 2001 From: reya <123083837+reyamir@users.noreply.github.com> Date: Sun, 4 Aug 2024 09:34:02 +0700 Subject: [PATCH] feat: improve performance --- package.json | 1 + pnpm-lock.yaml | 26 ++++++++ src-tauri/src/commands/account.rs | 47 +++++++++++++- src-tauri/src/commands/chat.rs | 88 +++++++++----------------- src-tauri/src/common.rs | 65 ------------------- src-tauri/src/main.rs | 1 - src/routes/$account.chats.$id.lazy.tsx | 6 +- src/routes/$account.chats.lazy.tsx | 62 +++++++++++------- 8 files changed, 144 insertions(+), 152 deletions(-) delete mode 100644 src-tauri/src/common.rs diff --git a/package.json b/package.json index 04acd17..e0d3d5c 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "@phosphor-icons/react": "^2.1.7", "@radix-ui/react-avatar": "^1.1.0", "@radix-ui/react-dialog": "^1.1.1", + "@radix-ui/react-progress": "^1.1.0", "@radix-ui/react-scroll-area": "^1.1.0", "@tanstack/react-query": "^5.51.21", "@tanstack/react-router": "^1.46.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 487af4b..1c21618 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,6 +17,9 @@ importers: '@radix-ui/react-dialog': specifier: ^1.1.1 version: 1.1.1(react-dom@19.0.0-rc-d025ddd3-20240722(react@19.0.0-rc-d025ddd3-20240722))(react@19.0.0-rc-d025ddd3-20240722)(types-react-dom@19.0.0-rc.1)(types-react@19.0.0-rc.1) + '@radix-ui/react-progress': + specifier: ^1.1.0 + version: 1.1.0(react-dom@19.0.0-rc-d025ddd3-20240722(react@19.0.0-rc-d025ddd3-20240722))(react@19.0.0-rc-d025ddd3-20240722)(types-react-dom@19.0.0-rc.1)(types-react@19.0.0-rc.1) '@radix-ui/react-scroll-area': specifier: ^1.1.0 version: 1.1.0(react-dom@19.0.0-rc-d025ddd3-20240722(react@19.0.0-rc-d025ddd3-20240722))(react@19.0.0-rc-d025ddd3-20240722)(types-react-dom@19.0.0-rc.1)(types-react@19.0.0-rc.1) @@ -629,6 +632,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-progress@1.1.0': + resolution: {integrity: sha512-aSzvnYpP725CROcxAOEBVZZSIQVQdHgBr2QQFKySsaD14u8dNT0batuXI+AAGDdAHfXH8rbnHmjYFqVJ21KkRg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-scroll-area@1.1.0': resolution: {integrity: sha512-9ArIZ9HWhsrfqS765h+GZuLoxaRHD/j0ZWOWilsCvYTpYJp8XwCqNG7Dt9Nu/TItKOdgLGkOPCodQvDc+UMwYg==} peerDependencies: @@ -2181,6 +2197,16 @@ snapshots: '@types/react': types-react@19.0.0-rc.1 '@types/react-dom': types-react-dom@19.0.0-rc.1 + '@radix-ui/react-progress@1.1.0(react-dom@19.0.0-rc-d025ddd3-20240722(react@19.0.0-rc-d025ddd3-20240722))(react@19.0.0-rc-d025ddd3-20240722)(types-react-dom@19.0.0-rc.1)(types-react@19.0.0-rc.1)': + dependencies: + '@radix-ui/react-context': 1.1.0(react@19.0.0-rc-d025ddd3-20240722)(types-react@19.0.0-rc.1) + '@radix-ui/react-primitive': 2.0.0(react-dom@19.0.0-rc-d025ddd3-20240722(react@19.0.0-rc-d025ddd3-20240722))(react@19.0.0-rc-d025ddd3-20240722)(types-react-dom@19.0.0-rc.1)(types-react@19.0.0-rc.1) + react: 19.0.0-rc-d025ddd3-20240722 + react-dom: 19.0.0-rc-d025ddd3-20240722(react@19.0.0-rc-d025ddd3-20240722) + optionalDependencies: + '@types/react': types-react@19.0.0-rc.1 + '@types/react-dom': types-react-dom@19.0.0-rc.1 + '@radix-ui/react-scroll-area@1.1.0(react-dom@19.0.0-rc-d025ddd3-20240722(react@19.0.0-rc-d025ddd3-20240722))(react@19.0.0-rc-d025ddd3-20240722)(types-react-dom@19.0.0-rc.1)(types-react@19.0.0-rc.1)': dependencies: '@radix-ui/number': 1.1.0 diff --git a/src-tauri/src/commands/account.rs b/src-tauri/src/commands/account.rs index 6deb72a..9fc9fab 100644 --- a/src-tauri/src/commands/account.rs +++ b/src-tauri/src/commands/account.rs @@ -2,7 +2,7 @@ use keyring::Entry; use keyring_search::{Limit, List, Search}; use nostr_sdk::prelude::*; use serde::Serialize; -use std::{collections::HashSet, time::Duration}; +use std::{collections::HashSet, str::FromStr, time::Duration}; use tauri::{Emitter, Manager, State}; use crate::{Nostr, BOOTSTRAP_RELAYS}; @@ -252,6 +252,16 @@ pub async fn login( let _ = client.connect_relay(url).await; } + // Workaround for https://github.com/rust-nostr/nostr/issues/509 + // TODO: remove this + let _ = client + .get_events_from( + urls.clone(), + vec![Filter::new().kind(Kind::TextNote).limit(0)], + None, + ) + .await; + let mut inbox_relays = state.inbox_relays.lock().await; inbox_relays.insert(public_key, urls); } else { @@ -275,6 +285,41 @@ pub async fn login( let state = handle.state::(); let client = &state.client; + let filter = Filter::new().kind(Kind::GiftWrap).pubkey(public_key); + + if let Ok(events) = client + .get_events_of_with_opts( + vec![filter], + Some(Duration::from_secs(20)), + FilterOptions::WaitDurationAfterEOSE(Duration::from_secs(20)), + ) + .await + { + // Use fake sig, it doesn't matter. + // TODO: Find better way to save unsigned event to database. + let fake_sig = Signature::from_str("f9e79d141c004977192d05a86f81ec7c585179c371f7350a5412d33575a2a356433f58e405c2296ed273e2fe0aafa25b641e39cc4e1f3f261ebf55bce0cbac83").unwrap(); + + for event in events.iter() { + if let Ok(UnwrappedGift { rumor, .. }) = client.unwrap_gift_wrap(event).await { + let rumor_clone = rumor.clone(); + let ev = Event::new( + rumor_clone.id.unwrap(), + rumor_clone.pubkey, + rumor_clone.created_at, + rumor_clone.kind, + rumor_clone.tags, + rumor_clone.content, + fake_sig, + ); + + if let Err(e) = client.database().save_event(&ev).await { + println!("Error: {}", e) + } + } + } + handle.emit("synchronized", ()).unwrap(); + } + client .handle_notifications(|notification| async { if let RelayPoolNotification::Event { event, subscription_id, .. } = notification { diff --git a/src-tauri/src/commands/chat.rs b/src-tauri/src/commands/chat.rs index 09efd45..f3ba8a9 100644 --- a/src-tauri/src/commands/chat.rs +++ b/src-tauri/src/commands/chat.rs @@ -1,85 +1,57 @@ +use itertools::Itertools; use nostr_sdk::prelude::*; -use serde::Serialize; +use std::cmp::Reverse; use std::time::Duration; -use tauri::{Emitter, Manager, State}; +use tauri::State; -use crate::{ - common::{process_chat_event, process_message_event}, - Nostr, -}; - -#[derive(Clone, Serialize)] -pub struct ChatPayload { - events: Vec, -} +use crate::Nostr; #[tauri::command] #[specta::specta] -pub async fn get_chats( - state: State<'_, Nostr>, - handle: tauri::AppHandle, -) -> Result, String> { +pub async fn get_chats(state: State<'_, Nostr>) -> Result, String> { let client = &state.client; - let database = client.database(); let signer = client.signer().await.map_err(|e| e.to_string())?; let public_key = signer.public_key().await.map_err(|e| e.to_string())?; - let filter = Filter::new().kind(Kind::GiftWrap).pubkey(public_key); + let filter = Filter::new().kind(Kind::PrivateDirectMessage).pubkey(public_key); - let events = match database.query(vec![filter.clone()], Order::Desc).await { - Ok(events) => process_chat_event(client, events).await, - Err(e) => return Err(e.to_string()), - }; + match client.database().query(vec![filter.clone()], Order::Desc).await { + Ok(events) => { + let ev = events + .into_iter() + .sorted_by_key(|ev| Reverse(ev.created_at)) + .filter(|ev| ev.pubkey != public_key) + .unique_by(|ev| ev.pubkey) + .map(|ev| ev.as_json()) + .collect::>(); - tauri::async_runtime::spawn(async move { - let state = handle.state::(); - let client = &state.client; - - if let Ok(events) = client.get_events_of(vec![filter], None).await { - let rumors = process_chat_event(client, events).await; - handle.emit("sync_chat", ChatPayload { events: rumors }).unwrap(); + Ok(ev) } - }); - - Ok(events) + Err(e) => Err(e.to_string()), + } } #[tauri::command] #[specta::specta] -pub async fn get_chat_messages( - id: String, - state: State<'_, Nostr>, - handle: tauri::AppHandle, -) -> Result, String> { +pub async fn get_chat_messages(id: String, state: State<'_, Nostr>) -> Result, String> { let client = &state.client; - let database = client.database(); - let signer = client.signer().await.map_err(|e| e.to_string())?; - let public_key = signer.public_key().await.map_err(|e| e.to_string())?; + let receiver = signer.public_key().await.map_err(|e| e.to_string())?; let sender = PublicKey::parse(id.clone()).map_err(|e| e.to_string())?; - let group = vec![public_key, sender]; - let filter = Filter::new().kind(Kind::GiftWrap).pubkey(public_key); + let recv_filter = + Filter::new().kind(Kind::PrivateDirectMessage).author(sender).pubkey(receiver); + let sender_filter = + Filter::new().kind(Kind::PrivateDirectMessage).author(receiver).pubkey(sender); - let rumors = match database.query(vec![filter.clone()], Order::Desc).await { - Ok(events) => process_message_event(client, events, &group).await, - Err(e) => return Err(e.to_string()), - }; - - tauri::async_runtime::spawn(async move { - let state = handle.state::(); - let client = &state.client; - - if let Ok(events) = client.get_events_of(vec![filter], None).await { - let rumors = process_message_event(client, events, &group).await; - let emit_to = format!("sync_chat_{}", id); - - handle.emit(&emit_to, ChatPayload { events: rumors }).unwrap(); + match client.database().query(vec![recv_filter, sender_filter], Order::Desc).await { + Ok(events) => { + let ev = events.into_iter().map(|ev| ev.as_json()).collect::>(); + Ok(ev) } - }); - - Ok(rumors) + Err(e) => Err(e.to_string()), + } } #[tauri::command] diff --git a/src-tauri/src/common.rs b/src-tauri/src/common.rs deleted file mode 100644 index e418c43..0000000 --- a/src-tauri/src/common.rs +++ /dev/null @@ -1,65 +0,0 @@ -use std::cmp::Reverse; - -use futures::stream::{self, StreamExt}; -use itertools::Itertools; -use nostr_sdk::prelude::*; - -pub async fn process_chat_event(client: &Client, events: Vec) -> Vec { - let rumors = stream::iter(events) - .filter_map(|ev| async move { - if let Ok(UnwrappedGift { rumor, .. }) = client.unwrap_gift_wrap(&ev).await { - if rumor.kind == Kind::PrivateDirectMessage { - Some(rumor) - } else { - None - } - } else { - None - } - }) - .collect::>() - .await; - - let signer = client.signer().await.unwrap(); - let public_key = signer.public_key().await.unwrap(); - - rumors - .into_iter() - .sorted_by_key(|ev| Reverse(ev.created_at)) - .filter(|ev| ev.pubkey != public_key) - .unique_by(|ev| ev.pubkey) - .map(|ev| ev.as_json()) - .collect::>() -} - -pub async fn process_message_event( - client: &Client, - events: Vec, - group: &Vec, -) -> Vec { - stream::iter(events) - .filter_map(|ev| async move { - if let Ok(UnwrappedGift { rumor, sender }) = client.unwrap_gift_wrap(&ev).await { - if group.contains(&sender) && is_member(group, &rumor.tags) { - Some(rumor.as_json()) - } else { - None - } - } else { - None - } - }) - .collect::>() - .await -} - -pub fn is_member(group: &Vec, tags: &Vec) -> bool { - for tag in tags { - if let Some(TagStandard::PublicKey { public_key, .. }) = tag.as_standardized() { - if group.contains(public_key) { - return true; - } - } - } - false -} diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 9b8861e..146783d 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -12,7 +12,6 @@ use tauri_plugin_decorum::WebviewWindowExt; use commands::{account::*, chat::*}; mod commands; -mod common; pub struct Nostr { client: Client, diff --git a/src/routes/$account.chats.$id.lazy.tsx b/src/routes/$account.chats.$id.lazy.tsx index d775fb3..8a766a1 100644 --- a/src/routes/$account.chats.$id.lazy.tsx +++ b/src/routes/$account.chats.$id.lazy.tsx @@ -235,17 +235,15 @@ function List() { > {isLoading ? ( <> -
+
-
-
+
-
) : isError ? ( diff --git a/src/routes/$account.chats.lazy.tsx b/src/routes/$account.chats.lazy.tsx index 9d2b85b..e57cef4 100644 --- a/src/routes/$account.chats.lazy.tsx +++ b/src/routes/$account.chats.lazy.tsx @@ -10,6 +10,7 @@ import { X, } from "@phosphor-icons/react"; import * as Dialog from "@radix-ui/react-dialog"; +import * as Progress from "@radix-ui/react-progress"; import * as ScrollArea from "@radix-ui/react-scroll-area"; import { useQuery } from "@tanstack/react-query"; import { Link, Outlet, createLazyFileRoute } from "@tanstack/react-router"; @@ -19,10 +20,6 @@ import { message } from "@tauri-apps/plugin-dialog"; import type { NostrEvent } from "nostr-tools"; import { useCallback, useEffect, useState, useTransition } from "react"; -type ChatPayload = { - events: string[]; -}; - type EventPayload = { event: string; sender: string; @@ -98,21 +95,21 @@ function ChatList() { refetchOnWindowFocus: false, }); - useEffect(() => { - const unlisten = listen("sync_chat", async (data) => { - const raw = data.payload.events; - const events: NostrEvent[] = raw.map((item) => JSON.parse(item)); - const chats: NostrEvent[] = await queryClient.getQueryData(["chats"]); + const [isSync, setIsSync] = useState(false); + const [progress, setProgress] = useState(0); - if (chats?.length) { - const newEvents = [...events, ...chats]; - const uniqs = [ - ...new Map(newEvents.map((item) => [item.pubkey, item])).values(), - ]; - await queryClient.setQueryData(["chats"], uniqs); - } else { - await queryClient.setQueryData(["chats"], events); - } + useEffect(() => { + const timer = setInterval( + () => setProgress((prev) => (prev <= 100 ? prev + 4 : 100)), + 1200, + ); + return () => clearInterval(timer); + }, []); + + useEffect(() => { + const unlisten = listen("synchronized", async () => { + await queryClient.refetchQueries({ queryKey: ["chats"] }); + setIsSync(true); }); return () => { @@ -158,11 +155,11 @@ function ChatList() { - {isLoading || !data.length ? ( -
+ {isLoading || !isSync ? ( + <> {[...Array(5).keys()].map((i) => (
))} -
- ) : !data?.length ? ( + + ) : isSync && !data.length ? (
No chats. @@ -213,6 +210,25 @@ function ChatList() { )) )} + {!isSync ? ( +
+
+ + + + Syncing message... +
+
+ ) : null}