chore: improve search (#83)
* . * . * wip: add nip05 search * add nip05 search * . * support cancel search * .
This commit is contained in:
40
Cargo.lock
generated
40
Cargo.lock
generated
@@ -521,7 +521,7 @@ dependencies = [
|
|||||||
"bitflags 2.9.1",
|
"bitflags 2.9.1",
|
||||||
"cexpr",
|
"cexpr",
|
||||||
"clang-sys",
|
"clang-sys",
|
||||||
"itertools 0.12.1",
|
"itertools 0.11.0",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"lazycell",
|
"lazycell",
|
||||||
"log",
|
"log",
|
||||||
@@ -544,7 +544,7 @@ dependencies = [
|
|||||||
"bitflags 2.9.1",
|
"bitflags 2.9.1",
|
||||||
"cexpr",
|
"cexpr",
|
||||||
"clang-sys",
|
"clang-sys",
|
||||||
"itertools 0.13.0",
|
"itertools 0.11.0",
|
||||||
"log",
|
"log",
|
||||||
"prettyplease",
|
"prettyplease",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
@@ -1075,7 +1075,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collections"
|
name = "collections"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#f1db3b4e1d639b3aacbe472ddeb4471d844d4e04"
|
source = "git+https://github.com/zed-industries/zed#df57754baf64b118972ee3c42f68a0acc23989ff"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap",
|
"indexmap",
|
||||||
"rustc-hash 2.1.1",
|
"rustc-hash 2.1.1",
|
||||||
@@ -1176,6 +1176,7 @@ dependencies = [
|
|||||||
"futures",
|
"futures",
|
||||||
"global",
|
"global",
|
||||||
"gpui",
|
"gpui",
|
||||||
|
"gpui_tokio",
|
||||||
"i18n",
|
"i18n",
|
||||||
"identity",
|
"identity",
|
||||||
"itertools 0.13.0",
|
"itertools 0.13.0",
|
||||||
@@ -1462,7 +1463,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "derive_refineable"
|
name = "derive_refineable"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#f1db3b4e1d639b3aacbe472ddeb4471d844d4e04"
|
source = "git+https://github.com/zed-industries/zed#df57754baf64b118972ee3c42f68a0acc23989ff"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -2308,7 +2309,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "gpui"
|
name = "gpui"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#f1db3b4e1d639b3aacbe472ddeb4471d844d4e04"
|
source = "git+https://github.com/zed-industries/zed#df57754baf64b118972ee3c42f68a0acc23989ff"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"as-raw-xcb-connection",
|
"as-raw-xcb-connection",
|
||||||
@@ -2400,7 +2401,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "gpui_macros"
|
name = "gpui_macros"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#f1db3b4e1d639b3aacbe472ddeb4471d844d4e04"
|
source = "git+https://github.com/zed-industries/zed#df57754baf64b118972ee3c42f68a0acc23989ff"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"heck 0.5.0",
|
"heck 0.5.0",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
@@ -2409,6 +2410,17 @@ dependencies = [
|
|||||||
"workspace-hack",
|
"workspace-hack",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "gpui_tokio"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "git+https://github.com/zed-industries/zed#df57754baf64b118972ee3c42f68a0acc23989ff"
|
||||||
|
dependencies = [
|
||||||
|
"gpui",
|
||||||
|
"tokio",
|
||||||
|
"util",
|
||||||
|
"workspace-hack",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "grid"
|
name = "grid"
|
||||||
version = "0.13.0"
|
version = "0.13.0"
|
||||||
@@ -2623,7 +2635,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "http_client"
|
name = "http_client"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#f1db3b4e1d639b3aacbe472ddeb4471d844d4e04"
|
source = "git+https://github.com/zed-industries/zed#df57754baf64b118972ee3c42f68a0acc23989ff"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bytes",
|
"bytes",
|
||||||
@@ -2640,7 +2652,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "http_client_tls"
|
name = "http_client_tls"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#f1db3b4e1d639b3aacbe472ddeb4471d844d4e04"
|
source = "git+https://github.com/zed-industries/zed#df57754baf64b118972ee3c42f68a0acc23989ff"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"rustls",
|
"rustls",
|
||||||
"rustls-platform-verifier",
|
"rustls-platform-verifier",
|
||||||
@@ -3417,7 +3429,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "media"
|
name = "media"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#f1db3b4e1d639b3aacbe472ddeb4471d844d4e04"
|
source = "git+https://github.com/zed-industries/zed#df57754baf64b118972ee3c42f68a0acc23989ff"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bindgen 0.71.1",
|
"bindgen 0.71.1",
|
||||||
@@ -4722,7 +4734,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "refineable"
|
name = "refineable"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#f1db3b4e1d639b3aacbe472ddeb4471d844d4e04"
|
source = "git+https://github.com/zed-industries/zed#df57754baf64b118972ee3c42f68a0acc23989ff"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"derive_refineable",
|
"derive_refineable",
|
||||||
"workspace-hack",
|
"workspace-hack",
|
||||||
@@ -4875,7 +4887,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "reqwest_client"
|
name = "reqwest_client"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#f1db3b4e1d639b3aacbe472ddeb4471d844d4e04"
|
source = "git+https://github.com/zed-industries/zed#df57754baf64b118972ee3c42f68a0acc23989ff"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bytes",
|
"bytes",
|
||||||
@@ -5357,7 +5369,7 @@ checksum = "0f7d95a54511e0c7be3f51e8867aa8cf35148d7b9445d44de2f943e2b206e749"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "semantic_version"
|
name = "semantic_version"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#f1db3b4e1d639b3aacbe472ddeb4471d844d4e04"
|
source = "git+https://github.com/zed-industries/zed#df57754baf64b118972ee3c42f68a0acc23989ff"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -5733,7 +5745,7 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "sum_tree"
|
name = "sum_tree"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#f1db3b4e1d639b3aacbe472ddeb4471d844d4e04"
|
source = "git+https://github.com/zed-industries/zed#df57754baf64b118972ee3c42f68a0acc23989ff"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arrayvec",
|
"arrayvec",
|
||||||
"log",
|
"log",
|
||||||
@@ -6642,7 +6654,7 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "util"
|
name = "util"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#f1db3b4e1d639b3aacbe472ddeb4471d844d4e04"
|
source = "git+https://github.com/zed-industries/zed#df57754baf64b118972ee3c42f68a0acc23989ff"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-fs",
|
"async-fs",
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ i18n = { path = "crates/i18n" }
|
|||||||
|
|
||||||
# GPUI
|
# GPUI
|
||||||
gpui = { git = "https://github.com/zed-industries/zed" }
|
gpui = { git = "https://github.com/zed-industries/zed" }
|
||||||
|
gpui_tokio = { git = "https://github.com/zed-industries/zed" }
|
||||||
reqwest_client = { git = "https://github.com/zed-industries/zed" }
|
reqwest_client = { git = "https://github.com/zed-industries/zed" }
|
||||||
|
|
||||||
# Nostr
|
# Nostr
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ use std::collections::HashSet;
|
|||||||
use std::hash::{DefaultHasher, Hash, Hasher};
|
use std::hash::{DefaultHasher, Hash, Hasher};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use anyhow::anyhow;
|
||||||
use gpui::{Image, ImageFormat};
|
use gpui::{Image, ImageFormat};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
@@ -32,6 +33,16 @@ pub fn room_hash(event: &Event) -> u64 {
|
|||||||
hasher.finish()
|
hasher.finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn parse_pubkey_from_str(content: &str) -> Result<PublicKey, anyhow::Error> {
|
||||||
|
if content.starts_with("nprofile1") {
|
||||||
|
Ok(Nip19Profile::from_bech32(content)?.public_key)
|
||||||
|
} else if content.starts_with("npub1") {
|
||||||
|
Ok(PublicKey::parse(content)?)
|
||||||
|
} else {
|
||||||
|
Err(anyhow!("Invalid public key"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn string_to_qr(data: &str) -> Option<Arc<Image>> {
|
pub fn string_to_qr(data: &str) -> Option<Arc<Image>> {
|
||||||
let Ok(bytes) = qrcode_generator::to_png_to_vec_from_str(data, QrCodeEcc::Medium, 256) else {
|
let Ok(bytes) = qrcode_generator::to_png_to_vec_from_str(data, QrCodeEcc::Medium, 256) else {
|
||||||
return None;
|
return None;
|
||||||
|
|||||||
@@ -1,48 +0,0 @@
|
|||||||
use global::constants::IMAGE_RESIZE_SERVICE;
|
|
||||||
use gpui::SharedString;
|
|
||||||
use nostr_sdk::prelude::*;
|
|
||||||
|
|
||||||
const FALLBACK_IMG: &str = "https://image.nostr.build/c30703b48f511c293a9003be8100cdad37b8798b77a1dc3ec6eb8a20443d5dea.png";
|
|
||||||
|
|
||||||
pub trait RenderProfile {
|
|
||||||
fn render_avatar(&self, proxy: bool) -> SharedString;
|
|
||||||
fn render_name(&self) -> SharedString;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RenderProfile for Profile {
|
|
||||||
fn render_avatar(&self, proxy: bool) -> SharedString {
|
|
||||||
self.metadata()
|
|
||||||
.picture
|
|
||||||
.as_ref()
|
|
||||||
.filter(|picture| !picture.is_empty())
|
|
||||||
.map(|picture| {
|
|
||||||
if proxy {
|
|
||||||
format!(
|
|
||||||
"{IMAGE_RESIZE_SERVICE}/?url={picture}&w=100&h=100&fit=cover&mask=circle&default={FALLBACK_IMG}&n=-1"
|
|
||||||
)
|
|
||||||
.into()
|
|
||||||
} else {
|
|
||||||
picture.into()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.unwrap_or_else(|| "brand/avatar.png".into())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_name(&self) -> SharedString {
|
|
||||||
if let Some(display_name) = self.metadata().display_name.as_ref() {
|
|
||||||
if !display_name.is_empty() {
|
|
||||||
return display_name.into();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(name) = self.metadata().name.as_ref() {
|
|
||||||
if !name.is_empty() {
|
|
||||||
return name.into();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let pubkey = self.public_key().to_hex();
|
|
||||||
|
|
||||||
format!("{}:{}", &pubkey[0..4], &pubkey[pubkey.len() - 4..]).into()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -22,6 +22,7 @@ auto_update = { path = "../auto_update" }
|
|||||||
rust-i18n.workspace = true
|
rust-i18n.workspace = true
|
||||||
i18n.workspace = true
|
i18n.workspace = true
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
|
gpui_tokio.workspace = true
|
||||||
reqwest_client.workspace = true
|
reqwest_client.workspace = true
|
||||||
|
|
||||||
nostr-connect.workspace = true
|
nostr-connect.workspace = true
|
||||||
|
|||||||
@@ -234,6 +234,8 @@ fn main() {
|
|||||||
// Root Entity
|
// Root Entity
|
||||||
cx.new(|cx| {
|
cx.new(|cx| {
|
||||||
cx.activate(true);
|
cx.activate(true);
|
||||||
|
// Initialize the tokio runtime
|
||||||
|
gpui_tokio::init(cx);
|
||||||
// Initialize components
|
// Initialize components
|
||||||
ui::init(cx);
|
ui::init(cx);
|
||||||
// Initialize app registry
|
// Initialize app registry
|
||||||
|
|||||||
@@ -147,16 +147,6 @@ impl Compose {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_pubkey(content: &str) -> Result<PublicKey, Error> {
|
|
||||||
if content.starts_with("nprofile1") {
|
|
||||||
Ok(Nip19Profile::from_bech32(content)?.public_key)
|
|
||||||
} else if content.starts_with("npub1") {
|
|
||||||
Ok(PublicKey::parse(content)?)
|
|
||||||
} else {
|
|
||||||
Err(anyhow!(t!("common.pubkey_invalid")))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn compose(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
pub fn compose(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
let public_keys: Vec<PublicKey> = self.selected(cx);
|
let public_keys: Vec<PublicKey> = self.selected(cx);
|
||||||
|
|
||||||
@@ -288,7 +278,7 @@ impl Compose {
|
|||||||
Err(anyhow!(t!("common.not_found")))
|
Err(anyhow!(t!("common.not_found")))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} else if let Ok(public_key) = Self::parse_pubkey(&content) {
|
} else if let Ok(public_key) = common::parse_pubkey_from_str(&content) {
|
||||||
cx.background_spawn(async move {
|
cx.background_spawn(async move {
|
||||||
let client = nostr_client();
|
let client = nostr_client();
|
||||||
let contact = Contact::new(public_key).select();
|
let contact = Contact::new(public_key).select();
|
||||||
|
|||||||
@@ -2,12 +2,12 @@ use std::collections::BTreeSet;
|
|||||||
use std::ops::Range;
|
use std::ops::Range;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use anyhow::Error;
|
use anyhow::{anyhow, Error};
|
||||||
use common::debounced_delay::DebouncedDelay;
|
use common::debounced_delay::DebouncedDelay;
|
||||||
use common::display::DisplayProfile;
|
use common::display::DisplayProfile;
|
||||||
use common::nip05::nip05_verify;
|
use common::nip05::nip05_verify;
|
||||||
use element::DisplayRoom;
|
use element::DisplayRoom;
|
||||||
use global::constants::{DEFAULT_MODAL_WIDTH, SEARCH_RELAYS};
|
use global::constants::{BOOTSTRAP_RELAYS, DEFAULT_MODAL_WIDTH, SEARCH_RELAYS};
|
||||||
use global::nostr_client;
|
use global::nostr_client;
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
@@ -16,6 +16,7 @@ use gpui::{
|
|||||||
Render, RetainAllImageCache, SharedString, StatefulInteractiveElement, Styled, Subscription,
|
Render, RetainAllImageCache, SharedString, StatefulInteractiveElement, Styled, Subscription,
|
||||||
Task, Window,
|
Task, Window,
|
||||||
};
|
};
|
||||||
|
use gpui_tokio::Tokio;
|
||||||
use i18n::t;
|
use i18n::t;
|
||||||
use identity::Identity;
|
use identity::Identity;
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
@@ -30,7 +31,6 @@ use ui::button::{Button, ButtonRounded, ButtonVariants};
|
|||||||
use ui::dock_area::panel::{Panel, PanelEvent};
|
use ui::dock_area::panel::{Panel, PanelEvent};
|
||||||
use ui::indicator::Indicator;
|
use ui::indicator::Indicator;
|
||||||
use ui::input::{InputEvent, InputState, TextInput};
|
use ui::input::{InputEvent, InputState, TextInput};
|
||||||
use ui::notification::Notification;
|
|
||||||
use ui::popup_menu::PopupMenu;
|
use ui::popup_menu::PopupMenu;
|
||||||
use ui::skeleton::Skeleton;
|
use ui::skeleton::Skeleton;
|
||||||
use ui::{ContextModal, IconName, Selectable, Sizable, StyledExt};
|
use ui::{ContextModal, IconName, Selectable, Sizable, StyledExt};
|
||||||
@@ -52,6 +52,7 @@ pub struct Sidebar {
|
|||||||
find_input: Entity<InputState>,
|
find_input: Entity<InputState>,
|
||||||
find_debouncer: DebouncedDelay<Self>,
|
find_debouncer: DebouncedDelay<Self>,
|
||||||
finding: bool,
|
finding: bool,
|
||||||
|
cancel_handle: Entity<Option<smol::channel::Sender<()>>>,
|
||||||
local_result: Entity<Option<Vec<Entity<Room>>>>,
|
local_result: Entity<Option<Vec<Entity<Room>>>>,
|
||||||
global_result: Entity<Option<Vec<Entity<Room>>>>,
|
global_result: Entity<Option<Vec<Entity<Room>>>>,
|
||||||
// Rooms
|
// Rooms
|
||||||
@@ -75,6 +76,7 @@ impl Sidebar {
|
|||||||
let indicator = cx.new(|_| None);
|
let indicator = cx.new(|_| None);
|
||||||
let local_result = cx.new(|_| None);
|
let local_result = cx.new(|_| None);
|
||||||
let global_result = cx.new(|_| None);
|
let global_result = cx.new(|_| None);
|
||||||
|
let cancel_handle = cx.new(|_| None);
|
||||||
|
|
||||||
let find_input = cx.new(|cx| {
|
let find_input = cx.new(|cx| {
|
||||||
InputState::new(window, cx).placeholder(t!("sidebar.find_or_start_conversation"))
|
InputState::new(window, cx).placeholder(t!("sidebar.find_or_start_conversation"))
|
||||||
@@ -105,7 +107,7 @@ impl Sidebar {
|
|||||||
InputEvent::Change(text) => {
|
InputEvent::Change(text) => {
|
||||||
// Clear the result when input is empty
|
// Clear the result when input is empty
|
||||||
if text.is_empty() {
|
if text.is_empty() {
|
||||||
this.clear_search_results(cx);
|
this.clear_search_results(window, cx);
|
||||||
} else {
|
} else {
|
||||||
// Run debounced search
|
// Run debounced search
|
||||||
this.find_debouncer.fire_new(
|
this.find_debouncer.fire_new(
|
||||||
@@ -128,6 +130,7 @@ impl Sidebar {
|
|||||||
find_debouncer: DebouncedDelay::new(),
|
find_debouncer: DebouncedDelay::new(),
|
||||||
finding: false,
|
finding: false,
|
||||||
trusted_only: false,
|
trusted_only: false,
|
||||||
|
cancel_handle,
|
||||||
indicator,
|
indicator,
|
||||||
active_filter,
|
active_filter,
|
||||||
find_input,
|
find_input,
|
||||||
@@ -137,6 +140,70 @@ impl Sidebar {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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, Kind::RelayList];
|
||||||
|
let filter = Filter::new().author(public_key).kinds(kinds).limit(10);
|
||||||
|
|
||||||
|
client
|
||||||
|
.subscribe_to(BOOTSTRAP_RELAYS, filter, Some(opts))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create_temp_room(identity: PublicKey, public_key: PublicKey) -> Result<Room, Error> {
|
||||||
|
let keys = Keys::generate();
|
||||||
|
let builder = EventBuilder::private_msg_rumor(public_key, "");
|
||||||
|
let event = builder.build(identity).sign(&keys).await?;
|
||||||
|
let room = Room::new(&event).kind(RoomKind::Ongoing);
|
||||||
|
|
||||||
|
Ok(room)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn nip50(identity: PublicKey, query: &str) -> BTreeSet<Room> {
|
||||||
|
let client = nostr_client();
|
||||||
|
let timeout = Duration::from_secs(2);
|
||||||
|
let mut rooms: BTreeSet<Room> = BTreeSet::new();
|
||||||
|
let mut processed: BTreeSet<PublicKey> = 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() {
|
||||||
|
if processed.contains(&event.pubkey) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
processed.insert(event.pubkey);
|
||||||
|
|
||||||
|
let metadata = Metadata::from_json(event.content).unwrap_or_default();
|
||||||
|
|
||||||
|
// Skip if NIP-05 is not found
|
||||||
|
let Some(target) = metadata.nip05.as_ref() else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Skip if NIP-05 is not valid or failed to verify
|
||||||
|
if !nip05_verify(event.pubkey, target).await.unwrap_or(false) {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Ok(room) = Self::create_temp_room(identity, event.pubkey).await {
|
||||||
|
rooms.insert(room);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rooms
|
||||||
|
}
|
||||||
|
|
||||||
fn debounced_search(&self, window: &mut Window, cx: &mut Context<Self>) -> Task<()> {
|
fn debounced_search(&self, window: &mut Window, cx: &mut Context<Self>) -> Task<()> {
|
||||||
cx.spawn_in(window, async move |this, cx| {
|
cx.spawn_in(window, async move |this, cx| {
|
||||||
cx.update(|window, cx| {
|
cx.update(|window, cx| {
|
||||||
@@ -149,97 +216,70 @@ impl Sidebar {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn search_by_nip50(&mut self, query: &str, window: &mut Window, cx: &mut Context<Self>) {
|
fn search_by_nip50(
|
||||||
|
&mut self,
|
||||||
|
query: &str,
|
||||||
|
rx: smol::channel::Receiver<()>,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
let Some(identity) = Identity::read_global(cx).public_key() else {
|
||||||
|
// User is not logged in. Stop searching
|
||||||
|
self.set_finding(false, window, cx);
|
||||||
|
self.set_cancel_handle(None, cx);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
let query = query.to_owned();
|
let query = query.to_owned();
|
||||||
let query_cloned = query.clone();
|
let query_cloned = query.clone();
|
||||||
|
|
||||||
let task: Task<Result<BTreeSet<Room>, Error>> = cx.background_spawn(async move {
|
let task = smol::future::or(
|
||||||
let client = nostr_client();
|
Tokio::spawn(cx, async move {
|
||||||
|
let rooms = Self::nip50(identity, &query).await;
|
||||||
let filter = Filter::new()
|
Some(rooms)
|
||||||
.kind(Kind::Metadata)
|
}),
|
||||||
.search(query.to_lowercase())
|
Tokio::spawn(cx, async move {
|
||||||
.limit(FIND_LIMIT);
|
let _ = rx.recv().await.is_ok();
|
||||||
|
None
|
||||||
let events = client
|
}),
|
||||||
.fetch_events_from(SEARCH_RELAYS, filter, Duration::from_secs(5))
|
);
|
||||||
.await?
|
|
||||||
.into_iter()
|
|
||||||
.unique_by(|event| event.pubkey)
|
|
||||||
.collect_vec();
|
|
||||||
|
|
||||||
let mut rooms = BTreeSet::new();
|
|
||||||
|
|
||||||
// Process to verify the search results
|
|
||||||
if !events.is_empty() {
|
|
||||||
let (tx, rx) = smol::channel::bounded::<Room>(events.len());
|
|
||||||
|
|
||||||
nostr_sdk::async_utility::task::spawn(async move {
|
|
||||||
let signer = client.signer().await.unwrap();
|
|
||||||
let public_key = signer.get_public_key().await.unwrap();
|
|
||||||
|
|
||||||
for event in events.into_iter() {
|
|
||||||
let metadata = Metadata::from_json(event.content).unwrap_or_default();
|
|
||||||
|
|
||||||
let Some(target) = metadata.nip05.as_ref() else {
|
|
||||||
// Skip if NIP-05 is not found
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
|
|
||||||
if !nip05_verify(event.pubkey, target).await.unwrap_or(false) {
|
|
||||||
// Skip if NIP-05 is not valid or failed to verify
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Ok(event) = EventBuilder::private_msg_rumor(event.pubkey, "")
|
|
||||||
.build(public_key)
|
|
||||||
.sign(&Keys::generate())
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
if let Err(e) = tx.send(Room::new(&event).kind(RoomKind::Ongoing)).await
|
|
||||||
{
|
|
||||||
log::error!("Send error: {e}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
while let Ok(room) = rx.recv().await {
|
|
||||||
rooms.insert(room);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(rooms)
|
|
||||||
});
|
|
||||||
|
|
||||||
cx.spawn_in(window, async move |this, cx| {
|
cx.spawn_in(window, async move |this, cx| {
|
||||||
match task.await {
|
match task.await {
|
||||||
Ok(result) => {
|
Ok(Some(results)) => {
|
||||||
cx.update(|window, cx| {
|
cx.update(|window, cx| {
|
||||||
this.update(cx, |this, cx| {
|
this.update(cx, |this, cx| {
|
||||||
if result.is_empty() {
|
let msg = t!("sidebar.empty", query = query_cloned);
|
||||||
window.push_notification(
|
let rooms = results.into_iter().map(|r| cx.new(|_| r)).collect_vec();
|
||||||
Notification::info(t!("sidebar.empty", query = query_cloned)),
|
|
||||||
cx,
|
if rooms.is_empty() {
|
||||||
);
|
window.push_notification(msg, cx);
|
||||||
this.set_finding(false, cx);
|
|
||||||
} else {
|
|
||||||
this.global_result(
|
|
||||||
result
|
|
||||||
.into_iter()
|
|
||||||
.map(|room| cx.new(|_| room))
|
|
||||||
.collect_vec(),
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.results(rooms, true, window, cx);
|
||||||
})
|
})
|
||||||
.ok();
|
.ok();
|
||||||
})
|
})
|
||||||
.ok();
|
.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) => {
|
Err(e) => {
|
||||||
cx.update(|window, cx| {
|
cx.update(|window, cx| {
|
||||||
window.push_notification(Notification::error(e.to_string()), cx);
|
this.update(cx, |this, cx| {
|
||||||
|
window.push_notification(e.to_string(), cx);
|
||||||
|
this.set_finding(false, window, cx);
|
||||||
|
this.set_cancel_handle(None, cx);
|
||||||
|
})
|
||||||
})
|
})
|
||||||
.ok();
|
.ok();
|
||||||
}
|
}
|
||||||
@@ -248,63 +288,110 @@ impl Sidebar {
|
|||||||
.detach();
|
.detach();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn search_by_user(&mut self, query: &str, window: &mut Window, cx: &mut Context<Self>) {
|
fn search_by_nip05(&mut self, query: &str, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
let public_key = if query.starts_with("npub1") {
|
let Some(identity) = Identity::read_global(cx).public_key() else {
|
||||||
PublicKey::parse(query).ok()
|
// User is not logged in. Stop searching
|
||||||
} else if query.starts_with("nprofile1") {
|
self.set_finding(false, window, cx);
|
||||||
Nip19Profile::from_bech32(query)
|
self.set_cancel_handle(None, cx);
|
||||||
.map(|nip19| nip19.public_key)
|
|
||||||
.ok()
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
let Some(public_key) = public_key else {
|
|
||||||
window.push_notification(t!("common.pubkey_invalid"), cx);
|
|
||||||
self.set_finding(false, cx);
|
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let task: Task<Result<(Profile, Room), Error>> = cx.background_spawn(async move {
|
let address = query.to_owned();
|
||||||
|
|
||||||
|
let task = Tokio::spawn(cx, async move {
|
||||||
let client = nostr_client();
|
let client = nostr_client();
|
||||||
let signer = client.signer().await.unwrap();
|
|
||||||
let user_pubkey = signer.get_public_key().await.unwrap();
|
|
||||||
|
|
||||||
let metadata = client
|
if let Ok(profile) = common::nip05::nip05_profile(&address).await {
|
||||||
.fetch_metadata(public_key, Duration::from_secs(3))
|
let public_key = profile.public_key;
|
||||||
.await?
|
// Request for user metadata
|
||||||
.unwrap_or_default();
|
Self::request_metadata(client, public_key).await.ok();
|
||||||
|
// Return a temporary room
|
||||||
let event = EventBuilder::private_msg_rumor(public_key, "")
|
Self::create_temp_room(identity, public_key).await
|
||||||
.build(user_pubkey)
|
} else {
|
||||||
.sign(&Keys::generate())
|
Err(anyhow!(t!("sidebar.addr_error")))
|
||||||
.await?;
|
}
|
||||||
|
|
||||||
let profile = Profile::new(public_key, metadata);
|
|
||||||
let room = Room::new(&event);
|
|
||||||
|
|
||||||
Ok((profile, room))
|
|
||||||
});
|
});
|
||||||
|
|
||||||
cx.spawn_in(window, async move |this, cx| {
|
cx.spawn_in(window, async move |this, cx| {
|
||||||
match task.await {
|
match task.await {
|
||||||
Ok((profile, room)) => {
|
Ok(Ok(room)) => {
|
||||||
this.update(cx, |this, cx| {
|
cx.update(|window, cx| {
|
||||||
let chats = Registry::global(cx);
|
this.update(cx, |this, cx| {
|
||||||
let result = chats
|
this.results(vec![cx.new(|_| room)], true, window, cx);
|
||||||
.read(cx)
|
})
|
||||||
.search_by_public_key(profile.public_key(), cx);
|
.ok();
|
||||||
|
})
|
||||||
if !result.is_empty() {
|
.ok();
|
||||||
this.local_result(result, cx);
|
}
|
||||||
}
|
Ok(Err(e)) => {
|
||||||
this.global_result(vec![cx.new(|_| room)], cx);
|
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();
|
.ok();
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
cx.update(|window, cx| {
|
cx.update(|window, cx| {
|
||||||
window.push_notification(Notification::error(e.to_string()), 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn search_by_pubkey(&mut self, query: &str, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
let Ok(public_key) = common::parse_pubkey_from_str(query) else {
|
||||||
|
window.push_notification(t!("common.pubkey_invalid"), cx);
|
||||||
|
self.set_finding(false, window, cx);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(identity) = Identity::read_global(cx).public_key() else {
|
||||||
|
// User is not logged in. Stop searching
|
||||||
|
self.set_finding(false, window, cx);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let task: Task<Result<Room, Error>> = cx.background_spawn(async move {
|
||||||
|
let client = nostr_client();
|
||||||
|
|
||||||
|
// Request metadata for this user
|
||||||
|
Self::request_metadata(client, public_key).await?;
|
||||||
|
|
||||||
|
// Create a gift wrap event to represent as room
|
||||||
|
Self::create_temp_room(identity, public_key).await
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.spawn_in(window, async move |this, cx| {
|
||||||
|
match task.await {
|
||||||
|
Ok(room) => {
|
||||||
|
cx.update(|window, cx| {
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
let registry = Registry::read_global(cx);
|
||||||
|
let result = registry.search_by_public_key(public_key, cx);
|
||||||
|
|
||||||
|
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| {
|
||||||
|
window.push_notification(e.to_string(), cx);
|
||||||
})
|
})
|
||||||
.ok();
|
.ok();
|
||||||
}
|
}
|
||||||
@@ -314,82 +401,119 @@ impl Sidebar {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn search(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
fn search(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
let query = self.find_input.read(cx).value().to_string();
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
// Return if search is in progress
|
// Return if search is in progress
|
||||||
if self.finding {
|
if self.finding {
|
||||||
window.push_notification(t!("sidebar.search_in_progress"), cx);
|
if self.cancel_handle.read(cx).is_none() {
|
||||||
return;
|
window.push_notification(t!("sidebar.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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return if the query is empty
|
let input = self.find_input.read(cx).value();
|
||||||
if query.is_empty() {
|
let query = input.to_string();
|
||||||
window.push_notification(t!("sidebar.empty_query"), cx);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return if the query starts with "nsec1" or "note1"
|
|
||||||
if query.starts_with("nsec1") || query.starts_with("note1") {
|
|
||||||
window.push_notification(t!("sidebar.not_support"), cx);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Block the input until the search process completes
|
// Block the input until the search process completes
|
||||||
self.set_finding(true, cx);
|
self.set_finding(true, window, cx);
|
||||||
|
|
||||||
// Process to search by user if query starts with npub or nprofile
|
// Process to search by pubkey if query starts with npub or nprofile
|
||||||
if query.starts_with("npub1") || query.starts_with("nprofile1") {
|
if query.starts_with("npub1") || query.starts_with("nprofile1") {
|
||||||
self.search_by_user(&query, window, cx);
|
self.search_by_pubkey(&query, window, cx);
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let chats = Registry::global(cx);
|
// Process to search by NIP05 if query is a valid NIP-05 identifier (name@domain.tld)
|
||||||
let result = chats.read(cx).search(&query, cx);
|
if query.split('@').count() == 2 {
|
||||||
|
let parts: Vec<&str> = query.split('@').collect();
|
||||||
|
if !parts[0].is_empty() && !parts[1].is_empty() && parts[1].contains('.') {
|
||||||
|
self.search_by_nip05(&query, window, cx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if result.is_empty() {
|
let chats = Registry::read_global(cx);
|
||||||
// There are no current rooms matching this query, so proceed with global search via NIP-50
|
// Get all local results with current query
|
||||||
self.search_by_nip50(&query, window, cx);
|
let local_results = chats.search(&query, cx);
|
||||||
|
|
||||||
|
if !local_results.is_empty() {
|
||||||
|
// Try to update with local results first
|
||||||
|
self.results(local_results, false, window, cx);
|
||||||
} else {
|
} else {
|
||||||
self.local_result(result, cx);
|
// 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn global_result(&mut self, rooms: Vec<Entity<Room>>, cx: &mut Context<Self>) {
|
fn results(
|
||||||
|
&mut self,
|
||||||
|
rooms: Vec<Entity<Room>>,
|
||||||
|
global: bool,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
if self.finding {
|
if self.finding {
|
||||||
self.set_finding(false, cx);
|
self.set_finding(false, window, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.global_result.update(cx, |this, cx| {
|
if self.cancel_handle.read(cx).is_some() {
|
||||||
*this = Some(rooms);
|
self.set_cancel_handle(None, cx);
|
||||||
cx.notify();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn local_result(&mut self, rooms: Vec<Entity<Room>>, cx: &mut Context<Self>) {
|
|
||||||
if self.finding {
|
|
||||||
self.set_finding(false, cx);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
self.local_result.update(cx, |this, cx| {
|
if !rooms.is_empty() {
|
||||||
*this = Some(rooms);
|
if global {
|
||||||
cx.notify();
|
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_finding(&mut self, status: bool, cx: &mut Context<Self>) {
|
fn set_finding(&mut self, status: bool, _window: &mut Window, cx: &mut Context<Self>) {
|
||||||
self.finding = status;
|
self.finding = status;
|
||||||
cx.notify();
|
|
||||||
// Disable the input to prevent duplicate requests
|
// Disable the input to prevent duplicate requests
|
||||||
self.find_input.update(cx, |this, cx| {
|
self.find_input.update(cx, |this, cx| {
|
||||||
this.set_disabled(status, cx);
|
this.set_disabled(status, cx);
|
||||||
this.set_loading(status, cx);
|
this.set_loading(status, cx);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn clear_search_results(&mut self, cx: &mut Context<Self>) {
|
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>) {
|
||||||
// Reset the input state
|
// Reset the input state
|
||||||
if self.finding {
|
if self.finding {
|
||||||
self.set_finding(false, cx);
|
self.set_finding(false, window, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear all local results
|
// Clear all local results
|
||||||
@@ -440,7 +564,7 @@ impl Sidebar {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Clear all search results
|
// Clear all search results
|
||||||
self.clear_search_results(cx);
|
self.clear_search_results(window, cx);
|
||||||
|
|
||||||
room
|
room
|
||||||
};
|
};
|
||||||
@@ -617,13 +741,14 @@ impl Focusable for Sidebar {
|
|||||||
impl Render for Sidebar {
|
impl Render for Sidebar {
|
||||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
let registry = Registry::read_global(cx);
|
let registry = Registry::read_global(cx);
|
||||||
|
|
||||||
let profile = Identity::read_global(cx)
|
let profile = Identity::read_global(cx)
|
||||||
.public_key()
|
.public_key()
|
||||||
.map(|pk| registry.get_person(&pk, cx));
|
.map(|pk| registry.get_person(&pk, cx));
|
||||||
|
|
||||||
// Get rooms from either search results or the chat registry
|
// Get rooms from either search results or the chat registry
|
||||||
let rooms = if let Some(results) = self.local_result.read(cx) {
|
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() {
|
||||||
results.to_owned()
|
results.to_owned()
|
||||||
} else {
|
} else {
|
||||||
#[allow(clippy::collapsible_else_if)]
|
#[allow(clippy::collapsible_else_if)]
|
||||||
@@ -647,37 +772,27 @@ impl Render for Sidebar {
|
|||||||
})
|
})
|
||||||
// Search Input
|
// Search Input
|
||||||
.child(
|
.child(
|
||||||
div().px_3().w_full().h_7().flex_none().child(
|
div()
|
||||||
TextInput::new(&self.find_input).small().suffix(
|
.relative()
|
||||||
Button::new("find")
|
.px_3()
|
||||||
.icon(IconName::Search)
|
.w_full()
|
||||||
.tooltip(t!("sidebar.press_enter_to_search"))
|
.h_7()
|
||||||
.transparent()
|
.flex_none()
|
||||||
.small(),
|
.flex()
|
||||||
|
.child(
|
||||||
|
TextInput::new(&self.find_input)
|
||||||
|
.small()
|
||||||
|
.cleanable()
|
||||||
|
.appearance(true)
|
||||||
|
.suffix(
|
||||||
|
Button::new("find")
|
||||||
|
.icon(IconName::Search)
|
||||||
|
.tooltip(t!("sidebar.press_enter_to_search"))
|
||||||
|
.transparent()
|
||||||
|
.small(),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
)
|
)
|
||||||
// Global Search Results
|
|
||||||
.when_some(self.global_result.read(cx).as_ref(), |this, rooms| {
|
|
||||||
this.child(div().px_2().w_full().flex().flex_col().gap_1().children({
|
|
||||||
let mut items = Vec::with_capacity(rooms.len());
|
|
||||||
|
|
||||||
for (ix, room) in rooms.iter().enumerate() {
|
|
||||||
let this = room.read(cx);
|
|
||||||
let id = this.id;
|
|
||||||
let label = this.display_name(cx);
|
|
||||||
let img = this.display_image(cx);
|
|
||||||
|
|
||||||
let handler = cx.listener(move |this, _, window, cx| {
|
|
||||||
this.open_room(id, window, cx);
|
|
||||||
});
|
|
||||||
|
|
||||||
items.push(DisplayRoom::new(ix).img(img).label(label).on_click(handler))
|
|
||||||
}
|
|
||||||
|
|
||||||
items
|
|
||||||
}))
|
|
||||||
})
|
|
||||||
// Chat Rooms
|
// Chat Rooms
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use gpui::{App, Styled};
|
use gpui::{App, Styled};
|
||||||
|
use i18n::t;
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
|
|
||||||
use crate::button::{Button, ButtonVariants as _};
|
use crate::button::{Button, ButtonVariants as _};
|
||||||
@@ -8,7 +9,8 @@ use crate::{Icon, IconName, Sizable as _};
|
|||||||
pub(crate) fn clear_button(cx: &App) -> Button {
|
pub(crate) fn clear_button(cx: &App) -> Button {
|
||||||
Button::new("clean")
|
Button::new("clean")
|
||||||
.icon(Icon::new(IconName::CloseCircle))
|
.icon(Icon::new(IconName::CloseCircle))
|
||||||
.ghost()
|
.tooltip(t!("common.clear"))
|
||||||
.xsmall()
|
.small()
|
||||||
.text_color(cx.theme().text_muted)
|
.text_color(cx.theme().text_muted)
|
||||||
|
.transparent()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -101,6 +101,16 @@ common:
|
|||||||
es: "Copiado"
|
es: "Copiado"
|
||||||
pt: "Copiado"
|
pt: "Copiado"
|
||||||
ko: "복사됨"
|
ko: "복사됨"
|
||||||
|
clear:
|
||||||
|
en: "Clear"
|
||||||
|
zh-CN: "清除"
|
||||||
|
zh-TW: "清除"
|
||||||
|
ru: "Очистить"
|
||||||
|
vi: "Xóa"
|
||||||
|
ja: "クリア"
|
||||||
|
es: "Limpiar"
|
||||||
|
pt: "Limpar"
|
||||||
|
ko: "지우기"
|
||||||
|
|
||||||
welcome:
|
welcome:
|
||||||
title:
|
title:
|
||||||
@@ -977,26 +987,16 @@ sidebar:
|
|||||||
es: "Hay otra búsqueda en curso"
|
es: "Hay otra búsqueda en curso"
|
||||||
pt: "Outra pesquisa está em andamento"
|
pt: "Outra pesquisa está em andamento"
|
||||||
ko: "다른 검색이 진행 중입니다"
|
ko: "다른 검색이 진행 중입니다"
|
||||||
empty_query:
|
addr_error:
|
||||||
en: "Cannot search with an empty query"
|
en: "Failed to get profile via address"
|
||||||
zh-CN: "无法使用空查询进行搜索"
|
zh-CN: "通过地址获取资料失败"
|
||||||
zh-TW: "無法使用空查詢進行搜尋"
|
zh-TW: "透過地址獲取資料失敗"
|
||||||
ru: "Невозможно выполнить поиск с пустым запросом"
|
ru: "Не удалось получить профиль по адресу"
|
||||||
vi: "Không thể tìm kiếm với truy vấn rỗng"
|
vi: "Không thể lấy thông tin theo địa chỉ"
|
||||||
ja: "空のクエリで検索することはできません"
|
ja: "アドレスからプロフィールを取得できませんでした"
|
||||||
es: "No se puede buscar con una consulta vacía"
|
es: "No se pudo obtener el perfil a través de la dirección"
|
||||||
pt: "Não é possível pesquisar com uma consulta vazia"
|
pt: "Falha ao obter perfil pelo endereço"
|
||||||
ko: "빈 쿼리로 검색할 수 없습니다"
|
ko: "주소를 통해 프로필을 가져오지 못했습니다"
|
||||||
not_support:
|
|
||||||
en: "Coop does not support searching with this query"
|
|
||||||
zh-CN: "Coop 不支持使用此查询进行搜索"
|
|
||||||
zh-TW: "Coop 不支援使用此查詢進行搜尋"
|
|
||||||
ru: "Coop не поддерживает поиск с этим запросом"
|
|
||||||
vi: "Coop không hỗ trợ tìm kiếm với truy vấn này"
|
|
||||||
ja: "Coopはこのクエリで検索をサポートしていません"
|
|
||||||
es: "Coop no admite buscar con esta consulta"
|
|
||||||
pt: "Coop não suporta pesquisar com esta consulta"
|
|
||||||
ko: "Coop은 이 쿼리로 검색을 지원하지 않습니다"
|
|
||||||
direct_messages:
|
direct_messages:
|
||||||
en: "Direct Messages"
|
en: "Direct Messages"
|
||||||
zh-CN: "私信"
|
zh-CN: "私信"
|
||||||
|
|||||||
Reference in New Issue
Block a user