From 7a447da447323537fac488454ddcc4817c528531 Mon Sep 17 00:00:00 2001 From: reya Date: Tue, 27 May 2025 08:59:51 +0700 Subject: [PATCH] chore: improve nostr connect and search --- crates/coop/src/main.rs | 2 +- crates/coop/src/views/compose.rs | 4 +- crates/coop/src/views/search.rs | 352 --------------------------- crates/coop/src/views/sidebar/mod.rs | 67 +++-- 4 files changed, 45 insertions(+), 380 deletions(-) delete mode 100644 crates/coop/src/views/search.rs diff --git a/crates/coop/src/main.rs b/crates/coop/src/main.rs index 3c297e11..8bfa79b1 100644 --- a/crates/coop/src/main.rs +++ b/crates/coop/src/main.rs @@ -350,7 +350,7 @@ async fn set_unwrapped(root: EventId, event: &Event, keys: &Keys) -> Result<(), let client = get_client(); let event = EventBuilder::new(Kind::Custom(9001), event.as_json()) .tags(vec![Tag::event(root)]) - .sign(keys) + .sign(keys) // keys must be random generated .await?; client.database().save_event(&event).await?; diff --git a/crates/coop/src/views/compose.rs b/crates/coop/src/views/compose.rs index 2e646309..45a5587b 100644 --- a/crates/coop/src/views/compose.rs +++ b/crates/coop/src/views/compose.rs @@ -135,13 +135,15 @@ impl Compose { let event: Task> = cx.background_spawn(async move { let client = get_client(); let signer = client.signer().await?; + let public_key = signer.get_public_key().await?; // [IMPORTANT] // Make sure this event is never send, // this event existed just use for convert to Coop's Room later. let event = EventBuilder::private_msg_rumor(*pubkeys.last().unwrap(), "") .tags(tags) - .sign(&signer) + .build(public_key) + .sign(&Keys::generate()) .await?; Ok(event) diff --git a/crates/coop/src/views/search.rs b/crates/coop/src/views/search.rs deleted file mode 100644 index 2dadda27..00000000 --- a/crates/coop/src/views/search.rs +++ /dev/null @@ -1,352 +0,0 @@ -use std::time::Duration; - -use anyhow::Error; -use async_utility::task::spawn; -use chats::ChatRegistry; -use common::profile::SharedProfile; -use global::{constants::SEARCH_RELAYS, get_client}; -use gpui::{ - div, img, prelude::FluentBuilder, px, red, relative, uniform_list, App, AppContext, Context, - Entity, InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled, - Subscription, Task, Window, -}; -use itertools::Itertools; -use nostr_sdk::prelude::*; -use smallvec::{smallvec, SmallVec}; -use theme::ActiveTheme; -use ui::{ - button::{Button, ButtonVariants}, - dock_area::dock::DockPlacement, - indicator::Indicator, - input::{InputEvent, TextInput}, - ContextModal, Disableable, IconName, Sizable, -}; - -use crate::chatspace::{AddPanel, PanelKind}; - -pub fn init(window: &mut Window, cx: &mut App) -> Entity { - Search::new(window, cx) -} - -pub struct Search { - input: Entity, - result: Entity>, - error: Entity>, - loading: bool, - #[allow(dead_code)] - subscriptions: SmallVec<[Subscription; 1]>, -} - -impl Search { - pub fn new(window: &mut Window, cx: &mut App) -> Entity { - let result = cx.new(|_| vec![]); - let error = cx.new(|_| None); - let input = cx.new(|cx| { - TextInput::new(window, cx) - .text_size(ui::Size::Small) - .placeholder("type something...") - }); - - cx.new(|cx| { - let mut subscriptions = smallvec![]; - - subscriptions.push(cx.subscribe_in( - &input, - window, - move |this: &mut Search, _, input_event, window, cx| { - if let InputEvent::PressEnter = input_event { - this.search(window, cx); - } - }, - )); - - Self { - input, - result, - error, - subscriptions, - loading: false, - } - }) - } - - fn search(&mut self, window: &mut Window, cx: &mut Context) { - if self.loading { - return; - }; - - // Show loading spinner - self.loading(true, cx); - - // Get search query - let query = self.input.read(cx).text(); - - let task: Task, Error>> = cx.background_spawn(async move { - let client = get_client(); - - let filter = Filter::new() - .kind(Kind::Metadata) - .search(query.to_lowercase()) - .limit(10); - - let events = client - .fetch_events_from(SEARCH_RELAYS, filter, Duration::from_secs(3)) - .await? - .into_iter() - .unique_by(|event| event.pubkey) - .collect_vec(); - - let mut users = vec![]; - let (tx, rx) = smol::channel::bounded::(events.len()); - - spawn(async move { - for event in events.into_iter() { - let metadata = Metadata::from_json(event.content).unwrap_or_default(); - - if let Some(target) = metadata.nip05.as_ref() { - if let Ok(verify) = nip05::verify(&event.pubkey, target, None).await { - if verify { - _ = tx.send(Profile::new(event.pubkey, metadata)).await; - } - } - } - } - }); - - while let Ok(profile) = rx.recv().await { - users.push(profile); - } - - Ok(users) - }); - - cx.spawn_in(window, async move |this, cx| match task.await { - Ok(users) => { - cx.update(|_, cx| { - this.update(cx, |this, cx| { - this.loading(false, cx); - this.result.update(cx, |this, cx| { - *this = users; - cx.notify(); - }); - }) - .ok(); - }) - .ok(); - } - Err(error) => { - cx.update(|_, cx| { - this.update(cx, |this, cx| { - this.loading(false, cx); - this.error.update(cx, |this, cx| { - *this = Some(error.to_string().into()); - cx.notify(); - }); - }) - .ok(); - }) - .ok(); - } - }) - .detach(); - } - - fn chat(&mut self, to: Profile, window: &mut Window, cx: &mut Context) { - let public_key = to.public_key(); - - let event: Task> = cx.background_spawn(async move { - let client = get_client(); - let signer = client.signer().await?; - // [IMPORTANT] - // Make sure this event is never send, - // this event existed just use for convert to Coop's Room later. - let event = EventBuilder::private_msg_rumor(public_key, "") - .sign(&signer) - .await?; - - Ok(event) - }); - - cx.spawn_in(window, async move |this, cx| match event.await { - Ok(event) => { - cx.update(|window, cx| { - ChatRegistry::global(cx).update(cx, |chats, cx| { - let id = chats.push(&event, window, cx); - window.close_modal(cx); - window.dispatch_action( - Box::new(AddPanel::new(PanelKind::Room(id), DockPlacement::Center)), - cx, - ); - }); - }) - .ok(); - } - Err(e) => { - this.update(cx, |this, cx| { - this.error.update(cx, |this, cx| { - *this = Some(e.to_string().into()); - cx.notify(); - }); - }) - .ok(); - } - }) - .detach(); - } - - fn loading(&mut self, status: bool, cx: &mut Context) { - self.loading = status; - cx.notify(); - } -} - -impl Render for Search { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - div() - .size_full() - .flex() - .flex_col() - .gap_3() - .mt_3() - .child( - div().px_3().child( - div() - .flex() - .gap_1() - .items_center() - .child(self.input.clone()) - .child( - Button::new("find") - .icon(IconName::Search) - .ghost() - .disabled(self.loading) - .on_click( - cx.listener(move |this, _, window, cx| this.search(window, cx)), - ), - ), - ), - ) - .when_some(self.error.read(cx).as_ref(), |this, error| { - this.child( - div() - .px_3() - .text_xs() - .text_color(red()) - .child(error.clone()), - ) - }) - .child(div().map(|this| { - let result = self.result.read(cx).clone(); - - if self.loading { - this.h_32() - .w_full() - .flex() - .items_center() - .justify_center() - .child(Indicator::new().small()) - } else if result.is_empty() { - this.h_32() - .w_full() - .flex() - .items_center() - .justify_center() - .text_sm() - .text_color(cx.theme().text_muted) - .child("No one with that query could be found.") - } else { - this.child( - uniform_list( - cx.entity(), - "find-result", - result.len(), - move |_, range, _window, cx| { - let mut items = Vec::new(); - - for ix in range { - let item = result.get(ix).cloned().unwrap(); - - items.push( - div() - .id(ix) - .group("") - .w_full() - .h_12() - .px_2() - .flex() - .items_center() - .justify_between() - .rounded(cx.theme().radius) - .child( - div() - .flex() - .items_center() - .gap_2() - .child( - img(item.shared_avatar()) - .size_8() - .flex_shrink_0(), - ) - .child( - div() - .flex() - .flex_col() - .child( - div() - .text_sm() - .line_height(relative(1.2)) - .child(item.shared_name()), - ) - .when_some( - item.metadata().nip05, - |this, nip05| { - this.child( - div() - .text_xs() - .text_color( - cx.theme() - .text_muted, - ) - .child(nip05), - ) - }, - ), - ), - ) - .child( - div() - .invisible() - .group_hover("", |this| this.visible()) - .child( - Button::new(ix) - .icon(IconName::ArrowRight) - .label("Chat") - .xsmall() - .primary() - .reverse() - .on_click(cx.listener( - move |this, _, window, cx| { - this.chat( - item.clone(), - window, - cx, - ); - }, - )), - ), - ) - .hover(|this| { - this.bg(cx.theme().elevated_surface_background) - }), - ); - } - - items - }, - ) - .min_h(px(150.)), - ) - } - })) - } -} diff --git a/crates/coop/src/views/sidebar/mod.rs b/crates/coop/src/views/sidebar/mod.rs index edeec6c8..d01aa8ec 100644 --- a/crates/coop/src/views/sidebar/mod.rs +++ b/crates/coop/src/views/sidebar/mod.rs @@ -144,21 +144,30 @@ impl Sidebar { spawn(async move { let client = get_client(); let signer = client.signer().await.expect("signer is required"); + let public_key = signer.get_public_key().await.expect("error"); for event in events.into_iter() { let metadata = Metadata::from_json(event.content).unwrap_or_default(); - if let Some(target) = metadata.nip05.as_ref() { - if let Ok(verify) = nip05::verify(&event.pubkey, target, None).await { - if verify { - if let Ok(event) = EventBuilder::private_msg_rumor(event.pubkey, "") - .sign(&signer) - .await - { - let room = Room::new(&event); - _ = tx.send(room).await; - } - } + let Some(target) = metadata.nip05.as_ref() else { + continue; + }; + + let Ok(verified) = nip05::verify(&event.pubkey, target, None).await else { + continue; + }; + + if !verified { + 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!("{e}") } } } @@ -281,8 +290,6 @@ impl Sidebar { let room = if let Some(room) = ChatRegistry::get_global(cx).room(&id, cx) { room } else { - self.clear_search_results(cx); - let Some(result) = self.global_result.read(cx).as_ref() else { window.push_notification("Failed to open room. Please try again later.", cx); return; @@ -293,6 +300,9 @@ impl Sidebar { return; }; + // Clear all search results + self.clear_search_results(cx); + room }; @@ -477,19 +487,24 @@ impl Render for Sidebar { ) // Global Search Results .when_some(self.global_result.read(cx).clone(), |this, rooms| { - this.child( - div().px_1().w_full().flex_1().overflow_y_hidden().child( - uniform_list( - cx.entity(), - "results", - rooms.len(), - move |this, range, _window, cx| { - this.render_uniform_item(&rooms, range, cx) - }, - ) - .h_full(), - ), - ) + 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.into_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 + })) }) .child( div()