Add channel notes view

Mikayla and Max created

co-authored-by: Max <max@zed.dev>

Change summary

Cargo.lock                                      |  1 
crates/channel/src/channel_buffer.rs            | 14 +++
crates/channel/src/channel_store.rs             | 10 ++
crates/collab/src/tests/channel_buffer_tests.rs | 19 +++--
crates/collab_ui/Cargo.toml                     |  1 
crates/collab_ui/src/channel_view.rs            | 69 +++++++++++++++++++
crates/collab_ui/src/collab_panel.rs            | 46 +++++++++++
crates/collab_ui/src/collab_ui.rs               |  1 
crates/gpui/src/app.rs                          |  3 
9 files changed, 150 insertions(+), 14 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -1529,6 +1529,7 @@ dependencies = [
  "futures 0.3.28",
  "fuzzy",
  "gpui",
+ "language",
  "log",
  "menu",
  "picker",

crates/channel/src/channel_buffer.rs 🔗

@@ -1,4 +1,4 @@
-use crate::ChannelId;
+use crate::{Channel, ChannelId, ChannelStore};
 use anyhow::Result;
 use client::Client;
 use gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task};
@@ -16,6 +16,7 @@ pub struct ChannelBuffer {
     channel_id: ChannelId,
     collaborators: Vec<proto::Collaborator>,
     buffer: ModelHandle<language::Buffer>,
+    channel_store: ModelHandle<ChannelStore>,
     client: Arc<Client>,
     _subscription: client::Subscription,
 }
