feat: replace sidebar tabs with a single collapsible list
This commit is contained in:
@@ -10,6 +10,7 @@ use crate::avatar::Avatar;
|
|||||||
use crate::button::Button;
|
use crate::button::Button;
|
||||||
use crate::menu::PopupMenu;
|
use crate::menu::PopupMenu;
|
||||||
use crate::nav::Nav;
|
use crate::nav::Nav;
|
||||||
|
use crate::nav_item::NavItem;
|
||||||
use crate::popover::{Popover, PopoverState};
|
use crate::popover::{Popover, PopoverState};
|
||||||
|
|
||||||
/// Builds the items of a popup menu on each render.
|
/// Builds the items of a popup menu on each render.
|
||||||
@@ -44,6 +45,8 @@ impl DropdownMenu for Avatar {}
|
|||||||
|
|
||||||
impl DropdownMenu for Nav {}
|
impl DropdownMenu for Nav {}
|
||||||
|
|
||||||
|
impl DropdownMenu for NavItem {}
|
||||||
|
|
||||||
#[derive(IntoElement)]
|
#[derive(IntoElement)]
|
||||||
pub struct DropdownMenuPopover<T: Selectable + IntoElement + 'static> {
|
pub struct DropdownMenuPopover<T: Selectable + IntoElement + 'static> {
|
||||||
id: ElementId,
|
id: ElementId,
|
||||||
|
|||||||
@@ -2,23 +2,26 @@ use std::rc::Rc;
|
|||||||
|
|
||||||
use gpui::prelude::FluentBuilder as _;
|
use gpui::prelude::FluentBuilder as _;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AnyElement, App, ClickEvent, ElementId, InteractiveElement, IntoElement, ParentElement,
|
AnyElement, App, ClickEvent, ElementId, InteractiveElement, Interactivity, IntoElement,
|
||||||
RenderOnce, SharedString, StatefulInteractiveElement as _, StyleRefinement, Styled, Window,
|
ParentElement, RenderOnce, SharedString, StatefulInteractiveElement as _, StyleRefinement,
|
||||||
div,
|
Styled, Window, div,
|
||||||
};
|
};
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
|
|
||||||
use crate::{StyledExt, h_flex};
|
use crate::{Selectable, StyledExt, h_flex};
|
||||||
|
|
||||||
/// A single navigation entry in a sidebar.
|
/// A single navigation entry in a sidebar.
|
||||||
#[allow(clippy::type_complexity)]
|
#[allow(clippy::type_complexity)]
|
||||||
#[derive(IntoElement)]
|
#[derive(IntoElement)]
|
||||||
pub struct NavItem {
|
pub struct NavItem {
|
||||||
id: ElementId,
|
id: ElementId,
|
||||||
|
interactivity: Interactivity,
|
||||||
style: StyleRefinement,
|
style: StyleRefinement,
|
||||||
icon: AnyElement,
|
icon: AnyElement,
|
||||||
label: SharedString,
|
label: SharedString,
|
||||||
suffix: Option<AnyElement>,
|
suffix: Option<AnyElement>,
|
||||||
|
selected: bool,
|
||||||
|
clickable: bool,
|
||||||
on_click: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>>,
|
on_click: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,16 +31,30 @@ impl NavItem {
|
|||||||
label: impl Into<SharedString>,
|
label: impl Into<SharedString>,
|
||||||
icon: impl IntoElement,
|
icon: impl IntoElement,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
|
let id = id.into();
|
||||||
|
let mut interactivity = Interactivity::default();
|
||||||
|
interactivity.element_id = Some(id.clone());
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
id: id.into(),
|
id,
|
||||||
|
interactivity,
|
||||||
style: StyleRefinement::default(),
|
style: StyleRefinement::default(),
|
||||||
icon: icon.into_any_element(),
|
icon: icon.into_any_element(),
|
||||||
label: label.into(),
|
label: label.into(),
|
||||||
suffix: None,
|
suffix: None,
|
||||||
|
selected: false,
|
||||||
|
clickable: false,
|
||||||
on_click: None,
|
on_click: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Marks the item as clickable even when it has no `on_click` handler, so it
|
||||||
|
/// still shows the hover affordance. Used by items whose click opens a menu.
|
||||||
|
pub fn clickable(mut self, clickable: bool) -> Self {
|
||||||
|
self.clickable = clickable;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub fn suffix(mut self, suffix: impl IntoElement) -> Self {
|
pub fn suffix(mut self, suffix: impl IntoElement) -> Self {
|
||||||
self.suffix = Some(suffix.into_any_element());
|
self.suffix = Some(suffix.into_any_element());
|
||||||
self
|
self
|
||||||
@@ -52,6 +69,23 @@ impl NavItem {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Selectable for NavItem {
|
||||||
|
fn selected(mut self, selected: bool) -> Self {
|
||||||
|
self.selected = selected;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_selected(&self) -> bool {
|
||||||
|
self.selected
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InteractiveElement for NavItem {
|
||||||
|
fn interactivity(&mut self) -> &mut Interactivity {
|
||||||
|
&mut self.interactivity
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Styled for NavItem {
|
impl Styled for NavItem {
|
||||||
fn style(&mut self) -> &mut StyleRefinement {
|
fn style(&mut self) -> &mut StyleRefinement {
|
||||||
&mut self.style
|
&mut self.style
|
||||||
@@ -60,7 +94,7 @@ impl Styled for NavItem {
|
|||||||
|
|
||||||
impl RenderOnce for NavItem {
|
impl RenderOnce for NavItem {
|
||||||
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||||
let clickable = self.on_click.is_some();
|
let clickable = self.on_click.is_some() || self.clickable;
|
||||||
|
|
||||||
h_flex()
|
h_flex()
|
||||||
.id(self.id)
|
.id(self.id)
|
||||||
@@ -86,6 +120,9 @@ impl RenderOnce for NavItem {
|
|||||||
.when(clickable, |this| {
|
.when(clickable, |this| {
|
||||||
this.cursor_pointer()
|
this.cursor_pointer()
|
||||||
.hover(|this| this.bg(cx.theme().ghost_element_hover))
|
.hover(|this| this.bg(cx.theme().ghost_element_hover))
|
||||||
|
.when(self.selected, |this| {
|
||||||
|
this.bg(cx.theme().ghost_element_active)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
.when_some(self.on_click, |this, handler| {
|
.when_some(self.on_click, |this, handler| {
|
||||||
this.on_click(move |event, window, cx| handler(event, window, cx))
|
this.on_click(move |event, window, cx| handler(event, window, cx))
|
||||||
|
|||||||
+193
-189
@@ -8,10 +8,10 @@ use chat::{ChatEvent, ChatRegistry, Room, RoomKind};
|
|||||||
use common::TimestampExt;
|
use common::TimestampExt;
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AnyElement, App, AppContext, ClipboardItem, Context, Div, Entity, EventEmitter, FocusHandle,
|
Anchor, AnyElement, App, AppContext, ClipboardItem, Context, Div, Entity, EventEmitter,
|
||||||
Focusable, InteractiveElement, IntoElement, ObjectFit, ParentElement, Render, SharedString,
|
FocusHandle, Focusable, InteractiveElement, IntoElement, ObjectFit, ParentElement, Render,
|
||||||
Stateful, Styled, StyledImage, Subscription, Task, UniformListScrollHandle, WeakEntity, Window,
|
SharedString, Stateful, Styled, StyledImage, Subscription, Task, UniformListScrollHandle,
|
||||||
div, img, px, relative, retain_all, uniform_list,
|
WeakEntity, Window, div, img, px, relative, retain_all, uniform_list,
|
||||||
};
|
};
|
||||||
use nip29::{Group, GroupKey, GroupsEvent, GroupsRegistry, Membership};
|
use nip29::{Group, GroupKey, GroupsEvent, GroupsRegistry, Membership};
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
@@ -30,8 +30,6 @@ use ui::nav::Nav;
|
|||||||
use ui::nav_item::NavItem;
|
use ui::nav_item::NavItem;
|
||||||
use ui::notification::Notification;
|
use ui::notification::Notification;
|
||||||
use ui::scroll::Scrollbar;
|
use ui::scroll::Scrollbar;
|
||||||
use ui::tab::Tab;
|
|
||||||
use ui::tab::tab_bar::TabBar;
|
|
||||||
use ui::{
|
use ui::{
|
||||||
Disableable, Icon, IconName, Selectable, Sizable, StyledExt, TRAFFIC_LIGHT_PADDING,
|
Disableable, Icon, IconName, Selectable, Sizable, StyledExt, TRAFFIC_LIGHT_PADDING,
|
||||||
WindowExtension, h_flex, title_bar_drag_handlers, v_flex,
|
WindowExtension, h_flex, title_bar_drag_handlers, v_flex,
|
||||||
@@ -40,19 +38,34 @@ use ui::{
|
|||||||
use crate::Command;
|
use crate::Command;
|
||||||
use crate::dialogs::{import, profile, screening};
|
use crate::dialogs::{import, profile, screening};
|
||||||
|
|
||||||
mod tab;
|
|
||||||
mod utils;
|
mod utils;
|
||||||
|
|
||||||
use tab::SidebarTab;
|
|
||||||
pub(crate) use utils::pick_banner;
|
pub(crate) use utils::pick_banner;
|
||||||
|
|
||||||
|
/// A collapsible section of the sidebar's main list.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum ListSection {
|
||||||
|
Chats,
|
||||||
|
Groups,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ListSection {
|
||||||
|
fn label(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Chats => "Chats",
|
||||||
|
Self::Groups => "Groups",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub enum SidebarRow {
|
pub enum SidebarRow {
|
||||||
|
Section(ListSection),
|
||||||
Room { room: Entity<Room> },
|
Room { room: Entity<Room> },
|
||||||
Group { group: Entity<Group> },
|
Group { group: Entity<Group> },
|
||||||
Contact { public_key: PublicKey },
|
Contact { public_key: PublicKey },
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A collapsible section of the group view that replaces the tab list.
|
/// A collapsible section of the group view.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
enum GroupSection {
|
enum GroupSection {
|
||||||
Admins,
|
Admins,
|
||||||
@@ -79,15 +92,14 @@ enum GroupRow {
|
|||||||
|
|
||||||
pub struct Sidebar {
|
pub struct Sidebar {
|
||||||
focus_handle: FocusHandle,
|
focus_handle: FocusHandle,
|
||||||
scroll_handles: [UniformListScrollHandle; 2],
|
list_scroll: UniformListScrollHandle,
|
||||||
dock: WeakEntity<DockArea>,
|
dock: WeakEntity<DockArea>,
|
||||||
/// Random background shown behind the signed-out screen.
|
/// Random background shown behind the signed-out screen.
|
||||||
banner: SharedString,
|
banner: SharedString,
|
||||||
active_tab: SidebarTab,
|
|
||||||
|
|
||||||
/// The group whose information the sidebar is browsing, if any.
|
/// The group whose information the sidebar is browsing, if any.
|
||||||
group: Option<WeakEntity<Group>>,
|
group: Option<WeakEntity<Group>>,
|
||||||
/// Whether the sidebar is showing the group's information instead of its tabs.
|
/// Whether the sidebar is showing the group's information instead of its list.
|
||||||
group_open: bool,
|
group_open: bool,
|
||||||
/// Scroll state of the group's member list.
|
/// Scroll state of the group's member list.
|
||||||
group_scroll: UniformListScrollHandle,
|
group_scroll: UniformListScrollHandle,
|
||||||
@@ -96,7 +108,12 @@ pub struct Sidebar {
|
|||||||
/// Whether the group view's members section is expanded.
|
/// Whether the group view's members section is expanded.
|
||||||
members_open: bool,
|
members_open: bool,
|
||||||
|
|
||||||
/// Whether the inbox list is showing contacts to start a chat with.
|
/// Whether the main list's chats section is expanded.
|
||||||
|
chats_open: bool,
|
||||||
|
/// Whether the main list's groups section is expanded.
|
||||||
|
groups_open: bool,
|
||||||
|
|
||||||
|
/// Whether the main list is showing contacts to start a chat with.
|
||||||
contacts_open: bool,
|
contacts_open: bool,
|
||||||
/// The signed-in user's contacts, loaded when the contact picker opens.
|
/// The signed-in user's contacts, loaded when the contact picker opens.
|
||||||
contacts: Option<Vec<PublicKey>>,
|
contacts: Option<Vec<PublicKey>>,
|
||||||
@@ -105,11 +122,10 @@ pub struct Sidebar {
|
|||||||
/// Scroll state of the contact picker's list.
|
/// Scroll state of the contact picker's list.
|
||||||
contacts_scroll: UniformListScrollHandle,
|
contacts_scroll: UniformListScrollHandle,
|
||||||
|
|
||||||
/// Whether the inbox list is showing chat requests instead of rooms.
|
/// Whether the main list is showing chat requests instead of chats and groups.
|
||||||
requests_open: bool,
|
requests_open: bool,
|
||||||
/// Scroll state of the requests list.
|
|
||||||
requests_scroll: UniformListScrollHandle,
|
|
||||||
new_requests: bool,
|
new_requests: bool,
|
||||||
|
requests_scroll: UniformListScrollHandle,
|
||||||
|
|
||||||
tasks: SmallVec<[Task<Result<(), Error>>; 1]>,
|
tasks: SmallVec<[Task<Result<(), Error>>; 1]>,
|
||||||
_subscriptions: SmallVec<[Subscription; 4]>,
|
_subscriptions: SmallVec<[Subscription; 4]>,
|
||||||
@@ -159,18 +175,16 @@ impl Sidebar {
|
|||||||
|
|
||||||
Self {
|
Self {
|
||||||
focus_handle: cx.focus_handle(),
|
focus_handle: cx.focus_handle(),
|
||||||
scroll_handles: [
|
list_scroll: UniformListScrollHandle::new(),
|
||||||
UniformListScrollHandle::new(),
|
|
||||||
UniformListScrollHandle::new(),
|
|
||||||
],
|
|
||||||
dock,
|
dock,
|
||||||
banner: pick_banner(),
|
banner: pick_banner(),
|
||||||
active_tab: SidebarTab::Inbox,
|
|
||||||
group: None,
|
group: None,
|
||||||
group_open: false,
|
group_open: false,
|
||||||
group_scroll: UniformListScrollHandle::new(),
|
group_scroll: UniformListScrollHandle::new(),
|
||||||
admins_open: true,
|
admins_open: true,
|
||||||
members_open: true,
|
members_open: true,
|
||||||
|
chats_open: true,
|
||||||
|
groups_open: true,
|
||||||
contacts_open: false,
|
contacts_open: false,
|
||||||
contacts: None,
|
contacts: None,
|
||||||
selected_contacts: HashSet::new(),
|
selected_contacts: HashSet::new(),
|
||||||
@@ -183,7 +197,7 @@ impl Sidebar {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Show the group view, replacing the tab list.
|
/// Show the group view, replacing the main list.
|
||||||
fn show_group(&mut self, group: &Entity<Group>, cx: &mut Context<Self>) {
|
fn show_group(&mut self, group: &Entity<Group>, cx: &mut Context<Self>) {
|
||||||
self.group = Some(group.downgrade());
|
self.group = Some(group.downgrade());
|
||||||
self.group_open = true;
|
self.group_open = true;
|
||||||
@@ -214,7 +228,7 @@ impl Sidebar {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Leave the group view, returning the sidebar to its tab list.
|
/// Leave the group view, returning the sidebar to its main list.
|
||||||
fn hide_group(&mut self, cx: &mut Context<Self>) {
|
fn hide_group(&mut self, cx: &mut Context<Self>) {
|
||||||
if !self.group_open {
|
if !self.group_open {
|
||||||
return;
|
return;
|
||||||
@@ -224,16 +238,6 @@ impl Sidebar {
|
|||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn select_tab(&mut self, tab: SidebarTab, cx: &mut Context<Self>) {
|
|
||||||
if self.active_tab == tab {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
self.active_tab = tab;
|
|
||||||
self.close_contacts();
|
|
||||||
self.close_requests();
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Leave the contact picker and clear its selection.
|
/// Leave the contact picker and clear its selection.
|
||||||
fn close_contacts(&mut self) {
|
fn close_contacts(&mut self) {
|
||||||
self.contacts_open = false;
|
self.contacts_open = false;
|
||||||
@@ -508,26 +512,20 @@ impl Sidebar {
|
|||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_tabs(&mut self, cx: &mut Context<Self>) -> AnyElement {
|
fn render_list(&mut self, cx: &mut Context<Self>) -> AnyElement {
|
||||||
let sidebar = cx.entity().downgrade();
|
let requests_open = self.requests_open;
|
||||||
let active_tab = self.active_tab;
|
let contacts_open = self.contacts_open && !requests_open;
|
||||||
let requests_open = self.requests_open && active_tab == SidebarTab::Inbox;
|
let rows = Rc::new(self.rows_for(cx));
|
||||||
let contacts_open = self.contacts_open && active_tab == SidebarTab::Inbox && !requests_open;
|
|
||||||
let rows = Rc::new(self.rows_for(active_tab, cx));
|
|
||||||
|
|
||||||
// Whether to show the create button for the active tab
|
// Whether to show the create button for the contact picker
|
||||||
let create_button = contacts_open && !self.selected_contacts.is_empty();
|
let create_button = contacts_open && !self.selected_contacts.is_empty();
|
||||||
|
|
||||||
let (list_id, scroll_handle, list_title) = if contacts_open {
|
let (list_id, scroll_handle, list_title) = if contacts_open {
|
||||||
("sidebar-contacts", &self.contacts_scroll, "Contacts")
|
("sidebar-contacts", &self.contacts_scroll, Some("Contacts"))
|
||||||
} else if requests_open {
|
} else if requests_open {
|
||||||
("sidebar-requests", &self.requests_scroll, "Requests")
|
("sidebar-requests", &self.requests_scroll, Some("Requests"))
|
||||||
} else {
|
} else {
|
||||||
(
|
("sidebar-list", &self.list_scroll, None)
|
||||||
active_tab.list_id(),
|
|
||||||
&self.scroll_handles[active_tab.index()],
|
|
||||||
active_tab.list_title(),
|
|
||||||
)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let request_count = ChatRegistry::global(cx)
|
let request_count = ChatRegistry::global(cx)
|
||||||
@@ -541,127 +539,93 @@ impl Sidebar {
|
|||||||
.relative()
|
.relative()
|
||||||
.gap_2()
|
.gap_2()
|
||||||
.child(
|
.child(
|
||||||
div().px_2().child(
|
v_flex()
|
||||||
TabBar::new("sidebar-tabs")
|
.px_2()
|
||||||
.segmented(true)
|
.gap_1()
|
||||||
.selected_index(active_tab.index())
|
.child(
|
||||||
.child(Tab::new().label(SidebarTab::Inbox.label()))
|
NavItem::new("new", "New", Icon::new(IconName::PlusCircle).small())
|
||||||
.child(Tab::new().label(SidebarTab::Groups.label()))
|
.clickable(true)
|
||||||
.on_click({
|
.dropdown_menu_with_anchor(Anchor::TopRight, |menu, _window, _cx| {
|
||||||
let sidebar = sidebar.clone();
|
menu.menu_with_icon(
|
||||||
move |index, _window, cx| {
|
"New Chat",
|
||||||
let Some(tab) = SidebarTab::ALL.get(*index).copied() else {
|
IconName::Message,
|
||||||
return;
|
Box::new(Command::NewChat),
|
||||||
};
|
)
|
||||||
if let Err(error) =
|
.menu_with_icon(
|
||||||
sidebar.update(cx, |this, cx| this.select_tab(tab, cx))
|
"New Group",
|
||||||
{
|
IconName::Group,
|
||||||
log::error!("Failed to switch sidebar tab: {error}");
|
Box::new(Command::NewGroup),
|
||||||
}
|
)
|
||||||
}
|
.separator()
|
||||||
}),
|
.menu_with_icon(
|
||||||
),
|
"Join Group",
|
||||||
)
|
IconName::Door,
|
||||||
.map(|this| match active_tab {
|
Box::new(Command::JoinGroup),
|
||||||
SidebarTab::Inbox => this.child(
|
)
|
||||||
v_flex()
|
}),
|
||||||
.px_2()
|
)
|
||||||
.gap_1()
|
.child(
|
||||||
.child(
|
NavItem::new("browse", "Browse", Icon::new(IconName::Compass).small())
|
||||||
NavItem::new("chat", "New Chat", Icon::new(IconName::Message).small())
|
.on_click(|_, window, cx| {
|
||||||
.on_click(|_event, window, cx| {
|
window.dispatch_action(Box::new(Command::ShowBrowse), cx)
|
||||||
window.dispatch_action(Box::new(Command::NewChat), cx)
|
}),
|
||||||
}),
|
)
|
||||||
)
|
.child(
|
||||||
.child(
|
NavItem::new("reqs", "Requests", Icon::new(IconName::Invite).small())
|
||||||
NavItem::new("reqs", "Requests", Icon::new(IconName::Invite).small())
|
.when(request_count > 0, |this| {
|
||||||
.when(request_count > 0, |this| {
|
this.suffix(
|
||||||
this.suffix(
|
h_flex()
|
||||||
h_flex()
|
.gap_1()
|
||||||
.gap_1()
|
.items_center()
|
||||||
.items_center()
|
.child(
|
||||||
.child(
|
h_flex()
|
||||||
h_flex()
|
.py_0p5()
|
||||||
.py_0p5()
|
.px_1()
|
||||||
.px_1()
|
.min_w_6()
|
||||||
.min_w_6()
|
.justify_center()
|
||||||
.justify_center()
|
.text_size(px(10.))
|
||||||
.text_size(px(10.))
|
.text_color(cx.theme().text_muted)
|
||||||
.text_color(cx.theme().text_muted)
|
.text_center()
|
||||||
.text_center()
|
.rounded(cx.theme().radius)
|
||||||
.rounded(cx.theme().radius)
|
.bg(cx.theme().elevated_surface_background)
|
||||||
.bg(cx.theme().elevated_surface_background)
|
.child(SharedString::from(
|
||||||
.child(SharedString::from(
|
request_count.to_string(),
|
||||||
request_count.to_string(),
|
)),
|
||||||
)),
|
)
|
||||||
|
.when(self.new_requests && !requests_open, |this| {
|
||||||
|
this.child(
|
||||||
|
div().size_1().rounded_full().bg(cx.theme().cursor),
|
||||||
)
|
)
|
||||||
.when(self.new_requests && !requests_open, |this| {
|
}),
|
||||||
this.child(
|
)
|
||||||
div()
|
})
|
||||||
.size_1()
|
.when(requests_open, |this| {
|
||||||
.rounded_full()
|
this.bg(cx.theme().ghost_element_active)
|
||||||
.bg(cx.theme().cursor),
|
})
|
||||||
)
|
.on_click(cx.listener(|this, _event, _window, cx| {
|
||||||
}),
|
this.toggle_requests(cx);
|
||||||
)
|
})),
|
||||||
})
|
)
|
||||||
.when(requests_open, |this| {
|
.child(
|
||||||
this.bg(cx.theme().ghost_element_active)
|
NavItem::new("contacts", "Contacts", Icon::new(IconName::Book).small())
|
||||||
})
|
.when(contacts_open, |this| {
|
||||||
.on_click(cx.listener(|this, _event, _window, cx| {
|
this.bg(cx.theme().ghost_element_active)
|
||||||
this.toggle_requests(cx);
|
})
|
||||||
})),
|
.on_click(cx.listener(|this, _event, window, cx| {
|
||||||
)
|
this.toggle_contacts(window, cx);
|
||||||
.child(
|
})),
|
||||||
NavItem::new("contacts", "Contacts", Icon::new(IconName::Book).small())
|
),
|
||||||
.when(contacts_open, |this| {
|
|
||||||
this.bg(cx.theme().ghost_element_active)
|
|
||||||
})
|
|
||||||
.on_click(cx.listener(|this, _event, window, cx| {
|
|
||||||
this.toggle_contacts(window, cx);
|
|
||||||
})),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SidebarTab::Groups => this.child(
|
|
||||||
v_flex()
|
|
||||||
.px_2()
|
|
||||||
.gap_1()
|
|
||||||
.child(
|
|
||||||
NavItem::new(
|
|
||||||
"new-group",
|
|
||||||
"New Group",
|
|
||||||
Icon::new(IconName::Group).small(),
|
|
||||||
)
|
|
||||||
.on_click(|_, window, cx| {
|
|
||||||
window.dispatch_action(Box::new(Command::NewGroup), cx)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
NavItem::new(
|
|
||||||
"join-group",
|
|
||||||
"Join Group",
|
|
||||||
Icon::new(IconName::Door).small(),
|
|
||||||
)
|
|
||||||
.on_click(|_, window, cx| {
|
|
||||||
window.dispatch_action(Box::new(Command::JoinGroup), cx)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
NavItem::new("browse", "Browse", Icon::new(IconName::Compass).small())
|
|
||||||
.on_click(|_, window, cx| {
|
|
||||||
window.dispatch_action(Box::new(Command::ShowBrowse), cx)
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
})
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.px_4()
|
|
||||||
.text_xs()
|
|
||||||
.font_semibold()
|
|
||||||
.text_color(cx.theme().text_placeholder)
|
|
||||||
.child(list_title),
|
|
||||||
)
|
)
|
||||||
|
.when_some(list_title, |this, list_title| {
|
||||||
|
this.child(
|
||||||
|
div()
|
||||||
|
.px_4()
|
||||||
|
.text_xs()
|
||||||
|
.font_semibold()
|
||||||
|
.text_color(cx.theme().text_placeholder)
|
||||||
|
.child(list_title),
|
||||||
|
)
|
||||||
|
})
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.min_h_0()
|
.min_h_0()
|
||||||
@@ -720,42 +684,54 @@ impl Sidebar {
|
|||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn rows_for(&self, tab: SidebarTab, cx: &App) -> Vec<SidebarRow> {
|
fn rows_for(&self, cx: &App) -> Vec<SidebarRow> {
|
||||||
match tab {
|
if self.contacts_open {
|
||||||
SidebarTab::Inbox if self.contacts_open => self
|
return self
|
||||||
.contacts
|
.contacts
|
||||||
.clone()
|
.clone()
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|public_key| SidebarRow::Contact { public_key })
|
.map(|public_key| SidebarRow::Contact { public_key })
|
||||||
.collect(),
|
.collect();
|
||||||
SidebarTab::Inbox if self.requests_open => {
|
}
|
||||||
let chat = ChatRegistry::global(cx);
|
|
||||||
chat.read(cx)
|
if self.requests_open {
|
||||||
.rooms(&RoomKind::Request, cx)
|
let chat = ChatRegistry::global(cx);
|
||||||
.into_iter()
|
return chat
|
||||||
.map(|room| SidebarRow::Room { room })
|
.read(cx)
|
||||||
.collect()
|
.rooms(&RoomKind::Request, cx)
|
||||||
}
|
.into_iter()
|
||||||
SidebarTab::Inbox => {
|
.map(|room| SidebarRow::Room { room })
|
||||||
let chat = ChatRegistry::global(cx);
|
.collect();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut rows = Vec::new();
|
||||||
|
|
||||||
|
let chat = ChatRegistry::global(cx);
|
||||||
|
rows.push(SidebarRow::Section(ListSection::Chats));
|
||||||
|
if self.chats_open {
|
||||||
|
rows.extend(
|
||||||
chat.read(cx)
|
chat.read(cx)
|
||||||
.rooms(&RoomKind::Ongoing, cx)
|
.rooms(&RoomKind::Ongoing, cx)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|room| SidebarRow::Room { room })
|
.map(|room| SidebarRow::Room { room }),
|
||||||
.collect()
|
);
|
||||||
}
|
}
|
||||||
SidebarTab::Groups => {
|
|
||||||
let registry = GroupsRegistry::global(cx);
|
let registry = GroupsRegistry::global(cx);
|
||||||
|
rows.push(SidebarRow::Section(ListSection::Groups));
|
||||||
|
if self.groups_open {
|
||||||
|
rows.extend(
|
||||||
registry
|
registry
|
||||||
.read(cx)
|
.read(cx)
|
||||||
.groups()
|
.groups()
|
||||||
.iter()
|
.iter()
|
||||||
.cloned()
|
.cloned()
|
||||||
.map(|group| SidebarRow::Group { group })
|
.map(|group| SidebarRow::Group { group }),
|
||||||
.collect()
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rows
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_rows(
|
fn render_rows(
|
||||||
@@ -774,6 +750,34 @@ impl Sidebar {
|
|||||||
let index = range.start + offset;
|
let index = range.start + offset;
|
||||||
|
|
||||||
match row {
|
match row {
|
||||||
|
SidebarRow::Section(section) => {
|
||||||
|
let section = *section;
|
||||||
|
let open = match section {
|
||||||
|
ListSection::Chats => self.chats_open,
|
||||||
|
ListSection::Groups => self.groups_open,
|
||||||
|
};
|
||||||
|
let icon = if open {
|
||||||
|
IconName::CaretDown
|
||||||
|
} else {
|
||||||
|
IconName::CaretRight
|
||||||
|
};
|
||||||
|
let label = section.label();
|
||||||
|
|
||||||
|
Nav::new(label)
|
||||||
|
.label(label)
|
||||||
|
.suffix(Icon::new(icon).small().text_color(cx.theme().icon_muted))
|
||||||
|
.text_xs()
|
||||||
|
.font_semibold()
|
||||||
|
.text_color(cx.theme().text_placeholder)
|
||||||
|
.on_click(cx.listener(move |this, _event, _window, cx| {
|
||||||
|
match section {
|
||||||
|
ListSection::Chats => this.chats_open = !this.chats_open,
|
||||||
|
ListSection::Groups => this.groups_open = !this.groups_open,
|
||||||
|
}
|
||||||
|
cx.notify();
|
||||||
|
}))
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
SidebarRow::Room { room } => {
|
SidebarRow::Room { room } => {
|
||||||
let name = room.read(cx).display_name(cx);
|
let name = room.read(cx).display_name(cx);
|
||||||
let picture = room.read(cx).display_image(cx);
|
let picture = room.read(cx).display_image(cx);
|
||||||
@@ -1261,7 +1265,7 @@ impl Render for Sidebar {
|
|||||||
})
|
})
|
||||||
.map(|this| match group {
|
.map(|this| match group {
|
||||||
Some(group) if self.group_open => this.child(self.render_group(group, cx)),
|
Some(group) if self.group_open => this.child(self.render_group(group, cx)),
|
||||||
_ => this.child(self.render_tabs(cx)),
|
_ => this.child(self.render_list(cx)),
|
||||||
})
|
})
|
||||||
.when(loading && logged_in, |this| {
|
.when(loading && logged_in, |this| {
|
||||||
this.child(
|
this.child(
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
||||||
pub enum SidebarTab {
|
|
||||||
Inbox,
|
|
||||||
Groups,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SidebarTab {
|
|
||||||
pub const ALL: [SidebarTab; 2] = [Self::Inbox, Self::Groups];
|
|
||||||
|
|
||||||
pub fn label(self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
Self::Inbox => "Inbox",
|
|
||||||
Self::Groups => "Groups",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Heading for the tab's list of items.
|
|
||||||
pub fn list_title(self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
Self::Inbox => "Direct Messages",
|
|
||||||
Self::Groups => "Groups",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn list_id(self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
Self::Inbox => "sidebar-inbox",
|
|
||||||
Self::Groups => "sidebar-groups",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn index(self) -> usize {
|
|
||||||
match self {
|
|
||||||
Self::Inbox => 0,
|
|
||||||
Self::Groups => 1,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user