From 14c36e4731591709b68c7cacd1ca78b11c00be4d Mon Sep 17 00:00:00 2001 From: reya <123083837+reyamir@users.noreply.github.com> Date: Sat, 22 Nov 2025 07:25:08 +0700 Subject: [PATCH] feat: refine the search bar (#207) * update deps * refactor the search cancellation * . * . --- Cargo.lock | 144 +++--- crates/common/src/constants.rs | 6 +- crates/coop/src/chatspace.rs | 4 +- crates/coop/src/login/mod.rs | 4 +- crates/coop/src/main.rs | 1 + .../coop/src/{views => }/sidebar/list_item.rs | 0 crates/coop/src/{views => }/sidebar/mod.rs | 437 ++++++++---------- crates/coop/src/views/mod.rs | 1 - crates/coop/src/views/onboarding.rs | 4 +- crates/coop/src/views/startup.rs | 4 +- crates/state/src/lib.rs | 10 +- 11 files changed, 271 insertions(+), 344 deletions(-) rename crates/coop/src/{views => }/sidebar/list_item.rs (100%) rename crates/coop/src/{views => }/sidebar/mod.rs (69%) diff --git a/Cargo.lock b/Cargo.lock index 67e509f0..105205f1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -504,15 +504,15 @@ dependencies = [ [[package]] name = "async_zip" -version = "0.0.17" +version = "0.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b9f7252833d5ed4b00aa9604b563529dd5e11de9c23615de2dcdf91eb87b52" +checksum = "0d8c50d65ce1b0e0cb65a785ff615f78860d7754290647d3b983208daa4f85e6" dependencies = [ "async-compression", "crc32fast", "futures-lite 2.6.1", "pin-project", - "thiserror 1.0.69", + "thiserror 2.0.17", ] [[package]] @@ -602,9 +602,9 @@ dependencies = [ [[package]] name = "aws-lc-rs" -version = "1.15.0" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5932a7d9d28b0d2ea34c6b3779d35e3dd6f6345317c34e73438c4f1f29144151" +checksum = "6b5ce75405893cd713f9ab8e297d8e438f624dde7d706108285f7e17a25a180f" dependencies = [ "aws-lc-sys", "zeroize", @@ -612,11 +612,10 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.33.0" +version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1826f2e4cfc2cd19ee53c42fbf68e2f81ec21108e0b7ecf6a71cf062137360fc" +checksum = "179c3777a8b5e70e90ea426114ffc565b2c1a9f82f6c4a0c5a34aa6ef5e781b6" dependencies = [ - "bindgen 0.72.1", "cc", "cmake", "dunce", @@ -667,26 +666,6 @@ dependencies = [ "syn 2.0.110", ] -[[package]] -name = "bindgen" -version = "0.72.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" -dependencies = [ - "bitflags 2.10.0", - "cexpr", - "clang-sys", - "itertools 0.13.0", - "log", - "prettyplease", - "proc-macro2", - "quote", - "regex", - "rustc-hash 2.1.1", - "shlex", - "syn 2.0.110", -] - [[package]] name = "bip39" version = "2.2.0" @@ -890,6 +869,12 @@ dependencies = [ "serde", ] +[[package]] +name = "btreecap" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6160c957d8aa33d0a8ba1dbab98e3cb57023ad9374c501441e88559f99e6c4c9" + [[package]] name = "built" version = "0.8.0" @@ -942,26 +927,25 @@ checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" [[package]] name = "calloop" -version = "0.13.0" +version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec" +checksum = "cb9f6e1368bd4621d2c86baa7e37de77a938adf5221e5dd3d6133340101b309e" dependencies = [ "bitflags 2.10.0", - "log", "polling", - "rustix 0.38.44", + "rustix 1.1.2", "slab", - "thiserror 1.0.69", + "tracing", ] [[package]] name = "calloop-wayland-source" -version = "0.3.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20" +checksum = "138efcf0940a02ebf0cc8d1eff41a1682a46b431630f4c52450d6265876021fa" dependencies = [ "calloop", - "rustix 0.38.44", + "rustix 1.1.2", "wayland-backend", "wayland-client", ] @@ -995,9 +979,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.46" +version = "1.2.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97463e1064cb1b1c1384ad0a0b9c8abd0988e2a91f52606c80ef14aadb63e36" +checksum = "cd405d82c84ff7f35739f175f67d8b9fb7687a0e84ccdc78bd3568839827cf07" dependencies = [ "find-msvc-tools", "jobserver", @@ -1242,7 +1226,7 @@ dependencies = [ [[package]] name = "collections" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#1e2f15a3d70258ab366e9ac9309605749d5b0a27" +source = "git+https://github.com/zed-industries/zed#de58a496efd3379dd787326c678b931ffff4213f" dependencies = [ "indexmap", "rustc-hash 2.1.1", @@ -1674,7 +1658,7 @@ dependencies = [ [[package]] name = "derive_refineable" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#1e2f15a3d70258ab366e9ac9309605749d5b0a27" +source = "git+https://github.com/zed-industries/zed#de58a496efd3379dd787326c678b931ffff4213f" dependencies = [ "proc-macro2", "quote", @@ -2614,13 +2598,13 @@ dependencies = [ [[package]] name = "gpui" version = "0.2.2" -source = "git+https://github.com/zed-industries/zed#1e2f15a3d70258ab366e9ac9309605749d5b0a27" +source = "git+https://github.com/zed-industries/zed#de58a496efd3379dd787326c678b931ffff4213f" dependencies = [ "anyhow", "as-raw-xcb-connection", "ashpd 0.11.0", "async-task", - "bindgen 0.71.1", + "bindgen", "bitflags 2.10.0", "blade-graphics", "blade-macros", @@ -2713,7 +2697,7 @@ dependencies = [ [[package]] name = "gpui_macros" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#1e2f15a3d70258ab366e9ac9309605749d5b0a27" +source = "git+https://github.com/zed-industries/zed#de58a496efd3379dd787326c678b931ffff4213f" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -2724,7 +2708,7 @@ dependencies = [ [[package]] name = "gpui_tokio" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#1e2f15a3d70258ab366e9ac9309605749d5b0a27" +source = "git+https://github.com/zed-industries/zed#de58a496efd3379dd787326c678b931ffff4213f" dependencies = [ "anyhow", "gpui", @@ -2795,9 +2779,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.16.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" [[package]] name = "heck" @@ -2953,7 +2937,7 @@ dependencies = [ [[package]] name = "http_client" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#1e2f15a3d70258ab366e9ac9309605749d5b0a27" +source = "git+https://github.com/zed-industries/zed#de58a496efd3379dd787326c678b931ffff4213f" dependencies = [ "anyhow", "async-compression", @@ -2979,7 +2963,7 @@ dependencies = [ [[package]] name = "http_client_tls" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#1e2f15a3d70258ab366e9ac9309605749d5b0a27" +source = "git+https://github.com/zed-industries/zed#de58a496efd3379dd787326c678b931ffff4213f" dependencies = [ "rustls", "rustls-platform-verifier", @@ -3269,12 +3253,12 @@ checksum = "e7c5cedc30da3a610cac6b4ba17597bdf7152cf974e8aab3afb3d54455e371c8" [[package]] name = "indexmap" -version = "2.12.0" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" +checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" dependencies = [ "equivalent", - "hashbrown 0.16.0", + "hashbrown 0.16.1", "serde", "serde_core", ] @@ -3775,10 +3759,10 @@ dependencies = [ [[package]] name = "media" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#1e2f15a3d70258ab366e9ac9309605749d5b0a27" +source = "git+https://github.com/zed-industries/zed#de58a496efd3379dd787326c678b931ffff4213f" dependencies = [ "anyhow", - "bindgen 0.71.1", + "bindgen", "core-foundation 0.10.0", "core-video", "ctor", @@ -4025,7 +4009,7 @@ dependencies = [ [[package]] name = "nostr" version = "0.44.1" -source = "git+https://github.com/rust-nostr/nostr#aa01f9fb8cd1ddbcc66e2b4ef58eeedbc0d54f56" +source = "git+https://github.com/rust-nostr/nostr#8ab324a972145713466145e5daaf1e8351e6d7e7" dependencies = [ "aes", "base64", @@ -4049,7 +4033,7 @@ dependencies = [ [[package]] name = "nostr-connect" version = "0.44.0" -source = "git+https://github.com/rust-nostr/nostr#aa01f9fb8cd1ddbcc66e2b4ef58eeedbc0d54f56" +source = "git+https://github.com/rust-nostr/nostr#8ab324a972145713466145e5daaf1e8351e6d7e7" dependencies = [ "async-utility", "nostr", @@ -4061,8 +4045,9 @@ dependencies = [ [[package]] name = "nostr-database" version = "0.44.0" -source = "git+https://github.com/rust-nostr/nostr#aa01f9fb8cd1ddbcc66e2b4ef58eeedbc0d54f56" +source = "git+https://github.com/rust-nostr/nostr#8ab324a972145713466145e5daaf1e8351e6d7e7" dependencies = [ + "btreecap", "flatbuffers", "lru", "nostr", @@ -4072,7 +4057,7 @@ dependencies = [ [[package]] name = "nostr-gossip" version = "0.44.0" -source = "git+https://github.com/rust-nostr/nostr#aa01f9fb8cd1ddbcc66e2b4ef58eeedbc0d54f56" +source = "git+https://github.com/rust-nostr/nostr#8ab324a972145713466145e5daaf1e8351e6d7e7" dependencies = [ "nostr", ] @@ -4080,7 +4065,7 @@ dependencies = [ [[package]] name = "nostr-lmdb" version = "0.44.0" -source = "git+https://github.com/rust-nostr/nostr#aa01f9fb8cd1ddbcc66e2b4ef58eeedbc0d54f56" +source = "git+https://github.com/rust-nostr/nostr#8ab324a972145713466145e5daaf1e8351e6d7e7" dependencies = [ "async-utility", "flume", @@ -4094,7 +4079,7 @@ dependencies = [ [[package]] name = "nostr-relay-pool" version = "0.44.0" -source = "git+https://github.com/rust-nostr/nostr#aa01f9fb8cd1ddbcc66e2b4ef58eeedbc0d54f56" +source = "git+https://github.com/rust-nostr/nostr#8ab324a972145713466145e5daaf1e8351e6d7e7" dependencies = [ "async-utility", "async-wsocket", @@ -4111,7 +4096,7 @@ dependencies = [ [[package]] name = "nostr-sdk" version = "0.44.1" -source = "git+https://github.com/rust-nostr/nostr#aa01f9fb8cd1ddbcc66e2b4ef58eeedbc0d54f56" +source = "git+https://github.com/rust-nostr/nostr#8ab324a972145713466145e5daaf1e8351e6d7e7" dependencies = [ "async-utility", "nostr", @@ -4628,7 +4613,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "perf" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#1e2f15a3d70258ab366e9ac9309605749d5b0a27" +source = "git+https://github.com/zed-industries/zed#de58a496efd3379dd787326c678b931ffff4213f" dependencies = [ "collections", "serde", @@ -5254,7 +5239,7 @@ dependencies = [ [[package]] name = "refineable" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#1e2f15a3d70258ab366e9ac9309605749d5b0a27" +source = "git+https://github.com/zed-industries/zed#de58a496efd3379dd787326c678b931ffff4213f" dependencies = [ "derive_refineable", ] @@ -5352,7 +5337,7 @@ dependencies = [ [[package]] name = "reqwest_client" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#1e2f15a3d70258ab366e9ac9309605749d5b0a27" +source = "git+https://github.com/zed-industries/zed#de58a496efd3379dd787326c678b931ffff4213f" dependencies = [ "anyhow", "bytes", @@ -5406,7 +5391,7 @@ dependencies = [ [[package]] name = "rope" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#1e2f15a3d70258ab366e9ac9309605749d5b0a27" +source = "git+https://github.com/zed-industries/zed#de58a496efd3379dd787326c678b931ffff4213f" dependencies = [ "arrayvec", "log", @@ -5872,7 +5857,7 @@ checksum = "16c2f82143577edb4921b71ede051dac62ca3c16084e918bf7b40c96ae10eb33" [[package]] name = "semantic_version" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#1e2f15a3d70258ab366e9ac9309605749d5b0a27" +source = "git+https://github.com/zed-industries/zed#de58a496efd3379dd787326c678b931ffff4213f" dependencies = [ "anyhow", "serde", @@ -6327,7 +6312,7 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "sum_tree" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#1e2f15a3d70258ab366e9ac9309605749d5b0a27" +source = "git+https://github.com/zed-industries/zed#de58a496efd3379dd787326c678b931ffff4213f" dependencies = [ "arrayvec", "log", @@ -6978,6 +6963,7 @@ version = "0.1.41" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ + "log", "pin-project-lite", "tracing-attributes", "tracing-core", @@ -7295,7 +7281,7 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "util" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#1e2f15a3d70258ab366e9ac9309605749d5b0a27" +source = "git+https://github.com/zed-industries/zed#de58a496efd3379dd787326c678b931ffff4213f" dependencies = [ "anyhow", "async-fs", @@ -7331,7 +7317,7 @@ dependencies = [ [[package]] name = "util_macros" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#1e2f15a3d70258ab366e9ac9309605749d5b0a27" +source = "git+https://github.com/zed-industries/zed#de58a496efd3379dd787326c678b931ffff4213f" dependencies = [ "perf", "quote", @@ -7370,9 +7356,9 @@ checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" [[package]] name = "value-bag" -version = "1.11.1" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "943ce29a8a743eb10d6082545d861b24f9d1b160b7d741e0f2cdf726bec909c5" +checksum = "7ba6f5989077681266825251a52748b8c1d8a4ad098cc37e440103d0ea717fc0" dependencies = [ "value-bag-serde1", "value-bag-sval2", @@ -7380,20 +7366,20 @@ dependencies = [ [[package]] name = "value-bag-serde1" -version = "1.11.1" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35540706617d373b118d550d41f5dfe0b78a0c195dc13c6815e92e2638432306" +checksum = "16530907bfe2999a1773ca5900a65101e092c70f642f25cc23ca0c43573262c5" dependencies = [ "erased-serde", - "serde", + "serde_core", "serde_fmt", ] [[package]] name = "value-bag-sval2" -version = "1.11.1" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fe7e140a2658cc16f7ee7a86e413e803fc8f9b5127adc8755c19f9fefa63a52" +checksum = "d00ae130edd690eaa877e4f40605d534790d1cf1d651e7685bd6a144521b251f" dependencies = [ "sval", "sval_buffer", @@ -8723,18 +8709,18 @@ checksum = "6df3dc4292935e51816d896edcd52aa30bc297907c26167fec31e2b0c6a32524" [[package]] name = "zerocopy" -version = "0.8.27" +version = "0.8.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" +checksum = "43fa6694ed34d6e57407afbccdeecfa268c470a7d2a5b0cf49ce9fcc345afb90" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.27" +version = "0.8.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" +checksum = "c640b22cd9817fae95be82f0d2f90b11f7605f6c319d16705c459b27ac2cbc26" dependencies = [ "proc-macro2", "quote", diff --git a/crates/common/src/constants.rs b/crates/common/src/constants.rs index 957f4106..4119c6db 100644 --- a/crates/common/src/constants.rs +++ b/crates/common/src/constants.rs @@ -11,7 +11,11 @@ pub const BOOTSTRAP_RELAYS: [&str; 5] = [ ]; /// Search Relays. -pub const SEARCH_RELAYS: [&str; 1] = ["wss://relay.nostr.band"]; +pub const SEARCH_RELAYS: [&str; 3] = [ + "wss://relay.nostr.band", + "wss://search.nos.today", + "wss://relay.noswhere.com", +]; /// Default relay for Nostr Connect pub const NOSTR_CONNECT_RELAY: &str = "wss://relay.nsec.app"; diff --git a/crates/coop/src/chatspace.rs b/crates/coop/src/chatspace.rs index 51b620f6..82ed3917 100644 --- a/crates/coop/src/chatspace.rs +++ b/crates/coop/src/chatspace.rs @@ -35,8 +35,8 @@ use ui::{h_flex, v_flex, ContextModal, IconName, Root, Sizable, StyledExt}; use crate::actions::{reset, DarkMode, KeyringPopup, Logout, Settings, ViewProfile, ViewRelays}; use crate::user::viewer; use crate::views::compose::compose_button; -use crate::views::{onboarding, preferences, setup_relay, sidebar, startup, welcome}; -use crate::{login, new_identity, user}; +use crate::views::{onboarding, preferences, setup_relay, startup, welcome}; +use crate::{login, new_identity, sidebar, user}; pub fn init(window: &mut Window, cx: &mut App) -> Entity { cx.new(|cx| ChatSpace::new(window, cx)) diff --git a/crates/coop/src/login/mod.rs b/crates/coop/src/login/mod.rs index 75e52210..a0d58bf2 100644 --- a/crates/coop/src/login/mod.rs +++ b/crates/coop/src/login/mod.rs @@ -111,7 +111,7 @@ impl Login { } fn login_with_bunker(&mut self, content: &str, window: &mut Window, cx: &mut Context) { - let Ok(uri) = NostrConnectURI::parse(content) else { + let Ok(uri) = NostrConnectUri::parse(content) else { self.set_error(t!("login.bunker_invalid"), cx); return; }; @@ -165,7 +165,7 @@ impl Login { fn save_connection( &mut self, keys: &Keys, - uri: &NostrConnectURI, + uri: &NostrConnectUri, window: &mut Window, cx: &mut Context, ) { diff --git a/crates/coop/src/main.rs b/crates/coop/src/main.rs index 8f496507..4493d62e 100644 --- a/crates/coop/src/main.rs +++ b/crates/coop/src/main.rs @@ -15,6 +15,7 @@ mod actions; mod chatspace; mod login; mod new_identity; +mod sidebar; mod user; mod views; diff --git a/crates/coop/src/views/sidebar/list_item.rs b/crates/coop/src/sidebar/list_item.rs similarity index 100% rename from crates/coop/src/views/sidebar/list_item.rs rename to crates/coop/src/sidebar/list_item.rs diff --git a/crates/coop/src/views/sidebar/mod.rs b/crates/coop/src/sidebar/mod.rs similarity index 69% rename from crates/coop/src/views/sidebar/mod.rs rename to crates/coop/src/sidebar/mod.rs index d0a45195..b923f3fe 100644 --- a/crates/coop/src/views/sidebar/mod.rs +++ b/crates/coop/src/sidebar/mod.rs @@ -1,7 +1,7 @@ -use std::collections::BTreeSet; use std::ops::Range; use std::time::Duration; +use account::Account; use anyhow::{anyhow, Error}; use chat::{ChatEvent, ChatRegistry, Room, RoomKind}; use common::{DebouncedDelay, RenderedTimestamp, TextUtils, BOOTSTRAP_RELAYS, SEARCH_RELAYS}; @@ -13,7 +13,6 @@ use gpui::{ }; use gpui_tokio::Tokio; use i18n::{shared_t, t}; -use itertools::Itertools; use list_item::RoomListItem; use nostr_sdk::prelude::*; use settings::AppSettings; @@ -31,42 +30,43 @@ use crate::actions::{RelayStatus, Reload}; mod list_item; const FIND_DELAY: u64 = 600; -const FIND_LIMIT: usize = 10; +const FIND_LIMIT: usize = 20; pub fn init(window: &mut Window, cx: &mut App) -> Entity { - Sidebar::new(window, cx) + cx.new(|cx| Sidebar::new(window, cx)) } pub struct Sidebar { name: SharedString, - // Search + + /// Focus handle for the sidebar + focus_handle: FocusHandle, + + /// Image cache + image_cache: Entity, + + /// Search results + search_results: Entity>>>, + + /// Async search operation + search_task: Option>, + find_input: Entity, find_debouncer: DebouncedDelay, finding: bool, - cancel_handle: Entity>>, - local_result: Entity>>>, - global_result: Entity>>>, - // Rooms + indicator: Entity>, active_filter: Entity, - // GPUI - focus_handle: FocusHandle, - image_cache: Entity, - #[allow(dead_code)] - subscriptions: SmallVec<[Subscription; 3]>, + + /// Event subscriptions + _subscriptions: SmallVec<[Subscription; 3]>, } impl Sidebar { - pub fn new(window: &mut Window, cx: &mut App) -> Entity { - cx.new(|cx| Self::view(window, cx)) - } - - fn view(window: &mut Window, cx: &mut Context) -> Self { + fn new(window: &mut Window, cx: &mut Context) -> Self { let active_filter = cx.new(|_| RoomKind::Ongoing); let indicator = cx.new(|_| None); - let local_result = cx.new(|_| None); - let global_result = cx.new(|_| None); - let cancel_handle = cx.new(|_| None); + let search_results = cx.new(|_| None); let find_input = cx.new(|cx| InputState::new(window, cx).placeholder(t!("sidebar.search_label"))); @@ -99,11 +99,13 @@ impl Sidebar { // Subscribe for find input events cx.subscribe_in(&find_input, window, |this, state, event, window, cx| { match event { - InputEvent::PressEnter { .. } => this.search(window, cx), + InputEvent::PressEnter { .. } => { + this.search(window, cx); + } InputEvent::Change => { // Clear the result when input is empty if state.read(cx).value().is_empty() { - this.clear_search_results(window, cx); + this.clear(window, cx); } else { // Run debounced search this.find_debouncer.fire_new( @@ -125,62 +127,65 @@ impl Sidebar { image_cache: RetainAllImageCache::new(cx), find_debouncer: DebouncedDelay::new(), finding: false, - cancel_handle, indicator, active_filter, find_input, - local_result, - global_result, - subscriptions, + search_results, + search_task: None, + _subscriptions: subscriptions, } } - async fn request_metadata(client: &Client, public_key: PublicKey) -> Result<(), Error> { - let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE); - let kinds = vec![Kind::Metadata, Kind::ContactList]; - let filter = Filter::new().author(public_key).kinds(kinds).limit(10); - - client - .subscribe_to(BOOTSTRAP_RELAYS, filter, Some(opts)) - .await?; - - Ok(()) - } - - async fn nip50(client: &Client, query: &str) -> Result, Error> { + async fn nip50(client: &Client, query: &str) -> Result, Error> { let signer = client.signer().await?; let public_key = signer.get_public_key().await?; - let timeout = Duration::from_secs(2); - let mut rooms: BTreeSet = BTreeSet::new(); - let filter = Filter::new() .kind(Kind::Metadata) .search(query.to_lowercase()) .limit(FIND_LIMIT); - if let Ok(events) = client - .fetch_events_from(SEARCH_RELAYS, filter, timeout) - .await - { - // Process to verify the search results - for event in events.into_iter().unique_by(|event| event.pubkey) { - // Skip if author is match current user - if event.pubkey == public_key { - continue; - } + let mut stream = client + .stream_events_from(SEARCH_RELAYS, filter, Duration::from_secs(3)) + .await?; - // Request metadata event's author - Self::request_metadata(client, event.pubkey).await?; + let mut results: Vec = Vec::with_capacity(FIND_LIMIT); - // Construct room - let room = Room::new(None, public_key, vec![event.pubkey]); - - rooms.insert(room); + while let Some(event) = stream.next().await { + // Skip if author is match current user + if event.pubkey == public_key { + continue; } + + // Skip if the event has already been added + if results.iter().any(|this| this.pubkey == event.pubkey) { + continue; + } + + results.push(event); } - Ok(rooms) + if results.is_empty() { + return Err(anyhow!("No results for query {query}")); + } + + // Get all public keys + let public_keys: Vec = results.iter().map(|event| event.pubkey).collect(); + + // Fetch metadata and contact lists if public keys is not empty + if !public_keys.is_empty() { + let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE); + let filter = Filter::new() + .kinds(vec![Kind::Metadata, Kind::ContactList]) + .limit(public_keys.len() * 2) + .authors(public_keys); + + client + .subscribe_to(BOOTSTRAP_RELAYS, filter, Some(opts)) + .await?; + } + + Ok(results) } fn debounced_search(&self, window: &mut Window, cx: &mut Context) -> Task<()> { @@ -192,63 +197,38 @@ impl Sidebar { }) } - fn search_by_nip50( - &mut self, - query: &str, - rx: smol::channel::Receiver<()>, - window: &mut Window, - cx: &mut Context, - ) { + fn search_by_nip50(&mut self, query: &str, window: &mut Window, cx: &mut Context) { + let account = Account::global(cx); + let public_key = account.read(cx).public_key(); + let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); + let query = query.to_owned(); - let query_cloned = query.clone(); - let task = smol::future::or( - Tokio::spawn(cx, async move { Self::nip50(&client, &query).await.ok() }), - Tokio::spawn(cx, async move { - let _ = rx.recv().await.is_ok(); - None - }), - ); + self.search_task = Some(cx.spawn_in(window, async move |this, cx| { + let result = Self::nip50(&client, &query).await; - cx.spawn_in(window, async move |this, cx| { - match task.await { - Ok(Some(results)) => { - this.update_in(cx, |this, window, cx| { - let msg = t!("sidebar.empty", query = query_cloned); - let rooms = results.into_iter().map(|r| cx.new(|_| r)).collect_vec(); + this.update_in(cx, |this, window, cx| { + match result { + Ok(results) => { + let rooms = results + .into_iter() + .map(|event| { + cx.new(|_| Room::new(None, public_key, vec![event.pubkey])) + }) + .collect(); - if rooms.is_empty() { - window.push_notification(msg, cx); - } - - this.results(rooms, true, window, cx); - }) - .ok(); - } - // User cancelled the search - Ok(None) => { - cx.update(|window, cx| { - this.update(cx, |this, cx| { - this.set_finding(false, window, cx); - this.set_cancel_handle(None, cx); - }) - }) - .ok(); - } - // Async task failed - Err(e) => { - this.update_in(cx, |this, window, cx| { + this.set_results(rooms, cx); + } + Err(e) => { window.push_notification(e.to_string(), cx); - this.set_finding(false, window, cx); - this.set_cancel_handle(None, cx); - }) - .ok(); - } - }; - }) - .detach(); + } + }; + this.set_finding(false, window, cx); + }) + .ok(); + })); } fn search_by_nip05(&mut self, query: &str, window: &mut Window, cx: &mut Context) { @@ -261,7 +241,8 @@ impl Sidebar { Ok(profile) => { let signer = client.signer().await?; let public_key = signer.get_public_key().await?; - let room = Room::new(None, public_key, vec![profile.public_key]); + let receivers = vec![profile.public_key]; + let room = Room::new(None, public_key, receivers); Ok(room) } @@ -269,40 +250,25 @@ impl Sidebar { } }); - cx.spawn_in(window, async move |this, cx| { - match task.await { - Ok(Ok(room)) => { - cx.update(|window, cx| { - this.update(cx, |this, cx| { - this.results(vec![cx.new(|_| room)], true, window, cx); - }) - .ok(); - }) - .ok(); + self.search_task = Some(cx.spawn_in(window, async move |this, cx| { + let result = task.await; + + this.update_in(cx, |this, window, cx| { + match result { + Ok(Ok(room)) => { + this.set_results(vec![cx.new(|_| room)], cx); + } + Ok(Err(e)) => { + window.push_notification(e.to_string(), cx); + } + Err(e) => { + window.push_notification(e.to_string(), cx); + } } - Ok(Err(e)) => { - cx.update(|window, cx| { - this.update(cx, |this, cx| { - window.push_notification(e.to_string(), cx); - this.set_cancel_handle(None, cx); - this.set_finding(false, window, cx); - }) - }) - .ok(); - } - Err(e) => { - cx.update(|window, cx| { - this.update(cx, |this, cx| { - window.push_notification(e.to_string(), cx); - this.set_cancel_handle(None, cx); - this.set_finding(false, window, cx); - }) - }) - .ok(); - } - }; - }) - .detach(); + this.set_finding(false, window, cx); + }) + .ok(); + })); } fn search_by_pubkey(&mut self, query: &str, window: &mut Window, cx: &mut Context) { @@ -310,7 +276,7 @@ impl Sidebar { let client = nostr.read(cx).client(); let Ok(public_key) = query.to_public_key() else { - window.push_notification(t!("common.pubkey_invalid"), cx); + window.push_notification("Public Key is invalid", cx); self.set_finding(false, window, cx); return; }; @@ -318,46 +284,49 @@ impl Sidebar { let task: Task> = cx.background_spawn(async move { let signer = client.signer().await?; let author = signer.get_public_key().await?; - let room = Room::new(None, author, vec![public_key]); - Self::request_metadata(&client, public_key).await?; + let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE); + let receivers = vec![public_key]; + let room = Room::new(None, author, receivers); + + let filter = Filter::new() + .kinds(vec![Kind::Metadata, Kind::ContactList]) + .author(public_key) + .limit(2); + + client + .subscribe_to(BOOTSTRAP_RELAYS, filter, Some(opts)) + .await?; Ok(room) }); - cx.spawn_in(window, async move |this, cx| { - match task.await { - Ok(room) => { - cx.update(|window, cx| { - this.update(cx, |this, cx| { - let chat = ChatRegistry::global(cx); - let result = chat.read(cx).search_by_public_key(public_key, cx); + self.search_task = Some(cx.spawn_in(window, async move |this, cx| { + let result = task.await; - if !result.is_empty() { - this.results(result, false, window, cx); - } else { - this.results(vec![cx.new(|_| room)], true, window, cx); - } - }) - .ok(); - }) - .ok(); - } - Err(e) => { - cx.update(|window, cx| { + this.update_in(cx, |this, window, cx| { + match result { + Ok(room) => { + let chat = ChatRegistry::global(cx); + let local_results = chat.read(cx).search_by_public_key(public_key, cx); + + if !local_results.is_empty() { + this.set_results(local_results, cx); + } else { + this.set_results(vec![cx.new(|_| room)], cx); + } + } + Err(e) => { window.push_notification(e.to_string(), cx); - }) - .ok(); - } - }; - }) - .detach(); + } + }; + this.set_finding(false, window, cx); + }) + .ok(); + })); } fn search(&mut self, window: &mut Window, cx: &mut Context) { - let (tx, rx) = smol::channel::bounded::<()>(1); - let tx_clone = tx.clone(); - // Return if the query is empty if self.find_input.read(cx).value().is_empty() { return; @@ -365,15 +334,12 @@ impl Sidebar { // Return if search is in progress if self.finding { - if self.cancel_handle.read(cx).is_none() { - window.push_notification(t!("sidebar.search_in_progress"), cx); + if self.search_task.is_none() { + window.push_notification("There is another search in progress", cx); return; } else { - // This is a hack to cancel ongoing search request - cx.background_spawn(async move { - tx.send(()).await.ok(); - }) - .detach(); + // Cancel ongoing search request + self.search_task = None; } } @@ -398,48 +364,25 @@ impl Sidebar { } } - let chat = ChatRegistry::global(cx); // Get all local results with current query + let chat = ChatRegistry::global(cx); let local_results = chat.read(cx).search(&query, cx); + // Try to update with local results first if !local_results.is_empty() { - // Try to update with local results first - self.results(local_results, false, window, cx); - } else { - // If no local results, try global search via NIP-50 - self.set_cancel_handle(Some(tx_clone), cx); - self.search_by_nip50(&query, rx, window, cx); - } + self.set_results(local_results, cx); + return; + }; + + // If no local results, try global search via NIP-50 + self.search_by_nip50(&query, window, cx); } - fn results( - &mut self, - rooms: Vec>, - global: bool, - window: &mut Window, - cx: &mut Context, - ) { - if self.finding { - self.set_finding(false, window, cx); - } - - if self.cancel_handle.read(cx).is_some() { - self.set_cancel_handle(None, cx); - } - - if !rooms.is_empty() { - if global { - self.global_result.update(cx, |this, cx| { - *this = Some(rooms); - cx.notify(); - }); - } else { - self.local_result.update(cx, |this, cx| { - *this = Some(rooms); - cx.notify(); - }); - } - } + fn set_results(&mut self, rooms: Vec>, cx: &mut Context) { + self.search_results.update(cx, |this, cx| { + *this = Some(rooms); + cx.notify(); + }); } fn set_finding(&mut self, status: bool, _window: &mut Window, cx: &mut Context) { @@ -453,31 +396,14 @@ impl Sidebar { cx.notify(); } - fn set_cancel_handle( - &mut self, - handle: Option>, - cx: &mut Context, - ) { - self.cancel_handle.update(cx, |this, cx| { - *this = handle; - cx.notify(); - }); - } - - fn clear_search_results(&mut self, window: &mut Window, cx: &mut Context) { + fn clear(&mut self, window: &mut Window, cx: &mut Context) { // Reset the input state if self.finding { self.set_finding(false, window, cx); } // Clear all local results - self.local_result.update(cx, |this, cx| { - *this = None; - cx.notify(); - }); - - // Clear all global results - self.global_result.update(cx, |this, cx| { + self.search_results.update(cx, |this, cx| { *this = None; cx.notify(); }); @@ -499,10 +425,11 @@ impl Sidebar { } fn open_room(&mut self, id: u64, window: &mut Window, cx: &mut Context) { - let room = if let Some(room) = ChatRegistry::global(cx).read(cx).room(&id, cx) { + let chat = ChatRegistry::global(cx); + let room = if let Some(room) = chat.read(cx).room(&id, cx) { room } else { - let Some(result) = self.global_result.read(cx).as_ref() else { + let Some(result) = self.search_results.read(cx).as_ref() else { window.push_notification(t!("common.room_error"), cx); return; }; @@ -513,12 +440,12 @@ impl Sidebar { }; // Clear all search results - self.clear_search_results(window, cx); + self.clear(window, cx); room }; - ChatRegistry::global(cx).update(cx, |this, cx| { + chat.update(cx, |this, cx| { this.push_room(room, cx); }); } @@ -673,12 +600,10 @@ impl Render for Sidebar { let loading = chat.read(cx).loading; // Get rooms from either search results or the chat registry - let rooms = if let Some(results) = self.local_result.read(cx).as_ref() { - results.to_owned() - } else if let Some(results) = self.global_result.read(cx).as_ref() { + let rooms = if let Some(results) = self.search_results.read(cx).as_ref() { results.to_owned() } else { - #[allow(clippy::collapsible_else_if)] + // Filter rooms based on the active filter if self.active_filter.read(cx) == &RoomKind::Ongoing { chat.read(cx).ongoing_rooms(cx) } else { @@ -717,13 +642,19 @@ impl Render for Sidebar { .cleanable() .appearance(true) .text_xs() - .suffix( - Button::new("find") - .icon(IconName::Search) - .tooltip(t!("sidebar.search_tooltip")) - .transparent() - .small(), - ), + .map(|this| { + if !self.find_input.read(cx).loading { + this.suffix( + Button::new("find") + .icon(IconName::Search) + .tooltip(t!("sidebar.search_tooltip")) + .transparent() + .small(), + ) + } else { + this + } + }), ), ) // Chat Rooms diff --git a/crates/coop/src/views/mod.rs b/crates/coop/src/views/mod.rs index eb878ebf..2e6c806b 100644 --- a/crates/coop/src/views/mod.rs +++ b/crates/coop/src/views/mod.rs @@ -3,6 +3,5 @@ pub mod onboarding; pub mod preferences; pub mod screening; pub mod setup_relay; -pub mod sidebar; pub mod startup; pub mod welcome; diff --git a/crates/coop/src/views/onboarding.rs b/crates/coop/src/views/onboarding.rs index df413258..bc1d7b00 100644 --- a/crates/coop/src/views/onboarding.rs +++ b/crates/coop/src/views/onboarding.rs @@ -78,7 +78,7 @@ impl Onboarding { let timeout = Duration::from_secs(NOSTR_CONNECT_TIMEOUT); let relay = RelayUrl::parse(NOSTR_CONNECT_RELAY).unwrap(); - let uri = NostrConnectURI::client(app_keys.public_key(), vec![relay], CLIENT_NAME); + let uri = NostrConnectUri::client(app_keys.public_key(), vec![relay], CLIENT_NAME); let qr_code = uri.to_string().to_qr(); // NIP46: https://github.com/nostr-protocol/nips/blob/master/46.md @@ -119,7 +119,7 @@ impl Onboarding { fn save_connection( &mut self, - uri: &NostrConnectURI, + uri: &NostrConnectUri, window: &mut Window, cx: &mut Context, ) { diff --git a/crates/coop/src/views/startup.rs b/crates/coop/src/views/startup.rs index 39cde991..fafc749a 100644 --- a/crates/coop/src/views/startup.rs +++ b/crates/coop/src/views/startup.rs @@ -76,7 +76,7 @@ impl Startup { // Try to login with bunker if secret.starts_with("bunker://") { - match NostrConnectURI::parse(secret) { + match NostrConnectUri::parse(secret) { Ok(uri) => { self.login_with_bunker(uri, window, cx); } @@ -102,7 +102,7 @@ impl Startup { fn login_with_bunker( &mut self, - uri: NostrConnectURI, + uri: NostrConnectUri, window: &mut Window, cx: &mut Context, ) { diff --git a/crates/state/src/lib.rs b/crates/state/src/lib.rs index afe0d7fa..9c1d14ec 100644 --- a/crates/state/src/lib.rs +++ b/crates/state/src/lib.rs @@ -69,9 +69,15 @@ impl NostrRegistry { timeout: Duration::from_secs(600), }); + // Construct the lmdb + let lmdb = cx.background_executor().block(async move { + let path = config_dir().join("nostr"); + NostrLMDB::open(path) + .await + .expect("Failed to initialize database") + }); + // Construct the nostr client - let path = config_dir().join("nostr"); - let lmdb = NostrLMDB::open(path).expect("Failed to initialize database"); let client = ClientBuilder::default().database(lmdb).opts(opts).build(); let tracker = Arc::new(RwLock::new(EventTracker::default()));