mirror of
https://github.com/zed-industries/zed.git
synced 2026-05-31 19:05:00 +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",
|
||||
"db",
|
||||
"editor",
|
||||
"feature_flags",
|
||||
"futures 0.3.32",
|
||||
"fuzzy",
|
||||
"gpui",
|
||||
|
|
|
|||
|
|
@ -112,6 +112,13 @@ impl AnyActiveCall for ActiveCallEntity {
|
|||
.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 {
|
||||
self.0.read(cx).room().map_or(false, |room| {
|
||||
!room.read(cx).remote_participants().is_empty()
|
||||
|
|
@ -209,6 +216,12 @@ impl AnyActiveCall for ActiveCallEntity {
|
|||
participant_id: *participant_id,
|
||||
})
|
||||
}
|
||||
room::Event::LocalScreenShareStarted => {
|
||||
Some(ActiveCallEvent::LocalScreenShareStarted)
|
||||
}
|
||||
room::Event::LocalScreenShareStopped => {
|
||||
Some(ActiveCallEvent::LocalScreenShareStopped)
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -66,6 +66,8 @@ pub enum Event {
|
|||
RoomLeft {
|
||||
channel_id: Option<ChannelId>,
|
||||
},
|
||||
LocalScreenShareStarted,
|
||||
LocalScreenShareStopped,
|
||||
}
|
||||
|
||||
pub struct Room {
|
||||
|
|
@ -1513,6 +1515,7 @@ impl Room {
|
|||
track_publication: publication,
|
||||
_stream: stream,
|
||||
};
|
||||
cx.emit(Event::LocalScreenShareStarted);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
|
|
@ -1674,6 +1677,7 @@ impl Room {
|
|||
let sid = track_publication.sid();
|
||||
cx.spawn(async move |_, cx| local_participant.unpublish_track(sid, cx).await)
|
||||
.detach_and_log_err(cx);
|
||||
cx.emit(Event::LocalScreenShareStopped);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,17 +7,16 @@ use cloud_api_client::{
|
|||
};
|
||||
use cloud_llm_client::{CurrentUsage, UsageData, UsageLimit};
|
||||
use futures::{StreamExt, stream::BoxStream};
|
||||
use gpui::{AppContext as _, Entity, TestAppContext};
|
||||
use gpui::{AppContext as _, TestAppContext};
|
||||
use http_client::{AsyncBody, Method, Request, http};
|
||||
use parking_lot::Mutex;
|
||||
use rpc::{ConnectionId, Peer, Receipt, TypedEnvelope, proto};
|
||||
|
||||
use crate::{Client, Connection, Credentials, EstablishConnectionError, UserStore};
|
||||
use crate::{Client, Connection, Credentials, EstablishConnectionError};
|
||||
|
||||
pub struct FakeServer {
|
||||
peer: Arc<Peer>,
|
||||
state: Arc<Mutex<FakeServerState>>,
|
||||
user_id: u64,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
|
|
@ -38,7 +37,6 @@ impl FakeServer {
|
|||
let server = Self {
|
||||
peer: Peer::new(0),
|
||||
state: Default::default(),
|
||||
user_id: client_user_id,
|
||||
};
|
||||
|
||||
client.http_client().as_fake().replace_handler({
|
||||
|
|
@ -213,23 +211,6 @@ impl FakeServer {
|
|||
fn connection_id(&self) -> ConnectionId {
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ pub async fn validate_header<B>(mut req: Request<B>, next: Next<B>) -> impl Into
|
|||
.await?
|
||||
.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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -37,7 +37,6 @@ use worktree_settings_file::LocalSettingsKind;
|
|||
|
||||
pub use ids::*;
|
||||
pub use sea_orm::ConnectOptions;
|
||||
pub use tables::user::Model as User;
|
||||
pub use tables::*;
|
||||
|
||||
#[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.
|
||||
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 {
|
||||
Ok(user::Entity::find()
|
||||
.filter(user::Column::GithubLogin.eq(github_login))
|
||||
|
|
@ -78,7 +81,7 @@ impl Database {
|
|||
github_name: Option<&str>,
|
||||
github_user_created_at: DateTimeUtc,
|
||||
initial_channel_id: Option<ChannelId>,
|
||||
) -> Result<User> {
|
||||
) -> Result<user::Model> {
|
||||
self.transaction(|tx| async move {
|
||||
self.update_or_create_user_by_github_account_tx(
|
||||
github_login,
|
||||
|
|
@ -103,7 +106,7 @@ impl Database {
|
|||
github_user_created_at: NaiveDateTime,
|
||||
initial_channel_id: Option<ChannelId>,
|
||||
tx: &DatabaseTransaction,
|
||||
) -> Result<User> {
|
||||
) -> Result<user::Model> {
|
||||
if let Some(existing_user) = self
|
||||
.get_user_by_github_user_id_or_github_login(github_user_id, github_login, tx)
|
||||
.await?
|
||||
|
|
@ -156,7 +159,7 @@ impl Database {
|
|||
github_user_id: i32,
|
||||
github_login: &str,
|
||||
tx: &DatabaseTransaction,
|
||||
) -> Result<Option<User>> {
|
||||
) -> Result<Option<user::Model>> {
|
||||
if let Some(user_by_github_user_id) = user::Entity::find()
|
||||
.filter(user::Column::GithubUserId.eq(github_user_id))
|
||||
.one(tx)
|
||||
|
|
@ -178,7 +181,7 @@ impl Database {
|
|||
|
||||
/// get_all_users returns the next page of users. To get more call again with
|
||||
/// 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 {
|
||||
Ok(user::Entity::find()
|
||||
.order_by_asc(user::Column::GithubLogin)
|
||||
|
|
@ -207,7 +210,11 @@ impl Database {
|
|||
}
|
||||
|
||||
/// 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 {
|
||||
let tx = tx;
|
||||
let like_string = Self::fuzzy_like_string(name_query);
|
||||
|
|
|
|||
|
|
@ -19,6 +19,17 @@ pub struct Model {
|
|||
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)]
|
||||
pub enum Relation {
|
||||
#[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 auth;
|
||||
pub mod db;
|
||||
pub mod entities;
|
||||
pub mod env;
|
||||
pub mod executor;
|
||||
pub mod rpc;
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
mod connection_pool;
|
||||
|
||||
use crate::api::{CloudflareIpCountryHeader, SystemIdHeader};
|
||||
use crate::entities::User;
|
||||
use crate::{
|
||||
AppState, Error, Result, auth,
|
||||
db::{
|
||||
self, BufferId, Capability, Channel, ChannelId, ChannelRole, ChannelsForUser, Database,
|
||||
InviteMemberResult, MembershipUpdated, NotificationId, ProjectId, RejoinedProject,
|
||||
RemoveChannelMemberResult, RespondToChannelInvite, RoomId, ServerId, SharedThreadId, User,
|
||||
RemoveChannelMemberResult, RespondToChannelInvite, RoomId, ServerId, SharedThreadId,
|
||||
UserId,
|
||||
},
|
||||
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};
|
||||
|
||||
mod agent_sharing_tests;
|
||||
mod auto_watch_tests;
|
||||
mod channel_buffer_tests;
|
||||
mod channel_guest_tests;
|
||||
mod channel_tests;
|
||||
|
|
|
|||
|
|
@ -294,7 +294,7 @@ impl TestServer {
|
|||
cx.background_spawn(server.handle_connection(
|
||||
server_conn,
|
||||
client_name,
|
||||
Principal::User(user),
|
||||
Principal::User(user.into()),
|
||||
ZedVersion(semver::Version::new(1, 0, 0)),
|
||||
Some("test".to_string()),
|
||||
None,
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ client.workspace = true
|
|||
collections.workspace = true
|
||||
db.workspace = true
|
||||
editor.workspace = true
|
||||
feature_flags.workspace = true
|
||||
futures.workspace = true
|
||||
fuzzy.workspace = true
|
||||
gpui.workspace = true
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ use collections::{HashMap, HashSet};
|
|||
use contact_finder::ContactFinder;
|
||||
use db::kvp::KeyValueStore;
|
||||
use editor::{Editor, EditorElement, EditorStyle};
|
||||
use feature_flags::{AutoWatchFeatureFlag, FeatureFlagAppExt as _};
|
||||
use fuzzy::{StringMatch, StringMatchCandidate, match_strings};
|
||||
use gpui::{
|
||||
AnyElement, App, AsyncWindowContext, Bounds, ClickEvent, ClipboardItem, DismissEvent, Div,
|
||||
|
|
@ -35,13 +36,13 @@ use theme::ActiveTheme;
|
|||
use theme_settings::ThemeSettings;
|
||||
use ui::{
|
||||
Avatar, AvatarAvailabilityIndicator, CollabNotification, ContextMenu, CopyButton, Facepile,
|
||||
HighlightedLabel, IconButtonShape, Indicator, ListHeader, ListItem, Tab, Tooltip, prelude::*,
|
||||
tooltip_container,
|
||||
HighlightedLabel, IconButtonShape, Indicator, ListHeader, ListItem, Tab, TintColor, Tooltip,
|
||||
prelude::*, tooltip_container,
|
||||
};
|
||||
use util::{ResultExt, TryFutureExt, maybe};
|
||||
use workspace::{
|
||||
CopyRoomId, Deafen, LeaveCall, MultiWorkspace, Mute, OpenChannelNotes, OpenChannelNotesById,
|
||||
ScreenShare, ShareProject, Workspace,
|
||||
AutoWatch, CopyRoomId, Deafen, LeaveCall, MultiWorkspace, Mute, OpenChannelNotes,
|
||||
OpenChannelNotesById, ScreenShare, ShareProject, Workspace,
|
||||
dock::{DockPosition, Panel, PanelEvent},
|
||||
notifications::{
|
||||
DetachAndPromptErr, Notification as WorkspaceNotification, NotificationId, NotifyResultExt,
|
||||
|
|
@ -2895,13 +2896,75 @@ impl CollabPanel {
|
|||
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 {
|
||||
Section::ActiveCall => channel_link.map(|channel_link| {
|
||||
CopyButton::new("copy-channel-link", channel_link)
|
||||
.visible_on_hover("section-header")
|
||||
.tooltip_label("Copy Channel Link")
|
||||
.into_any_element()
|
||||
}),
|
||||
Section::ActiveCall => {
|
||||
let has_auto_watch_flag = cx.has_flag::<AutoWatchFeatureFlag>();
|
||||
let show_auto_watch = has_auto_watch_flag && is_auto_watching;
|
||||
let show_copy = channel_link.is_some();
|
||||
|
||||
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(
|
||||
IconButton::new("add-contact", IconName::Plus)
|
||||
.icon_size(IconSize::Small)
|
||||
|
|
|
|||
|
|
@ -91,3 +91,11 @@ impl FeatureFlag for 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)
|
||||
}
|
||||
|
||||
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(())
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1363,6 +1363,7 @@ pub struct Workspace {
|
|||
project: Entity<Project>,
|
||||
follower_states: HashMap<CollaboratorId, FollowerState>,
|
||||
last_leaders_by_pane: HashMap<WeakEntity<Pane>, CollaboratorId>,
|
||||
auto_watch: AutoWatch,
|
||||
window_edited: bool,
|
||||
last_window_title: Option<String>,
|
||||
dirty_items: HashMap<EntityId, Subscription>,
|
||||
|
|
@ -1415,6 +1416,19 @@ pub struct FollowerState {
|
|||
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 {
|
||||
view: Box<dyn FollowableItemHandle>,
|
||||
location: Option<proto::PanelId>,
|
||||
|
|
@ -1793,6 +1807,7 @@ impl Workspace {
|
|||
project: project.clone(),
|
||||
follower_states: Default::default(),
|
||||
last_leaders_by_pane: Default::default(),
|
||||
auto_watch: AutoWatch::Off,
|
||||
dispatching_keystrokes: Default::default(),
|
||||
window_edited: false,
|
||||
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(
|
||||
&mut self,
|
||||
item: &dyn ItemHandle,
|
||||
|
|
@ -6512,10 +6614,22 @@ impl Workspace {
|
|||
cx: &mut Context<Self>,
|
||||
) {
|
||||
match event {
|
||||
ActiveCallEvent::ParticipantLocationChanged { participant_id }
|
||||
| ActiveCallEvent::RemoteVideoTracksChanged { participant_id } => {
|
||||
ActiveCallEvent::ParticipantLocationChanged { participant_id } => {
|
||||
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 remote_participant_for_peer_id(&self, _: PeerId, _: &App) -> Option<RemoteCollaborator>;
|
||||
fn is_sharing_project(&self, _: &App) -> bool;
|
||||
fn is_sharing_screen(&self, _: &App) -> bool;
|
||||
fn has_remote_participants(&self, _: &App) -> bool;
|
||||
fn local_participant_is_guest(&self, _: &App) -> bool;
|
||||
fn client(&self, _: &App) -> Arc<Client>;
|
||||
|
|
@ -7908,6 +8023,7 @@ pub trait AnyActiveCall {
|
|||
_: &mut Window,
|
||||
_: &mut App,
|
||||
) -> Option<Entity<SharedScreen>>;
|
||||
fn peer_ids_with_video_tracks(&self, _: &App) -> Vec<PeerId>;
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
|
|
@ -7961,6 +8077,8 @@ pub struct RemoteCollaborator {
|
|||
pub enum ActiveCallEvent {
|
||||
ParticipantLocationChanged { participant_id: PeerId },
|
||||
RemoteVideoTracksChanged { participant_id: PeerId },
|
||||
LocalScreenShareStarted,
|
||||
LocalScreenShareStopped,
|
||||
}
|
||||
|
||||
fn leader_border_for_pane(
|
||||
|
|
|
|||
Loading…
Reference in a new issue