Detailed changes
@@ -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": {
@@ -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<bool>,
-
- /// Whether your current project should be shared when joining an empty channel.
- ///
- /// Default: true
- pub share_on_join: Option<bool>,
}
impl Settings for CallSettings {
@@ -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<live_kit_client::Sid, Arc<RemoteVideoTrack>>,
pub audio_tracks: HashMap<live_kit_client::Sid, Arc<RemoteAudioTrack>>,
}
@@ -61,6 +61,7 @@ pub struct Room {
id: u64,
channel_id: Option<u64>,
live_kit: Option<LiveKitRoom>,
+ live_kit_connection_info: Option<proto::LiveKitConnectionInfo>,
status: RoomStatus,
shared_projects: HashSet<WeakModel<Project>>,
joined_projects: HashSet<WeakModel<Project>>,
@@ -112,91 +113,18 @@ impl Room {
user_store: Model<UserStore>,
cx: &mut ModelContext<Self>,
) -> Self {
- let live_kit_room = if let Some(connection_info) = live_kit_connection_info {
- let room = live_kit_client::Room::new();
- let mut status = room.status();
- // Consume the initial status of the room.
- let _ = status.try_recv();
- let _maintain_room = cx.spawn(|this, mut cx| async move {
- while let Some(status) = status.next().await {
- let this = if let Some(this) = this.upgrade() {
- this
- } else {
- break;
- };
-
- if status == live_kit_client::ConnectionState::Disconnected {
- this.update(&mut cx, |this, cx| this.leave(cx).log_err())
- .ok();
- break;
- }
- }
- });
-
- let _handle_updates = cx.spawn({
- let room = room.clone();
- move |this, mut cx| async move {
- let mut updates = room.updates();
- while let Some(update) = updates.next().await {
- let this = if let Some(this) = this.upgrade() {
- this
- } else {
- break;
- };
-
- this.update(&mut cx, |this, cx| {
- this.live_kit_room_updated(update, cx).log_err()
- })
- .ok();
- }
- }
- });
-
- let connect = room.connect(&connection_info.server_url, &connection_info.token);
- cx.spawn(|this, mut cx| async move {
- connect.await?;
- this.update(&mut cx, |this, cx| {
- if !this.read_only() {
- if let Some(live_kit) = &this.live_kit {
- if !live_kit.muted_by_user && !live_kit.deafened {
- return this.share_microphone(cx);
- }
- }
- }
- Task::ready(Ok(()))
- })?
- .await
- })
- .detach_and_log_err(cx);
-
- Some(LiveKitRoom {
- room,
- screen_track: LocalTrack::None,
- microphone_track: LocalTrack::None,
- next_publish_id: 0,
- muted_by_user: Self::mute_on_join(cx),
- deafened: false,
- speaking: false,
- _maintain_room,
- _handle_updates,
- })
- } else {
- None
- };
-
let maintain_connection = cx.spawn({
let client = client.clone();
move |this, cx| Self::maintain_connection(this, client.clone(), cx).log_err()
});
- Audio::play_sound(Sound::Joined, cx);
-
let (room_update_completed_tx, room_update_completed_rx) = watch::channel();
- 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<Model<Self>> {
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<Arc<User>> {
+ self.remote_participants()
+ .values()
+ .filter_map(|participant| {
+ if participant.in_call {
+ Some(participant.user.clone())
+ } else {
+ None
+ }
+ })
+ .chain(if self.in_call() {
+ self.user_store.read(cx).current_user()
+ } else {
+ None
+ })
+ .collect()
+ }
+
pub fn remote_participant_for_peer_id(&self, peer_id: PeerId) -> Option<&RemoteParticipant> {
self.remote_participants
.values()
@@ -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<Project>,
cx: &mut ModelContext<Self>,
@@ -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<bool> {
- 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<Self>) {
- 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 muted = !self.is_muted();
+ if let Some(task) = self.set_mute(muted, cx) {
+ task.detach_and_log_err(cx);
+ }
+ }
- if let Some(task) = self.set_mute(muted, cx) {
- task.detach_and_log_err(cx);
- }
+ pub fn join_call(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
+ if self.live_kit.is_some() {
+ return Task::ready(Ok(()));
+ }
- if should_undeafen {
- if let Some(task) = self.set_deafened(false, 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 status == live_kit_client::ConnectionState::Disconnected {
+ this.update(&mut cx, |this, cx| this.leave(cx).log_err())
+ .ok();
+ break;
}
}
- }
- }
+ });
- pub fn toggle_deafen(&mut self, cx: &mut ModelContext<Self>) {
- if let Some(live_kit) = self.live_kit.as_mut() {
- // When deafening, mute the microphone if it was not already muted.
- // When un-deafening, unmute the microphone, unless it was explicitly muted.
- let deafened = !live_kit.deafened;
- live_kit.deafened = deafened;
- let should_change_mute = !live_kit.muted_by_user;
+ 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_deafened(deafened, cx) {
- task.detach_and_log_err(cx);
+ 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![];
+ };
- if should_change_mute {
- if let Some(task) = self.set_mute(deafened, cx) {
- task.detach_and_log_err(cx);
+ 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<Self>) {
+ Audio::play_sound(Sound::Leave, cx);
+ if let Some(channel_id) = self.channel_id() {
+ let client = self.client.clone();
+ cx.background_executor()
+ .spawn(client.request(proto::LeaveChannelCall { channel_id }))
+ .detach_and_log_err(cx);
+ self.live_kit.take();
+ self.live_kit_connection_info.take();
+ cx.notify();
+ } else {
+ self.leave(cx).detach_and_log_err(cx)
}
}
@@ -1570,40 +1601,6 @@ impl Room {
}
}
- fn set_deafened(
- &mut self,
- deafened: bool,
- cx: &mut ModelContext<Self>,
- ) -> Option<Task<Result<()>>> {
- let live_kit = self.live_kit.as_mut()?;
- cx.notify();
-
- let mut track_updates = Vec::new();
- for participant in self.remote_participants.values() {
- for publication in live_kit
- .room
- .remote_audio_track_publications(&participant.user.id.to_string())
- {
- track_updates.push(publication.set_enabled(!deafened));
- }
-
- for track in participant.audio_tracks.values() {
- if deafened {
- track.stop();
- } else {
- track.start();
- }
- }
- }
-
- Some(cx.foreground_executor().spawn(async move {
- for result in futures::future::join_all(track_updates).await {
- result?;
- }
- Ok(())
- }))
- }
-
fn set_mute(
&mut self,
should_mute: bool,
@@ -1648,9 +1645,6 @@ struct LiveKitRoom {
room: Arc<live_kit_client::Room>,
screen_track: LocalTrack,
microphone_track: LocalTrack,
- /// Tracks whether we're currently in a muted state due to auto-mute from deafening or manual mute performed by user.
- muted_by_user: bool,
- deafened: bool,
speaking: bool,
next_publish_id: usize,
_maintain_room: Task<()>,
@@ -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");
@@ -0,0 +1,3 @@
+-- Add migration script here
+
+ALTER TABLE room_participants ADD COLUMN in_call BOOL NOT NULL DEFAULT FALSE;
@@ -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<MembershipUpdated>, 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))
})
@@ -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 {
@@ -20,6 +20,7 @@ pub struct Model {
pub calling_connection_server_id: Option<ServerId>,
pub participant_index: Option<i32>,
pub role: Option<ChannelRole>,
+ pub in_call: bool,
}
impl Model {
@@ -138,6 +138,7 @@ async fn test_joining_channels(db: &Arc<Database>) {
.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<Database>) {
.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)
@@ -105,6 +105,7 @@ struct Session {
zed_environment: Arc<str>,
user_id: UserId,
connection_id: ConnectionId,
+ zed_version: SemanticVersion,
db: Arc<tokio::sync::Mutex<DbHandle>>,
peer: Arc<Peer>,
connection_pool: Arc<parking_lot::Mutex<ConnectionPool>>,
@@ -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<User>,
mut send_connection_id: Option<oneshot::Sender<ConnectionId>>,
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<Server>) -> Router<Body> {
pub async fn handle_websocket_request(
TypedHeader(ProtocolVersion(protocol_version)): TypedHeader<ProtocolVersion>,
- _app_version_header: Option<TypedHeader<AppVersionHeader>>,
+ app_version_header: Option<TypedHeader<AppVersionHeader>>,
ConnectInfo(socket_address): ConnectInfo<SocketAddr>,
Extension(server): Extension<Arc<Server>>,
Extension(user): Extension<User>,
@@ -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<proto::JoinChannel>,
session: Session,
+) -> Result<()> {
+ session.endpoint_removed_in("join_channel", "0.123.0".parse().unwrap())?;
+
+ let channel_id = ChannelId::from_proto(request.channel_id);
+ join_channel_internal(channel_id, true, Box::new(response), session).await
+}
+
+async fn join_channel2(
+ request: proto::JoinChannel2,
+ response: Response<proto::JoinChannel2>,
+ session: Session,
+) -> Result<()> {
+ let channel_id = ChannelId::from_proto(request.channel_id);
+ join_channel_internal(channel_id, false, Box::new(response), session).await
+}
+
+async fn join_channel_call(
+ request: proto::JoinChannelCall,
+ response: Response<proto::JoinChannelCall>,
+ session: Session,
+) -> Result<()> {
+ let channel_id = ChannelId::from_proto(request.channel_id);
+ let db = session.db().await;
+ let (joined_room, role) = db
+ .set_in_channel_call(channel_id, session.user_id, true)
+ .await?;
+
+ let Some(connection_info) = session.live_kit_client.as_ref().and_then(|live_kit| {
+ live_kit_info_for_user(live_kit, &session.user_id, role, &joined_room.live_kit_room)
+ }) else {
+ Err(anyhow!("no live kit token info"))?
+ };
+
+ room_updated(&joined_room, &session.peer);
+ response.send(proto::JoinChannelCallResponse {
+ live_kit_connection_info: Some(connection_info),
+ })?;
+
+ Ok(())
+}
+
+async fn leave_channel_call(
+ request: proto::LeaveChannelCall,
+ response: Response<proto::LeaveChannelCall>,
+ session: Session,
) -> Result<()> {
let channel_id = ChannelId::from_proto(request.channel_id);
- join_channel_internal(channel_id, Box::new(response), session).await
+ 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<proto::JoinRoom> {
Response::<proto::JoinRoom>::send(self, result)
}
}
+impl JoinChannelInternalResponse for Response<proto::JoinChannel2> {
+ fn send(self, result: proto::JoinRoomResponse) -> Result<()> {
+ Response::<proto::JoinChannel2>::send(self, result)
+ }
+}
async fn join_channel_internal(
channel_id: ChannelId,
+ autojoin: bool,
response: Box<impl JoinChannelInternalResponse>,
session: Session,
) -> Result<()> {
@@ -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<dyn live_kit_server::api::Client>,
+ user_id: &UserId,
+ role: ChannelRole,
+ live_kit_room: &String,
+) -> Option<LiveKitConnectionInfo> {
+ let (can_publish, token) = if role == ChannelRole::Guest {
+ (
+ false,
+ live_kit
+ .guest_token(live_kit_room, &user_id.to_string())
+ .trace_err()?,
+ )
+ } else {
+ (
+ true,
+ live_kit
+ .room_token(live_kit_room, &user_id.to_string())
+ .trace_err()?,
+ )
+ };
+
+ Some(LiveKitConnectionInfo {
+ server_url: live_kit.url().into(),
+ token,
+ can_publish,
+ })
+}
+
/// Start editing the channel notes
async fn join_channel_buffer(
request: proto::JoinChannelBuffer,
@@ -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
@@ -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 =
@@ -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
}
@@ -1881,7 +1881,7 @@ fn active_call_events(cx: &mut TestAppContext) -> Rc<RefCell<Vec<room::Event>>>
}
#[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),
&[
@@ -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<Workspace>, &'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<anyhow::Result<()>> {
+ 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();
@@ -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::<CollabPanel>(cx);
});
- workspace.register_action(|_, _: &OpenChannelNotes, cx| {
- let channel_id = ActiveCall::global(cx)
- .read(cx)
- .room()
- .and_then(|room| room.read(cx).channel_id());
-
- if let Some(channel_id) = channel_id {
- let workspace = cx.view().clone();
- cx.window_context().defer(move |cx| {
- ChannelView::open(channel_id, None, workspace, cx).detach_and_log_err(cx)
- });
- }
- });
})
.detach();
}
@@ -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<Self>,
) -> ListItem {
let user_id = user.id;
- let is_current_user =
- self.user_store.read(cx).current_user().map(|user| user.id) == Some(user_id);
let tooltip = format!("Follow {}", user.github_login);
let is_call_admin = ActiveCall::global(cx).read(cx).room().is_some_and(|room| {
@@ -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<Self>,
+ ) -> impl IntoElement {
+ let (is_in_call, call_participants) = ActiveCall::global(cx)
+ .read(cx)
+ .room()
+ .map(|room| (room.read(cx).in_call(), room.read(cx).call_participants(cx)))
+ .unwrap_or_default();
+
+ const FACEPILE_LIMIT: usize = 3;
+
+ let face_pile = if !call_participants.is_empty() {
+ let extra_count = call_participants.len().saturating_sub(FACEPILE_LIMIT);
+ let result = FacePile::new(
+ call_participants
+ .iter()
+ .map(|user| Avatar::new(user.avatar_uri.clone()).into_any_element())
+ .take(FACEPILE_LIMIT)
+ .chain(if extra_count > 0 {
+ Some(
+ div()
+ .ml_2()
+ .child(Label::new(format!("+{extra_count}")))
+ .into_any_element(),
+ )
+ } else {
+ None
+ })
+ .collect::<SmallVec<_>>(),
+ );
+
+ Some(result)
+ } else {
+ None
+ };
+
+ ListItem::new("channel-call")
+ .selected(is_selected)
+ .start_slot(
+ h_flex()
+ .gap_1()
+ .child(render_tree_branch(false, true, cx))
+ .child(IconButton::new(0, IconName::AudioOn)),
+ )
+ .when(is_in_call, |el| {
+ el.end_slot(
+ IconButton::new(1, IconName::Exit)
+ .style(ButtonStyle::Filled)
+ .shape(ui::IconButtonShape::Square)
+ .tooltip(|cx| Tooltip::text("Leave call", cx))
+ .on_click(cx.listener(|this, _, cx| this.leave_channel_call(cx))),
+ )
+ })
+ .when(!is_in_call, |el| {
+ el.tooltip(move |cx| Tooltip::text("Join audio call", cx))
+ .on_click(cx.listener(move |this, _, cx| {
+ this.join_channel_call(channel_id, cx);
+ }))
+ })
+ .child(
+ div()
+ .text_ui()
+ .when(!call_participants.is_empty(), |el| {
+ el.font_weight(FontWeight::SEMIBOLD)
+ })
+ .child("call"),
+ )
+ .children(face_pile)
+ }
+
fn render_channel_notes(
&self,
channel_id: ChannelId,
is_selected: bool,
cx: &mut ViewContext<Self>,
) -> impl IntoElement {
+ let channel_store = self.channel_store.read(cx);
+ let has_notes_notification = channel_store.has_channel_buffer_changed(channel_id);
+
ListItem::new("channel-notes")
.selected(is_selected)
.on_click(cx.listener(move |this, _, cx| {
@@ -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<Self>,
) -> impl IntoElement {
+ let channel_store = self.channel_store.read(cx);
+ let has_messages_notification = channel_store.has_new_messages(channel_id);
ListItem::new("channel-chat")
.selected(is_selected)
.on_click(cx.listener(move |this, _, cx| {
@@ -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<Self>,
) {
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<Self>) {
+ fn open_channel(&self, channel_id: u64, cx: &mut ViewContext<Self>) {
let Some(workspace) = self.workspace.upgrade() else {
return;
};
let Some(handle) = cx.window_handle().downcast::<Workspace>() else {
return;
};
- 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<Self>) {
+ let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() else {
+ return;
+ };
+
+ room.update(cx, |room, cx| room.join_call(cx))
+ .detach_and_prompt_err("Failed to join call", cx, |_, _| None)
+ }
+
+ fn leave_channel_call(&mut self, cx: &mut ViewContext<Self>) {
+ let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() else {
+ return;
+ };
+
+ room.update(cx, |room, cx| room.leave_call(cx));
+ }
+
fn join_channel_chat(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
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<Self>,
) -> 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::<AnyElement>(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()
@@ -102,6 +102,10 @@ impl Render for CollabTitlebarItem {
room.remote_participants().values().collect::<Vec<_>>();
remote_participants.sort_by_key(|p| p.participant_index.0);
+ if !room.in_call() {
+ return this;
+ }
+
let current_user_face_pile = self.render_collaborator(
¤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)
@@ -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<AppState>, 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<dyn PlatformDisplay>,
window_size: Size<Pixels>,
@@ -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.
@@ -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<Sid, proto::ParticipantPermission>,
}
+impl Drop for TestServerRoom {
+ fn drop(&mut self) {
+ for room in self.client_rooms.values() {
+ let mut state = room.0.lock();
+ *state.connection.0.borrow_mut() = ConnectionState::Disconnected;
+ }
+ }
+}
+
#[derive(Debug)]
struct TestServerVideoTrack {
sid: Sid,
@@ -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
}
}
@@ -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;
}
@@ -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),
@@ -762,6 +762,7 @@ impl Pane {
save_intent: SaveIntent,
cx: &mut ViewContext<Self>,
) -> Task<Result<()>> {
+ println!("{}", std::backtrace::Backtrace::force_capture());
self.close_items(cx, save_intent, move |view_id| view_id == item_id_to_close)
}
@@ -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<WorkspaceLocation> {
DB.last_workspace().await.log_err().flatten()
}
-actions!(collab, [OpenChannelNotes]);
-
async fn join_channel_internal(
channel_id: u64,
app_state: &Arc<AppState>,
@@ -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<AppState>,
requesting_window: Option<WindowHandle<Workspace>>,
@@ -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);
}
@@ -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(())