feat: optimize spawn thread

This commit is contained in:
reya
2024-06-30 21:06:04 +07:00
parent fcb70c0e9a
commit 017a3676a4
5 changed files with 243 additions and 239 deletions

View File

@@ -202,10 +202,10 @@ const Search = memo(function Search() {
<button <button
type="button" type="button"
onClick={(e) => showContextMenu(e)} onClick={(e) => showContextMenu(e)}
className="inline-flex items-center gap-1 capitalize text-sm font-medium pr-2 border-r border-black/10 dark:border-white/10" className="inline-flex items-center gap-1 capitalize text-sm font-medium pr-2 border-r border-black/10 dark:border-white/10 text-black/50 dark:text-white/50"
> >
{searchType} {searchType}
<ChevronDownIcon className="size-3 text-black/50 dark:text-white/50" /> <ChevronDownIcon className="size-3" />
</button> </button>
<input <input
type="text" type="text"
@@ -217,7 +217,7 @@ const Search = memo(function Search() {
LumeWindow.openSearch(searchType, query); LumeWindow.openSearch(searchType, query);
} }
}} }}
className="h-full w-full px-3 text-sm rounded-full border-none ring-0 focus:ring-0 focus:outline-none bg-transparent placeholder:text-black/50 dark:placeholder:text-black/50" className="h-full w-full px-3 text-sm rounded-full border-none ring-0 focus:ring-0 focus:outline-none bg-transparent placeholder:text-black/50 dark:placeholder:text-white/50"
/> />
<SearchIcon className="size-5" /> <SearchIcon className="size-5" />
</div> </div>

View File

