Enable more collab UI features (#3496)

Max Brunsfeld created

* Current Call section of the collab panel
* Improve the collab titlebar
* Add basic UI for following

Following only partially works, but the UI for following is now in
place.

Change summary

Cargo.lock                                    |    4 
crates/call2/Cargo.toml                       |    4 
crates/call2/src/call2.rs                     |  257 -----
crates/call2/src/participant.rs               |    2 
crates/collab2/src/tests/channel_tests.rs     |   37 
crates/collab2/src/tests/integration_tests.rs |   11 
crates/collab2/src/tests/test_server.rs       |    1 
crates/collab_ui2/src/collab_panel.rs         | 1030 +++++++++-----------
crates/collab_ui2/src/collab_titlebar_item.rs |  230 +--
crates/collab_ui2/src/collab_ui.rs            |  115 +-
crates/editor2/src/element.rs                 |    4 
crates/gpui2/src/elements/canvas.rs           |   48 
crates/gpui2/src/elements/mod.rs              |    2 
crates/gpui2/src/text_system.rs               |   32 
crates/gpui2/src/text_system/line.rs          |    4 
crates/gpui2/src/window.rs                    |    7 
crates/ui2/src/components/list/list_item.rs   |   11 
crates/workspace2/Cargo.toml                  |    2 
crates/workspace2/src/pane_group.rs           |  105 +
crates/workspace2/src/shared_screen.rs        |    9 
crates/workspace2/src/workspace2.rs           |  939 ++++++++----------
crates/zed2/src/main.rs                       |    1 
22 files changed, 1,280 insertions(+), 1,575 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -1222,7 +1222,6 @@ version = "0.1.0"
 dependencies = [
  "anyhow",
  "async-broadcast",
- "async-trait",
  "audio2",
  "client2",
  "collections",
@@ -1242,9 +1241,7 @@ dependencies = [
  "serde_json",
  "settings2",
  "smallvec",
- "ui2",
  "util",
- "workspace2",
 ]
 
 [[package]]
@@ -11534,7 +11531,6 @@ version = "0.1.0"
 dependencies = [
  "anyhow",
  "async-recursion 1.0.5",
- "async-trait",
  "bincode",
  "call2",
  "client2",

crates/call2/Cargo.toml 🔗

@@ -31,9 +31,7 @@ media = { path = "../media" }
 project = { package = "project2", path = "../project2" }
 settings = { package = "settings2", path = "../settings2" }
 util = { path = "../util" }
-ui = {package = "ui2", path = "../ui2"}
-workspace = {package = "workspace2", path = "../workspace2"}
-async-trait.workspace = true
+
 anyhow.workspace = true
 async-broadcast = "0.4"
 futures.workspace = true

crates/call2/src/call2.rs 🔗

@@ -1,32 +1,25 @@
 pub mod call_settings;
 pub mod participant;
 pub mod room;
-mod shared_screen;
 
 use anyhow::{anyhow, Result};
-use async_trait::async_trait;
 use audio::Audio;
 use call_settings::CallSettings;
-use client::{
-    proto::{self, PeerId},
-    Client, TelemetrySettings, TypedEnvelope, User, UserStore, ZED_ALWAYS_ACTIVE,
-};
+use client::{proto, Client, TelemetrySettings, TypedEnvelope, User, UserStore, ZED_ALWAYS_ACTIVE};
 use collections::HashSet;
 use futures::{channel::oneshot, future::Shared, Future, FutureExt};
 use gpui::{
-    AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, PromptLevel,
-    Subscription, Task, View, ViewContext, VisualContext, WeakModel, WindowHandle,
+    AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, Subscription, Task,
+    WeakModel,
 };
-pub use participant::ParticipantLocation;
 use postage::watch;
 use project::Project;
 use room::Event;
-pub use room::Room;
 use settings::Settings;
-use shared_screen::SharedScreen;
 use std::sync::Arc;
-use util::ResultExt;
-use workspace::{item::ItemHandle, CallHandler, Pane, Workspace};
+
+pub use participant::ParticipantLocation;
+pub use room::Room;
 
 pub fn init(client: Arc<Client>, user_store: Model<UserStore>, cx: &mut AppContext) {
     CallSettings::register(cx);
@@ -334,55 +327,12 @@ impl ActiveCall {
     pub fn join_channel(
         &mut self,
         channel_id: u64,
-        requesting_window: Option<WindowHandle<Workspace>>,
         cx: &mut ModelContext<Self>,
     ) -> Task<Result<Option<Model<Room>>>> {
         if let Some(room) = self.room().cloned() {
             if room.read(cx).channel_id() == Some(channel_id) {
-                return cx.spawn(|_, _| async move {
-                    todo!();
-                    // let future = room.update(&mut cx, |room, cx| {
-                    //     room.most_active_project(cx).map(|(host, project)| {
-                    //         room.join_project(project, host, app_state.clone(), cx)
-                    //     })
-                    // })
-
-                    // if let Some(future) = future {
-                    //     future.await?;
-                    // }
-
-                    // Ok(Some(room))
-                });
-            }
-
-            let should_prompt = room.update(cx, |room, _| {
-                room.channel_id().is_some()
-                    && room.is_sharing_project()
-                    && room.remote_participants().len() > 0
-            });
-            if should_prompt && requesting_window.is_some() {
-                return cx.spawn(|this, mut cx| async move {
-                    let answer = requesting_window.unwrap().update(&mut cx, |_, cx| {
-                        cx.prompt(
-                            PromptLevel::Warning,
-                            "Leaving this call will unshare your current project.\nDo you want to switch channels?",
-                            &["Yes, Join Channel", "Cancel"],
-                        )
-                    })?;
-                    if answer.await? == 1 {
-                        return Ok(None);
-                    }
-
-                    room.update(&mut cx, |room, cx| room.clear_state(cx))?;
-
-                    this.update(&mut cx, |this, cx| {
-                        this.join_channel(channel_id, requesting_window, cx)
-                    })?
-                    .await
-                });
-            }
-
-            if room.read(cx).channel_id().is_some() {
+                return Task::ready(Ok(Some(room)));
+            } else {
                 room.update(cx, |room, cx| room.clear_state(cx));
             }
         }
@@ -555,197 +505,6 @@ pub fn report_call_event_for_channel(
     )
 }
 
-pub struct Call {
-    active_call: Option<(Model<ActiveCall>, Vec<Subscription>)>,
-}
-
-impl Call {
-    pub fn new(cx: &mut ViewContext<'_, Workspace>) -> Box<dyn CallHandler> {
-        let mut active_call = None;
-        if cx.has_global::<Model<ActiveCall>>() {
-            let call = cx.global::<Model<ActiveCall>>().clone();
-            let subscriptions = vec![cx.subscribe(&call, Self::on_active_call_event)];
-            active_call = Some((call, subscriptions));
-        }
-        Box::new(Self { active_call })
-    }
-    fn on_active_call_event(
-        workspace: &mut Workspace,
-        _: Model<ActiveCall>,
-        event: &room::Event,
-        cx: &mut ViewContext<Workspace>,
-    ) {
-        match event {
-            room::Event::ParticipantLocationChanged { participant_id }
-            | room::Event::RemoteVideoTracksChanged { participant_id } => {
-                workspace.leader_updated(*participant_id, cx);
-            }
-            _ => {}
-        }
-    }
-}
-
-#[async_trait(?Send)]
-impl CallHandler for Call {
-    fn peer_state(
-        &mut self,
-        leader_id: PeerId,
-        project: &Model<Project>,
-        cx: &mut ViewContext<Workspace>,
-    ) -> Option<(bool, bool)> {
-        let (call, _) = self.active_call.as_ref()?;
-        let room = call.read(cx).room()?.read(cx);
-        let participant = room.remote_participant_for_peer_id(leader_id)?;
-
-        let leader_in_this_app;
-        let leader_in_this_project;
-        match participant.location {
-            ParticipantLocation::SharedProject { project_id } => {
-                leader_in_this_app = true;
-                leader_in_this_project = Some(project_id) == project.read(cx).remote_id();
-            }
-            ParticipantLocation::UnsharedProject => {
-                leader_in_this_app = true;
-                leader_in_this_project = false;
-            }
-            ParticipantLocation::External => {
-                leader_in_this_app = false;
-                leader_in_this_project = false;
-            }
-        };
-
-        Some((leader_in_this_project, leader_in_this_app))
-    }
-
-    fn shared_screen_for_peer(
-        &self,
-        peer_id: PeerId,
-        pane: &View<Pane>,
-        cx: &mut ViewContext<Workspace>,
-    ) -> Option<Box<dyn ItemHandle>> {
-        let (call, _) = self.active_call.as_ref()?;
-        let room = call.read(cx).room()?.read(cx);
-        let participant = room.remote_participant_for_peer_id(peer_id)?;
-        let track = participant.video_tracks.values().next()?.clone();
-        let user = participant.user.clone();
-        for item in pane.read(cx).items_of_type::<SharedScreen>() {
-            if item.read(cx).peer_id == peer_id {
-                return Some(Box::new(item));
-            }
-        }
-
-        Some(Box::new(cx.build_view(|cx| {
-            SharedScreen::new(&track, peer_id, user.clone(), cx)
-        })))
-    }
-    fn room_id(&self, cx: &AppContext) -> Option<u64> {
-        Some(self.active_call.as_ref()?.0.read(cx).room()?.read(cx).id())
-    }
-    fn hang_up(&self, cx: &mut AppContext) -> Task<Result<()>> {
-        let Some((call, _)) = self.active_call.as_ref() else {
-            return Task::ready(Err(anyhow!("Cannot exit a call; not in a call")));
-        };
-
-        call.update(cx, |this, cx| this.hang_up(cx))
-    }
-    fn active_project(&self, cx: &AppContext) -> Option<WeakModel<Project>> {
-        ActiveCall::global(cx).read(cx).location().cloned()
-    }
-    fn invite(
-        &mut self,
-        called_user_id: u64,
-        initial_project: Option<Model<Project>>,
-        cx: &mut AppContext,
-    ) -> Task<Result<()>> {
-        ActiveCall::global(cx).update(cx, |this, cx| {
-            this.invite(called_user_id, initial_project, cx)
-        })
-    }
-    fn remote_participants(&self, cx: &AppContext) -> Option<Vec<(Arc<User>, PeerId)>> {
-        self.active_call
-            .as_ref()
-            .map(|call| {
-                call.0.read(cx).room().map(|room| {
-                    room.read(cx)
-                        .remote_participants()
-                        .iter()
-                        .map(|participant| {
-                            (participant.1.user.clone(), participant.1.peer_id.clone())
-                        })
-                        .collect()
-                })
-            })
-            .flatten()
-    }
-    fn is_muted(&self, cx: &AppContext) -> Option<bool> {
-        self.active_call
-            .as_ref()
-            .map(|call| {
-                call.0
-                    .read(cx)
-                    .room()
-                    .map(|room| room.read(cx).is_muted(cx))
-            })
-            .flatten()
-    }
-    fn toggle_mute(&self, cx: &mut AppContext) {
-        self.active_call.as_ref().map(|call| {
-            call.0.update(cx, |this, cx| {
-                this.room().map(|room| {
-                    let room = room.clone();
-                    cx.spawn(|_, mut cx| async move {
-                        room.update(&mut cx, |this, cx| this.toggle_mute(cx))??
-                            .await
-                    })
-                    .detach_and_log_err(cx);
-                })
-            })
-        });
-    }
-    fn toggle_screen_share(&self, cx: &mut AppContext) {
-        self.active_call.as_ref().map(|call| {
-            call.0.update(cx, |this, cx| {
-                this.room().map(|room| {
-                    room.update(cx, |this, cx| {
-                        if this.is_screen_sharing() {
-                            this.unshare_screen(cx).log_err();
-                        } else {
-                            let t = this.share_screen(cx);
-                            cx.spawn(move |_, _| async move {
-                                t.await.log_err();
-                            })
-                            .detach();
-                        }
-                    })
-                })
-            })
-        });
-    }
-    fn toggle_deafen(&self, cx: &mut AppContext) {
-        self.active_call.as_ref().map(|call| {
-            call.0.update(cx, |this, cx| {
-                this.room().map(|room| {
-                    room.update(cx, |this, cx| {
-                        this.toggle_deafen(cx).log_err();
-                    })
-                })
-            })
-        });
-    }
-    fn is_deafened(&self, cx: &AppContext) -> Option<bool> {
-        self.active_call
-            .as_ref()
-            .map(|call| {
-                call.0
-                    .read(cx)
-                    .room()
-                    .map(|room| room.read(cx).is_deafened())
-            })
-            .flatten()
-            .flatten()
-    }
-}
-
 #[cfg(test)]
 mod test {
     use gpui::TestAppContext;

crates/call2/src/participant.rs 🔗

@@ -4,7 +4,7 @@ use client::{proto, User};
 use collections::HashMap;
 use gpui::WeakModel;
 pub use live_kit_client::Frame;
-pub(crate) use live_kit_client::{RemoteAudioTrack, RemoteVideoTrack};
+pub use live_kit_client::{RemoteAudioTrack, RemoteVideoTrack};
 use project::Project;
 use std::sync::Arc;
 

crates/collab2/src/tests/channel_tests.rs 🔗

@@ -364,8 +364,7 @@ async fn test_joining_channel_ancestor_member(
     let active_call_b = cx_b.read(ActiveCall::global);
 
     assert!(active_call_b
-        .update(cx_b, |active_call, cx| active_call
-            .join_channel(sub_id, None, cx))
+        .update(cx_b, |active_call, cx| active_call.join_channel(sub_id, cx))
         .await
         .is_ok());
 }
@@ -395,9 +394,7 @@ async fn test_channel_room(
     let active_call_b = cx_b.read(ActiveCall::global);
 
     active_call_a
-        .update(cx_a, |active_call, cx| {
-            active_call.join_channel(zed_id, None, cx)
-        })
+        .update(cx_a, |active_call, cx| active_call.join_channel(zed_id, cx))
         .await
         .unwrap();
 
@@ -445,9 +442,7 @@ async fn test_channel_room(
     });
 
     active_call_b
-        .update(cx_b, |active_call, cx| {
-            active_call.join_channel(zed_id, None, cx)
-        })
+        .update(cx_b, |active_call, cx| active_call.join_channel(zed_id, cx))
         .await
         .unwrap();
 
@@ -564,16 +559,12 @@ async fn test_channel_room(
     });
 
     active_call_a
-        .update(cx_a, |active_call, cx| {
-            active_call.join_channel(zed_id, None, cx)
-        })
+        .update(cx_a, |active_call, cx| active_call.join_channel(zed_id, cx))
         .await
         .unwrap();
 
     active_call_b
-        .update(cx_b, |active_call, cx| {
-            active_call.join_channel(zed_id, None, cx)
-        })
+        .update(cx_b, |active_call, cx| active_call.join_channel(zed_id, cx))
         .await
         .unwrap();
 
@@ -617,9 +608,7 @@ async fn test_channel_jumping(executor: BackgroundExecutor, cx_a: &mut TestAppCo
     let active_call_a = cx_a.read(ActiveCall::global);
 
     active_call_a
-        .update(cx_a, |active_call, cx| {
-            active_call.join_channel(zed_id, None, cx)
-        })
+        .update(cx_a, |active_call, cx| active_call.join_channel(zed_id, cx))
         .await
         .unwrap();
 
@@ -638,7 +627,7 @@ async fn test_channel_jumping(executor: BackgroundExecutor, cx_a: &mut TestAppCo
 
     active_call_a
         .update(cx_a, |active_call, cx| {
-            active_call.join_channel(rust_id, None, cx)
+            active_call.join_channel(rust_id, cx)
         })
         .await
         .unwrap();
@@ -804,7 +793,7 @@ async fn test_call_from_channel(
     let active_call_b = cx_b.read(ActiveCall::global);
 
     active_call_a
-        .update(cx_a, |call, cx| call.join_channel(channel_id, None, cx))
+        .update(cx_a, |call, cx| call.join_channel(channel_id, cx))
         .await
         .unwrap();
 
@@ -1297,7 +1286,7 @@ async fn test_guest_access(
 
     // Non-members should not be allowed to join
     assert!(active_call_b
-        .update(cx_b, |call, cx| call.join_channel(channel_a, None, cx))
+        .update(cx_b, |call, cx| call.join_channel(channel_a, cx))
         .await
         .is_err());
 
@@ -1319,7 +1308,7 @@ async fn test_guest_access(
 
     // Client B joins channel A as a guest
     active_call_b
-        .update(cx_b, |call, cx| call.join_channel(channel_a, None, cx))
+        .update(cx_b, |call, cx| call.join_channel(channel_a, cx))
         .await
         .unwrap();
 
@@ -1352,7 +1341,7 @@ async fn test_guest_access(
     assert_channels_list_shape(client_b.channel_store(), cx_b, &[]);
 
     active_call_b
-        .update(cx_b, |call, cx| call.join_channel(channel_b, None, cx))
+        .update(cx_b, |call, cx| call.join_channel(channel_b, cx))
         .await
         .unwrap();
 
@@ -1383,7 +1372,7 @@ async fn test_invite_access(
 
     // should not be allowed to join
     assert!(active_call_b
-        .update(cx_b, |call, cx| call.join_channel(channel_b_id, None, cx))
+        .update(cx_b, |call, cx| call.join_channel(channel_b_id, cx))
         .await
         .is_err());
 
@@ -1401,7 +1390,7 @@ async fn test_invite_access(
         .unwrap();
 
     active_call_b
-        .update(cx_b, |call, cx| call.join_channel(channel_b_id, None, cx))
+        .update(cx_b, |call, cx| call.join_channel(channel_b_id, cx))
         .await
         .unwrap();
 

crates/collab2/src/tests/integration_tests.rs 🔗

@@ -510,10 +510,9 @@ async fn test_joining_channels_and_calling_multiple_users_simultaneously(
 
     // Simultaneously join channel 1 and then channel 2
     active_call_a
-        .update(cx_a, |call, cx| call.join_channel(channel_1, None, cx))
+        .update(cx_a, |call, cx| call.join_channel(channel_1, cx))
         .detach();
-    let join_channel_2 =
-        active_call_a.update(cx_a, |call, cx| call.join_channel(channel_2, None, cx));
+    let join_channel_2 = active_call_a.update(cx_a, |call, cx| call.join_channel(channel_2, cx));
 
     join_channel_2.await.unwrap();
 
@@ -539,8 +538,7 @@ async fn test_joining_channels_and_calling_multiple_users_simultaneously(
         call.invite(client_c.user_id().unwrap(), None, cx)
     });
 
-    let join_channel =
-        active_call_a.update(cx_a, |call, cx| call.join_channel(channel_1, None, cx));
+    let join_channel = active_call_a.update(cx_a, |call, cx| call.join_channel(channel_1, cx));
 
     b_invite.await.unwrap();
     c_invite.await.unwrap();
@@ -569,8 +567,7 @@ async fn test_joining_channels_and_calling_multiple_users_simultaneously(
         .unwrap();
 
     // Simultaneously join channel 1 and call user B and user C from client A.
-    let join_channel =
-        active_call_a.update(cx_a, |call, cx| call.join_channel(channel_1, None, cx));
+    let join_channel = active_call_a.update(cx_a, |call, cx| call.join_channel(channel_1, cx));
 
     let b_invite = active_call_a.update(cx_a, |call, cx| {
         call.invite(client_b.user_id().unwrap(), None, cx)

crates/collab2/src/tests/test_server.rs 🔗

@@ -221,7 +221,6 @@ impl TestServer {
             fs: fs.clone(),
             build_window_options: |_, _, _| Default::default(),
             node_runtime: FakeNodeRuntime::new(),
-            call_factory: |_| Box::new(workspace::TestCallHandler),
         });
 
         cx.update(|cx| {

crates/collab_ui2/src/collab_panel.rs 🔗

@@ -18,7 +18,7 @@ mod contact_finder;
 // };
 use contact_finder::ContactFinder;
 use menu::{Cancel, Confirm, SelectNext, SelectPrev};
-use rpc::proto;
+use rpc::proto::{self, PeerId};
 use theme::{ActiveTheme, ThemeSettings};
 // use context_menu::{ContextMenu, ContextMenuItem};
 // use db::kvp::KEY_VALUE_STORE;
@@ -169,11 +169,12 @@ use editor::Editor;
 use feature_flags::{ChannelsAlpha, FeatureFlagAppExt, FeatureFlagViewExt};
 use fuzzy::{match_strings, StringMatchCandidate};
 use gpui::{
-    actions, div, img, overlay, prelude::*, px, rems, serde_json, Action, AppContext,
-    AsyncWindowContext, Bounds, ClipboardItem, DismissEvent, Div, EventEmitter, FocusHandle,
-    Focusable, FocusableView, InteractiveElement, IntoElement, Model, MouseDownEvent,
-    ParentElement, Pixels, Point, PromptLevel, Render, RenderOnce, ScrollHandle, SharedString,
-    Stateful, Styled, Subscription, Task, View, ViewContext, VisualContext, WeakView,
+    actions, canvas, div, img, overlay, point, prelude::*, px, rems, serde_json, Action,
+    AppContext, AsyncWindowContext, Bounds, ClipboardItem, DismissEvent, Div, EventEmitter,
+    FocusHandle, Focusable, FocusableView, Hsla, InteractiveElement, IntoElement, Length, Model,
+    MouseDownEvent, ParentElement, Pixels, Point, PromptLevel, Quad, Render, RenderOnce,
+    ScrollHandle, SharedString, Size, Stateful, Styled, Subscription, Task, View, ViewContext,
+    VisualContext, WeakView,
 };
 use project::{Fs, Project};
 use serde_derive::{Deserialize, Serialize};
@@ -347,21 +348,21 @@ enum Section {
 #[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,
-    //     },
+    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>),
@@ -370,12 +371,12 @@ enum ListEntry {
         depth: usize,
         has_children: bool,
     },
-    //     ChannelNotes {
-    //         channel_id: ChannelId,
-    //     },
-    //     ChannelChat {
-    //         channel_id: ChannelId,
-    //     },
+    ChannelNotes {
+        channel_id: ChannelId,
+    },
+    ChannelChat {
+        channel_id: ChannelId,
+    },
     ChannelEditor {
         depth: usize,
     },
@@ -708,136 +709,136 @@ impl CollabPanel {
 
         let prev_selected_entry = self.selection.and_then(|ix| self.entries.get(ix).cloned());
         let old_entries = mem::take(&mut self.entries);
-        let 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;
-        //             }
+        let mut scroll_to_top = false;
 
-        //             if !self.collapsed_sections.contains(&Section::ActiveCall) {
-        //                 let room = room.read(cx);
+        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 let Some(channel_id) = room.channel_id() {
-        //                     self.entries.push(ListEntry::ChannelNotes { channel_id });
-        //                     self.entries.push(ListEntry::ChannelChat { channel_id })
-        //                 }
+            if !self.collapsed_sections.contains(&Section::ActiveCall) {
+                let room = room.read(cx);
 
-        //                 // 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,
-        //                             });
-        //                         }
-        //                     }
-        //                 }
+                if let Some(channel_id) = room.channel_id() {
+                    self.entries.push(ListEntry::ChannelNotes { channel_id });
+                    self.entries.push(ListEntry::ChannelChat { channel_id })
+                }
 
-        //                 // 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 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 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,
-        //                     }));
-        //             }
-        //         }
+                // 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();
 
@@ -1135,290 +1136,234 @@ impl CollabPanel {
         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_call_participant(
+        &self,
+        user: Arc<User>,
+        peer_id: Option<PeerId>,
+        is_pending: bool,
+        cx: &mut ViewContext<Self>,
+    ) -> impl IntoElement {
+        let is_current_user =
+            self.user_store.read(cx).current_user().map(|user| user.id) == Some(user.id);
+        let tooltip = format!("Follow {}", user.github_login);
 
-    //     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()
-    //     }
+        ListItem::new(SharedString::from(user.github_login.clone()))
+            .left_child(Avatar::data(user.avatar.clone().unwrap()))
+            .child(
+                h_stack()
+                    .w_full()
+                    .justify_between()
+                    .child(Label::new(user.github_login.clone()))
+                    .child(if is_pending {
+                        Label::new("Calling").color(Color::Muted).into_any_element()
+                    } else if is_current_user {
+                        IconButton::new("leave-call", Icon::ArrowRight)
+                            .on_click(cx.listener(move |this, _, cx| {
+                                Self::leave_call(cx);
+                            }))
+                            .tooltip(|cx| Tooltip::text("Leave Call", cx))
+                            .into_any_element()
+                    } else {
+                        div().into_any_element()
+                    }),
+            )
+            .when_some(peer_id, |this, peer_id| {
+                this.tooltip(move |cx| Tooltip::text(tooltip.clone(), cx))
+                    .on_click(cx.listener(move |this, _, cx| {
+                        this.workspace
+                            .update(cx, |workspace, cx| workspace.follow(peer_id, cx));
+                    }))
+            })
+    }
 
-    //     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 render_participant_project(
+        &self,
+        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>,
+    ) -> impl IntoElement {
+        let project_name: SharedString = if worktree_root_names.is_empty() {
+            "untitled".to_string()
+        } else {
+            worktree_root_names.join(", ")
+        }
+        .into();
+
+        let theme = cx.theme();
+
+        ListItem::new(project_id as usize)
+            .on_click(cx.listener(move |this, _, cx| {
+                this.workspace.update(cx, |workspace, cx| {
+                    let app_state = workspace.app_state().clone();
+                    workspace::join_remote_project(project_id, host_user_id, app_state, cx)
+                        .detach_and_log_err(cx);
+                });
+            }))
+            .left_child(IconButton::new(0, Icon::Folder))
+            .child(
+                h_stack()
+                    .w_full()
+                    .justify_between()
+                    .child(render_tree_branch(is_last, cx))
+                    .child(Label::new(project_name.clone())),
+            )
+            .tooltip(move |cx| Tooltip::text(format!("Open {}", project_name), cx))
+
+        //         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 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(
+        &self,
+        peer_id: Option<PeerId>,
+        is_last: bool,
+        cx: &mut ViewContext<Self>,
+    ) -> impl IntoElement {
+        //         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()
+
+        div()
+    }
 
     fn take_editing_state(&mut self, cx: &mut ViewContext<Self>) -> bool {
         if let Some(_) = self.channel_editing_state.take() {
@@ -1465,117 +1410,114 @@ impl CollabPanel {
     //         .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_notes(
+        &self,
+        channel_id: ChannelId,
+        cx: &mut ViewContext<Self>,
+    ) -> impl IntoElement {
+        //         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()
-    //     }
+        div()
+    }
+
+    fn render_channel_chat(
+        &self,
+        channel_id: ChannelId,
+        cx: &mut ViewContext<Self>,
+    ) -> impl IntoElement {
+        //         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()
+        div()
+    }
 
     //     fn render_channel_invite(
     //         channel: Arc<Channel>,

crates/collab_ui2/src/collab_titlebar_item.rs 🔗

@@ -31,9 +31,9 @@ use std::sync::Arc;
 use call::ActiveCall;
 use client::{Client, UserStore};
 use gpui::{
-    div, px, rems, AppContext, Div, Element, InteractiveElement, IntoElement, Model, MouseButton,
-    ParentElement, Render, RenderOnce, Stateful, StatefulInteractiveElement, Styled, Subscription,
-    ViewContext, VisualContext, WeakView, WindowBounds,
+    actions, div, px, rems, AppContext, Div, Element, InteractiveElement, IntoElement, Model,
+    MouseButton, ParentElement, Render, RenderOnce, Stateful, StatefulInteractiveElement, Styled,
+    Subscription, ViewContext, VisualContext, WeakView, WindowBounds,
 };
 use project::{Project, RepositoryEntry};
 use theme::ActiveTheme;
@@ -49,6 +49,14 @@ use crate::face_pile::FacePile;
 const MAX_PROJECT_NAME_LENGTH: usize = 40;
 const MAX_BRANCH_NAME_LENGTH: usize = 40;
 
+actions!(
+    ShareProject,
+    UnshareProject,
+    ToggleUserMenu,
+    ToggleProjectMenu,
+    SwitchBranch
+);
+
 // actions!(
 //     collab,
 //     [
@@ -91,37 +99,23 @@ impl Render for CollabTitlebarItem {
     type Element = Stateful<Div>;
 
     fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
-        let is_in_room = self
-            .workspace
-            .update(cx, |this, cx| this.call_state().is_in_room(cx))
-            .unwrap_or_default();
+        let room = ActiveCall::global(cx).read(cx).room();
+        let is_in_room = room.is_some();
         let is_shared = is_in_room && self.project.read(cx).is_shared();
         let current_user = self.user_store.read(cx).current_user();
         let client = self.client.clone();
-        let users = self
-            .workspace
-            .update(cx, |this, cx| this.call_state().remote_participants(cx))
-            .log_err()
-            .flatten();
-        let is_muted = self
-            .workspace
-            .update(cx, |this, cx| this.call_state().is_muted(cx))
-            .log_err()
-            .flatten()
-            .unwrap_or_default();
-        let is_deafened = self
-            .workspace
-            .update(cx, |this, cx| this.call_state().is_deafened(cx))
-            .log_err()
-            .flatten()
-            .unwrap_or_default();
-        let speakers_icon = if self
-            .workspace
-            .update(cx, |this, cx| this.call_state().is_deafened(cx))
-            .log_err()
-            .flatten()
-            .unwrap_or_default()
-        {
+        let remote_participants = room.map(|room| {
+            room.read(cx)
+                .remote_participants()
+                .values()
+                .map(|participant| (participant.user.clone(), participant.peer_id))
+                .collect::<Vec<_>>()
+        });
+        let is_muted = room.map_or(false, |room| room.read(cx).is_muted(cx));
+        let is_deafened = room
+            .and_then(|room| room.read(cx).is_deafened())
+            .unwrap_or(false);
+        let speakers_icon = if is_deafened {
             ui::Icon::AudioOff
         } else {
             ui::Icon::AudioOn
@@ -157,7 +151,7 @@ impl Render for CollabTitlebarItem {
                     .children(self.render_project_branch(cx)),
             )
             .when_some(
-                users.zip(current_user.clone()),
+                remote_participants.zip(current_user.clone()),
                 |this, (remote_participants, current_user)| {
                     let mut pile = FacePile::default();
                     pile.extend(
@@ -168,25 +162,30 @@ impl Render for CollabTitlebarItem {
                                 div().child(Avatar::data(avatar.clone())).into_any_element()
                             })
                             .into_iter()
-                            .chain(remote_participants.into_iter().flat_map(|(user, peer_id)| {
-                                user.avatar.as_ref().map(|avatar| {
-                                    div()
-                                        .child(
-                                            Avatar::data(avatar.clone()).into_element().into_any(),
-                                        )
-                                        .on_mouse_down(MouseButton::Left, {
-                                            let workspace = workspace.clone();
-                                            move |_, cx| {
-                                                workspace
-                                                    .update(cx, |this, cx| {
-                                                        this.open_shared_screen(peer_id, cx);
-                                                    })
-                                                    .log_err();
-                                            }
-                                        })
-                                        .into_any_element()
-                                })
-                            })),
+                            .chain(remote_participants.into_iter().filter_map(
+                                |(user, peer_id)| {
+                                    let avatar = user.avatar.as_ref()?;
+                                    Some(
+                                        div()
+                                            .child(
+                                                Avatar::data(avatar.clone())
+                                                    .into_element()
+                                                    .into_any(),
+                                            )
+                                            .on_mouse_down(MouseButton::Left, {
+                                                let workspace = workspace.clone();
+                                                move |_, cx| {
+                                                    workspace
+                                                        .update(cx, |this, cx| {
+                                                            this.open_shared_screen(peer_id, cx);
+                                                        })
+                                                        .log_err();
+                                                }
+                                            })
+                                            .into_any_element(),
+                                    )
+                                },
+                            )),
                     );
                     this.child(pile.render(cx))
                 },
@@ -204,20 +203,24 @@ impl Render for CollabTitlebarItem {
                                         "toggle_sharing",
                                         if is_shared { "Unshare" } else { "Share" },
                                     )
-                                    .style(ButtonStyle::Subtle),
+                                    .style(ButtonStyle::Subtle)
+                                    .on_click(cx.listener(
+                                        move |this, _, cx| {
+                                            if is_shared {
+                                                this.unshare_project(&Default::default(), cx);
+                                            } else {
+                                                this.share_project(&Default::default(), cx);
+                                            }
+                                        },
+                                    )),
                                 )
                                 .child(
                                     IconButton::new("leave-call", ui::Icon::Exit)
                                         .style(ButtonStyle::Subtle)
-                                        .on_click({
-                                            let workspace = workspace.clone();
-                                            move |_, cx| {
-                                                workspace
-                                                    .update(cx, |this, cx| {
-                                                        this.call_state().hang_up(cx).detach();
-                                                    })
-                                                    .log_err();
-                                            }
+                                        .on_click(move |_, cx| {
+                                            ActiveCall::global(cx)
+                                                .update(cx, |call, cx| call.hang_up(cx))
+                                                .detach_and_log_err(cx);
                                         }),
                                 ),
                         )
@@ -235,15 +238,8 @@ impl Render for CollabTitlebarItem {
                                     )
                                     .style(ButtonStyle::Subtle)
                                     .selected(is_muted)
-                                    .on_click({
-                                        let workspace = workspace.clone();
-                                        move |_, cx| {
-                                            workspace
-                                                .update(cx, |this, cx| {
-                                                    this.call_state().toggle_mute(cx);
-                                                })
-                                                .log_err();
-                                        }
+                                    .on_click(move |_, cx| {
+                                        crate::toggle_mute(&Default::default(), cx)
                                     }),
                                 )
                                 .child(
@@ -258,26 +254,15 @@ impl Render for CollabTitlebarItem {
                                                 cx,
                                             )
                                         })
-                                        .on_click({
-                                            let workspace = workspace.clone();
-                                            move |_, cx| {
-                                                workspace
-                                                    .update(cx, |this, cx| {
-                                                        this.call_state().toggle_deafen(cx);
-                                                    })
-                                                    .log_err();
-                                            }
+                                        .on_click(move |_, cx| {
+                                            crate::toggle_mute(&Default::default(), cx)
                                         }),
                                 )
                                 .child(
                                     IconButton::new("screen-share", ui::Icon::Screen)
                                         .style(ButtonStyle::Subtle)
                                         .on_click(move |_, cx| {
-                                            workspace
-                                                .update(cx, |this, cx| {
-                                                    this.call_state().toggle_screen_share(cx);
-                                                })
-                                                .log_err();
+                                            crate::toggle_screen_sharing(&Default::default(), cx)
                                         }),
                                 )
                                 .pl_2(),
@@ -451,46 +436,19 @@ impl CollabTitlebarItem {
     // render_project_owner -> resolve if you are in a room -> Option<foo>
 
     pub fn render_project_owner(&self, cx: &mut ViewContext<Self>) -> Option<impl Element> {
-        // TODO: We can't finish implementing this until project sharing works
-        // - [ ] Show the project owner when the project is remote (maybe done)
-        // - [x] Show the project owner when the project is local
-        // - [ ] Show the project owner with a lock icon when the project is local and unshared
-
-        let remote_id = self.project.read(cx).remote_id();
-        let is_local = remote_id.is_none();
-        let is_shared = self.project.read(cx).is_shared();
-        let (user_name, participant_index) = {
-            if let Some(host) = self.project.read(cx).host() {
-                debug_assert!(!is_local);
-                let (Some(host_user), Some(participant_index)) = (
-                    self.user_store.read(cx).get_cached_user(host.user_id),
-                    self.user_store
-                        .read(cx)
-                        .participant_indices()
-                        .get(&host.user_id),
-                ) else {
-                    return None;
-                };
-                (host_user.github_login.clone(), participant_index.0)
-            } else {
-                debug_assert!(is_local);
-                let name = self
-                    .user_store
-                    .read(cx)
-                    .current_user()
-                    .map(|user| user.github_login.clone())?;
-                (name, 0)
-            }
-        };
+        let host = self.project.read(cx).host()?;
+        let host = self.user_store.read(cx).get_cached_user(host.user_id)?;
+        let participant_index = self
+            .user_store
+            .read(cx)
+            .participant_indices()
+            .get(&host.id)?;
         Some(
             div().border().border_color(gpui::red()).child(
-                Button::new(
-                    "project_owner_trigger",
-                    format!("{user_name} ({})", !is_shared),
-                )
-                .color(Color::Player(participant_index))
-                .style(ButtonStyle::Subtle)
-                .tooltip(move |cx| Tooltip::text("Toggle following", cx)),
+                Button::new("project_owner_trigger", host.github_login.clone())
+                    .color(Color::Player(participant_index.0))
+                    .style(ButtonStyle::Subtle)
+                    .tooltip(move |cx| Tooltip::text("Toggle following", cx)),
             ),
         )
     }
@@ -730,21 +688,21 @@ 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 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();
-    // }
+    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| {

crates/collab_ui2/src/collab_ui.rs 🔗

@@ -9,22 +9,21 @@ mod panel_settings;
 
 use std::{rc::Rc, sync::Arc};
 
+use call::{report_call_event_for_room, ActiveCall, Room};
 pub use collab_panel::CollabPanel;
 pub use collab_titlebar_item::CollabTitlebarItem;
 use gpui::{
-    point, AppContext, GlobalPixels, Pixels, PlatformDisplay, Size, WindowBounds, WindowKind,
-    WindowOptions,
+    actions, point, AppContext, GlobalPixels, Pixels, PlatformDisplay, Size, Task, WindowBounds,
+    WindowKind, WindowOptions,
 };
 pub use panel_settings::{
     ChatPanelSettings, CollaborationPanelSettings, NotificationPanelSettings,
 };
 use settings::Settings;
+use util::ResultExt;
 use workspace::AppState;
 
-// actions!(
-//     collab,
-//     [ToggleScreenSharing, ToggleMute, ToggleDeafen, LeaveCall]
-// );
+actions!(ToggleScreenSharing, ToggleMute, ToggleDeafen, LeaveCall);
 
 pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
     CollaborationPanelSettings::register(cx);
@@ -42,61 +41,61 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
     // 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 PlatformDisplay>,

crates/editor2/src/element.rs 🔗

@@ -1755,7 +1755,7 @@ impl EditorElement {
             let gutter_width;
             let gutter_margin;
             if snapshot.show_gutter {
-                let descent = cx.text_system().descent(font_id, font_size).unwrap();
+                let descent = cx.text_system().descent(font_id, font_size);
 
                 let gutter_padding_factor = 3.5;
                 gutter_padding = (em_width * gutter_padding_factor).round();
@@ -3714,7 +3714,7 @@ fn compute_auto_height_layout(
     let gutter_width;
     let gutter_margin;
     if snapshot.show_gutter {
-        let descent = cx.text_system().descent(font_id, font_size).unwrap();
+        let descent = cx.text_system().descent(font_id, font_size);
         let gutter_padding_factor = 3.5;
         gutter_padding = (em_width * gutter_padding_factor).round();
         gutter_width = max_line_number_width + gutter_padding * 2.0;

crates/gpui2/src/elements/canvas.rs 🔗

@@ -0,0 +1,48 @@
+use crate::{Bounds, Element, IntoElement, Pixels, StyleRefinement, Styled, WindowContext};
+
+pub fn canvas(callback: impl 'static + FnOnce(Bounds<Pixels>, &mut WindowContext)) -> Canvas {
+    Canvas {
+        paint_callback: Box::new(callback),
+        style: Default::default(),
+    }
+}
+
+pub struct Canvas {
+    paint_callback: Box<dyn FnOnce(Bounds<Pixels>, &mut WindowContext)>,
+    style: StyleRefinement,
+}
+
+impl IntoElement for Canvas {
+    type Element = Self;
+
+    fn element_id(&self) -> Option<crate::ElementId> {
+        None
+    }
+
+    fn into_element(self) -> Self::Element {
+        self
+    }
+}
+
+impl Element for Canvas {
+    type State = ();
+
+    fn layout(
+        &mut self,
+        _: Option<Self::State>,
+        cx: &mut WindowContext,
+    ) -> (crate::LayoutId, Self::State) {
+        let layout_id = cx.request_layout(&self.style.clone().into(), []);
+        (layout_id, ())
+    }
+
+    fn paint(self, bounds: Bounds<Pixels>, _: &mut (), cx: &mut WindowContext) {
+        (self.paint_callback)(bounds, cx)
+    }
+}
+
+impl Styled for Canvas {
+    fn style(&mut self) -> &mut crate::StyleRefinement {
+        &mut self.style
+    }
+}

crates/gpui2/src/elements/mod.rs 🔗

@@ -1,3 +1,4 @@
+mod canvas;
 mod div;
 mod img;
 mod overlay;
@@ -5,6 +6,7 @@ mod svg;
 mod text;
 mod uniform_list;
 
+pub use canvas::*;
 pub use div::*;
 pub use img::*;
 pub use overlay::*;

crates/gpui2/src/text_system.rs 🔗

@@ -72,7 +72,7 @@ impl TextSystem {
         }
     }
 
-    pub fn bounding_box(&self, font_id: FontId, font_size: Pixels) -> Result<Bounds<Pixels>> {
+    pub fn bounding_box(&self, font_id: FontId, font_size: Pixels) -> Bounds<Pixels> {
         self.read_metrics(font_id, |metrics| metrics.bounding_box(font_size))
     }
 
@@ -89,9 +89,9 @@ impl TextSystem {
         let bounds = self
             .platform_text_system
             .typographic_bounds(font_id, glyph_id)?;
-        self.read_metrics(font_id, |metrics| {
+        Ok(self.read_metrics(font_id, |metrics| {
             (bounds / metrics.units_per_em as f32 * font_size.0).map(px)
-        })
+        }))
     }
 
     pub fn advance(&self, font_id: FontId, font_size: Pixels, ch: char) -> Result<Size<Pixels>> {
@@ -100,28 +100,28 @@ impl TextSystem {
             .glyph_for_char(font_id, ch)
             .ok_or_else(|| anyhow!("glyph not found for character '{}'", ch))?;
         let result = self.platform_text_system.advance(font_id, glyph_id)?
-            / self.units_per_em(font_id)? as f32;
+            / self.units_per_em(font_id) as f32;
 
         Ok(result * font_size)
     }
 
-    pub fn units_per_em(&self, font_id: FontId) -> Result<u32> {
+    pub fn units_per_em(&self, font_id: FontId) -> u32 {
         self.read_metrics(font_id, |metrics| metrics.units_per_em as u32)
     }
 
-    pub fn cap_height(&self, font_id: FontId, font_size: Pixels) -> Result<Pixels> {
+    pub fn cap_height(&self, font_id: FontId, font_size: Pixels) -> Pixels {
         self.read_metrics(font_id, |metrics| metrics.cap_height(font_size))
     }
 
-    pub fn x_height(&self, font_id: FontId, font_size: Pixels) -> Result<Pixels> {
+    pub fn x_height(&self, font_id: FontId, font_size: Pixels) -> Pixels {
         self.read_metrics(font_id, |metrics| metrics.x_height(font_size))
     }
 
-    pub fn ascent(&self, font_id: FontId, font_size: Pixels) -> Result<Pixels> {
+    pub fn ascent(&self, font_id: FontId, font_size: Pixels) -> Pixels {
         self.read_metrics(font_id, |metrics| metrics.ascent(font_size))
     }
 
-    pub fn descent(&self, font_id: FontId, font_size: Pixels) -> Result<Pixels> {
+    pub fn descent(&self, font_id: FontId, font_size: Pixels) -> Pixels {
         self.read_metrics(font_id, |metrics| metrics.descent(font_size))
     }
 
@@ -130,24 +130,24 @@ impl TextSystem {
         font_id: FontId,
         font_size: Pixels,
         line_height: Pixels,
-    ) -> Result<Pixels> {
-        let ascent = self.ascent(font_id, font_size)?;
-        let descent = self.descent(font_id, font_size)?;
+    ) -> Pixels {
+        let ascent = self.ascent(font_id, font_size);
+        let descent = self.descent(font_id, font_size);
         let padding_top = (line_height - ascent - descent) / 2.;
-        Ok(padding_top + ascent)
+        padding_top + ascent
     }
 
-    fn read_metrics<T>(&self, font_id: FontId, read: impl FnOnce(&FontMetrics) -> T) -> Result<T> {
+    fn read_metrics<T>(&self, font_id: FontId, read: impl FnOnce(&FontMetrics) -> T) -> T {
         let lock = self.font_metrics.upgradable_read();
 
         if let Some(metrics) = lock.get(&font_id) {
-            Ok(read(metrics))
+            read(metrics)
         } else {
             let mut lock = RwLockUpgradableReadGuard::upgrade(lock);
             let metrics = lock
                 .entry(font_id)
                 .or_insert_with(|| self.platform_text_system.font_metrics(font_id));
-            Ok(read(metrics))
+            read(metrics)
         }
     }
 

crates/gpui2/src/text_system/line.rs 🔗

@@ -101,9 +101,7 @@ fn paint_line(
     let mut glyph_origin = origin;
     let mut prev_glyph_position = Point::default();
     for (run_ix, run) in layout.runs.iter().enumerate() {
-        let max_glyph_size = text_system
-            .bounding_box(run.font_id, layout.font_size)?
-            .size;
+        let max_glyph_size = text_system.bounding_box(run.font_id, layout.font_size).size;
 
         for (glyph_ix, glyph) in run.glyphs.iter().enumerate() {
             glyph_origin.x += glyph.position.x - prev_glyph_position.x;

crates/gpui2/src/window.rs 🔗

@@ -2734,6 +2734,7 @@ pub enum ElementId {
     Integer(usize),
     Name(SharedString),
     FocusHandle(FocusId),
+    NamedInteger(SharedString, usize),
 }
 
 impl ElementId {
@@ -2783,3 +2784,9 @@ impl<'a> From<&'a FocusHandle> for ElementId {
         ElementId::FocusHandle(handle.id)
     }
 }
+
+impl From<(&'static str, EntityId)> for ElementId {
+    fn from((name, id): (&'static str, EntityId)) -> Self {
+        ElementId::NamedInteger(name.into(), id.as_u64() as usize)
+    }
+}

crates/ui2/src/components/list/list_item.rs 🔗

@@ -1,7 +1,8 @@
 use std::rc::Rc;
 
 use gpui::{
-    px, AnyElement, ClickEvent, Div, ImageSource, MouseButton, MouseDownEvent, Pixels, Stateful,
+    px, AnyElement, AnyView, ClickEvent, Div, ImageSource, MouseButton, MouseDownEvent, Pixels,
+    Stateful,
 };
 use smallvec::SmallVec;
 
@@ -21,6 +22,7 @@ pub struct ListItem {
     inset: bool,
     on_click: Option<Rc<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
     on_toggle: Option<Rc<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
+    tooltip: Option<Box<dyn Fn(&mut WindowContext) -> AnyView + 'static>>,
     on_secondary_mouse_down: Option<Rc<dyn Fn(&MouseDownEvent, &mut WindowContext) + 'static>>,
     children: SmallVec<[AnyElement; 2]>,
 }
@@ -38,6 +40,7 @@ impl ListItem {
             on_click: None,
             on_secondary_mouse_down: None,
             on_toggle: None,
+            tooltip: None,
             children: SmallVec::new(),
         }
     }
@@ -55,6 +58,11 @@ impl ListItem {
         self
     }
 
+    pub fn tooltip(mut self, tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static) -> Self {
+        self.tooltip = Some(Box::new(tooltip));
+        self
+    }
+
     pub fn inset(mut self, inset: bool) -> Self {
         self.inset = inset;
         self
@@ -149,6 +157,7 @@ impl RenderOnce for ListItem {
                     (on_mouse_down)(event, cx)
                 })
             })
+            .when_some(self.tooltip, |this, tooltip| this.tooltip(tooltip))
             .child(
                 div()
                     .when(self.inset, |this| this.px_2())

crates/workspace2/Cargo.toml 🔗

@@ -20,6 +20,7 @@ test-support = [
 
 [dependencies]
 db = { path = "../db2", package = "db2" }
+call = { path = "../call2", package = "call2" }
 client = { path = "../client2", package = "client2" }
 collections = { path = "../collections" }
 # context_menu = { path = "../context_menu" }
@@ -36,7 +37,6 @@ theme = { path = "../theme2", package = "theme2" }
 util = { path = "../util" }
 ui = { package = "ui2", path = "../ui2" }
 
-async-trait.workspace = true
 async-recursion = "1.0.0"
 itertools = "0.10"
 bincode = "1.2.1"

crates/workspace2/src/pane_group.rs 🔗

@@ -1,18 +1,20 @@
 use crate::{AppState, FollowerState, Pane, Workspace};
 use anyhow::{anyhow, bail, Result};
+use call::{ActiveCall, ParticipantLocation};
 use collections::HashMap;
 use db::sqlez::{
     bindable::{Bind, Column, StaticColumnCount},
     statement::Statement,
 };
 use gpui::{
-    point, size, AnyWeakView, Bounds, Div, IntoElement, Model, Pixels, Point, View, ViewContext,
+    point, size, AnyWeakView, Bounds, Div, Entity as _, IntoElement, Model, Pixels, Point, View,
+    ViewContext,
 };
 use parking_lot::Mutex;
 use project::Project;
 use serde::Deserialize;
 use std::sync::Arc;
-use ui::prelude::*;
+use ui::{prelude::*, Button};
 
 const HANDLE_HITBOX_SIZE: f32 = 4.0;
 const HORIZONTAL_MIN_SIZE: f32 = 80.;
@@ -126,6 +128,7 @@ impl PaneGroup {
         &self,
         project: &Model<Project>,
         follower_states: &HashMap<View<Pane>, FollowerState>,
+        active_call: Option<&Model<ActiveCall>>,
         active_pane: &View<Pane>,
         zoomed: Option<&AnyWeakView>,
         app_state: &Arc<AppState>,
@@ -135,6 +138,7 @@ impl PaneGroup {
             project,
             0,
             follower_states,
+            active_call,
             active_pane,
             zoomed,
             app_state,
@@ -196,6 +200,7 @@ impl Member {
         project: &Model<Project>,
         basis: usize,
         follower_states: &HashMap<View<Pane>, FollowerState>,
+        active_call: Option<&Model<ActiveCall>>,
         active_pane: &View<Pane>,
         zoomed: Option<&AnyWeakView>,
         app_state: &Arc<AppState>,
@@ -203,19 +208,89 @@ impl Member {
     ) -> impl IntoElement {
         match self {
             Member::Pane(pane) => {
-                // todo!()
-                // let pane_element = if Some(pane.into()) == zoomed {
-                //     None
-                // } else {
-                //     Some(pane)
-                // };
-
-                div().size_full().child(pane.clone()).into_any()
-
-                //         Stack::new()
-                //             .with_child(pane_element.contained().with_border(leader_border))
-                //             .with_children(leader_status_box)
-                //             .into_any()
+                let leader = follower_states.get(pane).and_then(|state| {
+                    let room = active_call?.read(cx).room()?.read(cx);
+                    room.remote_participant_for_peer_id(state.leader_id)
+                });
+
+                let mut leader_border = None;
+                let mut leader_status_box = None;
+                if let Some(leader) = &leader {
+                    let mut leader_color = cx
+                        .theme()
+                        .players()
+                        .color_for_participant(leader.participant_index.0)
+                        .cursor;
+                    leader_color.fade_out(0.3);
+                    leader_border = Some(leader_color);
+
+                    leader_status_box = match leader.location {
+                        ParticipantLocation::SharedProject {
+                            project_id: leader_project_id,
+                        } => {
+                            if Some(leader_project_id) == project.read(cx).remote_id() {
+                                None
+                            } else {
+                                let leader_user = leader.user.clone();
+                                let leader_user_id = leader.user.id;
+                                Some(
+                                    Button::new(
+                                        ("leader-status", pane.entity_id()),
+                                        format!(
+                                            "Follow {} to their active project",
+                                            leader_user.github_login,
+                                        ),
+                                    )
+                                    .on_click(cx.listener(
+                                        move |this, _, cx| {
+                                            crate::join_remote_project(
+                                                leader_project_id,
+                                                leader_user_id,
+                                                this.app_state().clone(),
+                                                cx,
+                                            )
+                                            .detach_and_log_err(cx);
+                                        },
+                                    )),
+                                )
+                            }
+                        }
+                        ParticipantLocation::UnsharedProject => Some(Button::new(
+                            ("leader-status", pane.entity_id()),
+                            format!(
+                                "{} is viewing an unshared Zed project",
+                                leader.user.github_login
+                            ),
+                        )),
+                        ParticipantLocation::External => Some(Button::new(
+                            ("leader-status", pane.entity_id()),
+                            format!(
+                                "{} is viewing a window outside of Zed",
+                                leader.user.github_login
+                            ),
+                        )),
+                    };
+                }
+
+                div()
+                    .relative()
+                    .size_full()
+                    .child(pane.clone())
+                    .when_some(leader_border, |this, color| {
+                        this.border_2().border_color(color)
+                    })
+                    .when_some(leader_status_box, |this, status_box| {
+                        this.child(
+                            div()
+                                .absolute()
+                                .w_96()
+                                .bottom_3()
+                                .right_3()
+                                .z_index(1)
+                                .child(status_box),
+                        )
+                    })
+                    .into_any()
 
                 // let el = div()
                 //     .flex()

crates/call2/src/shared_screen.rs → crates/workspace2/src/shared_screen.rs 🔗

@@ -1,5 +1,9 @@
-use crate::participant::{Frame, RemoteVideoTrack};
+use crate::{
+    item::{Item, ItemEvent},
+    ItemNavHistory, WorkspaceId,
+};
 use anyhow::Result;
+use call::participant::{Frame, RemoteVideoTrack};
 use client::{proto::PeerId, User};
 use futures::StreamExt;
 use gpui::{
@@ -9,7 +13,6 @@ use gpui::{
 };
 use std::sync::{Arc, Weak};
 use ui::{h_stack, Icon, IconElement};
-use workspace::{item::Item, ItemNavHistory, WorkspaceId};
 
 pub enum Event {
     Close,
@@ -56,7 +59,7 @@ impl SharedScreen {
 }
 
 impl EventEmitter<Event> for SharedScreen {}
-impl EventEmitter<workspace::item::ItemEvent> for SharedScreen {}
+impl EventEmitter<ItemEvent> for SharedScreen {}
 
 impl FocusableView for SharedScreen {
     fn focus_handle(&self, _: &AppContext) -> FocusHandle {

crates/workspace2/src/workspace2.rs 🔗

@@ -10,15 +10,16 @@ mod persistence;
 pub mod searchable;
 // todo!()
 mod modal_layer;
+mod shared_screen;
 mod status_bar;
 mod toolbar;
 mod workspace_settings;
 
 use anyhow::{anyhow, Context as _, Result};
-use async_trait::async_trait;
+use call::ActiveCall;
 use client::{
     proto::{self, PeerId},
-    Client, TypedEnvelope, User, UserStore,
+    Client, Status, TypedEnvelope, UserStore,
 };
 use collections::{hash_map, HashMap, HashSet};
 use dock::{Dock, DockPosition, Panel, PanelButtons, PanelHandle};
@@ -28,11 +29,11 @@ use futures::{
     Future, FutureExt, StreamExt,
 };
 use gpui::{
-    actions, div, point, size, Action, AnyModel, AnyView, AnyWeakView, AppContext, AsyncAppContext,
-    AsyncWindowContext, Bounds, Context, Div, Entity, EntityId, EventEmitter, FocusHandle,
-    FocusableView, GlobalPixels, InteractiveElement, KeyContext, ManagedView, Model, ModelContext,
-    ParentElement, PathPromptOptions, Point, PromptLevel, Render, Size, Styled, Subscription, Task,
-    View, ViewContext, VisualContext, WeakModel, WeakView, WindowBounds, WindowContext,
+    actions, div, point, size, Action, AnyModel, AnyView, AnyWeakView, AnyWindowHandle, AppContext,
+    AsyncAppContext, AsyncWindowContext, Bounds, Context, Div, Entity, EntityId, EventEmitter,
+    FocusHandle, FocusableView, GlobalPixels, InteractiveElement, KeyContext, ManagedView, Model,
+    ModelContext, ParentElement, PathPromptOptions, Point, PromptLevel, Render, Size, Styled,
+    Subscription, Task, View, ViewContext, VisualContext, WeakView, WindowBounds, WindowContext,
     WindowHandle, WindowOptions,
 };
 use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, ProjectItem};
@@ -52,6 +53,7 @@ use postage::stream::Stream;
 use project::{Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId};
 use serde::Deserialize;
 use settings::Settings;
+use shared_screen::SharedScreen;
 use status_bar::StatusBar;
 pub use status_bar::StatusItemView;
 use std::{
@@ -209,6 +211,7 @@ pub fn init_settings(cx: &mut AppContext) {
 pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
     init_settings(cx);
     notifications::init(cx);
+
     //     cx.add_global_action({
     //         let app_state = Arc::downgrade(&app_state);
     //         move |_: &Open, cx: &mut AppContext| {
@@ -302,7 +305,6 @@ pub struct AppState {
     pub user_store: Model<UserStore>,
     pub workspace_store: Model<WorkspaceStore>,
     pub fs: Arc<dyn fs::Fs>,
-    pub call_factory: CallFactory,
     pub build_window_options:
         fn(Option<WindowBounds>, Option<Uuid>, &mut AppContext) -> WindowOptions,
     pub node_runtime: Arc<dyn NodeRuntime>,
@@ -321,69 +323,6 @@ struct Follower {
     peer_id: PeerId,
 }
 
-#[cfg(any(test, feature = "test-support"))]
-pub struct TestCallHandler;
-
-#[cfg(any(test, feature = "test-support"))]
-impl CallHandler for TestCallHandler {
-    fn peer_state(
-        &mut self,
-        id: PeerId,
-        project: &Model<Project>,
-        cx: &mut ViewContext<Workspace>,
-    ) -> Option<(bool, bool)> {
-        None
-    }
-
-    fn shared_screen_for_peer(
-        &self,
-        peer_id: PeerId,
-        pane: &View<Pane>,
-        cx: &mut ViewContext<Workspace>,
-    ) -> Option<Box<dyn ItemHandle>> {
-        None
-    }
-
-    fn room_id(&self, cx: &AppContext) -> Option<u64> {
-        None
-    }
-
-    fn hang_up(&self, cx: &mut AppContext) -> Task<Result<()>> {
-        Task::ready(Err(anyhow!("TestCallHandler should not be hanging up")))
-    }
-
-    fn active_project(&self, cx: &AppContext) -> Option<WeakModel<Project>> {
-        None
-    }
-
-    fn invite(
-        &mut self,
-        called_user_id: u64,
-        initial_project: Option<Model<Project>>,
-        cx: &mut AppContext,
-    ) -> Task<Result<()>> {
-        unimplemented!()
-    }
-
-    fn remote_participants(&self, cx: &AppContext) -> Option<Vec<(Arc<User>, PeerId)>> {
-        None
-    }
-
-    fn is_muted(&self, cx: &AppContext) -> Option<bool> {
-        None
-    }
-
-    fn toggle_mute(&self, cx: &mut AppContext) {}
-
-    fn toggle_screen_share(&self, cx: &mut AppContext) {}
-
-    fn toggle_deafen(&self, cx: &mut AppContext) {}
-
-    fn is_deafened(&self, cx: &AppContext) -> Option<bool> {
-        None
-    }
-}
-
 impl AppState {
     #[cfg(any(test, feature = "test-support"))]
     pub fn test(cx: &mut AppContext) -> Arc<Self> {
@@ -414,7 +353,6 @@ impl AppState {
             workspace_store,
             node_runtime: FakeNodeRuntime::new(),
             build_window_options: |_, _, _| Default::default(),
-            call_factory: |_| Box::new(TestCallHandler),
         })
     }
 }
@@ -471,40 +409,6 @@ pub enum Event {
     WorkspaceCreated(WeakView<Workspace>),
 }
 
-#[async_trait(?Send)]
-pub trait CallHandler {
-    fn peer_state(
-        &mut self,
-        id: PeerId,
-        project: &Model<Project>,
-        cx: &mut ViewContext<Workspace>,
-    ) -> Option<(bool, bool)>;
-    fn shared_screen_for_peer(
-        &self,
-        peer_id: PeerId,
-        pane: &View<Pane>,
-        cx: &mut ViewContext<Workspace>,
-    ) -> Option<Box<dyn ItemHandle>>;
-    fn room_id(&self, cx: &AppContext) -> Option<u64>;
-    fn is_in_room(&self, cx: &mut ViewContext<Workspace>) -> bool {
-        self.room_id(cx).is_some()
-    }
-    fn hang_up(&self, cx: &mut AppContext) -> Task<Result<()>>;
-    fn active_project(&self, cx: &AppContext) -> Option<WeakModel<Project>>;
-    fn invite(
-        &mut self,
-        called_user_id: u64,
-        initial_project: Option<Model<Project>>,
-        cx: &mut AppContext,
-    ) -> Task<Result<()>>;
-    fn remote_participants(&self, cx: &AppContext) -> Option<Vec<(Arc<User>, PeerId)>>;
-    fn is_muted(&self, cx: &AppContext) -> Option<bool>;
-    fn is_deafened(&self, cx: &AppContext) -> Option<bool>;
-    fn toggle_mute(&self, cx: &mut AppContext);
-    fn toggle_deafen(&self, cx: &mut AppContext);
-    fn toggle_screen_share(&self, cx: &mut AppContext);
-}
-
 pub struct Workspace {
     window_self: WindowHandle<Self>,
     weak_self: WeakView<Self>,
@@ -525,10 +429,10 @@ pub struct Workspace {
     titlebar_item: Option<AnyView>,
     notifications: Vec<(TypeId, usize, Box<dyn NotificationHandle>)>,
     project: Model<Project>,
-    call_handler: Box<dyn CallHandler>,
     follower_states: HashMap<View<Pane>, FollowerState>,
     last_leaders_by_pane: HashMap<WeakView<Pane>, PeerId>,
     window_edited: bool,
+    active_call: Option<(Model<ActiveCall>, Vec<Subscription>)>,
     leader_updates_tx: mpsc::UnboundedSender<(PeerId, proto::UpdateFollowers)>,
     database_id: WorkspaceId,
     app_state: Arc<AppState>,
@@ -556,7 +460,6 @@ struct FollowerState {
 
 enum WorkspaceBounds {}
 
-type CallFactory = fn(&mut ViewContext<Workspace>) -> Box<dyn CallHandler>;
 impl Workspace {
     pub fn new(
         workspace_id: WorkspaceId,
@@ -648,19 +551,9 @@ impl Workspace {
             mpsc::unbounded::<(PeerId, proto::UpdateFollowers)>();
         let _apply_leader_updates = cx.spawn(|this, mut cx| async move {
             while let Some((leader_id, update)) = leader_updates_rx.next().await {
-                let mut cx2 = cx.clone();
-                let t = this.clone();
-
-                Workspace::process_leader_update(&this, leader_id, update, &mut cx)
+                Self::process_leader_update(&this, leader_id, update, &mut cx)
                     .await
                     .log_err();
-
-                // this.update(&mut cx, |this, cxx| {
-                //     this.call_handler
-                //         .process_leader_update(leader_id, update, cx2)
-                // })?
-                // .await
-                // .log_err();
             }
 
             Ok(())
@@ -693,6 +586,14 @@ impl Workspace {
         //     drag_and_drop.register_container(weak_handle.clone());
         // });
 
+        let mut active_call = None;
+        if cx.has_global::<Model<ActiveCall>>() {
+            let call = cx.global::<Model<ActiveCall>>().clone();
+            let mut subscriptions = Vec::new();
+            subscriptions.push(cx.subscribe(&call, Self::on_active_call_event));
+            active_call = Some((call, subscriptions));
+        }
+
         let subscriptions = vec![
             cx.observe_window_activation(Self::on_window_activation_changed),
             cx.observe_window_bounds(move |_, cx| {
@@ -769,8 +670,7 @@ impl Workspace {
             follower_states: Default::default(),
             last_leaders_by_pane: Default::default(),
             window_edited: false,
-
-            call_handler: (app_state.call_factory)(cx),
+            active_call,
             database_id: workspace_id,
             app_state,
             _observe_current_user,
@@ -1217,7 +1117,7 @@ impl Workspace {
         cx: &mut ViewContext<Self>,
     ) -> Task<Result<bool>> {
         //todo!(saveing)
-
+        let active_call = self.active_call().cloned();
         let window = cx.window_handle();
 
         cx.spawn(|this, mut cx| async move {
@@ -1228,27 +1128,27 @@ impl Workspace {
                     .count()
             })?;
 
-            if !quitting
-                && workspace_count == 1
-                && this
-                    .update(&mut cx, |this, cx| this.call_handler.is_in_room(cx))
-                    .log_err()
-                    .unwrap_or_default()
-            {
-                let answer = window.update(&mut cx, |_, cx| {
-                    cx.prompt(
-                        PromptLevel::Warning,
-                        "Do you want to leave the current call?",
-                        &["Close window and hang up", "Cancel"],
-                    )
-                })?;
+            if let Some(active_call) = active_call {
+                if !quitting
+                    && workspace_count == 1
+                    && active_call.read_with(&cx, |call, _| call.room().is_some())?
+                {
+                    let answer = window.update(&mut cx, |_, cx| {
+                        cx.prompt(
+                            PromptLevel::Warning,
+                            "Do you want to leave the current call?",
+                            &["Close window and hang up", "Cancel"],
+                        )
+                    })?;
 
-                if answer.await.log_err() == Some(1) {
-                    return anyhow::Ok(false);
-                } else {
-                    this.update(&mut cx, |this, cx| this.call_handler.hang_up(cx))?
-                        .await
-                        .log_err();
+                    if answer.await.log_err() == Some(1) {
+                        return anyhow::Ok(false);
+                    } else {
+                        active_call
+                            .update(&mut cx, |call, cx| call.hang_up(cx))?
+                            .await
+                            .log_err();
+                    }
                 }
             }
 
@@ -2032,7 +1932,7 @@ impl Workspace {
     pub fn open_shared_screen(&mut self, peer_id: PeerId, cx: &mut ViewContext<Self>) {
         if let Some(shared_screen) = self.shared_screen_for_peer(peer_id, &self.active_pane, cx) {
             self.active_pane.update(cx, |pane, cx| {
-                pane.add_item(shared_screen, false, true, None, cx)
+                pane.add_item(Box::new(shared_screen), false, true, None, cx)
             });
         }
     }
@@ -2370,60 +2270,60 @@ impl Workspace {
         cx.notify();
     }
 
-    //     fn start_following(
-    //         &mut self,
-    //         leader_id: PeerId,
-    //         cx: &mut ViewContext<Self>,
-    //     ) -> Option<Task<Result<()>>> {
-    //         let pane = self.active_pane().clone();
-
-    //         self.last_leaders_by_pane
-    //             .insert(pane.downgrade(), leader_id);
-    //         self.unfollow(&pane, cx);
-    //         self.follower_states.insert(
-    //             pane.clone(),
-    //             FollowerState {
-    //                 leader_id,
-    //                 active_view_id: None,
-    //                 items_by_leader_view_id: Default::default(),
-    //             },
-    //         );
-    //         cx.notify();
-
-    //         let room_id = self.active_call()?.read(cx).room()?.read(cx).id();
-    //         let project_id = self.project.read(cx).remote_id();
-    //         let request = self.app_state.client.request(proto::Follow {
-    //             room_id,
-    //             project_id,
-    //             leader_id: Some(leader_id),
-    //         });
+    fn start_following(
+        &mut self,
+        leader_id: PeerId,
+        cx: &mut ViewContext<Self>,
+    ) -> Option<Task<Result<()>>> {
+        let pane = self.active_pane().clone();
+
+        self.last_leaders_by_pane
+            .insert(pane.downgrade(), leader_id);
+        self.unfollow(&pane, cx);
+        self.follower_states.insert(
+            pane.clone(),
+            FollowerState {
+                leader_id,
+                active_view_id: None,
+                items_by_leader_view_id: Default::default(),
+            },
+        );
+        cx.notify();
 
-    //         Some(cx.spawn(|this, mut cx| async move {
-    //             let response = request.await?;
-    //             this.update(&mut cx, |this, _| {
-    //                 let state = this
-    //                     .follower_states
-    //                     .get_mut(&pane)
-    //                     .ok_or_else(|| anyhow!("following interrupted"))?;
-    //                 state.active_view_id = if let Some(active_view_id) = response.active_view_id {
-    //                     Some(ViewId::from_proto(active_view_id)?)
-    //                 } else {
-    //                     None
-    //                 };
-    //                 Ok::<_, anyhow::Error>(())
-    //             })??;
-    //             Self::add_views_from_leader(
-    //                 this.clone(),
-    //                 leader_id,
-    //                 vec![pane],
-    //                 response.views,
-    //                 &mut cx,
-    //             )
-    //             .await?;
-    //             this.update(&mut cx, |this, cx| this.leader_updated(leader_id, cx))?;
-    //             Ok(())
-    //         }))
-    //     }
+        let room_id = self.active_call()?.read(cx).room()?.read(cx).id();
+        let project_id = self.project.read(cx).remote_id();
+        let request = self.app_state.client.request(proto::Follow {
+            room_id,
+            project_id,
+            leader_id: Some(leader_id),
+        });
+
+        Some(cx.spawn(|this, mut cx| async move {
+            let response = request.await?;
+            this.update(&mut cx, |this, _| {
+                let state = this
+                    .follower_states
+                    .get_mut(&pane)
+                    .ok_or_else(|| anyhow!("following interrupted"))?;
+                state.active_view_id = if let Some(active_view_id) = response.active_view_id {
+                    Some(ViewId::from_proto(active_view_id)?)
+                } else {
+                    None
+                };
+                Ok::<_, anyhow::Error>(())
+            })??;
+            Self::add_views_from_leader(
+                this.clone(),
+                leader_id,
+                vec![pane],
+                response.views,
+                &mut cx,
+            )
+            .await?;
+            this.update(&mut cx, |this, cx| this.leader_updated(leader_id, cx))?;
+            Ok(())
+        }))
+    }
 
     //     pub fn follow_next_collaborator(
     //         &mut self,
@@ -2462,67 +2362,67 @@ impl Workspace {
     //         self.follow(leader_id, cx)
     //     }
 
-    //     pub fn follow(
-    //         &mut self,
-    //         leader_id: PeerId,
-    //         cx: &mut ViewContext<Self>,
-    //     ) -> Option<Task<Result<()>>> {
-    //         let room = ActiveCall::global(cx).read(cx).room()?.read(cx);
-    //         let project = self.project.read(cx);
+    pub fn follow(
+        &mut self,
+        leader_id: PeerId,
+        cx: &mut ViewContext<Self>,
+    ) -> Option<Task<Result<()>>> {
+        let room = ActiveCall::global(cx).read(cx).room()?.read(cx);
+        let project = self.project.read(cx);
 
-    //         let Some(remote_participant) = room.remote_participant_for_peer_id(leader_id) else {
-    //             return None;
-    //         };
+        let Some(remote_participant) = room.remote_participant_for_peer_id(leader_id) else {
+            return None;
+        };
 
-    //         let other_project_id = match remote_participant.location {
-    //             call::ParticipantLocation::External => None,
-    //             call::ParticipantLocation::UnsharedProject => None,
-    //             call::ParticipantLocation::SharedProject { project_id } => {
-    //                 if Some(project_id) == project.remote_id() {
-    //                     None
-    //                 } else {
-    //                     Some(project_id)
-    //                 }
-    //             }
-    //         };
+        let other_project_id = match remote_participant.location {
+            call::ParticipantLocation::External => None,
+            call::ParticipantLocation::UnsharedProject => None,
+            call::ParticipantLocation::SharedProject { project_id } => {
+                if Some(project_id) == project.remote_id() {
+                    None
+                } else {
+                    Some(project_id)
+                }
+            }
+        };
 
-    //         // if they are active in another project, follow there.
-    //         if let Some(project_id) = other_project_id {
-    //             let app_state = self.app_state.clone();
-    //             return Some(crate::join_remote_project(
-    //                 project_id,
-    //                 remote_participant.user.id,
-    //                 app_state,
-    //                 cx,
-    //             ));
-    //         }
+        // if they are active in another project, follow there.
+        if let Some(project_id) = other_project_id {
+            let app_state = self.app_state.clone();
+            return Some(crate::join_remote_project(
+                project_id,
+                remote_participant.user.id,
+                app_state,
+                cx,
+            ));
+        }
 
-    //         // if you're already following, find the right pane and focus it.
-    //         for (pane, state) in &self.follower_states {
-    //             if leader_id == state.leader_id {
-    //                 cx.focus(pane);
-    //                 return None;
-    //             }
-    //         }
+        // if you're already following, find the right pane and focus it.
+        for (pane, state) in &self.follower_states {
+            if leader_id == state.leader_id {
+                cx.focus_view(pane);
+                return None;
+            }
+        }
 
-    //         // Otherwise, follow.
-    //         self.start_following(leader_id, cx)
-    //     }
+        // Otherwise, follow.
+        self.start_following(leader_id, cx)
+    }
 
     pub fn unfollow(&mut self, pane: &View<Pane>, cx: &mut ViewContext<Self>) -> Option<PeerId> {
-        let follower_states = &mut self.follower_states;
-        let state = follower_states.remove(pane)?;
+        let state = self.follower_states.remove(pane)?;
         let leader_id = state.leader_id;
         for (_, item) in state.items_by_leader_view_id {
             item.set_leader_peer_id(None, cx);
         }
 
-        if follower_states
+        if self
+            .follower_states
             .values()
             .all(|state| state.leader_id != state.leader_id)
         {
             let project_id = self.project.read(cx).remote_id();
-            let room_id = self.call_handler.room_id(cx)?;
+            let room_id = self.active_call()?.read(cx).room()?.read(cx).id();
             self.app_state
                 .client
                 .send(proto::Unfollow {
@@ -2657,57 +2557,55 @@ impl Workspace {
         }
     }
 
-    //     // RPC handlers
+    // RPC handlers
 
     fn handle_follow(
         &mut self,
-        _follower_project_id: Option<u64>,
-        _cx: &mut ViewContext<Self>,
+        follower_project_id: Option<u64>,
+        cx: &mut ViewContext<Self>,
     ) -> proto::FollowResponse {
-        todo!()
+        let client = &self.app_state.client;
+        let project_id = self.project.read(cx).remote_id();
 
-        //     let client = &self.app_state.client;
-        //     let project_id = self.project.read(cx).remote_id();
+        let active_view_id = self.active_item(cx).and_then(|i| {
+            Some(
+                i.to_followable_item_handle(cx)?
+                    .remote_id(client, cx)?
+                    .to_proto(),
+            )
+        });
 
-        //     let active_view_id = self.active_item(cx).and_then(|i| {
-        //         Some(
-        //             i.to_followable_item_handle(cx)?
-        //                 .remote_id(client, cx)?
-        //                 .to_proto(),
-        //         )
-        //     });
+        cx.notify();
 
-        //     cx.notify();
-
-        //     self.last_active_view_id = active_view_id.clone();
-        //     proto::FollowResponse {
-        //         active_view_id,
-        //         views: self
-        //             .panes()
-        //             .iter()
-        //             .flat_map(|pane| {
-        //                 let leader_id = self.leader_for_pane(pane);
-        //                 pane.read(cx).items().filter_map({
-        //                     let cx = &cx;
-        //                     move |item| {
-        //                         let item = item.to_followable_item_handle(cx)?;
-        //                         if (project_id.is_none() || project_id != follower_project_id)
-        //                             && item.is_project_item(cx)
-        //                         {
-        //                             return None;
-        //                         }
-        //                         let id = item.remote_id(client, cx)?.to_proto();
-        //                         let variant = item.to_state_proto(cx)?;
-        //                         Some(proto::View {
-        //                             id: Some(id),
-        //                             leader_id,
-        //                             variant: Some(variant),
-        //                         })
-        //                     }
-        //                 })
-        //             })
-        //             .collect(),
-        //     }
+        self.last_active_view_id = active_view_id.clone();
+        proto::FollowResponse {
+            active_view_id,
+            views: self
+                .panes()
+                .iter()
+                .flat_map(|pane| {
+                    let leader_id = self.leader_for_pane(pane);
+                    pane.read(cx).items().filter_map({
+                        let cx = &cx;
+                        move |item| {
+                            let item = item.to_followable_item_handle(cx)?;
+                            if (project_id.is_none() || project_id != follower_project_id)
+                                && item.is_project_item(cx)
+                            {
+                                return None;
+                            }
+                            let id = item.remote_id(client, cx)?.to_proto();
+                            let variant = item.to_state_proto(cx)?;
+                            Some(proto::View {
+                                id: Some(id),
+                                leader_id,
+                                variant: Some(variant),
+                            })
+                        }
+                    })
+                })
+                .collect(),
+        }
     }
 
     fn handle_update_followers(
@@ -2727,6 +2625,8 @@ impl Workspace {
         update: proto::UpdateFollowers,
         cx: &mut AsyncWindowContext,
     ) -> Result<()> {
+        dbg!("process_leader_update", &update);
+
         match update.variant.ok_or_else(|| anyhow!("invalid update"))? {
             proto::update_followers::Variant::UpdateActiveView(update_active_view) => {
                 this.update(cx, |this, _| {
@@ -2878,9 +2778,8 @@ impl Workspace {
         } else {
             None
         };
-        let room_id = self.call_handler.room_id(cx)?;
         self.app_state().workspace_store.update(cx, |store, cx| {
-            store.update_followers(project_id, room_id, update, cx)
+            store.update_followers(project_id, update, cx)
         })
     }
 
@@ -2888,12 +2787,31 @@ impl Workspace {
         self.follower_states.get(pane).map(|state| state.leader_id)
     }
 
-    pub fn leader_updated(&mut self, leader_id: PeerId, cx: &mut ViewContext<Self>) -> Option<()> {
+    fn leader_updated(&mut self, leader_id: PeerId, cx: &mut ViewContext<Self>) -> Option<()> {
         cx.notify();
 
-        let (leader_in_this_project, leader_in_this_app) =
-            self.call_handler.peer_state(leader_id, &self.project, cx)?;
+        let call = self.active_call()?;
+        let room = call.read(cx).room()?.read(cx);
+        let participant = room.remote_participant_for_peer_id(leader_id)?;
         let mut items_to_activate = Vec::new();
+
+        let leader_in_this_app;
+        let leader_in_this_project;
+        match participant.location {
+            call::ParticipantLocation::SharedProject { project_id } => {
+                leader_in_this_app = true;
+                leader_in_this_project = Some(project_id) == self.project.read(cx).remote_id();
+            }
+            call::ParticipantLocation::UnsharedProject => {
+                leader_in_this_app = true;
+                leader_in_this_project = false;
+            }
+            call::ParticipantLocation::External => {
+                leader_in_this_app = false;
+                leader_in_this_project = false;
+            }
+        };
+
         for (pane, state) in &self.follower_states {
             if state.leader_id != leader_id {
                 continue;
@@ -2914,7 +2832,7 @@ impl Workspace {
             }
 
             if let Some(shared_screen) = self.shared_screen_for_peer(leader_id, pane, cx) {
-                items_to_activate.push((pane.clone(), shared_screen));
+                items_to_activate.push((pane.clone(), Box::new(shared_screen)));
             }
         }
 
@@ -2923,8 +2841,8 @@ impl Workspace {
             if let Some(index) = pane.update(cx, |pane, _| pane.index_for_item(item.as_ref())) {
                 pane.update(cx, |pane, cx| pane.activate_item(index, false, false, cx));
             } else {
-                pane.update(cx, |pane, mut cx| {
-                    pane.add_item(item.boxed_clone(), false, false, None, &mut cx)
+                pane.update(cx, |pane, cx| {
+                    pane.add_item(item.boxed_clone(), false, false, None, cx)
                 });
             }
 
@@ -2941,21 +2859,20 @@ impl Workspace {
         peer_id: PeerId,
         pane: &View<Pane>,
         cx: &mut ViewContext<Self>,
-    ) -> Option<Box<dyn ItemHandle>> {
-        self.call_handler.shared_screen_for_peer(peer_id, pane, cx)
-        // let call = self.active_call()?;
-        // let room = call.read(cx).room()?.read(cx);
-        // let participant = room.remote_participant_for_peer_id(peer_id)?;
-        // let track = participant.video_tracks.values().next()?.clone();
-        // let user = participant.user.clone();
-
-        // for item in pane.read(cx).items_of_type::<SharedScreen>() {
-        //     if item.read(cx).peer_id == peer_id {
-        //         return Some(item);
-        //     }
-        // }
+    ) -> Option<View<SharedScreen>> {
+        let call = self.active_call()?;
+        let room = call.read(cx).room()?.read(cx);
+        let participant = room.remote_participant_for_peer_id(peer_id)?;
+        let track = participant.video_tracks.values().next()?.clone();
+        let user = participant.user.clone();
+
+        for item in pane.read(cx).items_of_type::<SharedScreen>() {
+            if item.read(cx).peer_id == peer_id {
+                return Some(item);
+            }
+        }
 
-        // Some(cx.build_view(|cx| SharedScreen::new(&track, peer_id, user.clone(), cx)))
+        Some(cx.build_view(|cx| SharedScreen::new(&track, peer_id, user.clone(), cx)))
     }
 
     pub fn on_window_activation_changed(&mut self, cx: &mut ViewContext<Self>) {
@@ -2984,6 +2901,25 @@ impl Workspace {
         }
     }
 
+    fn active_call(&self) -> Option<&Model<ActiveCall>> {
+        self.active_call.as_ref().map(|(call, _)| call)
+    }
+
+    fn on_active_call_event(
+        &mut self,
+        _: Model<ActiveCall>,
+        event: &call::room::Event,
+        cx: &mut ViewContext<Self>,
+    ) {
+        match event {
+            call::room::Event::ParticipantLocationChanged { participant_id }
+            | call::room::Event::RemoteVideoTracksChanged { participant_id } => {
+                self.leader_updated(*participant_id, cx);
+            }
+            _ => {}
+        }
+    }
+
     pub fn database_id(&self) -> WorkspaceId {
         self.database_id
     }
@@ -3393,7 +3329,6 @@ impl Workspace {
             fs: project.read(cx).fs().clone(),
             build_window_options: |_, _, _| Default::default(),
             node_runtime: FakeNodeRuntime::new(),
-            call_factory: |_| Box::new(TestCallHandler),
         });
         let workspace = Self::new(0, project, app_state, cx);
         workspace.active_pane.update(cx, |pane, cx| pane.focus(cx));
@@ -3472,10 +3407,6 @@ impl Workspace {
         self.modal_layer
             .update(cx, |modal_layer, cx| modal_layer.toggle_modal(cx, build))
     }
-
-    pub fn call_state(&mut self) -> &mut dyn CallHandler {
-        &mut *self.call_handler
-    }
 }
 
 fn window_bounds_env_override(cx: &AsyncAppContext) -> Option<WindowBounds> {
@@ -3676,6 +3607,7 @@ impl Render for Workspace {
                                     .child(self.center.render(
                                         &self.project,
                                         &self.follower_states,
+                                        self.active_call(),
                                         &self.active_pane,
                                         self.zoomed.as_ref(),
                                         &self.app_state,
@@ -3830,15 +3762,15 @@ impl Render for Workspace {
 // }
 
 impl WorkspaceStore {
-    pub fn new(client: Arc<Client>, _cx: &mut ModelContext<Self>) -> Self {
+    pub fn new(client: Arc<Client>, cx: &mut ModelContext<Self>) -> Self {
         Self {
             workspaces: Default::default(),
             followers: Default::default(),
-            _subscriptions: vec![],
-            //     client.add_request_handler(cx.weak_model(), Self::handle_follow),
-            //     client.add_message_handler(cx.weak_model(), Self::handle_unfollow),
-            //     client.add_message_handler(cx.weak_model(), Self::handle_update_followers),
-            // ],
+            _subscriptions: vec![
+                client.add_request_handler(cx.weak_model(), Self::handle_follow),
+                client.add_message_handler(cx.weak_model(), Self::handle_unfollow),
+                client.add_message_handler(cx.weak_model(), Self::handle_update_followers),
+            ],
             client,
         }
     }
@@ -3846,10 +3778,14 @@ impl WorkspaceStore {
     pub fn update_followers(
         &self,
         project_id: Option<u64>,
-        room_id: u64,
         update: proto::update_followers::Variant,
         cx: &AppContext,
     ) -> Option<()> {
+        if !cx.has_global::<Model<ActiveCall>>() {
+            return None;
+        }
+
+        let room_id = ActiveCall::global(cx).read(cx).room()?.read(cx).id();
         let follower_ids: Vec<_> = self
             .followers
             .iter()
@@ -3885,17 +3821,9 @@ impl WorkspaceStore {
                 project_id: envelope.payload.project_id,
                 peer_id: envelope.original_sender_id()?,
             };
+            let active_project = ActiveCall::global(cx).read(cx).location().cloned();
+
             let mut response = proto::FollowResponse::default();
-            let active_project = this
-                .workspaces
-                .iter()
-                .next()
-                .and_then(|workspace| {
-                    workspace
-                        .read_with(cx, |this, cx| this.call_handler.active_project(cx))
-                        .log_err()
-                })
-                .flatten();
             for workspace in &this.workspaces {
                 workspace
                     .update(cx, |workspace, cx| {
@@ -3947,11 +3875,13 @@ impl WorkspaceStore {
         this: Model<Self>,
         envelope: TypedEnvelope<proto::UpdateFollowers>,
         _: Arc<Client>,
-        mut cx: AsyncWindowContext,
+        mut cx: AsyncAppContext,
     ) -> Result<()> {
         let leader_id = envelope.original_sender_id()?;
         let update = envelope.payload;
 
+        dbg!("handle_upate_followers");
+
         this.update(&mut cx, |this, cx| {
             for workspace in &this.workspaces {
                 workspace.update(cx, |workspace, cx| {
@@ -4048,187 +3978,184 @@ pub async fn last_opened_workspace_paths() -> Option<WorkspaceLocation> {
     DB.last_workspace().await.log_err().flatten()
 }
 
-// async fn join_channel_internal(
-//     channel_id: u64,
-//     app_state: &Arc<AppState>,
-//     requesting_window: Option<WindowHandle<Workspace>>,
-//     active_call: &ModelHandle<ActiveCall>,
-//     cx: &mut AsyncAppContext,
-// ) -> Result<bool> {
-//     let (should_prompt, open_room) = active_call.read_with(cx, |active_call, cx| {
-//         let Some(room) = active_call.room().map(|room| room.read(cx)) else {
-//             return (false, None);
-//         };
-
-//         let already_in_channel = room.channel_id() == Some(channel_id);
-//         let should_prompt = room.is_sharing_project()
-//             && room.remote_participants().len() > 0
-//             && !already_in_channel;
-//         let open_room = if already_in_channel {
-//             active_call.room().cloned()
-//         } else {
-//             None
-//         };
-//         (should_prompt, open_room)
-//     });
-
-//     if let Some(room) = open_room {
-//         let task = room.update(cx, |room, cx| {
-//             if let Some((project, host)) = room.most_active_project(cx) {
-//                 return Some(join_remote_project(project, host, app_state.clone(), cx));
-//             }
-
-//             None
-//         });
-//         if let Some(task) = task {
-//             task.await?;
-//         }
-//         return anyhow::Ok(true);
-//     }
+async fn join_channel_internal(
+    channel_id: u64,
+    app_state: &Arc<AppState>,
+    requesting_window: Option<WindowHandle<Workspace>>,
+    active_call: &Model<ActiveCall>,
+    cx: &mut AsyncAppContext,
+) -> Result<bool> {
+    let (should_prompt, open_room) = active_call.read_with(cx, |active_call, cx| {
+        let Some(room) = active_call.room().map(|room| room.read(cx)) else {
+            return (false, None);
+        };
 
-//     if should_prompt {
-//         if let Some(workspace) = requesting_window {
-//             if let Some(window) = workspace.update(cx, |cx| cx.window()) {
-//                 let answer = window.prompt(
-//                     PromptLevel::Warning,
-//                     "Leaving this call will unshare your current project.\nDo you want to switch channels?",
-//                     &["Yes, Join Channel", "Cancel"],
-//                     cx,
-//                 );
-
-//                 if let Some(mut answer) = answer {
-//                     if answer.next().await == Some(1) {
-//                         return Ok(false);
-//                     }
-//                 }
-//             } else {
-//                 return Ok(false); // unreachable!() hopefully
-//             }
-//         } else {
-//             return Ok(false); // unreachable!() hopefully
-//         }
-//     }
+        let already_in_channel = room.channel_id() == Some(channel_id);
+        let should_prompt = room.is_sharing_project()
+            && room.remote_participants().len() > 0
+            && !already_in_channel;
+        let open_room = if already_in_channel {
+            active_call.room().cloned()
+        } else {
+            None
+        };
+        (should_prompt, open_room)
+    })?;
 
-//     let client = cx.read(|cx| active_call.read(cx).client());
-
-//     let mut client_status = client.status();
-
-//     // this loop will terminate within client::CONNECTION_TIMEOUT seconds.
-//     'outer: loop {
-//         let Some(status) = client_status.recv().await else {
-//             return Err(anyhow!("error connecting"));
-//         };
-
-//         match status {
-//             Status::Connecting
-//             | Status::Authenticating
-//             | Status::Reconnecting
-//             | Status::Reauthenticating => continue,
-//             Status::Connected { .. } => break 'outer,
-//             Status::SignedOut => return Err(anyhow!("not signed in")),
-//             Status::UpgradeRequired => return Err(anyhow!("zed is out of date")),
-//             Status::ConnectionError | Status::ConnectionLost | Status::ReconnectionError { .. } => {
-//                 return Err(anyhow!("zed is offline"))
-//             }
-//         }
-//     }
+    if let Some(room) = open_room {
+        let task = room.update(cx, |room, cx| {
+            if let Some((project, host)) = room.most_active_project(cx) {
+                return Some(join_remote_project(project, host, app_state.clone(), cx));
+            }
 
-//     let room = active_call
-//         .update(cx, |active_call, cx| {
-//             active_call.join_channel(channel_id, cx)
-//         })
-//         .await?;
-
-//     room.update(cx, |room, _| room.room_update_completed())
-//         .await;
-
-//     let task = room.update(cx, |room, cx| {
-//         if let Some((project, host)) = room.most_active_project(cx) {
-//             return Some(join_remote_project(project, host, app_state.clone(), cx));
-//         }
-
-//         None
-//     });
-//     if let Some(task) = task {
-//         task.await?;
-//         return anyhow::Ok(true);
-//     }
-//     anyhow::Ok(false)
-// }
+            None
+        })?;
+        if let Some(task) = task {
+            task.await?;
+        }
+        return anyhow::Ok(true);
+    }
 
-// pub fn join_channel(
-//     channel_id: u64,
-//     app_state: Arc<AppState>,
-//     requesting_window: Option<WindowHandle<Workspace>>,
-//     cx: &mut AppContext,
-// ) -> Task<Result<()>> {
-//     let active_call = ActiveCall::global(cx);
-//     cx.spawn(|mut cx| async move {
-//         let result = join_channel_internal(
-//             channel_id,
-//             &app_state,
-//             requesting_window,
-//             &active_call,
-//             &mut cx,
-//         )
-//         .await;
-
-//         // join channel succeeded, and opened a window
-//         if matches!(result, Ok(true)) {
-//             return anyhow::Ok(());
-//         }
-
-//         if requesting_window.is_some() {
-//             return anyhow::Ok(());
-//         }
-
-//         // find an existing workspace to focus and show call controls
-//         let mut active_window = activate_any_workspace_window(&mut cx);
-//         if active_window.is_none() {
-//             // no open workspaces, make one to show the error in (blergh)
-//             cx.update(|cx| Workspace::new_local(vec![], app_state.clone(), requesting_window, cx))
-//                 .await;
-//         }
-
-//         active_window = activate_any_workspace_window(&mut cx);
-//         if active_window.is_none() {
-//             return result.map(|_| ()); // unreachable!() assuming new_local always opens a window
-//         }
-
-//         if let Err(err) = result {
-//             let prompt = active_window.unwrap().prompt(
-//                 PromptLevel::Critical,
-//                 &format!("Failed to join channel: {}", err),
-//                 &["Ok"],
-//                 &mut cx,
-//             );
-//             if let Some(mut prompt) = prompt {
-//                 prompt.next().await;
-//             } else {
-//                 return Err(err);
-//             }
-//         }
-
-//         // return ok, we showed the error to the user.
-//         return anyhow::Ok(());
-//     })
-// }
+    if should_prompt {
+        if let Some(workspace) = requesting_window {
+            let answer  = workspace.update(cx, |_, cx| {
+                cx.prompt(
+                    PromptLevel::Warning,
+                    "Leaving this call will unshare your current project.\nDo you want to switch channels?",
+                    &["Yes, Join Channel", "Cancel"],
+                )
+            })?.await;
 
-// pub fn activate_any_workspace_window(cx: &mut AsyncAppContext) -> Option<AnyWindowHandle> {
-//     for window in cx.windows() {
-//         let found = window.update(cx, |cx| {
-//             let is_workspace = cx.root_view().clone().downcast::<Workspace>().is_some();
-//             if is_workspace {
-//                 cx.activate_window();
-//             }
-//             is_workspace
-//         });
-//         if found == Some(true) {
-//             return Some(window);
-//         }
-//     }
-//     None
-// }
+            if answer == Ok(1) {
+                return Ok(false);
+            }
+        } else {
+            return Ok(false); // unreachable!() hopefully
+        }
+    }
+
+    let client = cx.update(|cx| active_call.read(cx).client())?;
+
+    let mut client_status = client.status();
+
+    // this loop will terminate within client::CONNECTION_TIMEOUT seconds.
+    'outer: loop {
+        let Some(status) = client_status.recv().await else {
+            return Err(anyhow!("error connecting"));
+        };
+
+        match status {
+            Status::Connecting
+            | Status::Authenticating
+            | Status::Reconnecting
+            | Status::Reauthenticating => continue,
+            Status::Connected { .. } => break 'outer,
+            Status::SignedOut => return Err(anyhow!("not signed in")),
+            Status::UpgradeRequired => return Err(anyhow!("zed is out of date")),
+            Status::ConnectionError | Status::ConnectionLost | Status::ReconnectionError { .. } => {
+                return Err(anyhow!("zed is offline"))
+            }
+        }
+    }
+
+    let room = active_call
+        .update(cx, |active_call, cx| {
+            active_call.join_channel(channel_id, cx)
+        })?
+        .await?;
+
+    let Some(room) = room else {
+        return anyhow::Ok(true);
+    };
+
+    room.update(cx, |room, _| room.room_update_completed())?
+        .await;
+
+    let task = room.update(cx, |room, cx| {
+        if let Some((project, host)) = room.most_active_project(cx) {
+            return Some(join_remote_project(project, host, app_state.clone(), cx));
+        }
+
+        None
+    })?;
+    if let Some(task) = task {
+        task.await?;
+        return anyhow::Ok(true);
+    }
+    anyhow::Ok(false)
+}
+
+pub fn join_channel(
+    channel_id: u64,
+    app_state: Arc<AppState>,
+    requesting_window: Option<WindowHandle<Workspace>>,
+    cx: &mut AppContext,
+) -> Task<Result<()>> {
+    let active_call = ActiveCall::global(cx);
+    cx.spawn(|mut cx| async move {
+        let result = join_channel_internal(
+            channel_id,
+            &app_state,
+            requesting_window,
+            &active_call,
+            &mut cx,
+        )
+        .await;
+
+        // join channel succeeded, and opened a window
+        if matches!(result, Ok(true)) {
+            return anyhow::Ok(());
+        }
+
+        if requesting_window.is_some() {
+            return anyhow::Ok(());
+        }
+
+        // find an existing workspace to focus and show call controls
+        let mut active_window = activate_any_workspace_window(&mut cx);
+        if active_window.is_none() {
+            // no open workspaces, make one to show the error in (blergh)
+            cx.update(|cx| Workspace::new_local(vec![], app_state.clone(), requesting_window, cx))?
+                .await?;
+        }
+
+        active_window = activate_any_workspace_window(&mut cx);
+        let Some(active_window) = active_window else {
+            return anyhow::Ok(());
+        };
+
+        if let Err(err) = result {
+            active_window
+                .update(&mut cx, |_, cx| {
+                    cx.prompt(
+                        PromptLevel::Critical,
+                        &format!("Failed to join channel: {}", err),
+                        &["Ok"],
+                    )
+                })?
+                .await
+                .ok();
+        }
+
+        // return ok, we showed the error to the user.
+        return anyhow::Ok(());
+    })
+}
+
+pub fn activate_any_workspace_window(cx: &mut AsyncAppContext) -> Option<AnyWindowHandle> {
+    cx.update(|cx| {
+        for window in cx.windows() {
+            let is_workspace = window.downcast::<Workspace>().is_some();
+            if is_workspace {
+                window.update(cx, |_, cx| cx.activate_window()).ok();
+                return Some(window);
+            }
+        }
+        None
+    })
+    .ok()
+    .flatten()
+}
 
 #[allow(clippy::type_complexity)]
 pub fn open_paths(

crates/zed2/src/main.rs 🔗

@@ -191,7 +191,6 @@ fn main() {
             user_store: user_store.clone(),
             fs,
             build_window_options,
-            call_factory: call::Call::new,
             workspace_store,
             node_runtime,
         });