Restore chat functionality with a very rough UI

Max Brunsfeld created

Change summary

crates/channel/Cargo.toml                      |   1 
crates/channel/src/channel_store.rs            |   4 
crates/channel/src/channel_store_tests.rs      | 221 ++++++++----------
crates/client/src/test.rs                      |   9 
crates/collab/src/db/queries/messages.rs       |   5 
crates/collab/src/db/tables/channel_message.rs |   4 
crates/collab/src/db/tests/message_tests.rs    |   6 
crates/collab_ui/src/chat_panel.rs             | 229 ++++++++++++++++---
crates/collab_ui/src/collab_panel.rs           |  15 
crates/collab_ui/src/collab_ui.rs              |   3 
crates/zed/src/zed.rs                          |  15 +
styles/src/style_tree/app.ts                   |   2 
styles/src/style_tree/chat_panel.ts            | 111 +++++++++
13 files changed, 440 insertions(+), 185 deletions(-)

Detailed changes

crates/channel/Cargo.toml 🔗

@@ -47,5 +47,6 @@ tempfile = "3"
 collections = { path = "../collections", features = ["test-support"] }
 gpui = { path = "../gpui", features = ["test-support"] }
 rpc = { path = "../rpc", features = ["test-support"] }
+client = { path = "../client", features = ["test-support"] }
 settings = { path = "../settings", features = ["test-support"] }
 util = { path = "../util", features = ["test-support"] }

crates/channel/src/channel_store.rs 🔗

@@ -111,6 +111,10 @@ impl ChannelStore {
         }
     }
 