@@ -122,6 +122,7 @@ function Screen() {
NostrQuery.getNotifications() NostrQuery.getNotifications()
.then((data) => { .then((data) => {
const sorted = data.sort((a, b) => b.created_at - a.created_at); const sorted = data.sort((a, b) => b.created_at - a.created_at);
console.log("sorted");
setEvents(sorted); setEvents(sorted);
}) })
.catch((e) => console.log(e)); .catch((e) => console.log(e));
@@ -156,7 +157,7 @@ function Screen() {
} }
return ( return (
<div className="flex flex-col w-full h-full"> <div className="flex flex-col size-full overflow-hidden">
<div className="flex items-center justify-between px-4 border-b h-11 shrink-0 border-black/5 dark:border-white/5"> <div className="flex items-center justify-between px-4 border-b h-11 shrink-0 border-black/5 dark:border-white/5">
<div> <div>
<h1 className="text-sm font-semibold">Notifications</h1> <h1 className="text-sm font-semibold">Notifications</h1>
@@ -176,10 +177,12 @@ function Screen() {
</button> </button>
</div> </div>
</div> </div>
<Tabs.Root <ScrollArea.Root
defaultValue="replies" type={"scroll"}
className="flex-1 overflow-x-hidden overflow-y-auto scrollbar-none" scrollHideDelay={300}
className="flex-1 overflow-x-hidden"
> >
<Tabs.Root defaultValue="replies" className="h-full">
<Tabs.List className="flex items-center"> <Tabs.List className="flex items-center">
<Tabs.Trigger <Tabs.Trigger
className="flex-1 inline-flex h-8 items-center justify-center gap-2 px-2 text-sm font-medium border-b border-black/10 dark:border-white/10 data-[state=active]:border-black/30 dark:data-[state=active]:border-white/30 data-[state=inactive]:opacity-50" className="flex-1 inline-flex h-8 items-center justify-center gap-2 px-2 text-sm font-medium border-b border-black/10 dark:border-white/10 data-[state=active]:border-black/30 dark:data-[state=active]:border-white/30 data-[state=inactive]:opacity-50"
@@ -258,7 +261,7 @@ function Screen() {
pubkey={event.tags.find((tag) => tag[0] === "P")[1]} pubkey={event.tags.find((tag) => tag[0] === "P")[1]}
> >
<User.Root className="shrink-0 flex gap-1.5 rounded-full h-8 bg-black/10 dark:bg-white/10 backdrop-blur-md p-[2px]"> <User.Root className="shrink-0 flex gap-1.5 rounded-full h-8 bg-black/10 dark:bg-white/10 backdrop-blur-md p-[2px]">
<User.Avatar className="flex-1 rounded-full size-7" /> <User.Avatar className="rounded-full size-7" />
<div className="flex-1 h-7 w-max pr-1.5 rounded-full inline-flex items-center justify-center text-sm truncate"> <div className="flex-1 h-7 w-max pr-1.5 rounded-full inline-flex items-center justify-center text-sm truncate">
{decodeZapInvoice(event.tags).bitcoinFormatted} {decodeZapInvoice(event.tags).bitcoinFormatted}
</div> </div>
@@ -272,6 +275,14 @@ function Screen() {
</Tab> </Tab>
</div> </div>
</Tabs.Root> </Tabs.Root>
<ScrollArea.Scrollbar
className="flex select-none touch-none p-0.5 duration-[160ms] ease-out data-[orientation=vertical]:w-2"
orientation="vertical"
>
<ScrollArea.Thumb className="flex-1 bg-black/10 dark:bg-white/10 rounded-full relative before:content-[''] before:absolute before:top-1/2 before:left-1/2 before:-translate-x-1/2 before:-translate-y-1/2 before:w-full before:h-full before:min-w-[44px] before:min-h-[44px]" />
</ScrollArea.Scrollbar>
<ScrollArea.Corner className="bg-transparent" />
</ScrollArea.Root>
</div> </div>
); );
} }
@@ -281,22 +292,9 @@ function Tab({ value, children }: { value: string; children: ReactNode[] }) {
return ( return (
<Tabs.Content value={value} className="size-full"> <Tabs.Content value={value} className="size-full">
<ScrollArea.Root
type={"scroll"}
scrollHideDelay={300}
className="overflow-hidden size-full"
>
<ScrollArea.Viewport ref={ref} className="h-full px-2 pt-2"> <ScrollArea.Viewport ref={ref} className="h-full px-2 pt-2">
<Virtualizer scrollRef={ref}>{children}</Virtualizer> <Virtualizer scrollRef={ref}>{children}</Virtualizer>
</ScrollArea.Viewport> </ScrollArea.Viewport>
<ScrollArea.Scrollbar
className="flex select-none touch-none p-0.5 duration-[160ms] ease-out data-[orientation=vertical]:w-2"
orientation="vertical"
>
<ScrollArea.Thumb className="flex-1 bg-black/10 dark:bg-white/10 rounded-full relative before:content-[''] before:absolute before:top-1/2 before:left-1/2 before:-translate-x-1/2 before:-translate-y-1/2 before:w-full before:h-full before:min-w-[44px] before:min-h-[44px]" />
</ScrollArea.Scrollbar>
<ScrollArea.Corner className="bg-transparent" />
</ScrollArea.Root>
</Tabs.Content> </Tabs.Content>
); );
} }

View File

@@ -0,0 +1,75 @@
use crate::Settings;
use nostr_sdk::prelude::*;
pub async fn init_nip65(client: &Client) {
let signer = client.signer().await.unwrap();
let public_key = signer.public_key().await.unwrap();
if let Ok(events) = client
.get_events_of(
vec![Filter::new()
.author(public_key)
.kind(Kind::RelayList)
.limit(1)],
None,
)
.await
{
if let Some(event) = events.first() {
let relay_list = nip65::extract_relay_list(event);
for item in relay_list.into_iter() {
let relay_url = item.0.to_string();
let opts = match item.1 {
Some(val) => {
if val == &RelayMetadata::Read {
RelayOptions::new().read(true).write(false)
} else {
RelayOptions::new().write(true).read(false)
}
}
None => RelayOptions::default(),
};
// Add relay to relay pool
let _ = client
.add_relay_with_opts(&relay_url, opts)
.await
.unwrap_or_default();
// Connect relay
client.connect_relay(relay_url).await.unwrap_or_default();
println!("connecting to relay: {} - {:?}", item.0, item.1);
}
}
};
}
pub async fn get_user_settings(client: &Client) -> Result<Settings, String> {
let ident = "lume:settings";
let signer = client.signer().await.unwrap();
let public_key = signer.public_key().await.unwrap();
let filter = Filter::new()
.author(public_key)
.kind(Kind::ApplicationSpecificData)
.identifier(ident)
.limit(1);
if let Ok(events) = client.get_events_of(vec![filter], None).await {
if let Some(event) = events.first() {
let content = event.content();
if let Ok(decrypted) = signer.nip44_decrypt(public_key, content).await {
match serde_json::from_str(&decrypted) {
Ok(parsed) => parsed,
Err(_) => Err("Could not parse settings payload".into()),
}
} else {
Err("Decrypt settings failed.".into())
}
} else {
Err("Settings not found.".into())
}
} else {
Err("Settings not found.".into())
}
}

