feat: improve performance
This commit is contained in:
@@ -13,6 +13,7 @@
|
|||||||
"@phosphor-icons/react": "^2.1.7",
|
"@phosphor-icons/react": "^2.1.7",
|
||||||
"@radix-ui/react-avatar": "^1.1.0",
|
"@radix-ui/react-avatar": "^1.1.0",
|
||||||
"@radix-ui/react-dialog": "^1.1.1",
|
"@radix-ui/react-dialog": "^1.1.1",
|
||||||
|
"@radix-ui/react-progress": "^1.1.0",
|
||||||
"@radix-ui/react-scroll-area": "^1.1.0",
|
"@radix-ui/react-scroll-area": "^1.1.0",
|
||||||
"@tanstack/react-query": "^5.51.21",
|
"@tanstack/react-query": "^5.51.21",
|
||||||
"@tanstack/react-router": "^1.46.3",
|
"@tanstack/react-router": "^1.46.3",
|
||||||
|
|||||||
26
pnpm-lock.yaml
generated
26
pnpm-lock.yaml
generated
@@ -17,6 +17,9 @@ importers:
|
|||||||
'@radix-ui/react-dialog':
|
'@radix-ui/react-dialog':
|
||||||
specifier: ^1.1.1
|
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)
|
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':
|
'@radix-ui/react-scroll-area':
|
||||||
specifier: ^1.1.0
|
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)
|
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':
|
'@types/react-dom':
|
||||||
optional: true
|
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':
|
'@radix-ui/react-scroll-area@1.1.0':
|
||||||
resolution: {integrity: sha512-9ArIZ9HWhsrfqS765h+GZuLoxaRHD/j0ZWOWilsCvYTpYJp8XwCqNG7Dt9Nu/TItKOdgLGkOPCodQvDc+UMwYg==}
|
resolution: {integrity: sha512-9ArIZ9HWhsrfqS765h+GZuLoxaRHD/j0ZWOWilsCvYTpYJp8XwCqNG7Dt9Nu/TItKOdgLGkOPCodQvDc+UMwYg==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -2181,6 +2197,16 @@ snapshots:
|
|||||||
'@types/react': types-react@19.0.0-rc.1
|
'@types/react': types-react@19.0.0-rc.1
|
||||||
'@types/react-dom': types-react-dom@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)':
|
'@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:
|
dependencies:
|
||||||
'@radix-ui/number': 1.1.0
|
'@radix-ui/number': 1.1.0
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use keyring::Entry;
|
|||||||
use keyring_search::{Limit, List, Search};
|
use keyring_search::{Limit, List, Search};
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use std::{collections::HashSet, time::Duration};
|
use std::{collections::HashSet, str::FromStr, time::Duration};
|
||||||
use tauri::{Emitter, Manager, State};
|
use tauri::{Emitter, Manager, State};
|
||||||
|
|
||||||
use crate::{Nostr, BOOTSTRAP_RELAYS};
|
use crate::{Nostr, BOOTSTRAP_RELAYS};
|
||||||
@@ -252,6 +252,16 @@ pub async fn login(
|
|||||||
let _ = client.connect_relay(url).await;
|
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;
|
let mut inbox_relays = state.inbox_relays.lock().await;
|
||||||
inbox_relays.insert(public_key, urls);
|
inbox_relays.insert(public_key, urls);
|
||||||
} else {
|
} else {
|
||||||
@@ -275,6 +285,41 @@ pub async fn login(
|
|||||||
let state = handle.state::<Nostr>();
|
let state = handle.state::<Nostr>();
|
||||||
let client = &state.client;
|
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
|
client
|
||||||
.handle_notifications(|notification| async {
|
.handle_notifications(|notification| async {
|
||||||
if let RelayPoolNotification::Event { event, subscription_id, .. } = notification {
|
if let RelayPoolNotification::Event { event, subscription_id, .. } = notification {
|
||||||
|
|||||||
@@ -1,85 +1,57 @@
|
|||||||
|
use itertools::Itertools;
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use serde::Serialize;
|
use std::cmp::Reverse;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use tauri::{Emitter, Manager, State};
|
use tauri::State;
|
||||||
|
|
||||||
use crate::{
|
use crate::Nostr;
|
||||||
common::{process_chat_event, process_message_event},
|
|
||||||
Nostr,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Clone, Serialize)]
|
|
||||||
pub struct ChatPayload {
|
|
||||||
events: Vec<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
#[specta::specta]
|
#[specta::specta]
|
||||||
pub async fn get_chats(
|
pub async fn get_chats(state: State<'_, Nostr>) -> Result<Vec<String>, String> {
|
||||||
state: State<'_, Nostr>,
|
|
||||||
handle: tauri::AppHandle,
|
|
||||||
) -> Result<Vec<String>, String> {
|
|
||||||
let client = &state.client;
|
let client = &state.client;
|
||||||
let database = client.database();
|
|
||||||
let signer = client.signer().await.map_err(|e| e.to_string())?;
|
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 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 {
|
match client.database().query(vec![filter.clone()], Order::Desc).await {
|
||||||
Ok(events) => process_chat_event(client, events).await,
|
Ok(events) => {
|
||||||
Err(e) => return Err(e.to_string()),
|
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::<Vec<_>>();
|
||||||
|
|
||||||
tauri::async_runtime::spawn(async move {
|
Ok(ev)
|
||||||
let state = handle.state::<Nostr>();
|
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
});
|
Err(e) => Err(e.to_string()),
|
||||||
|
}
|
||||||
Ok(events)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
#[specta::specta]
|
#[specta::specta]
|
||||||
pub async fn get_chat_messages(
|
pub async fn get_chat_messages(id: String, state: State<'_, Nostr>) -> Result<Vec<String>, String> {
|
||||||
id: String,
|
|
||||||
state: State<'_, Nostr>,
|
|
||||||
handle: tauri::AppHandle,
|
|
||||||
) -> Result<Vec<String>, String> {
|
|
||||||
let client = &state.client;
|
let client = &state.client;
|
||||||
let database = client.database();
|
|
||||||
|
|
||||||
let signer = client.signer().await.map_err(|e| e.to_string())?;
|
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 sender = PublicKey::parse(id.clone()).map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
let group = vec![public_key, sender];
|
let recv_filter =
|
||||||
let filter = Filter::new().kind(Kind::GiftWrap).pubkey(public_key);
|
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 {
|
match client.database().query(vec![recv_filter, sender_filter], Order::Desc).await {
|
||||||
Ok(events) => process_message_event(client, events, &group).await,
|
Ok(events) => {
|
||||||
Err(e) => return Err(e.to_string()),
|
let ev = events.into_iter().map(|ev| ev.as_json()).collect::<Vec<_>>();
|
||||||
};
|
Ok(ev)
|
||||||
|
|
||||||
tauri::async_runtime::spawn(async move {
|
|
||||||
let state = handle.state::<Nostr>();
|
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
});
|
Err(e) => Err(e.to_string()),
|
||||||
|
}
|
||||||
Ok(rumors)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
|
|||||||
@@ -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<Event>) -> Vec<String> {
|
|
||||||
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::<Vec<_>>()
|
|
||||||
.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::<Vec<_>>()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn process_message_event(
|
|
||||||
client: &Client,
|
|
||||||
events: Vec<Event>,
|
|
||||||
group: &Vec<PublicKey>,
|
|
||||||
) -> Vec<String> {
|
|
||||||
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::<Vec<_>>()
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_member(group: &Vec<PublicKey>, tags: &Vec<Tag>) -> bool {
|
|
||||||
for tag in tags {
|
|
||||||
if let Some(TagStandard::PublicKey { public_key, .. }) = tag.as_standardized() {
|
|
||||||
if group.contains(public_key) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
false
|
|
||||||
}
|
|
||||||
@@ -12,7 +12,6 @@ use tauri_plugin_decorum::WebviewWindowExt;
|
|||||||
use commands::{account::*, chat::*};
|
use commands::{account::*, chat::*};
|
||||||
|
|
||||||
mod commands;
|
mod commands;
|
||||||
mod common;
|
|
||||||
|
|
||||||
pub struct Nostr {
|
pub struct Nostr {
|
||||||
client: Client,
|
client: Client,
|
||||||
|
|||||||
@@ -235,17 +235,15 @@ function List() {
|
|||||||
>
|
>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<>
|
<>
|
||||||
<div className="flex items-center justify-between gap-3 my-1.5 px-3">
|
<div className="flex items-center gap-3 my-1.5 px-3">
|
||||||
<div className="flex-1 min-w-0 inline-flex">
|
<div className="flex-1 min-w-0 inline-flex">
|
||||||
<div className="w-44 h-[35px] py-2 max-w-[400px] bg-neutral-100 dark:bg-neutral-800 animate-pulse rounded-tl-3xl rounded-tr-3xl rounded-br-3xl rounded-bl-md" />
|
<div className="w-44 h-[35px] py-2 max-w-[400px] bg-neutral-100 dark:bg-neutral-800 animate-pulse rounded-tl-3xl rounded-tr-3xl rounded-br-3xl rounded-bl-md" />
|
||||||
</div>
|
</div>
|
||||||
<div className="shrink-0 w-16 flex items-center justify-end" />
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between gap-3 my-1.5 px-3">
|
<div className="flex items-center gap-3 my-1.5 px-3">
|
||||||
<div className="flex-1 min-w-0 inline-flex justify-end">
|
<div className="flex-1 min-w-0 inline-flex justify-end">
|
||||||
<div className="w-44 h-[35px] py-2 max-w-[400px] bg-blue-500 text-white animate-pulse rounded-tl-3xl rounded-tr-3xl rounded-br-md rounded-bl-3xl" />
|
<div className="w-44 h-[35px] py-2 max-w-[400px] bg-blue-500 text-white animate-pulse rounded-tl-3xl rounded-tr-3xl rounded-br-md rounded-bl-3xl" />
|
||||||
</div>
|
</div>
|
||||||
<div className="shrink-0 w-16 flex items-center justify-end" />
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
) : isError ? (
|
) : isError ? (
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
X,
|
X,
|
||||||
} from "@phosphor-icons/react";
|
} from "@phosphor-icons/react";
|
||||||
import * as Dialog from "@radix-ui/react-dialog";
|
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 * as ScrollArea from "@radix-ui/react-scroll-area";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { Link, Outlet, createLazyFileRoute } from "@tanstack/react-router";
|
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 type { NostrEvent } from "nostr-tools";
|
||||||
import { useCallback, useEffect, useState, useTransition } from "react";
|
import { useCallback, useEffect, useState, useTransition } from "react";
|
||||||
|
|
||||||
type ChatPayload = {
|
|
||||||
events: string[];
|
|
||||||
};
|
|
||||||
|
|
||||||
type EventPayload = {
|
type EventPayload = {
|
||||||
event: string;
|
event: string;
|
||||||
sender: string;
|
sender: string;
|
||||||
@@ -98,21 +95,21 @@ function ChatList() {
|
|||||||
refetchOnWindowFocus: false,
|
refetchOnWindowFocus: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
const [isSync, setIsSync] = useState(false);
|
||||||
const unlisten = listen<ChatPayload>("sync_chat", async (data) => {
|
const [progress, setProgress] = useState(0);
|
||||||
const raw = data.payload.events;
|
|
||||||
const events: NostrEvent[] = raw.map((item) => JSON.parse(item));
|
|
||||||
const chats: NostrEvent[] = await queryClient.getQueryData(["chats"]);
|
|
||||||
|
|
||||||
if (chats?.length) {
|
useEffect(() => {
|
||||||
const newEvents = [...events, ...chats];
|
const timer = setInterval(
|
||||||
const uniqs = [
|
() => setProgress((prev) => (prev <= 100 ? prev + 4 : 100)),
|
||||||
...new Map(newEvents.map((item) => [item.pubkey, item])).values(),
|
1200,
|
||||||
];
|
);
|
||||||
await queryClient.setQueryData(["chats"], uniqs);
|
return () => clearInterval(timer);
|
||||||
} else {
|
}, []);
|
||||||
await queryClient.setQueryData(["chats"], events);
|
|
||||||
}
|
useEffect(() => {
|
||||||
|
const unlisten = listen("synchronized", async () => {
|
||||||
|
await queryClient.refetchQueries({ queryKey: ["chats"] });
|
||||||
|
setIsSync(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
@@ -158,11 +155,11 @@ function ChatList() {
|
|||||||
<ScrollArea.Root
|
<ScrollArea.Root
|
||||||
type={"scroll"}
|
type={"scroll"}
|
||||||
scrollHideDelay={300}
|
scrollHideDelay={300}
|
||||||
className="overflow-hidden flex-1 w-full"
|
className="relative overflow-hidden flex-1 w-full"
|
||||||
>
|
>
|
||||||
<ScrollArea.Viewport className="relative h-full px-1.5">
|
<ScrollArea.Viewport className="relative h-full px-1.5">
|
||||||
{isLoading || !data.length ? (
|
{isLoading || !isSync ? (
|
||||||
<div>
|
<>
|
||||||
{[...Array(5).keys()].map((i) => (
|
{[...Array(5).keys()].map((i) => (
|
||||||
<div
|
<div
|
||||||
key={i}
|
key={i}
|
||||||
@@ -172,8 +169,8 @@ function ChatList() {
|
|||||||
<div className="size-4 w-20 rounded animate-pulse bg-black/10 dark:bg-white/10" />
|
<div className="size-4 w-20 rounded animate-pulse bg-black/10 dark:bg-white/10" />
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</>
|
||||||
) : !data?.length ? (
|
) : isSync && !data.length ? (
|
||||||
<div className="p-2">
|
<div className="p-2">
|
||||||
<div className="px-2 h-12 w-full rounded-lg bg-black/5 dark:bg-white/5 flex items-center justify-center text-sm">
|
<div className="px-2 h-12 w-full rounded-lg bg-black/5 dark:bg-white/5 flex items-center justify-center text-sm">
|
||||||
No chats.
|
No chats.
|
||||||
@@ -213,6 +210,25 @@ function ChatList() {
|
|||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
</ScrollArea.Viewport>
|
</ScrollArea.Viewport>
|
||||||
|
{!isSync ? (
|
||||||
|
<div className="absolute bottom-0 w-full p-4">
|
||||||
|
<div className="flex flex-col items-center gap-1.5">
|
||||||
|
<Progress.Root
|
||||||
|
className="relative overflow-hidden bg-black/20 dark:bg-white/20 rounded-full w-full h-1"
|
||||||
|
style={{
|
||||||
|
transform: "translateZ(0)",
|
||||||
|
}}
|
||||||
|
value={progress}
|
||||||
|
>
|
||||||
|
<Progress.Indicator
|
||||||
|
className="bg-blue-500 size-full transition-transform duration-[660ms] ease-[cubic-bezier(0.65, 0, 0.35, 1)]"
|
||||||
|
style={{ transform: `translateX(-${100 - progress}%)` }}
|
||||||
|
/>
|
||||||
|
</Progress.Root>
|
||||||
|
<span className="text-center text-xs">Syncing message...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
<ScrollArea.Scrollbar
|
<ScrollArea.Scrollbar
|
||||||
className="flex select-none touch-none p-0.5 duration-[160ms] ease-out data-[orientation=vertical]:w-2"
|
className="flex select-none touch-none p-0.5 duration-[160ms] ease-out data-[orientation=vertical]:w-2"
|
||||||
orientation="vertical"
|
orientation="vertical"
|
||||||
|
|||||||
Reference in New Issue
Block a user