Cargo.lock 🔗
@@ -2484,6 +2484,7 @@ dependencies = [
"settings",
"telemetry",
"util",
+ "workspace",
]
[[package]]
Jakub Konka and Piotr Osiewicz created
Release Notes:
- N/A
Co-authored-by: Piotr Osiewicz <piotr@zed.dev>
Cargo.lock | 1
crates/call/Cargo.toml | 5
crates/call/src/call_impl/mod.rs | 260 +++++++++++
crates/call/src/call_impl/participant.rs | 27 -
crates/call/src/call_impl/room.rs | 3
crates/collab/tests/integration/following_tests.rs | 7
crates/collab/tests/integration/integration_tests.rs | 4
crates/git_ui/src/git_panel.rs | 13
crates/title_bar/src/collab.rs | 4
crates/workspace/Cargo.toml | 1
crates/workspace/src/pane_group.rs | 15
crates/workspace/src/shared_screen.rs | 37 -
crates/workspace/src/workspace.rs | 311 ++++++++-----
13 files changed, 481 insertions(+), 207 deletions(-)
@@ -2484,6 +2484,7 @@ dependencies = [
"settings",
"telemetry",
"util",
+ "workspace",
]
[[package]]
@@ -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"] }
@@ -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<ActiveCall>);
-
-impl Global for GlobalActiveCall {}
+use crate::call_settings::CallSettings;
pub fn init(client: Arc<Client>, user_store: Entity<UserStore>, 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<ActiveCall>);
+
+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<u64> {
+ Some(self.0.read(cx).room()?.read(cx).id())
+ }
+
+ fn channel_id(&self, cx: &App) -> Option<ChannelId> {
+ self.0.read(cx).room()?.read(cx).channel_id()
+ }
+
+ fn hang_up(&self, cx: &mut App) -> Task<Result<()>> {
+ self.0.update(cx, |this, cx| this.hang_up(cx))
+ }
+
+ fn unshare_project(&self, project: Entity<Project>, 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<workspace::RemoteCollaborator> {
+ 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<Client> {
+ 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<Result<bool>> {
+ 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<Project>, cx: &mut App) -> Task<Result<u64>> {
+ self.0
+ .update(cx, |this, cx| this.share_project(project, cx))
+ }
+
+ fn join_project(
+ &self,
+ project_id: u64,
+ language_registry: Arc<language::LanguageRegistry>,
+ fs: Arc<dyn fs::Fs>,
+ cx: &mut App,
+ ) -> Task<Result<Entity<Project>>> {
+ 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<proto::PeerId> {
+ 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<Workspace>,
+ handler: Box<
+ dyn Fn(&mut Workspace, &ActiveCallEvent, &mut Window, &mut Context<Workspace>),
+ >,
+ ) -> 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<Pane>,
+ window: &mut Window,
+ cx: &mut App,
+ ) -> Option<Entity<workspace::SharedScreen>> {
+ 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::<SharedScreen>() {
+ if item.read(cx).peer_id == peer_id {
+ return Some(item);
+ }
+ }
+
+ Some(cx.new(|cx: &mut Context<SharedScreen>| {
+ let my_sid = track.sid();
+ cx.subscribe(
+ &room,
+ move |_: &mut SharedScreen,
+ _: Entity<Room>,
+ ev: &room::Event,
+ cx: &mut Context<SharedScreen>| {
+ 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<SharedScreen>| {
+ 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<RemoteVideoTrackView>,
+ ev: &RemoteVideoTrackViewEvent,
+ cx: &mut Context<SharedScreen>| 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::<RemoteVideoTrackView>()
+ .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<Self> {
- cx.global::<GlobalActiveCall>().0.clone()
+ Self::try_global(cx).unwrap()
}
pub fn try_global(cx: &App) -> Option<Entity<Self>> {
- cx.try_global::<GlobalActiveCall>()
- .map(|call| call.0.clone())
+ let any = cx.try_global::<GlobalAnyActiveCall>()?;
+ any.0.entity().downcast::<Self>().ok()
}
pub fn invite(
@@ -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<proto::ParticipantLocation>) -> Result<Self> {
- 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<proto::ParticipantProject>,
@@ -54,7 +29,7 @@ pub struct RemoteParticipant {
pub peer_id: proto::PeerId,
pub role: proto::ChannelRole,
pub projects: Vec<proto::ParticipantProject>,
- pub location: ParticipantLocation,
+ pub location: workspace::ParticipantLocation,
pub participant_index: ParticipantIndex,
pub muted: bool,
pub speaking: bool,
@@ -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);
@@ -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;
@@ -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() {
@@ -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<Self>) -> 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);
@@ -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;
@@ -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
@@ -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<Project>,
pub follower_states: &'a HashMap<CollaboratorId, FollowerState>,
- pub active_call: Option<&'a Entity<ActiveCall>>,
+ pub active_call: Option<&'a dyn AnyActiveCall>,
pub active_pane: &'a Entity<Pane>,
pub app_state: &'a Arc<AppState>,
pub workspace: &'a WeakEntity<Workspace>,
@@ -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();
};
@@ -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<User>,
nav_history: Option<ItemNavHistory>,
- view: Entity<RemoteVideoTrackView>,
+ 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<User>,
- room: Entity<Room>,
- window: &mut Window,
+ view: AnyView,
+ clone_view: fn(&AnyView, &mut Window, &mut App) -> AnyView,
cx: &mut Context<Self>,
) -> 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<Self>,
) -> Task<Option<Entity<Self>>> {
+ 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,
})))
}
@@ -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<String>,
dirty_items: HashMap<EntityId, Subscription>,
- active_call: Option<(Entity<ActiveCall>, Vec<Subscription>)>,
+ active_call: Option<(GlobalAnyActiveCall, Vec<Subscription>)>,
leader_updates_tx: mpsc::UnboundedSender<(PeerId, proto::UpdateFollowers)>,
database_id: Option<WorkspaceId>,
app_state: Arc<AppState>,
@@ -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<Self>,
) -> Task<Result<bool>> {
- 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<Self>,
) -> Option<(Option<PanelId>, Box<dyn ItemHandle>)> {
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<Entity<SharedScreen>> {
- 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::<SharedScreen>() {
- 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<Self>) {
@@ -5824,23 +5819,25 @@ impl Workspace {
}
}
- pub fn active_call(&self) -> Option<&Entity<ActiveCall>> {
- 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<GlobalAnyActiveCall> {
+ self.active_call.as_ref().map(|(call, _)| call.clone())
}
fn on_active_call_event(
&mut self,
- _: &Entity<ActiveCall>,
- event: &call::room::Event,
+ event: &ActiveCallEvent,
window: &mut Window,
cx: &mut Context<Self>,
) {
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<u64>;
+ fn channel_id(&self, _: &App) -> Option<ChannelId>;
+ fn hang_up(&self, _: &mut App) -> Task<Result<()>>;
+ 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 has_remote_participants(&self, _: &App) -> bool;
+ fn local_participant_is_guest(&self, _: &App) -> bool;
+ fn client(&self, _: &App) -> Arc<Client>;
+ fn share_on_join(&self, _: &App) -> bool;
+ fn join_channel(&self, _: ChannelId, _: &mut App) -> Task<Result<bool>>;
+ fn room_update_completed(&self, _: &mut App) -> Task<()>;
+ fn most_active_project(&self, _: &App) -> Option<(u64, u64)>;
+ fn share_project(&self, _: Entity<Project>, _: &mut App) -> Task<Result<u64>>;
+ fn join_project(
+ &self,
+ _: u64,
+ _: Arc<LanguageRegistry>,
+ _: Arc<dyn Fs>,
+ _: &mut App,
+ ) -> Task<Result<Entity<Project>>>;
+ fn peer_id_for_user_in_room(&self, _: u64, _: &App) -> Option<PeerId>;
+ fn subscribe(
+ &self,
+ _: &mut Window,
+ _: &mut Context<Workspace>,
+ _: Box<dyn Fn(&mut Workspace, &ActiveCallEvent, &mut Window, &mut Context<Workspace>)>,
+ ) -> Subscription;
+ fn create_shared_screen(
+ &self,
+ _: PeerId,
+ _: &Entity<Pane>,
+ _: &mut Window,
+ _: &mut App,
+ ) -> Option<Entity<SharedScreen>>;
+}
+
+#[derive(Clone)]
+pub struct GlobalAnyActiveCall(pub Arc<dyn AnyActiveCall>);
+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<proto::ParticipantLocation>) -> Result<Self> {
+ 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<User>,
+ 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<CollaboratorId, FollowerState>,
pane: &Entity<Pane>,
@@ -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<AppState>,
requesting_window: Option<WindowHandle<MultiWorkspace>>,
requesting_workspace: Option<WeakEntity<Workspace>>,
- active_call: &Entity<ActiveCall>,
+ active_call: &dyn AnyActiveCall,
cx: &mut AsyncApp,
) -> Result<bool> {
- 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<WeakEntity<Workspace>>,
cx: &mut App,
) -> Task<Result<()>> {
- 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);
}
});
})?;