diff --git a/Cargo.lock b/Cargo.lock index 79e76d76f4e..4c95e43eadb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3279,6 +3279,7 @@ dependencies = [ "collections", "db", "editor", + "feature_flags", "futures 0.3.32", "fuzzy", "gpui", diff --git a/crates/call/src/call_impl/mod.rs b/crates/call/src/call_impl/mod.rs index 39cb4cd9e3c..c0c1535cd45 100644 --- a/crates/call/src/call_impl/mod.rs +++ b/crates/call/src/call_impl/mod.rs @@ -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 { + 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 { diff --git a/crates/call/src/call_impl/room.rs b/crates/call/src/call_impl/room.rs index 37a3fd823ec..f9df2b758f7 100644 --- a/crates/call/src/call_impl/room.rs +++ b/crates/call/src/call_impl/room.rs @@ -66,6 +66,8 @@ pub enum Event { RoomLeft { channel_id: Option, }, + 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(); } diff --git a/crates/collab/tests/integration/auto_watch_tests.rs b/crates/collab/tests/integration/auto_watch_tests.rs new file mode 100644 index 00000000000..c8d395407b3 --- /dev/null +++ b/crates/collab/tests/integration/auto_watch_tests.rs @@ -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, +} + +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(); +} diff --git a/crates/collab/tests/integration/collab_tests.rs b/crates/collab/tests/integration/collab_tests.rs index 5079698a96a..921319487bf 100644 --- a/crates/collab/tests/integration/collab_tests.rs +++ b/crates/collab/tests/integration/collab_tests.rs @@ -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; diff --git a/crates/collab_ui/Cargo.toml b/crates/collab_ui/Cargo.toml index 920f620e0ea..978af1387cb 100644 --- a/crates/collab_ui/Cargo.toml +++ b/crates/collab_ui/Cargo.toml @@ -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 diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 908d11cd654..cea3806edb3 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -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::(); + 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) diff --git a/crates/feature_flags/src/flags.rs b/crates/feature_flags/src/flags.rs index b23f8dbc56a..56e3d135d9e 100644 --- a/crates/feature_flags/src/flags.rs +++ b/crates/feature_flags/src/flags.rs @@ -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); diff --git a/crates/livekit_client/src/test.rs b/crates/livekit_client/src/test.rs index 4b5efe0aafb..955f92dc19d 100644 --- a/crates/livekit_client/src/test.rs +++ b/crates/livekit_client/src/test.rs @@ -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(()) } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index b0c5d3cb97d..45a14fa1a04 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -1363,6 +1363,7 @@ pub struct Workspace { project: Entity, follower_states: HashMap, last_leaders_by_pane: HashMap, CollaboratorId>, + auto_watch: AutoWatch, window_edited: bool, last_window_title: Option, dirty_items: HashMap, @@ -1415,6 +1416,19 @@ pub struct FollowerState { items_by_leader_view_id: HashMap, } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum AutoWatch { + Off, + Active { watched_peer: Option }, + Paused, +} + +impl AutoWatch { + pub fn enabled(&self) -> bool { + matches!(self, AutoWatch::Active { .. } | AutoWatch::Paused) + } +} + struct FollowerView { view: Box, location: Option, @@ -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 { + 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) { + 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, + ) { + 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, + ) { + 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, ) { 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, _: &mut App) -> Result<()>; fn remote_participant_for_peer_id(&self, _: PeerId, _: &App) -> Option; 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; @@ -7908,6 +8023,7 @@ pub trait AnyActiveCall { _: &mut Window, _: &mut App, ) -> Option>; + fn peer_ids_with_video_tracks(&self, _: &App) -> Vec; } #[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(