diff --git a/assets/settings/default.json b/assets/settings/default.json index 7afa7f0d3b946a..6f575b47256049 100644 --- a/assets/settings/default.json +++ b/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": { diff --git a/crates/call/src/call.rs b/crates/call/src/call.rs index 10c747031079f8..11fc5490842823 100644 --- a/crates/call/src/call.rs +++ b/crates/call/src/call.rs @@ -84,7 +84,6 @@ pub struct ActiveCall { ), client: Arc, user_store: Model, - pending_channel_id: Option, _subscriptions: Vec, } @@ -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 { - self.pending_channel_id - } - async fn handle_incoming_call( this: Model, envelope: TypedEnvelope, @@ -345,13 +339,11 @@ impl ActiveCall { channel_id: u64, cx: &mut ModelContext, ) -> Task>>> { - 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) })?; diff --git a/crates/call/src/call_settings.rs b/crates/call/src/call_settings.rs index 441323ad5ffcf4..6aa42536891fbf 100644 --- a/crates/call/src/call_settings.rs +++ b/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, + + /// Whether your current project should be shared when joining an empty channel. + /// + /// Default: true + pub share_on_join: Option, } impl Settings for CallSettings { diff --git a/crates/call/src/participant.rs b/crates/call/src/participant.rs index dfbac1be9a6c86..9faefc63c36975 100644 --- a/crates/call/src/participant.rs +++ b/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>, pub audio_tracks: HashMap>, } diff --git a/crates/call/src/room.rs b/crates/call/src/room.rs index fa875e312d1fa9..cd8af385ed8bc3 100644 --- a/crates/call/src/room.rs +++ b/crates/call/src/room.rs @@ -61,7 +61,6 @@ pub struct Room { id: u64, channel_id: Option, live_kit: Option, - live_kit_connection_info: Option, status: RoomStatus, shared_projects: HashSet>, joined_projects: HashSet>, @@ -113,18 +112,91 @@ impl Room { user_store: Model, cx: &mut ModelContext, ) -> 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> { 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> { - 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, cx: &mut ModelContext, @@ -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 { + 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) { - 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) -> Task> { - 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) { + 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) { - 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, + ) -> Option>> { + 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, 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<()>, diff --git a/crates/collab/src/db/queries/channels.rs b/crates/collab/src/db/queries/channels.rs index 7c88cd8aa03cae..09ddd3746e1b28 100644 --- a/crates/collab/src/db/queries/channels.rs +++ b/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, 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)) }) diff --git a/crates/collab/src/db/queries/rooms.rs b/crates/collab/src/db/queries/rooms.rs index 1f8a445186933b..f8afbeab38130c 100644 --- a/crates/collab/src/db/queries/rooms.rs +++ b/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 { diff --git a/crates/collab/src/db/tables/room_participant.rs b/crates/collab/src/db/tables/room_participant.rs index c0edaf28ca81b8..c562111e96957c 100644 --- a/crates/collab/src/db/tables/room_participant.rs +++ b/crates/collab/src/db/tables/room_participant.rs @@ -20,7 +20,6 @@ pub struct Model { pub calling_connection_server_id: Option, pub participant_index: Option, pub role: Option, - pub in_call: bool, } impl Model { diff --git a/crates/collab/src/db/tests/channel_tests.rs b/crates/collab/src/db/tests/channel_tests.rs index 9de303ced4b9c6..a5e083f9356c5e 100644 --- a/crates/collab/src/db/tests/channel_tests.rs +++ b/crates/collab/src/db/tests/channel_tests.rs @@ -138,7 +138,6 @@ async fn test_joining_channels(db: &Arc) { .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) { .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) diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 38eff60ef03519..de1caf29b7f86a 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -105,7 +105,6 @@ struct Session { zed_environment: Arc, user_id: UserId, connection_id: ConnectionId, - zed_version: SemanticVersion, db: Arc>, peer: Arc, connection_pool: Arc>, @@ -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, mut send_connection_id: Option>, 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) -> Router { pub async fn handle_websocket_request( TypedHeader(ProtocolVersion(protocol_version)): TypedHeader, - app_version_header: Option>, + _app_version_header: Option>, ConnectInfo(socket_address): ConnectInfo, Extension(server): Extension>, Extension(user): Extension, @@ -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, 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, - 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, - 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, - 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 { Response::::send(self, result) } } -impl JoinChannelInternalResponse for Response { - fn send(self, result: proto::JoinRoomResponse) -> Result<()> { - Response::::send(self, result) - } -} async fn join_channel_internal( channel_id: ChannelId, - autojoin: bool, response: Box, 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, - user_id: &UserId, - role: ChannelRole, - live_kit_room: &String, -) -> Option { - 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, diff --git a/crates/collab/src/tests/channel_guest_tests.rs b/crates/collab/src/tests/channel_guest_tests.rs index b315b76ad92d0f..bb1f493f0c4034 100644 --- a/crates/collab/src/tests/channel_guest_tests.rs +++ b/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 diff --git a/crates/collab/src/tests/channel_tests.rs b/crates/collab/src/tests/channel_tests.rs index 95be2514a3f1b8..eda7377c777fb6 100644 --- a/crates/collab/src/tests/channel_tests.rs +++ 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 = diff --git a/crates/collab/src/tests/following_tests.rs b/crates/collab/src/tests/following_tests.rs index 7c930580f4c3ec..28fcc99271a1ad 100644 --- a/crates/collab/src/tests/following_tests.rs +++ b/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 } diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index 61bcbdc8846bb9..746f5aeeaf3b14 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -1881,7 +1881,7 @@ fn active_call_events(cx: &mut TestAppContext) -> Rc>> } #[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), &[ diff --git a/crates/collab/src/tests/test_server.rs b/crates/collab/src/tests/test_server.rs index e4bf377668c58e..62870b860c2c17 100644 --- a/crates/collab/src/tests/test_server.rs +++ 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, &'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> { - 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, diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 36782bcdd35419..a010abdbcb08ca 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/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::(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, ) -> 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, - ) -> 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::>(), - ); - - 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, ) -> 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, ) -> 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, ) { 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) { + fn join_channel(&self, channel_id: u64, cx: &mut ViewContext) { let Some(workspace) = self.workspace.upgrade() else { return; }; let Some(handle) = cx.window_handle().downcast::() 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) { - 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) { - 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) { @@ -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, ) -> 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::(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() diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index b149a683f4b12e..2d0177c34323d2 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -102,10 +102,6 @@ impl Render for CollabTitlebarItem { room.remote_participants().values().collect::>(); remote_participants.sort_by_key(|p| p.participant_index.0); - if !room.in_call() { - return this; - } - let current_user_face_pile = self.render_collaborator( ¤t_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) diff --git a/crates/collab_ui/src/collab_ui.rs b/crates/collab_ui/src/collab_ui.rs index 6b0a5c204364e1..76894ec17f5168 100644 --- a/crates/collab_ui/src/collab_ui.rs +++ b/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, 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, window_size: Size, diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index bfdf61f49e2498..f975a833c1eec2 100644 --- a/crates/editor/src/display_map.rs +++ b/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. diff --git a/crates/live_kit_client/src/test.rs b/crates/live_kit_client/src/test.rs index 421e23d3a2f180..677370c0b83219 100644 --- a/crates/live_kit_client/src/test.rs +++ b/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, } -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 } } diff --git a/crates/rpc/proto/buf.yaml b/crates/rpc/proto/buf.yaml new file mode 100644 index 00000000000000..93e819b2f771c2 --- /dev/null +++ b/crates/rpc/proto/buf.yaml @@ -0,0 +1,4 @@ +version: v1 +breaking: + use: + - WIRE diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 528cefe99d35ec..c60d3ae0d17700 100644 --- a/crates/rpc/proto/zed.proto +++ b/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; } diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index 6ebd93f6c8e085..9b885d1840f596 100644 --- a/crates/rpc/src/proto.rs +++ b/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), diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index fc5a82ee0f1753..55389cf8d0e2c4 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -762,7 +762,6 @@ impl Pane { save_intent: SaveIntent, cx: &mut ViewContext, ) -> Task> { - println!("{}", std::backtrace::Backtrace::force_capture()); self.close_items(cx, save_intent, move |view_id| view_id == item_id_to_close) } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 6723e5c9099d13..33531771179008 100644 --- a/crates/workspace/src/workspace.rs +++ b/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 { DB.last_workspace().await.log_err().flatten() } +actions!(collab, [OpenChannelNotes]); + async fn join_channel_internal( channel_id: u64, app_state: &Arc, @@ -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, requesting_window: Option>, @@ -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); } diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index b90c43b809768e..44c78cc2262ec0 100644 --- a/crates/zed/src/main.rs +++ b/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(())