diff --git a/assets/settings/default.json b/assets/settings/default.json index 06e0b98a41..139d64673f 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -104,10 +104,8 @@ "show_whitespaces": "selection", // Settings related to calls in Zed "calls": { - // 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 + // Join calls with the microphone muted by default + "mute_on_join": false }, // Toolbar related settings "toolbar": { diff --git a/crates/call/src/call_settings.rs b/crates/call/src/call_settings.rs index 6aa4253689..441323ad5f 100644 --- a/crates/call/src/call_settings.rs +++ b/crates/call/src/call_settings.rs @@ -7,7 +7,6 @@ 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. @@ -17,11 +16,6 @@ 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 9faefc63c3..dfbac1be9a 100644 --- a/crates/call/src/participant.rs +++ b/crates/call/src/participant.rs @@ -49,6 +49,7 @@ 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 cd8af385ed..fa875e312d 100644 --- a/crates/call/src/room.rs +++ b/crates/call/src/room.rs @@ -61,6 +61,7 @@ pub struct Room { id: u64, channel_id: Option, live_kit: Option, + live_kit_connection_info: Option, status: RoomStatus, shared_projects: HashSet>, joined_projects: HashSet>, @@ -112,91 +113,18 @@ 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(); - Self { + let mut this = Self { id, channel_id, - live_kit: live_kit_room, + live_kit: None, + live_kit_connection_info, status: RoomStatus::Online, shared_projects: Default::default(), joined_projects: Default::default(), @@ -220,7 +148,11 @@ 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( @@ -279,7 +211,7 @@ impl Room { cx: AsyncAppContext, ) -> Result> { Self::from_join_response( - client.request(proto::JoinChannel { channel_id }).await?, + client.request(proto::JoinChannel2 { channel_id }).await?, client, user_store, cx, @@ -324,7 +256,7 @@ impl Room { } pub fn mute_on_join(cx: &AppContext) -> bool { - CallSettings::get_global(cx).mute_on_join || client::IMPERSONATE_LOGIN.is_some() + CallSettings::get_global(cx).mute_on_join } fn from_join_response( @@ -374,7 +306,9 @@ impl Room { } log::info!("leaving room"); - Audio::play_sound(Sound::Leave, cx); + if self.live_kit.is_some() { + Audio::play_sound(Sound::Leave, cx); + } self.clear_state(cx); @@ -593,6 +527,24 @@ 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() @@ -617,10 +569,6 @@ 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, @@ -828,6 +776,7 @@ 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) = @@ -838,9 +787,15 @@ 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, }); @@ -857,12 +812,15 @@ impl Room { role, muted: true, speaking: false, + in_call: participant.in_call, video_tracks: Default::default(), audio_tracks: Default::default(), }, ); - Audio::play_sound(Sound::Joined, cx); + if participant.in_call { + Audio::play_sound(Sound::Joined, cx); + } if let Some(live_kit) = this.live_kit.as_ref() { let video_tracks = @@ -1051,15 +1009,6 @@ 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 @@ -1206,7 +1155,7 @@ impl Room { }) } - pub fn share_project( + pub(crate) fn share_project( &mut self, project: Model, cx: &mut ModelContext, @@ -1308,18 +1257,14 @@ 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(false, |live_kit| { - matches!(live_kit.microphone_track, LocalTrack::None) - || live_kit.muted_by_user - || live_kit.deafened - }) + 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(), + }) } pub fn read_only(&self) -> bool { @@ -1333,8 +1278,8 @@ impl Room { .map_or(false, |live_kit| live_kit.speaking) } - pub fn is_deafened(&self) -> Option { - self.live_kit.as_ref().map(|live_kit| live_kit.deafened) + pub fn in_call(&self) -> bool { + self.live_kit.is_some() } #[track_caller] @@ -1387,12 +1332,8 @@ 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, }; @@ -1496,50 +1437,140 @@ impl Room { } pub fn toggle_mute(&mut self, cx: &mut ModelContext) { - 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; - - if let Some(task) = self.set_mute(muted, cx) { - task.detach_and_log_err(cx); - } - - if should_undeafen { - if let Some(task) = self.set_deafened(false, cx) { - task.detach_and_log_err(cx); - } - } + let muted = !self.is_muted(); + if let Some(task) = self.set_mute(muted, cx) { + task.detach_and_log_err(cx); } } - 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; + pub fn join_call(&mut self, cx: &mut ModelContext) -> Task> { + if self.live_kit.is_some() { + return Task::ready(Ok(())); + } - if let Some(task) = self.set_deafened(deafened, cx) { - task.detach_and_log_err(cx); - } + 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 should_change_mute { - if let Some(task) = self.set_mute(deafened, cx) { - task.detach_and_log_err(cx); + 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(); + } + } + }); + + 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 + })?; + + if share_microphone { + this.update(&mut cx, |this, cx| this.share_microphone(cx))? + .await? + }; + + for result in futures::future::join_all(track_updates).await { + result?; + } + 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) } } @@ -1570,40 +1601,6 @@ 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, @@ -1648,9 +1645,6 @@ 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/migrations.sqlite/20221109000000_test_schema.sql b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql index 7be5725b68..1026fdea0d 100644 --- a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql +++ b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql @@ -163,7 +163,8 @@ CREATE TABLE "room_participants" ( "calling_connection_id" INTEGER NOT NULL, "calling_connection_server_id" INTEGER REFERENCES servers (id) ON DELETE SET NULL, "participant_index" INTEGER, - "role" TEXT + "role" TEXT, + "in_call" BOOLEAN NOT NULL DEFAULT FALSE ); CREATE UNIQUE INDEX "index_room_participants_on_user_id" ON "room_participants" ("user_id"); CREATE INDEX "index_room_participants_on_room_id" ON "room_participants" ("room_id"); diff --git a/crates/collab/migrations/20240207041417_add_in_call_column_to_room_participants.sql b/crates/collab/migrations/20240207041417_add_in_call_column_to_room_participants.sql new file mode 100644 index 0000000000..09463c6e78 --- /dev/null +++ b/crates/collab/migrations/20240207041417_add_in_call_column_to_room_participants.sql @@ -0,0 +1,3 @@ +-- Add migration script here + +ALTER TABLE room_participants ADD COLUMN in_call BOOL NOT NULL DEFAULT FALSE; diff --git a/crates/collab/src/db/queries/channels.rs b/crates/collab/src/db/queries/channels.rs index 76337381f7..c37ea26fc1 100644 --- a/crates/collab/src/db/queries/channels.rs +++ b/crates/collab/src/db/queries/channels.rs @@ -97,11 +97,57 @@ 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)> { @@ -166,7 +212,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, connection, role, &*tx) + self.join_channel_room_internal(room_id, user_id, autojoin, 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 f8afbeab38..1f8a445186 100644 --- a/crates/collab/src/db/queries/rooms.rs +++ b/crates/collab/src/db/queries/rooms.rs @@ -135,6 +135,7 @@ 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, @@ -187,6 +188,7 @@ 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, @@ -414,6 +416,7 @@ impl Database { &self, room_id: RoomId, user_id: UserId, + autojoin: bool, connection: ConnectionId, role: ChannelRole, tx: &DatabaseTransaction, @@ -437,6 +440,8 @@ 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, @@ -1258,6 +1263,7 @@ 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 c562111e96..c0edaf28ca 100644 --- a/crates/collab/src/db/tables/room_participant.rs +++ b/crates/collab/src/db/tables/room_participant.rs @@ -20,6 +20,7 @@ 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 a5e083f935..9de303ced4 100644 --- a/crates/collab/src/db/tests/channel_tests.rs +++ b/crates/collab/src/db/tests/channel_tests.rs @@ -138,6 +138,7 @@ async fn test_joining_channels(db: &Arc) { .join_channel( channel_1, user_1, + false, ConnectionId { owner_id, id: 1 }, TEST_RELEASE_CHANNEL, ) @@ -732,9 +733,15 @@ async fn test_guest_access(db: &Arc) { .await .is_err()); - db.join_channel(zed_channel, guest, guest_connection, TEST_RELEASE_CHANNEL) - .await - .unwrap(); + db.join_channel( + zed_channel, + guest, + false, + 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 e37681cd32..01fd0c1a8a 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -105,6 +105,7 @@ struct Session { zed_environment: Arc, user_id: UserId, connection_id: ConnectionId, + zed_version: SemanticVersion, db: Arc>, peer: Arc, connection_pool: Arc>, @@ -131,6 +132,19 @@ 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 { @@ -274,8 +288,11 @@ 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) @@ -559,6 +576,7 @@ impl Server { connection: Connection, address: String, user: User, + zed_version: SemanticVersion, impersonator: Option, mut send_connection_id: Option>, executor: Executor, @@ -616,6 +634,7 @@ 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(), @@ -866,7 +885,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, @@ -881,6 +900,12 @@ 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; @@ -895,6 +920,7 @@ pub async fn handle_websocket_request( connection, socket_address, user, + app_version, impersonator.0, None, Executor::Production, @@ -1037,7 +1063,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, Box::new(response), session).await; + return join_channel_internal(channel_id, true, Box::new(response), session).await; } let joined_room = { @@ -2700,14 +2726,67 @@ async fn respond_to_channel_invite( Ok(()) } -/// Join the channels' room +/// Join the channels' call 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, Box::new(response), session).await + 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(()) } trait JoinChannelInternalResponse { @@ -2723,9 +2802,15 @@ 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<()> { @@ -2737,39 +2822,22 @@ 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| { - 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, - }) + if !autojoin { + return None; + } + live_kit_info_for_user( + live_kit, + &session.user_id, + role, + &joined_room.room.live_kit_room, + ) }); response.send(proto::JoinRoomResponse { @@ -2805,6 +2873,35 @@ 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 bb1f493f0c..b315b76ad9 100644 --- a/crates/collab/src/tests/channel_guest_tests.rs +++ b/crates/collab/src/tests/channel_guest_tests.rs @@ -1,4 +1,7 @@ -use crate::{db::ChannelId, tests::TestServer}; +use crate::{ + db::ChannelId, + tests::{test_server::join_channel_call, TestServer}, +}; use call::ActiveCall; use editor::Editor; use gpui::{BackgroundExecutor, TestAppContext}; @@ -32,7 +35,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::join_channel(channel_id, client_b.app_state.clone(), None, cx)) + cx_b.update(|cx| workspace::open_channel(channel_id, client_b.app_state.clone(), None, cx)) .await .unwrap(); @@ -72,7 +75,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::join_channel(channel_id, client_a.app_state.clone(), None, cx)) + cx_a.update(|cx| workspace::open_channel(channel_id, client_a.app_state.clone(), None, cx)) .await .unwrap(); @@ -84,11 +87,13 @@ 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::join_channel(channel_id, client_b.app_state.clone(), None, cx)) + cx_b.update(|cx| workspace::open_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 eda7377c77..95be2514a3 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, RoomParticipants, TestServer}, + tests::{room_participants, test_server::join_channel_call, RoomParticipants, TestServer}, }; use call::ActiveCall; use channel::{ChannelId, ChannelMembership, ChannelStore}; @@ -382,6 +382,7 @@ 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(); @@ -429,7 +430,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| { @@ -552,6 +553,9 @@ 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 28fcc99271..7c930580f4 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(iterations = 10)] +#[gpui::test] async fn test_basic_following( cx_a: &mut TestAppContext, cx_b: &mut TestAppContext, @@ -437,6 +437,7 @@ async fn test_basic_following( }) .await .unwrap(); + executor.run_until_parked(); let shared_screen = workspace_a.update(cx_a, |workspace, cx| { workspace @@ -522,6 +523,7 @@ async fn test_basic_following( workspace_a.update(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)), None ); + executor.run_until_parked(); } #[gpui::test] @@ -2004,7 +2006,7 @@ async fn join_channel( client: &TestClient, cx: &mut TestAppContext, ) -> anyhow::Result<()> { - cx.update(|cx| workspace::join_channel(channel_id, client.app_state.clone(), None, cx)) + cx.update(|cx| workspace::open_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 746f5aeeaf..61bcbdc884 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_deafen( +async fn test_mute( executor: BackgroundExecutor, cx_a: &mut TestAppContext, cx_b: &mut TestAppContext, @@ -1920,7 +1920,7 @@ async fn test_mute_deafen( 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 muted. + // Users A and B are both unmuted. assert_eq!( participant_audio_state(&room_a, cx_a), &[ParticipantAudioState { @@ -1962,30 +1962,6 @@ async fn test_mute_deafen( }] ); - // 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| { @@ -2000,22 +1976,6 @@ async fn test_mute_deafen( .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 69e338b6ea..d7e19dc9a9 100644 --- a/crates/collab/src/tests/test_server.rs +++ b/crates/collab/src/tests/test_server.rs @@ -13,7 +13,7 @@ use client::{ use collections::{HashMap, HashSet}; use fs::FakeFs; use futures::{channel::oneshot, StreamExt as _}; -use gpui::{BackgroundExecutor, Context, Model, TestAppContext, View, VisualTestContext}; +use gpui::{BackgroundExecutor, Context, Model, Task, TestAppContext, View, VisualTestContext}; use language::LanguageRegistry; use node_runtime::FakeNodeRuntime; @@ -36,7 +36,7 @@ use std::{ Arc, }, }; -use util::http::FakeHttpClient; +use util::{http::FakeHttpClient, SemanticVersion}; use workspace::{Workspace, WorkspaceStore}; pub struct TestServer { @@ -230,6 +230,7 @@ impl TestServer { server_conn, client_name, user, + SemanticVersion::default(), None, Some(connection_id_tx), Executor::Deterministic(cx.background_executor().clone()), @@ -685,7 +686,7 @@ impl TestClient { channel_id: u64, cx: &'a mut TestAppContext, ) -> (View, &'a mut VisualTestContext) { - cx.update(|cx| workspace::join_channel(channel_id, self.app_state.clone(), None, cx)) + cx.update(|cx| workspace::open_channel(channel_id, self.app_state.clone(), None, cx)) .await .unwrap(); cx.run_until_parked(); @@ -760,6 +761,11 @@ 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)) +} + impl Drop for TestClient { fn drop(&mut self) { self.app_state.client.teardown(); diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index a010abdbcb..527e0baa53 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}, - OpenChannelNotes, Workspace, + Workspace, }; actions!( @@ -69,19 +69,6 @@ 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(); } @@ -175,6 +162,9 @@ enum ListEntry { depth: usize, has_children: bool, }, + ChannelCall { + channel_id: ChannelId, + }, ChannelNotes { channel_id: ChannelId, }, @@ -382,6 +372,7 @@ 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 }); } @@ -479,7 +470,7 @@ impl CollabPanel { && participant.video_tracks.is_empty(), }); } - if !participant.video_tracks.is_empty() { + if room.in_call() && !participant.video_tracks.is_empty() { self.entries.push(ListEntry::ParticipantScreen { peer_id: Some(participant.peer_id), is_last: true, @@ -832,8 +823,6 @@ 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| { @@ -846,12 +835,6 @@ 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 { @@ -953,12 +936,88 @@ 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| { @@ -970,7 +1029,14 @@ impl CollabPanel { .child(render_tree_branch(false, true, cx)) .child(IconButton::new(0, IconName::File)), ) - .child(Label::new("notes")) + .child( + div() + .text_ui() + .when(has_notes_notification, |el| { + el.font_weight(FontWeight::SEMIBOLD) + }) + .child("notes"), + ) .tooltip(move |cx| Tooltip::text("Open Channel Notes", cx)) } @@ -980,6 +1046,8 @@ 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| { @@ -991,7 +1059,14 @@ impl CollabPanel { .child(render_tree_branch(false, false, cx)) .child(IconButton::new(0, IconName::MessageBubbles)), ) - .child(Label::new("chat")) + .child( + div() + .text_ui() + .when(has_messages_notification, |el| { + el.font_weight(FontWeight::SEMIBOLD) + }) + .child("chat"), + ) .tooltip(move |cx| Tooltip::text("Open Chat", cx)) } @@ -1249,12 +1324,14 @@ impl CollabPanel { cx: &mut ViewContext, ) { let this = cx.view().clone(); - let in_room = ActiveCall::global(cx).read(cx).room().is_some(); + 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 context_menu = ContextMenu::build(cx, |mut context_menu, _| { let user_id = contact.user.id; - if contact.online && !contact.busy { + if contact.online && !contact.busy && (!in_room || in_call) { let label = if in_room { format!("Invite {} to join", contact.user.github_login) } else { @@ -1402,7 +1479,7 @@ impl CollabPanel { if is_active { self.open_channel_notes(channel.id, cx) } else { - self.join_channel(channel.id, cx) + self.open_channel(channel.id, cx) } } ListEntry::ContactPlaceholder => self.toggle_contact_finder(cx), @@ -1421,6 +1498,9 @@ 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) } @@ -1883,14 +1963,14 @@ impl CollabPanel { .detach_and_prompt_err("Call failed", cx, |_, _| None); } - fn join_channel(&self, channel_id: u64, cx: &mut ViewContext) { + fn open_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; }; - workspace::join_channel( + workspace::open_channel( channel_id, workspace.read(cx).app_state().clone(), Some(handle), @@ -1899,6 +1979,23 @@ impl CollabPanel { .detach_and_prompt_err("Failed to join channel", cx, |_, _| None) } + 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)); + } + fn join_channel_chat(&mut self, channel_id: ChannelId, cx: &mut ViewContext) { let Some(workspace) = self.workspace.upgrade() else { return; @@ -2024,6 +2121,9 @@ 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(), @@ -2089,7 +2189,6 @@ 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; @@ -2100,13 +2199,12 @@ impl CollabPanel { let channel = self.channel_store.read(cx).channel_for_id(channel_id)?; - channel_link = Some(channel.link()); (channel_icon, channel_tooltip_text) = match channel.visibility { proto::ChannelVisibility::Public => { - (Some("icons/public.svg"), Some("Copy public channel link.")) + (Some(IconName::Public), Some("Close Channel")) } proto::ChannelVisibility::Members => { - (Some("icons/hash.svg"), Some("Copy private channel link.")) + (Some(IconName::Hash), Some("Close Channel")) } }; @@ -2128,17 +2226,10 @@ impl CollabPanel { }; let button = match section { - 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)) + 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)) .into_any_element() }), Section::Contacts => Some( @@ -2173,6 +2264,9 @@ 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), @@ -2478,11 +2572,9 @@ impl CollabPanel { }), ) .on_click(cx.listener(move |this, _, cx| { - if is_active { - this.open_channel_notes(channel_id, cx) - } else { - this.join_channel(channel_id, cx) - } + this.open_channel(channel_id, cx); + this.open_channel_notes(channel_id, cx); + this.join_channel_chat(channel_id, cx); })) .on_secondary_mouse_down(cx.listener( move |this, event: &MouseDownEvent, cx| { @@ -2499,61 +2591,24 @@ impl CollabPanel { .color(Color::Muted), ) .child( - h_flex() - .id(channel_id as usize) - .child(Label::new(channel.name.clone())) - .children(face_pile.map(|face_pile| face_pile.p_1())), + 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()), + ), ), ) - .child( + .children(face_pile.map(|face_pile| { h_flex() .absolute() .right(rems(0.)) .z_index(1) .h_full() - .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("") - }), - ), - ), - ) + .child(face_pile.p_1()) + })) .tooltip({ let channel_store = self.channel_store.clone(); move |cx| { @@ -2757,6 +2812,14 @@ 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, @@ -2855,7 +2918,7 @@ impl Render for JoinChannelTooltip { .read(cx) .channel_participants(self.channel_id); - div.child(Label::new("Join Channel")) + div.child(Label::new("Open 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 2d0177c343..b149a683f4 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -102,6 +102,10 @@ 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, @@ -133,6 +137,10 @@ 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, @@ -185,7 +193,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_deafened = room.is_deafened().unwrap_or(false); + let is_connected_to_livekit = room.in_call(); let is_screen_sharing = room.is_screen_sharing(); let read_only = room.read_only(); @@ -220,22 +228,28 @@ impl Render for CollabTitlebarItem { )), ) }) - .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| { + .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| { this.child( IconButton::new( "mute-microphone", @@ -262,34 +276,7 @@ impl Render for CollabTitlebarItem { .on_click(move |_, cx| crate::toggle_mute(&Default::default(), cx)), ) }) - .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| { + .when(!read_only && is_connected_to_livekit, |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 76894ec17f..6b0a5c2043 100644 --- a/crates/collab_ui/src/collab_ui.rs +++ b/crates/collab_ui/src/collab_ui.rs @@ -22,10 +22,7 @@ pub use panel_settings::{ use settings::Settings; use workspace::{notifications::DetachAndPromptErr, AppState}; -actions!( - collab, - [ToggleScreenSharing, ToggleMute, ToggleDeafen, LeaveCall] -); +actions!(collab, [ToggleScreenSharing, ToggleMute, LeaveCall]); pub fn init(app_state: &Arc, cx: &mut AppContext) { CollaborationPanelSettings::register(cx); @@ -85,12 +82,6 @@ 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 f975a833c1..bfdf61f49e 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 677370c0b8..421e23d3a2 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"))? + .ok_or_else(|| anyhow!("no server found for url: {}", url))? .clone()) } @@ -160,7 +160,6 @@ 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 @@ -414,6 +413,15 @@ 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, @@ -694,11 +702,15 @@ impl LocalTrackPublication { pub fn is_muted(&self) -> bool { if let Some(room) = self.room.upgrade() { - room.test_server() - .is_track_muted(&room.token(), &self.sid) - .unwrap_or(false) + if room.is_connected() { + room.test_server() + .is_track_muted(&room.token(), &self.sid) + .unwrap_or(true) + } else { + true + } } else { - false + true } } diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 1823b9b6c5..528cefe99d 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -183,7 +183,12 @@ message Envelope { LspExtExpandMacroResponse lsp_ext_expand_macro_response = 155; SetRoomParticipantRole set_room_participant_role = 156; - UpdateUserChannels update_user_channels = 157; // current max + 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 } } @@ -291,6 +296,7 @@ message Participant { ParticipantLocation location = 4; uint32 participant_index = 5; ChannelRole role = 6; + bool in_call = 7; } message PendingParticipant { @@ -1033,6 +1039,22 @@ 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 9b885d1840..6ebd93f6c8 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -198,6 +198,7 @@ messages!( (InlayHints, Background), (InlayHintsResponse, Background), (InviteChannelMember, Foreground), + (JoinChannel2, Foreground), (JoinChannel, Foreground), (JoinChannelBuffer, Foreground), (JoinChannelBufferResponse, Foreground), @@ -208,6 +209,9 @@ messages!( (JoinRoom, Foreground), (JoinRoomResponse, Foreground), (LeaveChannelBuffer, Background), + (JoinChannelCall, Foreground), + (JoinChannelCallResponse, Foreground), + (LeaveChannelCall, Foreground), (LeaveChannelChat, Foreground), (LeaveProject, Foreground), (LeaveRoom, Foreground), @@ -324,6 +328,9 @@ 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 55389cf8d0..fc5a82ee0f 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -762,6 +762,7 @@ 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 89adb1e946..a4af8170aa 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::{call_settings::CallSettings, ActiveCall}; +use call::ActiveCall; use client::{ proto::{self, ErrorCode, PeerId}, Client, ErrorExt, Status, TypedEnvelope, UserStore, @@ -3977,8 +3977,6 @@ 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, @@ -4080,36 +4078,6 @@ 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 { @@ -4119,7 +4087,7 @@ async fn join_channel_internal( anyhow::Ok(false) } -pub fn join_channel( +pub fn open_channel( channel_id: u64, app_state: Arc, requesting_window: Option>, @@ -4152,12 +4120,6 @@ pub fn join_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 832712762e..de4004be32 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -314,7 +314,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::join_channel(channel_id, app_state, None, cx))? + cx.update(|cx| workspace::open_channel(channel_id, app_state, None, cx))? .await?; anyhow::Ok(()) }) @@ -369,7 +369,7 @@ fn main() { cx.update(|mut cx| { cx.spawn(|cx| async move { cx.update(|cx| { - workspace::join_channel(channel_id, app_state, None, cx) + workspace::open_channel(channel_id, app_state, None, cx) })? .await?; anyhow::Ok(())