diff --git a/crates/ui/src/nav_item.rs b/crates/ui/src/nav_item.rs index 5ab0cae9..a6c66e79 100644 --- a/crates/ui/src/nav_item.rs +++ b/crates/ui/src/nav_item.rs @@ -11,11 +11,6 @@ use theme::ActiveTheme; use crate::{StyledExt, h_flex}; /// A single navigation entry in a sidebar. -/// -/// It has an arbitrary leading element, such as an icon or avatar, and a text -/// label. It can carry an optional trailing suffix, such as a status icon, and -/// an optional click handler. Rows with a click handler are highlighted on -/// hover and show a pointer cursor. #[allow(clippy::type_complexity)] #[derive(IntoElement)] pub struct NavItem { @@ -23,7 +18,6 @@ pub struct NavItem { style: StyleRefinement, icon: AnyElement, label: SharedString, - /// Trailing element at the right edge of the row, after the ellipsized label. suffix: Option, on_click: Option>, } diff --git a/crates/workspace/src/lib.rs b/crates/workspace/src/lib.rs index 3e68ac3e..02dff88c 100644 --- a/crates/workspace/src/lib.rs +++ b/crates/workspace/src/lib.rs @@ -28,7 +28,8 @@ use crate::dialogs::import::ImportIdentity; use crate::dialogs::restore::RestoreEncryption; use crate::dialogs::settings; use crate::panels::{ - backup, browse, contact_list, greeter, inbox, messaging_relays, profile, relay_list, search, + backup, browse, contact_list, greeter, inbox, messaging_relays, profile, relay_list, requests, + search, }; use crate::sidebar::Sidebar; @@ -60,6 +61,7 @@ enum Command { ShowBackup, ShowContactList, ShowInbox, + ShowRequests, ShowBrowse, ShowSearch, } @@ -304,6 +306,14 @@ impl Workspace { Command::ShowInbox => { self.add_panel_to_dock(inbox::init(window, cx), DockPlacement::Center, window, cx); } + Command::ShowRequests => { + self.add_panel_to_dock( + requests::init(window, cx), + DockPlacement::Center, + window, + cx, + ); + } Command::ShowBrowse => { self.add_panel_to_dock(browse::init(window, cx), DockPlacement::Center, window, cx); } @@ -723,6 +733,8 @@ impl Render for Workspace { .flex_shrink_0() .h_full() .w(SIDEBAR_WIDTH) + .border_r_1() + .border_color(cx.theme().border_variant) .child(self.sidebar.clone()), ) .child(self.dock.clone()), diff --git a/crates/workspace/src/panels/mod.rs b/crates/workspace/src/panels/mod.rs index 88973725..16505f11 100644 --- a/crates/workspace/src/panels/mod.rs +++ b/crates/workspace/src/panels/mod.rs @@ -6,4 +6,5 @@ pub mod inbox; pub mod messaging_relays; pub mod profile; pub mod relay_list; +pub mod requests; pub mod search; diff --git a/crates/workspace/src/panels/requests.rs b/crates/workspace/src/panels/requests.rs new file mode 100644 index 00000000..cbb67c9d --- /dev/null +++ b/crates/workspace/src/panels/requests.rs @@ -0,0 +1,62 @@ +use gpui::{ + AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable, + IntoElement, ParentElement, Render, SharedString, Styled, Window, +}; +use theme::ActiveTheme; +use ui::dock::{Panel, PanelEvent}; +use ui::{Icon, IconName, Sizable, h_flex}; + +pub fn init(window: &mut Window, cx: &mut App) -> Entity { + cx.new(|cx| RequestsPanel::new(window, cx)) +} + +pub struct RequestsPanel { + name: SharedString, + focus_handle: FocusHandle, +} + +impl RequestsPanel { + fn new(_window: &mut Window, cx: &mut App) -> Self { + Self { + name: "Requests".into(), + focus_handle: cx.focus_handle(), + } + } +} + +impl Panel for RequestsPanel { + fn panel_id(&self) -> SharedString { + self.name.clone() + } + + fn title(&self, cx: &App) -> AnyElement { + h_flex() + .gap_1p5() + .child( + Icon::new(IconName::Invite) + .small() + .text_color(cx.theme().icon_muted), + ) + .child(self.name.clone()) + .into_any_element() + } +} + +impl EventEmitter for RequestsPanel {} + +impl Focusable for RequestsPanel { + fn focus_handle(&self, _: &App) -> FocusHandle { + self.focus_handle.clone() + } +} + +impl Render for RequestsPanel { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + h_flex() + .size_full() + .justify_center() + .text_sm() + .text_color(cx.theme().text_muted) + .child(self.name.clone()) + } +} diff --git a/crates/workspace/src/panels/search.rs b/crates/workspace/src/panels/search.rs index 628c7192..38261cda 100644 --- a/crates/workspace/src/panels/search.rs +++ b/crates/workspace/src/panels/search.rs @@ -6,7 +6,7 @@ use chat::{ChatRegistry, Room, RoomKind}; use common::DebouncedDelay; use gpui::prelude::FluentBuilder; use gpui::{ - AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable, + AnyElement, App, AppContext, Context, ElementId, Entity, EventEmitter, FocusHandle, Focusable, IntoElement, ParentElement, Render, SharedString, Styled, Subscription, Task, Window, div, uniform_list, }; @@ -22,7 +22,7 @@ use ui::input::{Input, InputEvent, InputState}; use ui::notification::Notification; use ui::{Icon, IconName, Selectable, Sizable, StyledExt, WindowExtension, h_flex, v_flex}; -use crate::sidebar::RoomEntry; +use crate::sidebar::{TreeRow, TreeRowKind}; const INPUT_PLACEHOLDER: &str = "Find or start a conversation"; @@ -341,13 +341,16 @@ impl SearchPanel { this.select(&pkey_clone, cx); }); - RoomEntry::new(range.start + ix) - .name(profile.name()) - .avatar(profile.avatar()) - .seed(profile.avatar_seed()) - .on_click(handler) - .selected(selected) - .into_any_element() + TreeRow::new( + ElementId::NamedInteger("search-result".into(), (range.start + ix) as u64), + TreeRowKind::Room, + profile.name(), + ) + .avatar(profile.avatar_seed()) + .picture(profile.avatar()) + .on_click(handler) + .selected(selected) + .into_any_element() }) .collect() } @@ -379,13 +382,16 @@ impl SearchPanel { this.select(&pkey_clone, cx); }); - RoomEntry::new(range.start + ix) - .name(profile.name().trim()) - .avatar(profile.avatar()) - .seed(profile.avatar_seed()) - .on_click(handler) - .selected(selected) - .into_any_element() + TreeRow::new( + ElementId::NamedInteger("contact".into(), (range.start + ix) as u64), + TreeRowKind::Room, + profile.name().trim(), + ) + .avatar(profile.avatar_seed()) + .picture(profile.avatar()) + .on_click(handler) + .selected(selected) + .into_any_element() }) .collect() } diff --git a/crates/workspace/src/sidebar/entry.rs b/crates/workspace/src/sidebar/entry.rs deleted file mode 100644 index f7c91233..00000000 --- a/crates/workspace/src/sidebar/entry.rs +++ /dev/null @@ -1,194 +0,0 @@ -use std::rc::Rc; - -use chat::RoomKind; -use gpui::prelude::FluentBuilder; -use gpui::{ - App, ClickEvent, InteractiveElement, IntoElement, ParentElement as _, RenderOnce, SharedString, - StatefulInteractiveElement, Styled, Window, div, px, -}; -use nostr_sdk::prelude::*; -use settings::AppSettings; -use theme::ActiveTheme; -use ui::avatar::Avatar; -use ui::dock::ClosePanel; -use ui::modal::ModalButtonProps; -use ui::{Icon, IconName, Selectable, Sizable, StyledExt, WindowExtension, h_flex}; - -use crate::dialogs::screening; - -#[derive(IntoElement)] -pub struct RoomEntry { - ix: usize, - public_key: Option, - name: Option, - avatar: Option, - seed: Option, - created_at: Option, - kind: Option, - depth: u8, - selected: bool, - #[allow(clippy::type_complexity)] - handler: Option>, -} - -impl RoomEntry { - pub fn new(ix: usize) -> Self { - Self { - ix, - public_key: None, - name: None, - avatar: None, - seed: None, - created_at: None, - kind: None, - depth: 0, - handler: None, - selected: false, - } - } - - pub fn public_key(mut self, public_key: PublicKey) -> Self { - self.public_key = Some(public_key); - self - } - - pub fn name(mut self, name: impl Into) -> Self { - self.name = Some(name.into()); - self - } - - pub fn avatar(mut self, picture: Option) -> Self { - self.avatar = picture; - self - } - - pub fn seed(mut self, seed: impl Into) -> Self { - self.seed = Some(seed.into()); - self - } - - pub fn created_at(mut self, created_at: impl Into) -> Self { - self.created_at = Some(created_at.into()); - self - } - - pub fn kind(mut self, kind: RoomKind) -> Self { - self.kind = Some(kind); - self - } - - pub fn depth(mut self, depth: u8) -> Self { - self.depth = depth; - self - } - - pub fn on_click( - mut self, - handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, - ) -> Self { - self.handler = Some(Rc::new(handler)); - self - } -} - -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(); - let avatar = match (self.avatar, self.seed) { - (None, None) => None, - (picture, seed) => Some( - Avatar::new(picture) - .when_some(seed, |avatar, seed| avatar.seed(seed)) - .xsmall() - .flex_shrink_0(), - ), - }; - - h_flex() - .id(self.ix) - .h_8() - .w_full() - .pl(px(6. + self.depth as f32 * 10.)) - .pr_1p5() - .gap_2() - .text_sm() - .rounded(cx.theme().radius) - .when(!hide_avatar, |this| this.children(avatar)) - .child( - div() - .flex_1() - .flex() - .items_center() - .justify_between() - .when_some(self.name, |this, name| { - this.child( - h_flex() - .flex_1() - .justify_between() - .line_clamp(1) - .text_ellipsis() - .truncate() - .font_medium() - .child(name) - .when(is_selected, |this| { - this.child( - Icon::new(IconName::CheckCircle) - .small() - .text_color(cx.theme().icon_accent), - ) - }), - ) - }) - .child( - h_flex() - .gap_1p5() - .flex_shrink_0() - .text_xs() - .text_color(cx.theme().text_placeholder) - .when_some(self.created_at, |this, created_at| this.child(created_at)), - ), - ) - .hover(|this| this.bg(cx.theme().elevated_surface_background)) - .when_some(self.handler, |this, handler| { - this.on_click(move |event, window, cx| { - handler(event, window, cx); - - if let Some(public_key) = public_key - && self.kind != Some(RoomKind::Ongoing) - && screening - { - let screening = screening::init(public_key, window, cx); - - window.open_modal(cx, move |this, _window, _cx| { - this.confirm() - .child(screening.clone()) - .button_props( - ModalButtonProps::default() - .cancel_text("Ignore") - .ok_text("Response"), - ) - .on_cancel(move |_event, window, cx| { - window.dispatch_action(Box::new(ClosePanel), cx); - true - }) - }); - } - }) - }) - } -} diff --git a/crates/workspace/src/sidebar/mod.rs b/crates/workspace/src/sidebar/mod.rs index 549f2f1a..f0bd516a 100644 --- a/crates/workspace/src/sidebar/mod.rs +++ b/crates/workspace/src/sidebar/mod.rs @@ -5,10 +5,10 @@ use std::rc::Rc; use auto_update::AutoUpdater; use chat::{ChatEvent, ChatRegistry, Room, RoomKind}; use common::TimestampExt; -use community::{CommunityEvent, CommunityMetadata, CommunityRegistry}; +use community::{CommunityEvent, CommunityRegistry}; use gpui::prelude::FluentBuilder; use gpui::{ - AnyElement, App, AppContext, Context, ElementId, Entity, EventEmitter, FocusHandle, Focusable, + AnyElement, App, Context, ElementId, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled, Subscription, UniformListScrollHandle, Window, div, px, retain_all, uniform_list, }; @@ -19,10 +19,10 @@ use state::NostrRegistry; use theme::{ActiveTheme, TABBAR_HEIGHT}; use ui::avatar::Avatar; use ui::button::{Button, ButtonVariants}; -use ui::dock::{Panel, PanelEvent}; +use ui::dock::{ClosePanel, Panel, PanelEvent}; use ui::indicator::Indicator; -use ui::input::{Input, InputState}; use ui::menu::{ContextMenu, DropdownMenu, PopupMenuItem}; +use ui::modal::ModalButtonProps; use ui::nav_item::NavItem; use ui::scroll::Scrollbar; use ui::{ @@ -31,28 +31,22 @@ use ui::{ }; use crate::Command; +use crate::dialogs::screening; -mod entry; mod tree; -pub(crate) use entry::RoomEntry; -use tree::{SidebarRow, TreeRow, TreeRowKind, TreeSection}; +use tree::{SidebarRow, TreeSection}; +pub(crate) use tree::{TreeRow, TreeRowKind}; -/// Sidebar. pub struct Sidebar { focus_handle: FocusHandle, scroll_handle: UniformListScrollHandle, - /// Whether there are new chat requests new_requests: bool, - /// Expanded tree sections expanded: BTreeSet, - /// Pinned room ids, in pin order pinned_rooms: Vec, - - /// Event subscriptions _subscriptions: SmallVec<[Subscription; 2]>, } @@ -100,10 +94,6 @@ impl Sidebar { self.expanded.insert(section); } - if section == TreeSection::Requests { - self.new_requests = false; - } - self.save_expanded(cx); cx.notify(); } @@ -156,36 +146,6 @@ impl Sidebar { self.pinned_rooms.contains(&room_id) } - fn new_community(&mut self, window: &mut Window, cx: &mut Context) { - let name_input = cx.new(|cx| InputState::new(window, cx).placeholder("Community name")); - - window.open_modal(cx, move |this, _window, _cx| { - let name_input = name_input.clone(); - - this.width(px(380.)) - .confirm() - .title("New community") - .child(Input::new(&name_input)) - .on_ok(move |_event, _window, cx| { - let name = name_input.read(cx).value().trim().to_owned(); - - if name.is_empty() { - return false; - } - - let metadata = CommunityMetadata { - name, - ..CommunityMetadata::default() - }; - - CommunityRegistry::global(cx) - .update(cx, |registry, cx| registry.create(metadata, cx)); - - true - }) - }); - } - fn tree_rows(&self, cx: &App) -> Vec { let chat = ChatRegistry::global(cx); let chat = chat.read(cx); @@ -206,35 +166,11 @@ impl Sidebar { }); if self.is_expanded(TreeSection::Pins) { - rows.extend(pinned.into_iter().map(|room| SidebarRow::Room { - room, - depth: 1, - pinned: true, - })); - } - } - - let requests = chat.rooms(&RoomKind::Request, cx); - rows.push(SidebarRow::Section { - section: TreeSection::Requests, - count: requests.len(), - }); - - if self.is_expanded(TreeSection::Requests) { - if requests.is_empty() { - rows.push(SidebarRow::Hint { - text: "No pending requests".into(), - depth: 1, - }); - } else { - rows.extend(requests.into_iter().map(|room| { - let pinned = self.is_pinned(room.read(cx).id); - SidebarRow::Room { - room, - depth: 1, - pinned, - } - })); + rows.extend( + pinned + .into_iter() + .map(|room| SidebarRow::Room { room, pinned: true }), + ); } } @@ -250,21 +186,15 @@ impl Sidebar { if communities.is_empty() { rows.push(SidebarRow::Hint { text: "No communities yet".into(), - depth: 1, }); } else { rows.extend( communities .iter() .cloned() - .map(|community| SidebarRow::Community { - community, - depth: 1, - }), + .map(|community| SidebarRow::Community { community }), ); } - - rows.push(SidebarRow::NewCommunity { depth: 1 }); } let messages = chat.rooms(&RoomKind::Ongoing, cx); @@ -277,16 +207,11 @@ impl Sidebar { if messages.is_empty() { rows.push(SidebarRow::Hint { text: "No conversations yet".into(), - depth: 1, }); } else { rows.extend(messages.into_iter().map(|room| { let pinned = self.is_pinned(room.read(cx).id); - SidebarRow::Room { - room, - depth: 1, - pinned, - } + SidebarRow::Room { room, pinned } })); } } @@ -321,22 +246,13 @@ impl Sidebar { } else { IconName::CaretRight }) - .icon(section.icon()) .count(*count) - .when( - section == TreeSection::Requests && self.new_requests, - |this| this.dot(), - ) .on_click(cx.listener(move |this, _event, _window, cx| { this.toggle_section(section, cx); })) .into_any_element() } - SidebarRow::Room { - room, - depth, - pinned, - } => { + SidebarRow::Room { room, pinned } => { let pinned = *pinned; let room_id = room.read(cx).id; let public_key = room.read(cx).display_member(cx).public_key(); @@ -346,23 +262,42 @@ impl Sidebar { let kind = room.read(cx).kind; let created_at = room.read(cx).created_at.to_ago(); let room_clone = room.clone(); + let sidebar = cx.entity().downgrade(); + let handler = cx.listener(move |_this, _event, window, cx| { ChatRegistry::global(cx).update(cx, |chat, cx| { chat.emit_room(&room_clone, window, cx); }); + + if kind != RoomKind::Ongoing && AppSettings::get_screening(cx) { + let screening = screening::init(public_key, window, cx); + + window.open_modal(cx, move |this, _window, _cx| { + this.confirm() + .child(screening.clone()) + .button_props( + ModalButtonProps::default() + .cancel_text("Ignore") + .ok_text("Response"), + ) + .on_cancel(move |_event, window, cx| { + window.dispatch_action(Box::new(ClosePanel), cx); + true + }) + }); + } }); - let entry = RoomEntry::new(index) - .name(name) - .avatar(picture) - .seed(seed) - .public_key(public_key) - .kind(kind) - .created_at(created_at) - .depth(*depth) - .on_click(handler); + let entry = TreeRow::new( + ElementId::NamedInteger("tree-row".into(), index as u64), + TreeRowKind::Room, + name, + ) + .avatar(seed) + .picture(picture) + .created_at(created_at) + .on_click(handler); - let sidebar = cx.entity().downgrade(); ContextMenu::new( ElementId::NamedInteger("room-context-menu".into(), index as u64), entry, @@ -394,7 +329,7 @@ impl Sidebar { ) .into_any_element() } - SidebarRow::Community { community, depth } => { + SidebarRow::Community { community } => { let community = community.read(cx); TreeRow::new( @@ -402,28 +337,15 @@ impl Sidebar { TreeRowKind::Community, community.name(), ) - .depth(*depth) .avatar(community.id().to_hex()) .picture(community.icon()) .into_any_element() } - SidebarRow::NewCommunity { depth } => TreeRow::new( - ElementId::NamedInteger("tree-row".into(), index as u64), - TreeRowKind::Hint, - "New community", - ) - .depth(*depth) - .icon(IconName::Plus) - .on_click(cx.listener(|this, _event, window, cx| { - this.new_community(window, cx); - })) - .into_any_element(), - SidebarRow::Hint { text, depth } => TreeRow::new( + SidebarRow::Hint { text } => TreeRow::new( ElementId::NamedInteger("tree-row".into(), index as u64), TreeRowKind::Hint, text.clone(), ) - .depth(*depth) .into_any_element(), } }) @@ -564,6 +486,7 @@ impl Render for Sidebar { let chat = ChatRegistry::global(cx); let loading = chat.read(cx).loading() && logged_in; + let sidebar = cx.entity().downgrade(); let rows = Rc::new(self.tree_rows(cx)); v_flex() @@ -582,6 +505,28 @@ impl Render for Sidebar { cx.dispatch_action(&Command::ShowInbox) }), ) + .child( + NavItem::new( + "nav-requests", + "Requests", + Icon::new(IconName::Invite).small(), + ) + .when(self.new_requests, |this| { + this.suffix(div().size_1().rounded_full().bg(cx.theme().cursor)) + }) + .on_click({ + let sidebar = sidebar.clone(); + move |_event, _window, cx| { + if let Err(error) = sidebar.update(cx, |this, cx| { + this.new_requests = false; + cx.notify(); + }) { + log::error!("Failed to clear new requests: {error}"); + } + cx.dispatch_action(&Command::ShowRequests); + } + }), + ) .child( NavItem::new("nav-browse", "Browse", Icon::new(IconName::Compass).small()) .on_click(|_event, _window, cx| { diff --git a/crates/workspace/src/sidebar/tree.rs b/crates/workspace/src/sidebar/tree.rs index 4c259157..823722ac 100644 --- a/crates/workspace/src/sidebar/tree.rs +++ b/crates/workspace/src/sidebar/tree.rs @@ -1,21 +1,20 @@ -use std::path::PathBuf; use std::rc::Rc; use chat::Room; use community::Community; use gpui::prelude::FluentBuilder; use gpui::{ - App, ClickEvent, ElementId, Entity, InteractiveElement, IntoElement, ParentElement, RenderOnce, - SharedString, StatefulInteractiveElement, Styled, Window, div, px, + App, ClickEvent, ElementId, Entity, ImageSource, InteractiveElement, IntoElement, + ParentElement, RenderOnce, SharedString, StatefulInteractiveElement, Styled, Window, div, }; +use settings::AppSettings; use theme::ActiveTheme; use ui::avatar::{Avatar, PixelAvatar}; -use ui::{Icon, IconName, Sizable, StyledExt, h_flex}; +use ui::{Icon, IconName, Selectable, Sizable, StyledExt, h_flex}; #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum TreeSection { Pins, - Requests, Community, Messages, } @@ -24,23 +23,14 @@ impl TreeSection { pub fn label(self) -> &'static str { match self { Self::Pins => "Pinned", - Self::Requests => "Requests", Self::Community => "Community", Self::Messages => "Messages", } } - pub fn icon(self) -> IconName { - match self { - Self::Pins | Self::Requests | Self::Community => IconName::Folder, - Self::Messages => IconName::Message, - } - } - pub fn key(self) -> &'static str { match self { Self::Pins => "pins", - Self::Requests => "requests", Self::Community => "community", Self::Messages => "messages", } @@ -49,7 +39,6 @@ impl TreeSection { pub fn from_key(key: &str) -> Option { match key { "pins" => Some(Self::Pins), - "requests" => Some(Self::Requests), "community" => Some(Self::Community), "messages" => Some(Self::Messages), _ => None, @@ -58,31 +47,16 @@ impl TreeSection { } pub enum SidebarRow { - Section { - section: TreeSection, - count: usize, - }, - Room { - room: Entity, - depth: u8, - pinned: bool, - }, - Community { - community: Entity, - depth: u8, - }, - NewCommunity { - depth: u8, - }, - Hint { - text: SharedString, - depth: u8, - }, + Section { section: TreeSection, count: usize }, + Room { room: Entity, pinned: bool }, + Community { community: Entity }, + Hint { text: SharedString }, } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum TreeRowKind { Section, + Room, Community, Hint, } @@ -91,14 +65,13 @@ pub enum TreeRowKind { pub struct TreeRow { id: ElementId, kind: TreeRowKind, - depth: u8, - caret: Option, - icon: Option, - avatar: Option, - picture: Option, label: SharedString, + avatar: Option, + picture: Option, + caret: Option, count: Option, - dot: bool, + created_at: Option, + selected: bool, #[allow(clippy::type_complexity)] on_click: Option>, } @@ -112,33 +85,22 @@ impl TreeRow { Self { id: id.into(), kind, - depth: 0, - caret: None, - icon: None, + label: label.into(), avatar: None, picture: None, - label: label.into(), + caret: None, count: None, - dot: false, + created_at: None, + selected: false, on_click: None, } } - pub fn depth(mut self, depth: u8) -> Self { - self.depth = depth; - self - } - pub fn caret(mut self, caret: IconName) -> Self { self.caret = Some(caret); self } - pub fn icon(mut self, icon: IconName) -> Self { - self.icon = Some(icon); - self - } - /// Sets the seed for the row's generated avatar. pub fn avatar(mut self, seed: impl Into) -> Self { self.avatar = Some(seed.into()); @@ -146,8 +108,8 @@ impl TreeRow { } /// Shows `picture` instead of the generated avatar. - pub fn picture(mut self, picture: Option) -> Self { - self.picture = picture; + pub fn picture(mut self, picture: Option>) -> Self { + self.picture = picture.map(Into::into); self } @@ -156,8 +118,8 @@ impl TreeRow { self } - pub fn dot(mut self) -> Self { - self.dot = true; + pub fn created_at(mut self, created_at: impl Into) -> Self { + self.created_at = Some(created_at.into()); self } @@ -170,76 +132,107 @@ impl TreeRow { } } +impl Selectable for TreeRow { + fn selected(mut self, selected: bool) -> Self { + self.selected = selected; + self + } + + fn is_selected(&self) -> bool { + self.selected + } +} + impl RenderOnce for TreeRow { fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { - let indent = px(6. + self.depth as f32 * 14.); + let hide_avatar = AppSettings::get_hide_avatar(cx); + let is_section = self.kind == TreeRowKind::Section; + let is_room = self.kind == TreeRowKind::Room; let is_community = self.kind == TreeRowKind::Community; let is_hint = self.kind == TreeRowKind::Hint; + let is_selected = self.selected; - let avatar = match (self.avatar, self.picture) { - (seed, Some(picture)) => Some( - Avatar::from_source(picture) - .when_some(seed, |avatar, seed| avatar.seed(seed)) - .xsmall() - .into_any_element(), - ), - (Some(seed), None) => Some(PixelAvatar::new(seed).xsmall().into_any_element()), - (None, None) => None, + let avatar = if hide_avatar { + None + } else { + match (self.avatar, self.picture) { + (None, None) => None, + (seed, Some(picture)) => Some( + Avatar::from_source(picture) + .when_some(seed, |avatar, seed| avatar.seed(seed)) + .xsmall() + .flex_shrink_0() + .into_any_element(), + ), + (Some(seed), None) => Some( + PixelAvatar::new(seed) + .xsmall() + .flex_shrink_0() + .into_any_element(), + ), + } }; h_flex() .id(self.id) .h_8() .w_full() - .pl(indent) - .pr_1p5() + .px_2() .gap_2() .rounded(cx.theme().radius) .when(is_section, |this| { - this.text_xs().text_color(cx.theme().text_muted) + this.text_xs() + .text_color(cx.theme().text_muted) + .font_semibold() }) - .when(is_community, |this| this.text_sm()) + .when(is_room || is_community, |this| this.text_sm()) .when(is_hint, |this| { this.text_xs() .font_normal() .text_color(cx.theme().text_placeholder) }) - .when_some(self.icon, |this, icon| { - this.child(Icon::new(icon).small().text_color(cx.theme().icon_muted)) - }) .when_some(avatar, |this, avatar| this.child(avatar)) .child( h_flex() .gap_1() .flex_1() - .child(div().truncate().min_w_0().child(self.label)) + .child( + div() + .truncate() + .min_w_0() + .when(is_room, |this| this.font_medium()) + .child(self.label), + ) + .when(is_selected, |this| { + this.child( + Icon::new(IconName::CheckCircle) + .small() + .flex_shrink_0() + .text_color(cx.theme().icon_accent), + ) + }) .when_some(self.count, |this, count| { + this.child(div().flex_shrink_0().font_normal().child(count.to_string())) + }) + .when_some(self.created_at, |this, created_at| { this.child( div() .flex_shrink_0() .text_xs() .text_color(cx.theme().text_placeholder) - .font_semibold() - .child(count.to_string()), + .child(created_at), ) }), ) .when_some(self.caret, |this, caret| { - this.child(Icon::new(caret).xsmall().text_color(cx.theme().icon_muted)) - }) - .when(self.dot, |this| { - this.child( - div() - .flex_shrink_0() - .size_1() - .rounded_full() - .bg(cx.theme().cursor), - ) + this.child(Icon::new(caret).small().text_color(cx.theme().icon_muted)) }) .when_some(self.on_click, |this, handler| { this.cursor_pointer() - .hover(|this| this.bg(cx.theme().ghost_element_hover)) + .when(!is_section, |this| { + this.hover(|this| this.bg(cx.theme().ghost_element_hover)) + }) .on_click(move |event, window, cx| handler(event, window, cx)) }) }