revert single channel click (#7738)

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

Release Notes:

- N/A

**or**

- N/A
This commit is contained in:
Conrad Irwin 2024-02-13 12:53:49 -07:00 committed by GitHub
parent ecd9b93cb1
commit 2294d99046
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 525 additions and 709 deletions

View file

@ -104,8 +104,10 @@
"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 muted by default // Join calls with the microphone live 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

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

View file

@ -7,6 +7,7 @@ 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.
@ -16,6 +17,11 @@ 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,7 +49,6 @@ 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,7 +61,6 @@ 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>>,
@ -113,18 +112,91 @@ 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();
let mut this = Self { Self {
id, id,
channel_id, channel_id,
live_kit: None, live_kit: live_kit_room,
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(),
@ -148,11 +220,7 @@ 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(
@ -211,7 +279,7 @@ impl Room {
cx: AsyncAppContext, cx: AsyncAppContext,
) -> Result<Model<Self>> { ) -> Result<Model<Self>> {
Self::from_join_response( Self::from_join_response(
client.request(proto::JoinChannel2 { channel_id }).await?, client.request(proto::JoinChannel { channel_id }).await?,
client, client,
user_store, user_store,
cx, cx,
@ -256,7 +324,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 CallSettings::get_global(cx).mute_on_join || client::IMPERSONATE_LOGIN.is_some()
} }
fn from_join_response( fn from_join_response(
@ -306,9 +374,7 @@ impl Room {
} }
log::info!("leaving room"); log::info!("leaving room");
if self.live_kit.is_some() { Audio::play_sound(Sound::Leave, cx);
Audio::play_sound(Sound::Leave, cx);
}
self.clear_state(cx); self.clear_state(cx);
@ -527,24 +593,6 @@ 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()
@ -569,6 +617,10 @@ 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,
@ -776,7 +828,6 @@ 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) =
@ -787,15 +838,9 @@ 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,
}); });
@ -812,15 +857,12 @@ 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(),
}, },
); );
if participant.in_call { Audio::play_sound(Sound::Joined, cx);
Audio::play_sound(Sound::Joined, cx);
}
if let Some(live_kit) = this.live_kit.as_ref() { if let Some(live_kit) = this.live_kit.as_ref() {
let video_tracks = let video_tracks =
@ -1009,6 +1051,15 @@ 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
@ -1155,7 +1206,7 @@ impl Room {
}) })
} }
pub(crate) fn share_project( pub fn share_project(
&mut self, &mut self,
project: Model<Project>, project: Model<Project>,
cx: &mut ModelContext<Self>, cx: &mut ModelContext<Self>,
@ -1257,14 +1308,18 @@ impl Room {
}) })
} }
pub fn is_sharing_mic(&self) -> bool {
self.live_kit.as_ref().map_or(false, |live_kit| {
!matches!(live_kit.microphone_track, LocalTrack::None)
})
}
pub fn is_muted(&self) -> bool { pub fn is_muted(&self) -> bool {
self.live_kit self.live_kit.as_ref().map_or(false, |live_kit| {
.as_ref() matches!(live_kit.microphone_track, LocalTrack::None)
.map_or(true, |live_kit| match &live_kit.microphone_track { || live_kit.muted_by_user
LocalTrack::None => true, || live_kit.deafened
LocalTrack::Pending { .. } => true, })
LocalTrack::Published { track_publication } => track_publication.is_muted(),
})
} }
pub fn read_only(&self) -> bool { pub fn read_only(&self) -> bool {
@ -1278,8 +1333,8 @@ impl Room {
.map_or(false, |live_kit| live_kit.speaking) .map_or(false, |live_kit| live_kit.speaking)
} }
pub fn in_call(&self) -> bool { pub fn is_deafened(&self) -> Option<bool> {
self.live_kit.is_some() self.live_kit.as_ref().map(|live_kit| live_kit.deafened)
} }
#[track_caller] #[track_caller]
@ -1332,8 +1387,12 @@ 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,
}; };
@ -1437,140 +1496,50 @@ impl Room {
} }
pub fn toggle_mute(&mut self, cx: &mut ModelContext<Self>) { pub fn toggle_mute(&mut self, cx: &mut ModelContext<Self>) {
let muted = !self.is_muted(); if let Some(live_kit) = self.live_kit.as_mut() {
if let Some(task) = self.set_mute(muted, cx) { // When unmuting, undeafen if the user was deafened before.
task.detach_and_log_err(cx); let was_deafened = live_kit.deafened;
if live_kit.muted_by_user
|| live_kit.deafened
|| matches!(live_kit.microphone_track, LocalTrack::None)
{
live_kit.muted_by_user = false;
live_kit.deafened = false;
} else {
live_kit.muted_by_user = true;
}
let muted = live_kit.muted_by_user;
let should_undeafen = was_deafened && !live_kit.deafened;
if let Some(task) = self.set_mute(muted, cx) {
task.detach_and_log_err(cx);
}
if should_undeafen {
if let Some(task) = self.set_deafened(false, cx) {
task.detach_and_log_err(cx);
}
}
} }
} }
pub fn join_call(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> { pub fn toggle_deafen(&mut self, cx: &mut ModelContext<Self>) {
if self.live_kit.is_some() { if let Some(live_kit) = self.live_kit.as_mut() {
return Task::ready(Ok(())); // 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 room = live_kit_client::Room::new(); if let Some(task) = self.set_deafened(deafened, cx) {
let mut status = room.status(); task.detach_and_log_err(cx);
// 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 { if should_change_mute {
this.update(&mut cx, |this, cx| this.leave(cx).log_err()) if let Some(task) = self.set_mute(deafened, cx) {
.ok(); task.detach_and_log_err(cx);
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)
} }
} }
@ -1601,6 +1570,40 @@ impl Room {
} }
} }
fn set_deafened(
&mut self,
deafened: bool,
cx: &mut ModelContext<Self>,
) -> Option<Task<Result<()>>> {
let live_kit = self.live_kit.as_mut()?;
cx.notify();
let mut track_updates = Vec::new();
for participant in self.remote_participants.values() {
for publication in live_kit
.room
.remote_audio_track_publications(&participant.user.id.to_string())
{
track_updates.push(publication.set_enabled(!deafened));
}
for track in participant.audio_tracks.values() {
if deafened {
track.stop();
} else {
track.start();
}
}
}
Some(cx.foreground_executor().spawn(async move {
for result in futures::future::join_all(track_updates).await {
result?;
}
Ok(())
}))
}
fn set_mute( fn set_mute(
&mut self, &mut self,
should_mute: bool, should_mute: bool,
@ -1645,6 +1648,9 @@ 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

@ -97,57 +97,11 @@ 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)> {
@ -212,7 +166,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, autojoin, connection, role, &*tx) self.join_channel_room_internal(room_id, user_id, connection, role, &*tx)
.await .await
.map(|jr| (jr, accept_invite_result, role)) .map(|jr| (jr, accept_invite_result, role))
}) })

