clean up the sidebar

This commit is contained in:
2026-09-23 14:54:34 +07:00
parent 7ba289440b
commit 273ddabde5
6 changed files with 517 additions and 718 deletions
+3
View File
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
<path d="M2.75 12C2.75 17.1086 6.89137 21.25 12 21.25C17.1086 21.25 21.25 17.1086 21.25 12C21.25 6.89137 17.1086 2.75 12 2.75C6.89137 2.75 2.75 6.89137 2.75 12Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M15.25 13L12 16.25L8.75 13" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M12 7.75V15.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 571 B

-3
View File
@@ -19,9 +19,6 @@ pub enum PanelEvent {
pub trait Panel: EventEmitter<PanelEvent> + Render + Focusable {
/// The name of the panel used to serialize, deserialize and identify the panel.
///
/// This is used to identify the panel when deserializing the panel.
/// Once you have defined a panel id, this must not be changed.
fn panel_id(&self) -> SharedString;
/// The title of the panel
+2
View File
@@ -22,6 +22,7 @@ impl<T: IconNamed> From<T> for Icon {
pub enum IconName {
ArrowLeft,
ArrowRight,
ArrowDownCircle,
Boom,
Book,
ChevronDown,
@@ -99,6 +100,7 @@ impl IconNamed for IconName {
match self {
Self::ArrowLeft => "icons/arrow-left.svg",
Self::ArrowRight => "icons/arrow-right.svg",
Self::ArrowDownCircle => "icons/arrow-down-circle.svg",
Self::Boom => "icons/boom.svg",
Self::Book => "icons/book.svg",
Self::ChevronDown => "icons/chevron-down.svg",
+4 -181
View File
@@ -1,4 +1,3 @@
use std::rc::Rc;
use std::sync::Arc;
use ::settings::AppSettings;
@@ -7,22 +6,19 @@ use auto_update::AutoUpdater;
use chat::{ChatEvent, ChatRegistry};
use common::download_dir;
use device::{DeviceEvent, DeviceRegistry};
use gpui::prelude::FluentBuilder;
use gpui::{
Action, AnyElement, App, AppContext, Context, Entity, InteractiveElement, IntoElement,
ParentElement, Render, SharedString, Styled, Subscription, Task, Window, div, px,
Action, App, AppContext, Context, Entity, InteractiveElement, IntoElement, ParentElement,
Render, SharedString, Styled, Subscription, Task, Window, div, px,
};
use nostr_sdk::prelude::*;
use person::{PersonRegistry, shorten_pubkey};
use serde::Deserialize;
use smallvec::{SmallVec, smallvec};
use state::{NostrRegistry, StateEvent};
use theme::{ActiveTheme, SIDEBAR_WIDTH, Theme, ThemeRegistry};
use ui::button::{Button, ButtonVariants};
use ui::dock::{self, DockArea, DockLayout, DockPlacement, Panel, PanelHandle};
use ui::menu::{DropdownMenu, PopupMenuItem};
use ui::notification::{Notification, NotificationKind};
use ui::{Icon, IconName, Root, Sizable, WindowExtension, h_flex, v_flex};
use ui::{IconName, Root, Sizable, WindowExtension, h_flex, v_flex};
use crate::dialogs::restore::RestoreEncryption;
use crate::dialogs::{new_chat, new_community, settings};
@@ -69,7 +65,6 @@ enum Command {
pub struct Workspace {
dock: Entity<DockArea>,
title_bar_chrome: Rc<dock::TitleBarChrome>,
/// Async tasks
tasks: Vec<Task<Result<(), Error>>>,
/// Event subscriptions
@@ -82,7 +77,7 @@ impl Workspace {
let device = DeviceRegistry::global(cx);
let nostr = NostrRegistry::global(cx);
let (dock, title_bar_chrome) = dock::dock_area("coop", window, cx);
let (dock, _) = dock::dock_area("coop", window, cx);
let sidebar = cx.new(|cx| Sidebar::new(window, dock.downgrade(), cx));
let mut subscriptions = smallvec![];
@@ -207,7 +202,6 @@ impl Workspace {
Self {
dock,
title_bar_chrome,
tasks: vec![],
_subscriptions: subscriptions,
}
@@ -510,174 +504,6 @@ impl Workspace {
}))
});
}
fn titlebar_right(_window: &mut Window, cx: &mut App) -> AnyElement {
let auto_updater = AutoUpdater::try_global(cx);
let chat = ChatRegistry::global(cx);
let nip4e_enabled = AppSettings::get_nip4e(cx);
let nostr = NostrRegistry::global(cx);
let Some(public_key) = nostr.read(cx).current_user() else {
return div().into_any_element();
};
let persons = PersonRegistry::global(cx);
let profile = persons.read(cx).get(&public_key, cx);
let announcement = profile.announcement();
let updater_status = auto_updater.as_ref().and_then(|updater| {
let updater = updater.read(cx);
(!updater.idle()).then(|| updater.status())
});
let staged_update = auto_updater
.as_ref()
.is_some_and(|updater| updater.read(cx).staged());
h_flex()
.when(!cx.theme().platform.is_mac(), |this| this.pr_2())
.gap_2()
.when_some(updater_status, |this, status| {
this.child(div().text_xs().italic().child(status))
})
.when(staged_update, |this| {
this.child(
Button::new("restart-to-update")
.label("Restart to Update")
.tooltip("Quit and relaunch into the installed update")
.small()
.ghost()
.on_click(|_event, _window, cx| {
if let Some(auto_updater) = AutoUpdater::try_global(cx) {
auto_updater.update(cx, |this, cx| this.restart(cx));
}
}),
)
})
.when(nip4e_enabled, |this| {
this.child(
Button::new("key")
.icon(IconName::UserKey)
.tooltip("Decoupled encryption key")
.small()
.ghost()
.dropdown_menu(move |this, _window, _cx| {
this.min_w(px(260.))
.label("Encryption Key")
.when_some(announcement.as_ref(), |this, announcement| {
let name = announcement.client_name();
let pkey = shorten_pubkey(announcement.public_key(), 8);
this.item(PopupMenuItem::element(move |_window, cx| {
h_flex()
.gap_1()
.text_sm()
.child(
Icon::new(IconName::Device)
.small()
.text_color(cx.theme().icon_muted),
)
.child(name.clone())
}))
.item(
PopupMenuItem::element(move |_window, cx| {
h_flex()
.gap_1()
.text_sm()
.child(
Icon::new(IconName::UserKey)
.small()
.text_color(cx.theme().icon_muted),
)
.child(SharedString::from(pkey.clone()))
}),
)
})
.separator()
.menu_with_icon(
"Backup",
IconName::Shield,
Box::new(Command::BackupEncryption),
)
.menu_with_icon(
"Restore from secret key",
IconName::Usb,
Box::new(Command::ImportEncryption),
)
.separator()
.menu_with_icon(
"Reload",
IconName::Refresh,
Box::new(Command::RefreshEncryption),
)
.menu_with_icon(
"Reset",
IconName::Warning,
Box::new(Command::ResetEncryption),
)
}),
)
})
.child(
Button::new("inbox")
.icon(IconName::Inbox)
.small()
.ghost()
.dropdown_menu(move |this, _window, cx| {
let urls: Vec<(SharedString, SharedString)> = profile
.messaging_relays()
.iter()
.map(|url| {
(
SharedString::from(url.to_string()),
chat.read(cx).count_messages(url).to_string().into(),
)
})
.collect();
// Header
let menu = this.min_w(px(260.)).label("Messaging Relays");
// Content
let menu = urls.into_iter().fold(menu, |this, (url, count)| {
this.item(PopupMenuItem::element(move |_window, cx| {
h_flex()
.px_1()
.w_full()
.text_sm()
.justify_between()
.child(url.clone())
.child(
div()
.text_xs()
.text_color(cx.theme().text_muted)
.child(count.clone()),
)
}))
});
// Footer
menu.separator()
.menu_with_icon(
"Manage gossip relays",
IconName::Relay,
Box::new(Command::ShowRelayList),
)
.menu_with_icon(
"Manage messaging relays",
IconName::Relay,
Box::new(Command::ShowMessaging),
)
.separator()
.menu_with_icon(
"Reload",
IconName::Refresh,
Box::new(Command::RefreshMessagingRelays),
)
}),
)
.into_any_element()
}
}
impl Render for Workspace {
@@ -685,9 +511,6 @@ impl Render for Workspace {
let modal_layer = Root::render_modal_layer(window, cx);
let notification_layer = Root::render_notification_layer(window, cx);
// Render the title bar chrome
self.title_bar_chrome.set_trailing(Self::titlebar_right);
div()
.id("workspace")
.on_action(cx.listener(Self::on_command))
+370 -322
View File
@@ -7,10 +7,10 @@ use common::TimestampExt;
use community::{ChannelId, Community, CommunityEvent, CommunityRegistry};
use gpui::prelude::FluentBuilder;
use gpui::{
AnyElement, App, Context, ElementId, Entity, EventEmitter, FocusHandle, Focusable,
AnyElement, App, Context, Div, ElementId, Entity, EventEmitter, FocusHandle, Focusable,
InteractiveElement, IntoElement, ObjectFit, ParentElement, Render, ScrollHandle, SharedString,
StatefulInteractiveElement, Styled, StyledImage, Subscription, UniformListScrollHandle,
WeakEntity, Window, div, img, px, retain_all, uniform_list,
Stateful, StatefulInteractiveElement, Styled, StyledImage, Subscription,
UniformListScrollHandle, WeakEntity, Window, div, img, px, retain_all, uniform_list,
};
use nostr_sdk::prelude::*;
use person::PersonRegistry;
@@ -32,8 +32,8 @@ use ui::{
};
use crate::Command;
use crate::dialogs::import;
mod onboarding;
mod tab;
mod tree;
@@ -51,11 +51,9 @@ pub struct Sidebar {
active_tab: SidebarTab,
/// The community the sidebar is browsing, if any
community: Option<WeakEntity<Community>>,
/// Expanded state of the community's sections
channels_open: bool,
admins_open: bool,
members_open: bool,
/// Whether there are new chat requests
new_requests: bool,
_subscriptions: SmallVec<[Subscription; 4]>,
}
@@ -64,7 +62,6 @@ impl Sidebar {
pub fn new(window: &mut Window, dock: WeakEntity<DockArea>, cx: &mut Context<Self>) -> Self {
let chat = ChatRegistry::global(cx);
let communities = CommunityRegistry::global(cx);
let nostr = NostrRegistry::global(cx);
let mut subscriptions = smallvec![];
@@ -82,16 +79,11 @@ impl Sidebar {
window,
|_this, _communities, event, window, cx| {
if let CommunityEvent::Error(error) = event {
window
.push_notification(Notification::error(error.clone()).autohide(false), cx);
window.push_notification(Notification::error(error.clone()), cx);
}
},
));
subscriptions.push(cx.observe(&nostr, |_this, _nostr, cx| cx.notify()));
subscriptions.push(cx.observe(&communities, |_this, _communities, cx| cx.notify()));
Self {
focus_handle: cx.focus_handle(),
scroll_handles: [
@@ -115,7 +107,6 @@ impl Sidebar {
if self.active_tab == tab {
return;
}
self.active_tab = tab;
cx.notify();
}
@@ -164,11 +155,11 @@ impl Sidebar {
cx.notify();
}
fn render_user(&self, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
fn render_user(&self, cx: &mut Context<Self>) -> Stateful<Div> {
let nostr = NostrRegistry::global(cx);
let current_user = nostr.read(cx).current_user();
let mut row = h_flex()
h_flex()
.id("sidebar-user")
.w_full()
.h(TABBAR_HEIGHT)
@@ -178,16 +169,15 @@ impl Sidebar {
.px_2()
.when(cfg!(target_os = "macos"), |this| {
this.pl(px(TRAFFIC_LIGHT_PADDING))
});
if let Some(public_key) = current_user.as_ref() {
})
.when_some(current_user.as_ref(), |this, public_key| {
let persons = PersonRegistry::global(cx);
let profile = persons.read(cx).get(public_key, cx);
let avatar = profile.avatar();
let avatar_seed = profile.avatar_seed();
let name = profile.name();
row = row.child(
this.child(
Button::new("current-user")
.child(
Avatar::new(avatar.clone())
@@ -217,7 +207,11 @@ impl Sidebar {
.child(name.clone())
}))
.separator()
.menu_with_icon("Inbox", IconName::Inbox, Box::new(Command::ShowInbox))
.menu_with_icon(
"Inbox",
IconName::Inbox,
Box::new(Command::ShowInbox),
)
.menu_with_icon(
"Search",
IconName::Search,
@@ -238,7 +232,11 @@ impl Sidebar {
IconName::UserKey,
Box::new(Command::ShowBackup),
)
.menu_with_icon("Themes", IconName::Sun, Box::new(Command::ToggleTheme))
.menu_with_icon(
"Themes",
IconName::Sun,
Box::new(Command::ToggleTheme),
)
.when(AutoUpdater::is_available(cx), |this| {
this.separator().menu_with_icon(
"Check for Updates",
@@ -252,11 +250,14 @@ impl Sidebar {
Box::new(Command::ShowSettings),
)
}),
);
}
if self.community.is_some() {
row = row.child(div().flex_1()).child(
)
})
.child(div().flex_1())
.when_some(AutoUpdater::try_global(cx), |this, updater| {
this.child(self.render_updater(updater, cx))
})
.when(self.community.is_some(), |this| {
this.child(
Button::new("sidebar-back")
.icon(IconName::ArrowLeft)
.tooltip("Back")
@@ -265,300 +266,42 @@ impl Sidebar {
.on_click(cx.listener(|this, _event, _window, cx| {
this.reset_community(cx);
})),
);
}
title_bar_drag_handlers(row, window, cx).into_any_element()
}
}
fn recent_communities(cx: &App) -> Vec<Entity<Community>> {
const LIMIT: usize = 3;
let registry = CommunityRegistry::global(cx);
let communities = registry.read(cx).communities();
let recent = AppSettings::get_recent_communities(cx);
let mut rows: Vec<Entity<Community>> = recent
.iter()
.filter_map(|id| {
communities
.iter()
.find(|community| community.read(cx).id().to_hex() == *id)
)
})
.take(LIMIT)
.cloned()
.collect();
if rows.is_empty() {
rows = communities.iter().take(LIMIT).cloned().collect();
}
rows
}
fn render_updater(&self, updater: Entity<AutoUpdater>, cx: &mut App) -> AnyElement {
let status = updater.read(cx).status();
let staged = updater.read(cx).staged();
fn rows_for(tab: SidebarTab, cx: &App) -> Vec<SidebarRow> {
match tab {
SidebarTab::Recents => {
let chat = ChatRegistry::global(cx);
let rooms = chat.read(cx).rooms(&RoomKind::Ongoing, cx);
let registry = CommunityRegistry::global(cx);
let community_count = registry.read(cx).communities().len();
let communities = recent_communities(cx);
if communities.is_empty() && rooms.is_empty() {
return vec![SidebarRow::Hint {
text: "Nothing recent yet".into(),
}];
}
let mut rows = Vec::new();
if !communities.is_empty() {
rows.push(SidebarRow::Section {
label: "Communities".into(),
count: community_count,
});
rows.extend(
communities
.into_iter()
.map(|community| SidebarRow::Community { community }),
);
rows.push(SidebarRow::Action {
label: "Show all communities".into(),
tab: SidebarTab::Communities,
});
}
if !rooms.is_empty() {
rows.push(SidebarRow::Section {
label: "Chats".into(),
count: rooms.len(),
});
rows.extend(
rooms
.into_iter()
.take(5)
.map(|room| SidebarRow::Room { room }),
);
rows.push(SidebarRow::Action {
label: "Show all chats".into(),
tab: SidebarTab::Chats,
});
}
rows
}
SidebarTab::Chats => {
let chat = ChatRegistry::global(cx);
let chat = chat.read(cx);
let messages = chat.rooms(&RoomKind::Ongoing, cx);
let mut rows = vec![SidebarRow::Section {
label: "Chats".into(),
count: messages.len(),
}];
if messages.is_empty() {
rows.push(SidebarRow::Hint {
text: "No conversations yet".into(),
});
} else {
rows.extend(messages.into_iter().map(|room| SidebarRow::Room { room }));
}
rows
}
SidebarTab::Communities => {
let registry = CommunityRegistry::global(cx);
let communities = registry.read(cx).communities();
let mut rows = vec![SidebarRow::Section {
label: "Communities".into(),
count: communities.len(),
}];
if communities.is_empty() {
rows.push(SidebarRow::Hint {
text: "No communities yet".into(),
});
} else {
rows.extend(
communities
.iter()
.cloned()
.map(|community| SidebarRow::Community { community }),
);
}
rows
}
}
}
fn render_rows(range: Range<usize>, rows: &[SidebarRow], cx: &Context<Sidebar>) -> Vec<AnyElement> {
rows.get(range.clone())
.into_iter()
.flatten()
.enumerate()
.map(|(offset, row)| {
let index = range.start + offset;
match row {
SidebarRow::Section { label, count } => TreeRow::new(
ElementId::NamedInteger("tree-row".into(), index as u64),
TreeRowKind::Section,
label.clone(),
)
.count(*count)
.into_any_element(),
SidebarRow::Room { room } => {
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 room_clone = room.clone();
let handler = cx.listener(move |this, _event, window, cx| {
this.open_room(room_clone.clone(), window, cx);
});
TreeRow::new(
ElementId::NamedInteger("tree-row".into(), index as u64),
TreeRowKind::Room,
name,
)
.avatar(seed)
.picture(picture)
.created_at(created_at)
.on_click(handler)
.into_any_element()
}
SidebarRow::Community { community } => {
let name = community.read(cx).name();
let seed = community.read(cx).id().to_hex();
let picture = community.read(cx).icon();
let community = community.clone();
TreeRow::new(
ElementId::NamedInteger("tree-row".into(), index as u64),
TreeRowKind::Community,
name,
)
.avatar(seed)
.picture(picture)
.on_click(cx.listener(move |this, _event, window, cx| {
this.open_community(community.clone(), window, cx);
}))
.into_any_element()
}
SidebarRow::Action { label, tab } => {
let tab = *tab;
TreeRow::new(
ElementId::NamedInteger("tree-row".into(), index as u64),
TreeRowKind::Action,
label.clone(),
)
.icon(IconName::ArrowRight)
.on_click(cx.listener(move |this, _event, _window, cx| {
this.select_tab(tab, cx);
}))
.into_any_element()
}
SidebarRow::Hint { text } => TreeRow::new(
ElementId::NamedInteger("tree-row".into(), index as u64),
TreeRowKind::Hint,
text.clone(),
)
.into_any_element(),
}
})
.collect()
}
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 (logged_in, ready) = {
let nostr = NostrRegistry::global(cx);
(
nostr.read(cx).current_user().is_some(),
nostr.read(cx).ready(),
)
};
if !logged_in {
if !ready {
return v_flex()
.size_full()
.bg(cx.theme().surface_background)
.border_r_1()
.border_color(cx.theme().border_variant)
.into_any_element();
}
return onboarding::render(window, cx).into_any_element();
}
let community = self
.community
.clone()
.and_then(|community| community.upgrade());
if community.is_none() {
self.community = None;
}
v_flex()
.image_cache(retain_all("sidebar"))
.size_full()
.relative()
h_flex()
.gap_2()
.bg(cx.theme().surface_background)
.border_r_1()
.border_color(cx.theme().border_variant)
.child(self.render_user(window, cx))
.map(|this| match community {
Some(community) => this.child(self.render_community(community, cx)),
None => this.child(self.render_tabs(cx)),
.text_xs()
.child(status)
.when(staged, |this| {
this.child(
Button::new("restart-to-update")
.icon(IconName::ArrowDownCircle)
.tooltip("Quit and relaunch into the installed update")
.small()
.ghost()
.on_click(move |_, _window, cx| {
updater.update(cx, |this, cx| {
this.restart(cx);
});
}),
)
})
.into_any_element()
}
}
impl Sidebar {
fn render_tabs(&mut self, cx: &mut Context<Self>) -> AnyElement {
let chat = ChatRegistry::global(cx);
let loading = chat.read(cx).loading();
let sidebar = cx.entity().downgrade();
let active_tab = self.active_tab;
let rows = Rc::new(rows_for(active_tab, cx));
let rows = Rc::new(self.rows_for(active_tab, cx));
let scroll_handle = &self.scroll_handles[active_tab.index()];
v_flex()
@@ -653,8 +396,8 @@ impl Sidebar {
uniform_list(
active_tab.list_id(),
rows.len(),
cx.processor(move |_this, range, _window, cx| {
render_rows(range, rows.as_slice(), cx)
cx.processor(move |this, range, _window, cx| {
this.render_rows(range, rows.as_slice(), cx)
}),
)
.track_scroll(scroll_handle)
@@ -742,7 +485,7 @@ impl Sidebar {
.pb_2()
.track_scroll(&self.community_scroll)
.overflow_y_scroll()
.child(section_row(
.child(self.section_row(
"channels",
"Channels",
channels.len(),
@@ -752,10 +495,10 @@ impl Sidebar {
))
.when(self.channels_open, |this| {
this.children(channels.into_iter().map(|(id, name, private)| {
channel_row(id, name, private, active == Some(id), &community, cx)
self.channel_row(id, name, private, active == Some(id), &community, cx)
}))
})
.child(section_row(
.child(self.section_row(
"admins",
"Admins",
admins.len(),
@@ -767,10 +510,10 @@ impl Sidebar {
this.children(
admins
.iter()
.map(|public_key| member_row("community-admin", *public_key, cx)),
.map(|public_key| self.member_row("community-admin", *public_key, cx)),
)
})
.child(section_row(
.child(self.section_row(
"members",
"Members",
members.len(),
@@ -782,7 +525,7 @@ impl Sidebar {
this.children(
members
.iter()
.map(|public_key| member_row("member", *public_key, cx)),
.map(|public_key| self.member_row("member", *public_key, cx)),
)
});
@@ -806,16 +549,226 @@ impl Sidebar {
.child(Scrollbar::vertical(&self.community_scroll))
.into_any_element()
}
}
fn section_row(
fn recent_communities(&self, cx: &App) -> Vec<Entity<Community>> {
const LIMIT: usize = 3;
let registry = CommunityRegistry::global(cx);
let communities = registry.read(cx).communities();
let recent = AppSettings::get_recent_communities(cx);
let mut rows: Vec<Entity<Community>> = recent
.iter()
.filter_map(|id| {
communities
.iter()
.find(|community| community.read(cx).id().to_hex() == *id)
})
.take(LIMIT)
.cloned()
.collect();
if rows.is_empty() {
rows = communities.iter().take(LIMIT).cloned().collect();
}
rows
}
fn rows_for(&self, tab: SidebarTab, cx: &App) -> Vec<SidebarRow> {
match tab {
SidebarTab::Recents => {
let chat = ChatRegistry::global(cx);
let rooms = chat.read(cx).rooms(&RoomKind::Ongoing, cx);
let registry = CommunityRegistry::global(cx);
let community_count = registry.read(cx).communities().len();
let communities = self.recent_communities(cx);
if communities.is_empty() && rooms.is_empty() {
return vec![SidebarRow::Hint {
text: "Nothing recent yet".into(),
}];
}
let mut rows = Vec::new();
if !communities.is_empty() {
rows.push(SidebarRow::Section {
label: "Communities".into(),
count: community_count,
});
rows.extend(
communities
.into_iter()
.map(|community| SidebarRow::Community { community }),
);
rows.push(SidebarRow::Action {
label: "Show all communities".into(),
tab: SidebarTab::Communities,
});
}
if !rooms.is_empty() {
rows.push(SidebarRow::Section {
label: "Chats".into(),
count: rooms.len(),
});
rows.extend(
rooms
.into_iter()
.take(5)
.map(|room| SidebarRow::Room { room }),
);
rows.push(SidebarRow::Action {
label: "Show all chats".into(),
tab: SidebarTab::Chats,
});
}
rows
}
SidebarTab::Chats => {
let chat = ChatRegistry::global(cx);
let chat = chat.read(cx);
let messages = chat.rooms(&RoomKind::Ongoing, cx);
let mut rows = vec![SidebarRow::Section {
label: "Chats".into(),
count: messages.len(),
}];
if messages.is_empty() {
rows.push(SidebarRow::Hint {
text: "No conversations yet".into(),
});
} else {
rows.extend(messages.into_iter().map(|room| SidebarRow::Room { room }));
}
rows
}
SidebarTab::Communities => {
let registry = CommunityRegistry::global(cx);
let communities = registry.read(cx).communities();
let mut rows = vec![SidebarRow::Section {
label: "Communities".into(),
count: communities.len(),
}];
if communities.is_empty() {
rows.push(SidebarRow::Hint {
text: "No communities yet".into(),
});
} else {
rows.extend(
communities
.iter()
.cloned()
.map(|community| SidebarRow::Community { community }),
);
}
rows
}
}
}
fn render_rows(
&self,
range: Range<usize>,
rows: &[SidebarRow],
cx: &Context<Sidebar>,
) -> Vec<AnyElement> {
rows.get(range.clone())
.into_iter()
.flatten()
.enumerate()
.map(|(offset, row)| {
let index = range.start + offset;
match row {
SidebarRow::Section { label, count } => TreeRow::new(
ElementId::NamedInteger("tree-row".into(), index as u64),
TreeRowKind::Section,
label.clone(),
)
.count(*count)
.into_any_element(),
SidebarRow::Room { room } => {
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 room_clone = room.clone();
let handler = cx.listener(move |this, _event, window, cx| {
this.open_room(room_clone.clone(), window, cx);
});
TreeRow::new(
ElementId::NamedInteger("tree-row".into(), index as u64),
TreeRowKind::Room,
name,
)
.avatar(seed)
.picture(picture)
.created_at(created_at)
.on_click(handler)
.into_any_element()
}
SidebarRow::Community { community } => {
let name = community.read(cx).name();
let seed = community.read(cx).id().to_hex();
let picture = community.read(cx).icon();
let community = community.clone();
TreeRow::new(
ElementId::NamedInteger("tree-row".into(), index as u64),
TreeRowKind::Community,
name,
)
.avatar(seed)
.picture(picture)
.on_click(cx.listener(move |this, _event, window, cx| {
this.open_community(community.clone(), window, cx);
}))
.into_any_element()
}
SidebarRow::Action { label, tab } => {
let tab = *tab;
TreeRow::new(
ElementId::NamedInteger("tree-row".into(), index as u64),
TreeRowKind::Action,
label.clone(),
)
.icon(IconName::ArrowRight)
.on_click(cx.listener(move |this, _event, _window, cx| {
this.select_tab(tab, cx);
}))
.into_any_element()
}
SidebarRow::Hint { text } => TreeRow::new(
ElementId::NamedInteger("tree-row".into(), index as u64),
TreeRowKind::Hint,
text.clone(),
)
.into_any_element(),
}
})
.collect()
}
fn section_row(
&self,
id: &'static str,
label: &'static str,
count: usize,
open: bool,
toggle: impl Fn(&mut Sidebar) + 'static,
cx: &mut Context<Sidebar>,
) -> AnyElement {
) -> AnyElement {
div()
.flex_shrink_0()
.child(
@@ -832,16 +785,17 @@ fn section_row(
})),
)
.into_any_element()
}
}
fn channel_row(
fn channel_row(
&self,
id: ChannelId,
name: String,
private: bool,
selected: bool,
community: &Entity<Community>,
cx: &mut Context<Sidebar>,
) -> AnyElement {
) -> AnyElement {
let community = community.clone();
div()
@@ -868,9 +822,9 @@ fn channel_row(
)
.when(selected, |this| this.bg(cx.theme().ghost_element_active))
.into_any_element()
}
}
fn member_row(prefix: &str, public_key: PublicKey, cx: &App) -> AnyElement {
fn member_row(&self, prefix: &str, public_key: PublicKey, cx: &App) -> AnyElement {
let persons = PersonRegistry::global(cx);
let person = persons.read(cx).get(&public_key, cx);
@@ -889,4 +843,98 @@ fn member_row(prefix: &str, public_key: PublicKey, cx: &App) -> AnyElement {
.picture(person.avatar()),
)
.into_any_element()
}
}
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 logged_in = { nostr.read(cx).current_user().is_some() };
let community = self
.community
.clone()
.and_then(|community| community.upgrade());
v_flex()
.image_cache(retain_all("sidebar"))
.size_full()
.relative()
.gap_2()
.bg(cx.theme().surface_background)
.border_r_1()
.border_color(cx.theme().border_variant)
.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(
v_flex()
.size_full()
.justify_end()
.gap_4()
.p_4()
.child(
v_flex().child(div().font_semibold().child("Coop")).child(
div()
.text_xs()
.text_color(cx.theme().text_muted)
.child("TODO"),
),
)
.child(
Button::new("import-identity")
.label("Import identity")
.primary()
.font_semibold()
.h_8()
.w_full()
.on_click(|_, window, cx| {
import::open(window, cx);
}),
),
)
})
.child(title_bar_drag_handlers(self.render_user(cx), window, cx))
.map(|this| match community {
Some(community) => this.child(self.render_community(community, cx)),
None => this.child(self.render_tabs(cx)),
})
.into_any_element()
}
}
@@ -1,74 +0,0 @@
use gpui::{App, InteractiveElement, IntoElement, ParentElement, Styled, Window, div, svg};
use theme::{ActiveTheme, TABBAR_HEIGHT};
use ui::button::{Button, ButtonVariants};
use ui::{StyledExt, h_flex, title_bar_drag_handlers, v_flex};
use crate::dialogs::import;
const TITLE: &str = "Welcome to Coop!";
const DESCRIPTION: &str = "Chat Freely, Stay Private on Nostr.";
pub(super) fn render(window: &mut Window, cx: &mut App) -> impl IntoElement {
v_flex()
.size_full()
.relative()
.bg(cx.theme().surface_background)
.child(title_bar_drag_handlers(
div()
.id("onboarding-drag")
.absolute()
.top_0()
.left_0()
.h(TABBAR_HEIGHT)
.w_full(),
window,
cx,
))
.child(
v_flex()
.size_full()
.justify_end()
.gap_4()
.p_4()
.child(
h_flex()
.gap_2()
.child(
svg()
.path("brand/coop.svg")
.size_8()
.text_color(cx.theme().icon_muted),
)
.child(
v_flex().child(div().font_semibold().child(TITLE)).child(
div()
.text_xs()
.text_color(cx.theme().text_muted)
.child(DESCRIPTION),
),
),
)
.child(
v_flex()
.gap_2()
.w_full()
.child(
Button::new("join-now")
.label("Join now")
.primary()
.font_semibold()
.h_8()
.w_full(),
)
.child(
Button::new("import-identity")
.label("Import identity")
.secondary()
.font_semibold()
.h_8()
.w_full()
.on_click(|_event, window, cx| import::open(window, cx)),
),
),
)
}