diff --git a/crates/chat/src/lib.rs b/crates/chat/src/lib.rs index d9b22462..1da7beaf 100644 --- a/crates/chat/src/lib.rs +++ b/crates/chat/src/lib.rs @@ -9,7 +9,7 @@ use fuzzy_matcher::FuzzyMatcher; use fuzzy_matcher::skim::SkimMatcherV2; use gpui::{ App, AppContext, Context, Entity, EventEmitter, Global, SharedString, Subscription, Task, - WeakEntity, Window, + WeakEntity, }; use instant::Duration; use nostr_sdk::prelude::*; @@ -37,15 +37,8 @@ impl Global for GlobalChatRegistry {} /// Chat event. #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] pub enum ChatEvent { - /// An event to open a room by its ID - OpenRoom(u64), - /// An event to close a room by its ID - CloseRoom(u64), - /// An event to notify UI about a new chat request Ping, - /// No Inbox Relays found, the app is not ready to subscribe messages InboxRelayNotFound, - /// An error occurred Error(String), } @@ -543,34 +536,16 @@ impl ChatRegistry { cx.notify(); } - /// Emit an open room event. - /// - /// If the room is new, add it to the registry. - pub fn emit_room(&mut self, room: &Entity, window: &mut Window, cx: &mut Context) { - // Get the room's ID. + /// Track a room so it is listed and can be looked up by id. + pub fn track_room(&mut self, room: &Entity, cx: &mut Context) { let id = room.read(cx).id; - // If the room is new, add it to the registry and index. if let hash_map::Entry::Vacant(e) = self.room_index.entry(id) { let entity = room.to_owned(); e.insert(entity.clone()); + self.rooms.insert(0, entity); - } - - // Emit the open room event deferred to avoid re-entrant reads - cx.defer_in(window, move |_this, _window, cx| { - cx.emit(ChatEvent::OpenRoom(id)); - }); - } - - /// Close a room. - pub fn close_room(&mut self, id: u64, window: &mut Window, cx: &mut Context) { - if self.room_index.contains_key(&id) { - self.room_index.remove(&id); - self.rooms.retain(|r| r.read(cx).id != id); - cx.defer_in(window, move |_this, _window, cx| { - cx.emit(ChatEvent::CloseRoom(id)); - }); + cx.notify(); } } diff --git a/crates/community/src/community.rs b/crates/community/src/community.rs index 51656464..fd183829 100644 --- a/crates/community/src/community.rs +++ b/crates/community/src/community.rs @@ -107,8 +107,6 @@ impl SubscriptionKey { #[derive(Debug, Clone)] pub enum CommunityEvent { Updated(CommunityId), - Open(CommunityId), - Close(CommunityId), Channel(CommunityId, ChannelId), /// History exists here that no held key can open. Unreadable(CommunityId), diff --git a/crates/community/src/lib.rs b/crates/community/src/lib.rs index 5b0dd7dd..71ca90fe 100644 --- a/crates/community/src/lib.rs +++ b/crates/community/src/lib.rs @@ -7,7 +7,7 @@ pub use concord::cord03::{ChatMessage, ReplyRef}; use concord::state::CommunityState; pub use concord::{ChannelId, CommunityId, Epoch}; use futures::future::{Either, select}; -use gpui::{App, AppContext, Context, Entity, EventEmitter, Global, Subscription, Task, Window}; +use gpui::{App, AppContext, Context, Entity, EventEmitter, Global, Subscription, Task}; use nostr_sdk::prelude::*; use smallvec::{SmallVec, smallvec}; use state::NostrRegistry; @@ -225,27 +225,6 @@ impl CommunityRegistry { self.index.get(id).cloned() } - /// Ask the workspace to open a community's panel. - pub fn emit_community( - &mut self, - community: &Entity, - window: &mut Window, - cx: &mut Context, - ) { - let id = community.read(cx).id(); - - cx.defer_in(window, move |_this, _window, cx| { - cx.emit(CommunityEvent::Open(id)); - }); - } - - /// Ask the workspace to close a community's panel. - pub fn emit_close(&mut self, id: CommunityId, window: &mut Window, cx: &mut Context) { - cx.defer_in(window, move |_this, _window, cx| { - cx.emit(CommunityEvent::Close(id)); - }); - } - /// Create a community owned by the current account and begin tracking it. pub fn create(&mut self, metadata: CommunityMetadata, cx: &mut Context) { let nostr = NostrRegistry::global(cx); diff --git a/crates/community_ui/src/lib.rs b/crates/community_ui/src/lib.rs index 524d532b..73177e7b 100644 --- a/crates/community_ui/src/lib.rs +++ b/crates/community_ui/src/lib.rs @@ -145,21 +145,19 @@ impl CommunityPanel { window, |_this, _community, event, window, cx| { match event { - CommunityEvent::Updated(_) - | CommunityEvent::Unreadable(_) - | CommunityEvent::Failed(_) => { - cx.defer_in(window, |this, window, cx| this.reload(window, cx)); - } CommunityEvent::Channel(..) => { - cx.defer_in(window, |this, window, cx| this.load(window, cx)); + cx.defer_in(window, |this, window, cx| { + this.load(window, cx); + }); } CommunityEvent::Error(error) => { - window.push_notification( - Notification::error(error.clone()).autohide(false), - cx, - ); + window.push_notification(Notification::error(error.clone()), cx); + } + _ => { + cx.defer_in(window, |this, window, cx| { + this.reload(window, cx); + }); } - CommunityEvent::Open(_) | CommunityEvent::Close(_) => {} }; }, )); diff --git a/crates/ui/src/dock/mod.rs b/crates/ui/src/dock/mod.rs index a30bcdca..55ca7115 100644 --- a/crates/ui/src/dock/mod.rs +++ b/crates/ui/src/dock/mod.rs @@ -95,6 +95,19 @@ pub fn add_panel( area.add_panel_view(Arc::new(panel), placement, None, window, cx); } +/// Add a panel to a dock reached through a weak handle. +pub fn add_panel_to( + dock: &WeakEntity, + panel: PanelHandle, + placement: DockPlacement, + window: &mut Window, + cx: &mut App, +) { + let _ = dock.update(cx, |area, cx| { + add_panel(area, panel, placement, window, cx); + }); +} + /// The panel in any region of `area` whose logical id is `key`. fn find_panel(area: &DockArea, key: &SharedString, cx: &App) -> Option<(PanelId, NodeId, usize)> { let placements = [ diff --git a/crates/workspace/src/dialogs/new_chat.rs b/crates/workspace/src/dialogs/new_chat.rs index 96710dc3..8e766050 100644 --- a/crates/workspace/src/dialogs/new_chat.rs +++ b/crates/workspace/src/dialogs/new_chat.rs @@ -2,17 +2,18 @@ use chat::{ChatRegistry, Room, RoomKind}; use gpui::prelude::FluentBuilder; use gpui::{ App, AppContext, Context, Entity, IntoElement, ParentElement, Render, SharedString, Styled, - Subscription, Window, div, px, + Subscription, WeakEntity, Window, div, px, }; use nostr_sdk::prelude::*; use state::NostrRegistry; use theme::ActiveTheme; use ui::button::{Button, ButtonVariants}; +use ui::dock::{DockArea, DockPlacement, PanelHandle}; use ui::input::{Input, InputEvent, InputState}; use ui::{StyledExt, WindowExtension, v_flex}; -pub fn open(window: &mut Window, cx: &mut App) { - let view = cx.new(|cx| NewChat::new(window, cx)); +pub fn open(dock: WeakEntity, window: &mut Window, cx: &mut App) { + let view = cx.new(|cx| NewChat::new(dock, window, cx)); window.open_modal(cx, move |this, _window, _cx| { this.width(px(420.)).title("New chat").child(view.clone()) @@ -20,6 +21,9 @@ pub fn open(window: &mut Window, cx: &mut App) { } pub struct NewChat { + /// The dock a started chat opens in + dock: WeakEntity, + /// Public key input input: Entity, @@ -31,7 +35,7 @@ pub struct NewChat { } impl NewChat { - fn new(window: &mut Window, cx: &mut Context) -> Self { + fn new(dock: WeakEntity, window: &mut Window, cx: &mut Context) -> Self { let input = cx.new(|cx| InputState::new(window, cx).placeholder("npub")); let subscription = cx.subscribe_in(&input, window, |this, _input, event, window, cx| { @@ -41,6 +45,7 @@ impl NewChat { }); Self { + dock, input, error: None, _subscription: Some(subscription), @@ -56,6 +61,8 @@ impl NewChat { }; let nostr = NostrRegistry::global(cx); + let chat = ChatRegistry::global(cx); + let Some(current_user) = nostr.read(cx).current_user() else { self.set_error("You are not signed in", cx); return; @@ -70,12 +77,20 @@ impl NewChat { .organize(¤t_user) .kind(RoomKind::Ongoing); - let chat = ChatRegistry::global(cx); - chat.update(cx, |chat, cx| { + let room = chat.update(cx, |chat, cx| { let room = cx.new(|_| room); - chat.emit_room(&room, window, cx); + chat.track_room(&room, cx); + room }); + ui::dock::add_panel_to( + &self.dock, + PanelHandle::new(chat_ui::init(room.downgrade(), window, cx)), + DockPlacement::Center, + window, + cx, + ); + window.close_modal(cx); } diff --git a/crates/workspace/src/lib.rs b/crates/workspace/src/lib.rs index 3af3c2cb..69e9a575 100644 --- a/crates/workspace/src/lib.rs +++ b/crates/workspace/src/lib.rs @@ -6,13 +6,11 @@ use anyhow::Error; use auto_update::AutoUpdater; use chat::{ChatEvent, ChatRegistry}; use common::download_dir; -use community::{CommunityEvent, CommunityRegistry}; -use community_ui::CommunityPanel; use device::{DeviceEvent, DeviceRegistry}; use gpui::prelude::FluentBuilder; use gpui::{ Action, AnyElement, App, AppContext, Context, Entity, InteractiveElement, IntoElement, - ParentElement, Render, SharedString, Styled, Subscription, Task, WeakEntity, Window, div, px, + ParentElement, Render, SharedString, Styled, Subscription, Task, Window, div, px, }; use nostr_sdk::prelude::*; use person::{PersonRegistry, shorten_pubkey}; @@ -21,7 +19,7 @@ use smallvec::{SmallVec, smallvec}; use state::{NostrRegistry, StateEvent}; use theme::{ActiveTheme, SIDEBAR_WIDTH, Theme, ThemeRegistry}; use ui::button::{Button, ButtonVariants}; -use ui::dock::{self, ClosePanel, 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::{Icon, IconName, Root, Sizable, WindowExtension, h_flex, v_flex}; @@ -72,8 +70,6 @@ enum Command { pub struct Workspace { dock: Entity, title_bar_chrome: Rc, - /// The community panel currently docked, if any - community_panel: Option>, /// Async tasks tasks: Vec>>, /// Event subscriptions @@ -83,12 +79,11 @@ pub struct Workspace { impl Workspace { fn new(window: &mut Window, cx: &mut Context) -> Self { let chat = ChatRegistry::global(cx); - let communities = CommunityRegistry::global(cx); let device = DeviceRegistry::global(cx); let nostr = NostrRegistry::global(cx); - let sidebar = cx.new(|cx| Sidebar::new(window, cx)); let (dock, title_bar_chrome) = dock::dock_area("coop", window, cx); + let sidebar = cx.new(|cx| Sidebar::new(window, dock.downgrade(), cx)); let mut subscriptions = smallvec![]; @@ -164,7 +159,7 @@ impl Workspace { subscriptions.push( // Observe all events emitted by the chat registry - cx.subscribe_in(&chat, window, move |this, chat, ev, window, cx| { + cx.subscribe_in(&chat, window, move |_this, _chat, ev, window, cx| { match ev { ChatEvent::InboxRelayNotFound => { const MSG: &str = "Messaging Relays not found. Cannot receive messages."; @@ -187,28 +182,6 @@ impl Workspace { cx, ); } - ChatEvent::OpenRoom(id) => { - if let Some(room) = chat.read(cx).room(id, cx) { - this.add_panel_to_dock( - chat_ui::init(room, window, cx), - DockPlacement::Center, - window, - cx, - ); - } - } - ChatEvent::CloseRoom(..) => { - this.dock.update(cx, |area, cx| { - // Force focus to the tab panel - ui::dock::focus_tab_panel(area, window, cx); - - // Dispatch the close panel action - cx.defer_in(window, |_, window, cx| { - window.dispatch_action(Box::new(ClosePanel), cx); - window.close_all_modals(cx); - }); - }); - } ChatEvent::Error(error) => { window.push_notification(Notification::error(error).autohide(false), cx); } @@ -217,71 +190,24 @@ impl Workspace { }), ); - subscriptions.push( - // Observe all events emitted by the community registry - cx.subscribe_in( - &communities, - window, - move |this, communities, event, window, cx| match event { - CommunityEvent::Open(id) => { - if let Some(community) = communities.read(cx).community(id) { - let panel = community_ui::init(community, window, cx); - - this.community_panel = Some(panel.downgrade()); - this.add_panel_to_dock(panel, DockPlacement::Center, window, cx); - } - } - CommunityEvent::Close(_) => { - let Some(panel) = this - .community_panel - .take() - .and_then(|panel| panel.upgrade()) - else { - return; - }; - - this.dock.update(cx, |area, cx| { - ui::dock::add_panel( - area, - PanelHandle::new(panel), - DockPlacement::Center, - window, - cx, - ); - ui::dock::focus_tab_panel(area, window, cx); - - cx.defer_in(window, |_, window, cx| { - window.dispatch_action(Box::new(ClosePanel), cx); - }); - }); - } - _ => {} - }, - ), - ); - cx.defer_in(window, move |this, window, cx| { let sidebar = PanelHandle::new(sidebar); - - this.dock.update(cx, |area, cx| { - let left = DockLayout::tabs().panel_view(Arc::new(sidebar), cx); - area.set_dock(DockPlacement::Left, left, window, cx); - area.set_dock_size(DockPlacement::Left, SIDEBAR_WIDTH, window, cx); - }); - let greeter = PanelHandle::new(greeter::init(window, cx)); - let center = DockLayout::v_split() - .child(DockLayout::tabs().panel_view(Arc::new(greeter), cx), None); - this.dock.update(cx, |area, cx| { - area.set_center(center, window, cx); + this.dock.update(cx, |this, cx| { + let left = DockLayout::tabs().panel_view(Arc::new(sidebar), cx); + let center = DockLayout::v_split() + .child(DockLayout::tabs().panel_view(Arc::new(greeter), cx), None); + + this.set_dock(DockPlacement::Left, left, window, cx); + this.set_dock_size(DockPlacement::Left, SIDEBAR_WIDTH, window, cx); + this.set_center(center, window, cx); }); }); Self { dock, title_bar_chrome, - community_panel: None, tasks: vec![], _subscriptions: subscriptions, } @@ -365,10 +291,11 @@ impl Workspace { self.add_panel_to_dock(browse::init(window, cx), DockPlacement::Center, window, cx); } Command::ShowSearch => { - self.add_panel_to_dock(search::init(window, cx), DockPlacement::Center, window, cx); + let panel = search::init(self.dock.downgrade(), window, cx); + self.add_panel_to_dock(panel, DockPlacement::Center, window, cx); } Command::NewChat => { - new_chat::open(window, cx); + new_chat::open(self.dock.downgrade(), window, cx); } Command::NewCommunity => { new_community::open(window, cx); diff --git a/crates/workspace/src/panels/search.rs b/crates/workspace/src/panels/search.rs index 38261cda..64feab68 100644 --- a/crates/workspace/src/panels/search.rs +++ b/crates/workspace/src/panels/search.rs @@ -7,8 +7,8 @@ use common::DebouncedDelay; use gpui::prelude::FluentBuilder; use gpui::{ AnyElement, App, AppContext, Context, ElementId, Entity, EventEmitter, FocusHandle, Focusable, - IntoElement, ParentElement, Render, SharedString, Styled, Subscription, Task, Window, div, - uniform_list, + IntoElement, ParentElement, Render, SharedString, Styled, Subscription, Task, WeakEntity, + Window, div, uniform_list, }; use instant::Duration; use nostr_sdk::prelude::*; @@ -17,7 +17,7 @@ use smallvec::{SmallVec, smallvec}; use state::{FIND_DELAY, NostrRegistry}; use theme::ActiveTheme; use ui::button::{Button, ButtonVariants}; -use ui::dock::{Panel, PanelEvent}; +use ui::dock::{DockArea, DockPlacement, Panel, PanelEvent, PanelHandle}; use ui::input::{Input, InputEvent, InputState}; use ui::notification::Notification; use ui::{Icon, IconName, Selectable, Sizable, StyledExt, WindowExtension, h_flex, v_flex}; @@ -26,13 +26,15 @@ use crate::sidebar::{TreeRow, TreeRowKind}; const INPUT_PLACEHOLDER: &str = "Find or start a conversation"; -pub fn init(window: &mut Window, cx: &mut App) -> Entity { - cx.new(|cx| SearchPanel::new(window, cx)) +pub fn init(dock: WeakEntity, window: &mut Window, cx: &mut App) -> Entity { + cx.new(|cx| SearchPanel::new(dock, window, cx)) } pub struct SearchPanel { name: SharedString, focus_handle: FocusHandle, + /// The dock a started chat opens in + dock: WeakEntity, /// Find input state find_input: Entity, @@ -63,7 +65,7 @@ pub struct SearchPanel { } impl SearchPanel { - fn new(window: &mut Window, cx: &mut Context) -> Self { + fn new(dock: WeakEntity, window: &mut Window, cx: &mut Context) -> Self { let contact_list = cx.new(|_| None); let selected_pkeys = cx.new(|_| HashSet::new()); let find_results = cx.new(|_| None); @@ -107,6 +109,7 @@ impl SearchPanel { Self { name: "Search".into(), focus_handle: cx.focus_handle(), + dock, find_input, find_debouncer: DebouncedDelay::new(), find_results, @@ -285,6 +288,7 @@ impl SearchPanel { fn create_room(&mut self, window: &mut Window, cx: &mut Context) { let chat = ChatRegistry::global(cx); let async_chat = chat.downgrade(); + let dock = self.dock.clone(); let nostr = NostrRegistry::global(cx); let Some(public_key) = nostr.read(cx).current_user() else { @@ -295,14 +299,26 @@ impl SearchPanel { let receivers = self.get_selected(cx); self.tasks.push(cx.spawn_in(window, async move |this, cx| { - // Create a new room and emit it - async_chat.update_in(cx, |this, _window, cx| { + // Create a new room and register it + let room = async_chat.update_in(cx, |chat, _window, cx| { let room = cx.new(|_| { Room::new(public_key, receivers) .organize(&public_key) .kind(RoomKind::Ongoing) }); - this.emit_room(&room, _window, cx); + chat.track_room(&room, cx); + room + })?; + + // Open it in the dock + cx.update(|window, cx| { + ui::dock::add_panel_to( + &dock, + PanelHandle::new(chat_ui::init(room.downgrade(), window, cx)), + DockPlacement::Center, + window, + cx, + ); })?; // Reset the find panel diff --git a/crates/workspace/src/sidebar/mod.rs b/crates/workspace/src/sidebar/mod.rs index 8e709ad6..55e02bc3 100644 --- a/crates/workspace/src/sidebar/mod.rs +++ b/crates/workspace/src/sidebar/mod.rs @@ -2,7 +2,7 @@ use std::ops::Range; use std::rc::Rc; use auto_update::AutoUpdater; -use chat::{ChatEvent, ChatRegistry, RoomKind}; +use chat::{ChatEvent, ChatRegistry, Room, RoomKind}; use common::TimestampExt; use community::{ChannelId, Community, CommunityEvent, CommunityRegistry}; use gpui::prelude::FluentBuilder; @@ -20,7 +20,7 @@ use state::NostrRegistry; use theme::{ActiveTheme, TABBAR_HEIGHT}; use ui::avatar::Avatar; use ui::button::{Button, ButtonVariants}; -use ui::dock::{Panel, PanelEvent}; +use ui::dock::{DockArea, DockPlacement, Panel, PanelEvent, PanelHandle}; use ui::indicator::Indicator; use ui::menu::{DropdownMenu, PopupMenuItem}; use ui::nav_item::NavItem; @@ -46,6 +46,8 @@ pub struct Sidebar { scroll_handles: [UniformListScrollHandle; 3], /// Scroll state of the channel and member lists community_scroll: ScrollHandle, + /// The dock the sidebar opens its panels in + dock: WeakEntity, active_tab: SidebarTab, /// The community the sidebar is browsing, if any community: Option>, @@ -59,7 +61,7 @@ pub struct Sidebar { } impl Sidebar { - pub fn new(window: &mut Window, cx: &mut Context) -> Self { + 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); @@ -98,6 +100,7 @@ impl Sidebar { UniformListScrollHandle::new(), ], community_scroll: ScrollHandle::default(), + dock, active_tab: SidebarTab::Recents, community: None, channels_open: true, @@ -130,25 +133,34 @@ impl Sidebar { settings.record_recent_community(id, cx); }); - CommunityRegistry::global(cx).update(cx, |registry, cx| { - registry.emit_community(&community, window, cx); - }); - self.community = Some(community.downgrade()); + + ui::dock::add_panel_to( + &self.dock, + PanelHandle::new(community_ui::init(community, window, cx)), + DockPlacement::Center, + window, + cx, + ); + cx.notify(); } - fn close_community(&mut self, window: &mut Window, cx: &mut Context) { - let Some(community) = self.community.take() else { + fn open_room(&mut self, room: Entity, window: &mut Window, cx: &mut Context) { + ui::dock::add_panel_to( + &self.dock, + PanelHandle::new(chat_ui::init(room.downgrade(), window, cx)), + DockPlacement::Center, + window, + cx, + ); + } + + /// Leave the community view, returning the sidebar to its tab list. + fn reset_community(&mut self, cx: &mut Context) { + if self.community.take().is_none() { return; - }; - - if let Ok(id) = community.read_with(cx, |community, _cx| community.id()) { - CommunityRegistry::global(cx).update(cx, |registry, cx| { - registry.emit_close(id, window, cx); - }); } - cx.notify(); } @@ -250,8 +262,8 @@ impl Sidebar { .tooltip("Back") .ghost() .small() - .on_click(cx.listener(|this, _event, window, cx| { - this.close_community(window, cx); + .on_click(cx.listener(|this, _event, _window, cx| { + this.reset_community(cx); })), ); } @@ -407,10 +419,8 @@ fn render_rows(range: Range, rows: &[SidebarRow], cx: &Context) let created_at = room.read(cx).created_at.to_ago(); let room_clone = room.clone(); - let handler = cx.listener(move |_this, _event, window, cx| { - ChatRegistry::global(cx).update(cx, |chat, cx| { - chat.emit_room(&room_clone, window, cx); - }); + let handler = cx.listener(move |this, _event, window, cx| { + this.open_room(room_clone.clone(), window, cx); }); TreeRow::new(