single click channel (#7596)

- Open channel notes and chat on channel click
- WIP
- Fix compile error
- Don't join live kit until requested
- Track in_call state separately from in_room



Release Notes:

- Improved channels: you can now be in a channel without joining the
audio call automatically

**or**

- N/A

---------

Co-authored-by: Nathan Sobo <nathan@zed.dev>
This commit is contained in:
Conrad Irwin 2024-02-09 14:18:27 -07:00 committed by GitHub
parent 2b39a9512a
commit efe23ebfcd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 659 additions and 489 deletions

View file

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

View file

@ -7,7 +7,6 @@ use settings::Settings;
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct CallSettings { pub struct CallSettings {
pub mute_on_join: bool, pub mute_on_join: bool,
pub share_on_join: bool,
} }
/// Configuration of voice calls in Zed. /// Configuration of voice calls in Zed.
@ -17,11 +16,6 @@ pub struct CallSettingsContent {
/// ///
/// Default: false /// Default: false
pub mute_on_join: Option<bool>, 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 { impl Settings for CallSettings {

View file

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

View file

@ -61,6 +61,7 @@ pub struct Room {
id: u64, id: u64,
channel_id: Option<u64>, channel_id: Option<u64>,
live_kit: Option<LiveKitRoom>, live_kit: Option<LiveKitRoom>,
live_kit_connection_info: Option<proto::LiveKitConnectionInfo>,
status: RoomStatus, status: RoomStatus,
shared_projects: HashSet<WeakModel<Project>>, shared_projects: HashSet<WeakModel<Project>>,
joined_projects: HashSet<WeakModel<Project>>, joined_projects: HashSet<WeakModel<Project>>,
@ -112,91 +113,18 @@ impl Room {
user_store: Model<UserStore>, user_store: Model<UserStore>,
cx: &mut ModelContext<Self>, cx: &mut ModelContext<Self>,
) -> 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 maintain_connection = cx.spawn({
let client = client.clone(); let client = client.clone();
move |this, cx| Self::maintain_connection(this, client.clone(), cx).log_err() move |this, cx| Self::maintain_connection(this, client.clone(), cx).log_err()
}); });
Audio::play_sound(Sound::Joined, cx);
let (room_update_completed_tx, room_update_completed_rx) = watch::channel(); let (room_update_completed_tx, room_update_completed_rx) = watch::channel();
Self { let mut this = Self {
id, id,
channel_id, channel_id,
live_kit: live_kit_room, live_kit: None,
live_kit_connection_info,
status: RoomStatus::Online, status: RoomStatus::Online,
shared_projects: Default::default(), shared_projects: Default::default(),
joined_projects: Default::default(), joined_projects: Default::default(),
@ -220,7 +148,11 @@ impl Room {
maintain_connection: Some(maintain_connection), maintain_connection: Some(maintain_connection),
room_update_completed_tx, room_update_completed_tx,
room_update_completed_rx, 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( pub(crate) fn create(
@ -279,7 +211,7 @@ impl Room {
cx: AsyncAppContext, cx: AsyncAppContext,
) -> Result<Model<Self>> { ) -> Result<Model<Self>> {
Self::from_join_response( Self::from_join_response(
client.request(proto::JoinChannel { channel_id }).await?, client.request(proto::JoinChannel2 { channel_id }).await?,
client, client,
user_store, user_store,
cx, cx,
@ -324,7 +256,7 @@ impl Room {
} }
pub fn mute_on_join(cx: &AppContext) -> bool { 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( fn from_join_response(
@ -374,7 +306,9 @@ impl Room {
} }
log::info!("leaving 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); self.clear_state(cx);
@ -593,6 +527,24 @@ impl Room {
&self.remote_participants &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> { pub fn remote_participant_for_peer_id(&self, peer_id: PeerId) -> Option<&RemoteParticipant> {
self.remote_participants self.remote_participants
.values() .values()
@ -617,10 +569,6 @@ impl Room {
self.local_participant.role == proto::ChannelRole::Admin 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( pub fn set_participant_role(
&mut self, &mut self,
user_id: u64, user_id: u64,
@ -828,6 +776,7 @@ impl Room {
} }
let role = participant.role(); let role = participant.role();
let in_call = participant.in_call;
let location = ParticipantLocation::from_proto(participant.location) let location = ParticipantLocation::from_proto(participant.location)
.unwrap_or(ParticipantLocation::External); .unwrap_or(ParticipantLocation::External);
if let Some(remote_participant) = if let Some(remote_participant) =
@ -838,9 +787,15 @@ impl Room {
remote_participant.participant_index = participant_index; remote_participant.participant_index = participant_index;
if location != remote_participant.location if location != remote_participant.location
|| role != remote_participant.role || 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.location = location;
remote_participant.role = role; remote_participant.role = role;
remote_participant.in_call = participant.in_call;
cx.emit(Event::ParticipantLocationChanged { cx.emit(Event::ParticipantLocationChanged {
participant_id: peer_id, participant_id: peer_id,
}); });
@ -857,12 +812,15 @@ impl Room {
role, role,
muted: true, muted: true,
speaking: false, speaking: false,
in_call: participant.in_call,
video_tracks: Default::default(), video_tracks: Default::default(),
audio_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() { if let Some(live_kit) = this.live_kit.as_ref() {
let video_tracks = let video_tracks =
@ -1051,15 +1009,6 @@ impl Room {
} }
RoomUpdate::SubscribedToRemoteAudioTrack(track, publication) => { 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 user_id = track.publisher_id().parse()?;
let track_id = track.sid().to_string(); let track_id = track.sid().to_string();
let participant = self let participant = self
@ -1206,7 +1155,7 @@ impl Room {
}) })
} }
pub fn share_project( pub(crate) fn share_project(
&mut self, &mut self,
project: Model<Project>, project: Model<Project>,
cx: &mut ModelContext<Self>, 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 { pub fn is_muted(&self) -> bool {
self.live_kit.as_ref().map_or(false, |live_kit| { self.live_kit
matches!(live_kit.microphone_track, LocalTrack::None) .as_ref()
|| live_kit.muted_by_user .map_or(true, |live_kit| match &live_kit.microphone_track {
|| live_kit.deafened LocalTrack::None => true,
}) LocalTrack::Pending { .. } => true,
LocalTrack::Published { track_publication } => track_publication.is_muted(),
})
} }
pub fn read_only(&self) -> bool { pub fn read_only(&self) -> bool {
@ -1333,8 +1278,8 @@ impl Room {
.map_or(false, |live_kit| live_kit.speaking) .map_or(false, |live_kit| live_kit.speaking)
} }
pub fn is_deafened(&self) -> Option<bool> { pub fn in_call(&self) -> bool {
self.live_kit.as_ref().map(|live_kit| live_kit.deafened) self.live_kit.is_some()
} }
#[track_caller] #[track_caller]
@ -1387,12 +1332,8 @@ impl Room {
Ok(publication) => { Ok(publication) => {
if canceled { if canceled {
live_kit.room.unpublish_track(publication); live_kit.room.unpublish_track(publication);
live_kit.microphone_track = LocalTrack::None;
} else { } 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 { live_kit.microphone_track = LocalTrack::Published {
track_publication: publication, track_publication: publication,
}; };
@ -1496,50 +1437,140 @@ impl Room {
} }
pub fn toggle_mute(&mut self, cx: &mut ModelContext<Self>) { pub fn toggle_mute(&mut self, cx: &mut ModelContext<Self>) {
if let Some(live_kit) = self.live_kit.as_mut() { let muted = !self.is_muted();
// When unmuting, undeafen if the user was deafened before. if let Some(task) = self.set_mute(muted, cx) {
let was_deafened = live_kit.deafened; task.detach_and_log_err(cx);
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);
}
}
} }
} }
pub fn toggle_deafen(&mut self, cx: &mut ModelContext<Self>) { pub fn join_call(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
if let Some(live_kit) = self.live_kit.as_mut() { if self.live_kit.is_some() {
// When deafening, mute the microphone if it was not already muted. return Task::ready(Ok(()));
// When un-deafening, unmute the microphone, unless it was explicitly muted. }
let deafened = !live_kit.deafened;
live_kit.deafened = deafened;
let should_change_mute = !live_kit.muted_by_user;
if let Some(task) = self.set_deafened(deafened, cx) { let room = live_kit_client::Room::new();
task.detach_and_log_err(cx); 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 status == live_kit_client::ConnectionState::Disconnected {
if let Some(task) = self.set_mute(deafened, cx) { this.update(&mut cx, |this, cx| this.leave(cx).log_err())
task.detach_and_log_err(cx); .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<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( fn set_mute(
&mut self, &mut self,
should_mute: bool, should_mute: bool,
@ -1648,9 +1645,6 @@ struct LiveKitRoom {
room: Arc<live_kit_client::Room>, room: Arc<live_kit_client::Room>,
screen_track: LocalTrack, screen_track: LocalTrack,
microphone_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, speaking: bool,
next_publish_id: usize, next_publish_id: usize,
_maintain_room: Task<()>, _maintain_room: Task<()>,

View file

@ -163,7 +163,8 @@ CREATE TABLE "room_participants" (
"calling_connection_id" INTEGER NOT NULL, "calling_connection_id" INTEGER NOT NULL,
"calling_connection_server_id" INTEGER REFERENCES servers (id) ON DELETE SET NULL, "calling_connection_server_id" INTEGER REFERENCES servers (id) ON DELETE SET NULL,
"participant_index" INTEGER, "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 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"); CREATE INDEX "index_room_participants_on_room_id" ON "room_participants" ("room_id");

View file

@ -0,0 +1,3 @@
-- Add migration script here
ALTER TABLE room_participants ADD COLUMN in_call BOOL NOT NULL DEFAULT FALSE;

View file

@ -97,11 +97,57 @@ impl Database {
.await .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. /// Adds a user to the specified channel.
pub async fn join_channel( pub async fn join_channel(
&self, &self,
channel_id: ChannelId, channel_id: ChannelId,
user_id: UserId, user_id: UserId,
autojoin: bool,
connection: ConnectionId, connection: ConnectionId,
environment: &str, environment: &str,
) -> Result<(JoinRoom, Option<MembershipUpdated>, ChannelRole)> { ) -> Result<(JoinRoom, Option<MembershipUpdated>, ChannelRole)> {
@ -166,7 +212,7 @@ impl Database {
.get_or_create_channel_room(channel_id, &live_kit_room, environment, &*tx) .get_or_create_channel_room(channel_id, &live_kit_room, environment, &*tx)
.await?; .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 .await
.map(|jr| (jr, accept_invite_result, role)) .map(|jr| (jr, accept_invite_result, role))
}) })

View file

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

View file

@ -20,6 +20,7 @@ pub struct Model {
pub calling_connection_server_id: Option<ServerId>, pub calling_connection_server_id: Option<ServerId>,
pub participant_index: Option<i32>, pub participant_index: Option<i32>,
pub role: Option<ChannelRole>, pub role: Option<ChannelRole>,
pub in_call: bool,
} }
impl Model { impl Model {

View file

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

View file

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

View file

@ -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 call::ActiveCall;
use editor::Editor; use editor::Editor;
use gpui::{BackgroundExecutor, TestAppContext}; use gpui::{BackgroundExecutor, TestAppContext};
@ -32,7 +35,7 @@ async fn test_channel_guests(
cx_a.executor().run_until_parked(); cx_a.executor().run_until_parked();
// Client B joins channel A as a guest // 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 .await
.unwrap(); .unwrap();
@ -72,7 +75,7 @@ async fn test_channel_guest_promotion(cx_a: &mut TestAppContext, cx_b: &mut Test
.await; .await;
let project_a = client_a.build_test_project(cx_a).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 .await
.unwrap(); .unwrap();
@ -84,11 +87,13 @@ async fn test_channel_guest_promotion(cx_a: &mut TestAppContext, cx_b: &mut Test
cx_a.run_until_parked(); cx_a.run_until_parked();
// Client B joins channel A as a guest // 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 .await
.unwrap(); .unwrap();
cx_a.run_until_parked(); cx_a.run_until_parked();
join_channel_call(cx_b).await.unwrap();
// client B opens 1.txt as a guest // client B opens 1.txt as a guest
let (workspace_b, cx_b) = client_b.active_workspace(cx_b); let (workspace_b, cx_b) = client_b.active_workspace(cx_b);
let room_b = cx_b let room_b = cx_b

View file

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

View file

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

View file

@ -1881,7 +1881,7 @@ fn active_call_events(cx: &mut TestAppContext) -> Rc<RefCell<Vec<room::Event>>>
} }
#[gpui::test] #[gpui::test]
async fn test_mute_deafen( async fn test_mute(
executor: BackgroundExecutor, executor: BackgroundExecutor,
cx_a: &mut TestAppContext, cx_a: &mut TestAppContext,
cx_b: &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_a.read_with(cx_a, |room, _| assert!(!room.is_muted()));
room_b.read_with(cx_b, |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!( assert_eq!(
participant_audio_state(&room_a, cx_a), participant_audio_state(&room_a, cx_a),
&[ParticipantAudioState { &[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. // User B calls user C, C joins.
active_call_b active_call_b
.update(cx_b, |call, cx| { .update(cx_b, |call, cx| {
@ -2000,22 +1976,6 @@ async fn test_mute_deafen(
.unwrap(); .unwrap();
executor.run_until_parked(); 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!( assert_eq!(
participant_audio_state(&room_b, cx_b), participant_audio_state(&room_b, cx_b),
&[ &[

View file

@ -13,7 +13,7 @@ use client::{
use collections::{HashMap, HashSet}; use collections::{HashMap, HashSet};
use fs::FakeFs; use fs::FakeFs;
use futures::{channel::oneshot, StreamExt as _}; 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 language::LanguageRegistry;
use node_runtime::FakeNodeRuntime; use node_runtime::FakeNodeRuntime;
@ -36,7 +36,7 @@ use std::{
Arc, Arc,
}, },
}; };
use util::http::FakeHttpClient; use util::{http::FakeHttpClient, SemanticVersion};
use workspace::{Workspace, WorkspaceStore}; use workspace::{Workspace, WorkspaceStore};
pub struct TestServer { pub struct TestServer {
@ -230,6 +230,7 @@ impl TestServer {
server_conn, server_conn,
client_name, client_name,
user, user,
SemanticVersion::default(),
None, None,
Some(connection_id_tx), Some(connection_id_tx),
Executor::Deterministic(cx.background_executor().clone()), Executor::Deterministic(cx.background_executor().clone()),
@ -685,7 +686,7 @@ impl TestClient {
channel_id: u64, channel_id: u64,
cx: &'a mut TestAppContext, cx: &'a mut TestAppContext,
) -> (View<Workspace>, &'a mut VisualTestContext) { ) -> (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 .await
.unwrap(); .unwrap();
cx.run_until_parked(); 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 { impl Drop for TestClient {
fn drop(&mut self) { fn drop(&mut self) {
self.app_state.client.teardown(); self.app_state.client.teardown();

View file

@ -40,7 +40,7 @@ use util::{maybe, ResultExt, TryFutureExt};
use workspace::{ use workspace::{
dock::{DockPosition, Panel, PanelEvent}, dock::{DockPosition, Panel, PanelEvent},
notifications::{DetachAndPromptErr, NotifyResultExt, NotifyTaskExt}, notifications::{DetachAndPromptErr, NotifyResultExt, NotifyTaskExt},
OpenChannelNotes, Workspace, Workspace,
}; };
actions!( actions!(
@ -69,19 +69,6 @@ pub fn init(cx: &mut AppContext) {
workspace.register_action(|workspace, _: &ToggleFocus, cx| { workspace.register_action(|workspace, _: &ToggleFocus, cx| {
workspace.toggle_panel_focus::<CollabPanel>(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(); .detach();
} }
@ -175,6 +162,9 @@ enum ListEntry {
depth: usize, depth: usize,
has_children: bool, has_children: bool,
}, },
ChannelCall {
channel_id: ChannelId,
},
ChannelNotes { ChannelNotes {
channel_id: ChannelId, channel_id: ChannelId,
}, },
@ -382,6 +372,7 @@ impl CollabPanel {
if query.is_empty() { if query.is_empty() {
if let Some(channel_id) = room.channel_id() { 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::ChannelNotes { channel_id });
self.entries.push(ListEntry::ChannelChat { channel_id }); self.entries.push(ListEntry::ChannelChat { channel_id });
} }
@ -479,7 +470,7 @@ impl CollabPanel {
&& participant.video_tracks.is_empty(), && 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 { self.entries.push(ListEntry::ParticipantScreen {
peer_id: Some(participant.peer_id), peer_id: Some(participant.peer_id),
is_last: true, is_last: true,
@ -832,8 +823,6 @@ impl CollabPanel {
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) -> ListItem { ) -> ListItem {
let user_id = user.id; 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 tooltip = format!("Follow {}", user.github_login);
let is_call_admin = ActiveCall::global(cx).read(cx).room().is_some_and(|room| { let is_call_admin = ActiveCall::global(cx).read(cx).room().is_some_and(|room| {
@ -846,12 +835,6 @@ impl CollabPanel {
.selected(is_selected) .selected(is_selected)
.end_slot(if is_pending { .end_slot(if is_pending {
Label::new("Calling").color(Color::Muted).into_any_element() 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 { } else if role == proto::ChannelRole::Guest {
Label::new("Guest").color(Color::Muted).into_any_element() Label::new("Guest").color(Color::Muted).into_any_element()
} else { } 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( fn render_channel_notes(
&self, &self,
channel_id: ChannelId, channel_id: ChannelId,
is_selected: bool, is_selected: bool,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) -> impl IntoElement { ) -> 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") ListItem::new("channel-notes")
.selected(is_selected) .selected(is_selected)
.on_click(cx.listener(move |this, _, cx| { .on_click(cx.listener(move |this, _, cx| {
@ -970,7 +1029,14 @@ impl CollabPanel {
.child(render_tree_branch(false, true, cx)) .child(render_tree_branch(false, true, cx))
.child(IconButton::new(0, IconName::File)), .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)) .tooltip(move |cx| Tooltip::text("Open Channel Notes", cx))
} }
@ -980,6 +1046,8 @@ impl CollabPanel {
is_selected: bool, is_selected: bool,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) -> impl IntoElement { ) -> 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") ListItem::new("channel-chat")
.selected(is_selected) .selected(is_selected)
.on_click(cx.listener(move |this, _, cx| { .on_click(cx.listener(move |this, _, cx| {
@ -991,7 +1059,14 @@ impl CollabPanel {
.child(render_tree_branch(false, false, cx)) .child(render_tree_branch(false, false, cx))
.child(IconButton::new(0, IconName::MessageBubbles)), .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)) .tooltip(move |cx| Tooltip::text("Open Chat", cx))
} }
@ -1249,12 +1324,14 @@ impl CollabPanel {
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) { ) {
let this = cx.view().clone(); 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 context_menu = ContextMenu::build(cx, |mut context_menu, _| {
let user_id = contact.user.id; 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 { let label = if in_room {
format!("Invite {} to join", contact.user.github_login) format!("Invite {} to join", contact.user.github_login)
} else { } else {
@ -1402,7 +1479,7 @@ impl CollabPanel {
if is_active { if is_active {
self.open_channel_notes(channel.id, cx) self.open_channel_notes(channel.id, cx)
} else { } else {
self.join_channel(channel.id, cx) self.open_channel(channel.id, cx)
} }
} }
ListEntry::ContactPlaceholder => self.toggle_contact_finder(cx), ListEntry::ContactPlaceholder => self.toggle_contact_finder(cx),
@ -1421,6 +1498,9 @@ impl CollabPanel {
ListEntry::ChannelInvite(channel) => { ListEntry::ChannelInvite(channel) => {
self.respond_to_channel_invite(channel.id, true, cx) self.respond_to_channel_invite(channel.id, true, cx)
} }
ListEntry::ChannelCall { channel_id } => {
self.join_channel_call(*channel_id, cx)
}
ListEntry::ChannelNotes { channel_id } => { ListEntry::ChannelNotes { channel_id } => {
self.open_channel_notes(*channel_id, cx) self.open_channel_notes(*channel_id, cx)
} }
@ -1883,14 +1963,14 @@ impl CollabPanel {
.detach_and_prompt_err("Call failed", cx, |_, _| None); .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 { let Some(workspace) = self.workspace.upgrade() else {
return; return;
}; };
let Some(handle) = cx.window_handle().downcast::<Workspace>() else { let Some(handle) = cx.window_handle().downcast::<Workspace>() else {
return; return;
}; };
workspace::join_channel( workspace::open_channel(
channel_id, channel_id,
workspace.read(cx).app_state().clone(), workspace.read(cx).app_state().clone(),
Some(handle), Some(handle),
@ -1899,6 +1979,23 @@ impl CollabPanel {
.detach_and_prompt_err("Failed to join channel", cx, |_, _| None) .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>) { fn join_channel_chat(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
let Some(workspace) = self.workspace.upgrade() else { let Some(workspace) = self.workspace.upgrade() else {
return; return;
@ -2024,6 +2121,9 @@ impl CollabPanel {
ListEntry::ParticipantScreen { peer_id, is_last } => self ListEntry::ParticipantScreen { peer_id, is_last } => self
.render_participant_screen(*peer_id, *is_last, is_selected, cx) .render_participant_screen(*peer_id, *is_last, is_selected, cx)
.into_any_element(), .into_any_element(),
ListEntry::ChannelCall { channel_id } => self
.render_channel_call(*channel_id, is_selected, cx)
.into_any_element(),
ListEntry::ChannelNotes { channel_id } => self ListEntry::ChannelNotes { channel_id } => self
.render_channel_notes(*channel_id, is_selected, cx) .render_channel_notes(*channel_id, is_selected, cx)
.into_any_element(), .into_any_element(),
@ -2089,7 +2189,6 @@ impl CollabPanel {
is_collapsed: bool, is_collapsed: bool,
cx: &ViewContext<Self>, cx: &ViewContext<Self>,
) -> impl IntoElement { ) -> impl IntoElement {
let mut channel_link = None;
let mut channel_tooltip_text = None; let mut channel_tooltip_text = None;
let mut channel_icon = None; let mut channel_icon = None;
@ -2100,13 +2199,12 @@ impl CollabPanel {
let channel = self.channel_store.read(cx).channel_for_id(channel_id)?; 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 { (channel_icon, channel_tooltip_text) = match channel.visibility {
proto::ChannelVisibility::Public => { proto::ChannelVisibility::Public => {
(Some("icons/public.svg"), Some("Copy public channel link.")) (Some(IconName::Public), Some("Close Channel"))
} }
proto::ChannelVisibility::Members => { 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 { let button = match section {
Section::ActiveCall => channel_link.map(|channel_link| { Section::ActiveCall => channel_icon.map(|_| {
let channel_link_copy = channel_link.clone(); IconButton::new("channel-link", IconName::Close)
IconButton::new("channel-link", IconName::Copy) .on_click(move |_, cx| Self::leave_call(cx))
.icon_size(IconSize::Small) .tooltip(|cx| Tooltip::text("Close channel", cx))
.size(ButtonSize::None)
.visible_on_hover("section-header")
.on_click(move |_, cx| {
let item = ClipboardItem::new(channel_link_copy.clone());
cx.write_to_clipboard(item)
})
.tooltip(|cx| Tooltip::text("Copy channel link", cx))
.into_any_element() .into_any_element()
}), }),
Section::Contacts => Some( Section::Contacts => Some(
@ -2173,6 +2264,9 @@ impl CollabPanel {
this.toggle_section_expanded(section, cx); 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) .inset(true)
.end_slot::<AnyElement>(button) .end_slot::<AnyElement>(button)
.selected(is_selected), .selected(is_selected),
@ -2478,11 +2572,9 @@ impl CollabPanel {
}), }),
) )
.on_click(cx.listener(move |this, _, cx| { .on_click(cx.listener(move |this, _, cx| {
if is_active { this.open_channel(channel_id, cx);
this.open_channel_notes(channel_id, cx) this.open_channel_notes(channel_id, cx);
} else { this.join_channel_chat(channel_id, cx);
this.join_channel(channel_id, cx)
}
})) }))
.on_secondary_mouse_down(cx.listener( .on_secondary_mouse_down(cx.listener(
move |this, event: &MouseDownEvent, cx| { move |this, event: &MouseDownEvent, cx| {
@ -2499,61 +2591,24 @@ impl CollabPanel {
.color(Color::Muted), .color(Color::Muted),
) )
.child( .child(
h_flex() h_flex().id(channel_id as usize).child(
.id(channel_id as usize) div()
.child(Label::new(channel.name.clone())) .text_ui()
.children(face_pile.map(|face_pile| face_pile.p_1())), .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() h_flex()
.absolute() .absolute()
.right(rems(0.)) .right(rems(0.))
.z_index(1) .z_index(1)
.h_full() .h_full()
.child( .child(face_pile.p_1())
h_flex() }))
.h_full()
.gap_1()
.px_1()
.child(
IconButton::new("channel_chat", IconName::MessageBubbles)
.style(ButtonStyle::Filled)
.shape(ui::IconButtonShape::Square)
.icon_size(IconSize::Small)
.icon_color(if has_messages_notification {
Color::Default
} else {
Color::Muted
})
.on_click(cx.listener(move |this, _, cx| {
this.join_channel_chat(channel_id, cx)
}))
.tooltip(|cx| Tooltip::text("Open channel chat", cx))
.when(!has_messages_notification, |this| {
this.visible_on_hover("")
}),
)
.child(
IconButton::new("channel_notes", IconName::File)
.style(ButtonStyle::Filled)
.shape(ui::IconButtonShape::Square)
.icon_size(IconSize::Small)
.icon_color(if has_notes_notification {
Color::Default
} else {
Color::Muted
})
.on_click(cx.listener(move |this, _, cx| {
this.open_channel_notes(channel_id, cx)
}))
.tooltip(|cx| Tooltip::text("Open channel notes", cx))
.when(!has_notes_notification, |this| {
this.visible_on_hover("")
}),
),
),
)
.tooltip({ .tooltip({
let channel_store = self.channel_store.clone(); let channel_store = self.channel_store.clone();
move |cx| { move |cx| {
@ -2757,6 +2812,14 @@ impl PartialEq for ListEntry {
return channel_1.id == channel_2.id; 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 } => { ListEntry::ChannelNotes { channel_id } => {
if let ListEntry::ChannelNotes { if let ListEntry::ChannelNotes {
channel_id: other_id, channel_id: other_id,
@ -2855,7 +2918,7 @@ impl Render for JoinChannelTooltip {
.read(cx) .read(cx)
.channel_participants(self.channel_id); .channel_participants(self.channel_id);
div.child(Label::new("Join Channel")) div.child(Label::new("Open Channel"))
.children(participants.iter().map(|participant| { .children(participants.iter().map(|participant| {
h_flex() h_flex()
.gap_2() .gap_2()

View file

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

View file

@ -22,10 +22,7 @@ pub use panel_settings::{
use settings::Settings; use settings::Settings;
use workspace::{notifications::DetachAndPromptErr, AppState}; use workspace::{notifications::DetachAndPromptErr, AppState};
actions!( actions!(collab, [ToggleScreenSharing, ToggleMute, LeaveCall]);
collab,
[ToggleScreenSharing, ToggleMute, ToggleDeafen, LeaveCall]
);
pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) { pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
CollaborationPanelSettings::register(cx); 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( fn notification_window_options(
screen: Rc<dyn PlatformDisplay>, screen: Rc<dyn PlatformDisplay>,
window_size: Size<Pixels>, window_size: Size<Pixels>,

View file

@ -3,7 +3,7 @@
//! Not literally though - rendering, layout and all that jazz is a responsibility of [`EditorElement`][EditorElement]. //! 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 //! 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 //! 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 //! 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): //! of several smaller structures that form a hierarchy (starting at the bottom):
//! - [`InlayMap`] that decides where the [`Inlay`]s should be displayed. //! - [`InlayMap`] that decides where the [`Inlay`]s should be displayed.

View file

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

View file

@ -183,7 +183,12 @@ message Envelope {
LspExtExpandMacroResponse lsp_ext_expand_macro_response = 155; LspExtExpandMacroResponse lsp_ext_expand_macro_response = 155;
SetRoomParticipantRole set_room_participant_role = 156; 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; ParticipantLocation location = 4;
uint32 participant_index = 5; uint32 participant_index = 5;
ChannelRole role = 6; ChannelRole role = 6;
bool in_call = 7;
} }
message PendingParticipant { message PendingParticipant {
@ -1033,6 +1039,22 @@ message JoinChannel {
uint64 channel_id = 1; 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 { message DeleteChannel {
uint64 channel_id = 1; uint64 channel_id = 1;
} }

View file

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

View file

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

View file

@ -12,7 +12,7 @@ mod toolbar;
mod workspace_settings; mod workspace_settings;
use anyhow::{anyhow, Context as _, Result}; use anyhow::{anyhow, Context as _, Result};
use call::{call_settings::CallSettings, ActiveCall}; use call::ActiveCall;
use client::{ use client::{
proto::{self, ErrorCode, PeerId}, proto::{self, ErrorCode, PeerId},
Client, ErrorExt, Status, TypedEnvelope, UserStore, 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() DB.last_workspace().await.log_err().flatten()
} }
actions!(collab, [OpenChannelNotes]);
async fn join_channel_internal( async fn join_channel_internal(
channel_id: u64, channel_id: u64,
app_state: &Arc<AppState>, app_state: &Arc<AppState>,
@ -4080,36 +4078,6 @@ async fn join_channel_internal(
return Some(join_remote_project(project, host, app_state.clone(), cx)); 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 None
})?; })?;
if let Some(task) = task { if let Some(task) = task {
@ -4119,7 +4087,7 @@ async fn join_channel_internal(
anyhow::Ok(false) anyhow::Ok(false)
} }
pub fn join_channel( pub fn open_channel(
channel_id: u64, channel_id: u64,
app_state: Arc<AppState>, app_state: Arc<AppState>,
requesting_window: Option<WindowHandle<Workspace>>, requesting_window: Option<WindowHandle<Workspace>>,
@ -4152,12 +4120,6 @@ pub fn join_channel(
})? })?
.await?; .await?;
if result.is_ok() {
cx.update(|cx| {
cx.dispatch_action(&OpenChannelNotes);
}).log_err();
}
active_window = Some(window_handle); active_window = Some(window_handle);
} }

View file

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