This commit is contained in:
2026-10-22 15:20:11 +07:00
parent b560ea4116
commit 49f2d9edc1
9 changed files with 468 additions and 610 deletions
+22 -36
View File
@@ -6,7 +6,7 @@ use gpui::{
ParentElement, RenderOnce, SharedString, StatefulInteractiveElement as _, StyleRefinement,
Styled, Window, div,
};
use gpui_kit::component::{ActiveTheme, Selectable, StyledExt as _, h_flex, v_flex};
use gpui_kit::component::{ActiveTheme, Selectable, StyledExt as _, h_flex};
/// A single navigation row, such as an entry in a sidebar list.
#[derive(IntoElement)]
@@ -101,41 +101,27 @@ impl Styled for Nav {
impl RenderOnce for Nav {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
let clickable = self.on_click.is_some() || self.clickable;
let text_color = if self.selected {
cx.theme().foreground
} else {
cx.theme().muted_foreground
};
v_flex().h_10().w_full().justify_center().child(
h_flex()
.id(self.id)
.h_9()
.w_full()
.px_1p5()
.gap_1p5()
.rounded(cx.theme().radius)
.text_color(text_color)
.when_some(self.prefix, |this, prefix| this.child(prefix))
.child(
h_flex()
.gap_1()
.flex_1()
.min_w_0()
.child(div().flex_1().min_w_0().truncate().child(self.label))
.when_some(self.suffix, |this, suffix| {
this.child(div().flex_shrink_0().child(suffix))
}),
)
.when(clickable, |this| {
this.cursor_pointer()
.hover(|this| this.bg(cx.theme().accent))
.when(self.selected, |this| this.bg(cx.theme().list_active))
})
.when_some(self.on_click, |this, handler| {
this.on_click(move |event, window, cx| handler(event, window, cx))
})
.refine_style(&self.style),
)
h_flex()
.id(self.id)
.h_8()
.w_full()
.px_2()
.gap_2()
.rounded(cx.theme().radius)
.when_some(self.prefix, |this, prefix| this.child(prefix))
.child(div().flex_1().min_w_0().truncate().child(self.label))
.when_some(self.suffix, |this, suffix| {
this.child(div().flex_shrink_0().child(suffix))
})
.when(clickable, |this| {
this.cursor_pointer()
.hover(|this| this.bg(cx.theme().list_hover))
.when(self.selected, |this| this.bg(cx.theme().list_active))
})
.when_some(self.on_click, |this, handler| {
this.on_click(move |event, window, cx| handler(event, window, cx))
})
.refine_style(&self.style)
}
}
+34 -11
View File
@@ -13,7 +13,6 @@ use gpui_kit::assets::IconName;
use gpui_kit::base::StyledExt as _;
use gpui_kit::component::button::{Button, ButtonVariants};
use gpui_kit::component::dock::{DockArea, DockPlacement, Panel, PanelEvent, PanelHandle};
use gpui_kit::component::group_box::{GroupBox, GroupBoxVariants};
use gpui_kit::component::scroll::ScrollableElement;
use gpui_kit::component::{ActiveTheme, Icon, Sizable, h_flex, v_flex};
use nostr_sdk::prelude::*;
@@ -302,11 +301,23 @@ impl GreeterPanel {
.collect()
};
GroupBox::new()
.id("requests")
.title("Requests")
.fill()
.child(v_flex().gap_0().w_full().children(rows))
v_flex()
.gap_2()
.child(
div()
.text_color(cx.theme().muted_foreground)
.text_xs()
.font_semibold()
.child("Requests"),
)
.child(
v_flex()
.p_2()
.rounded(cx.theme().radius)
.bg(cx.theme().group_box)
.text_color(cx.theme().group_box_foreground)
.children(rows),
)
.into_any_element()
}
@@ -408,11 +419,23 @@ impl GreeterPanel {
.collect();
Some(
GroupBox::new()
.id("follow-ups")
.title("Follow ups")
.fill()
.child(v_flex().gap_0().w_full().children(rows))
v_flex()
.gap_2()
.child(
div()
.text_color(cx.theme().muted_foreground)
.text_xs()
.font_semibold()
.child("Follow ups"),
)
.child(
v_flex()
.p_2()
.rounded(cx.theme().radius)
.bg(cx.theme().group_box)
.text_color(cx.theme().group_box_foreground)
.children(rows),
)
.into_any_element(),
)
}
+22 -23
View File
@@ -8,8 +8,8 @@ use gpui::{
};
use gpui_kit::assets::IconName;
use gpui_kit::base::StyledExt as _;
use gpui_kit::component::button::{Button, ButtonVariants};
use gpui_kit::component::menu::{ContextMenuExt as _, PopupMenuItem};
use gpui_kit::component::scroll::Scrollbar;
use gpui_kit::component::{ActiveTheme, Icon, Sizable, h_flex, v_flex};
use nip29::Group;
use nostr_sdk::prelude::*;
@@ -155,6 +155,16 @@ impl Sidebar {
.pl_0p5()
.gap_1()
.flex_shrink_1()
.child(
Button::new("sidebar-back")
.icon(IconName::ArrowLeft)
.tooltip("Back")
.ghost()
.small()
.on_click(cx.listener(|this, _event, _window, cx| {
this.hide_group(cx);
})),
)
.child(Avatar::new(picture).seed(seed.clone()).small())
.child(
v_flex()
@@ -184,28 +194,17 @@ impl Sidebar {
),
)
.child(
div()
.min_h_0()
.flex_1()
.child(
uniform_list(
"group-rows",
rows.len(),
cx.processor(move |this, range, _window, cx| {
this.render_group_rows(
range,
rows.as_slice(),
&member_group,
admin,
cx,
)
}),
)
.track_scroll(&self.group_scroll)
.h_full()
.px_2(),
div().min_h_0().flex_1().child(
uniform_list(
"group-rows",
rows.len(),
cx.processor(move |this, range, _window, cx| {
this.render_group_rows(range, rows.as_slice(), &member_group, admin, cx)
}),
)
.child(Scrollbar::vertical(&self.group_scroll)),
.h_full()
.px_2(),
),
)
.into_any_element()
}
@@ -292,7 +291,7 @@ impl Sidebar {
this.prefix(
Avatar::new(person.avatar())
.seed(person.avatar_seed())
.small(),
.xsmall(),
)
})
.when(!roles.is_empty(), |this| {
+124 -81
View File
@@ -2,18 +2,24 @@ use auto_update::AutoUpdater;
use gpui::prelude::FluentBuilder;
use gpui::{
AnyElement, App, Context, Div, Entity, InteractiveElement, IntoElement, ParentElement,
Stateful, Styled, div, px,
Stateful, Styled, Window, div, px,
};
use gpui_kit::assets::IconName;
use gpui_kit::component::button::{Button, ButtonVariants};
use gpui_kit::component::menu::DropdownMenu;
use gpui_kit::component::{Disableable, Selectable, Sizable, h_flex};
use gpui_kit::base::StyledExt as _;
use gpui_kit::component::button::{Button, ButtonVariant, ButtonVariants};
use gpui_kit::component::dialog::DialogButtonProps;
use gpui_kit::component::menu::{DropdownMenu, PopupMenuItem};
use gpui_kit::component::{
ActiveTheme, Disableable, Icon, Selectable, Sizable, WindowExt, h_flex, v_flex,
};
use nostr_sdk::prelude::*;
use state::NostrRegistry;
use ui::Avatar;
use super::Sidebar;
use crate::Command;
use crate::dock::TAB_BAR_HEIGHT;
use crate::nav::Nav;
impl Sidebar {
pub(super) fn render_user(
@@ -24,99 +30,114 @@ impl Sidebar {
let updater = AutoUpdater::try_global(cx);
let profile = self.directory.read(cx).person(current_user, cx);
let avatar = profile.avatar();
let name = profile.name();
h_flex()
.id("sidebar-user")
.w_full()
.h(TAB_BAR_HEIGHT)
.flex_shrink_0()
.items_center()
.gap_2()
.justify_between()
.px_2()
.when(cfg!(target_os = "macos"), |this| this.pl(px(80.)))
.child(
Button::new("current-user")
.child(
Avatar::new(avatar.clone())
.seed(profile.avatar_seed())
.small(),
)
.small()
.dropdown_caret(true)
.compact()
.text()
.dropdown_caret(true)
.min_w_0()
.flex_shrink_1()
.child(
h_flex()
.min_w_0()
.gap_1p5()
.child(Avatar::new(avatar).seed(profile.avatar_seed()).small())
.child(
div()
.min_w_0()
.truncate()
.text_xs()
.font_semibold()
.child(name),
),
)
.dropdown_menu(move |this, _window, _cx| {
this.menu_with_icon(
"Profile",
IconName::UserRound,
Box::new(Command::ShowProfile),
)
.menu_with_icon(
"Relays",
IconName::RadioTower,
Box::new(Command::ShowRelayList),
)
.separator()
.menu_with_icon(
"Settings",
IconName::Settings,
Box::new(Command::ShowSettings),
)
}),
)
.child(div().flex_1())
.when_some(updater, |this, updater| {
this.child(self.render_updater(updater, cx))
})
.child(
Button::new("sidebar-search")
.icon(IconName::Search)
.tooltip("Search")
.selected(self.search_open)
.ghost()
.small()
.on_click(cx.listener(|this, _event, window, cx| {
this.toggle_search(window, cx);
})),
)
.child(
Button::new("sidebar-new")
.icon(IconName::MessageSquarePlus)
.tooltip("New")
.ghost()
.small()
.dropdown_menu(|menu, _window, _cx| {
menu.label("Public Group")
.menu_with_icon(
"New Group",
IconName::Users,
Box::new(Command::NewGroup),
)
.menu_with_icon(
"Join Group",
IconName::DoorOpen,
Box::new(Command::JoinGroup),
)
this.menu("Profile", Box::new(Command::ShowProfile))
.menu("Relays", Box::new(Command::ShowRelayList))
.separator()
.menu_with_icon(
"New Chat",
IconName::MessageSquare,
Box::new(Command::NewChat),
.item(
PopupMenuItem::element(|_, cx| {
div().text_color(cx.theme().danger).child("Log out")
})
.on_click(|_, window, cx| {
open_logout_dialog(window, cx);
}),
)
}),
)
.when(self.group_open, |this| {
this.child(
Button::new("sidebar-back")
.icon(IconName::ArrowLeft)
.tooltip("Back")
.ghost()
.small()
.on_click(cx.listener(|this, _event, _window, cx| {
this.hide_group(cx);
})),
)
})
.child(
h_flex()
.when_some(updater, |this, updater| {
this.child(self.render_updater(updater, cx))
})
.child(
Button::new("sidebar-search")
.icon(IconName::Search)
.tooltip("Search")
.selected(self.search_open)
.ghost()
.small()
.flex_shrink_0()
.on_click(cx.listener(|this, _event, window, cx| {
this.toggle_search(window, cx);
})),
)
.child(
Button::new("sidebar-new")
.icon(IconName::MessageSquarePlus)
.tooltip("New")
.ghost()
.small()
.flex_shrink_0()
.dropdown_menu(|menu, _window, _cx| {
menu.label("Public Group")
.menu_with_icon(
"New Group",
IconName::Users,
Box::new(Command::NewGroup),
)
.menu_with_icon(
"Join Group",
IconName::DoorOpen,
Box::new(Command::JoinGroup),
)
.separator()
.menu_with_icon(
"New Chat",
IconName::MessageSquare,
Box::new(Command::NewChat),
)
}),
),
)
}
/// The bottom navigation, pinned below the list like signed's sidebar.
pub(super) fn render_footer(&self, _cx: &mut Context<Self>) -> Div {
v_flex()
.p_2()
.flex_shrink_0()
.gap_1()
.items_start()
.justify_start()
.child(
Nav::new("sidebar-settings")
.prefix(Icon::new(IconName::Settings).small())
.label("Settings")
.text_sm()
.on_click(|_, window, cx| {
window.dispatch_action(Box::new(Command::ShowSettings), cx)
}),
)
}
fn render_updater(&self, updater: Entity<AutoUpdater>, cx: &mut App) -> AnyElement {
@@ -153,3 +174,25 @@ impl Sidebar {
.into_any_element()
}
}
/// Confirm signing out, then drop the stored credential like signed's exit dialog.
fn open_logout_dialog(window: &mut Window, cx: &mut App) {
const DESCRIPTION: &str = "Are you sure you want to log out? You will need to re-enter your credentials to sign back in.";
window.open_alert_dialog(cx, |alert, _, _| {
alert
.title("Log Out?")
.description(DESCRIPTION)
.button_props(
DialogButtonProps::default()
.ok_text("Log out")
.ok_variant(ButtonVariant::Danger)
.show_cancel(true),
)
.on_ok(|_, _, cx| {
let nostr = NostrRegistry::global(cx);
nostr.update(cx, |nostr, cx| nostr.sign_out(cx));
true
})
});
}
+38 -33
View File
@@ -7,8 +7,7 @@ use gpui::{
};
use gpui_kit::assets::IconName;
use gpui_kit::base::StyledExt as _;
use gpui_kit::component::scroll::Scrollbar;
use gpui_kit::component::{ActiveTheme, Icon, h_flex, v_flex};
use gpui_kit::component::{ActiveTheme, Icon, Sizable, h_flex, v_flex};
use nip29::GroupsRegistry;
use settings::AppSettings;
@@ -44,12 +43,15 @@ impl Sidebar {
// Whether to show the create button for the contact picker
let create_button = contacts_open && !self.selected_contacts.is_empty();
let (list_id, scroll_handle, list_title) = if contacts_open {
("sidebar-contacts", &self.contacts_scroll, Some("Contacts"))
let (list_id, title) = if contacts_open {
("sidebar-contacts", Some(("Contacts", IconName::Book)))
} else if requests_open {
("sidebar-requests", &self.requests_scroll, Some("Requests"))
(
"sidebar-requests",
Some(("Requests", IconName::UserRoundPlus)),
)
} else {
("sidebar-list", &self.list_scroll, None)
("sidebar-list", None)
};
let request_count = ChatRegistry::global(cx)
@@ -61,13 +63,15 @@ impl Sidebar {
.flex_1()
.min_h_0()
.relative()
.gap_2()
.child(
v_flex()
.px_2()
.gap_1()
.items_start()
.justify_start()
.child(
Nav::new("browse")
.prefix(Icon::new(IconName::Compass))
.prefix(Icon::new(IconName::Compass).small())
.label("Browse")
.text_sm()
.on_click(|_, window, cx| {
@@ -76,7 +80,7 @@ impl Sidebar {
)
.child(
Nav::new("reqs")
.prefix(Icon::new(IconName::UserRoundPlus))
.prefix(Icon::new(IconName::UserRoundPlus).small())
.label("Requests")
.text_sm()
.when(request_count > 0, |this| {
@@ -117,7 +121,7 @@ impl Sidebar {
)
.child(
Nav::new("contacts")
.prefix(Icon::new(IconName::Book))
.prefix(Icon::new(IconName::Book).small())
.label("Contacts")
.text_sm()
.when(contacts_open, |this| this.bg(cx.theme().list_active))
@@ -126,33 +130,34 @@ impl Sidebar {
})),
),
)
.when_some(list_title, |this, list_title| {
.when_some(title, |this, (title, title_icon)| {
this.child(
div()
.px_4()
.text_xs()
.font_semibold()
.text_color(cx.theme().muted_foreground)
.child(list_title),
h_flex()
.h_10()
.w_full()
.flex_shrink_0()
.items_center()
.child(
h_flex()
.px_2()
.gap_2()
.text_color(cx.theme().muted_foreground)
.child(Icon::new(title_icon).small())
.child(div().text_xs().font_semibold().child(title)),
),
)
})
.child(
div()
.min_h_0()
.flex_1()
.child(
uniform_list(
list_id,
rows.len(),
cx.processor(move |this, range, _window, cx| {
this.render_rows(range, rows.as_slice(), cx)
}),
)
.track_scroll(scroll_handle)
.h_full()
.px_2(),
)
.child(Scrollbar::vertical(scroll_handle)),
uniform_list(
list_id,
rows.len(),
cx.processor(move |this, range, _window, cx| {
this.render_rows(range, rows.as_slice(), cx)
}),
)
.min_h_0()
.flex_1()
.px_2(),
)
.when(create_button, |this| {
this.child(self.render_create_overlay(cx))
+45 -52
View File
@@ -7,7 +7,7 @@ use gpui::prelude::FluentBuilder;
use gpui::{
App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement,
IntoElement, ObjectFit, ParentElement, Render, SharedString, Styled, StyledImage, Subscription,
Task, UniformListScrollHandle, WeakEntity, Window, div, img, retain_all,
Task, WeakEntity, Window, div, img, retain_all,
};
use gpui_kit::base::StyledExt as _;
use gpui_kit::component::dock::{DockArea, Panel, PanelEvent};
@@ -38,7 +38,6 @@ use search::SearchResult;
pub struct Sidebar {
focus_handle: FocusHandle,
list_scroll: UniformListScrollHandle,
dock: WeakEntity<DockArea>,
/// Random background shown behind the signed-out screen.
banner: SharedString,
@@ -47,8 +46,6 @@ pub struct Sidebar {
group: Option<WeakEntity<Group>>,
/// Whether the sidebar is showing the group's information instead of its list.
group_open: bool,
/// Scroll state of the group's member list.
group_scroll: UniformListScrollHandle,
/// Whether the group view's admins section is expanded.
admins_open: bool,
/// Whether the group view's members section is expanded.
@@ -69,13 +66,10 @@ pub struct Sidebar {
directory: Entity<Directory>,
/// Contacts selected in the contact picker.
selected_contacts: HashSet<PublicKey>,
/// Scroll state of the contact picker's list.
contacts_scroll: UniformListScrollHandle,
/// Whether the main list is showing chat requests instead of chats and groups.
requests_open: bool,
new_requests: bool,
requests_scroll: UniformListScrollHandle,
/// Whether the search bar is shown below the header.
search_open: bool,
@@ -93,8 +87,6 @@ pub struct Sidebar {
global_results: Option<Vec<PublicKey>>,
/// The async global profile search.
global_task: Option<Task<()>>,
/// Scroll state of the search results list.
search_scroll: UniformListScrollHandle,
tasks: SmallVec<[Task<Result<(), Error>>; 1]>,
_subscriptions: SmallVec<[Subscription; 4]>,
@@ -208,12 +200,10 @@ impl Sidebar {
Self {
focus_handle: cx.focus_handle(),
list_scroll: UniformListScrollHandle::new(),
dock,
banner: onboarding::pick_banner(),
group: None,
group_open: false,
group_scroll: UniformListScrollHandle::new(),
admins_open: true,
members_open: true,
pins_open: true,
@@ -223,9 +213,7 @@ impl Sidebar {
contacts: None,
directory: Directory::new(cx),
selected_contacts: HashSet::new(),
contacts_scroll: UniformListScrollHandle::new(),
requests_open: false,
requests_scroll: UniformListScrollHandle::new(),
new_requests: false,
search_open: false,
global_open: false,
@@ -235,7 +223,6 @@ impl Sidebar {
global_finding: false,
global_results: None,
global_task: None,
search_scroll: UniformListScrollHandle::new(),
tasks: smallvec![],
_subscriptions: subscriptions,
}
@@ -310,45 +297,51 @@ impl Render for Sidebar {
.image_cache(retain_all("sidebar"))
.size_full()
.relative()
.gap_2()
.justify_between()
.bg(cx.theme().sidebar)
.when_some(current_user.as_ref(), |this, current_user| {
this.child(title_bar_drag_handlers(
self.render_user(current_user, cx),
window,
cx,
))
})
.when(self.search_open, |this| this.child(self.render_find(cx)))
.when(!logged_in, |this| {
this.relative()
.child(title_bar_drag_handlers(
div()
.id("onboarding-drag")
.absolute()
.top_0()
.left_0()
.h(TAB_BAR_HEIGHT)
.w_full(),
window,
cx,
))
.child(
div().absolute().inset_0().child(
img(self.banner.clone())
.size_full()
.object_fit(ObjectFit::Cover),
),
)
.child(onboarding::render_signed_out(window, cx))
})
.map(|this| match group {
Some(group) if self.group_open && !searching => {
this.child(self.render_group(group, cx))
}
_ if searching => this.child(self.render_results(cx)),
_ => this.child(self.render_list(cx)),
})
.text_color(cx.theme().sidebar_foreground)
.child(
v_flex()
.flex_1()
.min_h_0()
.when_some(current_user.as_ref(), |this, current_user| {
this.child(title_bar_drag_handlers(
self.render_user(current_user, cx),
window,
cx,
))
})
.when(self.search_open, |this| this.child(self.render_find(cx)))
.when(!logged_in, |this| {
this.child(title_bar_drag_handlers(
div()
.id("onboarding-drag")
.absolute()
.top_0()
.left_0()
.h(TAB_BAR_HEIGHT)
.w_full(),
window,
cx,
))
.child(
div().absolute().inset_0().child(
img(self.banner.clone())
.size_full()
.object_fit(ObjectFit::Cover),
),
)
.child(onboarding::render_signed_out(window, cx))
})
.map(|this| match group {
Some(group) if self.group_open && !searching => {
this.child(self.render_group(group, cx))
}
_ if searching => this.child(self.render_results(cx)),
_ => this.child(self.render_list(cx)),
}),
)
.when(logged_in, |this| this.child(self.render_footer(cx)))
.when(loading && logged_in, |this| {
this.child(
div()
+7 -18
View File
@@ -12,7 +12,7 @@ use gpui_kit::base::StyledExt as _;
use gpui_kit::component::dialog::DialogButtonProps;
use gpui_kit::component::dock::{ClosePanel, DockArea, DockPlacement, PanelHandle};
use gpui_kit::component::menu::{ContextMenuExt as _, PopupMenuItem};
use gpui_kit::component::{ActiveTheme, Icon, Selectable, Sizable, WindowExt, h_flex, v_flex};
use gpui_kit::component::{ActiveTheme, Icon, Selectable, Sizable, WindowExt, h_flex};
use nip29::Group;
use nostr_sdk::prelude::*;
use settings::AppSettings;
@@ -151,24 +151,13 @@ impl Sidebar {
.text_color(cx.theme().muted_foreground)
.into_any_element()
}
SidebarRow::Empty(text) => v_flex()
.h_10()
SidebarRow::Empty(text) => h_flex()
.h_8()
.w_full()
.px_1p5()
.items_center()
.child(
h_flex()
.h_9()
.w_full()
.justify_center()
.border_1()
.border_dashed()
.border_color(cx.theme().border)
.rounded(cx.theme().radius)
.text_xs()
.text_color(cx.theme().muted_foreground)
.child(*text),
)
.px_2()
.text_xs()
.text_color(cx.theme().muted_foreground)
.child(*text)
.into_any_element(),
SidebarRow::Room { room } => {
let id = room.read(cx).id;
+14 -20
View File
@@ -1,8 +1,3 @@
use gpui_kit::assets::IconName;
use gpui_kit::component::button::{Button, ButtonVariants};
use gpui_kit::component::input::Input;
use gpui_kit::component::scroll::Scrollbar;
use gpui_kit::component::{ActiveTheme, Selectable, Sizable, h_flex, v_flex};
use std::collections::HashSet;
use std::rc::Rc;
@@ -11,6 +6,10 @@ use gpui::{
AnyElement, App, Context, Entity, IntoElement, ParentElement, SharedString, Styled, Task,
Window, div, uniform_list,
};
use gpui_kit::assets::IconName;
use gpui_kit::component::button::{Button, ButtonVariants};
use gpui_kit::component::input::Input;
use gpui_kit::component::{ActiveTheme, Selectable, Sizable, h_flex, v_flex};
use nip29::{Group, GroupsRegistry};
use nostr_sdk::prelude::*;
use state::{FIND_LIMIT, NostrRegistry};
@@ -245,22 +244,17 @@ impl Sidebar {
.min_h_0()
.relative()
.child(
div()
.min_h_0()
.flex_1()
.child(
uniform_list(
"sidebar-search",
rows.len(),
cx.processor(move |this, range, _window, cx| {
this.render_rows(range, rows.as_slice(), cx)
}),
)
.track_scroll(&self.search_scroll)
.h_full()
.px_2(),
div().min_h_0().flex_1().child(
uniform_list(
"sidebar-search",
rows.len(),
cx.processor(move |this, range, _window, cx| {
this.render_rows(range, rows.as_slice(), cx)
}),
)
.child(Scrollbar::vertical(&self.search_scroll)),
.h_full()
.px_2(),
),
)
.into_any_element()
}