@@ -3,22 +3,21 @@ use std::rc::Rc;
|
||||
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
AnyElement, ClipboardItem, Context, Entity, IntoElement, ParentElement, SharedString, Styled,
|
||||
WeakEntity, div, px, relative, uniform_list,
|
||||
AnyElement, ClipboardItem, Context, Entity, InteractiveElement as _, IntoElement,
|
||||
ParentElement, SharedString, Styled, WeakEntity, div, px, relative, uniform_list,
|
||||
};
|
||||
use gpui_kit::base::StyledExt as _;
|
||||
use gpui_kit::component::menu::{ContextMenuExt as _, PopupMenuItem};
|
||||
use gpui_kit::component::{ActiveTheme, Icon, Sizable, h_flex, v_flex};
|
||||
use nip29::Group;
|
||||
use nostr_sdk::prelude::*;
|
||||
use settings::AppSettings;
|
||||
use state::NostrRegistry;
|
||||
use theme::{ActiveTheme, TextStyle, Typeset as _};
|
||||
use ui::avatar::Avatar;
|
||||
use ui::menu::{DropdownMenu, PopupMenuItem};
|
||||
use ui::nav::Nav;
|
||||
use ui::scroll::Scrollbar;
|
||||
use ui::{Icon, IconName, Sizable, StyledExt, h_flex, v_flex};
|
||||
use ui::{Avatar, IconName};
|
||||
|
||||
use super::Sidebar;
|
||||
use crate::dialogs::profile;
|
||||
use crate::nav::Nav;
|
||||
|
||||
/// A collapsible section of the group view.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
@@ -143,34 +142,34 @@ impl Sidebar {
|
||||
.flex_1()
|
||||
.min_h_0()
|
||||
.w_full()
|
||||
.gap(px(theme::SPACE))
|
||||
.gap_2()
|
||||
.child(
|
||||
h_flex()
|
||||
.px_2()
|
||||
.px_4()
|
||||
.flex_shrink_0()
|
||||
.justify_between()
|
||||
.child(
|
||||
h_flex()
|
||||
.pl_0p5()
|
||||
.gap_1()
|
||||
.flex_shrink_1()
|
||||
.child(Avatar::from_source(picture).seed(seed).small())
|
||||
.child(Avatar::new(picture).seed(seed.clone()).small())
|
||||
.child(
|
||||
v_flex()
|
||||
.flex_1()
|
||||
.min_w_0()
|
||||
.child(
|
||||
div()
|
||||
.text_style(TextStyle::Caption)
|
||||
.text_xs()
|
||||
.truncate()
|
||||
.line_height(relative(1.2))
|
||||
.line_height(relative(1.125))
|
||||
.font_semibold()
|
||||
.child(name),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.text_style(TextStyle::Caption2)
|
||||
.text_color(cx.theme().text_muted)
|
||||
.text_size(px(8.0))
|
||||
.font_medium()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child(total_members),
|
||||
),
|
||||
),
|
||||
@@ -182,28 +181,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()
|
||||
}
|
||||
@@ -242,10 +230,14 @@ impl Sidebar {
|
||||
|
||||
Nav::new(label)
|
||||
.label(label)
|
||||
.suffix(Icon::new(icon).small().text_color(cx.theme().text_faint))
|
||||
.text_style(TextStyle::Caption)
|
||||
.suffix(
|
||||
Icon::new(icon)
|
||||
.small()
|
||||
.text_color(cx.theme().muted_foreground),
|
||||
)
|
||||
.text_xs()
|
||||
.font_semibold()
|
||||
.text_color(cx.theme().text_faint)
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.on_click(cx.listener(move |this, _event, _window, cx| {
|
||||
match section {
|
||||
GroupSection::Admins => this.admins_open = !this.admins_open,
|
||||
@@ -277,29 +269,32 @@ impl Sidebar {
|
||||
.current_user()
|
||||
.is_some_and(|me| me == public_key);
|
||||
|
||||
Nav::new(public_key.to_hex())
|
||||
let nav = Nav::new(public_key.to_hex())
|
||||
.label(person.name())
|
||||
.text_style(TextStyle::Callout)
|
||||
.font_medium()
|
||||
.text_sm()
|
||||
.clickable(true)
|
||||
.when(!hide_avatar, |this| {
|
||||
this.prefix(
|
||||
Avatar::new(person.avatar())
|
||||
.seed(person.avatar_seed())
|
||||
.small()
|
||||
.flex_shrink_0(),
|
||||
.small(),
|
||||
)
|
||||
})
|
||||
.when(!roles.is_empty(), |this| {
|
||||
this.suffix(
|
||||
div()
|
||||
.font_normal()
|
||||
.truncate()
|
||||
.text_style(TextStyle::Caption)
|
||||
.text_color(cx.theme().text_faint)
|
||||
.text_xs()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child(SharedString::from(roles.join(", "))),
|
||||
)
|
||||
})
|
||||
.dropdown_menu(move |menu, _window, _cx| {
|
||||
});
|
||||
let row = div()
|
||||
.id(public_key.to_hex())
|
||||
.w_full()
|
||||
.child(nav)
|
||||
.context_menu(move |menu, _window, _cx| {
|
||||
let menu = menu
|
||||
.item(
|
||||
PopupMenuItem::new("View profile").on_click(move |_, window, cx| {
|
||||
@@ -338,7 +333,8 @@ impl Sidebar {
|
||||
nip29_ui::remove_member(group.clone(), public_key, window, cx);
|
||||
}
|
||||
}))
|
||||
})
|
||||
.into_any_element()
|
||||
});
|
||||
|
||||
row.into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,17 +2,22 @@ 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::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 theme::TABBAR_HEIGHT;
|
||||
use ui::avatar::Avatar;
|
||||
use ui::button::{Button, ButtonVariants};
|
||||
use ui::menu::DropdownMenu;
|
||||
use ui::{Disableable, IconName, Selectable, Sizable, TRAFFIC_LIGHT_PADDING, h_flex};
|
||||
use state::NostrRegistry;
|
||||
use ui::{Avatar, IconName};
|
||||
|
||||
use super::Sidebar;
|
||||
use crate::Command;
|
||||
use crate::dock::TAB_BAR_HEIGHT;
|
||||
use crate::nav::Nav;
|
||||
|
||||
impl Sidebar {
|
||||
pub(super) fn render_user(
|
||||
@@ -23,30 +28,22 @@ impl Sidebar {
|
||||
let updater = AutoUpdater::try_global(cx);
|
||||
let profile = self.directory.read(cx).person(current_user, cx);
|
||||
let avatar = profile.avatar();
|
||||
let avatar_seed = profile.avatar_seed();
|
||||
|
||||
h_flex()
|
||||
.id("sidebar-user")
|
||||
.w_full()
|
||||
.h(TABBAR_HEIGHT)
|
||||
.h(TAB_BAR_HEIGHT)
|
||||
.flex_shrink_0()
|
||||
.items_center()
|
||||
.gap(px(theme::SPACE))
|
||||
.justify_between()
|
||||
.px_2()
|
||||
.when(cfg!(target_os = "macos"), |this| {
|
||||
this.pl(px(TRAFFIC_LIGHT_PADDING))
|
||||
})
|
||||
.when(cfg!(target_os = "macos"), |this| this.pl(px(80.)))
|
||||
.child(
|
||||
Button::new("current-user")
|
||||
.child(
|
||||
Avatar::new(avatar.clone())
|
||||
.seed(avatar_seed.clone())
|
||||
.small(),
|
||||
)
|
||||
.small()
|
||||
.caret()
|
||||
.compact()
|
||||
.transparent()
|
||||
.text()
|
||||
.dropdown_caret(true)
|
||||
.min_w_0()
|
||||
.flex_shrink_1()
|
||||
.child(Avatar::new(avatar).seed(profile.avatar_seed()).small())
|
||||
.dropdown_menu(move |this, _window, _cx| {
|
||||
this.menu_with_icon(
|
||||
"Profile",
|
||||
@@ -55,66 +52,80 @@ impl Sidebar {
|
||||
)
|
||||
.menu_with_icon("Relays", IconName::Relay, Box::new(Command::ShowRelayList))
|
||||
.separator()
|
||||
.menu_with_icon(
|
||||
"Settings",
|
||||
IconName::Settings,
|
||||
Box::new(Command::ShowSettings),
|
||||
.item(
|
||||
PopupMenuItem::element(|_, cx| {
|
||||
div().text_color(cx.theme().danger).child("Log out")
|
||||
})
|
||||
.on_click(|_, window, cx| {
|
||||
open_logout_dialog(window, cx);
|
||||
}),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.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);
|
||||
})),
|
||||
h_flex()
|
||||
.gap_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::PlusChat)
|
||||
.tooltip("New")
|
||||
.ghost()
|
||||
.small()
|
||||
.dropdown_menu(|menu, _window, _cx| {
|
||||
menu.label("Public Group")
|
||||
.menu("New Group", Box::new(Command::NewGroup))
|
||||
.menu("Join Group", Box::new(Command::JoinGroup))
|
||||
.separator()
|
||||
.menu("New Chat", Box::new(Command::NewChat))
|
||||
}),
|
||||
)
|
||||
.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);
|
||||
})),
|
||||
)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
/// 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(
|
||||
Button::new("sidebar-new")
|
||||
.icon(IconName::PlusChat)
|
||||
.tooltip("New")
|
||||
.ghost()
|
||||
.small()
|
||||
.dropdown_menu(|menu, _window, _cx| {
|
||||
menu.label("Public Group")
|
||||
.menu_with_icon(
|
||||
"New Group",
|
||||
IconName::Group,
|
||||
Box::new(Command::NewGroup),
|
||||
)
|
||||
.menu_with_icon(
|
||||
"Join Group",
|
||||
IconName::Door,
|
||||
Box::new(Command::JoinGroup),
|
||||
)
|
||||
.separator()
|
||||
.menu_with_icon(
|
||||
"New Chat",
|
||||
IconName::Message,
|
||||
Box::new(Command::NewChat),
|
||||
)
|
||||
Nav::new("sidebar-settings")
|
||||
.prefix(Icon::new(IconName::Settings).small())
|
||||
.center_prefix(true)
|
||||
.label("Settings")
|
||||
.text_sm()
|
||||
.on_click(|_, window, cx| {
|
||||
window.dispatch_action(Box::new(Command::ShowSettings), 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);
|
||||
})),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn render_updater(&self, updater: Entity<AutoUpdater>, cx: &mut App) -> AnyElement {
|
||||
@@ -123,7 +134,7 @@ impl Sidebar {
|
||||
let staged = updater.read(cx).staged();
|
||||
|
||||
h_flex()
|
||||
.gap(px(theme::SPACE))
|
||||
.gap_2()
|
||||
.when(!up_to_date, |this| {
|
||||
this.child(
|
||||
Button::new("update")
|
||||
@@ -151,3 +162,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
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4,13 +4,13 @@ use gpui::{
|
||||
AnyElement, App, AppContext, Context, IntoElement, ParentElement, SharedString, Styled, Task,
|
||||
Window, div,
|
||||
};
|
||||
use gpui_kit::base::StyledExt as _;
|
||||
use gpui_kit::component::button::{Button, ButtonVariants};
|
||||
use gpui_kit::component::dock::{DockPlacement, PanelHandle};
|
||||
use gpui_kit::component::notification::Notification;
|
||||
use gpui_kit::component::{ActiveTheme, WindowExt, h_flex, v_flex};
|
||||
use nostr_sdk::prelude::*;
|
||||
use state::NostrRegistry;
|
||||
use theme::{ActiveTheme, TextStyle, Typeset as _};
|
||||
use ui::button::{Button, ButtonVariants};
|
||||
use ui::dock::{DockPlacement, PanelHandle};
|
||||
use ui::notification::Notification;
|
||||
use ui::{StyledExt, WindowExtension, h_flex, v_flex};
|
||||
|
||||
use super::Sidebar;
|
||||
use super::rows::SidebarRow;
|
||||
@@ -127,7 +127,7 @@ impl Sidebar {
|
||||
})?;
|
||||
|
||||
cx.update(|window, cx| {
|
||||
ui::dock::add_panel_to(
|
||||
crate::dock::add_panel_to(
|
||||
&dock,
|
||||
PanelHandle::new(chat_ui::init(room.downgrade(), window, cx)),
|
||||
DockPlacement::Center,
|
||||
@@ -193,9 +193,8 @@ impl Sidebar {
|
||||
.h_20()
|
||||
.w_full()
|
||||
.px_4()
|
||||
.border_t_1()
|
||||
.border_color(cx.theme().border)
|
||||
.bg(cx.theme().bg)
|
||||
.bg(cx.theme().sidebar)
|
||||
.text_color(cx.theme().sidebar_foreground)
|
||||
.child(
|
||||
Button::new("create-chat")
|
||||
.label(if self.selected_contacts.len() > 1 {
|
||||
@@ -211,9 +210,11 @@ impl Sidebar {
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.text_style(TextStyle::Caption)
|
||||
.text_color(cx.theme().text_muted)
|
||||
.child("Selected")
|
||||
.text_xs()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.text_center()
|
||||
.justify_center()
|
||||
.child("Selected:")
|
||||
.child(
|
||||
div()
|
||||
.font_semibold()
|
||||
|
||||
@@ -3,19 +3,18 @@ use std::rc::Rc;
|
||||
use chat::{ChatRegistry, RoomKind};
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
AnyElement, App, Context, IntoElement, ParentElement, SharedString, Styled, div, px,
|
||||
uniform_list,
|
||||
AnyElement, App, Context, IntoElement, ParentElement, SharedString, Styled, div, uniform_list,
|
||||
};
|
||||
use gpui_kit::base::StyledExt as _;
|
||||
use gpui_kit::component::{ActiveTheme, Icon, Sizable, h_flex, v_flex};
|
||||
use nip29::GroupsRegistry;
|
||||
use settings::AppSettings;
|
||||
use theme::{ActiveTheme, TextStyle, Typeset as _, control_radius};
|
||||
use ui::nav_item::NavItem;
|
||||
use ui::scroll::Scrollbar;
|
||||
use ui::{Icon, IconName, Sizable, StyledExt, h_flex, v_flex};
|
||||
use ui::IconName;
|
||||
|
||||
use super::Sidebar;
|
||||
use super::rows::SidebarRow;
|
||||
use crate::Command;
|
||||
use crate::nav::Nav;
|
||||
|
||||
/// A collapsible section of the sidebar's main list.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
@@ -44,12 +43,12 @@ 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"))
|
||||
} else if requests_open {
|
||||
("sidebar-requests", &self.requests_scroll, Some("Requests"))
|
||||
("sidebar-requests", Some("Requests"))
|
||||
} else {
|
||||
("sidebar-list", &self.list_scroll, None)
|
||||
("sidebar-list", None)
|
||||
};
|
||||
|
||||
let request_count = ChatRegistry::global(cx)
|
||||
@@ -61,86 +60,96 @@ impl Sidebar {
|
||||
.flex_1()
|
||||
.min_h_0()
|
||||
.relative()
|
||||
.gap(px(theme::SPACE))
|
||||
.child(
|
||||
v_flex()
|
||||
.px_2()
|
||||
.gap_1()
|
||||
.items_start()
|
||||
.justify_start()
|
||||
.child(
|
||||
NavItem::new("browse", "Browse", Icon::new(IconName::Compass).small())
|
||||
Nav::new("browse")
|
||||
.prefix(Icon::new(IconName::Compass).small())
|
||||
.label("Browse")
|
||||
.text_sm()
|
||||
.on_click(|_, window, cx| {
|
||||
window.dispatch_action(Box::new(Command::ShowBrowse), cx)
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
NavItem::new("reqs", "Requests", Icon::new(IconName::Invite).small())
|
||||
Nav::new("reqs")
|
||||
.prefix(Icon::new(IconName::Invite).small())
|
||||
.label("Requests")
|
||||
.text_sm()
|
||||
.when(request_count > 0, |this| {
|
||||
this.suffix(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.items_center()
|
||||
.child(
|
||||
h_flex()
|
||||
.py_0p5()
|
||||
.px_1()
|
||||
.min_w_6()
|
||||
.justify_center()
|
||||
.text_style(TextStyle::Caption2)
|
||||
.text_color(cx.theme().text_muted)
|
||||
.text_xs()
|
||||
.font_medium()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.text_center()
|
||||
.rounded(px(control_radius()))
|
||||
.bg(cx.theme().surface_raised)
|
||||
.rounded(cx.theme().radius)
|
||||
.bg(cx.theme().secondary)
|
||||
.child(SharedString::from(
|
||||
request_count.to_string(),
|
||||
)),
|
||||
)
|
||||
.when(self.new_requests && !requests_open, |this| {
|
||||
this.child(
|
||||
div().size_1().rounded_full().bg(cx.theme().accent),
|
||||
div()
|
||||
.size_1()
|
||||
.rounded_full()
|
||||
.bg(cx.theme().primary),
|
||||
)
|
||||
}),
|
||||
)
|
||||
})
|
||||
.when(requests_open, |this| this.bg(cx.theme().element_active))
|
||||
.when(requests_open, |this| this.bg(cx.theme().list_active))
|
||||
.on_click(cx.listener(|this, _event, _window, cx| {
|
||||
this.toggle_requests(cx);
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
NavItem::new("contacts", "Contacts", Icon::new(IconName::Book).small())
|
||||
.when(contacts_open, |this| this.bg(cx.theme().element_active))
|
||||
Nav::new("contacts")
|
||||
.prefix(Icon::new(IconName::Book).small())
|
||||
.label("Contacts")
|
||||
.text_sm()
|
||||
.when(contacts_open, |this| this.bg(cx.theme().list_active))
|
||||
.on_click(cx.listener(|this, _event, window, cx| {
|
||||
this.toggle_contacts(window, cx);
|
||||
})),
|
||||
),
|
||||
)
|
||||
.when_some(list_title, |this, list_title| {
|
||||
.when_some(title, |this, title| {
|
||||
this.child(
|
||||
div()
|
||||
h_flex()
|
||||
.px_4()
|
||||
.text_style(TextStyle::Caption)
|
||||
.h_10()
|
||||
.w_full()
|
||||
.flex_shrink_0()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.text_xs()
|
||||
.font_semibold()
|
||||
.text_color(cx.theme().text_muted)
|
||||
.child(list_title),
|
||||
.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))
|
||||
|
||||
+376
-376
@@ -1,376 +1,376 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use anyhow::Error;
|
||||
use chat::{ChatEvent, ChatRegistry};
|
||||
use common::DebouncedDelay;
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable,
|
||||
InteractiveElement, IntoElement, ObjectFit, ParentElement, Render, SharedString, Styled,
|
||||
StyledImage, Subscription, Task, UniformListScrollHandle, WeakEntity, Window, div, img, px,
|
||||
retain_all,
|
||||
};
|
||||
use instant::Duration;
|
||||
use nip29::{Group, GroupKey, GroupsEvent, GroupsRegistry};
|
||||
use nostr_sdk::prelude::*;
|
||||
use person::{Directory, PersonRegistry};
|
||||
use smallvec::{SmallVec, smallvec};
|
||||
use state::{FIND_DELAY, NostrRegistry, StateEvent};
|
||||
use theme::{ActiveTheme, TABBAR_HEIGHT, TextStyle, Typeset as _};
|
||||
use ui::dock::{DockArea, Panel, PanelEvent};
|
||||
use ui::indicator::Indicator;
|
||||
use ui::input::{InputEvent, InputState};
|
||||
use ui::notification::Notification;
|
||||
use ui::{Sizable, StyledExt, WindowExtension, h_flex, title_bar_drag_handlers, v_flex};
|
||||
|
||||
mod group_view;
|
||||
mod header;
|
||||
mod inbox;
|
||||
mod list;
|
||||
mod onboarding;
|
||||
mod rows;
|
||||
mod search;
|
||||
|
||||
pub(crate) use rows::open_room;
|
||||
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,
|
||||
|
||||
/// The group whose information the sidebar is browsing, if any.
|
||||
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.
|
||||
members_open: bool,
|
||||
|
||||
/// Whether the main list's pinned section is expanded.
|
||||
pins_open: bool,
|
||||
/// Whether the user expanded or collapsed the main list's groups section.
|
||||
groups_open: Option<bool>,
|
||||
/// Same as `groups_open` for the chats section.
|
||||
chats_open: Option<bool>,
|
||||
|
||||
/// Whether the main list is showing contacts to start a chat with.
|
||||
contacts_open: bool,
|
||||
/// The signed-in user's contacts, loaded when the contact picker opens.
|
||||
contacts: Option<Vec<PublicKey>>,
|
||||
/// Person directory for contacts, search hits, and group members
|
||||
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,
|
||||
/// Whether searches also cover global (relay) profiles.
|
||||
global_open: bool,
|
||||
/// Debounced delay for the global search while typing.
|
||||
find_debouncer: DebouncedDelay<Self>,
|
||||
/// Search input state.
|
||||
find_input: Entity<InputState>,
|
||||
/// Search results: rooms and groups by name, then messages by content.
|
||||
results: Vec<SearchResult>,
|
||||
/// Whether a global (relay) search is in progress.
|
||||
global_finding: bool,
|
||||
/// Profiles found by the last global search, if one was triggered.
|
||||
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]>,
|
||||
}
|
||||
|
||||
impl Sidebar {
|
||||
pub fn new(window: &mut Window, dock: WeakEntity<DockArea>, cx: &mut Context<Self>) -> Self {
|
||||
let chat = ChatRegistry::global(cx);
|
||||
let groups = GroupsRegistry::global(cx);
|
||||
let nostr = NostrRegistry::global(cx);
|
||||
let wot = PersonRegistry::global(cx).read(cx).wot();
|
||||
|
||||
let find_input = cx.new(|cx| {
|
||||
InputState::new(window, cx)
|
||||
.placeholder("Type to search")
|
||||
.clean_on_escape()
|
||||
});
|
||||
|
||||
let mut subscriptions = smallvec![];
|
||||
|
||||
subscriptions.push(
|
||||
cx.subscribe_in(&chat, window, move |this, _s, event, _window, cx| {
|
||||
if event == &ChatEvent::Ping {
|
||||
this.new_requests = true;
|
||||
cx.notify();
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
subscriptions.push(
|
||||
cx.subscribe_in(&wot, window, |_this, _wot, _event, _window, cx| {
|
||||
cx.notify();
|
||||
}),
|
||||
);
|
||||
|
||||
subscriptions.push(
|
||||
cx.subscribe_in(&groups, window, |_this, _, event, window, cx| {
|
||||
match event {
|
||||
GroupsEvent::Updated => {
|
||||
// TODO: find a better way?
|
||||
cx.notify();
|
||||
}
|
||||
GroupsEvent::Error(error) => {
|
||||
window.push_notification(Notification::error(error.clone()), cx);
|
||||
}
|
||||
GroupsEvent::Created {
|
||||
warning: Some(warning),
|
||||
..
|
||||
} => {
|
||||
window.push_notification(Notification::warning(warning.clone()), cx);
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
subscriptions.push(
|
||||
cx.subscribe_in(
|
||||
&nostr,
|
||||
window,
|
||||
|this, state, event, window, cx| match event {
|
||||
StateEvent::NoSigner => {
|
||||
this.banner = onboarding::pick_banner();
|
||||
if this.search_open {
|
||||
this.toggle_search(window, cx);
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
StateEvent::SignerChanged => {
|
||||
if let Some(current_user) = state.read(cx).current_user() {
|
||||
this.directory.update(cx, |directory, cx| {
|
||||
directory.track([current_user], cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
subscriptions.push(cx.subscribe_in(
|
||||
&find_input,
|
||||
window,
|
||||
|this, state, event, window, cx| match event {
|
||||
InputEvent::PressEnter { .. } => {
|
||||
this.search(cx);
|
||||
|
||||
if this.global_open {
|
||||
this.global_search(cx);
|
||||
}
|
||||
}
|
||||
InputEvent::Change => {
|
||||
if state.read(cx).value().is_empty() {
|
||||
this.clear_results(cx);
|
||||
} else {
|
||||
this.search(cx);
|
||||
|
||||
if this.global_open {
|
||||
this.find_debouncer.fire_new(
|
||||
Duration::from_millis(FIND_DELAY),
|
||||
window,
|
||||
cx,
|
||||
|this, window, cx| this.debounced_global(window, cx),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
));
|
||||
|
||||
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,
|
||||
groups_open: None,
|
||||
chats_open: None,
|
||||
contacts_open: false,
|
||||
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,
|
||||
find_debouncer: DebouncedDelay::new(),
|
||||
find_input,
|
||||
results: Vec::new(),
|
||||
global_finding: false,
|
||||
global_results: None,
|
||||
global_task: None,
|
||||
search_scroll: UniformListScrollHandle::new(),
|
||||
tasks: smallvec![],
|
||||
_subscriptions: subscriptions,
|
||||
}
|
||||
}
|
||||
|
||||
/// Show the group view without toggling it closed.
|
||||
pub(crate) fn show_group_meta(&mut self, key: &GroupKey, cx: &mut Context<Self>) {
|
||||
let groups = GroupsRegistry::global(cx);
|
||||
|
||||
let Some(group) = groups.read(cx).group(key, cx) else {
|
||||
return;
|
||||
};
|
||||
|
||||
self.show_group(&group, cx);
|
||||
}
|
||||
|
||||
/// Hide the group view if the sidebar is showing the given group.
|
||||
pub(crate) fn hide_group_meta(&mut self, key: &GroupKey, cx: &mut Context<Self>) {
|
||||
let showing = self
|
||||
.group
|
||||
.as_ref()
|
||||
.and_then(|group| group.upgrade())
|
||||
.is_some_and(|group| group.read(cx).key() == key);
|
||||
|
||||
if showing {
|
||||
self.hide_group(cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Panel for Sidebar {
|
||||
fn panel_id(&self) -> SharedString {
|
||||
"Sidebar".into()
|
||||
}
|
||||
|
||||
fn title(&self, _cx: &App) -> AnyElement {
|
||||
SharedString::from("Sidebar").into_any_element()
|
||||
}
|
||||
|
||||
fn closable(&self, _cx: &App) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn zoomable(&self, _cx: &App) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<PanelEvent> for Sidebar {}
|
||||
|
||||
impl Focusable for Sidebar {
|
||||
fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for Sidebar {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let nostr = NostrRegistry::global(cx);
|
||||
let current_user = nostr.read(cx).current_user();
|
||||
let logged_in = current_user.is_some();
|
||||
|
||||
let chat = ChatRegistry::global(cx);
|
||||
let loading = chat.read(cx).loading();
|
||||
|
||||
let group = self.group.clone().and_then(|group| group.upgrade());
|
||||
let searching = self.search_open && !self.find_input.read(cx).value().is_empty();
|
||||
|
||||
v_flex()
|
||||
.image_cache(retain_all("sidebar"))
|
||||
.size_full()
|
||||
.relative()
|
||||
.gap(px(theme::SPACE))
|
||||
.bg(cx.theme().surface)
|
||||
.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(TABBAR_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(loading && logged_in, |this| {
|
||||
this.child(
|
||||
div()
|
||||
.absolute()
|
||||
.bottom_4()
|
||||
.left_0()
|
||||
.h_9()
|
||||
.w_full()
|
||||
.px_8()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap(px(theme::SPACE))
|
||||
.w_full()
|
||||
.h_9()
|
||||
.justify_center()
|
||||
.bg(cx.theme().bg.opacity(0.85))
|
||||
.when(cx.theme().shadow, |this| this.shadow_md())
|
||||
.rounded_full()
|
||||
.text_style(TextStyle::Caption)
|
||||
.font_semibold()
|
||||
.text_color(cx.theme().text_muted)
|
||||
.child(Indicator::new().small().color(cx.theme().accent))
|
||||
.child("Getting messages..."),
|
||||
),
|
||||
)
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
use std::collections::HashSet;
|
||||
|
||||
use anyhow::Error;
|
||||
use chat::{ChatEvent, ChatRegistry};
|
||||
use common::DebouncedDelay;
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement,
|
||||
IntoElement, ObjectFit, ParentElement, Render, SharedString, Styled, StyledImage, Subscription,
|
||||
Task, WeakEntity, Window, div, img, retain_all,
|
||||
};
|
||||
use gpui_kit::base::StyledExt as _;
|
||||
use gpui_kit::component::dock::{DockArea, Panel, PanelEvent};
|
||||
use gpui_kit::component::input::{InputEvent, InputState};
|
||||
use gpui_kit::component::notification::Notification;
|
||||
use gpui_kit::component::spinner::Spinner;
|
||||
use gpui_kit::component::{ActiveTheme, Sizable, WindowExt, h_flex, v_flex};
|
||||
use instant::Duration;
|
||||
use nip29::{Group, GroupKey, GroupsEvent, GroupsRegistry};
|
||||
use nostr_sdk::prelude::*;
|
||||
use person::{Directory, PersonRegistry};
|
||||
use smallvec::{SmallVec, smallvec};
|
||||
use state::{FIND_DELAY, NostrRegistry, StateEvent};
|
||||
use ui::title_bar_drag_handlers;
|
||||
|
||||
use crate::dock::TAB_BAR_HEIGHT;
|
||||
|
||||
mod group_view;
|
||||
mod header;
|
||||
mod inbox;
|
||||
mod list;
|
||||
mod onboarding;
|
||||
mod rows;
|
||||
mod search;
|
||||
|
||||
pub(crate) use rows::open_room;
|
||||
use search::SearchResult;
|
||||
|
||||
pub struct Sidebar {
|
||||
focus_handle: FocusHandle,
|
||||
dock: WeakEntity<DockArea>,
|
||||
/// Random background shown behind the signed-out screen.
|
||||
banner: SharedString,
|
||||
|
||||
/// The group whose information the sidebar is browsing, if any.
|
||||
group: Option<WeakEntity<Group>>,
|
||||
/// Whether the sidebar is showing the group's information instead of its list.
|
||||
group_open: bool,
|
||||
/// Whether the group view's admins section is expanded.
|
||||
admins_open: bool,
|
||||
/// Whether the group view's members section is expanded.
|
||||
members_open: bool,
|
||||
|
||||
/// Whether the main list's pinned section is expanded.
|
||||
pins_open: bool,
|
||||
/// Whether the user expanded or collapsed the main list's groups section.
|
||||
groups_open: Option<bool>,
|
||||
/// Same as `groups_open` for the chats section.
|
||||
chats_open: Option<bool>,
|
||||
|
||||
/// Whether the main list is showing contacts to start a chat with.
|
||||
contacts_open: bool,
|
||||
/// The signed-in user's contacts, loaded when the contact picker opens.
|
||||
contacts: Option<Vec<PublicKey>>,
|
||||
/// Person directory for contacts, search hits, and group members
|
||||
directory: Entity<Directory>,
|
||||
/// Contacts selected in the contact picker.
|
||||
selected_contacts: HashSet<PublicKey>,
|
||||
|
||||
/// Whether the main list is showing chat requests instead of chats and groups.
|
||||
requests_open: bool,
|
||||
new_requests: bool,
|
||||
|
||||
/// Whether the search bar is shown below the header.
|
||||
search_open: bool,
|
||||
/// Whether searches also cover global (relay) profiles.
|
||||
global_open: bool,
|
||||
/// Debounced delay for the global search while typing.
|
||||
find_debouncer: DebouncedDelay<Self>,
|
||||
/// Search input state.
|
||||
find_input: Entity<InputState>,
|
||||
/// Search results: rooms and groups by name, then messages by content.
|
||||
results: Vec<SearchResult>,
|
||||
/// Whether a global (relay) search is in progress.
|
||||
global_finding: bool,
|
||||
/// Profiles found by the last global search, if one was triggered.
|
||||
global_results: Option<Vec<PublicKey>>,
|
||||
/// The async global profile search.
|
||||
global_task: Option<Task<()>>,
|
||||
|
||||
tasks: SmallVec<[Task<Result<(), Error>>; 1]>,
|
||||
_subscriptions: SmallVec<[Subscription; 4]>,
|
||||
}
|
||||
|
||||
impl Sidebar {
|
||||
pub fn new(window: &mut Window, dock: WeakEntity<DockArea>, cx: &mut Context<Self>) -> Self {
|
||||
let chat = ChatRegistry::global(cx);
|
||||
let groups = GroupsRegistry::global(cx);
|
||||
let nostr = NostrRegistry::global(cx);
|
||||
let wot = PersonRegistry::global(cx).read(cx).wot();
|
||||
|
||||
let find_input = cx.new(|cx| {
|
||||
InputState::new(window, cx)
|
||||
.placeholder("Type to search")
|
||||
.clean_on_escape()
|
||||
});
|
||||
|
||||
let mut subscriptions = smallvec![];
|
||||
|
||||
subscriptions.push(
|
||||
cx.subscribe_in(&chat, window, move |this, _s, event, _window, cx| {
|
||||
if event == &ChatEvent::Ping {
|
||||
this.new_requests = true;
|
||||
cx.notify();
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
subscriptions.push(
|
||||
cx.subscribe_in(&wot, window, |_this, _wot, _event, _window, cx| {
|
||||
cx.notify();
|
||||
}),
|
||||
);
|
||||
|
||||
subscriptions.push(
|
||||
cx.subscribe_in(&groups, window, |_this, _, event, window, cx| {
|
||||
match event {
|
||||
GroupsEvent::Updated => {
|
||||
// TODO: find a better way?
|
||||
cx.notify();
|
||||
}
|
||||
GroupsEvent::Error(error) => {
|
||||
window.push_notification(Notification::error(error.clone()), cx);
|
||||
}
|
||||
GroupsEvent::Created {
|
||||
warning: Some(warning),
|
||||
..
|
||||
} => {
|
||||
window.push_notification(Notification::warning(warning.clone()), cx);
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
subscriptions.push(
|
||||
cx.subscribe_in(
|
||||
&nostr,
|
||||
window,
|
||||
|this, state, event, window, cx| match event {
|
||||
StateEvent::NoSigner => {
|
||||
this.banner = onboarding::pick_banner();
|
||||
if this.search_open {
|
||||
this.toggle_search(window, cx);
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
StateEvent::SignerChanged => {
|
||||
if let Some(current_user) = state.read(cx).current_user() {
|
||||
this.directory.update(cx, |directory, cx| {
|
||||
directory.track([current_user], cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
subscriptions.push(cx.subscribe_in(
|
||||
&find_input,
|
||||
window,
|
||||
|this, state, event, window, cx| match event {
|
||||
InputEvent::PressEnter { .. } => {
|
||||
this.search(cx);
|
||||
|
||||
if this.global_open {
|
||||
this.global_search(cx);
|
||||
}
|
||||
}
|
||||
InputEvent::Change => {
|
||||
if state.read(cx).value().is_empty() {
|
||||
this.clear_results(cx);
|
||||
} else {
|
||||
this.search(cx);
|
||||
|
||||
if this.global_open {
|
||||
this.find_debouncer.fire_new(
|
||||
Duration::from_millis(FIND_DELAY),
|
||||
window,
|
||||
cx,
|
||||
|this, window, cx| this.debounced_global(window, cx),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
));
|
||||
|
||||
Self {
|
||||
focus_handle: cx.focus_handle(),
|
||||
dock,
|
||||
banner: onboarding::pick_banner(),
|
||||
group: None,
|
||||
group_open: false,
|
||||
admins_open: true,
|
||||
members_open: true,
|
||||
pins_open: true,
|
||||
groups_open: None,
|
||||
chats_open: None,
|
||||
contacts_open: false,
|
||||
contacts: None,
|
||||
directory: Directory::new(cx),
|
||||
selected_contacts: HashSet::new(),
|
||||
requests_open: false,
|
||||
new_requests: false,
|
||||
search_open: false,
|
||||
global_open: false,
|
||||
find_debouncer: DebouncedDelay::new(),
|
||||
find_input,
|
||||
results: Vec::new(),
|
||||
global_finding: false,
|
||||
global_results: None,
|
||||
global_task: None,
|
||||
tasks: smallvec![],
|
||||
_subscriptions: subscriptions,
|
||||
}
|
||||
}
|
||||
|
||||
/// Show the group view without toggling it closed.
|
||||
pub(crate) fn show_group_meta(&mut self, key: &GroupKey, cx: &mut Context<Self>) {
|
||||
let groups = GroupsRegistry::global(cx);
|
||||
|
||||
let Some(group) = groups.read(cx).group(key, cx) else {
|
||||
return;
|
||||
};
|
||||
|
||||
self.show_group(&group, cx);
|
||||
}
|
||||
|
||||
/// Hide the group view if the sidebar is showing the given group.
|
||||
pub(crate) fn hide_group_meta(&mut self, key: &GroupKey, cx: &mut Context<Self>) {
|
||||
let showing = self
|
||||
.group
|
||||
.as_ref()
|
||||
.and_then(|group| group.upgrade())
|
||||
.is_some_and(|group| group.read(cx).key() == key);
|
||||
|
||||
if showing {
|
||||
self.hide_group(cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl gpui_kit::component::dock::BasePanel for Sidebar {
|
||||
fn panel_name(&self) -> &'static str {
|
||||
"Sidebar"
|
||||
}
|
||||
|
||||
fn closable(&self, _cx: &App) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn zoomable(&self, _cx: &App) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
impl Panel for Sidebar {
|
||||
fn title(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||
SharedString::from("Sidebar")
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<PanelEvent> for Sidebar {}
|
||||
|
||||
impl Focusable for Sidebar {
|
||||
fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for Sidebar {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let nostr = NostrRegistry::global(cx);
|
||||
let current_user = nostr.read(cx).current_user();
|
||||
let logged_in = current_user.is_some();
|
||||
|
||||
let chat = ChatRegistry::global(cx);
|
||||
let loading = chat.read(cx).loading();
|
||||
|
||||
let group = self.group.clone().and_then(|group| group.upgrade());
|
||||
let searching = self.search_open && !self.find_input.read(cx).value().is_empty();
|
||||
let picking_contacts = self.contacts_open && !self.requests_open;
|
||||
|
||||
v_flex()
|
||||
.image_cache(retain_all("sidebar"))
|
||||
.size_full()
|
||||
.relative()
|
||||
.justify_between()
|
||||
.bg(cx.theme().sidebar)
|
||||
.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 && !picking_contacts, |this| {
|
||||
this.child(self.render_footer(cx))
|
||||
})
|
||||
.when(loading && logged_in, |this| {
|
||||
this.child(
|
||||
div()
|
||||
.absolute()
|
||||
.bottom_4()
|
||||
.left_0()
|
||||
.h_9()
|
||||
.w_full()
|
||||
.px_8()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.w_full()
|
||||
.h_9()
|
||||
.justify_center()
|
||||
.bg(cx.theme().background.opacity(0.85))
|
||||
.when(cx.theme().shadow, |this| this.shadow_md())
|
||||
.rounded_full()
|
||||
.text_xs()
|
||||
.font_semibold()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child(Spinner::new().small().color(cx.theme().primary))
|
||||
.child("Getting messages..."),
|
||||
),
|
||||
)
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
use gpui::{AnyElement, App, IntoElement, ParentElement, SharedString, Styled, Window, img};
|
||||
use ui::button::{Button, ButtonCustomVariant, ButtonVariants};
|
||||
use ui::{Sizable, StyledExt, v_flex};
|
||||
use gpui_kit::component::button::{Button, ButtonVariants};
|
||||
use gpui_kit::component::v_flex;
|
||||
|
||||
use crate::dialogs::import;
|
||||
|
||||
@@ -16,11 +16,12 @@ pub(super) fn pick_banner() -> SharedString {
|
||||
.map(|elapsed| elapsed.subsec_nanos())
|
||||
.unwrap_or_default();
|
||||
let index = nanos as usize % SIGNED_OUT_BANNERS.len();
|
||||
|
||||
SIGNED_OUT_BANNERS[index].into()
|
||||
}
|
||||
|
||||
/// The headline and identity import shown while signed out.
|
||||
pub(super) fn render_signed_out(window: &mut Window, cx: &mut App) -> AnyElement {
|
||||
pub(super) fn render_signed_out(_window: &mut Window, _cx: &mut App) -> AnyElement {
|
||||
v_flex()
|
||||
.size_full()
|
||||
.justify_end()
|
||||
@@ -30,15 +31,7 @@ pub(super) fn render_signed_out(window: &mut Window, cx: &mut App) -> AnyElement
|
||||
.child(
|
||||
Button::new("import")
|
||||
.label("Import Identity")
|
||||
.custom(
|
||||
ButtonCustomVariant::new(window, cx)
|
||||
.color(gpui::white())
|
||||
.foreground(gpui::black())
|
||||
.hover(gpui::white().opacity(0.9))
|
||||
.active(gpui::white().opacity(0.8)),
|
||||
)
|
||||
.large()
|
||||
.font_semibold()
|
||||
.primary()
|
||||
.on_click(|_, window, cx| {
|
||||
import::open(window, cx);
|
||||
}),
|
||||
|
||||
@@ -4,24 +4,24 @@ use chat::{Room, RoomKind};
|
||||
use common::TimestampExt;
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
AnyElement, App, Context, Entity, IntoElement, ParentElement, SharedString, Styled, WeakEntity,
|
||||
Window, div, px,
|
||||
AnyElement, App, Context, Entity, InteractiveElement as _, IntoElement, ParentElement,
|
||||
SharedString, Styled, WeakEntity, Window, div, px,
|
||||
};
|
||||
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};
|
||||
use nip29::Group;
|
||||
use nostr_sdk::prelude::*;
|
||||
use settings::AppSettings;
|
||||
use theme::{ActiveTheme, TextStyle, Typeset as _, button_radius};
|
||||
use ui::avatar::Avatar;
|
||||
use ui::dialog::DialogButtonProps;
|
||||
use ui::dock::{ClosePanel, DockArea, DockPlacement, PanelHandle};
|
||||
use ui::menu::{ContextMenu, PopupMenuItem};
|
||||
use ui::nav::Nav;
|
||||
use ui::{Icon, IconName, Selectable, Sizable, StyledExt, WindowExtension, h_flex, v_flex};
|
||||
use ui::{Avatar, IconName};
|
||||
|
||||
use super::Sidebar;
|
||||
use super::list::ListSection;
|
||||
use super::search::MessageHit;
|
||||
use crate::dialogs::{profile, screening};
|
||||
use crate::nav::Nav;
|
||||
|
||||
/// Open a room in the dock, screening new requests when enabled.
|
||||
pub(crate) fn open_room(
|
||||
@@ -30,7 +30,7 @@ pub(crate) fn open_room(
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
ui::dock::add_panel_to(
|
||||
crate::dock::add_panel_to(
|
||||
dock,
|
||||
PanelHandle::new(chat_ui::init(room.downgrade(), window, cx)),
|
||||
DockPlacement::Center,
|
||||
@@ -42,8 +42,9 @@ pub(crate) fn open_room(
|
||||
let peer = room.read(cx).display_member(cx).public_key();
|
||||
let view = screening::init(peer, window, cx);
|
||||
|
||||
window.open_dialog(cx, move |this, _window, _cx| {
|
||||
this.confirm()
|
||||
window.open_alert_dialog(cx, move |alert, _window, _cx| {
|
||||
alert
|
||||
.confirm()
|
||||
.child(view.clone())
|
||||
.button_props(
|
||||
DialogButtonProps::default()
|
||||
@@ -58,337 +59,6 @@ pub(crate) fn open_room(
|
||||
}
|
||||
}
|
||||
|
||||
impl Sidebar {
|
||||
pub(super) fn render_rows(
|
||||
&self,
|
||||
range: Range<usize>,
|
||||
rows: &[SidebarRow],
|
||||
cx: &Context<Sidebar>,
|
||||
) -> Vec<AnyElement> {
|
||||
let hide_avatar = AppSettings::get_hide_avatar(cx);
|
||||
|
||||
rows.get(range.clone())
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.enumerate()
|
||||
.map(|(offset, row)| {
|
||||
let index = range.start + offset;
|
||||
|
||||
match row {
|
||||
SidebarRow::Section(section) => {
|
||||
let section = *section;
|
||||
let label = section.label();
|
||||
|
||||
let open = match section {
|
||||
ListSection::Pins => self.pins_open,
|
||||
ListSection::Groups => self.is_groups_open(cx),
|
||||
ListSection::Chats => self.is_chats_open(cx),
|
||||
};
|
||||
|
||||
let icon = if open {
|
||||
IconName::CaretDown
|
||||
} else {
|
||||
IconName::CaretRight
|
||||
};
|
||||
|
||||
Nav::new(label)
|
||||
.label(label)
|
||||
.suffix(Icon::new(icon).small().text_color(cx.theme().text_muted))
|
||||
.text_style(TextStyle::Caption)
|
||||
.font_semibold()
|
||||
.text_color(cx.theme().text_muted)
|
||||
.on_click(cx.listener(move |this, _event, _window, cx| {
|
||||
match section {
|
||||
ListSection::Pins => this.pins_open = !this.pins_open,
|
||||
ListSection::Groups => {
|
||||
this.groups_open = Some(!this.is_groups_open(cx))
|
||||
}
|
||||
ListSection::Chats => {
|
||||
this.chats_open = Some(!this.is_chats_open(cx))
|
||||
}
|
||||
}
|
||||
cx.notify();
|
||||
}))
|
||||
.into_any_element()
|
||||
}
|
||||
SidebarRow::Heading(label) => {
|
||||
let label = *label;
|
||||
|
||||
Nav::new(label)
|
||||
.label(label)
|
||||
.text_style(TextStyle::Caption)
|
||||
.font_semibold()
|
||||
.text_color(cx.theme().text_muted)
|
||||
.into_any_element()
|
||||
}
|
||||
SidebarRow::Empty(text) => v_flex()
|
||||
.h_10()
|
||||
.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(px(button_radius()))
|
||||
.text_style(TextStyle::Caption)
|
||||
.text_color(cx.theme().text_muted)
|
||||
.child(*text),
|
||||
)
|
||||
.into_any_element(),
|
||||
SidebarRow::Room { room } => {
|
||||
let id = room.read(cx).id;
|
||||
let name = room.read(cx).display_name(cx);
|
||||
let picture = room.read(cx).display_image(cx);
|
||||
let seed = room.read(cx).display_image_seed(cx);
|
||||
let created_at = room.read(cx).created_at.to_ago();
|
||||
let member = room.read(cx).members().first().copied();
|
||||
let spam =
|
||||
room.read(cx).is_spam(cx) && room.read(cx).kind == RoomKind::Request;
|
||||
|
||||
let pinned = AppSettings::global(cx).read(cx).pinned_room(id, cx);
|
||||
let dock = self.dock.clone();
|
||||
let room = room.clone();
|
||||
|
||||
let nav = Nav::new(index)
|
||||
.label(name)
|
||||
.text_style(TextStyle::Callout)
|
||||
.when(!hide_avatar, |this| {
|
||||
this.prefix(
|
||||
Avatar::from_source(picture)
|
||||
.seed(seed)
|
||||
.small()
|
||||
.flex_shrink_0(),
|
||||
)
|
||||
})
|
||||
.suffix(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.when(spam, |this| {
|
||||
this.child(
|
||||
div()
|
||||
.px_1()
|
||||
.rounded(px(button_radius()))
|
||||
.bg(cx.theme().danger.opacity(0.15))
|
||||
.text_size(px(8.0))
|
||||
.text_color(cx.theme().danger)
|
||||
.child("SPAM"),
|
||||
)
|
||||
})
|
||||
.when(!spam, |this| {
|
||||
this.child(
|
||||
div()
|
||||
.font_normal()
|
||||
.text_style(TextStyle::Caption)
|
||||
.text_color(cx.theme().text_faint)
|
||||
.child(created_at),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.on_click(move |_event, window, cx| {
|
||||
open_room(&room, &dock, window, cx);
|
||||
});
|
||||
|
||||
ContextMenu::new(
|
||||
format!("room-menu-{index}"),
|
||||
nav,
|
||||
move |menu, _window, _cx| {
|
||||
let menu = menu.item(
|
||||
PopupMenuItem::new(if pinned { "Unpin" } else { "Pin" })
|
||||
.on_click(move |_, _window, cx| {
|
||||
AppSettings::global(cx).update(cx, |settings, cx| {
|
||||
settings.toggle_pinned_room(id, cx);
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
if let Some(member) = member {
|
||||
menu.item(PopupMenuItem::new("View profile").on_click(
|
||||
move |_, window, cx| {
|
||||
profile::open(member, window, cx);
|
||||
},
|
||||
))
|
||||
} else {
|
||||
menu
|
||||
}
|
||||
},
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
SidebarRow::Contact { public_key } => {
|
||||
let person = self.directory.read(cx).person(public_key, cx);
|
||||
let selected = self.selected_contacts.contains(public_key);
|
||||
let sidebar = cx.entity().downgrade();
|
||||
let public_key = public_key.to_owned();
|
||||
|
||||
Nav::new(index)
|
||||
.label(person.name())
|
||||
.text_style(TextStyle::Callout)
|
||||
.when(!hide_avatar, |this| {
|
||||
this.prefix(
|
||||
Avatar::from_source(person.avatar())
|
||||
.seed(person.avatar_seed())
|
||||
.small()
|
||||
.flex_shrink_0(),
|
||||
)
|
||||
})
|
||||
.selected(selected)
|
||||
.on_click(move |_event, _window, cx| {
|
||||
if let Err(error) = sidebar.update(cx, |this, cx| {
|
||||
this.toggle_contact(&public_key, cx);
|
||||
}) {
|
||||
log::error!("Failed to select contact: {error}");
|
||||
}
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
SidebarRow::Profile { public_key } => {
|
||||
let person = self.directory.read(cx).person(public_key, cx);
|
||||
let public_key = public_key.to_owned();
|
||||
|
||||
Nav::new(index)
|
||||
.label(person.name())
|
||||
.text_style(TextStyle::Callout)
|
||||
.when(!hide_avatar, |this| {
|
||||
this.prefix(
|
||||
Avatar::from_source(person.avatar())
|
||||
.seed(person.avatar_seed())
|
||||
.small()
|
||||
.flex_shrink_0(),
|
||||
)
|
||||
})
|
||||
.on_click(move |_event, window, cx| {
|
||||
profile::open(public_key, window, cx);
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
SidebarRow::Group { group } => {
|
||||
let dock = self.dock.clone();
|
||||
let sidebar = cx.entity().downgrade();
|
||||
let group = group.clone();
|
||||
|
||||
let name = group.read(cx).display_name();
|
||||
let picture = group.read(cx).display_image();
|
||||
let seed = group.read(cx).key().cache_tag();
|
||||
let tag = seed.clone();
|
||||
let pinned = AppSettings::global(cx).read(cx).pinned_group(&tag, cx);
|
||||
let weak_group = group.downgrade();
|
||||
|
||||
let suffix = group
|
||||
.read(cx)
|
||||
.membership()
|
||||
.status()
|
||||
.or_else(|| group.read(cx).elsewhere().map(|_| "Moved"))
|
||||
.map(SharedString::from);
|
||||
|
||||
let nav = Nav::new(index)
|
||||
.label(name)
|
||||
.text_style(TextStyle::Callout)
|
||||
.when(!hide_avatar, |this| {
|
||||
this.prefix(
|
||||
Avatar::from_source(picture)
|
||||
.seed(seed)
|
||||
.small()
|
||||
.flex_shrink_0(),
|
||||
)
|
||||
})
|
||||
.when_some(suffix, |this, suffix| {
|
||||
this.suffix(
|
||||
div()
|
||||
.font_normal()
|
||||
.text_style(TextStyle::Caption)
|
||||
.text_color(cx.theme().text_faint)
|
||||
.child(suffix),
|
||||
)
|
||||
})
|
||||
.on_click(move |_event, window, cx| {
|
||||
ui::dock::add_panel_to(
|
||||
&dock,
|
||||
PanelHandle::new(nip29_ui::init(group.clone(), window, cx)),
|
||||
DockPlacement::Center,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
|
||||
if let Err(error) = sidebar.update(cx, |this, cx| {
|
||||
this.show_group(&group, cx);
|
||||
}) {
|
||||
log::error!("Failed to show the group in the sidebar: {error}");
|
||||
}
|
||||
});
|
||||
|
||||
ContextMenu::new(
|
||||
format!("group-menu-{index}"),
|
||||
nav,
|
||||
move |menu, _window, _cx| {
|
||||
menu.item(
|
||||
PopupMenuItem::new(if pinned { "Unpin" } else { "Pin" })
|
||||
.on_click({
|
||||
let tag = tag.clone();
|
||||
move |_, _window, cx| {
|
||||
AppSettings::global(cx).update(
|
||||
cx,
|
||||
|settings, cx| {
|
||||
settings.toggle_pinned_group(&tag, cx);
|
||||
},
|
||||
);
|
||||
}
|
||||
}),
|
||||
)
|
||||
.item(
|
||||
PopupMenuItem::new("Leave").on_click({
|
||||
let group = weak_group.clone();
|
||||
move |_, window, cx| {
|
||||
nip29_ui::confirm_leave(group.clone(), window, cx);
|
||||
}
|
||||
}),
|
||||
)
|
||||
},
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
SidebarRow::Message { hit } => {
|
||||
let person = self.directory.read(cx).person(&hit.author, cx);
|
||||
let name = hit.room.read(cx).display_name(cx);
|
||||
let detail = format!("{} · {}", name, hit.created_at.to_ago());
|
||||
let dock = self.dock.clone();
|
||||
let room = hit.room.clone();
|
||||
let content = hit.content.clone();
|
||||
|
||||
Nav::new(index)
|
||||
.label(content)
|
||||
.text_style(TextStyle::Callout)
|
||||
.font_medium()
|
||||
.when(!hide_avatar, |this| {
|
||||
this.prefix(
|
||||
Avatar::from_source(person.avatar())
|
||||
.seed(person.avatar_seed())
|
||||
.small()
|
||||
.flex_shrink_0(),
|
||||
)
|
||||
})
|
||||
.suffix(
|
||||
div()
|
||||
.font_normal()
|
||||
.text_style(TextStyle::Caption)
|
||||
.text_color(cx.theme().text_faint)
|
||||
.child(detail),
|
||||
)
|
||||
.on_click(move |_event, window, cx| {
|
||||
open_room(&room, &dock, window, cx);
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// A row shown in the sidebar's lists.
|
||||
pub(super) enum SidebarRow {
|
||||
Section(ListSection),
|
||||
@@ -412,3 +82,322 @@ pub(super) enum SidebarRow {
|
||||
Heading(&'static str),
|
||||
Empty(&'static str),
|
||||
}
|
||||
|
||||
impl Sidebar {
|
||||
pub(super) fn render_rows(
|
||||
&self,
|
||||
range: Range<usize>,
|
||||
rows: &[SidebarRow],
|
||||
cx: &Context<Sidebar>,
|
||||
) -> Vec<AnyElement> {
|
||||
let hide_avatar = AppSettings::get_hide_avatar(cx);
|
||||
|
||||
rows.get(range.clone())
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.enumerate()
|
||||
.map(|(offset, row)| {
|
||||
let index = range.start + offset;
|
||||
|
||||
let element = match row {
|
||||
SidebarRow::Section(section) => {
|
||||
let section = *section;
|
||||
let label = section.label();
|
||||
|
||||
let open = match section {
|
||||
ListSection::Pins => self.pins_open,
|
||||
ListSection::Groups => self.is_groups_open(cx),
|
||||
ListSection::Chats => self.is_chats_open(cx),
|
||||
};
|
||||
|
||||
let icon = if open {
|
||||
IconName::CaretDown
|
||||
} else {
|
||||
IconName::CaretRight
|
||||
};
|
||||
|
||||
Nav::new(label)
|
||||
.label(label)
|
||||
.suffix(
|
||||
Icon::new(icon)
|
||||
.small()
|
||||
.text_color(cx.theme().muted_foreground),
|
||||
)
|
||||
.text_xs()
|
||||
.font_semibold()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.on_click(cx.listener(move |this, _event, _window, cx| {
|
||||
match section {
|
||||
ListSection::Pins => this.pins_open = !this.pins_open,
|
||||
ListSection::Groups => {
|
||||
this.groups_open = Some(!this.is_groups_open(cx))
|
||||
}
|
||||
ListSection::Chats => {
|
||||
this.chats_open = Some(!this.is_chats_open(cx))
|
||||
}
|
||||
}
|
||||
cx.notify();
|
||||
}))
|
||||
.into_any_element()
|
||||
}
|
||||
SidebarRow::Heading(label) => {
|
||||
let label = *label;
|
||||
|
||||
Nav::new(label)
|
||||
.label(label)
|
||||
.text_xs()
|
||||
.font_semibold()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.into_any_element()
|
||||
}
|
||||
SidebarRow::Empty(text) => h_flex()
|
||||
.h_8()
|
||||
.w_full()
|
||||
.px_2()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child(*text)
|
||||
.into_any_element(),
|
||||
SidebarRow::Room { room } => {
|
||||
let id = room.read(cx).id;
|
||||
let name = room.read(cx).display_name(cx);
|
||||
let seed = room.read(cx).display_image_seed(cx);
|
||||
let created_at = room.read(cx).created_at.to_ago();
|
||||
let member = room.read(cx).members().first().copied();
|
||||
let spam =
|
||||
room.read(cx).is_spam(cx) && room.read(cx).kind == RoomKind::Request;
|
||||
|
||||
let pinned = AppSettings::global(cx).read(cx).pinned_room(id, cx);
|
||||
let dock = self.dock.clone();
|
||||
let room = room.clone();
|
||||
|
||||
let nav = Nav::new(index)
|
||||
.label(name)
|
||||
.text_sm()
|
||||
.when(!hide_avatar, |this| {
|
||||
this.prefix(
|
||||
Avatar::new(room.read(cx).display_image(cx))
|
||||
.seed(seed)
|
||||
.small(),
|
||||
)
|
||||
})
|
||||
.suffix(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.when(spam, |this| {
|
||||
this.child(
|
||||
div()
|
||||
.px_1()
|
||||
.rounded(cx.theme().radius)
|
||||
.bg(cx.theme().danger.opacity(0.15))
|
||||
.text_size(px(8.0))
|
||||
.text_color(cx.theme().danger)
|
||||
.child("SPAM"),
|
||||
)
|
||||
})
|
||||
.when(!spam, |this| {
|
||||
this.child(
|
||||
div()
|
||||
.font_normal()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child(created_at),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.on_click(move |_event, window, cx| {
|
||||
open_room(&room, &dock, window, cx);
|
||||
});
|
||||
|
||||
div()
|
||||
.id(index)
|
||||
.w_full()
|
||||
.child(nav)
|
||||
.context_menu(move |menu, _window, _cx| {
|
||||
let menu = menu.item(
|
||||
PopupMenuItem::new(if pinned { "Unpin" } else { "Pin" })
|
||||
.on_click(move |_, _window, cx| {
|
||||
AppSettings::global(cx).update(cx, |settings, cx| {
|
||||
settings.toggle_pinned_room(id, cx);
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
if let Some(member) = member {
|
||||
menu.item(PopupMenuItem::new("View profile").on_click(
|
||||
move |_, window, cx| {
|
||||
profile::open(member, window, cx);
|
||||
},
|
||||
))
|
||||
} else {
|
||||
menu
|
||||
}
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
SidebarRow::Contact { public_key } => {
|
||||
let person = self.directory.read(cx).person(public_key, cx);
|
||||
let selected = self.selected_contacts.contains(public_key);
|
||||
let sidebar = cx.entity().downgrade();
|
||||
let public_key = public_key.to_owned();
|
||||
|
||||
Nav::new(index)
|
||||
.label(person.name())
|
||||
.text_sm()
|
||||
.when(!hide_avatar, |this| {
|
||||
this.prefix(
|
||||
Avatar::new(person.avatar())
|
||||
.seed(person.avatar_seed())
|
||||
.small(),
|
||||
)
|
||||
})
|
||||
.selected(selected)
|
||||
.on_click(move |_event, _window, cx| {
|
||||
if let Err(error) = sidebar.update(cx, |this, cx| {
|
||||
this.toggle_contact(&public_key, cx);
|
||||
}) {
|
||||
log::error!("Failed to select contact: {error}");
|
||||
}
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
SidebarRow::Profile { public_key } => {
|
||||
let person = self.directory.read(cx).person(public_key, cx);
|
||||
let public_key = public_key.to_owned();
|
||||
|
||||
Nav::new(index)
|
||||
.label(person.name())
|
||||
.text_sm()
|
||||
.when(!hide_avatar, |this| {
|
||||
this.prefix(
|
||||
Avatar::new(person.avatar())
|
||||
.seed(person.avatar_seed())
|
||||
.small(),
|
||||
)
|
||||
})
|
||||
.on_click(move |_event, window, cx| {
|
||||
profile::open(public_key, window, cx);
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
SidebarRow::Group { group } => {
|
||||
let dock = self.dock.clone();
|
||||
let sidebar = cx.entity().downgrade();
|
||||
let group = group.clone();
|
||||
|
||||
let name = group.read(cx).display_name();
|
||||
let seed = group.read(cx).key().cache_tag();
|
||||
let tag = seed.clone();
|
||||
let pinned = AppSettings::global(cx).read(cx).pinned_group(&tag, cx);
|
||||
let weak_group = group.downgrade();
|
||||
|
||||
let suffix = group
|
||||
.read(cx)
|
||||
.membership()
|
||||
.status()
|
||||
.or_else(|| group.read(cx).elsewhere().map(|_| "Moved"))
|
||||
.map(SharedString::from);
|
||||
|
||||
let nav = Nav::new(index)
|
||||
.label(name)
|
||||
.text_sm()
|
||||
.when(!hide_avatar, |this| {
|
||||
this.prefix(
|
||||
Avatar::new(group.read(cx).display_image())
|
||||
.seed(seed)
|
||||
.small(),
|
||||
)
|
||||
})
|
||||
.when_some(suffix, |this, suffix| {
|
||||
this.suffix(
|
||||
div()
|
||||
.font_normal()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child(suffix),
|
||||
)
|
||||
})
|
||||
.on_click(move |_event, window, cx| {
|
||||
crate::dock::add_panel_to(
|
||||
&dock,
|
||||
PanelHandle::new(nip29_ui::init(group.clone(), window, cx)),
|
||||
DockPlacement::Center,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
|
||||
if let Err(error) = sidebar.update(cx, |this, cx| {
|
||||
this.show_group(&group, cx);
|
||||
}) {
|
||||
log::error!("Failed to show the group in the sidebar: {error}");
|
||||
}
|
||||
});
|
||||
|
||||
div()
|
||||
.id(index)
|
||||
.w_full()
|
||||
.child(nav)
|
||||
.context_menu(move |menu, _window, _cx| {
|
||||
menu.item(
|
||||
PopupMenuItem::new(if pinned { "Unpin" } else { "Pin" })
|
||||
.on_click({
|
||||
let tag = tag.clone();
|
||||
move |_, _window, cx| {
|
||||
AppSettings::global(cx).update(
|
||||
cx,
|
||||
|settings, cx| {
|
||||
settings.toggle_pinned_group(&tag, cx);
|
||||
},
|
||||
);
|
||||
}
|
||||
}),
|
||||
)
|
||||
.item(
|
||||
PopupMenuItem::new("Leave").on_click({
|
||||
let group = weak_group.clone();
|
||||
move |_, window, cx| {
|
||||
nip29_ui::confirm_leave(group.clone(), window, cx);
|
||||
}
|
||||
}),
|
||||
)
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
SidebarRow::Message { hit } => {
|
||||
let person = self.directory.read(cx).person(&hit.author, cx);
|
||||
let name = hit.room.read(cx).display_name(cx);
|
||||
let detail = format!("{} · {}", name, hit.created_at.to_ago());
|
||||
let dock = self.dock.clone();
|
||||
let room = hit.room.clone();
|
||||
let content = hit.content.clone();
|
||||
|
||||
Nav::new(index)
|
||||
.label(content)
|
||||
.text_sm()
|
||||
.font_medium()
|
||||
.when(!hide_avatar, |this| {
|
||||
this.prefix(
|
||||
Avatar::new(person.avatar())
|
||||
.seed(person.avatar_seed())
|
||||
.small(),
|
||||
)
|
||||
})
|
||||
.suffix(
|
||||
div()
|
||||
.font_normal()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child(detail),
|
||||
)
|
||||
.on_click(move |_event, window, cx| {
|
||||
open_room(&room, &dock, window, cx);
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
};
|
||||
// Keep a small gap between rows so selected rows read as pills.
|
||||
div().pb_0p5().child(element).into_any_element()
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,16 +4,15 @@ use std::rc::Rc;
|
||||
use chat::{ChatRegistry, Room};
|
||||
use gpui::{
|
||||
AnyElement, App, Context, Entity, IntoElement, ParentElement, SharedString, Styled, Task,
|
||||
Window, div, px, uniform_list,
|
||||
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};
|
||||
use theme::{ActiveTheme, button_radius};
|
||||
use ui::button::{Button, ButtonVariants};
|
||||
use ui::input::Input;
|
||||
use ui::scroll::Scrollbar;
|
||||
use ui::{IconName, Selectable, Sizable, h_flex, v_flex};
|
||||
|
||||
use super::Sidebar;
|
||||
use super::rows::SidebarRow;
|
||||
@@ -206,6 +205,7 @@ impl Sidebar {
|
||||
|
||||
pub(super) fn render_find(&self, cx: &mut Context<Self>) -> AnyElement {
|
||||
h_flex()
|
||||
.mb_2()
|
||||
.px_2()
|
||||
.flex_shrink_0()
|
||||
.w_full()
|
||||
@@ -214,9 +214,9 @@ impl Sidebar {
|
||||
.px_1p5()
|
||||
.gap_1()
|
||||
.w_full()
|
||||
.rounded(px(button_radius()))
|
||||
.bg(cx.theme().surface_raised)
|
||||
.child(Input::new(&self.find_input).appearance(false).small())
|
||||
.rounded(cx.theme().radius)
|
||||
.bg(cx.theme().input_background())
|
||||
.child(Input::new(&self.find_input).appearance(false))
|
||||
.child(
|
||||
Button::new("sidebar-global")
|
||||
.icon(IconName::Compass)
|
||||
@@ -225,7 +225,7 @@ impl Sidebar {
|
||||
} else {
|
||||
"Enable global search"
|
||||
})
|
||||
.small()
|
||||
.xsmall()
|
||||
.ghost()
|
||||
.selected(self.global_open)
|
||||
.loading(self.global_finding)
|
||||
@@ -245,22 +245,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()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user