update sidebar

This commit is contained in:
2026-09-20 09:02:57 +07:00
parent 8401628412
commit 9c735febc1
6 changed files with 104 additions and 73 deletions
-6
View File
@@ -116,26 +116,20 @@ pub trait ButtonVariants: Sized {
#[allow(clippy::type_complexity)] #[allow(clippy::type_complexity)]
pub struct Button { pub struct Button {
base: BaseButton, base: BaseButton,
icon: Option<Icon>, icon: Option<Icon>,
label: Option<SharedString>, label: Option<SharedString>,
tooltip: Option<SharedString>, tooltip: Option<SharedString>,
children: Vec<AnyElement>, children: Vec<AnyElement>,
variant: ButtonVariant, variant: ButtonVariant,
size: Size, size: Size,
disabled: bool, disabled: bool,
loading: bool, loading: bool,
rounded: bool, rounded: bool,
compact: bool, compact: bool,
caret: bool, caret: bool,
indicator: bool, indicator: bool,
on_click: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>>, on_click: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>>,
on_hover: Option<Rc<dyn Fn(&bool, &mut Window, &mut App)>>, on_hover: Option<Rc<dyn Fn(&bool, &mut Window, &mut App)>>,
tab_index: isize, tab_index: isize,
tab_stop: bool, tab_stop: bool,
+34 -28
View File
@@ -255,35 +255,40 @@ fn rows_for(tab: SidebarTab, cx: &App) -> Vec<SidebarRow> {
}]; }];
} }
let mut rows = vec![SidebarRow::Section { let mut rows = Vec::new();
label: "Communities".into(),
count: community_count,
}];
rows.extend( if !communities.is_empty() {
communities rows.push(SidebarRow::Section {
.into_iter() label: "Communities".into(),
.map(|community| SidebarRow::Community { community }), count: community_count,
); });
rows.push(SidebarRow::Action { rows.extend(
label: "Show all communities".into(), communities
tab: SidebarTab::Communities, .into_iter()
}); .map(|community| SidebarRow::Community { community }),
);
rows.push(SidebarRow::Action {
label: "Show all communities".into(),
tab: SidebarTab::Communities,
});
}
rows.push(SidebarRow::Section { if !rooms.is_empty() {
label: "Chats".into(), rows.push(SidebarRow::Section {
count: rooms.len(), label: "Chats".into(),
}); count: rooms.len(),
rows.extend( });
rooms rows.extend(
.into_iter() rooms
.take(5) .into_iter()
.map(|room| SidebarRow::Room { room }), .take(5)
); .map(|room| SidebarRow::Room { room }),
rows.push(SidebarRow::Action { );
label: "Show all chats".into(), rows.push(SidebarRow::Action {
tab: SidebarTab::Chats, label: "Show all chats".into(),
}); tab: SidebarTab::Chats,
});
}
rows rows
} }
@@ -400,6 +405,7 @@ fn render_rows(range: Range<usize>, rows: &[SidebarRow], cx: &Context<Sidebar>)
TreeRowKind::Action, TreeRowKind::Action,
label.clone(), label.clone(),
) )
.icon(IconName::ArrowRight)
.on_click(cx.listener(move |this, _event, _window, cx| { .on_click(cx.listener(move |this, _event, _window, cx| {
this.select_tab(tab, cx); this.select_tab(tab, cx);
})) }))
@@ -590,7 +596,7 @@ impl Render for Sidebar {
this.child( this.child(
div() div()
.absolute() .absolute()
.bottom_12() .bottom_16()
.left_0() .left_0()
.h_9() .h_9()
.w_full() .w_full()
+32 -24
View File
@@ -1,7 +1,7 @@
use std::rc::Rc; use std::rc::Rc;
use gpui::prelude::FluentBuilder; use gpui::prelude::FluentBuilder;
use gpui::{App, IntoElement, ParentElement, RenderOnce, Styled, Window, div}; use gpui::{App, InteractiveElement, IntoElement, ParentElement, RenderOnce, Styled, Window, div};
use theme::ActiveTheme; use theme::ActiveTheme;
use ui::button::{Button, ButtonVariants}; use ui::button::{Button, ButtonVariants};
use ui::{IconName, Selectable, h_flex}; use ui::{IconName, Selectable, h_flex};
@@ -85,29 +85,37 @@ impl RenderOnce for TabBar {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
let Self { active, on_select } = self; let Self { active, on_select } = self;
div().absolute().bottom_2().left_0().w_full().px_2().child( div()
h_flex() .id("sidebar-tabs")
.w_full() .absolute()
.p_1() .bottom_3()
.gap_1() .left_0()
.rounded(cx.theme().radius_lg) .w_full()
.bg(cx.theme().elevated_surface_background) .px_4()
.when(cx.theme().shadow, |this| this.shadow_md()) .child(
.children(SidebarTab::ALL.into_iter().map(|tab| { h_flex()
let on_select = on_select.clone(); .w_full()
.p_1()
.gap_1()
.rounded_full()
.bg(cx.theme().background)
.when(cx.theme().shadow, |this| this.shadow_md())
.children(SidebarTab::ALL.into_iter().map(|tab| {
let on_select = on_select.clone();
Button::new(format!("tab-{}", tab.list_id())) Button::new(format!("tab-{}", tab.list_id()))
.icon(tab.icon()) .icon(tab.icon())
.ghost() .ghost()
.flex_1() .flex_1()
.selected(tab == active) .rounded()
.tooltip(tab.label()) .selected(tab == active)
.on_click(move |_event, window, cx| { .tooltip(tab.label())
if let Some(on_select) = on_select.as_ref() { .on_click(move |_event, window, cx| {
on_select(tab, window, cx); if let Some(on_select) = on_select.as_ref() {
} on_select(tab, window, cx);
}) }
})), })
) })),
)
} }
} }
+25 -7
View File
@@ -5,7 +5,7 @@ use community::Community;
use gpui::prelude::FluentBuilder; use gpui::prelude::FluentBuilder;
use gpui::{ use gpui::{
App, ClickEvent, ElementId, Entity, ImageSource, InteractiveElement, IntoElement, App, ClickEvent, ElementId, Entity, ImageSource, InteractiveElement, IntoElement,
ParentElement, RenderOnce, SharedString, StatefulInteractiveElement, Styled, Window, div, ParentElement, RenderOnce, SharedString, StatefulInteractiveElement, Styled, Window, div, px,
}; };
use settings::AppSettings; use settings::AppSettings;
use theme::ActiveTheme; use theme::ActiveTheme;
@@ -50,6 +50,7 @@ pub struct TreeRow {
label: SharedString, label: SharedString,
avatar: Option<SharedString>, avatar: Option<SharedString>,
picture: Option<ImageSource>, picture: Option<ImageSource>,
icon: Option<IconName>,
count: Option<usize>, count: Option<usize>,
created_at: Option<SharedString>, created_at: Option<SharedString>,
selected: bool, selected: bool,
@@ -69,6 +70,7 @@ impl TreeRow {
label: label.into(), label: label.into(),
avatar: None, avatar: None,
picture: None, picture: None,
icon: None,
count: None, count: None,
created_at: None, created_at: None,
selected: false, selected: false,
@@ -88,6 +90,12 @@ impl TreeRow {
self self
} }
/// Shows `icon` in the avatar slot when the row has no avatar or picture.
pub fn icon(mut self, icon: IconName) -> Self {
self.icon = Some(icon);
self
}
pub fn count(mut self, count: usize) -> Self { pub fn count(mut self, count: usize) -> Self {
self.count = Some(count); self.count = Some(count);
self self
@@ -150,6 +158,18 @@ impl RenderOnce for TreeRow {
} }
}; };
let avatar = avatar.or_else(|| {
self.icon.map(|icon| {
h_flex()
.flex_shrink_0()
.w(px(20.))
.justify_center()
.text_color(cx.theme().icon_muted)
.child(Icon::new(icon).small())
.into_any_element()
})
});
h_flex() h_flex()
.id(self.id) .id(self.id)
.h_8() .h_8()
@@ -159,14 +179,12 @@ impl RenderOnce for TreeRow {
.rounded(cx.theme().radius) .rounded(cx.theme().radius)
.when(is_section, |this| { .when(is_section, |this| {
this.text_xs() this.text_xs()
.text_color(cx.theme().text_muted) .text_color(cx.theme().text_placeholder)
.font_semibold() .font_semibold()
}) })
.when(is_room || is_community, |this| this.text_sm()) .when(is_room || is_community, |this| this.text_sm())
.when(is_action, |this| { .when(is_action, |this| {
this.text_sm() this.text_sm().text_color(cx.theme().text_muted)
.font_medium()
.text_color(cx.theme().text_accent)
}) })
.when(is_hint, |this| { .when(is_hint, |this| {
this.text_xs() this.text_xs()
@@ -197,11 +215,11 @@ impl RenderOnce for TreeRow {
this.child(div().flex_shrink_0().font_normal().child(count.to_string())) this.child(div().flex_shrink_0().font_normal().child(count.to_string()))
}) })
.when_some(self.created_at, |this, created_at| { .when_some(self.created_at, |this, created_at| {
this.child( this.child(div().flex_1()).child(
div() div()
.flex_shrink_0() .flex_shrink_0()
.text_xs()
.text_color(cx.theme().text_placeholder) .text_color(cx.theme().text_placeholder)
.text_xs()
.child(created_at), .child(created_at),
) )
}), }),
+4 -3
View File
@@ -602,9 +602,10 @@ client.subscribe(filter).with_id(sub_id).await?;
`crates/community`.** `concord` has no subscriptions, no `init`, and no `crates/community`.** `concord` has no subscriptions, no `init`, and no
`Entity<Community>`; `community::CommunityRegistry` owns one `Entity<Community>` `Entity<Community>`; `community::CommunityRegistry` owns one `Entity<Community>`
per state document, subscribes when a community's plane set changes, and per state document, subscribes when a community's plane set changes, and
re-folds on an inbound wrap. The sidebar observes the registry, logs re-folds on an inbound wrap. The sidebar subscribes to the registry, surfaces
`CommunityEvent::Error` through `log::error!`, and its "New community" row opens `CommunityEvent::Error` as a window notification, and its "New community" row
a name prompt that calls `CommunityRegistry::create`. `create` persists the in the Communities tab dispatches `Command::NewCommunity`, whose name prompt
calls `CommunityRegistry::create`. `create` persists the
genesis locally, publishes the wraps to the community's relays, and records the genesis locally, publishes the wraps to the community's relays, and records the
membership in the account's Community List — all best-effort, so a relay that is membership in the account's Community List — all best-effort, so a relay that is
down warns without losing the community. Discovery down warns without losing the community. Discovery
+9 -5
View File
@@ -205,7 +205,7 @@ Empty list shows the existing "No communities yet" hint.
Each phase is independently reviewable and leaves the app runnable. Each phase is independently reviewable and leaves the app runnable.
### Phase 1 — tab shell ### Phase 1 — tab shell — DONE
Files: `crates/workspace/src/sidebar/mod.rs`, Files: `crates/workspace/src/sidebar/mod.rs`,
`crates/workspace/src/sidebar/tab.rs` (new), `sidebar/tree.rs`, `crates/workspace/src/sidebar/tab.rs` (new), `sidebar/tree.rs`,
@@ -228,7 +228,7 @@ Files: `crates/workspace/src/sidebar/mod.rs`,
Validation: app runs signed in and signed out; chats and communities list and Validation: app runs signed in and signed out; chats and communities list and
open as before; tab switching works; requests dot still clears. open as before; tab switching works; requests dot still clears.
### Phase 2 — Recents tab ### Phase 2 — Recents tab — DONE
Files: `crates/settings/src/lib.rs`, `crates/workspace/src/sidebar/mod.rs`, Files: `crates/settings/src/lib.rs`, `crates/workspace/src/sidebar/mod.rs`,
`sidebar/tree.rs`. `sidebar/tree.rs`.
@@ -242,7 +242,7 @@ Files: `crates/settings/src/lib.rs`, `crates/workspace/src/sidebar/mod.rs`,
Validation: `cargo test -p settings`; manually open communities, restart, and Validation: `cargo test -p settings`; manually open communities, restart, and
confirm the Recents order; confirm ≤3 / ≤5 rendering and both "Show all" rows. confirm the Recents order; confirm ≤3 / ≤5 rendering and both "Show all" rows.
### Phase 3 — tab actions ### Phase 3 — tab actions — DONE
Files: `crates/workspace/src/dialogs/new_chat.rs` (new), Files: `crates/workspace/src/dialogs/new_chat.rs` (new),
`dialogs/new_community.rs` (new), `crates/workspace/src/dialogs/mod.rs`, `dialogs/new_community.rs` (new), `crates/workspace/src/dialogs/mod.rs`,
@@ -264,7 +264,7 @@ Validation: create a chat from an npub and confirm the room opens; create a
community and confirm it appears in the Communities tab and in Recents; community and confirm it appears in the Communities tab and in Recents;
requests/contacts/browse still dispatch. requests/contacts/browse still dispatch.
### Phase 4 — onboarding sidebar ### Phase 4 — onboarding sidebar — DONE, except `Join now`
Files: `crates/state/src/lib.rs`, `crates/workspace/src/sidebar/mod.rs`, Files: `crates/state/src/lib.rs`, `crates/workspace/src/sidebar/mod.rs`,
`sidebar/onboarding.rs` (new), `crates/workspace/src/dialogs/create_identity.rs` `sidebar/onboarding.rs` (new), `crates/workspace/src/dialogs/create_identity.rs`
@@ -291,7 +291,11 @@ Validation: with no stored credentials the sidebar shows onboarding and no
modal; `Import identity` still signs in; `Join now` signs in with a fresh key; modal; `Import identity` still signs in; `Join now` signs in with a fresh key;
with bunker credentials the tabs appear without an onboarding flash. with bunker credentials the tabs appear without an onboarding flash.
### Phase 5 — polish and cleanup **Deferred.** `dialogs/create_identity.rs` is not implemented, so `Join now`
renders without a click handler, and the §2.1 banner assets were skipped in
favor of a plain theme-colored background with the brand mark.
### Phase 5 — polish and cleanup — DONE
- Reposition the "Getting messages…" pill above the tab bar. - Reposition the "Getting messages…" pill above the tab bar.
- Empty states and counts for all three tabs; truncation rules (§3). - Empty states and counts for all three tabs; truncation rules (§3).