diff --git a/assets/icons/arrow-down-circle.svg b/assets/icons/arrow-down-circle.svg new file mode 100644 index 00000000..fea6250e --- /dev/null +++ b/assets/icons/arrow-down-circle.svg @@ -0,0 +1,3 @@ + + + diff --git a/crates/ui/src/dock/panel.rs b/crates/ui/src/dock/panel.rs index af43c481..b4fdff01 100644 --- a/crates/ui/src/dock/panel.rs +++ b/crates/ui/src/dock/panel.rs @@ -19,9 +19,6 @@ pub enum PanelEvent { pub trait Panel: EventEmitter + 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 diff --git a/crates/ui/src/icon.rs b/crates/ui/src/icon.rs index 9b935db8..fef5db14 100644 --- a/crates/ui/src/icon.rs +++ b/crates/ui/src/icon.rs @@ -22,6 +22,7 @@ impl From 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", diff --git a/crates/workspace/src/lib.rs b/crates/workspace/src/lib.rs index 69e9a575..dff8213a 100644 --- a/crates/workspace/src/lib.rs +++ b/crates/workspace/src/lib.rs @@ -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, - title_bar_chrome: Rc, /// Async tasks tasks: Vec>>, /// 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)) diff --git a/crates/workspace/src/sidebar/mod.rs b/crates/workspace/src/sidebar/mod.rs index 55e02bc3..ef937d44 100644 --- a/crates/workspace/src/sidebar/mod.rs +++ b/crates/workspace/src/sidebar/mod.rs @@ -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>, - /// 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, cx: &mut Context) -> 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) -> AnyElement { + fn render_user(&self, cx: &mut Context) -> Stateful
{ 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,387 +169,139 @@ impl Sidebar { .px_2() .when(cfg!(target_os = "macos"), |this| { this.pl(px(TRAFFIC_LIGHT_PADDING)) - }); + }) + .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(); - if let Some(public_key) = current_user.as_ref() { - 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(); + this.child( + Button::new("current-user") + .child( + Avatar::new(avatar.clone()) + .seed(avatar_seed.clone()) + .xsmall(), + ) + .small() + .caret() + .compact() + .transparent() + .dropdown_menu(move |this, _window, cx| { + let avatar = avatar.clone(); + let avatar_seed = avatar_seed.clone(); + let name = name.clone(); - row = row.child( - Button::new("current-user") - .child( - Avatar::new(avatar.clone()) - .seed(avatar_seed.clone()) - .xsmall(), - ) - .small() - .caret() - .compact() - .transparent() - .dropdown_menu(move |this, _window, cx| { - let avatar = avatar.clone(); - let avatar_seed = avatar_seed.clone(); - let name = name.clone(); - - this.min_w(px(256.)) - .item(PopupMenuItem::element(move |_window, cx| { - h_flex() - .gap_1p5() - .text_xs() - .text_color(cx.theme().text_muted) - .child( - Avatar::new(avatar.clone()) - .seed(avatar_seed.clone()) - .xsmall(), - ) - .child(name.clone()) - })) - .separator() - .menu_with_icon("Inbox", IconName::Inbox, Box::new(Command::ShowInbox)) - .menu_with_icon( - "Search", - IconName::Search, - Box::new(Command::ShowSearch), - ) - .menu_with_icon( - "Profile", - IconName::Profile, - Box::new(Command::ShowProfile), - ) - .menu_with_icon( - "Contact List", - IconName::Book, - Box::new(Command::ShowContactList), - ) - .menu_with_icon( - "Backup", - IconName::UserKey, - Box::new(Command::ShowBackup), - ) - .menu_with_icon("Themes", IconName::Sun, Box::new(Command::ToggleTheme)) - .when(AutoUpdater::is_available(cx), |this| { - this.separator().menu_with_icon( - "Check for Updates", - IconName::Device, - Box::new(Command::Update), + this.min_w(px(256.)) + .item(PopupMenuItem::element(move |_window, cx| { + h_flex() + .gap_1p5() + .text_xs() + .text_color(cx.theme().text_muted) + .child( + Avatar::new(avatar.clone()) + .seed(avatar_seed.clone()) + .xsmall(), + ) + .child(name.clone()) + })) + .separator() + .menu_with_icon( + "Inbox", + IconName::Inbox, + Box::new(Command::ShowInbox), ) - }) - .menu_with_icon( - "Settings", - IconName::Settings, - Box::new(Command::ShowSettings), - ) - }), - ); - } - - if self.community.is_some() { - row = row.child(div().flex_1()).child( - Button::new("sidebar-back") - .icon(IconName::ArrowLeft) - .tooltip("Back") - .ghost() - .small() - .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> { - 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> = 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(tab: SidebarTab, cx: &App) -> Vec { - 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, rows: &[SidebarRow], cx: &Context) -> Vec { - 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(), + .menu_with_icon( + "Search", + IconName::Search, + Box::new(Command::ShowSearch), + ) + .menu_with_icon( + "Profile", + IconName::Profile, + Box::new(Command::ShowProfile), + ) + .menu_with_icon( + "Contact List", + IconName::Book, + Box::new(Command::ShowContactList), + ) + .menu_with_icon( + "Backup", + IconName::UserKey, + Box::new(Command::ShowBackup), + ) + .menu_with_icon( + "Themes", + IconName::Sun, + Box::new(Command::ToggleTheme), + ) + .when(AutoUpdater::is_available(cx), |this| { + this.separator().menu_with_icon( + "Check for Updates", + IconName::Device, + Box::new(Command::Update), + ) + }) + .menu_with_icon( + "Settings", + IconName::Settings, + Box::new(Command::ShowSettings), + ) + }), ) - .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(), + }) + .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") + .ghost() + .small() + .on_click(cx.listener(|this, _event, _window, cx| { + this.reset_community(cx); + })), ) - .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 render_updater(&self, updater: Entity, cx: &mut App) -> AnyElement { + let status = updater.read(cx).status(); + let staged = updater.read(cx).staged(); - fn closable(&self, _cx: &App) -> bool { - false - } - - fn zoomable(&self, _cx: &App) -> bool { - false - } -} - -impl EventEmitter 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) -> 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) -> 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,87 +549,392 @@ impl Sidebar { .child(Scrollbar::vertical(&self.community_scroll)) .into_any_element() } -} -fn section_row( - id: &'static str, - label: &'static str, - count: usize, - open: bool, - toggle: impl Fn(&mut Sidebar) + 'static, - cx: &mut Context, -) -> AnyElement { - div() - .flex_shrink_0() - .child( - TreeRow::new(ElementId::Name(id.into()), TreeRowKind::Section, label) - .icon(if open { - IconName::CaretDown + fn recent_communities(&self, cx: &App) -> Vec> { + 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> = 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 { + 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 { - IconName::CaretRight + 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, + rows: &[SidebarRow], + cx: &Context, + ) -> Vec { + 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, + ) -> AnyElement { + div() + .flex_shrink_0() + .child( + TreeRow::new(ElementId::Name(id.into()), TreeRowKind::Section, label) + .icon(if open { + IconName::CaretDown + } else { + IconName::CaretRight + }) + .count(count) + .on_click(cx.listener(move |this, _event, _window, cx| { + toggle(this); + cx.notify(); + })), + ) + .into_any_element() + } + + fn channel_row( + &self, + id: ChannelId, + name: String, + private: bool, + selected: bool, + community: &Entity, + cx: &mut Context, + ) -> AnyElement { + let community = community.clone(); + + div() + .flex_shrink_0() + .rounded(cx.theme().radius) + .child( + TreeRow::new( + ElementId::Name(SharedString::from(format!( + "community-channel-{}", + id.to_hex() + ))), + TreeRowKind::Room, + name, + ) + .icon(if private { + IconName::Lock + } else { + IconName::Message }) - .count(count) - .on_click(cx.listener(move |this, _event, _window, cx| { - toggle(this); + .on_click(cx.listener(move |_this, _event, _window, cx| { + community.update(cx, |community, cx| community.set_active_channel(id, cx)); cx.notify(); })), - ) - .into_any_element() + ) + .when(selected, |this| this.bg(cx.theme().ghost_element_active)) + .into_any_element() + } + + 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); + + div() + .flex_shrink_0() + .child( + TreeRow::new( + ElementId::Name(SharedString::from(format!( + "{prefix}-{}", + public_key.to_hex() + ))), + TreeRowKind::Room, + person.name(), + ) + .avatar(person.avatar_seed()) + .picture(person.avatar()), + ) + .into_any_element() + } } -fn channel_row( - id: ChannelId, - name: String, - private: bool, - selected: bool, - community: &Entity, - cx: &mut Context, -) -> AnyElement { - let community = community.clone(); +impl Panel for Sidebar { + fn panel_id(&self) -> SharedString { + "Sidebar".into() + } - div() - .flex_shrink_0() - .rounded(cx.theme().radius) - .child( - TreeRow::new( - ElementId::Name(SharedString::from(format!( - "community-channel-{}", - id.to_hex() - ))), - TreeRowKind::Room, - name, - ) - .icon(if private { - IconName::Lock - } else { - IconName::Message + 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 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) -> 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); + }), + ), + ) }) - .on_click(cx.listener(move |_this, _event, _window, cx| { - community.update(cx, |community, cx| community.set_active_channel(id, cx)); - cx.notify(); - })), - ) - .when(selected, |this| this.bg(cx.theme().ghost_element_active)) - .into_any_element() -} - -fn member_row(prefix: &str, public_key: PublicKey, cx: &App) -> AnyElement { - let persons = PersonRegistry::global(cx); - let person = persons.read(cx).get(&public_key, cx); - - div() - .flex_shrink_0() - .child( - TreeRow::new( - ElementId::Name(SharedString::from(format!( - "{prefix}-{}", - public_key.to_hex() - ))), - TreeRowKind::Room, - person.name(), - ) - .avatar(person.avatar_seed()) - .picture(person.avatar()), - ) - .into_any_element() + .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() + } } diff --git a/crates/workspace/src/sidebar/onboarding.rs b/crates/workspace/src/sidebar/onboarding.rs deleted file mode 100644 index abf2d9d4..00000000 --- a/crates/workspace/src/sidebar/onboarding.rs +++ /dev/null @@ -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)), - ), - ), - ) -}