View file

@ -135,7 +135,6 @@ 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,
@ -188,7 +187,6 @@ 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,
@ -416,7 +414,6 @@ 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,
@ -440,8 +437,6 @@ 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,
@ -1263,7 +1258,6 @@ 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,7 +20,6 @@ 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,7 +138,6 @@ 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,
) )
@ -733,15 +732,9 @@ async fn test_guest_access(db: &Arc<Database>) {
.await .await
.is_err()); .is_err());
db.join_channel( db.join_channel(zed_channel, guest, guest_connection, TEST_RELEASE_CHANNEL)
zed_channel, .await
guest, .unwrap();
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,7 +105,6 @@ 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>>,
@ -132,19 +131,6 @@ 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 {
@ -288,11 +274,8 @@ 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)
@ -576,7 +559,6 @@ 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,
@ -634,7 +616,6 @@ 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(),
@ -885,7 +866,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>,
@ -900,12 +881,6 @@ 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;
@ -920,7 +895,6 @@ 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,
@ -1063,7 +1037,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, true, Box::new(response), session).await; return join_channel_internal(channel_id, Box::new(response), session).await;
} }
let joined_room = { let joined_room = {
@ -2726,67 +2700,14 @@ async fn respond_to_channel_invite(
Ok(()) Ok(())
} }
/// Join the channels' call /// Join the channels' room
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, true, Box::new(response), session).await join_channel_internal(channel_id, 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 {
@ -2802,15 +2723,9 @@ 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<()> {
@ -2822,22 +2737,39 @@ 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| {
if !autojoin { let (can_publish, token) = if role == ChannelRole::Guest {
return None; (
} false,
live_kit_info_for_user( live_kit
live_kit, .guest_token(
&session.user_id, &joined_room.room.live_kit_room,
role, &session.user_id.to_string(),
&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 {
@ -2873,35 +2805,6 @@ 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,7 +1,4 @@
use crate::{ use crate::{db::ChannelId, tests::TestServer};
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};
@ -35,7 +32,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::open_channel(channel_id, client_b.app_state.clone(), None, cx)) cx_b.update(|cx| workspace::join_channel(channel_id, client_b.app_state.clone(), None, cx))
.await .await
.unwrap(); .unwrap();
@ -75,7 +72,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::open_channel(channel_id, client_a.app_state.clone(), None, cx)) cx_a.update(|cx| workspace::join_channel(channel_id, client_a.app_state.clone(), None, cx))
.await .await
.unwrap(); .unwrap();
@ -87,13 +84,11 @@ 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::open_channel(channel_id, client_b.app_state.clone(), None, cx)) cx_b.update(|cx| workspace::join_channel(channel_id, client_b.app_state.clone(), None, cx))
.await .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, test_server::join_channel_call, RoomParticipants, TestServer}, tests::{room_participants, RoomParticipants, TestServer},
}; };
use call::ActiveCall; use call::ActiveCall;
use channel::{ChannelId, ChannelMembership, ChannelStore}; use channel::{ChannelId, ChannelMembership, ChannelStore};
@ -382,7 +382,6 @@ async fn test_channel_room(
.update(cx_a, |active_call, cx| active_call.join_channel(zed_id, cx)) .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();
@ -430,7 +429,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| {
@ -553,9 +552,6 @@ 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] #[gpui::test(iterations = 10)]
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,7 +437,6 @@ 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
@ -523,7 +522,6 @@ 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]
@ -2006,7 +2004,7 @@ async fn join_channel(
client: &TestClient, client: &TestClient,
cx: &mut TestAppContext, cx: &mut TestAppContext,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
cx.update(|cx| workspace::open_channel(channel_id, client.app_state.clone(), None, cx)) cx.update(|cx| workspace::join_channel(channel_id, client.app_state.clone(), None, cx))
.await .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( async fn test_mute_deafen(
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(
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 unmuted. // Users A and B are both muted.
assert_eq!( assert_eq!(
participant_audio_state(&room_a, cx_a), participant_audio_state(&room_a, cx_a),
&[ParticipantAudioState { &[ParticipantAudioState {
@ -1962,6 +1962,30 @@ async fn test_mute(
}] }]
); );
// User A deafens
room_a.update(cx_a, |room, cx| room.toggle_deafen(cx));
executor.run_until_parked();
// User A does not hear user B.
room_a.read_with(cx_a, |room, _| assert!(room.is_muted()));
room_b.read_with(cx_b, |room, _| assert!(!room.is_muted()));
assert_eq!(
participant_audio_state(&room_a, cx_a),
&[ParticipantAudioState {
user_id: client_b.user_id().unwrap(),
is_muted: false,
audio_tracks_playing: vec![false],
}]
);
assert_eq!(
participant_audio_state(&room_b, cx_b),
&[ParticipantAudioState {
user_id: client_a.user_id().unwrap(),
is_muted: true,
audio_tracks_playing: vec![true],
}]
);
// User B calls user C, C joins. // User B calls user C, C joins.
active_call_b active_call_b
.update(cx_b, |call, cx| { .update(cx_b, |call, cx| {
@ -1976,6 +2000,22 @@ async fn test_mute(
.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

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

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

View file

@ -102,10 +102,6 @@ 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,
@ -137,10 +133,6 @@ 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,
@ -193,7 +185,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_connected_to_livekit = room.in_call(); let is_deafened = room.is_deafened().unwrap_or(false);
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();
@ -228,28 +220,22 @@ impl Render for CollabTitlebarItem {
)), )),
) )
}) })
.when(is_connected_to_livekit, |el| { .child(
el.child( div()
div() .child(
.child( IconButton::new("leave-call", ui::IconName::Exit)
IconButton::new("leave-call", ui::IconName::Exit) .style(ButtonStyle::Subtle)
.style(ButtonStyle::Subtle) .tooltip(|cx| Tooltip::text("Leave call", cx))
.tooltip(|cx| Tooltip::text("Leave call", cx)) .icon_size(IconSize::Small)
.icon_size(IconSize::Small) .on_click(move |_, cx| {
.on_click(move |_, cx| { ActiveCall::global(cx)
ActiveCall::global(cx).update(cx, |call, cx| { .update(cx, |call, cx| call.hang_up(cx))
if let Some(room) = call.room() { .detach_and_log_err(cx);
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",
@ -276,7 +262,34 @@ impl Render for CollabTitlebarItem {
.on_click(move |_, cx| crate::toggle_mute(&Default::default(), cx)), .on_click(move |_, cx| crate::toggle_mute(&Default::default(), cx)),
) )
}) })
.when(!read_only && is_connected_to_livekit, |this| { .child(
IconButton::new(
"mute-sound",
if is_deafened {
ui::IconName::AudioOff
} else {
ui::IconName::AudioOn
},
)
.style(ButtonStyle::Subtle)
.selected_style(ButtonStyle::Tinted(TintColor::Negative))
.icon_size(IconSize::Small)
.selected(is_deafened)
.tooltip(move |cx| {
if !read_only {
Tooltip::with_meta(
"Deafen Audio",
None,
"Mic will be muted",
cx,
)
} else {
Tooltip::text("Deafen Audio", cx)
}
})
.on_click(move |_, cx| crate::toggle_deafen(&Default::default(), cx)),
)
.when(!read_only, |this| {
this.child( 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,7 +22,10 @@ pub use panel_settings::{
use settings::Settings; use settings::Settings;
use workspace::{notifications::DetachAndPromptErr, AppState}; use workspace::{notifications::DetachAndPromptErr, AppState};
actions!(collab, [ToggleScreenSharing, ToggleMute, LeaveCall]); actions!(
collab,
[ToggleScreenSharing, ToggleMute, ToggleDeafen, LeaveCall]
);
pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) { pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
CollaborationPanelSettings::register(cx); CollaborationPanelSettings::register(cx);
@ -82,6 +85,12 @@ pub fn toggle_mute(_: &ToggleMute, cx: &mut AppContext) {
} }
} }
pub fn toggle_deafen(_: &ToggleDeafen, cx: &mut AppContext) {
if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
room.update(cx, |room, cx| room.toggle_deafen(cx));
}
}
fn notification_window_options( 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: {}", url))? .ok_or_else(|| anyhow!("no server found for url"))?
.clone()) .clone())
} }
@ -160,6 +160,7 @@ 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
@ -413,15 +414,6 @@ 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,
@ -702,15 +694,11 @@ 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() {
if room.is_connected() { room.test_server()
room.test_server() .is_track_muted(&room.token(), &self.sid)
.is_track_muted(&room.token(), &self.sid) .unwrap_or(false)
.unwrap_or(true)
} else {
true
}
} else { } else {
true false
} }
} }

View file

@ -0,0 +1,4 @@
version: v1
breaking:
use:
- WIRE

View file

@ -184,12 +184,9 @@ message Envelope {
SetRoomParticipantRole set_room_participant_role = 156; SetRoomParticipantRole set_room_participant_role = 156;
UpdateUserChannels update_user_channels = 157; UpdateUserChannels update_user_channels = 157;
JoinChannel2 join_channel2 = 158;
JoinChannelCall join_channel_call = 159;
JoinChannelCallResponse join_channel_call_response = 160;
LeaveChannelCall leave_channel_call = 161; // current max
} }
reserved 158 to 161;
} }
// Messages // Messages
@ -296,7 +293,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; reserved 7;
} }
message PendingParticipant { message PendingParticipant {
@ -1039,22 +1036,6 @@ 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,7 +198,6 @@ 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),
@ -209,9 +208,6 @@ 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),
@ -328,9 +324,6 @@ 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,7 +762,6 @@ 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::ActiveCall; use call::{call_settings::CallSettings, ActiveCall};
use client::{ use client::{
proto::{self, ErrorCode, PeerId}, proto::{self, ErrorCode, PeerId},
Client, ErrorExt, Status, TypedEnvelope, UserStore, Client, ErrorExt, Status, TypedEnvelope, UserStore,
@ -1217,9 +1217,7 @@ impl Workspace {
if let Some(active_call) = active_call { if let Some(active_call) = active_call {
if !quitting if !quitting
&& workspace_count == 1 && workspace_count == 1
&& active_call.read_with(&cx, |call, cx| { && active_call.read_with(&cx, |call, _| call.room().is_some())?
call.room().is_some_and(|room| room.read(cx).in_call())
})?
{ {
let answer = window.update(&mut cx, |_, cx| { let answer = window.update(&mut cx, |_, cx| {
cx.prompt( cx.prompt(
@ -1232,11 +1230,12 @@ impl Workspace {
if answer.await.log_err() == Some(1) { if answer.await.log_err() == Some(1) {
return anyhow::Ok(false); return anyhow::Ok(false);
} else {
active_call
.update(&mut cx, |call, cx| call.hang_up(cx))?
.await
.log_err();
} }
active_call
.update(&mut cx, |call, cx| call.hang_up(cx))?
.await
.log_err();
} }
} }
@ -3999,6 +3998,8 @@ pub async fn last_opened_workspace_paths() -> Option<WorkspaceLocation> {
DB.last_workspace().await.log_err().flatten() 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>,
@ -4100,6 +4101,36 @@ 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 {
@ -4109,7 +4140,7 @@ async fn join_channel_internal(
anyhow::Ok(false) anyhow::Ok(false)
} }
pub fn open_channel( pub fn join_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>>,
@ -4142,6 +4173,12 @@ pub fn open_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

@ -321,7 +321,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::open_channel(channel_id, app_state, None, cx))? cx.update(|cx| workspace::join_channel(channel_id, app_state, None, cx))?
.await?; .await?;
anyhow::Ok(()) anyhow::Ok(())
}) })
@ -376,7 +376,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::open_channel(channel_id, app_state, None, cx) workspace::join_channel(channel_id, app_state, None, cx)
})? })?
.await?; .await?;
anyhow::Ok(()) anyhow::Ok(())