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 { pub trait Panel: EventEmitter<PanelEvent> + Render + Focusable {
/// The name of the panel used to serialize, deserialize and identify the panel. /// 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; fn panel_id(&self) -> SharedString;
/// The title of the panel /// The title of the panel
+2
View File
@@ -22,6 +22,7 @@ impl<T: IconNamed> From<T> for Icon {
pub enum IconName { pub enum IconName {
ArrowLeft, ArrowLeft,
ArrowRight, ArrowRight,
ArrowDownCircle,
Boom, Boom,
Book, Book,
ChevronDown, ChevronDown,
@@ -99,6 +100,7 @@ impl IconNamed for IconName {
match self { match self {
Self::ArrowLeft => "icons/arrow-left.svg", Self::ArrowLeft => "icons/arrow-left.svg",
Self::ArrowRight => "icons/arrow-right.svg", Self::ArrowRight => "icons/arrow-right.svg",
Self::ArrowDownCircle => "icons/arrow-down-circle.svg",
Self::Boom => "icons/boom.svg", Self::Boom => "icons/boom.svg",
Self::Book => "icons/book.svg", Self::Book => "icons/book.svg",
Self::ChevronDown => "icons/chevron-down.svg", Self::ChevronDown => "icons/chevron-down.svg",
+4 -181
View File
@@ -1,4 +1,3 @@
use std::rc::Rc;
use std::sync::Arc; use std::sync::Arc;
use ::settings::AppSettings; use ::settings::AppSettings;
@@ -7,22 +6,19 @@ use auto_update::AutoUpdater;
use chat::{ChatEvent, ChatRegistry}; use chat::{ChatEvent, ChatRegistry};
use common::download_dir; use common::download_dir;
use device::{DeviceEvent, DeviceRegistry}; use device::{DeviceEvent, DeviceRegistry};
use gpui::prelude::FluentBuilder;
use gpui::{ use gpui::{
Action, AnyElement, App, AppContext, Context, Entity, InteractiveElement, IntoElement, Action, App, AppContext, Context, Entity, InteractiveElement, IntoElement, ParentElement,
ParentElement, Render, SharedString, Styled, Subscription, Task, Window, div, px, Render, SharedString, Styled, Subscription, Task, Window, div, px,
}; };
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use person::{PersonRegistry, shorten_pubkey};
use serde::Deserialize; use serde::Deserialize;
use smallvec::{SmallVec, smallvec}; use smallvec::{SmallVec, smallvec};
use state::{NostrRegistry, StateEvent}; use state::{NostrRegistry, StateEvent};
use theme::{ActiveTheme, SIDEBAR_WIDTH, Theme, ThemeRegistry}; use theme::{ActiveTheme, SIDEBAR_WIDTH, Theme, ThemeRegistry};
use ui::button::{Button, ButtonVariants}; use ui::button::{Button, ButtonVariants};
use ui::dock::{self, DockArea, DockLayout, DockPlacement, Panel, PanelHandle}; use ui::dock::{self, DockArea, DockLayout, DockPlacement, Panel, PanelHandle};
use ui::menu::{DropdownMenu, PopupMenuItem};
use ui::notification::{Notification, NotificationKind}; 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::restore::RestoreEncryption;
use crate::dialogs::{new_chat, new_community, settings}; use crate::dialogs::{new_chat, new_community, settings};
@@ -69,7 +65,6 @@ enum Command {
pub struct Workspace { pub struct Workspace {
dock: Entity<DockArea>, dock: Entity<DockArea>,
title_bar_chrome: Rc<dock::TitleBarChrome>,
/// Async tasks /// Async tasks
tasks: Vec<Task<Result<(), Error>>>, tasks: Vec<Task<Result<(), Error>>>,
/// Event subscriptions /// Event subscriptions
@@ -82,7 +77,7 @@ impl Workspace {
let device = DeviceRegistry::global(cx); let device = DeviceRegistry::global(cx);
let nostr = NostrRegistry::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 sidebar = cx.new(|cx| Sidebar::new(window, dock.downgrade(), cx));
let mut subscriptions = smallvec![]; let mut subscriptions = smallvec![];
@@ -207,7 +202,6 @@ impl Workspace {
Self { Self {
dock, dock,
title_bar_chrome,
tasks: vec![], tasks: vec![],
_subscriptions: subscriptions, _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 { impl Render for Workspace {
@@ -685,9 +511,6 @@ impl Render for Workspace {
let modal_layer = Root::render_modal_layer(window, cx); let modal_layer = Root::render_modal_layer(window, cx);
let notification_layer = Root::render_notification_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() div()
.id("workspace") .id("workspace")
.on_action(cx.listener(Self::on_command)) .on_action(cx.listener(Self::on_command))
+363 -315
View File
@@ -7,10 +7,10 @@ use common::TimestampExt;
use community::{ChannelId, Community, CommunityEvent, CommunityRegistry}; use community::{ChannelId, Community, CommunityEvent, CommunityRegistry};
use gpui::prelude::FluentBuilder; use gpui::prelude::FluentBuilder;
use gpui::{ 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, InteractiveElement, IntoElement, ObjectFit, ParentElement, Render, ScrollHandle, SharedString,
StatefulInteractiveElement, Styled, StyledImage, Subscription, UniformListScrollHandle, Stateful, StatefulInteractiveElement, Styled, StyledImage, Subscription,
WeakEntity, Window, div, img, px, retain_all, uniform_list, UniformListScrollHandle, WeakEntity, Window, div, img, px, retain_all, uniform_list,
}; };
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use person::PersonRegistry; use person::PersonRegistry;
@@ -32,8 +32,8 @@ use ui::{
}; };
use crate::Command; use crate::Command;
use crate::dialogs::import;
mod onboarding;
mod tab; mod tab;
mod tree; mod tree;
@@ -51,11 +51,9 @@ pub struct Sidebar {
active_tab: SidebarTab, active_tab: SidebarTab,
/// The community the sidebar is browsing, if any /// The community the sidebar is browsing, if any
community: Option<WeakEntity<Community>>, community: Option<WeakEntity<Community>>,
/// Expanded state of the community's sections
channels_open: bool, channels_open: bool,
admins_open: bool, admins_open: bool,
members_open: bool, members_open: bool,
/// Whether there are new chat requests
new_requests: bool, new_requests: bool,
_subscriptions: SmallVec<[Subscription; 4]>, _subscriptions: SmallVec<[Subscription; 4]>,
} }
@@ -64,7 +62,6 @@ impl Sidebar {
pub fn new(window: &mut Window, dock: WeakEntity<DockArea>, cx: &mut Context<Self>) -> Self { pub fn new(window: &mut Window, dock: WeakEntity<DockArea>, cx: &mut Context<Self>) -> Self {
let chat = ChatRegistry::global(cx); let chat = ChatRegistry::global(cx);
let communities = CommunityRegistry::global(cx); let communities = CommunityRegistry::global(cx);
let nostr = NostrRegistry::global(cx);
let mut subscriptions = smallvec![]; let mut subscriptions = smallvec![];
@@ -82,16 +79,11 @@ impl Sidebar {
window, window,
|_this, _communities, event, window, cx| { |_this, _communities, event, window, cx| {
if let CommunityEvent::Error(error) = event { if let CommunityEvent::Error(error) = event {
window window.push_notification(Notification::error(error.clone()), cx);
.push_notification(Notification::error(error.clone()).autohide(false), cx);
} }
}, },
)); ));
subscriptions.push(cx.observe(&nostr, |_this, _nostr, cx| cx.notify()));
subscriptions.push(cx.observe(&communities, |_this, _communities, cx| cx.notify()));
Self { Self {
focus_handle: cx.focus_handle(), focus_handle: cx.focus_handle(),
scroll_handles: [ scroll_handles: [
@@ -115,7 +107,6 @@ impl Sidebar {
if self.active_tab == tab { if self.active_tab == tab {
return; return;
} }
self.active_tab = tab; self.active_tab = tab;
cx.notify(); cx.notify();
} }
@@ -164,11 +155,11 @@ impl Sidebar {
cx.notify(); 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 nostr = NostrRegistry::global(cx);
let current_user = nostr.read(cx).current_user(); let current_user = nostr.read(cx).current_user();
let mut row = h_flex() h_flex()
.id("sidebar-user") .id("sidebar-user")
.w_full() .w_full()
.h(TABBAR_HEIGHT) .h(TABBAR_HEIGHT)
@@ -178,16 +169,15 @@ impl Sidebar {
.px_2() .px_2()
.when(cfg!(target_os = "macos"), |this| { .when(cfg!(target_os = "macos"), |this| {
this.pl(px(TRAFFIC_LIGHT_PADDING)) this.pl(px(TRAFFIC_LIGHT_PADDING))
}); })
.when_some(current_user.as_ref(), |this, public_key| {
if let Some(public_key) = current_user.as_ref() {
let persons = PersonRegistry::global(cx); let persons = PersonRegistry::global(cx);
let profile = persons.read(cx).get(public_key, cx); let profile = persons.read(cx).get(public_key, cx);
let avatar = profile.avatar(); let avatar = profile.avatar();
let avatar_seed = profile.avatar_seed(); let avatar_seed = profile.avatar_seed();
let name = profile.name(); let name = profile.name();
row = row.child( this.child(
Button::new("current-user") Button::new("current-user")
.child( .child(
Avatar::new(avatar.clone()) Avatar::new(avatar.clone())
@@ -217,7 +207,11 @@ impl Sidebar {
.child(name.clone()) .child(name.clone())
})) }))
.separator() .separator()
.menu_with_icon("Inbox", IconName::Inbox, Box::new(Command::ShowInbox)) .menu_with_icon(
"Inbox",
IconName::Inbox,
Box::new(Command::ShowInbox),
)
.menu_with_icon( .menu_with_icon(
"Search", "Search",
IconName::Search, IconName::Search,
@@ -238,7 +232,11 @@ impl Sidebar {
IconName::UserKey, IconName::UserKey,
Box::new(Command::ShowBackup), 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| { .when(AutoUpdater::is_available(cx), |this| {
this.separator().menu_with_icon( this.separator().menu_with_icon(
"Check for Updates", "Check for Updates",
@@ -252,11 +250,14 @@ impl Sidebar {
Box::new(Command::ShowSettings), Box::new(Command::ShowSettings),
) )
}), }),
); )
} })
.child(div().flex_1())
if self.community.is_some() { .when_some(AutoUpdater::try_global(cx), |this, updater| {
row = row.child(div().flex_1()).child( this.child(self.render_updater(updater, cx))
})
.when(self.community.is_some(), |this| {
this.child(
Button::new("sidebar-back") Button::new("sidebar-back")
.icon(IconName::ArrowLeft) .icon(IconName::ArrowLeft)
.tooltip("Back") .tooltip("Back")
@@ -265,300 +266,42 @@ impl Sidebar {
.on_click(cx.listener(|this, _event, _window, cx| { .on_click(cx.listener(|this, _event, _window, cx| {
this.reset_community(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> { h_flex()
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()
.gap_2() .gap_2()
.bg(cx.theme().surface_background) .text_xs()
.border_r_1() .child(status)
.border_color(cx.theme().border_variant) .when(staged, |this| {
.child(self.render_user(window, cx)) this.child(
.map(|this| match community { Button::new("restart-to-update")
Some(community) => this.child(self.render_community(community, cx)), .icon(IconName::ArrowDownCircle)
None => this.child(self.render_tabs(cx)), .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() .into_any_element()
} }
}
impl Sidebar {
fn render_tabs(&mut self, cx: &mut Context<Self>) -> AnyElement { fn render_tabs(&mut self, cx: &mut Context<Self>) -> AnyElement {
let chat = ChatRegistry::global(cx); let chat = ChatRegistry::global(cx);
let loading = chat.read(cx).loading(); let loading = chat.read(cx).loading();
let sidebar = cx.entity().downgrade(); let sidebar = cx.entity().downgrade();
let active_tab = self.active_tab; 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()]; let scroll_handle = &self.scroll_handles[active_tab.index()];
v_flex() v_flex()
@@ -653,8 +396,8 @@ impl Sidebar {
uniform_list( uniform_list(
active_tab.list_id(), active_tab.list_id(),
rows.len(), rows.len(),
cx.processor(move |_this, range, _window, cx| { cx.processor(move |this, range, _window, cx| {
render_rows(range, rows.as_slice(), cx) this.render_rows(range, rows.as_slice(), cx)
}), }),
) )
.track_scroll(scroll_handle) .track_scroll(scroll_handle)
@@ -742,7 +485,7 @@ impl Sidebar {
.pb_2() .pb_2()
.track_scroll(&self.community_scroll) .track_scroll(&self.community_scroll)
.overflow_y_scroll() .overflow_y_scroll()
.child(section_row( .child(self.section_row(
"channels", "channels",
"Channels", "Channels",
channels.len(), channels.len(),
@@ -752,10 +495,10 @@ impl Sidebar {
)) ))
.when(self.channels_open, |this| { .when(self.channels_open, |this| {
this.children(channels.into_iter().map(|(id, name, private)| { 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", "Admins",
admins.len(), admins.len(),
@@ -767,10 +510,10 @@ impl Sidebar {
this.children( this.children(
admins admins
.iter() .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", "Members",
members.len(), members.len(),
@@ -782,7 +525,7 @@ impl Sidebar {
this.children( this.children(
members members
.iter() .iter()
.map(|public_key| member_row("member", *public_key, cx)), .map(|public_key| self.member_row("member", *public_key, cx)),
) )
}); });
@@ -806,9 +549,219 @@ impl Sidebar {
.child(Scrollbar::vertical(&self.community_scroll)) .child(Scrollbar::vertical(&self.community_scroll))
.into_any_element() .into_any_element()
} }
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( fn section_row(
&self,
id: &'static str, id: &'static str,
label: &'static str, label: &'static str,
count: usize, count: usize,
@@ -835,6 +788,7 @@ fn section_row(
} }
fn channel_row( fn channel_row(
&self,
id: ChannelId, id: ChannelId,
name: String, name: String,
private: bool, private: bool,
@@ -870,7 +824,7 @@ fn channel_row(
.into_any_element() .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 persons = PersonRegistry::global(cx);
let person = persons.read(cx).get(&public_key, cx); let person = persons.read(cx).get(&public_key, cx);
@@ -890,3 +844,97 @@ fn member_row(prefix: &str, public_key: PublicKey, cx: &App) -> AnyElement {
) )
.into_any_element() .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)),
),
),
)
}