single click channel (#7596)

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



Release Notes:

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

**or**

- N/A

---------

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

View File

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

View File

@ -7,7 +7,6 @@ use settings::Settings;
#[derive(Deserialize, Debug)]
pub struct CallSettings {
pub mute_on_join: bool,
pub share_on_join: bool,
}
/// Configuration of voice calls in Zed.
@ -17,11 +16,6 @@ pub struct CallSettingsContent {
///
/// Default: false
pub mute_on_join: Option<bool>,
/// Whether your current project should be shared when joining an empty channel.
///
/// Default: true
pub share_on_join: Option<bool>,
}
impl Settings for CallSettings {

View File

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

View File

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

View File

@ -163,7 +163,8 @@ CREATE TABLE "room_participants" (
"calling_connection_id" INTEGER NOT NULL,
"calling_connection_server_id" INTEGER REFERENCES servers (id) ON DELETE SET NULL,
"participant_index" INTEGER,
"role" TEXT
"role" TEXT,
"in_call" BOOLEAN NOT NULL DEFAULT FALSE
);
CREATE UNIQUE INDEX "index_room_participants_on_user_id" ON "room_participants" ("user_id");
CREATE INDEX "index_room_participants_on_room_id" ON "room_participants" ("room_id");

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -13,7 +13,7 @@ use client::{
use collections::{HashMap, HashSet};
use fs::FakeFs;
use futures::{channel::oneshot, StreamExt as _};
use gpui::{BackgroundExecutor, Context, Model, TestAppContext, View, VisualTestContext};
use gpui::{BackgroundExecutor, Context, Model, Task, TestAppContext, View, VisualTestContext};
use language::LanguageRegistry;
use node_runtime::FakeNodeRuntime;
@ -36,7 +36,7 @@ use std::{
Arc,
},
};
use util::http::FakeHttpClient;
use util::{http::FakeHttpClient, SemanticVersion};
use workspace::{Workspace, WorkspaceStore};
pub struct TestServer {
@ -230,6 +230,7 @@ impl TestServer {
server_conn,
client_name,
user,
SemanticVersion::default(),
None,
Some(connection_id_tx),
Executor::Deterministic(cx.background_executor().clone()),
@ -685,7 +686,7 @@ impl TestClient {
channel_id: u64,
cx: &'a mut TestAppContext,
) -> (View<Workspace>, &'a mut VisualTestContext) {
cx.update(|cx| workspace::join_channel(channel_id, self.app_state.clone(), None, cx))
cx.update(|cx| workspace::open_channel(channel_id, self.app_state.clone(), None, cx))
.await
.unwrap();
cx.run_until_parked();
@ -760,6 +761,11 @@ impl TestClient {
}
}
pub fn join_channel_call(cx: &mut TestAppContext) -> Task<anyhow::Result<()>> {
let room = cx.read(|cx| ActiveCall::global(cx).read(cx).room().cloned());
room.unwrap().update(cx, |room, cx| room.join_call(cx))
}
impl Drop for TestClient {
fn drop(&mut self) {
self.app_state.client.teardown();

View File

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

View File

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

View File

@ -22,10 +22,7 @@ pub use panel_settings::{
use settings::Settings;
use workspace::{notifications::DetachAndPromptErr, AppState};
actions!(
collab,
[ToggleScreenSharing, ToggleMute, ToggleDeafen, LeaveCall]
);
actions!(collab, [ToggleScreenSharing, ToggleMute, LeaveCall]);
pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
CollaborationPanelSettings::register(cx);
@ -85,12 +82,6 @@ pub fn toggle_mute(_: &ToggleMute, cx: &mut AppContext) {
}
}
pub fn toggle_deafen(_: &ToggleDeafen, cx: &mut AppContext) {
if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
room.update(cx, |room, cx| room.toggle_deafen(cx));
}
}
fn notification_window_options(
screen: Rc<dyn PlatformDisplay>,
window_size: Size<Pixels>,

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

View File

@ -183,7 +183,12 @@ message Envelope {
LspExtExpandMacroResponse lsp_ext_expand_macro_response = 155;
SetRoomParticipantRole set_room_participant_role = 156;
UpdateUserChannels update_user_channels = 157; // current max
UpdateUserChannels update_user_channels = 157;
JoinChannel2 join_channel2 = 158;
JoinChannelCall join_channel_call = 159;
JoinChannelCallResponse join_channel_call_response = 160;
LeaveChannelCall leave_channel_call = 161; // current max
}
}
@ -291,6 +296,7 @@ message Participant {
ParticipantLocation location = 4;
uint32 participant_index = 5;
ChannelRole role = 6;
bool in_call = 7;
}
message PendingParticipant {
@ -1033,6 +1039,22 @@ message JoinChannel {
uint64 channel_id = 1;
}
message JoinChannel2 {
uint64 channel_id = 1;
}
message JoinChannelCall {
uint64 channel_id = 1;
}
message JoinChannelCallResponse {
LiveKitConnectionInfo live_kit_connection_info = 1;
}
message LeaveChannelCall {
uint64 channel_id = 1;
}
message DeleteChannel {
uint64 channel_id = 1;
}

View File

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

View File

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

View File

@ -12,7 +12,7 @@ mod toolbar;
mod workspace_settings;
use anyhow::{anyhow, Context as _, Result};
use call::{call_settings::CallSettings, ActiveCall};
use call::ActiveCall;
use client::{
proto::{self, ErrorCode, PeerId},
Client, ErrorExt, Status, TypedEnvelope, UserStore,
@ -3977,8 +3977,6 @@ pub async fn last_opened_workspace_paths() -> Option<WorkspaceLocation> {
DB.last_workspace().await.log_err().flatten()
}
actions!(collab, [OpenChannelNotes]);
async fn join_channel_internal(
channel_id: u64,
app_state: &Arc<AppState>,
@ -4080,36 +4078,6 @@ async fn join_channel_internal(
return Some(join_remote_project(project, host, app_state.clone(), cx));
}
// if you are the first to join a channel, share your project
if room.remote_participants().len() == 0 && !room.local_participant_is_guest() {
if let Some(workspace) = requesting_window {
let project = workspace.update(cx, |workspace, cx| {
if !CallSettings::get_global(cx).share_on_join {
return None;
}
let project = workspace.project.read(cx);
if project.is_local()
&& project.visible_worktrees(cx).any(|tree| {
tree.read(cx)
.root_entry()
.map_or(false, |entry| entry.is_dir())
})
{
Some(workspace.project.clone())
} else {
None
}
});
if let Ok(Some(project)) = project {
return Some(cx.spawn(|room, mut cx| async move {
room.update(&mut cx, |room, cx| room.share_project(project, cx))?
.await?;
Ok(())
}));
}
}
}
None
})?;
if let Some(task) = task {
@ -4119,7 +4087,7 @@ async fn join_channel_internal(
anyhow::Ok(false)
}
pub fn join_channel(
pub fn open_channel(
channel_id: u64,
app_state: Arc<AppState>,
requesting_window: Option<WindowHandle<Workspace>>,
@ -4152,12 +4120,6 @@ pub fn join_channel(
})?
.await?;
if result.is_ok() {
cx.update(|cx| {
cx.dispatch_action(&OpenChannelNotes);
}).log_err();
}
active_window = Some(window_handle);
}

View File

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