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",
// Settings related to calls in Zed
"calls": {
// Join calls with the microphone muted by default
"mute_on_join": false
// Join calls with the microphone live by default
"mute_on_join": false,
// Share your project when you are the first to join a channel
"share_on_join": true
},
// Toolbar related settings
"toolbar": {

View file

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

View file

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

View file

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

View file

@ -61,7 +61,6 @@ pub struct Room {
id: u64,
channel_id: Option<u64>,
live_kit: Option<LiveKitRoom>,
live_kit_connection_info: Option<proto::LiveKitConnectionInfo>,
status: RoomStatus,
shared_projects: HashSet<WeakModel<Project>>,
joined_projects: HashSet<WeakModel<Project>>,
@ -113,18 +112,91 @@ impl Room {
user_store: Model<UserStore>,
cx: &mut ModelContext<Self>,
) -> Self {
let live_kit_room = if let Some(connection_info) = live_kit_connection_info {
let room = live_kit_client::Room::new();
let mut status = room.status();
// Consume the initial status of the room.
let _ = status.try_recv();
let _maintain_room = cx.spawn(|this, mut cx| async move {
while let Some(status) = status.next().await {
let this = if let Some(this) = this.upgrade() {
this
} else {
break;
};
if status == live_kit_client::ConnectionState::Disconnected {
this.update(&mut cx, |this, cx| this.leave(cx).log_err())
.ok();
break;
}
}
});
let _handle_updates = cx.spawn({
let room = room.clone();
move |this, mut cx| async move {
let mut updates = room.updates();
while let Some(update) = updates.next().await {
let this = if let Some(this) = this.upgrade() {
this
} else {
break;
};
this.update(&mut cx, |this, cx| {
this.live_kit_room_updated(update, cx).log_err()
})
.ok();
}
}
});
let connect = room.connect(&connection_info.server_url, &connection_info.token);
cx.spawn(|this, mut cx| async move {
connect.await?;
this.update(&mut cx, |this, cx| {
if !this.read_only() {
if let Some(live_kit) = &this.live_kit {
if !live_kit.muted_by_user && !live_kit.deafened {
return this.share_microphone(cx);
}
}
}
Task::ready(Ok(()))
})?
.await
})
.detach_and_log_err(cx);
Some(LiveKitRoom {
room,
screen_track: LocalTrack::None,
microphone_track: LocalTrack::None,
next_publish_id: 0,
muted_by_user: Self::mute_on_join(cx),
deafened: false,
speaking: false,
_maintain_room,
_handle_updates,
})
} else {
None
};
let maintain_connection = cx.spawn({
let client = client.clone();
move |this, cx| Self::maintain_connection(this, client.clone(), cx).log_err()
});
Audio::play_sound(Sound::Joined, cx);
let (room_update_completed_tx, room_update_completed_rx) = watch::channel();
let mut this = Self {
Self {
id,
channel_id,
live_kit: None,
live_kit_connection_info,
live_kit: live_kit_room,
status: RoomStatus::Online,
shared_projects: Default::default(),
joined_projects: Default::default(),
@ -148,11 +220,7 @@ impl Room {
maintain_connection: Some(maintain_connection),
room_update_completed_tx,
room_update_completed_rx,
};
if this.live_kit_connection_info.is_some() {
this.join_call(cx).detach_and_log_err(cx);
}
this
}
pub(crate) fn create(
@ -211,7 +279,7 @@ impl Room {
cx: AsyncAppContext,
) -> Result<Model<Self>> {
Self::from_join_response(
client.request(proto::JoinChannel2 { channel_id }).await?,
client.request(proto::JoinChannel { channel_id }).await?,
client,
user_store,
cx,
@ -256,7 +324,7 @@ impl Room {
}
pub fn mute_on_join(cx: &AppContext) -> bool {
CallSettings::get_global(cx).mute_on_join
CallSettings::get_global(cx).mute_on_join || client::IMPERSONATE_LOGIN.is_some()
}
fn from_join_response(
@ -306,9 +374,7 @@ impl Room {
}
log::info!("leaving room");
if self.live_kit.is_some() {
Audio::play_sound(Sound::Leave, cx);
}
self.clear_state(cx);
@ -527,24 +593,6 @@ impl Room {
&self.remote_participants
}
pub fn call_participants(&self, cx: &AppContext) -> Vec<Arc<User>> {
self.remote_participants()
.values()
.filter_map(|participant| {
if participant.in_call {
Some(participant.user.clone())
} else {
None
}
})
.chain(if self.in_call() {
self.user_store.read(cx).current_user()
} else {
None
})
.collect()
}
pub fn remote_participant_for_peer_id(&self, peer_id: PeerId) -> Option<&RemoteParticipant> {
self.remote_participants
.values()
@ -569,6 +617,10 @@ impl Room {
self.local_participant.role == proto::ChannelRole::Admin
}
pub fn local_participant_is_guest(&self) -> bool {
self.local_participant.role == proto::ChannelRole::Guest
}
pub fn set_participant_role(
&mut self,
user_id: u64,
@ -776,7 +828,6 @@ impl Room {
}
let role = participant.role();
let in_call = participant.in_call;
let location = ParticipantLocation::from_proto(participant.location)
.unwrap_or(ParticipantLocation::External);
if let Some(remote_participant) =
@ -787,15 +838,9 @@ impl Room {
remote_participant.participant_index = participant_index;
if location != remote_participant.location
|| role != remote_participant.role
|| in_call != remote_participant.in_call
{
if in_call && !remote_participant.in_call {
Audio::play_sound(Sound::Joined, cx);
}
remote_participant.location = location;
remote_participant.role = role;
remote_participant.in_call = participant.in_call;
cx.emit(Event::ParticipantLocationChanged {
participant_id: peer_id,
});
@ -812,15 +857,12 @@ impl Room {
role,
muted: true,
speaking: false,
in_call: participant.in_call,
video_tracks: Default::default(),
audio_tracks: Default::default(),
},
);
if participant.in_call {
Audio::play_sound(Sound::Joined, cx);
}
if let Some(live_kit) = this.live_kit.as_ref() {
let video_tracks =
@ -1009,6 +1051,15 @@ impl Room {
}
RoomUpdate::SubscribedToRemoteAudioTrack(track, publication) => {
if let Some(live_kit) = &self.live_kit {
if live_kit.deafened {
track.stop();
cx.foreground_executor()
.spawn(publication.set_enabled(false))
.detach();
}
}
let user_id = track.publisher_id().parse()?;
let track_id = track.sid().to_string();
let participant = self
@ -1155,7 +1206,7 @@ impl Room {
})
}
pub(crate) fn share_project(
pub fn share_project(
&mut self,
project: Model<Project>,
cx: &mut ModelContext<Self>,
@ -1257,13 +1308,17 @@ impl Room {
})
}
pub fn is_sharing_mic(&self) -> bool {
self.live_kit.as_ref().map_or(false, |live_kit| {
!matches!(live_kit.microphone_track, LocalTrack::None)
})
}
pub fn is_muted(&self) -> bool {
self.live_kit
.as_ref()
.map_or(true, |live_kit| match &live_kit.microphone_track {
LocalTrack::None => true,
LocalTrack::Pending { .. } => true,
LocalTrack::Published { track_publication } => track_publication.is_muted(),
self.live_kit.as_ref().map_or(false, |live_kit| {
matches!(live_kit.microphone_track, LocalTrack::None)
|| live_kit.muted_by_user
|| live_kit.deafened
})
}
@ -1278,8 +1333,8 @@ impl Room {
.map_or(false, |live_kit| live_kit.speaking)
}
pub fn in_call(&self) -> bool {
self.live_kit.is_some()
pub fn is_deafened(&self) -> Option<bool> {
self.live_kit.as_ref().map(|live_kit| live_kit.deafened)
}
#[track_caller]
@ -1332,8 +1387,12 @@ impl Room {
Ok(publication) => {
if canceled {
live_kit.room.unpublish_track(publication);
live_kit.microphone_track = LocalTrack::None;
} else {
if live_kit.muted_by_user || live_kit.deafened {
cx.background_executor()
.spawn(publication.set_mute(true))
.detach();
}
live_kit.microphone_track = LocalTrack::Published {
track_publication: publication,
};
@ -1437,140 +1496,50 @@ impl Room {
}
pub fn toggle_mute(&mut self, cx: &mut ModelContext<Self>) {
let muted = !self.is_muted();
if let Some(live_kit) = self.live_kit.as_mut() {
// When unmuting, undeafen if the user was deafened before.
let was_deafened = live_kit.deafened;
if live_kit.muted_by_user
|| live_kit.deafened
|| matches!(live_kit.microphone_track, LocalTrack::None)
{
live_kit.muted_by_user = false;
live_kit.deafened = false;
} else {
live_kit.muted_by_user = true;
}
let muted = live_kit.muted_by_user;
let should_undeafen = was_deafened && !live_kit.deafened;
if let Some(task) = self.set_mute(muted, cx) {
task.detach_and_log_err(cx);
}
}
pub fn join_call(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
if self.live_kit.is_some() {
return Task::ready(Ok(()));
}
let room = live_kit_client::Room::new();
let mut status = room.status();
// Consume the initial status of the room.
let _ = status.try_recv();
let _maintain_room = cx.spawn(|this, mut cx| async move {
while let Some(status) = status.next().await {
let this = if let Some(this) = this.upgrade() {
this
} else {
break;
};
if status == live_kit_client::ConnectionState::Disconnected {
this.update(&mut cx, |this, cx| this.leave(cx).log_err())
.ok();
break;
if should_undeafen {
if let Some(task) = self.set_deafened(false, cx) {
task.detach_and_log_err(cx);
}
}
});
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,
});
pub fn toggle_deafen(&mut self, cx: &mut ModelContext<Self>) {
if let Some(live_kit) = self.live_kit.as_mut() {
// When deafening, mute the microphone if it was not already muted.
// When un-deafening, unmute the microphone, unless it was explicitly muted.
let deafened = !live_kit.deafened;
live_kit.deafened = deafened;
let should_change_mute = !live_kit.muted_by_user;
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));
if let Some(task) = self.set_deafened(deafened, cx) {
task.detach_and_log_err(cx);
}
for track in participant.audio_tracks.values() {
track.start();
if should_change_mute {
if let Some(task) = self.set_mute(deafened, cx) {
task.detach_and_log_err(cx);
}
}
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(
&mut self,
should_mute: bool,
@ -1645,6 +1648,9 @@ struct LiveKitRoom {
room: Arc<live_kit_client::Room>,
screen_track: LocalTrack,
microphone_track: LocalTrack,
/// Tracks whether we're currently in a muted state due to auto-mute from deafening or manual mute performed by user.
muted_by_user: bool,
deafened: bool,
speaking: bool,
next_publish_id: usize,
_maintain_room: Task<()>,

View file

@ -97,57 +97,11 @@ impl Database {
.await
}
pub async fn set_in_channel_call(
&self,
channel_id: ChannelId,
user_id: UserId,
in_call: bool,
) -> Result<(proto::Room, ChannelRole)> {
self.transaction(move |tx| async move {
let channel = self.get_channel_internal(channel_id, &*tx).await?;
let role = self.channel_role_for_user(&channel, user_id, &*tx).await?;
if role.is_none() || role == Some(ChannelRole::Banned) {
Err(ErrorCode::Forbidden.anyhow())?
}
let role = role.unwrap();
let Some(room) = room::Entity::find()
.filter(room::Column::ChannelId.eq(channel_id))
.one(&*tx)
.await?
else {
Err(anyhow!("no room exists"))?
};
let result = room_participant::Entity::update_many()
.filter(
Condition::all()
.add(room_participant::Column::RoomId.eq(room.id))
.add(room_participant::Column::UserId.eq(user_id)),
)
.set(room_participant::ActiveModel {
in_call: ActiveValue::Set(in_call),
..Default::default()
})
.exec(&*tx)
.await?;
if result.rows_affected != 1 {
Err(anyhow!("not in channel"))?
}
let room = self.get_room(room.id, &*tx).await?;
Ok((room, role))
})
.await
}
/// Adds a user to the specified channel.
pub async fn join_channel(
&self,
channel_id: ChannelId,
user_id: UserId,
autojoin: bool,
connection: ConnectionId,
environment: &str,
) -> Result<(JoinRoom, Option<MembershipUpdated>, ChannelRole)> {
@ -212,7 +166,7 @@ impl Database {
.get_or_create_channel_room(channel_id, &live_kit_room, environment, &*tx)
.await?;
self.join_channel_room_internal(room_id, user_id, autojoin, connection, role, &*tx)
self.join_channel_room_internal(room_id, user_id, connection, role, &*tx)
.await
.map(|jr| (jr, accept_invite_result, role))
})

View file

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

View file

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

View file

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

View file

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

View file

@ -1,7 +1,4 @@
use crate::{
db::ChannelId,
tests::{test_server::join_channel_call, TestServer},
};
use crate::{db::ChannelId, tests::TestServer};
use call::ActiveCall;
use editor::Editor;
use gpui::{BackgroundExecutor, TestAppContext};
@ -35,7 +32,7 @@ async fn test_channel_guests(
cx_a.executor().run_until_parked();
// Client B joins channel A as a guest
cx_b.update(|cx| workspace::open_channel(channel_id, client_b.app_state.clone(), None, cx))
cx_b.update(|cx| workspace::join_channel(channel_id, client_b.app_state.clone(), None, cx))
.await
.unwrap();
@ -75,7 +72,7 @@ async fn test_channel_guest_promotion(cx_a: &mut TestAppContext, cx_b: &mut Test
.await;
let project_a = client_a.build_test_project(cx_a).await;
cx_a.update(|cx| workspace::open_channel(channel_id, client_a.app_state.clone(), None, cx))
cx_a.update(|cx| workspace::join_channel(channel_id, client_a.app_state.clone(), None, cx))
.await
.unwrap();
@ -87,13 +84,11 @@ async fn test_channel_guest_promotion(cx_a: &mut TestAppContext, cx_b: &mut Test
cx_a.run_until_parked();
// Client B joins channel A as a guest
cx_b.update(|cx| workspace::open_channel(channel_id, client_b.app_state.clone(), None, cx))
cx_b.update(|cx| workspace::join_channel(channel_id, client_b.app_state.clone(), None, cx))
.await
.unwrap();
cx_a.run_until_parked();
join_channel_call(cx_b).await.unwrap();
// client B opens 1.txt as a guest
let (workspace_b, cx_b) = client_b.active_workspace(cx_b);
let room_b = cx_b

View file

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

View file

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

View file

@ -1881,7 +1881,7 @@ fn active_call_events(cx: &mut TestAppContext) -> Rc<RefCell<Vec<room::Event>>>
}
#[gpui::test]
async fn test_mute(
async fn test_mute_deafen(
executor: BackgroundExecutor,
cx_a: &mut TestAppContext,
cx_b: &mut TestAppContext,
@ -1920,7 +1920,7 @@ async fn test_mute(
room_a.read_with(cx_a, |room, _| assert!(!room.is_muted()));
room_b.read_with(cx_b, |room, _| assert!(!room.is_muted()));
// Users A and B are both unmuted.
// Users A and B are both muted.
assert_eq!(
participant_audio_state(&room_a, cx_a),
&[ParticipantAudioState {
@ -1962,6 +1962,30 @@ async fn test_mute(
}]
);
// User A deafens
room_a.update(cx_a, |room, cx| room.toggle_deafen(cx));
executor.run_until_parked();
// User A does not hear user B.
room_a.read_with(cx_a, |room, _| assert!(room.is_muted()));
room_b.read_with(cx_b, |room, _| assert!(!room.is_muted()));
assert_eq!(
participant_audio_state(&room_a, cx_a),
&[ParticipantAudioState {
user_id: client_b.user_id().unwrap(),
is_muted: false,
audio_tracks_playing: vec![false],
}]
);
assert_eq!(
participant_audio_state(&room_b, cx_b),
&[ParticipantAudioState {
user_id: client_a.user_id().unwrap(),
is_muted: true,
audio_tracks_playing: vec![true],
}]
);
// User B calls user C, C joins.
active_call_b
.update(cx_b, |call, cx| {
@ -1976,6 +2000,22 @@ async fn test_mute(
.unwrap();
executor.run_until_parked();
// User A does not hear users B or C.
assert_eq!(
participant_audio_state(&room_a, cx_a),
&[
ParticipantAudioState {
user_id: client_b.user_id().unwrap(),
is_muted: false,
audio_tracks_playing: vec![false],
},
ParticipantAudioState {
user_id: client_c.user_id().unwrap(),
is_muted: false,
audio_tracks_playing: vec![false],
}
]
);
assert_eq!(
participant_audio_state(&room_b, cx_b),
&[

View file

@ -37,7 +37,7 @@ use std::{
Arc,
},
};
use util::{http::FakeHttpClient, SemanticVersion};
use util::http::FakeHttpClient;
use workspace::{Workspace, WorkspaceStore};
pub struct TestServer {
@ -231,7 +231,6 @@ impl TestServer {
server_conn,
client_name,
user,
SemanticVersion::default(),
None,
Some(connection_id_tx),
Executor::Deterministic(cx.background_executor().clone()),
@ -687,7 +686,7 @@ impl TestClient {
channel_id: u64,
cx: &'a mut TestAppContext,
) -> (View<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
.unwrap();
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(
channel_id: u64,
cx: &mut VisualTestContext,

View file

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

View file

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

View file

@ -22,7 +22,10 @@ pub use panel_settings::{
use settings::Settings;
use workspace::{notifications::DetachAndPromptErr, AppState};
actions!(collab, [ToggleScreenSharing, ToggleMute, LeaveCall]);
actions!(
collab,
[ToggleScreenSharing, ToggleMute, ToggleDeafen, LeaveCall]
);
pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
CollaborationPanelSettings::register(cx);
@ -82,6 +85,12 @@ pub fn toggle_mute(_: &ToggleMute, cx: &mut AppContext) {
}
}
pub fn toggle_deafen(_: &ToggleDeafen, cx: &mut AppContext) {
if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
room.update(cx, |room, cx| room.toggle_deafen(cx));
}
}
fn notification_window_options(
screen: Rc<dyn PlatformDisplay>,
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].
//! Instead, [`DisplayMap`] decides where Inlays/Inlay hints are displayed, when
//! to apply a soft wrap, where to add fold indicators, whether there are any tabs in the buffer that
//! we display as spaces and where to display custom blocks (like diagnostics)
//! we display as spaces and where to display custom blocks (like diagnostics).
//! Seems like a lot? That's because it is. [`DisplayMap`] is conceptually made up
//! of several smaller structures that form a hierarchy (starting at the bottom):
//! - [`InlayMap`] that decides where the [`Inlay`]s should be displayed.

View file

@ -54,7 +54,7 @@ impl TestServer {
Ok(SERVERS
.lock()
.get(url)
.ok_or_else(|| anyhow!("no server found for url: {}", url))?
.ok_or_else(|| anyhow!("no server found for url"))?
.clone())
}
@ -160,6 +160,7 @@ impl TestServer {
async fn remove_participant(&self, room_name: String, identity: String) -> Result<()> {
// TODO: clear state associated with the `Room`.
self.executor.simulate_random_delay().await;
let mut server_rooms = self.rooms.lock();
let room = server_rooms
@ -413,15 +414,6 @@ struct TestServerRoom {
participant_permissions: HashMap<Sid, proto::ParticipantPermission>,
}
impl Drop for TestServerRoom {
fn drop(&mut self) {
for room in self.client_rooms.values() {
let mut state = room.0.lock();
*state.connection.0.borrow_mut() = ConnectionState::Disconnected;
}
}
}
#[derive(Debug)]
struct TestServerVideoTrack {
sid: Sid,
@ -702,15 +694,11 @@ impl LocalTrackPublication {
pub fn is_muted(&self) -> bool {
if let Some(room) = self.room.upgrade() {
if room.is_connected() {
room.test_server()
.is_track_muted(&room.token(), &self.sid)
.unwrap_or(true)
.unwrap_or(false)
} else {
true
}
} 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;
UpdateUserChannels update_user_channels = 157;
JoinChannel2 join_channel2 = 158;
JoinChannelCall join_channel_call = 159;
JoinChannelCallResponse join_channel_call_response = 160;
LeaveChannelCall leave_channel_call = 161; // current max
}
reserved 158 to 161;
}
// Messages
@ -296,7 +293,7 @@ message Participant {
ParticipantLocation location = 4;
uint32 participant_index = 5;
ChannelRole role = 6;
bool in_call = 7;
reserved 7;
}
message PendingParticipant {
@ -1039,22 +1036,6 @@ message JoinChannel {
uint64 channel_id = 1;
}
message JoinChannel2 {
uint64 channel_id = 1;
}
message JoinChannelCall {
uint64 channel_id = 1;
}
message JoinChannelCallResponse {
LiveKitConnectionInfo live_kit_connection_info = 1;
}
message LeaveChannelCall {
uint64 channel_id = 1;
}
message DeleteChannel {
uint64 channel_id = 1;
}

View file

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

View file

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

View file

@ -12,7 +12,7 @@ mod toolbar;
mod workspace_settings;
use anyhow::{anyhow, Context as _, Result};
use call::ActiveCall;
use call::{call_settings::CallSettings, ActiveCall};
use client::{
proto::{self, ErrorCode, PeerId},
Client, ErrorExt, Status, TypedEnvelope, UserStore,
@ -1217,9 +1217,7 @@ impl Workspace {
if let Some(active_call) = active_call {
if !quitting
&& workspace_count == 1
&& active_call.read_with(&cx, |call, cx| {
call.room().is_some_and(|room| room.read(cx).in_call())
})?
&& active_call.read_with(&cx, |call, _| call.room().is_some())?
{
let answer = window.update(&mut cx, |_, cx| {
cx.prompt(
@ -1232,13 +1230,14 @@ impl Workspace {
if answer.await.log_err() == Some(1) {
return anyhow::Ok(false);
}
} else {
active_call
.update(&mut cx, |call, cx| call.hang_up(cx))?
.await
.log_err();
}
}
}
Ok(this
.update(&mut cx, |this, cx| {
@ -3999,6 +3998,8 @@ pub async fn last_opened_workspace_paths() -> Option<WorkspaceLocation> {
DB.last_workspace().await.log_err().flatten()
}
actions!(collab, [OpenChannelNotes]);
async fn join_channel_internal(
channel_id: u64,
app_state: &Arc<AppState>,
@ -4100,6 +4101,36 @@ async fn join_channel_internal(
return Some(join_remote_project(project, host, app_state.clone(), cx));
}
// if you are the first to join a channel, share your project
if room.remote_participants().len() == 0 && !room.local_participant_is_guest() {
if let Some(workspace) = requesting_window {
let project = workspace.update(cx, |workspace, cx| {
if !CallSettings::get_global(cx).share_on_join {
return None;
}
let project = workspace.project.read(cx);
if project.is_local()
&& project.visible_worktrees(cx).any(|tree| {
tree.read(cx)
.root_entry()
.map_or(false, |entry| entry.is_dir())
})
{
Some(workspace.project.clone())
} else {
None
}
});
if let Ok(Some(project)) = project {
return Some(cx.spawn(|room, mut cx| async move {
room.update(&mut cx, |room, cx| room.share_project(project, cx))?
.await?;
Ok(())
}));
}
}
}
None
})?;
if let Some(task) = task {
@ -4109,7 +4140,7 @@ async fn join_channel_internal(
anyhow::Ok(false)
}
pub fn open_channel(
pub fn join_channel(
channel_id: u64,
app_state: Arc<AppState>,
requesting_window: Option<WindowHandle<Workspace>>,
@ -4142,6 +4173,12 @@ pub fn open_channel(
})?
.await?;
if result.is_ok() {
cx.update(|cx| {
cx.dispatch_action(&OpenChannelNotes);
}).log_err();
}
active_window = Some(window_handle);
}

View file

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