Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions crates/call/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,16 +31,17 @@ 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
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"] }
Expand Down
260 changes: 250 additions & 10 deletions crates/call/src/call_impl/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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(
Expand Down
27 changes: 1 addition & 26 deletions crates/call/src/call_impl/participant.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
use anyhow::{Context as _, Result};
use client::{ParticipantIndex, User, proto};
use collections::HashMap;
use gpui::WeakEntity;
Expand All @@ -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>,
Expand All @@ -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,
Expand Down
3 changes: 2 additions & 1 deletion crates/call/src/call_impl/room.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand All @@ -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);

Expand Down
7 changes: 5 additions & 2 deletions crates/collab/tests/integration/following_tests.rs
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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;

Expand Down
Loading