View File

@@ -1,16 +1,16 @@
use std::time::Duration;
use keyring::Entry; 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 specta::Type; use specta::Type;
use std::time::Duration;
use tauri::{EventTarget, Manager, State}; use tauri::{EventTarget, Manager, State};
use tauri_plugin_notification::NotificationExt; use tauri_plugin_notification::NotificationExt;
use crate::nostr::event::RichEvent; use crate::nostr::event::RichEvent;
use crate::nostr::internal::{get_user_settings, init_nip65};
use crate::nostr::utils::parse_event; use crate::nostr::utils::parse_event;
use crate::{Nostr, Settings, NEWSFEED_NEG_LIMIT, NOTIFICATION_NEG_LIMIT}; use crate::{Nostr, NEWSFEED_NEG_LIMIT, NOTIFICATION_NEG_LIMIT};
#[derive(Serialize, Type)] #[derive(Serialize, Type)]
pub struct Account { pub struct Account {
@@ -167,6 +167,7 @@ pub async fn load_account(
state: State<'_, Nostr>, state: State<'_, Nostr>,
app: tauri::AppHandle, app: tauri::AppHandle,
) -> Result<bool, String> { ) -> Result<bool, String> {
let handle = app.clone();
let client = &state.client; let client = &state.client;
let keyring = Entry::new(npub, "nostr_secret").unwrap(); let keyring = Entry::new(npub, "nostr_secret").unwrap();
@@ -198,111 +199,54 @@ pub async fn load_account(
} }
} }
// Verify signer
let signer = client.signer().await.unwrap();
let public_key = signer.public_key().await.unwrap();
// Connect to user's relay (NIP-65) // Connect to user's relay (NIP-65)
if let Ok(events) = client init_nip65(client).await;
.get_events_of(
vec![Filter::new()
.author(public_key)
.kind(Kind::RelayList)
.limit(1)],
None,
)
.await
{
if let Some(event) = events.first() {
let relay_list = nip65::extract_relay_list(event);
for item in relay_list.into_iter() {
let relay_url = item.0.to_string();
let opts = match item.1 {
Some(val) => {
if val == &RelayMetadata::Read {
RelayOptions::new().read(true).write(false)
} else {
RelayOptions::new().write(true).read(false)
}
}
None => RelayOptions::default(),
};
// Add relay to relay pool
let _ = client
.add_relay_with_opts(&relay_url, opts)
.await
.unwrap_or_default();
// Connect relay
client.connect_relay(relay_url).await.unwrap_or_default();
println!("connecting to relay: {} - {:?}", item.0, item.1);
}
}
};
// Get user's contact list // Get user's contact list
if let Ok(contacts) = client.get_contact_list(None).await { if let Ok(contacts) = client.get_contact_list(None).await {
*state.contact_list.lock().unwrap() = contacts *state.contact_list.lock().unwrap() = contacts
}; };
// Create a subscription for notification
let sub_id = SubscriptionId::new("notification");
let filter = Filter::new()
.pubkey(public_key)
.kinds(vec![
Kind::TextNote,
Kind::Repost,
Kind::Reaction,
Kind::ZapReceipt,
])
.since(Timestamp::now());
// Subscribe
print!("Subscribing for new notification...");
client.subscribe_with_id(sub_id, vec![filter], None).await;
// Get user's settings // Get user's settings
let handle = app.clone(); if let Ok(settings) = get_user_settings(client).await {
// Spawn a thread to handle it *state.settings.lock().unwrap() = settings
};
tauri::async_runtime::spawn(async move { tauri::async_runtime::spawn(async move {
let window = handle.get_window("main").unwrap(); let window = handle.get_window("main").unwrap();
let state = window.state::<Nostr>();
let client = &state.client;
let ident = "lume:settings";
let filter = Filter::new()
.author(public_key)
.kind(Kind::ApplicationSpecificData)
.identifier(ident)
.limit(1);
if let Ok(events) = client
.get_events_of(vec![filter], Some(Duration::from_secs(5)))
.await
{
if let Some(event) = events.first() {
let content = event.content();
if let Ok(decrypted) = signer.nip44_decrypt(public_key, content).await {
let parsed: Settings =
serde_json::from_str(&decrypted).expect("Could not parse settings payload");
*state.settings.lock().unwrap() = parsed;
}
}
}
});
// Run sync service
let handle = app.clone();
// Spawn a thread to handle it
tauri::async_runtime::spawn(async move {
let window = handle.get_window("main").unwrap();
let state = window.state::<Nostr>(); let state = window.state::<Nostr>();
let client = &state.client; let client = &state.client;
let contact_list = state.contact_list.lock().unwrap().clone(); let contact_list = state.contact_list.lock().unwrap().clone();
let notification = Filter::new() let signer = client.signer().await.unwrap();
let public_key = signer.public_key().await.unwrap();
if !contact_list.is_empty() {
let authors: Vec<PublicKey> = contact_list.into_iter().map(|f| f.public_key).collect();
match client
.reconcile(
Filter::new()
.authors(authors)
.kinds(vec![Kind::TextNote, Kind::Repost])
.limit(NEWSFEED_NEG_LIMIT),
NegentropyOptions::default(),
)
.await
{
Ok(_) => {
if handle.emit_to(EventTarget::Any, "synced", true).is_err() {
println!("Emit event failed.")
}
}
Err(_) => println!("Sync newsfeed failed."),
}
};
match client
.reconcile(
Filter::new()
.pubkey(public_key) .pubkey(public_key)
.kinds(vec![ .kinds(vec![
Kind::TextNote, Kind::TextNote,
@@ -310,10 +254,9 @@ pub async fn load_account(
Kind::Reaction, Kind::Reaction,
Kind::ZapReceipt, Kind::ZapReceipt,
]) ])
.limit(NOTIFICATION_NEG_LIMIT); .limit(NOTIFICATION_NEG_LIMIT),
NegentropyOptions::default(),
match client )
.reconcile(notification, NegentropyOptions::default())
.await .await
{ {
Ok(_) => { Ok(_) => {
@@ -324,34 +267,21 @@ pub async fn load_account(
Err(_) => println!("Sync notification failed."), Err(_) => println!("Sync notification failed."),
}; };
if !contact_list.is_empty() { let subscription_id = SubscriptionId::new("notification");
let authors: Vec<PublicKey> = contact_list.into_iter().map(|f| f.public_key).collect(); let subscription = Filter::new()
.pubkey(public_key)
.kinds(vec![
Kind::TextNote,
Kind::Repost,
Kind::Reaction,
Kind::ZapReceipt,
])
.since(Timestamp::now());
let newsfeed = Filter::new() // Subscribing for new notification...
.authors(authors) client
.kinds(vec![Kind::TextNote, Kind::Repost]) .subscribe_with_id(subscription_id, vec![subscription], None)
.limit(NEWSFEED_NEG_LIMIT); .await;
match client
.reconcile(newsfeed, NegentropyOptions::default())
.await
{
Ok(_) => {
if handle.emit_to(EventTarget::Any, "synced", true).is_err() {
println!("Emit event failed.")
}
}
Err(_) => println!("Sync newsfeed failed."),
}
}
});
// Run notification service
// Spawn a thread to handle it
tauri::async_runtime::spawn(async move {
let window = app.get_window("main").unwrap();
let state = window.state::<Nostr>();
let client = &state.client;
// Handle notifications // Handle notifications
client client

View File

@@ -1,4 +1,5 @@
pub mod event; pub mod event;
mod internal;
pub mod keys; pub mod keys;
pub mod metadata; pub mod metadata;
pub mod relay; pub mod relay;