diff --git a/src-tauri/src/commands/account.rs b/src-tauri/src/commands/account.rs index 75a971f..781b86e 100644 --- a/src-tauri/src/commands/account.rs +++ b/src-tauri/src/commands/account.rs @@ -1,23 +1,16 @@ use keyring::Entry; use keyring_search::{Limit, List, Search}; use nostr_sdk::prelude::*; -use serde::Serialize; use std::{collections::HashSet, time::Duration}; -use tauri::{Emitter, Manager, State}; +use tauri::State; use crate::{Nostr, BOOTSTRAP_RELAYS}; -#[derive(Clone, Serialize)] -struct EventPayload { - event: String, - sender: String, -} - #[tauri::command] #[specta::specta] pub fn get_accounts() -> Vec { let search = Search::new().expect("Unexpected."); - let results = search.by_user("nostr_secret"); + let results = search.by_service("coop"); let list = List::list_credentials(&results, Limit::All); let accounts: HashSet = list.split_whitespace().filter(|v| v.starts_with("npub1")).map(String::from).collect(); @@ -58,7 +51,7 @@ pub async fn create_account( let nsec = keys.secret_key().unwrap().to_bech32().map_err(|e| e.to_string())?; // Save account - let keyring = Entry::new(&npub, "nostr_secret").unwrap(); + let keyring = Entry::new("coop", &npub).unwrap(); let _ = keyring.set_password(&nsec); let signer = NostrSigner::Keys(keys); @@ -103,7 +96,7 @@ pub async fn import_key( let npub = nostr_keys.public_key().to_bech32().unwrap(); let nsec = nostr_keys.secret_key().unwrap().to_bech32().unwrap(); - let keyring = Entry::new(&npub, "nostr_secret").unwrap(); + let keyring = Entry::new("coop", &npub).unwrap(); let _ = keyring.set_password(&nsec); let signer = NostrSigner::Keys(nostr_keys); @@ -134,7 +127,7 @@ pub async fn connect_account(uri: &str, state: State<'_, Nostr>) -> Result { - let keyring = Entry::new(&remote_npub, "nostr_secret").unwrap(); + let keyring = Entry::new("coop", &remote_npub).unwrap(); let _ = keyring.set_password(&app_secret); // Update signer @@ -214,12 +207,11 @@ 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())?; let hex = public_key.to_hex(); - let keyring = Entry::new(&id, "nostr_secret").expect("Unexpected."); + let keyring = Entry::new("coop", &id).expect("Unexpected."); let password = match keyring.get_password() { Ok(pw) => pw, @@ -280,7 +272,7 @@ pub async fn login( } } - let sub_id = SubscriptionId::new("personal_inbox"); + let sub_id = SubscriptionId::new("inbox"); let new_message = Filter::new().kind(Kind::GiftWrap).pubkey(public_key).limit(0); if client.subscription(&sub_id).await.is_some() { @@ -292,31 +284,5 @@ pub async fn login( let _ = client.subscribe_with_id(sub_id, vec![new_message], None).await; } - tauri::async_runtime::spawn(async move { - let window = handle.get_webview_window("main").expect("Window is terminated."); - let state = window.state::(); - let client = &state.client; - - client - .handle_notifications(|notification| async { - if let RelayPoolNotification::Event { event, .. } = notification { - if event.kind == Kind::GiftWrap { - if let Ok(UnwrappedGift { rumor, sender }) = - client.unwrap_gift_wrap(&event).await - { - if let Err(e) = window.emit( - "event", - EventPayload { event: rumor.as_json(), sender: sender.to_hex() }, - ) { - println!("emit failed: {}", e) - } - } - } - } - Ok(false) - }) - .await - }); - Ok(hex) } diff --git a/src-tauri/src/commands/chat.rs b/src-tauri/src/commands/chat.rs index cb0dc6a..09efd45 100644 --- a/src-tauri/src/commands/chat.rs +++ b/src-tauri/src/commands/chat.rs @@ -1,85 +1,84 @@ -use futures::stream::{self, StreamExt}; -use itertools::Itertools; use nostr_sdk::prelude::*; -use std::{cmp::Reverse, time::Duration}; -use tauri::State; +use serde::Serialize; +use std::time::Duration; +use tauri::{Emitter, Manager, State}; -use crate::{common::is_member, Nostr}; +use crate::{ + common::{process_chat_event, process_message_event}, + Nostr, +}; + +#[derive(Clone, Serialize)] +pub struct ChatPayload { + events: Vec, +} #[tauri::command] #[specta::specta] -pub async fn get_chats(state: State<'_, Nostr>) -> Result, String> { +pub async fn get_chats( + state: State<'_, Nostr>, + handle: tauri::AppHandle, +) -> 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 rumors = match client.get_events_of(vec![filter], Some(Duration::from_secs(20))).await { - Ok(events) => { - 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 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()), }; - let uniqs = 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::>(); + tauri::async_runtime::spawn(async move { + let state = handle.state::(); + let client = &state.client; - Ok(uniqs) + 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(events) } #[tauri::command] #[specta::specta] -pub async fn get_chat_messages(id: String, state: State<'_, Nostr>) -> Result, String> { +pub async fn get_chat_messages( + id: String, + state: State<'_, Nostr>, + handle: tauri::AppHandle, +) -> Result, String> { let client = &state.client; + let database = client.database(); let signer = client.signer().await.map_err(|e| e.to_string())?; - let receiver_pk = signer.public_key().await.map_err(|e| e.to_string())?; - let sender_pk = PublicKey::parse(id).map_err(|e| e.to_string())?; - let filter = Filter::new().kind(Kind::GiftWrap).pubkeys(vec![receiver_pk, sender_pk]); + let public_key = signer.public_key().await.map_err(|e| e.to_string())?; + let sender = PublicKey::parse(id.clone()).map_err(|e| e.to_string())?; - let rumors = match client.get_events_of(vec![filter], None).await { - Ok(events) => { - stream::iter(events) - .filter_map(|ev| async move { - if let Ok(UnwrappedGift { rumor, sender }) = client.unwrap_gift_wrap(&ev).await - { - let groups = vec![&receiver_pk, &sender_pk]; + let group = vec![public_key, sender]; + let filter = Filter::new().kind(Kind::GiftWrap).pubkey(public_key); - if groups.contains(&&sender) && is_member(groups, &rumor.tags) { - Some(rumor.as_json()) - } else { - None - } - } else { - None - } - }) - .collect::>() - .await - } + 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(); + } + }); + Ok(rumors) } diff --git a/src-tauri/src/common.rs b/src-tauri/src/common.rs index 1ff394e..e418c43 100644 --- a/src-tauri/src/common.rs +++ b/src-tauri/src/common.rs @@ -1,9 +1,62 @@ +use std::cmp::Reverse; + +use futures::stream::{self, StreamExt}; +use itertools::Itertools; use nostr_sdk::prelude::*; -pub fn is_member(groups: Vec<&PublicKey>, tags: &Vec) -> bool { +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 groups.contains(&public_key) { + if group.contains(public_key) { return true; } } diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 63066f1..9b8861e 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -24,7 +24,7 @@ pub const BOOTSTRAP_RELAYS: [&str; 4] = [ "wss://relay.damus.io/", "wss://relay.nostr.net/", "wss://relay.0xchat.com/", - "wss://nostr.wine/", + "wss://auth.nostr1.com/", ]; fn main() { diff --git a/src/routes/$account.chats.$id.lazy.tsx b/src/routes/$account.chats.$id.lazy.tsx index 8c7d97c..8dda7bc 100644 --- a/src/routes/$account.chats.$id.lazy.tsx +++ b/src/routes/$account.chats.$id.lazy.tsx @@ -13,7 +13,11 @@ import { useCallback, useRef, useState, useTransition } from "react"; import { useEffect } from "react"; import { Virtualizer } from "virtua"; -type Payload = { +type ChatPayload = { + events: string[]; +}; + +type EventPayload = { event: string; sender: string; }; @@ -140,7 +144,29 @@ function List() { ); useEffect(() => { - const unlisten = listen("event", async (data) => { + const unlisten = listen(`sync_chat_${id}`, async (data) => { + const raw = data.payload.events; + const events: NostrEvent[] = raw.map((item) => JSON.parse(item)); + const chats: NostrEvent[] = await queryClient.getQueryData(["chats", id]); + + if (chats?.length) { + const newEvents = [...events, ...chats]; + const dedup = newEvents.filter( + (obj1, i, arr) => arr.findIndex((obj2) => obj2.id === obj1.id) === i, + ); + await queryClient.setQueryData(["chats", id], dedup); + } else { + await queryClient.setQueryData(["chats", id], events); + } + }); + + return () => { + unlisten.then((f) => f()); + }; + }, []); + + useEffect(() => { + const unlisten = listen("event", async (data) => { const event: NostrEvent = JSON.parse(data.payload.event); const sender = data.payload.sender; const receivers = getReceivers(event.tags); diff --git a/src/routes/$account.chats.lazy.tsx b/src/routes/$account.chats.lazy.tsx index a1a2c31..9d2b85b 100644 --- a/src/routes/$account.chats.lazy.tsx +++ b/src/routes/$account.chats.lazy.tsx @@ -19,6 +19,10 @@ 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; @@ -95,8 +99,20 @@ function ChatList() { }); useEffect(() => { - const unlisten = listen("synchronized", async () => { - await queryClient.refetchQueries({ queryKey: ["chats"] }); + 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"]); + + 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); + } }); return () => {