mirror of
https://github.com/zed-industries/zed.git
synced 2026-06-01 03:14:56 +07:00
Merge branch 'main' into feat/copyable-errors
This commit is contained in:
commit
dde52301f9
20 changed files with 622 additions and 44 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -3279,6 +3279,7 @@ dependencies = [
|
||||||
"collections",
|
"collections",
|
||||||
"db",
|
"db",
|
||||||
"editor",
|
"editor",
|
||||||
|
"feature_flags",
|
||||||
"futures 0.3.32",
|
"futures 0.3.32",
|
||||||
"fuzzy",
|
"fuzzy",
|
||||||
"gpui",
|
"gpui",
|
||||||
|
|
|
||||||
|
|
@ -112,6 +112,13 @@ impl AnyActiveCall for ActiveCallEntity {
|
||||||
.map_or(false, |room| room.read(cx).is_sharing_project())
|
.map_or(false, |room| room.read(cx).is_sharing_project())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_sharing_screen(&self, cx: &App) -> bool {
|
||||||
|
self.0
|
||||||
|
.read(cx)
|
||||||
|
.room()
|
||||||
|
.map_or(false, |room| room.read(cx).is_sharing_screen())
|
||||||
|
}
|
||||||
|
|
||||||
fn has_remote_participants(&self, cx: &App) -> bool {
|
fn has_remote_participants(&self, cx: &App) -> bool {
|
||||||
self.0.read(cx).room().map_or(false, |room| {
|
self.0.read(cx).room().map_or(false, |room| {
|
||||||
!room.read(cx).remote_participants().is_empty()
|
!room.read(cx).remote_participants().is_empty()
|
||||||
|
|
@ -209,6 +216,12 @@ impl AnyActiveCall for ActiveCallEntity {
|
||||||
participant_id: *participant_id,
|
participant_id: *participant_id,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
room::Event::LocalScreenShareStarted => {
|
||||||
|
Some(ActiveCallEvent::LocalScreenShareStarted)
|
||||||
|
}
|
||||||
|
room::Event::LocalScreenShareStopped => {
|
||||||
|
Some(ActiveCallEvent::LocalScreenShareStopped)
|
||||||
|
}
|
||||||
_ => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
if let Some(event) = mapped {
|
if let Some(event) = mapped {
|
||||||
|
|
@ -297,6 +310,18 @@ impl AnyActiveCall for ActiveCallEntity {
|
||||||
)
|
)
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn peer_ids_with_video_tracks(&self, cx: &App) -> Vec<proto::PeerId> {
|
||||||
|
let Some(room) = self.0.read(cx).room() else {
|
||||||
|
return Vec::new();
|
||||||
|
};
|
||||||
|
room.read(cx)
|
||||||
|
.remote_participants()
|
||||||
|
.values()
|
||||||
|
.filter(|p| p.has_video_tracks())
|
||||||
|
.map(|p| p.peer_id)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct OneAtATime {
|
pub struct OneAtATime {
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,8 @@ pub enum Event {
|
||||||
RoomLeft {
|
RoomLeft {
|
||||||
channel_id: Option<ChannelId>,
|
channel_id: Option<ChannelId>,
|
||||||
},
|
},
|
||||||
|
LocalScreenShareStarted,
|
||||||
|
LocalScreenShareStopped,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Room {
|
pub struct Room {
|
||||||
|
|
@ -1513,6 +1515,7 @@ impl Room {
|
||||||
track_publication: publication,
|
track_publication: publication,
|
||||||
_stream: stream,
|
_stream: stream,
|
||||||
};
|
};
|
||||||
|
cx.emit(Event::LocalScreenShareStarted);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1674,6 +1677,7 @@ impl Room {
|
||||||
let sid = track_publication.sid();
|
let sid = track_publication.sid();
|
||||||
cx.spawn(async move |_, cx| local_participant.unpublish_track(sid, cx).await)
|
cx.spawn(async move |_, cx| local_participant.unpublish_track(sid, cx).await)
|
||||||
.detach_and_log_err(cx);
|
.detach_and_log_err(cx);
|
||||||
|
cx.emit(Event::LocalScreenShareStopped);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,17 +7,16 @@ use cloud_api_client::{
|
||||||
};
|
};
|
||||||
use cloud_llm_client::{CurrentUsage, UsageData, UsageLimit};
|
use cloud_llm_client::{CurrentUsage, UsageData, UsageLimit};
|
||||||
use futures::{StreamExt, stream::BoxStream};
|
use futures::{StreamExt, stream::BoxStream};
|
||||||
use gpui::{AppContext as _, Entity, TestAppContext};
|
use gpui::{AppContext as _, TestAppContext};
|
||||||
use http_client::{AsyncBody, Method, Request, http};
|
use http_client::{AsyncBody, Method, Request, http};
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use rpc::{ConnectionId, Peer, Receipt, TypedEnvelope, proto};
|
use rpc::{ConnectionId, Peer, Receipt, TypedEnvelope, proto};
|
||||||
|
|
||||||
use crate::{Client, Connection, Credentials, EstablishConnectionError, UserStore};
|
use crate::{Client, Connection, Credentials, EstablishConnectionError};
|
||||||
|
|
||||||
pub struct FakeServer {
|
pub struct FakeServer {
|
||||||
peer: Arc<Peer>,
|
peer: Arc<Peer>,
|
||||||
state: Arc<Mutex<FakeServerState>>,
|
state: Arc<Mutex<FakeServerState>>,
|
||||||
user_id: u64,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
|
|
@ -38,7 +37,6 @@ impl FakeServer {
|
||||||
let server = Self {
|
let server = Self {
|
||||||
peer: Peer::new(0),
|
peer: Peer::new(0),
|
||||||
state: Default::default(),
|
state: Default::default(),
|
||||||
user_id: client_user_id,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
client.http_client().as_fake().replace_handler({
|
client.http_client().as_fake().replace_handler({
|
||||||
|
|
@ -213,23 +211,6 @@ impl FakeServer {
|
||||||
fn connection_id(&self) -> ConnectionId {
|
fn connection_id(&self) -> ConnectionId {
|
||||||
self.state.lock().connection_id.expect("not connected")
|
self.state.lock().connection_id.expect("not connected")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn build_user_store(
|
|
||||||
&self,
|
|
||||||
client: Arc<Client>,
|
|
||||||
cx: &mut TestAppContext,
|
|
||||||
) -> Entity<UserStore> {
|
|
||||||
let user_store = cx.new(|cx| UserStore::new(client, cx));
|
|
||||||
assert_eq!(
|
|
||||||
self.receive::<proto::GetUsers>()
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.payload
|
|
||||||
.user_ids,
|
|
||||||
&[self.user_id]
|
|
||||||
);
|
|
||||||
user_store
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Drop for FakeServer {
|
impl Drop for FakeServer {
|
||||||
|
|
|
||||||
|
|
@ -74,7 +74,7 @@ pub async fn validate_header<B>(mut req: Request<B>, next: Next<B>) -> impl Into
|
||||||
.await?
|
.await?
|
||||||
.with_context(|| format!("user {user_id} not found"))?;
|
.with_context(|| format!("user {user_id} not found"))?;
|
||||||
|
|
||||||
req.extensions_mut().insert(Principal::User(user));
|
req.extensions_mut().insert(Principal::User(user.into()));
|
||||||
return Ok::<_, Error>(next.run(req).await);
|
return Ok::<_, Error>(next.run(req).await);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,6 @@ use worktree_settings_file::LocalSettingsKind;
|
||||||
|
|
||||||
pub use ids::*;
|
pub use ids::*;
|
||||||
pub use sea_orm::ConnectOptions;
|
pub use sea_orm::ConnectOptions;
|
||||||
pub use tables::user::Model as User;
|
|
||||||
pub use tables::*;
|
pub use tables::*;
|
||||||
|
|
||||||
#[cfg(feature = "test-support")]
|
#[cfg(feature = "test-support")]
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,10 @@ impl Database {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a user by GitHub login. There are no access checks here, so this should only be used internally.
|
/// Returns a user by GitHub login. There are no access checks here, so this should only be used internally.
|
||||||
pub async fn get_user_by_github_login(&self, github_login: &str) -> Result<Option<User>> {
|
pub async fn get_user_by_github_login(
|
||||||
|
&self,
|
||||||
|
github_login: &str,
|
||||||
|
) -> Result<Option<user::Model>> {
|
||||||
self.transaction(|tx| async move {
|
self.transaction(|tx| async move {
|
||||||
Ok(user::Entity::find()
|
Ok(user::Entity::find()
|
||||||
.filter(user::Column::GithubLogin.eq(github_login))
|
.filter(user::Column::GithubLogin.eq(github_login))
|
||||||
|
|
@ -78,7 +81,7 @@ impl Database {
|
||||||
github_name: Option<&str>,
|
github_name: Option<&str>,
|
||||||
github_user_created_at: DateTimeUtc,
|
github_user_created_at: DateTimeUtc,
|
||||||
initial_channel_id: Option<ChannelId>,
|
initial_channel_id: Option<ChannelId>,
|
||||||
) -> Result<User> {
|
) -> Result<user::Model> {
|
||||||
self.transaction(|tx| async move {
|
self.transaction(|tx| async move {
|
||||||
self.update_or_create_user_by_github_account_tx(
|
self.update_or_create_user_by_github_account_tx(
|
||||||
github_login,
|
github_login,
|
||||||
|
|
@ -103,7 +106,7 @@ impl Database {
|
||||||
github_user_created_at: NaiveDateTime,
|
github_user_created_at: NaiveDateTime,
|
||||||
initial_channel_id: Option<ChannelId>,
|
initial_channel_id: Option<ChannelId>,
|
||||||
tx: &DatabaseTransaction,
|
tx: &DatabaseTransaction,
|
||||||
) -> Result<User> {
|
) -> Result<user::Model> {
|
||||||
if let Some(existing_user) = self
|
if let Some(existing_user) = self
|
||||||
.get_user_by_github_user_id_or_github_login(github_user_id, github_login, tx)
|
.get_user_by_github_user_id_or_github_login(github_user_id, github_login, tx)
|
||||||
.await?
|
.await?
|
||||||
|
|
@ -156,7 +159,7 @@ impl Database {
|
||||||
github_user_id: i32,
|
github_user_id: i32,
|
||||||
github_login: &str,
|
github_login: &str,
|
||||||
tx: &DatabaseTransaction,
|
tx: &DatabaseTransaction,
|
||||||
) -> Result<Option<User>> {
|
) -> Result<Option<user::Model>> {
|
||||||
if let Some(user_by_github_user_id) = user::Entity::find()
|
if let Some(user_by_github_user_id) = user::Entity::find()
|
||||||
.filter(user::Column::GithubUserId.eq(github_user_id))
|
.filter(user::Column::GithubUserId.eq(github_user_id))
|
||||||
.one(tx)
|
.one(tx)
|
||||||
|
|
@ -178,7 +181,7 @@ impl Database {
|
||||||
|
|
||||||
/// get_all_users returns the next page of users. To get more call again with
|
/// get_all_users returns the next page of users. To get more call again with
|
||||||
/// the same limit and the page incremented by 1.
|
/// the same limit and the page incremented by 1.
|
||||||
pub async fn get_all_users(&self, page: u32, limit: u32) -> Result<Vec<User>> {
|
pub async fn get_all_users(&self, page: u32, limit: u32) -> Result<Vec<user::Model>> {
|
||||||
self.transaction(|tx| async move {
|
self.transaction(|tx| async move {
|
||||||
Ok(user::Entity::find()
|
Ok(user::Entity::find()
|
||||||
.order_by_asc(user::Column::GithubLogin)
|
.order_by_asc(user::Column::GithubLogin)
|
||||||
|
|
@ -207,7 +210,11 @@ impl Database {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Find users where github_login ILIKE name_query.
|
/// Find users where github_login ILIKE name_query.
|
||||||
pub async fn fuzzy_search_users(&self, name_query: &str, limit: u32) -> Result<Vec<User>> {
|
pub async fn fuzzy_search_users(
|
||||||
|
&self,
|
||||||
|
name_query: &str,
|
||||||
|
limit: u32,
|
||||||
|
) -> Result<Vec<user::Model>> {
|
||||||
self.transaction(|tx| async {
|
self.transaction(|tx| async {
|
||||||
let tx = tx;
|
let tx = tx;
|
||||||
let like_string = Self::fuzzy_like_string(name_query);
|
let like_string = Self::fuzzy_like_string(name_query);
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,17 @@ pub struct Model {
|
||||||
pub created_at: NaiveDateTime,
|
pub created_at: NaiveDateTime,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<Model> for crate::entities::User {
|
||||||
|
fn from(user: Model) -> Self {
|
||||||
|
crate::entities::User {
|
||||||
|
id: user.id,
|
||||||
|
github_login: user.github_login,
|
||||||
|
admin: user.admin,
|
||||||
|
connected_once: user.connected_once,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||||
pub enum Relation {
|
pub enum Relation {
|
||||||
#[sea_orm(has_one = "super::room_participant::Entity")]
|
#[sea_orm(has_one = "super::room_participant::Entity")]
|
||||||
|
|
|
||||||
3
crates/collab/src/entities.rs
Normal file
3
crates/collab/src/entities.rs
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
mod user;
|
||||||
|
|
||||||
|
pub use user::*;
|
||||||
9
crates/collab/src/entities/user.rs
Normal file
9
crates/collab/src/entities/user.rs
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
use crate::db::UserId;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct User {
|
||||||
|
pub id: UserId,
|
||||||
|
pub github_login: String,
|
||||||
|
pub admin: bool,
|
||||||
|
pub connected_once: bool,
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
pub mod api;
|
pub mod api;
|
||||||
pub mod auth;
|
pub mod auth;
|
||||||
pub mod db;
|
pub mod db;
|
||||||
|
pub mod entities;
|
||||||
pub mod env;
|
pub mod env;
|
||||||
pub mod executor;
|
pub mod executor;
|
||||||
pub mod rpc;
|
pub mod rpc;
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,13 @@
|
||||||
mod connection_pool;
|
mod connection_pool;
|
||||||
|
|
||||||
use crate::api::{CloudflareIpCountryHeader, SystemIdHeader};
|
use crate::api::{CloudflareIpCountryHeader, SystemIdHeader};
|
||||||
|
use crate::entities::User;
|
||||||
use crate::{
|
use crate::{
|
||||||
AppState, Error, Result, auth,
|
AppState, Error, Result, auth,
|
||||||
db::{
|
db::{
|
||||||
self, BufferId, Capability, Channel, ChannelId, ChannelRole, ChannelsForUser, Database,
|
self, BufferId, Capability, Channel, ChannelId, ChannelRole, ChannelsForUser, Database,
|
||||||
InviteMemberResult, MembershipUpdated, NotificationId, ProjectId, RejoinedProject,
|
InviteMemberResult, MembershipUpdated, NotificationId, ProjectId, RejoinedProject,
|
||||||
RemoveChannelMemberResult, RespondToChannelInvite, RoomId, ServerId, SharedThreadId, User,
|
RemoveChannelMemberResult, RespondToChannelInvite, RoomId, ServerId, SharedThreadId,
|
||||||
UserId,
|
UserId,
|
||||||
},
|
},
|
||||||
executor::Executor,
|
executor::Executor,
|
||||||
|
|
|
||||||
272
crates/collab/tests/integration/auto_watch_tests.rs
Normal file
272
crates/collab/tests/integration/auto_watch_tests.rs
Normal file
|
|
@ -0,0 +1,272 @@
|
||||||
|
use crate::TestServer;
|
||||||
|
use call::ActiveCall;
|
||||||
|
use gpui::{App, BackgroundExecutor, Entity, TestAppContext, TestScreenCaptureSource};
|
||||||
|
use project::Project;
|
||||||
|
use serde_json::json;
|
||||||
|
use util::path;
|
||||||
|
use workspace::Workspace;
|
||||||
|
|
||||||
|
use super::TestClient;
|
||||||
|
|
||||||
|
struct AutoWatchTestSetup {
|
||||||
|
client_a: TestClient,
|
||||||
|
_client_b: TestClient,
|
||||||
|
_client_c: TestClient,
|
||||||
|
project_a: Entity<Project>,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn setup_auto_watch_test(
|
||||||
|
server: &mut TestServer,
|
||||||
|
user_a: &mut TestAppContext,
|
||||||
|
user_b: &mut TestAppContext,
|
||||||
|
user_c: &mut TestAppContext,
|
||||||
|
) -> AutoWatchTestSetup {
|
||||||
|
let client_a = server.create_client(user_a, "user_a").await;
|
||||||
|
let client_b = server.create_client(user_b, "user_b").await;
|
||||||
|
let client_c = server.create_client(user_c, "user_c").await;
|
||||||
|
server
|
||||||
|
.create_room(&mut [
|
||||||
|
(&client_a, user_a),
|
||||||
|
(&client_b, user_b),
|
||||||
|
(&client_c, user_c),
|
||||||
|
])
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let active_call_a = user_a.read(ActiveCall::global);
|
||||||
|
|
||||||
|
client_a
|
||||||
|
.fs()
|
||||||
|
.insert_tree(path!("/a"), json!({ "file.txt": "content" }))
|
||||||
|
.await;
|
||||||
|
let (project_a, _worktree_id) = client_a.build_local_project(path!("/a"), user_a).await;
|
||||||
|
active_call_a
|
||||||
|
.update(user_a, |call, cx| call.set_location(Some(&project_a), cx))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
AutoWatchTestSetup {
|
||||||
|
client_a,
|
||||||
|
_client_b: client_b,
|
||||||
|
_client_c: client_c,
|
||||||
|
project_a,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_auto_watch_opens_existing_share_on_toggle(
|
||||||
|
executor: BackgroundExecutor,
|
||||||
|
user_a: &mut TestAppContext,
|
||||||
|
user_b: &mut TestAppContext,
|
||||||
|
user_c: &mut TestAppContext,
|
||||||
|
) {
|
||||||
|
let mut server = TestServer::start(executor.clone()).await;
|
||||||
|
let setup = setup_auto_watch_test(&mut server, user_a, user_b, user_c).await;
|
||||||
|
let (workspace_a, user_a) = setup.client_a.build_workspace(&setup.project_a, user_a);
|
||||||
|
executor.run_until_parked();
|
||||||
|
|
||||||
|
start_screen_share(user_b).await;
|
||||||
|
executor.run_until_parked();
|
||||||
|
|
||||||
|
workspace_a.update_in(user_a, |workspace, window, cx| {
|
||||||
|
workspace.toggle_auto_watch(window, cx);
|
||||||
|
});
|
||||||
|
executor.run_until_parked();
|
||||||
|
|
||||||
|
workspace_a.update(user_a, |workspace, cx| {
|
||||||
|
assert_active_matches_title(workspace, "user_b's screen", cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_auto_watch_opens_share_when_no_one_is_sharing_yet(
|
||||||
|
executor: BackgroundExecutor,
|
||||||
|
user_a: &mut TestAppContext,
|
||||||
|
user_b: &mut TestAppContext,
|
||||||
|
user_c: &mut TestAppContext,
|
||||||
|
) {
|
||||||
|
let mut server = TestServer::start(executor.clone()).await;
|
||||||
|
let setup = setup_auto_watch_test(&mut server, user_a, user_b, user_c).await;
|
||||||
|
let (workspace_a, user_a) = setup.client_a.build_workspace(&setup.project_a, user_a);
|
||||||
|
|
||||||
|
workspace_a.update_in(user_a, |workspace, window, cx| {
|
||||||
|
workspace.toggle_auto_watch(window, cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
start_screen_share(user_b).await;
|
||||||
|
executor.run_until_parked();
|
||||||
|
|
||||||
|
workspace_a.update(user_a, |workspace, cx| {
|
||||||
|
assert_active_matches_title(workspace, "user_b's screen", cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_auto_watch_switches_to_next_share_on_share_end(
|
||||||
|
executor: BackgroundExecutor,
|
||||||
|
user_a: &mut TestAppContext,
|
||||||
|
user_b: &mut TestAppContext,
|
||||||
|
user_c: &mut TestAppContext,
|
||||||
|
) {
|
||||||
|
let mut server = TestServer::start(executor.clone()).await;
|
||||||
|
let setup = setup_auto_watch_test(&mut server, user_a, user_b, user_c).await;
|
||||||
|
let (workspace_a, user_a) = setup.client_a.build_workspace(&setup.project_a, user_a);
|
||||||
|
|
||||||
|
workspace_a.update_in(user_a, |workspace, window, cx| {
|
||||||
|
workspace.toggle_auto_watch(window, cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
start_screen_share(user_b).await;
|
||||||
|
executor.run_until_parked();
|
||||||
|
|
||||||
|
workspace_a.update(user_a, |workspace, cx| {
|
||||||
|
assert_active_matches_title(workspace, "user_b's screen", cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
start_screen_share(user_c).await;
|
||||||
|
executor.run_until_parked();
|
||||||
|
|
||||||
|
stop_screen_share(user_b);
|
||||||
|
executor.run_until_parked();
|
||||||
|
|
||||||
|
workspace_a.update(user_a, |workspace, cx| {
|
||||||
|
assert_active_matches_title(workspace, "user_c's screen", cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_auto_watch_ignores_shares_while_user_is_sharing(
|
||||||
|
executor: BackgroundExecutor,
|
||||||
|
user_a: &mut TestAppContext,
|
||||||
|
user_b: &mut TestAppContext,
|
||||||
|
user_c: &mut TestAppContext,
|
||||||
|
) {
|
||||||
|
let mut server = TestServer::start(executor.clone()).await;
|
||||||
|
let setup = setup_auto_watch_test(&mut server, user_a, user_b, user_c).await;
|
||||||
|
let (workspace_a, user_a) = setup.client_a.build_workspace(&setup.project_a, user_a);
|
||||||
|
|
||||||
|
start_screen_share(user_a).await;
|
||||||
|
executor.run_until_parked();
|
||||||
|
start_screen_share(user_b).await;
|
||||||
|
executor.run_until_parked();
|
||||||
|
|
||||||
|
// Should NOT open B's screen cause we are sharing
|
||||||
|
workspace_a.update_in(user_a, |workspace, window, cx| {
|
||||||
|
workspace.toggle_auto_watch(window, cx);
|
||||||
|
});
|
||||||
|
executor.run_until_parked();
|
||||||
|
|
||||||
|
// Ensure that no screen share is found in user a's tab bar
|
||||||
|
workspace_a.update(user_a, |workspace, cx| {
|
||||||
|
let has_shared_screen_tab = workspace
|
||||||
|
.active_pane()
|
||||||
|
.read(cx)
|
||||||
|
.items()
|
||||||
|
.any(|item| item.tab_content_text(0, cx).contains("screen"));
|
||||||
|
assert!(
|
||||||
|
!has_shared_screen_tab,
|
||||||
|
"should not open anyone's screen share when toggling on while sharing"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_auto_watch_opens_share_after_local_user_stops_sharing(
|
||||||
|
executor: BackgroundExecutor,
|
||||||
|
user_a: &mut TestAppContext,
|
||||||
|
user_b: &mut TestAppContext,
|
||||||
|
user_c: &mut TestAppContext,
|
||||||
|
) {
|
||||||
|
let mut server = TestServer::start(executor.clone()).await;
|
||||||
|
let setup = setup_auto_watch_test(&mut server, user_a, user_b, user_c).await;
|
||||||
|
let (workspace_a, user_a) = setup.client_a.build_workspace(&setup.project_a, user_a);
|
||||||
|
|
||||||
|
workspace_a.update_in(user_a, |workspace, window, cx| {
|
||||||
|
workspace.toggle_auto_watch(window, cx);
|
||||||
|
});
|
||||||
|
start_screen_share(user_a).await;
|
||||||
|
executor.run_until_parked();
|
||||||
|
|
||||||
|
start_screen_share(user_b).await;
|
||||||
|
executor.run_until_parked();
|
||||||
|
|
||||||
|
stop_screen_share(user_a);
|
||||||
|
executor.run_until_parked();
|
||||||
|
|
||||||
|
workspace_a.update(user_a, |workspace, cx| {
|
||||||
|
assert_active_matches_title(workspace, "user_b's screen", cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_auto_watch_toggle_off_leaves_tabs_open(
|
||||||
|
executor: BackgroundExecutor,
|
||||||
|
user_a: &mut TestAppContext,
|
||||||
|
user_b: &mut TestAppContext,
|
||||||
|
user_c: &mut TestAppContext,
|
||||||
|
) {
|
||||||
|
let mut server = TestServer::start(executor.clone()).await;
|
||||||
|
let setup = setup_auto_watch_test(&mut server, user_a, user_b, user_c).await;
|
||||||
|
let (workspace_a, user_a) = setup.client_a.build_workspace(&setup.project_a, user_a);
|
||||||
|
|
||||||
|
workspace_a.update_in(user_a, |workspace, window, cx| {
|
||||||
|
workspace.toggle_auto_watch(window, cx);
|
||||||
|
});
|
||||||
|
start_screen_share(user_b).await;
|
||||||
|
executor.run_until_parked();
|
||||||
|
|
||||||
|
workspace_a.update(user_a, |workspace, cx| {
|
||||||
|
assert_active_matches_title(workspace, "user_b's screen", cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
workspace_a.update_in(user_a, |workspace, window, cx| {
|
||||||
|
workspace.toggle_auto_watch(window, cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
workspace_a.update(user_a, |workspace, cx| {
|
||||||
|
assert_active_matches_title(workspace, "user_b's screen", cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[track_caller]
|
||||||
|
fn assert_active_matches_title(workspace: &Workspace, expected_title: &str, cx: &App) {
|
||||||
|
let active_item = workspace.active_item(cx).expect("no active item");
|
||||||
|
assert_eq!(
|
||||||
|
active_item.tab_content_text(0, cx),
|
||||||
|
expected_title,
|
||||||
|
"expected active item to be '{}'",
|
||||||
|
expected_title
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn start_screen_share(cx: &mut TestAppContext) {
|
||||||
|
let display = TestScreenCaptureSource::new();
|
||||||
|
cx.set_screen_capture_sources(vec![display]);
|
||||||
|
let screen = cx
|
||||||
|
.update(|cx| cx.screen_capture_sources())
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.unwrap()
|
||||||
|
.into_iter()
|
||||||
|
.next()
|
||||||
|
.unwrap();
|
||||||
|
let active_call = cx.read(ActiveCall::global);
|
||||||
|
active_call
|
||||||
|
.update(cx, |call, cx| {
|
||||||
|
call.room()
|
||||||
|
.unwrap()
|
||||||
|
.update(cx, |room, cx| room.share_screen(screen, cx))
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn stop_screen_share(cx: &mut TestAppContext) {
|
||||||
|
let active_call = cx.read(ActiveCall::global);
|
||||||
|
active_call
|
||||||
|
.update(cx, |call, cx| {
|
||||||
|
call.room()
|
||||||
|
.unwrap()
|
||||||
|
.update(cx, |room, cx| room.unshare_screen(true, cx))
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
@ -3,6 +3,7 @@ use client::ChannelId;
|
||||||
use gpui::{Entity, TestAppContext};
|
use gpui::{Entity, TestAppContext};
|
||||||
|
|
||||||
mod agent_sharing_tests;
|
mod agent_sharing_tests;
|
||||||
|
mod auto_watch_tests;
|
||||||
mod channel_buffer_tests;
|
mod channel_buffer_tests;
|
||||||
mod channel_guest_tests;
|
mod channel_guest_tests;
|
||||||
mod channel_tests;
|
mod channel_tests;
|
||||||
|
|
|
||||||
|
|
@ -294,7 +294,7 @@ impl TestServer {
|
||||||
cx.background_spawn(server.handle_connection(
|
cx.background_spawn(server.handle_connection(
|
||||||
server_conn,
|
server_conn,
|
||||||
client_name,
|
client_name,
|
||||||
Principal::User(user),
|
Principal::User(user.into()),
|
||||||
ZedVersion(semver::Version::new(1, 0, 0)),
|
ZedVersion(semver::Version::new(1, 0, 0)),
|
||||||
Some("test".to_string()),
|
Some("test".to_string()),
|
||||||
None,
|
None,
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ client.workspace = true
|
||||||
collections.workspace = true
|
collections.workspace = true
|
||||||
db.workspace = true
|
db.workspace = true
|
||||||
editor.workspace = true
|
editor.workspace = true
|
||||||
|
feature_flags.workspace = true
|
||||||
futures.workspace = true
|
futures.workspace = true
|
||||||
fuzzy.workspace = true
|
fuzzy.workspace = true
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ use collections::{HashMap, HashSet};
|
||||||
use contact_finder::ContactFinder;
|
use contact_finder::ContactFinder;
|
||||||
use db::kvp::KeyValueStore;
|
use db::kvp::KeyValueStore;
|
||||||
use editor::{Editor, EditorElement, EditorStyle};
|
use editor::{Editor, EditorElement, EditorStyle};
|
||||||
|
use feature_flags::{AutoWatchFeatureFlag, FeatureFlagAppExt as _};
|
||||||
use fuzzy::{StringMatch, StringMatchCandidate, match_strings};
|
use fuzzy::{StringMatch, StringMatchCandidate, match_strings};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AnyElement, App, AsyncWindowContext, Bounds, ClickEvent, ClipboardItem, DismissEvent, Div,
|
AnyElement, App, AsyncWindowContext, Bounds, ClickEvent, ClipboardItem, DismissEvent, Div,
|
||||||
|
|
@ -35,13 +36,13 @@ use theme::ActiveTheme;
|
||||||
use theme_settings::ThemeSettings;
|
use theme_settings::ThemeSettings;
|
||||||
use ui::{
|
use ui::{
|
||||||
Avatar, AvatarAvailabilityIndicator, CollabNotification, ContextMenu, CopyButton, Facepile,
|
Avatar, AvatarAvailabilityIndicator, CollabNotification, ContextMenu, CopyButton, Facepile,
|
||||||
HighlightedLabel, IconButtonShape, Indicator, ListHeader, ListItem, Tab, Tooltip, prelude::*,
|
HighlightedLabel, IconButtonShape, Indicator, ListHeader, ListItem, Tab, TintColor, Tooltip,
|
||||||
tooltip_container,
|
prelude::*, tooltip_container,
|
||||||
};
|
};
|
||||||
use util::{ResultExt, TryFutureExt, maybe};
|
use util::{ResultExt, TryFutureExt, maybe};
|
||||||
use workspace::{
|
use workspace::{
|
||||||
CopyRoomId, Deafen, LeaveCall, MultiWorkspace, Mute, OpenChannelNotes, OpenChannelNotesById,
|
AutoWatch, CopyRoomId, Deafen, LeaveCall, MultiWorkspace, Mute, OpenChannelNotes,
|
||||||
ScreenShare, ShareProject, Workspace,
|
OpenChannelNotesById, ScreenShare, ShareProject, Workspace,
|
||||||
dock::{DockPosition, Panel, PanelEvent},
|
dock::{DockPosition, Panel, PanelEvent},
|
||||||
notifications::{
|
notifications::{
|
||||||
DetachAndPromptErr, Notification as WorkspaceNotification, NotificationId, NotifyResultExt,
|
DetachAndPromptErr, Notification as WorkspaceNotification, NotificationId, NotifyResultExt,
|
||||||
|
|
@ -2895,13 +2896,75 @@ impl CollabPanel {
|
||||||
Section::Offline => SharedString::from("Offline"),
|
Section::Offline => SharedString::from("Offline"),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let auto_watch_state = self
|
||||||
|
.workspace
|
||||||
|
.upgrade()
|
||||||
|
.map_or(AutoWatch::Off, |workspace| {
|
||||||
|
*workspace.read(cx).auto_watch_state()
|
||||||
|
});
|
||||||
|
let is_auto_watching = auto_watch_state.enabled();
|
||||||
|
|
||||||
let button = match section {
|
let button = match section {
|
||||||
Section::ActiveCall => channel_link.map(|channel_link| {
|
Section::ActiveCall => {
|
||||||
CopyButton::new("copy-channel-link", channel_link)
|
let has_auto_watch_flag = cx.has_flag::<AutoWatchFeatureFlag>();
|
||||||
.visible_on_hover("section-header")
|
let show_auto_watch = has_auto_watch_flag && is_auto_watching;
|
||||||
.tooltip_label("Copy Channel Link")
|
let show_copy = channel_link.is_some();
|
||||||
.into_any_element()
|
|
||||||
}),
|
if show_auto_watch || show_copy {
|
||||||
|
Some(
|
||||||
|
h_flex()
|
||||||
|
.when(has_auto_watch_flag, |this| {
|
||||||
|
this.child(
|
||||||
|
IconButton::new(
|
||||||
|
"auto-watch-screens",
|
||||||
|
if is_auto_watching {
|
||||||
|
IconName::Eye
|
||||||
|
} else {
|
||||||
|
IconName::EyeOff
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.icon_size(IconSize::Small)
|
||||||
|
.toggle_state(is_auto_watching)
|
||||||
|
.selected_style(match auto_watch_state {
|
||||||
|
AutoWatch::Paused => {
|
||||||
|
ButtonStyle::Tinted(TintColor::Warning)
|
||||||
|
}
|
||||||
|
_ => ButtonStyle::Tinted(TintColor::Accent),
|
||||||
|
})
|
||||||
|
.when(!is_auto_watching, |this| {
|
||||||
|
this.visible_on_hover("section-header")
|
||||||
|
})
|
||||||
|
.tooltip(Tooltip::text(match auto_watch_state {
|
||||||
|
AutoWatch::Paused => {
|
||||||
|
"Auto Watch Screens (paused while sharing)"
|
||||||
|
}
|
||||||
|
AutoWatch::Active { .. } => "Stop Auto Watching Screens",
|
||||||
|
AutoWatch::Off => "Auto Watch Screens",
|
||||||
|
}))
|
||||||
|
.on_click(cx.listener(
|
||||||
|
|this, _, window, cx| {
|
||||||
|
this.workspace
|
||||||
|
.update(cx, |workspace, cx| {
|
||||||
|
workspace.toggle_auto_watch(window, cx)
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.when_some(channel_link, |this, channel_link| {
|
||||||
|
this.child(
|
||||||
|
CopyButton::new("copy-channel-link", channel_link)
|
||||||
|
.visible_on_hover("section-header")
|
||||||
|
.tooltip_label("Copy Channel Link"),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.into_any_element(),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
Section::Contacts => Some(
|
Section::Contacts => Some(
|
||||||
IconButton::new("add-contact", IconName::Plus)
|
IconButton::new("add-contact", IconName::Plus)
|
||||||
.icon_size(IconSize::Small)
|
.icon_size(IconSize::Small)
|
||||||
|
|
|
||||||
|
|
@ -91,3 +91,11 @@ impl FeatureFlag for AgentThreadWorktreeLabelFlag {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
register_feature_flag!(AgentThreadWorktreeLabelFlag);
|
register_feature_flag!(AgentThreadWorktreeLabelFlag);
|
||||||
|
|
||||||
|
pub struct AutoWatchFeatureFlag;
|
||||||
|
|
||||||
|
impl FeatureFlag for AutoWatchFeatureFlag {
|
||||||
|
const NAME: &'static str = "auto-watch-screens";
|
||||||
|
type Value = PresenceFlag;
|
||||||
|
}
|
||||||
|
register_feature_flag!(AutoWatchFeatureFlag);
|
||||||
|
|
|
||||||
|
|
@ -420,7 +420,80 @@ impl TestServer {
|
||||||
Ok(sid)
|
Ok(sid)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn unpublish_track(&self, _token: String, _track: &TrackSid) -> Result<()> {
|
pub(crate) async fn unpublish_track(&self, token: String, track_sid: &TrackSid) -> Result<()> {
|
||||||
|
let claims = livekit_api::token::validate(&token, &self.secret_key)?;
|
||||||
|
let identity = ParticipantIdentity(claims.sub.unwrap().to_string());
|
||||||
|
let room_name = claims.video.room.unwrap();
|
||||||
|
|
||||||
|
let mut server_rooms = self.rooms.lock();
|
||||||
|
let room = server_rooms
|
||||||
|
.get_mut(&*room_name)
|
||||||
|
.with_context(|| format!("room {room_name} does not exist"))?;
|
||||||
|
|
||||||
|
if let Some(video_to_unpublish) = room.video_tracks.iter().position(|t| t.sid == *track_sid)
|
||||||
|
{
|
||||||
|
let video_to_unpublish = room.video_tracks.remove(video_to_unpublish);
|
||||||
|
for client_room in room
|
||||||
|
.client_rooms
|
||||||
|
.iter()
|
||||||
|
.filter(|(id, _)| **id != identity)
|
||||||
|
.map(|(_, room)| room)
|
||||||
|
{
|
||||||
|
let track = RemoteTrack::Video(RemoteVideoTrack {
|
||||||
|
server_track: video_to_unpublish.clone(),
|
||||||
|
_room: client_room.downgrade(),
|
||||||
|
});
|
||||||
|
let publication = RemoteTrackPublication {
|
||||||
|
sid: track_sid.clone(),
|
||||||
|
room: client_room.downgrade(),
|
||||||
|
track: track.clone(),
|
||||||
|
};
|
||||||
|
let participant = RemoteParticipant {
|
||||||
|
identity: identity.clone(),
|
||||||
|
room: client_room.downgrade(),
|
||||||
|
};
|
||||||
|
let event = RoomEvent::TrackUnsubscribed {
|
||||||
|
track,
|
||||||
|
publication,
|
||||||
|
participant,
|
||||||
|
};
|
||||||
|
|
||||||
|
client_room.0.lock().updates_tx.blocking_send(event).ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(audio_to_unpublish) = room.audio_tracks.iter().position(|t| t.sid == *track_sid)
|
||||||
|
{
|
||||||
|
let audio_to_unpublish = room.audio_tracks.remove(audio_to_unpublish);
|
||||||
|
for client_room in room
|
||||||
|
.client_rooms
|
||||||
|
.iter()
|
||||||
|
.filter(|(id, _)| **id != identity)
|
||||||
|
.map(|(_, room)| room)
|
||||||
|
{
|
||||||
|
let track = RemoteTrack::Audio(RemoteAudioTrack {
|
||||||
|
server_track: audio_to_unpublish.clone(),
|
||||||
|
room: client_room.downgrade(),
|
||||||
|
});
|
||||||
|
let publication = RemoteTrackPublication {
|
||||||
|
sid: track_sid.clone(),
|
||||||
|
room: client_room.downgrade(),
|
||||||
|
track: track.clone(),
|
||||||
|
};
|
||||||
|
let participant = RemoteParticipant {
|
||||||
|
identity: identity.clone(),
|
||||||
|
room: client_room.downgrade(),
|
||||||
|
};
|
||||||
|
let event = RoomEvent::TrackUnsubscribed {
|
||||||
|
track,
|
||||||
|
publication,
|
||||||
|
participant,
|
||||||
|
};
|
||||||
|
|
||||||
|
client_room.0.lock().updates_tx.blocking_send(event).ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1363,6 +1363,7 @@ pub struct Workspace {
|
||||||
project: Entity<Project>,
|
project: Entity<Project>,
|
||||||
follower_states: HashMap<CollaboratorId, FollowerState>,
|
follower_states: HashMap<CollaboratorId, FollowerState>,
|
||||||
last_leaders_by_pane: HashMap<WeakEntity<Pane>, CollaboratorId>,
|
last_leaders_by_pane: HashMap<WeakEntity<Pane>, CollaboratorId>,
|
||||||
|
auto_watch: AutoWatch,
|
||||||
window_edited: bool,
|
window_edited: bool,
|
||||||
last_window_title: Option<String>,
|
last_window_title: Option<String>,
|
||||||
dirty_items: HashMap<EntityId, Subscription>,
|
dirty_items: HashMap<EntityId, Subscription>,
|
||||||
|
|
@ -1415,6 +1416,19 @@ pub struct FollowerState {
|
||||||
items_by_leader_view_id: HashMap<ViewId, FollowerView>,
|
items_by_leader_view_id: HashMap<ViewId, FollowerView>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum AutoWatch {
|
||||||
|
Off,
|
||||||
|
Active { watched_peer: Option<PeerId> },
|
||||||
|
Paused,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AutoWatch {
|
||||||
|
pub fn enabled(&self) -> bool {
|
||||||
|
matches!(self, AutoWatch::Active { .. } | AutoWatch::Paused)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct FollowerView {
|
struct FollowerView {
|
||||||
view: Box<dyn FollowableItemHandle>,
|
view: Box<dyn FollowableItemHandle>,
|
||||||
location: Option<proto::PanelId>,
|
location: Option<proto::PanelId>,
|
||||||
|
|
@ -1793,6 +1807,7 @@ impl Workspace {
|
||||||
project: project.clone(),
|
project: project.clone(),
|
||||||
follower_states: Default::default(),
|
follower_states: Default::default(),
|
||||||
last_leaders_by_pane: Default::default(),
|
last_leaders_by_pane: Default::default(),
|
||||||
|
auto_watch: AutoWatch::Off,
|
||||||
dispatching_keystrokes: Default::default(),
|
dispatching_keystrokes: Default::default(),
|
||||||
window_edited: false,
|
window_edited: false,
|
||||||
last_window_title: None,
|
last_window_title: None,
|
||||||
|
|
@ -4783,6 +4798,93 @@ impl Workspace {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn auto_watch_state(&self) -> &AutoWatch {
|
||||||
|
&self.auto_watch
|
||||||
|
}
|
||||||
|
|
||||||
|
fn next_watched_peer(&self, cx: &App) -> Option<PeerId> {
|
||||||
|
self.active_call()
|
||||||
|
.and_then(|call| call.peer_ids_with_video_tracks(cx).first().copied())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn toggle_auto_watch(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
if self.auto_watch.enabled() {
|
||||||
|
self.auto_watch = AutoWatch::Off;
|
||||||
|
cx.notify();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let active_pane = self.active_pane.clone();
|
||||||
|
self.unfollow_in_pane(&active_pane, window, cx);
|
||||||
|
|
||||||
|
let local_is_sharing = self
|
||||||
|
.active_call()
|
||||||
|
.map_or(false, |call| call.is_sharing_screen(cx));
|
||||||
|
|
||||||
|
if local_is_sharing {
|
||||||
|
self.auto_watch = AutoWatch::Paused;
|
||||||
|
} else {
|
||||||
|
let watched_peer = self.next_watched_peer(cx);
|
||||||
|
self.auto_watch = AutoWatch::Active { watched_peer };
|
||||||
|
|
||||||
|
if let Some(peer_id) = watched_peer {
|
||||||
|
self.open_shared_screen(peer_id, window, cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_auto_watch_video_tracks_changed(
|
||||||
|
&mut self,
|
||||||
|
peer_id: PeerId,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
let AutoWatch::Active { watched_peer } = self.auto_watch else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let peer_is_sharing = self.active_call().map_or(false, |call| {
|
||||||
|
call.peer_ids_with_video_tracks(cx).contains(&peer_id)
|
||||||
|
});
|
||||||
|
let should_watch_peer = peer_is_sharing && watched_peer.is_none();
|
||||||
|
let watched_peer_stopped_sharing = watched_peer == Some(peer_id) && !peer_is_sharing;
|
||||||
|
|
||||||
|
if should_watch_peer || watched_peer_stopped_sharing {
|
||||||
|
let next_watched_peer = if should_watch_peer {
|
||||||
|
Some(peer_id)
|
||||||
|
} else {
|
||||||
|
self.next_watched_peer(cx)
|
||||||
|
};
|
||||||
|
|
||||||
|
self.auto_watch = AutoWatch::Active {
|
||||||
|
watched_peer: next_watched_peer,
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(next_watched_peer) = next_watched_peer {
|
||||||
|
self.open_shared_screen(next_watched_peer, window, cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_auto_watch_local_share_stopped(
|
||||||
|
&mut self,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
let AutoWatch::Paused = self.auto_watch else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let watched_peer = self.next_watched_peer(cx);
|
||||||
|
self.auto_watch = AutoWatch::Active { watched_peer };
|
||||||
|
|
||||||
|
if let Some(peer_id) = watched_peer {
|
||||||
|
self.open_shared_screen(peer_id, window, cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn activate_item(
|
pub fn activate_item(
|
||||||
&mut self,
|
&mut self,
|
||||||
item: &dyn ItemHandle,
|
item: &dyn ItemHandle,
|
||||||
|
|
@ -6512,10 +6614,22 @@ impl Workspace {
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
match event {
|
match event {
|
||||||
ActiveCallEvent::ParticipantLocationChanged { participant_id }
|
ActiveCallEvent::ParticipantLocationChanged { participant_id } => {
|
||||||
| ActiveCallEvent::RemoteVideoTracksChanged { participant_id } => {
|
|
||||||
self.leader_updated(participant_id, window, cx);
|
self.leader_updated(participant_id, window, cx);
|
||||||
}
|
}
|
||||||
|
ActiveCallEvent::RemoteVideoTracksChanged { participant_id } => {
|
||||||
|
self.leader_updated(participant_id, window, cx);
|
||||||
|
self.handle_auto_watch_video_tracks_changed(*participant_id, window, cx);
|
||||||
|
}
|
||||||
|
ActiveCallEvent::LocalScreenShareStarted => {
|
||||||
|
if let AutoWatch::Active { .. } = self.auto_watch {
|
||||||
|
self.auto_watch = AutoWatch::Paused;
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ActiveCallEvent::LocalScreenShareStopped => {
|
||||||
|
self.handle_auto_watch_local_share_stopped(window, cx);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -7879,6 +7993,7 @@ pub trait AnyActiveCall {
|
||||||
fn unshare_project(&self, _: Entity<Project>, _: &mut App) -> Result<()>;
|
fn unshare_project(&self, _: Entity<Project>, _: &mut App) -> Result<()>;
|
||||||
fn remote_participant_for_peer_id(&self, _: PeerId, _: &App) -> Option<RemoteCollaborator>;
|
fn remote_participant_for_peer_id(&self, _: PeerId, _: &App) -> Option<RemoteCollaborator>;
|
||||||
fn is_sharing_project(&self, _: &App) -> bool;
|
fn is_sharing_project(&self, _: &App) -> bool;
|
||||||
|
fn is_sharing_screen(&self, _: &App) -> bool;
|
||||||
fn has_remote_participants(&self, _: &App) -> bool;
|
fn has_remote_participants(&self, _: &App) -> bool;
|
||||||
fn local_participant_is_guest(&self, _: &App) -> bool;
|
fn local_participant_is_guest(&self, _: &App) -> bool;
|
||||||
fn client(&self, _: &App) -> Arc<Client>;
|
fn client(&self, _: &App) -> Arc<Client>;
|
||||||
|
|
@ -7908,6 +8023,7 @@ pub trait AnyActiveCall {
|
||||||
_: &mut Window,
|
_: &mut Window,
|
||||||
_: &mut App,
|
_: &mut App,
|
||||||
) -> Option<Entity<SharedScreen>>;
|
) -> Option<Entity<SharedScreen>>;
|
||||||
|
fn peer_ids_with_video_tracks(&self, _: &App) -> Vec<PeerId>;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
|
|
@ -7961,6 +8077,8 @@ pub struct RemoteCollaborator {
|
||||||
pub enum ActiveCallEvent {
|
pub enum ActiveCallEvent {
|
||||||
ParticipantLocationChanged { participant_id: PeerId },
|
ParticipantLocationChanged { participant_id: PeerId },
|
||||||
RemoteVideoTracksChanged { participant_id: PeerId },
|
RemoteVideoTracksChanged { participant_id: PeerId },
|
||||||
|
LocalScreenShareStarted,
|
||||||
|
LocalScreenShareStopped,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn leader_border_for_pane(
|
fn leader_border_for_pane(
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue