feat: refine the search bar (#207)

* update deps

* refactor the search cancellation

* .

* .
This commit is contained in:
reya
2025-11-22 07:25:08 +07:00
committed by GitHub
parent a6e00b47d8
commit 14c36e4731
11 changed files with 271 additions and 344 deletions

144
Cargo.lock generated
View File

@@ -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",

View File

@@ -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";

View File

@@ -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<ChatSpace> {
cx.new(|cx| ChatSpace::new(window, cx))

View File

@@ -111,7 +111,7 @@ impl Login {
}
fn login_with_bunker(&mut self, content: &str, window: &mut Window, cx: &mut Context<Self>) {
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<Self>,
) {

View File

@@ -15,6 +15,7 @@ mod actions;
mod chatspace;
mod login;
mod new_identity;
mod sidebar;
mod user;
mod views;

View File

@@ -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> {
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<RetainAllImageCache>,
/// Search results
search_results: Entity<Option<Vec<Entity<Room>>>>,
/// Async search operation
search_task: Option<Task<()>>,
find_input: Entity<InputState>,
find_debouncer: DebouncedDelay<Self>,
finding: bool,
cancel_handle: Entity<Option<smol::channel::Sender<()>>>,
local_result: Entity<Option<Vec<Entity<Room>>>>,
global_result: Entity<Option<Vec<Entity<Room>>>>,
// Rooms
indicator: Entity<Option<RoomKind>>,
active_filter: Entity<RoomKind>,
// GPUI
focus_handle: FocusHandle,
image_cache: Entity<RetainAllImageCache>,
#[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<Self> {
cx.new(|cx| Self::view(window, cx))
}
fn view(window: &mut Window, cx: &mut Context<Self>) -> Self {
fn new(window: &mut Window, cx: &mut Context<Self>) -> 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<BTreeSet<Room>, Error> {
async fn nip50(client: &Client, query: &str) -> Result<Vec<Event>, Error> {
let signer = client.signer().await?;
let public_key = signer.get_public_key().await?;
let timeout = Duration::from_secs(2);
let mut rooms: BTreeSet<Room> = 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<Event> = 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<PublicKey> = 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<Self>) -> 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<Self>,
) {
fn search_by_nip50(&mut self, query: &str, window: &mut Window, cx: &mut Context<Self>) {
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<Self>) {
@@ -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<Self>) {
@@ -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<Result<Room, Error>> = 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<Self>) {
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<Entity<Room>>,
global: bool,
window: &mut Window,
cx: &mut Context<Self>,
) {
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<Entity<Room>>, cx: &mut Context<Self>) {
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<Self>) {
@@ -453,31 +396,14 @@ impl Sidebar {
cx.notify();
}
fn set_cancel_handle(
&mut self,
handle: Option<smol::channel::Sender<()>>,
cx: &mut Context<Self>,
) {
self.cancel_handle.update(cx, |this, cx| {
*this = handle;
cx.notify();
});
}
fn clear_search_results(&mut self, window: &mut Window, cx: &mut Context<Self>) {
fn clear(&mut self, window: &mut Window, cx: &mut Context<Self>) {
// 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<Self>) {
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

View File

@@ -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;

View File

@@ -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<Self>,
) {

View File

@@ -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<Self>,
) {

View File

@@ -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()));