From d2b5ae050791c9f43c92be84ce18013c5a342104 Mon Sep 17 00:00:00 2001 From: reya Date: Tue, 8 Oct 2024 10:36:31 +0700 Subject: [PATCH] feat: use negentropy as much as possible --- src-tauri/resources/relays.txt | 3 +- src-tauri/src/commands/account.rs | 237 +++++++++--------- src-tauri/src/commands/metadata.rs | 9 +- src-tauri/src/main.rs | 66 ++--- src/commands.gen.ts | 3 + src/components/repost.tsx | 2 +- src/routes/__root.tsx | 13 + .../columns/_layout/groups.$id.lazy.tsx | 14 +- .../columns/_layout/interests.$id.lazy.tsx | 16 +- src/routes/columns/_layout/newsfeed.lazy.tsx | 12 - src/routes/loading.tsx | 23 +- src/system/hooks/useEvent.ts | 13 +- 12 files changed, 202 insertions(+), 209 deletions(-) diff --git a/src-tauri/resources/relays.txt b/src-tauri/resources/relays.txt index 04b51823..d2969fba 100644 --- a/src-tauri/resources/relays.txt +++ b/src-tauri/resources/relays.txt @@ -1,3 +1,4 @@ wss://relay.damus.io, wss://relay.nostr.net, -wss://nos.lol, +wss://nostr.fmt.wiz.biz, +wss://offchain.pub, diff --git a/src-tauri/src/commands/account.rs b/src-tauri/src/commands/account.rs index 40dd3516..f260f298 100644 --- a/src-tauri/src/commands/account.rs +++ b/src-tauri/src/commands/account.rs @@ -10,8 +10,12 @@ use std::{ time::Duration, }; use tauri::{Emitter, Manager, State}; +use tokio::time::sleep; -use crate::{common::init_nip65, Nostr, NOTIFICATION_SUB_ID}; +use crate::{ + common::{get_latest_event, init_nip65}, + Nostr, NOTIFICATION_SUB_ID, +}; #[derive(Debug, Clone, Serialize, Deserialize, Type)] struct Account { @@ -217,6 +221,17 @@ pub fn is_account_sync(id: String, handle: tauri::AppHandle) -> bool { fs::metadata(config_dir.join(id)).is_ok() } +#[tauri::command] +#[specta::specta] +pub fn create_sync_file(id: String, handle: tauri::AppHandle) -> bool { + let config_dir = handle + .path() + .app_config_dir() + .expect("Error: app config directory not found."); + + File::create(config_dir.join(id)).is_ok() +} + #[tauri::command] #[specta::specta] pub async fn login( @@ -273,93 +288,27 @@ pub async fn login( // NIP-03: Get user's contact list let contact_list = { - let contacts = client.get_contact_list(None).await.unwrap(); - // Update app's state - state.contact_list.lock().await.clone_from(&contacts); - // Return - contacts + if let Ok(contacts) = client.get_contact_list(Some(Duration::from_secs(5))).await { + state.contact_list.lock().await.clone_from(&contacts); + contacts + } else { + Vec::new() + } }; - // Run seperate thread for syncing data - let pk = public_key.clone(); + let public_key_clone = public_key.clone(); + + // Run seperate thread for sync tauri::async_runtime::spawn(async move { - let config_dir = handle.path().app_config_dir().unwrap(); let state = handle.state::(); let client = &state.client; - - // Convert current user to PublicKey - let author = PublicKey::from_str(&pk).unwrap(); - - // Fetching user's metadata - if let Ok(report) = client - .reconcile( - Filter::new() - .author(author) - .kinds(vec![ - Kind::Metadata, - Kind::ContactList, - Kind::MuteList, - Kind::Bookmarks, - Kind::Interests, - Kind::InterestSet, - Kind::FollowSet, - Kind::PinList, - Kind::EventDeletion, - ]) - .limit(1000), - NegentropyOptions::default(), - ) - .await - { - println!("Received: {}", report.received.len()) - } - - // Fetching user's events - if let Ok(report) = client - .reconcile( - Filter::new() - .author(author) - .kinds(vec![Kind::TextNote, Kind::Repost]) - .limit(200), - NegentropyOptions::default(), - ) - .await - { - println!("Received: {}", report.received.len()) - } - - // Fetching user's notification - if let Ok(report) = client - .reconcile( - Filter::new() - .pubkey(author) - .kinds(vec![ - Kind::TextNote, - Kind::Repost, - Kind::Reaction, - Kind::ZapReceipt, - ]) - .limit(200), - NegentropyOptions::default(), - ) - .await - { - println!("Received: {}", report.received.len()) - } + let author = PublicKey::from_str(&public_key).unwrap(); // Subscribe for new notification if let Ok(e) = client .subscribe_with_id( SubscriptionId::new(NOTIFICATION_SUB_ID), - vec![Filter::new() - .pubkey(author) - .kinds(vec![ - Kind::TextNote, - Kind::Repost, - Kind::Reaction, - Kind::ZapReceipt, - ]) - .since(Timestamp::now())], + vec![Filter::new().pubkey(author).since(Timestamp::now())], None, ) .await @@ -371,13 +320,82 @@ pub async fn login( if !contact_list.is_empty() { let authors: Vec = contact_list.iter().map(|f| f.public_key).collect(); - // Fetching contact's metadata + // Syncing all metadata events from contact list if let Ok(report) = client .reconcile( Filter::new() .authors(authors.clone()) - .kinds(vec![Kind::Metadata, Kind::ContactList, Kind::MuteList]) - .limit(3000), + .kinds(vec![Kind::Metadata, Kind::ContactList]) + .limit(authors.len() * 10), + NegentropyOptions::default(), + ) + .await + { + println!("Received: {}", report.received.len()); + } + + // Syncing all events from contact list + if let Ok(report) = client + .reconcile( + Filter::new() + .authors(authors.clone()) + .kinds(vec![Kind::TextNote, Kind::Repost]) + .limit(authors.len() * 50), + NegentropyOptions::default(), + ) + .await + { + println!("Received: {}", report.received.len()); + } + + // Create the trusted public key list from contact list + // TODO: create a cached file + let mut trusted_list: HashSet = HashSet::new(); + + for author in authors.into_iter() { + trusted_list.insert(author); + + let filter = Filter::new() + .author(author) + .kind(Kind::ContactList) + .limit(1); + + if let Ok(events) = client.database().query(vec![filter]).await { + if let Some(event) = get_latest_event(&events) { + for tag in event.tags.iter() { + if let Some(TagStandard::PublicKey { + public_key, + uppercase: false, + .. + }) = tag.to_owned().to_standardized() + { + trusted_list.insert(public_key); + }; + } + } + } + } + + // Update app's state + state.trusted_list.lock().await.clone_from(&trusted_list); + + // Syncing all user's events + if let Ok(report) = client + .reconcile(Filter::new().author(author), NegentropyOptions::default()) + .await + { + println!("Received: {}", report.received.len()) + } + + // Syncing all tagged events for current user + if let Ok(report) = client + .reconcile( + Filter::new().pubkey(author).kinds(vec![ + Kind::TextNote, + Kind::Repost, + Kind::Reaction, + Kind::ZapReceipt, + ]), NegentropyOptions::default(), ) .await @@ -385,57 +403,30 @@ pub async fn login( println!("Received: {}", report.received.len()) } - // Fetching contact's events + // Syncing all events for trusted list + let trusted: Vec = trusted_list.into_iter().collect(); if let Ok(report) = client .reconcile( Filter::new() - .authors(authors.clone()) - .kinds(vec![Kind::TextNote, Kind::Repost]) - .limit(1000), + .authors(trusted) + .kinds(vec![Kind::Metadata, Kind::TextNote, Kind::Repost]) + .limit(20000), NegentropyOptions::default(), ) .await { - println!("Received: {}", report.received.len()); - - // Save the process status - let _ = File::create(config_dir.join(author.to_bech32().unwrap())); - // Update frontend - handle.emit("synchronized", ()).unwrap(); - }; - - for author in authors.into_iter() { - let filter = Filter::new() - .author(author) - .kind(Kind::ContactList) - .limit(1); - - let mut circles = state.circles.lock().await; - let mut list: Vec = Vec::new(); - - if let Ok(events) = client.database().query(vec![filter]).await { - if let Some(event) = events.into_iter().next() { - for tag in event.tags.into_iter() { - if let Some(TagStandard::PublicKey { - public_key, - uppercase: false, - .. - }) = tag.to_standardized() - { - list.push(public_key) - } - } - - if !list.is_empty() { - circles.insert(author, list); - }; - } - } + println!("Received: {}", report.received.len()) } - } else { - handle.emit("synchronized", ()).unwrap(); + + // Wait a little longer + // TODO: remove? + sleep(Duration::from_secs(5)).await; } + + handle + .emit("neg_synchronized", ()) + .expect("Something wrong!"); }); - Ok(public_key) + Ok(public_key_clone) } diff --git a/src-tauri/src/commands/metadata.rs b/src-tauri/src/commands/metadata.rs index 394859df..b49e4a16 100644 --- a/src-tauri/src/commands/metadata.rs +++ b/src-tauri/src/commands/metadata.rs @@ -48,7 +48,7 @@ pub async fn get_profile(id: Option, state: State<'_, Nostr>) -> Result< match client.database().query(vec![filter.clone()]).await { Ok(events) => { - if let Some(event) = events.first() { + if let Some(event) = get_latest_event(&events) { if let Ok(metadata) = Metadata::from_json(&event.content) { Ok(metadata.as_json()) } else { @@ -63,7 +63,7 @@ pub async fn get_profile(id: Option, state: State<'_, Nostr>) -> Result< .await { Ok(events) => { - if let Some(event) = events.first() { + if let Some(event) = get_latest_event(&events) { if let Ok(metadata) = Metadata::from_json(&event.content) { Ok(metadata.as_json()) } else { @@ -652,9 +652,8 @@ pub async fn verify_nip05(id: String, nip05: &str) -> Result { #[tauri::command] #[specta::specta] pub async fn is_trusted_user(id: String, state: State<'_, Nostr>) -> Result { - let circles = &state.circles.lock().await; + let trusted_list = &state.trusted_list.lock().await; let public_key = PublicKey::from_str(&id).map_err(|e| e.to_string())?; - let trusted = circles.values().any(|v| v.contains(&public_key)); - Ok(trusted) + Ok(trusted_list.contains(&public_key)) } diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index fd528e5b..5717378f 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -7,13 +7,12 @@ use border::WebviewWindowExt as BorderWebviewWindowExt; use commands::{account::*, event::*, metadata::*, relay::*, window::*}; use common::parse_event; -use nostr_relay_builder::prelude::*; -use nostr_sdk::prelude::*; +use nostr_sdk::prelude::{Profile as DatabaseProfile, *}; use serde::{Deserialize, Serialize}; use specta::Type; use specta_typescript::Typescript; use std::{ - collections::HashMap, + collections::HashSet, fs, io::{self, BufRead}, str::FromStr, @@ -32,7 +31,7 @@ pub struct Nostr { client: Client, settings: Mutex, contact_list: Mutex>, - circles: Mutex>>, + trusted_list: Mutex>, } #[derive(Clone, Serialize, Deserialize, Type)] @@ -84,7 +83,6 @@ struct NewSettings(Settings); pub const DEFAULT_DIFFICULTY: u8 = 21; pub const FETCH_LIMIT: usize = 100; -pub const NEWSFEED_NEG_LIMIT: usize = 512; pub const NOTIFICATION_NEG_LIMIT: usize = 64; pub const NOTIFICATION_SUB_ID: &str = "lume_notification"; @@ -108,6 +106,7 @@ fn main() { delete_account, reset_password, is_account_sync, + create_sync_file, login, get_profile, set_profile, @@ -274,7 +273,7 @@ fn main() { } // Connect - client.connect().await; + client.connect_with_timeout(Duration::from_secs(20)).await; client }); @@ -284,7 +283,7 @@ fn main() { client, settings: Mutex::new(Settings::default()), contact_list: Mutex::new(Vec::new()), - circles: Mutex::new(HashMap::new()), + trusted_list: Mutex::new(HashSet::new()), }); Subscription::listen_any(app, move |event| { @@ -432,14 +431,15 @@ fn main() { // Send native notification if allow_notification { let author = client - .fetch_metadata( - event.pubkey, - Some(Duration::from_secs(3)), - ) + .database() + .profile(event.pubkey) .await - .unwrap_or_else(|_| Metadata::new()); + .unwrap_or_else(|_| { + DatabaseProfile::new(event.pubkey, Metadata::new()) + }); + let metadata = author.metadata(); - send_event_notification(&event, author, &handle_clone); + send_event_notification(&event, metadata, &handle_clone); } } @@ -472,30 +472,36 @@ fn main() { if let tauri::WindowEvent::Focused(focused) = event { if !focused { let handle = window.app_handle().to_owned(); + let config_dir = handle.path().app_config_dir().unwrap(); tauri::async_runtime::spawn(async move { let state = handle.state::(); let client = &state.client; - if client.signer().await.is_ok() { - if let Ok(contact_list) = - client.get_contact_list(Some(Duration::from_secs(5))).await - { - let authors: Vec = - contact_list.iter().map(|f| f.public_key).collect(); + if let Ok(signer) = client.signer().await { + let public_key = signer.public_key().await.unwrap(); + let bech32 = public_key.to_bech32().unwrap(); - if client - .reconcile( - Filter::new() - .authors(authors) - .kinds(vec![Kind::TextNote, Kind::Repost]) - .limit(NEWSFEED_NEG_LIMIT), - NegentropyOptions::default(), - ) - .await - .is_ok() + if fs::metadata(config_dir.join(bech32)).is_ok() { + if let Ok(contact_list) = + client.get_contact_list(Some(Duration::from_secs(5))).await { - handle.emit("synchronized", ()).unwrap(); + let authors: Vec = + contact_list.iter().map(|f| f.public_key).collect(); + + if client + .reconcile( + Filter::new() + .authors(authors) + .kinds(vec![Kind::TextNote, Kind::Repost]) + .limit(1000), + NegentropyOptions::default(), + ) + .await + .is_ok() + { + handle.emit("synchronized", ()).unwrap(); + } } } } diff --git a/src/commands.gen.ts b/src/commands.gen.ts index 97edf48d..b0d8d332 100644 --- a/src/commands.gen.ts +++ b/src/commands.gen.ts @@ -99,6 +99,9 @@ async resetPassword(key: string, password: string) : Promise { return await TAURI_INVOKE("is_account_sync", { id }); }, +async createSyncFile(id: string) : Promise { + return await TAURI_INVOKE("create_sync_file", { id }); +}, async login(account: string, password: string) : Promise> { try { return { status: "ok", data: await TAURI_INVOKE("login", { account, password }) }; diff --git a/src/components/repost.tsx b/src/components/repost.tsx index e93fd8eb..ad2c7522 100644 --- a/src/components/repost.tsx +++ b/src/components/repost.tsx @@ -12,7 +12,7 @@ export const RepostNote = memo(function RepostNote({ event: LumeEvent; className?: string; }) { - const { isLoading, isError, data } = useEvent(event.repostId); + const { isLoading, isError, data } = useEvent(event.repostId, event.content); return ( diff --git a/src/routes/__root.tsx b/src/routes/__root.tsx index 24240556..7882d0d9 100644 --- a/src/routes/__root.tsx +++ b/src/routes/__root.tsx @@ -3,6 +3,7 @@ import { appSettings } from "@/commons"; import { Spinner } from "@/components"; import type { QueryClient } from "@tanstack/react-query"; import { Outlet, createRootRouteWithContext } from "@tanstack/react-router"; +import { listen } from "@tauri-apps/api/event"; import type { OsType } from "@tauri-apps/plugin-os"; import { useEffect } from "react"; @@ -17,6 +18,8 @@ export const Route = createRootRouteWithContext()({ }); function Screen() { + const { queryClient } = Route.useRouteContext(); + useEffect(() => { const unlisten = events.newSettings.listen((data) => { appSettings.setState((state) => { @@ -29,6 +32,16 @@ function Screen() { }; }, []); + useEffect(() => { + const unlisten = listen("synchronized", async () => { + await queryClient.invalidateQueries(); + }); + + return () => { + unlisten.then((f) => f()); + }; + }, []); + return ; } diff --git a/src/routes/columns/_layout/groups.$id.lazy.tsx b/src/routes/columns/_layout/groups.$id.lazy.tsx index 3582ea6a..75aeb320 100644 --- a/src/routes/columns/_layout/groups.$id.lazy.tsx +++ b/src/routes/columns/_layout/groups.$id.lazy.tsx @@ -7,8 +7,7 @@ import { ArrowDown } from "@phosphor-icons/react"; import * as ScrollArea from "@radix-ui/react-scroll-area"; import { useInfiniteQuery } from "@tanstack/react-query"; import { createLazyFileRoute } from "@tanstack/react-router"; -import { listen } from "@tauri-apps/api/event"; -import { useCallback, useEffect, useRef } from "react"; +import { useCallback, useRef } from "react"; import { Virtualizer } from "virtua"; export const Route = createLazyFileRoute("/columns/_layout/groups/$id")({ @@ -18,7 +17,6 @@ export const Route = createLazyFileRoute("/columns/_layout/groups/$id")({ export function Screen() { const group = Route.useLoaderData(); const params = Route.useParams(); - const { queryClient } = Route.useRouteContext(); const { data, @@ -84,16 +82,6 @@ export function Screen() { [data], ); - useEffect(() => { - const unlisten = listen("synchronized", async () => { - await queryClient.invalidateQueries({ queryKey: ["groups", params.id] }); - }); - - return () => { - unlisten.then((f) => f()); - }; - }, []); - return ( { - const unlisten = listen("synchronized", async () => { - await queryClient.invalidateQueries({ - queryKey: ["hashtags", params.id], - }); - }); - - return () => { - unlisten.then((f) => f()); - }; - }, []); - return ( { - const unlisten = listen("synchronized", async () => { - await queryClient.invalidateQueries({ queryKey: [label, account] }); - }); - - return () => { - unlisten.then((f) => f()); - }; - }, []); - return ( { - const unlisten = listen("synchronized", () => { - navigate({ - to: "/$account/home", - // @ts-ignore, this is tanstack router bug - params: { account: search.account }, - replace: true, - }); + const unlisten = listen("neg_synchronized", async () => { + const status = await commands.createSyncFile(search.account); + + if (status) { + navigate({ + to: "/$account/home", + // @ts-ignore, this is tanstack router bug + params: { account: search.account }, + replace: true, + }); + } else { + throw new Error("System error."); + } }); return () => { @@ -43,7 +50,7 @@ function Screen() { >

- Fetching necessary data for the first time login... + Syncing all necessary data for the first time login...

diff --git a/src/system/hooks/useEvent.ts b/src/system/hooks/useEvent.ts index 9260a75c..5dc8da65 100644 --- a/src/system/hooks/useEvent.ts +++ b/src/system/hooks/useEvent.ts @@ -4,11 +4,22 @@ import { useQuery } from "@tanstack/react-query"; import { nip19 } from "nostr-tools"; import { LumeEvent } from "../event"; -export function useEvent(id: string) { +export function useEvent(id: string, repost?: string) { const { isLoading, isError, error, data } = useQuery({ queryKey: ["event", id], queryFn: async () => { try { + if (repost?.length) { + const nostrEvent: NostrEvent = JSON.parse(repost); + const res = await commands.getEventMeta(nostrEvent.content); + + if (res.status === "ok") { + nostrEvent.meta = res.data; + } + + return new LumeEvent(nostrEvent); + } + // Validate ID let normalizeId: string = id .replace("nostr:", "")