Move placeholder titlebar render to collab_ui

Conrad Irwin created

Change summary

Cargo.lock                                    |    1 
Cargo.toml                                    |    1 
crates/collab_ui2/Cargo.toml                  |    1 
crates/collab_ui2/src/channel_view.rs         |  908 +-
crates/collab_ui2/src/chat_panel.rs           | 1966 ++--
crates/collab_ui2/src/collab_panel.rs         | 7096 ++++++++++----------
crates/collab_ui2/src/collab_titlebar_item.rs | 2549 +++---
crates/collab_ui2/src/collab_ui.rs            |  262 
crates/collab_ui2/src/face_pile.rs            |  196 
crates/collab_ui2/src/notification_panel.rs   | 1768 ++--
crates/collab_ui2/src/notifications.rs        |   18 
crates/collab_ui2/src/panel_settings.rs       |   14 
crates/workspace2/src/workspace2.rs           |  101 
crates/zed2/src/main.rs                       |    2 
crates/zed2/src/zed2.rs                       |    4 
15 files changed, 7,447 insertions(+), 7,440 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -1864,6 +1864,7 @@ dependencies = [
  "theme2",
  "time",
  "tree-sitter-markdown",
+ "ui2",
  "util",
  "workspace2",
  "zed_actions2",

Cargo.toml 🔗

@@ -18,6 +18,7 @@ members = [
     "crates/collab",
     "crates/collab2",
     "crates/collab_ui",
+    "crates/collab_ui2",
     "crates/collections",
     "crates/command_palette",
     "crates/command_palette2",

crates/collab_ui2/Cargo.toml 🔗

@@ -48,6 +48,7 @@ feature_flags = { package = "feature_flags2", path = "../feature_flags2"}
 theme = { package = "theme2", path = "../theme2" }
 # theme_selector = { path = "../theme_selector" }
 # vcs_menu = { path = "../vcs_menu" }
+ui = { package = "ui2", path = "../ui2" }
 util = { path = "../util" }
 workspace = { package = "workspace2", path = "../workspace2" }
 zed-actions = { package="zed_actions2", path = "../zed_actions2"}

crates/collab_ui2/src/channel_view.rs 🔗

@@ -1,454 +1,454 @@
-use anyhow::{anyhow, Result};
-use call::report_call_event_for_channel;
-use channel::{Channel, ChannelBuffer, ChannelBufferEvent, ChannelId, ChannelStore};
-use client::{
-    proto::{self, PeerId},
-    Collaborator, ParticipantIndex,
-};
-use collections::HashMap;
-use editor::{CollaborationHub, Editor};
-use gpui::{
-    actions,
-    elements::{ChildView, Label},
-    geometry::vector::Vector2F,
-    AnyElement, AnyViewHandle, AppContext, Element, Entity, ModelHandle, Subscription, Task, View,
-    ViewContext, ViewHandle,
-};
-use project::Project;
-use smallvec::SmallVec;
-use std::{
-    any::{Any, TypeId},
-    sync::Arc,
-};
-use util::ResultExt;
-use workspace::{
-    item::{FollowableItem, Item, ItemEvent, ItemHandle},
-    register_followable_item,
-    searchable::SearchableItemHandle,
-    ItemNavHistory, Pane, SaveIntent, ViewId, Workspace, WorkspaceId,
-};
-
-actions!(channel_view, [Deploy]);
-
-pub fn init(cx: &mut AppContext) {
-    register_followable_item::<ChannelView>(cx)
-}
-
-pub struct ChannelView {
-    pub editor: ViewHandle<Editor>,
-    project: ModelHandle<Project>,
-    channel_store: ModelHandle<ChannelStore>,
-    channel_buffer: ModelHandle<ChannelBuffer>,
-    remote_id: Option<ViewId>,
-    _editor_event_subscription: Subscription,
-}
-
-impl ChannelView {
-    pub fn open(
-        channel_id: ChannelId,
-        workspace: ViewHandle<Workspace>,
-        cx: &mut AppContext,
-    ) -> Task<Result<ViewHandle<Self>>> {
-        let pane = workspace.read(cx).active_pane().clone();
-        let channel_view = Self::open_in_pane(channel_id, pane.clone(), workspace.clone(), cx);
-        cx.spawn(|mut cx| async move {
-            let channel_view = channel_view.await?;
-            pane.update(&mut cx, |pane, cx| {
-                report_call_event_for_channel(
-                    "open channel notes",
-                    channel_id,
-                    &workspace.read(cx).app_state().client,
-                    cx,
-                );
-                pane.add_item(Box::new(channel_view.clone()), true, true, None, cx);
-            });
-            anyhow::Ok(channel_view)
-        })
-    }
-
-    pub fn open_in_pane(
-        channel_id: ChannelId,
-        pane: ViewHandle<Pane>,
-        workspace: ViewHandle<Workspace>,
-        cx: &mut AppContext,
-    ) -> Task<Result<ViewHandle<Self>>> {
-        let workspace = workspace.read(cx);
-        let project = workspace.project().to_owned();
-        let channel_store = ChannelStore::global(cx);
-        let language_registry = workspace.app_state().languages.clone();
-        let markdown = language_registry.language_for_name("Markdown");
-        let channel_buffer =
-            channel_store.update(cx, |store, cx| store.open_channel_buffer(channel_id, cx));
-
-        cx.spawn(|mut cx| async move {
-            let channel_buffer = channel_buffer.await?;
-            let markdown = markdown.await.log_err();
-
-            channel_buffer.update(&mut cx, |buffer, cx| {
-                buffer.buffer().update(cx, |buffer, cx| {
-                    buffer.set_language_registry(language_registry);
-                    if let Some(markdown) = markdown {
-                        buffer.set_language(Some(markdown), cx);
-                    }
-                })
-            });
-
-            pane.update(&mut cx, |pane, cx| {
-                let buffer_id = channel_buffer.read(cx).remote_id(cx);
-
-                let existing_view = pane
-                    .items_of_type::<Self>()
-                    .find(|view| view.read(cx).channel_buffer.read(cx).remote_id(cx) == buffer_id);
-
-                // If this channel buffer is already open in this pane, just return it.
-                if let Some(existing_view) = existing_view.clone() {
-                    if existing_view.read(cx).channel_buffer == channel_buffer {
-                        return existing_view;
-                    }
-                }
-
-                let view = cx.add_view(|cx| {
-                    let mut this = Self::new(project, channel_store, channel_buffer, cx);
-                    this.acknowledge_buffer_version(cx);
-                    this
-                });
-
-                // If the pane contained a disconnected view for this channel buffer,
-                // replace that.
-                if let Some(existing_item) = existing_view {
-                    if let Some(ix) = pane.index_for_item(&existing_item) {
-                        pane.close_item_by_id(existing_item.id(), SaveIntent::Skip, cx)
-                            .detach();
-                        pane.add_item(Box::new(view.clone()), true, true, Some(ix), cx);
-                    }
-                }
-
-                view
-            })
-            .ok_or_else(|| anyhow!("pane was dropped"))
-        })
-    }
-
-    pub fn new(
-        project: ModelHandle<Project>,
-        channel_store: ModelHandle<ChannelStore>,
-        channel_buffer: ModelHandle<ChannelBuffer>,
-        cx: &mut ViewContext<Self>,
-    ) -> Self {
-        let buffer = channel_buffer.read(cx).buffer();
-        let editor = cx.add_view(|cx| {
-            let mut editor = Editor::for_buffer(buffer, None, cx);
-            editor.set_collaboration_hub(Box::new(ChannelBufferCollaborationHub(
-                channel_buffer.clone(),
-            )));
-            editor.set_read_only(
-                !channel_buffer
-                    .read(cx)
-                    .channel(cx)
-                    .is_some_and(|c| c.can_edit_notes()),
-            );
-            editor
-        });
-        let _editor_event_subscription = cx.subscribe(&editor, |_, _, e, cx| cx.emit(e.clone()));
-
-        cx.subscribe(&channel_buffer, Self::handle_channel_buffer_event)
-            .detach();
-
-        Self {
-            editor,
-            project,
-            channel_store,
-            channel_buffer,
-            remote_id: None,
-            _editor_event_subscription,
-        }
-    }
-
-    pub fn channel(&self, cx: &AppContext) -> Option<Arc<Channel>> {
-        self.channel_buffer.read(cx).channel(cx)
-    }
-
-    fn handle_channel_buffer_event(
-        &mut self,
-        _: ModelHandle<ChannelBuffer>,
-        event: &ChannelBufferEvent,
-        cx: &mut ViewContext<Self>,
-    ) {
-        match event {
-            ChannelBufferEvent::Disconnected => self.editor.update(cx, |editor, cx| {
-                editor.set_read_only(true);
-                cx.notify();
-            }),
-            ChannelBufferEvent::ChannelChanged => {
-                self.editor.update(cx, |editor, cx| {
-                    editor.set_read_only(!self.channel(cx).is_some_and(|c| c.can_edit_notes()));
-                    cx.emit(editor::Event::TitleChanged);
-                    cx.notify()
-                });
-            }
-            ChannelBufferEvent::BufferEdited => {
-                if cx.is_self_focused() || self.editor.is_focused(cx) {
-                    self.acknowledge_buffer_version(cx);
-                } else {
-                    self.channel_store.update(cx, |store, cx| {
-                        let channel_buffer = self.channel_buffer.read(cx);
-                        store.notes_changed(
-                            channel_buffer.channel_id,
-                            channel_buffer.epoch(),
-                            &channel_buffer.buffer().read(cx).version(),
-                            cx,
-                        )
-                    });
-                }
-            }
-            ChannelBufferEvent::CollaboratorsChanged => {}
-        }
-    }
-
-    fn acknowledge_buffer_version(&mut self, cx: &mut ViewContext<'_, '_, ChannelView>) {
-        self.channel_store.update(cx, |store, cx| {
-            let channel_buffer = self.channel_buffer.read(cx);
-            store.acknowledge_notes_version(
-                channel_buffer.channel_id,
-                channel_buffer.epoch(),
-                &channel_buffer.buffer().read(cx).version(),
-                cx,
-            )
-        });
-        self.channel_buffer.update(cx, |buffer, cx| {
-            buffer.acknowledge_buffer_version(cx);
-        });
-    }
-}
-
-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()
-    }
-
-    fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
-        if cx.is_self_focused() {
-            self.acknowledge_buffer_version(cx);
-            cx.focus(self.editor.as_any())
-        }
-    }
-}
-
-impl Item for ChannelView {
-    fn act_as_type<'a>(
-        &'a self,
-        type_id: TypeId,
-        self_handle: &'a ViewHandle<Self>,
-        _: &'a AppContext,
-    ) -> Option<&'a AnyViewHandle> {
-        if type_id == TypeId::of::<Self>() {
-            Some(self_handle)
-        } else if type_id == TypeId::of::<Editor>() {
-            Some(&self.editor)
-        } else {
-            None
-        }
-    }
-
-    fn tab_content<V: 'static>(
-        &self,
-        _: Option<usize>,
-        style: &theme::Tab,
-        cx: &gpui::AppContext,
-    ) -> AnyElement<V> {
-        let label = if let Some(channel) = self.channel(cx) {
-            match (
-                channel.can_edit_notes(),
-                self.channel_buffer.read(cx).is_connected(),
-            ) {
-                (true, true) => format!("#{}", channel.name),
-                (false, true) => format!("#{} (read-only)", channel.name),
-                (_, false) => format!("#{} (disconnected)", channel.name),
-            }
-        } else {
-            format!("channel notes (disconnected)")
-        };
-        Label::new(label, style.label.to_owned()).into_any()
-    }
-
-    fn clone_on_split(&self, _: WorkspaceId, cx: &mut ViewContext<Self>) -> Option<Self> {
-        Some(Self::new(
-            self.project.clone(),
-            self.channel_store.clone(),
-            self.channel_buffer.clone(),
-            cx,
-        ))
-    }
-
-    fn is_singleton(&self, _cx: &AppContext) -> bool {
-        false
-    }
-
-    fn navigate(&mut self, data: Box<dyn Any>, cx: &mut ViewContext<Self>) -> bool {
-        self.editor
-            .update(cx, |editor, cx| editor.navigate(data, cx))
-    }
-
-    fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
-        self.editor
-            .update(cx, |editor, cx| Item::deactivated(editor, cx))
-    }
-
-    fn set_nav_history(&mut self, history: ItemNavHistory, cx: &mut ViewContext<Self>) {
-        self.editor
-            .update(cx, |editor, cx| Item::set_nav_history(editor, history, cx))
-    }
-
-    fn as_searchable(&self, _: &ViewHandle<Self>) -> Option<Box<dyn SearchableItemHandle>> {
-        Some(Box::new(self.editor.clone()))
-    }
-
-    fn show_toolbar(&self) -> bool {
-        true
-    }
-
-    fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option<Vector2F> {
-        self.editor.read(cx).pixel_position_of_cursor(cx)
-    }
-
-    fn to_item_events(event: &Self::Event) -> SmallVec<[ItemEvent; 2]> {
-        editor::Editor::to_item_events(event)
-    }
-}
-
-impl FollowableItem for ChannelView {
-    fn remote_id(&self) -> Option<workspace::ViewId> {
-        self.remote_id
-    }
-
-    fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant> {
-        let channel_buffer = self.channel_buffer.read(cx);
-        if !channel_buffer.is_connected() {
-            return None;
-        }
-
-        Some(proto::view::Variant::ChannelView(
-            proto::view::ChannelView {
-                channel_id: channel_buffer.channel_id,
-                editor: if let Some(proto::view::Variant::Editor(proto)) =
-                    self.editor.read(cx).to_state_proto(cx)
-                {
-                    Some(proto)
-                } else {
-                    None
-                },
-            },
-        ))
-    }
-
-    fn from_state_proto(
-        pane: ViewHandle<workspace::Pane>,
-        workspace: ViewHandle<workspace::Workspace>,
-        remote_id: workspace::ViewId,
-        state: &mut Option<proto::view::Variant>,
-        cx: &mut AppContext,
-    ) -> Option<gpui::Task<anyhow::Result<ViewHandle<Self>>>> {
-        let Some(proto::view::Variant::ChannelView(_)) = state else {
-            return None;
-        };
-        let Some(proto::view::Variant::ChannelView(state)) = state.take() else {
-            unreachable!()
-        };
-
-        let open = ChannelView::open_in_pane(state.channel_id, pane, workspace, cx);
-
-        Some(cx.spawn(|mut cx| async move {
-            let this = open.await?;
-
-            let task = this
-                .update(&mut cx, |this, cx| {
-                    this.remote_id = Some(remote_id);
-
-                    if let Some(state) = state.editor {
-                        Some(this.editor.update(cx, |editor, cx| {
-                            editor.apply_update_proto(
-                                &this.project,
-                                proto::update_view::Variant::Editor(proto::update_view::Editor {
-                                    selections: state.selections,
-                                    pending_selection: state.pending_selection,
-                                    scroll_top_anchor: state.scroll_top_anchor,
-                                    scroll_x: state.scroll_x,
-                                    scroll_y: state.scroll_y,
-                                    ..Default::default()
-                                }),
-                                cx,
-                            )
-                        }))
-                    } else {
-                        None
-                    }
-                })
-                .ok_or_else(|| anyhow!("window was closed"))?;
-
-            if let Some(task) = task {
-                task.await?;
-            }
-
-            Ok(this)
-        }))
-    }
-
-    fn add_event_to_update_proto(
-        &self,
-        event: &Self::Event,
-        update: &mut Option<proto::update_view::Variant>,
-        cx: &AppContext,
-    ) -> bool {
-        self.editor
-            .read(cx)
-            .add_event_to_update_proto(event, update, cx)
-    }
-
-    fn apply_update_proto(
-        &mut self,
-        project: &ModelHandle<Project>,
-        message: proto::update_view::Variant,
-        cx: &mut ViewContext<Self>,
-    ) -> gpui::Task<anyhow::Result<()>> {
-        self.editor.update(cx, |editor, cx| {
-            editor.apply_update_proto(project, message, cx)
-        })
-    }
-
-    fn set_leader_peer_id(&mut self, leader_peer_id: Option<PeerId>, cx: &mut ViewContext<Self>) {
-        self.editor.update(cx, |editor, cx| {
-            editor.set_leader_peer_id(leader_peer_id, cx)
-        })
-    }
-
-    fn should_unfollow_on_event(event: &Self::Event, cx: &AppContext) -> bool {
-        Editor::should_unfollow_on_event(event, cx)
-    }
-
-    fn is_project_item(&self, _cx: &AppContext) -> bool {
-        false
-    }
-}
-
-struct ChannelBufferCollaborationHub(ModelHandle<ChannelBuffer>);
-
-impl CollaborationHub for ChannelBufferCollaborationHub {
-    fn collaborators<'a>(&self, cx: &'a AppContext) -> &'a HashMap<PeerId, Collaborator> {
-        self.0.read(cx).collaborators()
-    }
-
-    fn user_participant_indices<'a>(
-        &self,
-        cx: &'a AppContext,
-    ) -> &'a HashMap<u64, ParticipantIndex> {
-        self.0.read(cx).user_store().read(cx).participant_indices()
-    }
-}
+// use anyhow::{anyhow, Result};
+// use call::report_call_event_for_channel;
+// use channel::{Channel, ChannelBuffer, ChannelBufferEvent, ChannelId, ChannelStore};
+// use client::{
+//     proto::{self, PeerId},
+//     Collaborator, ParticipantIndex,
+// };
+// use collections::HashMap;
+// use editor::{CollaborationHub, Editor};
+// use gpui::{
+//     actions,
+//     elements::{ChildView, Label},
+//     geometry::vector::Vector2F,
+//     AnyElement, AnyViewHandle, AppContext, Element, Entity, ModelHandle, Subscription, Task, View,
+//     ViewContext, ViewHandle,
+// };
+// use project::Project;
+// use smallvec::SmallVec;
+// use std::{
+//     any::{Any, TypeId},
+//     sync::Arc,
+// };
+// use util::ResultExt;
+// use workspace::{
+//     item::{FollowableItem, Item, ItemEvent, ItemHandle},
+//     register_followable_item,
+//     searchable::SearchableItemHandle,
+//     ItemNavHistory, Pane, SaveIntent, ViewId, Workspace, WorkspaceId,
+// };
+
+// actions!(channel_view, [Deploy]);
+
+// pub fn init(cx: &mut AppContext) {
+//     register_followable_item::<ChannelView>(cx)
+// }
+
+// pub struct ChannelView {
+//     pub editor: ViewHandle<Editor>,
+//     project: ModelHandle<Project>,
+//     channel_store: ModelHandle<ChannelStore>,
+//     channel_buffer: ModelHandle<ChannelBuffer>,
+//     remote_id: Option<ViewId>,
+//     _editor_event_subscription: Subscription,
+// }
+
+// impl ChannelView {
+//     pub fn open(
+//         channel_id: ChannelId,
+//         workspace: ViewHandle<Workspace>,
+//         cx: &mut AppContext,
+//     ) -> Task<Result<ViewHandle<Self>>> {
+//         let pane = workspace.read(cx).active_pane().clone();
+//         let channel_view = Self::open_in_pane(channel_id, pane.clone(), workspace.clone(), cx);
+//         cx.spawn(|mut cx| async move {
+//             let channel_view = channel_view.await?;
+//             pane.update(&mut cx, |pane, cx| {
+//                 report_call_event_for_channel(
+//                     "open channel notes",
+//                     channel_id,
+//                     &workspace.read(cx).app_state().client,
+//                     cx,
+//                 );
+//                 pane.add_item(Box::new(channel_view.clone()), true, true, None, cx);
+//             });
+//             anyhow::Ok(channel_view)
+//         })
+//     }
+
+//     pub fn open_in_pane(
+//         channel_id: ChannelId,
+//         pane: ViewHandle<Pane>,
+//         workspace: ViewHandle<Workspace>,
+//         cx: &mut AppContext,
+//     ) -> Task<Result<ViewHandle<Self>>> {
+//         let workspace = workspace.read(cx);
+//         let project = workspace.project().to_owned();
+//         let channel_store = ChannelStore::global(cx);
+//         let language_registry = workspace.app_state().languages.clone();
+//         let markdown = language_registry.language_for_name("Markdown");
+//         let channel_buffer =
+//             channel_store.update(cx, |store, cx| store.open_channel_buffer(channel_id, cx));
+
+//         cx.spawn(|mut cx| async move {
+//             let channel_buffer = channel_buffer.await?;
+//             let markdown = markdown.await.log_err();
+
+//             channel_buffer.update(&mut cx, |buffer, cx| {
+//                 buffer.buffer().update(cx, |buffer, cx| {
+//                     buffer.set_language_registry(language_registry);
+//                     if let Some(markdown) = markdown {
+//                         buffer.set_language(Some(markdown), cx);
+//                     }
+//                 })
+//             });
+
+//             pane.update(&mut cx, |pane, cx| {
+//                 let buffer_id = channel_buffer.read(cx).remote_id(cx);
+
+//                 let existing_view = pane
+//                     .items_of_type::<Self>()
+//                     .find(|view| view.read(cx).channel_buffer.read(cx).remote_id(cx) == buffer_id);
+
+//                 // If this channel buffer is already open in this pane, just return it.
+//                 if let Some(existing_view) = existing_view.clone() {
+//                     if existing_view.read(cx).channel_buffer == channel_buffer {
+//                         return existing_view;
+//                     }
+//                 }
+
+//                 let view = cx.add_view(|cx| {
+//                     let mut this = Self::new(project, channel_store, channel_buffer, cx);
+//                     this.acknowledge_buffer_version(cx);
+//                     this
+//                 });
+
+//                 // If the pane contained a disconnected view for this channel buffer,
+//                 // replace that.
+//                 if let Some(existing_item) = existing_view {
+//                     if let Some(ix) = pane.index_for_item(&existing_item) {
+//                         pane.close_item_by_id(existing_item.id(), SaveIntent::Skip, cx)
+//                             .detach();
+//                         pane.add_item(Box::new(view.clone()), true, true, Some(ix), cx);
+//                     }
+//                 }
+
+//                 view
+//             })
+//             .ok_or_else(|| anyhow!("pane was dropped"))
+//         })
+//     }
+
+//     pub fn new(
+//         project: ModelHandle<Project>,
+//         channel_store: ModelHandle<ChannelStore>,
+//         channel_buffer: ModelHandle<ChannelBuffer>,
+//         cx: &mut ViewContext<Self>,
+//     ) -> Self {
+//         let buffer = channel_buffer.read(cx).buffer();
+//         let editor = cx.add_view(|cx| {
+//             let mut editor = Editor::for_buffer(buffer, None, cx);
+//             editor.set_collaboration_hub(Box::new(ChannelBufferCollaborationHub(
+//                 channel_buffer.clone(),
+//             )));
+//             editor.set_read_only(
+//                 !channel_buffer
+//                     .read(cx)
+//                     .channel(cx)
+//                     .is_some_and(|c| c.can_edit_notes()),
+//             );
+//             editor
+//         });
+//         let _editor_event_subscription = cx.subscribe(&editor, |_, _, e, cx| cx.emit(e.clone()));
+
+//         cx.subscribe(&channel_buffer, Self::handle_channel_buffer_event)
+//             .detach();
+
+//         Self {
+//             editor,
+//             project,
+//             channel_store,
+//             channel_buffer,
+//             remote_id: None,
+//             _editor_event_subscription,
+//         }
+//     }
+
+//     pub fn channel(&self, cx: &AppContext) -> Option<Arc<Channel>> {
+//         self.channel_buffer.read(cx).channel(cx)
+//     }
+
+//     fn handle_channel_buffer_event(
+//         &mut self,
+//         _: ModelHandle<ChannelBuffer>,
+//         event: &ChannelBufferEvent,
+//         cx: &mut ViewContext<Self>,
+//     ) {
+//         match event {
+//             ChannelBufferEvent::Disconnected => self.editor.update(cx, |editor, cx| {
+//                 editor.set_read_only(true);
+//                 cx.notify();
+//             }),
+//             ChannelBufferEvent::ChannelChanged => {
+//                 self.editor.update(cx, |editor, cx| {
+//                     editor.set_read_only(!self.channel(cx).is_some_and(|c| c.can_edit_notes()));
+//                     cx.emit(editor::Event::TitleChanged);
+//                     cx.notify()
+//                 });
+//             }
+//             ChannelBufferEvent::BufferEdited => {
+//                 if cx.is_self_focused() || self.editor.is_focused(cx) {
+//                     self.acknowledge_buffer_version(cx);
+//                 } else {
+//                     self.channel_store.update(cx, |store, cx| {
+//                         let channel_buffer = self.channel_buffer.read(cx);
+//                         store.notes_changed(
+//                             channel_buffer.channel_id,
+//                             channel_buffer.epoch(),
+//                             &channel_buffer.buffer().read(cx).version(),
+//                             cx,
+//                         )
+//                     });
+//                 }
+//             }
+//             ChannelBufferEvent::CollaboratorsChanged => {}
+//         }
+//     }
+
+//     fn acknowledge_buffer_version(&mut self, cx: &mut ViewContext<'_, '_, ChannelView>) {
+//         self.channel_store.update(cx, |store, cx| {
+//             let channel_buffer = self.channel_buffer.read(cx);
+//             store.acknowledge_notes_version(
+//                 channel_buffer.channel_id,
+//                 channel_buffer.epoch(),
+//                 &channel_buffer.buffer().read(cx).version(),
+//                 cx,
+//             )
+//         });
+//         self.channel_buffer.update(cx, |buffer, cx| {
+//             buffer.acknowledge_buffer_version(cx);
+//         });
+//     }
+// }
+
+// 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()
+//     }
+
+//     fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
+//         if cx.is_self_focused() {
+//             self.acknowledge_buffer_version(cx);
+//             cx.focus(self.editor.as_any())
+//         }
+//     }
+// }
+
+// impl Item for ChannelView {
+//     fn act_as_type<'a>(
+//         &'a self,
+//         type_id: TypeId,
+//         self_handle: &'a ViewHandle<Self>,
+//         _: &'a AppContext,
+//     ) -> Option<&'a AnyViewHandle> {
+//         if type_id == TypeId::of::<Self>() {
+//             Some(self_handle)
+//         } else if type_id == TypeId::of::<Editor>() {
+//             Some(&self.editor)
+//         } else {
+//             None
+//         }
+//     }
+
+//     fn tab_content<V: 'static>(
+//         &self,
+//         _: Option<usize>,
+//         style: &theme::Tab,
+//         cx: &gpui::AppContext,
+//     ) -> AnyElement<V> {
+//         let label = if let Some(channel) = self.channel(cx) {
+//             match (
+//                 channel.can_edit_notes(),
+//                 self.channel_buffer.read(cx).is_connected(),
+//             ) {
+//                 (true, true) => format!("#{}", channel.name),
+//                 (false, true) => format!("#{} (read-only)", channel.name),
+//                 (_, false) => format!("#{} (disconnected)", channel.name),
+//             }
+//         } else {
+//             format!("channel notes (disconnected)")
+//         };
+//         Label::new(label, style.label.to_owned()).into_any()
+//     }
+
+//     fn clone_on_split(&self, _: WorkspaceId, cx: &mut ViewContext<Self>) -> Option<Self> {
+//         Some(Self::new(
+//             self.project.clone(),
+//             self.channel_store.clone(),
+//             self.channel_buffer.clone(),
+//             cx,
+//         ))
+//     }
+
+//     fn is_singleton(&self, _cx: &AppContext) -> bool {
+//         false
+//     }
+
+//     fn navigate(&mut self, data: Box<dyn Any>, cx: &mut ViewContext<Self>) -> bool {
+//         self.editor
+//             .update(cx, |editor, cx| editor.navigate(data, cx))
+//     }
+
+//     fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
+//         self.editor
+//             .update(cx, |editor, cx| Item::deactivated(editor, cx))
+//     }
+
+//     fn set_nav_history(&mut self, history: ItemNavHistory, cx: &mut ViewContext<Self>) {
+//         self.editor
+//             .update(cx, |editor, cx| Item::set_nav_history(editor, history, cx))
+//     }
+
+//     fn as_searchable(&self, _: &ViewHandle<Self>) -> Option<Box<dyn SearchableItemHandle>> {
+//         Some(Box::new(self.editor.clone()))
+//     }
+
+//     fn show_toolbar(&self) -> bool {
+//         true
+//     }
+
+//     fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option<Vector2F> {
+//         self.editor.read(cx).pixel_position_of_cursor(cx)
+//     }
+
+//     fn to_item_events(event: &Self::Event) -> SmallVec<[ItemEvent; 2]> {
+//         editor::Editor::to_item_events(event)
+//     }
+// }
+
+// impl FollowableItem for ChannelView {
+//     fn remote_id(&self) -> Option<workspace::ViewId> {
+//         self.remote_id
+//     }
+
+//     fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant> {
+//         let channel_buffer = self.channel_buffer.read(cx);
+//         if !channel_buffer.is_connected() {
+//             return None;
+//         }
+
+//         Some(proto::view::Variant::ChannelView(
+//             proto::view::ChannelView {
+//                 channel_id: channel_buffer.channel_id,
+//                 editor: if let Some(proto::view::Variant::Editor(proto)) =
+//                     self.editor.read(cx).to_state_proto(cx)
+//                 {
+//                     Some(proto)
+//                 } else {
+//                     None
+//                 },
+//             },
+//         ))
+//     }
+
+//     fn from_state_proto(
+//         pane: ViewHandle<workspace::Pane>,
+//         workspace: ViewHandle<workspace::Workspace>,
+//         remote_id: workspace::ViewId,
+//         state: &mut Option<proto::view::Variant>,
+//         cx: &mut AppContext,
+//     ) -> Option<gpui::Task<anyhow::Result<ViewHandle<Self>>>> {
+//         let Some(proto::view::Variant::ChannelView(_)) = state else {
+//             return None;
+//         };
+//         let Some(proto::view::Variant::ChannelView(state)) = state.take() else {
+//             unreachable!()
+//         };
+
+//         let open = ChannelView::open_in_pane(state.channel_id, pane, workspace, cx);
+
+//         Some(cx.spawn(|mut cx| async move {
+//             let this = open.await?;
+
+//             let task = this
+//                 .update(&mut cx, |this, cx| {
+//                     this.remote_id = Some(remote_id);
+
+//                     if let Some(state) = state.editor {
+//                         Some(this.editor.update(cx, |editor, cx| {
+//                             editor.apply_update_proto(
+//                                 &this.project,
+//                                 proto::update_view::Variant::Editor(proto::update_view::Editor {
+//                                     selections: state.selections,
+//                                     pending_selection: state.pending_selection,
+//                                     scroll_top_anchor: state.scroll_top_anchor,
+//                                     scroll_x: state.scroll_x,
+//                                     scroll_y: state.scroll_y,
+//                                     ..Default::default()
+//                                 }),
+//                                 cx,
+//                             )
+//                         }))
+//                     } else {
+//                         None
+//                     }
+//                 })
+//                 .ok_or_else(|| anyhow!("window was closed"))?;
+
+//             if let Some(task) = task {
+//                 task.await?;
+//             }
+
+//             Ok(this)
+//         }))
+//     }
+
+//     fn add_event_to_update_proto(
+//         &self,
+//         event: &Self::Event,
+//         update: &mut Option<proto::update_view::Variant>,
+//         cx: &AppContext,
+//     ) -> bool {
+//         self.editor
+//             .read(cx)
+//             .add_event_to_update_proto(event, update, cx)
+//     }
+
+//     fn apply_update_proto(
+//         &mut self,
+//         project: &ModelHandle<Project>,
+//         message: proto::update_view::Variant,
+//         cx: &mut ViewContext<Self>,
+//     ) -> gpui::Task<anyhow::Result<()>> {
+//         self.editor.update(cx, |editor, cx| {
+//             editor.apply_update_proto(project, message, cx)
+//         })
+//     }
+
+//     fn set_leader_peer_id(&mut self, leader_peer_id: Option<PeerId>, cx: &mut ViewContext<Self>) {
+//         self.editor.update(cx, |editor, cx| {
+//             editor.set_leader_peer_id(leader_peer_id, cx)
+//         })
+//     }
+
+//     fn should_unfollow_on_event(event: &Self::Event, cx: &AppContext) -> bool {
+//         Editor::should_unfollow_on_event(event, cx)
+//     }
+
+//     fn is_project_item(&self, _cx: &AppContext) -> bool {
+//         false
+//     }
+// }
+
+// struct ChannelBufferCollaborationHub(ModelHandle<ChannelBuffer>);
+
+// impl CollaborationHub for ChannelBufferCollaborationHub {
+//     fn collaborators<'a>(&self, cx: &'a AppContext) -> &'a HashMap<PeerId, Collaborator> {
+//         self.0.read(cx).collaborators()
+//     }
+
+//     fn user_participant_indices<'a>(
+//         &self,
+//         cx: &'a AppContext,
+//     ) -> &'a HashMap<u64, ParticipantIndex> {
+//         self.0.read(cx).user_store().read(cx).participant_indices()
+//     }
+// }

crates/collab_ui2/src/chat_panel.rs 🔗

@@ -1,983 +1,983 @@
-use crate::{
-    channel_view::ChannelView, is_channels_feature_enabled, render_avatar, ChatPanelSettings,
-};
-use anyhow::Result;
-use call::ActiveCall;
-use channel::{ChannelChat, ChannelChatEvent, ChannelMessageId, ChannelStore};
-use client::Client;
-use collections::HashMap;
-use db::kvp::KEY_VALUE_STORE;
-use editor::Editor;
-use gpui::{
-    actions,
-    elements::*,
-    platform::{CursorStyle, MouseButton},
-    serde_json,
-    views::{ItemType, Select, SelectStyle},
-    AnyViewHandle, AppContext, AsyncAppContext, Entity, ModelHandle, Subscription, Task, View,
-    ViewContext, ViewHandle, WeakViewHandle,
-};
-use language::LanguageRegistry;
-use menu::Confirm;
-use message_editor::MessageEditor;
-use project::Fs;
-use rich_text::RichText;
-use serde::{Deserialize, Serialize};
-use settings::SettingsStore;
-use std::sync::Arc;
-use theme::{IconButton, Theme};
-use time::{OffsetDateTime, UtcOffset};
-use util::{ResultExt, TryFutureExt};
-use workspace::{
-    dock::{DockPosition, Panel},
-    Workspace,
-};
-
-mod message_editor;
-
-const MESSAGE_LOADING_THRESHOLD: usize = 50;
-const CHAT_PANEL_KEY: &'static str = "ChatPanel";
-
-pub struct ChatPanel {
-    client: Arc<Client>,
-    channel_store: ModelHandle<ChannelStore>,
-    languages: Arc<LanguageRegistry>,
-    active_chat: Option<(ModelHandle<ChannelChat>, Subscription)>,
-    message_list: ListState<ChatPanel>,
-    input_editor: ViewHandle<MessageEditor>,
-    channel_select: ViewHandle<Select>,
-    local_timezone: UtcOffset,
-    fs: Arc<dyn Fs>,
-    width: Option<f32>,
-    active: bool,
-    pending_serialization: Task<Option<()>>,
-    subscriptions: Vec<gpui::Subscription>,
-    workspace: WeakViewHandle<Workspace>,
-    is_scrolled_to_bottom: bool,
-    has_focus: bool,
-    markdown_data: HashMap<ChannelMessageId, RichText>,
-}
-
-#[derive(Serialize, Deserialize)]
-struct SerializedChatPanel {
-    width: Option<f32>,
-}
-
-#[derive(Debug)]
-pub enum Event {
-    DockPositionChanged,
-    Focus,
-    Dismissed,
-}
-
-actions!(
-    chat_panel,
-    [LoadMoreMessages, ToggleFocus, OpenChannelNotes, JoinCall]
-);
-
-pub fn init(cx: &mut AppContext) {
-    cx.add_action(ChatPanel::send);
-    cx.add_action(ChatPanel::load_more_messages);
-    cx.add_action(ChatPanel::open_notes);
-    cx.add_action(ChatPanel::join_call);
-}
-
-impl ChatPanel {
-    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 = ChannelStore::global(cx);
-        let languages = workspace.app_state().languages.clone();
-
-        let input_editor = cx.add_view(|cx| {
-            MessageEditor::new(
-                languages.clone(),
-                channel_store.clone(),
-                cx.add_view(|cx| {
-                    Editor::auto_height(
-                        4,
-                        Some(Arc::new(|theme| theme.chat_panel.input_editor.clone())),
-                        cx,
-                    )
-                }),
-                cx,
-            )
-        });
-
-        let workspace_handle = workspace.weak_handle();
-
-        let channel_select = cx.add_view(|cx| {
-            let channel_store = channel_store.clone();
-            let workspace = workspace_handle.clone();
-            Select::new(0, cx, {
-                move |ix, item_type, is_hovered, cx| {
-                    Self::render_channel_name(
-                        &channel_store,
-                        ix,
-                        item_type,
-                        is_hovered,
-                        workspace,
-                        cx,
-                    )
-                }
-            })
-            .with_style(move |cx| {
-                let style = &theme::current(cx).chat_panel.channel_select;
-                SelectStyle {
-                    header: Default::default(),
-                    menu: style.menu,
-                }
-            })
-        });
-
-        let mut message_list =
-            ListState::<Self>::new(0, Orientation::Bottom, 10., move |this, ix, cx| {
-                this.render_message(ix, cx)
-            });
-        message_list.set_scroll_handler(|visible_range, count, this, cx| {
-            if visible_range.start < MESSAGE_LOADING_THRESHOLD {
-                this.load_more_messages(&LoadMoreMessages, cx);
-            }
-            this.is_scrolled_to_bottom = visible_range.end == count;
-        });
-
-        cx.add_view(|cx| {
-            let mut this = Self {
-                fs,
-                client,
-                channel_store,
-                languages,
-                active_chat: Default::default(),
-                pending_serialization: Task::ready(None),
-                message_list,
-                input_editor,
-                channel_select,
-                local_timezone: cx.platform().local_timezone(),
-                has_focus: false,
-                subscriptions: Vec::new(),
-                workspace: workspace_handle,
-                is_scrolled_to_bottom: true,
-                active: false,
-                width: None,
-                markdown_data: Default::default(),
-            };
-
-            let mut old_dock_position = this.position(cx);
-            this.subscriptions
-                .push(
-                    cx.observe_global::<SettingsStore, _>(move |this: &mut Self, cx| {
-                        let new_dock_position = this.position(cx);
-                        if new_dock_position != old_dock_position {
-                            old_dock_position = new_dock_position;
-                            cx.emit(Event::DockPositionChanged);
-                        }
-                        cx.notify();
-                    }),
-                );
-
-            this.update_channel_count(cx);
-            cx.observe(&this.channel_store, |this, _, cx| {
-                this.update_channel_count(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(selected_ix)
-                    .map(|e| e.id);
-                if let Some(selected_channel_id) = selected_channel_id {
-                    this.select_channel(selected_channel_id, None, cx)
-                        .detach_and_log_err(cx);
-                }
-            })
-            .detach();
-
-            this
-        })
-    }
-
-    pub fn is_scrolled_to_bottom(&self) -> bool {
-        self.is_scrolled_to_bottom
-    }
-
-    pub fn active_chat(&self) -> Option<ModelHandle<ChannelChat>> {
-        self.active_chat.as_ref().map(|(chat, _)| chat.clone())
-    }
-
-    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
-            })
-        })
-    }
-
-    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 update_channel_count(&mut self, cx: &mut ViewContext<Self>) {
-        let channel_count = self.channel_store.read(cx).channel_count();
-        self.channel_select.update(cx, |select, cx| {
-            select.set_item_count(channel_count, cx);
-        });
-    }
-
-    fn set_active_chat(&mut self, chat: ModelHandle<ChannelChat>, cx: &mut ViewContext<Self>) {
-        if self.active_chat.as_ref().map(|e| &e.0) != Some(&chat) {
-            let channel_id = chat.read(cx).channel_id;
-            {
-                self.markdown_data.clear();
-                let chat = chat.read(cx);
-                self.message_list.reset(chat.message_count());
-
-                let channel_name = chat.channel(cx).map(|channel| channel.name.clone());
-                self.input_editor.update(cx, |editor, cx| {
-                    editor.set_channel(channel_id, channel_name, cx);
-                });
-            };
-            let subscription = cx.subscribe(&chat, Self::channel_did_change);
-            self.active_chat = Some((chat, subscription));
-            self.acknowledge_last_message(cx);
-            self.channel_select.update(cx, |select, cx| {
-                if let Some(ix) = self.channel_store.read(cx).index_of_channel(channel_id) {
-                    select.set_selected_index(ix, cx);
-                }
-            });
-            cx.notify();
-        }
-    }
-
-    fn channel_did_change(
-        &mut self,
-        _: ModelHandle<ChannelChat>,
-        event: &ChannelChatEvent,
-        cx: &mut ViewContext<Self>,
-    ) {
-        match event {
-            ChannelChatEvent::MessagesUpdated {
-                old_range,
-                new_count,
-            } => {
-                self.message_list.splice(old_range.clone(), *new_count);
-                if self.active {
-                    self.acknowledge_last_message(cx);
-                }
-            }
-            ChannelChatEvent::NewMessage {
-                channel_id,
-                message_id,
-            } => {
-                if !self.active {
-                    self.channel_store.update(cx, |store, cx| {
-                        store.new_message(*channel_id, *message_id, cx)
-                    })
-                }
-            }
-        }
-        cx.notify();
-    }
-
-    fn acknowledge_last_message(&mut self, cx: &mut ViewContext<'_, '_, ChatPanel>) {
-        if self.active && self.is_scrolled_to_bottom {
-            if let Some((chat, _)) = &self.active_chat {
-                chat.update(cx, |chat, cx| {
-                    chat.acknowledge_last_message(cx);
-                });
-            }
-        }
-    }
-
-    fn render_channel(&self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
-        let theme = theme::current(cx);
-        Flex::column()
-            .with_child(
-                ChildView::new(&self.channel_select, cx)
-                    .contained()
-                    .with_style(theme.chat_panel.channel_select.container),
-            )
-            .with_child(self.render_active_channel_messages(&theme))
-            .with_child(self.render_input_box(&theme, cx))
-            .into_any()
-    }
-
-    fn render_active_channel_messages(&self, theme: &Arc<Theme>) -> AnyElement<Self> {
-        let messages = if self.active_chat.is_some() {
-            List::new(self.message_list.clone())
-                .contained()
-                .with_style(theme.chat_panel.list)
-                .into_any()
-        } else {
-            Empty::new().into_any()
-        };
-
-        messages.flex(1., true).into_any()
-    }
-
-    fn render_message(&mut self, ix: usize, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
-        let (message, is_continuation, is_last, is_admin) = self
-            .active_chat
-            .as_ref()
-            .unwrap()
-            .0
-            .update(cx, |active_chat, cx| {
-                let is_admin = self
-                    .channel_store
-                    .read(cx)
-                    .is_channel_admin(active_chat.channel_id);
-
-                let last_message = active_chat.message(ix.saturating_sub(1));
-                let this_message = active_chat.message(ix).clone();
-                let is_continuation = last_message.id != this_message.id
-                    && this_message.sender.id == last_message.sender.id;
-
-                if let ChannelMessageId::Saved(id) = this_message.id {
-                    if this_message
-                        .mentions
-                        .iter()
-                        .any(|(_, user_id)| Some(*user_id) == self.client.user_id())
-                    {
-                        active_chat.acknowledge_message(id);
-                    }
-                }
-
-                (
-                    this_message,
-                    is_continuation,
-                    active_chat.message_count() == ix + 1,
-                    is_admin,
-                )
-            });
-
-        let is_pending = message.is_pending();
-        let theme = theme::current(cx);
-        let text = self.markdown_data.entry(message.id).or_insert_with(|| {
-            Self::render_markdown_with_mentions(&self.languages, self.client.id(), &message)
-        });
-
-        let now = OffsetDateTime::now_utc();
-
-        let style = if is_pending {
-            &theme.chat_panel.pending_message
-        } else if is_continuation {
-            &theme.chat_panel.continuation_message
-        } else {
-            &theme.chat_panel.message
-        };
-
-        let belongs_to_user = Some(message.sender.id) == self.client.user_id();
-        let message_id_to_remove = if let (ChannelMessageId::Saved(id), true) =
-            (message.id, belongs_to_user || is_admin)
-        {
-            Some(id)
-        } else {
-            None
-        };
-
-        enum MessageBackgroundHighlight {}
-        MouseEventHandler::new::<MessageBackgroundHighlight, _>(ix, cx, |state, cx| {
-            let container = style.style_for(state);
-            if is_continuation {
-                Flex::row()
-                    .with_child(
-                        text.element(
-                            theme.editor.syntax.clone(),
-                            theme.chat_panel.rich_text.clone(),
-                            cx,
-                        )
-                        .flex(1., true),
-                    )
-                    .with_child(render_remove(message_id_to_remove, cx, &theme))
-                    .contained()
-                    .with_style(*container)
-                    .with_margin_bottom(if is_last {
-                        theme.chat_panel.last_message_bottom_spacing
-                    } else {
-                        0.
-                    })
-                    .into_any()
-            } else {
-                Flex::column()
-                    .with_child(
-                        Flex::row()
-                            .with_child(
-                                Flex::row()
-                                    .with_child(render_avatar(
-                                        message.sender.avatar.clone(),
-                                        &theme.chat_panel.avatar,
-                                        theme.chat_panel.avatar_container,
-                                    ))
-                                    .with_child(
-                                        Label::new(
-                                            message.sender.github_login.clone(),
-                                            theme.chat_panel.message_sender.text.clone(),
-                                        )
-                                        .contained()
-                                        .with_style(theme.chat_panel.message_sender.container),
-                                    )
-                                    .with_child(
-                                        Label::new(
-                                            format_timestamp(
-                                                message.timestamp,
-                                                now,
-                                                self.local_timezone,
-                                            ),
-                                            theme.chat_panel.message_timestamp.text.clone(),
-                                        )
-                                        .contained()
-                                        .with_style(theme.chat_panel.message_timestamp.container),
-                                    )
-                                    .align_children_center()
-                                    .flex(1., true),
-                            )
-                            .with_child(render_remove(message_id_to_remove, cx, &theme))
-                            .align_children_center(),
-                    )
-                    .with_child(
-                        Flex::row()
-                            .with_child(
-                                text.element(
-                                    theme.editor.syntax.clone(),
-                                    theme.chat_panel.rich_text.clone(),
-                                    cx,
-                                )
-                                .flex(1., true),
-                            )
-                            // Add a spacer to make everything line up
-                            .with_child(render_remove(None, cx, &theme)),
-                    )
-                    .contained()
-                    .with_style(*container)
-                    .with_margin_bottom(if is_last {
-                        theme.chat_panel.last_message_bottom_spacing
-                    } else {
-                        0.
-                    })
-                    .into_any()
-            }
-        })
-        .into_any()
-    }
-
-    fn render_markdown_with_mentions(
-        language_registry: &Arc<LanguageRegistry>,
-        current_user_id: u64,
-        message: &channel::ChannelMessage,
-    ) -> RichText {
-        let mentions = message
-            .mentions
-            .iter()
-            .map(|(range, user_id)| rich_text::Mention {
-                range: range.clone(),
-                is_self_mention: *user_id == current_user_id,
-            })
-            .collect::<Vec<_>>();
-
-        rich_text::render_markdown(message.body.clone(), &mentions, language_registry, None)
-    }
-
-    fn render_input_box(&self, theme: &Arc<Theme>, cx: &AppContext) -> AnyElement<Self> {
-        ChildView::new(&self.input_editor, cx)
-            .contained()
-            .with_style(theme.chat_panel.input_editor.container)
-            .into_any()
-    }
-
-    fn render_channel_name(
-        channel_store: &ModelHandle<ChannelStore>,
-        ix: usize,
-        item_type: ItemType,
-        is_hovered: bool,
-        workspace: WeakViewHandle<Workspace>,
-        cx: &mut ViewContext<Select>,
-    ) -> AnyElement<Select> {
-        let theme = theme::current(cx);
-        let tooltip_style = &theme.tooltip;
-        let theme = &theme.chat_panel;
-        let style = match (&item_type, is_hovered) {
-            (ItemType::Header, _) => &theme.channel_select.header,
-            (ItemType::Selected, _) => &theme.channel_select.active_item,
-            (ItemType::Unselected, false) => &theme.channel_select.item,
-            (ItemType::Unselected, true) => &theme.channel_select.hovered_item,
-        };
-
-        let channel = &channel_store.read(cx).channel_at(ix).unwrap();
-        let channel_id = channel.id;
-
-        let mut row = Flex::row()
-            .with_child(
-                Label::new("#".to_string(), style.hash.text.clone())
-                    .contained()
-                    .with_style(style.hash.container),
-            )
-            .with_child(Label::new(channel.name.clone(), style.name.clone()));
-
-        if matches!(item_type, ItemType::Header) {
-            row.add_children([
-                MouseEventHandler::new::<OpenChannelNotes, _>(0, cx, |mouse_state, _| {
-                    render_icon_button(theme.icon_button.style_for(mouse_state), "icons/file.svg")
-                })
-                .on_click(MouseButton::Left, move |_, _, cx| {
-                    if let Some(workspace) = workspace.upgrade(cx) {
-                        ChannelView::open(channel_id, workspace, cx).detach();
-                    }
-                })
-                .with_tooltip::<OpenChannelNotes>(
-                    channel_id as usize,
-                    "Open Notes",
-                    Some(Box::new(OpenChannelNotes)),
-                    tooltip_style.clone(),
-                    cx,
-                )
-                .flex_float(),
-                MouseEventHandler::new::<ActiveCall, _>(0, cx, |mouse_state, _| {
-                    render_icon_button(
-                        theme.icon_button.style_for(mouse_state),
-                        "icons/speaker-loud.svg",
-                    )
-                })
-                .on_click(MouseButton::Left, move |_, _, cx| {
-                    ActiveCall::global(cx)
-                        .update(cx, |call, cx| call.join_channel(channel_id, cx))
-                        .detach_and_log_err(cx);
-                })
-                .with_tooltip::<ActiveCall>(
-                    channel_id as usize,
-                    "Join Call",
-                    Some(Box::new(JoinCall)),
-                    tooltip_style.clone(),
-                    cx,
-                )
-                .flex_float(),
-            ]);
-        }
-
-        row.align_children_center()
-            .contained()
-            .with_style(style.container)
-            .into_any()
-    }
-
-    fn render_sign_in_prompt(
-        &self,
-        theme: &Arc<Theme>,
-        cx: &mut ViewContext<Self>,
-    ) -> AnyElement<Self> {
-        enum SignInPromptLabel {}
-
-        MouseEventHandler::new::<SignInPromptLabel, _>(0, cx, |mouse_state, _| {
-            Label::new(
-                "Sign in to use chat".to_string(),
-                theme
-                    .chat_panel
-                    .sign_in_prompt
-                    .style_for(mouse_state)
-                    .clone(),
-            )
-        })
-        .with_cursor_style(CursorStyle::PointingHand)
-        .on_click(MouseButton::Left, move |_, this, cx| {
-            let client = this.client.clone();
-            cx.spawn(|this, mut cx| async move {
-                if client
-                    .authenticate_and_connect(true, &cx)
-                    .log_err()
-                    .await
-                    .is_some()
-                {
-                    this.update(&mut cx, |this, cx| {
-                        if cx.handle().is_focused(cx) {
-                            cx.focus(&this.input_editor);
-                        }
-                    })
-                    .ok();
-                }
-            })
-            .detach();
-        })
-        .aligned()
-        .into_any()
-    }
-
-    fn send(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
-        if let Some((chat, _)) = self.active_chat.as_ref() {
-            let message = self
-                .input_editor
-                .update(cx, |editor, cx| editor.take_message(cx));
-
-            if let Some(task) = chat
-                .update(cx, |chat, cx| chat.send_message(message, cx))
-                .log_err()
-            {
-                task.detach();
-            }
-        }
-    }
-
-    fn remove_message(&mut self, id: u64, cx: &mut ViewContext<Self>) {
-        if let Some((chat, _)) = self.active_chat.as_ref() {
-            chat.update(cx, |chat, cx| chat.remove_message(id, cx).detach())
-        }
-    }
-
-    fn load_more_messages(&mut self, _: &LoadMoreMessages, cx: &mut ViewContext<Self>) {
-        if let Some((chat, _)) = self.active_chat.as_ref() {
-            chat.update(cx, |channel, cx| {
-                if let Some(task) = channel.load_more_messages(cx) {
-                    task.detach();
-                }
-            })
-        }
-    }
-
-    pub fn select_channel(
-        &mut self,
-        selected_channel_id: u64,
-        scroll_to_message_id: Option<u64>,
-        cx: &mut ViewContext<ChatPanel>,
-    ) -> Task<Result<()>> {
-        let open_chat = self
-            .active_chat
-            .as_ref()
-            .and_then(|(chat, _)| {
-                (chat.read(cx).channel_id == selected_channel_id)
-                    .then(|| Task::ready(anyhow::Ok(chat.clone())))
-            })
-            .unwrap_or_else(|| {
-                self.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_chat(chat.clone(), cx);
-            })?;
-
-            if let Some(message_id) = scroll_to_message_id {
-                if let Some(item_ix) =
-                    ChannelChat::load_history_since_message(chat.clone(), message_id, cx.clone())
-                        .await
-                {
-                    this.update(&mut cx, |this, cx| {
-                        if this.active_chat.as_ref().map_or(false, |(c, _)| *c == chat) {
-                            this.message_list.scroll_to(ListOffset {
-                                item_ix,
-                                offset_in_item: 0.,
-                            });
-                            cx.notify();
-                        }
-                    })?;
-                }
-            }
-
-            Ok(())
-        })
-    }
-
-    fn open_notes(&mut self, _: &OpenChannelNotes, cx: &mut ViewContext<Self>) {
-        if let Some((chat, _)) = &self.active_chat {
-            let channel_id = chat.read(cx).channel_id;
-            if let Some(workspace) = self.workspace.upgrade(cx) {
-                ChannelView::open(channel_id, workspace, cx).detach();
-            }
-        }
-    }
-
-    fn join_call(&mut self, _: &JoinCall, cx: &mut ViewContext<Self>) {
-        if let Some((chat, _)) = &self.active_chat {
-            let channel_id = chat.read(cx).channel_id;
-            ActiveCall::global(cx)
-                .update(cx, |call, cx| call.join_channel(channel_id, cx))
-                .detach_and_log_err(cx);
-        }
-    }
-}
-
-fn render_remove(
-    message_id_to_remove: Option<u64>,
-    cx: &mut ViewContext<'_, '_, ChatPanel>,
-    theme: &Arc<Theme>,
-) -> AnyElement<ChatPanel> {
-    enum DeleteMessage {}
-
-    message_id_to_remove
-        .map(|id| {
-            MouseEventHandler::new::<DeleteMessage, _>(id as usize, cx, |mouse_state, _| {
-                let button_style = theme.chat_panel.icon_button.style_for(mouse_state);
-                render_icon_button(button_style, "icons/x.svg")
-                    .aligned()
-                    .into_any()
-            })
-            .with_padding(Padding::uniform(2.))
-            .with_cursor_style(CursorStyle::PointingHand)
-            .on_click(MouseButton::Left, move |_, this, cx| {
-                this.remove_message(id, cx);
-            })
-            .flex_float()
-            .into_any()
-        })
-        .unwrap_or_else(|| {
-            let style = theme.chat_panel.icon_button.default;
-
-            Empty::new()
-                .constrained()
-                .with_width(style.icon_width)
-                .aligned()
-                .constrained()
-                .with_width(style.button_width)
-                .with_height(style.button_width)
-                .contained()
-                .with_uniform_padding(2.)
-                .flex_float()
-                .into_any()
-        })
-}
-
-impl Entity for ChatPanel {
-    type Event = Event;
-}
-
-impl View for ChatPanel {
-    fn ui_name() -> &'static str {
-        "ChatPanel"
-    }
-
-    fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
-        let theme = theme::current(cx);
-        let element = if self.client.user_id().is_some() {
-            self.render_channel(cx)
-        } else {
-            self.render_sign_in_prompt(&theme, cx)
-        };
-        element
-            .contained()
-            .with_style(theme.chat_panel.container)
-            .constrained()
-            .with_min_width(150.)
-            .into_any()
-    }
-
-    fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
-        self.has_focus = true;
-        if matches!(
-            *self.client.status().borrow(),
-            client::Status::Connected { .. }
-        ) {
-            let editor = self.input_editor.read(cx).editor.clone();
-            cx.focus(&editor);
-        }
-    }
-
-    fn focus_out(&mut self, _: AnyViewHandle, _: &mut ViewContext<Self>) {
-        self.has_focus = false;
-    }
-}
-
-impl Panel for ChatPanel {
-    fn position(&self, cx: &gpui::WindowContext) -> DockPosition {
-        settings::get::<ChatPanelSettings>(cx).dock
-    }
-
-    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::<ChatPanelSettings>(self.fs.clone(), cx, move |settings| {
-            settings.dock = Some(position)
-        });
-    }
-
-    fn size(&self, cx: &gpui::WindowContext) -> f32 {
-        self.width
-            .unwrap_or_else(|| settings::get::<ChatPanelSettings>(cx).default_width)
-    }
-
-    fn set_size(&mut self, size: Option<f32>, cx: &mut ViewContext<Self>) {
-        self.width = size;
-        self.serialize(cx);
-        cx.notify();
-    }
-
-    fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) {
-        self.active = active;
-        if active {
-            self.acknowledge_last_message(cx);
-            if !is_channels_feature_enabled(cx) {
-                cx.emit(Event::Dismissed);
-            }
-        }
-    }
-
-    fn icon_path(&self, cx: &gpui::WindowContext) -> Option<&'static str> {
-        (settings::get::<ChatPanelSettings>(cx).button && is_channels_feature_enabled(cx))
-            .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 should_close_on_event(event: &Self::Event) -> bool {
-        matches!(event, Event::Dismissed)
-    }
-
-    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,
-    local_timezone: UtcOffset,
-) -> String {
-    timestamp = timestamp.to_offset(local_timezone);
-    now = now.to_offset(local_timezone);
-
-    let today = now.date();
-    let date = timestamp.date();
-    let mut hour = timestamp.hour();
-    let mut part = "am";
-    if hour > 12 {
-        hour -= 12;
-        part = "pm";
-    }
-    if date == today {
-        format!("{:02}:{:02}{}", hour, timestamp.minute(), part)
-    } else if date.next_day() == Some(today) {
-        format!("yesterday at {:02}:{:02}{}", hour, timestamp.minute(), part)
-    } else {
-        format!("{:02}/{}/{}", date.month() as u32, date.day(), date.year())
-    }
-}
-
-fn render_icon_button<V: View>(style: &IconButton, svg_path: &'static str) -> impl Element<V> {
-    Svg::new(svg_path)
-        .with_color(style.color)
-        .constrained()
-        .with_width(style.icon_width)
-        .aligned()
-        .constrained()
-        .with_width(style.button_width)
-        .with_height(style.button_width)
-        .contained()
-        .with_style(style.container)
-}
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-    use gpui::fonts::HighlightStyle;
-    use pretty_assertions::assert_eq;
-    use rich_text::{BackgroundKind, Highlight, RenderedRegion};
-    use util::test::marked_text_ranges;
-
-    #[gpui::test]
-    fn test_render_markdown_with_mentions() {
-        let language_registry = Arc::new(LanguageRegistry::test());
-        let (body, ranges) = marked_text_ranges("*hi*, «@abc», let's **call** «@fgh»", false);
-        let message = channel::ChannelMessage {
-            id: ChannelMessageId::Saved(0),
-            body,
-            timestamp: OffsetDateTime::now_utc(),
-            sender: Arc::new(client::User {
-                github_login: "fgh".into(),
-                avatar: None,
-                id: 103,
-            }),
-            nonce: 5,
-            mentions: vec![(ranges[0].clone(), 101), (ranges[1].clone(), 102)],
-        };
-
-        let message = ChatPanel::render_markdown_with_mentions(&language_registry, 102, &message);
-
-        // Note that the "'" was replaced with ’ due to smart punctuation.
-        let (body, ranges) = marked_text_ranges("«hi», «@abc», let’s «call» «@fgh»", false);
-        assert_eq!(message.text, body);
-        assert_eq!(
-            message.highlights,
-            vec![
-                (
-                    ranges[0].clone(),
-                    HighlightStyle {
-                        italic: Some(true),
-                        ..Default::default()
-                    }
-                    .into()
-                ),
-                (ranges[1].clone(), Highlight::Mention),
-                (
-                    ranges[2].clone(),
-                    HighlightStyle {
-                        weight: Some(gpui::fonts::Weight::BOLD),
-                        ..Default::default()
-                    }
-                    .into()
-                ),
-                (ranges[3].clone(), Highlight::SelfMention)
-            ]
-        );
-        assert_eq!(
-            message.regions,
-            vec![
-                RenderedRegion {
-                    background_kind: Some(BackgroundKind::Mention),
-                    link_url: None
-                },
-                RenderedRegion {
-                    background_kind: Some(BackgroundKind::SelfMention),
-                    link_url: None
-                },
-            ]
-        );
-    }
-}
+// use crate::{
+//     channel_view::ChannelView, is_channels_feature_enabled, render_avatar, ChatPanelSettings,
+// };
+// use anyhow::Result;
+// use call::ActiveCall;
+// use channel::{ChannelChat, ChannelChatEvent, ChannelMessageId, ChannelStore};
+// use client::Client;
+// use collections::HashMap;
+// use db::kvp::KEY_VALUE_STORE;
+// use editor::Editor;
+// use gpui::{
+//     actions,
+//     elements::*,
+//     platform::{CursorStyle, MouseButton},
+//     serde_json,
+//     views::{ItemType, Select, SelectStyle},
+//     AnyViewHandle, AppContext, AsyncAppContext, Entity, ModelHandle, Subscription, Task, View,
+//     ViewContext, ViewHandle, WeakViewHandle,
+// };
+// use language::LanguageRegistry;
+// use menu::Confirm;
+// use message_editor::MessageEditor;
+// use project::Fs;
+// use rich_text::RichText;
+// use serde::{Deserialize, Serialize};
+// use settings::SettingsStore;
+// use std::sync::Arc;
+// use theme::{IconButton, Theme};
+// use time::{OffsetDateTime, UtcOffset};
+// use util::{ResultExt, TryFutureExt};
+// use workspace::{
+//     dock::{DockPosition, Panel},
+//     Workspace,
+// };
+
+// mod message_editor;
+
+// const MESSAGE_LOADING_THRESHOLD: usize = 50;
+// const CHAT_PANEL_KEY: &'static str = "ChatPanel";
+
+// pub struct ChatPanel {
+//     client: Arc<Client>,
+//     channel_store: ModelHandle<ChannelStore>,
+//     languages: Arc<LanguageRegistry>,
+//     active_chat: Option<(ModelHandle<ChannelChat>, Subscription)>,
+//     message_list: ListState<ChatPanel>,
+//     input_editor: ViewHandle<MessageEditor>,
+//     channel_select: ViewHandle<Select>,
+//     local_timezone: UtcOffset,
+//     fs: Arc<dyn Fs>,
+//     width: Option<f32>,
+//     active: bool,
+//     pending_serialization: Task<Option<()>>,
+//     subscriptions: Vec<gpui::Subscription>,
+//     workspace: WeakViewHandle<Workspace>,
+//     is_scrolled_to_bottom: bool,
+//     has_focus: bool,
+//     markdown_data: HashMap<ChannelMessageId, RichText>,
+// }
+
+// #[derive(Serialize, Deserialize)]
+// struct SerializedChatPanel {
+//     width: Option<f32>,
+// }
+
+// #[derive(Debug)]
+// pub enum Event {
+//     DockPositionChanged,
+//     Focus,
+//     Dismissed,
+// }
+
+// actions!(
+//     chat_panel,
+//     [LoadMoreMessages, ToggleFocus, OpenChannelNotes, JoinCall]
+// );
+
+// pub fn init(cx: &mut AppContext) {
+//     cx.add_action(ChatPanel::send);
+//     cx.add_action(ChatPanel::load_more_messages);
+//     cx.add_action(ChatPanel::open_notes);
+//     cx.add_action(ChatPanel::join_call);
+// }
+
+// impl ChatPanel {
+//     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 = ChannelStore::global(cx);
+//         let languages = workspace.app_state().languages.clone();
+
+//         let input_editor = cx.add_view(|cx| {
+//             MessageEditor::new(
+//                 languages.clone(),
+//                 channel_store.clone(),
+//                 cx.add_view(|cx| {
+//                     Editor::auto_height(
+//                         4,
+//                         Some(Arc::new(|theme| theme.chat_panel.input_editor.clone())),
+//                         cx,
+//                     )
+//                 }),
+//                 cx,
+//             )
+//         });
+
+//         let workspace_handle = workspace.weak_handle();
+
+//         let channel_select = cx.add_view(|cx| {
+//             let channel_store = channel_store.clone();
+//             let workspace = workspace_handle.clone();
+//             Select::new(0, cx, {
+//                 move |ix, item_type, is_hovered, cx| {
+//                     Self::render_channel_name(
+//                         &channel_store,
+//                         ix,
+//                         item_type,
+//                         is_hovered,
+//                         workspace,
+//                         cx,
+//                     )
+//                 }
+//             })
+//             .with_style(move |cx| {
+//                 let style = &theme::current(cx).chat_panel.channel_select;
+//                 SelectStyle {
+//                     header: Default::default(),
+//                     menu: style.menu,
+//                 }
+//             })
+//         });
+
+//         let mut message_list =
+//             ListState::<Self>::new(0, Orientation::Bottom, 10., move |this, ix, cx| {
+//                 this.render_message(ix, cx)
+//             });
+//         message_list.set_scroll_handler(|visible_range, count, this, cx| {
+//             if visible_range.start < MESSAGE_LOADING_THRESHOLD {
+//                 this.load_more_messages(&LoadMoreMessages, cx);
+//             }
+//             this.is_scrolled_to_bottom = visible_range.end == count;
+//         });
+
+//         cx.add_view(|cx| {
+//             let mut this = Self {
+//                 fs,
+//                 client,
+//                 channel_store,
+//                 languages,
+//                 active_chat: Default::default(),
+//                 pending_serialization: Task::ready(None),
+//                 message_list,
+//                 input_editor,
+//                 channel_select,
+//                 local_timezone: cx.platform().local_timezone(),
+//                 has_focus: false,
+//                 subscriptions: Vec::new(),
+//                 workspace: workspace_handle,
+//                 is_scrolled_to_bottom: true,
+//                 active: false,
+//                 width: None,
+//                 markdown_data: Default::default(),
+//             };
+
+//             let mut old_dock_position = this.position(cx);
+//             this.subscriptions
+//                 .push(
+//                     cx.observe_global::<SettingsStore, _>(move |this: &mut Self, cx| {
+//                         let new_dock_position = this.position(cx);
+//                         if new_dock_position != old_dock_position {
+//                             old_dock_position = new_dock_position;
+//                             cx.emit(Event::DockPositionChanged);
+//                         }
+//                         cx.notify();
+//                     }),
+//                 );
+
+//             this.update_channel_count(cx);
+//             cx.observe(&this.channel_store, |this, _, cx| {
+//                 this.update_channel_count(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(selected_ix)
+//                     .map(|e| e.id);
+//                 if let Some(selected_channel_id) = selected_channel_id {
+//                     this.select_channel(selected_channel_id, None, cx)
+//                         .detach_and_log_err(cx);
+//                 }
+//             })
+//             .detach();
+
+//             this
+//         })
+//     }
+
+//     pub fn is_scrolled_to_bottom(&self) -> bool {
+//         self.is_scrolled_to_bottom
+//     }
+
+//     pub fn active_chat(&self) -> Option<ModelHandle<ChannelChat>> {
+//         self.active_chat.as_ref().map(|(chat, _)| chat.clone())
+//     }
+
+//     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
+//             })
+//         })
+//     }
+
+//     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 update_channel_count(&mut self, cx: &mut ViewContext<Self>) {
+//         let channel_count = self.channel_store.read(cx).channel_count();
+//         self.channel_select.update(cx, |select, cx| {
+//             select.set_item_count(channel_count, cx);
+//         });
+//     }
+
+//     fn set_active_chat(&mut self, chat: ModelHandle<ChannelChat>, cx: &mut ViewContext<Self>) {
+//         if self.active_chat.as_ref().map(|e| &e.0) != Some(&chat) {
+//             let channel_id = chat.read(cx).channel_id;
+//             {
+//                 self.markdown_data.clear();
+//                 let chat = chat.read(cx);
+//                 self.message_list.reset(chat.message_count());
+
+//                 let channel_name = chat.channel(cx).map(|channel| channel.name.clone());
+//                 self.input_editor.update(cx, |editor, cx| {
+//                     editor.set_channel(channel_id, channel_name, cx);
+//                 });
+//             };
+//             let subscription = cx.subscribe(&chat, Self::channel_did_change);
+//             self.active_chat = Some((chat, subscription));
+//             self.acknowledge_last_message(cx);
+//             self.channel_select.update(cx, |select, cx| {
+//                 if let Some(ix) = self.channel_store.read(cx).index_of_channel(channel_id) {
+//                     select.set_selected_index(ix, cx);
+//                 }
+//             });
+//             cx.notify();
+//         }
+//     }
+
+//     fn channel_did_change(
+//         &mut self,
+//         _: ModelHandle<ChannelChat>,
+//         event: &ChannelChatEvent,
+//         cx: &mut ViewContext<Self>,
+//     ) {
+//         match event {
+//             ChannelChatEvent::MessagesUpdated {
+//                 old_range,
+//                 new_count,
+//             } => {
+//                 self.message_list.splice(old_range.clone(), *new_count);
+//                 if self.active {
+//                     self.acknowledge_last_message(cx);
+//                 }
+//             }
+//             ChannelChatEvent::NewMessage {
+//                 channel_id,
+//                 message_id,
+//             } => {
+//                 if !self.active {
+//                     self.channel_store.update(cx, |store, cx| {
+//                         store.new_message(*channel_id, *message_id, cx)
+//                     })
+//                 }
+//             }
+//         }
+//         cx.notify();
+//     }
+
+//     fn acknowledge_last_message(&mut self, cx: &mut ViewContext<'_, '_, ChatPanel>) {
+//         if self.active && self.is_scrolled_to_bottom {
+//             if let Some((chat, _)) = &self.active_chat {
+//                 chat.update(cx, |chat, cx| {
+//                     chat.acknowledge_last_message(cx);
+//                 });
+//             }
+//         }
+//     }
+
+//     fn render_channel(&self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
+//         let theme = theme::current(cx);
+//         Flex::column()
+//             .with_child(
+//                 ChildView::new(&self.channel_select, cx)
+//                     .contained()
+//                     .with_style(theme.chat_panel.channel_select.container),
+//             )
+//             .with_child(self.render_active_channel_messages(&theme))
+//             .with_child(self.render_input_box(&theme, cx))
+//             .into_any()
+//     }
+
+//     fn render_active_channel_messages(&self, theme: &Arc<Theme>) -> AnyElement<Self> {
+//         let messages = if self.active_chat.is_some() {
+//             List::new(self.message_list.clone())
+//                 .contained()
+//                 .with_style(theme.chat_panel.list)
+//                 .into_any()
+//         } else {
+//             Empty::new().into_any()
+//         };
+
+//         messages.flex(1., true).into_any()
+//     }
+
+//     fn render_message(&mut self, ix: usize, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
+//         let (message, is_continuation, is_last, is_admin) = self
+//             .active_chat
+//             .as_ref()
+//             .unwrap()
+//             .0
+//             .update(cx, |active_chat, cx| {
+//                 let is_admin = self
+//                     .channel_store
+//                     .read(cx)
+//                     .is_channel_admin(active_chat.channel_id);
+
+//                 let last_message = active_chat.message(ix.saturating_sub(1));
+//                 let this_message = active_chat.message(ix).clone();
+//                 let is_continuation = last_message.id != this_message.id
+//                     && this_message.sender.id == last_message.sender.id;
+
+//                 if let ChannelMessageId::Saved(id) = this_message.id {
+//                     if this_message
+//                         .mentions
+//                         .iter()
+//                         .any(|(_, user_id)| Some(*user_id) == self.client.user_id())
+//                     {
+//                         active_chat.acknowledge_message(id);
+//                     }
+//                 }
+
+//                 (
+//                     this_message,
+//                     is_continuation,
+//                     active_chat.message_count() == ix + 1,
+//                     is_admin,
+//                 )
+//             });
+
+//         let is_pending = message.is_pending();
+//         let theme = theme::current(cx);
+//         let text = self.markdown_data.entry(message.id).or_insert_with(|| {
+//             Self::render_markdown_with_mentions(&self.languages, self.client.id(), &message)
+//         });
+
+//         let now = OffsetDateTime::now_utc();
+
+//         let style = if is_pending {
+//             &theme.chat_panel.pending_message
+//         } else if is_continuation {
+//             &theme.chat_panel.continuation_message
+//         } else {
+//             &theme.chat_panel.message
+//         };
+
+//         let belongs_to_user = Some(message.sender.id) == self.client.user_id();
+//         let message_id_to_remove = if let (ChannelMessageId::Saved(id), true) =
+//             (message.id, belongs_to_user || is_admin)
+//         {
+//             Some(id)
+//         } else {
+//             None
+//         };
+
+//         enum MessageBackgroundHighlight {}
+//         MouseEventHandler::new::<MessageBackgroundHighlight, _>(ix, cx, |state, cx| {
+//             let container = style.style_for(state);
+//             if is_continuation {
+//                 Flex::row()
+//                     .with_child(
+//                         text.element(
+//                             theme.editor.syntax.clone(),
+//                             theme.chat_panel.rich_text.clone(),
+//                             cx,
+//                         )
+//                         .flex(1., true),
+//                     )
+//                     .with_child(render_remove(message_id_to_remove, cx, &theme))
+//                     .contained()
+//                     .with_style(*container)
+//                     .with_margin_bottom(if is_last {
+//                         theme.chat_panel.last_message_bottom_spacing
+//                     } else {
+//                         0.
+//                     })
+//                     .into_any()
+//             } else {
+//                 Flex::column()
+//                     .with_child(
+//                         Flex::row()
+//                             .with_child(
+//                                 Flex::row()
+//                                     .with_child(render_avatar(
+//                                         message.sender.avatar.clone(),
+//                                         &theme.chat_panel.avatar,
+//                                         theme.chat_panel.avatar_container,
+//                                     ))
+//                                     .with_child(
+//                                         Label::new(
+//                                             message.sender.github_login.clone(),
+//                                             theme.chat_panel.message_sender.text.clone(),
+//                                         )
+//                                         .contained()
+//                                         .with_style(theme.chat_panel.message_sender.container),
+//                                     )
+//                                     .with_child(
+//                                         Label::new(
+//                                             format_timestamp(
+//                                                 message.timestamp,
+//                                                 now,
+//                                                 self.local_timezone,
+//                                             ),
+//                                             theme.chat_panel.message_timestamp.text.clone(),
+//                                         )
+//                                         .contained()
+//                                         .with_style(theme.chat_panel.message_timestamp.container),
+//                                     )
+//                                     .align_children_center()
+//                                     .flex(1., true),
+//                             )
+//                             .with_child(render_remove(message_id_to_remove, cx, &theme))
+//                             .align_children_center(),
+//                     )
+//                     .with_child(
+//                         Flex::row()
+//                             .with_child(
+//                                 text.element(
+//                                     theme.editor.syntax.clone(),
+//                                     theme.chat_panel.rich_text.clone(),
+//                                     cx,
+//                                 )
+//                                 .flex(1., true),
+//                             )
+//                             // Add a spacer to make everything line up
+//                             .with_child(render_remove(None, cx, &theme)),
+//                     )
+//                     .contained()
+//                     .with_style(*container)
+//                     .with_margin_bottom(if is_last {
+//                         theme.chat_panel.last_message_bottom_spacing
+//                     } else {
+//                         0.
+//                     })
+//                     .into_any()
+//             }
+//         })
+//         .into_any()
+//     }
+
+//     fn render_markdown_with_mentions(
+//         language_registry: &Arc<LanguageRegistry>,
+//         current_user_id: u64,
+//         message: &channel::ChannelMessage,
+//     ) -> RichText {
+//         let mentions = message
+//             .mentions
+//             .iter()
+//             .map(|(range, user_id)| rich_text::Mention {
+//                 range: range.clone(),
+//                 is_self_mention: *user_id == current_user_id,
+//             })
+//             .collect::<Vec<_>>();
+
+//         rich_text::render_markdown(message.body.clone(), &mentions, language_registry, None)
+//     }
+
+//     fn render_input_box(&self, theme: &Arc<Theme>, cx: &AppContext) -> AnyElement<Self> {
+//         ChildView::new(&self.input_editor, cx)
+//             .contained()
+//             .with_style(theme.chat_panel.input_editor.container)
+//             .into_any()
+//     }
+
+//     fn render_channel_name(
+//         channel_store: &ModelHandle<ChannelStore>,
+//         ix: usize,
+//         item_type: ItemType,
+//         is_hovered: bool,
+//         workspace: WeakViewHandle<Workspace>,
+//         cx: &mut ViewContext<Select>,
+//     ) -> AnyElement<Select> {
+//         let theme = theme::current(cx);
+//         let tooltip_style = &theme.tooltip;
+//         let theme = &theme.chat_panel;
+//         let style = match (&item_type, is_hovered) {
+//             (ItemType::Header, _) => &theme.channel_select.header,
+//             (ItemType::Selected, _) => &theme.channel_select.active_item,
+//             (ItemType::Unselected, false) => &theme.channel_select.item,
+//             (ItemType::Unselected, true) => &theme.channel_select.hovered_item,
+//         };
+
+//         let channel = &channel_store.read(cx).channel_at(ix).unwrap();
+//         let channel_id = channel.id;
+
+//         let mut row = Flex::row()
+//             .with_child(
+//                 Label::new("#".to_string(), style.hash.text.clone())
+//                     .contained()
+//                     .with_style(style.hash.container),
+//             )
+//             .with_child(Label::new(channel.name.clone(), style.name.clone()));
+
+//         if matches!(item_type, ItemType::Header) {
+//             row.add_children([
+//                 MouseEventHandler::new::<OpenChannelNotes, _>(0, cx, |mouse_state, _| {
+//                     render_icon_button(theme.icon_button.style_for(mouse_state), "icons/file.svg")
+//                 })
+//                 .on_click(MouseButton::Left, move |_, _, cx| {
+//                     if let Some(workspace) = workspace.upgrade(cx) {
+//                         ChannelView::open(channel_id, workspace, cx).detach();
+//                     }
+//                 })
+//                 .with_tooltip::<OpenChannelNotes>(
+//                     channel_id as usize,
+//                     "Open Notes",
+//                     Some(Box::new(OpenChannelNotes)),
+//                     tooltip_style.clone(),
+//                     cx,
+//                 )
+//                 .flex_float(),
+//                 MouseEventHandler::new::<ActiveCall, _>(0, cx, |mouse_state, _| {
+//                     render_icon_button(
+//                         theme.icon_button.style_for(mouse_state),
+//                         "icons/speaker-loud.svg",
+//                     )
+//                 })
+//                 .on_click(MouseButton::Left, move |_, _, cx| {
+//                     ActiveCall::global(cx)
+//                         .update(cx, |call, cx| call.join_channel(channel_id, cx))
+//                         .detach_and_log_err(cx);
+//                 })
+//                 .with_tooltip::<ActiveCall>(
+//                     channel_id as usize,
+//                     "Join Call",
+//                     Some(Box::new(JoinCall)),
+//                     tooltip_style.clone(),
+//                     cx,
+//                 )
+//                 .flex_float(),
+//             ]);
+//         }
+
+//         row.align_children_center()
+//             .contained()
+//             .with_style(style.container)
+//             .into_any()
+//     }
+
+//     fn render_sign_in_prompt(
+//         &self,
+//         theme: &Arc<Theme>,
+//         cx: &mut ViewContext<Self>,
+//     ) -> AnyElement<Self> {
+//         enum SignInPromptLabel {}
+
+//         MouseEventHandler::new::<SignInPromptLabel, _>(0, cx, |mouse_state, _| {
+//             Label::new(
+//                 "Sign in to use chat".to_string(),
+//                 theme
+//                     .chat_panel
+//                     .sign_in_prompt
+//                     .style_for(mouse_state)
+//                     .clone(),
+//             )
+//         })
+//         .with_cursor_style(CursorStyle::PointingHand)
+//         .on_click(MouseButton::Left, move |_, this, cx| {
+//             let client = this.client.clone();
+//             cx.spawn(|this, mut cx| async move {
+//                 if client
+//                     .authenticate_and_connect(true, &cx)
+//                     .log_err()
+//                     .await
+//                     .is_some()
+//                 {
+//                     this.update(&mut cx, |this, cx| {
+//                         if cx.handle().is_focused(cx) {
+//                             cx.focus(&this.input_editor);
+//                         }
+//                     })
+//                     .ok();
+//                 }
+//             })
+//             .detach();
+//         })
+//         .aligned()
+//         .into_any()
+//     }
+
+//     fn send(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
+//         if let Some((chat, _)) = self.active_chat.as_ref() {
+//             let message = self
+//                 .input_editor
+//                 .update(cx, |editor, cx| editor.take_message(cx));
+
+//             if let Some(task) = chat
+//                 .update(cx, |chat, cx| chat.send_message(message, cx))
+//                 .log_err()
+//             {
+//                 task.detach();
+//             }
+//         }
+//     }
+
+//     fn remove_message(&mut self, id: u64, cx: &mut ViewContext<Self>) {
+//         if let Some((chat, _)) = self.active_chat.as_ref() {
+//             chat.update(cx, |chat, cx| chat.remove_message(id, cx).detach())
+//         }
+//     }
+
+//     fn load_more_messages(&mut self, _: &LoadMoreMessages, cx: &mut ViewContext<Self>) {
+//         if let Some((chat, _)) = self.active_chat.as_ref() {
+//             chat.update(cx, |channel, cx| {
+//                 if let Some(task) = channel.load_more_messages(cx) {
+//                     task.detach();
+//                 }
+//             })
+//         }
+//     }
+
+//     pub fn select_channel(
+//         &mut self,
+//         selected_channel_id: u64,
+//         scroll_to_message_id: Option<u64>,
+//         cx: &mut ViewContext<ChatPanel>,
+//     ) -> Task<Result<()>> {
+//         let open_chat = self
+//             .active_chat
+//             .as_ref()
+//             .and_then(|(chat, _)| {
+//                 (chat.read(cx).channel_id == selected_channel_id)
+//                     .then(|| Task::ready(anyhow::Ok(chat.clone())))
+//             })
+//             .unwrap_or_else(|| {
+//                 self.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_chat(chat.clone(), cx);
+//             })?;
+
+//             if let Some(message_id) = scroll_to_message_id {
+//                 if let Some(item_ix) =
+//                     ChannelChat::load_history_since_message(chat.clone(), message_id, cx.clone())
+//                         .await
+//                 {
+//                     this.update(&mut cx, |this, cx| {
+//                         if this.active_chat.as_ref().map_or(false, |(c, _)| *c == chat) {
+//                             this.message_list.scroll_to(ListOffset {
+//                                 item_ix,
+//                                 offset_in_item: 0.,
+//                             });
+//                             cx.notify();
+//                         }
+//                     })?;
+//                 }
+//             }
+
+//             Ok(())
+//         })
+//     }
+
+//     fn open_notes(&mut self, _: &OpenChannelNotes, cx: &mut ViewContext<Self>) {
+//         if let Some((chat, _)) = &self.active_chat {
+//             let channel_id = chat.read(cx).channel_id;
+//             if let Some(workspace) = self.workspace.upgrade(cx) {
+//                 ChannelView::open(channel_id, workspace, cx).detach();
+//             }
+//         }
+//     }
+
+//     fn join_call(&mut self, _: &JoinCall, cx: &mut ViewContext<Self>) {
+//         if let Some((chat, _)) = &self.active_chat {
+//             let channel_id = chat.read(cx).channel_id;
+//             ActiveCall::global(cx)
+//                 .update(cx, |call, cx| call.join_channel(channel_id, cx))
+//                 .detach_and_log_err(cx);
+//         }
+//     }
+// }
+
+// fn render_remove(
+//     message_id_to_remove: Option<u64>,
+//     cx: &mut ViewContext<'_, '_, ChatPanel>,
+//     theme: &Arc<Theme>,
+// ) -> AnyElement<ChatPanel> {
+//     enum DeleteMessage {}
+
+//     message_id_to_remove
+//         .map(|id| {
+//             MouseEventHandler::new::<DeleteMessage, _>(id as usize, cx, |mouse_state, _| {
+//                 let button_style = theme.chat_panel.icon_button.style_for(mouse_state);
+//                 render_icon_button(button_style, "icons/x.svg")
+//                     .aligned()
+//                     .into_any()
+//             })
+//             .with_padding(Padding::uniform(2.))
+//             .with_cursor_style(CursorStyle::PointingHand)
+//             .on_click(MouseButton::Left, move |_, this, cx| {
+//                 this.remove_message(id, cx);
+//             })
+//             .flex_float()
+//             .into_any()
+//         })
+//         .unwrap_or_else(|| {
+//             let style = theme.chat_panel.icon_button.default;
+
+//             Empty::new()
+//                 .constrained()
+//                 .with_width(style.icon_width)
+//                 .aligned()
+//                 .constrained()
+//                 .with_width(style.button_width)
+//                 .with_height(style.button_width)
+//                 .contained()
+//                 .with_uniform_padding(2.)
+//                 .flex_float()
+//                 .into_any()
+//         })
+// }
+
+// impl Entity for ChatPanel {
+//     type Event = Event;
+// }
+
+// impl View for ChatPanel {
+//     fn ui_name() -> &'static str {
+//         "ChatPanel"
+//     }
+
+//     fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
+//         let theme = theme::current(cx);
+//         let element = if self.client.user_id().is_some() {
+//             self.render_channel(cx)
+//         } else {
+//             self.render_sign_in_prompt(&theme, cx)
+//         };
+//         element
+//             .contained()
+//             .with_style(theme.chat_panel.container)
+//             .constrained()
+//             .with_min_width(150.)
+//             .into_any()
+//     }
+
+//     fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
+//         self.has_focus = true;
+//         if matches!(
+//             *self.client.status().borrow(),
+//             client::Status::Connected { .. }
+//         ) {
+//             let editor = self.input_editor.read(cx).editor.clone();
+//             cx.focus(&editor);
+//         }
+//     }
+
+//     fn focus_out(&mut self, _: AnyViewHandle, _: &mut ViewContext<Self>) {
+//         self.has_focus = false;
+//     }
+// }
+
+// impl Panel for ChatPanel {
+//     fn position(&self, cx: &gpui::WindowContext) -> DockPosition {
+//         settings::get::<ChatPanelSettings>(cx).dock
+//     }
+
+//     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::<ChatPanelSettings>(self.fs.clone(), cx, move |settings| {
+//             settings.dock = Some(position)
+//         });
+//     }
+
+//     fn size(&self, cx: &gpui::WindowContext) -> f32 {
+//         self.width
+//             .unwrap_or_else(|| settings::get::<ChatPanelSettings>(cx).default_width)
+//     }
+
+//     fn set_size(&mut self, size: Option<f32>, cx: &mut ViewContext<Self>) {
+//         self.width = size;
+//         self.serialize(cx);
+//         cx.notify();
+//     }
+
+//     fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) {
+//         self.active = active;
+//         if active {
+//             self.acknowledge_last_message(cx);
+//             if !is_channels_feature_enabled(cx) {
+//                 cx.emit(Event::Dismissed);
+//             }
+//         }
+//     }
+
+//     fn icon_path(&self, cx: &gpui::WindowContext) -> Option<&'static str> {
+//         (settings::get::<ChatPanelSettings>(cx).button && is_channels_feature_enabled(cx))
+//             .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 should_close_on_event(event: &Self::Event) -> bool {
+//         matches!(event, Event::Dismissed)
+//     }
+
+//     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,
+//     local_timezone: UtcOffset,
+// ) -> String {
+//     timestamp = timestamp.to_offset(local_timezone);
+//     now = now.to_offset(local_timezone);
+
+//     let today = now.date();
+//     let date = timestamp.date();
+//     let mut hour = timestamp.hour();
+//     let mut part = "am";
+//     if hour > 12 {
+//         hour -= 12;
+//         part = "pm";
+//     }
+//     if date == today {
+//         format!("{:02}:{:02}{}", hour, timestamp.minute(), part)
+//     } else if date.next_day() == Some(today) {
+//         format!("yesterday at {:02}:{:02}{}", hour, timestamp.minute(), part)
+//     } else {
+//         format!("{:02}/{}/{}", date.month() as u32, date.day(), date.year())
+//     }
+// }
+
+// fn render_icon_button<V: View>(style: &IconButton, svg_path: &'static str) -> impl Element<V> {
+//     Svg::new(svg_path)
+//         .with_color(style.color)
+//         .constrained()
+//         .with_width(style.icon_width)
+//         .aligned()
+//         .constrained()
+//         .with_width(style.button_width)
+//         .with_height(style.button_width)
+//         .contained()
+//         .with_style(style.container)
+// }
+
+// #[cfg(test)]
+// mod tests {
+//     use super::*;
+//     use gpui::fonts::HighlightStyle;
+//     use pretty_assertions::assert_eq;
+//     use rich_text::{BackgroundKind, Highlight, RenderedRegion};
+//     use util::test::marked_text_ranges;
+
+//     #[gpui::test]
+//     fn test_render_markdown_with_mentions() {
+//         let language_registry = Arc::new(LanguageRegistry::test());
+//         let (body, ranges) = marked_text_ranges("*hi*, «@abc», let's **call** «@fgh»", false);
+//         let message = channel::ChannelMessage {
+//             id: ChannelMessageId::Saved(0),
+//             body,
+//             timestamp: OffsetDateTime::now_utc(),
+//             sender: Arc::new(client::User {
+//                 github_login: "fgh".into(),
+//                 avatar: None,
+//                 id: 103,
+//             }),
+//             nonce: 5,
+//             mentions: vec![(ranges[0].clone(), 101), (ranges[1].clone(), 102)],
+//         };
+
+//         let message = ChatPanel::render_markdown_with_mentions(&language_registry, 102, &message);
+
+//         // Note that the "'" was replaced with ’ due to smart punctuation.
+//         let (body, ranges) = marked_text_ranges("«hi», «@abc», let’s «call» «@fgh»", false);
+//         assert_eq!(message.text, body);
+//         assert_eq!(
+//             message.highlights,
+//             vec![
+//                 (
+//                     ranges[0].clone(),
+//                     HighlightStyle {
+//                         italic: Some(true),
+//                         ..Default::default()
+//                     }
+//                     .into()
+//                 ),
+//                 (ranges[1].clone(), Highlight::Mention),
+//                 (
+//                     ranges[2].clone(),
+//                     HighlightStyle {
+//                         weight: Some(gpui::fonts::Weight::BOLD),
+//                         ..Default::default()
+//                     }
+//                     .into()
+//                 ),
+//                 (ranges[3].clone(), Highlight::SelfMention)
+//             ]
+//         );
+//         assert_eq!(
+//             message.regions,
+//             vec![
+//                 RenderedRegion {
+//                     background_kind: Some(BackgroundKind::Mention),
+//                     link_url: None
+//                 },
+//                 RenderedRegion {
+//                     background_kind: Some(BackgroundKind::SelfMention),
+//                     link_url: None
+//                 },
+//             ]
+//         );
+//     }
+// }

crates/collab_ui2/src/collab_panel.rs 🔗

@@ -1,3548 +1,3548 @@
-mod channel_modal;
-mod contact_finder;
-
-use crate::{
-    channel_view::{self, ChannelView},
-    chat_panel::ChatPanel,
-    face_pile::FacePile,
-    panel_settings, CollaborationPanelSettings,
-};
-use anyhow::Result;
-use call::ActiveCall;
-use channel::{Channel, ChannelEvent, ChannelId, ChannelStore};
-use channel_modal::ChannelModal;
-use client::{
-    proto::{self, PeerId},
-    Client, Contact, User, UserStore,
-};
-use contact_finder::ContactFinder;
-use context_menu::{ContextMenu, ContextMenuItem};
-use db::kvp::KEY_VALUE_STORE;
-use drag_and_drop::{DragAndDrop, Draggable};
-use editor::{Cancel, Editor};
-use feature_flags::{ChannelsAlpha, FeatureFlagAppExt, FeatureFlagViewExt};
-use futures::StreamExt;
-use fuzzy::{match_strings, StringMatchCandidate};
-use gpui::{
-    actions,
-    elements::{
-        Canvas, ChildView, Component, ContainerStyle, Empty, Flex, Image, Label, List, ListOffset,
-        ListState, MouseEventHandler, Orientation, OverlayPositionMode, Padding, ParentElement,
-        SafeStylable, Stack, Svg,
-    },
-    fonts::TextStyle,
-    geometry::{
-        rect::RectF,
-        vector::{vec2f, Vector2F},
-    },
-    impl_actions,
-    platform::{CursorStyle, MouseButton, PromptLevel},
-    serde_json, AnyElement, AppContext, AsyncAppContext, ClipboardItem, Element, Entity, FontCache,
-    ModelHandle, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle,
-};
-use menu::{Confirm, SelectNext, SelectPrev};
-use project::{Fs, Project};
-use serde_derive::{Deserialize, Serialize};
-use settings::SettingsStore;
-use std::{borrow::Cow, hash::Hash, mem, sync::Arc};
-use theme::{components::ComponentExt, IconButton, Interactive};
-use util::{maybe, ResultExt, TryFutureExt};
-use workspace::{
-    dock::{DockPosition, Panel},
-    item::ItemHandle,
-    FollowNextCollaborator, Workspace,
-};
-
-#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
-struct ToggleCollapse {
-    location: ChannelId,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
-struct NewChannel {
-    location: ChannelId,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
-struct RenameChannel {
-    channel_id: ChannelId,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
-struct ToggleSelectedIx {
-    ix: usize,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
-struct RemoveChannel {
-    channel_id: ChannelId,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
-struct InviteMembers {
-    channel_id: ChannelId,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
-struct ManageMembers {
-    channel_id: ChannelId,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
-pub struct OpenChannelNotes {
-    pub channel_id: ChannelId,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
-pub struct JoinChannelCall {
-    pub channel_id: u64,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
-pub struct JoinChannelChat {
-    pub channel_id: u64,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
-pub struct CopyChannelLink {
-    pub channel_id: u64,
-}
-
-#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
-struct StartMoveChannelFor {
-    channel_id: ChannelId,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
-struct MoveChannel {
-    to: ChannelId,
-}
-
-actions!(
-    collab_panel,
-    [
-        ToggleFocus,
-        Remove,
-        Secondary,
-        CollapseSelectedChannel,
-        ExpandSelectedChannel,
-        StartMoveChannel,
-        MoveSelected,
-        InsertSpace,
-    ]
-);
-
-impl_actions!(
-    collab_panel,
-    [
-        RemoveChannel,
-        NewChannel,
-        InviteMembers,
-        ManageMembers,
-        RenameChannel,
-        ToggleCollapse,
-        OpenChannelNotes,
-        JoinChannelCall,
-        JoinChannelChat,
-        CopyChannelLink,
-        StartMoveChannelFor,
-        MoveChannel,
-        ToggleSelectedIx
-    ]
-);
-
-#[derive(Debug, Copy, Clone, PartialEq, Eq)]
-struct ChannelMoveClipboard {
-    channel_id: ChannelId,
-}
-
-const COLLABORATION_PANEL_KEY: &'static str = "CollaborationPanel";
-
-pub fn init(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);
-    cx.add_action(CollabPanel::select_prev);
-    cx.add_action(CollabPanel::confirm);
-    cx.add_action(CollabPanel::insert_space);
-    cx.add_action(CollabPanel::remove);
-    cx.add_action(CollabPanel::remove_selected_channel);
-    cx.add_action(CollabPanel::show_inline_context_menu);
-    cx.add_action(CollabPanel::new_subchannel);
-    cx.add_action(CollabPanel::invite_members);
-    cx.add_action(CollabPanel::manage_members);
-    cx.add_action(CollabPanel::rename_selected_channel);
-    cx.add_action(CollabPanel::rename_channel);
-    cx.add_action(CollabPanel::toggle_channel_collapsed_action);
-    cx.add_action(CollabPanel::collapse_selected_channel);
-    cx.add_action(CollabPanel::expand_selected_channel);
-    cx.add_action(CollabPanel::open_channel_notes);
-    cx.add_action(CollabPanel::join_channel_chat);
-    cx.add_action(CollabPanel::copy_channel_link);
-
-    cx.add_action(
-        |panel: &mut CollabPanel, action: &ToggleSelectedIx, cx: &mut ViewContext<CollabPanel>| {
-            if panel.selection.take() != Some(action.ix) {
-                panel.selection = Some(action.ix)
-            }
-
-            cx.notify();
-        },
-    );
-
-    cx.add_action(
-        |panel: &mut CollabPanel,
-         action: &StartMoveChannelFor,
-         _: &mut ViewContext<CollabPanel>| {
-            panel.channel_clipboard = Some(ChannelMoveClipboard {
-                channel_id: action.channel_id,
-            });
-        },
-    );
-
-    cx.add_action(
-        |panel: &mut CollabPanel, _: &StartMoveChannel, _: &mut ViewContext<CollabPanel>| {
-            if let Some(channel) = panel.selected_channel() {
-                panel.channel_clipboard = Some(ChannelMoveClipboard {
-                    channel_id: channel.id,
-                })
-            }
-        },
-    );
-
-    cx.add_action(
-        |panel: &mut CollabPanel, _: &MoveSelected, cx: &mut ViewContext<CollabPanel>| {
-            let Some(clipboard) = panel.channel_clipboard.take() else {
-                return;
-            };
-            let Some(selected_channel) = panel.selected_channel() else {
-                return;
-            };
-
-            panel
-                .channel_store
-                .update(cx, |channel_store, cx| {
-                    channel_store.move_channel(clipboard.channel_id, Some(selected_channel.id), cx)
-                })
-                .detach_and_log_err(cx)
-        },
-    );
-
-    cx.add_action(
-        |panel: &mut CollabPanel, action: &MoveChannel, cx: &mut ViewContext<CollabPanel>| {
-            if let Some(clipboard) = panel.channel_clipboard.take() {
-                panel.channel_store.update(cx, |channel_store, cx| {
-                    channel_store
-                        .move_channel(clipboard.channel_id, Some(action.to), cx)
-                        .detach_and_log_err(cx)
-                })
-            }
-        },
-    );
-}
-
-#[derive(Debug)]
-pub enum ChannelEditingState {
-    Create {
-        location: Option<ChannelId>,
-        pending_name: Option<String>,
-    },
-    Rename {
-        location: ChannelId,
-        pending_name: Option<String>,
-    },
-}
-
-impl ChannelEditingState {
-    fn pending_name(&self) -> Option<&str> {
-        match self {
-            ChannelEditingState::Create { pending_name, .. } => pending_name.as_deref(),
-            ChannelEditingState::Rename { pending_name, .. } => pending_name.as_deref(),
-        }
-    }
-}
-
-pub struct CollabPanel {
-    width: Option<f32>,
-    fs: Arc<dyn Fs>,
-    has_focus: bool,
-    channel_clipboard: Option<ChannelMoveClipboard>,
-    pending_serialization: Task<Option<()>>,
-    context_menu: ViewHandle<ContextMenu>,
-    filter_editor: ViewHandle<Editor>,
-    channel_name_editor: ViewHandle<Editor>,
-    channel_editing_state: Option<ChannelEditingState>,
-    entries: Vec<ListEntry>,
-    selection: Option<usize>,
-    user_store: ModelHandle<UserStore>,
-    client: Arc<Client>,
-    channel_store: ModelHandle<ChannelStore>,
-    project: ModelHandle<Project>,
-    match_candidates: Vec<StringMatchCandidate>,
-    list_state: ListState<Self>,
-    subscriptions: Vec<Subscription>,
-    collapsed_sections: Vec<Section>,
-    collapsed_channels: Vec<ChannelId>,
-    drag_target_channel: ChannelDragTarget,
-    workspace: WeakViewHandle<Workspace>,
-    context_menu_on_selected: bool,
-}
-
-#[derive(PartialEq, Eq)]
-enum ChannelDragTarget {
-    None,
-    Root,
-    Channel(ChannelId),
-}
-
-#[derive(Serialize, Deserialize)]
-struct SerializedCollabPanel {
-    width: Option<f32>,
-    collapsed_channels: Option<Vec<ChannelId>>,
-}
-
-#[derive(Debug)]
-pub enum Event {
-    DockPositionChanged,
-    Focus,
-    Dismissed,
-}
-
-#[derive(Clone, Copy, PartialEq, Eq, Debug, PartialOrd, Ord)]
-enum Section {
-    ActiveCall,
-    Channels,
-    ChannelInvites,
-    ContactRequests,
-    Contacts,
-    Online,
-    Offline,
-}
-
-#[derive(Clone, Debug)]
-enum ListEntry {
-    Header(Section),
-    CallParticipant {
-        user: Arc<User>,
-        peer_id: Option<PeerId>,
-        is_pending: bool,
-    },
-    ParticipantProject {
-        project_id: u64,
-        worktree_root_names: Vec<String>,
-        host_user_id: u64,
-        is_last: bool,
-    },
-    ParticipantScreen {
-        peer_id: Option<PeerId>,
-        is_last: bool,
-    },
-    IncomingRequest(Arc<User>),
-    OutgoingRequest(Arc<User>),
-    ChannelInvite(Arc<Channel>),
-    Channel {
-        channel: Arc<Channel>,
-        depth: usize,
-        has_children: bool,
-    },
-    ChannelNotes {
-        channel_id: ChannelId,
-    },
-    ChannelChat {
-        channel_id: ChannelId,
-    },
-    ChannelEditor {
-        depth: usize,
-    },
-    Contact {
-        contact: Arc<Contact>,
-        calling: bool,
-    },
-    ContactPlaceholder,
-}
-
-impl Entity for CollabPanel {
-    type Event = Event;
-}
-
-impl CollabPanel {
-    pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> ViewHandle<Self> {
-        cx.add_view::<Self, _>(|cx| {
-            let view_id = cx.view_id();
-
-            let filter_editor = cx.add_view(|cx| {
-                let mut editor = Editor::single_line(
-                    Some(Arc::new(|theme| {
-                        theme.collab_panel.user_query_editor.clone()
-                    })),
-                    cx,
-                );
-                editor.set_placeholder_text("Filter channels, contacts", cx);
-                editor
-            });
-
-            cx.subscribe(&filter_editor, |this, _, event, cx| {
-                if let editor::Event::BufferEdited = event {
-                    let query = this.filter_editor.read(cx).text(cx);
-                    if !query.is_empty() {
-                        this.selection.take();
-                    }
-                    this.update_entries(true, cx);
-                    if !query.is_empty() {
-                        this.selection = this
-                            .entries
-                            .iter()
-                            .position(|entry| !matches!(entry, ListEntry::Header(_)));
-                    }
-                } else if let editor::Event::Blurred = event {
-                    let query = this.filter_editor.read(cx).text(cx);
-                    if query.is_empty() {
-                        this.selection.take();
-                        this.update_entries(true, cx);
-                    }
-                }
-            })
-            .detach();
-
-            let channel_name_editor = cx.add_view(|cx| {
-                Editor::single_line(
-                    Some(Arc::new(|theme| {
-                        theme.collab_panel.user_query_editor.clone()
-                    })),
-                    cx,
-                )
-            });
-
-            cx.subscribe(&channel_name_editor, |this, _, event, cx| {
-                if let editor::Event::Blurred = event {
-                    if let Some(state) = &this.channel_editing_state {
-                        if state.pending_name().is_some() {
-                            return;
-                        }
-                    }
-                    this.take_editing_state(cx);
-                    this.update_entries(false, cx);
-                    cx.notify();
-                }
-            })
-            .detach();
-
-            let list_state =
-                ListState::<Self>::new(0, Orientation::Top, 1000., move |this, ix, cx| {
-                    let theme = theme::current(cx).clone();
-                    let is_selected = this.selection == Some(ix);
-                    let current_project_id = this.project.read(cx).remote_id();
-
-                    match &this.entries[ix] {
-                        ListEntry::Header(section) => {
-                            let is_collapsed = this.collapsed_sections.contains(section);
-                            this.render_header(*section, &theme, is_selected, is_collapsed, cx)
-                        }
-                        ListEntry::CallParticipant {
-                            user,
-                            peer_id,
-                            is_pending,
-                        } => Self::render_call_participant(
-                            user,
-                            *peer_id,
-                            this.user_store.clone(),
-                            *is_pending,
-                            is_selected,
-                            &theme,
-                            cx,
-                        ),
-                        ListEntry::ParticipantProject {
-                            project_id,
-                            worktree_root_names,
-                            host_user_id,
-                            is_last,
-                        } => Self::render_participant_project(
-                            *project_id,
-                            worktree_root_names,
-                            *host_user_id,
-                            Some(*project_id) == current_project_id,
-                            *is_last,
-                            is_selected,
-                            &theme,
-                            cx,
-                        ),
-                        ListEntry::ParticipantScreen { peer_id, is_last } => {
-                            Self::render_participant_screen(
-                                *peer_id,
-                                *is_last,
-                                is_selected,
-                                &theme.collab_panel,
-                                cx,
-                            )
-                        }
-                        ListEntry::Channel {
-                            channel,
-                            depth,
-                            has_children,
-                        } => {
-                            let channel_row = this.render_channel(
-                                &*channel,
-                                *depth,
-                                &theme,
-                                is_selected,
-                                *has_children,
-                                ix,
-                                cx,
-                            );
-
-                            if is_selected && this.context_menu_on_selected {
-                                Stack::new()
-                                    .with_child(channel_row)
-                                    .with_child(
-                                        ChildView::new(&this.context_menu, cx)
-                                            .aligned()
-                                            .bottom()
-                                            .right(),
-                                    )
-                                    .into_any()
-                            } else {
-                                return channel_row;
-                            }
-                        }
-                        ListEntry::ChannelNotes { channel_id } => this.render_channel_notes(
-                            *channel_id,
-                            &theme.collab_panel,
-                            is_selected,
-                            ix,
-                            cx,
-                        ),
-                        ListEntry::ChannelChat { channel_id } => this.render_channel_chat(
-                            *channel_id,
-                            &theme.collab_panel,
-                            is_selected,
-                            ix,
-                            cx,
-                        ),
-                        ListEntry::ChannelInvite(channel) => Self::render_channel_invite(
-                            channel.clone(),
-                            this.channel_store.clone(),
-                            &theme.collab_panel,
-                            is_selected,
-                            cx,
-                        ),
-                        ListEntry::IncomingRequest(user) => Self::render_contact_request(
-                            user.clone(),
-                            this.user_store.clone(),
-                            &theme.collab_panel,
-                            true,
-                            is_selected,
-                            cx,
-                        ),
-                        ListEntry::OutgoingRequest(user) => Self::render_contact_request(
-                            user.clone(),
-                            this.user_store.clone(),
-                            &theme.collab_panel,
-                            false,
-                            is_selected,
-                            cx,
-                        ),
-                        ListEntry::Contact { contact, calling } => Self::render_contact(
-                            contact,
-                            *calling,
-                            &this.project,
-                            &theme,
-                            is_selected,
-                            cx,
-                        ),
-                        ListEntry::ChannelEditor { depth } => {
-                            this.render_channel_editor(&theme, *depth, cx)
-                        }
-                        ListEntry::ContactPlaceholder => {
-                            this.render_contact_placeholder(&theme.collab_panel, is_selected, cx)
-                        }
-                    }
-                });
-
-            let mut this = Self {
-                width: None,
-                has_focus: false,
-                channel_clipboard: None,
-                fs: workspace.app_state().fs.clone(),
-                pending_serialization: Task::ready(None),
-                context_menu: cx.add_view(|cx| ContextMenu::new(view_id, cx)),
-                channel_name_editor,
-                filter_editor,
-                entries: Vec::default(),
-                channel_editing_state: None,
-                selection: None,
-                user_store: workspace.user_store().clone(),
-                channel_store: ChannelStore::global(cx),
-                project: workspace.project().clone(),
-                subscriptions: Vec::default(),
-                match_candidates: Vec::default(),
-                collapsed_sections: vec![Section::Offline],
-                collapsed_channels: Vec::default(),
-                workspace: workspace.weak_handle(),
-                client: workspace.app_state().client.clone(),
-                context_menu_on_selected: true,
-                drag_target_channel: ChannelDragTarget::None,
-                list_state,
-            };
-
-            this.update_entries(false, cx);
-
-            // Update the dock position when the setting changes.
-            let mut old_dock_position = this.position(cx);
-            this.subscriptions
-                .push(
-                    cx.observe_global::<SettingsStore, _>(move |this: &mut Self, cx| {
-                        let new_dock_position = this.position(cx);
-                        if new_dock_position != old_dock_position {
-                            old_dock_position = new_dock_position;
-                            cx.emit(Event::DockPositionChanged);
-                        }
-                        cx.notify();
-                    }),
-                );
-
-            let active_call = ActiveCall::global(cx);
-            this.subscriptions
-                .push(cx.observe(&this.user_store, |this, _, cx| {
-                    this.update_entries(true, cx)
-                }));
-            this.subscriptions
-                .push(cx.observe(&this.channel_store, |this, _, cx| {
-                    this.update_entries(true, cx)
-                }));
-            this.subscriptions
-                .push(cx.observe(&active_call, |this, _, cx| this.update_entries(true, cx)));
-            this.subscriptions
-                .push(cx.observe_flag::<ChannelsAlpha, _>(move |_, this, cx| {
-                    this.update_entries(true, cx)
-                }));
-            this.subscriptions.push(cx.subscribe(
-                &this.channel_store,
-                |this, _channel_store, e, cx| match e {
-                    ChannelEvent::ChannelCreated(channel_id)
-                    | ChannelEvent::ChannelRenamed(channel_id) => {
-                        if this.take_editing_state(cx) {
-                            this.update_entries(false, cx);
-                            this.selection = this.entries.iter().position(|entry| {
-                                if let ListEntry::Channel { channel, .. } = entry {
-                                    channel.id == *channel_id
-                                } else {
-                                    false
-                                }
-                            });
-                        }
-                    }
-                },
-            ));
-
-            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(COLLABORATION_PANEL_KEY) })
-                .await
-                .log_err()
-                .flatten()
-            {
-                match serde_json::from_str::<SerializedCollabPanel>(&panel) {
-                    Ok(panel) => Some(panel),
-                    Err(err) => {
-                        log::error!("Failed to deserialize collaboration panel: {}", err);
-                        None
-                    }
-                }
-            } else {
-                None
-            };
-
-            workspace.update(&mut cx, |workspace, cx| {
-                let panel = CollabPanel::new(workspace, cx);
-                if let Some(serialized_panel) = serialized_panel {
-                    panel.update(cx, |panel, cx| {
-                        panel.width = serialized_panel.width;
-                        panel.collapsed_channels = serialized_panel
-                            .collapsed_channels
-                            .unwrap_or_else(|| Vec::new());
-                        cx.notify();
-                    });
-                }
-                panel
-            })
-        })
-    }
-
-    fn serialize(&mut self, cx: &mut ViewContext<Self>) {
-        let width = self.width;
-        let collapsed_channels = self.collapsed_channels.clone();
-        self.pending_serialization = cx.background().spawn(
-            async move {
-                KEY_VALUE_STORE
-                    .write_kvp(
-                        COLLABORATION_PANEL_KEY.into(),
-                        serde_json::to_string(&SerializedCollabPanel {
-                            width,
-                            collapsed_channels: Some(collapsed_channels),
-                        })?,
-                    )
-                    .await?;
-                anyhow::Ok(())
-            }
-            .log_err(),
-        );
-    }
-
-    fn update_entries(&mut self, select_same_item: bool, cx: &mut ViewContext<Self>) {
-        let channel_store = self.channel_store.read(cx);
-        let user_store = self.user_store.read(cx);
-        let query = self.filter_editor.read(cx).text(cx);
-        let executor = cx.background().clone();
-
-        let prev_selected_entry = self.selection.and_then(|ix| self.entries.get(ix).cloned());
-        let old_entries = mem::take(&mut self.entries);
-        let mut scroll_to_top = false;
-
-        if let Some(room) = ActiveCall::global(cx).read(cx).room() {
-            self.entries.push(ListEntry::Header(Section::ActiveCall));
-            if !old_entries
-                .iter()
-                .any(|entry| matches!(entry, ListEntry::Header(Section::ActiveCall)))
-            {
-                scroll_to_top = true;
-            }
-
-            if !self.collapsed_sections.contains(&Section::ActiveCall) {
-                let room = room.read(cx);
-
-                if let Some(channel_id) = room.channel_id() {
-                    self.entries.push(ListEntry::ChannelNotes { channel_id });
-                    self.entries.push(ListEntry::ChannelChat { channel_id })
-                }
-
-                // Populate the active user.
-                if let Some(user) = user_store.current_user() {
-                    self.match_candidates.clear();
-                    self.match_candidates.push(StringMatchCandidate {
-                        id: 0,
-                        string: user.github_login.clone(),
-                        char_bag: user.github_login.chars().collect(),
-                    });
-                    let matches = executor.block(match_strings(
-                        &self.match_candidates,
-                        &query,
-                        true,
-                        usize::MAX,
-                        &Default::default(),
-                        executor.clone(),
-                    ));
-                    if !matches.is_empty() {
-                        let user_id = user.id;
-                        self.entries.push(ListEntry::CallParticipant {
-                            user,
-                            peer_id: None,
-                            is_pending: false,
-                        });
-                        let mut projects = room.local_participant().projects.iter().peekable();
-                        while let Some(project) = projects.next() {
-                            self.entries.push(ListEntry::ParticipantProject {
-                                project_id: project.id,
-                                worktree_root_names: project.worktree_root_names.clone(),
-                                host_user_id: user_id,
-                                is_last: projects.peek().is_none() && !room.is_screen_sharing(),
-                            });
-                        }
-                        if room.is_screen_sharing() {
-                            self.entries.push(ListEntry::ParticipantScreen {
-                                peer_id: None,
-                                is_last: true,
-                            });
-                        }
-                    }
-                }
-
-                // Populate remote participants.
-                self.match_candidates.clear();
-                self.match_candidates
-                    .extend(room.remote_participants().iter().map(|(_, participant)| {
-                        StringMatchCandidate {
-                            id: participant.user.id as usize,
-                            string: participant.user.github_login.clone(),
-                            char_bag: participant.user.github_login.chars().collect(),
-                        }
-                    }));
-                let matches = executor.block(match_strings(
-                    &self.match_candidates,
-                    &query,
-                    true,
-                    usize::MAX,
-                    &Default::default(),
-                    executor.clone(),
-                ));
-                for mat in matches {
-                    let user_id = mat.candidate_id as u64;
-                    let participant = &room.remote_participants()[&user_id];
-                    self.entries.push(ListEntry::CallParticipant {
-                        user: participant.user.clone(),
-                        peer_id: Some(participant.peer_id),
-                        is_pending: false,
-                    });
-                    let mut projects = participant.projects.iter().peekable();
-                    while let Some(project) = projects.next() {
-                        self.entries.push(ListEntry::ParticipantProject {
-                            project_id: project.id,
-                            worktree_root_names: project.worktree_root_names.clone(),
-                            host_user_id: participant.user.id,
-                            is_last: projects.peek().is_none()
-                                && participant.video_tracks.is_empty(),
-                        });
-                    }
-                    if !participant.video_tracks.is_empty() {
-                        self.entries.push(ListEntry::ParticipantScreen {
-                            peer_id: Some(participant.peer_id),
-                            is_last: true,
-                        });
-                    }
-                }
-
-                // Populate pending participants.
-                self.match_candidates.clear();
-                self.match_candidates
-                    .extend(room.pending_participants().iter().enumerate().map(
-                        |(id, participant)| StringMatchCandidate {
-                            id,
-                            string: participant.github_login.clone(),
-                            char_bag: participant.github_login.chars().collect(),
-                        },
-                    ));
-                let matches = executor.block(match_strings(
-                    &self.match_candidates,
-                    &query,
-                    true,
-                    usize::MAX,
-                    &Default::default(),
-                    executor.clone(),
-                ));
-                self.entries
-                    .extend(matches.iter().map(|mat| ListEntry::CallParticipant {
-                        user: room.pending_participants()[mat.candidate_id].clone(),
-                        peer_id: None,
-                        is_pending: true,
-                    }));
-            }
-        }
-
-        let mut request_entries = Vec::new();
-
-        if cx.has_flag::<ChannelsAlpha>() {
-            self.entries.push(ListEntry::Header(Section::Channels));
-
-            if channel_store.channel_count() > 0 || self.channel_editing_state.is_some() {
-                self.match_candidates.clear();
-                self.match_candidates
-                    .extend(channel_store.ordered_channels().enumerate().map(
-                        |(ix, (_, channel))| StringMatchCandidate {
-                            id: ix,
-                            string: channel.name.clone(),
-                            char_bag: channel.name.chars().collect(),
-                        },
-                    ));
-                let matches = executor.block(match_strings(
-                    &self.match_candidates,
-                    &query,
-                    true,
-                    usize::MAX,
-                    &Default::default(),
-                    executor.clone(),
-                ));
-                if let Some(state) = &self.channel_editing_state {
-                    if matches!(state, ChannelEditingState::Create { location: None, .. }) {
-                        self.entries.push(ListEntry::ChannelEditor { depth: 0 });
-                    }
-                }
-                let mut collapse_depth = None;
-                for mat in matches {
-                    let channel = channel_store.channel_at_index(mat.candidate_id).unwrap();
-                    let depth = channel.parent_path.len();
-
-                    if collapse_depth.is_none() && self.is_channel_collapsed(channel.id) {
-                        collapse_depth = Some(depth);
-                    } else if let Some(collapsed_depth) = collapse_depth {
-                        if depth > collapsed_depth {
-                            continue;
-                        }
-                        if self.is_channel_collapsed(channel.id) {
-                            collapse_depth = Some(depth);
-                        } else {
-                            collapse_depth = None;
-                        }
-                    }
-
-                    let has_children = channel_store
-                        .channel_at_index(mat.candidate_id + 1)
-                        .map_or(false, |next_channel| {
-                            next_channel.parent_path.ends_with(&[channel.id])
-                        });
-
-                    match &self.channel_editing_state {
-                        Some(ChannelEditingState::Create {
-                            location: parent_id,
-                            ..
-                        }) if *parent_id == Some(channel.id) => {
-                            self.entries.push(ListEntry::Channel {
-                                channel: channel.clone(),
-                                depth,
-                                has_children: false,
-                            });
-                            self.entries
-                                .push(ListEntry::ChannelEditor { depth: depth + 1 });
-                        }
-                        Some(ChannelEditingState::Rename {
-                            location: parent_id,
-                            ..
-                        }) if parent_id == &channel.id => {
-                            self.entries.push(ListEntry::ChannelEditor { depth });
-                        }
-                        _ => {
-                            self.entries.push(ListEntry::Channel {
-                                channel: channel.clone(),
-                                depth,
-                                has_children,
-                            });
-                        }
-                    }
-                }
-            }
-
-            let channel_invites = channel_store.channel_invitations();
-            if !channel_invites.is_empty() {
-                self.match_candidates.clear();
-                self.match_candidates
-                    .extend(channel_invites.iter().enumerate().map(|(ix, channel)| {
-                        StringMatchCandidate {
-                            id: ix,
-                            string: channel.name.clone(),
-                            char_bag: channel.name.chars().collect(),
-                        }
-                    }));
-                let matches = executor.block(match_strings(
-                    &self.match_candidates,
-                    &query,
-                    true,
-                    usize::MAX,
-                    &Default::default(),
-                    executor.clone(),
-                ));
-                request_entries.extend(matches.iter().map(|mat| {
-                    ListEntry::ChannelInvite(channel_invites[mat.candidate_id].clone())
-                }));
-
-                if !request_entries.is_empty() {
-                    self.entries
-                        .push(ListEntry::Header(Section::ChannelInvites));
-                    if !self.collapsed_sections.contains(&Section::ChannelInvites) {
-                        self.entries.append(&mut request_entries);
-                    }
-                }
-            }
-        }
-
-        self.entries.push(ListEntry::Header(Section::Contacts));
-
-        request_entries.clear();
-        let incoming = user_store.incoming_contact_requests();
-        if !incoming.is_empty() {
-            self.match_candidates.clear();
-            self.match_candidates
-                .extend(
-                    incoming
-                        .iter()
-                        .enumerate()
-                        .map(|(ix, user)| StringMatchCandidate {
-                            id: ix,
-                            string: user.github_login.clone(),
-                            char_bag: user.github_login.chars().collect(),
-                        }),
-                );
-            let matches = executor.block(match_strings(
-                &self.match_candidates,
-                &query,
-                true,
-                usize::MAX,
-                &Default::default(),
-                executor.clone(),
-            ));
-            request_entries.extend(
-                matches
-                    .iter()
-                    .map(|mat| ListEntry::IncomingRequest(incoming[mat.candidate_id].clone())),
-            );
-        }
-
-        let outgoing = user_store.outgoing_contact_requests();
-        if !outgoing.is_empty() {
-            self.match_candidates.clear();
-            self.match_candidates
-                .extend(
-                    outgoing
-                        .iter()
-                        .enumerate()
-                        .map(|(ix, user)| StringMatchCandidate {
-                            id: ix,
-                            string: user.github_login.clone(),
-                            char_bag: user.github_login.chars().collect(),
-                        }),
-                );
-            let matches = executor.block(match_strings(
-                &self.match_candidates,
-                &query,
-                true,
-                usize::MAX,
-                &Default::default(),
-                executor.clone(),
-            ));
-            request_entries.extend(
-                matches
-                    .iter()
-                    .map(|mat| ListEntry::OutgoingRequest(outgoing[mat.candidate_id].clone())),
-            );
-        }
-
-        if !request_entries.is_empty() {
-            self.entries
-                .push(ListEntry::Header(Section::ContactRequests));
-            if !self.collapsed_sections.contains(&Section::ContactRequests) {
-                self.entries.append(&mut request_entries);
-            }
-        }
-
-        let contacts = user_store.contacts();
-        if !contacts.is_empty() {
-            self.match_candidates.clear();
-            self.match_candidates
-                .extend(
-                    contacts
-                        .iter()
-                        .enumerate()
-                        .map(|(ix, contact)| StringMatchCandidate {
-                            id: ix,
-                            string: contact.user.github_login.clone(),
-                            char_bag: contact.user.github_login.chars().collect(),
-                        }),
-                );
-
-            let matches = executor.block(match_strings(
-                &self.match_candidates,
-                &query,
-                true,
-                usize::MAX,
-                &Default::default(),
-                executor.clone(),
-            ));
-
-            let (online_contacts, offline_contacts) = matches
-                .iter()
-                .partition::<Vec<_>, _>(|mat| contacts[mat.candidate_id].online);
-
-            for (matches, section) in [
-                (online_contacts, Section::Online),
-                (offline_contacts, Section::Offline),
-            ] {
-                if !matches.is_empty() {
-                    self.entries.push(ListEntry::Header(section));
-                    if !self.collapsed_sections.contains(&section) {
-                        let active_call = &ActiveCall::global(cx).read(cx);
-                        for mat in matches {
-                            let contact = &contacts[mat.candidate_id];
-                            self.entries.push(ListEntry::Contact {
-                                contact: contact.clone(),
-                                calling: active_call.pending_invites().contains(&contact.user.id),
-                            });
-                        }
-                    }
-                }
-            }
-        }
-
-        if incoming.is_empty() && outgoing.is_empty() && contacts.is_empty() {
-            self.entries.push(ListEntry::ContactPlaceholder);
-        }
-
-        if select_same_item {
-            if let Some(prev_selected_entry) = prev_selected_entry {
-                self.selection.take();
-                for (ix, entry) in self.entries.iter().enumerate() {
-                    if *entry == prev_selected_entry {
-                        self.selection = Some(ix);
-                        break;
-                    }
-                }
-            }
-        } else {
-            self.selection = self.selection.and_then(|prev_selection| {
-                if self.entries.is_empty() {
-                    None
-                } else {
-                    Some(prev_selection.min(self.entries.len() - 1))
-                }
-            });
-        }
-
-        let old_scroll_top = self.list_state.logical_scroll_top();
-
-        self.list_state.reset(self.entries.len());
-
-        if scroll_to_top {
-            self.list_state.scroll_to(ListOffset::default());
-        } else {
-            // Attempt to maintain the same scroll position.
-            if let Some(old_top_entry) = old_entries.get(old_scroll_top.item_ix) {
-                let new_scroll_top = self
-                    .entries
-                    .iter()
-                    .position(|entry| entry == old_top_entry)
-                    .map(|item_ix| ListOffset {
-                        item_ix,
-                        offset_in_item: old_scroll_top.offset_in_item,
-                    })
-                    .or_else(|| {
-                        let entry_after_old_top = old_entries.get(old_scroll_top.item_ix + 1)?;
-                        let item_ix = self
-                            .entries
-                            .iter()
-                            .position(|entry| entry == entry_after_old_top)?;
-                        Some(ListOffset {
-                            item_ix,
-                            offset_in_item: 0.,
-                        })
-                    })
-                    .or_else(|| {
-                        let entry_before_old_top =
-                            old_entries.get(old_scroll_top.item_ix.saturating_sub(1))?;
-                        let item_ix = self
-                            .entries
-                            .iter()
-                            .position(|entry| entry == entry_before_old_top)?;
-                        Some(ListOffset {
-                            item_ix,
-                            offset_in_item: 0.,
-                        })
-                    });
-
-                self.list_state
-                    .scroll_to(new_scroll_top.unwrap_or(old_scroll_top));
-            }
-        }
-
-        cx.notify();
-    }
-
-    fn render_call_participant(
-        user: &User,
-        peer_id: Option<PeerId>,
-        user_store: ModelHandle<UserStore>,
-        is_pending: bool,
-        is_selected: bool,
-        theme: &theme::Theme,
-        cx: &mut ViewContext<Self>,
-    ) -> AnyElement<Self> {
-        enum CallParticipant {}
-        enum CallParticipantTooltip {}
-        enum LeaveCallButton {}
-        enum LeaveCallTooltip {}
-
-        let collab_theme = &theme.collab_panel;
-
-        let is_current_user =
-            user_store.read(cx).current_user().map(|user| user.id) == Some(user.id);
-
-        let content = MouseEventHandler::new::<CallParticipant, _>(
-            user.id as usize,
-            cx,
-            |mouse_state, cx| {
-                let style = if is_current_user {
-                    *collab_theme
-                        .contact_row
-                        .in_state(is_selected)
-                        .style_for(&mut Default::default())
-                } else {
-                    *collab_theme
-                        .contact_row
-                        .in_state(is_selected)
-                        .style_for(mouse_state)
-                };
-
-                Flex::row()
-                    .with_children(user.avatar.clone().map(|avatar| {
-                        Image::from_data(avatar)
-                            .with_style(collab_theme.contact_avatar)
-                            .aligned()
-                            .left()
-                    }))
-                    .with_child(
-                        Label::new(
-                            user.github_login.clone(),
-                            collab_theme.contact_username.text.clone(),
-                        )
-                        .contained()
-                        .with_style(collab_theme.contact_username.container)
-                        .aligned()
-                        .left()
-                        .flex(1., true),
-                    )
-                    .with_children(if is_pending {
-                        Some(
-                            Label::new("Calling", collab_theme.calling_indicator.text.clone())
-                                .contained()
-                                .with_style(collab_theme.calling_indicator.container)
-                                .aligned()
-                                .into_any(),
-                        )
-                    } else if is_current_user {
-                        Some(
-                            MouseEventHandler::new::<LeaveCallButton, _>(0, cx, |state, _| {
-                                render_icon_button(
-                                    theme
-                                        .collab_panel
-                                        .leave_call_button
-                                        .style_for(is_selected, state),
-                                    "icons/exit.svg",
-                                )
-                            })
-                            .with_cursor_style(CursorStyle::PointingHand)
-                            .on_click(MouseButton::Left, |_, _, cx| {
-                                Self::leave_call(cx);
-                            })
-                            .with_tooltip::<LeaveCallTooltip>(
-                                0,
-                                "Leave call",
-                                None,
-                                theme.tooltip.clone(),
-                                cx,
-                            )
-                            .into_any(),
-                        )
-                    } else {
-                        None
-                    })
-                    .constrained()
-                    .with_height(collab_theme.row_height)
-                    .contained()
-                    .with_style(style)
-            },
-        );
-
-        if is_current_user || is_pending || peer_id.is_none() {
-            return content.into_any();
-        }
-
-        let tooltip = format!("Follow {}", user.github_login);
-
-        content
-            .on_click(MouseButton::Left, move |_, this, cx| {
-                if let Some(workspace) = this.workspace.upgrade(cx) {
-                    workspace
-                        .update(cx, |workspace, cx| workspace.follow(peer_id.unwrap(), cx))
-                        .map(|task| task.detach_and_log_err(cx));
-                }
-            })
-            .with_cursor_style(CursorStyle::PointingHand)
-            .with_tooltip::<CallParticipantTooltip>(
-                user.id as usize,
-                tooltip,
-                Some(Box::new(FollowNextCollaborator)),
-                theme.tooltip.clone(),
-                cx,
-            )
-            .into_any()
-    }
-
-    fn render_participant_project(
-        project_id: u64,
-        worktree_root_names: &[String],
-        host_user_id: u64,
-        is_current: bool,
-        is_last: bool,
-        is_selected: bool,
-        theme: &theme::Theme,
-        cx: &mut ViewContext<Self>,
-    ) -> AnyElement<Self> {
-        enum JoinProject {}
-        enum JoinProjectTooltip {}
-
-        let collab_theme = &theme.collab_panel;
-        let host_avatar_width = collab_theme
-            .contact_avatar
-            .width
-            .or(collab_theme.contact_avatar.height)
-            .unwrap_or(0.);
-        let tree_branch = collab_theme.tree_branch;
-        let project_name = if worktree_root_names.is_empty() {
-            "untitled".to_string()
-        } else {
-            worktree_root_names.join(", ")
-        };
-
-        let content =
-            MouseEventHandler::new::<JoinProject, _>(project_id as usize, cx, |mouse_state, cx| {
-                let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state);
-                let row = if is_current {
-                    collab_theme
-                        .project_row
-                        .in_state(true)
-                        .style_for(&mut Default::default())
-                } else {
-                    collab_theme
-                        .project_row
-                        .in_state(is_selected)
-                        .style_for(mouse_state)
-                };
-
-                Flex::row()
-                    .with_child(render_tree_branch(
-                        tree_branch,
-                        &row.name.text,
-                        is_last,
-                        vec2f(host_avatar_width, collab_theme.row_height),
-                        cx.font_cache(),
-                    ))
-                    .with_child(
-                        Svg::new("icons/file_icons/folder.svg")
-                            .with_color(collab_theme.channel_hash.color)
-                            .constrained()
-                            .with_width(collab_theme.channel_hash.width)
-                            .aligned()
-                            .left(),
-                    )
-                    .with_child(
-                        Label::new(project_name.clone(), row.name.text.clone())
-                            .aligned()
-                            .left()
-                            .contained()
-                            .with_style(row.name.container)
-                            .flex(1., false),
-                    )
-                    .constrained()
-                    .with_height(collab_theme.row_height)
-                    .contained()
-                    .with_style(row.container)
-            });
-
-        if is_current {
-            return content.into_any();
-        }
-
-        content
-            .with_cursor_style(CursorStyle::PointingHand)
-            .on_click(MouseButton::Left, move |_, this, cx| {
-                if let Some(workspace) = this.workspace.upgrade(cx) {
-                    let app_state = workspace.read(cx).app_state().clone();
-                    workspace::join_remote_project(project_id, host_user_id, app_state, cx)
-                        .detach_and_log_err(cx);
-                }
-            })
-            .with_tooltip::<JoinProjectTooltip>(
-                project_id as usize,
-                format!("Open {}", project_name),
-                None,
-                theme.tooltip.clone(),
-                cx,
-            )
-            .into_any()
-    }
-
-    fn render_participant_screen(
-        peer_id: Option<PeerId>,
-        is_last: bool,
-        is_selected: bool,
-        theme: &theme::CollabPanel,
-        cx: &mut ViewContext<Self>,
-    ) -> AnyElement<Self> {
-        enum OpenSharedScreen {}
-
-        let host_avatar_width = theme
-            .contact_avatar
-            .width
-            .or(theme.contact_avatar.height)
-            .unwrap_or(0.);
-        let tree_branch = theme.tree_branch;
-
-        let handler = MouseEventHandler::new::<OpenSharedScreen, _>(
-            peer_id.map(|id| id.as_u64()).unwrap_or(0) as usize,
-            cx,
-            |mouse_state, cx| {
-                let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state);
-                let row = theme
-                    .project_row
-                    .in_state(is_selected)
-                    .style_for(mouse_state);
-
-                Flex::row()
-                    .with_child(render_tree_branch(
-                        tree_branch,
-                        &row.name.text,
-                        is_last,
-                        vec2f(host_avatar_width, theme.row_height),
-                        cx.font_cache(),
-                    ))
-                    .with_child(
-                        Svg::new("icons/desktop.svg")
-                            .with_color(theme.channel_hash.color)
-                            .constrained()
-                            .with_width(theme.channel_hash.width)
-                            .aligned()
-                            .left(),
-                    )
-                    .with_child(
-                        Label::new("Screen", row.name.text.clone())
-                            .aligned()
-                            .left()
-                            .contained()
-                            .with_style(row.name.container)
-                            .flex(1., false),
-                    )
-                    .constrained()
-                    .with_height(theme.row_height)
-                    .contained()
-                    .with_style(row.container)
-            },
-        );
-        if peer_id.is_none() {
-            return handler.into_any();
-        }
-        handler
-            .with_cursor_style(CursorStyle::PointingHand)
-            .on_click(MouseButton::Left, move |_, this, cx| {
-                if let Some(workspace) = this.workspace.upgrade(cx) {
-                    workspace.update(cx, |workspace, cx| {
-                        workspace.open_shared_screen(peer_id.unwrap(), cx)
-                    });
-                }
-            })
-            .into_any()
-    }
-
-    fn take_editing_state(&mut self, cx: &mut ViewContext<Self>) -> bool {
-        if let Some(_) = self.channel_editing_state.take() {
-            self.channel_name_editor.update(cx, |editor, cx| {
-                editor.set_text("", cx);
-            });
-            true
-        } else {
-            false
-        }
-    }
-
-    fn render_header(
-        &self,
-        section: Section,
-        theme: &theme::Theme,
-        is_selected: bool,
-        is_collapsed: bool,
-        cx: &mut ViewContext<Self>,
-    ) -> AnyElement<Self> {
-        enum Header {}
-        enum LeaveCallContactList {}
-        enum AddChannel {}
-
-        let tooltip_style = &theme.tooltip;
-        let mut channel_link = None;
-        let mut channel_tooltip_text = None;
-        let mut channel_icon = None;
-        let mut is_dragged_over = false;
-
-        let text = match section {
-            Section::ActiveCall => {
-                let channel_name = maybe!({
-                    let channel_id = ActiveCall::global(cx).read(cx).channel_id(cx)?;
-
-                    let channel = self.channel_store.read(cx).channel_for_id(channel_id)?;
-
-                    channel_link = Some(channel.link());
-                    (channel_icon, channel_tooltip_text) = match channel.visibility {
-                        proto::ChannelVisibility::Public => {
-                            (Some("icons/public.svg"), Some("Copy public channel link."))
-                        }
-                        proto::ChannelVisibility::Members => {
-                            (Some("icons/hash.svg"), Some("Copy private channel link."))
-                        }
-                    };
-
-                    Some(channel.name.as_str())
-                });
-
-                if let Some(name) = channel_name {
-                    Cow::Owned(format!("{}", name))
-                } else {
-                    Cow::Borrowed("Current Call")
-                }
-            }
-            Section::ContactRequests => Cow::Borrowed("Requests"),
-            Section::Contacts => Cow::Borrowed("Contacts"),
-            Section::Channels => Cow::Borrowed("Channels"),
-            Section::ChannelInvites => Cow::Borrowed("Invites"),
-            Section::Online => Cow::Borrowed("Online"),
-            Section::Offline => Cow::Borrowed("Offline"),
-        };
-
-        enum AddContact {}
-        let button = match section {
-            Section::ActiveCall => channel_link.map(|channel_link| {
-                let channel_link_copy = channel_link.clone();
-                MouseEventHandler::new::<AddContact, _>(0, cx, |state, _| {
-                    render_icon_button(
-                        theme
-                            .collab_panel
-                            .leave_call_button
-                            .style_for(is_selected, state),
-                        "icons/link.svg",
-                    )
-                })
-                .with_cursor_style(CursorStyle::PointingHand)
-                .on_click(MouseButton::Left, move |_, _, cx| {
-                    let item = ClipboardItem::new(channel_link_copy.clone());
-                    cx.write_to_clipboard(item)
-                })
-                .with_tooltip::<AddContact>(
-                    0,
-                    channel_tooltip_text.unwrap(),
-                    None,
-                    tooltip_style.clone(),
-                    cx,
-                )
-            }),
-            Section::Contacts => Some(
-                MouseEventHandler::new::<LeaveCallContactList, _>(0, cx, |state, _| {
-                    render_icon_button(
-                        theme
-                            .collab_panel
-                            .add_contact_button
-                            .style_for(is_selected, state),
-                        "icons/plus.svg",
-                    )
-                })
-                .with_cursor_style(CursorStyle::PointingHand)
-                .on_click(MouseButton::Left, |_, this, cx| {
-                    this.toggle_contact_finder(cx);
-                })
-                .with_tooltip::<LeaveCallContactList>(
-                    0,
-                    "Search for new contact",
-                    None,
-                    tooltip_style.clone(),
-                    cx,
-                ),
-            ),
-            Section::Channels => {
-                if cx
-                    .global::<DragAndDrop<Workspace>>()
-                    .currently_dragged::<Channel>(cx.window())
-                    .is_some()
-                    && self.drag_target_channel == ChannelDragTarget::Root
-                {
-                    is_dragged_over = true;
-                }
-
-                Some(
-                    MouseEventHandler::new::<AddChannel, _>(0, cx, |state, _| {
-                        render_icon_button(
-                            theme
-                                .collab_panel
-                                .add_contact_button
-                                .style_for(is_selected, state),
-                            "icons/plus.svg",
-                        )
-                    })
-                    .with_cursor_style(CursorStyle::PointingHand)
-                    .on_click(MouseButton::Left, |_, this, cx| this.new_root_channel(cx))
-                    .with_tooltip::<AddChannel>(
-                        0,
-                        "Create a channel",
-                        None,
-                        tooltip_style.clone(),
-                        cx,
-                    ),
-                )
-            }
-            _ => None,
-        };
-
-        let can_collapse = match section {
-            Section::ActiveCall | Section::Channels | Section::Contacts => false,
-            Section::ChannelInvites
-            | Section::ContactRequests
-            | Section::Online
-            | Section::Offline => true,
-        };
-        let icon_size = (&theme.collab_panel).section_icon_size;
-        let mut result = MouseEventHandler::new::<Header, _>(section as usize, cx, |state, _| {
-            let header_style = if can_collapse {
-                theme
-                    .collab_panel
-                    .subheader_row
-                    .in_state(is_selected)
-                    .style_for(state)
-            } else {
-                &theme.collab_panel.header_row
-            };
-
-            Flex::row()
-                .with_children(if can_collapse {
-                    Some(
-                        Svg::new(if is_collapsed {
-                            "icons/chevron_right.svg"
-                        } else {
-                            "icons/chevron_down.svg"
-                        })
-                        .with_color(header_style.text.color)
-                        .constrained()
-                        .with_max_width(icon_size)
-                        .with_max_height(icon_size)
-                        .aligned()
-                        .constrained()
-                        .with_width(icon_size)
-                        .contained()
-                        .with_margin_right(
-                            theme.collab_panel.contact_username.container.margin.left,
-                        ),
-                    )
-                } else if let Some(channel_icon) = channel_icon {
-                    Some(
-                        Svg::new(channel_icon)
-                            .with_color(header_style.text.color)
-                            .constrained()
-                            .with_max_width(icon_size)
-                            .with_max_height(icon_size)
-                            .aligned()
-                            .constrained()
-                            .with_width(icon_size)
-                            .contained()
-                            .with_margin_right(
-                                theme.collab_panel.contact_username.container.margin.left,
-                            ),
-                    )
-                } else {
-                    None
-                })
-                .with_child(
-                    Label::new(text, header_style.text.clone())
-                        .aligned()
-                        .left()
-                        .flex(1., true),
-                )
-                .with_children(button.map(|button| button.aligned().right()))
-                .constrained()
-                .with_height(theme.collab_panel.row_height)
-                .contained()
-                .with_style(if is_dragged_over {
-                    theme.collab_panel.dragged_over_header
-                } else {
-                    header_style.container
-                })
-        });
-
-        result = result
-            .on_move(move |_, this, cx| {
-                if cx
-                    .global::<DragAndDrop<Workspace>>()
-                    .currently_dragged::<Channel>(cx.window())
-                    .is_some()
-                {
-                    this.drag_target_channel = ChannelDragTarget::Root;
-                    cx.notify()
-                }
-            })
-            .on_up(MouseButton::Left, move |_, this, cx| {
-                if let Some((_, dragged_channel)) = cx
-                    .global::<DragAndDrop<Workspace>>()
-                    .currently_dragged::<Channel>(cx.window())
-                {
-                    this.channel_store
-                        .update(cx, |channel_store, cx| {
-                            channel_store.move_channel(dragged_channel.id, None, cx)
-                        })
-                        .detach_and_log_err(cx)
-                }
-            });
-
-        if can_collapse {
-            result = result
-                .with_cursor_style(CursorStyle::PointingHand)
-                .on_click(MouseButton::Left, move |_, this, cx| {
-                    if can_collapse {
-                        this.toggle_section_expanded(section, cx);
-                    }
-                })
-        }
-
-        result.into_any()
-    }
-
-    fn render_contact(
-        contact: &Contact,
-        calling: bool,
-        project: &ModelHandle<Project>,
-        theme: &theme::Theme,
-        is_selected: bool,
-        cx: &mut ViewContext<Self>,
-    ) -> AnyElement<Self> {
-        enum ContactTooltip {}
-
-        let collab_theme = &theme.collab_panel;
-        let online = contact.online;
-        let busy = contact.busy || calling;
-        let user_id = contact.user.id;
-        let github_login = contact.user.github_login.clone();
-        let initial_project = project.clone();
-
-        let event_handler =
-            MouseEventHandler::new::<Contact, _>(contact.user.id as usize, cx, |state, cx| {
-                Flex::row()
-                    .with_children(contact.user.avatar.clone().map(|avatar| {
-                        let status_badge = if contact.online {
-                            Some(
-                                Empty::new()
-                                    .collapsed()
-                                    .contained()
-                                    .with_style(if busy {
-                                        collab_theme.contact_status_busy
-                                    } else {
-                                        collab_theme.contact_status_free
-                                    })
-                                    .aligned(),
-                            )
-                        } else {
-                            None
-                        };
-                        Stack::new()
-                            .with_child(
-                                Image::from_data(avatar)
-                                    .with_style(collab_theme.contact_avatar)
-                                    .aligned()
-                                    .left(),
-                            )
-                            .with_children(status_badge)
-                    }))
-                    .with_child(
-                        Label::new(
-                            contact.user.github_login.clone(),
-                            collab_theme.contact_username.text.clone(),
-                        )
-                        .contained()
-                        .with_style(collab_theme.contact_username.container)
-                        .aligned()
-                        .left()
-                        .flex(1., true),
-                    )
-                    .with_children(if state.hovered() {
-                        Some(
-                            MouseEventHandler::new::<Cancel, _>(
-                                contact.user.id as usize,
-                                cx,
-                                |mouse_state, _| {
-                                    let button_style =
-                                        collab_theme.contact_button.style_for(mouse_state);
-                                    render_icon_button(button_style, "icons/x.svg")
-                                        .aligned()
-                                        .flex_float()
-                                },
-                            )
-                            .with_padding(Padding::uniform(2.))
-                            .with_cursor_style(CursorStyle::PointingHand)
-                            .on_click(MouseButton::Left, move |_, this, cx| {
-                                this.remove_contact(user_id, &github_login, cx);
-                            })
-                            .flex_float(),
-                        )
-                    } else {
-                        None
-                    })
-                    .with_children(if calling {
-                        Some(
-                            Label::new("Calling", collab_theme.calling_indicator.text.clone())
-                                .contained()
-                                .with_style(collab_theme.calling_indicator.container)
-                                .aligned(),
-                        )
-                    } else {
-                        None
-                    })
-                    .constrained()
-                    .with_height(collab_theme.row_height)
-                    .contained()
-                    .with_style(
-                        *collab_theme
-                            .contact_row
-                            .in_state(is_selected)
-                            .style_for(state),
-                    )
-            });
-
-        if online && !busy {
-            let room = ActiveCall::global(cx).read(cx).room();
-            let label = if room.is_some() {
-                format!("Invite {} to join call", contact.user.github_login)
-            } else {
-                format!("Call {}", contact.user.github_login)
-            };
-
-            event_handler
-                .on_click(MouseButton::Left, move |_, this, cx| {
-                    this.call(user_id, Some(initial_project.clone()), cx);
-                })
-                .with_cursor_style(CursorStyle::PointingHand)
-                .with_tooltip::<ContactTooltip>(
-                    contact.user.id as usize,
-                    label,
-                    None,
-                    theme.tooltip.clone(),
-                    cx,
-                )
-                .into_any()
-        } else {
-            event_handler
-                .with_tooltip::<ContactTooltip>(
-                    contact.user.id as usize,
-                    format!(
-                        "{} is {}",
-                        contact.user.github_login,
-                        if busy { "on a call" } else { "offline" }
-                    ),
-                    None,
-                    theme.tooltip.clone(),
-                    cx,
-                )
-                .into_any()
-        }
-    }
-
-    fn render_contact_placeholder(
-        &self,
-        theme: &theme::CollabPanel,
-        is_selected: bool,
-        cx: &mut ViewContext<Self>,
-    ) -> AnyElement<Self> {
-        enum AddContacts {}
-        MouseEventHandler::new::<AddContacts, _>(0, cx, |state, _| {
-            let style = theme.list_empty_state.style_for(is_selected, state);
-            Flex::row()
-                .with_child(
-                    Svg::new("icons/plus.svg")
-                        .with_color(theme.list_empty_icon.color)
-                        .constrained()
-                        .with_width(theme.list_empty_icon.width)
-                        .aligned()
-                        .left(),
-                )
-                .with_child(
-                    Label::new("Add a contact", style.text.clone())
-                        .contained()
-                        .with_style(theme.list_empty_label_container),
-                )
-                .align_children_center()
-                .contained()
-                .with_style(style.container)
-                .into_any()
-        })
-        .on_click(MouseButton::Left, |_, this, cx| {
-            this.toggle_contact_finder(cx);
-        })
-        .into_any()
-    }
-
-    fn render_channel_editor(
-        &self,
-        theme: &theme::Theme,
-        depth: usize,
-        cx: &AppContext,
-    ) -> AnyElement<Self> {
-        Flex::row()
-            .with_child(
-                Empty::new()
-                    .constrained()
-                    .with_width(theme.collab_panel.disclosure.button_space()),
-            )
-            .with_child(
-                Svg::new("icons/hash.svg")
-                    .with_color(theme.collab_panel.channel_hash.color)
-                    .constrained()
-                    .with_width(theme.collab_panel.channel_hash.width)
-                    .aligned()
-                    .left(),
-            )
-            .with_child(
-                if let Some(pending_name) = self
-                    .channel_editing_state
-                    .as_ref()
-                    .and_then(|state| state.pending_name())
-                {
-                    Label::new(
-                        pending_name.to_string(),
-                        theme.collab_panel.contact_username.text.clone(),
-                    )
-                    .contained()
-                    .with_style(theme.collab_panel.contact_username.container)
-                    .aligned()
-                    .left()
-                    .flex(1., true)
-                    .into_any()
-                } else {
-                    ChildView::new(&self.channel_name_editor, cx)
-                        .aligned()
-                        .left()
-                        .contained()
-                        .with_style(theme.collab_panel.channel_editor)
-                        .flex(1.0, true)
-                        .into_any()
-                },
-            )
-            .align_children_center()
-            .constrained()
-            .with_height(theme.collab_panel.row_height)
-            .contained()
-            .with_style(ContainerStyle {
-                background_color: Some(theme.editor.background),
-                ..*theme.collab_panel.contact_row.default_style()
-            })
-            .with_padding_left(
-                theme.collab_panel.contact_row.default_style().padding.left
-                    + theme.collab_panel.channel_indent * depth as f32,
-            )
-            .into_any()
-    }
-
-    fn render_channel(
-        &self,
-        channel: &Channel,
-        depth: usize,
-        theme: &theme::Theme,
-        is_selected: bool,
-        has_children: bool,
-        ix: usize,
-        cx: &mut ViewContext<Self>,
-    ) -> AnyElement<Self> {
-        let channel_id = channel.id;
-        let collab_theme = &theme.collab_panel;
-        let is_public = self
-            .channel_store
-            .read(cx)
-            .channel_for_id(channel_id)
-            .map(|channel| channel.visibility)
-            == Some(proto::ChannelVisibility::Public);
-        let other_selected = self.selected_channel().map(|channel| channel.id) == Some(channel.id);
-        let disclosed =
-            has_children.then(|| !self.collapsed_channels.binary_search(&channel.id).is_ok());
-
-        let is_active = maybe!({
-            let call_channel = ActiveCall::global(cx)
-                .read(cx)
-                .room()?
-                .read(cx)
-                .channel_id()?;
-            Some(call_channel == channel_id)
-        })
-        .unwrap_or(false);
-
-        const FACEPILE_LIMIT: usize = 3;
-
-        enum ChannelCall {}
-        enum ChannelNote {}
-        enum NotesTooltip {}
-        enum ChatTooltip {}
-        enum ChannelTooltip {}
-
-        let mut is_dragged_over = false;
-        if cx
-            .global::<DragAndDrop<Workspace>>()
-            .currently_dragged::<Channel>(cx.window())
-            .is_some()
-            && self.drag_target_channel == ChannelDragTarget::Channel(channel_id)
-        {
-            is_dragged_over = true;
-        }
-
-        let has_messages_notification = channel.unseen_message_id.is_some();
-
-        MouseEventHandler::new::<Channel, _>(ix, cx, |state, cx| {
-            let row_hovered = state.hovered();
-
-            let mut select_state = |interactive: &Interactive<ContainerStyle>| {
-                if state.clicked() == Some(MouseButton::Left) && interactive.clicked.is_some() {
-                    interactive.clicked.as_ref().unwrap().clone()
-                } else if state.hovered() || other_selected {
-                    interactive
-                        .hovered
-                        .as_ref()
-                        .unwrap_or(&interactive.default)
-                        .clone()
-                } else {
-                    interactive.default.clone()
-                }
-            };
-
-            Flex::<Self>::row()
-                .with_child(
-                    Svg::new(if is_public {
-                        "icons/public.svg"
-                    } else {
-                        "icons/hash.svg"
-                    })
-                    .with_color(collab_theme.channel_hash.color)
-                    .constrained()
-                    .with_width(collab_theme.channel_hash.width)
-                    .aligned()
-                    .left(),
-                )
-                .with_child({
-                    let style = collab_theme.channel_name.inactive_state();
-                    Flex::row()
-                        .with_child(
-                            Label::new(channel.name.clone(), style.text.clone())
-                                .contained()
-                                .with_style(style.container)
-                                .aligned()
-                                .left()
-                                .with_tooltip::<ChannelTooltip>(
-                                    ix,
-                                    "Join channel",
-                                    None,
-                                    theme.tooltip.clone(),
-                                    cx,
-                                ),
-                        )
-                        .with_children({
-                            let participants =
-                                self.channel_store.read(cx).channel_participants(channel_id);
-
-                            if !participants.is_empty() {
-                                let extra_count = participants.len().saturating_sub(FACEPILE_LIMIT);
-
-                                let result = FacePile::new(collab_theme.face_overlap)
-                                    .with_children(
-                                        participants
-                                            .iter()
-                                            .filter_map(|user| {
-                                                Some(
-                                                    Image::from_data(user.avatar.clone()?)
-                                                        .with_style(collab_theme.channel_avatar),
-                                                )
-                                            })
-                                            .take(FACEPILE_LIMIT),
-                                    )
-                                    .with_children((extra_count > 0).then(|| {
-                                        Label::new(
-                                            format!("+{}", extra_count),
-                                            collab_theme.extra_participant_label.text.clone(),
-                                        )
-                                        .contained()
-                                        .with_style(collab_theme.extra_participant_label.container)
-                                    }));
-
-                                Some(result)
-                            } else {
-                                None
-                            }
-                        })
-                        .with_spacing(8.)
-                        .align_children_center()
-                        .flex(1., true)
-                })
-                .with_child(
-                    MouseEventHandler::new::<ChannelNote, _>(ix, cx, move |mouse_state, _| {
-                        let container_style = collab_theme
-                            .disclosure
-                            .button
-                            .style_for(mouse_state)
-                            .container;
-
-                        if channel.unseen_message_id.is_some() {
-                            Svg::new("icons/conversations.svg")
-                                .with_color(collab_theme.channel_note_active_color)
-                                .constrained()
-                                .with_width(collab_theme.channel_hash.width)
-                                .contained()
-                                .with_style(container_style)
-                                .with_uniform_padding(4.)
-                                .into_any()
-                        } else if row_hovered {
-                            Svg::new("icons/conversations.svg")
-                                .with_color(collab_theme.channel_hash.color)
-                                .constrained()
-                                .with_width(collab_theme.channel_hash.width)
-                                .contained()
-                                .with_style(container_style)
-                                .with_uniform_padding(4.)
-                                .into_any()
-                        } else {
-                            Empty::new().into_any()
-                        }
-                    })
-                    .on_click(MouseButton::Left, move |_, this, cx| {
-                        this.join_channel_chat(&JoinChannelChat { channel_id }, cx);
-                    })
-                    .with_tooltip::<ChatTooltip>(
-                        ix,
-                        "Open channel chat",
-                        None,
-                        theme.tooltip.clone(),
-                        cx,
-                    )
-                    .contained()
-                    .with_margin_right(4.),
-                )
-                .with_child(
-                    MouseEventHandler::new::<ChannelCall, _>(ix, cx, move |mouse_state, cx| {
-                        let container_style = collab_theme
-                            .disclosure
-                            .button
-                            .style_for(mouse_state)
-                            .container;
-                        if row_hovered || channel.unseen_note_version.is_some() {
-                            Svg::new("icons/file.svg")
-                                .with_color(if channel.unseen_note_version.is_some() {
-                                    collab_theme.channel_note_active_color
-                                } else {
-                                    collab_theme.channel_hash.color
-                                })
-                                .constrained()
-                                .with_width(collab_theme.channel_hash.width)
-                                .contained()
-                                .with_style(container_style)
-                                .with_uniform_padding(4.)
-                                .with_margin_right(collab_theme.channel_hash.container.margin.left)
-                                .with_tooltip::<NotesTooltip>(
-                                    ix as usize,
-                                    "Open channel notes",
-                                    None,
-                                    theme.tooltip.clone(),
-                                    cx,
-                                )
-                                .into_any()
-                        } else if has_messages_notification {
-                            Empty::new()
-                                .constrained()
-                                .with_width(collab_theme.channel_hash.width)
-                                .contained()
-                                .with_uniform_padding(4.)
-                                .with_margin_right(collab_theme.channel_hash.container.margin.left)
-                                .into_any()
-                        } else {
-                            Empty::new().into_any()
-                        }
-                    })
-                    .on_click(MouseButton::Left, move |_, this, cx| {
-                        this.open_channel_notes(&OpenChannelNotes { channel_id }, cx);
-                    }),
-                )
-                .align_children_center()
-                .styleable_component()
-                .disclosable(
-                    disclosed,
-                    Box::new(ToggleCollapse {
-                        location: channel.id.clone(),
-                    }),
-                )
-                .with_id(ix)
-                .with_style(collab_theme.disclosure.clone())
-                .element()
-                .constrained()
-                .with_height(collab_theme.row_height)
-                .contained()
-                .with_style(select_state(
-                    collab_theme
-                        .channel_row
-                        .in_state(is_selected || is_active || is_dragged_over),
-                ))
-                .with_padding_left(
-                    collab_theme.channel_row.default_style().padding.left
-                        + collab_theme.channel_indent * depth as f32,
-                )
-        })
-        .on_click(MouseButton::Left, move |_, this, cx| {
-            if this.drag_target_channel == ChannelDragTarget::None {
-                if is_active {
-                    this.open_channel_notes(&OpenChannelNotes { channel_id }, cx)
-                } else {
-                    this.join_channel(channel_id, cx)
-                }
-            }
-        })
-        .on_click(MouseButton::Right, {
-            let channel = channel.clone();
-            move |e, this, cx| {
-                this.deploy_channel_context_menu(Some(e.position), &channel, ix, cx);
-            }
-        })
-        .on_up(MouseButton::Left, move |_, this, cx| {
-            if let Some((_, dragged_channel)) = cx
-                .global::<DragAndDrop<Workspace>>()
-                .currently_dragged::<Channel>(cx.window())
-            {
-                this.channel_store
-                    .update(cx, |channel_store, cx| {
-                        channel_store.move_channel(dragged_channel.id, Some(channel_id), cx)
-                    })
-                    .detach_and_log_err(cx)
-            }
-        })
-        .on_move({
-            let channel = channel.clone();
-            move |_, this, cx| {
-                if let Some((_, dragged_channel)) = cx
-                    .global::<DragAndDrop<Workspace>>()
-                    .currently_dragged::<Channel>(cx.window())
-                {
-                    if channel.id != dragged_channel.id {
-                        this.drag_target_channel = ChannelDragTarget::Channel(channel.id);
-                    }
-                    cx.notify()
-                }
-            }
-        })
-        .as_draggable::<_, Channel>(
-            channel.clone(),
-            move |_, channel, cx: &mut ViewContext<Workspace>| {
-                let theme = &theme::current(cx).collab_panel;
-
-                Flex::<Workspace>::row()
-                    .with_child(
-                        Svg::new("icons/hash.svg")
-                            .with_color(theme.channel_hash.color)
-                            .constrained()
-                            .with_width(theme.channel_hash.width)
-                            .aligned()
-                            .left(),
-                    )
-                    .with_child(
-                        Label::new(channel.name.clone(), theme.channel_name.text.clone())
-                            .contained()
-                            .with_style(theme.channel_name.container)
-                            .aligned()
-                            .left(),
-                    )
-                    .align_children_center()
-                    .contained()
-                    .with_background_color(
-                        theme
-                            .container
-                            .background_color
-                            .unwrap_or(gpui::color::Color::transparent_black()),
-                    )
-                    .contained()
-                    .with_padding_left(
-                        theme.channel_row.default_style().padding.left
-                            + theme.channel_indent * depth as f32,
-                    )
-                    .into_any()
-            },
-        )
-        .with_cursor_style(CursorStyle::PointingHand)
-        .into_any()
-    }
-
-    fn render_channel_notes(
-        &self,
-        channel_id: ChannelId,
-        theme: &theme::CollabPanel,
-        is_selected: bool,
-        ix: usize,
-        cx: &mut ViewContext<Self>,
-    ) -> AnyElement<Self> {
-        enum ChannelNotes {}
-        let host_avatar_width = theme
-            .contact_avatar
-            .width
-            .or(theme.contact_avatar.height)
-            .unwrap_or(0.);
-
-        MouseEventHandler::new::<ChannelNotes, _>(ix as usize, cx, |state, cx| {
-            let tree_branch = *theme.tree_branch.in_state(is_selected).style_for(state);
-            let row = theme.project_row.in_state(is_selected).style_for(state);
-
-            Flex::<Self>::row()
-                .with_child(render_tree_branch(
-                    tree_branch,
-                    &row.name.text,
-                    false,
-                    vec2f(host_avatar_width, theme.row_height),
-                    cx.font_cache(),
-                ))
-                .with_child(
-                    Svg::new("icons/file.svg")
-                        .with_color(theme.channel_hash.color)
-                        .constrained()
-                        .with_width(theme.channel_hash.width)
-                        .aligned()
-                        .left(),
-                )
-                .with_child(
-                    Label::new("notes", theme.channel_name.text.clone())
-                        .contained()
-                        .with_style(theme.channel_name.container)
-                        .aligned()
-                        .left()
-                        .flex(1., true),
-                )
-                .constrained()
-                .with_height(theme.row_height)
-                .contained()
-                .with_style(*theme.channel_row.style_for(is_selected, state))
-                .with_padding_left(theme.channel_row.default_style().padding.left)
-        })
-        .on_click(MouseButton::Left, move |_, this, cx| {
-            this.open_channel_notes(&OpenChannelNotes { channel_id }, cx);
-        })
-        .with_cursor_style(CursorStyle::PointingHand)
-        .into_any()
-    }
-
-    fn render_channel_chat(
-        &self,
-        channel_id: ChannelId,
-        theme: &theme::CollabPanel,
-        is_selected: bool,
-        ix: usize,
-        cx: &mut ViewContext<Self>,
-    ) -> AnyElement<Self> {
-        enum ChannelChat {}
-        let host_avatar_width = theme
-            .contact_avatar
-            .width
-            .or(theme.contact_avatar.height)
-            .unwrap_or(0.);
-
-        MouseEventHandler::new::<ChannelChat, _>(ix as usize, cx, |state, cx| {
-            let tree_branch = *theme.tree_branch.in_state(is_selected).style_for(state);
-            let row = theme.project_row.in_state(is_selected).style_for(state);
-
-            Flex::<Self>::row()
-                .with_child(render_tree_branch(
-                    tree_branch,
-                    &row.name.text,
-                    true,
-                    vec2f(host_avatar_width, theme.row_height),
-                    cx.font_cache(),
-                ))
-                .with_child(
-                    Svg::new("icons/conversations.svg")
-                        .with_color(theme.channel_hash.color)
-                        .constrained()
-                        .with_width(theme.channel_hash.width)
-                        .aligned()
-                        .left(),
-                )
-                .with_child(
-                    Label::new("chat", theme.channel_name.text.clone())
-                        .contained()
-                        .with_style(theme.channel_name.container)
-                        .aligned()
-                        .left()
-                        .flex(1., true),
-                )
-                .constrained()
-                .with_height(theme.row_height)
-                .contained()
-                .with_style(*theme.channel_row.style_for(is_selected, state))
-                .with_padding_left(theme.channel_row.default_style().padding.left)
-        })
-        .on_click(MouseButton::Left, move |_, this, cx| {
-            this.join_channel_chat(&JoinChannelChat { channel_id }, cx);
-        })
-        .with_cursor_style(CursorStyle::PointingHand)
-        .into_any()
-    }
-
-    fn render_channel_invite(
-        channel: Arc<Channel>,
-        channel_store: ModelHandle<ChannelStore>,
-        theme: &theme::CollabPanel,
-        is_selected: bool,
-        cx: &mut ViewContext<Self>,
-    ) -> AnyElement<Self> {
-        enum Decline {}
-        enum Accept {}
-
-        let channel_id = channel.id;
-        let is_invite_pending = channel_store
-            .read(cx)
-            .has_pending_channel_invite_response(&channel);
-        let button_spacing = theme.contact_button_spacing;
-
-        Flex::row()
-            .with_child(
-                Svg::new("icons/hash.svg")
-                    .with_color(theme.channel_hash.color)
-                    .constrained()
-                    .with_width(theme.channel_hash.width)
-                    .aligned()
-                    .left(),
-            )
-            .with_child(
-                Label::new(channel.name.clone(), theme.contact_username.text.clone())
-                    .contained()
-                    .with_style(theme.contact_username.container)
-                    .aligned()
-                    .left()
-                    .flex(1., true),
-            )
-            .with_child(
-                MouseEventHandler::new::<Decline, _>(channel.id as usize, cx, |mouse_state, _| {
-                    let button_style = if is_invite_pending {
-                        &theme.disabled_button
-                    } else {
-                        theme.contact_button.style_for(mouse_state)
-                    };
-                    render_icon_button(button_style, "icons/x.svg").aligned()
-                })
-                .with_cursor_style(CursorStyle::PointingHand)
-                .on_click(MouseButton::Left, move |_, this, cx| {
-                    this.respond_to_channel_invite(channel_id, false, cx);
-                })
-                .contained()
-                .with_margin_right(button_spacing),
-            )
-            .with_child(
-                MouseEventHandler::new::<Accept, _>(channel.id as usize, cx, |mouse_state, _| {
-                    let button_style = if is_invite_pending {
-                        &theme.disabled_button
-                    } else {
-                        theme.contact_button.style_for(mouse_state)
-                    };
-                    render_icon_button(button_style, "icons/check.svg")
-                        .aligned()
-                        .flex_float()
-                })
-                .with_cursor_style(CursorStyle::PointingHand)
-                .on_click(MouseButton::Left, move |_, this, cx| {
-                    this.respond_to_channel_invite(channel_id, true, cx);
-                }),
-            )
-            .constrained()
-            .with_height(theme.row_height)
-            .contained()
-            .with_style(
-                *theme
-                    .contact_row
-                    .in_state(is_selected)
-                    .style_for(&mut Default::default()),
-            )
-            .with_padding_left(
-                theme.contact_row.default_style().padding.left + theme.channel_indent,
-            )
-            .into_any()
-    }
-
-    fn render_contact_request(
-        user: Arc<User>,
-        user_store: ModelHandle<UserStore>,
-        theme: &theme::CollabPanel,
-        is_incoming: bool,
-        is_selected: bool,
-        cx: &mut ViewContext<Self>,
-    ) -> AnyElement<Self> {
-        enum Decline {}
-        enum Accept {}
-        enum Cancel {}
-
-        let mut row = Flex::row()
-            .with_children(user.avatar.clone().map(|avatar| {
-                Image::from_data(avatar)
-                    .with_style(theme.contact_avatar)
-                    .aligned()
-                    .left()
-            }))
-            .with_child(
-                Label::new(
-                    user.github_login.clone(),
-                    theme.contact_username.text.clone(),
-                )
-                .contained()
-                .with_style(theme.contact_username.container)
-                .aligned()
-                .left()
-                .flex(1., true),
-            );
-
-        let user_id = user.id;
-        let github_login = user.github_login.clone();
-        let is_contact_request_pending = user_store.read(cx).is_contact_request_pending(&user);
-        let button_spacing = theme.contact_button_spacing;
-
-        if is_incoming {
-            row.add_child(
-                MouseEventHandler::new::<Decline, _>(user.id as usize, cx, |mouse_state, _| {
-                    let button_style = if is_contact_request_pending {
-                        &theme.disabled_button
-                    } else {
-                        theme.contact_button.style_for(mouse_state)
-                    };
-                    render_icon_button(button_style, "icons/x.svg").aligned()
-                })
-                .with_cursor_style(CursorStyle::PointingHand)
-                .on_click(MouseButton::Left, move |_, this, cx| {
-                    this.respond_to_contact_request(user_id, false, cx);
-                })
-                .contained()
-                .with_margin_right(button_spacing),
-            );
-
-            row.add_child(
-                MouseEventHandler::new::<Accept, _>(user.id as usize, cx, |mouse_state, _| {
-                    let button_style = if is_contact_request_pending {
-                        &theme.disabled_button
-                    } else {
-                        theme.contact_button.style_for(mouse_state)
-                    };
-                    render_icon_button(button_style, "icons/check.svg")
-                        .aligned()
-                        .flex_float()
-                })
-                .with_cursor_style(CursorStyle::PointingHand)
-                .on_click(MouseButton::Left, move |_, this, cx| {
-                    this.respond_to_contact_request(user_id, true, cx);
-                }),
-            );
-        } else {
-            row.add_child(
-                MouseEventHandler::new::<Cancel, _>(user.id as usize, cx, |mouse_state, _| {
-                    let button_style = if is_contact_request_pending {
-                        &theme.disabled_button
-                    } else {
-                        theme.contact_button.style_for(mouse_state)
-                    };
-                    render_icon_button(button_style, "icons/x.svg")
-                        .aligned()
-                        .flex_float()
-                })
-                .with_padding(Padding::uniform(2.))
-                .with_cursor_style(CursorStyle::PointingHand)
-                .on_click(MouseButton::Left, move |_, this, cx| {
-                    this.remove_contact(user_id, &github_login, cx);
-                })
-                .flex_float(),
-            );
-        }
-
-        row.constrained()
-            .with_height(theme.row_height)
-            .contained()
-            .with_style(
-                *theme
-                    .contact_row
-                    .in_state(is_selected)
-                    .style_for(&mut Default::default()),
-            )
-            .into_any()
-    }
-
-    fn has_subchannels(&self, ix: usize) -> bool {
-        self.entries.get(ix).map_or(false, |entry| {
-            if let ListEntry::Channel { has_children, .. } = entry {
-                *has_children
-            } else {
-                false
-            }
-        })
-    }
-
-    fn deploy_channel_context_menu(
-        &mut self,
-        position: Option<Vector2F>,
-        channel: &Channel,
-        ix: usize,
-        cx: &mut ViewContext<Self>,
-    ) {
-        self.context_menu_on_selected = position.is_none();
-
-        let clipboard_channel_name = self.channel_clipboard.as_ref().and_then(|clipboard| {
-            self.channel_store
-                .read(cx)
-                .channel_for_id(clipboard.channel_id)
-                .map(|channel| channel.name.clone())
-        });
-
-        self.context_menu.update(cx, |context_menu, cx| {
-            context_menu.set_position_mode(if self.context_menu_on_selected {
-                OverlayPositionMode::Local
-            } else {
-                OverlayPositionMode::Window
-            });
-
-            let mut items = Vec::new();
-
-            let select_action_name = if self.selection == Some(ix) {
-                "Unselect"
-            } else {
-                "Select"
-            };
-
-            items.push(ContextMenuItem::action(
-                select_action_name,
-                ToggleSelectedIx { ix },
-            ));
-
-            if self.has_subchannels(ix) {
-                let expand_action_name = if self.is_channel_collapsed(channel.id) {
-                    "Expand Subchannels"
-                } else {
-                    "Collapse Subchannels"
-                };
-                items.push(ContextMenuItem::action(
-                    expand_action_name,
-                    ToggleCollapse {
-                        location: channel.id,
-                    },
-                ));
-            }
-
-            items.push(ContextMenuItem::action(
-                "Open Notes",
-                OpenChannelNotes {
-                    channel_id: channel.id,
-                },
-            ));
-
-            items.push(ContextMenuItem::action(
-                "Open Chat",
-                JoinChannelChat {
-                    channel_id: channel.id,
-                },
-            ));
-
-            items.push(ContextMenuItem::action(
-                "Copy Channel Link",
-                CopyChannelLink {
-                    channel_id: channel.id,
-                },
-            ));
-
-            if self.channel_store.read(cx).is_channel_admin(channel.id) {
-                items.extend([
-                    ContextMenuItem::Separator,
-                    ContextMenuItem::action(
-                        "New Subchannel",
-                        NewChannel {
-                            location: channel.id,
-                        },
-                    ),
-                    ContextMenuItem::action(
-                        "Rename",
-                        RenameChannel {
-                            channel_id: channel.id,
-                        },
-                    ),
-                    ContextMenuItem::action(
-                        "Move this channel",
-                        StartMoveChannelFor {
-                            channel_id: channel.id,
-                        },
-                    ),
-                ]);
-
-                if let Some(channel_name) = clipboard_channel_name {
-                    items.push(ContextMenuItem::Separator);
-                    items.push(ContextMenuItem::action(
-                        format!("Move '#{}' here", channel_name),
-                        MoveChannel { to: channel.id },
-                    ));
-                }
-
-                items.extend([
-                    ContextMenuItem::Separator,
-                    ContextMenuItem::action(
-                        "Invite Members",
-                        InviteMembers {
-                            channel_id: channel.id,
-                        },
-                    ),
-                    ContextMenuItem::action(
-                        "Manage Members",
-                        ManageMembers {
-                            channel_id: channel.id,
-                        },
-                    ),
-                    ContextMenuItem::Separator,
-                    ContextMenuItem::action(
-                        "Delete",
-                        RemoveChannel {
-                            channel_id: channel.id,
-                        },
-                    ),
-                ]);
-            }
-
-            context_menu.show(
-                position.unwrap_or_default(),
-                if self.context_menu_on_selected {
-                    gpui::elements::AnchorCorner::TopRight
-                } else {
-                    gpui::elements::AnchorCorner::BottomLeft
-                },
-                items,
-                cx,
-            );
-        });
-
-        cx.notify();
-    }
-
-    fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
-        if self.take_editing_state(cx) {
-            cx.focus(&self.filter_editor);
-        } else {
-            self.filter_editor.update(cx, |editor, cx| {
-                if editor.buffer().read(cx).len(cx) > 0 {
-                    editor.set_text("", cx);
-                }
-            });
-        }
-
-        self.update_entries(false, cx);
-    }
-
-    fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
-        let ix = self.selection.map_or(0, |ix| ix + 1);
-        if ix < self.entries.len() {
-            self.selection = Some(ix);
-        }
-
-        self.list_state.reset(self.entries.len());
-        if let Some(ix) = self.selection {
-            self.list_state.scroll_to(ListOffset {
-                item_ix: ix,
-                offset_in_item: 0.,
-            });
-        }
-        cx.notify();
-    }
-
-    fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
-        let ix = self.selection.take().unwrap_or(0);
-        if ix > 0 {
-            self.selection = Some(ix - 1);
-        }
-
-        self.list_state.reset(self.entries.len());
-        if let Some(ix) = self.selection {
-            self.list_state.scroll_to(ListOffset {
-                item_ix: ix,
-                offset_in_item: 0.,
-            });
-        }
-        cx.notify();
-    }
-
-    fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
-        if self.confirm_channel_edit(cx) {
-            return;
-        }
-
-        if let Some(selection) = self.selection {
-            if let Some(entry) = self.entries.get(selection) {
-                match entry {
-                    ListEntry::Header(section) => match section {
-                        Section::ActiveCall => Self::leave_call(cx),
-                        Section::Channels => self.new_root_channel(cx),
-                        Section::Contacts => self.toggle_contact_finder(cx),
-                        Section::ContactRequests
-                        | Section::Online
-                        | Section::Offline
-                        | Section::ChannelInvites => {
-                            self.toggle_section_expanded(*section, cx);
-                        }
-                    },
-                    ListEntry::Contact { contact, calling } => {
-                        if contact.online && !contact.busy && !calling {
-                            self.call(contact.user.id, Some(self.project.clone()), cx);
-                        }
-                    }
-                    ListEntry::ParticipantProject {
-                        project_id,
-                        host_user_id,
-                        ..
-                    } => {
-                        if let Some(workspace) = self.workspace.upgrade(cx) {
-                            let app_state = workspace.read(cx).app_state().clone();
-                            workspace::join_remote_project(
-                                *project_id,
-                                *host_user_id,
-                                app_state,
-                                cx,
-                            )
-                            .detach_and_log_err(cx);
-                        }
-                    }
-                    ListEntry::ParticipantScreen { peer_id, .. } => {
-                        let Some(peer_id) = peer_id else {
-                            return;
-                        };
-                        if let Some(workspace) = self.workspace.upgrade(cx) {
-                            workspace.update(cx, |workspace, cx| {
-                                workspace.open_shared_screen(*peer_id, cx)
-                            });
-                        }
-                    }
-                    ListEntry::Channel { channel, .. } => {
-                        let is_active = maybe!({
-                            let call_channel = ActiveCall::global(cx)
-                                .read(cx)
-                                .room()?
-                                .read(cx)
-                                .channel_id()?;
-
-                            Some(call_channel == channel.id)
-                        })
-                        .unwrap_or(false);
-                        if is_active {
-                            self.open_channel_notes(
-                                &OpenChannelNotes {
-                                    channel_id: channel.id,
-                                },
-                                cx,
-                            )
-                        } else {
-                            self.join_channel(channel.id, cx)
-                        }
-                    }
-                    ListEntry::ContactPlaceholder => self.toggle_contact_finder(cx),
-                    _ => {}
-                }
-            }
-        }
-    }
-
-    fn insert_space(&mut self, _: &InsertSpace, cx: &mut ViewContext<Self>) {
-        if self.channel_editing_state.is_some() {
-            self.channel_name_editor.update(cx, |editor, cx| {
-                editor.insert(" ", cx);
-            });
-        }
-    }
-
-    fn confirm_channel_edit(&mut self, cx: &mut ViewContext<CollabPanel>) -> bool {
-        if let Some(editing_state) = &mut self.channel_editing_state {
-            match editing_state {
-                ChannelEditingState::Create {
-                    location,
-                    pending_name,
-                    ..
-                } => {
-                    if pending_name.is_some() {
-                        return false;
-                    }
-                    let channel_name = self.channel_name_editor.read(cx).text(cx);
-
-                    *pending_name = Some(channel_name.clone());
-
-                    self.channel_store
-                        .update(cx, |channel_store, cx| {
-                            channel_store.create_channel(&channel_name, *location, cx)
-                        })
-                        .detach();
-                    cx.notify();
-                }
-                ChannelEditingState::Rename {
-                    location,
-                    pending_name,
-                } => {
-                    if pending_name.is_some() {
-                        return false;
-                    }
-                    let channel_name = self.channel_name_editor.read(cx).text(cx);
-                    *pending_name = Some(channel_name.clone());
-
-                    self.channel_store
-                        .update(cx, |channel_store, cx| {
-                            channel_store.rename(*location, &channel_name, cx)
-                        })
-                        .detach();
-                    cx.notify();
-                }
-            }
-            cx.focus_self();
-            true
-        } else {
-            false
-        }
-    }
-
-    fn toggle_section_expanded(&mut self, section: Section, cx: &mut ViewContext<Self>) {
-        if let Some(ix) = self.collapsed_sections.iter().position(|s| *s == section) {
-            self.collapsed_sections.remove(ix);
-        } else {
-            self.collapsed_sections.push(section);
-        }
-        self.update_entries(false, cx);
-    }
-
-    fn collapse_selected_channel(
-        &mut self,
-        _: &CollapseSelectedChannel,
-        cx: &mut ViewContext<Self>,
-    ) {
-        let Some(channel_id) = self.selected_channel().map(|channel| channel.id) else {
-            return;
-        };
-
-        if self.is_channel_collapsed(channel_id) {
-            return;
-        }
-
-        self.toggle_channel_collapsed(channel_id, cx);
-    }
-
-    fn expand_selected_channel(&mut self, _: &ExpandSelectedChannel, cx: &mut ViewContext<Self>) {
-        let Some(id) = self.selected_channel().map(|channel| channel.id) else {
-            return;
-        };
-
-        if !self.is_channel_collapsed(id) {
-            return;
-        }
-
-        self.toggle_channel_collapsed(id, cx)
-    }
-
-    fn toggle_channel_collapsed_action(
-        &mut self,
-        action: &ToggleCollapse,
-        cx: &mut ViewContext<Self>,
-    ) {
-        self.toggle_channel_collapsed(action.location, cx);
-    }
-
-    fn toggle_channel_collapsed<'a>(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
-        match self.collapsed_channels.binary_search(&channel_id) {
-            Ok(ix) => {
-                self.collapsed_channels.remove(ix);
-            }
-            Err(ix) => {
-                self.collapsed_channels.insert(ix, channel_id);
-            }
-        };
-        self.serialize(cx);
-        self.update_entries(true, cx);
-        cx.notify();
-        cx.focus_self();
-    }
-
-    fn is_channel_collapsed(&self, channel_id: ChannelId) -> bool {
-        self.collapsed_channels.binary_search(&channel_id).is_ok()
-    }
-
-    fn leave_call(cx: &mut ViewContext<Self>) {
-        ActiveCall::global(cx)
-            .update(cx, |call, cx| call.hang_up(cx))
-            .detach_and_log_err(cx);
-    }
-
-    fn toggle_contact_finder(&mut self, cx: &mut ViewContext<Self>) {
-        if let Some(workspace) = self.workspace.upgrade(cx) {
-            workspace.update(cx, |workspace, cx| {
-                workspace.toggle_modal(cx, |_, cx| {
-                    cx.add_view(|cx| {
-                        let mut finder = ContactFinder::new(self.user_store.clone(), cx);
-                        finder.set_query(self.filter_editor.read(cx).text(cx), cx);
-                        finder
-                    })
-                });
-            });
-        }
-    }
-
-    fn new_root_channel(&mut self, cx: &mut ViewContext<Self>) {
-        self.channel_editing_state = Some(ChannelEditingState::Create {
-            location: None,
-            pending_name: None,
-        });
-        self.update_entries(false, cx);
-        self.select_channel_editor();
-        cx.focus(self.channel_name_editor.as_any());
-        cx.notify();
-    }
-
-    fn select_channel_editor(&mut self) {
-        self.selection = self.entries.iter().position(|entry| match entry {
-            ListEntry::ChannelEditor { .. } => true,
-            _ => false,
-        });
-    }
-
-    fn new_subchannel(&mut self, action: &NewChannel, cx: &mut ViewContext<Self>) {
-        self.collapsed_channels
-            .retain(|channel| *channel != action.location);
-        self.channel_editing_state = Some(ChannelEditingState::Create {
-            location: Some(action.location.to_owned()),
-            pending_name: None,
-        });
-        self.update_entries(false, cx);
-        self.select_channel_editor();
-        cx.focus(self.channel_name_editor.as_any());
-        cx.notify();
-    }
-
-    fn invite_members(&mut self, action: &InviteMembers, cx: &mut ViewContext<Self>) {
-        self.show_channel_modal(action.channel_id, channel_modal::Mode::InviteMembers, cx);
-    }
-
-    fn manage_members(&mut self, action: &ManageMembers, cx: &mut ViewContext<Self>) {
-        self.show_channel_modal(action.channel_id, channel_modal::Mode::ManageMembers, cx);
-    }
-
-    fn remove(&mut self, _: &Remove, cx: &mut ViewContext<Self>) {
-        if let Some(channel) = self.selected_channel() {
-            self.remove_channel(channel.id, cx)
-        }
-    }
-
-    fn rename_selected_channel(&mut self, _: &menu::SecondaryConfirm, cx: &mut ViewContext<Self>) {
-        if let Some(channel) = self.selected_channel() {
-            self.rename_channel(
-                &RenameChannel {
-                    channel_id: channel.id,
-                },
-                cx,
-            );
-        }
-    }
-
-    fn rename_channel(&mut self, action: &RenameChannel, cx: &mut ViewContext<Self>) {
-        let channel_store = self.channel_store.read(cx);
-        if !channel_store.is_channel_admin(action.channel_id) {
-            return;
-        }
-        if let Some(channel) = channel_store.channel_for_id(action.channel_id).cloned() {
-            self.channel_editing_state = Some(ChannelEditingState::Rename {
-                location: action.channel_id.to_owned(),
-                pending_name: None,
-            });
-            self.channel_name_editor.update(cx, |editor, cx| {
-                editor.set_text(channel.name.clone(), cx);
-                editor.select_all(&Default::default(), cx);
-            });
-            cx.focus(self.channel_name_editor.as_any());
-            self.update_entries(false, cx);
-            self.select_channel_editor();
-        }
-    }
-
-    fn open_channel_notes(&mut self, action: &OpenChannelNotes, cx: &mut ViewContext<Self>) {
-        if let Some(workspace) = self.workspace.upgrade(cx) {
-            ChannelView::open(action.channel_id, workspace, cx).detach();
-        }
-    }
-
-    fn show_inline_context_menu(&mut self, _: &menu::ShowContextMenu, cx: &mut ViewContext<Self>) {
-        let Some(channel) = self.selected_channel() else {
-            return;
-        };
-
-        self.deploy_channel_context_menu(None, &channel.clone(), self.selection.unwrap(), cx);
-    }
-
-    fn selected_channel(&self) -> Option<&Arc<Channel>> {
-        self.selection
-            .and_then(|ix| self.entries.get(ix))
-            .and_then(|entry| match entry {
-                ListEntry::Channel { channel, .. } => Some(channel),
-                _ => None,
-            })
-    }
-
-    fn show_channel_modal(
-        &mut self,
-        channel_id: ChannelId,
-        mode: channel_modal::Mode,
-        cx: &mut ViewContext<Self>,
-    ) {
-        let workspace = self.workspace.clone();
-        let user_store = self.user_store.clone();
-        let channel_store = self.channel_store.clone();
-        let members = self.channel_store.update(cx, |channel_store, cx| {
-            channel_store.get_channel_member_details(channel_id, cx)
-        });
-
-        cx.spawn(|_, mut cx| async move {
-            let members = members.await?;
-            workspace.update(&mut cx, |workspace, cx| {
-                workspace.toggle_modal(cx, |_, cx| {
-                    cx.add_view(|cx| {
-                        ChannelModal::new(
-                            user_store.clone(),
-                            channel_store.clone(),
-                            channel_id,
-                            mode,
-                            members,
-                            cx,
-                        )
-                    })
-                });
-            })
-        })
-        .detach();
-    }
-
-    fn remove_selected_channel(&mut self, action: &RemoveChannel, cx: &mut ViewContext<Self>) {
-        self.remove_channel(action.channel_id, cx)
-    }
-
-    fn remove_channel(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
-        let channel_store = self.channel_store.clone();
-        if let Some(channel) = channel_store.read(cx).channel_for_id(channel_id) {
-            let prompt_message = format!(
-                "Are you sure you want to remove the channel \"{}\"?",
-                channel.name
-            );
-            let mut answer =
-                cx.prompt(PromptLevel::Warning, &prompt_message, &["Remove", "Cancel"]);
-            let window = cx.window();
-            cx.spawn(|this, mut cx| async move {
-                if answer.next().await == Some(0) {
-                    if let Err(e) = channel_store
-                        .update(&mut cx, |channels, _| channels.remove_channel(channel_id))
-                        .await
-                    {
-                        window.prompt(
-                            PromptLevel::Info,
-                            &format!("Failed to remove channel: {}", e),
-                            &["Ok"],
-                            &mut cx,
-                        );
-                    }
-                    this.update(&mut cx, |_, cx| cx.focus_self()).ok();
-                }
-            })
-            .detach();
-        }
-    }
-
-    // Should move to the filter editor if clicking on it
-    // Should move selection to the channel editor if activating it
-
-    fn remove_contact(&mut self, user_id: u64, github_login: &str, cx: &mut ViewContext<Self>) {
-        let user_store = self.user_store.clone();
-        let prompt_message = format!(
-            "Are you sure you want to remove \"{}\" from your contacts?",
-            github_login
-        );
-        let mut answer = cx.prompt(PromptLevel::Warning, &prompt_message, &["Remove", "Cancel"]);
-        let window = cx.window();
-        cx.spawn(|_, mut cx| async move {
-            if answer.next().await == Some(0) {
-                if let Err(e) = user_store
-                    .update(&mut cx, |store, cx| store.remove_contact(user_id, cx))
-                    .await
-                {
-                    window.prompt(
-                        PromptLevel::Info,
-                        &format!("Failed to remove contact: {}", e),
-                        &["Ok"],
-                        &mut cx,
-                    );
-                }
-            }
-        })
-        .detach();
-    }
-
-    fn respond_to_contact_request(
-        &mut self,
-        user_id: u64,
-        accept: bool,
-        cx: &mut ViewContext<Self>,
-    ) {
-        self.user_store
-            .update(cx, |store, cx| {
-                store.respond_to_contact_request(user_id, accept, cx)
-            })
-            .detach();
-    }
-
-    fn respond_to_channel_invite(
-        &mut self,
-        channel_id: u64,
-        accept: bool,
-        cx: &mut ViewContext<Self>,
-    ) {
-        self.channel_store
-            .update(cx, |store, cx| {
-                store.respond_to_channel_invite(channel_id, accept, cx)
-            })
-            .detach();
-    }
-
-    fn call(
-        &mut self,
-        recipient_user_id: u64,
-        initial_project: Option<ModelHandle<Project>>,
-        cx: &mut ViewContext<Self>,
-    ) {
-        ActiveCall::global(cx)
-            .update(cx, |call, cx| {
-                call.invite(recipient_user_id, initial_project, cx)
-            })
-            .detach_and_log_err(cx);
-    }
-
-    fn join_channel(&self, channel_id: u64, cx: &mut ViewContext<Self>) {
-        let Some(workspace) = self.workspace.upgrade(cx) else {
-            return;
-        };
-        let Some(handle) = cx.window().downcast::<Workspace>() else {
-            return;
-        };
-        workspace::join_channel(
-            channel_id,
-            workspace.read(cx).app_state().clone(),
-            Some(handle),
-            cx,
-        )
-        .detach_and_log_err(cx)
-    }
-
-    fn join_channel_chat(&mut self, action: &JoinChannelChat, cx: &mut ViewContext<Self>) {
-        let channel_id = action.channel_id;
-        if let Some(workspace) = self.workspace.upgrade(cx) {
-            cx.app_context().defer(move |cx| {
-                workspace.update(cx, |workspace, cx| {
-                    if let Some(panel) = workspace.focus_panel::<ChatPanel>(cx) {
-                        panel.update(cx, |panel, cx| {
-                            panel
-                                .select_channel(channel_id, None, cx)
-                                .detach_and_log_err(cx);
-                        });
-                    }
-                });
-            });
-        }
-    }
-
-    fn copy_channel_link(&mut self, action: &CopyChannelLink, cx: &mut ViewContext<Self>) {
-        let channel_store = self.channel_store.read(cx);
-        let Some(channel) = channel_store.channel_for_id(action.channel_id) else {
-            return;
-        };
-        let item = ClipboardItem::new(channel.link());
-        cx.write_to_clipboard(item)
-    }
-}
-
-fn render_tree_branch(
-    branch_style: theme::TreeBranch,
-    row_style: &TextStyle,
-    is_last: bool,
-    size: Vector2F,
-    font_cache: &FontCache,
-) -> gpui::elements::ConstrainedBox<CollabPanel> {
-    let line_height = row_style.line_height(font_cache);
-    let cap_height = row_style.cap_height(font_cache);
-    let baseline_offset = row_style.baseline_offset(font_cache) + (size.y() - line_height) / 2.;
-
-    Canvas::new(move |bounds, _, _, cx| {
-        cx.paint_layer(None, |cx| {
-            let start_x = bounds.min_x() + (bounds.width() / 2.) - (branch_style.width / 2.);
-            let end_x = bounds.max_x();
-            let start_y = bounds.min_y();
-            let end_y = bounds.min_y() + baseline_offset - (cap_height / 2.);
-
-            cx.scene().push_quad(gpui::Quad {
-                bounds: RectF::from_points(
-                    vec2f(start_x, start_y),
-                    vec2f(
-                        start_x + branch_style.width,
-                        if is_last { end_y } else { bounds.max_y() },
-                    ),
-                ),
-                background: Some(branch_style.color),
-                border: gpui::Border::default(),
-                corner_radii: (0.).into(),
-            });
-            cx.scene().push_quad(gpui::Quad {
-                bounds: RectF::from_points(
-                    vec2f(start_x, end_y),
-                    vec2f(end_x, end_y + branch_style.width),
-                ),
-                background: Some(branch_style.color),
-                border: gpui::Border::default(),
-                corner_radii: (0.).into(),
-            });
-        })
-    })
-    .constrained()
-    .with_width(size.x())
-}
-
-impl View for CollabPanel {
-    fn ui_name() -> &'static str {
-        "CollabPanel"
-    }
-
-    fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
-        if !self.has_focus {
-            self.has_focus = true;
-            if !self.context_menu.is_focused(cx) {
-                if let Some(editing_state) = &self.channel_editing_state {
-                    if editing_state.pending_name().is_none() {
-                        cx.focus(&self.channel_name_editor);
-                    } else {
-                        cx.focus(&self.filter_editor);
-                    }
-                } else {
-                    cx.focus(&self.filter_editor);
-                }
-            }
-            cx.emit(Event::Focus);
-        }
-    }
-
-    fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext<Self>) {
-        self.has_focus = false;
-    }
-
-    fn render(&mut self, cx: &mut gpui::ViewContext<'_, '_, Self>) -> gpui::AnyElement<Self> {
-        let theme = &theme::current(cx).collab_panel;
-
-        if self.user_store.read(cx).current_user().is_none() {
-            enum LogInButton {}
-
-            return Flex::column()
-                .with_child(
-                    MouseEventHandler::new::<LogInButton, _>(0, cx, |state, _| {
-                        let button = theme.log_in_button.style_for(state);
-                        Label::new("Sign in to collaborate", button.text.clone())
-                            .aligned()
-                            .left()
-                            .contained()
-                            .with_style(button.container)
-                    })
-                    .on_click(MouseButton::Left, |_, this, cx| {
-                        let client = this.client.clone();
-                        cx.spawn(|_, cx| async move {
-                            client.authenticate_and_connect(true, &cx).await.log_err();
-                        })
-                        .detach();
-                    })
-                    .with_cursor_style(CursorStyle::PointingHand),
-                )
-                .contained()
-                .with_style(theme.container)
-                .into_any();
-        }
-
-        enum PanelFocus {}
-        MouseEventHandler::new::<PanelFocus, _>(0, cx, |_, cx| {
-            Stack::new()
-                .with_child(
-                    Flex::column()
-                        .with_child(
-                            Flex::row().with_child(
-                                ChildView::new(&self.filter_editor, cx)
-                                    .contained()
-                                    .with_style(theme.user_query_editor.container)
-                                    .flex(1.0, true),
-                            ),
-                        )
-                        .with_child(List::new(self.list_state.clone()).flex(1., true).into_any())
-                        .contained()
-                        .with_style(theme.container)
-                        .into_any(),
-                )
-                .with_children(
-                    (!self.context_menu_on_selected)
-                        .then(|| ChildView::new(&self.context_menu, cx)),
-                )
-                .into_any()
-        })
-        .on_click(MouseButton::Left, |_, _, cx| cx.focus_self())
-        .into_any_named("collab panel")
-    }
-
-    fn update_keymap_context(
-        &self,
-        keymap: &mut gpui::keymap_matcher::KeymapContext,
-        _: &AppContext,
-    ) {
-        Self::reset_to_default_keymap_context(keymap);
-        if self.channel_editing_state.is_some() {
-            keymap.add_identifier("editing");
-        } else {
-            keymap.add_identifier("not_editing");
-        }
-    }
-}
-
-impl Panel for CollabPanel {
-    fn position(&self, cx: &gpui::WindowContext) -> DockPosition {
-        settings::get::<CollaborationPanelSettings>(cx).dock
-    }
-
-    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| settings.dock = Some(position),
-        );
-    }
-
-    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/user_group_16.svg")
-    }
-
-    fn icon_tooltip(&self) -> (String, Option<Box<dyn gpui::Action>>) {
-        (
-            "Collaboration 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)
-    }
-}
-
-impl PartialEq for ListEntry {
-    fn eq(&self, other: &Self) -> bool {
-        match self {
-            ListEntry::Header(section_1) => {
-                if let ListEntry::Header(section_2) = other {
-                    return section_1 == section_2;
-                }
-            }
-            ListEntry::CallParticipant { user: user_1, .. } => {
-                if let ListEntry::CallParticipant { user: user_2, .. } = other {
-                    return user_1.id == user_2.id;
-                }
-            }
-            ListEntry::ParticipantProject {
-                project_id: project_id_1,
-                ..
-            } => {
-                if let ListEntry::ParticipantProject {
-                    project_id: project_id_2,
-                    ..
-                } = other
-                {
-                    return project_id_1 == project_id_2;
-                }
-            }
-            ListEntry::ParticipantScreen {
-                peer_id: peer_id_1, ..
-            } => {
-                if let ListEntry::ParticipantScreen {
-                    peer_id: peer_id_2, ..
-                } = other
-                {
-                    return peer_id_1 == peer_id_2;
-                }
-            }
-            ListEntry::Channel {
-                channel: channel_1, ..
-            } => {
-                if let ListEntry::Channel {
-                    channel: channel_2, ..
-                } = other
-                {
-                    return channel_1.id == channel_2.id;
-                }
-            }
-            ListEntry::ChannelNotes { channel_id } => {
-                if let ListEntry::ChannelNotes {
-                    channel_id: other_id,
-                } = other
-                {
-                    return channel_id == other_id;
-                }
-            }
-            ListEntry::ChannelChat { channel_id } => {
-                if let ListEntry::ChannelChat {
-                    channel_id: other_id,
-                } = other
-                {
-                    return channel_id == other_id;
-                }
-            }
-            ListEntry::ChannelInvite(channel_1) => {
-                if let ListEntry::ChannelInvite(channel_2) = other {
-                    return channel_1.id == channel_2.id;
-                }
-            }
-            ListEntry::IncomingRequest(user_1) => {
-                if let ListEntry::IncomingRequest(user_2) = other {
-                    return user_1.id == user_2.id;
-                }
-            }
-            ListEntry::OutgoingRequest(user_1) => {
-                if let ListEntry::OutgoingRequest(user_2) = other {
-                    return user_1.id == user_2.id;
-                }
-            }
-            ListEntry::Contact {
-                contact: contact_1, ..
-            } => {
-                if let ListEntry::Contact {
-                    contact: contact_2, ..
-                } = other
-                {
-                    return contact_1.user.id == contact_2.user.id;
-                }
-            }
-            ListEntry::ChannelEditor { depth } => {
-                if let ListEntry::ChannelEditor { depth: other_depth } = other {
-                    return depth == other_depth;
-                }
-            }
-            ListEntry::ContactPlaceholder => {
-                if let ListEntry::ContactPlaceholder = other {
-                    return true;
-                }
-            }
-        }
-        false
-    }
-}
-
-fn render_icon_button(style: &IconButton, svg_path: &'static str) -> impl Element<CollabPanel> {
-    Svg::new(svg_path)
-        .with_color(style.color)
-        .constrained()
-        .with_width(style.icon_width)
-        .aligned()
-        .constrained()
-        .with_width(style.button_width)
-        .with_height(style.button_width)
-        .contained()
-        .with_style(style.container)
-}
+// mod channel_modal;
+// mod contact_finder;
+
+// use crate::{
+//     channel_view::{self, ChannelView},
+//     chat_panel::ChatPanel,
+//     face_pile::FacePile,
+//     panel_settings, CollaborationPanelSettings,
+// };
+// use anyhow::Result;
+// use call::ActiveCall;
+// use channel::{Channel, ChannelEvent, ChannelId, ChannelStore};
+// use channel_modal::ChannelModal;
+// use client::{
+//     proto::{self, PeerId},
+//     Client, Contact, User, UserStore,
+// };
+// use contact_finder::ContactFinder;
+// use context_menu::{ContextMenu, ContextMenuItem};
+// use db::kvp::KEY_VALUE_STORE;
+// use drag_and_drop::{DragAndDrop, Draggable};
+// use editor::{Cancel, Editor};
+// use feature_flags::{ChannelsAlpha, FeatureFlagAppExt, FeatureFlagViewExt};
+// use futures::StreamExt;
+// use fuzzy::{match_strings, StringMatchCandidate};
+// use gpui::{
+//     actions,
+//     elements::{
+//         Canvas, ChildView, Component, ContainerStyle, Empty, Flex, Image, Label, List, ListOffset,
+//         ListState, MouseEventHandler, Orientation, OverlayPositionMode, Padding, ParentElement,
+//         SafeStylable, Stack, Svg,
+//     },
+//     fonts::TextStyle,
+//     geometry::{
+//         rect::RectF,
+//         vector::{vec2f, Vector2F},
+//     },
+//     impl_actions,
+//     platform::{CursorStyle, MouseButton, PromptLevel},
+//     serde_json, AnyElement, AppContext, AsyncAppContext, ClipboardItem, Element, Entity, FontCache,
+//     ModelHandle, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle,
+// };
+// use menu::{Confirm, SelectNext, SelectPrev};
+// use project::{Fs, Project};
+// use serde_derive::{Deserialize, Serialize};
+// use settings::SettingsStore;
+// use std::{borrow::Cow, hash::Hash, mem, sync::Arc};
+// use theme::{components::ComponentExt, IconButton, Interactive};
+// use util::{maybe, ResultExt, TryFutureExt};
+// use workspace::{
+//     dock::{DockPosition, Panel},
+//     item::ItemHandle,
+//     FollowNextCollaborator, Workspace,
+// };
+
+// #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+// struct ToggleCollapse {
+//     location: ChannelId,
+// }
+
+// #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+// struct NewChannel {
+//     location: ChannelId,
+// }
+
+// #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+// struct RenameChannel {
+//     channel_id: ChannelId,
+// }
+
+// #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+// struct ToggleSelectedIx {
+//     ix: usize,
+// }
+
+// #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+// struct RemoveChannel {
+//     channel_id: ChannelId,
+// }
+
+// #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+// struct InviteMembers {
+//     channel_id: ChannelId,
+// }
+
+// #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+// struct ManageMembers {
+//     channel_id: ChannelId,
+// }
+
+// #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+// pub struct OpenChannelNotes {
+//     pub channel_id: ChannelId,
+// }
+
+// #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+// pub struct JoinChannelCall {
+//     pub channel_id: u64,
+// }
+
+// #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+// pub struct JoinChannelChat {
+//     pub channel_id: u64,
+// }
+
+// #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+// pub struct CopyChannelLink {
+//     pub channel_id: u64,
+// }
+
+// #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+// struct StartMoveChannelFor {
+//     channel_id: ChannelId,
+// }
+
+// #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+// struct MoveChannel {
+//     to: ChannelId,
+// }
+
+// actions!(
+//     collab_panel,
+//     [
+//         ToggleFocus,
+//         Remove,
+//         Secondary,
+//         CollapseSelectedChannel,
+//         ExpandSelectedChannel,
+//         StartMoveChannel,
+//         MoveSelected,
+//         InsertSpace,
+//     ]
+// );
+
+// impl_actions!(
+//     collab_panel,
+//     [
+//         RemoveChannel,
+//         NewChannel,
+//         InviteMembers,
+//         ManageMembers,
+//         RenameChannel,
+//         ToggleCollapse,
+//         OpenChannelNotes,
+//         JoinChannelCall,
+//         JoinChannelChat,
+//         CopyChannelLink,
+//         StartMoveChannelFor,
+//         MoveChannel,
+//         ToggleSelectedIx
+//     ]
+// );
+
+// #[derive(Debug, Copy, Clone, PartialEq, Eq)]
+// struct ChannelMoveClipboard {
+//     channel_id: ChannelId,
+// }
+
+// const COLLABORATION_PANEL_KEY: &'static str = "CollaborationPanel";
+
+// pub fn init(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);
+//     cx.add_action(CollabPanel::select_prev);
+//     cx.add_action(CollabPanel::confirm);
+//     cx.add_action(CollabPanel::insert_space);
+//     cx.add_action(CollabPanel::remove);
+//     cx.add_action(CollabPanel::remove_selected_channel);
+//     cx.add_action(CollabPanel::show_inline_context_menu);
+//     cx.add_action(CollabPanel::new_subchannel);
+//     cx.add_action(CollabPanel::invite_members);
+//     cx.add_action(CollabPanel::manage_members);
+//     cx.add_action(CollabPanel::rename_selected_channel);
+//     cx.add_action(CollabPanel::rename_channel);
+//     cx.add_action(CollabPanel::toggle_channel_collapsed_action);
+//     cx.add_action(CollabPanel::collapse_selected_channel);
+//     cx.add_action(CollabPanel::expand_selected_channel);
+//     cx.add_action(CollabPanel::open_channel_notes);
+//     cx.add_action(CollabPanel::join_channel_chat);
+//     cx.add_action(CollabPanel::copy_channel_link);
+
+//     cx.add_action(
+//         |panel: &mut CollabPanel, action: &ToggleSelectedIx, cx: &mut ViewContext<CollabPanel>| {
+//             if panel.selection.take() != Some(action.ix) {
+//                 panel.selection = Some(action.ix)
+//             }
+
+//             cx.notify();
+//         },
+//     );
+
+//     cx.add_action(
+//         |panel: &mut CollabPanel,
+//          action: &StartMoveChannelFor,
+//          _: &mut ViewContext<CollabPanel>| {
+//             panel.channel_clipboard = Some(ChannelMoveClipboard {
+//                 channel_id: action.channel_id,
+//             });
+//         },
+//     );
+
+//     cx.add_action(
+//         |panel: &mut CollabPanel, _: &StartMoveChannel, _: &mut ViewContext<CollabPanel>| {
+//             if let Some(channel) = panel.selected_channel() {
+//                 panel.channel_clipboard = Some(ChannelMoveClipboard {
+//                     channel_id: channel.id,
+//                 })
+//             }
+//         },
+//     );
+
+//     cx.add_action(
+//         |panel: &mut CollabPanel, _: &MoveSelected, cx: &mut ViewContext<CollabPanel>| {
+//             let Some(clipboard) = panel.channel_clipboard.take() else {
+//                 return;
+//             };
+//             let Some(selected_channel) = panel.selected_channel() else {
+//                 return;
+//             };
+
+//             panel
+//                 .channel_store
+//                 .update(cx, |channel_store, cx| {
+//                     channel_store.move_channel(clipboard.channel_id, Some(selected_channel.id), cx)
+//                 })
+//                 .detach_and_log_err(cx)
+//         },
+//     );
+
+//     cx.add_action(
+//         |panel: &mut CollabPanel, action: &MoveChannel, cx: &mut ViewContext<CollabPanel>| {
+//             if let Some(clipboard) = panel.channel_clipboard.take() {
+//                 panel.channel_store.update(cx, |channel_store, cx| {
+//                     channel_store
+//                         .move_channel(clipboard.channel_id, Some(action.to), cx)
+//                         .detach_and_log_err(cx)
+//                 })
+//             }
+//         },
+//     );
+// }
+
+// #[derive(Debug)]
+// pub enum ChannelEditingState {
+//     Create {
+//         location: Option<ChannelId>,
+//         pending_name: Option<String>,
+//     },
+//     Rename {
+//         location: ChannelId,
+//         pending_name: Option<String>,
+//     },
+// }
+
+// impl ChannelEditingState {
+//     fn pending_name(&self) -> Option<&str> {
+//         match self {
+//             ChannelEditingState::Create { pending_name, .. } => pending_name.as_deref(),
+//             ChannelEditingState::Rename { pending_name, .. } => pending_name.as_deref(),
+//         }
+//     }
+// }
+
+// pub struct CollabPanel {
+//     width: Option<f32>,
+//     fs: Arc<dyn Fs>,
+//     has_focus: bool,
+//     channel_clipboard: Option<ChannelMoveClipboard>,
+//     pending_serialization: Task<Option<()>>,
+//     context_menu: ViewHandle<ContextMenu>,
+//     filter_editor: ViewHandle<Editor>,
+//     channel_name_editor: ViewHandle<Editor>,
+//     channel_editing_state: Option<ChannelEditingState>,
+//     entries: Vec<ListEntry>,
+//     selection: Option<usize>,
+//     user_store: ModelHandle<UserStore>,
+//     client: Arc<Client>,
+//     channel_store: ModelHandle<ChannelStore>,
+//     project: ModelHandle<Project>,
+//     match_candidates: Vec<StringMatchCandidate>,
+//     list_state: ListState<Self>,
+//     subscriptions: Vec<Subscription>,
+//     collapsed_sections: Vec<Section>,
+//     collapsed_channels: Vec<ChannelId>,
+//     drag_target_channel: ChannelDragTarget,
+//     workspace: WeakViewHandle<Workspace>,
+//     context_menu_on_selected: bool,
+// }
+
+// #[derive(PartialEq, Eq)]
+// enum ChannelDragTarget {
+//     None,
+//     Root,
+//     Channel(ChannelId),
+// }
+
+// #[derive(Serialize, Deserialize)]
+// struct SerializedCollabPanel {
+//     width: Option<f32>,
+//     collapsed_channels: Option<Vec<ChannelId>>,
+// }
+
+// #[derive(Debug)]
+// pub enum Event {
+//     DockPositionChanged,
+//     Focus,
+//     Dismissed,
+// }
+
+// #[derive(Clone, Copy, PartialEq, Eq, Debug, PartialOrd, Ord)]
+// enum Section {
+//     ActiveCall,
+//     Channels,
+//     ChannelInvites,
+//     ContactRequests,
+//     Contacts,
+//     Online,
+//     Offline,
+// }
+
+// #[derive(Clone, Debug)]
+// enum ListEntry {
+//     Header(Section),
+//     CallParticipant {
+//         user: Arc<User>,
+//         peer_id: Option<PeerId>,
+//         is_pending: bool,
+//     },
+//     ParticipantProject {
+//         project_id: u64,
+//         worktree_root_names: Vec<String>,
+//         host_user_id: u64,
+//         is_last: bool,
+//     },
+//     ParticipantScreen {
+//         peer_id: Option<PeerId>,
+//         is_last: bool,
+//     },
+//     IncomingRequest(Arc<User>),
+//     OutgoingRequest(Arc<User>),
+//     ChannelInvite(Arc<Channel>),
+//     Channel {
+//         channel: Arc<Channel>,
+//         depth: usize,
+//         has_children: bool,
+//     },
+//     ChannelNotes {
+//         channel_id: ChannelId,
+//     },
+//     ChannelChat {
+//         channel_id: ChannelId,
+//     },
+//     ChannelEditor {
+//         depth: usize,
+//     },
+//     Contact {
+//         contact: Arc<Contact>,
+//         calling: bool,
+//     },
+//     ContactPlaceholder,
+// }
+
+// impl Entity for CollabPanel {
+//     type Event = Event;
+// }
+
+// impl CollabPanel {
+//     pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> ViewHandle<Self> {
+//         cx.add_view::<Self, _>(|cx| {
+//             let view_id = cx.view_id();
+
+//             let filter_editor = cx.add_view(|cx| {
+//                 let mut editor = Editor::single_line(
+//                     Some(Arc::new(|theme| {
+//                         theme.collab_panel.user_query_editor.clone()
+//                     })),
+//                     cx,
+//                 );
+//                 editor.set_placeholder_text("Filter channels, contacts", cx);
+//                 editor
+//             });
+
+//             cx.subscribe(&filter_editor, |this, _, event, cx| {
+//                 if let editor::Event::BufferEdited = event {
+//                     let query = this.filter_editor.read(cx).text(cx);
+//                     if !query.is_empty() {
+//                         this.selection.take();
+//                     }
+//                     this.update_entries(true, cx);
+//                     if !query.is_empty() {
+//                         this.selection = this
+//                             .entries
+//                             .iter()
+//                             .position(|entry| !matches!(entry, ListEntry::Header(_)));
+//                     }
+//                 } else if let editor::Event::Blurred = event {
+//                     let query = this.filter_editor.read(cx).text(cx);
+//                     if query.is_empty() {
+//                         this.selection.take();
+//                         this.update_entries(true, cx);
+//                     }
+//                 }
+//             })
+//             .detach();
+
+//             let channel_name_editor = cx.add_view(|cx| {
+//                 Editor::single_line(
+//                     Some(Arc::new(|theme| {
+//                         theme.collab_panel.user_query_editor.clone()
+//                     })),
+//                     cx,
+//                 )
+//             });
+
+//             cx.subscribe(&channel_name_editor, |this, _, event, cx| {
+//                 if let editor::Event::Blurred = event {
+//                     if let Some(state) = &this.channel_editing_state {
+//                         if state.pending_name().is_some() {
+//                             return;
+//                         }
+//                     }
+//                     this.take_editing_state(cx);
+//                     this.update_entries(false, cx);
+//                     cx.notify();
+//                 }
+//             })
+//             .detach();
+
+//             let list_state =
+//                 ListState::<Self>::new(0, Orientation::Top, 1000., move |this, ix, cx| {
+//                     let theme = theme::current(cx).clone();
+//                     let is_selected = this.selection == Some(ix);
+//                     let current_project_id = this.project.read(cx).remote_id();
+
+//                     match &this.entries[ix] {
+//                         ListEntry::Header(section) => {
+//                             let is_collapsed = this.collapsed_sections.contains(section);
+//                             this.render_header(*section, &theme, is_selected, is_collapsed, cx)
+//                         }
+//                         ListEntry::CallParticipant {
+//                             user,
+//                             peer_id,
+//                             is_pending,
+//                         } => Self::render_call_participant(
+//                             user,
+//                             *peer_id,
+//                             this.user_store.clone(),
+//                             *is_pending,
+//                             is_selected,
+//                             &theme,
+//                             cx,
+//                         ),
+//                         ListEntry::ParticipantProject {
+//                             project_id,
+//                             worktree_root_names,
+//                             host_user_id,
+//                             is_last,
+//                         } => Self::render_participant_project(
+//                             *project_id,
+//                             worktree_root_names,
+//                             *host_user_id,
+//                             Some(*project_id) == current_project_id,
+//                             *is_last,
+//                             is_selected,
+//                             &theme,
+//                             cx,
+//                         ),
+//                         ListEntry::ParticipantScreen { peer_id, is_last } => {
+//                             Self::render_participant_screen(
+//                                 *peer_id,
+//                                 *is_last,
+//                                 is_selected,
+//                                 &theme.collab_panel,
+//                                 cx,
+//                             )
+//                         }
+//                         ListEntry::Channel {
+//                             channel,
+//                             depth,
+//                             has_children,
+//                         } => {
+//                             let channel_row = this.render_channel(
+//                                 &*channel,
+//                                 *depth,
+//                                 &theme,
+//                                 is_selected,
+//                                 *has_children,
+//                                 ix,
+//                                 cx,
+//                             );
+
+//                             if is_selected && this.context_menu_on_selected {
+//                                 Stack::new()
+//                                     .with_child(channel_row)
+//                                     .with_child(
+//                                         ChildView::new(&this.context_menu, cx)
+//                                             .aligned()
+//                                             .bottom()
+//                                             .right(),
+//                                     )
+//                                     .into_any()
+//                             } else {
+//                                 return channel_row;
+//                             }
+//                         }
+//                         ListEntry::ChannelNotes { channel_id } => this.render_channel_notes(
+//                             *channel_id,
+//                             &theme.collab_panel,
+//                             is_selected,
+//                             ix,
+//                             cx,
+//                         ),
+//                         ListEntry::ChannelChat { channel_id } => this.render_channel_chat(
+//                             *channel_id,
+//                             &theme.collab_panel,
+//                             is_selected,
+//                             ix,
+//                             cx,
+//                         ),
+//                         ListEntry::ChannelInvite(channel) => Self::render_channel_invite(
+//                             channel.clone(),
+//                             this.channel_store.clone(),
+//                             &theme.collab_panel,
+//                             is_selected,
+//                             cx,
+//                         ),
+//                         ListEntry::IncomingRequest(user) => Self::render_contact_request(
+//                             user.clone(),
+//                             this.user_store.clone(),
+//                             &theme.collab_panel,
+//                             true,
+//                             is_selected,
+//                             cx,
+//                         ),
+//                         ListEntry::OutgoingRequest(user) => Self::render_contact_request(
+//                             user.clone(),
+//                             this.user_store.clone(),
+//                             &theme.collab_panel,
+//                             false,
+//                             is_selected,
+//                             cx,
+//                         ),
+//                         ListEntry::Contact { contact, calling } => Self::render_contact(
+//                             contact,
+//                             *calling,
+//                             &this.project,
+//                             &theme,
+//                             is_selected,
+//                             cx,
+//                         ),
+//                         ListEntry::ChannelEditor { depth } => {
+//                             this.render_channel_editor(&theme, *depth, cx)
+//                         }
+//                         ListEntry::ContactPlaceholder => {
+//                             this.render_contact_placeholder(&theme.collab_panel, is_selected, cx)
+//                         }
+//                     }
+//                 });
+
+//             let mut this = Self {
+//                 width: None,
+//                 has_focus: false,
+//                 channel_clipboard: None,
+//                 fs: workspace.app_state().fs.clone(),
+//                 pending_serialization: Task::ready(None),
+//                 context_menu: cx.add_view(|cx| ContextMenu::new(view_id, cx)),
+//                 channel_name_editor,
+//                 filter_editor,
+//                 entries: Vec::default(),
+//                 channel_editing_state: None,
+//                 selection: None,
+//                 user_store: workspace.user_store().clone(),
+//                 channel_store: ChannelStore::global(cx),
+//                 project: workspace.project().clone(),
+//                 subscriptions: Vec::default(),
+//                 match_candidates: Vec::default(),
+//                 collapsed_sections: vec![Section::Offline],
+//                 collapsed_channels: Vec::default(),
+//                 workspace: workspace.weak_handle(),
+//                 client: workspace.app_state().client.clone(),
+//                 context_menu_on_selected: true,
+//                 drag_target_channel: ChannelDragTarget::None,
+//                 list_state,
+//             };
+
+//             this.update_entries(false, cx);
+
+//             // Update the dock position when the setting changes.
+//             let mut old_dock_position = this.position(cx);
+//             this.subscriptions
+//                 .push(
+//                     cx.observe_global::<SettingsStore, _>(move |this: &mut Self, cx| {
+//                         let new_dock_position = this.position(cx);
+//                         if new_dock_position != old_dock_position {
+//                             old_dock_position = new_dock_position;
+//                             cx.emit(Event::DockPositionChanged);
+//                         }
+//                         cx.notify();
+//                     }),
+//                 );
+
+//             let active_call = ActiveCall::global(cx);
+//             this.subscriptions
+//                 .push(cx.observe(&this.user_store, |this, _, cx| {
+//                     this.update_entries(true, cx)
+//                 }));
+//             this.subscriptions
+//                 .push(cx.observe(&this.channel_store, |this, _, cx| {
+//                     this.update_entries(true, cx)
+//                 }));
+//             this.subscriptions
+//                 .push(cx.observe(&active_call, |this, _, cx| this.update_entries(true, cx)));
+//             this.subscriptions
+//                 .push(cx.observe_flag::<ChannelsAlpha, _>(move |_, this, cx| {
+//                     this.update_entries(true, cx)
+//                 }));
+//             this.subscriptions.push(cx.subscribe(
+//                 &this.channel_store,
+//                 |this, _channel_store, e, cx| match e {
+//                     ChannelEvent::ChannelCreated(channel_id)
+//                     | ChannelEvent::ChannelRenamed(channel_id) => {
+//                         if this.take_editing_state(cx) {
+//                             this.update_entries(false, cx);
+//                             this.selection = this.entries.iter().position(|entry| {
+//                                 if let ListEntry::Channel { channel, .. } = entry {
+//                                     channel.id == *channel_id
+//                                 } else {
+//                                     false
+//                                 }
+//                             });
+//                         }
+//                     }
+//                 },
+//             ));
+
+//             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(COLLABORATION_PANEL_KEY) })
+//                 .await
+//                 .log_err()
+//                 .flatten()
+//             {
+//                 match serde_json::from_str::<SerializedCollabPanel>(&panel) {
+//                     Ok(panel) => Some(panel),
+//                     Err(err) => {
+//                         log::error!("Failed to deserialize collaboration panel: {}", err);
+//                         None
+//                     }
+//                 }
+//             } else {
+//                 None
+//             };
+
+//             workspace.update(&mut cx, |workspace, cx| {
+//                 let panel = CollabPanel::new(workspace, cx);
+//                 if let Some(serialized_panel) = serialized_panel {
+//                     panel.update(cx, |panel, cx| {
+//                         panel.width = serialized_panel.width;
+//                         panel.collapsed_channels = serialized_panel
+//                             .collapsed_channels
+//                             .unwrap_or_else(|| Vec::new());
+//                         cx.notify();
+//                     });
+//                 }
+//                 panel
+//             })
+//         })
+//     }
+
+//     fn serialize(&mut self, cx: &mut ViewContext<Self>) {
+//         let width = self.width;
+//         let collapsed_channels = self.collapsed_channels.clone();
+//         self.pending_serialization = cx.background().spawn(
+//             async move {
+//                 KEY_VALUE_STORE
+//                     .write_kvp(
+//                         COLLABORATION_PANEL_KEY.into(),
+//                         serde_json::to_string(&SerializedCollabPanel {
+//                             width,
+//                             collapsed_channels: Some(collapsed_channels),
+//                         })?,
+//                     )
+//                     .await?;
+//                 anyhow::Ok(())
+//             }
+//             .log_err(),
+//         );
+//     }
+
+//     fn update_entries(&mut self, select_same_item: bool, cx: &mut ViewContext<Self>) {
+//         let channel_store = self.channel_store.read(cx);
+//         let user_store = self.user_store.read(cx);
+//         let query = self.filter_editor.read(cx).text(cx);
+//         let executor = cx.background().clone();
+
+//         let prev_selected_entry = self.selection.and_then(|ix| self.entries.get(ix).cloned());
+//         let old_entries = mem::take(&mut self.entries);
+//         let mut scroll_to_top = false;
+
+//         if let Some(room) = ActiveCall::global(cx).read(cx).room() {
+//             self.entries.push(ListEntry::Header(Section::ActiveCall));
+//             if !old_entries
+//                 .iter()
+//                 .any(|entry| matches!(entry, ListEntry::Header(Section::ActiveCall)))
+//             {
+//                 scroll_to_top = true;
+//             }
+
+//             if !self.collapsed_sections.contains(&Section::ActiveCall) {
+//                 let room = room.read(cx);
+
+//                 if let Some(channel_id) = room.channel_id() {
+//                     self.entries.push(ListEntry::ChannelNotes { channel_id });
+//                     self.entries.push(ListEntry::ChannelChat { channel_id })
+//                 }
+
+//                 // Populate the active user.
+//                 if let Some(user) = user_store.current_user() {
+//                     self.match_candidates.clear();
+//                     self.match_candidates.push(StringMatchCandidate {
+//                         id: 0,
+//                         string: user.github_login.clone(),
+//                         char_bag: user.github_login.chars().collect(),
+//                     });
+//                     let matches = executor.block(match_strings(
+//                         &self.match_candidates,
+//                         &query,
+//                         true,
+//                         usize::MAX,
+//                         &Default::default(),
+//                         executor.clone(),
+//                     ));
+//                     if !matches.is_empty() {
+//                         let user_id = user.id;
+//                         self.entries.push(ListEntry::CallParticipant {
+//                             user,
+//                             peer_id: None,
+//                             is_pending: false,
+//                         });
+//                         let mut projects = room.local_participant().projects.iter().peekable();
+//                         while let Some(project) = projects.next() {
+//                             self.entries.push(ListEntry::ParticipantProject {
+//                                 project_id: project.id,
+//                                 worktree_root_names: project.worktree_root_names.clone(),
+//                                 host_user_id: user_id,
+//                                 is_last: projects.peek().is_none() && !room.is_screen_sharing(),
+//                             });
+//                         }
+//                         if room.is_screen_sharing() {
+//                             self.entries.push(ListEntry::ParticipantScreen {
+//                                 peer_id: None,
+//                                 is_last: true,
+//                             });
+//                         }
+//                     }
+//                 }
+
+//                 // Populate remote participants.
+//                 self.match_candidates.clear();
+//                 self.match_candidates
+//                     .extend(room.remote_participants().iter().map(|(_, participant)| {
+//                         StringMatchCandidate {
+//                             id: participant.user.id as usize,
+//                             string: participant.user.github_login.clone(),
+//                             char_bag: participant.user.github_login.chars().collect(),
+//                         }
+//                     }));
+//                 let matches = executor.block(match_strings(
+//                     &self.match_candidates,
+//                     &query,
+//                     true,
+//                     usize::MAX,
+//                     &Default::default(),
+//                     executor.clone(),
+//                 ));
+//                 for mat in matches {
+//                     let user_id = mat.candidate_id as u64;
+//                     let participant = &room.remote_participants()[&user_id];
+//                     self.entries.push(ListEntry::CallParticipant {
+//                         user: participant.user.clone(),
+//                         peer_id: Some(participant.peer_id),
+//                         is_pending: false,
+//                     });
+//                     let mut projects = participant.projects.iter().peekable();
+//                     while let Some(project) = projects.next() {
+//                         self.entries.push(ListEntry::ParticipantProject {
+//                             project_id: project.id,
+//                             worktree_root_names: project.worktree_root_names.clone(),
+//                             host_user_id: participant.user.id,
+//                             is_last: projects.peek().is_none()
+//                                 && participant.video_tracks.is_empty(),
+//                         });
+//                     }
+//                     if !participant.video_tracks.is_empty() {
+//                         self.entries.push(ListEntry::ParticipantScreen {
+//                             peer_id: Some(participant.peer_id),
+//                             is_last: true,
+//                         });
+//                     }
+//                 }
+
+//                 // Populate pending participants.
+//                 self.match_candidates.clear();
+//                 self.match_candidates
+//                     .extend(room.pending_participants().iter().enumerate().map(
+//                         |(id, participant)| StringMatchCandidate {
+//                             id,
+//                             string: participant.github_login.clone(),
+//                             char_bag: participant.github_login.chars().collect(),
+//                         },
+//                     ));
+//                 let matches = executor.block(match_strings(
+//                     &self.match_candidates,
+//                     &query,
+//                     true,
+//                     usize::MAX,
+//                     &Default::default(),
+//                     executor.clone(),
+//                 ));
+//                 self.entries
+//                     .extend(matches.iter().map(|mat| ListEntry::CallParticipant {
+//                         user: room.pending_participants()[mat.candidate_id].clone(),
+//                         peer_id: None,
+//                         is_pending: true,
+//                     }));
+//             }
+//         }
+
+//         let mut request_entries = Vec::new();
+
+//         if cx.has_flag::<ChannelsAlpha>() {
+//             self.entries.push(ListEntry::Header(Section::Channels));
+
+//             if channel_store.channel_count() > 0 || self.channel_editing_state.is_some() {
+//                 self.match_candidates.clear();
+//                 self.match_candidates
+//                     .extend(channel_store.ordered_channels().enumerate().map(
+//                         |(ix, (_, channel))| StringMatchCandidate {
+//                             id: ix,
+//                             string: channel.name.clone(),
+//                             char_bag: channel.name.chars().collect(),
+//                         },
+//                     ));
+//                 let matches = executor.block(match_strings(
+//                     &self.match_candidates,
+//                     &query,
+//                     true,
+//                     usize::MAX,
+//                     &Default::default(),
+//                     executor.clone(),
+//                 ));
+//                 if let Some(state) = &self.channel_editing_state {
+//                     if matches!(state, ChannelEditingState::Create { location: None, .. }) {
+//                         self.entries.push(ListEntry::ChannelEditor { depth: 0 });
+//                     }
+//                 }
+//                 let mut collapse_depth = None;
+//                 for mat in matches {
+//                     let channel = channel_store.channel_at_index(mat.candidate_id).unwrap();
+//                     let depth = channel.parent_path.len();
+
+//                     if collapse_depth.is_none() && self.is_channel_collapsed(channel.id) {
+//                         collapse_depth = Some(depth);
+//                     } else if let Some(collapsed_depth) = collapse_depth {
+//                         if depth > collapsed_depth {
+//                             continue;
+//                         }
+//                         if self.is_channel_collapsed(channel.id) {
+//                             collapse_depth = Some(depth);
+//                         } else {
+//                             collapse_depth = None;
+//                         }
+//                     }
+
+//                     let has_children = channel_store
+//                         .channel_at_index(mat.candidate_id + 1)
+//                         .map_or(false, |next_channel| {
+//                             next_channel.parent_path.ends_with(&[channel.id])
+//                         });
+
+//                     match &self.channel_editing_state {
+//                         Some(ChannelEditingState::Create {
+//                             location: parent_id,
+//                             ..
+//                         }) if *parent_id == Some(channel.id) => {
+//                             self.entries.push(ListEntry::Channel {
+//                                 channel: channel.clone(),
+//                                 depth,
+//                                 has_children: false,
+//                             });
+//                             self.entries
+//                                 .push(ListEntry::ChannelEditor { depth: depth + 1 });
+//                         }
+//                         Some(ChannelEditingState::Rename {
+//                             location: parent_id,
+//                             ..
+//                         }) if parent_id == &channel.id => {
+//                             self.entries.push(ListEntry::ChannelEditor { depth });
+//                         }
+//                         _ => {
+//                             self.entries.push(ListEntry::Channel {
+//                                 channel: channel.clone(),
+//                                 depth,
+//                                 has_children,
+//                             });
+//                         }
+//                     }
+//                 }
+//             }
+
+//             let channel_invites = channel_store.channel_invitations();
+//             if !channel_invites.is_empty() {
+//                 self.match_candidates.clear();
+//                 self.match_candidates
+//                     .extend(channel_invites.iter().enumerate().map(|(ix, channel)| {
+//                         StringMatchCandidate {
+//                             id: ix,
+//                             string: channel.name.clone(),
+//                             char_bag: channel.name.chars().collect(),
+//                         }
+//                     }));
+//                 let matches = executor.block(match_strings(
+//                     &self.match_candidates,
+//                     &query,
+//                     true,
+//                     usize::MAX,
+//                     &Default::default(),
+//                     executor.clone(),
+//                 ));
+//                 request_entries.extend(matches.iter().map(|mat| {
+//                     ListEntry::ChannelInvite(channel_invites[mat.candidate_id].clone())
+//                 }));
+
+//                 if !request_entries.is_empty() {
+//                     self.entries
+//                         .push(ListEntry::Header(Section::ChannelInvites));
+//                     if !self.collapsed_sections.contains(&Section::ChannelInvites) {
+//                         self.entries.append(&mut request_entries);
+//                     }
+//                 }
+//             }
+//         }
+
+//         self.entries.push(ListEntry::Header(Section::Contacts));
+
+//         request_entries.clear();
+//         let incoming = user_store.incoming_contact_requests();
+//         if !incoming.is_empty() {
+//             self.match_candidates.clear();
+//             self.match_candidates
+//                 .extend(
+//                     incoming
+//                         .iter()
+//                         .enumerate()
+//                         .map(|(ix, user)| StringMatchCandidate {
+//                             id: ix,
+//                             string: user.github_login.clone(),
+//                             char_bag: user.github_login.chars().collect(),
+//                         }),
+//                 );
+//             let matches = executor.block(match_strings(
+//                 &self.match_candidates,
+//                 &query,
+//                 true,
+//                 usize::MAX,
+//                 &Default::default(),
+//                 executor.clone(),
+//             ));
+//             request_entries.extend(
+//                 matches
+//                     .iter()
+//                     .map(|mat| ListEntry::IncomingRequest(incoming[mat.candidate_id].clone())),
+//             );
+//         }
+
+//         let outgoing = user_store.outgoing_contact_requests();
+//         if !outgoing.is_empty() {
+//             self.match_candidates.clear();
+//             self.match_candidates
+//                 .extend(
+//                     outgoing
+//                         .iter()
+//                         .enumerate()
+//                         .map(|(ix, user)| StringMatchCandidate {
+//                             id: ix,
+//                             string: user.github_login.clone(),
+//                             char_bag: user.github_login.chars().collect(),
+//                         }),
+//                 );
+//             let matches = executor.block(match_strings(
+//                 &self.match_candidates,
+//                 &query,
+//                 true,
+//                 usize::MAX,
+//                 &Default::default(),
+//                 executor.clone(),
+//             ));
+//             request_entries.extend(
+//                 matches
+//                     .iter()
+//                     .map(|mat| ListEntry::OutgoingRequest(outgoing[mat.candidate_id].clone())),
+//             );
+//         }
+
+//         if !request_entries.is_empty() {
+//             self.entries
+//                 .push(ListEntry::Header(Section::ContactRequests));
+//             if !self.collapsed_sections.contains(&Section::ContactRequests) {
+//                 self.entries.append(&mut request_entries);
+//             }
+//         }
+
+//         let contacts = user_store.contacts();
+//         if !contacts.is_empty() {
+//             self.match_candidates.clear();
+//             self.match_candidates
+//                 .extend(
+//                     contacts
+//                         .iter()
+//                         .enumerate()
+//                         .map(|(ix, contact)| StringMatchCandidate {
+//                             id: ix,
+//                             string: contact.user.github_login.clone(),
+//                             char_bag: contact.user.github_login.chars().collect(),
+//                         }),
+//                 );
+
+//             let matches = executor.block(match_strings(
+//                 &self.match_candidates,
+//                 &query,
+//                 true,
+//                 usize::MAX,
+//                 &Default::default(),
+//                 executor.clone(),
+//             ));
+
+//             let (online_contacts, offline_contacts) = matches
+//                 .iter()
+//                 .partition::<Vec<_>, _>(|mat| contacts[mat.candidate_id].online);
+
+//             for (matches, section) in [
+//                 (online_contacts, Section::Online),
+//                 (offline_contacts, Section::Offline),
+//             ] {
+//                 if !matches.is_empty() {
+//                     self.entries.push(ListEntry::Header(section));
+//                     if !self.collapsed_sections.contains(&section) {
+//                         let active_call = &ActiveCall::global(cx).read(cx);
+//                         for mat in matches {
+//                             let contact = &contacts[mat.candidate_id];
+//                             self.entries.push(ListEntry::Contact {
+//                                 contact: contact.clone(),
+//                                 calling: active_call.pending_invites().contains(&contact.user.id),
+//                             });
+//                         }
+//                     }
+//                 }
+//             }
+//         }
+
+//         if incoming.is_empty() && outgoing.is_empty() && contacts.is_empty() {
+//             self.entries.push(ListEntry::ContactPlaceholder);
+//         }
+
+//         if select_same_item {
+//             if let Some(prev_selected_entry) = prev_selected_entry {
+//                 self.selection.take();
+//                 for (ix, entry) in self.entries.iter().enumerate() {
+//                     if *entry == prev_selected_entry {
+//                         self.selection = Some(ix);
+//                         break;
+//                     }
+//                 }
+//             }
+//         } else {
+//             self.selection = self.selection.and_then(|prev_selection| {
+//                 if self.entries.is_empty() {
+//                     None
+//                 } else {
+//                     Some(prev_selection.min(self.entries.len() - 1))
+//                 }
+//             });
+//         }
+
+//         let old_scroll_top = self.list_state.logical_scroll_top();
+
+//         self.list_state.reset(self.entries.len());
+
+//         if scroll_to_top {
+//             self.list_state.scroll_to(ListOffset::default());
+//         } else {
+//             // Attempt to maintain the same scroll position.
+//             if let Some(old_top_entry) = old_entries.get(old_scroll_top.item_ix) {
+//                 let new_scroll_top = self
+//                     .entries
+//                     .iter()
+//                     .position(|entry| entry == old_top_entry)
+//                     .map(|item_ix| ListOffset {
+//                         item_ix,
+//                         offset_in_item: old_scroll_top.offset_in_item,
+//                     })
+//                     .or_else(|| {
+//                         let entry_after_old_top = old_entries.get(old_scroll_top.item_ix + 1)?;
+//                         let item_ix = self
+//                             .entries
+//                             .iter()
+//                             .position(|entry| entry == entry_after_old_top)?;
+//                         Some(ListOffset {
+//                             item_ix,
+//                             offset_in_item: 0.,
+//                         })
+//                     })
+//                     .or_else(|| {
+//                         let entry_before_old_top =
+//                             old_entries.get(old_scroll_top.item_ix.saturating_sub(1))?;
+//                         let item_ix = self
+//                             .entries
+//                             .iter()
+//                             .position(|entry| entry == entry_before_old_top)?;
+//                         Some(ListOffset {
+//                             item_ix,
+//                             offset_in_item: 0.,
+//                         })
+//                     });
+
+//                 self.list_state
+//                     .scroll_to(new_scroll_top.unwrap_or(old_scroll_top));
+//             }
+//         }
+
+//         cx.notify();
+//     }
+
+//     fn render_call_participant(
+//         user: &User,
+//         peer_id: Option<PeerId>,
+//         user_store: ModelHandle<UserStore>,
+//         is_pending: bool,
+//         is_selected: bool,
+//         theme: &theme::Theme,
+//         cx: &mut ViewContext<Self>,
+//     ) -> AnyElement<Self> {
+//         enum CallParticipant {}
+//         enum CallParticipantTooltip {}
+//         enum LeaveCallButton {}
+//         enum LeaveCallTooltip {}
+
+//         let collab_theme = &theme.collab_panel;
+
+//         let is_current_user =
+//             user_store.read(cx).current_user().map(|user| user.id) == Some(user.id);
+
+//         let content = MouseEventHandler::new::<CallParticipant, _>(
+//             user.id as usize,
+//             cx,
+//             |mouse_state, cx| {
+//                 let style = if is_current_user {
+//                     *collab_theme
+//                         .contact_row
+//                         .in_state(is_selected)
+//                         .style_for(&mut Default::default())
+//                 } else {
+//                     *collab_theme
+//                         .contact_row
+//                         .in_state(is_selected)
+//                         .style_for(mouse_state)
+//                 };
+
+//                 Flex::row()
+//                     .with_children(user.avatar.clone().map(|avatar| {
+//                         Image::from_data(avatar)
+//                             .with_style(collab_theme.contact_avatar)
+//                             .aligned()
+//                             .left()
+//                     }))
+//                     .with_child(
+//                         Label::new(
+//                             user.github_login.clone(),
+//                             collab_theme.contact_username.text.clone(),
+//                         )
+//                         .contained()
+//                         .with_style(collab_theme.contact_username.container)
+//                         .aligned()
+//                         .left()
+//                         .flex(1., true),
+//                     )
+//                     .with_children(if is_pending {
+//                         Some(
+//                             Label::new("Calling", collab_theme.calling_indicator.text.clone())
+//                                 .contained()
+//                                 .with_style(collab_theme.calling_indicator.container)
+//                                 .aligned()
+//                                 .into_any(),
+//                         )
+//                     } else if is_current_user {
+//                         Some(
+//                             MouseEventHandler::new::<LeaveCallButton, _>(0, cx, |state, _| {
+//                                 render_icon_button(
+//                                     theme
+//                                         .collab_panel
+//                                         .leave_call_button
+//                                         .style_for(is_selected, state),
+//                                     "icons/exit.svg",
+//                                 )
+//                             })
+//                             .with_cursor_style(CursorStyle::PointingHand)
+//                             .on_click(MouseButton::Left, |_, _, cx| {
+//                                 Self::leave_call(cx);
+//                             })
+//                             .with_tooltip::<LeaveCallTooltip>(
+//                                 0,
+//                                 "Leave call",
+//                                 None,
+//                                 theme.tooltip.clone(),
+//                                 cx,
+//                             )
+//                             .into_any(),
+//                         )
+//                     } else {
+//                         None
+//                     })
+//                     .constrained()
+//                     .with_height(collab_theme.row_height)
+//                     .contained()
+//                     .with_style(style)
+//             },
+//         );
+
+//         if is_current_user || is_pending || peer_id.is_none() {
+//             return content.into_any();
+//         }
+
+//         let tooltip = format!("Follow {}", user.github_login);
+
+//         content
+//             .on_click(MouseButton::Left, move |_, this, cx| {
+//                 if let Some(workspace) = this.workspace.upgrade(cx) {
+//                     workspace
+//                         .update(cx, |workspace, cx| workspace.follow(peer_id.unwrap(), cx))
+//                         .map(|task| task.detach_and_log_err(cx));
+//                 }
+//             })
+//             .with_cursor_style(CursorStyle::PointingHand)
+//             .with_tooltip::<CallParticipantTooltip>(
+//                 user.id as usize,
+//                 tooltip,
+//                 Some(Box::new(FollowNextCollaborator)),
+//                 theme.tooltip.clone(),
+//                 cx,
+//             )
+//             .into_any()
+//     }
+
+//     fn render_participant_project(
+//         project_id: u64,
+//         worktree_root_names: &[String],
+//         host_user_id: u64,
+//         is_current: bool,
+//         is_last: bool,
+//         is_selected: bool,
+//         theme: &theme::Theme,
+//         cx: &mut ViewContext<Self>,
+//     ) -> AnyElement<Self> {
+//         enum JoinProject {}
+//         enum JoinProjectTooltip {}
+
+//         let collab_theme = &theme.collab_panel;
+//         let host_avatar_width = collab_theme
+//             .contact_avatar
+//             .width
+//             .or(collab_theme.contact_avatar.height)
+//             .unwrap_or(0.);
+//         let tree_branch = collab_theme.tree_branch;
+//         let project_name = if worktree_root_names.is_empty() {
+//             "untitled".to_string()
+//         } else {
+//             worktree_root_names.join(", ")
+//         };
+
+//         let content =
+//             MouseEventHandler::new::<JoinProject, _>(project_id as usize, cx, |mouse_state, cx| {
+//                 let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state);
+//                 let row = if is_current {
+//                     collab_theme
+//                         .project_row
+//                         .in_state(true)
+//                         .style_for(&mut Default::default())
+//                 } else {
+//                     collab_theme
+//                         .project_row
+//                         .in_state(is_selected)
+//                         .style_for(mouse_state)
+//                 };
+
+//                 Flex::row()
+//                     .with_child(render_tree_branch(
+//                         tree_branch,
+//                         &row.name.text,
+//                         is_last,
+//                         vec2f(host_avatar_width, collab_theme.row_height),
+//                         cx.font_cache(),
+//                     ))
+//                     .with_child(
+//                         Svg::new("icons/file_icons/folder.svg")
+//                             .with_color(collab_theme.channel_hash.color)
+//                             .constrained()
+//                             .with_width(collab_theme.channel_hash.width)
+//                             .aligned()
+//                             .left(),
+//                     )
+//                     .with_child(
+//                         Label::new(project_name.clone(), row.name.text.clone())
+//                             .aligned()
+//                             .left()
+//                             .contained()
+//                             .with_style(row.name.container)
+//                             .flex(1., false),
+//                     )
+//                     .constrained()
+//                     .with_height(collab_theme.row_height)
+//                     .contained()
+//                     .with_style(row.container)
+//             });
+
+//         if is_current {
+//             return content.into_any();
+//         }
+
+//         content
+//             .with_cursor_style(CursorStyle::PointingHand)
+//             .on_click(MouseButton::Left, move |_, this, cx| {
+//                 if let Some(workspace) = this.workspace.upgrade(cx) {
+//                     let app_state = workspace.read(cx).app_state().clone();
+//                     workspace::join_remote_project(project_id, host_user_id, app_state, cx)
+//                         .detach_and_log_err(cx);
+//                 }
+//             })
+//             .with_tooltip::<JoinProjectTooltip>(
+//                 project_id as usize,
+//                 format!("Open {}", project_name),
+//                 None,
+//                 theme.tooltip.clone(),
+//                 cx,
+//             )
+//             .into_any()
+//     }
+
+//     fn render_participant_screen(
+//         peer_id: Option<PeerId>,
+//         is_last: bool,
+//         is_selected: bool,
+//         theme: &theme::CollabPanel,
+//         cx: &mut ViewContext<Self>,
+//     ) -> AnyElement<Self> {
+//         enum OpenSharedScreen {}
+
+//         let host_avatar_width = theme
+//             .contact_avatar
+//             .width
+//             .or(theme.contact_avatar.height)
+//             .unwrap_or(0.);
+//         let tree_branch = theme.tree_branch;
+
+//         let handler = MouseEventHandler::new::<OpenSharedScreen, _>(
+//             peer_id.map(|id| id.as_u64()).unwrap_or(0) as usize,
+//             cx,
+//             |mouse_state, cx| {
+//                 let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state);
+//                 let row = theme
+//                     .project_row
+//                     .in_state(is_selected)
+//                     .style_for(mouse_state);
+
+//                 Flex::row()
+//                     .with_child(render_tree_branch(
+//                         tree_branch,
+//                         &row.name.text,
+//                         is_last,
+//                         vec2f(host_avatar_width, theme.row_height),
+//                         cx.font_cache(),
+//                     ))
+//                     .with_child(
+//                         Svg::new("icons/desktop.svg")
+//                             .with_color(theme.channel_hash.color)
+//                             .constrained()
+//                             .with_width(theme.channel_hash.width)
+//                             .aligned()
+//                             .left(),
+//                     )
+//                     .with_child(
+//                         Label::new("Screen", row.name.text.clone())
+//                             .aligned()
+//                             .left()
+//                             .contained()
+//                             .with_style(row.name.container)
+//                             .flex(1., false),
+//                     )
+//                     .constrained()
+//                     .with_height(theme.row_height)
+//                     .contained()
+//                     .with_style(row.container)
+//             },
+//         );
+//         if peer_id.is_none() {
+//             return handler.into_any();
+//         }
+//         handler
+//             .with_cursor_style(CursorStyle::PointingHand)
+//             .on_click(MouseButton::Left, move |_, this, cx| {
+//                 if let Some(workspace) = this.workspace.upgrade(cx) {
+//                     workspace.update(cx, |workspace, cx| {
+//                         workspace.open_shared_screen(peer_id.unwrap(), cx)
+//                     });
+//                 }
+//             })
+//             .into_any()
+//     }
+
+//     fn take_editing_state(&mut self, cx: &mut ViewContext<Self>) -> bool {
+//         if let Some(_) = self.channel_editing_state.take() {
+//             self.channel_name_editor.update(cx, |editor, cx| {
+//                 editor.set_text("", cx);
+//             });
+//             true
+//         } else {
+//             false
+//         }
+//     }
+
+//     fn render_header(
+//         &self,
+//         section: Section,
+//         theme: &theme::Theme,
+//         is_selected: bool,
+//         is_collapsed: bool,
+//         cx: &mut ViewContext<Self>,
+//     ) -> AnyElement<Self> {
+//         enum Header {}
+//         enum LeaveCallContactList {}
+//         enum AddChannel {}
+
+//         let tooltip_style = &theme.tooltip;
+//         let mut channel_link = None;
+//         let mut channel_tooltip_text = None;
+//         let mut channel_icon = None;
+//         let mut is_dragged_over = false;
+
+//         let text = match section {
+//             Section::ActiveCall => {
+//                 let channel_name = maybe!({
+//                     let channel_id = ActiveCall::global(cx).read(cx).channel_id(cx)?;
+
+//                     let channel = self.channel_store.read(cx).channel_for_id(channel_id)?;
+
+//                     channel_link = Some(channel.link());
+//                     (channel_icon, channel_tooltip_text) = match channel.visibility {
+//                         proto::ChannelVisibility::Public => {
+//                             (Some("icons/public.svg"), Some("Copy public channel link."))
+//                         }
+//                         proto::ChannelVisibility::Members => {
+//                             (Some("icons/hash.svg"), Some("Copy private channel link."))
+//                         }
+//                     };
+
+//                     Some(channel.name.as_str())
+//                 });
+
+//                 if let Some(name) = channel_name {
+//                     Cow::Owned(format!("{}", name))
+//                 } else {
+//                     Cow::Borrowed("Current Call")
+//                 }
+//             }
+//             Section::ContactRequests => Cow::Borrowed("Requests"),
+//             Section::Contacts => Cow::Borrowed("Contacts"),
+//             Section::Channels => Cow::Borrowed("Channels"),
+//             Section::ChannelInvites => Cow::Borrowed("Invites"),
+//             Section::Online => Cow::Borrowed("Online"),
+//             Section::Offline => Cow::Borrowed("Offline"),
+//         };
+
+//         enum AddContact {}
+//         let button = match section {
+//             Section::ActiveCall => channel_link.map(|channel_link| {
+//                 let channel_link_copy = channel_link.clone();
+//                 MouseEventHandler::new::<AddContact, _>(0, cx, |state, _| {
+//                     render_icon_button(
+//                         theme
+//                             .collab_panel
+//                             .leave_call_button
+//                             .style_for(is_selected, state),
+//                         "icons/link.svg",
+//                     )
+//                 })
+//                 .with_cursor_style(CursorStyle::PointingHand)
+//                 .on_click(MouseButton::Left, move |_, _, cx| {
+//                     let item = ClipboardItem::new(channel_link_copy.clone());
+//                     cx.write_to_clipboard(item)
+//                 })
+//                 .with_tooltip::<AddContact>(
+//                     0,
+//                     channel_tooltip_text.unwrap(),
+//                     None,
+//                     tooltip_style.clone(),
+//                     cx,
+//                 )
+//             }),
+//             Section::Contacts => Some(
+//                 MouseEventHandler::new::<LeaveCallContactList, _>(0, cx, |state, _| {
+//                     render_icon_button(
+//                         theme
+//                             .collab_panel
+//                             .add_contact_button
+//                             .style_for(is_selected, state),
+//                         "icons/plus.svg",
+//                     )
+//                 })
+//                 .with_cursor_style(CursorStyle::PointingHand)
+//                 .on_click(MouseButton::Left, |_, this, cx| {
+//                     this.toggle_contact_finder(cx);
+//                 })
+//                 .with_tooltip::<LeaveCallContactList>(
+//                     0,
+//                     "Search for new contact",
+//                     None,
+//                     tooltip_style.clone(),
+//                     cx,
+//                 ),
+//             ),
+//             Section::Channels => {
+//                 if cx
+//                     .global::<DragAndDrop<Workspace>>()
+//                     .currently_dragged::<Channel>(cx.window())
+//                     .is_some()
+//                     && self.drag_target_channel == ChannelDragTarget::Root
+//                 {
+//                     is_dragged_over = true;
+//                 }
+
+//                 Some(
+//                     MouseEventHandler::new::<AddChannel, _>(0, cx, |state, _| {
+//                         render_icon_button(
+//                             theme
+//                                 .collab_panel
+//                                 .add_contact_button
+//                                 .style_for(is_selected, state),
+//                             "icons/plus.svg",
+//                         )
+//                     })
+//                     .with_cursor_style(CursorStyle::PointingHand)
+//                     .on_click(MouseButton::Left, |_, this, cx| this.new_root_channel(cx))
+//                     .with_tooltip::<AddChannel>(
+//                         0,
+//                         "Create a channel",
+//                         None,
+//                         tooltip_style.clone(),
+//                         cx,
+//                     ),
+//                 )
+//             }
+//             _ => None,
+//         };
+
+//         let can_collapse = match section {
+//             Section::ActiveCall | Section::Channels | Section::Contacts => false,
+//             Section::ChannelInvites
+//             | Section::ContactRequests
+//             | Section::Online
+//             | Section::Offline => true,
+//         };
+//         let icon_size = (&theme.collab_panel).section_icon_size;
+//         let mut result = MouseEventHandler::new::<Header, _>(section as usize, cx, |state, _| {
+//             let header_style = if can_collapse {
+//                 theme
+//                     .collab_panel
+//                     .subheader_row
+//                     .in_state(is_selected)
+//                     .style_for(state)
+//             } else {
+//                 &theme.collab_panel.header_row
+//             };
+
+//             Flex::row()
+//                 .with_children(if can_collapse {
+//                     Some(
+//                         Svg::new(if is_collapsed {
+//                             "icons/chevron_right.svg"
+//                         } else {
+//                             "icons/chevron_down.svg"
+//                         })
+//                         .with_color(header_style.text.color)
+//                         .constrained()
+//                         .with_max_width(icon_size)
+//                         .with_max_height(icon_size)
+//                         .aligned()
+//                         .constrained()
+//                         .with_width(icon_size)
+//                         .contained()
+//                         .with_margin_right(
+//                             theme.collab_panel.contact_username.container.margin.left,
+//                         ),
+//                     )
+//                 } else if let Some(channel_icon) = channel_icon {
+//                     Some(
+//                         Svg::new(channel_icon)
+//                             .with_color(header_style.text.color)
+//                             .constrained()
+//                             .with_max_width(icon_size)
+//                             .with_max_height(icon_size)
+//                             .aligned()
+//                             .constrained()
+//                             .with_width(icon_size)
+//                             .contained()
+//                             .with_margin_right(
+//                                 theme.collab_panel.contact_username.container.margin.left,
+//                             ),
+//                     )
+//                 } else {
+//                     None
+//                 })
+//                 .with_child(
+//                     Label::new(text, header_style.text.clone())
+//                         .aligned()
+//                         .left()
+//                         .flex(1., true),
+//                 )
+//                 .with_children(button.map(|button| button.aligned().right()))
+//                 .constrained()
+//                 .with_height(theme.collab_panel.row_height)
+//                 .contained()
+//                 .with_style(if is_dragged_over {
+//                     theme.collab_panel.dragged_over_header
+//                 } else {
+//                     header_style.container
+//                 })
+//         });
+
+//         result = result
+//             .on_move(move |_, this, cx| {
+//                 if cx
+//                     .global::<DragAndDrop<Workspace>>()
+//                     .currently_dragged::<Channel>(cx.window())
+//                     .is_some()
+//                 {
+//                     this.drag_target_channel = ChannelDragTarget::Root;
+//                     cx.notify()
+//                 }
+//             })
+//             .on_up(MouseButton::Left, move |_, this, cx| {
+//                 if let Some((_, dragged_channel)) = cx
+//                     .global::<DragAndDrop<Workspace>>()
+//                     .currently_dragged::<Channel>(cx.window())
+//                 {
+//                     this.channel_store
+//                         .update(cx, |channel_store, cx| {
+//                             channel_store.move_channel(dragged_channel.id, None, cx)
+//                         })
+//                         .detach_and_log_err(cx)
+//                 }
+//             });
+
+//         if can_collapse {
+//             result = result
+//                 .with_cursor_style(CursorStyle::PointingHand)
+//                 .on_click(MouseButton::Left, move |_, this, cx| {
+//                     if can_collapse {
+//                         this.toggle_section_expanded(section, cx);
+//                     }
+//                 })
+//         }
+
+//         result.into_any()
+//     }
+
+//     fn render_contact(
+//         contact: &Contact,
+//         calling: bool,
+//         project: &ModelHandle<Project>,
+//         theme: &theme::Theme,
+//         is_selected: bool,
+//         cx: &mut ViewContext<Self>,
+//     ) -> AnyElement<Self> {
+//         enum ContactTooltip {}
+
+//         let collab_theme = &theme.collab_panel;
+//         let online = contact.online;
+//         let busy = contact.busy || calling;
+//         let user_id = contact.user.id;
+//         let github_login = contact.user.github_login.clone();
+//         let initial_project = project.clone();
+
+//         let event_handler =
+//             MouseEventHandler::new::<Contact, _>(contact.user.id as usize, cx, |state, cx| {
+//                 Flex::row()
+//                     .with_children(contact.user.avatar.clone().map(|avatar| {
+//                         let status_badge = if contact.online {
+//                             Some(
+//                                 Empty::new()
+//                                     .collapsed()
+//                                     .contained()
+//                                     .with_style(if busy {
+//                                         collab_theme.contact_status_busy
+//                                     } else {
+//                                         collab_theme.contact_status_free
+//                                     })
+//                                     .aligned(),
+//                             )
+//                         } else {
+//                             None
+//                         };
+//                         Stack::new()
+//                             .with_child(
+//                                 Image::from_data(avatar)
+//                                     .with_style(collab_theme.contact_avatar)
+//                                     .aligned()
+//                                     .left(),
+//                             )
+//                             .with_children(status_badge)
+//                     }))
+//                     .with_child(
+//                         Label::new(
+//                             contact.user.github_login.clone(),
+//                             collab_theme.contact_username.text.clone(),
+//                         )
+//                         .contained()
+//                         .with_style(collab_theme.contact_username.container)
+//                         .aligned()
+//                         .left()
+//                         .flex(1., true),
+//                     )
+//                     .with_children(if state.hovered() {
+//                         Some(
+//                             MouseEventHandler::new::<Cancel, _>(
+//                                 contact.user.id as usize,
+//                                 cx,
+//                                 |mouse_state, _| {
+//                                     let button_style =
+//                                         collab_theme.contact_button.style_for(mouse_state);
+//                                     render_icon_button(button_style, "icons/x.svg")
+//                                         .aligned()
+//                                         .flex_float()
+//                                 },
+//                             )
+//                             .with_padding(Padding::uniform(2.))
+//                             .with_cursor_style(CursorStyle::PointingHand)
+//                             .on_click(MouseButton::Left, move |_, this, cx| {
+//                                 this.remove_contact(user_id, &github_login, cx);
+//                             })
+//                             .flex_float(),
+//                         )
+//                     } else {
+//                         None
+//                     })
+//                     .with_children(if calling {
+//                         Some(
+//                             Label::new("Calling", collab_theme.calling_indicator.text.clone())
+//                                 .contained()
+//                                 .with_style(collab_theme.calling_indicator.container)
+//                                 .aligned(),
+//                         )
+//                     } else {
+//                         None
+//                     })
+//                     .constrained()
+//                     .with_height(collab_theme.row_height)
+//                     .contained()
+//                     .with_style(
+//                         *collab_theme
+//                             .contact_row
+//                             .in_state(is_selected)
+//                             .style_for(state),
+//                     )
+//             });
+
+//         if online && !busy {
+//             let room = ActiveCall::global(cx).read(cx).room();
+//             let label = if room.is_some() {
+//                 format!("Invite {} to join call", contact.user.github_login)
+//             } else {
+//                 format!("Call {}", contact.user.github_login)
+//             };
+
+//             event_handler
+//                 .on_click(MouseButton::Left, move |_, this, cx| {
+//                     this.call(user_id, Some(initial_project.clone()), cx);
+//                 })
+//                 .with_cursor_style(CursorStyle::PointingHand)
+//                 .with_tooltip::<ContactTooltip>(
+//                     contact.user.id as usize,
+//                     label,
+//                     None,
+//                     theme.tooltip.clone(),
+//                     cx,
+//                 )
+//                 .into_any()
+//         } else {
+//             event_handler
+//                 .with_tooltip::<ContactTooltip>(
+//                     contact.user.id as usize,
+//                     format!(
+//                         "{} is {}",
+//                         contact.user.github_login,
+//                         if busy { "on a call" } else { "offline" }
+//                     ),
+//                     None,
+//                     theme.tooltip.clone(),
+//                     cx,
+//                 )
+//                 .into_any()
+//         }
+//     }
+
+//     fn render_contact_placeholder(
+//         &self,
+//         theme: &theme::CollabPanel,
+//         is_selected: bool,
+//         cx: &mut ViewContext<Self>,
+//     ) -> AnyElement<Self> {
+//         enum AddContacts {}
+//         MouseEventHandler::new::<AddContacts, _>(0, cx, |state, _| {
+//             let style = theme.list_empty_state.style_for(is_selected, state);
+//             Flex::row()
+//                 .with_child(
+//                     Svg::new("icons/plus.svg")
+//                         .with_color(theme.list_empty_icon.color)
+//                         .constrained()
+//                         .with_width(theme.list_empty_icon.width)
+//                         .aligned()
+//                         .left(),
+//                 )
+//                 .with_child(
+//                     Label::new("Add a contact", style.text.clone())
+//                         .contained()
+//                         .with_style(theme.list_empty_label_container),
+//                 )
+//                 .align_children_center()
+//                 .contained()
+//                 .with_style(style.container)
+//                 .into_any()
+//         })
+//         .on_click(MouseButton::Left, |_, this, cx| {
+//             this.toggle_contact_finder(cx);
+//         })
+//         .into_any()
+//     }
+
+//     fn render_channel_editor(
+//         &self,
+//         theme: &theme::Theme,
+//         depth: usize,
+//         cx: &AppContext,
+//     ) -> AnyElement<Self> {
+//         Flex::row()
+//             .with_child(
+//                 Empty::new()
+//                     .constrained()
+//                     .with_width(theme.collab_panel.disclosure.button_space()),
+//             )
+//             .with_child(
+//                 Svg::new("icons/hash.svg")
+//                     .with_color(theme.collab_panel.channel_hash.color)
+//                     .constrained()
+//                     .with_width(theme.collab_panel.channel_hash.width)
+//                     .aligned()
+//                     .left(),
+//             )
+//             .with_child(
+//                 if let Some(pending_name) = self
+//                     .channel_editing_state
+//                     .as_ref()
+//                     .and_then(|state| state.pending_name())
+//                 {
+//                     Label::new(
+//                         pending_name.to_string(),
+//                         theme.collab_panel.contact_username.text.clone(),
+//                     )
+//                     .contained()
+//                     .with_style(theme.collab_panel.contact_username.container)
+//                     .aligned()
+//                     .left()
+//                     .flex(1., true)
+//                     .into_any()
+//                 } else {
+//                     ChildView::new(&self.channel_name_editor, cx)
+//                         .aligned()
+//                         .left()
+//                         .contained()
+//                         .with_style(theme.collab_panel.channel_editor)
+//                         .flex(1.0, true)
+//                         .into_any()
+//                 },
+//             )
+//             .align_children_center()
+//             .constrained()
+//             .with_height(theme.collab_panel.row_height)
+//             .contained()
+//             .with_style(ContainerStyle {
+//                 background_color: Some(theme.editor.background),
+//                 ..*theme.collab_panel.contact_row.default_style()
+//             })
+//             .with_padding_left(
+//                 theme.collab_panel.contact_row.default_style().padding.left
+//                     + theme.collab_panel.channel_indent * depth as f32,
+//             )
+//             .into_any()
+//     }
+
+//     fn render_channel(
+//         &self,
+//         channel: &Channel,
+//         depth: usize,
+//         theme: &theme::Theme,
+//         is_selected: bool,
+//         has_children: bool,
+//         ix: usize,
+//         cx: &mut ViewContext<Self>,
+//     ) -> AnyElement<Self> {
+//         let channel_id = channel.id;
+//         let collab_theme = &theme.collab_panel;
+//         let is_public = self
+//             .channel_store
+//             .read(cx)
+//             .channel_for_id(channel_id)
+//             .map(|channel| channel.visibility)
+//             == Some(proto::ChannelVisibility::Public);
+//         let other_selected = self.selected_channel().map(|channel| channel.id) == Some(channel.id);
+//         let disclosed =
+//             has_children.then(|| !self.collapsed_channels.binary_search(&channel.id).is_ok());
+
+//         let is_active = maybe!({
+//             let call_channel = ActiveCall::global(cx)
+//                 .read(cx)
+//                 .room()?
+//                 .read(cx)
+//                 .channel_id()?;
+//             Some(call_channel == channel_id)
+//         })
+//         .unwrap_or(false);
+
+//         const FACEPILE_LIMIT: usize = 3;
+
+//         enum ChannelCall {}
+//         enum ChannelNote {}
+//         enum NotesTooltip {}
+//         enum ChatTooltip {}
+//         enum ChannelTooltip {}
+
+//         let mut is_dragged_over = false;
+//         if cx
+//             .global::<DragAndDrop<Workspace>>()
+//             .currently_dragged::<Channel>(cx.window())
+//             .is_some()
+//             && self.drag_target_channel == ChannelDragTarget::Channel(channel_id)
+//         {
+//             is_dragged_over = true;
+//         }
+
+//         let has_messages_notification = channel.unseen_message_id.is_some();
+
+//         MouseEventHandler::new::<Channel, _>(ix, cx, |state, cx| {
+//             let row_hovered = state.hovered();
+
+//             let mut select_state = |interactive: &Interactive<ContainerStyle>| {
+//                 if state.clicked() == Some(MouseButton::Left) && interactive.clicked.is_some() {
+//                     interactive.clicked.as_ref().unwrap().clone()
+//                 } else if state.hovered() || other_selected {
+//                     interactive
+//                         .hovered
+//                         .as_ref()
+//                         .unwrap_or(&interactive.default)
+//                         .clone()
+//                 } else {
+//                     interactive.default.clone()
+//                 }
+//             };
+
+//             Flex::<Self>::row()
+//                 .with_child(
+//                     Svg::new(if is_public {
+//                         "icons/public.svg"
+//                     } else {
+//                         "icons/hash.svg"
+//                     })
+//                     .with_color(collab_theme.channel_hash.color)
+//                     .constrained()
+//                     .with_width(collab_theme.channel_hash.width)
+//                     .aligned()
+//                     .left(),
+//                 )
+//                 .with_child({
+//                     let style = collab_theme.channel_name.inactive_state();
+//                     Flex::row()
+//                         .with_child(
+//                             Label::new(channel.name.clone(), style.text.clone())
+//                                 .contained()
+//                                 .with_style(style.container)
+//                                 .aligned()
+//                                 .left()
+//                                 .with_tooltip::<ChannelTooltip>(
+//                                     ix,
+//                                     "Join channel",
+//                                     None,
+//                                     theme.tooltip.clone(),
+//                                     cx,
+//                                 ),
+//                         )
+//                         .with_children({
+//                             let participants =
+//                                 self.channel_store.read(cx).channel_participants(channel_id);
+
+//                             if !participants.is_empty() {
+//                                 let extra_count = participants.len().saturating_sub(FACEPILE_LIMIT);
+
+//                                 let result = FacePile::new(collab_theme.face_overlap)
+//                                     .with_children(
+//                                         participants
+//                                             .iter()
+//                                             .filter_map(|user| {
+//                                                 Some(
+//                                                     Image::from_data(user.avatar.clone()?)
+//                                                         .with_style(collab_theme.channel_avatar),
+//                                                 )
+//                                             })
+//                                             .take(FACEPILE_LIMIT),
+//                                     )
+//                                     .with_children((extra_count > 0).then(|| {
+//                                         Label::new(
+//                                             format!("+{}", extra_count),
+//                                             collab_theme.extra_participant_label.text.clone(),
+//                                         )
+//                                         .contained()
+//                                         .with_style(collab_theme.extra_participant_label.container)
+//                                     }));
+
+//                                 Some(result)
+//                             } else {
+//                                 None
+//                             }
+//                         })
+//                         .with_spacing(8.)
+//                         .align_children_center()
+//                         .flex(1., true)
+//                 })
+//                 .with_child(
+//                     MouseEventHandler::new::<ChannelNote, _>(ix, cx, move |mouse_state, _| {
+//                         let container_style = collab_theme
+//                             .disclosure
+//                             .button
+//                             .style_for(mouse_state)
+//                             .container;
+
+//                         if channel.unseen_message_id.is_some() {
+//                             Svg::new("icons/conversations.svg")
+//                                 .with_color(collab_theme.channel_note_active_color)
+//                                 .constrained()
+//                                 .with_width(collab_theme.channel_hash.width)
+//                                 .contained()
+//                                 .with_style(container_style)
+//                                 .with_uniform_padding(4.)
+//                                 .into_any()
+//                         } else if row_hovered {
+//                             Svg::new("icons/conversations.svg")
+//                                 .with_color(collab_theme.channel_hash.color)
+//                                 .constrained()
+//                                 .with_width(collab_theme.channel_hash.width)
+//                                 .contained()
+//                                 .with_style(container_style)
+//                                 .with_uniform_padding(4.)
+//                                 .into_any()
+//                         } else {
+//                             Empty::new().into_any()
+//                         }
+//                     })
+//                     .on_click(MouseButton::Left, move |_, this, cx| {
+//                         this.join_channel_chat(&JoinChannelChat { channel_id }, cx);
+//                     })
+//                     .with_tooltip::<ChatTooltip>(
+//                         ix,
+//                         "Open channel chat",
+//                         None,
+//                         theme.tooltip.clone(),
+//                         cx,
+//                     )
+//                     .contained()
+//                     .with_margin_right(4.),
+//                 )
+//                 .with_child(
+//                     MouseEventHandler::new::<ChannelCall, _>(ix, cx, move |mouse_state, cx| {
+//                         let container_style = collab_theme
+//                             .disclosure
+//                             .button
+//                             .style_for(mouse_state)
+//                             .container;
+//                         if row_hovered || channel.unseen_note_version.is_some() {
+//                             Svg::new("icons/file.svg")
+//                                 .with_color(if channel.unseen_note_version.is_some() {
+//                                     collab_theme.channel_note_active_color
+//                                 } else {
+//                                     collab_theme.channel_hash.color
+//                                 })
+//                                 .constrained()
+//                                 .with_width(collab_theme.channel_hash.width)
+//                                 .contained()
+//                                 .with_style(container_style)
+//                                 .with_uniform_padding(4.)
+//                                 .with_margin_right(collab_theme.channel_hash.container.margin.left)
+//                                 .with_tooltip::<NotesTooltip>(
+//                                     ix as usize,
+//                                     "Open channel notes",
+//                                     None,
+//                                     theme.tooltip.clone(),
+//                                     cx,
+//                                 )
+//                                 .into_any()
+//                         } else if has_messages_notification {
+//                             Empty::new()
+//                                 .constrained()
+//                                 .with_width(collab_theme.channel_hash.width)
+//                                 .contained()
+//                                 .with_uniform_padding(4.)
+//                                 .with_margin_right(collab_theme.channel_hash.container.margin.left)
+//                                 .into_any()
+//                         } else {
+//                             Empty::new().into_any()
+//                         }
+//                     })
+//                     .on_click(MouseButton::Left, move |_, this, cx| {
+//                         this.open_channel_notes(&OpenChannelNotes { channel_id }, cx);
+//                     }),
+//                 )
+//                 .align_children_center()
+//                 .styleable_component()
+//                 .disclosable(
+//                     disclosed,
+//                     Box::new(ToggleCollapse {
+//                         location: channel.id.clone(),
+//                     }),
+//                 )
+//                 .with_id(ix)
+//                 .with_style(collab_theme.disclosure.clone())
+//                 .element()
+//                 .constrained()
+//                 .with_height(collab_theme.row_height)
+//                 .contained()
+//                 .with_style(select_state(
+//                     collab_theme
+//                         .channel_row
+//                         .in_state(is_selected || is_active || is_dragged_over),
+//                 ))
+//                 .with_padding_left(
+//                     collab_theme.channel_row.default_style().padding.left
+//                         + collab_theme.channel_indent * depth as f32,
+//                 )
+//         })
+//         .on_click(MouseButton::Left, move |_, this, cx| {
+//             if this.drag_target_channel == ChannelDragTarget::None {
+//                 if is_active {
+//                     this.open_channel_notes(&OpenChannelNotes { channel_id }, cx)
+//                 } else {
+//                     this.join_channel(channel_id, cx)
+//                 }
+//             }
+//         })
+//         .on_click(MouseButton::Right, {
+//             let channel = channel.clone();
+//             move |e, this, cx| {
+//                 this.deploy_channel_context_menu(Some(e.position), &channel, ix, cx);
+//             }
+//         })
+//         .on_up(MouseButton::Left, move |_, this, cx| {
+//             if let Some((_, dragged_channel)) = cx
+//                 .global::<DragAndDrop<Workspace>>()
+//                 .currently_dragged::<Channel>(cx.window())
+//             {
+//                 this.channel_store
+//                     .update(cx, |channel_store, cx| {
+//                         channel_store.move_channel(dragged_channel.id, Some(channel_id), cx)
+//                     })
+//                     .detach_and_log_err(cx)
+//             }
+//         })
+//         .on_move({
+//             let channel = channel.clone();
+//             move |_, this, cx| {
+//                 if let Some((_, dragged_channel)) = cx
+//                     .global::<DragAndDrop<Workspace>>()
+//                     .currently_dragged::<Channel>(cx.window())
+//                 {
+//                     if channel.id != dragged_channel.id {
+//                         this.drag_target_channel = ChannelDragTarget::Channel(channel.id);
+//                     }
+//                     cx.notify()
+//                 }
+//             }
+//         })
+//         .as_draggable::<_, Channel>(
+//             channel.clone(),
+//             move |_, channel, cx: &mut ViewContext<Workspace>| {
+//                 let theme = &theme::current(cx).collab_panel;
+
+//                 Flex::<Workspace>::row()
+//                     .with_child(
+//                         Svg::new("icons/hash.svg")
+//                             .with_color(theme.channel_hash.color)
+//                             .constrained()
+//                             .with_width(theme.channel_hash.width)
+//                             .aligned()
+//                             .left(),
+//                     )
+//                     .with_child(
+//                         Label::new(channel.name.clone(), theme.channel_name.text.clone())
+//                             .contained()
+//                             .with_style(theme.channel_name.container)
+//                             .aligned()
+//                             .left(),
+//                     )
+//                     .align_children_center()
+//                     .contained()
+//                     .with_background_color(
+//                         theme
+//                             .container
+//                             .background_color
+//                             .unwrap_or(gpui::color::Color::transparent_black()),
+//                     )
+//                     .contained()
+//                     .with_padding_left(
+//                         theme.channel_row.default_style().padding.left
+//                             + theme.channel_indent * depth as f32,
+//                     )
+//                     .into_any()
+//             },
+//         )
+//         .with_cursor_style(CursorStyle::PointingHand)
+//         .into_any()
+//     }
+
+//     fn render_channel_notes(
+//         &self,
+//         channel_id: ChannelId,
+//         theme: &theme::CollabPanel,
+//         is_selected: bool,
+//         ix: usize,
+//         cx: &mut ViewContext<Self>,
+//     ) -> AnyElement<Self> {
+//         enum ChannelNotes {}
+//         let host_avatar_width = theme
+//             .contact_avatar
+//             .width
+//             .or(theme.contact_avatar.height)
+//             .unwrap_or(0.);
+
+//         MouseEventHandler::new::<ChannelNotes, _>(ix as usize, cx, |state, cx| {
+//             let tree_branch = *theme.tree_branch.in_state(is_selected).style_for(state);
+//             let row = theme.project_row.in_state(is_selected).style_for(state);
+
+//             Flex::<Self>::row()
+//                 .with_child(render_tree_branch(
+//                     tree_branch,
+//                     &row.name.text,
+//                     false,
+//                     vec2f(host_avatar_width, theme.row_height),
+//                     cx.font_cache(),
+//                 ))
+//                 .with_child(
+//                     Svg::new("icons/file.svg")
+//                         .with_color(theme.channel_hash.color)
+//                         .constrained()
+//                         .with_width(theme.channel_hash.width)
+//                         .aligned()
+//                         .left(),
+//                 )
+//                 .with_child(
+//                     Label::new("notes", theme.channel_name.text.clone())
+//                         .contained()
+//                         .with_style(theme.channel_name.container)
+//                         .aligned()
+//                         .left()
+//                         .flex(1., true),
+//                 )
+//                 .constrained()
+//                 .with_height(theme.row_height)
+//                 .contained()
+//                 .with_style(*theme.channel_row.style_for(is_selected, state))
+//                 .with_padding_left(theme.channel_row.default_style().padding.left)
+//         })
+//         .on_click(MouseButton::Left, move |_, this, cx| {
+//             this.open_channel_notes(&OpenChannelNotes { channel_id }, cx);
+//         })
+//         .with_cursor_style(CursorStyle::PointingHand)
+//         .into_any()
+//     }
+
+//     fn render_channel_chat(
+//         &self,
+//         channel_id: ChannelId,
+//         theme: &theme::CollabPanel,
+//         is_selected: bool,
+//         ix: usize,
+//         cx: &mut ViewContext<Self>,
+//     ) -> AnyElement<Self> {
+//         enum ChannelChat {}
+//         let host_avatar_width = theme
+//             .contact_avatar
+//             .width
+//             .or(theme.contact_avatar.height)
+//             .unwrap_or(0.);
+
+//         MouseEventHandler::new::<ChannelChat, _>(ix as usize, cx, |state, cx| {
+//             let tree_branch = *theme.tree_branch.in_state(is_selected).style_for(state);
+//             let row = theme.project_row.in_state(is_selected).style_for(state);
+
+//             Flex::<Self>::row()
+//                 .with_child(render_tree_branch(
+//                     tree_branch,
+//                     &row.name.text,
+//                     true,
+//                     vec2f(host_avatar_width, theme.row_height),
+//                     cx.font_cache(),
+//                 ))
+//                 .with_child(
+//                     Svg::new("icons/conversations.svg")
+//                         .with_color(theme.channel_hash.color)
+//                         .constrained()
+//                         .with_width(theme.channel_hash.width)
+//                         .aligned()
+//                         .left(),
+//                 )
+//                 .with_child(
+//                     Label::new("chat", theme.channel_name.text.clone())
+//                         .contained()
+//                         .with_style(theme.channel_name.container)
+//                         .aligned()
+//                         .left()
+//                         .flex(1., true),
+//                 )
+//                 .constrained()
+//                 .with_height(theme.row_height)
+//                 .contained()
+//                 .with_style(*theme.channel_row.style_for(is_selected, state))
+//                 .with_padding_left(theme.channel_row.default_style().padding.left)
+//         })
+//         .on_click(MouseButton::Left, move |_, this, cx| {
+//             this.join_channel_chat(&JoinChannelChat { channel_id }, cx);
+//         })
+//         .with_cursor_style(CursorStyle::PointingHand)
+//         .into_any()
+//     }
+
+//     fn render_channel_invite(
+//         channel: Arc<Channel>,
+//         channel_store: ModelHandle<ChannelStore>,
+//         theme: &theme::CollabPanel,
+//         is_selected: bool,
+//         cx: &mut ViewContext<Self>,
+//     ) -> AnyElement<Self> {
+//         enum Decline {}
+//         enum Accept {}
+
+//         let channel_id = channel.id;
+//         let is_invite_pending = channel_store
+//             .read(cx)
+//             .has_pending_channel_invite_response(&channel);
+//         let button_spacing = theme.contact_button_spacing;
+
+//         Flex::row()
+//             .with_child(
+//                 Svg::new("icons/hash.svg")
+//                     .with_color(theme.channel_hash.color)
+//                     .constrained()
+//                     .with_width(theme.channel_hash.width)
+//                     .aligned()
+//                     .left(),
+//             )
+//             .with_child(
+//                 Label::new(channel.name.clone(), theme.contact_username.text.clone())
+//                     .contained()
+//                     .with_style(theme.contact_username.container)
+//                     .aligned()
+//                     .left()
+//                     .flex(1., true),
+//             )
+//             .with_child(
+//                 MouseEventHandler::new::<Decline, _>(channel.id as usize, cx, |mouse_state, _| {
+//                     let button_style = if is_invite_pending {
+//                         &theme.disabled_button
+//                     } else {
+//                         theme.contact_button.style_for(mouse_state)
+//                     };
+//                     render_icon_button(button_style, "icons/x.svg").aligned()
+//                 })
+//                 .with_cursor_style(CursorStyle::PointingHand)
+//                 .on_click(MouseButton::Left, move |_, this, cx| {
+//                     this.respond_to_channel_invite(channel_id, false, cx);
+//                 })
+//                 .contained()
+//                 .with_margin_right(button_spacing),
+//             )
+//             .with_child(
+//                 MouseEventHandler::new::<Accept, _>(channel.id as usize, cx, |mouse_state, _| {
+//                     let button_style = if is_invite_pending {
+//                         &theme.disabled_button
+//                     } else {
+//                         theme.contact_button.style_for(mouse_state)
+//                     };
+//                     render_icon_button(button_style, "icons/check.svg")
+//                         .aligned()
+//                         .flex_float()
+//                 })
+//                 .with_cursor_style(CursorStyle::PointingHand)
+//                 .on_click(MouseButton::Left, move |_, this, cx| {
+//                     this.respond_to_channel_invite(channel_id, true, cx);
+//                 }),
+//             )
+//             .constrained()
+//             .with_height(theme.row_height)
+//             .contained()
+//             .with_style(
+//                 *theme
+//                     .contact_row
+//                     .in_state(is_selected)
+//                     .style_for(&mut Default::default()),
+//             )
+//             .with_padding_left(
+//                 theme.contact_row.default_style().padding.left + theme.channel_indent,
+//             )
+//             .into_any()
+//     }
+
+//     fn render_contact_request(
+//         user: Arc<User>,
+//         user_store: ModelHandle<UserStore>,
+//         theme: &theme::CollabPanel,
+//         is_incoming: bool,
+//         is_selected: bool,
+//         cx: &mut ViewContext<Self>,
+//     ) -> AnyElement<Self> {
+//         enum Decline {}
+//         enum Accept {}
+//         enum Cancel {}
+
+//         let mut row = Flex::row()
+//             .with_children(user.avatar.clone().map(|avatar| {
+//                 Image::from_data(avatar)
+//                     .with_style(theme.contact_avatar)
+//                     .aligned()
+//                     .left()
+//             }))
+//             .with_child(
+//                 Label::new(
+//                     user.github_login.clone(),
+//                     theme.contact_username.text.clone(),
+//                 )
+//                 .contained()
+//                 .with_style(theme.contact_username.container)
+//                 .aligned()
+//                 .left()
+//                 .flex(1., true),
+//             );
+
+//         let user_id = user.id;
+//         let github_login = user.github_login.clone();
+//         let is_contact_request_pending = user_store.read(cx).is_contact_request_pending(&user);
+//         let button_spacing = theme.contact_button_spacing;
+
+//         if is_incoming {
+//             row.add_child(
+//                 MouseEventHandler::new::<Decline, _>(user.id as usize, cx, |mouse_state, _| {
+//                     let button_style = if is_contact_request_pending {
+//                         &theme.disabled_button
+//                     } else {
+//                         theme.contact_button.style_for(mouse_state)
+//                     };
+//                     render_icon_button(button_style, "icons/x.svg").aligned()
+//                 })
+//                 .with_cursor_style(CursorStyle::PointingHand)
+//                 .on_click(MouseButton::Left, move |_, this, cx| {
+//                     this.respond_to_contact_request(user_id, false, cx);
+//                 })
+//                 .contained()
+//                 .with_margin_right(button_spacing),
+//             );
+
+//             row.add_child(
+//                 MouseEventHandler::new::<Accept, _>(user.id as usize, cx, |mouse_state, _| {
+//                     let button_style = if is_contact_request_pending {
+//                         &theme.disabled_button
+//                     } else {
+//                         theme.contact_button.style_for(mouse_state)
+//                     };
+//                     render_icon_button(button_style, "icons/check.svg")
+//                         .aligned()
+//                         .flex_float()
+//                 })
+//                 .with_cursor_style(CursorStyle::PointingHand)
+//                 .on_click(MouseButton::Left, move |_, this, cx| {
+//                     this.respond_to_contact_request(user_id, true, cx);
+//                 }),
+//             );
+//         } else {
+//             row.add_child(
+//                 MouseEventHandler::new::<Cancel, _>(user.id as usize, cx, |mouse_state, _| {
+//                     let button_style = if is_contact_request_pending {
+//                         &theme.disabled_button
+//                     } else {
+//                         theme.contact_button.style_for(mouse_state)
+//                     };
+//                     render_icon_button(button_style, "icons/x.svg")
+//                         .aligned()
+//                         .flex_float()
+//                 })
+//                 .with_padding(Padding::uniform(2.))
+//                 .with_cursor_style(CursorStyle::PointingHand)
+//                 .on_click(MouseButton::Left, move |_, this, cx| {
+//                     this.remove_contact(user_id, &github_login, cx);
+//                 })
+//                 .flex_float(),
+//             );
+//         }
+
+//         row.constrained()
+//             .with_height(theme.row_height)
+//             .contained()
+//             .with_style(
+//                 *theme
+//                     .contact_row
+//                     .in_state(is_selected)
+//                     .style_for(&mut Default::default()),
+//             )
+//             .into_any()
+//     }
+
+//     fn has_subchannels(&self, ix: usize) -> bool {
+//         self.entries.get(ix).map_or(false, |entry| {
+//             if let ListEntry::Channel { has_children, .. } = entry {
+//                 *has_children
+//             } else {
+//                 false
+//             }
+//         })
+//     }
+
+//     fn deploy_channel_context_menu(
+//         &mut self,
+//         position: Option<Vector2F>,
+//         channel: &Channel,
+//         ix: usize,
+//         cx: &mut ViewContext<Self>,
+//     ) {
+//         self.context_menu_on_selected = position.is_none();
+
+//         let clipboard_channel_name = self.channel_clipboard.as_ref().and_then(|clipboard| {
+//             self.channel_store
+//                 .read(cx)
+//                 .channel_for_id(clipboard.channel_id)
+//                 .map(|channel| channel.name.clone())
+//         });
+
+//         self.context_menu.update(cx, |context_menu, cx| {
+//             context_menu.set_position_mode(if self.context_menu_on_selected {
+//                 OverlayPositionMode::Local
+//             } else {
+//                 OverlayPositionMode::Window
+//             });
+
+//             let mut items = Vec::new();
+
+//             let select_action_name = if self.selection == Some(ix) {
+//                 "Unselect"
+//             } else {
+//                 "Select"
+//             };
+
+//             items.push(ContextMenuItem::action(
+//                 select_action_name,
+//                 ToggleSelectedIx { ix },
+//             ));
+
+//             if self.has_subchannels(ix) {
+//                 let expand_action_name = if self.is_channel_collapsed(channel.id) {
+//                     "Expand Subchannels"
+//                 } else {
+//                     "Collapse Subchannels"
+//                 };
+//                 items.push(ContextMenuItem::action(
+//                     expand_action_name,
+//                     ToggleCollapse {
+//                         location: channel.id,
+//                     },
+//                 ));
+//             }
+
+//             items.push(ContextMenuItem::action(
+//                 "Open Notes",
+//                 OpenChannelNotes {
+//                     channel_id: channel.id,
+//                 },
+//             ));
+
+//             items.push(ContextMenuItem::action(
+//                 "Open Chat",
+//                 JoinChannelChat {
+//                     channel_id: channel.id,
+//                 },
+//             ));
+
+//             items.push(ContextMenuItem::action(
+//                 "Copy Channel Link",
+//                 CopyChannelLink {
+//                     channel_id: channel.id,
+//                 },
+//             ));
+
+//             if self.channel_store.read(cx).is_channel_admin(channel.id) {
+//                 items.extend([
+//                     ContextMenuItem::Separator,
+//                     ContextMenuItem::action(
+//                         "New Subchannel",
+//                         NewChannel {
+//                             location: channel.id,
+//                         },
+//                     ),
+//                     ContextMenuItem::action(
+//                         "Rename",
+//                         RenameChannel {
+//                             channel_id: channel.id,
+//                         },
+//                     ),
+//                     ContextMenuItem::action(
+//                         "Move this channel",
+//                         StartMoveChannelFor {
+//                             channel_id: channel.id,
+//                         },
+//                     ),
+//                 ]);
+
+//                 if let Some(channel_name) = clipboard_channel_name {
+//                     items.push(ContextMenuItem::Separator);
+//                     items.push(ContextMenuItem::action(
+//                         format!("Move '#{}' here", channel_name),
+//                         MoveChannel { to: channel.id },
+//                     ));
+//                 }
+
+//                 items.extend([
+//                     ContextMenuItem::Separator,
+//                     ContextMenuItem::action(
+//                         "Invite Members",
+//                         InviteMembers {
+//                             channel_id: channel.id,
+//                         },
+//                     ),
+//                     ContextMenuItem::action(
+//                         "Manage Members",
+//                         ManageMembers {
+//                             channel_id: channel.id,
+//                         },
+//                     ),
+//                     ContextMenuItem::Separator,
+//                     ContextMenuItem::action(
+//                         "Delete",
+//                         RemoveChannel {
+//                             channel_id: channel.id,
+//                         },
+//                     ),
+//                 ]);
+//             }
+
+//             context_menu.show(
+//                 position.unwrap_or_default(),
+//                 if self.context_menu_on_selected {
+//                     gpui::elements::AnchorCorner::TopRight
+//                 } else {
+//                     gpui::elements::AnchorCorner::BottomLeft
+//                 },
+//                 items,
+//                 cx,
+//             );
+//         });
+
+//         cx.notify();
+//     }
+
+//     fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
+//         if self.take_editing_state(cx) {
+//             cx.focus(&self.filter_editor);
+//         } else {
+//             self.filter_editor.update(cx, |editor, cx| {
+//                 if editor.buffer().read(cx).len(cx) > 0 {
+//                     editor.set_text("", cx);
+//                 }
+//             });
+//         }
+
+//         self.update_entries(false, cx);
+//     }
+
+//     fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
+//         let ix = self.selection.map_or(0, |ix| ix + 1);
+//         if ix < self.entries.len() {
+//             self.selection = Some(ix);
+//         }
+
+//         self.list_state.reset(self.entries.len());
+//         if let Some(ix) = self.selection {
+//             self.list_state.scroll_to(ListOffset {
+//                 item_ix: ix,
+//                 offset_in_item: 0.,
+//             });
+//         }
+//         cx.notify();
+//     }
+
+//     fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
+//         let ix = self.selection.take().unwrap_or(0);
+//         if ix > 0 {
+//             self.selection = Some(ix - 1);
+//         }
+
+//         self.list_state.reset(self.entries.len());
+//         if let Some(ix) = self.selection {
+//             self.list_state.scroll_to(ListOffset {
+//                 item_ix: ix,
+//                 offset_in_item: 0.,
+//             });
+//         }
+//         cx.notify();
+//     }
+
+//     fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
+//         if self.confirm_channel_edit(cx) {
+//             return;
+//         }
+
+//         if let Some(selection) = self.selection {
+//             if let Some(entry) = self.entries.get(selection) {
+//                 match entry {
+//                     ListEntry::Header(section) => match section {
+//                         Section::ActiveCall => Self::leave_call(cx),
+//                         Section::Channels => self.new_root_channel(cx),
+//                         Section::Contacts => self.toggle_contact_finder(cx),
+//                         Section::ContactRequests
+//                         | Section::Online
+//                         | Section::Offline
+//                         | Section::ChannelInvites => {
+//                             self.toggle_section_expanded(*section, cx);
+//                         }
+//                     },
+//                     ListEntry::Contact { contact, calling } => {
+//                         if contact.online && !contact.busy && !calling {
+//                             self.call(contact.user.id, Some(self.project.clone()), cx);
+//                         }
+//                     }
+//                     ListEntry::ParticipantProject {
+//                         project_id,
+//                         host_user_id,
+//                         ..
+//                     } => {
+//                         if let Some(workspace) = self.workspace.upgrade(cx) {
+//                             let app_state = workspace.read(cx).app_state().clone();
+//                             workspace::join_remote_project(
+//                                 *project_id,
+//                                 *host_user_id,
+//                                 app_state,
+//                                 cx,
+//                             )
+//                             .detach_and_log_err(cx);
+//                         }
+//                     }
+//                     ListEntry::ParticipantScreen { peer_id, .. } => {
+//                         let Some(peer_id) = peer_id else {
+//                             return;
+//                         };
+//                         if let Some(workspace) = self.workspace.upgrade(cx) {
+//                             workspace.update(cx, |workspace, cx| {
+//                                 workspace.open_shared_screen(*peer_id, cx)
+//                             });
+//                         }
+//                     }
+//                     ListEntry::Channel { channel, .. } => {
+//                         let is_active = maybe!({
+//                             let call_channel = ActiveCall::global(cx)
+//                                 .read(cx)
+//                                 .room()?
+//                                 .read(cx)
+//                                 .channel_id()?;
+
+//                             Some(call_channel == channel.id)
+//                         })
+//                         .unwrap_or(false);
+//                         if is_active {
+//                             self.open_channel_notes(
+//                                 &OpenChannelNotes {
+//                                     channel_id: channel.id,
+//                                 },
+//                                 cx,
+//                             )
+//                         } else {
+//                             self.join_channel(channel.id, cx)
+//                         }
+//                     }
+//                     ListEntry::ContactPlaceholder => self.toggle_contact_finder(cx),
+//                     _ => {}
+//                 }
+//             }
+//         }
+//     }
+
+//     fn insert_space(&mut self, _: &InsertSpace, cx: &mut ViewContext<Self>) {
+//         if self.channel_editing_state.is_some() {
+//             self.channel_name_editor.update(cx, |editor, cx| {
+//                 editor.insert(" ", cx);
+//             });
+//         }
+//     }
+
+//     fn confirm_channel_edit(&mut self, cx: &mut ViewContext<CollabPanel>) -> bool {
+//         if let Some(editing_state) = &mut self.channel_editing_state {
+//             match editing_state {
+//                 ChannelEditingState::Create {
+//                     location,
+//                     pending_name,
+//                     ..
+//                 } => {
+//                     if pending_name.is_some() {
+//                         return false;
+//                     }
+//                     let channel_name = self.channel_name_editor.read(cx).text(cx);
+
+//                     *pending_name = Some(channel_name.clone());
+
+//                     self.channel_store
+//                         .update(cx, |channel_store, cx| {
+//                             channel_store.create_channel(&channel_name, *location, cx)
+//                         })
+//                         .detach();
+//                     cx.notify();
+//                 }
+//                 ChannelEditingState::Rename {
+//                     location,
+//                     pending_name,
+//                 } => {
+//                     if pending_name.is_some() {
+//                         return false;
+//                     }
+//                     let channel_name = self.channel_name_editor.read(cx).text(cx);
+//                     *pending_name = Some(channel_name.clone());
+
+//                     self.channel_store
+//                         .update(cx, |channel_store, cx| {
+//                             channel_store.rename(*location, &channel_name, cx)
+//                         })
+//                         .detach();
+//                     cx.notify();
+//                 }
+//             }
+//             cx.focus_self();
+//             true
+//         } else {
+//             false
+//         }
+//     }
+
+//     fn toggle_section_expanded(&mut self, section: Section, cx: &mut ViewContext<Self>) {
+//         if let Some(ix) = self.collapsed_sections.iter().position(|s| *s == section) {
+//             self.collapsed_sections.remove(ix);
+//         } else {
+//             self.collapsed_sections.push(section);
+//         }
+//         self.update_entries(false, cx);
+//     }
+
+//     fn collapse_selected_channel(
+//         &mut self,
+//         _: &CollapseSelectedChannel,
+//         cx: &mut ViewContext<Self>,
+//     ) {
+//         let Some(channel_id) = self.selected_channel().map(|channel| channel.id) else {
+//             return;
+//         };
+
+//         if self.is_channel_collapsed(channel_id) {
+//             return;
+//         }
+
+//         self.toggle_channel_collapsed(channel_id, cx);
+//     }
+
+//     fn expand_selected_channel(&mut self, _: &ExpandSelectedChannel, cx: &mut ViewContext<Self>) {
+//         let Some(id) = self.selected_channel().map(|channel| channel.id) else {
+//             return;
+//         };
+
+//         if !self.is_channel_collapsed(id) {
+//             return;
+//         }
+
+//         self.toggle_channel_collapsed(id, cx)
+//     }
+
+//     fn toggle_channel_collapsed_action(
+//         &mut self,
+//         action: &ToggleCollapse,
+//         cx: &mut ViewContext<Self>,
+//     ) {
+//         self.toggle_channel_collapsed(action.location, cx);
+//     }
+
+//     fn toggle_channel_collapsed<'a>(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
+//         match self.collapsed_channels.binary_search(&channel_id) {
+//             Ok(ix) => {
+//                 self.collapsed_channels.remove(ix);
+//             }
+//             Err(ix) => {
+//                 self.collapsed_channels.insert(ix, channel_id);
+//             }
+//         };
+//         self.serialize(cx);
+//         self.update_entries(true, cx);
+//         cx.notify();
+//         cx.focus_self();
+//     }
+
+//     fn is_channel_collapsed(&self, channel_id: ChannelId) -> bool {
+//         self.collapsed_channels.binary_search(&channel_id).is_ok()
+//     }
+
+//     fn leave_call(cx: &mut ViewContext<Self>) {
+//         ActiveCall::global(cx)
+//             .update(cx, |call, cx| call.hang_up(cx))
+//             .detach_and_log_err(cx);
+//     }
+
+//     fn toggle_contact_finder(&mut self, cx: &mut ViewContext<Self>) {
+//         if let Some(workspace) = self.workspace.upgrade(cx) {
+//             workspace.update(cx, |workspace, cx| {
+//                 workspace.toggle_modal(cx, |_, cx| {
+//                     cx.add_view(|cx| {
+//                         let mut finder = ContactFinder::new(self.user_store.clone(), cx);
+//                         finder.set_query(self.filter_editor.read(cx).text(cx), cx);
+//                         finder
+//                     })
+//                 });
+//             });
+//         }
+//     }
+
+//     fn new_root_channel(&mut self, cx: &mut ViewContext<Self>) {
+//         self.channel_editing_state = Some(ChannelEditingState::Create {
+//             location: None,
+//             pending_name: None,
+//         });
+//         self.update_entries(false, cx);
+//         self.select_channel_editor();
+//         cx.focus(self.channel_name_editor.as_any());
+//         cx.notify();
+//     }
+
+//     fn select_channel_editor(&mut self) {
+//         self.selection = self.entries.iter().position(|entry| match entry {
+//             ListEntry::ChannelEditor { .. } => true,
+//             _ => false,
+//         });
+//     }
+
+//     fn new_subchannel(&mut self, action: &NewChannel, cx: &mut ViewContext<Self>) {
+//         self.collapsed_channels
+//             .retain(|channel| *channel != action.location);
+//         self.channel_editing_state = Some(ChannelEditingState::Create {
+//             location: Some(action.location.to_owned()),
+//             pending_name: None,
+//         });
+//         self.update_entries(false, cx);
+//         self.select_channel_editor();
+//         cx.focus(self.channel_name_editor.as_any());
+//         cx.notify();
+//     }
+
+//     fn invite_members(&mut self, action: &InviteMembers, cx: &mut ViewContext<Self>) {
+//         self.show_channel_modal(action.channel_id, channel_modal::Mode::InviteMembers, cx);
+//     }
+
+//     fn manage_members(&mut self, action: &ManageMembers, cx: &mut ViewContext<Self>) {
+//         self.show_channel_modal(action.channel_id, channel_modal::Mode::ManageMembers, cx);
+//     }
+
+//     fn remove(&mut self, _: &Remove, cx: &mut ViewContext<Self>) {
+//         if let Some(channel) = self.selected_channel() {
+//             self.remove_channel(channel.id, cx)
+//         }
+//     }
+
+//     fn rename_selected_channel(&mut self, _: &menu::SecondaryConfirm, cx: &mut ViewContext<Self>) {
+//         if let Some(channel) = self.selected_channel() {
+//             self.rename_channel(
+//                 &RenameChannel {
+//                     channel_id: channel.id,
+//                 },
+//                 cx,
+//             );
+//         }
+//     }
+
+//     fn rename_channel(&mut self, action: &RenameChannel, cx: &mut ViewContext<Self>) {
+//         let channel_store = self.channel_store.read(cx);
+//         if !channel_store.is_channel_admin(action.channel_id) {
+//             return;
+//         }
+//         if let Some(channel) = channel_store.channel_for_id(action.channel_id).cloned() {
+//             self.channel_editing_state = Some(ChannelEditingState::Rename {
+//                 location: action.channel_id.to_owned(),
+//                 pending_name: None,
+//             });
+//             self.channel_name_editor.update(cx, |editor, cx| {
+//                 editor.set_text(channel.name.clone(), cx);
+//                 editor.select_all(&Default::default(), cx);
+//             });
+//             cx.focus(self.channel_name_editor.as_any());
+//             self.update_entries(false, cx);
+//             self.select_channel_editor();
+//         }
+//     }
+
+//     fn open_channel_notes(&mut self, action: &OpenChannelNotes, cx: &mut ViewContext<Self>) {
+//         if let Some(workspace) = self.workspace.upgrade(cx) {
+//             ChannelView::open(action.channel_id, workspace, cx).detach();
+//         }
+//     }
+
+//     fn show_inline_context_menu(&mut self, _: &menu::ShowContextMenu, cx: &mut ViewContext<Self>) {
+//         let Some(channel) = self.selected_channel() else {
+//             return;
+//         };
+
+//         self.deploy_channel_context_menu(None, &channel.clone(), self.selection.unwrap(), cx);
+//     }
+
+//     fn selected_channel(&self) -> Option<&Arc<Channel>> {
+//         self.selection
+//             .and_then(|ix| self.entries.get(ix))
+//             .and_then(|entry| match entry {
+//                 ListEntry::Channel { channel, .. } => Some(channel),
+//                 _ => None,
+//             })
+//     }
+
+//     fn show_channel_modal(
+//         &mut self,
+//         channel_id: ChannelId,
+//         mode: channel_modal::Mode,
+//         cx: &mut ViewContext<Self>,
+//     ) {
+//         let workspace = self.workspace.clone();
+//         let user_store = self.user_store.clone();
+//         let channel_store = self.channel_store.clone();
+//         let members = self.channel_store.update(cx, |channel_store, cx| {
+//             channel_store.get_channel_member_details(channel_id, cx)
+//         });
+
+//         cx.spawn(|_, mut cx| async move {
+//             let members = members.await?;
+//             workspace.update(&mut cx, |workspace, cx| {
+//                 workspace.toggle_modal(cx, |_, cx| {
+//                     cx.add_view(|cx| {
+//                         ChannelModal::new(
+//                             user_store.clone(),
+//                             channel_store.clone(),
+//                             channel_id,
+//                             mode,
+//                             members,
+//                             cx,
+//                         )
+//                     })
+//                 });
+//             })
+//         })
+//         .detach();
+//     }
+
+//     fn remove_selected_channel(&mut self, action: &RemoveChannel, cx: &mut ViewContext<Self>) {
+//         self.remove_channel(action.channel_id, cx)
+//     }
+
+//     fn remove_channel(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
+//         let channel_store = self.channel_store.clone();
+//         if let Some(channel) = channel_store.read(cx).channel_for_id(channel_id) {
+//             let prompt_message = format!(
+//                 "Are you sure you want to remove the channel \"{}\"?",
+//                 channel.name
+//             );
+//             let mut answer =
+//                 cx.prompt(PromptLevel::Warning, &prompt_message, &["Remove", "Cancel"]);
+//             let window = cx.window();
+//             cx.spawn(|this, mut cx| async move {
+//                 if answer.next().await == Some(0) {
+//                     if let Err(e) = channel_store
+//                         .update(&mut cx, |channels, _| channels.remove_channel(channel_id))
+//                         .await
+//                     {
+//                         window.prompt(
+//                             PromptLevel::Info,
+//                             &format!("Failed to remove channel: {}", e),
+//                             &["Ok"],
+//                             &mut cx,
+//                         );
+//                     }
+//                     this.update(&mut cx, |_, cx| cx.focus_self()).ok();
+//                 }
+//             })
+//             .detach();
+//         }
+//     }
+
+//     // Should move to the filter editor if clicking on it
+//     // Should move selection to the channel editor if activating it
+
+//     fn remove_contact(&mut self, user_id: u64, github_login: &str, cx: &mut ViewContext<Self>) {
+//         let user_store = self.user_store.clone();
+//         let prompt_message = format!(
+//             "Are you sure you want to remove \"{}\" from your contacts?",
+//             github_login
+//         );
+//         let mut answer = cx.prompt(PromptLevel::Warning, &prompt_message, &["Remove", "Cancel"]);
+//         let window = cx.window();
+//         cx.spawn(|_, mut cx| async move {
+//             if answer.next().await == Some(0) {
+//                 if let Err(e) = user_store
+//                     .update(&mut cx, |store, cx| store.remove_contact(user_id, cx))
+//                     .await
+//                 {
+//                     window.prompt(
+//                         PromptLevel::Info,
+//                         &format!("Failed to remove contact: {}", e),
+//                         &["Ok"],
+//                         &mut cx,
+//                     );
+//                 }
+//             }
+//         })
+//         .detach();
+//     }
+
+//     fn respond_to_contact_request(
+//         &mut self,
+//         user_id: u64,
+//         accept: bool,
+//         cx: &mut ViewContext<Self>,
+//     ) {
+//         self.user_store
+//             .update(cx, |store, cx| {
+//                 store.respond_to_contact_request(user_id, accept, cx)
+//             })
+//             .detach();
+//     }
+
+//     fn respond_to_channel_invite(
+//         &mut self,
+//         channel_id: u64,
+//         accept: bool,
+//         cx: &mut ViewContext<Self>,
+//     ) {
+//         self.channel_store
+//             .update(cx, |store, cx| {
+//                 store.respond_to_channel_invite(channel_id, accept, cx)
+//             })
+//             .detach();
+//     }
+
+//     fn call(
+//         &mut self,
+//         recipient_user_id: u64,
+//         initial_project: Option<ModelHandle<Project>>,
+//         cx: &mut ViewContext<Self>,
+//     ) {
+//         ActiveCall::global(cx)
+//             .update(cx, |call, cx| {
+//                 call.invite(recipient_user_id, initial_project, cx)
+//             })
+//             .detach_and_log_err(cx);
+//     }
+
+//     fn join_channel(&self, channel_id: u64, cx: &mut ViewContext<Self>) {
+//         let Some(workspace) = self.workspace.upgrade(cx) else {
+//             return;
+//         };
+//         let Some(handle) = cx.window().downcast::<Workspace>() else {
+//             return;
+//         };
+//         workspace::join_channel(
+//             channel_id,
+//             workspace.read(cx).app_state().clone(),
+//             Some(handle),
+//             cx,
+//         )
+//         .detach_and_log_err(cx)
+//     }
+
+//     fn join_channel_chat(&mut self, action: &JoinChannelChat, cx: &mut ViewContext<Self>) {
+//         let channel_id = action.channel_id;
+//         if let Some(workspace) = self.workspace.upgrade(cx) {
+//             cx.app_context().defer(move |cx| {
+//                 workspace.update(cx, |workspace, cx| {
+//                     if let Some(panel) = workspace.focus_panel::<ChatPanel>(cx) {
+//                         panel.update(cx, |panel, cx| {
+//                             panel
+//                                 .select_channel(channel_id, None, cx)
+//                                 .detach_and_log_err(cx);
+//                         });
+//                     }
+//                 });
+//             });
+//         }
+//     }
+
+//     fn copy_channel_link(&mut self, action: &CopyChannelLink, cx: &mut ViewContext<Self>) {
+//         let channel_store = self.channel_store.read(cx);
+//         let Some(channel) = channel_store.channel_for_id(action.channel_id) else {
+//             return;
+//         };
+//         let item = ClipboardItem::new(channel.link());
+//         cx.write_to_clipboard(item)
+//     }
+// }
+
+// fn render_tree_branch(
+//     branch_style: theme::TreeBranch,
+//     row_style: &TextStyle,
+//     is_last: bool,
+//     size: Vector2F,
+//     font_cache: &FontCache,
+// ) -> gpui::elements::ConstrainedBox<CollabPanel> {
+//     let line_height = row_style.line_height(font_cache);
+//     let cap_height = row_style.cap_height(font_cache);
+//     let baseline_offset = row_style.baseline_offset(font_cache) + (size.y() - line_height) / 2.;
+
+//     Canvas::new(move |bounds, _, _, cx| {
+//         cx.paint_layer(None, |cx| {
+//             let start_x = bounds.min_x() + (bounds.width() / 2.) - (branch_style.width / 2.);
+//             let end_x = bounds.max_x();
+//             let start_y = bounds.min_y();
+//             let end_y = bounds.min_y() + baseline_offset - (cap_height / 2.);
+
+//             cx.scene().push_quad(gpui::Quad {
+//                 bounds: RectF::from_points(
+//                     vec2f(start_x, start_y),
+//                     vec2f(
+//                         start_x + branch_style.width,
+//                         if is_last { end_y } else { bounds.max_y() },
+//                     ),
+//                 ),
+//                 background: Some(branch_style.color),
+//                 border: gpui::Border::default(),
+//                 corner_radii: (0.).into(),
+//             });
+//             cx.scene().push_quad(gpui::Quad {
+//                 bounds: RectF::from_points(
+//                     vec2f(start_x, end_y),
+//                     vec2f(end_x, end_y + branch_style.width),
+//                 ),
+//                 background: Some(branch_style.color),
+//                 border: gpui::Border::default(),
+//                 corner_radii: (0.).into(),
+//             });
+//         })
+//     })
+//     .constrained()
+//     .with_width(size.x())
+// }
+
+// impl View for CollabPanel {
+//     fn ui_name() -> &'static str {
+//         "CollabPanel"
+//     }
+
+//     fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
+//         if !self.has_focus {
+//             self.has_focus = true;
+//             if !self.context_menu.is_focused(cx) {
+//                 if let Some(editing_state) = &self.channel_editing_state {
+//                     if editing_state.pending_name().is_none() {
+//                         cx.focus(&self.channel_name_editor);
+//                     } else {
+//                         cx.focus(&self.filter_editor);
+//                     }
+//                 } else {
+//                     cx.focus(&self.filter_editor);
+//                 }
+//             }
+//             cx.emit(Event::Focus);
+//         }
+//     }
+
+//     fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext<Self>) {
+//         self.has_focus = false;
+//     }
+
+//     fn render(&mut self, cx: &mut gpui::ViewContext<'_, '_, Self>) -> gpui::AnyElement<Self> {
+//         let theme = &theme::current(cx).collab_panel;
+
+//         if self.user_store.read(cx).current_user().is_none() {
+//             enum LogInButton {}
+
+//             return Flex::column()
+//                 .with_child(
+//                     MouseEventHandler::new::<LogInButton, _>(0, cx, |state, _| {
+//                         let button = theme.log_in_button.style_for(state);
+//                         Label::new("Sign in to collaborate", button.text.clone())
+//                             .aligned()
+//                             .left()
+//                             .contained()
+//                             .with_style(button.container)
+//                     })
+//                     .on_click(MouseButton::Left, |_, this, cx| {
+//                         let client = this.client.clone();
+//                         cx.spawn(|_, cx| async move {
+//                             client.authenticate_and_connect(true, &cx).await.log_err();
+//                         })
+//                         .detach();
+//                     })
+//                     .with_cursor_style(CursorStyle::PointingHand),
+//                 )
+//                 .contained()
+//                 .with_style(theme.container)
+//                 .into_any();
+//         }
+
+//         enum PanelFocus {}
+//         MouseEventHandler::new::<PanelFocus, _>(0, cx, |_, cx| {
+//             Stack::new()
+//                 .with_child(
+//                     Flex::column()
+//                         .with_child(
+//                             Flex::row().with_child(
+//                                 ChildView::new(&self.filter_editor, cx)
+//                                     .contained()
+//                                     .with_style(theme.user_query_editor.container)
+//                                     .flex(1.0, true),
+//                             ),
+//                         )
+//                         .with_child(List::new(self.list_state.clone()).flex(1., true).into_any())
+//                         .contained()
+//                         .with_style(theme.container)
+//                         .into_any(),
+//                 )
+//                 .with_children(
+//                     (!self.context_menu_on_selected)
+//                         .then(|| ChildView::new(&self.context_menu, cx)),
+//                 )
+//                 .into_any()
+//         })
+//         .on_click(MouseButton::Left, |_, _, cx| cx.focus_self())
+//         .into_any_named("collab panel")
+//     }
+
+//     fn update_keymap_context(
+//         &self,
+//         keymap: &mut gpui::keymap_matcher::KeymapContext,
+//         _: &AppContext,
+//     ) {
+//         Self::reset_to_default_keymap_context(keymap);
+//         if self.channel_editing_state.is_some() {
+//             keymap.add_identifier("editing");
+//         } else {
+//             keymap.add_identifier("not_editing");
+//         }
+//     }
+// }
+
+// impl Panel for CollabPanel {
+//     fn position(&self, cx: &gpui::WindowContext) -> DockPosition {
+//         settings::get::<CollaborationPanelSettings>(cx).dock
+//     }
+
+//     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| settings.dock = Some(position),
+//         );
+//     }
+
+//     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/user_group_16.svg")
+//     }
+
+//     fn icon_tooltip(&self) -> (String, Option<Box<dyn gpui::Action>>) {
+//         (
+//             "Collaboration 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)
+//     }
+// }
+
+// impl PartialEq for ListEntry {
+//     fn eq(&self, other: &Self) -> bool {
+//         match self {
+//             ListEntry::Header(section_1) => {
+//                 if let ListEntry::Header(section_2) = other {
+//                     return section_1 == section_2;
+//                 }
+//             }
+//             ListEntry::CallParticipant { user: user_1, .. } => {
+//                 if let ListEntry::CallParticipant { user: user_2, .. } = other {
+//                     return user_1.id == user_2.id;
+//                 }
+//             }
+//             ListEntry::ParticipantProject {
+//                 project_id: project_id_1,
+//                 ..
+//             } => {
+//                 if let ListEntry::ParticipantProject {
+//                     project_id: project_id_2,
+//                     ..
+//                 } = other
+//                 {
+//                     return project_id_1 == project_id_2;
+//                 }
+//             }
+//             ListEntry::ParticipantScreen {
+//                 peer_id: peer_id_1, ..
+//             } => {
+//                 if let ListEntry::ParticipantScreen {
+//                     peer_id: peer_id_2, ..
+//                 } = other
+//                 {
+//                     return peer_id_1 == peer_id_2;
+//                 }
+//             }
+//             ListEntry::Channel {
+//                 channel: channel_1, ..
+//             } => {
+//                 if let ListEntry::Channel {
+//                     channel: channel_2, ..
+//                 } = other
+//                 {
+//                     return channel_1.id == channel_2.id;
+//                 }
+//             }
+//             ListEntry::ChannelNotes { channel_id } => {
+//                 if let ListEntry::ChannelNotes {
+//                     channel_id: other_id,
+//                 } = other
+//                 {
+//                     return channel_id == other_id;
+//                 }
+//             }
+//             ListEntry::ChannelChat { channel_id } => {
+//                 if let ListEntry::ChannelChat {
+//                     channel_id: other_id,
+//                 } = other
+//                 {
+//                     return channel_id == other_id;
+//                 }
+//             }
+//             ListEntry::ChannelInvite(channel_1) => {
+//                 if let ListEntry::ChannelInvite(channel_2) = other {
+//                     return channel_1.id == channel_2.id;
+//                 }
+//             }
+//             ListEntry::IncomingRequest(user_1) => {
+//                 if let ListEntry::IncomingRequest(user_2) = other {
+//                     return user_1.id == user_2.id;
+//                 }
+//             }
+//             ListEntry::OutgoingRequest(user_1) => {
+//                 if let ListEntry::OutgoingRequest(user_2) = other {
+//                     return user_1.id == user_2.id;
+//                 }
+//             }
+//             ListEntry::Contact {
+//                 contact: contact_1, ..
+//             } => {
+//                 if let ListEntry::Contact {
+//                     contact: contact_2, ..
+//                 } = other
+//                 {
+//                     return contact_1.user.id == contact_2.user.id;
+//                 }
+//             }
+//             ListEntry::ChannelEditor { depth } => {
+//                 if let ListEntry::ChannelEditor { depth: other_depth } = other {
+//                     return depth == other_depth;
+//                 }
+//             }
+//             ListEntry::ContactPlaceholder => {
+//                 if let ListEntry::ContactPlaceholder = other {
+//                     return true;
+//                 }
+//             }
+//         }
+//         false
+//     }
+// }
+
+// fn render_icon_button(style: &IconButton, svg_path: &'static str) -> impl Element<CollabPanel> {
+//     Svg::new(svg_path)
+//         .with_color(style.color)
+//         .constrained()
+//         .with_width(style.icon_width)
+//         .aligned()
+//         .constrained()
+//         .with_width(style.button_width)
+//         .with_height(style.button_width)
+//         .contained()
+//         .with_style(style.container)
+// }

crates/collab_ui2/src/collab_titlebar_item.rs 🔗

@@ -1,162 +1,255 @@
-use crate::{
-    face_pile::FacePile, toggle_deafen, toggle_mute, toggle_screen_sharing, LeaveCall,
-    ToggleDeafen, ToggleMute, ToggleScreenSharing,
-};
-use auto_update::AutoUpdateStatus;
-use call::{ActiveCall, ParticipantLocation, Room};
-use client::{proto::PeerId, Client, SignIn, SignOut, User, UserStore};
-use clock::ReplicaId;
-use context_menu::{ContextMenu, ContextMenuItem};
+// use crate::{
+//     face_pile::FacePile, toggle_deafen, toggle_mute, toggle_screen_sharing, LeaveCall,
+//     ToggleDeafen, ToggleMute, ToggleScreenSharing,
+// };
+// use auto_update::AutoUpdateStatus;
+// use call::{ActiveCall, ParticipantLocation, Room};
+// use client::{proto::PeerId, Client, SignIn, SignOut, User, UserStore};
+// use clock::ReplicaId;
+// use context_menu::{ContextMenu, ContextMenuItem};
+// use gpui::{
+//     actions,
+//     color::Color,
+//     elements::*,
+//     geometry::{rect::RectF, vector::vec2f, PathBuilder},
+//     json::{self, ToJson},
+//     platform::{CursorStyle, MouseButton},
+//     AppContext, Entity, ImageData, ModelHandle, Subscription, View, ViewContext, ViewHandle,
+//     WeakViewHandle,
+// };
+// use picker::PickerEvent;
+// use project::{Project, RepositoryEntry};
+// use recent_projects::{build_recent_projects, RecentProjects};
+// use std::{ops::Range, sync::Arc};
+// use theme::{AvatarStyle, Theme};
+// use util::ResultExt;
+// use vcs_menu::{build_branch_list, BranchList, OpenRecent as ToggleVcsMenu};
+// use workspace::{FollowNextCollaborator, Workspace, WORKSPACE_DB};
+
+use std::sync::Arc;
+
+use call::ActiveCall;
+use client::{Client, UserStore};
 use gpui::{
-    actions,
-    color::Color,
-    elements::*,
-    geometry::{rect::RectF, vector::vec2f, PathBuilder},
-    json::{self, ToJson},
-    platform::{CursorStyle, MouseButton},
-    AppContext, Entity, ImageData, ModelHandle, Subscription, View, ViewContext, ViewHandle,
-    WeakViewHandle,
+    div, rems, AppContext, Component, Div, InteractiveComponent, Model, ParentComponent, Render,
+    Stateful, StatefulInteractiveComponent, Styled, Subscription, ViewContext, VisualContext,
+    WeakView, WindowBounds,
 };
-use picker::PickerEvent;
-use project::{Project, RepositoryEntry};
-use recent_projects::{build_recent_projects, RecentProjects};
-use std::{ops::Range, sync::Arc};
-use theme::{AvatarStyle, Theme};
-use util::ResultExt;
-use vcs_menu::{build_branch_list, BranchList, OpenRecent as ToggleVcsMenu};
-use workspace::{FollowNextCollaborator, Workspace, WORKSPACE_DB};
-
-const MAX_PROJECT_NAME_LENGTH: usize = 40;
-const MAX_BRANCH_NAME_LENGTH: usize = 40;
-
-actions!(
-    collab,
-    [
-        ToggleUserMenu,
-        ToggleProjectMenu,
-        SwitchBranch,
-        ShareProject,
-        UnshareProject,
-    ]
-);
+use project::Project;
+use theme::ActiveTheme;
+use ui::{h_stack, Button, ButtonVariant, KeyBinding, Label, TextColor, TextTooltip};
+use workspace::Workspace;
+
+// const MAX_PROJECT_NAME_LENGTH: usize = 40;
+// const MAX_BRANCH_NAME_LENGTH: usize = 40;
+
+// actions!(
+//     collab,
+//     [
+//         ToggleUserMenu,
+//         ToggleProjectMenu,
+//         SwitchBranch,
+//         ShareProject,
+//         UnshareProject,
+//     ]
+// );
 
 pub fn init(cx: &mut AppContext) {
-    cx.add_action(CollabTitlebarItem::share_project);
-    cx.add_action(CollabTitlebarItem::unshare_project);
-    cx.add_action(CollabTitlebarItem::toggle_user_menu);
-    cx.add_action(CollabTitlebarItem::toggle_vcs_menu);
-    cx.add_action(CollabTitlebarItem::toggle_project_menu);
+    cx.observe_new_views(|workspace: &mut Workspace, cx| {
+        let titlebar_item = cx.build_view(|cx| CollabTitlebarItem::new(workspace, cx));
+        workspace.set_titlebar_item(titlebar_item.into(), cx)
+    })
+    .detach();
+    // cx.add_action(CollabTitlebarItem::share_project);
+    // cx.add_action(CollabTitlebarItem::unshare_project);
+    // cx.add_action(CollabTitlebarItem::toggle_user_menu);
+    // cx.add_action(CollabTitlebarItem::toggle_vcs_menu);
+    // cx.add_action(CollabTitlebarItem::toggle_project_menu);
 }
 
 pub struct CollabTitlebarItem {
-    project: ModelHandle<Project>,
-    user_store: ModelHandle<UserStore>,
+    project: Model<Project>,
+    #[allow(unused)] // todo!()
+    user_store: Model<UserStore>,
+    #[allow(unused)] // todo!()
     client: Arc<Client>,
-    workspace: WeakViewHandle<Workspace>,
-    branch_popover: Option<ViewHandle<BranchList>>,
-    project_popover: Option<ViewHandle<recent_projects::RecentProjects>>,
-    user_menu: ViewHandle<ContextMenu>,
+    #[allow(unused)] // todo!()
+    workspace: WeakView<Workspace>,
+    //branch_popover: Option<ViewHandle<BranchList>>,
+    //project_popover: Option<ViewHandle<recent_projects::RecentProjects>>,
+    //user_menu: ViewHandle<ContextMenu>,
     _subscriptions: Vec<Subscription>,
 }
 
-impl Entity for CollabTitlebarItem {
-    type Event = ();
-}
-
-impl View for CollabTitlebarItem {
-    fn ui_name() -> &'static str {
-        "CollabTitlebarItem"
-    }
-
-    fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
-        let workspace = if let Some(workspace) = self.workspace.upgrade(cx) {
-            workspace
-        } else {
-            return Empty::new().into_any();
-        };
-
-        let theme = theme::current(cx).clone();
-        let mut left_container = Flex::row();
-        let mut right_container = Flex::row().align_children_center();
-
-        left_container.add_child(self.collect_title_root_names(theme.clone(), cx));
-
-        let user = self.user_store.read(cx).current_user();
-        let peer_id = self.client.peer_id();
-        if let Some(((user, peer_id), room)) = user
-            .as_ref()
-            .zip(peer_id)
-            .zip(ActiveCall::global(cx).read(cx).room().cloned())
-        {
-            if room.read(cx).can_publish() {
-                right_container
-                    .add_children(self.render_in_call_share_unshare_button(&workspace, &theme, cx));
-            }
-            right_container.add_child(self.render_leave_call(&theme, cx));
-            let muted = room.read(cx).is_muted(cx);
-            let speaking = room.read(cx).is_speaking();
-            left_container.add_child(
-                self.render_current_user(&workspace, &theme, &user, peer_id, muted, speaking, cx),
-            );
-            left_container.add_children(self.render_collaborators(&workspace, &theme, &room, cx));
-            if room.read(cx).can_publish() {
-                right_container.add_child(self.render_toggle_mute(&theme, &room, cx));
-            }
-            right_container.add_child(self.render_toggle_deafen(&theme, &room, cx));
-            if room.read(cx).can_publish() {
-                right_container
-                    .add_child(self.render_toggle_screen_sharing_button(&theme, &room, cx));
-            }
-        }
-
-        let status = workspace.read(cx).client().status();
-        let status = &*status.borrow();
-        if matches!(status, client::Status::Connected { .. }) {
-            let avatar = user.as_ref().and_then(|user| user.avatar.clone());
-            right_container.add_child(self.render_user_menu_button(&theme, avatar, cx));
-        } else {
-            right_container.add_children(self.render_connection_status(status, cx));
-            right_container.add_child(self.render_sign_in_button(&theme, cx));
-            right_container.add_child(self.render_user_menu_button(&theme, None, cx));
-        }
+impl Render for CollabTitlebarItem {
+    type Element = Stateful<Self, Div<Self>>;
 
-        Stack::new()
-            .with_child(left_container)
-            .with_child(
-                Flex::row()
-                    .with_child(
-                        right_container.contained().with_background_color(
-                            theme
-                                .titlebar
-                                .container
-                                .background_color
-                                .unwrap_or_else(|| Color::transparent_black()),
-                        ),
-                    )
-                    .aligned()
-                    .right(),
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+        h_stack()
+            .id("titlebar")
+            .justify_between()
+            .when(
+                !matches!(cx.window_bounds(), WindowBounds::Fullscreen),
+                |s| s.pl_20(),
             )
-            .into_any()
+            .w_full()
+            .h(rems(1.75))
+            .bg(cx.theme().colors().title_bar_background)
+            .on_click(|_, event, cx| {
+                if event.up.click_count == 2 {
+                    cx.zoom_window();
+                }
+            })
+            .child(
+                h_stack()
+                    // TODO - Add player menu
+                    .child(
+                        div()
+                            .id("project_owner_indicator")
+                            .child(
+                                Button::new("player")
+                                    .variant(ButtonVariant::Ghost)
+                                    .color(Some(TextColor::Player(0))),
+                            )
+                            .tooltip(move |_, cx| {
+                                cx.build_view(|_| TextTooltip::new("Toggle following"))
+                            }),
+                    )
+                    // TODO - Add project menu
+                    .child(
+                        div()
+                            .id("titlebar_project_menu_button")
+                            .child(Button::new("project_name").variant(ButtonVariant::Ghost))
+                            .tooltip(move |_, cx| {
+                                cx.build_view(|_| TextTooltip::new("Recent Projects"))
+                            }),
+                    )
+                    // TODO - Add git menu
+                    .child(
+                        div()
+                            .id("titlebar_git_menu_button")
+                            .child(
+                                Button::new("branch_name")
+                                    .variant(ButtonVariant::Ghost)
+                                    .color(Some(TextColor::Muted)),
+                            )
+                            .tooltip(move |_, cx| {
+                                // todo!() Replace with real action.
+                                #[gpui::action]
+                                struct NoAction {}
+
+                                cx.build_view(|_| {
+                                    TextTooltip::new("Recent Branches")
+                                        .key_binding(KeyBinding::new(gpui::KeyBinding::new(
+                                            "cmd-b",
+                                            NoAction {},
+                                            None,
+                                        )))
+                                        .meta("Only local branches shown")
+                                })
+                            }),
+                    ),
+            ) // self.titlebar_item
+            .child(h_stack().child(Label::new("Right side titlebar item")))
     }
 }
 
+// impl Entity for CollabTitlebarItem {
+//     type Event = ();
+// }
+
+// impl View for CollabTitlebarItem {
+//     fn ui_name() -> &'static str {
+//         "CollabTitlebarItem"
+//     }
+
+//     fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
+//         let workspace = if let Some(workspace) = self.workspace.upgrade(cx) {
+//             workspace
+//         } else {
+//             return Empty::new().into_any();
+//         };
+
+//         let theme = theme::current(cx).clone();
+//         let mut left_container = Flex::row();
+//         let mut right_container = Flex::row().align_children_center();
+
+//         left_container.add_child(self.collect_title_root_names(theme.clone(), cx));
+
+//         let user = self.user_store.read(cx).current_user();
+//         let peer_id = self.client.peer_id();
+//         if let Some(((user, peer_id), room)) = user
+//             .as_ref()
+//             .zip(peer_id)
+//             .zip(ActiveCall::global(cx).read(cx).room().cloned())
+//         {
+//             if room.read(cx).can_publish() {
+//                 right_container
+//                     .add_children(self.render_in_call_share_unshare_button(&workspace, &theme, cx));
+//             }
+//             right_container.add_child(self.render_leave_call(&theme, cx));
+//             let muted = room.read(cx).is_muted(cx);
+//             let speaking = room.read(cx).is_speaking();
+//             left_container.add_child(
+//                 self.render_current_user(&workspace, &theme, &user, peer_id, muted, speaking, cx),
+//             );
+//             left_container.add_children(self.render_collaborators(&workspace, &theme, &room, cx));
+//             if room.read(cx).can_publish() {
+//                 right_container.add_child(self.render_toggle_mute(&theme, &room, cx));
+//             }
+//             right_container.add_child(self.render_toggle_deafen(&theme, &room, cx));
+//             if room.read(cx).can_publish() {
+//                 right_container
+//                     .add_child(self.render_toggle_screen_sharing_button(&theme, &room, cx));
+//             }
+//         }
+
+//         let status = workspace.read(cx).client().status();
+//         let status = &*status.borrow();
+//         if matches!(status, client::Status::Connected { .. }) {
+//             let avatar = user.as_ref().and_then(|user| user.avatar.clone());
+//             right_container.add_child(self.render_user_menu_button(&theme, avatar, cx));
+//         } else {
+//             right_container.add_children(self.render_connection_status(status, cx));
+//             right_container.add_child(self.render_sign_in_button(&theme, cx));
+//             right_container.add_child(self.render_user_menu_button(&theme, None, cx));
+//         }
+
+//         Stack::new()
+//             .with_child(left_container)
+//             .with_child(
+//                 Flex::row()
+//                     .with_child(
+//                         right_container.contained().with_background_color(
+//                             theme
+//                                 .titlebar
+//                                 .container
+//                                 .background_color
+//                                 .unwrap_or_else(|| Color::transparent_black()),
+//                         ),
+//                     )
+//                     .aligned()
+//                     .right(),
+//             )
+//             .into_any()
+//     }
+// }
+
 impl CollabTitlebarItem {
-    pub fn new(
-        workspace: &Workspace,
-        workspace_handle: &ViewHandle<Workspace>,
-        cx: &mut ViewContext<Self>,
-    ) -> Self {
+    pub fn new(workspace: &Workspace, cx: &mut ViewContext<Self>) -> Self {
         let project = workspace.project().clone();
         let user_store = workspace.app_state().user_store.clone();
         let client = workspace.app_state().client.clone();
         let active_call = ActiveCall::global(cx);
         let mut subscriptions = Vec::new();
-        subscriptions.push(cx.observe(workspace_handle, |_, _, cx| cx.notify()));
+        subscriptions.push(
+            cx.observe(&workspace.weak_handle().upgrade().unwrap(), |_, _, cx| {
+                cx.notify()
+            }),
+        );
         subscriptions.push(cx.observe(&project, |_, _, cx| cx.notify()));
         subscriptions.push(cx.observe(&active_call, |this, _, cx| this.active_call_changed(cx)));
-        subscriptions.push(cx.observe_window_activation(|this, active, cx| {
-            this.window_activation_changed(active, cx)
-        }));
+        subscriptions.push(cx.observe_window_activation(Self::window_activation_changed));
         subscriptions.push(cx.observe(&user_store, |_, _, cx| cx.notify()));
 
         Self {
@@ -164,184 +257,184 @@ impl CollabTitlebarItem {
             project,
             user_store,
             client,
-            user_menu: cx.add_view(|cx| {
-                let view_id = cx.view_id();
-                let mut menu = ContextMenu::new(view_id, cx);
-                menu.set_position_mode(OverlayPositionMode::Local);
-                menu
-            }),
-            branch_popover: None,
-            project_popover: None,
+            //         user_menu: cx.add_view(|cx| {
+            //             let view_id = cx.view_id();
+            //             let mut menu = ContextMenu::new(view_id, cx);
+            //             menu.set_position_mode(OverlayPositionMode::Local);
+            //             menu
+            //         }),
+            //         branch_popover: None,
+            //         project_popover: None,
             _subscriptions: subscriptions,
         }
     }
 
-    fn collect_title_root_names(
-        &self,
-        theme: Arc<Theme>,
-        cx: &mut ViewContext<Self>,
-    ) -> AnyElement<Self> {
-        let project = self.project.read(cx);
-
-        let (name, entry) = {
-            let mut names_and_branches = project.visible_worktrees(cx).map(|worktree| {
-                let worktree = worktree.read(cx);
-                (worktree.root_name(), worktree.root_git_entry())
-            });
-
-            names_and_branches.next().unwrap_or(("", None))
-        };
-
-        let name = util::truncate_and_trailoff(name, MAX_PROJECT_NAME_LENGTH);
-        let branch_prepended = entry
-            .as_ref()
-            .and_then(RepositoryEntry::branch)
-            .map(|branch| util::truncate_and_trailoff(&branch, MAX_BRANCH_NAME_LENGTH));
-        let project_style = theme.titlebar.project_menu_button.clone();
-        let git_style = theme.titlebar.git_menu_button.clone();
-        let item_spacing = theme.titlebar.item_spacing;
-
-        let mut ret = Flex::row();
-
-        if let Some(project_host) = self.collect_project_host(theme.clone(), cx) {
-            ret = ret.with_child(project_host)
-        }
-
-        ret = ret.with_child(
-            Stack::new()
-                .with_child(
-                    MouseEventHandler::new::<ToggleProjectMenu, _>(0, cx, |mouse_state, cx| {
-                        let style = project_style
-                            .in_state(self.project_popover.is_some())
-                            .style_for(mouse_state);
-                        enum RecentProjectsTooltip {}
-                        Label::new(name, style.text.clone())
-                            .contained()
-                            .with_style(style.container)
-                            .aligned()
-                            .left()
-                            .with_tooltip::<RecentProjectsTooltip>(
-                                0,
-                                "Recent projects",
-                                Some(Box::new(recent_projects::OpenRecent)),
-                                theme.tooltip.clone(),
-                                cx,
-                            )
-                            .into_any_named("title-project-name")
-                    })
-                    .with_cursor_style(CursorStyle::PointingHand)
-                    .on_down(MouseButton::Left, move |_, this, cx| {
-                        this.toggle_project_menu(&Default::default(), cx)
-                    })
-                    .on_click(MouseButton::Left, move |_, _, _| {}),
-                )
-                .with_children(self.render_project_popover_host(&theme.titlebar, cx)),
-        );
-        if let Some(git_branch) = branch_prepended {
-            ret = ret.with_child(
-                Flex::row().with_child(
-                    Stack::new()
-                        .with_child(
-                            MouseEventHandler::new::<ToggleVcsMenu, _>(0, cx, |mouse_state, cx| {
-                                enum BranchPopoverTooltip {}
-                                let style = git_style
-                                    .in_state(self.branch_popover.is_some())
-                                    .style_for(mouse_state);
-                                Label::new(git_branch, style.text.clone())
-                                    .contained()
-                                    .with_style(style.container.clone())
-                                    .with_margin_right(item_spacing)
-                                    .aligned()
-                                    .left()
-                                    .with_tooltip::<BranchPopoverTooltip>(
-                                        0,
-                                        "Recent branches",
-                                        Some(Box::new(ToggleVcsMenu)),
-                                        theme.tooltip.clone(),
-                                        cx,
-                                    )
-                                    .into_any_named("title-project-branch")
-                            })
-                            .with_cursor_style(CursorStyle::PointingHand)
-                            .on_down(MouseButton::Left, move |_, this, cx| {
-                                this.toggle_vcs_menu(&Default::default(), cx)
-                            })
-                            .on_click(MouseButton::Left, move |_, _, _| {}),
-                        )
-                        .with_children(self.render_branches_popover_host(&theme.titlebar, cx)),
-                ),
-            )
-        }
-        ret.into_any()
-    }
-
-    fn collect_project_host(
-        &self,
-        theme: Arc<Theme>,
-        cx: &mut ViewContext<Self>,
-    ) -> Option<AnyElement<Self>> {
-        if ActiveCall::global(cx).read(cx).room().is_none() {
-            return None;
-        }
-        let project = self.project.read(cx);
-        let user_store = self.user_store.read(cx);
-
-        if project.is_local() {
-            return None;
-        }
-
-        let Some(host) = project.host() else {
-            return None;
-        };
-        let (Some(host_user), Some(participant_index)) = (
-            user_store.get_cached_user(host.user_id),
-            user_store.participant_indices().get(&host.user_id),
-        ) else {
-            return None;
-        };
-
-        enum ProjectHost {}
-        enum ProjectHostTooltip {}
-
-        let host_style = theme.titlebar.project_host.clone();
-        let selection_style = theme
-            .editor
-            .selection_style_for_room_participant(participant_index.0);
-        let peer_id = host.peer_id.clone();
-
-        Some(
-            MouseEventHandler::new::<ProjectHost, _>(0, cx, |mouse_state, _| {
-                let mut host_style = host_style.style_for(mouse_state).clone();
-                host_style.text.color = selection_style.cursor;
-                Label::new(host_user.github_login.clone(), host_style.text)
-                    .contained()
-                    .with_style(host_style.container)
-                    .aligned()
-                    .left()
-            })
-            .with_cursor_style(CursorStyle::PointingHand)
-            .on_click(MouseButton::Left, move |_, this, cx| {
-                if let Some(workspace) = this.workspace.upgrade(cx) {
-                    if let Some(task) =
-                        workspace.update(cx, |workspace, cx| workspace.follow(peer_id, cx))
-                    {
-                        task.detach_and_log_err(cx);
-                    }
-                }
-            })
-            .with_tooltip::<ProjectHostTooltip>(
-                0,
-                host_user.github_login.clone() + " is sharing this project. Click to follow.",
-                None,
-                theme.tooltip.clone(),
-                cx,
-            )
-            .into_any_named("project-host"),
-        )
-    }
-
-    fn window_activation_changed(&mut self, active: bool, cx: &mut ViewContext<Self>) {
-        let project = if active {
+    // fn collect_title_root_names(
+    //     &self,
+    //     theme: Arc<Theme>,
+    //     cx: &mut ViewContext<Self>,
+    // ) -> AnyElement<Self> {
+    //     let project = self.project.read(cx);
+
+    //     let (name, entry) = {
+    //         let mut names_and_branches = project.visible_worktrees(cx).map(|worktree| {
+    //             let worktree = worktree.read(cx);
+    //             (worktree.root_name(), worktree.root_git_entry())
+    //         });
+
+    //         names_and_branches.next().unwrap_or(("", None))
+    //     };
+
+    //     let name = util::truncate_and_trailoff(name, MAX_PROJECT_NAME_LENGTH);
+    //     let branch_prepended = entry
+    //         .as_ref()
+    //         .and_then(RepositoryEntry::branch)
+    //         .map(|branch| util::truncate_and_trailoff(&branch, MAX_BRANCH_NAME_LENGTH));
+    //     let project_style = theme.titlebar.project_menu_button.clone();
+    //     let git_style = theme.titlebar.git_menu_button.clone();
+    //     let item_spacing = theme.titlebar.item_spacing;
+
+    //     let mut ret = Flex::row();
+
+    //     if let Some(project_host) = self.collect_project_host(theme.clone(), cx) {
+    //         ret = ret.with_child(project_host)
+    //     }
+
+    //     ret = ret.with_child(
+    //         Stack::new()
+    //             .with_child(
+    //                 MouseEventHandler::new::<ToggleProjectMenu, _>(0, cx, |mouse_state, cx| {
+    //                     let style = project_style
+    //                         .in_state(self.project_popover.is_some())
+    //                         .style_for(mouse_state);
+    //                     enum RecentProjectsTooltip {}
+    //                     Label::new(name, style.text.clone())
+    //                         .contained()
+    //                         .with_style(style.container)
+    //                         .aligned()
+    //                         .left()
+    //                         .with_tooltip::<RecentProjectsTooltip>(
+    //                             0,
+    //                             "Recent projects",
+    //                             Some(Box::new(recent_projects::OpenRecent)),
+    //                             theme.tooltip.clone(),
+    //                             cx,
+    //                         )
+    //                         .into_any_named("title-project-name")
+    //                 })
+    //                 .with_cursor_style(CursorStyle::PointingHand)
+    //                 .on_down(MouseButton::Left, move |_, this, cx| {
+    //                     this.toggle_project_menu(&Default::default(), cx)
+    //                 })
+    //                 .on_click(MouseButton::Left, move |_, _, _| {}),
+    //             )
+    //             .with_children(self.render_project_popover_host(&theme.titlebar, cx)),
+    //     );
+    //     if let Some(git_branch) = branch_prepended {
+    //         ret = ret.with_child(
+    //             Flex::row().with_child(
+    //                 Stack::new()
+    //                     .with_child(
+    //                         MouseEventHandler::new::<ToggleVcsMenu, _>(0, cx, |mouse_state, cx| {
+    //                             enum BranchPopoverTooltip {}
+    //                             let style = git_style
+    //                                 .in_state(self.branch_popover.is_some())
+    //                                 .style_for(mouse_state);
+    //                             Label::new(git_branch, style.text.clone())
+    //                                 .contained()
+    //                                 .with_style(style.container.clone())
+    //                                 .with_margin_right(item_spacing)
+    //                                 .aligned()
+    //                                 .left()
+    //                                 .with_tooltip::<BranchPopoverTooltip>(
+    //                                     0,
+    //                                     "Recent branches",
+    //                                     Some(Box::new(ToggleVcsMenu)),
+    //                                     theme.tooltip.clone(),
+    //                                     cx,
+    //                                 )
+    //                                 .into_any_named("title-project-branch")
+    //                         })
+    //                         .with_cursor_style(CursorStyle::PointingHand)
+    //                         .on_down(MouseButton::Left, move |_, this, cx| {
+    //                             this.toggle_vcs_menu(&Default::default(), cx)
+    //                         })
+    //                         .on_click(MouseButton::Left, move |_, _, _| {}),
+    //                     )
+    //                     .with_children(self.render_branches_popover_host(&theme.titlebar, cx)),
+    //             ),
+    //         )
+    //     }
+    //     ret.into_any()
+    // }
+
+    // fn collect_project_host(
+    //     &self,
+    //     theme: Arc<Theme>,
+    //     cx: &mut ViewContext<Self>,
+    // ) -> Option<AnyElement<Self>> {
+    //     if ActiveCall::global(cx).read(cx).room().is_none() {
+    //         return None;
+    //     }
+    //     let project = self.project.read(cx);
+    //     let user_store = self.user_store.read(cx);
+
+    //     if project.is_local() {
+    //         return None;
+    //     }
+
+    //     let Some(host) = project.host() else {
+    //         return None;
+    //     };
+    //     let (Some(host_user), Some(participant_index)) = (
+    //         user_store.get_cached_user(host.user_id),
+    //         user_store.participant_indices().get(&host.user_id),
+    //     ) else {
+    //         return None;
+    //     };
+
+    //     enum ProjectHost {}
+    //     enum ProjectHostTooltip {}
+
+    //     let host_style = theme.titlebar.project_host.clone();
+    //     let selection_style = theme
+    //         .editor
+    //         .selection_style_for_room_participant(participant_index.0);
+    //     let peer_id = host.peer_id.clone();
+
+    //     Some(
+    //         MouseEventHandler::new::<ProjectHost, _>(0, cx, |mouse_state, _| {
+    //             let mut host_style = host_style.style_for(mouse_state).clone();
+    //             host_style.text.color = selection_style.cursor;
+    //             Label::new(host_user.github_login.clone(), host_style.text)
+    //                 .contained()
+    //                 .with_style(host_style.container)
+    //                 .aligned()
+    //                 .left()
+    //         })
+    //         .with_cursor_style(CursorStyle::PointingHand)
+    //         .on_click(MouseButton::Left, move |_, this, cx| {
+    //             if let Some(workspace) = this.workspace.upgrade(cx) {
+    //                 if let Some(task) =
+    //                     workspace.update(cx, |workspace, cx| workspace.follow(peer_id, cx))
+    //                 {
+    //                     task.detach_and_log_err(cx);
+    //                 }
+    //             }
+    //         })
+    //         .with_tooltip::<ProjectHostTooltip>(
+    //             0,
+    //             host_user.github_login.clone() + " is sharing this project. Click to follow.",
+    //             None,
+    //             theme.tooltip.clone(),
+    //             cx,
+    //         )
+    //         .into_any_named("project-host"),
+    //     )
+    // }
+
+    fn window_activation_changed(&mut self, cx: &mut ViewContext<Self>) {
+        let project = if cx.is_window_active() {
             Some(self.project.clone())
         } else {
             None
@@ -355,924 +448,924 @@ impl CollabTitlebarItem {
         cx.notify();
     }
 
-    fn share_project(&mut self, _: &ShareProject, cx: &mut ViewContext<Self>) {
-        let active_call = ActiveCall::global(cx);
-        let project = self.project.clone();
-        active_call
-            .update(cx, |call, cx| call.share_project(project, cx))
-            .detach_and_log_err(cx);
-    }
-
-    fn unshare_project(&mut self, _: &UnshareProject, cx: &mut ViewContext<Self>) {
-        let active_call = ActiveCall::global(cx);
-        let project = self.project.clone();
-        active_call
-            .update(cx, |call, cx| call.unshare_project(project, cx))
-            .log_err();
-    }
-
-    pub fn toggle_user_menu(&mut self, _: &ToggleUserMenu, cx: &mut ViewContext<Self>) {
-        self.user_menu.update(cx, |user_menu, cx| {
-            let items = if let Some(_) = self.user_store.read(cx).current_user() {
-                vec![
-                    ContextMenuItem::action("Settings", zed_actions::OpenSettings),
-                    ContextMenuItem::action("Theme", theme_selector::Toggle),
-                    ContextMenuItem::separator(),
-                    ContextMenuItem::action(
-                        "Share Feedback",
-                        feedback::feedback_editor::GiveFeedback,
-                    ),
-                    ContextMenuItem::action("Sign Out", SignOut),
-                ]
-            } else {
-                vec![
-                    ContextMenuItem::action("Settings", zed_actions::OpenSettings),
-                    ContextMenuItem::action("Theme", theme_selector::Toggle),
-                    ContextMenuItem::separator(),
-                    ContextMenuItem::action(
-                        "Share Feedback",
-                        feedback::feedback_editor::GiveFeedback,
-                    ),
-                ]
-            };
-            user_menu.toggle(Default::default(), AnchorCorner::TopRight, items, cx);
-        });
-    }
-
-    fn render_branches_popover_host<'a>(
-        &'a self,
-        _theme: &'a theme::Titlebar,
-        cx: &'a mut ViewContext<Self>,
-    ) -> Option<AnyElement<Self>> {
-        self.branch_popover.as_ref().map(|child| {
-            let theme = theme::current(cx).clone();
-            let child = ChildView::new(child, cx);
-            let child = MouseEventHandler::new::<BranchList, _>(0, cx, |_, _| {
-                child
-                    .flex(1., true)
-                    .contained()
-                    .constrained()
-                    .with_width(theme.titlebar.menu.width)
-                    .with_height(theme.titlebar.menu.height)
-            })
-            .on_click(MouseButton::Left, |_, _, _| {})
-            .on_down_out(MouseButton::Left, move |_, this, cx| {
-                this.branch_popover.take();
-                cx.emit(());
-                cx.notify();
-            })
-            .contained()
-            .into_any();
-
-            Overlay::new(child)
-                .with_fit_mode(OverlayFitMode::SwitchAnchor)
-                .with_anchor_corner(AnchorCorner::TopLeft)
-                .with_z_index(999)
-                .aligned()
-                .bottom()
-                .left()
-                .into_any()
-        })
-    }
-
-    fn render_project_popover_host<'a>(
-        &'a self,
-        _theme: &'a theme::Titlebar,
-        cx: &'a mut ViewContext<Self>,
-    ) -> Option<AnyElement<Self>> {
-        self.project_popover.as_ref().map(|child| {
-            let theme = theme::current(cx).clone();
-            let child = ChildView::new(child, cx);
-            let child = MouseEventHandler::new::<RecentProjects, _>(0, cx, |_, _| {
-                child
-                    .flex(1., true)
-                    .contained()
-                    .constrained()
-                    .with_width(theme.titlebar.menu.width)
-                    .with_height(theme.titlebar.menu.height)
-            })
-            .on_click(MouseButton::Left, |_, _, _| {})
-            .on_down_out(MouseButton::Left, move |_, this, cx| {
-                this.project_popover.take();
-                cx.emit(());
-                cx.notify();
-            })
-            .into_any();
-
-            Overlay::new(child)
-                .with_fit_mode(OverlayFitMode::SwitchAnchor)
-                .with_anchor_corner(AnchorCorner::TopLeft)
-                .with_z_index(999)
-                .aligned()
-                .bottom()
-                .left()
-                .into_any()
-        })
-    }
-
-    pub fn toggle_vcs_menu(&mut self, _: &ToggleVcsMenu, cx: &mut ViewContext<Self>) {
-        if self.branch_popover.take().is_none() {
-            if let Some(workspace) = self.workspace.upgrade(cx) {
-                let Some(view) =
-                    cx.add_option_view(|cx| build_branch_list(workspace, cx).log_err())
-                else {
-                    return;
-                };
-                cx.subscribe(&view, |this, _, event, cx| {
-                    match event {
-                        PickerEvent::Dismiss => {
-                            this.branch_popover = None;
-                        }
-                    }
-
-                    cx.notify();
-                })
-                .detach();
-                self.project_popover.take();
-                cx.focus(&view);
-                self.branch_popover = Some(view);
-            }
-        }
-
-        cx.notify();
-    }
-
-    pub fn toggle_project_menu(&mut self, _: &ToggleProjectMenu, cx: &mut ViewContext<Self>) {
-        let workspace = self.workspace.clone();
-        if self.project_popover.take().is_none() {
-            cx.spawn(|this, mut cx| async move {
-                let workspaces = WORKSPACE_DB
-                    .recent_workspaces_on_disk()
-                    .await
-                    .unwrap_or_default()
-                    .into_iter()
-                    .map(|(_, location)| location)
-                    .collect();
-
-                let workspace = workspace.clone();
-                this.update(&mut cx, move |this, cx| {
-                    let view = cx.add_view(|cx| build_recent_projects(workspace, workspaces, cx));
-
-                    cx.subscribe(&view, |this, _, event, cx| {
-                        match event {
-                            PickerEvent::Dismiss => {
-                                this.project_popover = None;
-                            }
-                        }
-
-                        cx.notify();
-                    })
-                    .detach();
-                    cx.focus(&view);
-                    this.branch_popover.take();
-                    this.project_popover = Some(view);
-                    cx.notify();
-                })
-                .log_err();
-            })
-            .detach();
-        }
-        cx.notify();
-    }
-
-    fn render_toggle_screen_sharing_button(
-        &self,
-        theme: &Theme,
-        room: &ModelHandle<Room>,
-        cx: &mut ViewContext<Self>,
-    ) -> AnyElement<Self> {
-        let icon;
-        let tooltip;
-        if room.read(cx).is_screen_sharing() {
-            icon = "icons/desktop.svg";
-            tooltip = "Stop Sharing Screen"
-        } else {
-            icon = "icons/desktop.svg";
-            tooltip = "Share Screen";
-        }
-
-        let active = room.read(cx).is_screen_sharing();
-        let titlebar = &theme.titlebar;
-        MouseEventHandler::new::<ToggleScreenSharing, _>(0, cx, |state, _| {
-            let style = titlebar
-                .screen_share_button
-                .in_state(active)
-                .style_for(state);
-
-            Svg::new(icon)
-                .with_color(style.color)
-                .constrained()
-                .with_width(style.icon_width)
-                .aligned()
-                .constrained()
-                .with_width(style.button_width)
-                .with_height(style.button_width)
-                .contained()
-                .with_style(style.container)
-        })
-        .with_cursor_style(CursorStyle::PointingHand)
-        .on_click(MouseButton::Left, move |_, _, cx| {
-            toggle_screen_sharing(&Default::default(), cx)
-        })
-        .with_tooltip::<ToggleScreenSharing>(
-            0,
-            tooltip,
-            Some(Box::new(ToggleScreenSharing)),
-            theme.tooltip.clone(),
-            cx,
-        )
-        .aligned()
-        .into_any()
-    }
-    fn render_toggle_mute(
-        &self,
-        theme: &Theme,
-        room: &ModelHandle<Room>,
-        cx: &mut ViewContext<Self>,
-    ) -> AnyElement<Self> {
-        let icon;
-        let tooltip;
-        let is_muted = room.read(cx).is_muted(cx);
-        if is_muted {
-            icon = "icons/mic-mute.svg";
-            tooltip = "Unmute microphone";
-        } else {
-            icon = "icons/mic.svg";
-            tooltip = "Mute microphone";
-        }
-
-        let titlebar = &theme.titlebar;
-        MouseEventHandler::new::<ToggleMute, _>(0, cx, |state, _| {
-            let style = titlebar
-                .toggle_microphone_button
-                .in_state(is_muted)
-                .style_for(state);
-            let image = Svg::new(icon)
-                .with_color(style.color)
-                .constrained()
-                .with_width(style.icon_width)
-                .aligned()
-                .constrained()
-                .with_width(style.button_width)
-                .with_height(style.button_width)
-                .contained()
-                .with_style(style.container);
-            if let Some(color) = style.container.background_color {
-                image.with_background_color(color)
-            } else {
-                image
-            }
-        })
-        .with_cursor_style(CursorStyle::PointingHand)
-        .on_click(MouseButton::Left, move |_, _, cx| {
-            toggle_mute(&Default::default(), cx)
-        })
-        .with_tooltip::<ToggleMute>(
-            0,
-            tooltip,
-            Some(Box::new(ToggleMute)),
-            theme.tooltip.clone(),
-            cx,
-        )
-        .aligned()
-        .into_any()
-    }
-    fn render_toggle_deafen(
-        &self,
-        theme: &Theme,
-        room: &ModelHandle<Room>,
-        cx: &mut ViewContext<Self>,
-    ) -> AnyElement<Self> {
-        let icon;
-        let tooltip;
-        let is_deafened = room.read(cx).is_deafened().unwrap_or(false);
-        if is_deafened {
-            icon = "icons/speaker-off.svg";
-            tooltip = "Unmute speakers";
-        } else {
-            icon = "icons/speaker-loud.svg";
-            tooltip = "Mute speakers";
-        }
-
-        let titlebar = &theme.titlebar;
-        MouseEventHandler::new::<ToggleDeafen, _>(0, cx, |state, _| {
-            let style = titlebar
-                .toggle_speakers_button
-                .in_state(is_deafened)
-                .style_for(state);
-            Svg::new(icon)
-                .with_color(style.color)
-                .constrained()
-                .with_width(style.icon_width)
-                .aligned()
-                .constrained()
-                .with_width(style.button_width)
-                .with_height(style.button_width)
-                .contained()
-                .with_style(style.container)
-        })
-        .with_cursor_style(CursorStyle::PointingHand)
-        .on_click(MouseButton::Left, move |_, _, cx| {
-            toggle_deafen(&Default::default(), cx)
-        })
-        .with_tooltip::<ToggleDeafen>(
-            0,
-            tooltip,
-            Some(Box::new(ToggleDeafen)),
-            theme.tooltip.clone(),
-            cx,
-        )
-        .aligned()
-        .into_any()
-    }
-    fn render_leave_call(&self, theme: &Theme, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
-        let icon = "icons/exit.svg";
-        let tooltip = "Leave call";
-
-        let titlebar = &theme.titlebar;
-        MouseEventHandler::new::<LeaveCall, _>(0, cx, |state, _| {
-            let style = titlebar.leave_call_button.style_for(state);
-            Svg::new(icon)
-                .with_color(style.color)
-                .constrained()
-                .with_width(style.icon_width)
-                .aligned()
-                .constrained()
-                .with_width(style.button_width)
-                .with_height(style.button_width)
-                .contained()
-                .with_style(style.container)
-        })
-        .with_cursor_style(CursorStyle::PointingHand)
-        .on_click(MouseButton::Left, move |_, _, cx| {
-            ActiveCall::global(cx)
-                .update(cx, |call, cx| call.hang_up(cx))
-                .detach_and_log_err(cx);
-        })
-        .with_tooltip::<LeaveCall>(
-            0,
-            tooltip,
-            Some(Box::new(LeaveCall)),
-            theme.tooltip.clone(),
-            cx,
-        )
-        .aligned()
-        .into_any()
-    }
-    fn render_in_call_share_unshare_button(
-        &self,
-        workspace: &ViewHandle<Workspace>,
-        theme: &Theme,
-        cx: &mut ViewContext<Self>,
-    ) -> Option<AnyElement<Self>> {
-        let project = workspace.read(cx).project();
-        if project.read(cx).is_remote() {
-            return None;
-        }
-
-        let is_shared = project.read(cx).is_shared();
-        let label = if is_shared { "Stop Sharing" } else { "Share" };
-        let tooltip = if is_shared {
-            "Stop sharing project with call participants"
-        } else {
-            "Share project with call participants"
-        };
-
-        let titlebar = &theme.titlebar;
-
-        enum ShareUnshare {}
-        Some(
-            Stack::new()
-                .with_child(
-                    MouseEventHandler::new::<ShareUnshare, _>(0, cx, |state, _| {
-                        //TODO: Ensure this button has consistent width for both text variations
-                        let style = titlebar.share_button.inactive_state().style_for(state);
-                        Label::new(label, style.text.clone())
-                            .contained()
-                            .with_style(style.container)
-                    })
-                    .with_cursor_style(CursorStyle::PointingHand)
-                    .on_click(MouseButton::Left, move |_, this, cx| {
-                        if is_shared {
-                            this.unshare_project(&Default::default(), cx);
-                        } else {
-                            this.share_project(&Default::default(), cx);
-                        }
-                    })
-                    .with_tooltip::<ShareUnshare>(
-                        0,
-                        tooltip.to_owned(),
-                        None,
-                        theme.tooltip.clone(),
-                        cx,
-                    ),
-                )
-                .aligned()
-                .contained()
-                .with_margin_left(theme.titlebar.item_spacing)
-                .into_any(),
-        )
-    }
-
-    fn render_user_menu_button(
-        &self,
-        theme: &Theme,
-        avatar: Option<Arc<ImageData>>,
-        cx: &mut ViewContext<Self>,
-    ) -> AnyElement<Self> {
-        let tooltip = theme.tooltip.clone();
-        let user_menu_button_style = if avatar.is_some() {
-            &theme.titlebar.user_menu.user_menu_button_online
-        } else {
-            &theme.titlebar.user_menu.user_menu_button_offline
-        };
-
-        let avatar_style = &user_menu_button_style.avatar;
-        Stack::new()
-            .with_child(
-                MouseEventHandler::new::<ToggleUserMenu, _>(0, cx, |state, _| {
-                    let style = user_menu_button_style
-                        .user_menu
-                        .inactive_state()
-                        .style_for(state);
-
-                    let mut dropdown = Flex::row().align_children_center();
-
-                    if let Some(avatar_img) = avatar {
-                        dropdown = dropdown.with_child(Self::render_face(
-                            avatar_img,
-                            *avatar_style,
-                            Color::transparent_black(),
-                            None,
-                        ));
-                    };
-
-                    dropdown
-                        .with_child(
-                            Svg::new("icons/caret_down.svg")
-                                .with_color(user_menu_button_style.icon.color)
-                                .constrained()
-                                .with_width(user_menu_button_style.icon.width)
-                                .contained()
-                                .into_any(),
-                        )
-                        .aligned()
-                        .constrained()
-                        .with_height(style.width)
-                        .contained()
-                        .with_style(style.container)
-                        .into_any()
-                })
-                .with_cursor_style(CursorStyle::PointingHand)
-                .on_down(MouseButton::Left, move |_, this, cx| {
-                    this.user_menu.update(cx, |menu, _| menu.delay_cancel());
-                })
-                .on_click(MouseButton::Left, move |_, this, cx| {
-                    this.toggle_user_menu(&Default::default(), cx)
-                })
-                .with_tooltip::<ToggleUserMenu>(
-                    0,
-                    "Toggle User Menu".to_owned(),
-                    Some(Box::new(ToggleUserMenu)),
-                    tooltip,
-                    cx,
-                )
-                .contained(),
-            )
-            .with_child(
-                ChildView::new(&self.user_menu, cx)
-                    .aligned()
-                    .bottom()
-                    .right(),
-            )
-            .into_any()
-    }
-
-    fn render_sign_in_button(&self, theme: &Theme, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
-        let titlebar = &theme.titlebar;
-        MouseEventHandler::new::<SignIn, _>(0, cx, |state, _| {
-            let style = titlebar.sign_in_button.inactive_state().style_for(state);
-            Label::new("Sign In", style.text.clone())
-                .contained()
-                .with_style(style.container)
-        })
-        .with_cursor_style(CursorStyle::PointingHand)
-        .on_click(MouseButton::Left, move |_, this, cx| {
-            let client = this.client.clone();
-            cx.app_context()
-                .spawn(|cx| async move { client.authenticate_and_connect(true, &cx).await })
-                .detach_and_log_err(cx);
-        })
-        .into_any()
-    }
-
-    fn render_collaborators(
-        &self,
-        workspace: &ViewHandle<Workspace>,
-        theme: &Theme,
-        room: &ModelHandle<Room>,
-        cx: &mut ViewContext<Self>,
-    ) -> Vec<Container<Self>> {
-        let mut participants = room
-            .read(cx)
-            .remote_participants()
-            .values()
-            .cloned()
-            .collect::<Vec<_>>();
-        participants.sort_by_cached_key(|p| p.user.github_login.clone());
-
-        participants
-            .into_iter()
-            .filter_map(|participant| {
-                let project = workspace.read(cx).project().read(cx);
-                let replica_id = project
-                    .collaborators()
-                    .get(&participant.peer_id)
-                    .map(|collaborator| collaborator.replica_id);
-                let user = participant.user.clone();
-                Some(
-                    Container::new(self.render_face_pile(
-                        &user,
-                        replica_id,
-                        participant.peer_id,
-                        Some(participant.location),
-                        participant.muted,
-                        participant.speaking,
-                        workspace,
-                        theme,
-                        cx,
-                    ))
-                    .with_margin_right(theme.titlebar.face_pile_spacing),
-                )
-            })
-            .collect()
-    }
-
-    fn render_current_user(
-        &self,
-        workspace: &ViewHandle<Workspace>,
-        theme: &Theme,
-        user: &Arc<User>,
-        peer_id: PeerId,
-        muted: bool,
-        speaking: bool,
-        cx: &mut ViewContext<Self>,
-    ) -> AnyElement<Self> {
-        let replica_id = workspace.read(cx).project().read(cx).replica_id();
-
-        Container::new(self.render_face_pile(
-            user,
-            Some(replica_id),
-            peer_id,
-            None,
-            muted,
-            speaking,
-            workspace,
-            theme,
-            cx,
-        ))
-        .with_margin_right(theme.titlebar.item_spacing)
-        .into_any()
-    }
-
-    fn render_face_pile(
-        &self,
-        user: &User,
-        _replica_id: Option<ReplicaId>,
-        peer_id: PeerId,
-        location: Option<ParticipantLocation>,
-        muted: bool,
-        speaking: bool,
-        workspace: &ViewHandle<Workspace>,
-        theme: &Theme,
-        cx: &mut ViewContext<Self>,
-    ) -> AnyElement<Self> {
-        let user_id = user.id;
-        let project_id = workspace.read(cx).project().read(cx).remote_id();
-        let room = ActiveCall::global(cx).read(cx).room().cloned();
-        let self_peer_id = workspace.read(cx).client().peer_id();
-        let self_following = workspace.read(cx).is_being_followed(peer_id);
-        let self_following_initialized = self_following
-            && room.as_ref().map_or(false, |room| match project_id {
-                None => true,
-                Some(project_id) => room
-                    .read(cx)
-                    .followers_for(peer_id, project_id)
-                    .iter()
-                    .any(|&follower| Some(follower) == self_peer_id),
-            });
-
-        let leader_style = theme.titlebar.leader_avatar;
-        let follower_style = theme.titlebar.follower_avatar;
-
-        let microphone_state = if muted {
-            Some(theme.titlebar.muted)
-        } else if speaking {
-            Some(theme.titlebar.speaking)
-        } else {
-            None
-        };
-
-        let mut background_color = theme
-            .titlebar
-            .container
-            .background_color
-            .unwrap_or_default();
-
-        let participant_index = self
-            .user_store
-            .read(cx)
-            .participant_indices()
-            .get(&user_id)
-            .copied();
-        if let Some(participant_index) = participant_index {
-            if self_following_initialized {
-                let selection = theme
-                    .editor
-                    .selection_style_for_room_participant(participant_index.0)
-                    .selection;
-                background_color = Color::blend(selection, background_color);
-                background_color.a = 255;
-            }
-        }
-
-        enum TitlebarParticipant {}
-
-        let content = MouseEventHandler::new::<TitlebarParticipant, _>(
-            peer_id.as_u64() as usize,
-            cx,
-            move |_, cx| {
-                Stack::new()
-                    .with_children(user.avatar.as_ref().map(|avatar| {
-                        let face_pile = FacePile::new(theme.titlebar.follower_avatar_overlap)
-                            .with_child(Self::render_face(
-                                avatar.clone(),
-                                Self::location_style(workspace, location, leader_style, cx),
-                                background_color,
-                                microphone_state,
-                            ))
-                            .with_children(
-                                (|| {
-                                    let project_id = project_id?;
-                                    let room = room?.read(cx);
-                                    let followers = room.followers_for(peer_id, project_id);
-                                    Some(followers.into_iter().filter_map(|&follower| {
-                                        if Some(follower) == self_peer_id {
-                                            return None;
-                                        }
-                                        let participant =
-                                            room.remote_participant_for_peer_id(follower)?;
-                                        Some(Self::render_face(
-                                            participant.user.avatar.clone()?,
-                                            follower_style,
-                                            background_color,
-                                            None,
-                                        ))
-                                    }))
-                                })()
-                                .into_iter()
-                                .flatten(),
-                            )
-                            .with_children(
-                                self_following_initialized
-                                    .then(|| self.user_store.read(cx).current_user())
-                                    .and_then(|user| {
-                                        Some(Self::render_face(
-                                            user?.avatar.clone()?,
-                                            follower_style,
-                                            background_color,
-                                            None,
-                                        ))
-                                    }),
-                            );
-
-                        let mut container = face_pile
-                            .contained()
-                            .with_style(theme.titlebar.leader_selection);
-
-                        if let Some(participant_index) = participant_index {
-                            if self_following_initialized {
-                                let color = theme
-                                    .editor
-                                    .selection_style_for_room_participant(participant_index.0)
-                                    .selection;
-                                container = container.with_background_color(color);
-                            }
-                        }
-
-                        container
-                    }))
-                    .with_children((|| {
-                        let participant_index = participant_index?;
-                        let color = theme
-                            .editor
-                            .selection_style_for_room_participant(participant_index.0)
-                            .cursor;
-                        Some(
-                            AvatarRibbon::new(color)
-                                .constrained()
-                                .with_width(theme.titlebar.avatar_ribbon.width)
-                                .with_height(theme.titlebar.avatar_ribbon.height)
-                                .aligned()
-                                .bottom(),
-                        )
-                    })())
-            },
-        );
-
-        if Some(peer_id) == self_peer_id {
-            return content.into_any();
-        }
-
-        content
-            .with_cursor_style(CursorStyle::PointingHand)
-            .on_click(MouseButton::Left, move |_, this, cx| {
-                let Some(workspace) = this.workspace.upgrade(cx) else {
-                    return;
-                };
-                if let Some(task) =
-                    workspace.update(cx, |workspace, cx| workspace.follow(peer_id, cx))
-                {
-                    task.detach_and_log_err(cx);
-                }
-            })
-            .with_tooltip::<TitlebarParticipant>(
-                peer_id.as_u64() as usize,
-                format!("Follow {}", user.github_login),
-                Some(Box::new(FollowNextCollaborator)),
-                theme.tooltip.clone(),
-                cx,
-            )
-            .into_any()
-    }
-
-    fn location_style(
-        workspace: &ViewHandle<Workspace>,
-        location: Option<ParticipantLocation>,
-        mut style: AvatarStyle,
-        cx: &ViewContext<Self>,
-    ) -> AvatarStyle {
-        if let Some(location) = location {
-            if let ParticipantLocation::SharedProject { project_id } = location {
-                if Some(project_id) != workspace.read(cx).project().read(cx).remote_id() {
-                    style.image.grayscale = true;
-                }
-            } else {
-                style.image.grayscale = true;
-            }
-        }
-
-        style
-    }
-
-    fn render_face<V: 'static>(
-        avatar: Arc<ImageData>,
-        avatar_style: AvatarStyle,
-        background_color: Color,
-        microphone_state: Option<Color>,
-    ) -> AnyElement<V> {
-        Image::from_data(avatar)
-            .with_style(avatar_style.image)
-            .aligned()
-            .contained()
-            .with_background_color(microphone_state.unwrap_or(background_color))
-            .with_corner_radius(avatar_style.outer_corner_radius)
-            .constrained()
-            .with_width(avatar_style.outer_width)
-            .with_height(avatar_style.outer_width)
-            .aligned()
-            .into_any()
-    }
-
-    fn render_connection_status(
-        &self,
-        status: &client::Status,
-        cx: &mut ViewContext<Self>,
-    ) -> Option<AnyElement<Self>> {
-        enum ConnectionStatusButton {}
-
-        let theme = &theme::current(cx).clone();
-        match status {
-            client::Status::ConnectionError
-            | client::Status::ConnectionLost
-            | client::Status::Reauthenticating { .. }
-            | client::Status::Reconnecting { .. }
-            | client::Status::ReconnectionError { .. } => Some(
-                Svg::new("icons/disconnected.svg")
-                    .with_color(theme.titlebar.offline_icon.color)
-                    .constrained()
-                    .with_width(theme.titlebar.offline_icon.width)
-                    .aligned()
-                    .contained()
-                    .with_style(theme.titlebar.offline_icon.container)
-                    .into_any(),
-            ),
-            client::Status::UpgradeRequired => {
-                let auto_updater = auto_update::AutoUpdater::get(cx);
-                let label = match auto_updater.map(|auto_update| auto_update.read(cx).status()) {
-                    Some(AutoUpdateStatus::Updated) => "Please restart Zed to Collaborate",
-                    Some(AutoUpdateStatus::Installing)
-                    | Some(AutoUpdateStatus::Downloading)
-                    | Some(AutoUpdateStatus::Checking) => "Updating...",
-                    Some(AutoUpdateStatus::Idle) | Some(AutoUpdateStatus::Errored) | None => {
-                        "Please update Zed to Collaborate"
-                    }
-                };
-
-                Some(
-                    MouseEventHandler::new::<ConnectionStatusButton, _>(0, cx, |_, _| {
-                        Label::new(label, theme.titlebar.outdated_warning.text.clone())
-                            .contained()
-                            .with_style(theme.titlebar.outdated_warning.container)
-                            .aligned()
-                    })
-                    .with_cursor_style(CursorStyle::PointingHand)
-                    .on_click(MouseButton::Left, |_, _, cx| {
-                        if let Some(auto_updater) = auto_update::AutoUpdater::get(cx) {
-                            if auto_updater.read(cx).status() == AutoUpdateStatus::Updated {
-                                workspace::restart(&Default::default(), cx);
-                                return;
-                            }
-                        }
-                        auto_update::check(&Default::default(), cx);
-                    })
-                    .into_any(),
-                )
-            }
-            _ => None,
-        }
-    }
-}
-
-pub struct AvatarRibbon {
-    color: Color,
+    // fn share_project(&mut self, _: &ShareProject, cx: &mut ViewContext<Self>) {
+    //     let active_call = ActiveCall::global(cx);
+    //     let project = self.project.clone();
+    //     active_call
+    //         .update(cx, |call, cx| call.share_project(project, cx))
+    //         .detach_and_log_err(cx);
+    // }
+
+    // fn unshare_project(&mut self, _: &UnshareProject, cx: &mut ViewContext<Self>) {
+    //     let active_call = ActiveCall::global(cx);
+    //     let project = self.project.clone();
+    //     active_call
+    //         .update(cx, |call, cx| call.unshare_project(project, cx))
+    //         .log_err();
+    // }
+
+    // pub fn toggle_user_menu(&mut self, _: &ToggleUserMenu, cx: &mut ViewContext<Self>) {
+    //     self.user_menu.update(cx, |user_menu, cx| {
+    //         let items = if let Some(_) = self.user_store.read(cx).current_user() {
+    //             vec![
+    //                 ContextMenuItem::action("Settings", zed_actions::OpenSettings),
+    //                 ContextMenuItem::action("Theme", theme_selector::Toggle),
+    //                 ContextMenuItem::separator(),
+    //                 ContextMenuItem::action(
+    //                     "Share Feedback",
+    //                     feedback::feedback_editor::GiveFeedback,
+    //                 ),
+    //                 ContextMenuItem::action("Sign Out", SignOut),
+    //             ]
+    //         } else {
+    //             vec![
+    //                 ContextMenuItem::action("Settings", zed_actions::OpenSettings),
+    //                 ContextMenuItem::action("Theme", theme_selector::Toggle),
+    //                 ContextMenuItem::separator(),
+    //                 ContextMenuItem::action(
+    //                     "Share Feedback",
+    //                     feedback::feedback_editor::GiveFeedback,
+    //                 ),
+    //             ]
+    //         };
+    //         user_menu.toggle(Default::default(), AnchorCorner::TopRight, items, cx);
+    //     });
+    // }
+
+    // fn render_branches_popover_host<'a>(
+    //     &'a self,
+    //     _theme: &'a theme::Titlebar,
+    //     cx: &'a mut ViewContext<Self>,
+    // ) -> Option<AnyElement<Self>> {
+    //     self.branch_popover.as_ref().map(|child| {
+    //         let theme = theme::current(cx).clone();
+    //         let child = ChildView::new(child, cx);
+    //         let child = MouseEventHandler::new::<BranchList, _>(0, cx, |_, _| {
+    //             child
+    //                 .flex(1., true)
+    //                 .contained()
+    //                 .constrained()
+    //                 .with_width(theme.titlebar.menu.width)
+    //                 .with_height(theme.titlebar.menu.height)
+    //         })
+    //         .on_click(MouseButton::Left, |_, _, _| {})
+    //         .on_down_out(MouseButton::Left, move |_, this, cx| {
+    //             this.branch_popover.take();
+    //             cx.emit(());
+    //             cx.notify();
+    //         })
+    //         .contained()
+    //         .into_any();
+
+    //         Overlay::new(child)
+    //             .with_fit_mode(OverlayFitMode::SwitchAnchor)
+    //             .with_anchor_corner(AnchorCorner::TopLeft)
+    //             .with_z_index(999)
+    //             .aligned()
+    //             .bottom()
+    //             .left()
+    //             .into_any()
+    //     })
+    // }
+
+    // fn render_project_popover_host<'a>(
+    //     &'a self,
+    //     _theme: &'a theme::Titlebar,
+    //     cx: &'a mut ViewContext<Self>,
+    // ) -> Option<AnyElement<Self>> {
+    //     self.project_popover.as_ref().map(|child| {
+    //         let theme = theme::current(cx).clone();
+    //         let child = ChildView::new(child, cx);
+    //         let child = MouseEventHandler::new::<RecentProjects, _>(0, cx, |_, _| {
+    //             child
+    //                 .flex(1., true)
+    //                 .contained()
+    //                 .constrained()
+    //                 .with_width(theme.titlebar.menu.width)
+    //                 .with_height(theme.titlebar.menu.height)
+    //         })
+    //         .on_click(MouseButton::Left, |_, _, _| {})
+    //         .on_down_out(MouseButton::Left, move |_, this, cx| {
+    //             this.project_popover.take();
+    //             cx.emit(());
+    //             cx.notify();
+    //         })
+    //         .into_any();
+
+    //         Overlay::new(child)
+    //             .with_fit_mode(OverlayFitMode::SwitchAnchor)
+    //             .with_anchor_corner(AnchorCorner::TopLeft)
+    //             .with_z_index(999)
+    //             .aligned()
+    //             .bottom()
+    //             .left()
+    //             .into_any()
+    //     })
+    // }
+
+    // pub fn toggle_vcs_menu(&mut self, _: &ToggleVcsMenu, cx: &mut ViewContext<Self>) {
+    //     if self.branch_popover.take().is_none() {
+    //         if let Some(workspace) = self.workspace.upgrade(cx) {
+    //             let Some(view) =
+    //                 cx.add_option_view(|cx| build_branch_list(workspace, cx).log_err())
+    //             else {
+    //                 return;
+    //             };
+    //             cx.subscribe(&view, |this, _, event, cx| {
+    //                 match event {
+    //                     PickerEvent::Dismiss => {
+    //                         this.branch_popover = None;
+    //                     }
+    //                 }
+
+    //                 cx.notify();
+    //             })
+    //             .detach();
+    //             self.project_popover.take();
+    //             cx.focus(&view);
+    //             self.branch_popover = Some(view);
+    //         }
+    //     }
+
+    //     cx.notify();
+    // }
+
+    // pub fn toggle_project_menu(&mut self, _: &ToggleProjectMenu, cx: &mut ViewContext<Self>) {
+    //     let workspace = self.workspace.clone();
+    //     if self.project_popover.take().is_none() {
+    //         cx.spawn(|this, mut cx| async move {
+    //             let workspaces = WORKSPACE_DB
+    //                 .recent_workspaces_on_disk()
+    //                 .await
+    //                 .unwrap_or_default()
+    //                 .into_iter()
+    //                 .map(|(_, location)| location)
+    //                 .collect();
+
+    //             let workspace = workspace.clone();
+    //             this.update(&mut cx, move |this, cx| {
+    //                 let view = cx.add_view(|cx| build_recent_projects(workspace, workspaces, cx));
+
+    //                 cx.subscribe(&view, |this, _, event, cx| {
+    //                     match event {
+    //                         PickerEvent::Dismiss => {
+    //                             this.project_popover = None;
+    //                         }
+    //                     }
+
+    //                     cx.notify();
+    //                 })
+    //                 .detach();
+    //                 cx.focus(&view);
+    //                 this.branch_popover.take();
+    //                 this.project_popover = Some(view);
+    //                 cx.notify();
+    //             })
+    //             .log_err();
+    //         })
+    //         .detach();
+    //     }
+    //     cx.notify();
+    // }
+
+    // fn render_toggle_screen_sharing_button(
+    //     &self,
+    //     theme: &Theme,
+    //     room: &ModelHandle<Room>,
+    //     cx: &mut ViewContext<Self>,
+    // ) -> AnyElement<Self> {
+    //     let icon;
+    //     let tooltip;
+    //     if room.read(cx).is_screen_sharing() {
+    //         icon = "icons/desktop.svg";
+    //         tooltip = "Stop Sharing Screen"
+    //     } else {
+    //         icon = "icons/desktop.svg";
+    //         tooltip = "Share Screen";
+    //     }
+
+    //     let active = room.read(cx).is_screen_sharing();
+    //     let titlebar = &theme.titlebar;
+    //     MouseEventHandler::new::<ToggleScreenSharing, _>(0, cx, |state, _| {
+    //         let style = titlebar
+    //             .screen_share_button
+    //             .in_state(active)
+    //             .style_for(state);
+
+    //         Svg::new(icon)
+    //             .with_color(style.color)
+    //             .constrained()
+    //             .with_width(style.icon_width)
+    //             .aligned()
+    //             .constrained()
+    //             .with_width(style.button_width)
+    //             .with_height(style.button_width)
+    //             .contained()
+    //             .with_style(style.container)
+    //     })
+    //     .with_cursor_style(CursorStyle::PointingHand)
+    //     .on_click(MouseButton::Left, move |_, _, cx| {
+    //         toggle_screen_sharing(&Default::default(), cx)
+    //     })
+    //     .with_tooltip::<ToggleScreenSharing>(
+    //         0,
+    //         tooltip,
+    //         Some(Box::new(ToggleScreenSharing)),
+    //         theme.tooltip.clone(),
+    //         cx,
+    //     )
+    //     .aligned()
+    //     .into_any()
+    // }
+    // fn render_toggle_mute(
+    //     &self,
+    //     theme: &Theme,
+    //     room: &ModelHandle<Room>,
+    //     cx: &mut ViewContext<Self>,
+    // ) -> AnyElement<Self> {
+    //     let icon;
+    //     let tooltip;
+    //     let is_muted = room.read(cx).is_muted(cx);
+    //     if is_muted {
+    //         icon = "icons/mic-mute.svg";
+    //         tooltip = "Unmute microphone";
+    //     } else {
+    //         icon = "icons/mic.svg";
+    //         tooltip = "Mute microphone";
+    //     }
+
+    //     let titlebar = &theme.titlebar;
+    //     MouseEventHandler::new::<ToggleMute, _>(0, cx, |state, _| {
+    //         let style = titlebar
+    //             .toggle_microphone_button
+    //             .in_state(is_muted)
+    //             .style_for(state);
+    //         let image = Svg::new(icon)
+    //             .with_color(style.color)
+    //             .constrained()
+    //             .with_width(style.icon_width)
+    //             .aligned()
+    //             .constrained()
+    //             .with_width(style.button_width)
+    //             .with_height(style.button_width)
+    //             .contained()
+    //             .with_style(style.container);
+    //         if let Some(color) = style.container.background_color {
+    //             image.with_background_color(color)
+    //         } else {
+    //             image
+    //         }
+    //     })
+    //     .with_cursor_style(CursorStyle::PointingHand)
+    //     .on_click(MouseButton::Left, move |_, _, cx| {
+    //         toggle_mute(&Default::default(), cx)
+    //     })
+    //     .with_tooltip::<ToggleMute>(
+    //         0,
+    //         tooltip,
+    //         Some(Box::new(ToggleMute)),
+    //         theme.tooltip.clone(),
+    //         cx,
+    //     )
+    //     .aligned()
+    //     .into_any()
+    // }
+    // fn render_toggle_deafen(
+    //     &self,
+    //     theme: &Theme,
+    //     room: &ModelHandle<Room>,
+    //     cx: &mut ViewContext<Self>,
+    // ) -> AnyElement<Self> {
+    //     let icon;
+    //     let tooltip;
+    //     let is_deafened = room.read(cx).is_deafened().unwrap_or(false);
+    //     if is_deafened {
+    //         icon = "icons/speaker-off.svg";
+    //         tooltip = "Unmute speakers";
+    //     } else {
+    //         icon = "icons/speaker-loud.svg";
+    //         tooltip = "Mute speakers";
+    //     }
+
+    //     let titlebar = &theme.titlebar;
+    //     MouseEventHandler::new::<ToggleDeafen, _>(0, cx, |state, _| {
+    //         let style = titlebar
+    //             .toggle_speakers_button
+    //             .in_state(is_deafened)
+    //             .style_for(state);
+    //         Svg::new(icon)
+    //             .with_color(style.color)
+    //             .constrained()
+    //             .with_width(style.icon_width)
+    //             .aligned()
+    //             .constrained()
+    //             .with_width(style.button_width)
+    //             .with_height(style.button_width)
+    //             .contained()
+    //             .with_style(style.container)
+    //     })
+    //     .with_cursor_style(CursorStyle::PointingHand)
+    //     .on_click(MouseButton::Left, move |_, _, cx| {
+    //         toggle_deafen(&Default::default(), cx)
+    //     })
+    //     .with_tooltip::<ToggleDeafen>(
+    //         0,
+    //         tooltip,
+    //         Some(Box::new(ToggleDeafen)),
+    //         theme.tooltip.clone(),
+    //         cx,
+    //     )
+    //     .aligned()
+    //     .into_any()
+    // }
+    // fn render_leave_call(&self, theme: &Theme, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
+    //     let icon = "icons/exit.svg";
+    //     let tooltip = "Leave call";
+
+    //     let titlebar = &theme.titlebar;
+    //     MouseEventHandler::new::<LeaveCall, _>(0, cx, |state, _| {
+    //         let style = titlebar.leave_call_button.style_for(state);
+    //         Svg::new(icon)
+    //             .with_color(style.color)
+    //             .constrained()
+    //             .with_width(style.icon_width)
+    //             .aligned()
+    //             .constrained()
+    //             .with_width(style.button_width)
+    //             .with_height(style.button_width)
+    //             .contained()
+    //             .with_style(style.container)
+    //     })
+    //     .with_cursor_style(CursorStyle::PointingHand)
+    //     .on_click(MouseButton::Left, move |_, _, cx| {
+    //         ActiveCall::global(cx)
+    //             .update(cx, |call, cx| call.hang_up(cx))
+    //             .detach_and_log_err(cx);
+    //     })
+    //     .with_tooltip::<LeaveCall>(
+    //         0,
+    //         tooltip,
+    //         Some(Box::new(LeaveCall)),
+    //         theme.tooltip.clone(),
+    //         cx,
+    //     )
+    //     .aligned()
+    //     .into_any()
+    // }
+    // fn render_in_call_share_unshare_button(
+    //     &self,
+    //     workspace: &ViewHandle<Workspace>,
+    //     theme: &Theme,
+    //     cx: &mut ViewContext<Self>,
+    // ) -> Option<AnyElement<Self>> {
+    //     let project = workspace.read(cx).project();
+    //     if project.read(cx).is_remote() {
+    //         return None;
+    //     }
+
+    //     let is_shared = project.read(cx).is_shared();
+    //     let label = if is_shared { "Stop Sharing" } else { "Share" };
+    //     let tooltip = if is_shared {
+    //         "Stop sharing project with call participants"
+    //     } else {
+    //         "Share project with call participants"
+    //     };
+
+    //     let titlebar = &theme.titlebar;
+
+    //     enum ShareUnshare {}
+    //     Some(
+    //         Stack::new()
+    //             .with_child(
+    //                 MouseEventHandler::new::<ShareUnshare, _>(0, cx, |state, _| {
+    //                     //TODO: Ensure this button has consistent width for both text variations
+    //                     let style = titlebar.share_button.inactive_state().style_for(state);
+    //                     Label::new(label, style.text.clone())
+    //                         .contained()
+    //                         .with_style(style.container)
+    //                 })
+    //                 .with_cursor_style(CursorStyle::PointingHand)
+    //                 .on_click(MouseButton::Left, move |_, this, cx| {
+    //                     if is_shared {
+    //                         this.unshare_project(&Default::default(), cx);
+    //                     } else {
+    //                         this.share_project(&Default::default(), cx);
+    //                     }
+    //                 })
+    //                 .with_tooltip::<ShareUnshare>(
+    //                     0,
+    //                     tooltip.to_owned(),
+    //                     None,
+    //                     theme.tooltip.clone(),
+    //                     cx,
+    //                 ),
+    //             )
+    //             .aligned()
+    //             .contained()
+    //             .with_margin_left(theme.titlebar.item_spacing)
+    //             .into_any(),
+    //     )
+    // }
+
+    // fn render_user_menu_button(
+    //     &self,
+    //     theme: &Theme,
+    //     avatar: Option<Arc<ImageData>>,
+    //     cx: &mut ViewContext<Self>,
+    // ) -> AnyElement<Self> {
+    //     let tooltip = theme.tooltip.clone();
+    //     let user_menu_button_style = if avatar.is_some() {
+    //         &theme.titlebar.user_menu.user_menu_button_online
+    //     } else {
+    //         &theme.titlebar.user_menu.user_menu_button_offline
+    //     };
+
+    //     let avatar_style = &user_menu_button_style.avatar;
+    //     Stack::new()
+    //         .with_child(
+    //             MouseEventHandler::new::<ToggleUserMenu, _>(0, cx, |state, _| {
+    //                 let style = user_menu_button_style
+    //                     .user_menu
+    //                     .inactive_state()
+    //                     .style_for(state);
+
+    //                 let mut dropdown = Flex::row().align_children_center();
+
+    //                 if let Some(avatar_img) = avatar {
+    //                     dropdown = dropdown.with_child(Self::render_face(
+    //                         avatar_img,
+    //                         *avatar_style,
+    //                         Color::transparent_black(),
+    //                         None,
+    //                     ));
+    //                 };
+
+    //                 dropdown
+    //                     .with_child(
+    //                         Svg::new("icons/caret_down.svg")
+    //                             .with_color(user_menu_button_style.icon.color)
+    //                             .constrained()
+    //                             .with_width(user_menu_button_style.icon.width)
+    //                             .contained()
+    //                             .into_any(),
+    //                     )
+    //                     .aligned()
+    //                     .constrained()
+    //                     .with_height(style.width)
+    //                     .contained()
+    //                     .with_style(style.container)
+    //                     .into_any()
+    //             })
+    //             .with_cursor_style(CursorStyle::PointingHand)
+    //             .on_down(MouseButton::Left, move |_, this, cx| {
+    //                 this.user_menu.update(cx, |menu, _| menu.delay_cancel());
+    //             })
+    //             .on_click(MouseButton::Left, move |_, this, cx| {
+    //                 this.toggle_user_menu(&Default::default(), cx)
+    //             })
+    //             .with_tooltip::<ToggleUserMenu>(
+    //                 0,
+    //                 "Toggle User Menu".to_owned(),
+    //                 Some(Box::new(ToggleUserMenu)),
+    //                 tooltip,
+    //                 cx,
+    //             )
+    //             .contained(),
+    //         )
+    //         .with_child(
+    //             ChildView::new(&self.user_menu, cx)
+    //                 .aligned()
+    //                 .bottom()
+    //                 .right(),
+    //         )
+    //         .into_any()
+    // }
+
+    // fn render_sign_in_button(&self, theme: &Theme, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
+    //     let titlebar = &theme.titlebar;
+    //     MouseEventHandler::new::<SignIn, _>(0, cx, |state, _| {
+    //         let style = titlebar.sign_in_button.inactive_state().style_for(state);
+    //         Label::new("Sign In", style.text.clone())
+    //             .contained()
+    //             .with_style(style.container)
+    //     })
+    //     .with_cursor_style(CursorStyle::PointingHand)
+    //     .on_click(MouseButton::Left, move |_, this, cx| {
+    //         let client = this.client.clone();
+    //         cx.app_context()
+    //             .spawn(|cx| async move { client.authenticate_and_connect(true, &cx).await })
+    //             .detach_and_log_err(cx);
+    //     })
+    //     .into_any()
+    // }
+
+    // fn render_collaborators(
+    //     &self,
+    //     workspace: &ViewHandle<Workspace>,
+    //     theme: &Theme,
+    //     room: &ModelHandle<Room>,
+    //     cx: &mut ViewContext<Self>,
+    // ) -> Vec<Container<Self>> {
+    //     let mut participants = room
+    //         .read(cx)
+    //         .remote_participants()
+    //         .values()
+    //         .cloned()
+    //         .collect::<Vec<_>>();
+    //     participants.sort_by_cached_key(|p| p.user.github_login.clone());
+
+    //     participants
+    //         .into_iter()
+    //         .filter_map(|participant| {
+    //             let project = workspace.read(cx).project().read(cx);
+    //             let replica_id = project
+    //                 .collaborators()
+    //                 .get(&participant.peer_id)
+    //                 .map(|collaborator| collaborator.replica_id);
+    //             let user = participant.user.clone();
+    //             Some(
+    //                 Container::new(self.render_face_pile(
+    //                     &user,
+    //                     replica_id,
+    //                     participant.peer_id,
+    //                     Some(participant.location),
+    //                     participant.muted,
+    //                     participant.speaking,
+    //                     workspace,
+    //                     theme,
+    //                     cx,
+    //                 ))
+    //                 .with_margin_right(theme.titlebar.face_pile_spacing),
+    //             )
+    //         })
+    //         .collect()
+    // }
+
+    // fn render_current_user(
+    //     &self,
+    //     workspace: &ViewHandle<Workspace>,
+    //     theme: &Theme,
+    //     user: &Arc<User>,
+    //     peer_id: PeerId,
+    //     muted: bool,
+    //     speaking: bool,
+    //     cx: &mut ViewContext<Self>,
+    // ) -> AnyElement<Self> {
+    //     let replica_id = workspace.read(cx).project().read(cx).replica_id();
+
+    //     Container::new(self.render_face_pile(
+    //         user,
+    //         Some(replica_id),
+    //         peer_id,
+    //         None,
+    //         muted,
+    //         speaking,
+    //         workspace,
+    //         theme,
+    //         cx,
+    //     ))
+    //     .with_margin_right(theme.titlebar.item_spacing)
+    //     .into_any()
+    // }
+
+    // fn render_face_pile(
+    //     &self,
+    //     user: &User,
+    //     _replica_id: Option<ReplicaId>,
+    //     peer_id: PeerId,
+    //     location: Option<ParticipantLocation>,
+    //     muted: bool,
+    //     speaking: bool,
+    //     workspace: &ViewHandle<Workspace>,
+    //     theme: &Theme,
+    //     cx: &mut ViewContext<Self>,
+    // ) -> AnyElement<Self> {
+    //     let user_id = user.id;
+    //     let project_id = workspace.read(cx).project().read(cx).remote_id();
+    //     let room = ActiveCall::global(cx).read(cx).room().cloned();
+    //     let self_peer_id = workspace.read(cx).client().peer_id();
+    //     let self_following = workspace.read(cx).is_being_followed(peer_id);
+    //     let self_following_initialized = self_following
+    //         && room.as_ref().map_or(false, |room| match project_id {
+    //             None => true,
+    //             Some(project_id) => room
+    //                 .read(cx)
+    //                 .followers_for(peer_id, project_id)
+    //                 .iter()
+    //                 .any(|&follower| Some(follower) == self_peer_id),
+    //         });
+
+    //     let leader_style = theme.titlebar.leader_avatar;
+    //     let follower_style = theme.titlebar.follower_avatar;
+
+    //     let microphone_state = if muted {
+    //         Some(theme.titlebar.muted)
+    //     } else if speaking {
+    //         Some(theme.titlebar.speaking)
+    //     } else {
+    //         None
+    //     };
+
+    //     let mut background_color = theme
+    //         .titlebar
+    //         .container
+    //         .background_color
+    //         .unwrap_or_default();
+
+    //     let participant_index = self
+    //         .user_store
+    //         .read(cx)
+    //         .participant_indices()
+    //         .get(&user_id)
+    //         .copied();
+    //     if let Some(participant_index) = participant_index {
+    //         if self_following_initialized {
+    //             let selection = theme
+    //                 .editor
+    //                 .selection_style_for_room_participant(participant_index.0)
+    //                 .selection;
+    //             background_color = Color::blend(selection, background_color);
+    //             background_color.a = 255;
+    //         }
+    //     }
+
+    //     enum TitlebarParticipant {}
+
+    //     let content = MouseEventHandler::new::<TitlebarParticipant, _>(
+    //         peer_id.as_u64() as usize,
+    //         cx,
+    //         move |_, cx| {
+    //             Stack::new()
+    //                 .with_children(user.avatar.as_ref().map(|avatar| {
+    //                     let face_pile = FacePile::new(theme.titlebar.follower_avatar_overlap)
+    //                         .with_child(Self::render_face(
+    //                             avatar.clone(),
+    //                             Self::location_style(workspace, location, leader_style, cx),
+    //                             background_color,
+    //                             microphone_state,
+    //                         ))
+    //                         .with_children(
+    //                             (|| {
+    //                                 let project_id = project_id?;
+    //                                 let room = room?.read(cx);
+    //                                 let followers = room.followers_for(peer_id, project_id);
+    //                                 Some(followers.into_iter().filter_map(|&follower| {
+    //                                     if Some(follower) == self_peer_id {
+    //                                         return None;
+    //                                     }
+    //                                     let participant =
+    //                                         room.remote_participant_for_peer_id(follower)?;
+    //                                     Some(Self::render_face(
+    //                                         participant.user.avatar.clone()?,
+    //                                         follower_style,
+    //                                         background_color,
+    //                                         None,
+    //                                     ))
+    //                                 }))
+    //                             })()
+    //                             .into_iter()
+    //                             .flatten(),
+    //                         )
+    //                         .with_children(
+    //                             self_following_initialized
+    //                                 .then(|| self.user_store.read(cx).current_user())
+    //                                 .and_then(|user| {
+    //                                     Some(Self::render_face(
+    //                                         user?.avatar.clone()?,
+    //                                         follower_style,
+    //                                         background_color,
+    //                                         None,
+    //                                     ))
+    //                                 }),
+    //                         );
+
+    //                     let mut container = face_pile
+    //                         .contained()
+    //                         .with_style(theme.titlebar.leader_selection);
+
+    //                     if let Some(participant_index) = participant_index {
+    //                         if self_following_initialized {
+    //                             let color = theme
+    //                                 .editor
+    //                                 .selection_style_for_room_participant(participant_index.0)
+    //                                 .selection;
+    //                             container = container.with_background_color(color);
+    //                         }
+    //                     }
+
+    //                     container
+    //                 }))
+    //                 .with_children((|| {
+    //                     let participant_index = participant_index?;
+    //                     let color = theme
+    //                         .editor
+    //                         .selection_style_for_room_participant(participant_index.0)
+    //                         .cursor;
+    //                     Some(
+    //                         AvatarRibbon::new(color)
+    //                             .constrained()
+    //                             .with_width(theme.titlebar.avatar_ribbon.width)
+    //                             .with_height(theme.titlebar.avatar_ribbon.height)
+    //                             .aligned()
+    //                             .bottom(),
+    //                     )
+    //                 })())
+    //         },
+    //     );
+
+    //     if Some(peer_id) == self_peer_id {
+    //         return content.into_any();
+    //     }
+
+    //     content
+    //         .with_cursor_style(CursorStyle::PointingHand)
+    //         .on_click(MouseButton::Left, move |_, this, cx| {
+    //             let Some(workspace) = this.workspace.upgrade(cx) else {
+    //                 return;
+    //             };
+    //             if let Some(task) =
+    //                 workspace.update(cx, |workspace, cx| workspace.follow(peer_id, cx))
+    //             {
+    //                 task.detach_and_log_err(cx);
+    //             }
+    //         })
+    //         .with_tooltip::<TitlebarParticipant>(
+    //             peer_id.as_u64() as usize,
+    //             format!("Follow {}", user.github_login),
+    //             Some(Box::new(FollowNextCollaborator)),
+    //             theme.tooltip.clone(),
+    //             cx,
+    //         )
+    //         .into_any()
+    // }
+
+    // fn location_style(
+    //     workspace: &ViewHandle<Workspace>,
+    //     location: Option<ParticipantLocation>,
+    //     mut style: AvatarStyle,
+    //     cx: &ViewContext<Self>,
+    // ) -> AvatarStyle {
+    //     if let Some(location) = location {
+    //         if let ParticipantLocation::SharedProject { project_id } = location {
+    //             if Some(project_id) != workspace.read(cx).project().read(cx).remote_id() {
+    //                 style.image.grayscale = true;
+    //             }
+    //         } else {
+    //             style.image.grayscale = true;
+    //         }
+    //     }
+
+    //     style
+    // }
+
+    // fn render_face<V: 'static>(
+    //     avatar: Arc<ImageData>,
+    //     avatar_style: AvatarStyle,
+    //     background_color: Color,
+    //     microphone_state: Option<Color>,
+    // ) -> AnyElement<V> {
+    //     Image::from_data(avatar)
+    //         .with_style(avatar_style.image)
+    //         .aligned()
+    //         .contained()
+    //         .with_background_color(microphone_state.unwrap_or(background_color))
+    //         .with_corner_radius(avatar_style.outer_corner_radius)
+    //         .constrained()
+    //         .with_width(avatar_style.outer_width)
+    //         .with_height(avatar_style.outer_width)
+    //         .aligned()
+    //         .into_any()
+    // }
+
+    // fn render_connection_status(
+    //     &self,
+    //     status: &client::Status,
+    //     cx: &mut ViewContext<Self>,
+    // ) -> Option<AnyElement<Self>> {
+    //     enum ConnectionStatusButton {}
+
+    //     let theme = &theme::current(cx).clone();
+    //     match status {
+    //         client::Status::ConnectionError
+    //         | client::Status::ConnectionLost
+    //         | client::Status::Reauthenticating { .. }
+    //         | client::Status::Reconnecting { .. }
+    //         | client::Status::ReconnectionError { .. } => Some(
+    //             Svg::new("icons/disconnected.svg")
+    //                 .with_color(theme.titlebar.offline_icon.color)
+    //                 .constrained()
+    //                 .with_width(theme.titlebar.offline_icon.width)
+    //                 .aligned()
+    //                 .contained()
+    //                 .with_style(theme.titlebar.offline_icon.container)
+    //                 .into_any(),
+    //         ),
+    //         client::Status::UpgradeRequired => {
+    //             let auto_updater = auto_update::AutoUpdater::get(cx);
+    //             let label = match auto_updater.map(|auto_update| auto_update.read(cx).status()) {
+    //                 Some(AutoUpdateStatus::Updated) => "Please restart Zed to Collaborate",
+    //                 Some(AutoUpdateStatus::Installing)
+    //                 | Some(AutoUpdateStatus::Downloading)
+    //                 | Some(AutoUpdateStatus::Checking) => "Updating...",
+    //                 Some(AutoUpdateStatus::Idle) | Some(AutoUpdateStatus::Errored) | None => {
+    //                     "Please update Zed to Collaborate"
+    //                 }
+    //             };
+
+    //             Some(
+    //                 MouseEventHandler::new::<ConnectionStatusButton, _>(0, cx, |_, _| {
+    //                     Label::new(label, theme.titlebar.outdated_warning.text.clone())
+    //                         .contained()
+    //                         .with_style(theme.titlebar.outdated_warning.container)
+    //                         .aligned()
+    //                 })
+    //                 .with_cursor_style(CursorStyle::PointingHand)
+    //                 .on_click(MouseButton::Left, |_, _, cx| {
+    //                     if let Some(auto_updater) = auto_update::AutoUpdater::get(cx) {
+    //                         if auto_updater.read(cx).status() == AutoUpdateStatus::Updated {
+    //                             workspace::restart(&Default::default(), cx);
+    //                             return;
+    //                         }
+    //                     }
+    //                     auto_update::check(&Default::default(), cx);
+    //                 })
+    //                 .into_any(),
+    //             )
+    //         }
+    //         _ => None,
+    //     }
+    // }
 }
 
-impl AvatarRibbon {
-    pub fn new(color: Color) -> AvatarRibbon {
-        AvatarRibbon { color }
-    }
-}
-
-impl Element<CollabTitlebarItem> for AvatarRibbon {
-    type LayoutState = ();
-
-    type PaintState = ();
-
-    fn layout(
-        &mut self,
-        constraint: gpui::SizeConstraint,
-        _: &mut CollabTitlebarItem,
-        _: &mut ViewContext<CollabTitlebarItem>,
-    ) -> (gpui::geometry::vector::Vector2F, Self::LayoutState) {
-        (constraint.max, ())
-    }
-
-    fn paint(
-        &mut self,
-        bounds: RectF,
-        _: RectF,
-        _: &mut Self::LayoutState,
-        _: &mut CollabTitlebarItem,
-        cx: &mut ViewContext<CollabTitlebarItem>,
-    ) -> Self::PaintState {
-        let mut path = PathBuilder::new();
-        path.reset(bounds.lower_left());
-        path.curve_to(
-            bounds.origin() + vec2f(bounds.height(), 0.),
-            bounds.origin(),
-        );
-        path.line_to(bounds.upper_right() - vec2f(bounds.height(), 0.));
-        path.curve_to(bounds.lower_right(), bounds.upper_right());
-        path.line_to(bounds.lower_left());
-        cx.scene().push_path(path.build(self.color, None));
-    }
-
-    fn rect_for_text_range(
-        &self,
-        _: Range<usize>,
-        _: RectF,
-        _: RectF,
-        _: &Self::LayoutState,
-        _: &Self::PaintState,
-        _: &CollabTitlebarItem,
-        _: &ViewContext<CollabTitlebarItem>,
-    ) -> Option<RectF> {
-        None
-    }
-
-    fn debug(
-        &self,
-        bounds: RectF,
-        _: &Self::LayoutState,
-        _: &Self::PaintState,
-        _: &CollabTitlebarItem,
-        _: &ViewContext<CollabTitlebarItem>,
-    ) -> gpui::json::Value {
-        json::json!({
-            "type": "AvatarRibbon",
-            "bounds": bounds.to_json(),
-            "color": self.color.to_json(),
-        })
-    }
-}
+// pub struct AvatarRibbon {
+//     color: Color,
+// }
+
+// impl AvatarRibbon {
+//     pub fn new(color: Color) -> AvatarRibbon {
+//         AvatarRibbon { color }
+//     }
+// }
+
+// impl Element<CollabTitlebarItem> for AvatarRibbon {
+//     type LayoutState = ();
+
+//     type PaintState = ();
+
+//     fn layout(
+//         &mut self,
+//         constraint: gpui::SizeConstraint,
+//         _: &mut CollabTitlebarItem,
+//         _: &mut ViewContext<CollabTitlebarItem>,
+//     ) -> (gpui::geometry::vector::Vector2F, Self::LayoutState) {
+//         (constraint.max, ())
+//     }
+
+//     fn paint(
+//         &mut self,
+//         bounds: RectF,
+//         _: RectF,
+//         _: &mut Self::LayoutState,
+//         _: &mut CollabTitlebarItem,
+//         cx: &mut ViewContext<CollabTitlebarItem>,
+//     ) -> Self::PaintState {
+//         let mut path = PathBuilder::new();
+//         path.reset(bounds.lower_left());
+//         path.curve_to(
+//             bounds.origin() + vec2f(bounds.height(), 0.),
+//             bounds.origin(),
+//         );
+//         path.line_to(bounds.upper_right() - vec2f(bounds.height(), 0.));
+//         path.curve_to(bounds.lower_right(), bounds.upper_right());
+//         path.line_to(bounds.lower_left());
+//         cx.scene().push_path(path.build(self.color, None));
+//     }
+
+//     fn rect_for_text_range(
+//         &self,
+//         _: Range<usize>,
+//         _: RectF,
+//         _: RectF,
+//         _: &Self::LayoutState,
+//         _: &Self::PaintState,
+//         _: &CollabTitlebarItem,
+//         _: &ViewContext<CollabTitlebarItem>,
+//     ) -> Option<RectF> {
+//         None
+//     }
+
+//     fn debug(
+//         &self,
+//         bounds: RectF,
+//         _: &Self::LayoutState,
+//         _: &Self::PaintState,
+//         _: &CollabTitlebarItem,
+//         _: &ViewContext<CollabTitlebarItem>,
+//     ) -> gpui::json::Value {
+//         json::json!({
+//             "type": "AvatarRibbon",
+//             "bounds": bounds.to_json(),
+//             "color": self.color.to_json(),
+//         })
+//     }
+// }

crates/collab_ui2/src/collab_ui.rs 🔗

@@ -7,159 +7,147 @@ pub mod notification_panel;
 pub mod notifications;
 mod panel_settings;
 
-use call::{report_call_event_for_room, ActiveCall, Room};
-use feature_flags::{ChannelsAlpha, FeatureFlagAppExt};
-use gpui::{
-    actions,
-    elements::{ContainerStyle, Empty, Image},
-    geometry::{
-        rect::RectF,
-        vector::{vec2f, Vector2F},
-    },
-    platform::{Screen, WindowBounds, WindowKind, WindowOptions},
-    AnyElement, AppContext, Element, ImageData, Task,
-};
-use std::{rc::Rc, sync::Arc};
-use theme::AvatarStyle;
-use util::ResultExt;
-use workspace::AppState;
+use std::sync::Arc;
 
 pub use collab_titlebar_item::CollabTitlebarItem;
+use gpui::AppContext;
 pub use panel_settings::{
     ChatPanelSettings, CollaborationPanelSettings, NotificationPanelSettings,
 };
+use settings::Settings;
+use workspace::AppState;
 
-actions!(
-    collab,
-    [ToggleScreenSharing, ToggleMute, ToggleDeafen, LeaveCall]
-);
+// actions!(
+//     collab,
+//     [ToggleScreenSharing, ToggleMute, ToggleDeafen, LeaveCall]
+// );
 
-pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
-    settings::register::<CollaborationPanelSettings>(cx);
-    settings::register::<ChatPanelSettings>(cx);
-    settings::register::<NotificationPanelSettings>(cx);
+pub fn init(_app_state: &Arc<AppState>, cx: &mut AppContext) {
+    CollaborationPanelSettings::register(cx);
+    ChatPanelSettings::register(cx);
+    NotificationPanelSettings::register(cx);
 
-    vcs_menu::init(cx);
+    // vcs_menu::init(cx);
     collab_titlebar_item::init(cx);
-    collab_panel::init(cx);
-    chat_panel::init(cx);
-    notifications::init(&app_state, cx);
+    // collab_panel::init(cx);
+    // chat_panel::init(cx);
+    // notifications::init(&app_state, cx);
 
-    cx.add_global_action(toggle_screen_sharing);
-    cx.add_global_action(toggle_mute);
-    cx.add_global_action(toggle_deafen);
+    // cx.add_global_action(toggle_screen_sharing);
+    // cx.add_global_action(toggle_mute);
+    // cx.add_global_action(toggle_deafen);
 }
 
-pub fn toggle_screen_sharing(_: &ToggleScreenSharing, cx: &mut AppContext) {
-    let call = ActiveCall::global(cx).read(cx);
-    if let Some(room) = call.room().cloned() {
-        let client = call.client();
-        let toggle_screen_sharing = room.update(cx, |room, cx| {
-            if room.is_screen_sharing() {
-                report_call_event_for_room(
-                    "disable screen share",
-                    room.id(),
-                    room.channel_id(),
-                    &client,
-                    cx,
-                );
-                Task::ready(room.unshare_screen(cx))
-            } else {
-                report_call_event_for_room(
-                    "enable screen share",
-                    room.id(),
-                    room.channel_id(),
-                    &client,
-                    cx,
-                );
-                room.share_screen(cx)
-            }
-        });
-        toggle_screen_sharing.detach_and_log_err(cx);
-    }
-}
+// pub fn toggle_screen_sharing(_: &ToggleScreenSharing, cx: &mut AppContext) {
+//     let call = ActiveCall::global(cx).read(cx);
+//     if let Some(room) = call.room().cloned() {
+//         let client = call.client();
+//         let toggle_screen_sharing = room.update(cx, |room, cx| {
+//             if room.is_screen_sharing() {
+//                 report_call_event_for_room(
+//                     "disable screen share",
+//                     room.id(),
+//                     room.channel_id(),
+//                     &client,
+//                     cx,
+//                 );
+//                 Task::ready(room.unshare_screen(cx))
+//             } else {
+//                 report_call_event_for_room(
+//                     "enable screen share",
+//                     room.id(),
+//                     room.channel_id(),
+//                     &client,
+//                     cx,
+//                 );
+//                 room.share_screen(cx)
+//             }
+//         });
+//         toggle_screen_sharing.detach_and_log_err(cx);
+//     }
+// }
 
-pub fn toggle_mute(_: &ToggleMute, cx: &mut AppContext) {
-    let call = ActiveCall::global(cx).read(cx);
-    if let Some(room) = call.room().cloned() {
-        let client = call.client();
-        room.update(cx, |room, cx| {
-            let operation = if room.is_muted(cx) {
-                "enable microphone"
-            } else {
-                "disable microphone"
-            };
-            report_call_event_for_room(operation, room.id(), room.channel_id(), &client, cx);
+// pub fn toggle_mute(_: &ToggleMute, cx: &mut AppContext) {
+//     let call = ActiveCall::global(cx).read(cx);
+//     if let Some(room) = call.room().cloned() {
+//         let client = call.client();
+//         room.update(cx, |room, cx| {
+//             let operation = if room.is_muted(cx) {
+//                 "enable microphone"
+//             } else {
+//                 "disable microphone"
+//             };
+//             report_call_event_for_room(operation, room.id(), room.channel_id(), &client, cx);
 
-            room.toggle_mute(cx)
-        })
-        .map(|task| task.detach_and_log_err(cx))
-        .log_err();
-    }
-}
+//             room.toggle_mute(cx)
+//         })
+//         .map(|task| task.detach_and_log_err(cx))
+//         .log_err();
+//     }
+// }
 
-pub fn toggle_deafen(_: &ToggleDeafen, cx: &mut AppContext) {
-    if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
-        room.update(cx, Room::toggle_deafen)
-            .map(|task| task.detach_and_log_err(cx))
-            .log_err();
-    }
-}
+// pub fn toggle_deafen(_: &ToggleDeafen, cx: &mut AppContext) {
+//     if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
+//         room.update(cx, Room::toggle_deafen)
+//             .map(|task| task.detach_and_log_err(cx))
+//             .log_err();
+//     }
+// }
 
-fn notification_window_options(
-    screen: Rc<dyn Screen>,
-    window_size: Vector2F,
-) -> WindowOptions<'static> {
-    const NOTIFICATION_PADDING: f32 = 16.;
+// fn notification_window_options(
+//     screen: Rc<dyn Screen>,
+//     window_size: Vector2F,
+// ) -> WindowOptions<'static> {
+//     const NOTIFICATION_PADDING: f32 = 16.;
 
-    let screen_bounds = screen.content_bounds();
-    WindowOptions {
-        bounds: WindowBounds::Fixed(RectF::new(
-            screen_bounds.upper_right()
-                + vec2f(
-                    -NOTIFICATION_PADDING - window_size.x(),
-                    NOTIFICATION_PADDING,
-                ),
-            window_size,
-        )),
-        titlebar: None,
-        center: false,
-        focus: false,
-        show: true,
-        kind: WindowKind::PopUp,
-        is_movable: false,
-        screen: Some(screen),
-    }
-}
+//     let screen_bounds = screen.content_bounds();
+//     WindowOptions {
+//         bounds: WindowBounds::Fixed(RectF::new(
+//             screen_bounds.upper_right()
+//                 + vec2f(
+//                     -NOTIFICATION_PADDING - window_size.x(),
+//                     NOTIFICATION_PADDING,
+//                 ),
+//             window_size,
+//         )),
+//         titlebar: None,
+//         center: false,
+//         focus: false,
+//         show: true,
+//         kind: WindowKind::PopUp,
+//         is_movable: false,
+//         screen: Some(screen),
+//     }
+// }
 
-fn render_avatar<T: 'static>(
-    avatar: Option<Arc<ImageData>>,
-    avatar_style: &AvatarStyle,
-    container: ContainerStyle,
-) -> AnyElement<T> {
-    avatar
-        .map(|avatar| {
-            Image::from_data(avatar)
-                .with_style(avatar_style.image)
-                .aligned()
-                .contained()
-                .with_corner_radius(avatar_style.outer_corner_radius)
-                .constrained()
-                .with_width(avatar_style.outer_width)
-                .with_height(avatar_style.outer_width)
-                .into_any()
-        })
-        .unwrap_or_else(|| {
-            Empty::new()
-                .constrained()
-                .with_width(avatar_style.outer_width)
-                .into_any()
-        })
-        .contained()
-        .with_style(container)
-        .into_any()
-}
+// fn render_avatar<T: 'static>(
+//     avatar: Option<Arc<ImageData>>,
+//     avatar_style: &AvatarStyle,
+//     container: ContainerStyle,
+// ) -> AnyElement<T> {
+//     avatar
+//         .map(|avatar| {
+//             Image::from_data(avatar)
+//                 .with_style(avatar_style.image)
+//                 .aligned()
+//                 .contained()
+//                 .with_corner_radius(avatar_style.outer_corner_radius)
+//                 .constrained()
+//                 .with_width(avatar_style.outer_width)
+//                 .with_height(avatar_style.outer_width)
+//                 .into_any()
+//         })
+//         .unwrap_or_else(|| {
+//             Empty::new()
+//                 .constrained()
+//                 .with_width(avatar_style.outer_width)
+//                 .into_any()
+//         })
+//         .contained()
+//         .with_style(container)
+//         .into_any()
+// }
 
-fn is_channels_feature_enabled(cx: &gpui::WindowContext<'_>) -> bool {
-    cx.is_staff() || cx.has_flag::<ChannelsAlpha>()
-}
+// fn is_channels_feature_enabled(cx: &gpui::WindowContext<'_>) -> bool {
+//     cx.is_staff() || cx.has_flag::<ChannelsAlpha>()
+// }

crates/collab_ui2/src/face_pile.rs 🔗

@@ -1,113 +1,113 @@
-use std::ops::Range;
+// use std::ops::Range;
 
-use gpui::{
-    geometry::{
-        rect::RectF,
-        vector::{vec2f, Vector2F},
-    },
-    json::ToJson,
-    serde_json::{self, json},
-    AnyElement, Axis, Element, View, ViewContext,
-};
+// use gpui::{
+//     geometry::{
+//         rect::RectF,
+//         vector::{vec2f, Vector2F},
+//     },
+//     json::ToJson,
+//     serde_json::{self, json},
+//     AnyElement, Axis, Element, View, ViewContext,
+// };
 
-pub(crate) struct FacePile<V: View> {
-    overlap: f32,
-    faces: Vec<AnyElement<V>>,
-}
+// pub(crate) struct FacePile<V: View> {
+//     overlap: f32,
+//     faces: Vec<AnyElement<V>>,
+// }
 
-impl<V: View> FacePile<V> {
-    pub fn new(overlap: f32) -> Self {
-        Self {
-            overlap,
-            faces: Vec::new(),
-        }
-    }
-}
+// impl<V: View> FacePile<V> {
+//     pub fn new(overlap: f32) -> Self {
+//         Self {
+//             overlap,
+//             faces: Vec::new(),
+//         }
+//     }
+// }
 
-impl<V: View> Element<V> for FacePile<V> {
-    type LayoutState = ();
-    type PaintState = ();
+// impl<V: View> Element<V> for FacePile<V> {
+//     type LayoutState = ();
+//     type PaintState = ();
 
-    fn layout(
-        &mut self,
-        constraint: gpui::SizeConstraint,
-        view: &mut V,
-        cx: &mut ViewContext<V>,
-    ) -> (Vector2F, Self::LayoutState) {
-        debug_assert!(constraint.max_along(Axis::Horizontal) == f32::INFINITY);
+//     fn layout(
+//         &mut self,
+//         constraint: gpui::SizeConstraint,
+//         view: &mut V,
+//         cx: &mut ViewContext<V>,
+//     ) -> (Vector2F, Self::LayoutState) {
+//         debug_assert!(constraint.max_along(Axis::Horizontal) == f32::INFINITY);
 
-        let mut width = 0.;
-        let mut max_height = 0.;
-        for face in &mut self.faces {
-            let layout = face.layout(constraint, view, cx);
-            width += layout.x();
-            max_height = f32::max(max_height, layout.y());
-        }
-        width -= self.overlap * self.faces.len().saturating_sub(1) as f32;
+//         let mut width = 0.;
+//         let mut max_height = 0.;
+//         for face in &mut self.faces {
+//             let layout = face.layout(constraint, view, cx);
+//             width += layout.x();
+//             max_height = f32::max(max_height, layout.y());
+//         }
+//         width -= self.overlap * self.faces.len().saturating_sub(1) as f32;
 
-        (
-            Vector2F::new(width, max_height.clamp(1., constraint.max.y())),
-            (),
-        )
-    }
+//         (
+//             Vector2F::new(width, max_height.clamp(1., constraint.max.y())),
+//             (),
+//         )
+//     }
 
-    fn paint(
-        &mut self,
-        bounds: RectF,
-        visible_bounds: RectF,
-        _layout: &mut Self::LayoutState,
-        view: &mut V,
-        cx: &mut ViewContext<V>,
-    ) -> Self::PaintState {
-        let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default();
+//     fn paint(
+//         &mut self,
+//         bounds: RectF,
+//         visible_bounds: RectF,
+//         _layout: &mut Self::LayoutState,
+//         view: &mut V,
+//         cx: &mut ViewContext<V>,
+//     ) -> Self::PaintState {
+//         let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default();
 
-        let origin_y = bounds.upper_right().y();
-        let mut origin_x = bounds.upper_right().x();
+//         let origin_y = bounds.upper_right().y();
+//         let mut origin_x = bounds.upper_right().x();
 
-        for face in self.faces.iter_mut().rev() {
-            let size = face.size();
-            origin_x -= size.x();
-            let origin_y = origin_y + (bounds.height() - size.y()) / 2.0;
+//         for face in self.faces.iter_mut().rev() {
+//             let size = face.size();
+//             origin_x -= size.x();
+//             let origin_y = origin_y + (bounds.height() - size.y()) / 2.0;
 
-            cx.scene().push_layer(None);
-            face.paint(vec2f(origin_x, origin_y), visible_bounds, view, cx);
-            cx.scene().pop_layer();
-            origin_x += self.overlap;
-        }
+//             cx.scene().push_layer(None);
+//             face.paint(vec2f(origin_x, origin_y), visible_bounds, view, cx);
+//             cx.scene().pop_layer();
+//             origin_x += self.overlap;
+//         }
 
-        ()
-    }
+//         ()
+//     }
 
-    fn rect_for_text_range(
-        &self,
-        _: Range<usize>,
-        _: RectF,
-        _: RectF,
-        _: &Self::LayoutState,
-        _: &Self::PaintState,
-        _: &V,
-        _: &ViewContext<V>,
-    ) -> Option<RectF> {
-        None
-    }
+//     fn rect_for_text_range(
+//         &self,
+//         _: Range<usize>,
+//         _: RectF,
+//         _: RectF,
+//         _: &Self::LayoutState,
+//         _: &Self::PaintState,
+//         _: &V,
+//         _: &ViewContext<V>,
+//     ) -> Option<RectF> {
+//         None
+//     }
 
-    fn debug(
-        &self,
-        bounds: RectF,
-        _: &Self::LayoutState,
-        _: &Self::PaintState,
-        _: &V,
-        _: &ViewContext<V>,
-    ) -> serde_json::Value {
-        json!({
-            "type": "FacePile",
-            "bounds": bounds.to_json()
-        })
-    }
-}
+//     fn debug(
+//         &self,
+//         bounds: RectF,
+//         _: &Self::LayoutState,
+//         _: &Self::PaintState,
+//         _: &V,
+//         _: &ViewContext<V>,
+//     ) -> serde_json::Value {
+//         json!({
+//             "type": "FacePile",
+//             "bounds": bounds.to_json()
+//         })
+//     }
+// }
 
-impl<V: View> Extend<AnyElement<V>> for FacePile<V> {
-    fn extend<T: IntoIterator<Item = AnyElement<V>>>(&mut self, children: T) {
-        self.faces.extend(children);
-    }
-}
+// impl<V: View> Extend<AnyElement<V>> for FacePile<V> {
+//     fn extend<T: IntoIterator<Item = AnyElement<V>>>(&mut self, children: T) {
+//         self.faces.extend(children);
+//     }
+// }

crates/collab_ui2/src/notification_panel.rs 🔗

@@ -1,884 +1,884 @@
-use crate::{chat_panel::ChatPanel, render_avatar, NotificationPanelSettings};
-use anyhow::Result;
-use channel::ChannelStore;
-use client::{Client, Notification, User, UserStore};
-use collections::HashMap;
-use db::kvp::KEY_VALUE_STORE;
-use futures::StreamExt;
-use gpui::{
-    actions,
-    elements::*,
-    platform::{CursorStyle, MouseButton},
-    serde_json, AnyViewHandle, AppContext, AsyncAppContext, Entity, ModelHandle, Task, View,
-    ViewContext, ViewHandle, WeakViewHandle, WindowContext,
-};
-use notifications::{NotificationEntry, NotificationEvent, NotificationStore};
-use project::Fs;
-use rpc::proto;
-use serde::{Deserialize, Serialize};
-use settings::SettingsStore;
-use std::{sync::Arc, time::Duration};
-use theme::{ui, Theme};
-use time::{OffsetDateTime, UtcOffset};
-use util::{ResultExt, TryFutureExt};
-use workspace::{
-    dock::{DockPosition, Panel},
-    Workspace,
-};
-
-const LOADING_THRESHOLD: usize = 30;
-const MARK_AS_READ_DELAY: Duration = Duration::from_secs(1);
-const TOAST_DURATION: Duration = Duration::from_secs(5);
-const NOTIFICATION_PANEL_KEY: &'static str = "NotificationPanel";
-
-pub struct NotificationPanel {
-    client: Arc<Client>,
-    user_store: ModelHandle<UserStore>,
-    channel_store: ModelHandle<ChannelStore>,
-    notification_store: ModelHandle<NotificationStore>,
-    fs: Arc<dyn Fs>,
-    width: Option<f32>,
-    active: bool,
-    notification_list: ListState<Self>,
-    pending_serialization: Task<Option<()>>,
-    subscriptions: Vec<gpui::Subscription>,
-    workspace: WeakViewHandle<Workspace>,
-    current_notification_toast: Option<(u64, Task<()>)>,
-    local_timezone: UtcOffset,
-    has_focus: bool,
-    mark_as_read_tasks: HashMap<u64, Task<Result<()>>>,
-}
-
-#[derive(Serialize, Deserialize)]
-struct SerializedNotificationPanel {
-    width: Option<f32>,
-}
-
-#[derive(Debug)]
-pub enum Event {
-    DockPositionChanged,
-    Focus,
-    Dismissed,
-}
-
-pub struct NotificationPresenter {
-    pub actor: Option<Arc<client::User>>,
-    pub text: String,
-    pub icon: &'static str,
-    pub needs_response: bool,
-    pub can_navigate: bool,
-}
-
-actions!(notification_panel, [ToggleFocus]);
-
-pub fn init(_cx: &mut AppContext) {}
-
-impl NotificationPanel {
-    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 user_store = workspace.app_state().user_store.clone();
-        let workspace_handle = workspace.weak_handle();
-
-        cx.add_view(|cx| {
-            let mut status = client.status();
-            cx.spawn(|this, mut cx| async move {
-                while let Some(_) = status.next().await {
-                    if this
-                        .update(&mut cx, |_, cx| {
-                            cx.notify();
-                        })
-                        .is_err()
-                    {
-                        break;
-                    }
-                }
-            })
-            .detach();
-
-            let mut notification_list =
-                ListState::<Self>::new(0, Orientation::Top, 1000., move |this, ix, cx| {
-                    this.render_notification(ix, cx)
-                        .unwrap_or_else(|| Empty::new().into_any())
-                });
-            notification_list.set_scroll_handler(|visible_range, count, this, cx| {
-                if count.saturating_sub(visible_range.end) < LOADING_THRESHOLD {
-                    if let Some(task) = this
-                        .notification_store
-                        .update(cx, |store, cx| store.load_more_notifications(false, cx))
-                    {
-                        task.detach();
-                    }
-                }
-            });
-
-            let mut this = Self {
-                fs,
-                client,
-                user_store,
-                local_timezone: cx.platform().local_timezone(),
-                channel_store: ChannelStore::global(cx),
-                notification_store: NotificationStore::global(cx),
-                notification_list,
-                pending_serialization: Task::ready(None),
-                workspace: workspace_handle,
-                has_focus: false,
-                current_notification_toast: None,
-                subscriptions: Vec::new(),
-                active: false,
-                mark_as_read_tasks: HashMap::default(),
-                width: None,
-            };
-
-            let mut old_dock_position = this.position(cx);
-            this.subscriptions.extend([
-                cx.observe(&this.notification_store, |_, _, cx| cx.notify()),
-                cx.subscribe(&this.notification_store, Self::on_notification_event),
-                cx.observe_global::<SettingsStore, _>(move |this: &mut Self, cx| {
-                    let new_dock_position = this.position(cx);
-                    if new_dock_position != old_dock_position {
-                        old_dock_position = new_dock_position;
-                        cx.emit(Event::DockPositionChanged);
-                    }
-                    cx.notify();
-                }),
-            ]);
-            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(NOTIFICATION_PANEL_KEY) })
-                .await
-                .log_err()
-                .flatten()
-            {
-                Some(serde_json::from_str::<SerializedNotificationPanel>(&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
-            })
-        })
-    }
-
-    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(
-                        NOTIFICATION_PANEL_KEY.into(),
-                        serde_json::to_string(&SerializedNotificationPanel { width })?,
-                    )
-                    .await?;
-                anyhow::Ok(())
-            }
-            .log_err(),
-        );
-    }
-
-    fn render_notification(
-        &mut self,
-        ix: usize,
-        cx: &mut ViewContext<Self>,
-    ) -> Option<AnyElement<Self>> {
-        let entry = self.notification_store.read(cx).notification_at(ix)?;
-        let notification_id = entry.id;
-        let now = OffsetDateTime::now_utc();
-        let timestamp = entry.timestamp;
-        let NotificationPresenter {
-            actor,
-            text,
-            needs_response,
-            can_navigate,
-            ..
-        } = self.present_notification(entry, cx)?;
-
-        let theme = theme::current(cx);
-        let style = &theme.notification_panel;
-        let response = entry.response;
-        let notification = entry.notification.clone();
-
-        let message_style = if entry.is_read {
-            style.read_text.clone()
-        } else {
-            style.unread_text.clone()
-        };
-
-        if self.active && !entry.is_read {
-            self.did_render_notification(notification_id, &notification, cx);
-        }
-
-        enum Decline {}
-        enum Accept {}
-
-        Some(
-            MouseEventHandler::new::<NotificationEntry, _>(ix, cx, |_, cx| {
-                let container = message_style.container;
-
-                Flex::row()
-                    .with_children(actor.map(|actor| {
-                        render_avatar(actor.avatar.clone(), &style.avatar, style.avatar_container)
-                    }))
-                    .with_child(
-                        Flex::column()
-                            .with_child(Text::new(text, message_style.text.clone()))
-                            .with_child(
-                                Flex::row()
-                                    .with_child(
-                                        Label::new(
-                                            format_timestamp(timestamp, now, self.local_timezone),
-                                            style.timestamp.text.clone(),
-                                        )
-                                        .contained()
-                                        .with_style(style.timestamp.container),
-                                    )
-                                    .with_children(if let Some(is_accepted) = response {
-                                        Some(
-                                            Label::new(
-                                                if is_accepted {
-                                                    "You accepted"
-                                                } else {
-                                                    "You declined"
-                                                },
-                                                style.read_text.text.clone(),
-                                            )
-                                            .flex_float()
-                                            .into_any(),
-                                        )
-                                    } else if needs_response {
-                                        Some(
-                                            Flex::row()
-                                                .with_children([
-                                                    MouseEventHandler::new::<Decline, _>(
-                                                        ix,
-                                                        cx,
-                                                        |state, _| {
-                                                            let button =
-                                                                style.button.style_for(state);
-                                                            Label::new(
-                                                                "Decline",
-                                                                button.text.clone(),
-                                                            )
-                                                            .contained()
-                                                            .with_style(button.container)
-                                                        },
-                                                    )
-                                                    .with_cursor_style(CursorStyle::PointingHand)
-                                                    .on_click(MouseButton::Left, {
-                                                        let notification = notification.clone();
-                                                        move |_, view, cx| {
-                                                            view.respond_to_notification(
-                                                                notification.clone(),
-                                                                false,
-                                                                cx,
-                                                            );
-                                                        }
-                                                    }),
-                                                    MouseEventHandler::new::<Accept, _>(
-                                                        ix,
-                                                        cx,
-                                                        |state, _| {
-                                                            let button =
-                                                                style.button.style_for(state);
-                                                            Label::new(
-                                                                "Accept",
-                                                                button.text.clone(),
-                                                            )
-                                                            .contained()
-                                                            .with_style(button.container)
-                                                        },
-                                                    )
-                                                    .with_cursor_style(CursorStyle::PointingHand)
-                                                    .on_click(MouseButton::Left, {
-                                                        let notification = notification.clone();
-                                                        move |_, view, cx| {
-                                                            view.respond_to_notification(
-                                                                notification.clone(),
-                                                                true,
-                                                                cx,
-                                                            );
-                                                        }
-                                                    }),
-                                                ])
-                                                .flex_float()
-                                                .into_any(),
-                                        )
-                                    } else {
-                                        None
-                                    }),
-                            )
-                            .flex(1.0, true),
-                    )
-                    .contained()
-                    .with_style(container)
-                    .into_any()
-            })
-            .with_cursor_style(if can_navigate {
-                CursorStyle::PointingHand
-            } else {
-                CursorStyle::default()
-            })
-            .on_click(MouseButton::Left, {
-                let notification = notification.clone();
-                move |_, this, cx| this.did_click_notification(&notification, cx)
-            })
-            .into_any(),
-        )
-    }
-
-    fn present_notification(
-        &self,
-        entry: &NotificationEntry,
-        cx: &AppContext,
-    ) -> Option<NotificationPresenter> {
-        let user_store = self.user_store.read(cx);
-        let channel_store = self.channel_store.read(cx);
-        match entry.notification {
-            Notification::ContactRequest { sender_id } => {
-                let requester = user_store.get_cached_user(sender_id)?;
-                Some(NotificationPresenter {
-                    icon: "icons/plus.svg",
-                    text: format!("{} wants to add you as a contact", requester.github_login),
-                    needs_response: user_store.has_incoming_contact_request(requester.id),
-                    actor: Some(requester),
-                    can_navigate: false,
-                })
-            }
-            Notification::ContactRequestAccepted { responder_id } => {
-                let responder = user_store.get_cached_user(responder_id)?;
-                Some(NotificationPresenter {
-                    icon: "icons/plus.svg",
-                    text: format!("{} accepted your contact invite", responder.github_login),
-                    needs_response: false,
-                    actor: Some(responder),
-                    can_navigate: false,
-                })
-            }
-            Notification::ChannelInvitation {
-                ref channel_name,
-                channel_id,
-                inviter_id,
-            } => {
-                let inviter = user_store.get_cached_user(inviter_id)?;
-                Some(NotificationPresenter {
-                    icon: "icons/hash.svg",
-                    text: format!(
-                        "{} invited you to join the #{channel_name} channel",
-                        inviter.github_login
-                    ),
-                    needs_response: channel_store.has_channel_invitation(channel_id),
-                    actor: Some(inviter),
-                    can_navigate: false,
-                })
-            }
-            Notification::ChannelMessageMention {
-                sender_id,
-                channel_id,
-                message_id,
-            } => {
-                let sender = user_store.get_cached_user(sender_id)?;
-                let channel = channel_store.channel_for_id(channel_id)?;
-                let message = self
-                    .notification_store
-                    .read(cx)
-                    .channel_message_for_id(message_id)?;
-                Some(NotificationPresenter {
-                    icon: "icons/conversations.svg",
-                    text: format!(
-                        "{} mentioned you in #{}:\n{}",
-                        sender.github_login, channel.name, message.body,
-                    ),
-                    needs_response: false,
-                    actor: Some(sender),
-                    can_navigate: true,
-                })
-            }
-        }
-    }
-
-    fn did_render_notification(
-        &mut self,
-        notification_id: u64,
-        notification: &Notification,
-        cx: &mut ViewContext<Self>,
-    ) {
-        let should_mark_as_read = match notification {
-            Notification::ContactRequestAccepted { .. } => true,
-            Notification::ContactRequest { .. }
-            | Notification::ChannelInvitation { .. }
-            | Notification::ChannelMessageMention { .. } => false,
-        };
-
-        if should_mark_as_read {
-            self.mark_as_read_tasks
-                .entry(notification_id)
-                .or_insert_with(|| {
-                    let client = self.client.clone();
-                    cx.spawn(|this, mut cx| async move {
-                        cx.background().timer(MARK_AS_READ_DELAY).await;
-                        client
-                            .request(proto::MarkNotificationRead { notification_id })
-                            .await?;
-                        this.update(&mut cx, |this, _| {
-                            this.mark_as_read_tasks.remove(&notification_id);
-                        })?;
-                        Ok(())
-                    })
-                });
-        }
-    }
-
-    fn did_click_notification(&mut self, notification: &Notification, cx: &mut ViewContext<Self>) {
-        if let Notification::ChannelMessageMention {
-            message_id,
-            channel_id,
-            ..
-        } = notification.clone()
-        {
-            if let Some(workspace) = self.workspace.upgrade(cx) {
-                cx.app_context().defer(move |cx| {
-                    workspace.update(cx, |workspace, cx| {
-                        if let Some(panel) = workspace.focus_panel::<ChatPanel>(cx) {
-                            panel.update(cx, |panel, cx| {
-                                panel
-                                    .select_channel(channel_id, Some(message_id), cx)
-                                    .detach_and_log_err(cx);
-                            });
-                        }
-                    });
-                });
-            }
-        }
-    }
-
-    fn is_showing_notification(&self, notification: &Notification, cx: &AppContext) -> bool {
-        if let Notification::ChannelMessageMention { channel_id, .. } = &notification {
-            if let Some(workspace) = self.workspace.upgrade(cx) {
-                return workspace
-                    .read_with(cx, |workspace, cx| {
-                        if let Some(panel) = workspace.panel::<ChatPanel>(cx) {
-                            return panel.read_with(cx, |panel, cx| {
-                                panel.is_scrolled_to_bottom()
-                                    && panel.active_chat().map_or(false, |chat| {
-                                        chat.read(cx).channel_id == *channel_id
-                                    })
-                            });
-                        }
-                        false
-                    })
-                    .unwrap_or_default();
-            }
-        }
-
-        false
-    }
-
-    fn render_sign_in_prompt(
-        &self,
-        theme: &Arc<Theme>,
-        cx: &mut ViewContext<Self>,
-    ) -> AnyElement<Self> {
-        enum SignInPromptLabel {}
-
-        MouseEventHandler::new::<SignInPromptLabel, _>(0, cx, |mouse_state, _| {
-            Label::new(
-                "Sign in to view your notifications".to_string(),
-                theme
-                    .chat_panel
-                    .sign_in_prompt
-                    .style_for(mouse_state)
-                    .clone(),
-            )
-        })
-        .with_cursor_style(CursorStyle::PointingHand)
-        .on_click(MouseButton::Left, move |_, this, cx| {
-            let client = this.client.clone();
-            cx.spawn(|_, cx| async move {
-                client.authenticate_and_connect(true, &cx).log_err().await;
-            })
-            .detach();
-        })
-        .aligned()
-        .into_any()
-    }
-
-    fn render_empty_state(
-        &self,
-        theme: &Arc<Theme>,
-        _cx: &mut ViewContext<Self>,
-    ) -> AnyElement<Self> {
-        Label::new(
-            "You have no notifications".to_string(),
-            theme.chat_panel.sign_in_prompt.default.clone(),
-        )
-        .aligned()
-        .into_any()
-    }
-
-    fn on_notification_event(
-        &mut self,
-        _: ModelHandle<NotificationStore>,
-        event: &NotificationEvent,
-        cx: &mut ViewContext<Self>,
-    ) {
-        match event {
-            NotificationEvent::NewNotification { entry } => self.add_toast(entry, cx),
-            NotificationEvent::NotificationRemoved { entry }
-            | NotificationEvent::NotificationRead { entry } => self.remove_toast(entry.id, cx),
-            NotificationEvent::NotificationsUpdated {
-                old_range,
-                new_count,
-            } => {
-                self.notification_list.splice(old_range.clone(), *new_count);
-                cx.notify();
-            }
-        }
-    }
-
-    fn add_toast(&mut self, entry: &NotificationEntry, cx: &mut ViewContext<Self>) {
-        if self.is_showing_notification(&entry.notification, cx) {
-            return;
-        }
-
-        let Some(NotificationPresenter { actor, text, .. }) = self.present_notification(entry, cx)
-        else {
-            return;
-        };
-
-        let notification_id = entry.id;
-        self.current_notification_toast = Some((
-            notification_id,
-            cx.spawn(|this, mut cx| async move {
-                cx.background().timer(TOAST_DURATION).await;
-                this.update(&mut cx, |this, cx| this.remove_toast(notification_id, cx))
-                    .ok();
-            }),
-        ));
-
-        self.workspace
-            .update(cx, |workspace, cx| {
-                workspace.dismiss_notification::<NotificationToast>(0, cx);
-                workspace.show_notification(0, cx, |cx| {
-                    let workspace = cx.weak_handle();
-                    cx.add_view(|_| NotificationToast {
-                        notification_id,
-                        actor,
-                        text,
-                        workspace,
-                    })
-                })
-            })
-            .ok();
-    }
-
-    fn remove_toast(&mut self, notification_id: u64, cx: &mut ViewContext<Self>) {
-        if let Some((current_id, _)) = &self.current_notification_toast {
-            if *current_id == notification_id {
-                self.current_notification_toast.take();
-                self.workspace
-                    .update(cx, |workspace, cx| {
-                        workspace.dismiss_notification::<NotificationToast>(0, cx)
-                    })
-                    .ok();
-            }
-        }
-    }
-
-    fn respond_to_notification(
-        &mut self,
-        notification: Notification,
-        response: bool,
-        cx: &mut ViewContext<Self>,
-    ) {
-        self.notification_store.update(cx, |store, cx| {
-            store.respond_to_notification(notification, response, cx);
-        });
-    }
-}
-
-impl Entity for NotificationPanel {
-    type Event = Event;
-}
-
-impl View for NotificationPanel {
-    fn ui_name() -> &'static str {
-        "NotificationPanel"
-    }
-
-    fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
-        let theme = theme::current(cx);
-        let style = &theme.notification_panel;
-        let element = if self.client.user_id().is_none() {
-            self.render_sign_in_prompt(&theme, cx)
-        } else if self.notification_list.item_count() == 0 {
-            self.render_empty_state(&theme, cx)
-        } else {
-            Flex::column()
-                .with_child(
-                    Flex::row()
-                        .with_child(Label::new("Notifications", style.title.text.clone()))
-                        .with_child(ui::svg(&style.title_icon).flex_float())
-                        .align_children_center()
-                        .contained()
-                        .with_style(style.title.container)
-                        .constrained()
-                        .with_height(style.title_height),
-                )
-                .with_child(
-                    List::new(self.notification_list.clone())
-                        .contained()
-                        .with_style(style.list)
-                        .flex(1., true),
-                )
-                .into_any()
-        };
-        element
-            .contained()
-            .with_style(style.container)
-            .constrained()
-            .with_min_width(150.)
-            .into_any()
-    }
-
-    fn focus_in(&mut self, _: AnyViewHandle, _: &mut ViewContext<Self>) {
-        self.has_focus = true;
-    }
-
-    fn focus_out(&mut self, _: AnyViewHandle, _: &mut ViewContext<Self>) {
-        self.has_focus = false;
-    }
-}
-
-impl Panel for NotificationPanel {
-    fn position(&self, cx: &gpui::WindowContext) -> DockPosition {
-        settings::get::<NotificationPanelSettings>(cx).dock
-    }
-
-    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::<NotificationPanelSettings>(
-            self.fs.clone(),
-            cx,
-            move |settings| settings.dock = Some(position),
-        );
-    }
-
-    fn size(&self, cx: &gpui::WindowContext) -> f32 {
-        self.width
-            .unwrap_or_else(|| settings::get::<NotificationPanelSettings>(cx).default_width)
-    }
-
-    fn set_size(&mut self, size: Option<f32>, cx: &mut ViewContext<Self>) {
-        self.width = size;
-        self.serialize(cx);
-        cx.notify();
-    }
-
-    fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) {
-        self.active = active;
-        if self.notification_store.read(cx).notification_count() == 0 {
-            cx.emit(Event::Dismissed);
-        }
-    }
-
-    fn icon_path(&self, cx: &gpui::WindowContext) -> Option<&'static str> {
-        (settings::get::<NotificationPanelSettings>(cx).button
-            && self.notification_store.read(cx).notification_count() > 0)
-            .then(|| "icons/bell.svg")
-    }
-
-    fn icon_tooltip(&self) -> (String, Option<Box<dyn gpui::Action>>) {
-        (
-            "Notification Panel".to_string(),
-            Some(Box::new(ToggleFocus)),
-        )
-    }
-
-    fn icon_label(&self, cx: &WindowContext) -> Option<String> {
-        let count = self.notification_store.read(cx).unread_notification_count();
-        if count == 0 {
-            None
-        } else {
-            Some(count.to_string())
-        }
-    }
-
-    fn should_change_position_on_event(event: &Self::Event) -> bool {
-        matches!(event, Event::DockPositionChanged)
-    }
-
-    fn should_close_on_event(event: &Self::Event) -> bool {
-        matches!(event, Event::Dismissed)
-    }
-
-    fn has_focus(&self, _cx: &gpui::WindowContext) -> bool {
-        self.has_focus
-    }
-
-    fn is_focus_event(event: &Self::Event) -> bool {
-        matches!(event, Event::Focus)
-    }
-}
-
-pub struct NotificationToast {
-    notification_id: u64,
-    actor: Option<Arc<User>>,
-    text: String,
-    workspace: WeakViewHandle<Workspace>,
-}
-
-pub enum ToastEvent {
-    Dismiss,
-}
-
-impl NotificationToast {
-    fn focus_notification_panel(&self, cx: &mut AppContext) {
-        let workspace = self.workspace.clone();
-        let notification_id = self.notification_id;
-        cx.defer(move |cx| {
-            workspace
-                .update(cx, |workspace, cx| {
-                    if let Some(panel) = workspace.focus_panel::<NotificationPanel>(cx) {
-                        panel.update(cx, |panel, cx| {
-                            let store = panel.notification_store.read(cx);
-                            if let Some(entry) = store.notification_for_id(notification_id) {
-                                panel.did_click_notification(&entry.clone().notification, cx);
-                            }
-                        });
-                    }
-                })
-                .ok();
-        })
-    }
-}
-
-impl Entity for NotificationToast {
-    type Event = ToastEvent;
-}
-
-impl View for NotificationToast {
-    fn ui_name() -> &'static str {
-        "ContactNotification"
-    }
-
-    fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
-        let user = self.actor.clone();
-        let theme = theme::current(cx).clone();
-        let theme = &theme.contact_notification;
-
-        MouseEventHandler::new::<Self, _>(0, cx, |_, cx| {
-            Flex::row()
-                .with_children(user.and_then(|user| {
-                    Some(
-                        Image::from_data(user.avatar.clone()?)
-                            .with_style(theme.header_avatar)
-                            .aligned()
-                            .constrained()
-                            .with_height(
-                                cx.font_cache()
-                                    .line_height(theme.header_message.text.font_size),
-                            )
-                            .aligned()
-                            .top(),
-                    )
-                }))
-                .with_child(
-                    Text::new(self.text.clone(), theme.header_message.text.clone())
-                        .contained()
-                        .with_style(theme.header_message.container)
-                        .aligned()
-                        .top()
-                        .left()
-                        .flex(1., true),
-                )
-                .with_child(
-                    MouseEventHandler::new::<ToastEvent, _>(0, cx, |state, _| {
-                        let style = theme.dismiss_button.style_for(state);
-                        Svg::new("icons/x.svg")
-                            .with_color(style.color)
-                            .constrained()
-                            .with_width(style.icon_width)
-                            .aligned()
-                            .contained()
-                            .with_style(style.container)
-                            .constrained()
-                            .with_width(style.button_width)
-                            .with_height(style.button_width)
-                    })
-                    .with_cursor_style(CursorStyle::PointingHand)
-                    .with_padding(Padding::uniform(5.))
-                    .on_click(MouseButton::Left, move |_, _, cx| {
-                        cx.emit(ToastEvent::Dismiss)
-                    })
-                    .aligned()
-                    .constrained()
-                    .with_height(
-                        cx.font_cache()
-                            .line_height(theme.header_message.text.font_size),
-                    )
-                    .aligned()
-                    .top()
-                    .flex_float(),
-                )
-                .contained()
-        })
-        .with_cursor_style(CursorStyle::PointingHand)
-        .on_click(MouseButton::Left, move |_, this, cx| {
-            this.focus_notification_panel(cx);
-            cx.emit(ToastEvent::Dismiss);
-        })
-        .into_any()
-    }
-}
-
-impl workspace::notifications::Notification for NotificationToast {
-    fn should_dismiss_notification_on_event(&self, event: &<Self as Entity>::Event) -> bool {
-        matches!(event, ToastEvent::Dismiss)
-    }
-}
-
-fn format_timestamp(
-    mut timestamp: OffsetDateTime,
-    mut now: OffsetDateTime,
-    local_timezone: UtcOffset,
-) -> String {
-    timestamp = timestamp.to_offset(local_timezone);
-    now = now.to_offset(local_timezone);
-
-    let today = now.date();
-    let date = timestamp.date();
-    if date == today {
-        let difference = now - timestamp;
-        if difference >= Duration::from_secs(3600) {
-            format!("{}h", difference.whole_seconds() / 3600)
-        } else if difference >= Duration::from_secs(60) {
-            format!("{}m", difference.whole_seconds() / 60)
-        } else {
-            "just now".to_string()
-        }
-    } else if date.next_day() == Some(today) {
-        format!("yesterday")
-    } else {
-        format!("{:02}/{}/{}", date.month() as u32, date.day(), date.year())
-    }
-}
+// use crate::{chat_panel::ChatPanel, render_avatar, NotificationPanelSettings};
+// use anyhow::Result;
+// use channel::ChannelStore;
+// use client::{Client, Notification, User, UserStore};
+// use collections::HashMap;
+// use db::kvp::KEY_VALUE_STORE;
+// use futures::StreamExt;
+// use gpui::{
+//     actions,
+//     elements::*,
+//     platform::{CursorStyle, MouseButton},
+//     serde_json, AnyViewHandle, AppContext, AsyncAppContext, Entity, ModelHandle, Task, View,
+//     ViewContext, ViewHandle, WeakViewHandle, WindowContext,
+// };
+// use notifications::{NotificationEntry, NotificationEvent, NotificationStore};
+// use project::Fs;
+// use rpc::proto;
+// use serde::{Deserialize, Serialize};
+// use settings::SettingsStore;
+// use std::{sync::Arc, time::Duration};
+// use theme::{ui, Theme};
+// use time::{OffsetDateTime, UtcOffset};
+// use util::{ResultExt, TryFutureExt};
+// use workspace::{
+//     dock::{DockPosition, Panel},
+//     Workspace,
+// };
+
+// const LOADING_THRESHOLD: usize = 30;
+// const MARK_AS_READ_DELAY: Duration = Duration::from_secs(1);
+// const TOAST_DURATION: Duration = Duration::from_secs(5);
+// const NOTIFICATION_PANEL_KEY: &'static str = "NotificationPanel";
+
+// pub struct NotificationPanel {
+//     client: Arc<Client>,
+//     user_store: ModelHandle<UserStore>,
+//     channel_store: ModelHandle<ChannelStore>,
+//     notification_store: ModelHandle<NotificationStore>,
+//     fs: Arc<dyn Fs>,
+//     width: Option<f32>,
+//     active: bool,
+//     notification_list: ListState<Self>,
+//     pending_serialization: Task<Option<()>>,
+//     subscriptions: Vec<gpui::Subscription>,
+//     workspace: WeakViewHandle<Workspace>,
+//     current_notification_toast: Option<(u64, Task<()>)>,
+//     local_timezone: UtcOffset,
+//     has_focus: bool,
+//     mark_as_read_tasks: HashMap<u64, Task<Result<()>>>,
+// }
+
+// #[derive(Serialize, Deserialize)]
+// struct SerializedNotificationPanel {
+//     width: Option<f32>,
+// }
+
+// #[derive(Debug)]
+// pub enum Event {
+//     DockPositionChanged,
+//     Focus,
+//     Dismissed,
+// }
+
+// pub struct NotificationPresenter {
+//     pub actor: Option<Arc<client::User>>,
+//     pub text: String,
+//     pub icon: &'static str,
+//     pub needs_response: bool,
+//     pub can_navigate: bool,
+// }
+
+// actions!(notification_panel, [ToggleFocus]);
+
+// pub fn init(_cx: &mut AppContext) {}
+
+// impl NotificationPanel {
+//     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 user_store = workspace.app_state().user_store.clone();
+//         let workspace_handle = workspace.weak_handle();
+
+//         cx.add_view(|cx| {
+//             let mut status = client.status();
+//             cx.spawn(|this, mut cx| async move {
+//                 while let Some(_) = status.next().await {
+//                     if this
+//                         .update(&mut cx, |_, cx| {
+//                             cx.notify();
+//                         })
+//                         .is_err()
+//                     {
+//                         break;
+//                     }
+//                 }
+//             })
+//             .detach();
+
+//             let mut notification_list =
+//                 ListState::<Self>::new(0, Orientation::Top, 1000., move |this, ix, cx| {
+//                     this.render_notification(ix, cx)
+//                         .unwrap_or_else(|| Empty::new().into_any())
+//                 });
+//             notification_list.set_scroll_handler(|visible_range, count, this, cx| {
+//                 if count.saturating_sub(visible_range.end) < LOADING_THRESHOLD {
+//                     if let Some(task) = this
+//                         .notification_store
+//                         .update(cx, |store, cx| store.load_more_notifications(false, cx))
+//                     {
+//                         task.detach();
+//                     }
+//                 }
+//             });
+
+//             let mut this = Self {
+//                 fs,
+//                 client,
+//                 user_store,
+//                 local_timezone: cx.platform().local_timezone(),
+//                 channel_store: ChannelStore::global(cx),
+//                 notification_store: NotificationStore::global(cx),
+//                 notification_list,
+//                 pending_serialization: Task::ready(None),
+//                 workspace: workspace_handle,
+//                 has_focus: false,
+//                 current_notification_toast: None,
+//                 subscriptions: Vec::new(),
+//                 active: false,
+//                 mark_as_read_tasks: HashMap::default(),
+//                 width: None,
+//             };
+
+//             let mut old_dock_position = this.position(cx);
+//             this.subscriptions.extend([
+//                 cx.observe(&this.notification_store, |_, _, cx| cx.notify()),
+//                 cx.subscribe(&this.notification_store, Self::on_notification_event),
+//                 cx.observe_global::<SettingsStore, _>(move |this: &mut Self, cx| {
+//                     let new_dock_position = this.position(cx);
+//                     if new_dock_position != old_dock_position {
+//                         old_dock_position = new_dock_position;
+//                         cx.emit(Event::DockPositionChanged);
+//                     }
+//                     cx.notify();
+//                 }),
+//             ]);
+//             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(NOTIFICATION_PANEL_KEY) })
+//                 .await
+//                 .log_err()
+//                 .flatten()
+//             {
+//                 Some(serde_json::from_str::<SerializedNotificationPanel>(&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
+//             })
+//         })
+//     }
+
+//     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(
+//                         NOTIFICATION_PANEL_KEY.into(),
+//                         serde_json::to_string(&SerializedNotificationPanel { width })?,
+//                     )
+//                     .await?;
+//                 anyhow::Ok(())
+//             }
+//             .log_err(),
+//         );
+//     }
+
+//     fn render_notification(
+//         &mut self,
+//         ix: usize,
+//         cx: &mut ViewContext<Self>,
+//     ) -> Option<AnyElement<Self>> {
+//         let entry = self.notification_store.read(cx).notification_at(ix)?;
+//         let notification_id = entry.id;
+//         let now = OffsetDateTime::now_utc();
+//         let timestamp = entry.timestamp;
+//         let NotificationPresenter {
+//             actor,
+//             text,
+//             needs_response,
+//             can_navigate,
+//             ..
+//         } = self.present_notification(entry, cx)?;
+
+//         let theme = theme::current(cx);
+//         let style = &theme.notification_panel;
+//         let response = entry.response;
+//         let notification = entry.notification.clone();
+
+//         let message_style = if entry.is_read {
+//             style.read_text.clone()
+//         } else {
+//             style.unread_text.clone()
+//         };
+
+//         if self.active && !entry.is_read {
+//             self.did_render_notification(notification_id, &notification, cx);
+//         }
+
+//         enum Decline {}
+//         enum Accept {}
+
+//         Some(
+//             MouseEventHandler::new::<NotificationEntry, _>(ix, cx, |_, cx| {
+//                 let container = message_style.container;
+
+//                 Flex::row()
+//                     .with_children(actor.map(|actor| {
+//                         render_avatar(actor.avatar.clone(), &style.avatar, style.avatar_container)
+//                     }))
+//                     .with_child(
+//                         Flex::column()
+//                             .with_child(Text::new(text, message_style.text.clone()))
+//                             .with_child(
+//                                 Flex::row()
+//                                     .with_child(
+//                                         Label::new(
+//                                             format_timestamp(timestamp, now, self.local_timezone),
+//                                             style.timestamp.text.clone(),
+//                                         )
+//                                         .contained()
+//                                         .with_style(style.timestamp.container),
+//                                     )
+//                                     .with_children(if let Some(is_accepted) = response {
+//                                         Some(
+//                                             Label::new(
+//                                                 if is_accepted {
+//                                                     "You accepted"
+//                                                 } else {
+//                                                     "You declined"
+//                                                 },
+//                                                 style.read_text.text.clone(),
+//                                             )
+//                                             .flex_float()
+//                                             .into_any(),
+//                                         )
+//                                     } else if needs_response {
+//                                         Some(
+//                                             Flex::row()
+//                                                 .with_children([
+//                                                     MouseEventHandler::new::<Decline, _>(
+//                                                         ix,
+//                                                         cx,
+//                                                         |state, _| {
+//                                                             let button =
+//                                                                 style.button.style_for(state);
+//                                                             Label::new(
+//                                                                 "Decline",
+//                                                                 button.text.clone(),
+//                                                             )
+//                                                             .contained()
+//                                                             .with_style(button.container)
+//                                                         },
+//                                                     )
+//                                                     .with_cursor_style(CursorStyle::PointingHand)
+//                                                     .on_click(MouseButton::Left, {
+//                                                         let notification = notification.clone();
+//                                                         move |_, view, cx| {
+//                                                             view.respond_to_notification(
+//                                                                 notification.clone(),
+//                                                                 false,
+//                                                                 cx,
+//                                                             );
+//                                                         }
+//                                                     }),
+//                                                     MouseEventHandler::new::<Accept, _>(
+//                                                         ix,
+//                                                         cx,
+//                                                         |state, _| {
+//                                                             let button =
+//                                                                 style.button.style_for(state);
+//                                                             Label::new(
+//                                                                 "Accept",
+//                                                                 button.text.clone(),
+//                                                             )
+//                                                             .contained()
+//                                                             .with_style(button.container)
+//                                                         },
+//                                                     )
+//                                                     .with_cursor_style(CursorStyle::PointingHand)
+//                                                     .on_click(MouseButton::Left, {
+//                                                         let notification = notification.clone();
+//                                                         move |_, view, cx| {
+//                                                             view.respond_to_notification(
+//                                                                 notification.clone(),
+//                                                                 true,
+//                                                                 cx,
+//                                                             );
+//                                                         }
+//                                                     }),
+//                                                 ])
+//                                                 .flex_float()
+//                                                 .into_any(),
+//                                         )
+//                                     } else {
+//                                         None
+//                                     }),
+//                             )
+//                             .flex(1.0, true),
+//                     )
+//                     .contained()
+//                     .with_style(container)
+//                     .into_any()
+//             })
+//             .with_cursor_style(if can_navigate {
+//                 CursorStyle::PointingHand
+//             } else {
+//                 CursorStyle::default()
+//             })
+//             .on_click(MouseButton::Left, {
+//                 let notification = notification.clone();
+//                 move |_, this, cx| this.did_click_notification(&notification, cx)
+//             })
+//             .into_any(),
+//         )
+//     }
+
+//     fn present_notification(
+//         &self,
+//         entry: &NotificationEntry,
+//         cx: &AppContext,
+//     ) -> Option<NotificationPresenter> {
+//         let user_store = self.user_store.read(cx);
+//         let channel_store = self.channel_store.read(cx);
+//         match entry.notification {
+//             Notification::ContactRequest { sender_id } => {
+//                 let requester = user_store.get_cached_user(sender_id)?;
+//                 Some(NotificationPresenter {
+//                     icon: "icons/plus.svg",
+//                     text: format!("{} wants to add you as a contact", requester.github_login),
+//                     needs_response: user_store.has_incoming_contact_request(requester.id),
+//                     actor: Some(requester),
+//                     can_navigate: false,
+//                 })
+//             }
+//             Notification::ContactRequestAccepted { responder_id } => {
+//                 let responder = user_store.get_cached_user(responder_id)?;
+//                 Some(NotificationPresenter {
+//                     icon: "icons/plus.svg",
+//                     text: format!("{} accepted your contact invite", responder.github_login),
+//                     needs_response: false,
+//                     actor: Some(responder),
+//                     can_navigate: false,
+//                 })
+//             }
+//             Notification::ChannelInvitation {
+//                 ref channel_name,
+//                 channel_id,
+//                 inviter_id,
+//             } => {
+//                 let inviter = user_store.get_cached_user(inviter_id)?;
+//                 Some(NotificationPresenter {
+//                     icon: "icons/hash.svg",
+//                     text: format!(
+//                         "{} invited you to join the #{channel_name} channel",
+//                         inviter.github_login
+//                     ),
+//                     needs_response: channel_store.has_channel_invitation(channel_id),
+//                     actor: Some(inviter),
+//                     can_navigate: false,
+//                 })
+//             }
+//             Notification::ChannelMessageMention {
+//                 sender_id,
+//                 channel_id,
+//                 message_id,
+//             } => {
+//                 let sender = user_store.get_cached_user(sender_id)?;
+//                 let channel = channel_store.channel_for_id(channel_id)?;
+//                 let message = self
+//                     .notification_store
+//                     .read(cx)
+//                     .channel_message_for_id(message_id)?;
+//                 Some(NotificationPresenter {
+//                     icon: "icons/conversations.svg",
+//                     text: format!(
+//                         "{} mentioned you in #{}:\n{}",
+//                         sender.github_login, channel.name, message.body,
+//                     ),
+//                     needs_response: false,
+//                     actor: Some(sender),
+//                     can_navigate: true,
+//                 })
+//             }
+//         }
+//     }
+
+//     fn did_render_notification(
+//         &mut self,
+//         notification_id: u64,
+//         notification: &Notification,
+//         cx: &mut ViewContext<Self>,
+//     ) {
+//         let should_mark_as_read = match notification {
+//             Notification::ContactRequestAccepted { .. } => true,
+//             Notification::ContactRequest { .. }
+//             | Notification::ChannelInvitation { .. }
+//             | Notification::ChannelMessageMention { .. } => false,
+//         };
+
+//         if should_mark_as_read {
+//             self.mark_as_read_tasks
+//                 .entry(notification_id)
+//                 .or_insert_with(|| {
+//                     let client = self.client.clone();
+//                     cx.spawn(|this, mut cx| async move {
+//                         cx.background().timer(MARK_AS_READ_DELAY).await;
+//                         client
+//                             .request(proto::MarkNotificationRead { notification_id })
+//                             .await?;
+//                         this.update(&mut cx, |this, _| {
+//                             this.mark_as_read_tasks.remove(&notification_id);
+//                         })?;
+//                         Ok(())
+//                     })
+//                 });
+//         }
+//     }
+
+//     fn did_click_notification(&mut self, notification: &Notification, cx: &mut ViewContext<Self>) {
+//         if let Notification::ChannelMessageMention {
+//             message_id,
+//             channel_id,
+//             ..
+//         } = notification.clone()
+//         {
+//             if let Some(workspace) = self.workspace.upgrade(cx) {
+//                 cx.app_context().defer(move |cx| {
+//                     workspace.update(cx, |workspace, cx| {
+//                         if let Some(panel) = workspace.focus_panel::<ChatPanel>(cx) {
+//                             panel.update(cx, |panel, cx| {
+//                                 panel
+//                                     .select_channel(channel_id, Some(message_id), cx)
+//                                     .detach_and_log_err(cx);
+//                             });
+//                         }
+//                     });
+//                 });
+//             }
+//         }
+//     }
+
+//     fn is_showing_notification(&self, notification: &Notification, cx: &AppContext) -> bool {
+//         if let Notification::ChannelMessageMention { channel_id, .. } = &notification {
+//             if let Some(workspace) = self.workspace.upgrade(cx) {
+//                 return workspace
+//                     .read_with(cx, |workspace, cx| {
+//                         if let Some(panel) = workspace.panel::<ChatPanel>(cx) {
+//                             return panel.read_with(cx, |panel, cx| {
+//                                 panel.is_scrolled_to_bottom()
+//                                     && panel.active_chat().map_or(false, |chat| {
+//                                         chat.read(cx).channel_id == *channel_id
+//                                     })
+//                             });
+//                         }
+//                         false
+//                     })
+//                     .unwrap_or_default();
+//             }
+//         }
+
+//         false
+//     }
+
+//     fn render_sign_in_prompt(
+//         &self,
+//         theme: &Arc<Theme>,
+//         cx: &mut ViewContext<Self>,
+//     ) -> AnyElement<Self> {
+//         enum SignInPromptLabel {}
+
+//         MouseEventHandler::new::<SignInPromptLabel, _>(0, cx, |mouse_state, _| {
+//             Label::new(
+//                 "Sign in to view your notifications".to_string(),
+//                 theme
+//                     .chat_panel
+//                     .sign_in_prompt
+//                     .style_for(mouse_state)
+//                     .clone(),
+//             )
+//         })
+//         .with_cursor_style(CursorStyle::PointingHand)
+//         .on_click(MouseButton::Left, move |_, this, cx| {
+//             let client = this.client.clone();
+//             cx.spawn(|_, cx| async move {
+//                 client.authenticate_and_connect(true, &cx).log_err().await;
+//             })
+//             .detach();
+//         })
+//         .aligned()
+//         .into_any()
+//     }
+
+//     fn render_empty_state(
+//         &self,
+//         theme: &Arc<Theme>,
+//         _cx: &mut ViewContext<Self>,
+//     ) -> AnyElement<Self> {
+//         Label::new(
+//             "You have no notifications".to_string(),
+//             theme.chat_panel.sign_in_prompt.default.clone(),
+//         )
+//         .aligned()
+//         .into_any()
+//     }
+
+//     fn on_notification_event(
+//         &mut self,
+//         _: ModelHandle<NotificationStore>,
+//         event: &NotificationEvent,
+//         cx: &mut ViewContext<Self>,
+//     ) {
+//         match event {
+//             NotificationEvent::NewNotification { entry } => self.add_toast(entry, cx),
+//             NotificationEvent::NotificationRemoved { entry }
+//             | NotificationEvent::NotificationRead { entry } => self.remove_toast(entry.id, cx),
+//             NotificationEvent::NotificationsUpdated {
+//                 old_range,
+//                 new_count,
+//             } => {
+//                 self.notification_list.splice(old_range.clone(), *new_count);
+//                 cx.notify();
+//             }
+//         }
+//     }
+
+//     fn add_toast(&mut self, entry: &NotificationEntry, cx: &mut ViewContext<Self>) {
+//         if self.is_showing_notification(&entry.notification, cx) {
+//             return;
+//         }
+
+//         let Some(NotificationPresenter { actor, text, .. }) = self.present_notification(entry, cx)
+//         else {
+//             return;
+//         };
+
+//         let notification_id = entry.id;
+//         self.current_notification_toast = Some((
+//             notification_id,
+//             cx.spawn(|this, mut cx| async move {
+//                 cx.background().timer(TOAST_DURATION).await;
+//                 this.update(&mut cx, |this, cx| this.remove_toast(notification_id, cx))
+//                     .ok();
+//             }),
+//         ));
+
+//         self.workspace
+//             .update(cx, |workspace, cx| {
+//                 workspace.dismiss_notification::<NotificationToast>(0, cx);
+//                 workspace.show_notification(0, cx, |cx| {
+//                     let workspace = cx.weak_handle();
+//                     cx.add_view(|_| NotificationToast {
+//                         notification_id,
+//                         actor,
+//                         text,
+//                         workspace,
+//                     })
+//                 })
+//             })
+//             .ok();
+//     }
+
+//     fn remove_toast(&mut self, notification_id: u64, cx: &mut ViewContext<Self>) {
+//         if let Some((current_id, _)) = &self.current_notification_toast {
+//             if *current_id == notification_id {
+//                 self.current_notification_toast.take();
+//                 self.workspace
+//                     .update(cx, |workspace, cx| {
+//                         workspace.dismiss_notification::<NotificationToast>(0, cx)
+//                     })
+//                     .ok();
+//             }
+//         }
+//     }
+
+//     fn respond_to_notification(
+//         &mut self,
+//         notification: Notification,
+//         response: bool,
+//         cx: &mut ViewContext<Self>,
+//     ) {
+//         self.notification_store.update(cx, |store, cx| {
+//             store.respond_to_notification(notification, response, cx);
+//         });
+//     }
+// }
+
+// impl Entity for NotificationPanel {
+//     type Event = Event;
+// }
+
+// impl View for NotificationPanel {
+//     fn ui_name() -> &'static str {
+//         "NotificationPanel"
+//     }
+
+//     fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
+//         let theme = theme::current(cx);
+//         let style = &theme.notification_panel;
+//         let element = if self.client.user_id().is_none() {
+//             self.render_sign_in_prompt(&theme, cx)
+//         } else if self.notification_list.item_count() == 0 {
+//             self.render_empty_state(&theme, cx)
+//         } else {
+//             Flex::column()
+//                 .with_child(
+//                     Flex::row()
+//                         .with_child(Label::new("Notifications", style.title.text.clone()))
+//                         .with_child(ui::svg(&style.title_icon).flex_float())
+//                         .align_children_center()
+//                         .contained()
+//                         .with_style(style.title.container)
+//                         .constrained()
+//                         .with_height(style.title_height),
+//                 )
+//                 .with_child(
+//                     List::new(self.notification_list.clone())
+//                         .contained()
+//                         .with_style(style.list)
+//                         .flex(1., true),
+//                 )
+//                 .into_any()
+//         };
+//         element
+//             .contained()
+//             .with_style(style.container)
+//             .constrained()
+//             .with_min_width(150.)
+//             .into_any()
+//     }
+
+//     fn focus_in(&mut self, _: AnyViewHandle, _: &mut ViewContext<Self>) {
+//         self.has_focus = true;
+//     }
+
+//     fn focus_out(&mut self, _: AnyViewHandle, _: &mut ViewContext<Self>) {
+//         self.has_focus = false;
+//     }
+// }
+
+// impl Panel for NotificationPanel {
+//     fn position(&self, cx: &gpui::WindowContext) -> DockPosition {
+//         settings::get::<NotificationPanelSettings>(cx).dock
+//     }
+
+//     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::<NotificationPanelSettings>(
+//             self.fs.clone(),
+//             cx,
+//             move |settings| settings.dock = Some(position),
+//         );
+//     }
+
+//     fn size(&self, cx: &gpui::WindowContext) -> f32 {
+//         self.width
+//             .unwrap_or_else(|| settings::get::<NotificationPanelSettings>(cx).default_width)
+//     }
+
+//     fn set_size(&mut self, size: Option<f32>, cx: &mut ViewContext<Self>) {
+//         self.width = size;
+//         self.serialize(cx);
+//         cx.notify();
+//     }
+
+//     fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) {
+//         self.active = active;
+//         if self.notification_store.read(cx).notification_count() == 0 {
+//             cx.emit(Event::Dismissed);
+//         }
+//     }
+
+//     fn icon_path(&self, cx: &gpui::WindowContext) -> Option<&'static str> {
+//         (settings::get::<NotificationPanelSettings>(cx).button
+//             && self.notification_store.read(cx).notification_count() > 0)
+//             .then(|| "icons/bell.svg")
+//     }
+
+//     fn icon_tooltip(&self) -> (String, Option<Box<dyn gpui::Action>>) {
+//         (
+//             "Notification Panel".to_string(),
+//             Some(Box::new(ToggleFocus)),
+//         )
+//     }
+
+//     fn icon_label(&self, cx: &WindowContext) -> Option<String> {
+//         let count = self.notification_store.read(cx).unread_notification_count();
+//         if count == 0 {
+//             None
+//         } else {
+//             Some(count.to_string())
+//         }
+//     }
+
+//     fn should_change_position_on_event(event: &Self::Event) -> bool {
+//         matches!(event, Event::DockPositionChanged)
+//     }
+
+//     fn should_close_on_event(event: &Self::Event) -> bool {
+//         matches!(event, Event::Dismissed)
+//     }
+
+//     fn has_focus(&self, _cx: &gpui::WindowContext) -> bool {
+//         self.has_focus
+//     }
+
+//     fn is_focus_event(event: &Self::Event) -> bool {
+//         matches!(event, Event::Focus)
+//     }
+// }
+
+// pub struct NotificationToast {
+//     notification_id: u64,
+//     actor: Option<Arc<User>>,
+//     text: String,
+//     workspace: WeakViewHandle<Workspace>,
+// }
+
+// pub enum ToastEvent {
+//     Dismiss,
+// }
+
+// impl NotificationToast {
+//     fn focus_notification_panel(&self, cx: &mut AppContext) {
+//         let workspace = self.workspace.clone();
+//         let notification_id = self.notification_id;
+//         cx.defer(move |cx| {
+//             workspace
+//                 .update(cx, |workspace, cx| {
+//                     if let Some(panel) = workspace.focus_panel::<NotificationPanel>(cx) {
+//                         panel.update(cx, |panel, cx| {
+//                             let store = panel.notification_store.read(cx);
+//                             if let Some(entry) = store.notification_for_id(notification_id) {
+//                                 panel.did_click_notification(&entry.clone().notification, cx);
+//                             }
+//                         });
+//                     }
+//                 })
+//                 .ok();
+//         })
+//     }
+// }
+
+// impl Entity for NotificationToast {
+//     type Event = ToastEvent;
+// }
+
+// impl View for NotificationToast {
+//     fn ui_name() -> &'static str {
+//         "ContactNotification"
+//     }
+
+//     fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
+//         let user = self.actor.clone();
+//         let theme = theme::current(cx).clone();
+//         let theme = &theme.contact_notification;
+
+//         MouseEventHandler::new::<Self, _>(0, cx, |_, cx| {
+//             Flex::row()
+//                 .with_children(user.and_then(|user| {
+//                     Some(
+//                         Image::from_data(user.avatar.clone()?)
+//                             .with_style(theme.header_avatar)
+//                             .aligned()
+//                             .constrained()
+//                             .with_height(
+//                                 cx.font_cache()
+//                                     .line_height(theme.header_message.text.font_size),
+//                             )
+//                             .aligned()
+//                             .top(),
+//                     )
+//                 }))
+//                 .with_child(
+//                     Text::new(self.text.clone(), theme.header_message.text.clone())
+//                         .contained()
+//                         .with_style(theme.header_message.container)
+//                         .aligned()
+//                         .top()
+//                         .left()
+//                         .flex(1., true),
+//                 )
+//                 .with_child(
+//                     MouseEventHandler::new::<ToastEvent, _>(0, cx, |state, _| {
+//                         let style = theme.dismiss_button.style_for(state);
+//                         Svg::new("icons/x.svg")
+//                             .with_color(style.color)
+//                             .constrained()
+//                             .with_width(style.icon_width)
+//                             .aligned()
+//                             .contained()
+//                             .with_style(style.container)
+//                             .constrained()
+//                             .with_width(style.button_width)
+//                             .with_height(style.button_width)
+//                     })
+//                     .with_cursor_style(CursorStyle::PointingHand)
+//                     .with_padding(Padding::uniform(5.))
+//                     .on_click(MouseButton::Left, move |_, _, cx| {
+//                         cx.emit(ToastEvent::Dismiss)
+//                     })
+//                     .aligned()
+//                     .constrained()
+//                     .with_height(
+//                         cx.font_cache()
+//                             .line_height(theme.header_message.text.font_size),
+//                     )
+//                     .aligned()
+//                     .top()
+//                     .flex_float(),
+//                 )
+//                 .contained()
+//         })
+//         .with_cursor_style(CursorStyle::PointingHand)
+//         .on_click(MouseButton::Left, move |_, this, cx| {
+//             this.focus_notification_panel(cx);
+//             cx.emit(ToastEvent::Dismiss);
+//         })
+//         .into_any()
+//     }
+// }
+
+// impl workspace::notifications::Notification for NotificationToast {
+//     fn should_dismiss_notification_on_event(&self, event: &<Self as Entity>::Event) -> bool {
+//         matches!(event, ToastEvent::Dismiss)
+//     }
+// }
+
+// fn format_timestamp(
+//     mut timestamp: OffsetDateTime,
+//     mut now: OffsetDateTime,
+//     local_timezone: UtcOffset,
+// ) -> String {
+//     timestamp = timestamp.to_offset(local_timezone);
+//     now = now.to_offset(local_timezone);
+
+//     let today = now.date();
+//     let date = timestamp.date();
+//     if date == today {
+//         let difference = now - timestamp;
+//         if difference >= Duration::from_secs(3600) {
+//             format!("{}h", difference.whole_seconds() / 3600)
+//         } else if difference >= Duration::from_secs(60) {
+//             format!("{}m", difference.whole_seconds() / 60)
+//         } else {
+//             "just now".to_string()
+//         }
+//     } else if date.next_day() == Some(today) {
+//         format!("yesterday")
+//     } else {
+//         format!("{:02}/{}/{}", date.month() as u32, date.day(), date.year())
+//     }
+// }

crates/collab_ui2/src/notifications.rs 🔗

@@ -1,11 +1,11 @@
-use gpui::AppContext;
-use std::sync::Arc;
-use workspace::AppState;
+// use gpui::AppContext;
+// use std::sync::Arc;
+// use workspace::AppState;
 
-pub mod incoming_call_notification;
-pub mod project_shared_notification;
+// pub mod incoming_call_notification;
+// pub mod project_shared_notification;
 
-pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
-    incoming_call_notification::init(app_state, cx);
-    project_shared_notification::init(app_state, cx);
-}
+// pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
+//     incoming_call_notification::init(app_state, cx);
+//     project_shared_notification::init(app_state, cx);
+// }

crates/collab_ui2/src/panel_settings.rs 🔗

@@ -1,7 +1,7 @@
 use anyhow;
 use schemars::JsonSchema;
 use serde_derive::{Deserialize, Serialize};
-use settings::Setting;
+use settings::Settings;
 use workspace::dock::DockPosition;
 
 #[derive(Deserialize, Debug)]
@@ -32,37 +32,37 @@ pub struct PanelSettingsContent {
     pub default_width: Option<f32>,
 }
 
-impl Setting for CollaborationPanelSettings {
+impl Settings for CollaborationPanelSettings {
     const KEY: Option<&'static str> = Some("collaboration_panel");
     type FileContent = PanelSettingsContent;
     fn load(
         default_value: &Self::FileContent,
         user_values: &[&Self::FileContent],
-        _: &gpui::AppContext,
+        _: &mut gpui::AppContext,
     ) -> anyhow::Result<Self> {
         Self::load_via_json_merge(default_value, user_values)
     }
 }
 
-impl Setting for ChatPanelSettings {
+impl Settings for ChatPanelSettings {
     const KEY: Option<&'static str> = Some("chat_panel");
     type FileContent = PanelSettingsContent;
     fn load(
         default_value: &Self::FileContent,
         user_values: &[&Self::FileContent],
-        _: &gpui::AppContext,
+        _: &mut gpui::AppContext,
     ) -> anyhow::Result<Self> {
         Self::load_via_json_merge(default_value, user_values)
     }
 }
 
-impl Setting for NotificationPanelSettings {
+impl Settings for NotificationPanelSettings {
     const KEY: Option<&'static str> = Some("notification_panel");
     type FileContent = PanelSettingsContent;
     fn load(
         default_value: &Self::FileContent,
         user_values: &[&Self::FileContent],
-        _: &gpui::AppContext,
+        _: &mut gpui::AppContext,
     ) -> anyhow::Result<Self> {
         Self::load_via_json_merge(default_value, user_values)
     }

crates/workspace2/src/workspace2.rs 🔗

@@ -36,11 +36,10 @@ use futures::{
     Future, FutureExt, StreamExt,
 };
 use gpui::{
-    actions, div, point, prelude::*, rems, size, Action, AnyModel, AnyView, AnyWeakView,
-    AppContext, AsyncAppContext, AsyncWindowContext, Bounds, Component, Div, Entity, EntityId,
-    EventEmitter, GlobalPixels, KeyContext, Model, ModelContext, ParentComponent, Point, Render,
-    Size, Styled, Subscription, Task, View, ViewContext, WeakView, WindowBounds, WindowContext,
-    WindowHandle, WindowOptions,
+    actions, div, point, prelude::*, size, Action, AnyModel, AnyView, AnyWeakView, AppContext,
+    AsyncAppContext, AsyncWindowContext, Bounds, Div, Entity, EntityId, EventEmitter, GlobalPixels,
+    KeyContext, Model, ModelContext, ParentComponent, Point, Render, Size, Styled, Subscription,
+    Task, View, ViewContext, WeakView, WindowBounds, WindowContext, WindowHandle, WindowOptions,
 };
 use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, ProjectItem};
 use itertools::Itertools;
@@ -68,8 +67,6 @@ use std::{
 };
 use theme2::{ActiveTheme, ThemeSettings};
 pub use toolbar::{ToolbarItemLocation, ToolbarItemView};
-use ui::TextColor;
-use ui::{h_stack, Button, ButtonVariant, KeyBinding, Label, TextTooltip};
 use util::ResultExt;
 use uuid::Uuid;
 pub use workspace_settings::{AutosaveSetting, WorkspaceSettings};
@@ -447,7 +444,7 @@ pub struct Workspace {
     last_active_view_id: Option<proto::ViewId>,
     status_bar: View<StatusBar>,
     modal_layer: View<ModalLayer>,
-    //     titlebar_item: Option<AnyViewHandle>,
+    titlebar_item: Option<AnyView>,
     notifications: Vec<(TypeId, usize, Box<dyn NotificationHandle>)>,
     project: Model<Project>,
     follower_states: HashMap<View<Pane>, FollowerState>,
@@ -660,7 +657,7 @@ impl Workspace {
             last_active_view_id: None,
             status_bar,
             modal_layer,
-            // titlebar_item: None,
+            titlebar_item: None,
             notifications: Default::default(),
             left_dock,
             bottom_dock,
@@ -1033,15 +1030,14 @@ impl Workspace {
         &self.app_state.client
     }
 
-    // todo!()
-    // pub fn set_titlebar_item(&mut self, item: AnyViewHandle, cx: &mut ViewContext<Self>) {
-    //     self.titlebar_item = Some(item);
-    //     cx.notify();
-    // }
+    pub fn set_titlebar_item(&mut self, item: AnyView, cx: &mut ViewContext<Self>) {
+        self.titlebar_item = Some(item);
+        cx.notify();
+    }
 
-    // pub fn titlebar_item(&self) -> Option<AnyViewHandle> {
-    //     self.titlebar_item.clone()
-    // }
+    pub fn titlebar_item(&self) -> Option<AnyView> {
+        self.titlebar_item.clone()
+    }
 
     /// Call the given callback with a workspace whose project is local.
     ///
@@ -2448,75 +2444,6 @@ impl Workspace {
     //             .any(|state| state.leader_id == peer_id)
     //     }
 
-    fn render_titlebar(&self, cx: &mut ViewContext<Self>) -> impl Component<Self> {
-        h_stack()
-            .id("titlebar")
-            .justify_between()
-            .when(
-                !matches!(cx.window_bounds(), WindowBounds::Fullscreen),
-                |s| s.pl_20(),
-            )
-            .w_full()
-            .h(rems(1.75))
-            .bg(cx.theme().colors().title_bar_background)
-            .on_click(|_, event, cx| {
-                if event.up.click_count == 2 {
-                    cx.zoom_window();
-                }
-            })
-            .child(
-                h_stack()
-                    // TODO - Add player menu
-                    .child(
-                        div()
-                            .id("project_owner_indicator")
-                            .child(
-                                Button::new("player")
-                                    .variant(ButtonVariant::Ghost)
-                                    .color(Some(TextColor::Player(0))),
-                            )
-                            .tooltip(move |_, cx| {
-                                cx.build_view(|cx| TextTooltip::new("Toggle following"))
-                            }),
-                    )
-                    // TODO - Add project menu
-                    .child(
-                        div()
-                            .id("titlebar_project_menu_button")
-                            .child(Button::new("project_name").variant(ButtonVariant::Ghost))
-                            .tooltip(move |_, cx| {
-                                cx.build_view(|cx| TextTooltip::new("Recent Projects"))
-                            }),
-                    )
-                    // TODO - Add git menu
-                    .child(
-                        div()
-                            .id("titlebar_git_menu_button")
-                            .child(
-                                Button::new("branch_name")
-                                    .variant(ButtonVariant::Ghost)
-                                    .color(Some(TextColor::Muted)),
-                            )
-                            .tooltip(move |_, cx| {
-                                // todo!() Replace with real action.
-                                #[gpui::action]
-                                struct NoAction {}
-
-                                cx.build_view(|cx| {
-                                    TextTooltip::new("Recent Branches")
-                                        .key_binding(KeyBinding::new(gpui::KeyBinding::new(
-                                            "cmd-b",
-                                            NoAction {},
-                                            None,
-                                        )))
-                                        .meta("Only local branches shown")
-                                })
-                            }),
-                    ),
-            ) // self.titlebar_item
-            .child(h_stack().child(Label::new("Right side titlebar item")))
-    }
-
     fn active_item_path_changed(&mut self, cx: &mut ViewContext<Self>) {
         let active_entry = self.active_project_path(cx);
         self.project
@@ -3719,7 +3646,7 @@ impl Render for Workspace {
             .items_start()
             .text_color(cx.theme().colors().text)
             .bg(cx.theme().colors().background)
-            .child(self.render_titlebar(cx))
+            .children(self.titlebar_item.clone())
             .child(
                 // todo! should this be a component a view?
                 div()

crates/zed2/src/main.rs 🔗

@@ -207,7 +207,7 @@ fn main() {
         // activity_indicator::init(cx);
         // language_tools::init(cx);
         call::init(app_state.client.clone(), app_state.user_store.clone(), cx);
-        // collab_ui::init(&app_state, cx);
+        collab_ui::init(&app_state, cx);
         // feedback::init(cx);
         // welcome::init(cx);
         // zed::init(&app_state, cx);

crates/zed2/src/zed2.rs 🔗

@@ -341,10 +341,6 @@ pub fn initialize_workspace(
             //         workspace.active_pane().clone(),
             //     ));
 
-            //     let collab_titlebar_item =
-            //         cx.add_view(|cx| CollabTitlebarItem::new(workspace, &workspace_handle, cx));
-            //     workspace.set_titlebar_item(collab_titlebar_item.into_any(), cx);
-
             //     let copilot =
             //         cx.add_view(|cx| copilot_button::CopilotButton::new(app_state.fs.clone(), cx));
             //     let diagnostic_summary =