+    pub fn client(&self) -> Arc<Client> {
+        self.client.clone()
+    }
+
     pub fn has_children(&self, channel_id: ChannelId) -> bool {
         self.channel_paths.iter().any(|path| {
             if let Some(ix) = path.iter().position(|id| *id == channel_id) {

crates/channel/src/channel_store_tests.rs 🔗

@@ -4,15 +4,12 @@ use super::*;
 use client::{test::FakeServer, Client, UserStore};
 use gpui::{AppContext, ModelHandle, TestAppContext};
 use rpc::proto;
+use settings::SettingsStore;
 use util::http::FakeHttpClient;
 
 #[gpui::test]
 fn test_update_channels(cx: &mut AppContext) {
-    let http = FakeHttpClient::with_404_response();
-    let client = Client::new(http.clone(), cx);
-    let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx));
-
-    let channel_store = cx.add_model(|cx| ChannelStore::new(client, user_store, cx));
+    let channel_store = init_test(cx);
 
     update_channels(
         &channel_store,
@@ -80,11 +77,7 @@ fn test_update_channels(cx: &mut AppContext) {
 
 #[gpui::test]
 fn test_dangling_channel_paths(cx: &mut AppContext) {
-    let http = FakeHttpClient::with_404_response();
-    let client = Client::new(http.clone(), cx);
-    let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx));
-
-    let channel_store = cx.add_model(|cx| ChannelStore::new(client, user_store, cx));
+    let channel_store = init_test(cx);
 
     update_channels(
         &channel_store,
@@ -141,18 +134,11 @@ fn test_dangling_channel_paths(cx: &mut AppContext) {
 
 #[gpui::test]
 async fn test_channel_messages(cx: &mut TestAppContext) {
-    cx.foreground().forbid_parking();
-
     let user_id = 5;
-    let http_client = FakeHttpClient::with_404_response();
-    let client = cx.update(|cx| Client::new(http_client.clone(), cx));
-    let server = FakeServer::for_client(user_id, &client, cx).await;
-    let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
-    crate::init(&client);
-
-    let channel_store = cx.add_model(|cx| ChannelStore::new(client, user_store, cx));
-
     let channel_id = 5;
+    let channel_store = cx.update(init_test);
+    let client = channel_store.read_with(cx, |s, _| s.client());
+    let server = FakeServer::for_client(user_id, &client, cx).await;
 
     // Get the available channels.
     server.send(proto::UpdateChannels {
@@ -163,85 +149,71 @@ async fn test_channel_messages(cx: &mut TestAppContext) {
         }],
         ..Default::default()
     });
-    channel_store.next_notification(cx).await;
+    cx.foreground().run_until_parked();
     cx.read(|cx| {
         assert_channels(&channel_store, &[(0, "the-channel".to_string(), false)], cx);
     });
 
     let get_users = server.receive::<proto::GetUsers>().await.unwrap();
     assert_eq!(get_users.payload.user_ids, vec![5]);
-    server
-        .respond(
-            get_users.receipt(),
-            proto::UsersResponse {
-                users: vec![proto::User {
-                    id: 5,
-                    github_login: "nathansobo".into(),
-                    avatar_url: "http://avatar.com/nathansobo".into(),
-                }],
-            },
-        )
-        .await;
+    server.respond(
+        get_users.receipt(),
+        proto::UsersResponse {
+            users: vec![proto::User {
+                id: 5,
+                github_login: "nathansobo".into(),
+                avatar_url: "http://avatar.com/nathansobo".into(),
+            }],
+        },
+    );
 
     // Join a channel and populate its existing messages.
-    let channel = channel_store
-        .update(cx, |store, cx| {
-            let channel_id = store.channels().next().unwrap().1.id;
-            store.open_channel_chat(channel_id, cx)
-        })
-        .await
-        .unwrap();
-    channel.read_with(cx, |channel, _| assert!(channel.messages().is_empty()));
+    let channel = channel_store.update(cx, |store, cx| {
+        let channel_id = store.channels().next().unwrap().1.id;
+        store.open_channel_chat(channel_id, cx)
+    });
     let join_channel = server.receive::<proto::JoinChannelChat>().await.unwrap();
-    server
-        .respond(
-            join_channel.receipt(),
-            proto::JoinChannelChatResponse {
-                messages: vec![
-                    proto::ChannelMessage {
-                        id: 10,
-                        body: "a".into(),
-                        timestamp: 1000,
-                        sender_id: 5,
-                        nonce: Some(1.into()),
-                    },
-                    proto::ChannelMessage {
-                        id: 11,
-                        body: "b".into(),
-                        timestamp: 1001,
-                        sender_id: 6,
-                        nonce: Some(2.into()),
-                    },
-                ],
-                done: false,
-            },
-        )
-        .await;
+    server.respond(
+        join_channel.receipt(),
+        proto::JoinChannelChatResponse {
+            messages: vec![
+                proto::ChannelMessage {
+                    id: 10,
+                    body: "a".into(),
+                    timestamp: 1000,
+                    sender_id: 5,
+                    nonce: Some(1.into()),
+                },
+                proto::ChannelMessage {
+                    id: 11,
+                    body: "b".into(),
+                    timestamp: 1001,
+                    sender_id: 6,
+                    nonce: Some(2.into()),
+                },
+            ],
+            done: false,
+        },
+    );
+
+    cx.foreground().start_waiting();
 
     // Client requests all users for the received messages
     let mut get_users = server.receive::<proto::GetUsers>().await.unwrap();
     get_users.payload.user_ids.sort();
     assert_eq!(get_users.payload.user_ids, vec![6]);
-    server
-        .respond(
-            get_users.receipt(),
-            proto::UsersResponse {
-                users: vec![proto::User {
-                    id: 6,
-                    github_login: "maxbrunsfeld".into(),
-                    avatar_url: "http://avatar.com/maxbrunsfeld".into(),
-                }],
-            },
-        )
-        .await;
-
-    assert_eq!(
-        channel.next_event(cx).await,
-        ChannelChatEvent::MessagesUpdated {
-            old_range: 0..0,
-            new_count: 2,
-        }
+    server.respond(
+        get_users.receipt(),
+        proto::UsersResponse {
+            users: vec![proto::User {
+                id: 6,
+                github_login: "maxbrunsfeld".into(),
+                avatar_url: "http://avatar.com/maxbrunsfeld".into(),
+            }],
+        },
     );
+
+    let channel = channel.await.unwrap();
     channel.read_with(cx, |channel, _| {
         assert_eq!(
             channel
@@ -270,18 +242,16 @@ async fn test_channel_messages(cx: &mut TestAppContext) {
     // Client requests user for message since they haven't seen them yet
     let get_users = server.receive::<proto::GetUsers>().await.unwrap();
     assert_eq!(get_users.payload.user_ids, vec![7]);
-    server
-        .respond(
-            get_users.receipt(),
-            proto::UsersResponse {
-                users: vec![proto::User {
-                    id: 7,
-                    github_login: "as-cii".into(),
-                    avatar_url: "http://avatar.com/as-cii".into(),
-                }],
-            },
-        )
-        .await;
+    server.respond(
+        get_users.receipt(),
+        proto::UsersResponse {
+            users: vec![proto::User {
+                id: 7,
+                github_login: "as-cii".into(),
+                avatar_url: "http://avatar.com/as-cii".into(),
+            }],
+        },
+    );
 
     assert_eq!(
         channel.next_event(cx).await,
@@ -307,30 +277,28 @@ async fn test_channel_messages(cx: &mut TestAppContext) {
     let get_messages = server.receive::<proto::GetChannelMessages>().await.unwrap();
     assert_eq!(get_messages.payload.channel_id, 5);
     assert_eq!(get_messages.payload.before_message_id, 10);
-    server
-        .respond(
-            get_messages.receipt(),
-            proto::GetChannelMessagesResponse {
-                done: true,
-                messages: vec![
-                    proto::ChannelMessage {
-                        id: 8,
-                        body: "y".into(),
-                        timestamp: 998,
-                        sender_id: 5,
-                        nonce: Some(4.into()),
-                    },
-                    proto::ChannelMessage {
-                        id: 9,
-                        body: "z".into(),
-                        timestamp: 999,
-                        sender_id: 6,
-                        nonce: Some(5.into()),
-                    },
-                ],
-            },
-        )
-        .await;
+    server.respond(
+        get_messages.receipt(),
+        proto::GetChannelMessagesResponse {
+            done: true,
+            messages: vec![
+                proto::ChannelMessage {
+                    id: 8,
+                    body: "y".into(),
+                    timestamp: 998,
+                    sender_id: 5,
+                    nonce: Some(4.into()),
+                },
+                proto::ChannelMessage {
+                    id: 9,
+                    body: "z".into(),
+                    timestamp: 999,
+                    sender_id: 6,
+                    nonce: Some(5.into()),
+                },
+            ],
+        },
+    );
 
     assert_eq!(
         channel.next_event(cx).await,
@@ -353,6 +321,19 @@ async fn test_channel_messages(cx: &mut TestAppContext) {
     });
 }
 
+fn init_test(cx: &mut AppContext) -> ModelHandle<ChannelStore> {
+    let http = FakeHttpClient::with_404_response();
+    let client = Client::new(http.clone(), cx);
+    let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx));
+
+    cx.foreground().forbid_parking();
+    cx.set_global(SettingsStore::test(cx));
+    crate::init(&client);
+    client::init(&client, cx);
+
+    cx.add_model(|cx| ChannelStore::new(client, user_store, cx))
+}
+
 fn update_channels(
     channel_store: &ModelHandle<ChannelStore>,
     message: proto::UpdateChannels,

crates/client/src/test.rs 🔗

@@ -170,8 +170,7 @@ impl FakeServer {
                         staff: false,
                         flags: Default::default(),
                     },
-                )
-                .await;
+                );
                 continue;
             }
 
@@ -182,11 +181,7 @@ impl FakeServer {
         }
     }
 
-    pub async fn respond<T: proto::RequestMessage>(
-        &self,
-        receipt: Receipt<T>,
-        response: T::Response,
-    ) {
+    pub fn respond<T: proto::RequestMessage>(&self, receipt: Receipt<T>, response: T::Response) {
         self.peer.respond(receipt, response).unwrap()
     }
 

crates/collab/src/db/queries/messages.rs 🔗

@@ -82,7 +82,7 @@ impl Database {
                     id: row.id.to_proto(),
                     sender_id: row.sender_id.to_proto(),
                     body: row.body,
-                    timestamp: row.sent_at.unix_timestamp() as u64,
+                    timestamp: row.sent_at.assume_utc().unix_timestamp() as u64,
                     nonce: Some(proto::Nonce {
                         upper_half: nonce.0,
                         lower_half: nonce.1,
@@ -124,6 +124,9 @@ impl Database {
                 Err(anyhow!("not a chat participant"))?;
             }
 
+            let timestamp = timestamp.to_offset(time::UtcOffset::UTC);
+            let timestamp = time::PrimitiveDateTime::new(timestamp.date(), timestamp.time());
+
             let message = channel_message::Entity::insert(channel_message::ActiveModel {
                 channel_id: ActiveValue::Set(channel_id),
                 sender_id: ActiveValue::Set(user_id),

crates/collab/src/db/tables/channel_message.rs 🔗

@@ -1,6 +1,6 @@
 use crate::db::{ChannelId, MessageId, UserId};
 use sea_orm::entity::prelude::*;
-use time::OffsetDateTime;
+use time::PrimitiveDateTime;
 
 #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
 #[sea_orm(table_name = "channel_messages")]
@@ -10,7 +10,7 @@ pub struct Model {
     pub channel_id: ChannelId,
     pub sender_id: UserId,
     pub body: String,
-    pub sent_at: OffsetDateTime,
+    pub sent_at: PrimitiveDateTime,
     pub nonce: Uuid,
 }
 

crates/collab/src/db/tests/message_tests.rs 🔗

@@ -30,6 +30,12 @@ async fn test_channel_message_nonces(db: &Arc<Database>) {
         .await
         .unwrap();
 
+    let owner_id = db.create_server("test").await.unwrap().0 as u32;
+
+    db.join_channel_chat(channel, rpc::ConnectionId { owner_id, id: 0 }, user)
+        .await
+        .unwrap();
+
     let msg1_id = db
         .create_channel_message(channel, user, "1", OffsetDateTime::now_utc(), 1)
         .await

crates/collab_ui/src/chat_panel.rs 🔗

@@ -1,21 +1,33 @@
+use crate::collab_panel::{CollaborationPanelDockPosition, CollaborationPanelSettings};
+use anyhow::Result;
 use channel::{ChannelChat, ChannelChatEvent, ChannelMessage, ChannelStore};
 use client::Client;
+use db::kvp::KEY_VALUE_STORE;
 use editor::Editor;
 use gpui::{
     actions,
     elements::*,
     platform::{CursorStyle, MouseButton},
+    serde_json,
     views::{ItemType, Select, SelectStyle},
-    AnyViewHandle, AppContext, Entity, ModelHandle, Subscription, View, ViewContext, ViewHandle,
+    AnyViewHandle, AppContext, AsyncAppContext, Entity, ModelHandle, Subscription, Task, View,
+    ViewContext, ViewHandle, WeakViewHandle,
 };
 use language::language_settings::SoftWrap;
 use menu::Confirm;
+use project::Fs;
+use serde::{Deserialize, Serialize};
 use std::sync::Arc;
 use theme::Theme;
 use time::{OffsetDateTime, UtcOffset};
 use util::{ResultExt, TryFutureExt};
+use workspace::{
+    dock::{DockPosition, Panel},
+    Workspace,
+};
 
 const MESSAGE_LOADING_THRESHOLD: usize = 50;
+const CHAT_PANEL_KEY: &'static str = "ChatPanel";
 
 pub struct ChatPanel {
     client: Arc<Client>,
@@ -25,11 +37,25 @@ pub struct ChatPanel {
     input_editor: ViewHandle<Editor>,
     channel_select: ViewHandle<Select>,
     local_timezone: UtcOffset,
+    fs: Arc<dyn Fs>,
+    width: Option<f32>,
+    pending_serialization: Task<Option<()>>,
+    has_focus: bool,
+}
+
+#[derive(Serialize, Deserialize)]
+struct SerializedChatPanel {
+    width: Option<f32>,
 }
 
-pub enum Event {}
+#[derive(Debug)]
+pub enum Event {
+    DockPositionChanged,
+    Focus,
+    Dismissed,
+}
 
-actions!(chat_panel, [LoadMoreMessages]);
+actions!(chat_panel, [LoadMoreMessages, ToggleFocus]);
 
 pub fn init(cx: &mut AppContext) {
     cx.add_action(ChatPanel::send);
@@ -37,11 +63,11 @@ pub fn init(cx: &mut AppContext) {
 }
 
 impl ChatPanel {
-    pub fn new(
-        rpc: Arc<Client>,
-        channel_list: ModelHandle<ChannelStore>,
-        cx: &mut ViewContext<Self>,
-    ) -> Self {
+    pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> ViewHandle<Self> {
+        let fs = workspace.app_state().fs.clone();
+        let client = workspace.app_state().client.clone();
+        let channel_store = workspace.app_state().channel_store.clone();
+
         let input_editor = cx.add_view(|cx| {
             let mut editor = Editor::auto_height(
                 4,
@@ -51,12 +77,13 @@ impl ChatPanel {
             editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
             editor
         });
+
         let channel_select = cx.add_view(|cx| {
-            let channel_list = channel_list.clone();
+            let channel_store = channel_store.clone();
             Select::new(0, cx, {
                 move |ix, item_type, is_hovered, cx| {
                     Self::render_channel_name(
-                        &channel_list,
+                        &channel_store,
                         ix,
                         item_type,
                         is_hovered,
@@ -85,45 +112,97 @@ impl ChatPanel {
             }
         });
 
-        let mut this = Self {
-            client: rpc,
-            channel_store: channel_list,
-            active_channel: Default::default(),
-            message_list,
-            input_editor,
-            channel_select,
-            local_timezone: cx.platform().local_timezone(),
-        };
+        cx.add_view(|cx| {
+            let mut this = Self {
+                fs,
+                client,
+                channel_store,
+                active_channel: Default::default(),
+                pending_serialization: Task::ready(None),
+                message_list,
+                input_editor,
+                channel_select,
+                local_timezone: cx.platform().local_timezone(),
+                has_focus: false,
+                width: None,
+            };
 
-        this.init_active_channel(cx);
-        cx.observe(&this.channel_store, |this, _, cx| {
             this.init_active_channel(cx);
-        })
-        .detach();
-
-        cx.observe(&this.channel_select, |this, channel_select, cx| {
-            let selected_ix = channel_select.read(cx).selected_index();
-            let selected_channel_id = this
-                .channel_store
-                .read(cx)
-                .channel_at_index(selected_ix)
-                .map(|e| e.1.id);
-            if let Some(selected_channel_id) = selected_channel_id {
-                let open_chat = this.channel_store.update(cx, |store, cx| {
-                    store.open_channel_chat(selected_channel_id, cx)
-                });
-                cx.spawn(|this, mut cx| async move {
-                    let chat = open_chat.await?;
-                    this.update(&mut cx, |this, cx| {
-                        this.set_active_channel(chat, cx);
+            cx.observe(&this.channel_store, |this, _, cx| {
+                this.init_active_channel(cx);
+            })
+            .detach();
+
+            cx.observe(&this.channel_select, |this, channel_select, cx| {
+                let selected_ix = channel_select.read(cx).selected_index();
+                let selected_channel_id = this
+                    .channel_store
+                    .read(cx)
+                    .channel_at_index(selected_ix)
+                    .map(|e| e.1.id);
+                if let Some(selected_channel_id) = selected_channel_id {
+                    let open_chat = this.channel_store.update(cx, |store, cx| {
+                        store.open_channel_chat(selected_channel_id, cx)
+                    });
+                    cx.spawn(|this, mut cx| async move {
+                        let chat = open_chat.await?;
+                        this.update(&mut cx, |this, cx| {
+                            this.set_active_channel(chat, cx);
+                        })
                     })
-                })
-                .detach_and_log_err(cx);
-            }
+                    .detach_and_log_err(cx);
+                }
+            })
+            .detach();
+
+            this
+        })
+    }
+
+    pub fn load(
+        workspace: WeakViewHandle<Workspace>,
+        cx: AsyncAppContext,
+    ) -> Task<Result<ViewHandle<Self>>> {
+        cx.spawn(|mut cx| async move {
+            let serialized_panel = if let Some(panel) = cx
+                .background()
+                .spawn(async move { KEY_VALUE_STORE.read_kvp(CHAT_PANEL_KEY) })
+                .await
+                .log_err()
+                .flatten()
+            {
+                Some(serde_json::from_str::<SerializedChatPanel>(&panel)?)
+            } else {
+                None
+            };
+
+            workspace.update(&mut cx, |workspace, cx| {
+                let panel = Self::new(workspace, cx);
+                if let Some(serialized_panel) = serialized_panel {
+                    panel.update(cx, |panel, cx| {
+                        panel.width = serialized_panel.width;
+                        cx.notify();
+                    });
+                }
+                panel
+            })
         })
-        .detach();
+    }
 
-        this
+    fn serialize(&mut self, cx: &mut ViewContext<Self>) {
+        let width = self.width;
+        self.pending_serialization = cx.background().spawn(
+            async move {
+                KEY_VALUE_STORE
+                    .write_kvp(
+                        CHAT_PANEL_KEY.into(),
+                        serde_json::to_string(&SerializedChatPanel { width })?,
+                    )
+                    .await?;
+                anyhow::Ok(())
+            }
+            .log_err(),
+        );
     }
 
     fn init_active_channel(&mut self, cx: &mut ViewContext<Self>) {
@@ -365,6 +444,68 @@ impl View for ChatPanel {
     }
 }
 
+impl Panel for ChatPanel {
+    fn position(&self, cx: &gpui::WindowContext) -> DockPosition {
+        match settings::get::<CollaborationPanelSettings>(cx).dock {
+            CollaborationPanelDockPosition::Left => DockPosition::Left,
+            CollaborationPanelDockPosition::Right => DockPosition::Right,
+        }
+    }
+
+    fn position_is_valid(&self, position: DockPosition) -> bool {
+        matches!(position, DockPosition::Left | DockPosition::Right)
+    }
+
+    fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
+        settings::update_settings_file::<CollaborationPanelSettings>(
+            self.fs.clone(),
+            cx,
+            move |settings| {
+                let dock = match position {
+                    DockPosition::Left | DockPosition::Bottom => {
+                        CollaborationPanelDockPosition::Left
+                    }
+                    DockPosition::Right => CollaborationPanelDockPosition::Right,
+                };
+                settings.dock = Some(dock);
+            },
+        );
+    }
+
+    fn size(&self, cx: &gpui::WindowContext) -> f32 {
+        self.width
+            .unwrap_or_else(|| settings::get::<CollaborationPanelSettings>(cx).default_width)
+    }
+
+    fn set_size(&mut self, size: Option<f32>, cx: &mut ViewContext<Self>) {
+        self.width = size;
+        self.serialize(cx);
+        cx.notify();
+    }
+
+    fn icon_path(&self, cx: &gpui::WindowContext) -> Option<&'static str> {
+        settings::get::<CollaborationPanelSettings>(cx)
+            .button
+            .then(|| "icons/conversations.svg")
+    }
+
+    fn icon_tooltip(&self) -> (String, Option<Box<dyn gpui::Action>>) {
+        ("Chat Panel".to_string(), Some(Box::new(ToggleFocus)))
+    }
+
+    fn should_change_position_on_event(event: &Self::Event) -> bool {
+        matches!(event, Event::DockPositionChanged)
+    }
+
+    fn has_focus(&self, _cx: &gpui::WindowContext) -> bool {
+        self.has_focus
+    }
+
+    fn is_focus_event(event: &Self::Event) -> bool {
+        matches!(event, Event::Focus)
+    }
+}
+
 fn format_timestamp(
     mut timestamp: OffsetDateTime,
     mut now: OffsetDateTime,

crates/collab_ui/src/collab_panel.rs 🔗

@@ -2,14 +2,18 @@ mod channel_modal;
 mod contact_finder;
 mod panel_settings;
 
+use crate::{
+    channel_view::{self, ChannelView},
+    face_pile::FacePile,
+};
 use anyhow::Result;
 use call::ActiveCall;
 use channel::{Channel, ChannelEvent, ChannelId, ChannelStore};
+use channel_modal::ChannelModal;
 use client::{proto::PeerId, Client, Contact, User, UserStore};
 use context_menu::{ContextMenu, ContextMenuItem};
 use db::kvp::KEY_VALUE_STORE;
 use editor::{Cancel, Editor};
-
 use feature_flags::{ChannelsAlpha, FeatureFlagAppExt, FeatureFlagViewExt};
 use futures::StreamExt;
 use fuzzy::{match_strings, StringMatchCandidate};
@@ -31,7 +35,6 @@ use gpui::{
     Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle,
 };
 use menu::{Confirm, SelectNext, SelectPrev};
-use panel_settings::{CollaborationPanelDockPosition, CollaborationPanelSettings};
 use project::{Fs, Project};
 use serde_derive::{Deserialize, Serialize};
 use settings::SettingsStore;
@@ -44,11 +47,7 @@ use workspace::{
     Workspace,
 };
 
-use crate::{
-    channel_view::{self, ChannelView},
-    face_pile::FacePile,
-};
-use channel_modal::ChannelModal;
+pub use panel_settings::{CollaborationPanelDockPosition, CollaborationPanelSettings};
 
 use self::contact_finder::ContactFinder;
 
@@ -113,7 +112,7 @@ impl_actions!(
 
 const COLLABORATION_PANEL_KEY: &'static str = "CollaborationPanel";
 
-pub fn init(_client: Arc<Client>, cx: &mut AppContext) {
+pub fn init(cx: &mut AppContext) {
     settings::register::<panel_settings::CollaborationPanelSettings>(cx);
     contact_finder::init(cx);
     channel_modal::init(cx);

crates/collab_ui/src/collab_ui.rs 🔗

@@ -32,7 +32,8 @@ actions!(
 pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
     vcs_menu::init(cx);
     collab_titlebar_item::init(cx);
-    collab_panel::init(app_state.client.clone(), cx);
+    collab_panel::init(cx);
+    chat_panel::init(cx);
     incoming_call_notification::init(&app_state, cx);
     project_shared_notification::init(&app_state, cx);
     sharing_status_indicator::init(cx);

crates/zed/src/zed.rs 🔗

@@ -214,6 +214,13 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::AppContext) {
             workspace.toggle_panel_focus::<collab_ui::collab_panel::CollabPanel>(cx);
         },
     );
+    cx.add_action(
+        |workspace: &mut Workspace,
+         _: &collab_ui::chat_panel::ToggleFocus,
+         cx: &mut ViewContext<Workspace>| {
+            workspace.toggle_panel_focus::<collab_ui::chat_panel::ChatPanel>(cx);
+        },
+    );
     cx.add_action(
         |workspace: &mut Workspace,
          _: &terminal_panel::ToggleFocus,
@@ -338,11 +345,14 @@ pub fn initialize_workspace(
         let assistant_panel = AssistantPanel::load(workspace_handle.clone(), cx.clone());
         let channels_panel =
             collab_ui::collab_panel::CollabPanel::load(workspace_handle.clone(), cx.clone());
-        let (project_panel, terminal_panel, assistant_panel, channels_panel) = futures::try_join!(
+        let chat_panel =
+            collab_ui::chat_panel::ChatPanel::load(workspace_handle.clone(), cx.clone());
+        let (project_panel, terminal_panel, assistant_panel, channels_panel, chat_panel) = futures::try_join!(
             project_panel,
             terminal_panel,
             assistant_panel,
-            channels_panel
+            channels_panel,
+            chat_panel,
         )?;
         workspace_handle.update(&mut cx, |workspace, cx| {
             let project_panel_position = project_panel.position(cx);
@@ -362,6 +372,7 @@ pub fn initialize_workspace(
             workspace.add_panel(terminal_panel, cx);
             workspace.add_panel(assistant_panel, cx);
             workspace.add_panel(channels_panel, cx);
+            workspace.add_panel(chat_panel, cx);
 
             if !was_deserialized
                 && workspace

styles/src/style_tree/app.ts 🔗

@@ -12,6 +12,7 @@ import simple_message_notification from "./simple_message_notification"
 import project_shared_notification from "./project_shared_notification"
 import tooltip from "./tooltip"
 import terminal from "./terminal"
+import chat_panel from "./chat_panel"
 import collab_panel from "./collab_panel"
 import toolbar_dropdown_menu from "./toolbar_dropdown_menu"
 import incoming_call_notification from "./incoming_call_notification"
@@ -55,6 +56,7 @@ export default function app(): any {
         terminal: terminal(),
         assistant: assistant(),
         feedback: feedback(),
+        chat_panel: chat_panel(),
         component_test: component_test(),
     }
 }

styles/src/style_tree/chat_panel.ts 🔗

@@ -0,0 +1,111 @@
+import {
+    background,
+    border,
+    border_color,
+    foreground,
+    text,
+} from "./components"
+import { interactive, toggleable } from "../element"
+import { useTheme } from "../theme"
+import collab_modals from "./collab_modals"
+import { icon_button, toggleable_icon_button } from "../component/icon_button"
+import { indicator } from "../component/indicator"
+
+export default function contacts_panel(): any {
+    const theme = useTheme()
+
+    const CHANNEL_SPACING = 4 as const
+    const NAME_MARGIN = 6 as const
+    const SPACING = 12 as const
+    const INDENT_SIZE = 8 as const
+    const ITEM_HEIGHT = 28 as const
+
+    const layer = theme.middle
+
+    const input_editor = {
+        background: background(layer, "on"),
+        corner_radius: 6,
+        text: text(layer, "sans", "base"),
+        placeholder_text: text(layer, "sans", "base", "disabled", {
+            size: "xs",
+        }),
+        selection: theme.players[0],
+        border: border(layer, "on"),
+        padding: {
+            bottom: 4,
+            left: 8,
+            right: 8,
+            top: 4,
+        },
+        margin: {
+            left: SPACING,
+            right: SPACING,
+        },
+    }
+
+    const channel_name = {
+        padding: {
+            top: 4,
+            bottom: 4,
+            left: 4,
+            right: 4,
+        },
+        hash: {
+            ...text(layer, "sans", "base"),
+        },
+        name: text(layer, "sans", "base"),
+    }
+
+    return {
+        background: background(layer),
+        channel_select: {
+            header: channel_name,
+            item: channel_name,
+            active_item: channel_name,
+            hovered_item: channel_name,
+            hovered_active_item: channel_name,
+            menu: {
+                padding: {
+                    top: 10,
+                    bottom: 10,
+                }
+            }
+        },
+        input_editor,
+        message: {
+            body: text(layer, "sans", "base"),
+            sender: {
+                padding: {
+                    left: 4,
+                    right: 4,
+                },
+                ...text(layer, "sans", "base", "disabled"),
+            },
+            timestamp: text(layer, "sans", "base"),
+        },
+        pending_message: {
+            body: text(layer, "sans", "base"),
+            sender: {
+                padding: {
+                    left: 4,
+                    right: 4,
+                },
+                ...text(layer, "sans", "base", "disabled"),
+            },
+            timestamp: text(layer, "sans", "base"),
+        },
+        sign_in_prompt: {
+            default: text(layer, "sans", "base"),
+        },
+        timestamp: {
+            body: text(layer, "sans", "base"),
+            sender: {
+                padding: {
+                    left: 4,
+                    right: 4,
+                },
+                ...text(layer, "sans", "base", "disabled"),
+            }
+        }
+    }
+}