Continue redesign for the v1 stable release #5

Merged
reya merged 11 commits from redesign-2 into master 2026-02-12 08:32:17 +00:00
6 changed files with 126 additions and 688 deletions
Showing only changes of commit 0581cd2969 - Show all commits

View File

@@ -177,7 +177,9 @@ impl Room {
where
T: IntoIterator<Item = PublicKey>,
{
// 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

View File

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

View File

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

View File

@@ -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<HashSet<PublicKey>>,
/// User's contacts
contact_list: Entity<Vec<PublicKey>>,
/// Whether to show the contact list
show_contact_list: bool,
/// Find input state
find_input: Entity<InputState>,
/// Debounced delay for find input
find_debouncer: DebouncedDelay<Self>,
/// Whether a search is in progress
finding: bool,
/// Find results
find_results: Entity<Option<Vec<PublicKey>>>,
/// Async find operation
find_task: Option<Task<Result<(), Error>>>,
/// Image cache for avatars
image_cache: Entity<RetainAllImageCache>,
/// Async tasks
tasks: SmallVec<[Task<()>; 1]>,
/// Event subscriptions
_subscriptions: SmallVec<[Subscription; 1]>,
}
impl CommandBar {
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> 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<Self>) {
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<I>(&mut self, contacts: I, cx: &mut Context<Self>)
where
I: IntoIterator<Item = PublicKey>,
{
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>) {
self.show_contact_list = !self.show_contact_list;
cx.notify();
}
fn debounced_search(&self, window: &mut Window, cx: &mut Context<Self>) -> 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<Self>) {
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<PublicKey>, cx: &mut Context<Self>) {
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<Self>) {
// 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<Self>) {
// 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<Self>) {
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<Result<(), Error>> = 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>) {
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<Self>) -> HashSet<PublicKey> {
self.selected_pkeys.read(cx).clone()
}
fn render_results(&self, range: Range<usize>, cx: &Context<Self>) -> Vec<impl IntoElement> {
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<usize>, cx: &Context<Self>) -> Vec<impl IntoElement> {
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<Self>) -> 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);
},
)),
)
},
),
),
),
),
))
})
}
}

View File

@@ -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<PublicKey>,
name: Option<SharedString>,
avatar: Option<SharedString>,
created_at: Option<SharedString>,
kind: Option<RoomKind>,
selected: bool,
#[allow(clippy::type_complexity)]
handler: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>>,
}
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);
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
})
});
}
})
}
})
})
}

View File

@@ -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<Self>) {
fn set_input_focus(&mut self, window: &mut Window, cx: &mut Context<Self>) {
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<Self>) {
@@ -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<Self>) {
fn select(&mut self, public_key: &PublicKey, cx: &mut Context<Self>) {
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<Result<(), Error>> = cx.spawn_in(window, async move |_this, cx| {
let task: Task<Result<(), Error>> = 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<usize>, cx: &Context<Self>) -> Vec<impl IntoElement> {
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<usize>, cx: &Context<Self>) -> Vec<impl IntoElement> {
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()