diff --git a/Cargo.lock b/Cargo.lock index 964fce6bf3acaadff8a539df9937c84b1d0bdb74..934e0d1a01482d57e456057860ee45037f39d570 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2484,6 +2484,7 @@ dependencies = [ "settings", "telemetry", "util", + "workspace", ] [[package]] diff --git a/crates/call/Cargo.toml b/crates/call/Cargo.toml index ff034f914b0be44e6ec9f6475881ed79c368cd8a..2e46b58b74b826e8892d1e9da28c3cf06c99aa9b 100644 --- a/crates/call/Cargo.toml +++ b/crates/call/Cargo.toml @@ -31,7 +31,9 @@ fs.workspace = true futures.workspace = true feature_flags.workspace = true gpui = { workspace = true, features = ["screen-capture"] } +gpui_tokio.workspace = true language.workspace = true +livekit_client.workspace = true log.workspace = true postage.workspace = true project.workspace = true @@ -39,8 +41,7 @@ serde.workspace = true settings.workspace = true telemetry.workspace = true util.workspace = true -gpui_tokio.workspace = true -livekit_client.workspace = true +workspace.workspace = true [dev-dependencies] client = { workspace = true, features = ["test-support"] } diff --git a/crates/call/src/call_impl/mod.rs b/crates/call/src/call_impl/mod.rs index 08d3a28e10787ada3664c970ab52ea968ca54860..e3945cf2c746f4c598caa7996deb2c76fc859e64 100644 --- a/crates/call/src/call_impl/mod.rs +++ b/crates/call/src/call_impl/mod.rs @@ -7,25 +7,265 @@ use client::{ChannelId, Client, TypedEnvelope, User, UserStore, ZED_ALWAYS_ACTIV use collections::HashSet; use futures::{Future, FutureExt, channel::oneshot, future::Shared}; use gpui::{ - App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, Global, Subscription, Task, - WeakEntity, + AnyView, App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, Subscription, Task, + WeakEntity, Window, }; use postage::watch; use project::Project; use room::Event; +use settings::Settings; use std::sync::Arc; +use workspace::{ + ActiveCallEvent, AnyActiveCall, GlobalAnyActiveCall, Pane, RemoteCollaborator, SharedScreen, + Workspace, +}; pub use livekit_client::{RemoteVideoTrack, RemoteVideoTrackView, RemoteVideoTrackViewEvent}; -pub use participant::ParticipantLocation; pub use room::Room; -struct GlobalActiveCall(Entity); - -impl Global for GlobalActiveCall {} +use crate::call_settings::CallSettings; pub fn init(client: Arc, user_store: Entity, cx: &mut App) { let active_call = cx.new(|cx| ActiveCall::new(client, user_store, cx)); - cx.set_global(GlobalActiveCall(active_call)); + cx.set_global(GlobalAnyActiveCall(Arc::new(ActiveCallEntity(active_call)))) +} + +#[derive(Clone)] +struct ActiveCallEntity(Entity); + +impl AnyActiveCall for ActiveCallEntity { + fn entity(&self) -> gpui::AnyEntity { + self.0.clone().into_any() + } + + fn is_in_room(&self, cx: &App) -> bool { + self.0.read(cx).room().is_some() + } + + fn room_id(&self, cx: &App) -> Option { + Some(self.0.read(cx).room()?.read(cx).id()) + } + + fn channel_id(&self, cx: &App) -> Option { + self.0.read(cx).room()?.read(cx).channel_id() + } + + fn hang_up(&self, cx: &mut App) -> Task> { + self.0.update(cx, |this, cx| this.hang_up(cx)) + } + + fn unshare_project(&self, project: Entity, cx: &mut App) -> Result<()> { + self.0 + .update(cx, |this, cx| this.unshare_project(project, cx)) + } + + fn remote_participant_for_peer_id( + &self, + peer_id: proto::PeerId, + cx: &App, + ) -> Option { + let room = self.0.read(cx).room()?.read(cx); + let participant = room.remote_participant_for_peer_id(peer_id)?; + Some(RemoteCollaborator { + user: participant.user.clone(), + peer_id: participant.peer_id, + location: participant.location, + participant_index: participant.participant_index, + }) + } + + fn is_sharing_project(&self, cx: &App) -> bool { + self.0 + .read(cx) + .room() + .map_or(false, |room| room.read(cx).is_sharing_project()) + } + + fn has_remote_participants(&self, cx: &App) -> bool { + self.0.read(cx).room().map_or(false, |room| { + !room.read(cx).remote_participants().is_empty() + }) + } + + fn local_participant_is_guest(&self, cx: &App) -> bool { + self.0 + .read(cx) + .room() + .map_or(false, |room| room.read(cx).local_participant_is_guest()) + } + + fn client(&self, cx: &App) -> Arc { + self.0.read(cx).client() + } + + fn share_on_join(&self, cx: &App) -> bool { + CallSettings::get_global(cx).share_on_join + } + + fn join_channel(&self, channel_id: ChannelId, cx: &mut App) -> Task> { + let task = self + .0 + .update(cx, |this, cx| this.join_channel(channel_id, cx)); + cx.spawn(async move |_cx| { + let result = task.await?; + Ok(result.is_some()) + }) + } + + fn room_update_completed(&self, cx: &mut App) -> Task<()> { + let Some(room) = self.0.read(cx).room().cloned() else { + return Task::ready(()); + }; + let future = room.update(cx, |room, _cx| room.room_update_completed()); + cx.spawn(async move |_cx| { + future.await; + }) + } + + fn most_active_project(&self, cx: &App) -> Option<(u64, u64)> { + let room = self.0.read(cx).room()?; + room.read(cx).most_active_project(cx) + } + + fn share_project(&self, project: Entity, cx: &mut App) -> Task> { + self.0 + .update(cx, |this, cx| this.share_project(project, cx)) + } + + fn join_project( + &self, + project_id: u64, + language_registry: Arc, + fs: Arc, + cx: &mut App, + ) -> Task>> { + let Some(room) = self.0.read(cx).room().cloned() else { + return Task::ready(Err(anyhow::anyhow!("not in a call"))); + }; + room.update(cx, |room, cx| { + room.join_project(project_id, language_registry, fs, cx) + }) + } + + fn peer_id_for_user_in_room(&self, user_id: u64, cx: &App) -> Option { + let room = self.0.read(cx).room()?.read(cx); + room.remote_participants() + .values() + .find(|p| p.user.id == user_id) + .map(|p| p.peer_id) + } + + fn subscribe( + &self, + window: &mut Window, + cx: &mut Context, + handler: Box< + dyn Fn(&mut Workspace, &ActiveCallEvent, &mut Window, &mut Context), + >, + ) -> Subscription { + cx.subscribe_in( + &self.0, + window, + move |workspace, _, event: &room::Event, window, cx| { + let mapped = match event { + room::Event::ParticipantLocationChanged { participant_id } => { + Some(ActiveCallEvent::ParticipantLocationChanged { + participant_id: *participant_id, + }) + } + room::Event::RemoteVideoTracksChanged { participant_id } => { + Some(ActiveCallEvent::RemoteVideoTracksChanged { + participant_id: *participant_id, + }) + } + _ => None, + }; + if let Some(event) = mapped { + handler(workspace, &event, window, cx); + } + }, + ) + } + + fn create_shared_screen( + &self, + peer_id: client::proto::PeerId, + pane: &Entity, + window: &mut Window, + cx: &mut App, + ) -> Option> { + let room = self.0.read(cx).room()?.clone(); + let participant = room.read(cx).remote_participant_for_peer_id(peer_id)?; + let track = participant.video_tracks.values().next()?.clone(); + let user = participant.user.clone(); + + for item in pane.read(cx).items_of_type::() { + if item.read(cx).peer_id == peer_id { + return Some(item); + } + } + + Some(cx.new(|cx: &mut Context| { + let my_sid = track.sid(); + cx.subscribe( + &room, + move |_: &mut SharedScreen, + _: Entity, + ev: &room::Event, + cx: &mut Context| { + if let room::Event::RemoteVideoTrackUnsubscribed { sid } = ev + && *sid == my_sid + { + cx.emit(workspace::shared_screen::Event::Close); + } + }, + ) + .detach(); + + cx.observe_release( + &room, + |_: &mut SharedScreen, _: &mut Room, cx: &mut Context| { + cx.emit(workspace::shared_screen::Event::Close); + }, + ) + .detach(); + + let view = cx.new(|cx| RemoteVideoTrackView::new(track.clone(), window, cx)); + cx.subscribe( + &view, + |_: &mut SharedScreen, + _: Entity, + ev: &RemoteVideoTrackViewEvent, + cx: &mut Context| match ev { + RemoteVideoTrackViewEvent::Close => { + cx.emit(workspace::shared_screen::Event::Close); + } + }, + ) + .detach(); + + pub(super) fn clone_remote_video_track_view( + view: &AnyView, + window: &mut Window, + cx: &mut App, + ) -> AnyView { + let view = view + .clone() + .downcast::() + .expect("SharedScreen view must be a RemoteVideoTrackView"); + let cloned = view.update(cx, |view, cx| view.clone(window, cx)); + AnyView::from(cloned) + } + + SharedScreen::new( + peer_id, + user, + AnyView::from(view), + clone_remote_video_track_view, + cx, + ) + })) + } } pub struct OneAtATime { @@ -152,12 +392,12 @@ impl ActiveCall { } pub fn global(cx: &App) -> Entity { - cx.global::().0.clone() + Self::try_global(cx).unwrap() } pub fn try_global(cx: &App) -> Option> { - cx.try_global::() - .map(|call| call.0.clone()) + let any = cx.try_global::()?; + any.0.entity().downcast::().ok() } pub fn invite( diff --git a/crates/call/src/call_impl/participant.rs b/crates/call/src/call_impl/participant.rs index 6fb6a2eb79b537aa9d7296a323f7d45221a4b05d..58d3329f853bda1e0f5b5463c1b5bacacf787adc 100644 --- a/crates/call/src/call_impl/participant.rs +++ b/crates/call/src/call_impl/participant.rs @@ -1,4 +1,3 @@ -use anyhow::{Context as _, Result}; use client::{ParticipantIndex, User, proto}; use collections::HashMap; use gpui::WeakEntity; @@ -9,30 +8,6 @@ use std::sync::Arc; pub use livekit_client::TrackSid; pub use livekit_client::{RemoteAudioTrack, RemoteVideoTrack}; -#[derive(Copy, Clone, Debug, Eq, PartialEq)] -pub enum ParticipantLocation { - SharedProject { project_id: u64 }, - UnsharedProject, - External, -} - -impl ParticipantLocation { - pub fn from_proto(location: Option) -> Result { - match location - .and_then(|l| l.variant) - .context("participant location was not provided")? - { - proto::participant_location::Variant::SharedProject(project) => { - Ok(Self::SharedProject { - project_id: project.id, - }) - } - proto::participant_location::Variant::UnsharedProject(_) => Ok(Self::UnsharedProject), - proto::participant_location::Variant::External(_) => Ok(Self::External), - } - } -} - #[derive(Clone, Default)] pub struct LocalParticipant { pub projects: Vec, @@ -54,7 +29,7 @@ pub struct RemoteParticipant { pub peer_id: proto::PeerId, pub role: proto::ChannelRole, pub projects: Vec, - pub location: ParticipantLocation, + pub location: workspace::ParticipantLocation, pub participant_index: ParticipantIndex, pub muted: bool, pub speaking: bool, diff --git a/crates/call/src/call_impl/room.rs b/crates/call/src/call_impl/room.rs index a2e1ac2fcc2779f2340dd35d5800749cb6bfcbb2..701d7dd65423f97b3f4d5cfa4a198083593211e6 100644 --- a/crates/call/src/call_impl/room.rs +++ b/crates/call/src/call_impl/room.rs @@ -1,6 +1,6 @@ use crate::{ call_settings::CallSettings, - participant::{LocalParticipant, ParticipantLocation, RemoteParticipant}, + participant::{LocalParticipant, RemoteParticipant}, }; use anyhow::{Context as _, Result, anyhow}; use audio::{Audio, Sound}; @@ -25,6 +25,7 @@ use project::Project; use settings::Settings as _; use std::{future::Future, mem, rc::Rc, sync::Arc, time::Duration, time::Instant}; use util::{ResultExt, TryFutureExt, paths::PathStyle, post_inc}; +use workspace::ParticipantLocation; pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30); diff --git a/crates/collab/tests/integration/following_tests.rs b/crates/collab/tests/integration/following_tests.rs index 6bdb06a6c5a0ffb95bc75a026a26c4797030f8ce..b761bef9ec3be679d55d1c82e3cb5cce0ac7f14e 100644 --- a/crates/collab/tests/integration/following_tests.rs +++ b/crates/collab/tests/integration/following_tests.rs @@ -1,6 +1,6 @@ #![allow(clippy::reversed_empty_ranges)] use crate::TestServer; -use call::{ActiveCall, ParticipantLocation}; +use call::ActiveCall; use client::ChannelId; use collab_ui::{ channel_view::ChannelView, @@ -17,7 +17,10 @@ use serde_json::json; use settings::SettingsStore; use text::{Point, ToPoint}; use util::{path, rel_path::rel_path, test::sample_text}; -use workspace::{CollaboratorId, MultiWorkspace, SplitDirection, Workspace, item::ItemHandle as _}; +use workspace::{ + CollaboratorId, MultiWorkspace, ParticipantLocation, SplitDirection, Workspace, + item::ItemHandle as _, +}; use super::TestClient; diff --git a/crates/collab/tests/integration/integration_tests.rs b/crates/collab/tests/integration/integration_tests.rs index 413aa802a1e63982de4a4563917cdcf7e6a55c81..c26f20c1e294326f275dbfda1d2d41603719cd3e 100644 --- a/crates/collab/tests/integration/integration_tests.rs +++ b/crates/collab/tests/integration/integration_tests.rs @@ -6,7 +6,7 @@ use anyhow::{Result, anyhow}; use assistant_slash_command::SlashCommandWorkingSet; use assistant_text_thread::TextThreadStore; use buffer_diff::{DiffHunkSecondaryStatus, DiffHunkStatus, assert_hunks}; -use call::{ActiveCall, ParticipantLocation, Room, room}; +use call::{ActiveCall, Room, room}; use client::{RECEIVE_TIMEOUT, User}; use collab::rpc::{CLEANUP_TIMEOUT, RECONNECT_TIMEOUT}; use collections::{BTreeMap, HashMap, HashSet}; @@ -51,7 +51,7 @@ use std::{ }; use unindent::Unindent as _; use util::{path, rel_path::rel_path, uri}; -use workspace::Pane; +use workspace::{Pane, ParticipantLocation}; #[ctor::ctor] fn init_logger() { diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index 830870db63a3377bd3fff07eee57f53b6ae87d44..b8caf478305609b7ea95874333f1483c448ac242 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -3257,10 +3257,8 @@ impl GitPanel { let mut new_co_authors = Vec::new(); let project = self.project.read(cx); - let Some(room) = self - .workspace - .upgrade() - .and_then(|workspace| workspace.read(cx).active_call()?.read(cx).room().cloned()) + let Some(room) = + call::ActiveCall::try_global(cx).and_then(|call| call.read(cx).room().cloned()) else { return Vec::default(); }; @@ -5520,10 +5518,9 @@ impl Render for GitPanel { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let project = self.project.read(cx); let has_entries = !self.entries.is_empty(); - let room = self - .workspace - .upgrade() - .and_then(|workspace| workspace.read(cx).active_call()?.read(cx).room().cloned()); + let room = self.workspace.upgrade().and_then(|_workspace| { + call::ActiveCall::try_global(cx).and_then(|call| call.read(cx).room().cloned()) + }); let has_write_access = self.has_write_access(cx); diff --git a/crates/title_bar/src/collab.rs b/crates/title_bar/src/collab.rs index c5071d1fff466a4352781be88d728fafe4f4ce78..58e4d2a8fcfdfe885b7ddf51b20e193625950ce0 100644 --- a/crates/title_bar/src/collab.rs +++ b/crates/title_bar/src/collab.rs @@ -1,7 +1,7 @@ use std::rc::Rc; use std::sync::Arc; -use call::{ActiveCall, ParticipantLocation, Room}; +use call::{ActiveCall, Room}; use channel::ChannelStore; use client::{User, proto::PeerId}; use gpui::{ @@ -18,7 +18,7 @@ use ui::{ Facepile, PopoverMenu, SplitButton, SplitButtonStyle, TintColor, Tooltip, prelude::*, }; use util::rel_path::RelPath; -use workspace::notifications::DetachAndPromptErr; +use workspace::{ParticipantLocation, notifications::DetachAndPromptErr}; use crate::TitleBar; diff --git a/crates/workspace/Cargo.toml b/crates/workspace/Cargo.toml index 3d9146250cd1df385761676b18d47ddcd3813dc6..dcd0bf640fdf279fb1874ba77307ccbd3c431393 100644 --- a/crates/workspace/Cargo.toml +++ b/crates/workspace/Cargo.toml @@ -30,7 +30,6 @@ test-support = [ any_vec.workspace = true anyhow.workspace = true async-recursion.workspace = true -call.workspace = true client.workspace = true chrono.workspace = true clock.workspace = true diff --git a/crates/workspace/src/pane_group.rs b/crates/workspace/src/pane_group.rs index 0f8cef616f5ed03c31eaf3511c58922ae230e385..1d28b05514baa53244926bfad906e667b0b287cd 100644 --- a/crates/workspace/src/pane_group.rs +++ b/crates/workspace/src/pane_group.rs @@ -1,10 +1,10 @@ use crate::{ - AppState, CollaboratorId, FollowerState, Pane, Workspace, WorkspaceSettings, + AnyActiveCall, AppState, CollaboratorId, FollowerState, Pane, ParticipantLocation, Workspace, + WorkspaceSettings, pane_group::element::pane_axis, workspace_settings::{PaneSplitDirectionHorizontal, PaneSplitDirectionVertical}, }; use anyhow::Result; -use call::{ActiveCall, ParticipantLocation}; use collections::HashMap; use gpui::{ Along, AnyView, AnyWeakView, Axis, Bounds, Entity, Hsla, IntoElement, MouseButton, Pixels, @@ -296,7 +296,7 @@ impl Member { pub struct PaneRenderContext<'a> { pub project: &'a Entity, pub follower_states: &'a HashMap, - pub active_call: Option<&'a Entity>, + pub active_call: Option<&'a dyn AnyActiveCall>, pub active_pane: &'a Entity, pub app_state: &'a Arc, pub workspace: &'a WeakEntity, @@ -358,10 +358,11 @@ impl PaneLeaderDecorator for PaneRenderContext<'_> { let status_box; match leader_id { CollaboratorId::PeerId(peer_id) => { - let Some(leader) = self.active_call.as_ref().and_then(|call| { - let room = call.read(cx).room()?.read(cx); - room.remote_participant_for_peer_id(peer_id) - }) else { + let Some(leader) = self + .active_call + .as_ref() + .and_then(|call| call.remote_participant_for_peer_id(peer_id, cx)) + else { return LeaderDecoration::default(); }; diff --git a/crates/workspace/src/shared_screen.rs b/crates/workspace/src/shared_screen.rs index fc4ae7292a04781cb8cf790154c8b04a4c5e9bc5..136f552fee23231b45fcb867d2ce8bab02dca7e8 100644 --- a/crates/workspace/src/shared_screen.rs +++ b/crates/workspace/src/shared_screen.rs @@ -2,10 +2,9 @@ use crate::{ ItemNavHistory, WorkspaceId, item::{Item, ItemEvent}, }; -use call::{RemoteVideoTrack, RemoteVideoTrackView, Room}; use client::{User, proto::PeerId}; use gpui::{ - AppContext as _, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement, + AnyView, AppContext as _, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement, ParentElement, Render, SharedString, Styled, Task, div, }; use std::sync::Arc; @@ -19,45 +18,26 @@ pub struct SharedScreen { pub peer_id: PeerId, user: Arc, nav_history: Option, - view: Entity, + view: AnyView, + clone_view: fn(&AnyView, &mut Window, &mut App) -> AnyView, focus: FocusHandle, } impl SharedScreen { pub fn new( - track: RemoteVideoTrack, peer_id: PeerId, user: Arc, - room: Entity, - window: &mut Window, + view: AnyView, + clone_view: fn(&AnyView, &mut Window, &mut App) -> AnyView, cx: &mut Context, ) -> Self { - let my_sid = track.sid(); - cx.subscribe(&room, move |_, _, ev, cx| { - if let call::room::Event::RemoteVideoTrackUnsubscribed { sid } = ev - && sid == &my_sid - { - cx.emit(Event::Close) - } - }) - .detach(); - - cx.observe_release(&room, |_, _, cx| { - cx.emit(Event::Close); - }) - .detach(); - - let view = cx.new(|cx| RemoteVideoTrackView::new(track.clone(), window, cx)); - cx.subscribe(&view, |_, _, ev, cx| match ev { - call::RemoteVideoTrackViewEvent::Close => cx.emit(Event::Close), - }) - .detach(); Self { view, peer_id, user, nav_history: Default::default(), focus: cx.focus_handle(), + clone_view, } } } @@ -124,12 +104,15 @@ impl Item for SharedScreen { window: &mut Window, cx: &mut Context, ) -> Task>> { + let clone_view = self.clone_view; + let cloned_view = clone_view(&self.view, window, cx); Task::ready(Some(cx.new(|cx| Self { - view: self.view.update(cx, |view, cx| view.clone(window, cx)), + view: cloned_view, peer_id: self.peer_id, user: self.user.clone(), nav_history: Default::default(), focus: cx.focus_handle(), + clone_view, }))) } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 4db38daa2ae6718ac6c3e0dad7d1d46433b27e07..ac83809f8d313e842e72d19fb98b8b5d1b69df0f 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -12,6 +12,7 @@ mod persistence; pub mod searchable; mod security_modal; pub mod shared_screen; +pub use shared_screen::SharedScreen; mod status_bar; pub mod tasks; mod theme_preview; @@ -31,13 +32,13 @@ pub use path_list::PathList; pub use toast_layer::{ToastAction, ToastLayer, ToastView}; use anyhow::{Context as _, Result, anyhow}; -use call::{ActiveCall, call_settings::CallSettings}; use client::{ - ChannelId, Client, ErrorExt, Status, TypedEnvelope, UserStore, + ChannelId, Client, ErrorExt, ParticipantIndex, Status, TypedEnvelope, User, UserStore, proto::{self, ErrorCode, PanelId, PeerId}, }; use collections::{HashMap, HashSet, hash_map}; use dock::{Dock, DockPosition, PanelButtons, PanelHandle, RESIZE_HANDLE_SIZE}; +use fs::Fs; use futures::{ Future, FutureExt, StreamExt, channel::{ @@ -97,7 +98,7 @@ use session::AppSession; use settings::{ CenteredPaddingSettings, Settings, SettingsLocation, SettingsStore, update_settings_file, }; -use shared_screen::SharedScreen; + use sqlez::{ bindable::{Bind, Column, StaticColumnCount}, statement::Statement, @@ -1258,7 +1259,7 @@ pub struct Workspace { window_edited: bool, last_window_title: Option, dirty_items: HashMap, - active_call: Option<(Entity, Vec)>, + active_call: Option<(GlobalAnyActiveCall, Vec)>, leader_updates_tx: mpsc::UnboundedSender<(PeerId, proto::UpdateFollowers)>, database_id: Option, app_state: Arc, @@ -1572,8 +1573,12 @@ impl Workspace { let session_id = app_state.session.read(cx).id().to_owned(); let mut active_call = None; - if let Some(call) = ActiveCall::try_global(cx) { - let subscriptions = vec![cx.subscribe_in(&call, window, Self::on_active_call_event)]; + if let Some(call) = GlobalAnyActiveCall::try_global(cx).cloned() { + let subscriptions = + vec![ + call.0 + .subscribe(window, cx, Box::new(Self::on_active_call_event)), + ]; active_call = Some((call, subscriptions)); } @@ -2692,7 +2697,7 @@ impl Workspace { window: &mut Window, cx: &mut Context, ) -> Task> { - let active_call = self.active_call().cloned(); + let active_call = self.active_global_call(); cx.spawn_in(window, async move |this, cx| { this.update(cx, |this, _| { @@ -2734,7 +2739,9 @@ impl Workspace { if let Some(active_call) = active_call && workspace_count == 1 - && active_call.read_with(cx, |call, _| call.room().is_some()) + && cx + .update(|_window, cx| active_call.0.is_in_room(cx)) + .unwrap_or(false) { if close_intent == CloseIntent::CloseWindow { let answer = cx.update(|window, cx| { @@ -2750,14 +2757,13 @@ impl Workspace { if answer.await.log_err() == Some(1) { return anyhow::Ok(false); } else { - active_call - .update(cx, |call, cx| call.hang_up(cx)) - .await - .log_err(); + if let Ok(task) = cx.update(|_window, cx| active_call.0.hang_up(cx)) { + task.await.log_err(); + } } } if close_intent == CloseIntent::ReplaceWindow { - _ = active_call.update(cx, |this, cx| { + _ = cx.update(|_window, cx| { let multi_workspace = cx .windows() .iter() @@ -2771,10 +2777,10 @@ impl Workspace { .project .clone(); if project.read(cx).is_shared() { - this.unshare_project(project, cx)?; + active_call.0.unshare_project(project, cx)?; } Ok::<_, anyhow::Error>(()) - })?; + }); } } @@ -4944,7 +4950,7 @@ impl Workspace { match leader_id { CollaboratorId::PeerId(leader_peer_id) => { - let room_id = self.active_call()?.read(cx).room()?.read(cx).id(); + let room_id = self.active_call()?.room_id(cx)?; let project_id = self.project.read(cx).remote_id(); let request = self.app_state.client.request(proto::Follow { room_id, @@ -5038,20 +5044,21 @@ impl Workspace { let leader_id = leader_id.into(); if let CollaboratorId::PeerId(peer_id) = leader_id { - let Some(room) = ActiveCall::global(cx).read(cx).room() else { + let Some(active_call) = GlobalAnyActiveCall::try_global(cx) else { return; }; - let room = room.read(cx); - let Some(remote_participant) = room.remote_participant_for_peer_id(peer_id) else { + let Some(remote_participant) = + active_call.0.remote_participant_for_peer_id(peer_id, cx) + else { return; }; let project = self.project.read(cx); let other_project_id = match remote_participant.location { - call::ParticipantLocation::External => None, - call::ParticipantLocation::UnsharedProject => None, - call::ParticipantLocation::SharedProject { project_id } => { + ParticipantLocation::External => None, + ParticipantLocation::UnsharedProject => None, + ParticipantLocation::SharedProject { project_id } => { if Some(project_id) == project.remote_id() { None } else { @@ -5097,7 +5104,7 @@ impl Workspace { if let CollaboratorId::PeerId(leader_peer_id) = leader_id { let project_id = self.project.read(cx).remote_id(); - let room_id = self.active_call()?.read(cx).room()?.read(cx).id(); + let room_id = self.active_call()?.room_id(cx)?; self.app_state .client .send(proto::Unfollow { @@ -5740,20 +5747,19 @@ impl Workspace { cx: &mut Context, ) -> Option<(Option, Box)> { let call = self.active_call()?; - let room = call.read(cx).room()?.read(cx); - let participant = room.remote_participant_for_peer_id(peer_id)?; + let participant = call.remote_participant_for_peer_id(peer_id, cx)?; let leader_in_this_app; let leader_in_this_project; match participant.location { - call::ParticipantLocation::SharedProject { project_id } => { + ParticipantLocation::SharedProject { project_id } => { leader_in_this_app = true; leader_in_this_project = Some(project_id) == self.project.read(cx).remote_id(); } - call::ParticipantLocation::UnsharedProject => { + ParticipantLocation::UnsharedProject => { leader_in_this_app = true; leader_in_this_project = false; } - call::ParticipantLocation::External => { + ParticipantLocation::External => { leader_in_this_app = false; leader_in_this_project = false; } @@ -5781,19 +5787,8 @@ impl Workspace { window: &mut Window, cx: &mut App, ) -> Option> { - let call = self.active_call()?; - let room = call.read(cx).room()?.clone(); - let participant = room.read(cx).remote_participant_for_peer_id(peer_id)?; - let track = participant.video_tracks.values().next()?.clone(); - let user = participant.user.clone(); - - for item in pane.read(cx).items_of_type::() { - if item.read(cx).peer_id == peer_id { - return Some(item); - } - } - - Some(cx.new(|cx| SharedScreen::new(track, peer_id, user.clone(), room.clone(), window, cx))) + self.active_call()? + .create_shared_screen(peer_id, pane, window, cx) } pub fn on_window_activation_changed(&mut self, window: &mut Window, cx: &mut Context) { @@ -5824,23 +5819,25 @@ impl Workspace { } } - pub fn active_call(&self) -> Option<&Entity> { - self.active_call.as_ref().map(|(call, _)| call) + pub fn active_call(&self) -> Option<&dyn AnyActiveCall> { + self.active_call.as_ref().map(|(call, _)| &*call.0) + } + + pub fn active_global_call(&self) -> Option { + self.active_call.as_ref().map(|(call, _)| call.clone()) } fn on_active_call_event( &mut self, - _: &Entity, - event: &call::room::Event, + event: &ActiveCallEvent, window: &mut Window, cx: &mut Context, ) { match event { - call::room::Event::ParticipantLocationChanged { participant_id } - | call::room::Event::RemoteVideoTracksChanged { participant_id } => { + ActiveCallEvent::ParticipantLocationChanged { participant_id } + | ActiveCallEvent::RemoteVideoTracksChanged { participant_id } => { self.leader_updated(participant_id, window, cx); } - _ => {} } } @@ -7027,6 +7024,98 @@ impl Workspace { } } +pub trait AnyActiveCall { + fn entity(&self) -> AnyEntity; + fn is_in_room(&self, _: &App) -> bool; + fn room_id(&self, _: &App) -> Option; + fn channel_id(&self, _: &App) -> Option; + fn hang_up(&self, _: &mut App) -> Task>; + 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 has_remote_participants(&self, _: &App) -> bool; + fn local_participant_is_guest(&self, _: &App) -> bool; + fn client(&self, _: &App) -> Arc; + fn share_on_join(&self, _: &App) -> bool; + fn join_channel(&self, _: ChannelId, _: &mut App) -> Task>; + fn room_update_completed(&self, _: &mut App) -> Task<()>; + fn most_active_project(&self, _: &App) -> Option<(u64, u64)>; + fn share_project(&self, _: Entity, _: &mut App) -> Task>; + fn join_project( + &self, + _: u64, + _: Arc, + _: Arc, + _: &mut App, + ) -> Task>>; + fn peer_id_for_user_in_room(&self, _: u64, _: &App) -> Option; + fn subscribe( + &self, + _: &mut Window, + _: &mut Context, + _: Box)>, + ) -> Subscription; + fn create_shared_screen( + &self, + _: PeerId, + _: &Entity, + _: &mut Window, + _: &mut App, + ) -> Option>; +} + +#[derive(Clone)] +pub struct GlobalAnyActiveCall(pub Arc); +impl Global for GlobalAnyActiveCall {} + +impl GlobalAnyActiveCall { + pub(crate) fn try_global(cx: &App) -> Option<&Self> { + cx.try_global() + } + + pub(crate) fn global(cx: &App) -> &Self { + cx.global() + } +} +/// Workspace-local view of a remote participant's location. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ParticipantLocation { + SharedProject { project_id: u64 }, + UnsharedProject, + External, +} + +impl ParticipantLocation { + pub fn from_proto(location: Option) -> Result { + match location + .and_then(|l| l.variant) + .context("participant location was not provided")? + { + proto::participant_location::Variant::SharedProject(project) => { + Ok(Self::SharedProject { + project_id: project.id, + }) + } + proto::participant_location::Variant::UnsharedProject(_) => Ok(Self::UnsharedProject), + proto::participant_location::Variant::External(_) => Ok(Self::External), + } + } +} +/// Workspace-local view of a remote collaborator's state. +/// This is the subset of `call::RemoteParticipant` that workspace needs. +#[derive(Clone)] +pub struct RemoteCollaborator { + pub user: Arc, + pub peer_id: PeerId, + pub location: ParticipantLocation, + pub participant_index: ParticipantIndex, +} + +pub enum ActiveCallEvent { + ParticipantLocationChanged { participant_id: PeerId }, + RemoteVideoTracksChanged { participant_id: PeerId }, +} + fn leader_border_for_pane( follower_states: &HashMap, pane: &Entity, @@ -7043,8 +7132,9 @@ fn leader_border_for_pane( let mut leader_color = match leader_id { CollaboratorId::PeerId(leader_peer_id) => { - let room = ActiveCall::try_global(cx)?.read(cx).room()?.read(cx); - let leader = room.remote_participant_for_peer_id(leader_peer_id)?; + let leader = GlobalAnyActiveCall::try_global(cx)? + .0 + .remote_participant_for_peer_id(leader_peer_id, cx)?; cx.theme() .players() @@ -7786,8 +7876,8 @@ impl WorkspaceStore { update: proto::update_followers::Variant, cx: &App, ) -> Option<()> { - let active_call = ActiveCall::try_global(cx)?; - let room_id = active_call.read(cx).room()?.read(cx).id(); + let active_call = GlobalAnyActiveCall::try_global(cx)?; + let room_id = active_call.0.room_id(cx)?; self.client .send(proto::UpdateFollowers { room_id, @@ -8100,33 +8190,28 @@ async fn join_channel_internal( app_state: &Arc, requesting_window: Option>, requesting_workspace: Option>, - active_call: &Entity, + active_call: &dyn AnyActiveCall, cx: &mut AsyncApp, ) -> Result { - let (should_prompt, open_room) = active_call.update(cx, |active_call, cx| { - let Some(room) = active_call.room().map(|room| room.read(cx)) else { - return (false, None); - }; + let (should_prompt, already_in_channel) = cx.update(|cx| { + if !active_call.is_in_room(cx) { + return (false, false); + } - let already_in_channel = room.channel_id() == Some(channel_id); - let should_prompt = room.is_sharing_project() - && !room.remote_participants().is_empty() + let already_in_channel = active_call.channel_id(cx) == Some(channel_id); + let should_prompt = active_call.is_sharing_project(cx) + && active_call.has_remote_participants(cx) && !already_in_channel; - let open_room = if already_in_channel { - active_call.room().cloned() - } else { - None - }; - (should_prompt, open_room) + (should_prompt, already_in_channel) }); - if let Some(room) = open_room { - let task = room.update(cx, |room, cx| { - if let Some((project, host)) = room.most_active_project(cx) { - return Some(join_in_room_project(project, host, app_state.clone(), cx)); + if already_in_channel { + let task = cx.update(|cx| { + if let Some((project, host)) = active_call.most_active_project(cx) { + Some(join_in_room_project(project, host, app_state.clone(), cx)) + } else { + None } - - None }); if let Some(task) = task { task.await?; @@ -8152,11 +8237,11 @@ async fn join_channel_internal( return Ok(false); } } else { - return Ok(false); // unreachable!() hopefully + return Ok(false); } } - let client = cx.update(|cx| active_call.read(cx).client()); + let client = cx.update(|cx| active_call.client(cx)); let mut client_status = client.status(); @@ -8184,33 +8269,30 @@ async fn join_channel_internal( } } - let room = active_call - .update(cx, |active_call, cx| { - active_call.join_channel(channel_id, cx) - }) + let joined = cx + .update(|cx| active_call.join_channel(channel_id, cx)) .await?; - let Some(room) = room else { + if !joined { return anyhow::Ok(true); - }; + } - room.update(cx, |room, _| room.room_update_completed()) - .await; + cx.update(|cx| active_call.room_update_completed(cx)).await; - let task = room.update(cx, |room, cx| { - if let Some((project, host)) = room.most_active_project(cx) { + let task = cx.update(|cx| { + if let Some((project, host)) = active_call.most_active_project(cx) { return Some(join_in_room_project(project, host, app_state.clone(), cx)); } // If you are the first to join a channel, see if you should share your project. - if room.remote_participants().is_empty() - && !room.local_participant_is_guest() + if !active_call.has_remote_participants(cx) + && !active_call.local_participant_is_guest(cx) && let Some(workspace) = requesting_workspace.as_ref().and_then(|w| w.upgrade()) { let project = workspace.update(cx, |workspace, cx| { let project = workspace.project.read(cx); - if !CallSettings::get_global(cx).share_on_join { + if !active_call.share_on_join(cx) { return None; } @@ -8227,9 +8309,9 @@ async fn join_channel_internal( } }); if let Some(project) = project { - return Some(cx.spawn(async move |room, cx| { - room.update(cx, |room, cx| room.share_project(project, cx))? - .await?; + let share_task = active_call.share_project(project, cx); + return Some(cx.spawn(async move |_cx| -> Result<()> { + share_task.await?; Ok(()) })); } @@ -8251,14 +8333,14 @@ pub fn join_channel( requesting_workspace: Option>, cx: &mut App, ) -> Task> { - let active_call = ActiveCall::global(cx); + let active_call = GlobalAnyActiveCall::global(cx).clone(); cx.spawn(async move |cx| { let result = join_channel_internal( channel_id, &app_state, requesting_window, requesting_workspace, - &active_call, + &*active_call.0, cx, ) .await; @@ -9102,13 +9184,10 @@ pub fn join_in_room_project( .ok(); existing_window } else { - let active_call = cx.update(|cx| ActiveCall::global(cx)); - let room = active_call - .read_with(cx, |call, _| call.room().cloned()) - .context("not in a call")?; - let project = room - .update(cx, |room, cx| { - room.join_project( + let active_call = cx.update(|cx| GlobalAnyActiveCall::global(cx).clone()); + let project = cx + .update(|cx| { + active_call.0.join_project( project_id, app_state.languages.clone(), app_state.fs.clone(), @@ -9137,27 +9216,21 @@ pub fn join_in_room_project( // We set the active workspace above, so this is the correct workspace. let workspace = multi_workspace.workspace().clone(); workspace.update(cx, |workspace, cx| { - if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() { - let follow_peer_id = room - .read(cx) - .remote_participants() - .iter() - .find(|(_, participant)| participant.user.id == follow_user_id) - .map(|(_, p)| p.peer_id) - .or_else(|| { - // If we couldn't follow the given user, follow the host instead. - let collaborator = workspace - .project() - .read(cx) - .collaborators() - .values() - .find(|collaborator| collaborator.is_host)?; - Some(collaborator.peer_id) - }); + let follow_peer_id = GlobalAnyActiveCall::try_global(cx) + .and_then(|call| call.0.peer_id_for_user_in_room(follow_user_id, cx)) + .or_else(|| { + // If we couldn't follow the given user, follow the host instead. + let collaborator = workspace + .project() + .read(cx) + .collaborators() + .values() + .find(|collaborator| collaborator.is_host)?; + Some(collaborator.peer_id) + }); - if let Some(follow_peer_id) = follow_peer_id { - workspace.follow(follow_peer_id, window, cx); - } + if let Some(follow_peer_id) = follow_peer_id { + workspace.follow(follow_peer_id, window, cx); } }); })?;