revert single channel click (#7738)

Conrad Irwin created

- Revert "collab tweaks (#7706)"
- Revert "2112 (#7640)"
- Revert "single click channel (#7596)"
- Reserve protobufs
- Don't revert migrations

Release Notes:

- N/A

**or**

- N/A

Change summary

assets/settings/default.json                    |   6 
crates/call/src/call.rs                         |  21 
crates/call/src/call_settings.rs                |   6 
crates/call/src/participant.rs                  |   1 
crates/call/src/room.rs                         | 360 +++++++++---------
crates/collab/src/db/queries/channels.rs        |  48 --
crates/collab/src/db/queries/rooms.rs           |   6 
crates/collab/src/db/tables/room_participant.rs |   1 
crates/collab/src/db/tests/channel_tests.rs     |  13 
crates/collab/src/rpc.rs                        | 159 +------
crates/collab/src/tests/channel_guest_tests.rs  |  13 
crates/collab/src/tests/channel_tests.rs        |   8 
crates/collab/src/tests/following_tests.rs      |   6 
crates/collab/src/tests/integration_tests.rs    |  44 ++
crates/collab/src/tests/test_server.rs          |  10 
crates/collab_ui/src/collab_panel.rs            | 316 ++++++----------
crates/collab_ui/src/collab_titlebar_item.rs    |  77 ++-
crates/collab_ui/src/collab_ui.rs               |  11 
crates/editor/src/display_map.rs                |   2 
crates/live_kit_client/src/test.rs              |  24 
crates/rpc/proto/buf.yaml                       |   4 
crates/rpc/proto/zed.proto                      |  25 -
crates/rpc/src/proto.rs                         |   7 
crates/workspace/src/pane.rs                    |   1 
crates/workspace/src/workspace.rs               |  55 ++
crates/zed/src/main.rs                          |   4 
26 files changed, 522 insertions(+), 706 deletions(-)

Detailed changes

assets/settings/default.json 🔗

@@ -104,8 +104,10 @@
   "show_whitespaces": "selection",
   // Settings related to calls in Zed
   "calls": {
-    // Join calls with the microphone muted by default
-    "mute_on_join": false
+    // Join calls with the microphone live by default
+    "mute_on_join": false,
+    // Share your project when you are the first to join a channel
+    "share_on_join": true
   },
   // Toolbar related settings
   "toolbar": {

crates/call/src/call.rs 🔗

@@ -84,7 +84,6 @@ pub struct ActiveCall {
     ),
     client: Arc<Client>,
     user_store: Model<UserStore>,
-    pending_channel_id: Option<u64>,
     _subscriptions: Vec<client::Subscription>,
 }
 
@@ -98,7 +97,6 @@ impl ActiveCall {
             location: None,
             pending_invites: Default::default(),
             incoming_call: watch::channel(),
-            pending_channel_id: None,
             _join_debouncer: OneAtATime { cancel: None },
             _subscriptions: vec![
                 client.add_request_handler(cx.weak_model(), Self::handle_incoming_call),
@@ -113,10 +111,6 @@ impl ActiveCall {
         self.room()?.read(cx).channel_id()
     }
 
-    pub fn pending_channel_id(&self) -> Option<u64> {
-        self.pending_channel_id
-    }
-
     async fn handle_incoming_call(
         this: Model<Self>,
         envelope: TypedEnvelope<proto::IncomingCall>,
@@ -345,13 +339,11 @@ impl ActiveCall {
         channel_id: u64,
         cx: &mut ModelContext<Self>,
     ) -> Task<Result<Option<Model<Room>>>> {
-        let mut leave = None;
         if let Some(room) = self.room().cloned() {
             if room.read(cx).channel_id() == Some(channel_id) {
                 return Task::ready(Ok(Some(room)));
             } else {
-                let (room, _) = self.room.take().unwrap();
-                leave = room.update(cx, |room, cx| Some(room.leave(cx)));
+                room.update(cx, |room, cx| room.clear_state(cx));
             }
         }
 
@@ -361,21 +353,14 @@ impl ActiveCall {
 
         let client = self.client.clone();
         let user_store = self.user_store.clone();
-        self.pending_channel_id = Some(channel_id);
         let join = self._join_debouncer.spawn(cx, move |cx| async move {
-            if let Some(task) = leave {
-                task.await?
-            }
             Room::join_channel(channel_id, client, user_store, cx).await
         });
 
         cx.spawn(|this, mut cx| async move {
             let room = join.await?;
-            this.update(&mut cx, |this, cx| {
-                this.pending_channel_id.take();
-                this.set_room(room.clone(), cx)
-            })?
-            .await?;
+            this.update(&mut cx, |this, cx| this.set_room(room.clone(), cx))?
+                .await?;
             this.update(&mut cx, |this, cx| {
                 this.report_call_event("join channel", cx)
             })?;

crates/call/src/call_settings.rs 🔗

@@ -7,6 +7,7 @@ use settings::Settings;
 #[derive(Deserialize, Debug)]
 pub struct CallSettings {
     pub mute_on_join: bool,
+    pub share_on_join: bool,
 }
 
 /// Configuration of voice calls in Zed.
@@ -16,6 +17,11 @@ pub struct CallSettingsContent {
     ///
     /// Default: false
     pub mute_on_join: Option<bool>,
+
+    /// Whether your current project should be shared when joining an empty channel.
+    ///
+    /// Default: true
+    pub share_on_join: Option<bool>,
 }
 
 impl Settings for CallSettings {

crates/call/src/participant.rs 🔗

@@ -49,7 +49,6 @@ pub struct RemoteParticipant {
     pub participant_index: ParticipantIndex,
     pub muted: bool,
     pub speaking: bool,
-    pub in_call: bool,
     pub video_tracks: HashMap<live_kit_client::Sid, Arc<RemoteVideoTrack>>,
     pub audio_tracks: HashMap<live_kit_client::Sid, Arc<RemoteAudioTrack>>,
 }

crates/call/src/room.rs 🔗

@@ -61,7 +61,6 @@ pub struct Room {
     id: u64,
     channel_id: Option<u64>,
     live_kit: Option<LiveKitRoom>,
-    live_kit_connection_info: Option<proto::LiveKitConnectionInfo>,
     status: RoomStatus,
     shared_projects: HashSet<WeakModel<Project>>,
     joined_projects: HashSet<WeakModel<Project>>,
@@ -113,18 +112,91 @@ impl Room {
         user_store: Model<UserStore>,
         cx: &mut ModelContext<Self>,
     ) -> Self {
+        let live_kit_room = if let Some(connection_info) = live_kit_connection_info {
+            let room = live_kit_client::Room::new();
+            let mut status = room.status();
+            // Consume the initial status of the room.
+            let _ = status.try_recv();
+            let _maintain_room = cx.spawn(|this, mut cx| async move {
+                while let Some(status) = status.next().await {
+                    let this = if let Some(this) = this.upgrade() {
+                        this
+                    } else {
+                        break;
+                    };
+
+                    if status == live_kit_client::ConnectionState::Disconnected {
+                        this.update(&mut cx, |this, cx| this.leave(cx).log_err())
+                            .ok();
+                        break;
+                    }
+                }
+            });
+
+            let _handle_updates = cx.spawn({
+                let room = room.clone();
+                move |this, mut cx| async move {
+                    let mut updates = room.updates();
+                    while let Some(update) = updates.next().await {
+                        let this = if let Some(this) = this.upgrade() {
+                            this
+                        } else {
+                            break;
+                        };
+
+                        this.update(&mut cx, |this, cx| {
+                            this.live_kit_room_updated(update, cx).log_err()
+                        })
+                        .ok();
+                    }
+                }
+            });
+
+            let connect = room.connect(&connection_info.server_url, &connection_info.token);
+            cx.spawn(|this, mut cx| async move {
+                connect.await?;
+                this.update(&mut cx, |this, cx| {
+                    if !this.read_only() {
+                        if let Some(live_kit) = &this.live_kit {
+                            if !live_kit.muted_by_user && !live_kit.deafened {
+                                return this.share_microphone(cx);
+                            }
+                        }
+                    }
+                    Task::ready(Ok(()))
+                })?
+                .await
+            })
+            .detach_and_log_err(cx);
+
+            Some(LiveKitRoom {
+                room,
+                screen_track: LocalTrack::None,
+                microphone_track: LocalTrack::None,
+                next_publish_id: 0,
+                muted_by_user: Self::mute_on_join(cx),
+                deafened: false,
+                speaking: false,
+                _maintain_room,
+                _handle_updates,
+            })
+        } else {
+            None
+        };
+
         let maintain_connection = cx.spawn({
             let client = client.clone();
             move |this, cx| Self::maintain_connection(this, client.clone(), cx).log_err()
         });
 
+        Audio::play_sound(Sound::Joined, cx);
+
         let (room_update_completed_tx, room_update_completed_rx) = watch::channel();
 
-        let mut this = Self {
+        Self {
             id,
             channel_id,
-            live_kit: None,
-            live_kit_connection_info,
+            live_kit: live_kit_room,
             status: RoomStatus::Online,
             shared_projects: Default::default(),
             joined_projects: Default::default(),
@@ -148,11 +220,7 @@ impl Room {
             maintain_connection: Some(maintain_connection),
             room_update_completed_tx,
             room_update_completed_rx,
-        };
-        if this.live_kit_connection_info.is_some() {
-            this.join_call(cx).detach_and_log_err(cx);
         }
-        this
     }
 
     pub(crate) fn create(
@@ -211,7 +279,7 @@ impl Room {
         cx: AsyncAppContext,
     ) -> Result<Model<Self>> {
         Self::from_join_response(
-            client.request(proto::JoinChannel2 { channel_id }).await?,
+            client.request(proto::JoinChannel { channel_id }).await?,
             client,
             user_store,
             cx,
@@ -256,7 +324,7 @@ impl Room {
     }
 
     pub fn mute_on_join(cx: &AppContext) -> bool {
-        CallSettings::get_global(cx).mute_on_join
+        CallSettings::get_global(cx).mute_on_join || client::IMPERSONATE_LOGIN.is_some()
     }
 
     fn from_join_response(
@@ -306,9 +374,7 @@ impl Room {
         }
 
         log::info!("leaving room");
-        if self.live_kit.is_some() {
-            Audio::play_sound(Sound::Leave, cx);
-        }
+        Audio::play_sound(Sound::Leave, cx);
 
         self.clear_state(cx);
 
@@ -527,24 +593,6 @@ impl Room {
         &self.remote_participants
     }
 
-    pub fn call_participants(&self, cx: &AppContext) -> Vec<Arc<User>> {
-        self.remote_participants()
-            .values()
-            .filter_map(|participant| {
-                if participant.in_call {
-                    Some(participant.user.clone())
-                } else {
-                    None
-                }
-            })
-            .chain(if self.in_call() {
-                self.user_store.read(cx).current_user()
-            } else {
-                None
-            })
-            .collect()
-    }
-
     pub fn remote_participant_for_peer_id(&self, peer_id: PeerId) -> Option<&RemoteParticipant> {
         self.remote_participants
             .values()
@@ -569,6 +617,10 @@ impl Room {
         self.local_participant.role == proto::ChannelRole::Admin
     }
 
+    pub fn local_participant_is_guest(&self) -> bool {
+        self.local_participant.role == proto::ChannelRole::Guest
+    }
+
     pub fn set_participant_role(
         &mut self,
         user_id: u64,
@@ -776,7 +828,6 @@ impl Room {
                         }
 
                         let role = participant.role();
-                        let in_call = participant.in_call;
                         let location = ParticipantLocation::from_proto(participant.location)
                             .unwrap_or(ParticipantLocation::External);
                         if let Some(remote_participant) =
@@ -787,15 +838,9 @@ impl Room {
                             remote_participant.participant_index = participant_index;
                             if location != remote_participant.location
                                 || role != remote_participant.role
-                                || in_call != remote_participant.in_call
                             {
-                                if in_call && !remote_participant.in_call {
-                                    Audio::play_sound(Sound::Joined, cx);
-                                }
                                 remote_participant.location = location;
                                 remote_participant.role = role;
-                                remote_participant.in_call = participant.in_call;
-
                                 cx.emit(Event::ParticipantLocationChanged {
                                     participant_id: peer_id,
                                 });
@@ -812,15 +857,12 @@ impl Room {
                                     role,
                                     muted: true,
                                     speaking: false,
-                                    in_call: participant.in_call,
                                     video_tracks: Default::default(),
                                     audio_tracks: Default::default(),
                                 },
                             );
 
-                            if participant.in_call {
-                                Audio::play_sound(Sound::Joined, cx);
-                            }
+                            Audio::play_sound(Sound::Joined, cx);
 
                             if let Some(live_kit) = this.live_kit.as_ref() {
                                 let video_tracks =
@@ -1009,6 +1051,15 @@ impl Room {
             }
 
             RoomUpdate::SubscribedToRemoteAudioTrack(track, publication) => {
+                if let Some(live_kit) = &self.live_kit {
+                    if live_kit.deafened {
+                        track.stop();
+                        cx.foreground_executor()
+                            .spawn(publication.set_enabled(false))
+                            .detach();
+                    }
+                }
+
                 let user_id = track.publisher_id().parse()?;
                 let track_id = track.sid().to_string();
                 let participant = self
@@ -1155,7 +1206,7 @@ impl Room {
         })
     }
 
-    pub(crate) fn share_project(
+    pub fn share_project(
         &mut self,
         project: Model<Project>,
         cx: &mut ModelContext<Self>,
@@ -1257,14 +1308,18 @@ impl Room {
         })
     }
 
+    pub fn is_sharing_mic(&self) -> bool {
+        self.live_kit.as_ref().map_or(false, |live_kit| {
+            !matches!(live_kit.microphone_track, LocalTrack::None)
+        })
+    }
+
     pub fn is_muted(&self) -> bool {
-        self.live_kit
-            .as_ref()
-            .map_or(true, |live_kit| match &live_kit.microphone_track {
-                LocalTrack::None => true,
-                LocalTrack::Pending { .. } => true,
-                LocalTrack::Published { track_publication } => track_publication.is_muted(),
-            })
+        self.live_kit.as_ref().map_or(false, |live_kit| {
+            matches!(live_kit.microphone_track, LocalTrack::None)
+                || live_kit.muted_by_user
+                || live_kit.deafened
+        })
     }
 
     pub fn read_only(&self) -> bool {
@@ -1278,8 +1333,8 @@ impl Room {
             .map_or(false, |live_kit| live_kit.speaking)
     }
 
-    pub fn in_call(&self) -> bool {
-        self.live_kit.is_some()
+    pub fn is_deafened(&self) -> Option<bool> {
+        self.live_kit.as_ref().map(|live_kit| live_kit.deafened)
     }
 
     #[track_caller]
@@ -1332,8 +1387,12 @@ impl Room {
                         Ok(publication) => {
                             if canceled {
                                 live_kit.room.unpublish_track(publication);
-                                live_kit.microphone_track = LocalTrack::None;
                             } else {
+                                if live_kit.muted_by_user || live_kit.deafened {
+                                    cx.background_executor()
+                                        .spawn(publication.set_mute(true))
+                                        .detach();
+                                }
                                 live_kit.microphone_track = LocalTrack::Published {
                                     track_publication: publication,
                                 };
@@ -1437,140 +1496,50 @@ impl Room {
     }
 
     pub fn toggle_mute(&mut self, cx: &mut ModelContext<Self>) {
-        let muted = !self.is_muted();
-        if let Some(task) = self.set_mute(muted, cx) {
-            task.detach_and_log_err(cx);
-        }
-    }
-
-    pub fn join_call(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
-        if self.live_kit.is_some() {
-            return Task::ready(Ok(()));
-        }
-
-        let room = live_kit_client::Room::new();
-        let mut status = room.status();
-        // Consume the initial status of the room.
-        let _ = status.try_recv();
-        let _maintain_room = cx.spawn(|this, mut cx| async move {
-            while let Some(status) = status.next().await {
-                let this = if let Some(this) = this.upgrade() {
-                    this
-                } else {
-                    break;
-                };
-
-                if status == live_kit_client::ConnectionState::Disconnected {
-                    this.update(&mut cx, |this, cx| this.leave(cx).log_err())
-                        .ok();
-                    break;
-                }
+        if let Some(live_kit) = self.live_kit.as_mut() {
+            // When unmuting, undeafen if the user was deafened before.
+            let was_deafened = live_kit.deafened;
+            if live_kit.muted_by_user
+                || live_kit.deafened
+                || matches!(live_kit.microphone_track, LocalTrack::None)
+            {
+                live_kit.muted_by_user = false;
+                live_kit.deafened = false;
+            } else {
+                live_kit.muted_by_user = true;
             }
-        });
+            let muted = live_kit.muted_by_user;
+            let should_undeafen = was_deafened && !live_kit.deafened;
 
-        let _handle_updates = cx.spawn({
-            let room = room.clone();
-            move |this, mut cx| async move {
-                let mut updates = room.updates();
-                while let Some(update) = updates.next().await {
-                    let this = if let Some(this) = this.upgrade() {
-                        this
-                    } else {
-                        break;
-                    };
+            if let Some(task) = self.set_mute(muted, cx) {
+                task.detach_and_log_err(cx);
+            }
 
-                    this.update(&mut cx, |this, cx| {
-                        this.live_kit_room_updated(update, cx).log_err()
-                    })
-                    .ok();
+            if should_undeafen {
+                if let Some(task) = self.set_deafened(false, cx) {
+                    task.detach_and_log_err(cx);
                 }
             }
-        });
-
-        self.live_kit = Some(LiveKitRoom {
-            room: room.clone(),
-            screen_track: LocalTrack::None,
-            microphone_track: LocalTrack::None,
-            next_publish_id: 0,
-            speaking: false,
-            _maintain_room,
-            _handle_updates,
-        });
-
-        cx.spawn({
-            let client = self.client.clone();
-            let share_microphone = !self.read_only() && !Self::mute_on_join(cx);
-            let connection_info = self.live_kit_connection_info.clone();
-            let channel_id = self.channel_id;
-
-            move |this, mut cx| async move {
-                let connection_info = if let Some(connection_info) = connection_info {
-                    connection_info.clone()
-                } else if let Some(channel_id) = channel_id {
-                    if let Some(connection_info) = client
-                        .request(proto::JoinChannelCall { channel_id })
-                        .await?
-                        .live_kit_connection_info
-                    {
-                        connection_info
-                    } else {
-                        return Err(anyhow!("failed to get connection info from server"));
-                    }
-                } else {
-                    return Err(anyhow!(
-                        "tried to connect to livekit without connection info"
-                    ));
-                };
-                room.connect(&connection_info.server_url, &connection_info.token)
-                    .await?;
-
-                let track_updates = this.update(&mut cx, |this, cx| {
-                    Audio::play_sound(Sound::Joined, cx);
-                    let Some(live_kit) = this.live_kit.as_mut() else {
-                        return vec![];
-                    };
-
-                    let mut track_updates = Vec::new();
-                    for participant in this.remote_participants.values() {
-                        for publication in live_kit
-                            .room
-                            .remote_audio_track_publications(&participant.user.id.to_string())
-                        {
-                            track_updates.push(publication.set_enabled(true));
-                        }
+        }
+    }
 
-                        for track in participant.audio_tracks.values() {
-                            track.start();
-                        }
-                    }
-                    track_updates
-                })?;
+    pub fn toggle_deafen(&mut self, cx: &mut ModelContext<Self>) {
+        if let Some(live_kit) = self.live_kit.as_mut() {
+            // When deafening, mute the microphone if it was not already muted.
+            // When un-deafening, unmute the microphone, unless it was explicitly muted.
+            let deafened = !live_kit.deafened;
+            live_kit.deafened = deafened;
+            let should_change_mute = !live_kit.muted_by_user;
 
-                if share_microphone {
-                    this.update(&mut cx, |this, cx| this.share_microphone(cx))?
-                        .await?
-                };
+            if let Some(task) = self.set_deafened(deafened, cx) {
+                task.detach_and_log_err(cx);
+            }
 
-                for result in futures::future::join_all(track_updates).await {
-                    result?;
+            if should_change_mute {
+                if let Some(task) = self.set_mute(deafened, cx) {
+                    task.detach_and_log_err(cx);
                 }
-                anyhow::Ok(())
             }
-        })
-    }
-
-    pub fn leave_call(&mut self, cx: &mut ModelContext<Self>) {
-        Audio::play_sound(Sound::Leave, cx);
-        if let Some(channel_id) = self.channel_id() {
-            let client = self.client.clone();
-            cx.background_executor()
-                .spawn(client.request(proto::LeaveChannelCall { channel_id }))
-                .detach_and_log_err(cx);
-            self.live_kit.take();
-            self.live_kit_connection_info.take();
-            cx.notify();
-        } else {
-            self.leave(cx).detach_and_log_err(cx)
         }
     }
 
@@ -1601,6 +1570,40 @@ impl Room {
         }
     }
 
+    fn set_deafened(
+        &mut self,
+        deafened: bool,
+        cx: &mut ModelContext<Self>,
+    ) -> Option<Task<Result<()>>> {
+        let live_kit = self.live_kit.as_mut()?;
+        cx.notify();
+
+        let mut track_updates = Vec::new();
+        for participant in self.remote_participants.values() {
+            for publication in live_kit
+                .room
+                .remote_audio_track_publications(&participant.user.id.to_string())
+            {
+                track_updates.push(publication.set_enabled(!deafened));
+            }
+
+            for track in participant.audio_tracks.values() {
+                if deafened {
+                    track.stop();
+                } else {
+                    track.start();
+                }
+            }
+        }
+
+        Some(cx.foreground_executor().spawn(async move {
+            for result in futures::future::join_all(track_updates).await {
+                result?;
+            }
+            Ok(())
+        }))
+    }
+
     fn set_mute(
         &mut self,
         should_mute: bool,
@@ -1645,6 +1648,9 @@ struct LiveKitRoom {
     room: Arc<live_kit_client::Room>,
     screen_track: LocalTrack,
     microphone_track: LocalTrack,
+    /// Tracks whether we're currently in a muted state due to auto-mute from deafening or manual mute performed by user.
+    muted_by_user: bool,
+    deafened: bool,
     speaking: bool,
     next_publish_id: usize,
     _maintain_room: Task<()>,

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

@@ -97,57 +97,11 @@ impl Database {
         .await
     }
 
-    pub async fn set_in_channel_call(
-        &self,
-        channel_id: ChannelId,
-        user_id: UserId,
-        in_call: bool,
-    ) -> Result<(proto::Room, ChannelRole)> {
-        self.transaction(move |tx| async move {
-            let channel = self.get_channel_internal(channel_id, &*tx).await?;
-            let role = self.channel_role_for_user(&channel, user_id, &*tx).await?;
-            if role.is_none() || role == Some(ChannelRole::Banned) {
-                Err(ErrorCode::Forbidden.anyhow())?
-            }
-            let role = role.unwrap();
-
-            let Some(room) = room::Entity::find()
-                .filter(room::Column::ChannelId.eq(channel_id))
-                .one(&*tx)
-                .await?
-            else {
-                Err(anyhow!("no room exists"))?
-            };
-
-            let result = room_participant::Entity::update_many()
-                .filter(
-                    Condition::all()
-                        .add(room_participant::Column::RoomId.eq(room.id))
-                        .add(room_participant::Column::UserId.eq(user_id)),
-                )
-                .set(room_participant::ActiveModel {
-                    in_call: ActiveValue::Set(in_call),
-                    ..Default::default()
-                })
-                .exec(&*tx)
-                .await?;
-
-            if result.rows_affected != 1 {
-                Err(anyhow!("not in channel"))?
-            }
-
-            let room = self.get_room(room.id, &*tx).await?;
-            Ok((room, role))
-        })
-        .await
-    }
-
     /// Adds a user to the specified channel.
     pub async fn join_channel(
         &self,
         channel_id: ChannelId,
         user_id: UserId,
-        autojoin: bool,
         connection: ConnectionId,
         environment: &str,
     ) -> Result<(JoinRoom, Option<MembershipUpdated>, ChannelRole)> {
@@ -212,7 +166,7 @@ impl Database {
                 .get_or_create_channel_room(channel_id, &live_kit_room, environment, &*tx)
                 .await?;
 
-            self.join_channel_room_internal(room_id, user_id, autojoin, connection, role, &*tx)
+            self.join_channel_room_internal(room_id, user_id, connection, role, &*tx)
                 .await
                 .map(|jr| (jr, accept_invite_result, role))
         })

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

@@ -135,7 +135,6 @@ impl Database {
                 ))),
                 participant_index: ActiveValue::set(Some(0)),
                 role: ActiveValue::set(Some(ChannelRole::Admin)),
-                in_call: ActiveValue::set(true),
 
                 id: ActiveValue::NotSet,
                 location_kind: ActiveValue::NotSet,
@@ -188,7 +187,6 @@ impl Database {
                 ))),
                 initial_project_id: ActiveValue::set(initial_project_id),
                 role: ActiveValue::set(Some(called_user_role)),
-                in_call: ActiveValue::set(true),
 
                 id: ActiveValue::NotSet,
                 answering_connection_id: ActiveValue::NotSet,
@@ -416,7 +414,6 @@ impl Database {
         &self,
         room_id: RoomId,
         user_id: UserId,
-        autojoin: bool,
         connection: ConnectionId,
         role: ChannelRole,
         tx: &DatabaseTransaction,
@@ -440,8 +437,6 @@ impl Database {
             ))),
             participant_index: ActiveValue::Set(Some(participant_index)),
             role: ActiveValue::set(Some(role)),
-            in_call: ActiveValue::set(autojoin),
-
             id: ActiveValue::NotSet,
             location_kind: ActiveValue::NotSet,
             location_project_id: ActiveValue::NotSet,
@@ -1263,7 +1258,6 @@ impl Database {
                         location: Some(proto::ParticipantLocation { variant: location }),
                         participant_index: participant_index as u32,
                         role: db_participant.role.unwrap_or(ChannelRole::Member).into(),
-                        in_call: db_participant.in_call,
                     },
                 );
             } else {

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

@@ -138,7 +138,6 @@ async fn test_joining_channels(db: &Arc<Database>) {
         .join_channel(
             channel_1,
             user_1,
-            false,
             ConnectionId { owner_id, id: 1 },
             TEST_RELEASE_CHANNEL,
         )
@@ -733,15 +732,9 @@ async fn test_guest_access(db: &Arc<Database>) {
         .await
         .is_err());
 
-    db.join_channel(
-        zed_channel,
-        guest,
-        false,
-        guest_connection,
-        TEST_RELEASE_CHANNEL,
-    )
-    .await
-    .unwrap();
+    db.join_channel(zed_channel, guest, guest_connection, TEST_RELEASE_CHANNEL)
+        .await
+        .unwrap();
 
     assert!(db
         .join_channel_chat(zed_channel, guest_connection, guest)

crates/collab/src/rpc.rs 🔗

@@ -105,7 +105,6 @@ struct Session {
     zed_environment: Arc<str>,
     user_id: UserId,
     connection_id: ConnectionId,
-    zed_version: SemanticVersion,
     db: Arc<tokio::sync::Mutex<DbHandle>>,
     peer: Arc<Peer>,
     connection_pool: Arc<parking_lot::Mutex<ConnectionPool>>,
@@ -132,19 +131,6 @@ impl Session {
             _not_send: PhantomData,
         }
     }
-
-    fn endpoint_removed_in(&self, endpoint: &str, version: SemanticVersion) -> anyhow::Result<()> {
-        if self.zed_version > version {
-            Err(anyhow!(
-                "{} was removed in {} (you're on {})",
-                endpoint,
-                version,
-                self.zed_version
-            ))
-        } else {
-            Ok(())
-        }
-    }
 }
 
 impl fmt::Debug for Session {
@@ -288,11 +274,8 @@ impl Server {
             .add_request_handler(get_channel_members)
             .add_request_handler(respond_to_channel_invite)
             .add_request_handler(join_channel)
-            .add_request_handler(join_channel2)
             .add_request_handler(join_channel_chat)
             .add_message_handler(leave_channel_chat)
-            .add_request_handler(join_channel_call)
-            .add_request_handler(leave_channel_call)
             .add_request_handler(send_channel_message)
             .add_request_handler(remove_channel_message)
             .add_request_handler(get_channel_messages)
@@ -576,7 +559,6 @@ impl Server {
         connection: Connection,
         address: String,
         user: User,
-        zed_version: SemanticVersion,
         impersonator: Option<User>,
         mut send_connection_id: Option<oneshot::Sender<ConnectionId>>,
         executor: Executor,
@@ -634,7 +616,6 @@ impl Server {
             let session = Session {
                 user_id,
                 connection_id,
-                zed_version,
                 db: Arc::new(tokio::sync::Mutex::new(DbHandle(this.app_state.db.clone()))),
                 zed_environment: this.app_state.config.zed_environment.clone(),
                 peer: this.peer.clone(),
@@ -885,7 +866,7 @@ pub fn routes(server: Arc<Server>) -> Router<Body> {
 
 pub async fn handle_websocket_request(
     TypedHeader(ProtocolVersion(protocol_version)): TypedHeader<ProtocolVersion>,
-    app_version_header: Option<TypedHeader<AppVersionHeader>>,
+    _app_version_header: Option<TypedHeader<AppVersionHeader>>,
     ConnectInfo(socket_address): ConnectInfo<SocketAddr>,
     Extension(server): Extension<Arc<Server>>,
     Extension(user): Extension<User>,
@@ -900,12 +881,6 @@ pub async fn handle_websocket_request(
             .into_response();
     }
 
-    // zed 0.122.x was the first version that sent an app header, so once that hits stable
-    // we can return UPGRADE_REQUIRED instead of unwrap_or_default();
-    let app_version = app_version_header
-        .map(|header| header.0 .0)
-        .unwrap_or_default();
-
     let socket_address = socket_address.to_string();
     ws.on_upgrade(move |socket| {
         use util::ResultExt;
@@ -920,7 +895,6 @@ pub async fn handle_websocket_request(
                     connection,
                     socket_address,
                     user,
-                    app_version,
                     impersonator.0,
                     None,
                     Executor::Production,
@@ -1063,7 +1037,7 @@ async fn join_room(
     let channel_id = session.db().await.channel_id_for_room(room_id).await?;
 
     if let Some(channel_id) = channel_id {
-        return join_channel_internal(channel_id, true, Box::new(response), session).await;
+        return join_channel_internal(channel_id, Box::new(response), session).await;
     }
 
     let joined_room = {
@@ -2726,67 +2700,14 @@ async fn respond_to_channel_invite(
     Ok(())
 }
 
-/// Join the channels' call
+/// Join the channels' room
 async fn join_channel(
     request: proto::JoinChannel,
     response: Response<proto::JoinChannel>,
     session: Session,
-) -> Result<()> {
-    session.endpoint_removed_in("join_channel", "0.123.0".parse().unwrap())?;
-
-    let channel_id = ChannelId::from_proto(request.channel_id);
-    join_channel_internal(channel_id, true, Box::new(response), session).await
-}
-
-async fn join_channel2(
-    request: proto::JoinChannel2,
-    response: Response<proto::JoinChannel2>,
-    session: Session,
-) -> Result<()> {
-    let channel_id = ChannelId::from_proto(request.channel_id);
-    join_channel_internal(channel_id, false, Box::new(response), session).await
-}
-
-async fn join_channel_call(
-    request: proto::JoinChannelCall,
-    response: Response<proto::JoinChannelCall>,
-    session: Session,
-) -> Result<()> {
-    let channel_id = ChannelId::from_proto(request.channel_id);
-    let db = session.db().await;
-    let (joined_room, role) = db
-        .set_in_channel_call(channel_id, session.user_id, true)
-        .await?;
-
-    let Some(connection_info) = session.live_kit_client.as_ref().and_then(|live_kit| {
-        live_kit_info_for_user(live_kit, &session.user_id, role, &joined_room.live_kit_room)
-    }) else {
-        Err(anyhow!("no live kit token info"))?
-    };
-
-    room_updated(&joined_room, &session.peer);
-    response.send(proto::JoinChannelCallResponse {
-        live_kit_connection_info: Some(connection_info),
-    })?;
-
-    Ok(())
-}
-
-async fn leave_channel_call(
-    request: proto::LeaveChannelCall,
-    response: Response<proto::LeaveChannelCall>,
-    session: Session,
 ) -> Result<()> {
     let channel_id = ChannelId::from_proto(request.channel_id);
-    let db = session.db().await;
-    let (joined_room, _) = db
-        .set_in_channel_call(channel_id, session.user_id, false)
-        .await?;
-
-    room_updated(&joined_room, &session.peer);
-    response.send(proto::Ack {})?;
-
-    Ok(())
+    join_channel_internal(channel_id, Box::new(response), session).await
 }
 
 trait JoinChannelInternalResponse {
@@ -2802,15 +2723,9 @@ impl JoinChannelInternalResponse for Response<proto::JoinRoom> {
         Response::<proto::JoinRoom>::send(self, result)
     }
 }
-impl JoinChannelInternalResponse for Response<proto::JoinChannel2> {
-    fn send(self, result: proto::JoinRoomResponse) -> Result<()> {
-        Response::<proto::JoinChannel2>::send(self, result)
-    }
-}
 
 async fn join_channel_internal(
     channel_id: ChannelId,
-    autojoin: bool,
     response: Box<impl JoinChannelInternalResponse>,
     session: Session,
 ) -> Result<()> {
@@ -2822,22 +2737,39 @@ async fn join_channel_internal(
             .join_channel(
                 channel_id,
                 session.user_id,
-                autojoin,
                 session.connection_id,
                 session.zed_environment.as_ref(),
             )
             .await?;
 
         let live_kit_connection_info = session.live_kit_client.as_ref().and_then(|live_kit| {
-            if !autojoin {
-                return None;
-            }
-            live_kit_info_for_user(
-                live_kit,
-                &session.user_id,
-                role,
-                &joined_room.room.live_kit_room,
-            )
+            let (can_publish, token) = if role == ChannelRole::Guest {
+                (
+                    false,
+                    live_kit
+                        .guest_token(
+                            &joined_room.room.live_kit_room,
+                            &session.user_id.to_string(),
+                        )
+                        .trace_err()?,
+                )
+            } else {
+                (
+                    true,
+                    live_kit
+                        .room_token(
+                            &joined_room.room.live_kit_room,
+                            &session.user_id.to_string(),
+                        )
+                        .trace_err()?,
+                )
+            };
+
+            Some(LiveKitConnectionInfo {
+                server_url: live_kit.url().into(),
+                token,
+                can_publish,
+            })
         });
 
         response.send(proto::JoinRoomResponse {
@@ -2873,35 +2805,6 @@ async fn join_channel_internal(
     Ok(())
 }
 
-fn live_kit_info_for_user(
-    live_kit: &Arc<dyn live_kit_server::api::Client>,
-    user_id: &UserId,
-    role: ChannelRole,
-    live_kit_room: &String,
-) -> Option<LiveKitConnectionInfo> {
-    let (can_publish, token) = if role == ChannelRole::Guest {
-        (
-            false,
-            live_kit
-                .guest_token(live_kit_room, &user_id.to_string())
-                .trace_err()?,
-        )
-    } else {
-        (
-            true,
-            live_kit
-                .room_token(live_kit_room, &user_id.to_string())
-                .trace_err()?,
-        )
-    };
-
-    Some(LiveKitConnectionInfo {
-        server_url: live_kit.url().into(),
-        token,
-        can_publish,
-    })
-}
-
 /// Start editing the channel notes
 async fn join_channel_buffer(
     request: proto::JoinChannelBuffer,

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

@@ -1,7 +1,4 @@
-use crate::{
-    db::ChannelId,
-    tests::{test_server::join_channel_call, TestServer},
-};
+use crate::{db::ChannelId, tests::TestServer};
 use call::ActiveCall;
 use editor::Editor;
 use gpui::{BackgroundExecutor, TestAppContext};
@@ -35,7 +32,7 @@ async fn test_channel_guests(
     cx_a.executor().run_until_parked();
 
     // Client B joins channel A as a guest
-    cx_b.update(|cx| workspace::open_channel(channel_id, client_b.app_state.clone(), None, cx))
+    cx_b.update(|cx| workspace::join_channel(channel_id, client_b.app_state.clone(), None, cx))
         .await
         .unwrap();
 
@@ -75,7 +72,7 @@ async fn test_channel_guest_promotion(cx_a: &mut TestAppContext, cx_b: &mut Test
         .await;
 
     let project_a = client_a.build_test_project(cx_a).await;
-    cx_a.update(|cx| workspace::open_channel(channel_id, client_a.app_state.clone(), None, cx))
+    cx_a.update(|cx| workspace::join_channel(channel_id, client_a.app_state.clone(), None, cx))
         .await
         .unwrap();
 
@@ -87,13 +84,11 @@ async fn test_channel_guest_promotion(cx_a: &mut TestAppContext, cx_b: &mut Test
     cx_a.run_until_parked();
 
     // Client B joins channel A as a guest
-    cx_b.update(|cx| workspace::open_channel(channel_id, client_b.app_state.clone(), None, cx))
+    cx_b.update(|cx| workspace::join_channel(channel_id, client_b.app_state.clone(), None, cx))
         .await
         .unwrap();
     cx_a.run_until_parked();
 
-    join_channel_call(cx_b).await.unwrap();
-
     // client B opens 1.txt as a guest
     let (workspace_b, cx_b) = client_b.active_workspace(cx_b);
     let room_b = cx_b

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

@@ -1,7 +1,7 @@
 use crate::{
     db::{self, UserId},
     rpc::RECONNECT_TIMEOUT,
-    tests::{room_participants, test_server::join_channel_call, RoomParticipants, TestServer},
+    tests::{room_participants, RoomParticipants, TestServer},
 };
 use call::ActiveCall;
 use channel::{ChannelId, ChannelMembership, ChannelStore};
@@ -382,7 +382,6 @@ async fn test_channel_room(
         .update(cx_a, |active_call, cx| active_call.join_channel(zed_id, cx))
         .await
         .unwrap();
-    join_channel_call(cx_a).await.unwrap();
 
     // Give everyone a chance to observe user A joining
     executor.run_until_parked();
@@ -430,7 +429,7 @@ async fn test_channel_room(
         .update(cx_b, |active_call, cx| active_call.join_channel(zed_id, cx))
         .await
         .unwrap();
-    join_channel_call(cx_b).await.unwrap();
+
     executor.run_until_parked();
 
     cx_a.read(|cx| {
@@ -553,9 +552,6 @@ async fn test_channel_room(
         .await
         .unwrap();
 
-    join_channel_call(cx_a).await.unwrap();
-    join_channel_call(cx_b).await.unwrap();
-
     executor.run_until_parked();
 
     let room_a =

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

@@ -24,7 +24,7 @@ use workspace::{
 
 use super::TestClient;
 
-#[gpui::test]
+#[gpui::test(iterations = 10)]
 async fn test_basic_following(
     cx_a: &mut TestAppContext,
     cx_b: &mut TestAppContext,
@@ -437,7 +437,6 @@ async fn test_basic_following(
         })
         .await
         .unwrap();
-
     executor.run_until_parked();
     let shared_screen = workspace_a.update(cx_a, |workspace, cx| {
         workspace
@@ -523,7 +522,6 @@ async fn test_basic_following(
         workspace_a.update(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
         None
     );
-    executor.run_until_parked();
 }
 
 #[gpui::test]
@@ -2006,7 +2004,7 @@ async fn join_channel(
     client: &TestClient,
     cx: &mut TestAppContext,
 ) -> anyhow::Result<()> {
-    cx.update(|cx| workspace::open_channel(channel_id, client.app_state.clone(), None, cx))
+    cx.update(|cx| workspace::join_channel(channel_id, client.app_state.clone(), None, cx))
         .await
 }
 

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

@@ -1881,7 +1881,7 @@ fn active_call_events(cx: &mut TestAppContext) -> Rc<RefCell<Vec<room::Event>>>
 }
 
 #[gpui::test]
-async fn test_mute(
+async fn test_mute_deafen(
     executor: BackgroundExecutor,
     cx_a: &mut TestAppContext,
     cx_b: &mut TestAppContext,
@@ -1920,7 +1920,7 @@ async fn test_mute(
     room_a.read_with(cx_a, |room, _| assert!(!room.is_muted()));
     room_b.read_with(cx_b, |room, _| assert!(!room.is_muted()));
 
-    // Users A and B are both unmuted.
+    // Users A and B are both muted.
     assert_eq!(
         participant_audio_state(&room_a, cx_a),
         &[ParticipantAudioState {
@@ -1962,6 +1962,30 @@ async fn test_mute(
         }]
     );
 
+    // User A deafens
+    room_a.update(cx_a, |room, cx| room.toggle_deafen(cx));
+    executor.run_until_parked();
+
+    // User A does not hear user B.
+    room_a.read_with(cx_a, |room, _| assert!(room.is_muted()));
+    room_b.read_with(cx_b, |room, _| assert!(!room.is_muted()));
+    assert_eq!(
+        participant_audio_state(&room_a, cx_a),
+        &[ParticipantAudioState {
+            user_id: client_b.user_id().unwrap(),
+            is_muted: false,
+            audio_tracks_playing: vec![false],
+        }]
+    );
+    assert_eq!(
+        participant_audio_state(&room_b, cx_b),
+        &[ParticipantAudioState {
+            user_id: client_a.user_id().unwrap(),
+            is_muted: true,
+            audio_tracks_playing: vec![true],
+        }]
+    );
+
     // User B calls user C, C joins.
     active_call_b
         .update(cx_b, |call, cx| {
@@ -1976,6 +2000,22 @@ async fn test_mute(
         .unwrap();
     executor.run_until_parked();
 
+    // User A does not hear users B or C.
+    assert_eq!(
+        participant_audio_state(&room_a, cx_a),
+        &[
+            ParticipantAudioState {
+                user_id: client_b.user_id().unwrap(),
+                is_muted: false,
+                audio_tracks_playing: vec![false],
+            },
+            ParticipantAudioState {
+                user_id: client_c.user_id().unwrap(),
+                is_muted: false,
+                audio_tracks_playing: vec![false],
+            }
+        ]
+    );
     assert_eq!(
         participant_audio_state(&room_b, cx_b),
         &[

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

@@ -37,7 +37,7 @@ use std::{
         Arc,
     },
 };
-use util::{http::FakeHttpClient, SemanticVersion};
+use util::http::FakeHttpClient;
 use workspace::{Workspace, WorkspaceStore};
 
 pub struct TestServer {
@@ -231,7 +231,6 @@ impl TestServer {
                                 server_conn,
                                 client_name,
                                 user,
-                                SemanticVersion::default(),
                                 None,
                                 Some(connection_id_tx),
                                 Executor::Deterministic(cx.background_executor().clone()),
@@ -687,7 +686,7 @@ impl TestClient {
         channel_id: u64,
         cx: &'a mut TestAppContext,
     ) -> (View<Workspace>, &'a mut VisualTestContext) {
-        cx.update(|cx| workspace::open_channel(channel_id, self.app_state.clone(), None, cx))
+        cx.update(|cx| workspace::join_channel(channel_id, self.app_state.clone(), None, cx))
             .await
             .unwrap();
         cx.run_until_parked();
@@ -762,11 +761,6 @@ impl TestClient {
     }
 }
 
-pub fn join_channel_call(cx: &mut TestAppContext) -> Task<anyhow::Result<()>> {
-    let room = cx.read(|cx| ActiveCall::global(cx).read(cx).room().cloned());
-    room.unwrap().update(cx, |room, cx| room.join_call(cx))
-}
-
 pub fn open_channel_notes(
     channel_id: u64,
     cx: &mut VisualTestContext,

crates/collab_ui/src/collab_panel.rs 🔗

@@ -40,7 +40,7 @@ use util::{maybe, ResultExt, TryFutureExt};
 use workspace::{
     dock::{DockPosition, Panel, PanelEvent},
     notifications::{DetachAndPromptErr, NotifyResultExt, NotifyTaskExt},
-    Workspace,
+    OpenChannelNotes, Workspace,
 };
 
 actions!(
@@ -69,6 +69,19 @@ pub fn init(cx: &mut AppContext) {
         workspace.register_action(|workspace, _: &ToggleFocus, cx| {
             workspace.toggle_panel_focus::<CollabPanel>(cx);
         });
+        workspace.register_action(|_, _: &OpenChannelNotes, cx| {
+            let channel_id = ActiveCall::global(cx)
+                .read(cx)
+                .room()
+                .and_then(|room| room.read(cx).channel_id());
+
+            if let Some(channel_id) = channel_id {
+                let workspace = cx.view().clone();
+                cx.window_context().defer(move |cx| {
+                    ChannelView::open(channel_id, None, workspace, cx).detach_and_log_err(cx)
+                });
+            }
+        });
     })
     .detach();
 }
@@ -162,9 +175,6 @@ enum ListEntry {
         depth: usize,
         has_children: bool,
     },
-    ChannelCall {
-        channel_id: ChannelId,
-    },
     ChannelNotes {
         channel_id: ChannelId,
     },
@@ -372,7 +382,6 @@ impl CollabPanel {
 
                 if query.is_empty() {
                     if let Some(channel_id) = room.channel_id() {
-                        self.entries.push(ListEntry::ChannelCall { channel_id });
                         self.entries.push(ListEntry::ChannelNotes { channel_id });
                         self.entries.push(ListEntry::ChannelChat { channel_id });
                     }
@@ -470,7 +479,7 @@ impl CollabPanel {
                                 && participant.video_tracks.is_empty(),
                         });
                     }
-                    if room.in_call() && !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,
@@ -504,20 +513,6 @@ impl CollabPanel {
                         role: proto::ChannelRole::Member,
                     }));
             }
-        } else if let Some(channel_id) = ActiveCall::global(cx).read(cx).pending_channel_id() {
-            self.entries.push(ListEntry::Header(Section::ActiveCall));
-            if !old_entries
-                .iter()
-                .any(|entry| matches!(entry, ListEntry::Header(Section::ActiveCall)))
-            {
-                scroll_to_top = true;
-            }
-
-            if query.is_empty() {
-                self.entries.push(ListEntry::ChannelCall { channel_id });
-                self.entries.push(ListEntry::ChannelNotes { channel_id });
-                self.entries.push(ListEntry::ChannelChat { channel_id });
-            }
         }
 
         let mut request_entries = Vec::new();
@@ -837,6 +832,8 @@ impl CollabPanel {
         cx: &mut ViewContext<Self>,
     ) -> ListItem {
         let user_id = user.id;
+        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);
 
         let is_call_admin = ActiveCall::global(cx).read(cx).room().is_some_and(|room| {
@@ -849,6 +846,12 @@ impl CollabPanel {
             .selected(is_selected)
             .end_slot(if is_pending {
                 Label::new("Calling").color(Color::Muted).into_any_element()
+            } else if is_current_user {
+                IconButton::new("leave-call", IconName::Exit)
+                    .style(ButtonStyle::Subtle)
+                    .on_click(move |_, cx| Self::leave_call(cx))
+                    .tooltip(|cx| Tooltip::text("Leave Call", cx))
+                    .into_any_element()
             } else if role == proto::ChannelRole::Guest {
                 Label::new("Guest").color(Color::Muted).into_any_element()
             } else {
@@ -950,88 +953,12 @@ impl CollabPanel {
         }
     }
 
-    fn render_channel_call(
-        &self,
-        channel_id: ChannelId,
-        is_selected: bool,
-        cx: &mut ViewContext<Self>,
-    ) -> impl IntoElement {
-        let (is_in_call, call_participants) = ActiveCall::global(cx)
-            .read(cx)
-            .room()
-            .map(|room| (room.read(cx).in_call(), room.read(cx).call_participants(cx)))
-            .unwrap_or_default();
-
-        const FACEPILE_LIMIT: usize = 3;
-
-        let face_pile = if !call_participants.is_empty() {
-            let extra_count = call_participants.len().saturating_sub(FACEPILE_LIMIT);
-            let result = FacePile::new(
-                call_participants
-                    .iter()
-                    .map(|user| Avatar::new(user.avatar_uri.clone()).into_any_element())
-                    .take(FACEPILE_LIMIT)
-                    .chain(if extra_count > 0 {
-                        Some(
-                            div()
-                                .ml_2()
-                                .child(Label::new(format!("+{extra_count}")))
-                                .into_any_element(),
-                        )
-                    } else {
-                        None
-                    })
-                    .collect::<SmallVec<_>>(),
-            );
-
-            Some(result)
-        } else {
-            None
-        };
-
-        ListItem::new("channel-call")
-            .selected(is_selected)
-            .start_slot(
-                h_flex()
-                    .gap_1()
-                    .child(render_tree_branch(false, true, cx))
-                    .child(IconButton::new(0, IconName::AudioOn)),
-            )
-            .when(is_in_call, |el| {
-                el.end_slot(
-                    IconButton::new(1, IconName::Exit)
-                        .style(ButtonStyle::Filled)
-                        .shape(ui::IconButtonShape::Square)
-                        .tooltip(|cx| Tooltip::text("Leave call", cx))
-                        .on_click(cx.listener(|this, _, cx| this.leave_channel_call(cx))),
-                )
-            })
-            .when(!is_in_call, |el| {
-                el.tooltip(move |cx| Tooltip::text("Join audio call", cx))
-                    .on_click(cx.listener(move |this, _, cx| {
-                        this.join_channel_call(channel_id, cx);
-                    }))
-            })
-            .child(
-                div()
-                    .text_ui()
-                    .when(!call_participants.is_empty(), |el| {
-                        el.font_weight(FontWeight::SEMIBOLD)
-                    })
-                    .child("call"),
-            )
-            .children(face_pile)
-    }
-
     fn render_channel_notes(
         &self,
         channel_id: ChannelId,
         is_selected: bool,
         cx: &mut ViewContext<Self>,
     ) -> impl IntoElement {
-        let channel_store = self.channel_store.read(cx);
-        let has_notes_notification = channel_store.has_channel_buffer_changed(channel_id);
-
         ListItem::new("channel-notes")
             .selected(is_selected)
             .on_click(cx.listener(move |this, _, cx| {
@@ -1043,14 +970,7 @@ impl CollabPanel {
                     .child(render_tree_branch(false, true, cx))
                     .child(IconButton::new(0, IconName::File)),
             )
-            .child(
-                div()
-                    .text_ui()
-                    .when(has_notes_notification, |el| {
-                        el.font_weight(FontWeight::SEMIBOLD)
-                    })
-                    .child("notes"),
-            )
+            .child(Label::new("notes"))
             .tooltip(move |cx| Tooltip::text("Open Channel Notes", cx))
     }
 
@@ -1060,8 +980,6 @@ impl CollabPanel {
         is_selected: bool,
         cx: &mut ViewContext<Self>,
     ) -> impl IntoElement {
-        let channel_store = self.channel_store.read(cx);
-        let has_messages_notification = channel_store.has_new_messages(channel_id);
         ListItem::new("channel-chat")
             .selected(is_selected)
             .on_click(cx.listener(move |this, _, cx| {
@@ -1073,14 +991,7 @@ impl CollabPanel {
                     .child(render_tree_branch(false, false, cx))
                     .child(IconButton::new(0, IconName::MessageBubbles)),
             )
-            .child(
-                div()
-                    .text_ui()
-                    .when(has_messages_notification, |el| {
-                        el.font_weight(FontWeight::SEMIBOLD)
-                    })
-                    .child("chat"),
-            )
+            .child(Label::new("chat"))
             .tooltip(move |cx| Tooltip::text("Open Chat", cx))
     }
 
@@ -1338,14 +1249,12 @@ impl CollabPanel {
         cx: &mut ViewContext<Self>,
     ) {
         let this = cx.view().clone();
-        let room = ActiveCall::global(cx).read(cx).room();
-        let in_room = room.is_some();
-        let in_call = room.is_some_and(|room| room.read(cx).in_call());
+        let in_room = ActiveCall::global(cx).read(cx).room().is_some();
 
         let context_menu = ContextMenu::build(cx, |mut context_menu, _| {
             let user_id = contact.user.id;
 
-            if contact.online && !contact.busy && (!in_room || in_call) {
+            if contact.online && !contact.busy {
                 let label = if in_room {
                     format!("Invite {} to join", contact.user.github_login)
                 } else {
@@ -1479,7 +1388,23 @@ impl CollabPanel {
                             });
                         }
                     }
-                    ListEntry::Channel { channel, .. } => self.open_channel(channel.id, cx),
+                    ListEntry::Channel { channel, .. } => {
+                        let is_active = maybe!({
+                            let call_channel = ActiveCall::global(cx)
+                                .read(cx)
+                                .room()?
+                                .read(cx)
+                                .channel_id()?;
+
+                            Some(call_channel == channel.id)
+                        })
+                        .unwrap_or(false);
+                        if is_active {
+                            self.open_channel_notes(channel.id, cx)
+                        } else {
+                            self.join_channel(channel.id, cx)
+                        }
+                    }
                     ListEntry::ContactPlaceholder => self.toggle_contact_finder(cx),
                     ListEntry::CallParticipant { user, peer_id, .. } => {
                         if Some(user) == self.user_store.read(cx).current_user().as_ref() {
@@ -1496,9 +1421,6 @@ impl CollabPanel {
                     ListEntry::ChannelInvite(channel) => {
                         self.respond_to_channel_invite(channel.id, true, cx)
                     }
-                    ListEntry::ChannelCall { channel_id } => {
-                        self.join_channel_call(*channel_id, cx)
-                    }
                     ListEntry::ChannelNotes { channel_id } => {
                         self.open_channel_notes(*channel_id, cx)
                     }
@@ -1961,47 +1883,20 @@ impl CollabPanel {
             .detach_and_prompt_err("Call failed", cx, |_, _| None);
     }
 
-    fn open_channel(&mut self, channel_id: u64, cx: &mut ViewContext<Self>) {
+    fn join_channel(&self, channel_id: u64, cx: &mut ViewContext<Self>) {
         let Some(workspace) = self.workspace.upgrade() else {
             return;
         };
         let Some(handle) = cx.window_handle().downcast::<Workspace>() else {
             return;
         };
-        let is_in_call = ActiveCall::global(cx)
-            .read(cx)
-            .room()
-            .map(|room| room.read(cx).in_call())
-            .unwrap_or(false);
-        if !is_in_call {
-            workspace::open_channel(
-                channel_id,
-                workspace.read(cx).app_state().clone(),
-                Some(handle),
-                cx,
-            )
-            .detach_and_prompt_err("Failed to join channel", cx, |_, _| None);
-        }
-
-        self.open_channel_notes(channel_id, cx);
-        self.join_channel_chat(channel_id, cx);
-    }
-
-    fn join_channel_call(&mut self, _channel_id: ChannelId, cx: &mut ViewContext<Self>) {
-        let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() else {
-            return;
-        };
-
-        room.update(cx, |room, cx| room.join_call(cx))
-            .detach_and_prompt_err("Failed to join call", cx, |_, _| None)
-    }
-
-    fn leave_channel_call(&mut self, cx: &mut ViewContext<Self>) {
-        let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() else {
-            return;
-        };
-
-        room.update(cx, |room, cx| room.leave_call(cx));
+        workspace::join_channel(
+            channel_id,
+            workspace.read(cx).app_state().clone(),
+            Some(handle),
+            cx,
+        )
+        .detach_and_prompt_err("Failed to join channel", cx, |_, _| None)
     }
 
     fn join_channel_chat(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
@@ -2129,9 +2024,6 @@ impl CollabPanel {
             ListEntry::ParticipantScreen { peer_id, is_last } => self
                 .render_participant_screen(*peer_id, *is_last, is_selected, cx)
                 .into_any_element(),
-            ListEntry::ChannelCall { channel_id } => self
-                .render_channel_call(*channel_id, is_selected, cx)
-                .into_any_element(),
             ListEntry::ChannelNotes { channel_id } => self
                 .render_channel_notes(*channel_id, is_selected, cx)
                 .into_any_element(),
@@ -2197,25 +2089,24 @@ impl CollabPanel {
         is_collapsed: bool,
         cx: &ViewContext<Self>,
     ) -> impl IntoElement {
+        let mut channel_link = None;
         let mut channel_tooltip_text = None;
         let mut channel_icon = None;
 
         let text = match section {
             Section::ActiveCall => {
                 let channel_name = maybe!({
-                    let channel_id = ActiveCall::global(cx)
-                        .read(cx)
-                        .channel_id(cx)
-                        .or_else(|| ActiveCall::global(cx).read(cx).pending_channel_id())?;
+                    let channel_id = ActiveCall::global(cx).read(cx).channel_id(cx)?;
 
                     let channel = self.channel_store.read(cx).channel_for_id(channel_id)?;
 
+                    channel_link = Some(channel.link());
                     (channel_icon, channel_tooltip_text) = match channel.visibility {
                         proto::ChannelVisibility::Public => {
-                            (Some(IconName::Public), Some("Close Channel"))
+                            (Some("icons/public.svg"), Some("Copy public channel link."))
                         }
                         proto::ChannelVisibility::Members => {
-                            (Some(IconName::Hash), Some("Close Channel"))
+                            (Some("icons/hash.svg"), Some("Copy private channel link."))
                         }
                     };
 
@@ -2237,10 +2128,17 @@ impl CollabPanel {
         };
 
         let button = match section {
-            Section::ActiveCall => channel_icon.map(|_| {
-                IconButton::new("channel-link", IconName::Close)
-                    .on_click(move |_, cx| Self::leave_call(cx))
-                    .tooltip(|cx| Tooltip::text("Close channel", cx))
+            Section::ActiveCall => channel_link.map(|channel_link| {
+                let channel_link_copy = channel_link.clone();
+                IconButton::new("channel-link", IconName::Copy)
+                    .icon_size(IconSize::Small)
+                    .size(ButtonSize::None)
+                    .visible_on_hover("section-header")
+                    .on_click(move |_, cx| {
+                        let item = ClipboardItem::new(channel_link_copy.clone());
+                        cx.write_to_clipboard(item)
+                    })
+                    .tooltip(|cx| Tooltip::text("Copy channel link", cx))
                     .into_any_element()
             }),
             Section::Contacts => Some(
@@ -2275,9 +2173,6 @@ impl CollabPanel {
                             this.toggle_section_expanded(section, cx);
                         }))
                 })
-                .when_some(channel_icon, |el, channel_icon| {
-                    el.start_slot(Icon::new(channel_icon).color(Color::Muted))
-                })
                 .inset(true)
                 .end_slot::<AnyElement>(button)
                 .selected(is_selected),
@@ -2583,7 +2478,11 @@ impl CollabPanel {
                         }),
                     )
                     .on_click(cx.listener(move |this, _, cx| {
-                        this.open_channel(channel_id, cx);
+                        if is_active {
+                            this.open_channel_notes(channel_id, cx)
+                        } else {
+                            this.join_channel(channel_id, cx)
+                        }
                     }))
                     .on_secondary_mouse_down(cx.listener(
                         move |this, event: &MouseDownEvent, cx| {
@@ -2600,24 +2499,61 @@ impl CollabPanel {
                         .color(Color::Muted),
                     )
                     .child(
-                        h_flex().id(channel_id as usize).child(
-                            div()
-                                .text_ui()
-                                .when(has_messages_notification || has_notes_notification, |el| {
-                                    el.font_weight(FontWeight::SEMIBOLD)
-                                })
-                                .child(channel.name.clone()),
-                        ),
+                        h_flex()
+                            .id(channel_id as usize)
+                            .child(Label::new(channel.name.clone()))
+                            .children(face_pile.map(|face_pile| face_pile.p_1())),
                     ),
             )
-            .children(face_pile.map(|face_pile| {
+            .child(
                 h_flex()
                     .absolute()
                     .right(rems(0.))
                     .z_index(1)
                     .h_full()
-                    .child(face_pile.p_1())
-            }))
+                    .child(
+                        h_flex()
+                            .h_full()
+                            .gap_1()
+                            .px_1()
+                            .child(
+                                IconButton::new("channel_chat", IconName::MessageBubbles)
+                                    .style(ButtonStyle::Filled)
+                                    .shape(ui::IconButtonShape::Square)
+                                    .icon_size(IconSize::Small)
+                                    .icon_color(if has_messages_notification {
+                                        Color::Default
+                                    } else {
+                                        Color::Muted
+                                    })
+                                    .on_click(cx.listener(move |this, _, cx| {
+                                        this.join_channel_chat(channel_id, cx)
+                                    }))
+                                    .tooltip(|cx| Tooltip::text("Open channel chat", cx))
+                                    .when(!has_messages_notification, |this| {
+                                        this.visible_on_hover("")
+                                    }),
+                            )
+                            .child(
+                                IconButton::new("channel_notes", IconName::File)
+                                    .style(ButtonStyle::Filled)
+                                    .shape(ui::IconButtonShape::Square)
+                                    .icon_size(IconSize::Small)
+                                    .icon_color(if has_notes_notification {
+                                        Color::Default
+                                    } else {
+                                        Color::Muted
+                                    })
+                                    .on_click(cx.listener(move |this, _, cx| {
+                                        this.open_channel_notes(channel_id, cx)
+                                    }))
+                                    .tooltip(|cx| Tooltip::text("Open channel notes", cx))
+                                    .when(!has_notes_notification, |this| {
+                                        this.visible_on_hover("")
+                                    }),
+                            ),
+                    ),
+            )
             .tooltip({
                 let channel_store = self.channel_store.clone();
                 move |cx| {
@@ -2821,14 +2757,6 @@ impl PartialEq for ListEntry {
                     return channel_1.id == channel_2.id;
                 }
             }
-            ListEntry::ChannelCall { channel_id } => {
-                if let ListEntry::ChannelCall {
-                    channel_id: other_id,
-                } = other
-                {
-                    return channel_id == other_id;
-                }
-            }
             ListEntry::ChannelNotes { channel_id } => {
                 if let ListEntry::ChannelNotes {
                     channel_id: other_id,
@@ -2927,7 +2855,7 @@ impl Render for JoinChannelTooltip {
                 .read(cx)
                 .channel_participants(self.channel_id);
 
-            div.child(Label::new("Open Channel"))
+            div.child(Label::new("Join Channel"))
                 .children(participants.iter().map(|participant| {
                     h_flex()
                         .gap_2()

crates/collab_ui/src/collab_titlebar_item.rs 🔗

@@ -102,10 +102,6 @@ impl Render for CollabTitlebarItem {
                                 room.remote_participants().values().collect::<Vec<_>>();
                             remote_participants.sort_by_key(|p| p.participant_index.0);
 
-                            if !room.in_call() {
-                                return this;
-                            }
-
                             let current_user_face_pile = self.render_collaborator(
                                 &current_user,
                                 peer_id,
@@ -137,10 +133,6 @@ impl Render for CollabTitlebarItem {
                                             == ParticipantLocation::SharedProject { project_id }
                                     });
 
-                                    if !collaborator.in_call {
-                                        return None;
-                                    }
-
                                     let face_pile = self.render_collaborator(
                                         &collaborator.user,
                                         collaborator.peer_id,
@@ -193,7 +185,7 @@ impl Render for CollabTitlebarItem {
                         let is_local = project.is_local();
                         let is_shared = is_local && project.is_shared();
                         let is_muted = room.is_muted();
-                        let is_connected_to_livekit = room.in_call();
+                        let is_deafened = room.is_deafened().unwrap_or(false);
                         let is_screen_sharing = room.is_screen_sharing();
                         let read_only = room.read_only();
 
@@ -228,28 +220,22 @@ impl Render for CollabTitlebarItem {
                                 )),
                             )
                         })
-                        .when(is_connected_to_livekit, |el| {
-                            el.child(
-                                div()
-                                    .child(
-                                        IconButton::new("leave-call", ui::IconName::Exit)
-                                            .style(ButtonStyle::Subtle)
-                                            .tooltip(|cx| Tooltip::text("Leave call", cx))
-                                            .icon_size(IconSize::Small)
-                                            .on_click(move |_, cx| {
-                                                ActiveCall::global(cx).update(cx, |call, cx| {
-                                                    if let Some(room) = call.room() {
-                                                        room.update(cx, |room, cx| {
-                                                            room.leave_call(cx)
-                                                        })
-                                                    }
-                                                })
-                                            }),
-                                    )
-                                    .pl_2(),
-                            )
-                        })
-                        .when(!read_only && is_connected_to_livekit, |this| {
+                        .child(
+                            div()
+                                .child(
+                                    IconButton::new("leave-call", ui::IconName::Exit)
+                                        .style(ButtonStyle::Subtle)
+                                        .tooltip(|cx| Tooltip::text("Leave call", cx))
+                                        .icon_size(IconSize::Small)
+                                        .on_click(move |_, cx| {
+                                            ActiveCall::global(cx)
+                                                .update(cx, |call, cx| call.hang_up(cx))
+                                                .detach_and_log_err(cx);
+                                        }),
+                                )
+                                .pr_2(),
+                        )
+                        .when(!read_only, |this| {
                             this.child(
                                 IconButton::new(
                                     "mute-microphone",
@@ -276,7 +262,34 @@ impl Render for CollabTitlebarItem {
                                 .on_click(move |_, cx| crate::toggle_mute(&Default::default(), cx)),
                             )
                         })
-                        .when(!read_only && is_connected_to_livekit, |this| {
+                        .child(
+                            IconButton::new(
+                                "mute-sound",
+                                if is_deafened {
+                                    ui::IconName::AudioOff
+                                } else {
+                                    ui::IconName::AudioOn
+                                },
+                            )
+                            .style(ButtonStyle::Subtle)
+                            .selected_style(ButtonStyle::Tinted(TintColor::Negative))
+                            .icon_size(IconSize::Small)
+                            .selected(is_deafened)
+                            .tooltip(move |cx| {
+                                if !read_only {
+                                    Tooltip::with_meta(
+                                        "Deafen Audio",
+                                        None,
+                                        "Mic will be muted",
+                                        cx,
+                                    )
+                                } else {
+                                    Tooltip::text("Deafen Audio", cx)
+                                }
+                            })
+                            .on_click(move |_, cx| crate::toggle_deafen(&Default::default(), cx)),
+                        )
+                        .when(!read_only, |this| {
                             this.child(
                                 IconButton::new("screen-share", ui::IconName::Screen)
                                     .style(ButtonStyle::Subtle)

crates/collab_ui/src/collab_ui.rs 🔗

@@ -22,7 +22,10 @@ pub use panel_settings::{
 use settings::Settings;
 use workspace::{notifications::DetachAndPromptErr, AppState};
 
-actions!(collab, [ToggleScreenSharing, ToggleMute, LeaveCall]);
+actions!(
+    collab,
+    [ToggleScreenSharing, ToggleMute, ToggleDeafen, LeaveCall]
+);
 
 pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
     CollaborationPanelSettings::register(cx);
@@ -82,6 +85,12 @@ pub fn toggle_mute(_: &ToggleMute, cx: &mut AppContext) {
     }
 }
 
+pub fn toggle_deafen(_: &ToggleDeafen, cx: &mut AppContext) {
+    if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
+        room.update(cx, |room, cx| room.toggle_deafen(cx));
+    }
+}
+
 fn notification_window_options(
     screen: Rc<dyn PlatformDisplay>,
     window_size: Size<Pixels>,

crates/editor/src/display_map.rs 🔗

@@ -3,7 +3,7 @@
 //! Not literally though - rendering, layout and all that jazz is a responsibility of [`EditorElement`][EditorElement].
 //! Instead, [`DisplayMap`] decides where Inlays/Inlay hints are displayed, when
 //! to apply a soft wrap, where to add fold indicators, whether there are any tabs in the buffer that
-//! we display as spaces and where to display custom blocks (like diagnostics)
+//! we display as spaces and where to display custom blocks (like diagnostics).
 //! Seems like a lot? That's because it is. [`DisplayMap`] is conceptually made up
 //! of several smaller structures that form a hierarchy (starting at the bottom):
 //! - [`InlayMap`] that decides where the [`Inlay`]s should be displayed.

crates/live_kit_client/src/test.rs 🔗

@@ -54,7 +54,7 @@ impl TestServer {
         Ok(SERVERS
             .lock()
             .get(url)
-            .ok_or_else(|| anyhow!("no server found for url: {}", url))?
+            .ok_or_else(|| anyhow!("no server found for url"))?
             .clone())
     }
 
@@ -160,6 +160,7 @@ impl TestServer {
 
     async fn remove_participant(&self, room_name: String, identity: String) -> Result<()> {
         // TODO: clear state associated with the `Room`.
+
         self.executor.simulate_random_delay().await;
         let mut server_rooms = self.rooms.lock();
         let room = server_rooms
@@ -413,15 +414,6 @@ struct TestServerRoom {
     participant_permissions: HashMap<Sid, proto::ParticipantPermission>,
 }
 
-impl Drop for TestServerRoom {
-    fn drop(&mut self) {
-        for room in self.client_rooms.values() {
-            let mut state = room.0.lock();
-            *state.connection.0.borrow_mut() = ConnectionState::Disconnected;
-        }
-    }
-}
-
 #[derive(Debug)]
 struct TestServerVideoTrack {
     sid: Sid,
@@ -702,15 +694,11 @@ impl LocalTrackPublication {
 
     pub fn is_muted(&self) -> bool {
         if let Some(room) = self.room.upgrade() {
-            if room.is_connected() {
-                room.test_server()
-                    .is_track_muted(&room.token(), &self.sid)
-                    .unwrap_or(true)
-            } else {
-                true
-            }
+            room.test_server()
+                .is_track_muted(&room.token(), &self.sid)
+                .unwrap_or(false)
         } else {
-            true
+            false
         }
     }
 

crates/rpc/proto/zed.proto 🔗

@@ -184,12 +184,9 @@ message Envelope {
         SetRoomParticipantRole set_room_participant_role = 156;
 
         UpdateUserChannels update_user_channels = 157;
-
-        JoinChannel2 join_channel2 = 158;
-        JoinChannelCall join_channel_call = 159;
-        JoinChannelCallResponse join_channel_call_response = 160;
-        LeaveChannelCall leave_channel_call = 161; // current max
     }
+
+    reserved 158 to 161;
 }
 
 // Messages
@@ -296,7 +293,7 @@ message Participant {
     ParticipantLocation location = 4;
     uint32 participant_index = 5;
     ChannelRole role = 6;
-    bool in_call = 7;
+    reserved 7;
 }
 
 message PendingParticipant {
@@ -1039,22 +1036,6 @@ message JoinChannel {
     uint64 channel_id = 1;
 }
 
-message JoinChannel2 {
-    uint64 channel_id = 1;
-}
-
-message JoinChannelCall {
-    uint64 channel_id = 1;
-}
-
-message JoinChannelCallResponse {
-    LiveKitConnectionInfo live_kit_connection_info = 1;
-}
-
-message LeaveChannelCall {
-    uint64 channel_id = 1;
-}
-
 message DeleteChannel {
     uint64 channel_id = 1;
 }

crates/rpc/src/proto.rs 🔗

@@ -198,7 +198,6 @@ messages!(
     (InlayHints, Background),
     (InlayHintsResponse, Background),
     (InviteChannelMember, Foreground),
-    (JoinChannel2, Foreground),
     (JoinChannel, Foreground),
     (JoinChannelBuffer, Foreground),
     (JoinChannelBufferResponse, Foreground),
@@ -209,9 +208,6 @@ messages!(
     (JoinRoom, Foreground),
     (JoinRoomResponse, Foreground),
     (LeaveChannelBuffer, Background),
-    (JoinChannelCall, Foreground),
-    (JoinChannelCallResponse, Foreground),
-    (LeaveChannelCall, Foreground),
     (LeaveChannelChat, Foreground),
     (LeaveProject, Foreground),
     (LeaveRoom, Foreground),
@@ -328,9 +324,6 @@ request_messages!(
     (InlayHints, InlayHintsResponse),
     (InviteChannelMember, Ack),
     (JoinChannel, JoinRoomResponse),
-    (JoinChannel2, JoinRoomResponse),
-    (JoinChannelCall, JoinChannelCallResponse),
-    (LeaveChannelCall, Ack),
     (JoinChannelBuffer, JoinChannelBufferResponse),
     (JoinChannelChat, JoinChannelChatResponse),
     (JoinProject, JoinProjectResponse),

crates/workspace/src/pane.rs 🔗

@@ -762,7 +762,6 @@ impl Pane {
         save_intent: SaveIntent,
         cx: &mut ViewContext<Self>,
     ) -> Task<Result<()>> {
-        println!("{}", std::backtrace::Backtrace::force_capture());
         self.close_items(cx, save_intent, move |view_id| view_id == item_id_to_close)
     }
 

crates/workspace/src/workspace.rs 🔗

@@ -12,7 +12,7 @@ mod toolbar;
 mod workspace_settings;
 
 use anyhow::{anyhow, Context as _, Result};
-use call::ActiveCall;
+use call::{call_settings::CallSettings, ActiveCall};
 use client::{
     proto::{self, ErrorCode, PeerId},
     Client, ErrorExt, Status, TypedEnvelope, UserStore,
@@ -1217,9 +1217,7 @@ impl Workspace {
             if let Some(active_call) = active_call {
                 if !quitting
                     && workspace_count == 1
-                    && active_call.read_with(&cx, |call, cx| {
-                        call.room().is_some_and(|room| room.read(cx).in_call())
-                    })?
+                    && active_call.read_with(&cx, |call, _| call.room().is_some())?
                 {
                     let answer = window.update(&mut cx, |_, cx| {
                         cx.prompt(
@@ -1232,11 +1230,12 @@ impl Workspace {
 
                     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();
                     }
-                    active_call
-                        .update(&mut cx, |call, cx| call.hang_up(cx))?
-                        .await
-                        .log_err();
                 }
             }
 
@@ -3999,6 +3998,8 @@ pub async fn last_opened_workspace_paths() -> Option<WorkspaceLocation> {
     DB.last_workspace().await.log_err().flatten()
 }
 
+actions!(collab, [OpenChannelNotes]);
+
 async fn join_channel_internal(
     channel_id: u64,
     app_state: &Arc<AppState>,
@@ -4100,6 +4101,36 @@ async fn join_channel_internal(
             return Some(join_remote_project(project, host, app_state.clone(), cx));
         }
 
+        // if you are the first to join a channel, share your project
+        if room.remote_participants().len() == 0 && !room.local_participant_is_guest() {
+            if let Some(workspace) = requesting_window {
+                let project = workspace.update(cx, |workspace, cx| {
+                    if !CallSettings::get_global(cx).share_on_join {
+                        return None;
+                    }
+                    let project = workspace.project.read(cx);
+                    if project.is_local()
+                        && project.visible_worktrees(cx).any(|tree| {
+                            tree.read(cx)
+                                .root_entry()
+                                .map_or(false, |entry| entry.is_dir())
+                        })
+                    {
+                        Some(workspace.project.clone())
+                    } else {
+                        None
+                    }
+                });
+                if let Ok(Some(project)) = project {
+                    return Some(cx.spawn(|room, mut cx| async move {
+                        room.update(&mut cx, |room, cx| room.share_project(project, cx))?
+                            .await?;
+                        Ok(())
+                    }));
+                }
+            }
+        }
+
         None
     })?;
     if let Some(task) = task {
@@ -4109,7 +4140,7 @@ async fn join_channel_internal(
     anyhow::Ok(false)
 }
 
-pub fn open_channel(
+pub fn join_channel(
     channel_id: u64,
     app_state: Arc<AppState>,
     requesting_window: Option<WindowHandle<Workspace>>,
@@ -4142,6 +4173,12 @@ pub fn open_channel(
                 })?
                 .await?;
 
+            if result.is_ok() {
+                cx.update(|cx| {
+                    cx.dispatch_action(&OpenChannelNotes);
+                }).log_err();
+            }
+
             active_window = Some(window_handle);
         }
 

crates/zed/src/main.rs 🔗

@@ -321,7 +321,7 @@ fn main() {
                 cx.spawn(|cx| async move {
                     // ignore errors here, we'll show a generic "not signed in"
                     let _ = authenticate(client, &cx).await;
-                    cx.update(|cx| workspace::open_channel(channel_id, app_state, None, cx))?
+                    cx.update(|cx| workspace::join_channel(channel_id, app_state, None, cx))?
                         .await?;
                     anyhow::Ok(())
                 })
@@ -376,7 +376,7 @@ fn main() {
                         cx.update(|mut cx| {
                             cx.spawn(|cx| async move {
                                 cx.update(|cx| {
-                                    workspace::open_channel(channel_id, app_state, None, cx)
+                                    workspace::join_channel(channel_id, app_state, None, cx)
                                 })?
                                 .await?;
                                 anyhow::Ok(())