diff --git a/src-tauri/src/commands/account.rs b/src-tauri/src/commands/account.rs index 781b86e..b492f91 100644 --- a/src-tauri/src/commands/account.rs +++ b/src-tauri/src/commands/account.rs @@ -1,11 +1,18 @@ use keyring::Entry; use keyring_search::{Limit, List, Search}; use nostr_sdk::prelude::*; +use serde::Serialize; use std::{collections::HashSet, time::Duration}; -use tauri::State; +use tauri::{Emitter, Manager, State}; use crate::{Nostr, BOOTSTRAP_RELAYS}; +#[derive(Clone, Serialize)] +pub struct EventPayload { + event: String, // JSON String + sender: String, +} + #[tauri::command] #[specta::specta] pub fn get_accounts() -> Vec { @@ -207,6 +214,7 @@ pub async fn login( id: String, bunker: Option, state: State<'_, Nostr>, + handle: tauri::AppHandle, ) -> Result { let client = &state.client; let public_key = PublicKey::parse(&id).map_err(|e| e.to_string())?; @@ -281,8 +289,31 @@ pub async fn login( // Resubscribe new message for current user let _ = client.subscribe_with_id(sub_id.clone(), vec![new_message], None).await; } else { - let _ = client.subscribe_with_id(sub_id, vec![new_message], None).await; + let _ = client.subscribe_with_id(sub_id.clone(), vec![new_message], None).await; } + tauri::async_runtime::spawn(async move { + let state = handle.state::(); + let client = &state.client; + + client + .handle_notifications(|notification| async { + if let RelayPoolNotification::Event { event, subscription_id, .. } = notification { + if subscription_id == sub_id && event.kind == Kind::GiftWrap { + if let Ok(UnwrappedGift { rumor, sender }) = + client.unwrap_gift_wrap(&event).await + { + let payload = + EventPayload { event: rumor.as_json(), sender: sender.to_hex() }; + + handle.emit("event", payload).unwrap(); + } + } + } + Ok(false) + }) + .await + }); + Ok(hex) } diff --git a/src/routes/$account.chats.$id.lazy.tsx b/src/routes/$account.chats.$id.lazy.tsx index 8dda7bc..d775fb3 100644 --- a/src/routes/$account.chats.$id.lazy.tsx +++ b/src/routes/$account.chats.$id.lazy.tsx @@ -9,9 +9,15 @@ import { createLazyFileRoute } from "@tanstack/react-router"; import { listen } from "@tauri-apps/api/event"; import { message } from "@tauri-apps/plugin-dialog"; import type { NostrEvent } from "nostr-tools"; -import { useCallback, useRef, useState, useTransition } from "react"; +import { + useCallback, + useLayoutEffect, + useRef, + useState, + useTransition, +} from "react"; import { useEffect } from "react"; -import { Virtualizer } from "virtua"; +import { Virtualizer, type VirtualizerHandle } from "virtua"; type ChatPayload = { events: string[]; @@ -104,7 +110,10 @@ function List() { }); const queryClient = useQueryClient(); - const ref = useRef(null); + const scrollRef = useRef(null); + const ref = useRef(null); + const isPrepend = useRef(false); + const shouldStickToBottom = useRef(true); const renderItem = useCallback( (item: NostrEvent, idx: number) => { @@ -189,6 +198,20 @@ function List() { }; }, []); + useEffect(() => { + if (!data?.length) return; + if (!ref.current) return; + if (!shouldStickToBottom.current) return; + + ref.current.scrollToIndex(data.length - 1, { + align: "end", + }); + }, [data]); + + useLayoutEffect(() => { + isPrepend.current = false; + }); + return ( - + { + if (!ref.current) return; + shouldStickToBottom.current = + offset - ref.current.scrollSize + ref.current.viewportSize >= + -1.5; + }} + > {isLoading ? ( <>