@@ -33,7 +34,8 @@ impl Entity for ChannelBuffer {
 }
 
 impl ChannelBuffer {
-    pub fn join_channel(
+    pub(crate) fn new(
+        channel_store: ModelHandle<ChannelStore>,
         channel_id: ChannelId,
         client: Arc<Client>,
         cx: &mut AppContext,
@@ -65,6 +67,7 @@ impl ChannelBuffer {
                     buffer,
                     client,
                     channel_id,
+                    channel_store,
                     collaborators,
                     _subscription: subscription.set_model(&cx.handle(), &mut cx.to_async()),
                 }
@@ -161,4 +164,11 @@ impl ChannelBuffer {
     pub fn collaborators(&self) -> &[proto::Collaborator] {
         &self.collaborators
     }
+
+    pub fn channel(&self, cx: &AppContext) -> Option<Arc<Channel>> {
+        self.channel_store
+            .read(cx)
+            .channel_for_id(self.channel_id)
+            .cloned()
+    }
 }

crates/channel/src/channel_store.rs 🔗

@@ -13,6 +13,8 @@ use rpc::{proto, TypedEnvelope};
 use std::sync::Arc;
 use util::ResultExt;
 
+use crate::channel_buffer::ChannelBuffer;
+
 pub type ChannelId = u64;
 
 pub struct ChannelStore {
@@ -151,6 +153,14 @@ impl ChannelStore {
         self.channels_by_id.get(&channel_id)
     }
 
+    pub fn open_channel_buffer(
+        &self,
+        channel_id: ChannelId,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<ModelHandle<ChannelBuffer>>> {
+        ChannelBuffer::new(cx.handle(), channel_id, self.client.clone(), cx)
+    }
+
     pub fn is_user_admin(&self, channel_id: ChannelId) -> bool {
         self.channel_paths.iter().any(|path| {
             if let Some(ix) = path.iter().position(|id| *id == channel_id) {

crates/collab/src/tests/channel_buffer_tests.rs 🔗

@@ -1,6 +1,5 @@
 use crate::{rpc::RECONNECT_TIMEOUT, tests::TestServer};
 
-use channel::channel_buffer::ChannelBuffer;
 use client::UserId;
 use gpui::{executor::Deterministic, ModelHandle, TestAppContext};
 use rpc::{proto, RECEIVE_TIMEOUT};
@@ -22,8 +21,9 @@ async fn test_core_channel_buffers(
         .await;
 
     // Client A joins the channel buffer
-    let channel_buffer_a = cx_a
-        .update(|cx| ChannelBuffer::join_channel(zed_id, client_a.client().to_owned(), cx))
+    let channel_buffer_a = client_a
+        .channel_store()
+        .update(cx_a, |channel, cx| channel.open_channel_buffer(zed_id, cx))
         .await
         .unwrap();
 
@@ -45,8 +45,9 @@ async fn test_core_channel_buffers(
     assert_eq!(buffer_text(&buffer_a, cx_a), "hello, cruel world");
 
     // Client B joins the channel buffer
-    let channel_buffer_b = cx_b
-        .update(|cx| ChannelBuffer::join_channel(zed_id, client_b.client().to_owned(), cx))
+    let channel_buffer_b = client_b
+        .channel_store()
+        .update(cx_b, |channel, cx| channel.open_channel_buffer(zed_id, cx))
         .await
         .unwrap();
 
@@ -79,8 +80,9 @@ async fn test_core_channel_buffers(
     });
 
     // Client A rejoins the channel buffer
-    let _channel_buffer_a = cx_a
-        .update(|cx| ChannelBuffer::join_channel(zed_id, client_a.client().to_owned(), cx))
+    let _channel_buffer_a = client_a
+        .channel_store()
+        .update(cx_a, |channels, cx| channels.open_channel_buffer(zed_id, cx))
         .await
         .unwrap();
     deterministic.run_until_parked();
@@ -104,7 +106,8 @@ async fn test_core_channel_buffers(
     });
 
     // TODO:
-    // - Test synchronizing offline updates, what happens to A's channel buffer?
+    // - Test synchronizing offline updates, what happens to A's channel buffer when A disconnects
+    // - Test interaction with channel deletion while buffer is open
 }
 
 #[track_caller]

crates/collab_ui/Cargo.toml 🔗

@@ -34,6 +34,7 @@ editor = { path = "../editor" }
 feedback = { path = "../feedback" }
 fuzzy = { path = "../fuzzy" }
 gpui = { path = "../gpui" }
+language = { path = "../language" }
 menu = { path = "../menu" }
 picker = { path = "../picker" }
 project = { path = "../project" }

crates/collab_ui/src/channel_view.rs 🔗

@@ -0,0 +1,69 @@
+use channel::channel_buffer::ChannelBuffer;
+use editor::Editor;
+use gpui::{
+    actions,
+    elements::{ChildView, Label},
+    AnyElement, AppContext, Element, Entity, ModelHandle, View, ViewContext, ViewHandle,
+};
+use language::Language;
+use std::sync::Arc;
+use workspace::item::{Item, ItemHandle};
+
+actions!(channel_view, [Deploy]);
+
+pub(crate) fn init(cx: &mut AppContext) {
+    // TODO
+}
+
+pub struct ChannelView {
+    editor: ViewHandle<Editor>,
+    channel_buffer: ModelHandle<ChannelBuffer>,
+}
+
+impl ChannelView {
+    pub fn new(
+        channel_buffer: ModelHandle<ChannelBuffer>,
+        language: Arc<Language>,
+        cx: &mut ViewContext<Self>,
+    ) -> Self {
+        let buffer = channel_buffer.read(cx).buffer();
+        buffer.update(cx, |buffer, cx| buffer.set_language(Some(language), cx));
+        let editor = cx.add_view(|cx| Editor::for_buffer(buffer, None, cx));
+        Self {
+            editor,
+            channel_buffer,
+        }
+    }
+}
+
+impl Entity for ChannelView {
+    type Event = editor::Event;
+}
+
+impl View for ChannelView {
+    fn ui_name() -> &'static str {
+        "ChannelView"
+    }
+
+    fn render(&mut self, cx: &mut ViewContext<'_, '_, Self>) -> AnyElement<Self> {
+        ChildView::new(self.editor.as_any(), cx).into_any()
+    }
+}
+
+impl Item for ChannelView {
+    fn tab_content<V: 'static>(
+        &self,
+        _: Option<usize>,
+        style: &theme::Tab,
+        cx: &gpui::AppContext,
+    ) -> AnyElement<V> {
+        let channel_name = self
+            .channel_buffer
+            .read(cx)
+            .channel(cx)
+            .map_or("[Deleted channel]".to_string(), |channel| {
+                format!("#{}", channel.name)
+            });
+        Label::new(channel_name, style.label.to_owned()).into_any()
+    }
+}

crates/collab_ui/src/collab_panel.rs 🔗

@@ -42,7 +42,10 @@ use workspace::{
     Workspace,
 };
 
-use crate::face_pile::FacePile;
+use crate::{
+    channel_view::{self, ChannelView},
+    face_pile::FacePile,
+};
 use channel_modal::ChannelModal;
 
 use self::contact_finder::ContactFinder;
@@ -77,6 +80,11 @@ struct RenameChannel {
     channel_id: u64,
 }
 
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+struct OpenChannelBuffer {
+    channel_id: u64,
+}
+
 actions!(
     collab_panel,
     [
@@ -96,7 +104,8 @@ impl_actions!(
         InviteMembers,
         ManageMembers,
         RenameChannel,
-        ToggleCollapse
+        ToggleCollapse,
+        OpenChannelBuffer
     ]
 );
 
@@ -106,6 +115,7 @@ pub fn init(_client: Arc<Client>, cx: &mut AppContext) {
     settings::register::<panel_settings::CollaborationPanelSettings>(cx);
     contact_finder::init(cx);
     channel_modal::init(cx);
+    channel_view::init(cx);
 
     cx.add_action(CollabPanel::cancel);
     cx.add_action(CollabPanel::select_next);
@@ -121,7 +131,8 @@ pub fn init(_client: Arc<Client>, cx: &mut AppContext) {
     cx.add_action(CollabPanel::rename_channel);
     cx.add_action(CollabPanel::toggle_channel_collapsed);
     cx.add_action(CollabPanel::collapse_selected_channel);
-    cx.add_action(CollabPanel::expand_selected_channel)
+    cx.add_action(CollabPanel::expand_selected_channel);
+    cx.add_action(CollabPanel::open_channel_buffer);
 }
 
 #[derive(Debug)]
@@ -1888,6 +1899,7 @@ impl CollabPanel {
                     vec![
                         ContextMenuItem::action(expand_action_name, ToggleCollapse { channel_id }),
                         ContextMenuItem::action("New Subchannel", NewChannel { channel_id }),
+                        ContextMenuItem::action("Open Notes", OpenChannelBuffer { channel_id }),
                         ContextMenuItem::Separator,
                         ContextMenuItem::action("Invite to Channel", InviteMembers { channel_id }),
                         ContextMenuItem::Separator,
@@ -2207,6 +2219,34 @@ impl CollabPanel {
         }
     }
 
+    fn open_channel_buffer(&mut self, action: &OpenChannelBuffer, cx: &mut ViewContext<Self>) {
+        let workspace = self.workspace;
+        let open = self.channel_store.update(cx, |channel_store, cx| {
+            channel_store.open_channel_buffer(action.channel_id, cx)
+        });
+
+        cx.spawn(|_, mut cx| async move {
+            let channel_buffer = open.await?;
+
+            let markdown = workspace
+                .read_with(&cx, |workspace, _| {
+                    workspace
+                        .app_state()
+                        .languages
+                        .language_for_name("Markdown")
+                })?
+                .await?;
+
+            workspace.update(&mut cx, |workspace, cx| {
+                let channel_view = cx.add_view(|cx| ChannelView::new(channel_buffer, markdown, cx));
+                workspace.add_item(Box::new(channel_view), cx);
+            })?;
+
+            anyhow::Ok(())
+        })
+        .detach();
+    }
+
     fn show_inline_context_menu(&mut self, _: &menu::ShowContextMenu, cx: &mut ViewContext<Self>) {
         let Some(channel) = self.selected_channel() else {
             return;

crates/gpui/src/app.rs 🔗

@@ -4687,12 +4687,13 @@ impl AnyWeakModelHandle {
     }
 }
 
-#[derive(Copy)]
 pub struct WeakViewHandle<T> {
     any_handle: AnyWeakViewHandle,
     view_type: PhantomData<T>,
 }
 
+impl<T> Copy for WeakViewHandle<T> {}
+
 impl<T> Debug for WeakViewHandle<T> {
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
         f.debug_struct(&format!("WeakViewHandle<{}>", type_name::<T>()))