From 0581cd2969d94187582c76c85f9303335176d9d0 Mon Sep 17 00:00:00 2001 From: reya Date: Wed, 11 Feb 2026 15:24:04 +0700 Subject: [PATCH] . --- crates/chat/src/room.rs | 2 + crates/coop/src/actions.rs | 19 - crates/coop/src/main.rs | 7 +- crates/coop/src/sidebar/command_bar.rs | 586 ------------------ .../src/sidebar/{list_item.rs => entry.rs} | 50 +- crates/coop/src/sidebar/mod.rs | 150 +++-- 6 files changed, 126 insertions(+), 688 deletions(-) delete mode 100644 crates/coop/src/actions.rs delete mode 100644 crates/coop/src/sidebar/command_bar.rs rename crates/coop/src/sidebar/{list_item.rs => entry.rs} (76%) diff --git a/crates/chat/src/room.rs b/crates/chat/src/room.rs index 516e785..f83cced 100644 --- a/crates/chat/src/room.rs +++ b/crates/chat/src/room.rs @@ -177,7 +177,9 @@ impl Room { where T: IntoIterator, { + // Map receiver public keys to tags let tags = Tags::from_list(receivers.into_iter().map(Tag::public_key).collect()); + // Construct an unsigned event for a direct message // // WARNING: never sign this event diff --git a/crates/coop/src/actions.rs b/crates/coop/src/actions.rs deleted file mode 100644 index 37c2cb9..0000000 --- a/crates/coop/src/actions.rs +++ /dev/null @@ -1,19 +0,0 @@ -use gpui::actions; - -// Sidebar actions -actions!(sidebar, [Reload, RelayStatus]); - -// User actions -actions!( - coop, - [ - KeyringPopup, - DarkMode, - ViewProfile, - ViewRelays, - Themes, - Settings, - Logout, - Quit - ] -); diff --git a/crates/coop/src/main.rs b/crates/coop/src/main.rs index 02df297..0336fbd 100644 --- a/crates/coop/src/main.rs +++ b/crates/coop/src/main.rs @@ -2,21 +2,20 @@ use std::sync::{Arc, Mutex}; use assets::Assets; use gpui::{ - point, px, size, App, AppContext, Application, Bounds, KeyBinding, Menu, MenuItem, + actions, point, px, size, App, AppContext, Application, Bounds, KeyBinding, Menu, MenuItem, SharedString, Size, TitlebarOptions, WindowBackgroundAppearance, WindowBounds, WindowDecorations, WindowKind, WindowOptions, }; use state::{APP_ID, CLIENT_NAME}; use ui::Root; -use crate::actions::Quit; - -mod actions; mod dialogs; mod panels; mod sidebar; mod workspace; +actions!(coop, [Quit]); + fn main() { // Initialize logging tracing_subscriber::fmt::init(); diff --git a/crates/coop/src/sidebar/command_bar.rs b/crates/coop/src/sidebar/command_bar.rs deleted file mode 100644 index 09962d2..0000000 --- a/crates/coop/src/sidebar/command_bar.rs +++ /dev/null @@ -1,586 +0,0 @@ -use std::collections::HashSet; -use std::ops::Range; -use std::time::Duration; - -use anyhow::Error; -use chat::{ChatRegistry, Room}; -use common::DebouncedDelay; -use gpui::prelude::FluentBuilder; -use gpui::{ - anchored, deferred, div, point, px, rems, uniform_list, App, AppContext, Bounds, Context, - Entity, Focusable, InteractiveElement, IntoElement, MouseButton, ParentElement, Pixels, Point, - Render, RetainAllImageCache, SharedString, StatefulInteractiveElement, Styled, Subscription, - Task, Window, -}; -use nostr_sdk::prelude::*; -use person::PersonRegistry; -use settings::AppSettings; -use smallvec::{smallvec, SmallVec}; -use state::{NostrRegistry, FIND_DELAY}; -use theme::{ActiveTheme, TITLEBAR_HEIGHT}; -use ui::avatar::Avatar; -use ui::button::{Button, ButtonVariants}; -use ui::input::{InputEvent, InputState, TextInput}; -use ui::notification::Notification; -use ui::{h_flex, v_flex, window_paddings, Icon, IconName, Sizable, WindowExtension}; - -const WIDTH: Pixels = px(425.); - -/// Command bar for searching conversations. -pub struct CommandBar { - /// Selected public keys - selected_pkeys: Entity>, - - /// User's contacts - contact_list: Entity>, - - /// Whether to show the contact list - show_contact_list: bool, - - /// Find input state - find_input: Entity, - - /// Debounced delay for find input - find_debouncer: DebouncedDelay, - - /// Whether a search is in progress - finding: bool, - - /// Find results - find_results: Entity>>, - - /// Async find operation - find_task: Option>>, - - /// Image cache for avatars - image_cache: Entity, - - /// Async tasks - tasks: SmallVec<[Task<()>; 1]>, - - /// Event subscriptions - _subscriptions: SmallVec<[Subscription; 1]>, -} - -impl CommandBar { - pub fn new(window: &mut Window, cx: &mut Context) -> Self { - let selected_pkeys = cx.new(|_| HashSet::new()); - let contact_list = cx.new(|_| vec![]); - let find_results = cx.new(|_| None); - let find_input = cx.new(|cx| { - InputState::new(window, cx) - .placeholder("Find or start a conversation") - .clean_on_escape() - }); - - let mut subscriptions = smallvec![]; - - subscriptions.push( - // Subscribe to find input events - cx.subscribe_in(&find_input, window, |this, state, event, window, cx| { - let delay = Duration::from_millis(FIND_DELAY); - - match event { - InputEvent::PressEnter { .. } => { - this.search(window, cx); - } - InputEvent::Change => { - if state.read(cx).value().is_empty() { - // Clear results when input is empty - this.reset(window, cx); - } else { - // Run debounced search - this.find_debouncer - .fire_new(delay, window, cx, |this, window, cx| { - this.debounced_search(window, cx) - }); - } - } - InputEvent::Focus => { - this.get_contact_list(window, cx); - } - _ => {} - }; - }), - ); - - Self { - selected_pkeys, - contact_list, - show_contact_list: false, - find_debouncer: DebouncedDelay::new(), - finding: false, - find_input, - find_results, - find_task: None, - image_cache: RetainAllImageCache::new(cx), - tasks: smallvec![], - _subscriptions: subscriptions, - } - } - - fn get_contact_list(&mut self, window: &mut Window, cx: &mut Context) { - let nostr = NostrRegistry::global(cx); - let task = nostr.read(cx).get_contact_list(cx); - - self.tasks.push(cx.spawn_in(window, async move |this, cx| { - match task.await { - Ok(contacts) => { - this.update(cx, |this, cx| { - this.extend_contacts(contacts, cx); - }) - .ok(); - } - Err(e) => { - cx.update(|window, cx| { - window.push_notification(Notification::error(e.to_string()), cx); - }) - .ok(); - } - }; - })); - } - - /// Extend the contact list with new contacts. - fn extend_contacts(&mut self, contacts: I, cx: &mut Context) - where - I: IntoIterator, - { - self.contact_list.update(cx, |this, cx| { - this.extend(contacts); - cx.notify(); - }); - } - - /// Toggle the visibility of the contact list. - fn toggle_contact_list(&mut self, cx: &mut Context) { - self.show_contact_list = !self.show_contact_list; - cx.notify(); - } - - fn debounced_search(&self, window: &mut Window, cx: &mut Context) -> Task<()> { - cx.spawn_in(window, async move |this, cx| { - this.update_in(cx, |this, window, cx| { - this.search(window, cx); - }) - .ok(); - }) - } - - fn search(&mut self, window: &mut Window, cx: &mut Context) { - let nostr = NostrRegistry::global(cx); - let query = self.find_input.read(cx).value(); - - // Return if the query is empty - if query.is_empty() { - return; - } - - // Return if a search is already in progress - if self.finding { - if self.find_task.is_none() { - window.push_notification("There is another search in progress", cx); - return; - } else { - // Cancel the ongoing search request - self.find_task = None; - } - } - - // Block the input until the search completes - self.set_finding(true, window, cx); - - let find_users = nostr.read(cx).search(&query, cx); - - // Run task in the main thread - self.find_task = Some(cx.spawn_in(window, async move |this, cx| { - let rooms = find_users.await?; - // Update the UI with the search results - this.update_in(cx, |this, window, cx| { - this.set_results(rooms, cx); - this.set_finding(false, window, cx); - })?; - - Ok(()) - })); - } - - fn set_results(&mut self, results: Vec, cx: &mut Context) { - self.find_results.update(cx, |this, cx| { - *this = Some(results); - cx.notify(); - }); - } - - fn set_finding(&mut self, status: bool, _window: &mut Window, cx: &mut Context) { - // Disable the input to prevent duplicate requests - self.find_input.update(cx, |this, cx| { - this.set_disabled(status, cx); - this.set_loading(status, cx); - }); - // Set the search status - self.finding = status; - cx.notify(); - } - - fn reset(&mut self, window: &mut Window, cx: &mut Context) { - // Clear all search results - self.find_results.update(cx, |this, cx| { - *this = None; - cx.notify(); - }); - - // Reset the search status - self.set_finding(false, window, cx); - - // Cancel the current search task - self.find_task = None; - cx.notify(); - } - - fn create(&mut self, window: &mut Window, cx: &mut Context) { - let chat = ChatRegistry::global(cx); - let async_chat = chat.downgrade(); - - let nostr = NostrRegistry::global(cx); - let signer_pkey = nostr.read(cx).signer_pkey(cx); - - // Get all selected public keys - let receivers = self.selected(cx); - - let task: Task> = cx.spawn_in(window, async move |_this, cx| { - let public_key = signer_pkey.await?; - - async_chat.update_in(cx, |this, window, cx| { - let room = cx.new(|_| Room::new(public_key, receivers)); - this.emit_room(room.downgrade(), cx); - - window.close_modal(cx); - })?; - - Ok(()) - }); - - task.detach(); - } - - fn select(&mut self, pkey: PublicKey, cx: &mut Context) { - self.selected_pkeys.update(cx, |this, cx| { - if this.contains(&pkey) { - this.remove(&pkey); - } else { - this.insert(pkey); - } - cx.notify(); - }); - } - - fn is_selected(&self, pkey: PublicKey, cx: &App) -> bool { - self.selected_pkeys.read(cx).contains(&pkey) - } - - fn selected(&self, cx: &Context) -> HashSet { - self.selected_pkeys.read(cx).clone() - } - - fn render_results(&self, range: Range, cx: &Context) -> Vec { - let persons = PersonRegistry::global(cx); - let hide_avatar = AppSettings::get_hide_avatar(cx); - - let Some(rooms) = self.find_results.read(cx) else { - return vec![]; - }; - - rooms - .get(range.clone()) - .into_iter() - .flatten() - .enumerate() - .map(|(ix, item)| { - let profile = persons.read(cx).get(item, cx); - let pkey = item.to_owned(); - let id = range.start + ix; - - h_flex() - .id(id) - .h_8() - .w_full() - .px_1() - .gap_2() - .rounded(cx.theme().radius) - .when(!hide_avatar, |this| { - this.child( - div() - .flex_shrink_0() - .size_6() - .rounded_full() - .overflow_hidden() - .child(Avatar::new(profile.avatar()).size(rems(1.5))), - ) - }) - .child( - h_flex() - .flex_1() - .justify_between() - .line_clamp(1) - .text_ellipsis() - .truncate() - .text_sm() - .child(profile.name()) - .when(self.is_selected(pkey, cx), |this| { - this.child( - Icon::new(IconName::CheckCircle) - .small() - .text_color(cx.theme().icon_accent), - ) - }), - ) - .hover(|this| this.bg(cx.theme().elevated_surface_background)) - .on_click(cx.listener(move |this, _ev, _window, cx| { - this.select(pkey, cx); - })) - .into_any_element() - }) - .collect() - } - - fn render_contacts(&self, range: Range, cx: &Context) -> Vec { - let persons = PersonRegistry::global(cx); - let hide_avatar = AppSettings::get_hide_avatar(cx); - let contacts = self.contact_list.read(cx); - - contacts - .get(range.clone()) - .into_iter() - .flatten() - .enumerate() - .map(|(ix, item)| { - let profile = persons.read(cx).get(item, cx); - let pkey = item.to_owned(); - let id = range.start + ix; - - h_flex() - .id(id) - .h_8() - .w_full() - .px_1() - .gap_2() - .rounded(cx.theme().radius) - .when(!hide_avatar, |this| { - this.child( - div() - .flex_shrink_0() - .size_6() - .rounded_full() - .overflow_hidden() - .child(Avatar::new(profile.avatar()).size(rems(1.5))), - ) - }) - .child( - h_flex() - .flex_1() - .justify_between() - .line_clamp(1) - .text_ellipsis() - .truncate() - .text_sm() - .child(profile.name()) - .when(self.is_selected(pkey, cx), |this| { - this.child( - Icon::new(IconName::CheckCircle) - .small() - .text_color(cx.theme().icon_accent), - ) - }), - ) - .hover(|this| this.bg(cx.theme().elevated_surface_background)) - .on_click(cx.listener(move |this, _ev, _window, cx| { - this.select(pkey, cx); - })) - .into_any_element() - }) - .collect() - } -} - -impl Render for CommandBar { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let window_paddings = window_paddings(window, cx); - let view_size = window.viewport_size() - - gpui::size( - window_paddings.left + window_paddings.right, - window_paddings.top + window_paddings.bottom, - ); - - let bounds = Bounds { - origin: Point::default(), - size: view_size, - }; - - let x = bounds.center().x - WIDTH / 2.; - let y = TITLEBAR_HEIGHT; - - let input_focus_handle = self.find_input.read(cx).focus_handle(cx); - let input_focused = input_focus_handle.is_focused(window); - - let results = self.find_results.read(cx).as_ref(); - let total_results = results.map_or(0, |r| r.len()); - - let contacts = self.contact_list.read(cx); - let button_label = if self.selected_pkeys.read(cx).len() > 1 { - "Create Group DM" - } else { - "Create DM" - }; - - div() - .image_cache(self.image_cache.clone()) - .w_full() - .child( - TextInput::new(&self.find_input) - .appearance(false) - .bordered(false) - .small() - .text_xs() - .when(!self.find_input.read(cx).loading, |this| { - this.suffix( - Button::new("find-icon") - .icon(IconName::Search) - .tooltip("Press Enter to search") - .transparent() - .small(), - ) - }), - ) - .when(input_focused, |this| { - this.child(deferred( - anchored() - .position(point(window_paddings.left, window_paddings.top)) - .snap_to_window() - .child( - div() - .occlude() - .w(view_size.width) - .h(view_size.height) - .on_mouse_down(MouseButton::Left, move |_ev, window, cx| { - window.focus_prev(cx); - }) - .child( - v_flex() - .absolute() - .occlude() - .relative() - .left(x) - .top(y) - .w(WIDTH) - .min_h_24() - .overflow_y_hidden() - .p_1() - .gap_1() - .justify_between() - .border_1() - .border_color(cx.theme().border.alpha(0.4)) - .bg(cx.theme().surface_background) - .shadow_md() - .rounded(cx.theme().radius_lg) - .map(|this| { - if self.show_contact_list { - this.child( - uniform_list( - "contacts", - contacts.len(), - cx.processor(|this, range, _window, cx| { - this.render_contacts(range, cx) - }), - ) - .when(!contacts.is_empty(), |this| this.h_40()), - ) - .when(contacts.is_empty(), |this| { - this.child( - h_flex() - .h_10() - .w_full() - .items_center() - .justify_center() - .text_center() - .text_xs() - .text_color(cx.theme().text_muted) - .child(SharedString::from( - "Your contact list is empty", - )), - ) - }) - } else { - this.child( - uniform_list( - "rooms", - total_results, - cx.processor(|this, range, _window, cx| { - this.render_results(range, cx) - }), - ) - .when(total_results > 0, |this| this.h_40()), - ) - .when(total_results == 0, |this| { - this.child( - h_flex() - .h_10() - .w_full() - .items_center() - .justify_center() - .text_center() - .text_xs() - .text_color(cx.theme().text_muted) - .child(SharedString::from( - "Search results appear here", - )), - ) - }) - } - }) - .child( - h_flex() - .pt_1() - .border_t_1() - .border_color(cx.theme().border_variant) - .justify_end() - .child( - Button::new("show-contacts") - .label({ - if self.show_contact_list { - "Hide contact list" - } else { - "Show contact list" - } - }) - .ghost() - .xsmall() - .on_click(cx.listener( - move |this, _ev, _window, cx| { - this.toggle_contact_list(cx); - }, - )), - ) - .when( - !self.selected_pkeys.read(cx).is_empty(), - |this| { - this.child( - Button::new("create") - .label(button_label) - .primary() - .xsmall() - .on_click(cx.listener( - move |this, _ev, window, cx| { - this.create(window, cx); - }, - )), - ) - }, - ), - ), - ), - ), - )) - }) - } -} diff --git a/crates/coop/src/sidebar/list_item.rs b/crates/coop/src/sidebar/entry.rs similarity index 76% rename from crates/coop/src/sidebar/list_item.rs rename to crates/coop/src/sidebar/entry.rs index a49f78f..e8c25d9 100644 --- a/crates/coop/src/sidebar/list_item.rs +++ b/crates/coop/src/sidebar/entry.rs @@ -14,23 +14,24 @@ use theme::ActiveTheme; use ui::avatar::Avatar; use ui::context_menu::ContextMenuExt; use ui::modal::ModalButtonProps; -use ui::{h_flex, StyledExt, WindowExtension}; +use ui::{h_flex, Icon, IconName, Selectable, Sizable, StyledExt, WindowExtension}; use crate::dialogs::screening; #[derive(IntoElement)] -pub struct RoomListItem { +pub struct RoomEntry { ix: usize, public_key: Option, name: Option, avatar: Option, created_at: Option, kind: Option, + selected: bool, #[allow(clippy::type_complexity)] handler: Option>, } -impl RoomListItem { +impl RoomEntry { pub fn new(ix: usize) -> Self { Self { ix, @@ -40,6 +41,7 @@ impl RoomListItem { created_at: None, kind: None, handler: None, + selected: false, } } @@ -77,11 +79,25 @@ impl RoomListItem { } } -impl RenderOnce for RoomListItem { +impl Selectable for RoomEntry { + fn selected(mut self, selected: bool) -> Self { + self.selected = selected; + self + } + + fn is_selected(&self) -> bool { + self.selected + } +} + +impl RenderOnce for RoomEntry { fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { let hide_avatar = AppSettings::get_hide_avatar(cx); let screening = AppSettings::get_screening(cx); + let public_key = self.public_key; + let is_selected = self.is_selected(); + h_flex() .id(self.ix) .h_9() @@ -110,13 +126,21 @@ impl RenderOnce for RoomListItem { .justify_between() .when_some(self.name, |this, name| { this.child( - div() + h_flex() .flex_1() + .justify_between() .line_clamp(1) .text_ellipsis() .truncate() .font_medium() - .child(name), + .child(name) + .when(is_selected, |this| { + this.child( + Icon::new(IconName::CheckCircle) + .small() + .text_color(cx.theme().icon_accent), + ) + }), ) }) .child( @@ -129,15 +153,17 @@ impl RenderOnce for RoomListItem { ), ) .hover(|this| this.bg(cx.theme().elevated_surface_background)) - .when_some(self.public_key, |this, public_key| { + .when_some(public_key, |this, public_key| { this.context_menu(move |this, _window, _cx| { this.menu("View Profile", Box::new(OpenPublicKey(public_key))) .menu("Copy Public Key", Box::new(CopyPublicKey(public_key))) }) - .when_some(self.handler, |this, handler| { - this.on_click(move |event, window, cx| { - handler(event, window, cx); + }) + .when_some(self.handler, |this, handler| { + this.on_click(move |event, window, cx| { + handler(event, window, cx); + if let Some(public_key) = public_key { if self.kind != Some(RoomKind::Ongoing) && screening { let screening = screening::init(public_key, window, cx); @@ -152,12 +178,12 @@ impl RenderOnce for RoomListItem { .on_cancel(move |_event, window, cx| { window.dispatch_action(Box::new(ClosePanel), cx); // Prevent closing the modal on click - // Modal will be automatically closed after closing panel + // modal will be automatically closed after closing panel false }) }); } - }) + } }) }) } diff --git a/crates/coop/src/sidebar/mod.rs b/crates/coop/src/sidebar/mod.rs index 041d181..22faa11 100644 --- a/crates/coop/src/sidebar/mod.rs +++ b/crates/coop/src/sidebar/mod.rs @@ -6,30 +6,26 @@ use anyhow::Error; use chat::{ChatEvent, ChatRegistry, Room, RoomKind}; use common::{DebouncedDelay, RenderedTimestamp}; use dock::panel::{Panel, PanelEvent}; +use entry::RoomEntry; use gpui::prelude::FluentBuilder; use gpui::{ - div, rems, uniform_list, App, AppContext, Context, Entity, EventEmitter, FocusHandle, - Focusable, InteractiveElement, IntoElement, ParentElement, Render, RetainAllImageCache, - SharedString, StatefulInteractiveElement, Styled, Subscription, Task, Window, + div, uniform_list, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable, + IntoElement, ParentElement, Render, RetainAllImageCache, SharedString, Styled, Subscription, + Task, Window, }; -use list_item::RoomListItem; use nostr_sdk::prelude::*; use person::PersonRegistry; -use settings::AppSettings; use smallvec::{smallvec, SmallVec}; use state::{NostrRegistry, FIND_DELAY}; use theme::{ActiveTheme, TABBAR_HEIGHT}; -use ui::avatar::Avatar; use ui::button::{Button, ButtonVariants}; use ui::divider::Divider; use ui::indicator::Indicator; use ui::input::{InputEvent, InputState, TextInput}; use ui::notification::Notification; -use ui::{ - h_flex, v_flex, Disableable, Icon, IconName, Selectable, Sizable, StyledExt, WindowExtension, -}; +use ui::{h_flex, v_flex, Disableable, IconName, Selectable, Sizable, StyledExt, WindowExtension}; -mod list_item; +mod entry; const INPUT_PLACEHOLDER: &str = "Find or start a conversation"; @@ -264,9 +260,14 @@ impl Sidebar { cx.notify(); } - fn set_input_focus(&mut self, cx: &mut Context) { + fn set_input_focus(&mut self, window: &mut Window, cx: &mut Context) { self.find_focused = !self.find_focused; cx.notify(); + + // Reset the find panel + if !self.find_focused { + self.reset(window, cx); + } } fn reset(&mut self, window: &mut Window, cx: &mut Context) { @@ -276,6 +277,12 @@ impl Sidebar { cx.notify(); }); + // Clear all selected public keys + self.selected_pkeys.update(cx, |this, cx| { + this.clear(); + cx.notify(); + }); + // Reset the search status self.set_finding(false, window, cx); @@ -285,20 +292,20 @@ impl Sidebar { } /// Select a public key in the sidebar. - fn select(&mut self, pkey: PublicKey, cx: &mut Context) { + fn select(&mut self, public_key: &PublicKey, cx: &mut Context) { self.selected_pkeys.update(cx, |this, cx| { - if this.contains(&pkey) { - this.remove(&pkey); + if this.contains(public_key) { + this.remove(public_key); } else { - this.insert(pkey); + this.insert(public_key.to_owned()); } cx.notify(); }); } /// Check if a public key is selected in the sidebar. - fn is_selected(&self, pkey: PublicKey, cx: &App) -> bool { - self.selected_pkeys.read(cx).contains(&pkey) + fn is_selected(&self, public_key: &PublicKey, cx: &App) -> bool { + self.selected_pkeys.read(cx).contains(public_key) } /// Get all selected public keys in the sidebar. @@ -317,14 +324,18 @@ impl Sidebar { // Get all selected public keys let receivers = self.selected(cx); - let task: Task> = cx.spawn_in(window, async move |_this, cx| { + let task: Task> = cx.spawn_in(window, async move |this, cx| { let public_key = signer_pkey.await?; - async_chat.update_in(cx, |this, window, cx| { - let room = cx.new(|_| Room::new(public_key, receivers)); - this.emit_room(room.downgrade(), cx); + // Reset the find panel + this.update_in(cx, |this, window, cx| { + this.reset(window, cx); + })?; - window.close_modal(cx); + // Create a new room and emit it + async_chat.update_in(cx, |this, _window, cx| { + let room = cx.new(|_| Room::new(public_key, receivers).kind(RoomKind::Ongoing)); + this.emit_room(room.downgrade(), cx); })?; Ok(()) @@ -366,7 +377,7 @@ impl Sidebar { }); }); - RoomListItem::new(range.start + ix) + RoomEntry::new(range.start + ix) .name(room.display_name(cx)) .avatar(room.display_image(cx)) .public_key(public_key) @@ -378,62 +389,67 @@ impl Sidebar { .collect() } + /// Render the contact list + fn render_results(&self, range: Range, cx: &Context) -> Vec { + let persons = PersonRegistry::global(cx); + + // Get the contact list + let Some(results) = self.find_results.read(cx) else { + return vec![]; + }; + + // Map the contact list to a list of elements + results + .get(range.clone()) + .into_iter() + .flatten() + .enumerate() + .map(|(ix, public_key)| { + let selected = self.is_selected(public_key, cx); + let profile = persons.read(cx).get(public_key, cx); + let pkey_clone = public_key.to_owned(); + let handler = cx.listener(move |this, _ev, _window, cx| { + this.select(&pkey_clone, cx); + }); + + RoomEntry::new(range.start + ix) + .name(profile.name()) + .avatar(profile.avatar()) + .on_click(handler) + .selected(selected) + .into_any_element() + }) + .collect() + } + + /// Render the contact list fn render_contacts(&self, range: Range, cx: &Context) -> Vec { let persons = PersonRegistry::global(cx); - let hide_avatar = AppSettings::get_hide_avatar(cx); + // Get the contact list let Some(contacts) = self.contact_list.read(cx) else { return vec![]; }; + // Map the contact list to a list of elements contacts .get(range.clone()) .into_iter() .flatten() .enumerate() - .map(|(ix, item)| { - let profile = persons.read(cx).get(item, cx); - let pkey = item.to_owned(); - let id = range.start + ix; + .map(|(ix, public_key)| { + let selected = self.is_selected(public_key, cx); + let profile = persons.read(cx).get(public_key, cx); + let pkey_clone = public_key.to_owned(); + let handler = cx.listener(move |this, _ev, _window, cx| { + this.select(&pkey_clone, cx); + }); - h_flex() - .id(id) - .h_8() - .w_full() - .px_1() - .gap_2() - .rounded(cx.theme().radius) - .when(!hide_avatar, |this| { - this.child( - div() - .flex_shrink_0() - .size_6() - .rounded_full() - .overflow_hidden() - .child(Avatar::new(profile.avatar()).size(rems(1.5))), - ) - }) - .child( - h_flex() - .flex_1() - .justify_between() - .line_clamp(1) - .text_ellipsis() - .truncate() - .text_sm() - .child(profile.name()) - .when(self.is_selected(pkey, cx), |this| { - this.child( - Icon::new(IconName::CheckCircle) - .small() - .text_color(cx.theme().icon_accent), - ) - }), - ) - .hover(|this| this.bg(cx.theme().elevated_surface_background)) - .on_click(cx.listener(move |this, _ev, _window, cx| { - this.select(pkey, cx); - })) + RoomEntry::new(range.start + ix) + .name(profile.name()) + .avatar(profile.avatar()) + .on_click(handler) + .selected(selected) .into_any_element() }) .collect() @@ -630,7 +646,7 @@ impl Render for Sidebar { "rooms", results.len(), cx.processor(|this, range, _window, cx| { - this.render_list_items(range, cx) + this.render_results(range, cx) }), ) .flex_1()