From 46019f85379c48a47e1405e8593548af98baced8 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 19 Sep 2022 18:01:34 +0200 Subject: [PATCH 001/112] WIP --- Cargo.lock | 11 +++++++++ crates/room/Cargo.toml | 27 ++++++++++++++++++++ crates/room/src/participant.rs | 15 ++++++++++++ crates/room/src/room.rs | 45 ++++++++++++++++++++++++++++++++++ 4 files changed, 98 insertions(+) create mode 100644 crates/room/Cargo.toml create mode 100644 crates/room/src/participant.rs create mode 100644 crates/room/src/room.rs diff --git a/Cargo.lock b/Cargo.lock index 925a4323a41e682202619c27643ffa97d7eefa9d..d30cbcee69754d800f32a8253152d8241aea19a1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4451,6 +4451,17 @@ dependencies = [ "librocksdb-sys", ] +[[package]] +name = "room" +version = "0.1.0" +dependencies = [ + "anyhow", + "client", + "gpui", + "project", + "workspace", +] + [[package]] name = "roxmltree" version = "0.14.1" diff --git a/crates/room/Cargo.toml b/crates/room/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..767ba399d62a79104d1e9003f9f58aa44b857601 --- /dev/null +++ b/crates/room/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "room" +version = "0.1.0" +edition = "2021" + +[lib] +path = "src/room.rs" +doctest = false + +[features] +test-support = [ + "client/test-support", + "gpui/test-support", + "project/test-support", +] + +[dependencies] +anyhow = "1.0.38" +client = { path = "../client" } +gpui = { path = "../gpui" } +project = { path = "../project" } +workspace = { path = "../workspace" } + +[dev-dependencies] +client = { path = "../client", features = ["test-support"] } +gpui = { path = "../gpui", features = ["test-support"] } +project = { path = "../project", features = ["test-support"] } diff --git a/crates/room/src/participant.rs b/crates/room/src/participant.rs new file mode 100644 index 0000000000000000000000000000000000000000..a5b02b05a6a84349c5deb70300183f4270aea66c --- /dev/null +++ b/crates/room/src/participant.rs @@ -0,0 +1,15 @@ +use client::User; +use gpui::{ModelHandle, ViewHandle}; +use project::Project; +use workspace::Workspace; + +pub struct LocalParticipant { + user: User, + workspaces: Vec>, +} + +pub struct RemoteParticipant { + user: User, + workspaces: Vec>, + active_workspace_id: usize, +} diff --git a/crates/room/src/room.rs b/crates/room/src/room.rs new file mode 100644 index 0000000000000000000000000000000000000000..b18cb800e48a0111d1378d943e2a8d74118b3735 --- /dev/null +++ b/crates/room/src/room.rs @@ -0,0 +1,45 @@ +mod participant; + +use anyhow::Result; +use client::Client; +use gpui::ModelHandle; +use participant::{LocalParticipant, RemoteParticipant}; +use project::Project; +use std::sync::Arc; + +pub struct Room { + id: u64, + local_participant: LocalParticipant, + remote_participants: Vec, + client: Arc, +} + +impl Room { + pub async fn create(client: Arc) -> Result { + todo!() + } + + pub async fn join(id: u64, client: Arc) -> Result { + todo!() + } + + pub async fn invite(&mut self, user_id: u64) -> Result<()> { + todo!() + } + + pub async fn share(&mut self) -> Result<()> { + todo!() + } + + pub async fn unshare(&mut self) -> Result<()> { + todo!() + } + + pub async fn mute(&mut self) -> Result<()> { + todo!() + } + + pub async fn unmute(&mut self) -> Result<()> { + todo!() + } +} From 8fec7da7998d0c297b1c4dbbabaa5b02fb64c177 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 22 Sep 2022 17:08:36 +0200 Subject: [PATCH 002/112] WIP --- Cargo.lock | 1 - crates/collab/src/integration_tests.rs | 36 ++++++++++++++++++++++++++ crates/collab/src/rpc/store.rs | 14 ++++++++++ crates/room/Cargo.toml | 1 - crates/room/src/participant.rs | 14 ++++++---- crates/room/src/room.rs | 27 ++++++++++++++----- 6 files changed, 80 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d30cbcee69754d800f32a8253152d8241aea19a1..2d25f79fe2d6a3e87a508396e3fbbd29018b4d0b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4459,7 +4459,6 @@ dependencies = [ "client", "gpui", "project", - "workspace", ] [[package]] diff --git a/crates/collab/src/integration_tests.rs b/crates/collab/src/integration_tests.rs index 073547472854be6be9dedec921a17a89b467d5e6..c91e3e4ea52fa586e612526eaf45b4ef61a5e437 100644 --- a/crates/collab/src/integration_tests.rs +++ b/crates/collab/src/integration_tests.rs @@ -60,6 +60,42 @@ fn init_logger() { } } +#[gpui::test(iterations = 10)] +async fn test_share_project_in_room( + deterministic: Arc, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, +) { + deterministic.forbid_parking(); + let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + server + .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) + .await; + + client_a + .fs + .insert_tree( + "/a", + json!({ + ".gitignore": "ignored-dir", + "a.txt": "a-contents", + "b.txt": "b-contents", + "ignored-dir": { + "c.txt": "", + "d.txt": "", + } + }), + ) + .await; + + let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; + let project_id = project_a.update(cx_a, |project, cx| project.share(cx)).await.unwrap(); + + +} + #[gpui::test(iterations = 10)] async fn test_share_project( deterministic: Arc, diff --git a/crates/collab/src/rpc/store.rs b/crates/collab/src/rpc/store.rs index fe18e0404b214e47e7f2e9d42019e8ae9b86bf0a..4d23a4d7418c0657c18b68a506123f728c3ddcdd 100644 --- a/crates/collab/src/rpc/store.rs +++ b/crates/collab/src/rpc/store.rs @@ -7,10 +7,13 @@ use std::{mem, path::PathBuf, str, time::Duration}; use time::OffsetDateTime; use tracing::instrument; +pub type RoomId = u64; + #[derive(Default, Serialize)] pub struct Store { connections: BTreeMap, connections_by_user_id: BTreeMap>, + rooms: BTreeMap, projects: BTreeMap, #[serde(skip)] channels: BTreeMap, @@ -25,6 +28,17 @@ struct ConnectionState { channels: HashSet, } +#[derive(Serialize)] +struct Room { + participants: HashMap, +} + +#[derive(Serialize)] +struct Participant { + user_id: UserId, + shared_projects: HashSet, +} + #[derive(Serialize)] pub struct Project { pub online: bool, diff --git a/crates/room/Cargo.toml b/crates/room/Cargo.toml index 767ba399d62a79104d1e9003f9f58aa44b857601..80b0a1f459792e2a256ed4b7854c355709e5391b 100644 --- a/crates/room/Cargo.toml +++ b/crates/room/Cargo.toml @@ -19,7 +19,6 @@ anyhow = "1.0.38" client = { path = "../client" } gpui = { path = "../gpui" } project = { path = "../project" } -workspace = { path = "../workspace" } [dev-dependencies] client = { path = "../client", features = ["test-support"] } diff --git a/crates/room/src/participant.rs b/crates/room/src/participant.rs index a5b02b05a6a84349c5deb70300183f4270aea66c..50b873a781a94599a4fb19bca00e434bd53105da 100644 --- a/crates/room/src/participant.rs +++ b/crates/room/src/participant.rs @@ -1,15 +1,19 @@ use client::User; -use gpui::{ModelHandle, ViewHandle}; +use gpui::ModelHandle; use project::Project; -use workspace::Workspace; + +pub enum Location { + Project { project_id: usize }, + External, +} pub struct LocalParticipant { user: User, - workspaces: Vec>, + projects: Vec>, } pub struct RemoteParticipant { user: User, - workspaces: Vec>, - active_workspace_id: usize, + projects: Vec>, + location: Location, } diff --git a/crates/room/src/room.rs b/crates/room/src/room.rs index b18cb800e48a0111d1378d943e2a8d74118b3735..e4017e96428566314aef65e4d2b34c50ba1ab444 100644 --- a/crates/room/src/room.rs +++ b/crates/room/src/room.rs @@ -1,19 +1,27 @@ mod participant; use anyhow::Result; -use client::Client; -use gpui::ModelHandle; +use client::{Client, PeerId}; +use gpui::{Entity, ModelHandle}; use participant::{LocalParticipant, RemoteParticipant}; use project::Project; -use std::sync::Arc; +use std::{collections::HashMap, sync::Arc}; + +pub enum Event { + PeerChangedActiveProject, +} pub struct Room { id: u64, local_participant: LocalParticipant, - remote_participants: Vec, + remote_participants: HashMap, client: Arc, } +impl Entity for Room { + type Event = Event; +} + impl Room { pub async fn create(client: Arc) -> Result { todo!() @@ -27,11 +35,18 @@ impl Room { todo!() } - pub async fn share(&mut self) -> Result<()> { + pub async fn share_project(&mut self, project: ModelHandle) -> Result<()> { + todo!() + } + + pub async fn unshare_project(&mut self, project: ModelHandle) -> Result<()> { todo!() } - pub async fn unshare(&mut self) -> Result<()> { + pub async fn set_active_project( + &mut self, + project: Option<&ModelHandle>, + ) -> Result<()> { todo!() } From 0b1e372d11eb401b5f86481e5df9ca96bd548350 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 22 Sep 2022 19:41:51 +0200 Subject: [PATCH 003/112] Start sketching out an integration test for calls Co-Authored-By: Nathan Sobo --- Cargo.lock | 1 + crates/collab/Cargo.toml | 3 ++- crates/collab/src/integration_tests.rs | 12 +++++++----- crates/room/src/room.rs | 4 ++-- 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2d25f79fe2d6a3e87a508396e3fbbd29018b4d0b..f853a2059c73c6e22771817ed16cc947dc1581a5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1040,6 +1040,7 @@ dependencies = [ "prometheus", "rand 0.8.5", "reqwest", + "room", "rpc", "scrypt", "serde", diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index 9b3603e6e43e29acb6071a9c7670d07d412a91ba..8603f675572b7c0d03853b1e81047c71ad41cd15 100644 --- a/crates/collab/Cargo.toml +++ b/crates/collab/Cargo.toml @@ -55,13 +55,14 @@ features = ["runtime-tokio-rustls", "postgres", "time", "uuid"] [dev-dependencies] collections = { path = "../collections", features = ["test-support"] } gpui = { path = "../gpui", features = ["test-support"] } -rpc = { path = "../rpc", features = ["test-support"] } client = { path = "../client", features = ["test-support"] } editor = { path = "../editor", features = ["test-support"] } language = { path = "../language", features = ["test-support"] } log = { version = "0.4.16", features = ["kv_unstable_serde"] } lsp = { path = "../lsp", features = ["test-support"] } project = { path = "../project", features = ["test-support"] } +room = { path = "../room", features = ["test-support"] } +rpc = { path = "../rpc", features = ["test-support"] } settings = { path = "../settings", features = ["test-support"] } theme = { path = "../theme" } workspace = { path = "../workspace", features = ["test-support"] } diff --git a/crates/collab/src/integration_tests.rs b/crates/collab/src/integration_tests.rs index c91e3e4ea52fa586e612526eaf45b4ef61a5e437..36ecaf41b498aebfd4541e75c1f07b5b6d19343b 100644 --- a/crates/collab/src/integration_tests.rs +++ b/crates/collab/src/integration_tests.rs @@ -90,10 +90,15 @@ async fn test_share_project_in_room( ) .await; + let room_a = Room::create(client_a.clone()).await.unwrap(); let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; - let project_id = project_a.update(cx_a, |project, cx| project.share(cx)).await.unwrap(); - + // room.publish_project(project_a.clone()).await.unwrap(); + let incoming_calls_b = client_b.user_store.incoming_calls(); + let user_b_joined = room_a.invite(client_b.user_id().unwrap()); + let call_b = incoming_calls_b.next().await.unwrap(); + let room_b = Room::join(call_b.room_id, client_b.clone()).await.unwrap(); + user_b_joined.await.unwrap(); } #[gpui::test(iterations = 10)] @@ -5469,9 +5474,6 @@ impl TestClient { worktree .read_with(cx, |tree, _| tree.as_local().unwrap().scan_complete()) .await; - project - .update(cx, |project, _| project.next_remote_id()) - .await; (project, worktree.read_with(cx, |tree, _| tree.id())) } diff --git a/crates/room/src/room.rs b/crates/room/src/room.rs index e4017e96428566314aef65e4d2b34c50ba1ab444..965804efc8a2eaa3841836fe92ad7f058e553912 100644 --- a/crates/room/src/room.rs +++ b/crates/room/src/room.rs @@ -35,11 +35,11 @@ impl Room { todo!() } - pub async fn share_project(&mut self, project: ModelHandle) -> Result<()> { + pub async fn publish_project(&mut self, project: ModelHandle) -> Result<()> { todo!() } - pub async fn unshare_project(&mut self, project: ModelHandle) -> Result<()> { + pub async fn unpublish_project(&mut self, project: ModelHandle) -> Result<()> { todo!() } From ebb5ffcedc645f1f4e6984bcfc632825c9999325 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 23 Sep 2022 15:05:32 +0200 Subject: [PATCH 004/112] Introduce the ability of creating rooms on the server --- Cargo.lock | 1 + crates/client/src/call.rs | 8 + crates/client/src/client.rs | 1 + crates/client/src/user.rs | 19 +- crates/collab/src/integration_tests.rs | 19 +- crates/collab/src/rpc.rs | 11 ++ crates/collab/src/rpc/store.rs | 40 ++-- crates/room/Cargo.toml | 3 + crates/room/src/participant.rs | 28 ++- crates/room/src/room.rs | 62 +++++- crates/rpc/proto/zed.proto | 250 +++++++++++++++---------- crates/rpc/src/proto.rs | 8 +- 12 files changed, 312 insertions(+), 138 deletions(-) create mode 100644 crates/client/src/call.rs diff --git a/Cargo.lock b/Cargo.lock index f853a2059c73c6e22771817ed16cc947dc1581a5..08e183810d6f6927f8073f2f2eb92617f01dd29c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4458,6 +4458,7 @@ version = "0.1.0" dependencies = [ "anyhow", "client", + "collections", "gpui", "project", ] diff --git a/crates/client/src/call.rs b/crates/client/src/call.rs new file mode 100644 index 0000000000000000000000000000000000000000..2e7bd799f073f588f1ee934692658b978d1328a0 --- /dev/null +++ b/crates/client/src/call.rs @@ -0,0 +1,8 @@ +use crate::User; +use std::sync::Arc; + +#[derive(Clone)] +pub struct Call { + pub from: Vec>, + pub room_id: u64, +} diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index e328108a520b10b353aaf9e4c7e3dc73499802d9..20563272abef74cda64ed76620ad2f6bbf6dfb0e 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -1,6 +1,7 @@ #[cfg(any(test, feature = "test-support"))] pub mod test; +pub mod call; pub mod channel; pub mod http; pub mod user; diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index 149d22e77aec8fdb0803764eb7f9c863ed018797..71e8de12e5844b5f2e17759a86b80f5ca1c9b1ce 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -1,9 +1,11 @@ +use crate::call::Call; + use super::{http::HttpClient, proto, Client, Status, TypedEnvelope}; use anyhow::{anyhow, Context, Result}; use collections::{hash_map::Entry, BTreeSet, HashMap, HashSet}; -use futures::{channel::mpsc, future, AsyncReadExt, Future, StreamExt}; +use futures::{channel::mpsc, future, AsyncReadExt, Future, Stream, StreamExt}; use gpui::{AsyncAppContext, Entity, ImageData, ModelContext, ModelHandle, Task}; -use postage::{prelude::Stream, sink::Sink, watch}; +use postage::{broadcast, sink::Sink, watch}; use rpc::proto::{RequestMessage, UsersResponse}; use std::sync::{Arc, Weak}; use util::TryFutureExt as _; @@ -66,6 +68,7 @@ pub struct UserStore { outgoing_contact_requests: Vec>, pending_contact_requests: HashMap, invite_info: Option, + incoming_calls: broadcast::Sender, client: Weak, http: Arc, _maintain_contacts: Task<()>, @@ -116,6 +119,7 @@ impl UserStore { client.add_message_handler(cx.handle(), Self::handle_update_invite_info), client.add_message_handler(cx.handle(), Self::handle_show_contacts), ]; + let (incoming_calls, _) = broadcast::channel(32); Self { users: Default::default(), current_user: current_user_rx, @@ -123,6 +127,7 @@ impl UserStore { incoming_contact_requests: Default::default(), outgoing_contact_requests: Default::default(), invite_info: None, + incoming_calls, client: Arc::downgrade(&client), update_contacts_tx, http, @@ -138,7 +143,7 @@ impl UserStore { }), _maintain_current_user: cx.spawn_weak(|this, mut cx| async move { let mut status = client.status(); - while let Some(status) = status.recv().await { + while let Some(status) = status.next().await { match status { Status::Connected { .. } => { if let Some((this, user_id)) = this.upgrade(&cx).zip(client.user_id()) { @@ -198,6 +203,10 @@ impl UserStore { self.invite_info.as_ref() } + pub fn incoming_calls(&self) -> impl 'static + Stream { + self.incoming_calls.subscribe() + } + async fn handle_update_contacts( this: ModelHandle, message: TypedEnvelope, @@ -493,7 +502,7 @@ impl UserStore { .unbounded_send(UpdateContacts::Clear(tx)) .unwrap(); async move { - rx.recv().await; + rx.next().await; } } @@ -503,7 +512,7 @@ impl UserStore { .unbounded_send(UpdateContacts::Wait(tx)) .unwrap(); async move { - rx.recv().await; + rx.next().await; } } diff --git a/crates/collab/src/integration_tests.rs b/crates/collab/src/integration_tests.rs index 36ecaf41b498aebfd4541e75c1f07b5b6d19343b..cc80f96ad8e245bb313ab9a805be485b4550dab8 100644 --- a/crates/collab/src/integration_tests.rs +++ b/crates/collab/src/integration_tests.rs @@ -34,6 +34,7 @@ use project::{ DiagnosticSummary, Project, ProjectPath, ProjectStore, WorktreeId, }; use rand::prelude::*; +use room::Room; use rpc::PeerId; use serde_json::json; use settings::{Formatter, Settings}; @@ -90,14 +91,24 @@ async fn test_share_project_in_room( ) .await; - let room_a = Room::create(client_a.clone()).await.unwrap(); + let room_a = cx_a + .update(|cx| Room::create(client_a.clone(), cx)) + .await + .unwrap(); let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; // room.publish_project(project_a.clone()).await.unwrap(); - let incoming_calls_b = client_b.user_store.incoming_calls(); - let user_b_joined = room_a.invite(client_b.user_id().unwrap()); + let mut incoming_calls_b = client_b + .user_store + .read_with(cx_b, |user, _| user.incoming_calls()); + let user_b_joined = room_a.update(cx_a, |room, cx| { + room.invite(client_b.user_id().unwrap(), cx) + }); let call_b = incoming_calls_b.next().await.unwrap(); - let room_b = Room::join(call_b.room_id, client_b.clone()).await.unwrap(); + let room_b = cx_b + .update(|cx| Room::join(call_b.room_id, client_b.clone(), cx)) + .await + .unwrap(); user_b_joined.await.unwrap(); } diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index dab7df3e674c9d835383318e4f0892c23c52c1f5..6434a97de5b8ecd18d529cc5e2794020ad40dbce 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -151,6 +151,7 @@ impl Server { server .add_request_handler(Server::ping) + .add_request_handler(Server::create_room) .add_request_handler(Server::register_project) .add_request_handler(Server::unregister_project) .add_request_handler(Server::join_project) @@ -593,6 +594,16 @@ impl Server { Ok(()) } + async fn create_room( + self: Arc, + request: TypedEnvelope, + response: Response, + ) -> Result<()> { + let room_id = self.store().await.create_room(request.sender_id)?; + response.send(proto::CreateRoomResponse { id: room_id })?; + Ok(()) + } + async fn register_project( self: Arc, request: TypedEnvelope, diff --git a/crates/collab/src/rpc/store.rs b/crates/collab/src/rpc/store.rs index 4d23a4d7418c0657c18b68a506123f728c3ddcdd..9ce6931477e82ddf2486a2718937139fd73a6fdf 100644 --- a/crates/collab/src/rpc/store.rs +++ b/crates/collab/src/rpc/store.rs @@ -6,6 +6,7 @@ use serde::Serialize; use std::{mem, path::PathBuf, str, time::Duration}; use time::OffsetDateTime; use tracing::instrument; +use util::post_inc; pub type RoomId = u64; @@ -13,7 +14,8 @@ pub type RoomId = u64; pub struct Store { connections: BTreeMap, connections_by_user_id: BTreeMap>, - rooms: BTreeMap, + next_room_id: RoomId, + rooms: BTreeMap, projects: BTreeMap, #[serde(skip)] channels: BTreeMap, @@ -23,22 +25,12 @@ pub struct Store { struct ConnectionState { user_id: UserId, admin: bool, + rooms: BTreeSet, projects: BTreeSet, requested_projects: HashSet, channels: HashSet, } -#[derive(Serialize)] -struct Room { - participants: HashMap, -} - -#[derive(Serialize)] -struct Participant { - user_id: UserId, - shared_projects: HashSet, -} - #[derive(Serialize)] pub struct Project { pub online: bool, @@ -148,6 +140,7 @@ impl Store { ConnectionState { user_id, admin, + rooms: Default::default(), projects: Default::default(), requested_projects: Default::default(), channels: Default::default(), @@ -335,6 +328,29 @@ impl Store { metadata } + pub fn create_room(&mut self, creator_connection_id: ConnectionId) -> Result { + let connection = self + .connections + .get_mut(&creator_connection_id) + .ok_or_else(|| anyhow!("no such connection"))?; + let mut room = proto::Room::default(); + room.participants.push(proto::Participant { + user_id: connection.user_id.to_proto(), + peer_id: creator_connection_id.0, + project_ids: Default::default(), + location: Some(proto::ParticipantLocation { + variant: Some(proto::participant_location::Variant::External( + proto::participant_location::External {}, + )), + }), + }); + + let room_id = post_inc(&mut self.next_room_id); + self.rooms.insert(room_id, room); + connection.rooms.insert(room_id); + Ok(room_id) + } + pub fn register_project( &mut self, host_connection_id: ConnectionId, diff --git a/crates/room/Cargo.toml b/crates/room/Cargo.toml index 80b0a1f459792e2a256ed4b7854c355709e5391b..f329d5ae878e9662898f11276dc84efe66578e97 100644 --- a/crates/room/Cargo.toml +++ b/crates/room/Cargo.toml @@ -10,6 +10,7 @@ doctest = false [features] test-support = [ "client/test-support", + "collections/test-support", "gpui/test-support", "project/test-support", ] @@ -17,10 +18,12 @@ test-support = [ [dependencies] anyhow = "1.0.38" client = { path = "../client" } +collections = { path = "../collections" } gpui = { path = "../gpui" } project = { path = "../project" } [dev-dependencies] client = { path = "../client", features = ["test-support"] } +collections = { path = "../collections", features = ["test-support"] } gpui = { path = "../gpui", features = ["test-support"] } project = { path = "../project", features = ["test-support"] } diff --git a/crates/room/src/participant.rs b/crates/room/src/participant.rs index 50b873a781a94599a4fb19bca00e434bd53105da..cde17b45c2b447af11b5e76bda7b80706ec476d0 100644 --- a/crates/room/src/participant.rs +++ b/crates/room/src/participant.rs @@ -1,19 +1,31 @@ -use client::User; +use anyhow::{anyhow, Result}; +use client::proto; use gpui::ModelHandle; use project::Project; -pub enum Location { - Project { project_id: usize }, +pub enum ParticipantLocation { + Project { project_id: u64 }, External, } +impl ParticipantLocation { + pub fn from_proto(location: Option) -> Result { + match location.and_then(|l| l.variant) { + Some(proto::participant_location::Variant::Project(project)) => Ok(Self::Project { + project_id: project.id, + }), + Some(proto::participant_location::Variant::External(_)) => Ok(Self::External), + None => Err(anyhow!("participant location was not provided")), + } + } +} + pub struct LocalParticipant { - user: User, - projects: Vec>, + pub projects: Vec>, } pub struct RemoteParticipant { - user: User, - projects: Vec>, - location: Location, + pub user_id: u64, + pub projects: Vec>, + pub location: ParticipantLocation, } diff --git a/crates/room/src/room.rs b/crates/room/src/room.rs index 965804efc8a2eaa3841836fe92ad7f058e553912..c444db4316efde57b7f1c397556024026503b0d5 100644 --- a/crates/room/src/room.rs +++ b/crates/room/src/room.rs @@ -1,11 +1,12 @@ mod participant; -use anyhow::Result; -use client::{Client, PeerId}; -use gpui::{Entity, ModelHandle}; -use participant::{LocalParticipant, RemoteParticipant}; +use anyhow::{anyhow, Result}; +use client::{proto, Client, PeerId}; +use collections::HashMap; +use gpui::{Entity, ModelContext, ModelHandle, MutableAppContext, Task}; +use participant::{LocalParticipant, ParticipantLocation, RemoteParticipant}; use project::Project; -use std::{collections::HashMap, sync::Arc}; +use std::sync::Arc; pub enum Event { PeerChangedActiveProject, @@ -23,15 +24,56 @@ impl Entity for Room { } impl Room { - pub async fn create(client: Arc) -> Result { - todo!() + pub fn create( + client: Arc, + cx: &mut MutableAppContext, + ) -> Task>> { + cx.spawn(|mut cx| async move { + let room = client.request(proto::CreateRoom {}).await?; + Ok(cx.add_model(|cx| Self::new(room.id, client, cx))) + }) } - pub async fn join(id: u64, client: Arc) -> Result { - todo!() + pub fn join( + id: u64, + client: Arc, + cx: &mut MutableAppContext, + ) -> Task>> { + cx.spawn(|mut cx| async move { + let response = client.request(proto::JoinRoom { id }).await?; + let room_proto = response.room.ok_or_else(|| anyhow!("invalid room"))?; + let room = cx.add_model(|cx| Self::new(id, client, cx)); + room.update(&mut cx, |room, cx| room.refresh(room_proto, cx))?; + Ok(room) + }) + } + + fn new(id: u64, client: Arc, _: &mut ModelContext) -> Self { + Self { + id, + local_participant: LocalParticipant { + projects: Default::default(), + }, + remote_participants: Default::default(), + client, + } + } + + fn refresh(&mut self, room: proto::Room, cx: &mut ModelContext) -> Result<()> { + for participant in room.participants { + self.remote_participants.insert( + PeerId(participant.peer_id), + RemoteParticipant { + user_id: participant.user_id, + projects: Default::default(), // TODO: populate projects + location: ParticipantLocation::from_proto(participant.location)?, + }, + ); + } + Ok(()) } - pub async fn invite(&mut self, user_id: u64) -> Result<()> { + pub fn invite(&mut self, user_id: u64, cx: &mut ModelContext) -> Task> { todo!() } diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 7840829b4461bc7ca497f7bf1bcc7b34e397f0f8..3d8f7f9a6e87f9cce4deaca1f1a3cf03e7f48e01 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -10,104 +10,112 @@ message Envelope { Error error = 5; Ping ping = 6; Test test = 7; - - RegisterProject register_project = 8; - RegisterProjectResponse register_project_response = 9; - UnregisterProject unregister_project = 10; - RequestJoinProject request_join_project = 11; - RespondToJoinProjectRequest respond_to_join_project_request = 12; - JoinProjectRequestCancelled join_project_request_cancelled = 13; - JoinProject join_project = 14; - JoinProjectResponse join_project_response = 15; - LeaveProject leave_project = 16; - AddProjectCollaborator add_project_collaborator = 17; - RemoveProjectCollaborator remove_project_collaborator = 18; - ProjectUnshared project_unshared = 19; - - GetDefinition get_definition = 20; - GetDefinitionResponse get_definition_response = 21; - GetTypeDefinition get_type_definition = 22; - GetTypeDefinitionResponse get_type_definition_response = 23; - GetReferences get_references = 24; - GetReferencesResponse get_references_response = 25; - GetDocumentHighlights get_document_highlights = 26; - GetDocumentHighlightsResponse get_document_highlights_response = 27; - GetProjectSymbols get_project_symbols = 28; - GetProjectSymbolsResponse get_project_symbols_response = 29; - OpenBufferForSymbol open_buffer_for_symbol = 30; - OpenBufferForSymbolResponse open_buffer_for_symbol_response = 31; - - UpdateProject update_project = 32; - RegisterProjectActivity register_project_activity = 33; - UpdateWorktree update_worktree = 34; - UpdateWorktreeExtensions update_worktree_extensions = 35; - - CreateProjectEntry create_project_entry = 36; - RenameProjectEntry rename_project_entry = 37; - CopyProjectEntry copy_project_entry = 38; - DeleteProjectEntry delete_project_entry = 39; - ProjectEntryResponse project_entry_response = 40; - - UpdateDiagnosticSummary update_diagnostic_summary = 41; - StartLanguageServer start_language_server = 42; - UpdateLanguageServer update_language_server = 43; - - OpenBufferById open_buffer_by_id = 44; - OpenBufferByPath open_buffer_by_path = 45; - OpenBufferResponse open_buffer_response = 46; - CreateBufferForPeer create_buffer_for_peer = 47; - UpdateBuffer update_buffer = 48; - UpdateBufferFile update_buffer_file = 49; - SaveBuffer save_buffer = 50; - BufferSaved buffer_saved = 51; - BufferReloaded buffer_reloaded = 52; - ReloadBuffers reload_buffers = 53; - ReloadBuffersResponse reload_buffers_response = 54; - FormatBuffers format_buffers = 55; - FormatBuffersResponse format_buffers_response = 56; - GetCompletions get_completions = 57; - GetCompletionsResponse get_completions_response = 58; - ApplyCompletionAdditionalEdits apply_completion_additional_edits = 59; - ApplyCompletionAdditionalEditsResponse apply_completion_additional_edits_response = 60; - GetCodeActions get_code_actions = 61; - GetCodeActionsResponse get_code_actions_response = 62; - GetHover get_hover = 63; - GetHoverResponse get_hover_response = 64; - ApplyCodeAction apply_code_action = 65; - ApplyCodeActionResponse apply_code_action_response = 66; - PrepareRename prepare_rename = 67; - PrepareRenameResponse prepare_rename_response = 68; - PerformRename perform_rename = 69; - PerformRenameResponse perform_rename_response = 70; - SearchProject search_project = 71; - SearchProjectResponse search_project_response = 72; - - GetChannels get_channels = 73; - GetChannelsResponse get_channels_response = 74; - JoinChannel join_channel = 75; - JoinChannelResponse join_channel_response = 76; - LeaveChannel leave_channel = 77; - SendChannelMessage send_channel_message = 78; - SendChannelMessageResponse send_channel_message_response = 79; - ChannelMessageSent channel_message_sent = 80; - GetChannelMessages get_channel_messages = 81; - GetChannelMessagesResponse get_channel_messages_response = 82; - - UpdateContacts update_contacts = 83; - UpdateInviteInfo update_invite_info = 84; - ShowContacts show_contacts = 85; - - GetUsers get_users = 86; - FuzzySearchUsers fuzzy_search_users = 87; - UsersResponse users_response = 88; - RequestContact request_contact = 89; - RespondToContactRequest respond_to_contact_request = 90; - RemoveContact remove_contact = 91; - - Follow follow = 92; - FollowResponse follow_response = 93; - UpdateFollowers update_followers = 94; - Unfollow unfollow = 95; + + CreateRoom create_room = 8; + CreateRoomResponse create_room_response = 9; + JoinRoom join_room = 10; + JoinRoomResponse join_room_response = 11; + Call call = 12; + CallResponse call_response = 13; + RoomUpdated room_updated = 14; + + RegisterProject register_project = 15; + RegisterProjectResponse register_project_response = 16; + UnregisterProject unregister_project = 17; + RequestJoinProject request_join_project = 18; + RespondToJoinProjectRequest respond_to_join_project_request = 19; + JoinProjectRequestCancelled join_project_request_cancelled = 20; + JoinProject join_project = 21; + JoinProjectResponse join_project_response = 22; + LeaveProject leave_project = 23; + AddProjectCollaborator add_project_collaborator = 24; + RemoveProjectCollaborator remove_project_collaborator = 25; + ProjectUnshared project_unshared = 26; + + GetDefinition get_definition = 27; + GetDefinitionResponse get_definition_response = 28; + GetTypeDefinition get_type_definition = 29; + GetTypeDefinitionResponse get_type_definition_response = 30; + GetReferences get_references = 31; + GetReferencesResponse get_references_response = 32; + GetDocumentHighlights get_document_highlights = 33; + GetDocumentHighlightsResponse get_document_highlights_response = 34; + GetProjectSymbols get_project_symbols = 35; + GetProjectSymbolsResponse get_project_symbols_response = 36; + OpenBufferForSymbol open_buffer_for_symbol = 37; + OpenBufferForSymbolResponse open_buffer_for_symbol_response = 38; + + UpdateProject update_project = 39; + RegisterProjectActivity register_project_activity = 40; + UpdateWorktree update_worktree = 41; + UpdateWorktreeExtensions update_worktree_extensions = 42; + + CreateProjectEntry create_project_entry = 43; + RenameProjectEntry rename_project_entry = 44; + CopyProjectEntry copy_project_entry = 45; + DeleteProjectEntry delete_project_entry = 46; + ProjectEntryResponse project_entry_response = 47; + + UpdateDiagnosticSummary update_diagnostic_summary = 48; + StartLanguageServer start_language_server = 49; + UpdateLanguageServer update_language_server = 50; + + OpenBufferById open_buffer_by_id = 51; + OpenBufferByPath open_buffer_by_path = 52; + OpenBufferResponse open_buffer_response = 53; + CreateBufferForPeer create_buffer_for_peer = 54; + UpdateBuffer update_buffer = 55; + UpdateBufferFile update_buffer_file = 56; + SaveBuffer save_buffer = 57; + BufferSaved buffer_saved = 58; + BufferReloaded buffer_reloaded = 59; + ReloadBuffers reload_buffers = 60; + ReloadBuffersResponse reload_buffers_response = 61; + FormatBuffers format_buffers = 62; + FormatBuffersResponse format_buffers_response = 63; + GetCompletions get_completions = 64; + GetCompletionsResponse get_completions_response = 65; + ApplyCompletionAdditionalEdits apply_completion_additional_edits = 66; + ApplyCompletionAdditionalEditsResponse apply_completion_additional_edits_response = 67; + GetCodeActions get_code_actions = 68; + GetCodeActionsResponse get_code_actions_response = 69; + GetHover get_hover = 70; + GetHoverResponse get_hover_response = 71; + ApplyCodeAction apply_code_action = 72; + ApplyCodeActionResponse apply_code_action_response = 73; + PrepareRename prepare_rename = 74; + PrepareRenameResponse prepare_rename_response = 75; + PerformRename perform_rename = 76; + PerformRenameResponse perform_rename_response = 77; + SearchProject search_project = 78; + SearchProjectResponse search_project_response = 79; + + GetChannels get_channels = 80; + GetChannelsResponse get_channels_response = 81; + JoinChannel join_channel = 82; + JoinChannelResponse join_channel_response = 83; + LeaveChannel leave_channel = 84; + SendChannelMessage send_channel_message = 85; + SendChannelMessageResponse send_channel_message_response = 86; + ChannelMessageSent channel_message_sent = 87; + GetChannelMessages get_channel_messages = 88; + GetChannelMessagesResponse get_channel_messages_response = 89; + + UpdateContacts update_contacts = 90; + UpdateInviteInfo update_invite_info = 91; + ShowContacts show_contacts = 92; + + GetUsers get_users = 93; + FuzzySearchUsers fuzzy_search_users = 94; + UsersResponse users_response = 95; + RequestContact request_contact = 96; + RespondToContactRequest respond_to_contact_request = 97; + RemoveContact remove_contact = 98; + + Follow follow = 99; + FollowResponse follow_response = 100; + UpdateFollowers update_followers = 101; + Unfollow unfollow = 102; } } @@ -125,6 +133,52 @@ message Test { uint64 id = 1; } +message CreateRoom {} + +message CreateRoomResponse { + uint64 id = 1; +} + +message JoinRoom { + uint64 id = 1; +} + +message JoinRoomResponse { + Room room = 1; +} + +message Room { + repeated Participant participants = 1; +} + +message Participant { + uint64 user_id = 1; + uint32 peer_id = 2; + repeated uint64 project_ids = 3; + ParticipantLocation location = 4; +} + +message ParticipantLocation { + oneof variant { + Project project = 1; + External external = 2; + } + + message Project { + uint64 id = 1; + } + + message External {} +} + +message Call {} + +message CallResponse {} + +message RoomUpdated { + Room room = 1; +} + message RegisterProject { bool online = 1; } diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index 2ba3fa18bacdd7db5972a798ac63dfd8912a0eea..0e3de8878d78278652751fbfc2a216fe10b190a6 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -83,11 +83,12 @@ messages!( (ApplyCompletionAdditionalEditsResponse, Background), (BufferReloaded, Foreground), (BufferSaved, Foreground), - (RemoveContact, Foreground), (ChannelMessageSent, Foreground), (CopyProjectEntry, Foreground), (CreateBufferForPeer, Foreground), (CreateProjectEntry, Foreground), + (CreateRoom, Foreground), + (CreateRoomResponse, Foreground), (DeleteProjectEntry, Foreground), (Error, Foreground), (Follow, Foreground), @@ -122,6 +123,8 @@ messages!( (JoinProject, Foreground), (JoinProjectResponse, Foreground), (JoinProjectRequestCancelled, Foreground), + (JoinRoom, Foreground), + (JoinRoomResponse, Foreground), (LeaveChannel, Foreground), (LeaveProject, Foreground), (OpenBufferById, Background), @@ -136,6 +139,7 @@ messages!( (ProjectEntryResponse, Foreground), (ProjectUnshared, Foreground), (RegisterProjectResponse, Foreground), + (RemoveContact, Foreground), (Ping, Foreground), (RegisterProject, Foreground), (RegisterProjectActivity, Foreground), @@ -177,6 +181,7 @@ request_messages!( ), (CopyProjectEntry, ProjectEntryResponse), (CreateProjectEntry, ProjectEntryResponse), + (CreateRoom, CreateRoomResponse), (DeleteProjectEntry, ProjectEntryResponse), (Follow, FollowResponse), (FormatBuffers, FormatBuffersResponse), @@ -194,6 +199,7 @@ request_messages!( (GetUsers, UsersResponse), (JoinChannel, JoinChannelResponse), (JoinProject, JoinProjectResponse), + (JoinRoom, JoinRoomResponse), (OpenBufferById, OpenBufferResponse), (OpenBufferByPath, OpenBufferResponse), (OpenBufferForSymbol, OpenBufferForSymbolResponse), From 4a9bf8f4fe174bdb97a7834ad3467c350fa80915 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 23 Sep 2022 18:16:57 +0200 Subject: [PATCH 005/112] Introduce call infrastructure Co-Authored-By: Nathan Sobo --- crates/client/src/call.rs | 3 +- crates/client/src/channel.rs | 2 +- crates/client/src/client.rs | 23 ++++++ crates/client/src/user.rs | 97 +++++++++++++++++++------- crates/collab/src/integration_tests.rs | 12 ++-- crates/collab/src/rpc.rs | 63 +++++++++++++++++ crates/collab/src/rpc/store.rs | 39 +++++++++++ crates/project/src/project.rs | 6 +- crates/room/src/room.rs | 24 +++++-- crates/rpc/proto/zed.proto | 20 +++++- crates/rpc/src/proto.rs | 5 ++ 11 files changed, 249 insertions(+), 45 deletions(-) diff --git a/crates/client/src/call.rs b/crates/client/src/call.rs index 2e7bd799f073f588f1ee934692658b978d1328a0..3111a049495ea312c2f37d7b7cdb05a41457d1e2 100644 --- a/crates/client/src/call.rs +++ b/crates/client/src/call.rs @@ -3,6 +3,7 @@ use std::sync::Arc; #[derive(Clone)] pub struct Call { - pub from: Vec>, pub room_id: u64, + pub from: Arc, + pub participants: Vec>, } diff --git a/crates/client/src/channel.rs b/crates/client/src/channel.rs index a88f872d11761f9e1ba461720ecc34c590b1f150..99e4d5fa96363d05d690a7e5e4b735b762a0678a 100644 --- a/crates/client/src/channel.rs +++ b/crates/client/src/channel.rs @@ -530,7 +530,7 @@ impl ChannelMessage { ) -> Result { let sender = user_store .update(cx, |user_store, cx| { - user_store.fetch_user(message.sender_id, cx) + user_store.get_user(message.sender_id, cx) }) .await?; Ok(ChannelMessage { diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 20563272abef74cda64ed76620ad2f6bbf6dfb0e..16f91f0680438e640a80ae4b45eff43e80f05567 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -422,6 +422,29 @@ impl Client { } } + pub fn add_request_handler( + self: &Arc, + model: ModelHandle, + handler: H, + ) -> Subscription + where + M: RequestMessage, + E: Entity, + H: 'static + + Send + + Sync + + Fn(ModelHandle, TypedEnvelope, Arc, AsyncAppContext) -> F, + F: 'static + Future>, + { + self.add_message_handler(model, move |handle, envelope, this, cx| { + Self::respond_to_request( + envelope.receipt(), + handler(handle, envelope, this.clone(), cx), + this, + ) + }) + } + pub fn add_view_message_handler(self: &Arc, handler: H) where M: EntityMessage, diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index 71e8de12e5844b5f2e17759a86b80f5ca1c9b1ce..f025642b21a8518c34908218014c1d6459dfbb48 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -5,7 +5,7 @@ use anyhow::{anyhow, Context, Result}; use collections::{hash_map::Entry, BTreeSet, HashMap, HashSet}; use futures::{channel::mpsc, future, AsyncReadExt, Future, Stream, StreamExt}; use gpui::{AsyncAppContext, Entity, ImageData, ModelContext, ModelHandle, Task}; -use postage::{broadcast, sink::Sink, watch}; +use postage::{sink::Sink, watch}; use rpc::proto::{RequestMessage, UsersResponse}; use std::sync::{Arc, Weak}; use util::TryFutureExt as _; @@ -68,7 +68,7 @@ pub struct UserStore { outgoing_contact_requests: Vec>, pending_contact_requests: HashMap, invite_info: Option, - incoming_calls: broadcast::Sender, + incoming_calls: Vec>, client: Weak, http: Arc, _maintain_contacts: Task<()>, @@ -118,8 +118,8 @@ impl UserStore { client.add_message_handler(cx.handle(), Self::handle_update_contacts), client.add_message_handler(cx.handle(), Self::handle_update_invite_info), client.add_message_handler(cx.handle(), Self::handle_show_contacts), + client.add_request_handler(cx.handle(), Self::handle_incoming_call), ]; - let (incoming_calls, _) = broadcast::channel(32); Self { users: Default::default(), current_user: current_user_rx, @@ -127,7 +127,7 @@ impl UserStore { incoming_contact_requests: Default::default(), outgoing_contact_requests: Default::default(), invite_info: None, - incoming_calls, + incoming_calls: Default::default(), client: Arc::downgrade(&client), update_contacts_tx, http, @@ -148,7 +148,7 @@ impl UserStore { Status::Connected { .. } => { if let Some((this, user_id)) = this.upgrade(&cx).zip(client.user_id()) { let user = this - .update(&mut cx, |this, cx| this.fetch_user(user_id, cx)) + .update(&mut cx, |this, cx| this.get_user(user_id, cx)) .log_err() .await; current_user_tx.send(user).await.ok(); @@ -199,12 +199,41 @@ impl UserStore { Ok(()) } + async fn handle_incoming_call( + this: ModelHandle, + envelope: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result { + let call = Call { + room_id: envelope.payload.room_id, + participants: this + .update(&mut cx, |this, cx| { + this.get_users(envelope.payload.participant_user_ids, cx) + }) + .await?, + from: this + .update(&mut cx, |this, cx| { + this.get_user(envelope.payload.from_user_id, cx) + }) + .await?, + }; + this.update(&mut cx, |this, _| { + this.incoming_calls + .retain(|tx| tx.unbounded_send(call.clone()).is_ok()); + }); + + Ok(proto::Ack {}) + } + pub fn invite_info(&self) -> Option<&InviteInfo> { self.invite_info.as_ref() } - pub fn incoming_calls(&self) -> impl 'static + Stream { - self.incoming_calls.subscribe() + pub fn incoming_calls(&mut self) -> impl 'static + Stream { + let (tx, rx) = mpsc::unbounded(); + self.incoming_calls.push(tx); + rx } async fn handle_update_contacts( @@ -266,9 +295,7 @@ impl UserStore { for request in message.incoming_requests { incoming_requests.push({ let user = this - .update(&mut cx, |this, cx| { - this.fetch_user(request.requester_id, cx) - }) + .update(&mut cx, |this, cx| this.get_user(request.requester_id, cx)) .await?; (user, request.should_notify) }); @@ -277,7 +304,7 @@ impl UserStore { let mut outgoing_requests = Vec::new(); for requested_user_id in message.outgoing_requests { outgoing_requests.push( - this.update(&mut cx, |this, cx| this.fetch_user(requested_user_id, cx)) + this.update(&mut cx, |this, cx| this.get_user(requested_user_id, cx)) .await?, ); } @@ -518,19 +545,37 @@ impl UserStore { pub fn get_users( &mut self, - mut user_ids: Vec, + user_ids: Vec, cx: &mut ModelContext, - ) -> Task> { - user_ids.retain(|id| !self.users.contains_key(id)); - if user_ids.is_empty() { - Task::ready(Ok(())) - } else { - let load = self.load_users(proto::GetUsers { user_ids }, cx); - cx.foreground().spawn(async move { - load.await?; - Ok(()) + ) -> Task>>> { + let mut user_ids_to_fetch = user_ids.clone(); + user_ids_to_fetch.retain(|id| !self.users.contains_key(id)); + + cx.spawn(|this, mut cx| async move { + if !user_ids_to_fetch.is_empty() { + this.update(&mut cx, |this, cx| { + this.load_users( + proto::GetUsers { + user_ids: user_ids_to_fetch, + }, + cx, + ) + }) + .await?; + } + + this.read_with(&cx, |this, _| { + user_ids + .iter() + .map(|user_id| { + this.users + .get(user_id) + .cloned() + .ok_or_else(|| anyhow!("user {} not found", user_id)) + }) + .collect() }) - } + }) } pub fn fuzzy_search_users( @@ -541,7 +586,7 @@ impl UserStore { self.load_users(proto::FuzzySearchUsers { query }, cx) } - pub fn fetch_user( + pub fn get_user( &mut self, user_id: u64, cx: &mut ModelContext, @@ -621,7 +666,7 @@ impl Contact { ) -> Result { let user = user_store .update(cx, |user_store, cx| { - user_store.fetch_user(contact.user_id, cx) + user_store.get_user(contact.user_id, cx) }) .await?; let mut projects = Vec::new(); @@ -630,9 +675,7 @@ impl Contact { for participant_id in project.guests { guests.insert( user_store - .update(cx, |user_store, cx| { - user_store.fetch_user(participant_id, cx) - }) + .update(cx, |user_store, cx| user_store.get_user(participant_id, cx)) .await?, ); } diff --git a/crates/collab/src/integration_tests.rs b/crates/collab/src/integration_tests.rs index cc80f96ad8e245bb313ab9a805be485b4550dab8..741d53801a5cb822379bf2763286c3fd6a6045ba 100644 --- a/crates/collab/src/integration_tests.rs +++ b/crates/collab/src/integration_tests.rs @@ -100,16 +100,16 @@ async fn test_share_project_in_room( let mut incoming_calls_b = client_b .user_store - .read_with(cx_b, |user, _| user.incoming_calls()); - let user_b_joined = room_a.update(cx_a, |room, cx| { - room.invite(client_b.user_id().unwrap(), cx) - }); + .update(cx_b, |user, _| user.incoming_calls()); + room_a + .update(cx_a, |room, cx| room.call(client_b.user_id().unwrap(), cx)) + .await + .unwrap(); let call_b = incoming_calls_b.next().await.unwrap(); let room_b = cx_b .update(|cx| Room::join(call_b.room_id, client_b.clone(), cx)) .await .unwrap(); - user_b_joined.await.unwrap(); } #[gpui::test(iterations = 10)] @@ -512,7 +512,7 @@ async fn test_cancel_join_request( let user_b = client_a .user_store .update(cx_a, |store, cx| { - store.fetch_user(client_b.user_id().unwrap(), cx) + store.get_user(client_b.user_id().unwrap(), cx) }) .await .unwrap(); diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 6434a97de5b8ecd18d529cc5e2794020ad40dbce..8de2d643c6db8090d5f4e456e89505b42e185200 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -152,6 +152,7 @@ impl Server { server .add_request_handler(Server::ping) .add_request_handler(Server::create_room) + .add_request_handler(Server::call) .add_request_handler(Server::register_project) .add_request_handler(Server::unregister_project) .add_request_handler(Server::join_project) @@ -604,6 +605,68 @@ impl Server { Ok(()) } + async fn call( + self: Arc, + request: TypedEnvelope, + response: Response, + ) -> Result<()> { + let to_user_id = UserId::from_proto(request.payload.to_user_id); + let room_id = request.payload.room_id; + let (from_user_id, receiver_ids, room) = + self.store() + .await + .call(room_id, request.sender_id, to_user_id)?; + for participant in &room.participants { + self.peer + .send( + ConnectionId(participant.peer_id), + proto::RoomUpdated { + room: Some(room.clone()), + }, + ) + .trace_err(); + } + + let mut calls = receiver_ids + .into_iter() + .map(|receiver_id| { + self.peer.request( + receiver_id, + proto::IncomingCall { + room_id, + from_user_id: from_user_id.to_proto(), + participant_user_ids: room.participants.iter().map(|p| p.user_id).collect(), + }, + ) + }) + .collect::>(); + + while let Some(call_response) = calls.next().await { + match call_response.as_ref() { + Ok(_) => { + response.send(proto::Ack {})?; + return Ok(()); + } + Err(_) => { + call_response.trace_err(); + } + } + } + + let room = self.store().await.call_failed(room_id, to_user_id)?; + for participant in &room.participants { + self.peer + .send( + ConnectionId(participant.peer_id), + proto::RoomUpdated { + room: Some(room.clone()), + }, + ) + .trace_err(); + } + Err(anyhow!("failed to ring call recipient"))? + } + async fn register_project( self: Arc, request: TypedEnvelope, diff --git a/crates/collab/src/rpc/store.rs b/crates/collab/src/rpc/store.rs index 9ce6931477e82ddf2486a2718937139fd73a6fdf..20057aa8da66bbd93e50c5b7dc66918508b36311 100644 --- a/crates/collab/src/rpc/store.rs +++ b/crates/collab/src/rpc/store.rs @@ -351,6 +351,45 @@ impl Store { Ok(room_id) } + pub fn call( + &mut self, + room_id: RoomId, + from_connection_id: ConnectionId, + to_user_id: UserId, + ) -> Result<(UserId, Vec, proto::Room)> { + let from_user_id = self.user_id_for_connection(from_connection_id)?; + let to_connection_ids = self.connection_ids_for_user(to_user_id).collect::>(); + let room = self + .rooms + .get_mut(&room_id) + .ok_or_else(|| anyhow!("no such room"))?; + anyhow::ensure!( + room.participants + .iter() + .any(|participant| participant.peer_id == from_connection_id.0), + "no such room" + ); + anyhow::ensure!( + room.pending_calls_to_user_ids + .iter() + .all(|user_id| UserId::from_proto(*user_id) != to_user_id), + "cannot call the same user more than once" + ); + room.pending_calls_to_user_ids.push(to_user_id.to_proto()); + + Ok((from_user_id, to_connection_ids, room.clone())) + } + + pub fn call_failed(&mut self, room_id: RoomId, to_user_id: UserId) -> Result { + let room = self + .rooms + .get_mut(&room_id) + .ok_or_else(|| anyhow!("no such room"))?; + room.pending_calls_to_user_ids + .retain(|user_id| UserId::from_proto(*user_id) != to_user_id); + Ok(room.clone()) + } + pub fn register_project( &mut self, host_connection_id: ConnectionId, diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 36d0b4835a3c8f91790e2bb68d3e8505d88a3e39..f6c20ff837c5061de81d0293a413d30686925110 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -4744,7 +4744,7 @@ impl Project { } else { let user_store = this.read_with(&cx, |this, _| this.user_store.clone()); let user = user_store - .update(&mut cx, |store, cx| store.fetch_user(user_id, cx)) + .update(&mut cx, |store, cx| store.get_user(user_id, cx)) .await?; this.update(&mut cx, |_, cx| cx.emit(Event::ContactRequestedJoin(user))); } @@ -4828,7 +4828,7 @@ impl Project { let user = this .update(&mut cx, |this, cx| { this.user_store.update(cx, |user_store, cx| { - user_store.fetch_user(envelope.payload.requester_id, cx) + user_store.get_user(envelope.payload.requester_id, cx) }) }) .await?; @@ -6258,7 +6258,7 @@ impl Collaborator { cx: &mut AsyncAppContext, ) -> impl Future> { let user = user_store.update(cx, |user_store, cx| { - user_store.fetch_user(message.user_id, cx) + user_store.get_user(message.user_id, cx) }); async move { diff --git a/crates/room/src/room.rs b/crates/room/src/room.rs index c444db4316efde57b7f1c397556024026503b0d5..db114452c765733aa29d1b6cc8dbb0a17b083bf0 100644 --- a/crates/room/src/room.rs +++ b/crates/room/src/room.rs @@ -12,6 +12,11 @@ pub enum Event { PeerChangedActiveProject, } +pub enum CallResponse { + Accepted, + Rejected, +} + pub struct Room { id: u64, local_participant: LocalParticipant, @@ -43,7 +48,7 @@ impl Room { let response = client.request(proto::JoinRoom { id }).await?; let room_proto = response.room.ok_or_else(|| anyhow!("invalid room"))?; let room = cx.add_model(|cx| Self::new(id, client, cx)); - room.update(&mut cx, |room, cx| room.refresh(room_proto, cx))?; + room.update(&mut cx, |room, cx| room.apply_update(room_proto, cx))?; Ok(room) }) } @@ -59,7 +64,7 @@ impl Room { } } - fn refresh(&mut self, room: proto::Room, cx: &mut ModelContext) -> Result<()> { + fn apply_update(&mut self, room: proto::Room, cx: &mut ModelContext) -> Result<()> { for participant in room.participants { self.remote_participants.insert( PeerId(participant.peer_id), @@ -70,11 +75,22 @@ impl Room { }, ); } + cx.notify(); Ok(()) } - pub fn invite(&mut self, user_id: u64, cx: &mut ModelContext) -> Task> { - todo!() + pub fn call(&mut self, to_user_id: u64, cx: &mut ModelContext) -> Task> { + let client = self.client.clone(); + let room_id = self.id; + cx.foreground().spawn(async move { + client + .request(proto::Call { + room_id, + to_user_id, + }) + .await?; + Ok(()) + }) } pub async fn publish_project(&mut self, project: ModelHandle) -> Result<()> { diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 3d8f7f9a6e87f9cce4deaca1f1a3cf03e7f48e01..95a8ab2efb728b4907a7ebdbb919993fb46ef550 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -16,7 +16,8 @@ message Envelope { JoinRoom join_room = 10; JoinRoomResponse join_room_response = 11; Call call = 12; - CallResponse call_response = 13; + IncomingCall incoming_call = 1000; + RespondToCall respond_to_call = 13; RoomUpdated room_updated = 14; RegisterProject register_project = 15; @@ -149,6 +150,7 @@ message JoinRoomResponse { message Room { repeated Participant participants = 1; + repeated uint64 pending_calls_to_user_ids = 2; } message Participant { @@ -171,9 +173,21 @@ message ParticipantLocation { message External {} } -message Call {} +message Call { + uint64 room_id = 1; + uint64 to_user_id = 2; +} + +message IncomingCall { + uint64 room_id = 1; + uint64 from_user_id = 2; + repeated uint64 participant_user_ids = 3; +} -message CallResponse {} +message RespondToCall { + uint64 room_id = 1; + bool accept = 2; +} message RoomUpdated { Room room = 1; diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index 0e3de8878d78278652751fbfc2a216fe10b190a6..46247cc46a8035d4d8f2a82fa63dbe94f63d6be5 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -83,6 +83,7 @@ messages!( (ApplyCompletionAdditionalEditsResponse, Background), (BufferReloaded, Foreground), (BufferSaved, Foreground), + (Call, Foreground), (ChannelMessageSent, Foreground), (CopyProjectEntry, Foreground), (CreateBufferForPeer, Foreground), @@ -117,6 +118,7 @@ messages!( (GetProjectSymbols, Background), (GetProjectSymbolsResponse, Background), (GetUsers, Foreground), + (IncomingCall, Foreground), (UsersResponse, Foreground), (JoinChannel, Foreground), (JoinChannelResponse, Foreground), @@ -151,6 +153,7 @@ messages!( (RequestJoinProject, Foreground), (RespondToContactRequest, Foreground), (RespondToJoinProjectRequest, Foreground), + (RoomUpdated, Foreground), (SaveBuffer, Foreground), (SearchProject, Background), (SearchProjectResponse, Background), @@ -179,6 +182,7 @@ request_messages!( ApplyCompletionAdditionalEdits, ApplyCompletionAdditionalEditsResponse ), + (Call, Ack), (CopyProjectEntry, ProjectEntryResponse), (CreateProjectEntry, ProjectEntryResponse), (CreateRoom, CreateRoomResponse), @@ -200,6 +204,7 @@ request_messages!( (JoinChannel, JoinChannelResponse), (JoinProject, JoinProjectResponse), (JoinRoom, JoinRoomResponse), + (IncomingCall, Ack), (OpenBufferById, OpenBufferResponse), (OpenBufferByPath, OpenBufferResponse), (OpenBufferForSymbol, OpenBufferForSymbolResponse), From 55b095cbd39bfef35d7f2ba2f06847c1acfd8f8d Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 26 Sep 2022 10:34:26 +0200 Subject: [PATCH 006/112] Implement joining a room and sending updates after people join/leave --- crates/client/src/user.rs | 31 ++++++---- crates/collab/src/integration_tests.rs | 39 ++++++++++-- crates/collab/src/rpc.rs | 84 +++++++++++++++++--------- crates/collab/src/rpc/store.rs | 60 +++++++++++++++--- crates/room/src/room.rs | 57 ++++++++++++----- crates/rpc/proto/zed.proto | 8 ++- crates/rpc/src/proto.rs | 3 + 7 files changed, 211 insertions(+), 71 deletions(-) diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index f025642b21a8518c34908218014c1d6459dfbb48..0e09c7636a6a127ee06dfce37aec13f5eb8d9338 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -1,9 +1,8 @@ -use crate::call::Call; - use super::{http::HttpClient, proto, Client, Status, TypedEnvelope}; +use crate::call::Call; use anyhow::{anyhow, Context, Result}; use collections::{hash_map::Entry, BTreeSet, HashMap, HashSet}; -use futures::{channel::mpsc, future, AsyncReadExt, Future, Stream, StreamExt}; +use futures::{channel::mpsc, future, AsyncReadExt, Future, StreamExt}; use gpui::{AsyncAppContext, Entity, ImageData, ModelContext, ModelHandle, Task}; use postage::{sink::Sink, watch}; use rpc::proto::{RequestMessage, UsersResponse}; @@ -68,7 +67,7 @@ pub struct UserStore { outgoing_contact_requests: Vec>, pending_contact_requests: HashMap, invite_info: Option, - incoming_calls: Vec>, + incoming_call: (watch::Sender>, watch::Receiver>), client: Weak, http: Arc, _maintain_contacts: Task<()>, @@ -119,6 +118,7 @@ impl UserStore { client.add_message_handler(cx.handle(), Self::handle_update_invite_info), client.add_message_handler(cx.handle(), Self::handle_show_contacts), client.add_request_handler(cx.handle(), Self::handle_incoming_call), + client.add_message_handler(cx.handle(), Self::handle_cancel_call), ]; Self { users: Default::default(), @@ -127,7 +127,7 @@ impl UserStore { incoming_contact_requests: Default::default(), outgoing_contact_requests: Default::default(), invite_info: None, - incoming_calls: Default::default(), + incoming_call: watch::channel(), client: Arc::downgrade(&client), update_contacts_tx, http, @@ -219,21 +219,30 @@ impl UserStore { .await?, }; this.update(&mut cx, |this, _| { - this.incoming_calls - .retain(|tx| tx.unbounded_send(call.clone()).is_ok()); + *this.incoming_call.0.borrow_mut() = Some(call); }); Ok(proto::Ack {}) } + async fn handle_cancel_call( + this: ModelHandle, + _: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result<()> { + this.update(&mut cx, |this, _| { + *this.incoming_call.0.borrow_mut() = None; + }); + Ok(()) + } + pub fn invite_info(&self) -> Option<&InviteInfo> { self.invite_info.as_ref() } - pub fn incoming_calls(&mut self) -> impl 'static + Stream { - let (tx, rx) = mpsc::unbounded(); - self.incoming_calls.push(tx); - rx + pub fn incoming_call(&self) -> watch::Receiver> { + self.incoming_call.1.clone() } async fn handle_update_contacts( diff --git a/crates/collab/src/integration_tests.rs b/crates/collab/src/integration_tests.rs index 741d53801a5cb822379bf2763286c3fd6a6045ba..63a2efa0fb2521787bf59b0cc506af4a2d349e1a 100644 --- a/crates/collab/src/integration_tests.rs +++ b/crates/collab/src/integration_tests.rs @@ -98,18 +98,49 @@ async fn test_share_project_in_room( let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; // room.publish_project(project_a.clone()).await.unwrap(); - let mut incoming_calls_b = client_b + let mut incoming_call_b = client_b .user_store - .update(cx_b, |user, _| user.incoming_calls()); + .update(cx_b, |user, _| user.incoming_call()); room_a .update(cx_a, |room, cx| room.call(client_b.user_id().unwrap(), cx)) .await .unwrap(); - let call_b = incoming_calls_b.next().await.unwrap(); + let call_b = incoming_call_b.next().await.unwrap().unwrap(); let room_b = cx_b - .update(|cx| Room::join(call_b.room_id, client_b.clone(), cx)) + .update(|cx| Room::join(&call_b, client_b.clone(), cx)) .await .unwrap(); + assert!(incoming_call_b.next().await.unwrap().is_none()); + assert_eq!( + remote_participants(&room_a, &client_a, cx_a).await, + vec!["user_b"] + ); + assert_eq!( + remote_participants(&room_b, &client_b, cx_b).await, + vec!["user_a"] + ); + + async fn remote_participants( + room: &ModelHandle, + client: &TestClient, + cx: &mut TestAppContext, + ) -> Vec { + let users = room.update(cx, |room, cx| { + room.remote_participants() + .values() + .map(|participant| { + client + .user_store + .update(cx, |users, cx| users.get_user(participant.user_id, cx)) + }) + .collect::>() + }); + let users = futures::future::try_join_all(users).await.unwrap(); + users + .into_iter() + .map(|user| user.github_login.clone()) + .collect() + } } #[gpui::test(iterations = 10)] diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 8de2d643c6db8090d5f4e456e89505b42e185200..dfaaa8a03da75e4337605287fd58e67f9cf24694 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -152,6 +152,7 @@ impl Server { server .add_request_handler(Server::ping) .add_request_handler(Server::create_room) + .add_request_handler(Server::join_room) .add_request_handler(Server::call) .add_request_handler(Server::register_project) .add_request_handler(Server::unregister_project) @@ -605,6 +606,26 @@ impl Server { Ok(()) } + async fn join_room( + self: Arc, + request: TypedEnvelope, + response: Response, + ) -> Result<()> { + let room_id = request.payload.id; + let mut store = self.store().await; + let (room, recipient_ids) = store.join_room(room_id, request.sender_id)?; + for receiver_id in recipient_ids { + self.peer + .send(receiver_id, proto::CancelCall {}) + .trace_err(); + } + response.send(proto::JoinRoomResponse { + room: Some(room.clone()), + })?; + self.room_updated(room); + Ok(()) + } + async fn call( self: Arc, request: TypedEnvelope, @@ -612,34 +633,29 @@ impl Server { ) -> Result<()> { let to_user_id = UserId::from_proto(request.payload.to_user_id); let room_id = request.payload.room_id; - let (from_user_id, receiver_ids, room) = - self.store() - .await - .call(room_id, request.sender_id, to_user_id)?; - for participant in &room.participants { - self.peer - .send( - ConnectionId(participant.peer_id), - proto::RoomUpdated { - room: Some(room.clone()), - }, - ) - .trace_err(); - } - - let mut calls = receiver_ids - .into_iter() - .map(|receiver_id| { - self.peer.request( - receiver_id, - proto::IncomingCall { - room_id, - from_user_id: from_user_id.to_proto(), - participant_user_ids: room.participants.iter().map(|p| p.user_id).collect(), - }, - ) - }) - .collect::>(); + let mut calls = { + let mut store = self.store().await; + let (from_user_id, recipient_ids, room) = + store.call(room_id, request.sender_id, to_user_id)?; + self.room_updated(room); + recipient_ids + .into_iter() + .map(|recipient_id| { + self.peer.request( + recipient_id, + proto::IncomingCall { + room_id, + from_user_id: from_user_id.to_proto(), + participant_user_ids: room + .participants + .iter() + .map(|p| p.user_id) + .collect(), + }, + ) + }) + .collect::>() + }; while let Some(call_response) = calls.next().await { match call_response.as_ref() { @@ -653,7 +669,16 @@ impl Server { } } - let room = self.store().await.call_failed(room_id, to_user_id)?; + { + let mut store = self.store().await; + let room = store.call_failed(room_id, to_user_id)?; + self.room_updated(&room); + } + + Err(anyhow!("failed to ring call recipient"))? + } + + fn room_updated(&self, room: &proto::Room) { for participant in &room.participants { self.peer .send( @@ -664,7 +689,6 @@ impl Server { ) .trace_err(); } - Err(anyhow!("failed to ring call recipient"))? } async fn register_project( diff --git a/crates/collab/src/rpc/store.rs b/crates/collab/src/rpc/store.rs index 20057aa8da66bbd93e50c5b7dc66918508b36311..6b756918021f9771e08e00ded5f033ae5ad9a9ec 100644 --- a/crates/collab/src/rpc/store.rs +++ b/crates/collab/src/rpc/store.rs @@ -25,7 +25,7 @@ pub struct Store { struct ConnectionState { user_id: UserId, admin: bool, - rooms: BTreeSet, + room: Option, projects: BTreeSet, requested_projects: HashSet, channels: HashSet, @@ -140,7 +140,7 @@ impl Store { ConnectionState { user_id, admin, - rooms: Default::default(), + room: Default::default(), projects: Default::default(), requested_projects: Default::default(), channels: Default::default(), @@ -333,6 +333,11 @@ impl Store { .connections .get_mut(&creator_connection_id) .ok_or_else(|| anyhow!("no such connection"))?; + anyhow::ensure!( + connection.room.is_none(), + "cannot participate in more than one room at once" + ); + let mut room = proto::Room::default(); room.participants.push(proto::Participant { user_id: connection.user_id.to_proto(), @@ -347,16 +352,57 @@ impl Store { let room_id = post_inc(&mut self.next_room_id); self.rooms.insert(room_id, room); - connection.rooms.insert(room_id); + connection.room = Some(room_id); Ok(room_id) } + pub fn join_room( + &mut self, + room_id: u64, + connection_id: ConnectionId, + ) -> Result<(&proto::Room, Vec)> { + let connection = self + .connections + .get_mut(&connection_id) + .ok_or_else(|| anyhow!("no such connection"))?; + anyhow::ensure!( + connection.room.is_none(), + "cannot participate in more than one room at once" + ); + + let user_id = connection.user_id; + let recipient_ids = self.connection_ids_for_user(user_id).collect::>(); + + let room = self + .rooms + .get_mut(&room_id) + .ok_or_else(|| anyhow!("no such room"))?; + anyhow::ensure!( + room.pending_calls_to_user_ids.contains(&user_id.to_proto()), + anyhow!("no such room") + ); + room.pending_calls_to_user_ids + .retain(|pending| *pending != user_id.to_proto()); + room.participants.push(proto::Participant { + user_id: user_id.to_proto(), + peer_id: connection_id.0, + project_ids: Default::default(), + location: Some(proto::ParticipantLocation { + variant: Some(proto::participant_location::Variant::External( + proto::participant_location::External {}, + )), + }), + }); + + Ok((room, recipient_ids)) + } + pub fn call( &mut self, room_id: RoomId, from_connection_id: ConnectionId, to_user_id: UserId, - ) -> Result<(UserId, Vec, proto::Room)> { + ) -> Result<(UserId, Vec, &proto::Room)> { let from_user_id = self.user_id_for_connection(from_connection_id)?; let to_connection_ids = self.connection_ids_for_user(to_user_id).collect::>(); let room = self @@ -377,17 +423,17 @@ impl Store { ); room.pending_calls_to_user_ids.push(to_user_id.to_proto()); - Ok((from_user_id, to_connection_ids, room.clone())) + Ok((from_user_id, to_connection_ids, room)) } - pub fn call_failed(&mut self, room_id: RoomId, to_user_id: UserId) -> Result { + pub fn call_failed(&mut self, room_id: RoomId, to_user_id: UserId) -> Result<&proto::Room> { let room = self .rooms .get_mut(&room_id) .ok_or_else(|| anyhow!("no such room"))?; room.pending_calls_to_user_ids .retain(|user_id| UserId::from_proto(*user_id) != to_user_id); - Ok(room.clone()) + Ok(room) } pub fn register_project( diff --git a/crates/room/src/room.rs b/crates/room/src/room.rs index db114452c765733aa29d1b6cc8dbb0a17b083bf0..78de99497862d408657c4cbb143451e9beb440d8 100644 --- a/crates/room/src/room.rs +++ b/crates/room/src/room.rs @@ -1,9 +1,9 @@ mod participant; use anyhow::{anyhow, Result}; -use client::{proto, Client, PeerId}; +use client::{call::Call, proto, Client, PeerId, TypedEnvelope}; use collections::HashMap; -use gpui::{Entity, ModelContext, ModelHandle, MutableAppContext, Task}; +use gpui::{AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Task}; use participant::{LocalParticipant, ParticipantLocation, RemoteParticipant}; use project::Project; use std::sync::Arc; @@ -22,6 +22,7 @@ pub struct Room { local_participant: LocalParticipant, remote_participants: HashMap, client: Arc, + _subscriptions: Vec, } impl Entity for Room { @@ -40,40 +41,64 @@ impl Room { } pub fn join( - id: u64, + call: &Call, client: Arc, cx: &mut MutableAppContext, ) -> Task>> { + let room_id = call.room_id; cx.spawn(|mut cx| async move { - let response = client.request(proto::JoinRoom { id }).await?; + let response = client.request(proto::JoinRoom { id: room_id }).await?; let room_proto = response.room.ok_or_else(|| anyhow!("invalid room"))?; - let room = cx.add_model(|cx| Self::new(id, client, cx)); - room.update(&mut cx, |room, cx| room.apply_update(room_proto, cx))?; + let room = cx.add_model(|cx| Self::new(room_id, client, cx)); + room.update(&mut cx, |room, cx| room.apply_room_update(room_proto, cx))?; Ok(room) }) } - fn new(id: u64, client: Arc, _: &mut ModelContext) -> Self { + fn new(id: u64, client: Arc, cx: &mut ModelContext) -> Self { Self { id, local_participant: LocalParticipant { projects: Default::default(), }, remote_participants: Default::default(), + _subscriptions: vec![client.add_message_handler(cx.handle(), Self::handle_room_updated)], client, } } - fn apply_update(&mut self, room: proto::Room, cx: &mut ModelContext) -> Result<()> { + pub fn remote_participants(&self) -> &HashMap { + &self.remote_participants + } + + async fn handle_room_updated( + this: ModelHandle, + envelope: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result<()> { + let room = envelope + .payload + .room + .ok_or_else(|| anyhow!("invalid room"))?; + this.update(&mut cx, |this, cx| this.apply_room_update(room, cx))?; + Ok(()) + } + + fn apply_room_update(&mut self, room: proto::Room, cx: &mut ModelContext) -> Result<()> { + // TODO: compute diff instead of clearing participants + self.remote_participants.clear(); for participant in room.participants { - self.remote_participants.insert( - PeerId(participant.peer_id), - RemoteParticipant { - user_id: participant.user_id, - projects: Default::default(), // TODO: populate projects - location: ParticipantLocation::from_proto(participant.location)?, - }, - ); + if Some(participant.user_id) != self.client.user_id() { + self.remote_participants.insert( + PeerId(participant.peer_id), + RemoteParticipant { + user_id: participant.user_id, + projects: Default::default(), // TODO: populate projects + location: ParticipantLocation::from_proto(participant.location)?, + }, + ); + } } cx.notify(); Ok(()) diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 95a8ab2efb728b4907a7ebdbb919993fb46ef550..6c0c929f8253525f013cf4ae34b05f00abf87ccf 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -17,7 +17,8 @@ message Envelope { JoinRoomResponse join_room_response = 11; Call call = 12; IncomingCall incoming_call = 1000; - RespondToCall respond_to_call = 13; + CancelCall cancel_call = 1001; + DeclineCall decline_call = 13; RoomUpdated room_updated = 14; RegisterProject register_project = 15; @@ -184,9 +185,10 @@ message IncomingCall { repeated uint64 participant_user_ids = 3; } -message RespondToCall { +message CancelCall {} + +message DeclineCall { uint64 room_id = 1; - bool accept = 2; } message RoomUpdated { diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index 46247cc46a8035d4d8f2a82fa63dbe94f63d6be5..94690e29e123057809c426b1b21655b1709887ec 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -84,12 +84,14 @@ messages!( (BufferReloaded, Foreground), (BufferSaved, Foreground), (Call, Foreground), + (CancelCall, Foreground), (ChannelMessageSent, Foreground), (CopyProjectEntry, Foreground), (CreateBufferForPeer, Foreground), (CreateProjectEntry, Foreground), (CreateRoom, Foreground), (CreateRoomResponse, Foreground), + (DeclineCall, Foreground), (DeleteProjectEntry, Foreground), (Error, Foreground), (Follow, Foreground), @@ -186,6 +188,7 @@ request_messages!( (CopyProjectEntry, ProjectEntryResponse), (CreateProjectEntry, ProjectEntryResponse), (CreateRoom, CreateRoomResponse), + (DeclineCall, Ack), (DeleteProjectEntry, ProjectEntryResponse), (Follow, FollowResponse), (FormatBuffers, FormatBuffersResponse), From f4697ff4d14300b9a18619b1a113faa2cac2e8c7 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 26 Sep 2022 11:13:34 +0200 Subject: [PATCH 007/112] Prevent the same user from being called more than once --- crates/collab/src/rpc/store.rs | 83 ++++++++++++++++++++++++++-------- 1 file changed, 64 insertions(+), 19 deletions(-) diff --git a/crates/collab/src/rpc/store.rs b/crates/collab/src/rpc/store.rs index 6b756918021f9771e08e00ded5f033ae5ad9a9ec..d19ae122e095a048c66b58893014f4f76d87bce6 100644 --- a/crates/collab/src/rpc/store.rs +++ b/crates/collab/src/rpc/store.rs @@ -13,7 +13,7 @@ pub type RoomId = u64; #[derive(Default, Serialize)] pub struct Store { connections: BTreeMap, - connections_by_user_id: BTreeMap>, + connections_by_user_id: BTreeMap, next_room_id: RoomId, rooms: BTreeMap, projects: BTreeMap, @@ -21,16 +21,27 @@ pub struct Store { channels: BTreeMap, } +#[derive(Default, Serialize)] +struct UserConnectionState { + connection_ids: HashSet, + room: Option, +} + #[derive(Serialize)] struct ConnectionState { user_id: UserId, admin: bool, - room: Option, projects: BTreeSet, requested_projects: HashSet, channels: HashSet, } +#[derive(Copy, Clone, Eq, PartialEq, Serialize)] +enum RoomState { + Joined, + Calling { room_id: RoomId }, +} + #[derive(Serialize)] pub struct Project { pub online: bool, @@ -140,7 +151,6 @@ impl Store { ConnectionState { user_id, admin, - room: Default::default(), projects: Default::default(), requested_projects: Default::default(), channels: Default::default(), @@ -149,6 +159,7 @@ impl Store { self.connections_by_user_id .entry(user_id) .or_default() + .connection_ids .insert(connection_id); } @@ -185,9 +196,9 @@ impl Store { } } - let user_connections = self.connections_by_user_id.get_mut(&user_id).unwrap(); - user_connections.remove(&connection_id); - if user_connections.is_empty() { + let user_connection_state = self.connections_by_user_id.get_mut(&user_id).unwrap(); + user_connection_state.connection_ids.remove(&connection_id); + if user_connection_state.connection_ids.is_empty() { self.connections_by_user_id.remove(&user_id); } @@ -239,6 +250,7 @@ impl Store { self.connections_by_user_id .get(&user_id) .into_iter() + .map(|state| &state.connection_ids) .flatten() .copied() } @@ -248,6 +260,7 @@ impl Store { .connections_by_user_id .get(&user_id) .unwrap_or(&Default::default()) + .connection_ids .is_empty() } @@ -295,9 +308,10 @@ impl Store { } pub fn project_metadata_for_user(&self, user_id: UserId) -> Vec { - let connection_ids = self.connections_by_user_id.get(&user_id); - let project_ids = connection_ids.iter().flat_map(|connection_ids| { - connection_ids + let user_connection_state = self.connections_by_user_id.get(&user_id); + let project_ids = user_connection_state.iter().flat_map(|state| { + state + .connection_ids .iter() .filter_map(|connection_id| self.connections.get(connection_id)) .flat_map(|connection| connection.projects.iter().copied()) @@ -333,8 +347,12 @@ impl Store { .connections .get_mut(&creator_connection_id) .ok_or_else(|| anyhow!("no such connection"))?; + let user_connection_state = self + .connections_by_user_id + .get_mut(&connection.user_id) + .ok_or_else(|| anyhow!("no such connection"))?; anyhow::ensure!( - connection.room.is_none(), + user_connection_state.room.is_none(), "cannot participate in more than one room at once" ); @@ -352,7 +370,7 @@ impl Store { let room_id = post_inc(&mut self.next_room_id); self.rooms.insert(room_id, room); - connection.room = Some(room_id); + user_connection_state.room = Some(RoomState::Joined); Ok(room_id) } @@ -365,14 +383,20 @@ impl Store { .connections .get_mut(&connection_id) .ok_or_else(|| anyhow!("no such connection"))?; + let user_id = connection.user_id; + let recipient_ids = self.connection_ids_for_user(user_id).collect::>(); + + let mut user_connection_state = self + .connections_by_user_id + .get_mut(&user_id) + .ok_or_else(|| anyhow!("no such connection"))?; anyhow::ensure!( - connection.room.is_none(), + user_connection_state + .room + .map_or(true, |room| room == RoomState::Calling { room_id }), "cannot participate in more than one room at once" ); - let user_id = connection.user_id; - let recipient_ids = self.connection_ids_for_user(user_id).collect::>(); - let room = self .rooms .get_mut(&room_id) @@ -393,6 +417,7 @@ impl Store { )), }), }); + user_connection_state.room = Some(RoomState::Joined); Ok((room, recipient_ids)) } @@ -404,7 +429,17 @@ impl Store { to_user_id: UserId, ) -> Result<(UserId, Vec, &proto::Room)> { let from_user_id = self.user_id_for_connection(from_connection_id)?; + let to_connection_ids = self.connection_ids_for_user(to_user_id).collect::>(); + let mut to_user_connection_state = self + .connections_by_user_id + .get_mut(&to_user_id) + .ok_or_else(|| anyhow!("no such connection"))?; + anyhow::ensure!( + to_user_connection_state.room.is_none(), + "recipient is already on another call" + ); + let room = self .rooms .get_mut(&room_id) @@ -422,11 +457,18 @@ impl Store { "cannot call the same user more than once" ); room.pending_calls_to_user_ids.push(to_user_id.to_proto()); + to_user_connection_state.room = Some(RoomState::Calling { room_id }); Ok((from_user_id, to_connection_ids, room)) } pub fn call_failed(&mut self, room_id: RoomId, to_user_id: UserId) -> Result<&proto::Room> { + let mut to_user_connection_state = self + .connections_by_user_id + .get_mut(&to_user_id) + .ok_or_else(|| anyhow!("no such connection"))?; + anyhow::ensure!(to_user_connection_state.room == Some(RoomState::Calling { room_id })); + to_user_connection_state.room = None; let room = self .rooms .get_mut(&room_id) @@ -548,10 +590,12 @@ impl Store { } for requester_user_id in project.join_requests.keys() { - if let Some(requester_connection_ids) = + if let Some(requester_user_connection_state) = self.connections_by_user_id.get_mut(requester_user_id) { - for requester_connection_id in requester_connection_ids.iter() { + for requester_connection_id in + &requester_user_connection_state.connection_ids + { if let Some(requester_connection) = self.connections.get_mut(requester_connection_id) { @@ -907,11 +951,12 @@ impl Store { .connections_by_user_id .get(&connection.user_id) .unwrap() + .connection_ids .contains(connection_id)); } - for (user_id, connection_ids) in &self.connections_by_user_id { - for connection_id in connection_ids { + for (user_id, state) in &self.connections_by_user_id { + for connection_id in &state.connection_ids { assert_eq!( self.connections.get(connection_id).unwrap().user_id, *user_id From bb9ce86a29c300fdcf227dcc35a6d3748371d078 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 26 Sep 2022 11:56:19 +0200 Subject: [PATCH 008/112] Introduce the ability of declining calls --- crates/client/src/user.rs | 11 +++ crates/collab/src/integration_tests.rs | 120 ++++++++++++++++++++++--- crates/collab/src/rpc.rs | 26 ++++-- crates/collab/src/rpc/store.rs | 40 +++++++-- crates/room/src/room.rs | 7 ++ crates/rpc/proto/zed.proto | 6 +- 6 files changed, 181 insertions(+), 29 deletions(-) diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index 0e09c7636a6a127ee06dfce37aec13f5eb8d9338..5be0125ff8ef97a9f226a1629275b464c771fa71 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -245,6 +245,17 @@ impl UserStore { self.incoming_call.1.clone() } + pub fn decline_call(&mut self) -> Result<()> { + let mut incoming_call = self.incoming_call.0.borrow_mut(); + if incoming_call.is_some() { + if let Some(client) = self.client.upgrade() { + client.send(proto::DeclineCall {})?; + } + *incoming_call = None; + } + Ok(()) + } + async fn handle_update_contacts( this: ModelHandle, message: TypedEnvelope, diff --git a/crates/collab/src/integration_tests.rs b/crates/collab/src/integration_tests.rs index 63a2efa0fb2521787bf59b0cc506af4a2d349e1a..550a13a2a9cbc5a21ceb1181a92eb92e6b571eec 100644 --- a/crates/collab/src/integration_tests.rs +++ b/crates/collab/src/integration_tests.rs @@ -66,13 +66,15 @@ async fn test_share_project_in_room( deterministic: Arc, cx_a: &mut TestAppContext, cx_b: &mut TestAppContext, + cx_c: &mut TestAppContext, ) { deterministic.forbid_parking(); let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; + let client_c = server.create_client(cx_c, "user_c").await; server - .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) + .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)]) .await; client_a @@ -95,6 +97,13 @@ async fn test_share_project_in_room( .update(|cx| Room::create(client_a.clone(), cx)) .await .unwrap(); + assert_eq!( + participants(&room_a, &client_a, cx_a).await, + RoomParticipants { + remote: Default::default(), + pending: Default::default() + } + ); let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; // room.publish_project(project_a.clone()).await.unwrap(); @@ -105,27 +114,94 @@ async fn test_share_project_in_room( .update(cx_a, |room, cx| room.call(client_b.user_id().unwrap(), cx)) .await .unwrap(); + assert_eq!( + participants(&room_a, &client_a, cx_a).await, + RoomParticipants { + remote: Default::default(), + pending: vec!["user_b".to_string()] + } + ); + let call_b = incoming_call_b.next().await.unwrap().unwrap(); let room_b = cx_b .update(|cx| Room::join(&call_b, client_b.clone(), cx)) .await .unwrap(); assert!(incoming_call_b.next().await.unwrap().is_none()); + + deterministic.run_until_parked(); assert_eq!( - remote_participants(&room_a, &client_a, cx_a).await, - vec!["user_b"] + participants(&room_a, &client_a, cx_a).await, + RoomParticipants { + remote: vec!["user_b".to_string()], + pending: Default::default() + } ); assert_eq!( - remote_participants(&room_b, &client_b, cx_b).await, - vec!["user_a"] + participants(&room_b, &client_b, cx_b).await, + RoomParticipants { + remote: vec!["user_a".to_string()], + pending: Default::default() + } ); - async fn remote_participants( + let mut incoming_call_c = client_c + .user_store + .update(cx_c, |user, _| user.incoming_call()); + room_a + .update(cx_a, |room, cx| room.call(client_c.user_id().unwrap(), cx)) + .await + .unwrap(); + assert_eq!( + participants(&room_a, &client_a, cx_a).await, + RoomParticipants { + remote: vec!["user_b".to_string()], + pending: vec!["user_c".to_string()] + } + ); + assert_eq!( + participants(&room_b, &client_b, cx_b).await, + RoomParticipants { + remote: vec!["user_a".to_string()], + pending: vec!["user_c".to_string()] + } + ); + let _call_c = incoming_call_c.next().await.unwrap().unwrap(); + + client_c + .user_store + .update(cx_c, |user, _| user.decline_call()) + .unwrap(); + assert!(incoming_call_c.next().await.unwrap().is_none()); + + deterministic.run_until_parked(); + assert_eq!( + participants(&room_a, &client_a, cx_a).await, + RoomParticipants { + remote: vec!["user_b".to_string()], + pending: Default::default() + } + ); + assert_eq!( + participants(&room_b, &client_b, cx_b).await, + RoomParticipants { + remote: vec!["user_a".to_string()], + pending: Default::default() + } + ); + + #[derive(Debug, Eq, PartialEq)] + struct RoomParticipants { + remote: Vec, + pending: Vec, + } + + async fn participants( room: &ModelHandle, client: &TestClient, cx: &mut TestAppContext, - ) -> Vec { - let users = room.update(cx, |room, cx| { + ) -> RoomParticipants { + let remote_users = room.update(cx, |room, cx| { room.remote_participants() .values() .map(|participant| { @@ -135,11 +211,29 @@ async fn test_share_project_in_room( }) .collect::>() }); - let users = futures::future::try_join_all(users).await.unwrap(); - users - .into_iter() - .map(|user| user.github_login.clone()) - .collect() + let remote_users = futures::future::try_join_all(remote_users).await.unwrap(); + let pending_users = room.update(cx, |room, cx| { + room.pending_user_ids() + .iter() + .map(|user_id| { + client + .user_store + .update(cx, |users, cx| users.get_user(*user_id, cx)) + }) + .collect::>() + }); + let pending_users = futures::future::try_join_all(pending_users).await.unwrap(); + + RoomParticipants { + remote: remote_users + .into_iter() + .map(|user| user.github_login.clone()) + .collect(), + pending: pending_users + .into_iter() + .map(|user| user.github_login.clone()) + .collect(), + } } } diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index dfaaa8a03da75e4337605287fd58e67f9cf24694..fb8bbdb85afd2645fcf90356f99329cbba33a0f4 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -154,6 +154,7 @@ impl Server { .add_request_handler(Server::create_room) .add_request_handler(Server::join_room) .add_request_handler(Server::call) + .add_message_handler(Server::decline_call) .add_request_handler(Server::register_project) .add_request_handler(Server::unregister_project) .add_request_handler(Server::join_project) @@ -613,10 +614,10 @@ impl Server { ) -> Result<()> { let room_id = request.payload.id; let mut store = self.store().await; - let (room, recipient_ids) = store.join_room(room_id, request.sender_id)?; - for receiver_id in recipient_ids { + let (room, recipient_connection_ids) = store.join_room(room_id, request.sender_id)?; + for recipient_id in recipient_connection_ids { self.peer - .send(receiver_id, proto::CancelCall {}) + .send(recipient_id, proto::CancelCall {}) .trace_err(); } response.send(proto::JoinRoomResponse { @@ -635,10 +636,10 @@ impl Server { let room_id = request.payload.room_id; let mut calls = { let mut store = self.store().await; - let (from_user_id, recipient_ids, room) = + let (from_user_id, recipient_connection_ids, room) = store.call(room_id, request.sender_id, to_user_id)?; self.room_updated(room); - recipient_ids + recipient_connection_ids .into_iter() .map(|recipient_id| { self.peer.request( @@ -678,6 +679,21 @@ impl Server { Err(anyhow!("failed to ring call recipient"))? } + async fn decline_call( + self: Arc, + message: TypedEnvelope, + ) -> Result<()> { + let mut store = self.store().await; + let (room, recipient_connection_ids) = store.call_declined(message.sender_id)?; + for recipient_id in recipient_connection_ids { + self.peer + .send(recipient_id, proto::CancelCall {}) + .trace_err(); + } + self.room_updated(room); + Ok(()) + } + fn room_updated(&self, room: &proto::Room) { for participant in &room.participants { self.peer diff --git a/crates/collab/src/rpc/store.rs b/crates/collab/src/rpc/store.rs index d19ae122e095a048c66b58893014f4f76d87bce6..fc8576224bd227b9809f046458289c2d1b43eea9 100644 --- a/crates/collab/src/rpc/store.rs +++ b/crates/collab/src/rpc/store.rs @@ -384,7 +384,7 @@ impl Store { .get_mut(&connection_id) .ok_or_else(|| anyhow!("no such connection"))?; let user_id = connection.user_id; - let recipient_ids = self.connection_ids_for_user(user_id).collect::>(); + let recipient_connection_ids = self.connection_ids_for_user(user_id).collect::>(); let mut user_connection_state = self .connections_by_user_id @@ -402,10 +402,10 @@ impl Store { .get_mut(&room_id) .ok_or_else(|| anyhow!("no such room"))?; anyhow::ensure!( - room.pending_calls_to_user_ids.contains(&user_id.to_proto()), + room.pending_user_ids.contains(&user_id.to_proto()), anyhow!("no such room") ); - room.pending_calls_to_user_ids + room.pending_user_ids .retain(|pending| *pending != user_id.to_proto()); room.participants.push(proto::Participant { user_id: user_id.to_proto(), @@ -419,7 +419,7 @@ impl Store { }); user_connection_state.room = Some(RoomState::Joined); - Ok((room, recipient_ids)) + Ok((room, recipient_connection_ids)) } pub fn call( @@ -451,12 +451,12 @@ impl Store { "no such room" ); anyhow::ensure!( - room.pending_calls_to_user_ids + room.pending_user_ids .iter() .all(|user_id| UserId::from_proto(*user_id) != to_user_id), "cannot call the same user more than once" ); - room.pending_calls_to_user_ids.push(to_user_id.to_proto()); + room.pending_user_ids.push(to_user_id.to_proto()); to_user_connection_state.room = Some(RoomState::Calling { room_id }); Ok((from_user_id, to_connection_ids, room)) @@ -473,11 +473,37 @@ impl Store { .rooms .get_mut(&room_id) .ok_or_else(|| anyhow!("no such room"))?; - room.pending_calls_to_user_ids + room.pending_user_ids .retain(|user_id| UserId::from_proto(*user_id) != to_user_id); Ok(room) } + pub fn call_declined( + &mut self, + recipient_connection_id: ConnectionId, + ) -> Result<(&proto::Room, Vec)> { + let recipient_user_id = self.user_id_for_connection(recipient_connection_id)?; + let mut to_user_connection_state = self + .connections_by_user_id + .get_mut(&recipient_user_id) + .ok_or_else(|| anyhow!("no such connection"))?; + if let Some(RoomState::Calling { room_id }) = to_user_connection_state.room { + to_user_connection_state.room = None; + let recipient_connection_ids = self + .connection_ids_for_user(recipient_user_id) + .collect::>(); + let room = self + .rooms + .get_mut(&room_id) + .ok_or_else(|| anyhow!("no such room"))?; + room.pending_user_ids + .retain(|user_id| UserId::from_proto(*user_id) != recipient_user_id); + Ok((room, recipient_connection_ids)) + } else { + Err(anyhow!("user is not being called")) + } + } + pub fn register_project( &mut self, host_connection_id: ConnectionId, diff --git a/crates/room/src/room.rs b/crates/room/src/room.rs index 78de99497862d408657c4cbb143451e9beb440d8..6dddfeda3f8311ff45b6b65d4334b0d33dc3764e 100644 --- a/crates/room/src/room.rs +++ b/crates/room/src/room.rs @@ -21,6 +21,7 @@ pub struct Room { id: u64, local_participant: LocalParticipant, remote_participants: HashMap, + pending_user_ids: Vec, client: Arc, _subscriptions: Vec, } @@ -62,6 +63,7 @@ impl Room { projects: Default::default(), }, remote_participants: Default::default(), + pending_user_ids: Default::default(), _subscriptions: vec![client.add_message_handler(cx.handle(), Self::handle_room_updated)], client, } @@ -71,6 +73,10 @@ impl Room { &self.remote_participants } + pub fn pending_user_ids(&self) -> &[u64] { + &self.pending_user_ids + } + async fn handle_room_updated( this: ModelHandle, envelope: TypedEnvelope, @@ -100,6 +106,7 @@ impl Room { ); } } + self.pending_user_ids = room.pending_user_ids; cx.notify(); Ok(()) } diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 6c0c929f8253525f013cf4ae34b05f00abf87ccf..bcc762283d2ed0742c06893101a437afe2e3f1f9 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -151,7 +151,7 @@ message JoinRoomResponse { message Room { repeated Participant participants = 1; - repeated uint64 pending_calls_to_user_ids = 2; + repeated uint64 pending_user_ids = 2; } message Participant { @@ -187,9 +187,7 @@ message IncomingCall { message CancelCall {} -message DeclineCall { - uint64 room_id = 1; -} +message DeclineCall {} message RoomUpdated { Room room = 1; From df285def59c816fb7966ba05a50744d8aee48789 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 26 Sep 2022 12:02:54 +0200 Subject: [PATCH 009/112] :lipstick: --- crates/collab/src/integration_tests.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/crates/collab/src/integration_tests.rs b/crates/collab/src/integration_tests.rs index 550a13a2a9cbc5a21ceb1181a92eb92e6b571eec..24326181991c96361d1dcc834642b337f36c2a66 100644 --- a/crates/collab/src/integration_tests.rs +++ b/crates/collab/src/integration_tests.rs @@ -74,7 +74,11 @@ async fn test_share_project_in_room( let client_b = server.create_client(cx_b, "user_b").await; let client_c = server.create_client(cx_c, "user_c").await; server - .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)]) + .make_contacts(vec![ + (&client_a, cx_a), + (&client_b, cx_b), + (&client_c, cx_c), + ]) .await; client_a @@ -166,8 +170,8 @@ async fn test_share_project_in_room( pending: vec!["user_c".to_string()] } ); - let _call_c = incoming_call_c.next().await.unwrap().unwrap(); + let _call_c = incoming_call_c.next().await.unwrap().unwrap(); client_c .user_store .update(cx_c, |user, _| user.decline_call()) From 573086eed2fb0ae81703b8a66bce0f4cc5cf81c4 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 26 Sep 2022 12:05:59 +0200 Subject: [PATCH 010/112] Always rely on the server to cancel the incoming call --- crates/client/src/user.rs | 8 ++------ crates/collab/src/integration_tests.rs | 4 ++++ 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index 5be0125ff8ef97a9f226a1629275b464c771fa71..57a403ebadc964e574a194b5b82bbad74f638c99 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -246,12 +246,8 @@ impl UserStore { } pub fn decline_call(&mut self) -> Result<()> { - let mut incoming_call = self.incoming_call.0.borrow_mut(); - if incoming_call.is_some() { - if let Some(client) = self.client.upgrade() { - client.send(proto::DeclineCall {})?; - } - *incoming_call = None; + if let Some(client) = self.client.upgrade() { + client.send(proto::DeclineCall {})?; } Ok(()) } diff --git a/crates/collab/src/integration_tests.rs b/crates/collab/src/integration_tests.rs index 24326181991c96361d1dcc834642b337f36c2a66..9372bf5b2996dd04b4aca6a39bf4236e0cf64f7c 100644 --- a/crates/collab/src/integration_tests.rs +++ b/crates/collab/src/integration_tests.rs @@ -118,6 +118,8 @@ async fn test_share_project_in_room( .update(cx_a, |room, cx| room.call(client_b.user_id().unwrap(), cx)) .await .unwrap(); + + deterministic.run_until_parked(); assert_eq!( participants(&room_a, &client_a, cx_a).await, RoomParticipants { @@ -156,6 +158,8 @@ async fn test_share_project_in_room( .update(cx_a, |room, cx| room.call(client_c.user_id().unwrap(), cx)) .await .unwrap(); + + deterministic.run_until_parked(); assert_eq!( participants(&room_a, &client_a, cx_a).await, RoomParticipants { From e55e7e48443687fea0afe701855f46527d06e33b Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 26 Sep 2022 14:27:52 +0200 Subject: [PATCH 011/112] Leave room when `Room` entity is dropped --- crates/collab/src/integration_tests.rs | 21 +++++++++++-- crates/collab/src/rpc.rs | 9 ++++++ crates/collab/src/rpc/store.rs | 41 +++++++++++++++++++++++--- crates/room/src/room.rs | 32 ++++++++++++-------- crates/rpc/proto/zed.proto | 5 ++++ crates/rpc/src/proto.rs | 1 + 6 files changed, 89 insertions(+), 20 deletions(-) diff --git a/crates/collab/src/integration_tests.rs b/crates/collab/src/integration_tests.rs index 9372bf5b2996dd04b4aca6a39bf4236e0cf64f7c..d7a8d2bcfe2027a046dfdb8520f75d1dbd77852e 100644 --- a/crates/collab/src/integration_tests.rs +++ b/crates/collab/src/integration_tests.rs @@ -62,7 +62,7 @@ fn init_logger() { } #[gpui::test(iterations = 10)] -async fn test_share_project_in_room( +async fn test_basic_calls( deterministic: Arc, cx_a: &mut TestAppContext, cx_b: &mut TestAppContext, @@ -111,6 +111,7 @@ async fn test_share_project_in_room( let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; // room.publish_project(project_a.clone()).await.unwrap(); + // Call user B from client A. let mut incoming_call_b = client_b .user_store .update(cx_b, |user, _| user.incoming_call()); @@ -128,6 +129,7 @@ async fn test_share_project_in_room( } ); + // User B receives the call and joins the room. let call_b = incoming_call_b.next().await.unwrap().unwrap(); let room_b = cx_b .update(|cx| Room::join(&call_b, client_b.clone(), cx)) @@ -151,11 +153,12 @@ async fn test_share_project_in_room( } ); + // Call user C from client B. let mut incoming_call_c = client_c .user_store .update(cx_c, |user, _| user.incoming_call()); - room_a - .update(cx_a, |room, cx| room.call(client_c.user_id().unwrap(), cx)) + room_b + .update(cx_b, |room, cx| room.call(client_c.user_id().unwrap(), cx)) .await .unwrap(); @@ -175,6 +178,7 @@ async fn test_share_project_in_room( } ); + // User C receives the call, but declines it. let _call_c = incoming_call_c.next().await.unwrap().unwrap(); client_c .user_store @@ -198,6 +202,17 @@ async fn test_share_project_in_room( } ); + // User A leaves the room. + cx_a.update(|_| drop(room_a)); + deterministic.run_until_parked(); + assert_eq!( + participants(&room_b, &client_b, cx_b).await, + RoomParticipants { + remote: Default::default(), + pending: Default::default() + } + ); + #[derive(Debug, Eq, PartialEq)] struct RoomParticipants { remote: Vec, diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index fb8bbdb85afd2645fcf90356f99329cbba33a0f4..4b366387e4ebfab0e102260186acee882a66948e 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -153,6 +153,7 @@ impl Server { .add_request_handler(Server::ping) .add_request_handler(Server::create_room) .add_request_handler(Server::join_room) + .add_message_handler(Server::leave_room) .add_request_handler(Server::call) .add_message_handler(Server::decline_call) .add_request_handler(Server::register_project) @@ -627,6 +628,14 @@ impl Server { Ok(()) } + async fn leave_room(self: Arc, message: TypedEnvelope) -> Result<()> { + let room_id = message.payload.id; + let mut store = self.store().await; + let room = store.leave_room(room_id, message.sender_id)?; + self.room_updated(room); + Ok(()) + } + async fn call( self: Arc, request: TypedEnvelope, diff --git a/crates/collab/src/rpc/store.rs b/crates/collab/src/rpc/store.rs index fc8576224bd227b9809f046458289c2d1b43eea9..ba4e644f5ce05e2433065170280391c10b897bb1 100644 --- a/crates/collab/src/rpc/store.rs +++ b/crates/collab/src/rpc/store.rs @@ -38,7 +38,7 @@ struct ConnectionState { #[derive(Copy, Clone, Eq, PartialEq, Serialize)] enum RoomState { - Joined, + Joined { room_id: RoomId }, Calling { room_id: RoomId }, } @@ -370,13 +370,13 @@ impl Store { let room_id = post_inc(&mut self.next_room_id); self.rooms.insert(room_id, room); - user_connection_state.room = Some(RoomState::Joined); + user_connection_state.room = Some(RoomState::Joined { room_id }); Ok(room_id) } pub fn join_room( &mut self, - room_id: u64, + room_id: RoomId, connection_id: ConnectionId, ) -> Result<(&proto::Room, Vec)> { let connection = self @@ -417,11 +417,44 @@ impl Store { )), }), }); - user_connection_state.room = Some(RoomState::Joined); + user_connection_state.room = Some(RoomState::Joined { room_id }); Ok((room, recipient_connection_ids)) } + pub fn leave_room( + &mut self, + room_id: RoomId, + connection_id: ConnectionId, + ) -> Result<&proto::Room> { + let connection = self + .connections + .get_mut(&connection_id) + .ok_or_else(|| anyhow!("no such connection"))?; + let user_id = connection.user_id; + + let mut user_connection_state = self + .connections_by_user_id + .get_mut(&user_id) + .ok_or_else(|| anyhow!("no such connection"))?; + anyhow::ensure!( + user_connection_state + .room + .map_or(false, |room| room == RoomState::Joined { room_id }), + "cannot leave a room before joining it" + ); + + let room = self + .rooms + .get_mut(&room_id) + .ok_or_else(|| anyhow!("no such room"))?; + room.participants + .retain(|participant| participant.peer_id != connection_id.0); + user_connection_state.room = None; + + Ok(room) + } + pub fn call( &mut self, room_id: RoomId, diff --git a/crates/room/src/room.rs b/crates/room/src/room.rs index 6dddfeda3f8311ff45b6b65d4334b0d33dc3764e..2a9318f1d7b17c93bec7937dd00f9432914324d7 100644 --- a/crates/room/src/room.rs +++ b/crates/room/src/room.rs @@ -31,6 +31,19 @@ impl Entity for Room { } impl Room { + fn new(id: u64, client: Arc, cx: &mut ModelContext) -> Self { + Self { + id, + local_participant: LocalParticipant { + projects: Default::default(), + }, + remote_participants: Default::default(), + pending_user_ids: Default::default(), + _subscriptions: vec![client.add_message_handler(cx.handle(), Self::handle_room_updated)], + client, + } + } + pub fn create( client: Arc, cx: &mut MutableAppContext, @@ -56,19 +69,6 @@ impl Room { }) } - fn new(id: u64, client: Arc, cx: &mut ModelContext) -> Self { - Self { - id, - local_participant: LocalParticipant { - projects: Default::default(), - }, - remote_participants: Default::default(), - pending_user_ids: Default::default(), - _subscriptions: vec![client.add_message_handler(cx.handle(), Self::handle_room_updated)], - client, - } - } - pub fn remote_participants(&self) -> &HashMap { &self.remote_participants } @@ -148,3 +148,9 @@ impl Room { todo!() } } + +impl Drop for Room { + fn drop(&mut self) { + let _ = self.client.send(proto::LeaveRoom { id: self.id }); + } +} diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index bcc762283d2ed0742c06893101a437afe2e3f1f9..47f33a5d259d8ffaf76d339e6f145250b7412b5f 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -15,6 +15,7 @@ message Envelope { CreateRoomResponse create_room_response = 9; JoinRoom join_room = 10; JoinRoomResponse join_room_response = 11; + LeaveRoom leave_room = 1002; Call call = 12; IncomingCall incoming_call = 1000; CancelCall cancel_call = 1001; @@ -149,6 +150,10 @@ message JoinRoomResponse { Room room = 1; } +message LeaveRoom { + uint64 id = 1; +} + message Room { repeated Participant participants = 1; repeated uint64 pending_user_ids = 2; diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index 94690e29e123057809c426b1b21655b1709887ec..814983938c35e412b21a84d9cad18ed449bca86f 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -131,6 +131,7 @@ messages!( (JoinRoomResponse, Foreground), (LeaveChannel, Foreground), (LeaveProject, Foreground), + (LeaveRoom, Foreground), (OpenBufferById, Background), (OpenBufferByPath, Background), (OpenBufferForSymbol, Background), From f0c45cbceb6bd59ddecd75118216a628bab8aa9c Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 26 Sep 2022 14:28:58 +0200 Subject: [PATCH 012/112] Remove projects from basic calls test for now --- crates/collab/src/integration_tests.rs | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/crates/collab/src/integration_tests.rs b/crates/collab/src/integration_tests.rs index d7a8d2bcfe2027a046dfdb8520f75d1dbd77852e..30513172ad57253f1f184e63d98be6b6949bfe42 100644 --- a/crates/collab/src/integration_tests.rs +++ b/crates/collab/src/integration_tests.rs @@ -81,22 +81,6 @@ async fn test_basic_calls( ]) .await; - client_a - .fs - .insert_tree( - "/a", - json!({ - ".gitignore": "ignored-dir", - "a.txt": "a-contents", - "b.txt": "b-contents", - "ignored-dir": { - "c.txt": "", - "d.txt": "", - } - }), - ) - .await; - let room_a = cx_a .update(|cx| Room::create(client_a.clone(), cx)) .await @@ -108,8 +92,6 @@ async fn test_basic_calls( pending: Default::default() } ); - let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; - // room.publish_project(project_a.clone()).await.unwrap(); // Call user B from client A. let mut incoming_call_b = client_b From 6aa0f0b200e339c2229d205fb2db1cb981e1fc6d Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 26 Sep 2022 17:15:53 +0200 Subject: [PATCH 013/112] Leave room automatically on disconnection Co-Authored-By: Nathan Sobo --- Cargo.lock | 1 + crates/collab/src/integration_tests.rs | 177 ++++++++++++++++++------- crates/collab/src/rpc.rs | 7 + crates/collab/src/rpc/store.rs | 147 ++++++++++++-------- crates/room/Cargo.toml | 4 +- crates/room/src/room.rs | 83 ++++++++++-- 6 files changed, 299 insertions(+), 120 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 08e183810d6f6927f8073f2f2eb92617f01dd29c..4738e69852ddf963d07adcfeaa6a6b3a2d4db8dd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4459,6 +4459,7 @@ dependencies = [ "anyhow", "client", "collections", + "futures", "gpui", "project", ] diff --git a/crates/collab/src/integration_tests.rs b/crates/collab/src/integration_tests.rs index 30513172ad57253f1f184e63d98be6b6949bfe42..7834c3da7f84a66b4b51910346a9bf5959452d58 100644 --- a/crates/collab/src/integration_tests.rs +++ b/crates/collab/src/integration_tests.rs @@ -86,7 +86,7 @@ async fn test_basic_calls( .await .unwrap(); assert_eq!( - participants(&room_a, &client_a, cx_a).await, + room_participants(&room_a, &client_a, cx_a).await, RoomParticipants { remote: Default::default(), pending: Default::default() @@ -104,7 +104,7 @@ async fn test_basic_calls( deterministic.run_until_parked(); assert_eq!( - participants(&room_a, &client_a, cx_a).await, + room_participants(&room_a, &client_a, cx_a).await, RoomParticipants { remote: Default::default(), pending: vec!["user_b".to_string()] @@ -121,14 +121,14 @@ async fn test_basic_calls( deterministic.run_until_parked(); assert_eq!( - participants(&room_a, &client_a, cx_a).await, + room_participants(&room_a, &client_a, cx_a).await, RoomParticipants { remote: vec!["user_b".to_string()], pending: Default::default() } ); assert_eq!( - participants(&room_b, &client_b, cx_b).await, + room_participants(&room_b, &client_b, cx_b).await, RoomParticipants { remote: vec!["user_a".to_string()], pending: Default::default() @@ -146,14 +146,14 @@ async fn test_basic_calls( deterministic.run_until_parked(); assert_eq!( - participants(&room_a, &client_a, cx_a).await, + room_participants(&room_a, &client_a, cx_a).await, RoomParticipants { remote: vec!["user_b".to_string()], pending: vec!["user_c".to_string()] } ); assert_eq!( - participants(&room_b, &client_b, cx_b).await, + room_participants(&room_b, &client_b, cx_b).await, RoomParticipants { remote: vec!["user_a".to_string()], pending: vec!["user_c".to_string()] @@ -170,14 +170,14 @@ async fn test_basic_calls( deterministic.run_until_parked(); assert_eq!( - participants(&room_a, &client_a, cx_a).await, + room_participants(&room_a, &client_a, cx_a).await, RoomParticipants { remote: vec!["user_b".to_string()], pending: Default::default() } ); assert_eq!( - participants(&room_b, &client_b, cx_b).await, + room_participants(&room_b, &client_b, cx_b).await, RoomParticipants { remote: vec!["user_a".to_string()], pending: Default::default() @@ -185,61 +185,90 @@ async fn test_basic_calls( ); // User A leaves the room. - cx_a.update(|_| drop(room_a)); + room_a.update(cx_a, |room, cx| room.leave(cx)).unwrap(); deterministic.run_until_parked(); assert_eq!( - participants(&room_b, &client_b, cx_b).await, + room_participants(&room_a, &client_a, cx_a).await, RoomParticipants { remote: Default::default(), pending: Default::default() } ); + assert_eq!( + room_participants(&room_b, &client_b, cx_b).await, + RoomParticipants { + remote: Default::default(), + pending: Default::default() + } + ); +} - #[derive(Debug, Eq, PartialEq)] - struct RoomParticipants { - remote: Vec, - pending: Vec, - } +#[gpui::test(iterations = 10)] +async fn test_leaving_room_on_disconnection( + deterministic: Arc, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, +) { + deterministic.forbid_parking(); + let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + server + .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) + .await; - async fn participants( - room: &ModelHandle, - client: &TestClient, - cx: &mut TestAppContext, - ) -> RoomParticipants { - let remote_users = room.update(cx, |room, cx| { - room.remote_participants() - .values() - .map(|participant| { - client - .user_store - .update(cx, |users, cx| users.get_user(participant.user_id, cx)) - }) - .collect::>() - }); - let remote_users = futures::future::try_join_all(remote_users).await.unwrap(); - let pending_users = room.update(cx, |room, cx| { - room.pending_user_ids() - .iter() - .map(|user_id| { - client - .user_store - .update(cx, |users, cx| users.get_user(*user_id, cx)) - }) - .collect::>() - }); - let pending_users = futures::future::try_join_all(pending_users).await.unwrap(); + let room_a = cx_a + .update(|cx| Room::create(client_a.clone(), cx)) + .await + .unwrap(); + + // Call user B from client A. + let mut incoming_call_b = client_b + .user_store + .update(cx_b, |user, _| user.incoming_call()); + room_a + .update(cx_a, |room, cx| room.call(client_b.user_id().unwrap(), cx)) + .await + .unwrap(); + + // User B receives the call and joins the room. + let call_b = incoming_call_b.next().await.unwrap().unwrap(); + let room_b = cx_b + .update(|cx| Room::join(&call_b, client_b.clone(), cx)) + .await + .unwrap(); + deterministic.run_until_parked(); + assert_eq!( + room_participants(&room_a, &client_a, cx_a).await, + RoomParticipants { + remote: vec!["user_b".to_string()], + pending: Default::default() + } + ); + assert_eq!( + room_participants(&room_b, &client_b, cx_b).await, + RoomParticipants { + remote: vec!["user_a".to_string()], + pending: Default::default() + } + ); + server.disconnect_client(client_a.current_user_id(cx_a)); + cx_a.foreground().advance_clock(rpc::RECEIVE_TIMEOUT); + assert_eq!( + room_participants(&room_a, &client_a, cx_a).await, RoomParticipants { - remote: remote_users - .into_iter() - .map(|user| user.github_login.clone()) - .collect(), - pending: pending_users - .into_iter() - .map(|user| user.github_login.clone()) - .collect(), + remote: Default::default(), + pending: Default::default() } - } + ); + assert_eq!( + room_participants(&room_b, &client_b, cx_b).await, + RoomParticipants { + remote: Default::default(), + pending: Default::default() + } + ); } #[gpui::test(iterations = 10)] @@ -6169,3 +6198,49 @@ fn channel_messages(channel: &Channel) -> Vec<(String, String, bool)> { }) .collect() } + +#[derive(Debug, Eq, PartialEq)] +struct RoomParticipants { + remote: Vec, + pending: Vec, +} + +async fn room_participants( + room: &ModelHandle, + client: &TestClient, + cx: &mut TestAppContext, +) -> RoomParticipants { + let remote_users = room.update(cx, |room, cx| { + room.remote_participants() + .values() + .map(|participant| { + client + .user_store + .update(cx, |users, cx| users.get_user(participant.user_id, cx)) + }) + .collect::>() + }); + let remote_users = futures::future::try_join_all(remote_users).await.unwrap(); + let pending_users = room.update(cx, |room, cx| { + room.pending_user_ids() + .iter() + .map(|user_id| { + client + .user_store + .update(cx, |users, cx| users.get_user(*user_id, cx)) + }) + .collect::>() + }); + let pending_users = futures::future::try_join_all(pending_users).await.unwrap(); + + RoomParticipants { + remote: remote_users + .into_iter() + .map(|user| user.github_login.clone()) + .collect(), + pending: pending_users + .into_iter() + .map(|user| user.github_login.clone()) + .collect(), + } +} diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 4b366387e4ebfab0e102260186acee882a66948e..04eaad4edb948959d80e64a991a33e552d738117 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -528,6 +528,13 @@ impl Server { } } + if let Some(room) = removed_connection + .room_id + .and_then(|room_id| store.room(room_id)) + { + self.room_updated(room); + } + removed_user_id = removed_connection.user_id; }; diff --git a/crates/collab/src/rpc/store.rs b/crates/collab/src/rpc/store.rs index ba4e644f5ce05e2433065170280391c10b897bb1..f55da1763b1b91ee1d4ca8f10059b5eb2c8c8fad 100644 --- a/crates/collab/src/rpc/store.rs +++ b/crates/collab/src/rpc/store.rs @@ -13,7 +13,7 @@ pub type RoomId = u64; #[derive(Default, Serialize)] pub struct Store { connections: BTreeMap, - connections_by_user_id: BTreeMap, + connected_users: BTreeMap, next_room_id: RoomId, rooms: BTreeMap, projects: BTreeMap, @@ -22,9 +22,9 @@ pub struct Store { } #[derive(Default, Serialize)] -struct UserConnectionState { +struct ConnectedUser { connection_ids: HashSet, - room: Option, + active_call: Option, } #[derive(Serialize)] @@ -37,9 +37,9 @@ struct ConnectionState { } #[derive(Copy, Clone, Eq, PartialEq, Serialize)] -enum RoomState { - Joined { room_id: RoomId }, - Calling { room_id: RoomId }, +struct CallState { + room_id: RoomId, + joined: bool, } #[derive(Serialize)] @@ -89,6 +89,7 @@ pub struct RemovedConnectionState { pub hosted_projects: HashMap, pub guest_project_ids: HashSet, pub contact_ids: HashSet, + pub room_id: Option, } pub struct LeftProject { @@ -156,7 +157,7 @@ impl Store { channels: Default::default(), }, ); - self.connections_by_user_id + self.connected_users .entry(user_id) .or_default() .connection_ids @@ -196,10 +197,32 @@ impl Store { } } - let user_connection_state = self.connections_by_user_id.get_mut(&user_id).unwrap(); - user_connection_state.connection_ids.remove(&connection_id); - if user_connection_state.connection_ids.is_empty() { - self.connections_by_user_id.remove(&user_id); + let connected_user = self.connected_users.get_mut(&user_id).unwrap(); + connected_user.connection_ids.remove(&connection_id); + if let Some(active_call) = connected_user.active_call.as_ref() { + if let Some(room) = self.rooms.get_mut(&active_call.room_id) { + let prev_participant_count = room.participants.len(); + room.participants + .retain(|participant| participant.peer_id != connection_id.0); + if prev_participant_count == room.participants.len() { + if connected_user.connection_ids.is_empty() { + room.pending_user_ids + .retain(|pending_user_id| *pending_user_id != user_id.to_proto()); + result.room_id = Some(active_call.room_id); + connected_user.active_call = None; + } + } else { + result.room_id = Some(active_call.room_id); + connected_user.active_call = None; + } + } else { + tracing::error!("disconnected user claims to be in a room that does not exist"); + connected_user.active_call = None; + } + } + + if connected_user.connection_ids.is_empty() { + self.connected_users.remove(&user_id); } self.connections.remove(&connection_id).unwrap(); @@ -247,7 +270,7 @@ impl Store { &self, user_id: UserId, ) -> impl Iterator + '_ { - self.connections_by_user_id + self.connected_users .get(&user_id) .into_iter() .map(|state| &state.connection_ids) @@ -257,7 +280,7 @@ impl Store { pub fn is_user_online(&self, user_id: UserId) -> bool { !self - .connections_by_user_id + .connected_users .get(&user_id) .unwrap_or(&Default::default()) .connection_ids @@ -308,7 +331,7 @@ impl Store { } pub fn project_metadata_for_user(&self, user_id: UserId) -> Vec { - let user_connection_state = self.connections_by_user_id.get(&user_id); + let user_connection_state = self.connected_users.get(&user_id); let project_ids = user_connection_state.iter().flat_map(|state| { state .connection_ids @@ -347,13 +370,13 @@ impl Store { .connections .get_mut(&creator_connection_id) .ok_or_else(|| anyhow!("no such connection"))?; - let user_connection_state = self - .connections_by_user_id + let connected_user = self + .connected_users .get_mut(&connection.user_id) .ok_or_else(|| anyhow!("no such connection"))?; anyhow::ensure!( - user_connection_state.room.is_none(), - "cannot participate in more than one room at once" + connected_user.active_call.is_none(), + "can't create a room with an active call" ); let mut room = proto::Room::default(); @@ -370,7 +393,10 @@ impl Store { let room_id = post_inc(&mut self.next_room_id); self.rooms.insert(room_id, room); - user_connection_state.room = Some(RoomState::Joined { room_id }); + connected_user.active_call = Some(CallState { + room_id, + joined: true, + }); Ok(room_id) } @@ -386,15 +412,15 @@ impl Store { let user_id = connection.user_id; let recipient_connection_ids = self.connection_ids_for_user(user_id).collect::>(); - let mut user_connection_state = self - .connections_by_user_id + let mut connected_user = self + .connected_users .get_mut(&user_id) .ok_or_else(|| anyhow!("no such connection"))?; anyhow::ensure!( - user_connection_state - .room - .map_or(true, |room| room == RoomState::Calling { room_id }), - "cannot participate in more than one room at once" + connected_user + .active_call + .map_or(false, |call| call.room_id == room_id && !call.joined), + "not being called on this room" ); let room = self @@ -417,7 +443,10 @@ impl Store { )), }), }); - user_connection_state.room = Some(RoomState::Joined { room_id }); + connected_user.active_call = Some(CallState { + room_id, + joined: true, + }); Ok((room, recipient_connection_ids)) } @@ -433,14 +462,14 @@ impl Store { .ok_or_else(|| anyhow!("no such connection"))?; let user_id = connection.user_id; - let mut user_connection_state = self - .connections_by_user_id + let mut connected_user = self + .connected_users .get_mut(&user_id) .ok_or_else(|| anyhow!("no such connection"))?; anyhow::ensure!( - user_connection_state - .room - .map_or(false, |room| room == RoomState::Joined { room_id }), + connected_user + .active_call + .map_or(false, |call| call.room_id == room_id && call.joined), "cannot leave a room before joining it" ); @@ -450,26 +479,32 @@ impl Store { .ok_or_else(|| anyhow!("no such room"))?; room.participants .retain(|participant| participant.peer_id != connection_id.0); - user_connection_state.room = None; + connected_user.active_call = None; Ok(room) } + pub fn room(&self, room_id: RoomId) -> Option<&proto::Room> { + self.rooms.get(&room_id) + } + pub fn call( &mut self, room_id: RoomId, from_connection_id: ConnectionId, - to_user_id: UserId, + recipient_id: UserId, ) -> Result<(UserId, Vec, &proto::Room)> { let from_user_id = self.user_id_for_connection(from_connection_id)?; - let to_connection_ids = self.connection_ids_for_user(to_user_id).collect::>(); - let mut to_user_connection_state = self - .connections_by_user_id - .get_mut(&to_user_id) + let recipient_connection_ids = self + .connection_ids_for_user(recipient_id) + .collect::>(); + let mut recipient = self + .connected_users + .get_mut(&recipient_id) .ok_or_else(|| anyhow!("no such connection"))?; anyhow::ensure!( - to_user_connection_state.room.is_none(), + recipient.active_call.is_none(), "recipient is already on another call" ); @@ -486,22 +521,27 @@ impl Store { anyhow::ensure!( room.pending_user_ids .iter() - .all(|user_id| UserId::from_proto(*user_id) != to_user_id), + .all(|user_id| UserId::from_proto(*user_id) != recipient_id), "cannot call the same user more than once" ); - room.pending_user_ids.push(to_user_id.to_proto()); - to_user_connection_state.room = Some(RoomState::Calling { room_id }); + room.pending_user_ids.push(recipient_id.to_proto()); + recipient.active_call = Some(CallState { + room_id, + joined: false, + }); - Ok((from_user_id, to_connection_ids, room)) + Ok((from_user_id, recipient_connection_ids, room)) } pub fn call_failed(&mut self, room_id: RoomId, to_user_id: UserId) -> Result<&proto::Room> { - let mut to_user_connection_state = self - .connections_by_user_id + let mut recipient = self + .connected_users .get_mut(&to_user_id) .ok_or_else(|| anyhow!("no such connection"))?; - anyhow::ensure!(to_user_connection_state.room == Some(RoomState::Calling { room_id })); - to_user_connection_state.room = None; + anyhow::ensure!(recipient + .active_call + .map_or(false, |call| call.room_id == room_id && !call.joined)); + recipient.active_call = None; let room = self .rooms .get_mut(&room_id) @@ -516,18 +556,17 @@ impl Store { recipient_connection_id: ConnectionId, ) -> Result<(&proto::Room, Vec)> { let recipient_user_id = self.user_id_for_connection(recipient_connection_id)?; - let mut to_user_connection_state = self - .connections_by_user_id + let recipient = self + .connected_users .get_mut(&recipient_user_id) .ok_or_else(|| anyhow!("no such connection"))?; - if let Some(RoomState::Calling { room_id }) = to_user_connection_state.room { - to_user_connection_state.room = None; + if let Some(active_call) = recipient.active_call.take() { let recipient_connection_ids = self .connection_ids_for_user(recipient_user_id) .collect::>(); let room = self .rooms - .get_mut(&room_id) + .get_mut(&active_call.room_id) .ok_or_else(|| anyhow!("no such room"))?; room.pending_user_ids .retain(|user_id| UserId::from_proto(*user_id) != recipient_user_id); @@ -650,7 +689,7 @@ impl Store { for requester_user_id in project.join_requests.keys() { if let Some(requester_user_connection_state) = - self.connections_by_user_id.get_mut(requester_user_id) + self.connected_users.get_mut(requester_user_id) { for requester_connection_id in &requester_user_connection_state.connection_ids @@ -1007,14 +1046,14 @@ impl Store { assert!(channel.connection_ids.contains(connection_id)); } assert!(self - .connections_by_user_id + .connected_users .get(&connection.user_id) .unwrap() .connection_ids .contains(connection_id)); } - for (user_id, state) in &self.connections_by_user_id { + for (user_id, state) in &self.connected_users { for connection_id in &state.connection_ids { assert_eq!( self.connections.get(connection_id).unwrap().user_id, diff --git a/crates/room/Cargo.toml b/crates/room/Cargo.toml index f329d5ae878e9662898f11276dc84efe66578e97..33b6620b2758f7cdc3fccb329c0bcf8585c5d0fd 100644 --- a/crates/room/Cargo.toml +++ b/crates/room/Cargo.toml @@ -16,12 +16,14 @@ test-support = [ ] [dependencies] -anyhow = "1.0.38" client = { path = "../client" } collections = { path = "../collections" } gpui = { path = "../gpui" } project = { path = "../project" } +anyhow = "1.0.38" +futures = "0.3" + [dev-dependencies] client = { path = "../client", features = ["test-support"] } collections = { path = "../collections", features = ["test-support"] } diff --git a/crates/room/src/room.rs b/crates/room/src/room.rs index 2a9318f1d7b17c93bec7937dd00f9432914324d7..8d80b475085cf55189197d27320379a9d8ca801f 100644 --- a/crates/room/src/room.rs +++ b/crates/room/src/room.rs @@ -3,6 +3,7 @@ mod participant; use anyhow::{anyhow, Result}; use client::{call::Call, proto, Client, PeerId, TypedEnvelope}; use collections::HashMap; +use futures::StreamExt; use gpui::{AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Task}; use participant::{LocalParticipant, ParticipantLocation, RemoteParticipant}; use project::Project; @@ -12,13 +13,9 @@ pub enum Event { PeerChangedActiveProject, } -pub enum CallResponse { - Accepted, - Rejected, -} - pub struct Room { id: u64, + status: RoomStatus, local_participant: LocalParticipant, remote_participants: HashMap, pending_user_ids: Vec, @@ -32,8 +29,24 @@ impl Entity for Room { impl Room { fn new(id: u64, client: Arc, cx: &mut ModelContext) -> Self { + let mut client_status = client.status(); + cx.spawn_weak(|this, mut cx| async move { + let is_connected = client_status + .next() + .await + .map_or(false, |s| s.is_connected()); + // Even if we're initially connected, any future change of the status means we momentarily disconnected. + if !is_connected || client_status.next().await.is_some() { + if let Some(this) = this.upgrade(&cx) { + let _ = this.update(&mut cx, |this, cx| this.leave(cx)); + } + } + }) + .detach(); + Self { id, + status: RoomStatus::Online, local_participant: LocalParticipant { projects: Default::default(), }, @@ -69,6 +82,18 @@ impl Room { }) } + pub fn leave(&mut self, cx: &mut ModelContext) -> Result<()> { + if self.status.is_offline() { + return Err(anyhow!("room is offline")); + } + + self.status = RoomStatus::Offline; + self.remote_participants.clear(); + self.client.send(proto::LeaveRoom { id: self.id })?; + cx.notify(); + Ok(()) + } + pub fn remote_participants(&self) -> &HashMap { &self.remote_participants } @@ -112,6 +137,10 @@ impl Room { } pub fn call(&mut self, to_user_id: u64, cx: &mut ModelContext) -> Task> { + if self.status.is_offline() { + return Task::ready(Err(anyhow!("room is offline"))); + } + let client = self.client.clone(); let room_id = self.id; cx.foreground().spawn(async move { @@ -125,32 +154,58 @@ impl Room { }) } - pub async fn publish_project(&mut self, project: ModelHandle) -> Result<()> { + pub fn publish_project(&mut self, project: ModelHandle) -> Task> { + if self.status.is_offline() { + return Task::ready(Err(anyhow!("room is offline"))); + } + todo!() } - pub async fn unpublish_project(&mut self, project: ModelHandle) -> Result<()> { + pub fn unpublish_project(&mut self, project: ModelHandle) -> Task> { + if self.status.is_offline() { + return Task::ready(Err(anyhow!("room is offline"))); + } + todo!() } - pub async fn set_active_project( + pub fn set_active_project( &mut self, project: Option<&ModelHandle>, - ) -> Result<()> { + ) -> Task> { + if self.status.is_offline() { + return Task::ready(Err(anyhow!("room is offline"))); + } + todo!() } - pub async fn mute(&mut self) -> Result<()> { + pub fn mute(&mut self) -> Task> { + if self.status.is_offline() { + return Task::ready(Err(anyhow!("room is offline"))); + } + todo!() } - pub async fn unmute(&mut self) -> Result<()> { + pub fn unmute(&mut self) -> Task> { + if self.status.is_offline() { + return Task::ready(Err(anyhow!("room is offline"))); + } + todo!() } } -impl Drop for Room { - fn drop(&mut self) { - let _ = self.client.send(proto::LeaveRoom { id: self.id }); +#[derive(Copy, Clone, PartialEq, Eq)] +pub enum RoomStatus { + Online, + Offline, +} + +impl RoomStatus { + fn is_offline(&self) -> bool { + matches!(self, RoomStatus::Offline) } } From 80ab144bf3f5d1c45739d76390ca97836020fcc9 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 26 Sep 2022 17:37:58 +0200 Subject: [PATCH 014/112] Ring users upon connection if somebody was calling them before connecting Co-Authored-By: Nathan Sobo --- crates/client/src/user.rs | 2 +- crates/collab/src/integration_tests.rs | 13 ++++- crates/collab/src/rpc.rs | 30 ++++------ crates/collab/src/rpc/store.rs | 80 ++++++++++++++++++-------- crates/room/src/room.rs | 8 ++- crates/rpc/proto/zed.proto | 4 +- 6 files changed, 90 insertions(+), 47 deletions(-) diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index 57a403ebadc964e574a194b5b82bbad74f638c99..0dbb8bb19812f7c6a294dd62c3e5532dded28472 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -214,7 +214,7 @@ impl UserStore { .await?, from: this .update(&mut cx, |this, cx| { - this.get_user(envelope.payload.from_user_id, cx) + this.get_user(envelope.payload.caller_user_id, cx) }) .await?, }; diff --git a/crates/collab/src/integration_tests.rs b/crates/collab/src/integration_tests.rs index 7834c3da7f84a66b4b51910346a9bf5959452d58..c235a31c557f8a07ba73dfb10a536d83ae99a46c 100644 --- a/crates/collab/src/integration_tests.rs +++ b/crates/collab/src/integration_tests.rs @@ -66,6 +66,7 @@ async fn test_basic_calls( deterministic: Arc, cx_a: &mut TestAppContext, cx_b: &mut TestAppContext, + cx_b2: &mut TestAppContext, cx_c: &mut TestAppContext, ) { deterministic.forbid_parking(); @@ -111,8 +112,18 @@ async fn test_basic_calls( } ); - // User B receives the call and joins the room. + // User B receives the call. let call_b = incoming_call_b.next().await.unwrap().unwrap(); + + // User B connects via another client and also receives a ring on the newly-connected client. + let client_b2 = server.create_client(cx_b2, "user_b").await; + let mut incoming_call_b2 = client_b2 + .user_store + .update(cx_b2, |user, _| user.incoming_call()); + deterministic.run_until_parked(); + let _call_b2 = incoming_call_b2.next().await.unwrap().unwrap(); + + // User B joins the room using the first client. let room_b = cx_b .update(|cx| Room::join(&call_b, client_b.clone(), cx)) .await diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 04eaad4edb948959d80e64a991a33e552d738117..55c3414d85672f0bc82626340be76ae40d9ff05d 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -388,7 +388,11 @@ impl Server { { let mut store = this.store().await; - store.add_connection(connection_id, user_id, user.admin); + let incoming_call = store.add_connection(connection_id, user_id, user.admin); + if let Some(incoming_call) = incoming_call { + this.peer.send(connection_id, incoming_call)?; + } + this.peer.send(connection_id, store.build_initial_contacts_update(contacts))?; if let Some((code, count)) = invite_code { @@ -648,28 +652,18 @@ impl Server { request: TypedEnvelope, response: Response, ) -> Result<()> { - let to_user_id = UserId::from_proto(request.payload.to_user_id); + let recipient_user_id = UserId::from_proto(request.payload.recipient_user_id); let room_id = request.payload.room_id; let mut calls = { let mut store = self.store().await; - let (from_user_id, recipient_connection_ids, room) = - store.call(room_id, request.sender_id, to_user_id)?; + let (room, recipient_connection_ids, incoming_call) = + store.call(room_id, request.sender_id, recipient_user_id)?; self.room_updated(room); recipient_connection_ids .into_iter() - .map(|recipient_id| { - self.peer.request( - recipient_id, - proto::IncomingCall { - room_id, - from_user_id: from_user_id.to_proto(), - participant_user_ids: room - .participants - .iter() - .map(|p| p.user_id) - .collect(), - }, - ) + .map(|recipient_connection_id| { + self.peer + .request(recipient_connection_id, incoming_call.clone()) }) .collect::>() }; @@ -688,7 +682,7 @@ impl Server { { let mut store = self.store().await; - let room = store.call_failed(room_id, to_user_id)?; + let room = store.call_failed(room_id, recipient_user_id)?; self.room_updated(&room); } diff --git a/crates/collab/src/rpc/store.rs b/crates/collab/src/rpc/store.rs index f55da1763b1b91ee1d4ca8f10059b5eb2c8c8fad..1c69a7c2f801f73f766d4d418e1febaeab9ce2d4 100644 --- a/crates/collab/src/rpc/store.rs +++ b/crates/collab/src/rpc/store.rs @@ -24,7 +24,7 @@ pub struct Store { #[derive(Default, Serialize)] struct ConnectedUser { connection_ids: HashSet, - active_call: Option, + active_call: Option, } #[derive(Serialize)] @@ -37,9 +37,10 @@ struct ConnectionState { } #[derive(Copy, Clone, Eq, PartialEq, Serialize)] -struct CallState { - room_id: RoomId, - joined: bool, +pub struct Call { + pub caller_user_id: UserId, + pub room_id: RoomId, + pub joined: bool, } #[derive(Serialize)] @@ -146,7 +147,12 @@ impl Store { } #[instrument(skip(self))] - pub fn add_connection(&mut self, connection_id: ConnectionId, user_id: UserId, admin: bool) { + pub fn add_connection( + &mut self, + connection_id: ConnectionId, + user_id: UserId, + admin: bool, + ) -> Option { self.connections.insert( connection_id, ConnectionState { @@ -157,11 +163,26 @@ impl Store { channels: Default::default(), }, ); - self.connected_users - .entry(user_id) - .or_default() - .connection_ids - .insert(connection_id); + let connected_user = self.connected_users.entry(user_id).or_default(); + connected_user.connection_ids.insert(connection_id); + if let Some(active_call) = connected_user.active_call { + if active_call.joined { + None + } else { + let room = self.room(active_call.room_id)?; + Some(proto::IncomingCall { + room_id: active_call.room_id, + caller_user_id: active_call.caller_user_id.to_proto(), + participant_user_ids: room + .participants + .iter() + .map(|participant| participant.user_id) + .collect(), + }) + } + } else { + None + } } #[instrument(skip(self))] @@ -393,7 +414,8 @@ impl Store { let room_id = post_inc(&mut self.next_room_id); self.rooms.insert(room_id, room); - connected_user.active_call = Some(CallState { + connected_user.active_call = Some(Call { + caller_user_id: connection.user_id, room_id, joined: true, }); @@ -412,14 +434,16 @@ impl Store { let user_id = connection.user_id; let recipient_connection_ids = self.connection_ids_for_user(user_id).collect::>(); - let mut connected_user = self + let connected_user = self .connected_users .get_mut(&user_id) .ok_or_else(|| anyhow!("no such connection"))?; + let active_call = connected_user + .active_call + .as_mut() + .ok_or_else(|| anyhow!("not being called"))?; anyhow::ensure!( - connected_user - .active_call - .map_or(false, |call| call.room_id == room_id && !call.joined), + active_call.room_id == room_id && !active_call.joined, "not being called on this room" ); @@ -443,10 +467,7 @@ impl Store { )), }), }); - connected_user.active_call = Some(CallState { - room_id, - joined: true, - }); + active_call.joined = true; Ok((room, recipient_connection_ids)) } @@ -493,8 +514,8 @@ impl Store { room_id: RoomId, from_connection_id: ConnectionId, recipient_id: UserId, - ) -> Result<(UserId, Vec, &proto::Room)> { - let from_user_id = self.user_id_for_connection(from_connection_id)?; + ) -> Result<(&proto::Room, Vec, proto::IncomingCall)> { + let caller_user_id = self.user_id_for_connection(from_connection_id)?; let recipient_connection_ids = self .connection_ids_for_user(recipient_id) @@ -525,12 +546,25 @@ impl Store { "cannot call the same user more than once" ); room.pending_user_ids.push(recipient_id.to_proto()); - recipient.active_call = Some(CallState { + recipient.active_call = Some(Call { + caller_user_id, room_id, joined: false, }); - Ok((from_user_id, recipient_connection_ids, room)) + Ok(( + room, + recipient_connection_ids, + proto::IncomingCall { + room_id, + caller_user_id: caller_user_id.to_proto(), + participant_user_ids: room + .participants + .iter() + .map(|participant| participant.user_id) + .collect(), + }, + )) } pub fn call_failed(&mut self, room_id: RoomId, to_user_id: UserId) -> Result<&proto::Room> { diff --git a/crates/room/src/room.rs b/crates/room/src/room.rs index 8d80b475085cf55189197d27320379a9d8ca801f..c6daacd7e2d3f5da67a8fdbcabd339c2bc6d1661 100644 --- a/crates/room/src/room.rs +++ b/crates/room/src/room.rs @@ -136,7 +136,11 @@ impl Room { Ok(()) } - pub fn call(&mut self, to_user_id: u64, cx: &mut ModelContext) -> Task> { + pub fn call( + &mut self, + recipient_user_id: u64, + cx: &mut ModelContext, + ) -> Task> { if self.status.is_offline() { return Task::ready(Err(anyhow!("room is offline"))); } @@ -147,7 +151,7 @@ impl Room { client .request(proto::Call { room_id, - to_user_id, + recipient_user_id, }) .await?; Ok(()) diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 47f33a5d259d8ffaf76d339e6f145250b7412b5f..1125c2b3ad9dc89c7c6cd8d0f1b7c6dff4f0227c 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -181,12 +181,12 @@ message ParticipantLocation { message Call { uint64 room_id = 1; - uint64 to_user_id = 2; + uint64 recipient_user_id = 2; } message IncomingCall { uint64 room_id = 1; - uint64 from_user_id = 2; + uint64 caller_user_id = 2; repeated uint64 participant_user_ids = 3; } From c8a48e8990f643b735d579c9af42175ac2635556 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 27 Sep 2022 12:17:00 +0200 Subject: [PATCH 015/112] Extract contacts titlebar item into a separate crate This allows us to implement a new contacts popover that uses the `editor` crate. --- Cargo.lock | 24 +- crates/contacts_titlebar_item/Cargo.toml | 48 +++ .../src/contacts_titlebar_item.rs | 304 ++++++++++++++++++ crates/workspace/Cargo.toml | 1 - crates/workspace/src/workspace.rs | 289 ++--------------- crates/zed/Cargo.toml | 1 + crates/zed/src/zed.rs | 7 +- 7 files changed, 408 insertions(+), 266 deletions(-) create mode 100644 crates/contacts_titlebar_item/Cargo.toml create mode 100644 crates/contacts_titlebar_item/src/contacts_titlebar_item.rs diff --git a/Cargo.lock b/Cargo.lock index 4738e69852ddf963d07adcfeaa6a6b3a2d4db8dd..8537c516115a1f88e6f2dcb916f00f14915941fb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1151,6 +1151,28 @@ dependencies = [ "workspace", ] +[[package]] +name = "contacts_titlebar_item" +version = "0.1.0" +dependencies = [ + "anyhow", + "client", + "clock", + "collections", + "editor", + "futures", + "fuzzy", + "gpui", + "log", + "postage", + "project", + "serde", + "settings", + "theme", + "util", + "workspace", +] + [[package]] name = "context_menu" version = "0.1.0" @@ -7084,7 +7106,6 @@ version = "0.1.0" dependencies = [ "anyhow", "client", - "clock", "collections", "context_menu", "drag_and_drop", @@ -7163,6 +7184,7 @@ dependencies = [ "command_palette", "contacts_panel", "contacts_status_item", + "contacts_titlebar_item", "context_menu", "ctor", "diagnostics", diff --git a/crates/contacts_titlebar_item/Cargo.toml b/crates/contacts_titlebar_item/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..771e364218354fc2a427688cc65bf1d4e16ccd9e --- /dev/null +++ b/crates/contacts_titlebar_item/Cargo.toml @@ -0,0 +1,48 @@ +[package] +name = "contacts_titlebar_item" +version = "0.1.0" +edition = "2021" + +[lib] +path = "src/contacts_titlebar_item.rs" +doctest = false + +[features] +test-support = [ + "client/test-support", + "collections/test-support", + "editor/test-support", + "gpui/test-support", + "project/test-support", + "settings/test-support", + "util/test-support", + "workspace/test-support", +] + +[dependencies] +client = { path = "../client" } +clock = { path = "../clock" } +collections = { path = "../collections" } +editor = { path = "../editor" } +fuzzy = { path = "../fuzzy" } +gpui = { path = "../gpui" } +project = { path = "../project" } +settings = { path = "../settings" } +theme = { path = "../theme" } +util = { path = "../util" } +workspace = { path = "../workspace" } +anyhow = "1.0" +futures = "0.3" +log = "0.4" +postage = { version = "0.4.1", features = ["futures-traits"] } +serde = { version = "1.0", features = ["derive", "rc"] } + +[dev-dependencies] +client = { path = "../client", features = ["test-support"] } +collections = { path = "../collections", features = ["test-support"] } +editor = { path = "../editor", features = ["test-support"] } +gpui = { path = "../gpui", features = ["test-support"] } +project = { path = "../project", features = ["test-support"] } +settings = { path = "../settings", features = ["test-support"] } +util = { path = "../util", features = ["test-support"] } +workspace = { path = "../workspace", features = ["test-support"] } diff --git a/crates/contacts_titlebar_item/src/contacts_titlebar_item.rs b/crates/contacts_titlebar_item/src/contacts_titlebar_item.rs new file mode 100644 index 0000000000000000000000000000000000000000..7035585a2fccdf54dfc24010849c4a00975ae1e7 --- /dev/null +++ b/crates/contacts_titlebar_item/src/contacts_titlebar_item.rs @@ -0,0 +1,304 @@ +use client::{Authenticate, PeerId}; +use clock::ReplicaId; +use gpui::{ + color::Color, + elements::*, + geometry::{rect::RectF, vector::vec2f, PathBuilder}, + json::{self, ToJson}, + Border, CursorStyle, Entity, ImageData, MouseButton, RenderContext, Subscription, View, + ViewContext, ViewHandle, WeakViewHandle, +}; +use settings::Settings; +use std::{ops::Range, sync::Arc}; +use theme::Theme; +use workspace::{FollowNextCollaborator, ToggleFollow, Workspace}; + +pub struct ContactsTitlebarItem { + workspace: WeakViewHandle, + _subscriptions: Vec, +} + +impl Entity for ContactsTitlebarItem { + type Event = (); +} + +impl View for ContactsTitlebarItem { + fn ui_name() -> &'static str { + "ContactsTitlebarItem" + } + + fn render(&mut self, cx: &mut RenderContext) -> ElementBox { + let workspace = if let Some(workspace) = self.workspace.upgrade(cx) { + workspace + } else { + return Empty::new().boxed(); + }; + + let theme = cx.global::().theme.clone(); + Flex::row() + .with_children(self.render_collaborators(&workspace, &theme, cx)) + .with_children(self.render_current_user(&workspace, &theme, cx)) + .with_children(self.render_connection_status(&workspace, cx)) + .boxed() + } +} + +impl ContactsTitlebarItem { + pub fn new(workspace: &ViewHandle, cx: &mut ViewContext) -> Self { + let observe_workspace = cx.observe(workspace, |_, _, cx| cx.notify()); + Self { + workspace: workspace.downgrade(), + _subscriptions: vec![observe_workspace], + } + } + + fn render_collaborators( + &self, + workspace: &ViewHandle, + theme: &Theme, + cx: &mut RenderContext, + ) -> Vec { + let mut collaborators = workspace + .read(cx) + .project() + .read(cx) + .collaborators() + .values() + .cloned() + .collect::>(); + collaborators.sort_unstable_by_key(|collaborator| collaborator.replica_id); + collaborators + .into_iter() + .filter_map(|collaborator| { + Some(self.render_avatar( + collaborator.user.avatar.clone()?, + collaborator.replica_id, + Some((collaborator.peer_id, &collaborator.user.github_login)), + workspace, + theme, + cx, + )) + }) + .collect() + } + + fn render_current_user( + &self, + workspace: &ViewHandle, + theme: &Theme, + cx: &mut RenderContext, + ) -> Option { + let user = workspace.read(cx).user_store().read(cx).current_user(); + let replica_id = workspace.read(cx).project().read(cx).replica_id(); + let status = *workspace.read(cx).client().status().borrow(); + if let Some(avatar) = user.and_then(|user| user.avatar.clone()) { + Some(self.render_avatar(avatar, replica_id, None, workspace, theme, cx)) + } else if matches!(status, client::Status::UpgradeRequired) { + None + } else { + Some( + MouseEventHandler::::new(0, cx, |state, _| { + let style = theme + .workspace + .titlebar + .sign_in_prompt + .style_for(state, false); + Label::new("Sign in".to_string(), style.text.clone()) + .contained() + .with_style(style.container) + .boxed() + }) + .on_click(MouseButton::Left, |_, cx| cx.dispatch_action(Authenticate)) + .with_cursor_style(CursorStyle::PointingHand) + .aligned() + .boxed(), + ) + } + } + + fn render_avatar( + &self, + avatar: Arc, + replica_id: ReplicaId, + peer: Option<(PeerId, &str)>, + workspace: &ViewHandle, + theme: &Theme, + cx: &mut RenderContext, + ) -> ElementBox { + let replica_color = theme.editor.replica_selection_style(replica_id).cursor; + let is_followed = peer.map_or(false, |(peer_id, _)| { + workspace.read(cx).is_following(peer_id) + }); + let mut avatar_style = theme.workspace.titlebar.avatar; + if is_followed { + avatar_style.border = Border::all(1.0, replica_color); + } + let content = Stack::new() + .with_child( + Image::new(avatar) + .with_style(avatar_style) + .constrained() + .with_width(theme.workspace.titlebar.avatar_width) + .aligned() + .boxed(), + ) + .with_child( + AvatarRibbon::new(replica_color) + .constrained() + .with_width(theme.workspace.titlebar.avatar_ribbon.width) + .with_height(theme.workspace.titlebar.avatar_ribbon.height) + .aligned() + .bottom() + .boxed(), + ) + .constrained() + .with_width(theme.workspace.titlebar.avatar_width) + .contained() + .with_margin_left(theme.workspace.titlebar.avatar_margin) + .boxed(); + + if let Some((peer_id, peer_github_login)) = peer { + MouseEventHandler::::new(replica_id.into(), cx, move |_, _| content) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, move |_, cx| { + cx.dispatch_action(ToggleFollow(peer_id)) + }) + .with_tooltip::( + peer_id.0 as usize, + if is_followed { + format!("Unfollow {}", peer_github_login) + } else { + format!("Follow {}", peer_github_login) + }, + Some(Box::new(FollowNextCollaborator)), + theme.tooltip.clone(), + cx, + ) + .boxed() + } else { + content + } + } + + fn render_connection_status( + &self, + workspace: &ViewHandle, + cx: &mut RenderContext, + ) -> Option { + let theme = &cx.global::().theme; + match &*workspace.read(cx).client().status().borrow() { + client::Status::ConnectionError + | client::Status::ConnectionLost + | client::Status::Reauthenticating { .. } + | client::Status::Reconnecting { .. } + | client::Status::ReconnectionError { .. } => Some( + Container::new( + Align::new( + ConstrainedBox::new( + Svg::new("icons/cloud_slash_12.svg") + .with_color(theme.workspace.titlebar.offline_icon.color) + .boxed(), + ) + .with_width(theme.workspace.titlebar.offline_icon.width) + .boxed(), + ) + .boxed(), + ) + .with_style(theme.workspace.titlebar.offline_icon.container) + .boxed(), + ), + client::Status::UpgradeRequired => Some( + Label::new( + "Please update Zed to collaborate".to_string(), + theme.workspace.titlebar.outdated_warning.text.clone(), + ) + .contained() + .with_style(theme.workspace.titlebar.outdated_warning.container) + .aligned() + .boxed(), + ), + _ => None, + } + } +} + +pub struct AvatarRibbon { + color: Color, +} + +impl AvatarRibbon { + pub fn new(color: Color) -> AvatarRibbon { + AvatarRibbon { color } + } +} + +impl Element for AvatarRibbon { + type LayoutState = (); + + type PaintState = (); + + fn layout( + &mut self, + constraint: gpui::SizeConstraint, + _: &mut gpui::LayoutContext, + ) -> (gpui::geometry::vector::Vector2F, Self::LayoutState) { + (constraint.max, ()) + } + + fn paint( + &mut self, + bounds: gpui::geometry::rect::RectF, + _: gpui::geometry::rect::RectF, + _: &mut Self::LayoutState, + cx: &mut gpui::PaintContext, + ) -> Self::PaintState { + let mut path = PathBuilder::new(); + path.reset(bounds.lower_left()); + path.curve_to( + bounds.origin() + vec2f(bounds.height(), 0.), + bounds.origin(), + ); + path.line_to(bounds.upper_right() - vec2f(bounds.height(), 0.)); + path.curve_to(bounds.lower_right(), bounds.upper_right()); + path.line_to(bounds.lower_left()); + cx.scene.push_path(path.build(self.color, None)); + } + + fn dispatch_event( + &mut self, + _: &gpui::Event, + _: RectF, + _: RectF, + _: &mut Self::LayoutState, + _: &mut Self::PaintState, + _: &mut gpui::EventContext, + ) -> bool { + false + } + + fn rect_for_text_range( + &self, + _: Range, + _: RectF, + _: RectF, + _: &Self::LayoutState, + _: &Self::PaintState, + _: &gpui::MeasurementContext, + ) -> Option { + None + } + + fn debug( + &self, + bounds: gpui::geometry::rect::RectF, + _: &Self::LayoutState, + _: &Self::PaintState, + _: &gpui::DebugContext, + ) -> gpui::json::Value { + json::json!({ + "type": "AvatarRibbon", + "bounds": bounds.to_json(), + "color": self.color.to_json(), + }) + } +} diff --git a/crates/workspace/Cargo.toml b/crates/workspace/Cargo.toml index c40ce5638947b63bedf05ecd071592539065009a..759bff2cbd7c7e0a5e4184f89db28d72791f0c2c 100644 --- a/crates/workspace/Cargo.toml +++ b/crates/workspace/Cargo.toml @@ -12,7 +12,6 @@ test-support = ["client/test-support", "project/test-support", "settings/test-su [dependencies] client = { path = "../client" } -clock = { path = "../clock" } collections = { path = "../collections" } context_menu = { path = "../context_menu" } drag_and_drop = { path = "../drag_and_drop" } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 017964d9a16a19d5c4590f563f9eab8b26377523..04bbc094b252e77d1b1a474814de4fa97eb4d3ef 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -13,25 +13,19 @@ mod toolbar; mod waiting_room; use anyhow::{anyhow, Context, Result}; -use client::{ - proto, Authenticate, Client, Contact, PeerId, Subscription, TypedEnvelope, User, UserStore, -}; -use clock::ReplicaId; +use client::{proto, Client, Contact, PeerId, Subscription, TypedEnvelope, UserStore}; use collections::{hash_map, HashMap, HashSet}; use dock::{DefaultItemFactory, Dock, ToggleDockButton}; use drag_and_drop::DragAndDrop; use futures::{channel::oneshot, FutureExt}; use gpui::{ actions, - color::Color, elements::*, - geometry::{rect::RectF, vector::vec2f, PathBuilder}, impl_actions, impl_internal_actions, - json::{self, ToJson}, platform::{CursorStyle, WindowOptions}, - AnyModelHandle, AnyViewHandle, AppContext, AsyncAppContext, Border, Entity, ImageData, - ModelContext, ModelHandle, MouseButton, MutableAppContext, PathPromptOptions, PromptLevel, - RenderContext, Task, View, ViewContext, ViewHandle, WeakViewHandle, + AnyModelHandle, AnyViewHandle, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, + MouseButton, MutableAppContext, PathPromptOptions, PromptLevel, RenderContext, Task, View, + ViewContext, ViewHandle, WeakViewHandle, }; use language::LanguageRegistry; use log::{error, warn}; @@ -53,7 +47,6 @@ use std::{ fmt, future::Future, mem, - ops::Range, path::{Path, PathBuf}, rc::Rc, sync::{ @@ -895,6 +888,7 @@ pub struct Workspace { active_pane: ViewHandle, last_active_center_pane: Option>, status_bar: ViewHandle, + titlebar_item: Option, dock: Dock, notifications: Vec<(TypeId, usize, Box)>, project: ModelHandle, @@ -1024,6 +1018,7 @@ impl Workspace { active_pane: center_pane.clone(), last_active_center_pane: Some(center_pane.clone()), status_bar, + titlebar_item: None, notifications: Default::default(), client, remote_entity_subscription: None, @@ -1068,6 +1063,19 @@ impl Workspace { &self.project } + pub fn client(&self) -> &Arc { + &self.client + } + + pub fn set_titlebar_item( + &mut self, + item: impl Into, + cx: &mut ViewContext, + ) { + self.titlebar_item = Some(item.into()); + cx.notify(); + } + /// Call the given callback with a workspace whose project is local. /// /// If the given workspace has a local project, then it will be passed @@ -1968,46 +1976,12 @@ impl Workspace { None } - fn render_connection_status(&self, cx: &mut RenderContext) -> Option { - let theme = &cx.global::().theme; - match &*self.client.status().borrow() { - client::Status::ConnectionError - | client::Status::ConnectionLost - | client::Status::Reauthenticating { .. } - | client::Status::Reconnecting { .. } - | client::Status::ReconnectionError { .. } => Some( - Container::new( - Align::new( - ConstrainedBox::new( - Svg::new("icons/cloud_slash_12.svg") - .with_color(theme.workspace.titlebar.offline_icon.color) - .boxed(), - ) - .with_width(theme.workspace.titlebar.offline_icon.width) - .boxed(), - ) - .boxed(), - ) - .with_style(theme.workspace.titlebar.offline_icon.container) - .boxed(), - ), - client::Status::UpgradeRequired => Some( - Label::new( - "Please update Zed to collaborate".to_string(), - theme.workspace.titlebar.outdated_warning.text.clone(), - ) - .contained() - .with_style(theme.workspace.titlebar.outdated_warning.container) - .aligned() - .boxed(), - ), - _ => None, - } + pub fn is_following(&self, peer_id: PeerId) -> bool { + self.follower_states_by_leader.contains_key(&peer_id) } fn render_titlebar(&self, theme: &Theme, cx: &mut RenderContext) -> ElementBox { let project = &self.project.read(cx); - let replica_id = project.replica_id(); let mut worktree_root_names = String::new(); for (i, name) in project.worktree_root_names(cx).enumerate() { if i > 0 { @@ -2029,7 +2003,7 @@ impl Workspace { enum TitleBar {} ConstrainedBox::new( - MouseEventHandler::::new(0, cx, |_, cx| { + MouseEventHandler::::new(0, cx, |_, _| { Container::new( Stack::new() .with_child( @@ -2038,21 +2012,10 @@ impl Workspace { .left() .boxed(), ) - .with_child( - Align::new( - Flex::row() - .with_children(self.render_collaborators(theme, cx)) - .with_children(self.render_current_user( - self.user_store.read(cx).current_user().as_ref(), - replica_id, - theme, - cx, - )) - .with_children(self.render_connection_status(cx)) - .boxed(), - ) - .right() - .boxed(), + .with_children( + self.titlebar_item + .as_ref() + .map(|item| ChildView::new(item).aligned().right().boxed()), ) .boxed(), ) @@ -2121,125 +2084,6 @@ impl Workspace { } } - fn render_collaborators(&self, theme: &Theme, cx: &mut RenderContext) -> Vec { - let mut collaborators = self - .project - .read(cx) - .collaborators() - .values() - .cloned() - .collect::>(); - collaborators.sort_unstable_by_key(|collaborator| collaborator.replica_id); - collaborators - .into_iter() - .filter_map(|collaborator| { - Some(self.render_avatar( - collaborator.user.avatar.clone()?, - collaborator.replica_id, - Some((collaborator.peer_id, &collaborator.user.github_login)), - theme, - cx, - )) - }) - .collect() - } - - fn render_current_user( - &self, - user: Option<&Arc>, - replica_id: ReplicaId, - theme: &Theme, - cx: &mut RenderContext, - ) -> Option { - let status = *self.client.status().borrow(); - if let Some(avatar) = user.and_then(|user| user.avatar.clone()) { - Some(self.render_avatar(avatar, replica_id, None, theme, cx)) - } else if matches!(status, client::Status::UpgradeRequired) { - None - } else { - Some( - MouseEventHandler::::new(0, cx, |state, _| { - let style = theme - .workspace - .titlebar - .sign_in_prompt - .style_for(state, false); - Label::new("Sign in".to_string(), style.text.clone()) - .contained() - .with_style(style.container) - .boxed() - }) - .on_click(MouseButton::Left, |_, cx| cx.dispatch_action(Authenticate)) - .with_cursor_style(CursorStyle::PointingHand) - .aligned() - .boxed(), - ) - } - } - - fn render_avatar( - &self, - avatar: Arc, - replica_id: ReplicaId, - peer: Option<(PeerId, &str)>, - theme: &Theme, - cx: &mut RenderContext, - ) -> ElementBox { - let replica_color = theme.editor.replica_selection_style(replica_id).cursor; - let is_followed = peer.map_or(false, |(peer_id, _)| { - self.follower_states_by_leader.contains_key(&peer_id) - }); - let mut avatar_style = theme.workspace.titlebar.avatar; - if is_followed { - avatar_style.border = Border::all(1.0, replica_color); - } - let content = Stack::new() - .with_child( - Image::new(avatar) - .with_style(avatar_style) - .constrained() - .with_width(theme.workspace.titlebar.avatar_width) - .aligned() - .boxed(), - ) - .with_child( - AvatarRibbon::new(replica_color) - .constrained() - .with_width(theme.workspace.titlebar.avatar_ribbon.width) - .with_height(theme.workspace.titlebar.avatar_ribbon.height) - .aligned() - .bottom() - .boxed(), - ) - .constrained() - .with_width(theme.workspace.titlebar.avatar_width) - .contained() - .with_margin_left(theme.workspace.titlebar.avatar_margin) - .boxed(); - - if let Some((peer_id, peer_github_login)) = peer { - MouseEventHandler::::new(replica_id.into(), cx, move |_, _| content) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, move |_, cx| { - cx.dispatch_action(ToggleFollow(peer_id)) - }) - .with_tooltip::( - peer_id.0 as usize, - if is_followed { - format!("Unfollow {}", peer_github_login) - } else { - format!("Follow {}", peer_github_login) - }, - Some(Box::new(FollowNextCollaborator)), - theme.tooltip.clone(), - cx, - ) - .boxed() - } else { - content - } - } - fn render_disconnected_overlay(&self, cx: &mut RenderContext) -> Option { if self.project.read(cx).is_read_only() { enum DisconnectedOverlay {} @@ -2714,87 +2558,6 @@ impl WorkspaceHandle for ViewHandle { } } -pub struct AvatarRibbon { - color: Color, -} - -impl AvatarRibbon { - pub fn new(color: Color) -> AvatarRibbon { - AvatarRibbon { color } - } -} - -impl Element for AvatarRibbon { - type LayoutState = (); - - type PaintState = (); - - fn layout( - &mut self, - constraint: gpui::SizeConstraint, - _: &mut gpui::LayoutContext, - ) -> (gpui::geometry::vector::Vector2F, Self::LayoutState) { - (constraint.max, ()) - } - - fn paint( - &mut self, - bounds: gpui::geometry::rect::RectF, - _: gpui::geometry::rect::RectF, - _: &mut Self::LayoutState, - cx: &mut gpui::PaintContext, - ) -> Self::PaintState { - let mut path = PathBuilder::new(); - path.reset(bounds.lower_left()); - path.curve_to( - bounds.origin() + vec2f(bounds.height(), 0.), - bounds.origin(), - ); - path.line_to(bounds.upper_right() - vec2f(bounds.height(), 0.)); - path.curve_to(bounds.lower_right(), bounds.upper_right()); - path.line_to(bounds.lower_left()); - cx.scene.push_path(path.build(self.color, None)); - } - - fn dispatch_event( - &mut self, - _: &gpui::Event, - _: RectF, - _: RectF, - _: &mut Self::LayoutState, - _: &mut Self::PaintState, - _: &mut gpui::EventContext, - ) -> bool { - false - } - - fn rect_for_text_range( - &self, - _: Range, - _: RectF, - _: RectF, - _: &Self::LayoutState, - _: &Self::PaintState, - _: &gpui::MeasurementContext, - ) -> Option { - None - } - - fn debug( - &self, - bounds: gpui::geometry::rect::RectF, - _: &Self::LayoutState, - _: &Self::PaintState, - _: &gpui::DebugContext, - ) -> gpui::json::Value { - json::json!({ - "type": "AvatarRibbon", - "bounds": bounds.to_json(), - "color": self.color.to_json(), - }) - } -} - impl std::fmt::Debug for OpenPaths { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("OpenPaths") diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index dc2b0abd03e59e753f13eb323a372e4c817ad079..170f554814a21cc971cadfb2f1176bcd9d287107 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -27,6 +27,7 @@ context_menu = { path = "../context_menu" } client = { path = "../client" } clock = { path = "../clock" } contacts_panel = { path = "../contacts_panel" } +contacts_titlebar_item = { path = "../contacts_titlebar_item" } contacts_status_item = { path = "../contacts_status_item" } diagnostics = { path = "../diagnostics" } editor = { path = "../editor" } diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index cd906500eef218db07642db27bc5ea84bae22e90..42bcd6b9bd150985cfc8c483b627a66fa006d7ae 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -13,6 +13,7 @@ pub use client; use collections::VecDeque; pub use contacts_panel; use contacts_panel::ContactsPanel; +use contacts_titlebar_item::ContactsTitlebarItem; pub use editor; use editor::{Editor, MultiBuffer}; use gpui::{ @@ -224,7 +225,8 @@ pub fn initialize_workspace( app_state: &Arc, cx: &mut ViewContext, ) { - cx.subscribe(&cx.handle(), { + let workspace_handle = cx.handle(); + cx.subscribe(&workspace_handle, { move |_, _, event, cx| { if let workspace::Event::PaneAdded(pane) = event { pane.update(cx, |pane, cx| { @@ -278,6 +280,9 @@ pub fn initialize_workspace( })); }); + let contacts_titlebar_item = cx.add_view(|cx| ContactsTitlebarItem::new(&workspace_handle, cx)); + workspace.set_titlebar_item(contacts_titlebar_item, cx); + let project_panel = ProjectPanel::new(workspace.project().clone(), cx); let contact_panel = cx.add_view(|cx| { ContactsPanel::new( From 5a3a85b2c860f8d3bdb5daa463021f5f8c567632 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 27 Sep 2022 13:45:11 +0200 Subject: [PATCH 016/112] Introduce a `+` button in the titlebar --- .../src/contacts_titlebar_item.rs | 65 ++++++++++++++++++- crates/room/src/room.rs | 18 +---- crates/theme/src/theme.rs | 1 + crates/zed/src/main.rs | 1 + styles/src/styleTree/workspace.ts | 13 +++- 5 files changed, 78 insertions(+), 20 deletions(-) diff --git a/crates/contacts_titlebar_item/src/contacts_titlebar_item.rs b/crates/contacts_titlebar_item/src/contacts_titlebar_item.rs index 7035585a2fccdf54dfc24010849c4a00975ae1e7..a32b2923af4ea83657c64b2130878165a6f07b91 100644 --- a/crates/contacts_titlebar_item/src/contacts_titlebar_item.rs +++ b/crates/contacts_titlebar_item/src/contacts_titlebar_item.rs @@ -4,15 +4,27 @@ use gpui::{ color::Color, elements::*, geometry::{rect::RectF, vector::vec2f, PathBuilder}, + impl_internal_actions, json::{self, ToJson}, - Border, CursorStyle, Entity, ImageData, MouseButton, RenderContext, Subscription, View, - ViewContext, ViewHandle, WeakViewHandle, + Border, CursorStyle, Entity, ImageData, MouseButton, MutableAppContext, RenderContext, + Subscription, View, ViewContext, ViewHandle, WeakViewHandle, }; use settings::Settings; use std::{ops::Range, sync::Arc}; use theme::Theme; use workspace::{FollowNextCollaborator, ToggleFollow, Workspace}; +impl_internal_actions!(contacts_titlebar_item, [ToggleAddContactsPopover]); + +pub fn init(cx: &mut MutableAppContext) { + cx.add_action(ContactsTitlebarItem::toggle_add_contacts_popover); +} + +#[derive(Clone, PartialEq)] +struct ToggleAddContactsPopover { + button_rect: RectF, +} + pub struct ContactsTitlebarItem { workspace: WeakViewHandle, _subscriptions: Vec, @@ -36,6 +48,7 @@ impl View for ContactsTitlebarItem { let theme = cx.global::().theme.clone(); Flex::row() + .with_children(self.render_toggle_contacts_button(&workspace, &theme, cx)) .with_children(self.render_collaborators(&workspace, &theme, cx)) .with_children(self.render_current_user(&workspace, &theme, cx)) .with_children(self.render_connection_status(&workspace, cx)) @@ -52,6 +65,54 @@ impl ContactsTitlebarItem { } } + fn toggle_add_contacts_popover( + &mut self, + _action: &ToggleAddContactsPopover, + _cx: &mut ViewContext, + ) { + dbg!("!!!!!!!!!"); + } + + fn render_toggle_contacts_button( + &self, + workspace: &ViewHandle, + theme: &Theme, + cx: &mut RenderContext, + ) -> Option { + if !workspace.read(cx).client().status().borrow().is_connected() { + return None; + } + + Some( + MouseEventHandler::::new(0, cx, |state, _| { + let style = theme + .workspace + .titlebar + .add_collaborator_button + .style_for(state, false); + Svg::new("icons/plus_8.svg") + .with_color(style.color) + .constrained() + .with_width(style.icon_width) + .aligned() + .constrained() + .with_width(style.button_width) + .with_height(style.button_width) + .contained() + .with_style(style.container) + .boxed() + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, |event, cx| { + cx.dispatch_action(ToggleAddContactsPopover { + button_rect: event.region, + }); + }) + .aligned() + .boxed(), + ) + } + fn render_collaborators( &self, workspace: &ViewHandle, diff --git a/crates/room/src/room.rs b/crates/room/src/room.rs index c6daacd7e2d3f5da67a8fdbcabd339c2bc6d1661..82363d4b19868ff7ef9e3a9ee08acc9979799391 100644 --- a/crates/room/src/room.rs +++ b/crates/room/src/room.rs @@ -87,10 +87,10 @@ impl Room { return Err(anyhow!("room is offline")); } + cx.notify(); self.status = RoomStatus::Offline; self.remote_participants.clear(); self.client.send(proto::LeaveRoom { id: self.id })?; - cx.notify(); Ok(()) } @@ -184,22 +184,6 @@ impl Room { todo!() } - - pub fn mute(&mut self) -> Task> { - if self.status.is_offline() { - return Task::ready(Err(anyhow!("room is offline"))); - } - - todo!() - } - - pub fn unmute(&mut self) -> Task> { - if self.status.is_offline() { - return Task::ready(Err(anyhow!("room is offline"))); - } - - todo!() - } } #[derive(Copy, Clone, PartialEq, Eq)] diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 739a4c76869b82b9ab066bb32eff8bb58a0cd253..ca952a27fe9682748e0936822ddbdcbda38e27d4 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -74,6 +74,7 @@ pub struct Titlebar { pub avatar: ImageStyle, pub sign_in_prompt: Interactive, pub outdated_warning: ContainedText, + pub add_collaborator_button: Interactive, } #[derive(Clone, Deserialize, Default)] diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 3bfd5e6e1a08791e262eab09ad6ede4771382278..aa84d6475b3ae4497da00c1416158d789871b195 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -108,6 +108,7 @@ fn main() { client::Channel::init(&client); client::init(client.clone(), cx); command_palette::init(cx); + contacts_titlebar_item::init(cx); editor::init(cx); go_to_line::init(cx); file_finder::init(cx); diff --git a/styles/src/styleTree/workspace.ts b/styles/src/styleTree/workspace.ts index 9e81a06d3f092f6e0ae716b3975854ead1d80823..0473c974f7e51e7d392121f25e996b717cfe13b5 100644 --- a/styles/src/styleTree/workspace.ts +++ b/styles/src/styleTree/workspace.ts @@ -16,6 +16,7 @@ export function workspaceBackground(theme: Theme) { export default function workspace(theme: Theme) { const titlebarPadding = 6; + const titlebarHeight = 33; return { background: backgroundColor(theme, 300), @@ -54,7 +55,7 @@ export default function workspace(theme: Theme) { titlebar: { avatarWidth: 18, avatarMargin: 8, - height: 33, + height: titlebarHeight, background: backgroundColor(theme, 100), padding: { left: 80, @@ -118,6 +119,16 @@ export default function workspace(theme: Theme) { }, cornerRadius: 6, }, + addCollaboratorButton: { + cornerRadius: 6, + color: iconColor(theme, "secondary"), + iconWidth: 8, + buttonWidth: 20, + hover: { + background: backgroundColor(theme, "on300", "hovered"), + color: iconColor(theme, "active"), + }, + }, }, toolbar: { height: 34, From 782309f369f366815fca39c886aef3db745ec82e Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 27 Sep 2022 13:51:21 +0200 Subject: [PATCH 017/112] Rename `contacts_titlebar_item` to `collab_titlebar_item` --- Cargo.lock | 46 +++++++++---------- .../Cargo.toml | 4 +- .../src/collab_titlebar_item.rs} | 24 +++++----- crates/zed/Cargo.toml | 2 +- crates/zed/src/main.rs | 2 +- crates/zed/src/zed.rs | 6 +-- 6 files changed, 42 insertions(+), 42 deletions(-) rename crates/{contacts_titlebar_item => collab_titlebar_item}/Cargo.toml (95%) rename crates/{contacts_titlebar_item/src/contacts_titlebar_item.rs => collab_titlebar_item/src/collab_titlebar_item.rs} (95%) diff --git a/Cargo.lock b/Cargo.lock index 8537c516115a1f88e6f2dcb916f00f14915941fb..b895cd669e0ac2ada483b312f2bb4ef5551639ba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1062,6 +1062,28 @@ dependencies = [ "workspace", ] +[[package]] +name = "collab_titlebar_item" +version = "0.1.0" +dependencies = [ + "anyhow", + "client", + "clock", + "collections", + "editor", + "futures", + "fuzzy", + "gpui", + "log", + "postage", + "project", + "serde", + "settings", + "theme", + "util", + "workspace", +] + [[package]] name = "collections" version = "0.1.0" @@ -1151,28 +1173,6 @@ dependencies = [ "workspace", ] -[[package]] -name = "contacts_titlebar_item" -version = "0.1.0" -dependencies = [ - "anyhow", - "client", - "clock", - "collections", - "editor", - "futures", - "fuzzy", - "gpui", - "log", - "postage", - "project", - "serde", - "settings", - "theme", - "util", - "workspace", -] - [[package]] name = "context_menu" version = "0.1.0" @@ -7180,11 +7180,11 @@ dependencies = [ "cli", "client", "clock", + "collab_titlebar_item", "collections", "command_palette", "contacts_panel", "contacts_status_item", - "contacts_titlebar_item", "context_menu", "ctor", "diagnostics", diff --git a/crates/contacts_titlebar_item/Cargo.toml b/crates/collab_titlebar_item/Cargo.toml similarity index 95% rename from crates/contacts_titlebar_item/Cargo.toml rename to crates/collab_titlebar_item/Cargo.toml index 771e364218354fc2a427688cc65bf1d4e16ccd9e..f165753992e71fa2e9003b4eb792fa922e2a441b 100644 --- a/crates/contacts_titlebar_item/Cargo.toml +++ b/crates/collab_titlebar_item/Cargo.toml @@ -1,10 +1,10 @@ [package] -name = "contacts_titlebar_item" +name = "collab_titlebar_item" version = "0.1.0" edition = "2021" [lib] -path = "src/contacts_titlebar_item.rs" +path = "src/collab_titlebar_item.rs" doctest = false [features] diff --git a/crates/contacts_titlebar_item/src/contacts_titlebar_item.rs b/crates/collab_titlebar_item/src/collab_titlebar_item.rs similarity index 95% rename from crates/contacts_titlebar_item/src/contacts_titlebar_item.rs rename to crates/collab_titlebar_item/src/collab_titlebar_item.rs index a32b2923af4ea83657c64b2130878165a6f07b91..c3810992b64787971278cd39e01ad7f1a828c13f 100644 --- a/crates/contacts_titlebar_item/src/contacts_titlebar_item.rs +++ b/crates/collab_titlebar_item/src/collab_titlebar_item.rs @@ -14,29 +14,29 @@ use std::{ops::Range, sync::Arc}; use theme::Theme; use workspace::{FollowNextCollaborator, ToggleFollow, Workspace}; -impl_internal_actions!(contacts_titlebar_item, [ToggleAddContactsPopover]); +impl_internal_actions!(contacts_titlebar_item, [ToggleAddParticipantPopover]); pub fn init(cx: &mut MutableAppContext) { - cx.add_action(ContactsTitlebarItem::toggle_add_contacts_popover); + cx.add_action(CollabTitlebarItem::toggle_add_participant_popover); } #[derive(Clone, PartialEq)] -struct ToggleAddContactsPopover { +struct ToggleAddParticipantPopover { button_rect: RectF, } -pub struct ContactsTitlebarItem { +pub struct CollabTitlebarItem { workspace: WeakViewHandle, _subscriptions: Vec, } -impl Entity for ContactsTitlebarItem { +impl Entity for CollabTitlebarItem { type Event = (); } -impl View for ContactsTitlebarItem { +impl View for CollabTitlebarItem { fn ui_name() -> &'static str { - "ContactsTitlebarItem" + "CollabTitlebarItem" } fn render(&mut self, cx: &mut RenderContext) -> ElementBox { @@ -56,7 +56,7 @@ impl View for ContactsTitlebarItem { } } -impl ContactsTitlebarItem { +impl CollabTitlebarItem { pub fn new(workspace: &ViewHandle, cx: &mut ViewContext) -> Self { let observe_workspace = cx.observe(workspace, |_, _, cx| cx.notify()); Self { @@ -65,9 +65,9 @@ impl ContactsTitlebarItem { } } - fn toggle_add_contacts_popover( + fn toggle_add_participant_popover( &mut self, - _action: &ToggleAddContactsPopover, + _action: &ToggleAddParticipantPopover, _cx: &mut ViewContext, ) { dbg!("!!!!!!!!!"); @@ -84,7 +84,7 @@ impl ContactsTitlebarItem { } Some( - MouseEventHandler::::new(0, cx, |state, _| { + MouseEventHandler::::new(0, cx, |state, _| { let style = theme .workspace .titlebar @@ -104,7 +104,7 @@ impl ContactsTitlebarItem { }) .with_cursor_style(CursorStyle::PointingHand) .on_click(MouseButton::Left, |event, cx| { - cx.dispatch_action(ToggleAddContactsPopover { + cx.dispatch_action(ToggleAddParticipantPopover { button_rect: event.region, }); }) diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 170f554814a21cc971cadfb2f1176bcd9d287107..6c2ce8ff07b9aecdc8e204d0c162761a719d75c5 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -21,13 +21,13 @@ auto_update = { path = "../auto_update" } breadcrumbs = { path = "../breadcrumbs" } chat_panel = { path = "../chat_panel" } cli = { path = "../cli" } +collab_titlebar_item = { path = "../collab_titlebar_item" } collections = { path = "../collections" } command_palette = { path = "../command_palette" } context_menu = { path = "../context_menu" } client = { path = "../client" } clock = { path = "../clock" } contacts_panel = { path = "../contacts_panel" } -contacts_titlebar_item = { path = "../contacts_titlebar_item" } contacts_status_item = { path = "../contacts_status_item" } diagnostics = { path = "../diagnostics" } editor = { path = "../editor" } diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index aa84d6475b3ae4497da00c1416158d789871b195..99983c4d6dc2def796ac4d498f1ba25aeb8d9f91 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -107,8 +107,8 @@ fn main() { project::Project::init(&client); client::Channel::init(&client); client::init(client.clone(), cx); + collab_titlebar_item::init(cx); command_palette::init(cx); - contacts_titlebar_item::init(cx); editor::init(cx); go_to_line::init(cx); file_finder::init(cx); diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 42bcd6b9bd150985cfc8c483b627a66fa006d7ae..26888dc0d71e5ebadbda060f8f7b23a553e276ee 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -10,10 +10,10 @@ use anyhow::{anyhow, Context, Result}; use assets::Assets; use breadcrumbs::Breadcrumbs; pub use client; +use collab_titlebar_item::CollabTitlebarItem; use collections::VecDeque; pub use contacts_panel; use contacts_panel::ContactsPanel; -use contacts_titlebar_item::ContactsTitlebarItem; pub use editor; use editor::{Editor, MultiBuffer}; use gpui::{ @@ -280,8 +280,8 @@ pub fn initialize_workspace( })); }); - let contacts_titlebar_item = cx.add_view(|cx| ContactsTitlebarItem::new(&workspace_handle, cx)); - workspace.set_titlebar_item(contacts_titlebar_item, cx); + let collab_titlebar_item = cx.add_view(|cx| CollabTitlebarItem::new(&workspace_handle, cx)); + workspace.set_titlebar_item(collab_titlebar_item, cx); let project_panel = ProjectPanel::new(workspace.project().clone(), cx); let contact_panel = cx.add_view(|cx| { From 0db6eb2fb8afc395da70385abcdf05e23e4587d3 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 27 Sep 2022 14:27:06 +0200 Subject: [PATCH 018/112] Show add participant popover on click --- .../src/add_participant_popover.rs | 36 ++++++++ .../src/collab_titlebar_item.rs | 89 ++++++++++++------- crates/theme/src/theme.rs | 11 ++- styles/src/styleTree/workspace.ts | 16 +++- 4 files changed, 114 insertions(+), 38 deletions(-) create mode 100644 crates/collab_titlebar_item/src/add_participant_popover.rs diff --git a/crates/collab_titlebar_item/src/add_participant_popover.rs b/crates/collab_titlebar_item/src/add_participant_popover.rs new file mode 100644 index 0000000000000000000000000000000000000000..8c30501629660d8b0a02ffd17c97c3b166be7564 --- /dev/null +++ b/crates/collab_titlebar_item/src/add_participant_popover.rs @@ -0,0 +1,36 @@ +use gpui::{elements::*, Entity, RenderContext, View}; +use settings::Settings; + +pub struct AddParticipantPopover {} + +impl Entity for AddParticipantPopover { + type Event = (); +} + +impl View for AddParticipantPopover { + fn ui_name() -> &'static str { + "AddParticipantPopover" + } + + fn render(&mut self, cx: &mut RenderContext) -> ElementBox { + let theme = &cx + .global::() + .theme + .workspace + .titlebar + .add_participant_popover; + Empty::new() + .contained() + .with_style(theme.container) + .constrained() + .with_width(theme.width) + .with_height(theme.height) + .boxed() + } +} + +impl AddParticipantPopover { + pub fn new() -> Self { + Self {} + } +} diff --git a/crates/collab_titlebar_item/src/collab_titlebar_item.rs b/crates/collab_titlebar_item/src/collab_titlebar_item.rs index c3810992b64787971278cd39e01ad7f1a828c13f..de27fd4eb8a2e55cf34967f9da953c9441c9fbb1 100644 --- a/crates/collab_titlebar_item/src/collab_titlebar_item.rs +++ b/crates/collab_titlebar_item/src/collab_titlebar_item.rs @@ -1,10 +1,13 @@ +mod add_participant_popover; + +use add_participant_popover::AddParticipantPopover; use client::{Authenticate, PeerId}; use clock::ReplicaId; use gpui::{ + actions, color::Color, elements::*, geometry::{rect::RectF, vector::vec2f, PathBuilder}, - impl_internal_actions, json::{self, ToJson}, Border, CursorStyle, Entity, ImageData, MouseButton, MutableAppContext, RenderContext, Subscription, View, ViewContext, ViewHandle, WeakViewHandle, @@ -14,19 +17,15 @@ use std::{ops::Range, sync::Arc}; use theme::Theme; use workspace::{FollowNextCollaborator, ToggleFollow, Workspace}; -impl_internal_actions!(contacts_titlebar_item, [ToggleAddParticipantPopover]); +actions!(contacts_titlebar_item, [ToggleAddParticipantPopover]); pub fn init(cx: &mut MutableAppContext) { cx.add_action(CollabTitlebarItem::toggle_add_participant_popover); } -#[derive(Clone, PartialEq)] -struct ToggleAddParticipantPopover { - button_rect: RectF, -} - pub struct CollabTitlebarItem { workspace: WeakViewHandle, + add_participant_popover: Option>, _subscriptions: Vec, } @@ -61,16 +60,24 @@ impl CollabTitlebarItem { let observe_workspace = cx.observe(workspace, |_, _, cx| cx.notify()); Self { workspace: workspace.downgrade(), + add_participant_popover: None, _subscriptions: vec![observe_workspace], } } fn toggle_add_participant_popover( &mut self, - _action: &ToggleAddParticipantPopover, - _cx: &mut ViewContext, + _: &ToggleAddParticipantPopover, + cx: &mut ViewContext, ) { - dbg!("!!!!!!!!!"); + match self.add_participant_popover.take() { + Some(_) => {} + None => { + let view = cx.add_view(|_| AddParticipantPopover::new()); + self.add_participant_popover = Some(view); + } + } + cx.notify(); } fn render_toggle_contacts_button( @@ -83,33 +90,47 @@ impl CollabTitlebarItem { return None; } + let titlebar = &theme.workspace.titlebar; + Some( - MouseEventHandler::::new(0, cx, |state, _| { - let style = theme - .workspace - .titlebar - .add_collaborator_button - .style_for(state, false); - Svg::new("icons/plus_8.svg") - .with_color(style.color) - .constrained() - .with_width(style.icon_width) + Stack::new() + .with_child( + MouseEventHandler::::new(0, cx, |state, _| { + let style = titlebar.add_participant_button.style_for(state, false); + Svg::new("icons/plus_8.svg") + .with_color(style.color) + .constrained() + .with_width(style.icon_width) + .aligned() + .constrained() + .with_width(style.button_width) + .with_height(style.button_width) + .contained() + .with_style(style.container) + .boxed() + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, |_, cx| { + cx.dispatch_action(ToggleAddParticipantPopover); + }) .aligned() - .constrained() - .with_width(style.button_width) - .with_height(style.button_width) - .contained() - .with_style(style.container) + .boxed(), + ) + .with_children(self.add_participant_popover.as_ref().map(|popover| { + Overlay::new( + ChildView::new(popover) + .contained() + .with_margin_top(titlebar.height) + .with_margin_right( + -titlebar.add_participant_button.default.button_width, + ) + .boxed(), + ) + .with_fit_mode(OverlayFitMode::SwitchAnchor) + .with_anchor_corner(AnchorCorner::BottomLeft) .boxed() - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, |event, cx| { - cx.dispatch_action(ToggleAddParticipantPopover { - button_rect: event.region, - }); - }) - .aligned() - .boxed(), + })) + .boxed(), ) } diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index ca952a27fe9682748e0936822ddbdcbda38e27d4..4446e4e06fec57dc07f3926fb55a30e5d5b8b2ee 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -74,7 +74,16 @@ pub struct Titlebar { pub avatar: ImageStyle, pub sign_in_prompt: Interactive, pub outdated_warning: ContainedText, - pub add_collaborator_button: Interactive, + pub add_participant_button: Interactive, + pub add_participant_popover: AddParticipantPopover, +} + +#[derive(Clone, Deserialize, Default)] +pub struct AddParticipantPopover { + #[serde(flatten)] + pub container: ContainerStyle, + pub height: f32, + pub width: f32, } #[derive(Clone, Deserialize, Default)] diff --git a/styles/src/styleTree/workspace.ts b/styles/src/styleTree/workspace.ts index 0473c974f7e51e7d392121f25e996b717cfe13b5..b10828828b9164b2378cea6fea68002bc0bbd4e4 100644 --- a/styles/src/styleTree/workspace.ts +++ b/styles/src/styleTree/workspace.ts @@ -5,6 +5,7 @@ import { border, iconColor, modalShadow, + popoverShadow, text, } from "./components"; import statusBar from "./statusBar"; @@ -16,7 +17,6 @@ export function workspaceBackground(theme: Theme) { export default function workspace(theme: Theme) { const titlebarPadding = 6; - const titlebarHeight = 33; return { background: backgroundColor(theme, 300), @@ -55,7 +55,7 @@ export default function workspace(theme: Theme) { titlebar: { avatarWidth: 18, avatarMargin: 8, - height: titlebarHeight, + height: 33, background: backgroundColor(theme, 100), padding: { left: 80, @@ -119,7 +119,7 @@ export default function workspace(theme: Theme) { }, cornerRadius: 6, }, - addCollaboratorButton: { + addParticipantButton: { cornerRadius: 6, color: iconColor(theme, "secondary"), iconWidth: 8, @@ -129,6 +129,16 @@ export default function workspace(theme: Theme) { color: iconColor(theme, "active"), }, }, + addParticipantPopover: { + background: backgroundColor(theme, 300, "base"), + cornerRadius: 6, + padding: 6, + shadow: popoverShadow(theme), + border: border(theme, "primary"), + margin: { top: -5 }, + width: 255, + height: 200 + } }, toolbar: { height: 34, From 0a29e13d4ac9ac1351bbb7f5fda7996709fe520a Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 27 Sep 2022 14:34:13 +0200 Subject: [PATCH 019/112] Add active style when participant popover is open --- crates/collab_titlebar_item/src/collab_titlebar_item.rs | 4 +++- styles/src/styleTree/workspace.ts | 4 ++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/crates/collab_titlebar_item/src/collab_titlebar_item.rs b/crates/collab_titlebar_item/src/collab_titlebar_item.rs index de27fd4eb8a2e55cf34967f9da953c9441c9fbb1..a48318d204e916af4c908ef2ae46140f09af4234 100644 --- a/crates/collab_titlebar_item/src/collab_titlebar_item.rs +++ b/crates/collab_titlebar_item/src/collab_titlebar_item.rs @@ -96,7 +96,9 @@ impl CollabTitlebarItem { Stack::new() .with_child( MouseEventHandler::::new(0, cx, |state, _| { - let style = titlebar.add_participant_button.style_for(state, false); + let style = titlebar + .add_participant_button + .style_for(state, self.add_participant_popover.is_some()); Svg::new("icons/plus_8.svg") .with_color(style.color) .constrained() diff --git a/styles/src/styleTree/workspace.ts b/styles/src/styleTree/workspace.ts index b10828828b9164b2378cea6fea68002bc0bbd4e4..75f11b3942ce589045815a678bd83cf8741305dc 100644 --- a/styles/src/styleTree/workspace.ts +++ b/styles/src/styleTree/workspace.ts @@ -124,6 +124,10 @@ export default function workspace(theme: Theme) { color: iconColor(theme, "secondary"), iconWidth: 8, buttonWidth: 20, + active: { + background: backgroundColor(theme, "on300", "active"), + color: iconColor(theme, "active"), + }, hover: { background: backgroundColor(theme, "on300", "hovered"), color: iconColor(theme, "active"), From 4b7323997225e9a9ceb922d965f49cacf3033cb3 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 27 Sep 2022 15:55:02 +0200 Subject: [PATCH 020/112] WIP: Start moving contacts panel into "add participants" popover --- Cargo.lock | 1 + crates/collab_titlebar_item/Cargo.toml | 1 + .../src/add_participant_popover.rs | 704 +++++++++++++++++- .../src/collab_titlebar_item.rs | 20 +- styles/src/styleTree/workspace.ts | 6 +- 5 files changed, 710 insertions(+), 22 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b895cd669e0ac2ada483b312f2bb4ef5551639ba..a8a89478fe3d8d83555637accd12496f12d714ea 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1075,6 +1075,7 @@ dependencies = [ "fuzzy", "gpui", "log", + "menu", "postage", "project", "serde", diff --git a/crates/collab_titlebar_item/Cargo.toml b/crates/collab_titlebar_item/Cargo.toml index f165753992e71fa2e9003b4eb792fa922e2a441b..4f85ddd8d9b76acf072440d57ebc580f533efd93 100644 --- a/crates/collab_titlebar_item/Cargo.toml +++ b/crates/collab_titlebar_item/Cargo.toml @@ -26,6 +26,7 @@ collections = { path = "../collections" } editor = { path = "../editor" } fuzzy = { path = "../fuzzy" } gpui = { path = "../gpui" } +menu = { path = "../menu" } project = { path = "../project" } settings = { path = "../settings" } theme = { path = "../theme" } diff --git a/crates/collab_titlebar_item/src/add_participant_popover.rs b/crates/collab_titlebar_item/src/add_participant_popover.rs index 8c30501629660d8b0a02ffd17c97c3b166be7564..95d37849cbb6d0101887ad57b371e19614f19baf 100644 --- a/crates/collab_titlebar_item/src/add_participant_popover.rs +++ b/crates/collab_titlebar_item/src/add_participant_popover.rs @@ -1,10 +1,577 @@ -use gpui::{elements::*, Entity, RenderContext, View}; +use std::sync::Arc; + +use client::{Contact, User, UserStore}; +use editor::{Cancel, Editor}; +use fuzzy::{match_strings, StringMatchCandidate}; +use gpui::{ + elements::*, impl_internal_actions, keymap, AppContext, ClipboardItem, CursorStyle, Entity, + ModelHandle, MouseButton, MutableAppContext, RenderContext, Subscription, View, ViewContext, + ViewHandle, +}; +use menu::{Confirm, SelectNext, SelectPrev}; use settings::Settings; +use theme::IconButton; + +impl_internal_actions!(contacts_panel, [ToggleExpanded]); + +pub fn init(cx: &mut MutableAppContext) { + cx.add_action(AddParticipantPopover::clear_filter); + cx.add_action(AddParticipantPopover::select_next); + cx.add_action(AddParticipantPopover::select_prev); + cx.add_action(AddParticipantPopover::confirm); + cx.add_action(AddParticipantPopover::toggle_expanded); +} + +#[derive(Clone, PartialEq)] +struct ToggleExpanded(Section); + +#[derive(Clone, Copy, PartialEq, Eq, Debug, PartialOrd, Ord)] +enum Section { + Requests, + Online, + Offline, +} + +#[derive(Clone)] +enum ContactEntry { + Header(Section), + IncomingRequest(Arc), + OutgoingRequest(Arc), + Contact(Arc), +} + +impl PartialEq for ContactEntry { + fn eq(&self, other: &Self) -> bool { + match self { + ContactEntry::Header(section_1) => { + if let ContactEntry::Header(section_2) = other { + return section_1 == section_2; + } + } + ContactEntry::IncomingRequest(user_1) => { + if let ContactEntry::IncomingRequest(user_2) = other { + return user_1.id == user_2.id; + } + } + ContactEntry::OutgoingRequest(user_1) => { + if let ContactEntry::OutgoingRequest(user_2) = other { + return user_1.id == user_2.id; + } + } + ContactEntry::Contact(contact_1) => { + if let ContactEntry::Contact(contact_2) = other { + return contact_1.user.id == contact_2.user.id; + } + } + } + false + } +} + +pub enum Event { + Dismissed, +} + +pub struct AddParticipantPopover { + entries: Vec, + match_candidates: Vec, + list_state: ListState, + user_store: ModelHandle, + filter_editor: ViewHandle, + collapsed_sections: Vec
, + selection: Option, + _maintain_contacts: Subscription, +} + +impl AddParticipantPopover { + pub fn new(user_store: ModelHandle, cx: &mut ViewContext) -> Self { + let filter_editor = cx.add_view(|cx| { + let mut editor = Editor::single_line( + Some(|theme| theme.contacts_panel.user_query_editor.clone()), + cx, + ); + editor.set_placeholder_text("Filter contacts", cx); + editor + }); + + cx.subscribe(&filter_editor, |this, _, event, cx| { + if let editor::Event::BufferEdited = event { + let query = this.filter_editor.read(cx).text(cx); + if !query.is_empty() { + this.selection.take(); + } + this.update_entries(cx); + if !query.is_empty() { + this.selection = this + .entries + .iter() + .position(|entry| !matches!(entry, ContactEntry::Header(_))); + } + } + }) + .detach(); + + let list_state = ListState::new(0, Orientation::Top, 1000., cx, move |this, ix, cx| { + let theme = cx.global::().theme.clone(); + let is_selected = this.selection == Some(ix); + + match &this.entries[ix] { + ContactEntry::Header(section) => { + let is_collapsed = this.collapsed_sections.contains(section); + Self::render_header( + *section, + &theme.contacts_panel, + is_selected, + is_collapsed, + cx, + ) + } + ContactEntry::IncomingRequest(user) => Self::render_contact_request( + user.clone(), + this.user_store.clone(), + &theme.contacts_panel, + true, + is_selected, + cx, + ), + ContactEntry::OutgoingRequest(user) => Self::render_contact_request( + user.clone(), + this.user_store.clone(), + &theme.contacts_panel, + false, + is_selected, + cx, + ), + ContactEntry::Contact(contact) => { + Self::render_contact(&contact.user, &theme.contacts_panel, is_selected) + } + } + }); + + let mut this = Self { + list_state, + selection: None, + collapsed_sections: Default::default(), + entries: Default::default(), + match_candidates: Default::default(), + filter_editor, + _maintain_contacts: cx.observe(&user_store, |this, _, cx| this.update_entries(cx)), + user_store, + }; + this.update_entries(cx); + this + } + + fn clear_filter(&mut self, _: &Cancel, cx: &mut ViewContext) { + let did_clear = self.filter_editor.update(cx, |editor, cx| { + if editor.buffer().read(cx).len(cx) > 0 { + editor.set_text("", cx); + true + } else { + false + } + }); + if !did_clear { + cx.emit(Event::Dismissed); + } + } + + fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext) { + if let Some(ix) = self.selection { + if self.entries.len() > ix + 1 { + self.selection = Some(ix + 1); + } + } else if !self.entries.is_empty() { + self.selection = Some(0); + } + cx.notify(); + self.list_state.reset(self.entries.len()); + } + + fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext) { + if let Some(ix) = self.selection { + if ix > 0 { + self.selection = Some(ix - 1); + } else { + self.selection = None; + } + } + cx.notify(); + self.list_state.reset(self.entries.len()); + } + + fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext) { + if let Some(selection) = self.selection { + if let Some(entry) = self.entries.get(selection) { + match entry { + ContactEntry::Header(section) => { + let section = *section; + self.toggle_expanded(&ToggleExpanded(section), cx); + } + _ => {} + } + } + } + } + + fn toggle_expanded(&mut self, action: &ToggleExpanded, cx: &mut ViewContext) { + let section = action.0; + if let Some(ix) = self.collapsed_sections.iter().position(|s| *s == section) { + self.collapsed_sections.remove(ix); + } else { + self.collapsed_sections.push(section); + } + self.update_entries(cx); + } + + fn update_entries(&mut self, cx: &mut ViewContext) { + let user_store = self.user_store.read(cx); + let query = self.filter_editor.read(cx).text(cx); + let executor = cx.background().clone(); + + let prev_selected_entry = self.selection.and_then(|ix| self.entries.get(ix).cloned()); + self.entries.clear(); + + let mut request_entries = Vec::new(); + let incoming = user_store.incoming_contact_requests(); + if !incoming.is_empty() { + self.match_candidates.clear(); + self.match_candidates + .extend( + incoming + .iter() + .enumerate() + .map(|(ix, user)| StringMatchCandidate { + id: ix, + string: user.github_login.clone(), + char_bag: user.github_login.chars().collect(), + }), + ); + let matches = executor.block(match_strings( + &self.match_candidates, + &query, + true, + usize::MAX, + &Default::default(), + executor.clone(), + )); + request_entries.extend( + matches + .iter() + .map(|mat| ContactEntry::IncomingRequest(incoming[mat.candidate_id].clone())), + ); + } + + let outgoing = user_store.outgoing_contact_requests(); + if !outgoing.is_empty() { + self.match_candidates.clear(); + self.match_candidates + .extend( + outgoing + .iter() + .enumerate() + .map(|(ix, user)| StringMatchCandidate { + id: ix, + string: user.github_login.clone(), + char_bag: user.github_login.chars().collect(), + }), + ); + let matches = executor.block(match_strings( + &self.match_candidates, + &query, + true, + usize::MAX, + &Default::default(), + executor.clone(), + )); + request_entries.extend( + matches + .iter() + .map(|mat| ContactEntry::OutgoingRequest(outgoing[mat.candidate_id].clone())), + ); + } -pub struct AddParticipantPopover {} + if !request_entries.is_empty() { + self.entries.push(ContactEntry::Header(Section::Requests)); + if !self.collapsed_sections.contains(&Section::Requests) { + self.entries.append(&mut request_entries); + } + } + + let current_user = user_store.current_user(); + + let contacts = user_store.contacts(); + if !contacts.is_empty() { + // Always put the current user first. + self.match_candidates.clear(); + self.match_candidates.reserve(contacts.len()); + self.match_candidates.push(StringMatchCandidate { + id: 0, + string: Default::default(), + char_bag: Default::default(), + }); + for (ix, contact) in contacts.iter().enumerate() { + let candidate = StringMatchCandidate { + id: ix, + string: contact.user.github_login.clone(), + char_bag: contact.user.github_login.chars().collect(), + }; + if current_user + .as_ref() + .map_or(false, |current_user| current_user.id == contact.user.id) + { + self.match_candidates[0] = candidate; + } else { + self.match_candidates.push(candidate); + } + } + if self.match_candidates[0].string.is_empty() { + self.match_candidates.remove(0); + } + + let matches = executor.block(match_strings( + &self.match_candidates, + &query, + true, + usize::MAX, + &Default::default(), + executor.clone(), + )); + + let (online_contacts, offline_contacts) = matches + .iter() + .partition::, _>(|mat| contacts[mat.candidate_id].online); + + for (matches, section) in [ + (online_contacts, Section::Online), + (offline_contacts, Section::Offline), + ] { + if !matches.is_empty() { + self.entries.push(ContactEntry::Header(section)); + if !self.collapsed_sections.contains(§ion) { + for mat in matches { + let contact = &contacts[mat.candidate_id]; + self.entries.push(ContactEntry::Contact(contact.clone())); + } + } + } + } + } + + if let Some(prev_selected_entry) = prev_selected_entry { + self.selection.take(); + for (ix, entry) in self.entries.iter().enumerate() { + if *entry == prev_selected_entry { + self.selection = Some(ix); + break; + } + } + } + + self.list_state.reset(self.entries.len()); + cx.notify(); + } + + fn render_header( + section: Section, + theme: &theme::ContactsPanel, + is_selected: bool, + is_collapsed: bool, + cx: &mut RenderContext, + ) -> ElementBox { + enum Header {} + + let header_style = theme.header_row.style_for(Default::default(), is_selected); + let text = match section { + Section::Requests => "Requests", + Section::Online => "Online", + Section::Offline => "Offline", + }; + let icon_size = theme.section_icon_size; + MouseEventHandler::
::new(section as usize, cx, |_, _| { + Flex::row() + .with_child( + Svg::new(if is_collapsed { + "icons/chevron_right_8.svg" + } else { + "icons/chevron_down_8.svg" + }) + .with_color(header_style.text.color) + .constrained() + .with_max_width(icon_size) + .with_max_height(icon_size) + .aligned() + .constrained() + .with_width(icon_size) + .boxed(), + ) + .with_child( + Label::new(text.to_string(), header_style.text.clone()) + .aligned() + .left() + .contained() + .with_margin_left(theme.contact_username.container.margin.left) + .flex(1., true) + .boxed(), + ) + .constrained() + .with_height(theme.row_height) + .contained() + .with_style(header_style.container) + .boxed() + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, move |_, cx| { + cx.dispatch_action(ToggleExpanded(section)) + }) + .boxed() + } + + fn render_contact(user: &User, theme: &theme::ContactsPanel, is_selected: bool) -> ElementBox { + Flex::row() + .with_children(user.avatar.clone().map(|avatar| { + Image::new(avatar) + .with_style(theme.contact_avatar) + .aligned() + .left() + .boxed() + })) + .with_child( + Label::new( + user.github_login.clone(), + theme.contact_username.text.clone(), + ) + .contained() + .with_style(theme.contact_username.container) + .aligned() + .left() + .flex(1., true) + .boxed(), + ) + .constrained() + .with_height(theme.row_height) + .contained() + .with_style(*theme.contact_row.style_for(Default::default(), is_selected)) + .boxed() + } + + fn render_contact_request( + user: Arc, + user_store: ModelHandle, + theme: &theme::ContactsPanel, + is_incoming: bool, + is_selected: bool, + cx: &mut RenderContext, + ) -> ElementBox { + enum Decline {} + enum Accept {} + enum Cancel {} + + let mut row = Flex::row() + .with_children(user.avatar.clone().map(|avatar| { + Image::new(avatar) + .with_style(theme.contact_avatar) + .aligned() + .left() + .boxed() + })) + .with_child( + Label::new( + user.github_login.clone(), + theme.contact_username.text.clone(), + ) + .contained() + .with_style(theme.contact_username.container) + .aligned() + .left() + .flex(1., true) + .boxed(), + ); + + let user_id = user.id; + let is_contact_request_pending = user_store.read(cx).is_contact_request_pending(&user); + let button_spacing = theme.contact_button_spacing; + + if is_incoming { + row.add_children([ + MouseEventHandler::::new(user.id as usize, cx, |mouse_state, _| { + let button_style = if is_contact_request_pending { + &theme.disabled_button + } else { + theme.contact_button.style_for(mouse_state, false) + }; + render_icon_button(button_style, "icons/x_mark_8.svg") + .aligned() + // .flex_float() + .boxed() + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, move |_, cx| { + todo!(); + // cx.dispatch_action(RespondToContactRequest { + // user_id, + // accept: false, + // }) + }) + // .flex_float() + .contained() + .with_margin_right(button_spacing) + .boxed(), + MouseEventHandler::::new(user.id as usize, cx, |mouse_state, _| { + let button_style = if is_contact_request_pending { + &theme.disabled_button + } else { + theme.contact_button.style_for(mouse_state, false) + }; + render_icon_button(button_style, "icons/check_8.svg") + .aligned() + .flex_float() + .boxed() + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, move |_, cx| { + todo!() + // cx.dispatch_action(RespondToContactRequest { + // user_id, + // accept: true, + // }) + }) + .boxed(), + ]); + } else { + row.add_child( + MouseEventHandler::::new(user.id as usize, cx, |mouse_state, _| { + let button_style = if is_contact_request_pending { + &theme.disabled_button + } else { + theme.contact_button.style_for(mouse_state, false) + }; + render_icon_button(button_style, "icons/x_mark_8.svg") + .aligned() + .flex_float() + .boxed() + }) + .with_padding(Padding::uniform(2.)) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, move |_, cx| { + todo!() + // cx.dispatch_action(RemoveContact(user_id)) + }) + .flex_float() + .boxed(), + ); + } + + row.constrained() + .with_height(theme.row_height) + .contained() + .with_style(*theme.contact_row.style_for(Default::default(), is_selected)) + .boxed() + } +} impl Entity for AddParticipantPopover { - type Event = (); + type Event = Event; } impl View for AddParticipantPopover { @@ -12,25 +579,128 @@ impl View for AddParticipantPopover { "AddParticipantPopover" } + fn keymap_context(&self, _: &AppContext) -> keymap::Context { + let mut cx = Self::default_keymap_context(); + cx.set.insert("menu".into()); + cx + } + fn render(&mut self, cx: &mut RenderContext) -> ElementBox { - let theme = &cx - .global::() - .theme - .workspace - .titlebar - .add_participant_popover; - Empty::new() + enum AddContact {} + let theme = cx.global::().theme.clone(); + + Flex::column() + .with_child( + Flex::row() + .with_child( + ChildView::new(self.filter_editor.clone()) + .contained() + .with_style(theme.contacts_panel.user_query_editor.container) + .flex(1., true) + .boxed(), + ) + .with_child( + MouseEventHandler::::new(0, cx, |_, _| { + Svg::new("icons/user_plus_16.svg") + .with_color(theme.contacts_panel.add_contact_button.color) + .constrained() + .with_height(16.) + .contained() + .with_style(theme.contacts_panel.add_contact_button.container) + .aligned() + .boxed() + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, |_, cx| { + todo!() + // cx.dispatch_action(contact_finder::Toggle) + }) + .boxed(), + ) + .constrained() + .with_height(theme.contacts_panel.user_query_editor_height) + .boxed(), + ) + .with_child(List::new(self.list_state.clone()).flex(1., false).boxed()) + .with_children( + self.user_store + .read(cx) + .invite_info() + .cloned() + .and_then(|info| { + enum InviteLink {} + + if info.count > 0 { + Some( + MouseEventHandler::::new(0, cx, |state, cx| { + let style = theme + .contacts_panel + .invite_row + .style_for(state, false) + .clone(); + + let copied = cx.read_from_clipboard().map_or(false, |item| { + item.text().as_str() == info.url.as_ref() + }); + + Label::new( + format!( + "{} invite link ({} left)", + if copied { "Copied" } else { "Copy" }, + info.count + ), + style.label.clone(), + ) + .aligned() + .left() + .constrained() + .with_height(theme.contacts_panel.row_height) + .contained() + .with_style(style.container) + .boxed() + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, move |_, cx| { + cx.write_to_clipboard(ClipboardItem::new(info.url.to_string())); + cx.notify(); + }) + .boxed(), + ) + } else { + None + } + }), + ) .contained() - .with_style(theme.container) + .with_style(theme.workspace.titlebar.add_participant_popover.container) .constrained() - .with_width(theme.width) - .with_height(theme.height) + .with_width(theme.workspace.titlebar.add_participant_popover.width) + .with_height(theme.workspace.titlebar.add_participant_popover.height) .boxed() } -} -impl AddParticipantPopover { - pub fn new() -> Self { - Self {} + fn on_focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext) { + if !self.filter_editor.is_focused(cx) { + cx.focus(&self.filter_editor); + } } + + fn on_focus_out(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext) { + if !self.filter_editor.is_focused(cx) { + cx.emit(Event::Dismissed); + } + } +} + +fn render_icon_button(style: &IconButton, svg_path: &'static str) -> impl Element { + Svg::new(svg_path) + .with_color(style.color) + .constrained() + .with_width(style.icon_width) + .aligned() + .contained() + .with_style(style.container) + .constrained() + .with_width(style.button_width) + .with_height(style.button_width) } diff --git a/crates/collab_titlebar_item/src/collab_titlebar_item.rs b/crates/collab_titlebar_item/src/collab_titlebar_item.rs index a48318d204e916af4c908ef2ae46140f09af4234..b080242a8fe6348a5f10bc78cf516d74bc8b1f51 100644 --- a/crates/collab_titlebar_item/src/collab_titlebar_item.rs +++ b/crates/collab_titlebar_item/src/collab_titlebar_item.rs @@ -20,6 +20,7 @@ use workspace::{FollowNextCollaborator, ToggleFollow, Workspace}; actions!(contacts_titlebar_item, [ToggleAddParticipantPopover]); pub fn init(cx: &mut MutableAppContext) { + add_participant_popover::init(cx); cx.add_action(CollabTitlebarItem::toggle_add_participant_popover); } @@ -73,8 +74,23 @@ impl CollabTitlebarItem { match self.add_participant_popover.take() { Some(_) => {} None => { - let view = cx.add_view(|_| AddParticipantPopover::new()); - self.add_participant_popover = Some(view); + if let Some(workspace) = self.workspace.upgrade(cx) { + let user_store = workspace.read(cx).user_store().clone(); + let view = cx.add_view(|cx| AddParticipantPopover::new(user_store, cx)); + cx.focus(&view); + cx.subscribe(&view, |this, _, event, cx| { + match event { + add_participant_popover::Event::Dismissed => { + dbg!("dismissed"); + this.add_participant_popover = None; + } + } + + cx.notify(); + }) + .detach(); + self.add_participant_popover = Some(view); + } } } cx.notify(); diff --git a/styles/src/styleTree/workspace.ts b/styles/src/styleTree/workspace.ts index 75f11b3942ce589045815a678bd83cf8741305dc..156ed62fbaef7c465eb6fdef6a2f9adcdd498ed9 100644 --- a/styles/src/styleTree/workspace.ts +++ b/styles/src/styleTree/workspace.ts @@ -136,12 +136,12 @@ export default function workspace(theme: Theme) { addParticipantPopover: { background: backgroundColor(theme, 300, "base"), cornerRadius: 6, - padding: 6, + padding: { top: 6 }, shadow: popoverShadow(theme), border: border(theme, "primary"), margin: { top: -5 }, - width: 255, - height: 200 + width: 250, + height: 300 } }, toolbar: { From 1d1bd3975a50fa513ec5ada150a218eb2c248c9b Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 27 Sep 2022 18:24:22 +0200 Subject: [PATCH 021/112] Remove current user from contacts Co-Authored-By: Nathan Sobo Co-Authored-By: Mikayla Maki --- crates/collab/src/db.rs | 220 +++-------- crates/collab/src/integration_tests.rs | 342 ++++++++++-------- .../src/add_participant_popover.rs | 37 +- .../src/collab_titlebar_item.rs | 1 - 4 files changed, 264 insertions(+), 336 deletions(-) diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index eeb598413ef86e9b5bdf496f3b6f02baa7114c6c..876d16b60b3f9c4790747b2626e07cb75ab8d351 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -842,10 +842,7 @@ impl Db for PostgresDb { .bind(user_id) .fetch(&self.pool); - let mut contacts = vec![Contact::Accepted { - user_id, - should_notify: false, - }]; + let mut contacts = Vec::new(); while let Some(row) = rows.next().await { let (user_id_a, user_id_b, a_to_b, accepted, should_notify) = row?; @@ -2026,13 +2023,7 @@ pub mod tests { let user_3 = db.create_user("user3", None, false).await.unwrap(); // User starts with no contacts - assert_eq!( - db.get_contacts(user_1).await.unwrap(), - vec![Contact::Accepted { - user_id: user_1, - should_notify: false - }], - ); + assert_eq!(db.get_contacts(user_1).await.unwrap(), vec![]); // User requests a contact. Both users see the pending request. db.send_contact_request(user_1, user_2).await.unwrap(); @@ -2040,26 +2031,14 @@ pub mod tests { assert!(!db.has_contact(user_2, user_1).await.unwrap()); assert_eq!( db.get_contacts(user_1).await.unwrap(), - &[ - Contact::Accepted { - user_id: user_1, - should_notify: false - }, - Contact::Outgoing { user_id: user_2 } - ], + &[Contact::Outgoing { user_id: user_2 }], ); assert_eq!( db.get_contacts(user_2).await.unwrap(), - &[ - Contact::Incoming { - user_id: user_1, - should_notify: true - }, - Contact::Accepted { - user_id: user_2, - should_notify: false - }, - ] + &[Contact::Incoming { + user_id: user_1, + should_notify: true + }] ); // User 2 dismisses the contact request notification without accepting or rejecting. @@ -2072,16 +2051,10 @@ pub mod tests { .unwrap(); assert_eq!( db.get_contacts(user_2).await.unwrap(), - &[ - Contact::Incoming { - user_id: user_1, - should_notify: false - }, - Contact::Accepted { - user_id: user_2, - should_notify: false - }, - ] + &[Contact::Incoming { + user_id: user_1, + should_notify: false + }] ); // User can't accept their own contact request @@ -2095,31 +2068,19 @@ pub mod tests { .unwrap(); assert_eq!( db.get_contacts(user_1).await.unwrap(), - &[ - Contact::Accepted { - user_id: user_1, - should_notify: false - }, - Contact::Accepted { - user_id: user_2, - should_notify: true - } - ], + &[Contact::Accepted { + user_id: user_2, + should_notify: true + }], ); assert!(db.has_contact(user_1, user_2).await.unwrap()); assert!(db.has_contact(user_2, user_1).await.unwrap()); assert_eq!( db.get_contacts(user_2).await.unwrap(), - &[ - Contact::Accepted { - user_id: user_1, - should_notify: false, - }, - Contact::Accepted { - user_id: user_2, - should_notify: false, - }, - ] + &[Contact::Accepted { + user_id: user_1, + should_notify: false, + }] ); // Users cannot re-request existing contacts. @@ -2132,16 +2093,10 @@ pub mod tests { .unwrap_err(); assert_eq!( db.get_contacts(user_1).await.unwrap(), - &[ - Contact::Accepted { - user_id: user_1, - should_notify: false - }, - Contact::Accepted { - user_id: user_2, - should_notify: true, - }, - ] + &[Contact::Accepted { + user_id: user_2, + should_notify: true, + }] ); // Users can dismiss notifications of other users accepting their requests. @@ -2150,16 +2105,10 @@ pub mod tests { .unwrap(); assert_eq!( db.get_contacts(user_1).await.unwrap(), - &[ - Contact::Accepted { - user_id: user_1, - should_notify: false - }, - Contact::Accepted { - user_id: user_2, - should_notify: false, - }, - ] + &[Contact::Accepted { + user_id: user_2, + should_notify: false, + },] ); // Users send each other concurrent contact requests and @@ -2169,10 +2118,6 @@ pub mod tests { assert_eq!( db.get_contacts(user_1).await.unwrap(), &[ - Contact::Accepted { - user_id: user_1, - should_notify: false - }, Contact::Accepted { user_id: user_2, should_notify: false, @@ -2185,16 +2130,10 @@ pub mod tests { ); assert_eq!( db.get_contacts(user_3).await.unwrap(), - &[ - Contact::Accepted { - user_id: user_1, - should_notify: false - }, - Contact::Accepted { - user_id: user_3, - should_notify: false - } - ], + &[Contact::Accepted { + user_id: user_1, + should_notify: false + }], ); // User declines a contact request. Both users see that it is gone. @@ -2206,29 +2145,17 @@ pub mod tests { assert!(!db.has_contact(user_3, user_2).await.unwrap()); assert_eq!( db.get_contacts(user_2).await.unwrap(), - &[ - Contact::Accepted { - user_id: user_1, - should_notify: false - }, - Contact::Accepted { - user_id: user_2, - should_notify: false - } - ] + &[Contact::Accepted { + user_id: user_1, + should_notify: false + }] ); assert_eq!( db.get_contacts(user_3).await.unwrap(), - &[ - Contact::Accepted { - user_id: user_1, - should_notify: false - }, - Contact::Accepted { - user_id: user_3, - should_notify: false - } - ], + &[Contact::Accepted { + user_id: user_1, + should_notify: false + }], ); } } @@ -2261,29 +2188,17 @@ pub mod tests { assert_eq!(invite_count, 1); assert_eq!( db.get_contacts(user1).await.unwrap(), - [ - Contact::Accepted { - user_id: user1, - should_notify: false - }, - Contact::Accepted { - user_id: user2, - should_notify: true - } - ] + [Contact::Accepted { + user_id: user2, + should_notify: true + }] ); assert_eq!( db.get_contacts(user2).await.unwrap(), - [ - Contact::Accepted { - user_id: user1, - should_notify: false - }, - Contact::Accepted { - user_id: user2, - should_notify: false - } - ] + [Contact::Accepted { + user_id: user1, + should_notify: false + }] ); // User 3 redeems the invite code and becomes a contact of user 1. @@ -2296,10 +2211,6 @@ pub mod tests { assert_eq!( db.get_contacts(user1).await.unwrap(), [ - Contact::Accepted { - user_id: user1, - should_notify: false - }, Contact::Accepted { user_id: user2, should_notify: true @@ -2312,16 +2223,10 @@ pub mod tests { ); assert_eq!( db.get_contacts(user3).await.unwrap(), - [ - Contact::Accepted { - user_id: user1, - should_notify: false - }, - Contact::Accepted { - user_id: user3, - should_notify: false - }, - ] + [Contact::Accepted { + user_id: user1, + should_notify: false + }] ); // Trying to reedem the code for the third time results in an error. @@ -2346,10 +2251,6 @@ pub mod tests { assert_eq!( db.get_contacts(user1).await.unwrap(), [ - Contact::Accepted { - user_id: user1, - should_notify: false - }, Contact::Accepted { user_id: user2, should_notify: true @@ -2366,16 +2267,10 @@ pub mod tests { ); assert_eq!( db.get_contacts(user4).await.unwrap(), - [ - Contact::Accepted { - user_id: user1, - should_notify: false - }, - Contact::Accepted { - user_id: user4, - should_notify: false - }, - ] + [Contact::Accepted { + user_id: user1, + should_notify: false + }] ); // An existing user cannot redeem invite codes. @@ -2704,10 +2599,7 @@ pub mod tests { async fn get_contacts(&self, id: UserId) -> Result> { self.background.simulate_random_delay().await; - let mut contacts = vec![Contact::Accepted { - user_id: id, - should_notify: false, - }]; + let mut contacts = Vec::new(); for contact in self.contacts.lock().iter() { if contact.requester_id == id { diff --git a/crates/collab/src/integration_tests.rs b/crates/collab/src/integration_tests.rs index c235a31c557f8a07ba73dfb10a536d83ae99a46c..e04bd80c795bb0b4b90f2de98ebef7b9fd376d70 100644 --- a/crates/collab/src/integration_tests.rs +++ b/crates/collab/src/integration_tests.rs @@ -3909,78 +3909,122 @@ async fn test_contacts( .await; deterministic.run_until_parked(); - for (client, cx) in [(&client_a, &cx_a), (&client_b, &cx_b), (&client_c, &cx_c)] { - client.user_store.read_with(*cx, |store, _| { - assert_eq!( - contacts(store), - [ - ("user_a", true, vec![]), - ("user_b", true, vec![]), - ("user_c", true, vec![]) - ], - "{} has the wrong contacts", - client.username - ) - }); - } + assert_eq!( + contacts(&client_a, cx_a), + [ + ("user_b".to_string(), true, vec![]), + ("user_c".to_string(), true, vec![]) + ] + ); + assert_eq!( + contacts(&client_b, cx_b), + [ + ("user_a".to_string(), true, vec![]), + ("user_c".to_string(), true, vec![]) + ] + ); + assert_eq!( + contacts(&client_c, cx_c), + [ + ("user_a".to_string(), true, vec![]), + ("user_b".to_string(), true, vec![]) + ] + ); // Share a project as client A. client_a.fs.create_dir(Path::new("/a")).await.unwrap(); let (project_a, _) = client_a.build_local_project("/a", cx_a).await; deterministic.run_until_parked(); - for (client, cx) in [(&client_a, &cx_a), (&client_b, &cx_b), (&client_c, &cx_c)] { - client.user_store.read_with(*cx, |store, _| { - assert_eq!( - contacts(store), - [ - ("user_a", true, vec![("a", vec![])]), - ("user_b", true, vec![]), - ("user_c", true, vec![]) - ], - "{} has the wrong contacts", - client.username - ) - }); - } + assert_eq!( + contacts(&client_a, cx_a), + [ + ("user_b".to_string(), true, vec![]), + ("user_c".to_string(), true, vec![]) + ] + ); + assert_eq!( + contacts(&client_b, cx_b), + [ + ("user_a".to_string(), true, vec![("a".to_string(), vec![])]), + ("user_c".to_string(), true, vec![]) + ] + ); + assert_eq!( + contacts(&client_c, cx_c), + [ + ("user_a".to_string(), true, vec![("a".to_string(), vec![])]), + ("user_b".to_string(), true, vec![]) + ] + ); let _project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await; deterministic.run_until_parked(); - for (client, cx) in [(&client_a, &cx_a), (&client_b, &cx_b), (&client_c, &cx_c)] { - client.user_store.read_with(*cx, |store, _| { - assert_eq!( - contacts(store), - [ - ("user_a", true, vec![("a", vec!["user_b"])]), - ("user_b", true, vec![]), - ("user_c", true, vec![]) - ], - "{} has the wrong contacts", - client.username - ) - }); - } + assert_eq!( + contacts(&client_a, cx_a), + [ + ("user_b".to_string(), true, vec![]), + ("user_c".to_string(), true, vec![]) + ] + ); + assert_eq!( + contacts(&client_b, cx_b), + [ + ( + "user_a".to_string(), + true, + vec![("a".to_string(), vec!["user_b".to_string()])] + ), + ("user_c".to_string(), true, vec![]) + ] + ); + assert_eq!( + contacts(&client_c, cx_c), + [ + ( + "user_a".to_string(), + true, + vec![("a".to_string(), vec!["user_b".to_string()])] + ), + ("user_b".to_string(), true, vec![]) + ] + ); // Add a local project as client B client_a.fs.create_dir("/b".as_ref()).await.unwrap(); let (_project_b, _) = client_b.build_local_project("/b", cx_b).await; deterministic.run_until_parked(); - for (client, cx) in [(&client_a, &cx_a), (&client_b, &cx_b), (&client_c, &cx_c)] { - client.user_store.read_with(*cx, |store, _| { - assert_eq!( - contacts(store), - [ - ("user_a", true, vec![("a", vec!["user_b"])]), - ("user_b", true, vec![("b", vec![])]), - ("user_c", true, vec![]) - ], - "{} has the wrong contacts", - client.username - ) - }); - } + assert_eq!( + contacts(&client_a, cx_a), + [ + ("user_b".to_string(), true, vec![("b".to_string(), vec![])]), + ("user_c".to_string(), true, vec![]) + ] + ); + assert_eq!( + contacts(&client_b, cx_b), + [ + ( + "user_a".to_string(), + true, + vec![("a".to_string(), vec!["user_b".to_string()])] + ), + ("user_c".to_string(), true, vec![]) + ] + ); + assert_eq!( + contacts(&client_c, cx_c), + [ + ( + "user_a".to_string(), + true, + vec![("a".to_string(), vec!["user_b".to_string()])] + ), + ("user_b".to_string(), true, vec![("b".to_string(), vec![])]) + ] + ); project_a .condition(cx_a, |project, _| { @@ -3990,41 +4034,46 @@ async fn test_contacts( cx_a.update(move |_| drop(project_a)); deterministic.run_until_parked(); - for (client, cx) in [(&client_a, &cx_a), (&client_b, &cx_b), (&client_c, &cx_c)] { - client.user_store.read_with(*cx, |store, _| { - assert_eq!( - contacts(store), - [ - ("user_a", true, vec![]), - ("user_b", true, vec![("b", vec![])]), - ("user_c", true, vec![]) - ], - "{} has the wrong contacts", - client.username - ) - }); - } + assert_eq!( + contacts(&client_a, cx_a), + [ + ("user_b".to_string(), true, vec![("b".to_string(), vec![])]), + ("user_c".to_string(), true, vec![]) + ] + ); + assert_eq!( + contacts(&client_b, cx_b), + [ + ("user_a".to_string(), true, vec![]), + ("user_c".to_string(), true, vec![]) + ] + ); + assert_eq!( + contacts(&client_c, cx_c), + [ + ("user_a".to_string(), true, vec![]), + ("user_b".to_string(), true, vec![("b".to_string(), vec![])]) + ] + ); server.disconnect_client(client_c.current_user_id(cx_c)); server.forbid_connections(); deterministic.advance_clock(rpc::RECEIVE_TIMEOUT); - for (client, cx) in [(&client_a, &cx_a), (&client_b, &cx_b)] { - client.user_store.read_with(*cx, |store, _| { - assert_eq!( - contacts(store), - [ - ("user_a", true, vec![]), - ("user_b", true, vec![("b", vec![])]), - ("user_c", false, vec![]) - ], - "{} has the wrong contacts", - client.username - ) - }); - } - client_c - .user_store - .read_with(cx_c, |store, _| assert_eq!(contacts(store), [])); + assert_eq!( + contacts(&client_a, cx_a), + [ + ("user_b".to_string(), true, vec![("b".to_string(), vec![])]), + ("user_c".to_string(), false, vec![]) + ] + ); + assert_eq!( + contacts(&client_b, cx_b), + [ + ("user_a".to_string(), true, vec![]), + ("user_c".to_string(), false, vec![]) + ] + ); + assert_eq!(contacts(&client_c, cx_c), []); server.allow_connections(); client_c @@ -4033,40 +4082,52 @@ async fn test_contacts( .unwrap(); deterministic.run_until_parked(); - for (client, cx) in [(&client_a, &cx_a), (&client_b, &cx_b), (&client_c, &cx_c)] { - client.user_store.read_with(*cx, |store, _| { - assert_eq!( - contacts(store), - [ - ("user_a", true, vec![]), - ("user_b", true, vec![("b", vec![])]), - ("user_c", true, vec![]) - ], - "{} has the wrong contacts", - client.username - ) - }); - } + assert_eq!( + contacts(&client_a, cx_a), + [ + ("user_b".to_string(), true, vec![("b".to_string(), vec![])]), + ("user_c".to_string(), true, vec![]) + ] + ); + assert_eq!( + contacts(&client_b, cx_b), + [ + ("user_a".to_string(), true, vec![]), + ("user_c".to_string(), true, vec![]) + ] + ); + assert_eq!( + contacts(&client_c, cx_c), + [ + ("user_a".to_string(), true, vec![]), + ("user_b".to_string(), true, vec![("b".to_string(), vec![])]) + ] + ); #[allow(clippy::type_complexity)] - fn contacts(user_store: &UserStore) -> Vec<(&str, bool, Vec<(&str, Vec<&str>)>)> { - user_store - .contacts() - .iter() - .map(|contact| { - let projects = contact - .projects - .iter() - .map(|p| { - ( - p.visible_worktree_root_names[0].as_str(), - p.guests.iter().map(|p| p.github_login.as_str()).collect(), - ) - }) - .collect(); - (contact.user.github_login.as_str(), contact.online, projects) - }) - .collect() + fn contacts( + client: &TestClient, + cx: &TestAppContext, + ) -> Vec<(String, bool, Vec<(String, Vec)>)> { + client.user_store.read_with(cx, |store, _| { + store + .contacts() + .iter() + .map(|contact| { + let projects = contact + .projects + .iter() + .map(|p| { + ( + p.visible_worktree_root_names[0].clone(), + p.guests.iter().map(|p| p.github_login.clone()).collect(), + ) + }) + .collect(); + (contact.user.github_login.clone(), contact.online, projects) + }) + .collect() + }) } } @@ -4169,18 +4230,18 @@ async fn test_contact_requests( // User B sees user A as their contact now in all client, and the incoming request from them is removed. let contacts_b = client_b.summarize_contacts(cx_b); - assert_eq!(contacts_b.current, &["user_a", "user_b"]); + assert_eq!(contacts_b.current, &["user_a"]); assert_eq!(contacts_b.incoming_requests, &["user_c"]); let contacts_b2 = client_b2.summarize_contacts(cx_b2); - assert_eq!(contacts_b2.current, &["user_a", "user_b"]); + assert_eq!(contacts_b2.current, &["user_a"]); assert_eq!(contacts_b2.incoming_requests, &["user_c"]); // User A sees user B as their contact now in all clients, and the outgoing request to them is removed. let contacts_a = client_a.summarize_contacts(cx_a); - assert_eq!(contacts_a.current, &["user_a", "user_b"]); + assert_eq!(contacts_a.current, &["user_b"]); assert!(contacts_a.outgoing_requests.is_empty()); let contacts_a2 = client_a2.summarize_contacts(cx_a2); - assert_eq!(contacts_a2.current, &["user_a", "user_b"]); + assert_eq!(contacts_a2.current, &["user_b"]); assert!(contacts_a2.outgoing_requests.is_empty()); // Contacts are present upon connecting (tested here via disconnect/reconnect) @@ -4188,19 +4249,13 @@ async fn test_contact_requests( disconnect_and_reconnect(&client_b, cx_b).await; disconnect_and_reconnect(&client_c, cx_c).await; executor.run_until_parked(); - assert_eq!( - client_a.summarize_contacts(cx_a).current, - &["user_a", "user_b"] - ); - assert_eq!( - client_b.summarize_contacts(cx_b).current, - &["user_a", "user_b"] - ); + assert_eq!(client_a.summarize_contacts(cx_a).current, &["user_b"]); + assert_eq!(client_b.summarize_contacts(cx_b).current, &["user_a"]); assert_eq!( client_b.summarize_contacts(cx_b).incoming_requests, &["user_c"] ); - assert_eq!(client_c.summarize_contacts(cx_c).current, &["user_c"]); + assert!(client_c.summarize_contacts(cx_c).current.is_empty()); assert_eq!( client_c.summarize_contacts(cx_c).outgoing_requests, &["user_b"] @@ -4219,18 +4274,18 @@ async fn test_contact_requests( // User B doesn't see user C as their contact, and the incoming request from them is removed. let contacts_b = client_b.summarize_contacts(cx_b); - assert_eq!(contacts_b.current, &["user_a", "user_b"]); + assert_eq!(contacts_b.current, &["user_a"]); assert!(contacts_b.incoming_requests.is_empty()); let contacts_b2 = client_b2.summarize_contacts(cx_b2); - assert_eq!(contacts_b2.current, &["user_a", "user_b"]); + assert_eq!(contacts_b2.current, &["user_a"]); assert!(contacts_b2.incoming_requests.is_empty()); // User C doesn't see user B as their contact, and the outgoing request to them is removed. let contacts_c = client_c.summarize_contacts(cx_c); - assert_eq!(contacts_c.current, &["user_c"]); + assert!(contacts_c.current.is_empty()); assert!(contacts_c.outgoing_requests.is_empty()); let contacts_c2 = client_c2.summarize_contacts(cx_c2); - assert_eq!(contacts_c2.current, &["user_c"]); + assert!(contacts_c2.current.is_empty()); assert!(contacts_c2.outgoing_requests.is_empty()); // Incoming/outgoing requests are not present upon connecting (tested here via disconnect/reconnect) @@ -4238,19 +4293,13 @@ async fn test_contact_requests( disconnect_and_reconnect(&client_b, cx_b).await; disconnect_and_reconnect(&client_c, cx_c).await; executor.run_until_parked(); - assert_eq!( - client_a.summarize_contacts(cx_a).current, - &["user_a", "user_b"] - ); - assert_eq!( - client_b.summarize_contacts(cx_b).current, - &["user_a", "user_b"] - ); + assert_eq!(client_a.summarize_contacts(cx_a).current, &["user_b"]); + assert_eq!(client_b.summarize_contacts(cx_b).current, &["user_a"]); assert!(client_b .summarize_contacts(cx_b) .incoming_requests .is_empty()); - assert_eq!(client_c.summarize_contacts(cx_c).current, &["user_c"]); + assert!(client_c.summarize_contacts(cx_c).current.is_empty()); assert!(client_c .summarize_contacts(cx_c) .outgoing_requests @@ -5655,6 +5704,9 @@ impl TestClient { worktree .read_with(cx, |tree, _| tree.as_local().unwrap().scan_complete()) .await; + project + .update(cx, |project, _| project.next_remote_id()) + .await; (project, worktree.read_with(cx, |tree, _| tree.id())) } diff --git a/crates/collab_titlebar_item/src/add_participant_popover.rs b/crates/collab_titlebar_item/src/add_participant_popover.rs index 95d37849cbb6d0101887ad57b371e19614f19baf..b6fa8125f7566d5889c4f8651879d8b0891a1194 100644 --- a/crates/collab_titlebar_item/src/add_participant_popover.rs +++ b/crates/collab_titlebar_item/src/add_participant_popover.rs @@ -298,36 +298,21 @@ impl AddParticipantPopover { } } - let current_user = user_store.current_user(); - let contacts = user_store.contacts(); if !contacts.is_empty() { // Always put the current user first. self.match_candidates.clear(); - self.match_candidates.reserve(contacts.len()); - self.match_candidates.push(StringMatchCandidate { - id: 0, - string: Default::default(), - char_bag: Default::default(), - }); - for (ix, contact) in contacts.iter().enumerate() { - let candidate = StringMatchCandidate { - id: ix, - string: contact.user.github_login.clone(), - char_bag: contact.user.github_login.chars().collect(), - }; - if current_user - .as_ref() - .map_or(false, |current_user| current_user.id == contact.user.id) - { - self.match_candidates[0] = candidate; - } else { - self.match_candidates.push(candidate); - } - } - if self.match_candidates[0].string.is_empty() { - self.match_candidates.remove(0); - } + self.match_candidates + .extend( + contacts + .iter() + .enumerate() + .map(|(ix, contact)| StringMatchCandidate { + id: ix, + string: contact.user.github_login.clone(), + char_bag: contact.user.github_login.chars().collect(), + }), + ); let matches = executor.block(match_strings( &self.match_candidates, diff --git a/crates/collab_titlebar_item/src/collab_titlebar_item.rs b/crates/collab_titlebar_item/src/collab_titlebar_item.rs index b080242a8fe6348a5f10bc78cf516d74bc8b1f51..e0ad25325190ed00c19393c371a26a4fe07b7385 100644 --- a/crates/collab_titlebar_item/src/collab_titlebar_item.rs +++ b/crates/collab_titlebar_item/src/collab_titlebar_item.rs @@ -81,7 +81,6 @@ impl CollabTitlebarItem { cx.subscribe(&view, |this, _, event, cx| { match event { add_participant_popover::Event::Dismissed => { - dbg!("dismissed"); this.add_participant_popover = None; } } From f5b2d56efd2ade03a9491aa4a57c92423be57275 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 28 Sep 2022 09:06:28 -0600 Subject: [PATCH 022/112] Remove contacts menu bar extra Co-Authored-By: Antonio Scandurra --- Cargo.lock | 25 ----- crates/contacts_status_item/Cargo.toml | 32 ------- .../src/contacts_popover.rs | 94 ------------------- .../src/contacts_status_item.rs | 94 ------------------- crates/theme/src/theme.rs | 6 -- crates/zed/Cargo.toml | 1 - 6 files changed, 252 deletions(-) delete mode 100644 crates/contacts_status_item/Cargo.toml delete mode 100644 crates/contacts_status_item/src/contacts_popover.rs delete mode 100644 crates/contacts_status_item/src/contacts_status_item.rs diff --git a/Cargo.lock b/Cargo.lock index a8a89478fe3d8d83555637accd12496f12d714ea..c06a3ee93cc1abd90b71e362771dcd8ccd5727ba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1150,30 +1150,6 @@ dependencies = [ "workspace", ] -[[package]] -name = "contacts_status_item" -version = "0.1.0" -dependencies = [ - "anyhow", - "client", - "collections", - "editor", - "futures", - "fuzzy", - "gpui", - "language", - "log", - "menu", - "picker", - "postage", - "project", - "serde", - "settings", - "theme", - "util", - "workspace", -] - [[package]] name = "context_menu" version = "0.1.0" @@ -7185,7 +7161,6 @@ dependencies = [ "collections", "command_palette", "contacts_panel", - "contacts_status_item", "context_menu", "ctor", "diagnostics", diff --git a/crates/contacts_status_item/Cargo.toml b/crates/contacts_status_item/Cargo.toml deleted file mode 100644 index df115a384220553afd75c37b6276931fe6794fb3..0000000000000000000000000000000000000000 --- a/crates/contacts_status_item/Cargo.toml +++ /dev/null @@ -1,32 +0,0 @@ -[package] -name = "contacts_status_item" -version = "0.1.0" -edition = "2021" - -[lib] -path = "src/contacts_status_item.rs" -doctest = false - -[dependencies] -client = { path = "../client" } -collections = { path = "../collections" } -editor = { path = "../editor" } -fuzzy = { path = "../fuzzy" } -gpui = { path = "../gpui" } -menu = { path = "../menu" } -picker = { path = "../picker" } -project = { path = "../project" } -settings = { path = "../settings" } -theme = { path = "../theme" } -util = { path = "../util" } -workspace = { path = "../workspace" } -anyhow = "1.0" -futures = "0.3" -log = "0.4" -postage = { version = "0.4.1", features = ["futures-traits"] } -serde = { version = "1.0", features = ["derive", "rc"] } - -[dev-dependencies] -language = { path = "../language", features = ["test-support"] } -project = { path = "../project", features = ["test-support"] } -workspace = { path = "../workspace", features = ["test-support"] } diff --git a/crates/contacts_status_item/src/contacts_popover.rs b/crates/contacts_status_item/src/contacts_popover.rs deleted file mode 100644 index 2998d74ed8c3b608210c9d482831c3407a973c7f..0000000000000000000000000000000000000000 --- a/crates/contacts_status_item/src/contacts_popover.rs +++ /dev/null @@ -1,94 +0,0 @@ -use editor::Editor; -use gpui::{elements::*, Entity, RenderContext, View, ViewContext, ViewHandle}; -use settings::Settings; - -pub enum Event { - Deactivated, -} - -pub struct ContactsPopover { - filter_editor: ViewHandle, -} - -impl Entity for ContactsPopover { - type Event = Event; -} - -impl View for ContactsPopover { - fn ui_name() -> &'static str { - "ContactsPopover" - } - - fn render(&mut self, cx: &mut RenderContext) -> ElementBox { - let theme = &cx.global::().theme.contacts_popover; - - Flex::row() - .with_child( - ChildView::new(self.filter_editor.clone()) - .contained() - .with_style( - cx.global::() - .theme - .contacts_panel - .user_query_editor - .container, - ) - .flex(1., true) - .boxed(), - ) - // .with_child( - // MouseEventHandler::::new(0, cx, |_, _| { - // Svg::new("icons/user_plus_16.svg") - // .with_color(theme.add_contact_button.color) - // .constrained() - // .with_height(16.) - // .contained() - // .with_style(theme.add_contact_button.container) - // .aligned() - // .boxed() - // }) - // .with_cursor_style(CursorStyle::PointingHand) - // .on_click(MouseButton::Left, |_, cx| { - // cx.dispatch_action(contact_finder::Toggle) - // }) - // .boxed(), - // ) - .constrained() - .with_height( - cx.global::() - .theme - .contacts_panel - .user_query_editor_height, - ) - .aligned() - .top() - .contained() - .with_background_color(theme.background) - .with_uniform_padding(4.) - .boxed() - } -} - -impl ContactsPopover { - pub fn new(cx: &mut ViewContext) -> Self { - cx.observe_window_activation(Self::window_activation_changed) - .detach(); - - let filter_editor = cx.add_view(|cx| { - let mut editor = Editor::single_line( - Some(|theme| theme.contacts_panel.user_query_editor.clone()), - cx, - ); - editor.set_placeholder_text("Filter contacts", cx); - editor - }); - - Self { filter_editor } - } - - fn window_activation_changed(&mut self, is_active: bool, cx: &mut ViewContext) { - if !is_active { - cx.emit(Event::Deactivated); - } - } -} diff --git a/crates/contacts_status_item/src/contacts_status_item.rs b/crates/contacts_status_item/src/contacts_status_item.rs deleted file mode 100644 index 5d471abcdf51f2153a5cd98627ef5478fb3ceccb..0000000000000000000000000000000000000000 --- a/crates/contacts_status_item/src/contacts_status_item.rs +++ /dev/null @@ -1,94 +0,0 @@ -mod contacts_popover; - -use contacts_popover::ContactsPopover; -use gpui::{ - actions, - color::Color, - elements::*, - geometry::{rect::RectF, vector::vec2f}, - Appearance, Entity, MouseButton, MutableAppContext, RenderContext, View, ViewContext, - ViewHandle, WindowKind, -}; - -actions!(contacts_status_item, [ToggleContactsPopover]); - -pub fn init(cx: &mut MutableAppContext) { - cx.add_action(ContactsStatusItem::toggle_contacts_popover); -} - -pub struct ContactsStatusItem { - popover: Option>, -} - -impl Entity for ContactsStatusItem { - type Event = (); -} - -impl View for ContactsStatusItem { - fn ui_name() -> &'static str { - "ContactsStatusItem" - } - - fn render(&mut self, cx: &mut RenderContext) -> ElementBox { - let color = match cx.appearance { - Appearance::Light | Appearance::VibrantLight => Color::black(), - Appearance::Dark | Appearance::VibrantDark => Color::white(), - }; - MouseEventHandler::::new(0, cx, |_, _| { - Svg::new("icons/zed_22.svg") - .with_color(color) - .aligned() - .boxed() - }) - .on_click(MouseButton::Left, |_, cx| { - cx.dispatch_action(ToggleContactsPopover); - }) - .boxed() - } -} - -impl ContactsStatusItem { - pub fn new() -> Self { - Self { popover: None } - } - - fn toggle_contacts_popover(&mut self, _: &ToggleContactsPopover, cx: &mut ViewContext) { - match self.popover.take() { - Some(popover) => { - cx.remove_window(popover.window_id()); - } - None => { - let window_bounds = cx.window_bounds(); - let size = vec2f(360., 460.); - let origin = window_bounds.lower_left() - + vec2f(window_bounds.width() / 2. - size.x() / 2., 0.); - let (_, popover) = cx.add_window( - gpui::WindowOptions { - bounds: gpui::WindowBounds::Fixed(RectF::new(origin, size)), - titlebar: None, - center: false, - kind: WindowKind::PopUp, - is_movable: false, - }, - |cx| ContactsPopover::new(cx), - ); - cx.subscribe(&popover, Self::on_popover_event).detach(); - self.popover = Some(popover); - } - } - } - - fn on_popover_event( - &mut self, - popover: ViewHandle, - event: &contacts_popover::Event, - cx: &mut ViewContext, - ) { - match event { - contacts_popover::Event::Deactivated => { - self.popover.take(); - cx.remove_window(popover.window_id()); - } - } - } -} diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 4446e4e06fec57dc07f3926fb55a30e5d5b8b2ee..4192bc0752c51ea3835b3a98631cb564a20b1672 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -19,7 +19,6 @@ pub struct Theme { pub workspace: Workspace, pub context_menu: ContextMenu, pub chat_panel: ChatPanel, - pub contacts_popover: ContactsPopover, pub contacts_panel: ContactsPanel, pub contact_finder: ContactFinder, pub project_panel: ProjectPanel, @@ -325,11 +324,6 @@ pub struct CommandPalette { pub keystroke_spacing: f32, } -#[derive(Deserialize, Default)] -pub struct ContactsPopover { - pub background: Color, -} - #[derive(Deserialize, Default)] pub struct ContactsPanel { #[serde(flatten)] diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 6c2ce8ff07b9aecdc8e204d0c162761a719d75c5..82629705a169c8ec3c0bcb8dfa68b01aa7b4ab5a 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -28,7 +28,6 @@ context_menu = { path = "../context_menu" } client = { path = "../client" } clock = { path = "../clock" } contacts_panel = { path = "../contacts_panel" } -contacts_status_item = { path = "../contacts_status_item" } diagnostics = { path = "../diagnostics" } editor = { path = "../editor" } file_finder = { path = "../file_finder" } From 815cf4464760c499b89f3b999a14fe72f11dff37 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 28 Sep 2022 09:10:01 -0600 Subject: [PATCH 023/112] Rename AddParticipantPopover to ContactsPopover Co-Authored-By: Antonio Scandurra --- .../src/collab_titlebar_item.rs | 42 +++++++++---------- ...icipant_popover.rs => contacts_popover.rs} | 26 ++++++------ crates/theme/src/theme.rs | 4 +- styles/src/styleTree/workspace.ts | 4 +- 4 files changed, 36 insertions(+), 40 deletions(-) rename crates/collab_titlebar_item/src/{add_participant_popover.rs => contacts_popover.rs} (97%) diff --git a/crates/collab_titlebar_item/src/collab_titlebar_item.rs b/crates/collab_titlebar_item/src/collab_titlebar_item.rs index e0ad25325190ed00c19393c371a26a4fe07b7385..c27a2b3452e08fe9828dd63235882ab73df43028 100644 --- a/crates/collab_titlebar_item/src/collab_titlebar_item.rs +++ b/crates/collab_titlebar_item/src/collab_titlebar_item.rs @@ -1,8 +1,8 @@ -mod add_participant_popover; +mod contacts_popover; -use add_participant_popover::AddParticipantPopover; use client::{Authenticate, PeerId}; use clock::ReplicaId; +use contacts_popover::ContactsPopover; use gpui::{ actions, color::Color, @@ -17,16 +17,16 @@ use std::{ops::Range, sync::Arc}; use theme::Theme; use workspace::{FollowNextCollaborator, ToggleFollow, Workspace}; -actions!(contacts_titlebar_item, [ToggleAddParticipantPopover]); +actions!(contacts_titlebar_item, [ToggleContactsPopover]); pub fn init(cx: &mut MutableAppContext) { - add_participant_popover::init(cx); - cx.add_action(CollabTitlebarItem::toggle_add_participant_popover); + contacts_popover::init(cx); + cx.add_action(CollabTitlebarItem::toggle_contacts_popover); } pub struct CollabTitlebarItem { workspace: WeakViewHandle, - add_participant_popover: Option>, + contacts_popover: Option>, _subscriptions: Vec, } @@ -61,34 +61,30 @@ impl CollabTitlebarItem { let observe_workspace = cx.observe(workspace, |_, _, cx| cx.notify()); Self { workspace: workspace.downgrade(), - add_participant_popover: None, + contacts_popover: None, _subscriptions: vec![observe_workspace], } } - fn toggle_add_participant_popover( - &mut self, - _: &ToggleAddParticipantPopover, - cx: &mut ViewContext, - ) { - match self.add_participant_popover.take() { + fn toggle_contacts_popover(&mut self, _: &ToggleContactsPopover, cx: &mut ViewContext) { + match self.contacts_popover.take() { Some(_) => {} None => { if let Some(workspace) = self.workspace.upgrade(cx) { let user_store = workspace.read(cx).user_store().clone(); - let view = cx.add_view(|cx| AddParticipantPopover::new(user_store, cx)); + let view = cx.add_view(|cx| ContactsPopover::new(user_store, cx)); cx.focus(&view); cx.subscribe(&view, |this, _, event, cx| { match event { - add_participant_popover::Event::Dismissed => { - this.add_participant_popover = None; + contacts_popover::Event::Dismissed => { + this.contacts_popover = None; } } cx.notify(); }) .detach(); - self.add_participant_popover = Some(view); + self.contacts_popover = Some(view); } } } @@ -110,10 +106,10 @@ impl CollabTitlebarItem { Some( Stack::new() .with_child( - MouseEventHandler::::new(0, cx, |state, _| { + MouseEventHandler::::new(0, cx, |state, _| { let style = titlebar - .add_participant_button - .style_for(state, self.add_participant_popover.is_some()); + .toggle_contacts_button + .style_for(state, self.contacts_popover.is_some()); Svg::new("icons/plus_8.svg") .with_color(style.color) .constrained() @@ -128,18 +124,18 @@ impl CollabTitlebarItem { }) .with_cursor_style(CursorStyle::PointingHand) .on_click(MouseButton::Left, |_, cx| { - cx.dispatch_action(ToggleAddParticipantPopover); + cx.dispatch_action(ToggleContactsPopover); }) .aligned() .boxed(), ) - .with_children(self.add_participant_popover.as_ref().map(|popover| { + .with_children(self.contacts_popover.as_ref().map(|popover| { Overlay::new( ChildView::new(popover) .contained() .with_margin_top(titlebar.height) .with_margin_right( - -titlebar.add_participant_button.default.button_width, + -titlebar.toggle_contacts_button.default.button_width, ) .boxed(), ) diff --git a/crates/collab_titlebar_item/src/add_participant_popover.rs b/crates/collab_titlebar_item/src/contacts_popover.rs similarity index 97% rename from crates/collab_titlebar_item/src/add_participant_popover.rs rename to crates/collab_titlebar_item/src/contacts_popover.rs index b6fa8125f7566d5889c4f8651879d8b0891a1194..e44f6a23312577e80a2365abbddccf1ed31cf724 100644 --- a/crates/collab_titlebar_item/src/add_participant_popover.rs +++ b/crates/collab_titlebar_item/src/contacts_popover.rs @@ -15,11 +15,11 @@ use theme::IconButton; impl_internal_actions!(contacts_panel, [ToggleExpanded]); pub fn init(cx: &mut MutableAppContext) { - cx.add_action(AddParticipantPopover::clear_filter); - cx.add_action(AddParticipantPopover::select_next); - cx.add_action(AddParticipantPopover::select_prev); - cx.add_action(AddParticipantPopover::confirm); - cx.add_action(AddParticipantPopover::toggle_expanded); + cx.add_action(ContactsPopover::clear_filter); + cx.add_action(ContactsPopover::select_next); + cx.add_action(ContactsPopover::select_prev); + cx.add_action(ContactsPopover::confirm); + cx.add_action(ContactsPopover::toggle_expanded); } #[derive(Clone, PartialEq)] @@ -72,7 +72,7 @@ pub enum Event { Dismissed, } -pub struct AddParticipantPopover { +pub struct ContactsPopover { entries: Vec, match_candidates: Vec, list_state: ListState, @@ -83,7 +83,7 @@ pub struct AddParticipantPopover { _maintain_contacts: Subscription, } -impl AddParticipantPopover { +impl ContactsPopover { pub fn new(user_store: ModelHandle, cx: &mut ViewContext) -> Self { let filter_editor = cx.add_view(|cx| { let mut editor = Editor::single_line( @@ -555,13 +555,13 @@ impl AddParticipantPopover { } } -impl Entity for AddParticipantPopover { +impl Entity for ContactsPopover { type Event = Event; } -impl View for AddParticipantPopover { +impl View for ContactsPopover { fn ui_name() -> &'static str { - "AddParticipantPopover" + "ContactsPopover" } fn keymap_context(&self, _: &AppContext) -> keymap::Context { @@ -657,10 +657,10 @@ impl View for AddParticipantPopover { }), ) .contained() - .with_style(theme.workspace.titlebar.add_participant_popover.container) + .with_style(theme.workspace.titlebar.contacts_popover.container) .constrained() - .with_width(theme.workspace.titlebar.add_participant_popover.width) - .with_height(theme.workspace.titlebar.add_participant_popover.height) + .with_width(theme.workspace.titlebar.contacts_popover.width) + .with_height(theme.workspace.titlebar.contacts_popover.height) .boxed() } diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 4192bc0752c51ea3835b3a98631cb564a20b1672..28c8eb30917ac581b5d6e2b82c4f723959d29a63 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -73,8 +73,8 @@ pub struct Titlebar { pub avatar: ImageStyle, pub sign_in_prompt: Interactive, pub outdated_warning: ContainedText, - pub add_participant_button: Interactive, - pub add_participant_popover: AddParticipantPopover, + pub toggle_contacts_button: Interactive, + pub contacts_popover: AddParticipantPopover, } #[derive(Clone, Deserialize, Default)] diff --git a/styles/src/styleTree/workspace.ts b/styles/src/styleTree/workspace.ts index 156ed62fbaef7c465eb6fdef6a2f9adcdd498ed9..8bd1e3800fe5ff50c02b46c65c437242ef412fcd 100644 --- a/styles/src/styleTree/workspace.ts +++ b/styles/src/styleTree/workspace.ts @@ -119,7 +119,7 @@ export default function workspace(theme: Theme) { }, cornerRadius: 6, }, - addParticipantButton: { + toggleContactsButton: { cornerRadius: 6, color: iconColor(theme, "secondary"), iconWidth: 8, @@ -133,7 +133,7 @@ export default function workspace(theme: Theme) { color: iconColor(theme, "active"), }, }, - addParticipantPopover: { + contactsPopover: { background: backgroundColor(theme, 300, "base"), cornerRadius: 6, padding: { top: 6 }, From 8ff4f044b724d5bcdbe72b4bb42f2c14c9e10f58 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 28 Sep 2022 11:02:26 -0600 Subject: [PATCH 024/112] Start a call when clicking on a contact in the contacts popover Co-Authored-By: Antonio Scandurra --- Cargo.lock | 2 + crates/collab/src/integration_tests.rs | 20 +-- crates/collab_titlebar_item/Cargo.toml | 3 + .../src/collab_titlebar_item.rs | 3 +- .../src/contacts_popover.rs | 163 +++++++++++++++--- crates/gpui/src/app.rs | 11 ++ crates/room/Cargo.toml | 3 + crates/room/src/room.rs | 74 +++++++- crates/zed/src/zed.rs | 3 +- 9 files changed, 229 insertions(+), 53 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c06a3ee93cc1abd90b71e362771dcd8ccd5727ba..64202ed1c7a25ea63a4908cd86f57a0c6b76ed52 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1078,6 +1078,7 @@ dependencies = [ "menu", "postage", "project", + "room", "serde", "settings", "theme", @@ -4461,6 +4462,7 @@ dependencies = [ "futures", "gpui", "project", + "util", ] [[package]] diff --git a/crates/collab/src/integration_tests.rs b/crates/collab/src/integration_tests.rs index e04bd80c795bb0b4b90f2de98ebef7b9fd376d70..bd60893a57aee911320298519de672ced6bb567c 100644 --- a/crates/collab/src/integration_tests.rs +++ b/crates/collab/src/integration_tests.rs @@ -83,7 +83,7 @@ async fn test_basic_calls( .await; let room_a = cx_a - .update(|cx| Room::create(client_a.clone(), cx)) + .update(|cx| Room::create(client_a.clone(), client_a.user_store.clone(), cx)) .await .unwrap(); assert_eq!( @@ -125,7 +125,7 @@ async fn test_basic_calls( // User B joins the room using the first client. let room_b = cx_b - .update(|cx| Room::join(&call_b, client_b.clone(), cx)) + .update(|cx| Room::join(&call_b, client_b.clone(), client_b.user_store.clone(), cx)) .await .unwrap(); assert!(incoming_call_b.next().await.unwrap().is_none()); @@ -229,7 +229,7 @@ async fn test_leaving_room_on_disconnection( .await; let room_a = cx_a - .update(|cx| Room::create(client_a.clone(), cx)) + .update(|cx| Room::create(client_a.clone(), client_a.user_store.clone(), cx)) .await .unwrap(); @@ -245,7 +245,7 @@ async fn test_leaving_room_on_disconnection( // User B receives the call and joins the room. let call_b = incoming_call_b.next().await.unwrap().unwrap(); let room_b = cx_b - .update(|cx| Room::join(&call_b, client_b.clone(), cx)) + .update(|cx| Room::join(&call_b, client_b.clone(), client_b.user_store.clone(), cx)) .await .unwrap(); deterministic.run_until_parked(); @@ -6284,17 +6284,9 @@ async fn room_participants( .collect::>() }); let remote_users = futures::future::try_join_all(remote_users).await.unwrap(); - let pending_users = room.update(cx, |room, cx| { - room.pending_user_ids() - .iter() - .map(|user_id| { - client - .user_store - .update(cx, |users, cx| users.get_user(*user_id, cx)) - }) - .collect::>() + let pending_users = room.read_with(cx, |room, _| { + room.pending_users().iter().cloned().collect::>() }); - let pending_users = futures::future::try_join_all(pending_users).await.unwrap(); RoomParticipants { remote: remote_users diff --git a/crates/collab_titlebar_item/Cargo.toml b/crates/collab_titlebar_item/Cargo.toml index 4f85ddd8d9b76acf072440d57ebc580f533efd93..fbdfb34386acb9702a7841de5beb50630ae52550 100644 --- a/crates/collab_titlebar_item/Cargo.toml +++ b/crates/collab_titlebar_item/Cargo.toml @@ -14,6 +14,7 @@ test-support = [ "editor/test-support", "gpui/test-support", "project/test-support", + "room/test-support", "settings/test-support", "util/test-support", "workspace/test-support", @@ -28,6 +29,7 @@ fuzzy = { path = "../fuzzy" } gpui = { path = "../gpui" } menu = { path = "../menu" } project = { path = "../project" } +room = { path = "../room" } settings = { path = "../settings" } theme = { path = "../theme" } util = { path = "../util" } @@ -44,6 +46,7 @@ collections = { path = "../collections", features = ["test-support"] } editor = { path = "../editor", features = ["test-support"] } gpui = { path = "../gpui", features = ["test-support"] } project = { path = "../project", features = ["test-support"] } +room = { path = "../room", features = ["test-support"] } settings = { path = "../settings", features = ["test-support"] } util = { path = "../util", features = ["test-support"] } workspace = { path = "../workspace", features = ["test-support"] } diff --git a/crates/collab_titlebar_item/src/collab_titlebar_item.rs b/crates/collab_titlebar_item/src/collab_titlebar_item.rs index c27a2b3452e08fe9828dd63235882ab73df43028..f9da9e5b7a70cd75f27f52246736dd4722c28406 100644 --- a/crates/collab_titlebar_item/src/collab_titlebar_item.rs +++ b/crates/collab_titlebar_item/src/collab_titlebar_item.rs @@ -71,8 +71,9 @@ impl CollabTitlebarItem { Some(_) => {} None => { if let Some(workspace) = self.workspace.upgrade(cx) { + let client = workspace.read(cx).client().clone(); let user_store = workspace.read(cx).user_store().clone(); - let view = cx.add_view(|cx| ContactsPopover::new(user_store, cx)); + let view = cx.add_view(|cx| ContactsPopover::new(client, user_store, cx)); cx.focus(&view); cx.subscribe(&view, |this, _, event, cx| { match event { diff --git a/crates/collab_titlebar_item/src/contacts_popover.rs b/crates/collab_titlebar_item/src/contacts_popover.rs index e44f6a23312577e80a2365abbddccf1ed31cf724..26c1194d748ae491c2606cedae4b42403b57bace 100644 --- a/crates/collab_titlebar_item/src/contacts_popover.rs +++ b/crates/collab_titlebar_item/src/contacts_popover.rs @@ -1,6 +1,6 @@ use std::sync::Arc; -use client::{Contact, User, UserStore}; +use client::{Client, Contact, User, UserStore}; use editor::{Cancel, Editor}; use fuzzy::{match_strings, StringMatchCandidate}; use gpui::{ @@ -9,10 +9,11 @@ use gpui::{ ViewHandle, }; use menu::{Confirm, SelectNext, SelectPrev}; +use room::Room; use settings::Settings; use theme::IconButton; -impl_internal_actions!(contacts_panel, [ToggleExpanded]); +impl_internal_actions!(contacts_panel, [ToggleExpanded, Call]); pub fn init(cx: &mut MutableAppContext) { cx.add_action(ContactsPopover::clear_filter); @@ -20,11 +21,17 @@ pub fn init(cx: &mut MutableAppContext) { cx.add_action(ContactsPopover::select_prev); cx.add_action(ContactsPopover::confirm); cx.add_action(ContactsPopover::toggle_expanded); + cx.add_action(ContactsPopover::call); } #[derive(Clone, PartialEq)] struct ToggleExpanded(Section); +#[derive(Clone, PartialEq)] +struct Call { + recipient_user_id: u64, +} + #[derive(Clone, Copy, PartialEq, Eq, Debug, PartialOrd, Ord)] enum Section { Requests, @@ -73,18 +80,24 @@ pub enum Event { } pub struct ContactsPopover { + room: Option<(ModelHandle, Subscription)>, entries: Vec, match_candidates: Vec, list_state: ListState, + client: Arc, user_store: ModelHandle, filter_editor: ViewHandle, collapsed_sections: Vec
, selection: Option, - _maintain_contacts: Subscription, + _subscriptions: Vec, } impl ContactsPopover { - pub fn new(user_store: ModelHandle, cx: &mut ViewContext) -> Self { + pub fn new( + client: Arc, + user_store: ModelHandle, + cx: &mut ViewContext, + ) -> Self { let filter_editor = cx.add_view(|cx| { let mut editor = Editor::single_line( Some(|theme| theme.contacts_panel.user_query_editor.clone()), @@ -143,25 +156,52 @@ impl ContactsPopover { cx, ), ContactEntry::Contact(contact) => { - Self::render_contact(&contact.user, &theme.contacts_panel, is_selected) + Self::render_contact(contact, &theme.contacts_panel, is_selected, cx) } } }); + let mut subscriptions = Vec::new(); + subscriptions.push(cx.observe(&user_store, |this, _, cx| this.update_entries(cx))); + + let weak_self = cx.weak_handle(); + subscriptions.push(Room::observe(cx, move |room, cx| { + if let Some(this) = weak_self.upgrade(cx) { + this.update(cx, |this, cx| this.set_room(room, cx)); + } + })); + let mut this = Self { + room: None, list_state, selection: None, collapsed_sections: Default::default(), entries: Default::default(), match_candidates: Default::default(), filter_editor, - _maintain_contacts: cx.observe(&user_store, |this, _, cx| this.update_entries(cx)), + _subscriptions: subscriptions, + client, user_store, }; this.update_entries(cx); this } + fn set_room(&mut self, room: Option>, cx: &mut ViewContext) { + if let Some(room) = room { + let observation = cx.observe(&room, |this, room, cx| this.room_updated(room, cx)); + self.room = Some((room, observation)); + } else { + self.room = None; + } + + cx.notify(); + } + + fn room_updated(&mut self, room: ModelHandle, cx: &mut ViewContext) { + cx.notify(); + } + fn clear_filter(&mut self, _: &Cancel, cx: &mut ViewContext) { let did_clear = self.filter_editor.update(cx, |editor, cx| { if editor.buffer().read(cx).len(cx) > 0 { @@ -357,6 +397,43 @@ impl ContactsPopover { cx.notify(); } + fn render_active_call(&self, cx: &mut RenderContext) -> Option { + let (room, _) = self.room.as_ref()?; + let theme = &cx.global::().theme.contacts_panel; + + Some( + Flex::column() + .with_children(room.read(cx).pending_users().iter().map(|user| { + Flex::row() + .with_children(user.avatar.clone().map(|avatar| { + Image::new(avatar) + .with_style(theme.contact_avatar) + .aligned() + .left() + .boxed() + })) + .with_child( + Label::new( + user.github_login.clone(), + theme.contact_username.text.clone(), + ) + .contained() + .with_style(theme.contact_username.container) + .aligned() + .left() + .flex(1., true) + .boxed(), + ) + .constrained() + .with_height(theme.row_height) + .contained() + .with_style(theme.contact_row.default) + .boxed() + })) + .boxed(), + ) + } + fn render_header( section: Section, theme: &theme::ContactsPanel, @@ -412,32 +489,46 @@ impl ContactsPopover { .boxed() } - fn render_contact(user: &User, theme: &theme::ContactsPanel, is_selected: bool) -> ElementBox { - Flex::row() - .with_children(user.avatar.clone().map(|avatar| { - Image::new(avatar) - .with_style(theme.contact_avatar) + fn render_contact( + contact: &Contact, + theme: &theme::ContactsPanel, + is_selected: bool, + cx: &mut RenderContext, + ) -> ElementBox { + let user_id = contact.user.id; + MouseEventHandler::::new(contact.user.id as usize, cx, |_, _| { + Flex::row() + .with_children(contact.user.avatar.clone().map(|avatar| { + Image::new(avatar) + .with_style(theme.contact_avatar) + .aligned() + .left() + .boxed() + })) + .with_child( + Label::new( + contact.user.github_login.clone(), + theme.contact_username.text.clone(), + ) + .contained() + .with_style(theme.contact_username.container) .aligned() .left() - .boxed() - })) - .with_child( - Label::new( - user.github_login.clone(), - theme.contact_username.text.clone(), + .flex(1., true) + .boxed(), ) + .constrained() + .with_height(theme.row_height) .contained() - .with_style(theme.contact_username.container) - .aligned() - .left() - .flex(1., true) - .boxed(), - ) - .constrained() - .with_height(theme.row_height) - .contained() - .with_style(*theme.contact_row.style_for(Default::default(), is_selected)) - .boxed() + .with_style(*theme.contact_row.style_for(Default::default(), is_selected)) + .boxed() + }) + .on_click(MouseButton::Left, move |_, cx| { + cx.dispatch_action(Call { + recipient_user_id: user_id, + }) + }) + .boxed() } fn render_contact_request( @@ -553,6 +644,21 @@ impl ContactsPopover { .with_style(*theme.contact_row.style_for(Default::default(), is_selected)) .boxed() } + + fn call(&mut self, action: &Call, cx: &mut ViewContext) { + let client = self.client.clone(); + let user_store = self.user_store.clone(); + let recipient_user_id = action.recipient_user_id; + cx.spawn_weak(|_, mut cx| async move { + let room = cx + .update(|cx| Room::get_or_create(&client, &user_store, cx)) + .await?; + room.update(&mut cx, |room, cx| room.call(recipient_user_id, cx)) + .await?; + anyhow::Ok(()) + }) + .detach(); + } } impl Entity for ContactsPopover { @@ -606,6 +712,7 @@ impl View for ContactsPopover { .with_height(theme.contacts_panel.user_query_editor_height) .boxed(), ) + .with_children(self.render_active_call(cx)) .with_child(List::new(self.list_state.clone()).flex(1., false).boxed()) .with_children( self.user_store diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 308ea6c831757326e799eb5fa7a36d035335d1da..18a2f8a4d0820607b55eb2f14a4791ca3b777fd0 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -1519,6 +1519,17 @@ impl MutableAppContext { } } + pub fn observe_default_global(&mut self, observe: F) -> Subscription + where + G: Any + Default, + F: 'static + FnMut(&mut MutableAppContext), + { + if !self.has_global::() { + self.set_global(G::default()); + } + self.observe_global::(observe) + } + pub fn observe_release(&mut self, handle: &H, callback: F) -> Subscription where E: Entity, diff --git a/crates/room/Cargo.toml b/crates/room/Cargo.toml index 33b6620b2758f7cdc3fccb329c0bcf8585c5d0fd..169f04d35218373adf0ed111007c9dfdc877a3a9 100644 --- a/crates/room/Cargo.toml +++ b/crates/room/Cargo.toml @@ -13,6 +13,7 @@ test-support = [ "collections/test-support", "gpui/test-support", "project/test-support", + "util/test-support" ] [dependencies] @@ -20,6 +21,7 @@ client = { path = "../client" } collections = { path = "../collections" } gpui = { path = "../gpui" } project = { path = "../project" } +util = { path = "../util" } anyhow = "1.0.38" futures = "0.3" @@ -29,3 +31,4 @@ client = { path = "../client", features = ["test-support"] } collections = { path = "../collections", features = ["test-support"] } gpui = { path = "../gpui", features = ["test-support"] } project = { path = "../project", features = ["test-support"] } +util = { path = "../util", features = ["test-support"] } diff --git a/crates/room/src/room.rs b/crates/room/src/room.rs index 82363d4b19868ff7ef9e3a9ee08acc9979799391..f7d5a58fa6aec6019367e699ae345959d0b85eec 100644 --- a/crates/room/src/room.rs +++ b/crates/room/src/room.rs @@ -1,13 +1,14 @@ mod participant; use anyhow::{anyhow, Result}; -use client::{call::Call, proto, Client, PeerId, TypedEnvelope}; +use client::{call::Call, proto, Client, PeerId, TypedEnvelope, User, UserStore}; use collections::HashMap; use futures::StreamExt; use gpui::{AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Task}; use participant::{LocalParticipant, ParticipantLocation, RemoteParticipant}; use project::Project; use std::sync::Arc; +use util::ResultExt; pub enum Event { PeerChangedActiveProject, @@ -18,9 +19,11 @@ pub struct Room { status: RoomStatus, local_participant: LocalParticipant, remote_participants: HashMap, - pending_user_ids: Vec, + pending_users: Vec>, client: Arc, + user_store: ModelHandle, _subscriptions: Vec, + _load_pending_users: Option>, } impl Entity for Room { @@ -28,7 +31,44 @@ impl Entity for Room { } impl Room { - fn new(id: u64, client: Arc, cx: &mut ModelContext) -> Self { + pub fn observe(cx: &mut MutableAppContext, mut callback: F) -> gpui::Subscription + where + F: 'static + FnMut(Option>, &mut MutableAppContext), + { + cx.observe_default_global::>, _>(move |cx| { + let room = cx.global::>>().clone(); + callback(room, cx); + }) + } + + pub fn get_or_create( + client: &Arc, + user_store: &ModelHandle, + cx: &mut MutableAppContext, + ) -> Task>> { + if let Some(room) = cx.global::>>() { + Task::ready(Ok(room.clone())) + } else { + let client = client.clone(); + let user_store = user_store.clone(); + cx.spawn(|mut cx| async move { + let room = cx.update(|cx| Room::create(client, user_store, cx)).await?; + cx.update(|cx| cx.set_global(Some(room.clone()))); + Ok(room) + }) + } + } + + pub fn clear(cx: &mut MutableAppContext) { + cx.set_global::>>(None); + } + + fn new( + id: u64, + client: Arc, + user_store: ModelHandle, + cx: &mut ModelContext, + ) -> Self { let mut client_status = client.status(); cx.spawn_weak(|this, mut cx| async move { let is_connected = client_status @@ -51,32 +91,36 @@ impl Room { projects: Default::default(), }, remote_participants: Default::default(), - pending_user_ids: Default::default(), + pending_users: Default::default(), _subscriptions: vec![client.add_message_handler(cx.handle(), Self::handle_room_updated)], + _load_pending_users: None, client, + user_store, } } pub fn create( client: Arc, + user_store: ModelHandle, cx: &mut MutableAppContext, ) -> Task>> { cx.spawn(|mut cx| async move { let room = client.request(proto::CreateRoom {}).await?; - Ok(cx.add_model(|cx| Self::new(room.id, client, cx))) + Ok(cx.add_model(|cx| Self::new(room.id, client, user_store, cx))) }) } pub fn join( call: &Call, client: Arc, + user_store: ModelHandle, cx: &mut MutableAppContext, ) -> Task>> { let room_id = call.room_id; cx.spawn(|mut cx| async move { let response = client.request(proto::JoinRoom { id: room_id }).await?; let room_proto = response.room.ok_or_else(|| anyhow!("invalid room"))?; - let room = cx.add_model(|cx| Self::new(room_id, client, cx)); + let room = cx.add_model(|cx| Self::new(room_id, client, user_store, cx)); room.update(&mut cx, |room, cx| room.apply_room_update(room_proto, cx))?; Ok(room) }) @@ -98,8 +142,8 @@ impl Room { &self.remote_participants } - pub fn pending_user_ids(&self) -> &[u64] { - &self.pending_user_ids + pub fn pending_users(&self) -> &[Arc] { + &self.pending_users } async fn handle_room_updated( @@ -131,7 +175,19 @@ impl Room { ); } } - self.pending_user_ids = room.pending_user_ids; + + let pending_users = self.user_store.update(cx, move |user_store, cx| { + user_store.get_users(room.pending_user_ids, cx) + }); + self._load_pending_users = Some(cx.spawn(|this, mut cx| async move { + if let Some(pending_users) = pending_users.await.log_err() { + this.update(&mut cx, |this, cx| { + this.pending_users = pending_users; + cx.notify(); + }); + } + })); + cx.notify(); Ok(()) } diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 26888dc0d71e5ebadbda060f8f7b23a553e276ee..bd6f28a4024e561d31ef6a0a690a67b80a0d1a76 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -21,10 +21,11 @@ use gpui::{ geometry::vector::vec2f, impl_actions, platform::{WindowBounds, WindowOptions}, - AssetSource, AsyncAppContext, TitlebarOptions, ViewContext, WindowKind, + AssetSource, AsyncAppContext, ModelHandle, TitlebarOptions, ViewContext, WindowKind, }; use language::Rope; pub use lsp; +use postage::watch; pub use project::{self, fs}; use project_panel::ProjectPanel; use search::{BufferSearchBar, ProjectSearchBar}; From aa3cb8e35e1d89c3542acffde6a83f7e915a1bb1 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 28 Sep 2022 19:14:31 +0200 Subject: [PATCH 025/112] Rename `collab_titlebar_item` crate to `collab_ui` Co-Authored-By: Nathan Sobo --- Cargo.lock | 4 ++-- crates/{collab_titlebar_item => collab_ui}/Cargo.toml | 4 ++-- .../src/collab_titlebar_item.rs | 4 +--- crates/collab_ui/src/collab_ui.rs | 10 ++++++++++ .../src/contacts_popover.rs | 6 +----- crates/zed/Cargo.toml | 2 +- crates/zed/src/main.rs | 2 +- crates/zed/src/zed.rs | 5 ++--- 8 files changed, 20 insertions(+), 17 deletions(-) rename crates/{collab_titlebar_item => collab_ui}/Cargo.toml (95%) rename crates/{collab_titlebar_item => collab_ui}/src/collab_titlebar_item.rs (99%) create mode 100644 crates/collab_ui/src/collab_ui.rs rename crates/{collab_titlebar_item => collab_ui}/src/contacts_popover.rs (99%) diff --git a/Cargo.lock b/Cargo.lock index 64202ed1c7a25ea63a4908cd86f57a0c6b76ed52..655ec8ab03ffe91673300d87dd2d4b00c315bfb8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1063,7 +1063,7 @@ dependencies = [ ] [[package]] -name = "collab_titlebar_item" +name = "collab_ui" version = "0.1.0" dependencies = [ "anyhow", @@ -7159,7 +7159,7 @@ dependencies = [ "cli", "client", "clock", - "collab_titlebar_item", + "collab_ui", "collections", "command_palette", "contacts_panel", diff --git a/crates/collab_titlebar_item/Cargo.toml b/crates/collab_ui/Cargo.toml similarity index 95% rename from crates/collab_titlebar_item/Cargo.toml rename to crates/collab_ui/Cargo.toml index fbdfb34386acb9702a7841de5beb50630ae52550..aa2cdb86283f8156fcf0f7213b0307949d427611 100644 --- a/crates/collab_titlebar_item/Cargo.toml +++ b/crates/collab_ui/Cargo.toml @@ -1,10 +1,10 @@ [package] -name = "collab_titlebar_item" +name = "collab_ui" version = "0.1.0" edition = "2021" [lib] -path = "src/collab_titlebar_item.rs" +path = "src/collab_ui.rs" doctest = false [features] diff --git a/crates/collab_titlebar_item/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs similarity index 99% rename from crates/collab_titlebar_item/src/collab_titlebar_item.rs rename to crates/collab_ui/src/collab_titlebar_item.rs index f9da9e5b7a70cd75f27f52246736dd4722c28406..00f37fddeed84184f160f5ffeac472df558a2b14 100644 --- a/crates/collab_titlebar_item/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -1,5 +1,4 @@ -mod contacts_popover; - +use crate::contacts_popover; use client::{Authenticate, PeerId}; use clock::ReplicaId; use contacts_popover::ContactsPopover; @@ -20,7 +19,6 @@ use workspace::{FollowNextCollaborator, ToggleFollow, Workspace}; actions!(contacts_titlebar_item, [ToggleContactsPopover]); pub fn init(cx: &mut MutableAppContext) { - contacts_popover::init(cx); cx.add_action(CollabTitlebarItem::toggle_contacts_popover); } diff --git a/crates/collab_ui/src/collab_ui.rs b/crates/collab_ui/src/collab_ui.rs new file mode 100644 index 0000000000000000000000000000000000000000..312c7478e2f7b213656d6ee6bc2eb2b80633df6e --- /dev/null +++ b/crates/collab_ui/src/collab_ui.rs @@ -0,0 +1,10 @@ +mod collab_titlebar_item; +mod contacts_popover; + +pub use collab_titlebar_item::CollabTitlebarItem; +use gpui::MutableAppContext; + +pub fn init(cx: &mut MutableAppContext) { + contacts_popover::init(cx); + collab_titlebar_item::init(cx); +} diff --git a/crates/collab_titlebar_item/src/contacts_popover.rs b/crates/collab_ui/src/contacts_popover.rs similarity index 99% rename from crates/collab_titlebar_item/src/contacts_popover.rs rename to crates/collab_ui/src/contacts_popover.rs index 26c1194d748ae491c2606cedae4b42403b57bace..624030fe462a1c1b6af25cae7c1bcea15339a1e8 100644 --- a/crates/collab_titlebar_item/src/contacts_popover.rs +++ b/crates/collab_ui/src/contacts_popover.rs @@ -189,7 +189,7 @@ impl ContactsPopover { fn set_room(&mut self, room: Option>, cx: &mut ViewContext) { if let Some(room) = room { - let observation = cx.observe(&room, |this, room, cx| this.room_updated(room, cx)); + let observation = cx.observe(&room, |_, _, cx| cx.notify()); self.room = Some((room, observation)); } else { self.room = None; @@ -198,10 +198,6 @@ impl ContactsPopover { cx.notify(); } - fn room_updated(&mut self, room: ModelHandle, cx: &mut ViewContext) { - cx.notify(); - } - fn clear_filter(&mut self, _: &Cancel, cx: &mut ViewContext) { let did_clear = self.filter_editor.update(cx, |editor, cx| { if editor.buffer().read(cx).len(cx) > 0 { diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 82629705a169c8ec3c0bcb8dfa68b01aa7b4ab5a..667e3d79847b5a29f134707ee9aeba148080f945 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -21,7 +21,7 @@ auto_update = { path = "../auto_update" } breadcrumbs = { path = "../breadcrumbs" } chat_panel = { path = "../chat_panel" } cli = { path = "../cli" } -collab_titlebar_item = { path = "../collab_titlebar_item" } +collab_ui = { path = "../collab_ui" } collections = { path = "../collections" } command_palette = { path = "../command_palette" } context_menu = { path = "../context_menu" } diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 99983c4d6dc2def796ac4d498f1ba25aeb8d9f91..6233f0a0371f4aba6886aaa584117d335850acb3 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -107,7 +107,7 @@ fn main() { project::Project::init(&client); client::Channel::init(&client); client::init(client.clone(), cx); - collab_titlebar_item::init(cx); + collab_ui::init(cx); command_palette::init(cx); editor::init(cx); go_to_line::init(cx); diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index bd6f28a4024e561d31ef6a0a690a67b80a0d1a76..a4cc8da6334d628f3c94e9da635a7efdf71c2b2f 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -10,7 +10,7 @@ use anyhow::{anyhow, Context, Result}; use assets::Assets; use breadcrumbs::Breadcrumbs; pub use client; -use collab_titlebar_item::CollabTitlebarItem; +use collab_ui::CollabTitlebarItem; use collections::VecDeque; pub use contacts_panel; use contacts_panel::ContactsPanel; @@ -21,11 +21,10 @@ use gpui::{ geometry::vector::vec2f, impl_actions, platform::{WindowBounds, WindowOptions}, - AssetSource, AsyncAppContext, ModelHandle, TitlebarOptions, ViewContext, WindowKind, + AssetSource, AsyncAppContext, TitlebarOptions, ViewContext, WindowKind, }; use language::Rope; pub use lsp; -use postage::watch; pub use project::{self, fs}; use project_panel::ProjectPanel; use search::{BufferSearchBar, ProjectSearchBar}; From 46b61feb9aad46bfc8ddd35e2d52d30c9e4bb361 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 28 Sep 2022 19:35:24 +0200 Subject: [PATCH 026/112] Open popup window when receiving a call We still need to style and allow people to accept the call but this is a good starting point. Co-Authored-By: Nathan Sobo --- crates/collab_ui/src/collab_ui.rs | 80 ++++++++++++++++++++++++++++++- crates/gpui/src/app.rs | 4 ++ crates/zed/src/main.rs | 2 +- 3 files changed, 83 insertions(+), 3 deletions(-) diff --git a/crates/collab_ui/src/collab_ui.rs b/crates/collab_ui/src/collab_ui.rs index 312c7478e2f7b213656d6ee6bc2eb2b80633df6e..d3f12fdf6faa00552800559149191336135c7757 100644 --- a/crates/collab_ui/src/collab_ui.rs +++ b/crates/collab_ui/src/collab_ui.rs @@ -1,10 +1,86 @@ mod collab_titlebar_item; mod contacts_popover; +use client::{call::Call, UserStore}; pub use collab_titlebar_item::CollabTitlebarItem; -use gpui::MutableAppContext; +use futures::StreamExt; +use gpui::{ + elements::*, + geometry::{rect::RectF, vector::vec2f}, + Entity, ModelHandle, MutableAppContext, View, WindowBounds, WindowKind, WindowOptions, +}; +use settings::Settings; -pub fn init(cx: &mut MutableAppContext) { +pub fn init(user_store: ModelHandle, cx: &mut MutableAppContext) { contacts_popover::init(cx); collab_titlebar_item::init(cx); + + let mut incoming_call = user_store.read(cx).incoming_call(); + cx.spawn(|mut cx| async move { + let mut notification_window = None; + while let Some(incoming_call) = incoming_call.next().await { + if let Some(window_id) = notification_window.take() { + cx.remove_window(window_id); + } + + if let Some(incoming_call) = incoming_call { + let (window_id, _) = cx.add_window( + WindowOptions { + bounds: WindowBounds::Fixed(RectF::new(vec2f(0., 0.), vec2f(300., 400.))), + titlebar: None, + center: true, + kind: WindowKind::PopUp, + is_movable: false, + }, + |_| IncomingCallNotification::new(incoming_call), + ); + notification_window = Some(window_id); + } + } + }) + .detach(); +} + +struct IncomingCallNotification { + call: Call, +} + +impl IncomingCallNotification { + fn new(call: Call) -> Self { + Self { call } + } +} + +impl Entity for IncomingCallNotification { + type Event = (); +} + +impl View for IncomingCallNotification { + fn ui_name() -> &'static str { + "IncomingCallNotification" + } + + fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> gpui::ElementBox { + let theme = &cx.global::().theme.contacts_panel; + Flex::row() + .with_children(self.call.from.avatar.clone().map(|avatar| { + Image::new(avatar) + .with_style(theme.contact_avatar) + .aligned() + .left() + .boxed() + })) + .with_child( + Label::new( + self.call.from.github_login.clone(), + theme.contact_username.text.clone(), + ) + .contained() + .aligned() + .left() + .flex(1., true) + .boxed(), + ) + .boxed() + } } diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 18a2f8a4d0820607b55eb2f14a4791ca3b777fd0..04e27a8279836a2bc4c7b4ea89fae6bf18d9dc48 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -786,6 +786,10 @@ impl AsyncAppContext { self.update(|cx| cx.add_window(window_options, build_root_view)) } + pub fn remove_window(&mut self, window_id: usize) { + self.update(|cx| cx.remove_window(window_id)) + } + pub fn platform(&self) -> Arc { self.0.borrow().platform() } diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 6233f0a0371f4aba6886aaa584117d335850acb3..de769a6e5ec24c262d4881a3b4aeea393967f342 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -107,7 +107,7 @@ fn main() { project::Project::init(&client); client::Channel::init(&client); client::init(client.clone(), cx); - collab_ui::init(cx); + collab_ui::init(user_store.clone(), cx); command_palette::init(cx); editor::init(cx); go_to_line::init(cx); From 04d194924e471eed785e13446fb9d372b0643817 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 28 Sep 2022 19:50:13 +0200 Subject: [PATCH 027/112] WIP: Start on `ActiveCall` Co-Authored-By: Nathan Sobo --- crates/room/src/active_call.rs | 55 ++++++++++++++++++++++++++++++++++ crates/room/src/room.rs | 1 + 2 files changed, 56 insertions(+) create mode 100644 crates/room/src/active_call.rs diff --git a/crates/room/src/active_call.rs b/crates/room/src/active_call.rs new file mode 100644 index 0000000000000000000000000000000000000000..63ca9583c2074af40fb2ae9eb593fda3bf03f141 --- /dev/null +++ b/crates/room/src/active_call.rs @@ -0,0 +1,55 @@ +use crate::Room; +use gpui::{Entity, ModelHandle, MutableAppContext}; + +#[derive(Default)] +pub struct ActiveCall { + room: Option>, +} + +impl Entity for ActiveCall { + type Event = (); +} + +impl ActiveCall { + pub fn global(cx: &mut MutableAppContext) -> ModelHandle { + if cx.has_global::>() { + let active_call = cx.add_model(|_| ActiveCall::default()); + cx.set_global(active_call.clone()); + active_call + } else { + cx.global::>().clone() + } + } + + pub fn observe(cx: &mut MutableAppContext, mut callback: F) -> gpui::Subscription + where + F: 'static + FnMut(Option>, &mut MutableAppContext), + { + cx.observe_default_global::>, _>(move |cx| { + let room = cx.global::>>().clone(); + callback(room, cx); + }) + } + + pub fn get_or_create( + client: &Arc, + user_store: &ModelHandle, + cx: &mut MutableAppContext, + ) -> Task>> { + if let Some(room) = cx.global::>>() { + Task::ready(Ok(room.clone())) + } else { + let client = client.clone(); + let user_store = user_store.clone(); + cx.spawn(|mut cx| async move { + let room = cx.update(|cx| Room::create(client, user_store, cx)).await?; + cx.update(|cx| cx.set_global(Some(room.clone()))); + Ok(room) + }) + } + } + + pub fn clear(cx: &mut MutableAppContext) { + cx.set_global::>>(None); + } +} diff --git a/crates/room/src/room.rs b/crates/room/src/room.rs index f7d5a58fa6aec6019367e699ae345959d0b85eec..ba0a37e98091a60626d19de63f5b1a00a38dbf9d 100644 --- a/crates/room/src/room.rs +++ b/crates/room/src/room.rs @@ -1,3 +1,4 @@ +mod active_call; mod participant; use anyhow::{anyhow, Result}; From 634f9de7e6152d28108cac94ac266f2efc77953b Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 29 Sep 2022 10:48:51 +0200 Subject: [PATCH 028/112] Avoid using global for `Room` and extract that logic into `ActiveCall` --- crates/collab_ui/src/contacts_popover.rs | 40 ++++++-------- crates/room/src/active_call.rs | 67 +++++++++++++++++------- crates/room/src/room.rs | 33 +----------- 3 files changed, 63 insertions(+), 77 deletions(-) diff --git a/crates/collab_ui/src/contacts_popover.rs b/crates/collab_ui/src/contacts_popover.rs index 624030fe462a1c1b6af25cae7c1bcea15339a1e8..2ea0c75623d570f6122db05758c85b22823fea05 100644 --- a/crates/collab_ui/src/contacts_popover.rs +++ b/crates/collab_ui/src/contacts_popover.rs @@ -9,7 +9,7 @@ use gpui::{ ViewHandle, }; use menu::{Confirm, SelectNext, SelectPrev}; -use room::Room; +use room::{ActiveCall, Room}; use settings::Settings; use theme::IconButton; @@ -80,7 +80,7 @@ pub enum Event { } pub struct ContactsPopover { - room: Option<(ModelHandle, Subscription)>, + room_subscription: Option, entries: Vec, match_candidates: Vec, list_state: ListState, @@ -161,18 +161,20 @@ impl ContactsPopover { } }); + let active_call = ActiveCall::global(cx); let mut subscriptions = Vec::new(); subscriptions.push(cx.observe(&user_store, |this, _, cx| this.update_entries(cx))); - - let weak_self = cx.weak_handle(); - subscriptions.push(Room::observe(cx, move |room, cx| { - if let Some(this) = weak_self.upgrade(cx) { - this.update(cx, |this, cx| this.set_room(room, cx)); + subscriptions.push(cx.observe(&active_call, |this, active_call, cx| { + if let Some(room) = active_call.read(cx).room().cloned() { + this.room_subscription = Some(cx.observe(&room, |_, _, cx| cx.notify())); + } else { + this.room_subscription = None; } + cx.notify(); })); let mut this = Self { - room: None, + room_subscription: None, list_state, selection: None, collapsed_sections: Default::default(), @@ -187,17 +189,6 @@ impl ContactsPopover { this } - fn set_room(&mut self, room: Option>, cx: &mut ViewContext) { - if let Some(room) = room { - let observation = cx.observe(&room, |_, _, cx| cx.notify()); - self.room = Some((room, observation)); - } else { - self.room = None; - } - - cx.notify(); - } - fn clear_filter(&mut self, _: &Cancel, cx: &mut ViewContext) { let did_clear = self.filter_editor.update(cx, |editor, cx| { if editor.buffer().read(cx).len(cx) > 0 { @@ -394,7 +385,7 @@ impl ContactsPopover { } fn render_active_call(&self, cx: &mut RenderContext) -> Option { - let (room, _) = self.room.as_ref()?; + let room = ActiveCall::global(cx).read(cx).room()?; let theme = &cx.global::().theme.contacts_panel; Some( @@ -642,13 +633,12 @@ impl ContactsPopover { } fn call(&mut self, action: &Call, cx: &mut ViewContext) { - let client = self.client.clone(); - let user_store = self.user_store.clone(); let recipient_user_id = action.recipient_user_id; + let room = ActiveCall::global(cx).update(cx, |active_call, cx| { + active_call.get_or_create(&self.client, &self.user_store, cx) + }); cx.spawn_weak(|_, mut cx| async move { - let room = cx - .update(|cx| Room::get_or_create(&client, &user_store, cx)) - .await?; + let room = room.await?; room.update(&mut cx, |room, cx| room.call(recipient_user_id, cx)) .await?; anyhow::Ok(()) diff --git a/crates/room/src/active_call.rs b/crates/room/src/active_call.rs index 63ca9583c2074af40fb2ae9eb593fda3bf03f141..de0bc5e6397c7faa1b0ba4b7afd55e780d04e5ce 100644 --- a/crates/room/src/active_call.rs +++ b/crates/room/src/active_call.rs @@ -1,5 +1,9 @@ use crate::Room; -use gpui::{Entity, ModelHandle, MutableAppContext}; +use anyhow::{anyhow, Result}; +use client::{call::Call, Client, UserStore}; +use gpui::{Entity, ModelContext, ModelHandle, MutableAppContext, Task}; +use std::sync::Arc; +use util::ResultExt; #[derive(Default)] pub struct ActiveCall { @@ -13,43 +17,66 @@ impl Entity for ActiveCall { impl ActiveCall { pub fn global(cx: &mut MutableAppContext) -> ModelHandle { if cx.has_global::>() { + cx.global::>().clone() + } else { let active_call = cx.add_model(|_| ActiveCall::default()); cx.set_global(active_call.clone()); active_call - } else { - cx.global::>().clone() } } - pub fn observe(cx: &mut MutableAppContext, mut callback: F) -> gpui::Subscription - where - F: 'static + FnMut(Option>, &mut MutableAppContext), - { - cx.observe_default_global::>, _>(move |cx| { - let room = cx.global::>>().clone(); - callback(room, cx); - }) - } - pub fn get_or_create( + &mut self, client: &Arc, user_store: &ModelHandle, - cx: &mut MutableAppContext, + cx: &mut ModelContext, ) -> Task>> { - if let Some(room) = cx.global::>>() { - Task::ready(Ok(room.clone())) + if let Some(room) = self.room.clone() { + Task::ready(Ok(room)) } else { let client = client.clone(); let user_store = user_store.clone(); - cx.spawn(|mut cx| async move { + cx.spawn(|this, mut cx| async move { let room = cx.update(|cx| Room::create(client, user_store, cx)).await?; - cx.update(|cx| cx.set_global(Some(room.clone()))); + this.update(&mut cx, |this, cx| { + this.room = Some(room.clone()); + cx.notify(); + }); Ok(room) }) } } - pub fn clear(cx: &mut MutableAppContext) { - cx.set_global::>>(None); + pub fn join( + &mut self, + call: &Call, + client: &Arc, + user_store: &ModelHandle, + cx: &mut ModelContext, + ) -> Task>> { + if self.room.is_some() { + return Task::ready(Err(anyhow!("cannot join while on another call"))); + } + + let join = Room::join(call, client.clone(), user_store.clone(), cx); + cx.spawn(|this, mut cx| async move { + let room = join.await?; + this.update(&mut cx, |this, cx| { + this.room = Some(room.clone()); + cx.notify(); + }); + Ok(room) + }) + } + + pub fn room(&self) -> Option<&ModelHandle> { + self.room.as_ref() + } + + pub fn clear(&mut self, cx: &mut ModelContext) { + if let Some(room) = self.room.take() { + room.update(cx, |room, cx| room.leave(cx)).log_err(); + cx.notify(); + } } } diff --git a/crates/room/src/room.rs b/crates/room/src/room.rs index ba0a37e98091a60626d19de63f5b1a00a38dbf9d..229ee69aeb7dbbb356af00ef5900fffaddf78cbe 100644 --- a/crates/room/src/room.rs +++ b/crates/room/src/room.rs @@ -1,6 +1,7 @@ mod active_call; mod participant; +pub use active_call::ActiveCall; use anyhow::{anyhow, Result}; use client::{call::Call, proto, Client, PeerId, TypedEnvelope, User, UserStore}; use collections::HashMap; @@ -32,38 +33,6 @@ impl Entity for Room { } impl Room { - pub fn observe(cx: &mut MutableAppContext, mut callback: F) -> gpui::Subscription - where - F: 'static + FnMut(Option>, &mut MutableAppContext), - { - cx.observe_default_global::>, _>(move |cx| { - let room = cx.global::>>().clone(); - callback(room, cx); - }) - } - - pub fn get_or_create( - client: &Arc, - user_store: &ModelHandle, - cx: &mut MutableAppContext, - ) -> Task>> { - if let Some(room) = cx.global::>>() { - Task::ready(Ok(room.clone())) - } else { - let client = client.clone(); - let user_store = user_store.clone(); - cx.spawn(|mut cx| async move { - let room = cx.update(|cx| Room::create(client, user_store, cx)).await?; - cx.update(|cx| cx.set_global(Some(room.clone()))); - Ok(room) - }) - } - } - - pub fn clear(cx: &mut MutableAppContext) { - cx.set_global::>>(None); - } - fn new( id: u64, client: Arc, From 1158911560586b6e5466d3188bc9d3e343971f33 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 29 Sep 2022 15:33:33 +0200 Subject: [PATCH 029/112] Wire up accepting/declining a call --- crates/client/src/call.rs | 2 +- crates/client/src/user.rs | 2 +- crates/collab_ui/src/collab_ui.rs | 84 +--------- crates/collab_ui/src/contacts_popover.rs | 2 +- .../src/incoming_call_notification.rs | 152 ++++++++++++++++++ crates/zed/src/main.rs | 2 +- 6 files changed, 162 insertions(+), 82 deletions(-) create mode 100644 crates/collab_ui/src/incoming_call_notification.rs diff --git a/crates/client/src/call.rs b/crates/client/src/call.rs index 3111a049495ea312c2f37d7b7cdb05a41457d1e2..9b7ce06df488f646b459650a7388367e3629b26a 100644 --- a/crates/client/src/call.rs +++ b/crates/client/src/call.rs @@ -4,6 +4,6 @@ use std::sync::Arc; #[derive(Clone)] pub struct Call { pub room_id: u64, - pub from: Arc, + pub caller: Arc, pub participants: Vec>, } diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index 0dbb8bb19812f7c6a294dd62c3e5532dded28472..d285cb09db447ee45b9b293b290191b78acd2265 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -212,7 +212,7 @@ impl UserStore { this.get_users(envelope.payload.participant_user_ids, cx) }) .await?, - from: this + caller: this .update(&mut cx, |this, cx| { this.get_user(envelope.payload.caller_user_id, cx) }) diff --git a/crates/collab_ui/src/collab_ui.rs b/crates/collab_ui/src/collab_ui.rs index d3f12fdf6faa00552800559149191336135c7757..b101bb991d8864591be750b94dfc796949c7fd45 100644 --- a/crates/collab_ui/src/collab_ui.rs +++ b/crates/collab_ui/src/collab_ui.rs @@ -1,86 +1,14 @@ mod collab_titlebar_item; mod contacts_popover; +mod incoming_call_notification; -use client::{call::Call, UserStore}; +use client::{Client, UserStore}; pub use collab_titlebar_item::CollabTitlebarItem; -use futures::StreamExt; -use gpui::{ - elements::*, - geometry::{rect::RectF, vector::vec2f}, - Entity, ModelHandle, MutableAppContext, View, WindowBounds, WindowKind, WindowOptions, -}; -use settings::Settings; +use gpui::{ModelHandle, MutableAppContext}; +use std::sync::Arc; -pub fn init(user_store: ModelHandle, cx: &mut MutableAppContext) { +pub fn init(client: Arc, user_store: ModelHandle, cx: &mut MutableAppContext) { contacts_popover::init(cx); collab_titlebar_item::init(cx); - - let mut incoming_call = user_store.read(cx).incoming_call(); - cx.spawn(|mut cx| async move { - let mut notification_window = None; - while let Some(incoming_call) = incoming_call.next().await { - if let Some(window_id) = notification_window.take() { - cx.remove_window(window_id); - } - - if let Some(incoming_call) = incoming_call { - let (window_id, _) = cx.add_window( - WindowOptions { - bounds: WindowBounds::Fixed(RectF::new(vec2f(0., 0.), vec2f(300., 400.))), - titlebar: None, - center: true, - kind: WindowKind::PopUp, - is_movable: false, - }, - |_| IncomingCallNotification::new(incoming_call), - ); - notification_window = Some(window_id); - } - } - }) - .detach(); -} - -struct IncomingCallNotification { - call: Call, -} - -impl IncomingCallNotification { - fn new(call: Call) -> Self { - Self { call } - } -} - -impl Entity for IncomingCallNotification { - type Event = (); -} - -impl View for IncomingCallNotification { - fn ui_name() -> &'static str { - "IncomingCallNotification" - } - - fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> gpui::ElementBox { - let theme = &cx.global::().theme.contacts_panel; - Flex::row() - .with_children(self.call.from.avatar.clone().map(|avatar| { - Image::new(avatar) - .with_style(theme.contact_avatar) - .aligned() - .left() - .boxed() - })) - .with_child( - Label::new( - self.call.from.github_login.clone(), - theme.contact_username.text.clone(), - ) - .contained() - .aligned() - .left() - .flex(1., true) - .boxed(), - ) - .boxed() - } + incoming_call_notification::init(client, user_store, cx); } diff --git a/crates/collab_ui/src/contacts_popover.rs b/crates/collab_ui/src/contacts_popover.rs index 2ea0c75623d570f6122db05758c85b22823fea05..15a987d6c268b3955903610c72d705bce3a06571 100644 --- a/crates/collab_ui/src/contacts_popover.rs +++ b/crates/collab_ui/src/contacts_popover.rs @@ -9,7 +9,7 @@ use gpui::{ ViewHandle, }; use menu::{Confirm, SelectNext, SelectPrev}; -use room::{ActiveCall, Room}; +use room::ActiveCall; use settings::Settings; use theme::IconButton; diff --git a/crates/collab_ui/src/incoming_call_notification.rs b/crates/collab_ui/src/incoming_call_notification.rs new file mode 100644 index 0000000000000000000000000000000000000000..ec959f01ea10d720dbcc1613af34152536c4352c --- /dev/null +++ b/crates/collab_ui/src/incoming_call_notification.rs @@ -0,0 +1,152 @@ +use std::sync::Arc; + +use client::{call::Call, Client, UserStore}; +use futures::StreamExt; +use gpui::{ + elements::*, + geometry::{rect::RectF, vector::vec2f}, + impl_internal_actions, Entity, ModelHandle, MouseButton, MutableAppContext, RenderContext, + View, ViewContext, WindowBounds, WindowKind, WindowOptions, +}; +use room::ActiveCall; +use settings::Settings; +use util::ResultExt; + +impl_internal_actions!(incoming_call_notification, [RespondToCall]); + +pub fn init(client: Arc, user_store: ModelHandle, cx: &mut MutableAppContext) { + cx.add_action(IncomingCallNotification::respond_to_call); + + let mut incoming_call = user_store.read(cx).incoming_call(); + cx.spawn(|mut cx| async move { + let mut notification_window = None; + while let Some(incoming_call) = incoming_call.next().await { + if let Some(window_id) = notification_window.take() { + cx.remove_window(window_id); + } + + if let Some(incoming_call) = incoming_call { + let (window_id, _) = cx.add_window( + WindowOptions { + bounds: WindowBounds::Fixed(RectF::new(vec2f(0., 0.), vec2f(300., 400.))), + titlebar: None, + center: true, + kind: WindowKind::PopUp, + is_movable: false, + }, + |_| { + IncomingCallNotification::new( + incoming_call, + client.clone(), + user_store.clone(), + ) + }, + ); + notification_window = Some(window_id); + } + } + }) + .detach(); +} + +#[derive(Clone, PartialEq)] +struct RespondToCall { + accept: bool, +} + +pub struct IncomingCallNotification { + call: Call, + client: Arc, + user_store: ModelHandle, +} + +impl IncomingCallNotification { + pub fn new(call: Call, client: Arc, user_store: ModelHandle) -> Self { + Self { + call, + client, + user_store, + } + } + + fn respond_to_call(&mut self, action: &RespondToCall, cx: &mut ViewContext) { + if action.accept { + ActiveCall::global(cx) + .update(cx, |active_call, cx| { + active_call.join(&self.call, &self.client, &self.user_store, cx) + }) + .detach_and_log_err(cx); + } else { + self.user_store + .update(cx, |user_store, _| user_store.decline_call().log_err()); + } + + let window_id = cx.window_id(); + cx.remove_window(window_id); + } + + fn render_caller(&self, cx: &mut RenderContext) -> ElementBox { + let theme = &cx.global::().theme.contacts_panel; + Flex::row() + .with_children( + self.call + .caller + .avatar + .clone() + .map(|avatar| Image::new(avatar).with_style(theme.contact_avatar).boxed()), + ) + .with_child( + Label::new( + self.call.caller.github_login.clone(), + theme.contact_username.text.clone(), + ) + .boxed(), + ) + .boxed() + } + + fn render_buttons(&self, cx: &mut RenderContext) -> ElementBox { + enum Accept {} + enum Decline {} + + Flex::row() + .with_child( + MouseEventHandler::::new(0, cx, |_, cx| { + let theme = &cx.global::().theme.contacts_panel; + Label::new("Accept".to_string(), theme.contact_username.text.clone()).boxed() + }) + .on_click(MouseButton::Left, |_, cx| { + cx.dispatch_action(RespondToCall { accept: true }); + }) + .boxed(), + ) + .with_child( + MouseEventHandler::::new(0, cx, |_, cx| { + let theme = &cx.global::().theme.contacts_panel; + Label::new("Decline".to_string(), theme.contact_username.text.clone()).boxed() + }) + .on_click(MouseButton::Left, |_, cx| { + cx.dispatch_action(RespondToCall { accept: false }); + }) + .boxed(), + ) + .boxed() + } +} + +impl Entity for IncomingCallNotification { + type Event = (); +} + +impl View for IncomingCallNotification { + fn ui_name() -> &'static str { + "IncomingCallNotification" + } + + fn render(&mut self, cx: &mut RenderContext) -> gpui::ElementBox { + Flex::column() + .with_child(self.render_caller(cx)) + .with_child(self.render_buttons(cx)) + .boxed() + } +} diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index de769a6e5ec24c262d4881a3b4aeea393967f342..29ad1c5eb03a551f3d595c028ba9e30409f4b366 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -107,7 +107,7 @@ fn main() { project::Project::init(&client); client::Channel::init(&client); client::init(client.clone(), cx); - collab_ui::init(user_store.clone(), cx); + collab_ui::init(client.clone(), user_store.clone(), cx); command_palette::init(cx); editor::init(cx); go_to_line::init(cx); From e0db62173aba87cfd2d9eb4245e77501cbb37bf5 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 29 Sep 2022 17:24:31 +0200 Subject: [PATCH 030/112] Rename `room` crate to `call` Also, rename `client::Call` to `client::IncomingCall`. Co-Authored-By: Nathan Sobo --- Cargo.lock | 30 +++++++++---------- crates/{room => call}/Cargo.toml | 4 +-- .../src/active_call.rs => call/src/call.rs} | 9 ++++-- crates/{room => call}/src/participant.rs | 0 crates/{room => call}/src/room.rs | 10 ++----- crates/client/src/client.rs | 2 +- .../client/src/{call.rs => incoming_call.rs} | 2 +- crates/client/src/user.rs | 11 ++++--- crates/collab/Cargo.toml | 2 +- crates/collab/src/integration_tests.rs | 2 +- crates/collab_ui/Cargo.toml | 6 ++-- crates/collab_ui/src/contacts_popover.rs | 2 +- .../src/incoming_call_notification.rs | 12 +++++--- 13 files changed, 49 insertions(+), 43 deletions(-) rename crates/{room => call}/Cargo.toml (95%) rename crates/{room/src/active_call.rs => call/src/call.rs} (94%) rename crates/{room => call}/src/participant.rs (100%) rename crates/{room => call}/src/room.rs (96%) rename crates/client/src/{call.rs => incoming_call.rs} (84%) diff --git a/Cargo.lock b/Cargo.lock index 655ec8ab03ffe91673300d87dd2d4b00c315bfb8..b3fe2f899be2fdcc2133806e6e73b59d85196b04 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -684,6 +684,19 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1db59621ec70f09c5e9b597b220c7a2b43611f4710dc03ceb8748637775692c" +[[package]] +name = "call" +version = "0.1.0" +dependencies = [ + "anyhow", + "client", + "collections", + "futures", + "gpui", + "project", + "util", +] + [[package]] name = "cap-fs-ext" version = "0.24.4" @@ -1019,6 +1032,7 @@ dependencies = [ "axum", "axum-extra", "base64", + "call", "clap 3.2.8", "client", "collections", @@ -1040,7 +1054,6 @@ dependencies = [ "prometheus", "rand 0.8.5", "reqwest", - "room", "rpc", "scrypt", "serde", @@ -1067,6 +1080,7 @@ name = "collab_ui" version = "0.1.0" dependencies = [ "anyhow", + "call", "client", "clock", "collections", @@ -1078,7 +1092,6 @@ dependencies = [ "menu", "postage", "project", - "room", "serde", "settings", "theme", @@ -4452,19 +4465,6 @@ dependencies = [ "librocksdb-sys", ] -[[package]] -name = "room" -version = "0.1.0" -dependencies = [ - "anyhow", - "client", - "collections", - "futures", - "gpui", - "project", - "util", -] - [[package]] name = "roxmltree" version = "0.14.1" diff --git a/crates/room/Cargo.toml b/crates/call/Cargo.toml similarity index 95% rename from crates/room/Cargo.toml rename to crates/call/Cargo.toml index 169f04d35218373adf0ed111007c9dfdc877a3a9..cf5e7d6702152b3c1eb646e637f108fcf54e251a 100644 --- a/crates/room/Cargo.toml +++ b/crates/call/Cargo.toml @@ -1,10 +1,10 @@ [package] -name = "room" +name = "call" version = "0.1.0" edition = "2021" [lib] -path = "src/room.rs" +path = "src/call.rs" doctest = false [features] diff --git a/crates/room/src/active_call.rs b/crates/call/src/call.rs similarity index 94% rename from crates/room/src/active_call.rs rename to crates/call/src/call.rs index de0bc5e6397c7faa1b0ba4b7afd55e780d04e5ce..11dde75697def6b9185ddb3d4eb8ac3cb00e6c97 100644 --- a/crates/room/src/active_call.rs +++ b/crates/call/src/call.rs @@ -1,7 +1,10 @@ -use crate::Room; +mod participant; +mod room; + use anyhow::{anyhow, Result}; -use client::{call::Call, Client, UserStore}; +use client::{incoming_call::IncomingCall, Client, UserStore}; use gpui::{Entity, ModelContext, ModelHandle, MutableAppContext, Task}; +pub use room::Room; use std::sync::Arc; use util::ResultExt; @@ -49,7 +52,7 @@ impl ActiveCall { pub fn join( &mut self, - call: &Call, + call: &IncomingCall, client: &Arc, user_store: &ModelHandle, cx: &mut ModelContext, diff --git a/crates/room/src/participant.rs b/crates/call/src/participant.rs similarity index 100% rename from crates/room/src/participant.rs rename to crates/call/src/participant.rs diff --git a/crates/room/src/room.rs b/crates/call/src/room.rs similarity index 96% rename from crates/room/src/room.rs rename to crates/call/src/room.rs index 229ee69aeb7dbbb356af00ef5900fffaddf78cbe..adf3a676aad22b40a29d38fe63421c917db9ee83 100644 --- a/crates/room/src/room.rs +++ b/crates/call/src/room.rs @@ -1,13 +1,9 @@ -mod active_call; -mod participant; - -pub use active_call::ActiveCall; +use crate::participant::{LocalParticipant, ParticipantLocation, RemoteParticipant}; use anyhow::{anyhow, Result}; -use client::{call::Call, proto, Client, PeerId, TypedEnvelope, User, UserStore}; +use client::{incoming_call::IncomingCall, proto, Client, PeerId, TypedEnvelope, User, UserStore}; use collections::HashMap; use futures::StreamExt; use gpui::{AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Task}; -use participant::{LocalParticipant, ParticipantLocation, RemoteParticipant}; use project::Project; use std::sync::Arc; use util::ResultExt; @@ -81,7 +77,7 @@ impl Room { } pub fn join( - call: &Call, + call: &IncomingCall, client: Arc, user_store: ModelHandle, cx: &mut MutableAppContext, diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 16f91f0680438e640a80ae4b45eff43e80f05567..9c5b8e35c9c9f83dd389b628e767c5d23a9cd502 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -1,9 +1,9 @@ #[cfg(any(test, feature = "test-support"))] pub mod test; -pub mod call; pub mod channel; pub mod http; +pub mod incoming_call; pub mod user; use anyhow::{anyhow, Context, Result}; diff --git a/crates/client/src/call.rs b/crates/client/src/incoming_call.rs similarity index 84% rename from crates/client/src/call.rs rename to crates/client/src/incoming_call.rs index 9b7ce06df488f646b459650a7388367e3629b26a..75d8411ec3879af9d5c9d9d6ac666d7cd98e8f6a 100644 --- a/crates/client/src/call.rs +++ b/crates/client/src/incoming_call.rs @@ -2,7 +2,7 @@ use crate::User; use std::sync::Arc; #[derive(Clone)] -pub struct Call { +pub struct IncomingCall { pub room_id: u64, pub caller: Arc, pub participants: Vec>, diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index d285cb09db447ee45b9b293b290191b78acd2265..ff5f03d5ef44e0835591312bf0fe8a47127dddfa 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -1,5 +1,5 @@ use super::{http::HttpClient, proto, Client, Status, TypedEnvelope}; -use crate::call::Call; +use crate::incoming_call::IncomingCall; use anyhow::{anyhow, Context, Result}; use collections::{hash_map::Entry, BTreeSet, HashMap, HashSet}; use futures::{channel::mpsc, future, AsyncReadExt, Future, StreamExt}; @@ -67,7 +67,10 @@ pub struct UserStore { outgoing_contact_requests: Vec>, pending_contact_requests: HashMap, invite_info: Option, - incoming_call: (watch::Sender>, watch::Receiver>), + incoming_call: ( + watch::Sender>, + watch::Receiver>, + ), client: Weak, http: Arc, _maintain_contacts: Task<()>, @@ -205,7 +208,7 @@ impl UserStore { _: Arc, mut cx: AsyncAppContext, ) -> Result { - let call = Call { + let call = IncomingCall { room_id: envelope.payload.room_id, participants: this .update(&mut cx, |this, cx| { @@ -241,7 +244,7 @@ impl UserStore { self.invite_info.as_ref() } - pub fn incoming_call(&self) -> watch::Receiver> { + pub fn incoming_call(&self) -> watch::Receiver> { self.incoming_call.1.clone() } diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index 8603f675572b7c0d03853b1e81047c71ad41cd15..88c3318416b4772a11172e48c1d7dcb92e86cffb 100644 --- a/crates/collab/Cargo.toml +++ b/crates/collab/Cargo.toml @@ -55,13 +55,13 @@ features = ["runtime-tokio-rustls", "postgres", "time", "uuid"] [dev-dependencies] collections = { path = "../collections", features = ["test-support"] } gpui = { path = "../gpui", features = ["test-support"] } +call = { path = "../call", features = ["test-support"] } client = { path = "../client", features = ["test-support"] } editor = { path = "../editor", features = ["test-support"] } language = { path = "../language", features = ["test-support"] } log = { version = "0.4.16", features = ["kv_unstable_serde"] } lsp = { path = "../lsp", features = ["test-support"] } project = { path = "../project", features = ["test-support"] } -room = { path = "../room", features = ["test-support"] } rpc = { path = "../rpc", features = ["test-support"] } settings = { path = "../settings", features = ["test-support"] } theme = { path = "../theme" } diff --git a/crates/collab/src/integration_tests.rs b/crates/collab/src/integration_tests.rs index bd60893a57aee911320298519de672ced6bb567c..d16bff2f37ba046db62532f4e35961efb64ee7bb 100644 --- a/crates/collab/src/integration_tests.rs +++ b/crates/collab/src/integration_tests.rs @@ -5,6 +5,7 @@ use crate::{ }; use ::rpc::Peer; use anyhow::anyhow; +use call::Room; use client::{ self, proto, test::FakeHttpClient, Channel, ChannelDetails, ChannelList, Client, Connection, Credentials, EstablishConnectionError, ProjectMetadata, UserStore, RECEIVE_TIMEOUT, @@ -34,7 +35,6 @@ use project::{ DiagnosticSummary, Project, ProjectPath, ProjectStore, WorktreeId, }; use rand::prelude::*; -use room::Room; use rpc::PeerId; use serde_json::json; use settings::{Formatter, Settings}; diff --git a/crates/collab_ui/Cargo.toml b/crates/collab_ui/Cargo.toml index aa2cdb86283f8156fcf0f7213b0307949d427611..cf3a78a0b5304c1a1c7f58715cef95410b29434c 100644 --- a/crates/collab_ui/Cargo.toml +++ b/crates/collab_ui/Cargo.toml @@ -9,18 +9,19 @@ doctest = false [features] test-support = [ + "call/test-support", "client/test-support", "collections/test-support", "editor/test-support", "gpui/test-support", "project/test-support", - "room/test-support", "settings/test-support", "util/test-support", "workspace/test-support", ] [dependencies] +call = { path = "../call" } client = { path = "../client" } clock = { path = "../clock" } collections = { path = "../collections" } @@ -29,7 +30,6 @@ fuzzy = { path = "../fuzzy" } gpui = { path = "../gpui" } menu = { path = "../menu" } project = { path = "../project" } -room = { path = "../room" } settings = { path = "../settings" } theme = { path = "../theme" } util = { path = "../util" } @@ -41,12 +41,12 @@ postage = { version = "0.4.1", features = ["futures-traits"] } serde = { version = "1.0", features = ["derive", "rc"] } [dev-dependencies] +call = { path = "../call", features = ["test-support"] } client = { path = "../client", features = ["test-support"] } collections = { path = "../collections", features = ["test-support"] } editor = { path = "../editor", features = ["test-support"] } gpui = { path = "../gpui", features = ["test-support"] } project = { path = "../project", features = ["test-support"] } -room = { path = "../room", features = ["test-support"] } settings = { path = "../settings", features = ["test-support"] } util = { path = "../util", features = ["test-support"] } workspace = { path = "../workspace", features = ["test-support"] } diff --git a/crates/collab_ui/src/contacts_popover.rs b/crates/collab_ui/src/contacts_popover.rs index 15a987d6c268b3955903610c72d705bce3a06571..b728a9219807da7d2c8638cbd87915eb2b9031f3 100644 --- a/crates/collab_ui/src/contacts_popover.rs +++ b/crates/collab_ui/src/contacts_popover.rs @@ -1,5 +1,6 @@ use std::sync::Arc; +use call::ActiveCall; use client::{Client, Contact, User, UserStore}; use editor::{Cancel, Editor}; use fuzzy::{match_strings, StringMatchCandidate}; @@ -9,7 +10,6 @@ use gpui::{ ViewHandle, }; use menu::{Confirm, SelectNext, SelectPrev}; -use room::ActiveCall; use settings::Settings; use theme::IconButton; diff --git a/crates/collab_ui/src/incoming_call_notification.rs b/crates/collab_ui/src/incoming_call_notification.rs index ec959f01ea10d720dbcc1613af34152536c4352c..d1ea216195143e4d446771fdf50ebd2f226e5aed 100644 --- a/crates/collab_ui/src/incoming_call_notification.rs +++ b/crates/collab_ui/src/incoming_call_notification.rs @@ -1,6 +1,7 @@ use std::sync::Arc; -use client::{call::Call, Client, UserStore}; +use call::ActiveCall; +use client::{incoming_call::IncomingCall, Client, UserStore}; use futures::StreamExt; use gpui::{ elements::*, @@ -8,7 +9,6 @@ use gpui::{ impl_internal_actions, Entity, ModelHandle, MouseButton, MutableAppContext, RenderContext, View, ViewContext, WindowBounds, WindowKind, WindowOptions, }; -use room::ActiveCall; use settings::Settings; use util::ResultExt; @@ -55,13 +55,17 @@ struct RespondToCall { } pub struct IncomingCallNotification { - call: Call, + call: IncomingCall, client: Arc, user_store: ModelHandle, } impl IncomingCallNotification { - pub fn new(call: Call, client: Arc, user_store: ModelHandle) -> Self { + pub fn new( + call: IncomingCall, + client: Arc, + user_store: ModelHandle, + ) -> Self { Self { call, client, From 1898e813f59984c755948dc1b34068b2478f15ec Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 29 Sep 2022 17:39:53 +0200 Subject: [PATCH 031/112] Encapsulate `Room` interaction within `ActiveCall` Co-Authored-By: Nathan Sobo --- Cargo.lock | 1 + crates/call/src/call.rs | 77 +++++++++---------- crates/collab_ui/src/collab_titlebar_item.rs | 3 +- crates/collab_ui/src/collab_ui.rs | 7 +- crates/collab_ui/src/contacts_popover.rs | 26 ++----- .../src/incoming_call_notification.rs | 31 ++------ crates/zed/Cargo.toml | 7 +- crates/zed/src/main.rs | 2 +- crates/zed/src/zed.rs | 1 + 9 files changed, 63 insertions(+), 92 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b3fe2f899be2fdcc2133806e6e73b59d85196b04..0211f4d1e1e6fcd24a28cbb303d292b179833e47 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7154,6 +7154,7 @@ dependencies = [ "auto_update", "backtrace", "breadcrumbs", + "call", "chat_panel", "chrono", "cli", diff --git a/crates/call/src/call.rs b/crates/call/src/call.rs index 11dde75697def6b9185ddb3d4eb8ac3cb00e6c97..0fcf5d76986c2684750f028ad5bb21599c913a2f 100644 --- a/crates/call/src/call.rs +++ b/crates/call/src/call.rs @@ -6,11 +6,16 @@ use client::{incoming_call::IncomingCall, Client, UserStore}; use gpui::{Entity, ModelContext, ModelHandle, MutableAppContext, Task}; pub use room::Room; use std::sync::Arc; -use util::ResultExt; -#[derive(Default)] +pub fn init(client: Arc, user_store: ModelHandle, cx: &mut MutableAppContext) { + let active_call = cx.add_model(|_| ActiveCall::new(client, user_store)); + cx.set_global(active_call); +} + pub struct ActiveCall { room: Option>, + client: Arc, + user_store: ModelHandle, } impl Entity for ActiveCall { @@ -18,68 +23,62 @@ impl Entity for ActiveCall { } impl ActiveCall { - pub fn global(cx: &mut MutableAppContext) -> ModelHandle { - if cx.has_global::>() { - cx.global::>().clone() - } else { - let active_call = cx.add_model(|_| ActiveCall::default()); - cx.set_global(active_call.clone()); - active_call + fn new(client: Arc, user_store: ModelHandle) -> Self { + Self { + room: None, + client, + user_store, } } - pub fn get_or_create( + pub fn global(cx: &mut MutableAppContext) -> ModelHandle { + cx.global::>().clone() + } + + pub fn invite( &mut self, - client: &Arc, - user_store: &ModelHandle, + recipient_user_id: u64, cx: &mut ModelContext, - ) -> Task>> { - if let Some(room) = self.room.clone() { - Task::ready(Ok(room)) - } else { - let client = client.clone(); - let user_store = user_store.clone(); - cx.spawn(|this, mut cx| async move { + ) -> Task> { + let room = self.room.clone(); + + let client = self.client.clone(); + let user_store = self.user_store.clone(); + cx.spawn(|this, mut cx| async move { + let room = if let Some(room) = room { + room + } else { let room = cx.update(|cx| Room::create(client, user_store, cx)).await?; this.update(&mut cx, |this, cx| { this.room = Some(room.clone()); cx.notify(); }); - Ok(room) - }) - } + room + }; + room.update(&mut cx, |room, cx| room.call(recipient_user_id, cx)) + .await?; + + Ok(()) + }) } - pub fn join( - &mut self, - call: &IncomingCall, - client: &Arc, - user_store: &ModelHandle, - cx: &mut ModelContext, - ) -> Task>> { + pub fn join(&mut self, call: &IncomingCall, cx: &mut ModelContext) -> Task> { if self.room.is_some() { return Task::ready(Err(anyhow!("cannot join while on another call"))); } - let join = Room::join(call, client.clone(), user_store.clone(), cx); + let join = Room::join(call, self.client.clone(), self.user_store.clone(), cx); cx.spawn(|this, mut cx| async move { let room = join.await?; this.update(&mut cx, |this, cx| { - this.room = Some(room.clone()); + this.room = Some(room); cx.notify(); }); - Ok(room) + Ok(()) }) } pub fn room(&self) -> Option<&ModelHandle> { self.room.as_ref() } - - pub fn clear(&mut self, cx: &mut ModelContext) { - if let Some(room) = self.room.take() { - room.update(cx, |room, cx| room.leave(cx)).log_err(); - cx.notify(); - } - } } diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index 00f37fddeed84184f160f5ffeac472df558a2b14..770b9f29e653c61f3ee646f0519ae8d76cab1a77 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -69,9 +69,8 @@ impl CollabTitlebarItem { Some(_) => {} None => { if let Some(workspace) = self.workspace.upgrade(cx) { - let client = workspace.read(cx).client().clone(); let user_store = workspace.read(cx).user_store().clone(); - let view = cx.add_view(|cx| ContactsPopover::new(client, user_store, cx)); + let view = cx.add_view(|cx| ContactsPopover::new(user_store, cx)); cx.focus(&view); cx.subscribe(&view, |this, _, event, cx| { match event { diff --git a/crates/collab_ui/src/collab_ui.rs b/crates/collab_ui/src/collab_ui.rs index b101bb991d8864591be750b94dfc796949c7fd45..4bb08607047ed43507d86da53765579e5646faf6 100644 --- a/crates/collab_ui/src/collab_ui.rs +++ b/crates/collab_ui/src/collab_ui.rs @@ -2,13 +2,12 @@ mod collab_titlebar_item; mod contacts_popover; mod incoming_call_notification; -use client::{Client, UserStore}; +use client::UserStore; pub use collab_titlebar_item::CollabTitlebarItem; use gpui::{ModelHandle, MutableAppContext}; -use std::sync::Arc; -pub fn init(client: Arc, user_store: ModelHandle, cx: &mut MutableAppContext) { +pub fn init(user_store: ModelHandle, cx: &mut MutableAppContext) { contacts_popover::init(cx); collab_titlebar_item::init(cx); - incoming_call_notification::init(client, user_store, cx); + incoming_call_notification::init(user_store, cx); } diff --git a/crates/collab_ui/src/contacts_popover.rs b/crates/collab_ui/src/contacts_popover.rs index b728a9219807da7d2c8638cbd87915eb2b9031f3..aff159127f95d2e0d40c1657348e8ea99b8ae52a 100644 --- a/crates/collab_ui/src/contacts_popover.rs +++ b/crates/collab_ui/src/contacts_popover.rs @@ -1,7 +1,7 @@ use std::sync::Arc; use call::ActiveCall; -use client::{Client, Contact, User, UserStore}; +use client::{Contact, User, UserStore}; use editor::{Cancel, Editor}; use fuzzy::{match_strings, StringMatchCandidate}; use gpui::{ @@ -84,7 +84,6 @@ pub struct ContactsPopover { entries: Vec, match_candidates: Vec, list_state: ListState, - client: Arc, user_store: ModelHandle, filter_editor: ViewHandle, collapsed_sections: Vec
, @@ -93,11 +92,7 @@ pub struct ContactsPopover { } impl ContactsPopover { - pub fn new( - client: Arc, - user_store: ModelHandle, - cx: &mut ViewContext, - ) -> Self { + pub fn new(user_store: ModelHandle, cx: &mut ViewContext) -> Self { let filter_editor = cx.add_view(|cx| { let mut editor = Editor::single_line( Some(|theme| theme.contacts_panel.user_query_editor.clone()), @@ -182,7 +177,6 @@ impl ContactsPopover { match_candidates: Default::default(), filter_editor, _subscriptions: subscriptions, - client, user_store, }; this.update_entries(cx); @@ -633,17 +627,11 @@ impl ContactsPopover { } fn call(&mut self, action: &Call, cx: &mut ViewContext) { - let recipient_user_id = action.recipient_user_id; - let room = ActiveCall::global(cx).update(cx, |active_call, cx| { - active_call.get_or_create(&self.client, &self.user_store, cx) - }); - cx.spawn_weak(|_, mut cx| async move { - let room = room.await?; - room.update(&mut cx, |room, cx| room.call(recipient_user_id, cx)) - .await?; - anyhow::Ok(()) - }) - .detach(); + ActiveCall::global(cx) + .update(cx, |active_call, cx| { + active_call.invite(action.recipient_user_id, cx) + }) + .detach_and_log_err(cx); } } diff --git a/crates/collab_ui/src/incoming_call_notification.rs b/crates/collab_ui/src/incoming_call_notification.rs index d1ea216195143e4d446771fdf50ebd2f226e5aed..a239acc7e6faec47c137670c13ef29a626c48182 100644 --- a/crates/collab_ui/src/incoming_call_notification.rs +++ b/crates/collab_ui/src/incoming_call_notification.rs @@ -1,7 +1,5 @@ -use std::sync::Arc; - use call::ActiveCall; -use client::{incoming_call::IncomingCall, Client, UserStore}; +use client::{incoming_call::IncomingCall, UserStore}; use futures::StreamExt; use gpui::{ elements::*, @@ -14,7 +12,7 @@ use util::ResultExt; impl_internal_actions!(incoming_call_notification, [RespondToCall]); -pub fn init(client: Arc, user_store: ModelHandle, cx: &mut MutableAppContext) { +pub fn init(user_store: ModelHandle, cx: &mut MutableAppContext) { cx.add_action(IncomingCallNotification::respond_to_call); let mut incoming_call = user_store.read(cx).incoming_call(); @@ -34,13 +32,7 @@ pub fn init(client: Arc, user_store: ModelHandle, cx: &mut Mu kind: WindowKind::PopUp, is_movable: false, }, - |_| { - IncomingCallNotification::new( - incoming_call, - client.clone(), - user_store.clone(), - ) - }, + |_| IncomingCallNotification::new(incoming_call, user_store.clone()), ); notification_window = Some(window_id); } @@ -56,29 +48,18 @@ struct RespondToCall { pub struct IncomingCallNotification { call: IncomingCall, - client: Arc, user_store: ModelHandle, } impl IncomingCallNotification { - pub fn new( - call: IncomingCall, - client: Arc, - user_store: ModelHandle, - ) -> Self { - Self { - call, - client, - user_store, - } + pub fn new(call: IncomingCall, user_store: ModelHandle) -> Self { + Self { call, user_store } } fn respond_to_call(&mut self, action: &RespondToCall, cx: &mut ViewContext) { if action.accept { ActiveCall::global(cx) - .update(cx, |active_call, cx| { - active_call.join(&self.call, &self.client, &self.user_store, cx) - }) + .update(cx, |active_call, cx| active_call.join(&self.call, cx)) .detach_and_log_err(cx); } else { self.user_store diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 667e3d79847b5a29f134707ee9aeba148080f945..f2eb7653533cd9a24579afd13ea636b0a915bc55 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -19,6 +19,7 @@ activity_indicator = { path = "../activity_indicator" } assets = { path = "../assets" } auto_update = { path = "../auto_update" } breadcrumbs = { path = "../breadcrumbs" } +call = { path = "../call" } chat_panel = { path = "../chat_panel" } cli = { path = "../cli" } collab_ui = { path = "../collab_ui" } @@ -103,17 +104,19 @@ tree-sitter-typescript = "0.20.1" url = "2.2" [dev-dependencies] -text = { path = "../text", features = ["test-support"] } +call = { path = "../call", features = ["test-support"] } +client = { path = "../client", features = ["test-support"] } editor = { path = "../editor", features = ["test-support"] } gpui = { path = "../gpui", features = ["test-support"] } language = { path = "../language", features = ["test-support"] } lsp = { path = "../lsp", features = ["test-support"] } project = { path = "../project", features = ["test-support"] } rpc = { path = "../rpc", features = ["test-support"] } -client = { path = "../client", features = ["test-support"] } settings = { path = "../settings", features = ["test-support"] } +text = { path = "../text", features = ["test-support"] } util = { path = "../util", features = ["test-support"] } workspace = { path = "../workspace", features = ["test-support"] } + env_logger = "0.9" serde_json = { version = "1.0", features = ["preserve_order"] } unindent = "0.1.7" diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 29ad1c5eb03a551f3d595c028ba9e30409f4b366..de769a6e5ec24c262d4881a3b4aeea393967f342 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -107,7 +107,7 @@ fn main() { project::Project::init(&client); client::Channel::init(&client); client::init(client.clone(), cx); - collab_ui::init(client.clone(), user_store.clone(), cx); + collab_ui::init(user_store.clone(), cx); command_palette::init(cx); editor::init(cx); go_to_line::init(cx); diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index a4cc8da6334d628f3c94e9da635a7efdf71c2b2f..fcb6f8f74e1fb6ea0b8e86ea270998a3069cccda 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -217,6 +217,7 @@ pub fn init(app_state: &Arc, cx: &mut gpui::MutableAppContext) { ); activity_indicator::init(cx); + call::init(app_state.client.clone(), app_state.user_store.clone(), cx); settings::KeymapFileContent::load_defaults(cx); } From b35e8f0164e75eec56f47281d86da18b7bc025c3 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 29 Sep 2022 19:40:36 +0200 Subject: [PATCH 032/112] Remove projects from contact updates Co-Authored-By: Nathan Sobo --- crates/client/src/user.rs | 34 +- crates/collab/src/integration_tests.rs | 449 +------------- crates/collab/src/rpc/store.rs | 36 -- crates/contacts_panel/src/contacts_panel.rs | 617 +------------------- crates/rpc/proto/zed.proto | 11 +- crates/workspace/src/waiting_room.rs | 185 ------ crates/workspace/src/workspace.rs | 32 - 7 files changed, 35 insertions(+), 1329 deletions(-) delete mode 100644 crates/workspace/src/waiting_room.rs diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index ff5f03d5ef44e0835591312bf0fe8a47127dddfa..9f86020044b6ce43740b98054309b26a1b9052e8 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -1,7 +1,7 @@ use super::{http::HttpClient, proto, Client, Status, TypedEnvelope}; use crate::incoming_call::IncomingCall; use anyhow::{anyhow, Context, Result}; -use collections::{hash_map::Entry, BTreeSet, HashMap, HashSet}; +use collections::{hash_map::Entry, HashMap, HashSet}; use futures::{channel::mpsc, future, AsyncReadExt, Future, StreamExt}; use gpui::{AsyncAppContext, Entity, ImageData, ModelContext, ModelHandle, Task}; use postage::{sink::Sink, watch}; @@ -40,14 +40,6 @@ impl Eq for User {} pub struct Contact { pub user: Arc, pub online: bool, - pub projects: Vec, -} - -#[derive(Clone, Debug, PartialEq)] -pub struct ProjectMetadata { - pub id: u64, - pub visible_worktree_root_names: Vec, - pub guests: BTreeSet>, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -290,7 +282,6 @@ impl UserStore { let mut user_ids = HashSet::default(); for contact in &message.contacts { user_ids.insert(contact.user_id); - user_ids.extend(contact.projects.iter().flat_map(|w| &w.guests).copied()); } user_ids.extend(message.incoming_requests.iter().map(|req| req.requester_id)); user_ids.extend(message.outgoing_requests.iter()); @@ -688,34 +679,11 @@ impl Contact { user_store.get_user(contact.user_id, cx) }) .await?; - let mut projects = Vec::new(); - for project in contact.projects { - let mut guests = BTreeSet::new(); - for participant_id in project.guests { - guests.insert( - user_store - .update(cx, |user_store, cx| user_store.get_user(participant_id, cx)) - .await?, - ); - } - projects.push(ProjectMetadata { - id: project.id, - visible_worktree_root_names: project.visible_worktree_root_names.clone(), - guests, - }); - } Ok(Self { user, online: contact.online, - projects, }) } - - pub fn non_empty_projects(&self) -> impl Iterator { - self.projects - .iter() - .filter(|project| !project.visible_worktree_root_names.is_empty()) - } } async fn fetch_avatar(http: &dyn HttpClient, url: &str) -> Result> { diff --git a/crates/collab/src/integration_tests.rs b/crates/collab/src/integration_tests.rs index d16bff2f37ba046db62532f4e35961efb64ee7bb..d000ebb309856733f7cbd238a990b6496f5ede65 100644 --- a/crates/collab/src/integration_tests.rs +++ b/crates/collab/src/integration_tests.rs @@ -8,7 +8,7 @@ use anyhow::anyhow; use call::Room; use client::{ self, proto, test::FakeHttpClient, Channel, ChannelDetails, ChannelList, Client, Connection, - Credentials, EstablishConnectionError, ProjectMetadata, UserStore, RECEIVE_TIMEOUT, + Credentials, EstablishConnectionError, UserStore, RECEIVE_TIMEOUT, }; use collections::{BTreeMap, HashMap, HashSet}; use editor::{ @@ -731,294 +731,6 @@ async fn test_cancel_join_request( ); } -#[gpui::test(iterations = 10)] -async fn test_offline_projects( - deterministic: Arc, - cx_a: &mut TestAppContext, - cx_b: &mut TestAppContext, - cx_c: &mut TestAppContext, -) { - cx_a.foreground().forbid_parking(); - let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; - let client_a = server.create_client(cx_a, "user_a").await; - let client_b = server.create_client(cx_b, "user_b").await; - let client_c = server.create_client(cx_c, "user_c").await; - let user_a = UserId::from_proto(client_a.user_id().unwrap()); - server - .make_contacts(vec![ - (&client_a, cx_a), - (&client_b, cx_b), - (&client_c, cx_c), - ]) - .await; - - // Set up observers of the project and user stores. Any time either of - // these models update, they should be in a consistent state with each - // other. There should not be an observable moment where the current - // user's contact entry contains a project that does not match one of - // the current open projects. That would cause a duplicate entry to be - // shown in the contacts panel. - let mut subscriptions = vec![]; - let (window_id, view) = cx_a.add_window(|cx| { - subscriptions.push(cx.observe(&client_a.user_store, { - let project_store = client_a.project_store.clone(); - let user_store = client_a.user_store.clone(); - move |_, _, cx| check_project_list(project_store.clone(), user_store.clone(), cx) - })); - - subscriptions.push(cx.observe(&client_a.project_store, { - let project_store = client_a.project_store.clone(); - let user_store = client_a.user_store.clone(); - move |_, _, cx| check_project_list(project_store.clone(), user_store.clone(), cx) - })); - - fn check_project_list( - project_store: ModelHandle, - user_store: ModelHandle, - cx: &mut gpui::MutableAppContext, - ) { - let user_store = user_store.read(cx); - for contact in user_store.contacts() { - if contact.user.id == user_store.current_user().unwrap().id { - for project in &contact.projects { - let store_contains_project = project_store - .read(cx) - .projects(cx) - .filter_map(|project| project.read(cx).remote_id()) - .any(|x| x == project.id); - - if !store_contains_project { - panic!( - concat!( - "current user's contact data has a project", - "that doesn't match any open project {:?}", - ), - project - ); - } - } - } - } - } - - EmptyView - }); - - // Build an offline project with two worktrees. - client_a - .fs - .insert_tree( - "/code", - json!({ - "crate1": { "a.rs": "" }, - "crate2": { "b.rs": "" }, - }), - ) - .await; - let project = cx_a.update(|cx| { - Project::local( - false, - client_a.client.clone(), - client_a.user_store.clone(), - client_a.project_store.clone(), - client_a.language_registry.clone(), - client_a.fs.clone(), - cx, - ) - }); - project - .update(cx_a, |p, cx| { - p.find_or_create_local_worktree("/code/crate1", true, cx) - }) - .await - .unwrap(); - project - .update(cx_a, |p, cx| { - p.find_or_create_local_worktree("/code/crate2", true, cx) - }) - .await - .unwrap(); - project - .update(cx_a, |p, cx| p.restore_state(cx)) - .await - .unwrap(); - - // When a project is offline, we still create it on the server but is invisible - // to other users. - deterministic.run_until_parked(); - assert!(server - .store - .lock() - .await - .project_metadata_for_user(user_a) - .is_empty()); - project.read_with(cx_a, |project, _| { - assert!(project.remote_id().is_some()); - assert!(!project.is_online()); - }); - assert!(client_b - .user_store - .read_with(cx_b, |store, _| { store.contacts()[0].projects.is_empty() })); - - // When the project is taken online, its metadata is sent to the server - // and broadcasted to other users. - project.update(cx_a, |p, cx| p.set_online(true, cx)); - deterministic.run_until_parked(); - let project_id = project.read_with(cx_a, |p, _| p.remote_id()).unwrap(); - client_b.user_store.read_with(cx_b, |store, _| { - assert_eq!( - store.contacts()[0].projects, - &[ProjectMetadata { - id: project_id, - visible_worktree_root_names: vec!["crate1".into(), "crate2".into()], - guests: Default::default(), - }] - ); - }); - - // The project is registered again when the host loses and regains connection. - server.disconnect_client(user_a); - server.forbid_connections(); - cx_a.foreground().advance_clock(rpc::RECEIVE_TIMEOUT); - assert!(server - .store - .lock() - .await - .project_metadata_for_user(user_a) - .is_empty()); - assert!(project.read_with(cx_a, |p, _| p.remote_id().is_none())); - assert!(client_b - .user_store - .read_with(cx_b, |store, _| { store.contacts()[0].projects.is_empty() })); - - server.allow_connections(); - cx_b.foreground().advance_clock(Duration::from_secs(10)); - let project_id = project.read_with(cx_a, |p, _| p.remote_id()).unwrap(); - client_b.user_store.read_with(cx_b, |store, _| { - assert_eq!( - store.contacts()[0].projects, - &[ProjectMetadata { - id: project_id, - visible_worktree_root_names: vec!["crate1".into(), "crate2".into()], - guests: Default::default(), - }] - ); - }); - - project - .update(cx_a, |p, cx| { - p.find_or_create_local_worktree("/code/crate3", true, cx) - }) - .await - .unwrap(); - deterministic.run_until_parked(); - client_b.user_store.read_with(cx_b, |store, _| { - assert_eq!( - store.contacts()[0].projects, - &[ProjectMetadata { - id: project_id, - visible_worktree_root_names: vec![ - "crate1".into(), - "crate2".into(), - "crate3".into() - ], - guests: Default::default(), - }] - ); - }); - - // Build another project using a directory which was previously part of - // an online project. Restore the project's state from the host's database. - let project2_a = cx_a.update(|cx| { - Project::local( - false, - client_a.client.clone(), - client_a.user_store.clone(), - client_a.project_store.clone(), - client_a.language_registry.clone(), - client_a.fs.clone(), - cx, - ) - }); - project2_a - .update(cx_a, |p, cx| { - p.find_or_create_local_worktree("/code/crate3", true, cx) - }) - .await - .unwrap(); - project2_a - .update(cx_a, |project, cx| project.restore_state(cx)) - .await - .unwrap(); - - // This project is now online, because its directory was previously online. - project2_a.read_with(cx_a, |project, _| assert!(project.is_online())); - deterministic.run_until_parked(); - let project2_id = project2_a.read_with(cx_a, |p, _| p.remote_id()).unwrap(); - client_b.user_store.read_with(cx_b, |store, _| { - assert_eq!( - store.contacts()[0].projects, - &[ - ProjectMetadata { - id: project_id, - visible_worktree_root_names: vec![ - "crate1".into(), - "crate2".into(), - "crate3".into() - ], - guests: Default::default(), - }, - ProjectMetadata { - id: project2_id, - visible_worktree_root_names: vec!["crate3".into()], - guests: Default::default(), - } - ] - ); - }); - - let project2_b = client_b.build_remote_project(&project2_a, cx_a, cx_b).await; - let project2_c = cx_c.foreground().spawn(Project::remote( - project2_id, - client_c.client.clone(), - client_c.user_store.clone(), - client_c.project_store.clone(), - client_c.language_registry.clone(), - FakeFs::new(cx_c.background()), - cx_c.to_async(), - )); - deterministic.run_until_parked(); - - // Taking a project offline unshares the project, rejects any pending join request and - // disconnects existing guests. - project2_a.update(cx_a, |project, cx| project.set_online(false, cx)); - deterministic.run_until_parked(); - project2_a.read_with(cx_a, |project, _| assert!(!project.is_shared())); - project2_b.read_with(cx_b, |project, _| assert!(project.is_read_only())); - project2_c.await.unwrap_err(); - - client_b.user_store.read_with(cx_b, |store, _| { - assert_eq!( - store.contacts()[0].projects, - &[ProjectMetadata { - id: project_id, - visible_worktree_root_names: vec![ - "crate1".into(), - "crate2".into(), - "crate3".into() - ], - guests: Default::default(), - },] - ); - }); - - cx_a.update(|cx| { - drop(subscriptions); - drop(view); - cx.remove_window(window_id); - }); -} - #[gpui::test(iterations = 10)] async fn test_propagate_saves_and_fs_changes( cx_a: &mut TestAppContext, @@ -3911,24 +3623,15 @@ async fn test_contacts( deterministic.run_until_parked(); assert_eq!( contacts(&client_a, cx_a), - [ - ("user_b".to_string(), true, vec![]), - ("user_c".to_string(), true, vec![]) - ] + [("user_b".to_string(), true), ("user_c".to_string(), true)] ); assert_eq!( contacts(&client_b, cx_b), - [ - ("user_a".to_string(), true, vec![]), - ("user_c".to_string(), true, vec![]) - ] + [("user_a".to_string(), true), ("user_c".to_string(), true)] ); assert_eq!( contacts(&client_c, cx_c), - [ - ("user_a".to_string(), true, vec![]), - ("user_b".to_string(), true, vec![]) - ] + [("user_a".to_string(), true), ("user_b".to_string(), true)] ); // Share a project as client A. @@ -3938,24 +3641,15 @@ async fn test_contacts( deterministic.run_until_parked(); assert_eq!( contacts(&client_a, cx_a), - [ - ("user_b".to_string(), true, vec![]), - ("user_c".to_string(), true, vec![]) - ] + [("user_b".to_string(), true), ("user_c".to_string(), true)] ); assert_eq!( contacts(&client_b, cx_b), - [ - ("user_a".to_string(), true, vec![("a".to_string(), vec![])]), - ("user_c".to_string(), true, vec![]) - ] + [("user_a".to_string(), true), ("user_c".to_string(), true)] ); assert_eq!( contacts(&client_c, cx_c), - [ - ("user_a".to_string(), true, vec![("a".to_string(), vec![])]), - ("user_b".to_string(), true, vec![]) - ] + [("user_a".to_string(), true), ("user_b".to_string(), true)] ); let _project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await; @@ -3963,32 +3657,15 @@ async fn test_contacts( deterministic.run_until_parked(); assert_eq!( contacts(&client_a, cx_a), - [ - ("user_b".to_string(), true, vec![]), - ("user_c".to_string(), true, vec![]) - ] + [("user_b".to_string(), true), ("user_c".to_string(), true)] ); assert_eq!( contacts(&client_b, cx_b), - [ - ( - "user_a".to_string(), - true, - vec![("a".to_string(), vec!["user_b".to_string()])] - ), - ("user_c".to_string(), true, vec![]) - ] + [("user_a".to_string(), true,), ("user_c".to_string(), true)] ); assert_eq!( contacts(&client_c, cx_c), - [ - ( - "user_a".to_string(), - true, - vec![("a".to_string(), vec!["user_b".to_string()])] - ), - ("user_b".to_string(), true, vec![]) - ] + [("user_a".to_string(), true,), ("user_b".to_string(), true)] ); // Add a local project as client B @@ -3998,32 +3675,15 @@ async fn test_contacts( deterministic.run_until_parked(); assert_eq!( contacts(&client_a, cx_a), - [ - ("user_b".to_string(), true, vec![("b".to_string(), vec![])]), - ("user_c".to_string(), true, vec![]) - ] + [("user_b".to_string(), true), ("user_c".to_string(), true)] ); assert_eq!( contacts(&client_b, cx_b), - [ - ( - "user_a".to_string(), - true, - vec![("a".to_string(), vec!["user_b".to_string()])] - ), - ("user_c".to_string(), true, vec![]) - ] + [("user_a".to_string(), true), ("user_c".to_string(), true)] ); assert_eq!( contacts(&client_c, cx_c), - [ - ( - "user_a".to_string(), - true, - vec![("a".to_string(), vec!["user_b".to_string()])] - ), - ("user_b".to_string(), true, vec![("b".to_string(), vec![])]) - ] + [("user_a".to_string(), true,), ("user_b".to_string(), true)] ); project_a @@ -4036,24 +3696,15 @@ async fn test_contacts( deterministic.run_until_parked(); assert_eq!( contacts(&client_a, cx_a), - [ - ("user_b".to_string(), true, vec![("b".to_string(), vec![])]), - ("user_c".to_string(), true, vec![]) - ] + [("user_b".to_string(), true), ("user_c".to_string(), true)] ); assert_eq!( contacts(&client_b, cx_b), - [ - ("user_a".to_string(), true, vec![]), - ("user_c".to_string(), true, vec![]) - ] + [("user_a".to_string(), true), ("user_c".to_string(), true)] ); assert_eq!( contacts(&client_c, cx_c), - [ - ("user_a".to_string(), true, vec![]), - ("user_b".to_string(), true, vec![("b".to_string(), vec![])]) - ] + [("user_a".to_string(), true), ("user_b".to_string(), true)] ); server.disconnect_client(client_c.current_user_id(cx_c)); @@ -4061,17 +3712,11 @@ async fn test_contacts( deterministic.advance_clock(rpc::RECEIVE_TIMEOUT); assert_eq!( contacts(&client_a, cx_a), - [ - ("user_b".to_string(), true, vec![("b".to_string(), vec![])]), - ("user_c".to_string(), false, vec![]) - ] + [("user_b".to_string(), true), ("user_c".to_string(), false)] ); assert_eq!( contacts(&client_b, cx_b), - [ - ("user_a".to_string(), true, vec![]), - ("user_c".to_string(), false, vec![]) - ] + [("user_a".to_string(), true), ("user_c".to_string(), false)] ); assert_eq!(contacts(&client_c, cx_c), []); @@ -4084,48 +3729,24 @@ async fn test_contacts( deterministic.run_until_parked(); assert_eq!( contacts(&client_a, cx_a), - [ - ("user_b".to_string(), true, vec![("b".to_string(), vec![])]), - ("user_c".to_string(), true, vec![]) - ] + [("user_b".to_string(), true), ("user_c".to_string(), true)] ); assert_eq!( contacts(&client_b, cx_b), - [ - ("user_a".to_string(), true, vec![]), - ("user_c".to_string(), true, vec![]) - ] + [("user_a".to_string(), true), ("user_c".to_string(), true)] ); assert_eq!( contacts(&client_c, cx_c), - [ - ("user_a".to_string(), true, vec![]), - ("user_b".to_string(), true, vec![("b".to_string(), vec![])]) - ] + [("user_a".to_string(), true), ("user_b".to_string(), true)] ); #[allow(clippy::type_complexity)] - fn contacts( - client: &TestClient, - cx: &TestAppContext, - ) -> Vec<(String, bool, Vec<(String, Vec)>)> { + fn contacts(client: &TestClient, cx: &TestAppContext) -> Vec<(String, bool)> { client.user_store.read_with(cx, |store, _| { store .contacts() .iter() - .map(|contact| { - let projects = contact - .projects - .iter() - .map(|p| { - ( - p.visible_worktree_root_names[0].clone(), - p.guests.iter().map(|p| p.github_login.clone()).collect(), - ) - }) - .collect(); - (contact.user.github_login.clone(), contact.online, projects) - }) + .map(|contact| (contact.user.github_login.clone(), contact.online)) .collect() }) } @@ -5155,22 +4776,6 @@ async fn test_random_collaboration( log::error!("{} error - {:?}", guest.username, guest_err); } - let contacts = server - .app_state - .db - .get_contacts(guest.current_user_id(&guest_cx)) - .await - .unwrap(); - let contacts = server - .store - .lock() - .await - .build_initial_contacts_update(contacts) - .contacts; - assert!(!contacts - .iter() - .flat_map(|contact| &contact.projects) - .any(|project| project.id == host_project_id)); guest_project.read_with(&guest_cx, |project, _| assert!(project.is_read_only())); guest_cx.update(|_| drop((guest, guest_project))); } @@ -5259,14 +4864,6 @@ async fn test_random_collaboration( "removed guest is still a contact of another peer" ); } - for project in contact.projects { - for project_guest_id in project.guests { - assert_ne!( - project_guest_id, removed_guest_id.0 as u64, - "removed guest appears as still participating on a project" - ); - } - } } } diff --git a/crates/collab/src/rpc/store.rs b/crates/collab/src/rpc/store.rs index 1c69a7c2f801f73f766d4d418e1febaeab9ce2d4..54c3a25e27390c9425e16dc646a2e978130176ab 100644 --- a/crates/collab/src/rpc/store.rs +++ b/crates/collab/src/rpc/store.rs @@ -345,47 +345,11 @@ impl Store { pub fn contact_for_user(&self, user_id: UserId, should_notify: bool) -> proto::Contact { proto::Contact { user_id: user_id.to_proto(), - projects: self.project_metadata_for_user(user_id), online: self.is_user_online(user_id), should_notify, } } - pub fn project_metadata_for_user(&self, user_id: UserId) -> Vec { - let user_connection_state = self.connected_users.get(&user_id); - let project_ids = user_connection_state.iter().flat_map(|state| { - state - .connection_ids - .iter() - .filter_map(|connection_id| self.connections.get(connection_id)) - .flat_map(|connection| connection.projects.iter().copied()) - }); - - let mut metadata = Vec::new(); - for project_id in project_ids { - if let Some(project) = self.projects.get(&project_id) { - if project.host.user_id == user_id && project.online { - metadata.push(proto::ProjectMetadata { - id: project_id.to_proto(), - visible_worktree_root_names: project - .worktrees - .values() - .filter(|worktree| worktree.visible) - .map(|worktree| worktree.root_name.clone()) - .collect(), - guests: project - .guests - .values() - .map(|guest| guest.user_id.to_proto()) - .collect(), - }); - } - } - } - - metadata - } - pub fn create_room(&mut self, creator_connection_id: ConnectionId) -> Result { let connection = self .connections diff --git a/crates/contacts_panel/src/contacts_panel.rs b/crates/contacts_panel/src/contacts_panel.rs index b5460f4d063714f2107d56e94f04f0d61d8b159b..a0259ab8c53ffd301cf7e7f888c212cad74246cd 100644 --- a/crates/contacts_panel/src/contacts_panel.rs +++ b/crates/contacts_panel/src/contacts_panel.rs @@ -8,23 +8,19 @@ use contact_notification::ContactNotification; use editor::{Cancel, Editor}; use fuzzy::{match_strings, StringMatchCandidate}; use gpui::{ - actions, - elements::*, - geometry::{rect::RectF, vector::vec2f}, - impl_actions, impl_internal_actions, - platform::CursorStyle, + actions, elements::*, impl_actions, impl_internal_actions, platform::CursorStyle, AnyViewHandle, AppContext, ClipboardItem, Element, ElementBox, Entity, ModelHandle, MouseButton, MutableAppContext, RenderContext, Subscription, View, ViewContext, ViewHandle, - WeakModelHandle, WeakViewHandle, + WeakViewHandle, }; use join_project_notification::JoinProjectNotification; use menu::{Confirm, SelectNext, SelectPrev}; -use project::{Project, ProjectStore}; +use project::ProjectStore; use serde::Deserialize; use settings::Settings; -use std::{ops::DerefMut, sync::Arc}; +use std::sync::Arc; use theme::IconButton; -use workspace::{sidebar::SidebarItem, JoinProject, ToggleProjectOnline, Workspace}; +use workspace::{sidebar::SidebarItem, Workspace}; actions!(contacts_panel, [ToggleFocus]); @@ -48,8 +44,6 @@ enum ContactEntry { IncomingRequest(Arc), OutgoingRequest(Arc), Contact(Arc), - ContactProject(Arc, usize, Option>), - OfflineProject(WeakModelHandle), } #[derive(Clone, PartialEq)] @@ -181,7 +175,6 @@ impl ContactsPanel { let list_state = ListState::new(0, Orientation::Top, 1000., cx, move |this, ix, cx| { let theme = cx.global::().theme.clone(); - let current_user_id = this.user_store.read(cx).current_user().map(|user| user.id); let is_selected = this.selection == Some(ix); match &this.entries[ix] { @@ -214,34 +207,6 @@ impl ContactsPanel { ContactEntry::Contact(contact) => { Self::render_contact(&contact.user, &theme.contacts_panel, is_selected) } - ContactEntry::ContactProject(contact, project_ix, open_project) => { - let is_last_project_for_contact = - this.entries.get(ix + 1).map_or(true, |next| { - if let ContactEntry::ContactProject(next_contact, _, _) = next { - next_contact.user.id != contact.user.id - } else { - true - } - }); - Self::render_project( - contact.clone(), - current_user_id, - *project_ix, - *open_project, - &theme.contacts_panel, - &theme.tooltip, - is_last_project_for_contact, - is_selected, - cx, - ) - } - ContactEntry::OfflineProject(project) => Self::render_offline_project( - *project, - &theme.contacts_panel, - &theme.tooltip, - is_selected, - cx, - ), } }); @@ -343,260 +308,6 @@ impl ContactsPanel { .boxed() } - #[allow(clippy::too_many_arguments)] - fn render_project( - contact: Arc, - current_user_id: Option, - project_index: usize, - open_project: Option>, - theme: &theme::ContactsPanel, - tooltip_style: &TooltipStyle, - is_last_project: bool, - is_selected: bool, - cx: &mut RenderContext, - ) -> ElementBox { - enum ToggleOnline {} - - let project = &contact.projects[project_index]; - let project_id = project.id; - let is_host = Some(contact.user.id) == current_user_id; - let open_project = open_project.and_then(|p| p.upgrade(cx.deref_mut())); - - let font_cache = cx.font_cache(); - let host_avatar_height = theme - .contact_avatar - .width - .or(theme.contact_avatar.height) - .unwrap_or(0.); - let row = &theme.project_row.default; - let tree_branch = theme.tree_branch; - let line_height = row.name.text.line_height(font_cache); - let cap_height = row.name.text.cap_height(font_cache); - let baseline_offset = - row.name.text.baseline_offset(font_cache) + (theme.row_height - line_height) / 2.; - - MouseEventHandler::::new(project_id as usize, cx, |mouse_state, cx| { - let tree_branch = *tree_branch.style_for(mouse_state, is_selected); - let row = theme.project_row.style_for(mouse_state, is_selected); - - Flex::row() - .with_child( - Stack::new() - .with_child( - Canvas::new(move |bounds, _, cx| { - let start_x = bounds.min_x() + (bounds.width() / 2.) - - (tree_branch.width / 2.); - let end_x = bounds.max_x(); - let start_y = bounds.min_y(); - let end_y = bounds.min_y() + baseline_offset - (cap_height / 2.); - - cx.scene.push_quad(gpui::Quad { - bounds: RectF::from_points( - vec2f(start_x, start_y), - vec2f( - start_x + tree_branch.width, - if is_last_project { - end_y - } else { - bounds.max_y() - }, - ), - ), - background: Some(tree_branch.color), - border: gpui::Border::default(), - corner_radius: 0., - }); - cx.scene.push_quad(gpui::Quad { - bounds: RectF::from_points( - vec2f(start_x, end_y), - vec2f(end_x, end_y + tree_branch.width), - ), - background: Some(tree_branch.color), - border: gpui::Border::default(), - corner_radius: 0., - }); - }) - .boxed(), - ) - .with_children(open_project.and_then(|open_project| { - let is_going_offline = !open_project.read(cx).is_online(); - if !mouse_state.hovered && !is_going_offline { - return None; - } - - let button = MouseEventHandler::::new( - project_id as usize, - cx, - |state, _| { - let mut icon_style = - *theme.private_button.style_for(state, false); - icon_style.container.background_color = - row.container.background_color; - if is_going_offline { - icon_style.color = theme.disabled_button.color; - } - render_icon_button(&icon_style, "icons/lock_8.svg") - .aligned() - .boxed() - }, - ); - - if is_going_offline { - Some(button.boxed()) - } else { - Some( - button - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, move |_, cx| { - cx.dispatch_action(ToggleProjectOnline { - project: Some(open_project.clone()), - }) - }) - .with_tooltip::( - project_id as usize, - "Take project offline".to_string(), - None, - tooltip_style.clone(), - cx, - ) - .boxed(), - ) - } - })) - .constrained() - .with_width(host_avatar_height) - .boxed(), - ) - .with_child( - Label::new( - project.visible_worktree_root_names.join(", "), - row.name.text.clone(), - ) - .aligned() - .left() - .contained() - .with_style(row.name.container) - .flex(1., false) - .boxed(), - ) - .with_children(project.guests.iter().filter_map(|participant| { - participant.avatar.clone().map(|avatar| { - Image::new(avatar) - .with_style(row.guest_avatar) - .aligned() - .left() - .contained() - .with_margin_right(row.guest_avatar_spacing) - .boxed() - }) - })) - .constrained() - .with_height(theme.row_height) - .contained() - .with_style(row.container) - .boxed() - }) - .with_cursor_style(if !is_host { - CursorStyle::PointingHand - } else { - CursorStyle::Arrow - }) - .on_click(MouseButton::Left, move |_, cx| { - if !is_host { - cx.dispatch_global_action(JoinProject { - contact: contact.clone(), - project_index, - }); - } - }) - .boxed() - } - - fn render_offline_project( - project_handle: WeakModelHandle, - theme: &theme::ContactsPanel, - tooltip_style: &TooltipStyle, - is_selected: bool, - cx: &mut RenderContext, - ) -> ElementBox { - let host_avatar_height = theme - .contact_avatar - .width - .or(theme.contact_avatar.height) - .unwrap_or(0.); - - enum LocalProject {} - enum ToggleOnline {} - - let project_id = project_handle.id(); - MouseEventHandler::::new(project_id, cx, |state, cx| { - let row = theme.project_row.style_for(state, is_selected); - let mut worktree_root_names = String::new(); - let project = if let Some(project) = project_handle.upgrade(cx.deref_mut()) { - project.read(cx) - } else { - return Empty::new().boxed(); - }; - let is_going_online = project.is_online(); - for tree in project.visible_worktrees(cx) { - if !worktree_root_names.is_empty() { - worktree_root_names.push_str(", "); - } - worktree_root_names.push_str(tree.read(cx).root_name()); - } - - Flex::row() - .with_child({ - let button = - MouseEventHandler::::new(project_id, cx, |state, _| { - let mut style = *theme.private_button.style_for(state, false); - if is_going_online { - style.color = theme.disabled_button.color; - } - render_icon_button(&style, "icons/lock_8.svg") - .aligned() - .constrained() - .with_width(host_avatar_height) - .boxed() - }); - - if is_going_online { - button.boxed() - } else { - button - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, move |_, cx| { - let project = project_handle.upgrade(cx.app); - cx.dispatch_action(ToggleProjectOnline { project }) - }) - .with_tooltip::( - project_id, - "Take project online".to_string(), - None, - tooltip_style.clone(), - cx, - ) - .boxed() - } - }) - .with_child( - Label::new(worktree_root_names, row.name.text.clone()) - .aligned() - .left() - .contained() - .with_style(row.name.container) - .flex(1., false) - .boxed(), - ) - .constrained() - .with_height(theme.row_height) - .contained() - .with_style(row.container) - .boxed() - }) - .boxed() - } - fn render_contact_request( user: Arc, user_store: ModelHandle, @@ -710,7 +421,6 @@ impl ContactsPanel { fn update_entries(&mut self, cx: &mut ViewContext) { let user_store = self.user_store.read(cx); - let project_store = self.project_store.read(cx); let query = self.filter_editor.read(cx).text(cx); let executor = cx.background().clone(); @@ -837,60 +547,6 @@ impl ContactsPanel { for mat in matches { let contact = &contacts[mat.candidate_id]; self.entries.push(ContactEntry::Contact(contact.clone())); - - let is_current_user = current_user - .as_ref() - .map_or(false, |user| user.id == contact.user.id); - if is_current_user { - let mut open_projects = - project_store.projects(cx).collect::>(); - self.entries.extend( - contact.projects.iter().enumerate().filter_map( - |(ix, project)| { - let open_project = open_projects - .iter() - .position(|p| { - p.read(cx).remote_id() == Some(project.id) - }) - .map(|ix| open_projects.remove(ix).downgrade()); - if project.visible_worktree_root_names.is_empty() { - None - } else { - Some(ContactEntry::ContactProject( - contact.clone(), - ix, - open_project, - )) - } - }, - ), - ); - self.entries.extend(open_projects.into_iter().filter_map( - |project| { - if project.read(cx).visible_worktrees(cx).next().is_none() { - None - } else { - Some(ContactEntry::OfflineProject(project.downgrade())) - } - }, - )); - } else { - self.entries.extend( - contact.projects.iter().enumerate().filter_map( - |(ix, project)| { - if project.visible_worktree_root_names.is_empty() { - None - } else { - Some(ContactEntry::ContactProject( - contact.clone(), - ix, - None, - )) - } - }, - ), - ); - } } } } @@ -981,18 +637,6 @@ impl ContactsPanel { let section = *section; self.toggle_expanded(&ToggleExpanded(section), cx); } - ContactEntry::ContactProject(contact, project_index, open_project) => { - if let Some(open_project) = open_project { - workspace::activate_workspace_for_project(cx, |_, cx| { - cx.model_id() == open_project.id() - }); - } else { - cx.dispatch_global_action(JoinProject { - contact: contact.clone(), - project_index: *project_index, - }) - } - } _ => {} } } @@ -1181,16 +825,6 @@ impl PartialEq for ContactEntry { return contact_1.user.id == contact_2.user.id; } } - ContactEntry::ContactProject(contact_1, ix_1, _) => { - if let ContactEntry::ContactProject(contact_2, ix_2, _) = other { - return contact_1.user.id == contact_2.user.id && ix_1 == ix_2; - } - } - ContactEntry::OfflineProject(project_1) => { - if let ContactEntry::OfflineProject(project_2) = other { - return project_1.id() == project_2.id(); - } - } } false } @@ -1205,7 +839,7 @@ mod tests { Client, }; use collections::HashSet; - use gpui::{serde_json::json, TestAppContext}; + use gpui::TestAppContext; use language::LanguageRegistry; use project::{FakeFs, Project}; @@ -1221,8 +855,6 @@ mod tests { let project_store = cx.add_model(|_| ProjectStore::new(project::Db::open_fake())); let server = FakeServer::for_client(current_user_id, &client, cx).await; let fs = FakeFs::new(cx.background()); - fs.insert_tree("/private_dir", json!({ "one.rs": "" })) - .await; let project = cx.update(|cx| { Project::local( false, @@ -1234,14 +866,6 @@ mod tests { cx, ) }); - let worktree_id = project - .update(cx, |project, cx| { - project.find_or_create_local_worktree("/private_dir", true, cx) - }) - .await - .unwrap() - .0 - .read_with(cx, |worktree, _| worktree.id().to_proto()); let (_, workspace) = cx.add_window(|cx| Workspace::new(project.clone(), |_, _| unimplemented!(), cx)); @@ -1315,211 +939,26 @@ mod tests { user_id: 3, online: true, should_notify: false, - projects: vec![proto::ProjectMetadata { - id: 101, - visible_worktree_root_names: vec!["dir1".to_string()], - guests: vec![2], - }], }, proto::Contact { user_id: 4, online: true, should_notify: false, - projects: vec![proto::ProjectMetadata { - id: 102, - visible_worktree_root_names: vec!["dir2".to_string()], - guests: vec![2], - }], }, proto::Contact { user_id: 5, online: false, should_notify: false, - projects: vec![], }, proto::Contact { user_id: current_user_id, online: true, should_notify: false, - projects: vec![proto::ProjectMetadata { - id: 103, - visible_worktree_root_names: vec!["dir3".to_string()], - guests: vec![3], - }], }, ], ..Default::default() }); - assert_eq!( - server - .receive::() - .await - .unwrap() - .payload, - proto::UpdateProject { - project_id: 200, - online: false, - worktrees: vec![] - }, - ); - - cx.foreground().run_until_parked(); - assert_eq!( - cx.read(|cx| render_to_strings(&panel, cx)), - &[ - "v Requests", - " incoming user_one", - " outgoing user_two", - "v Online", - " the_current_user", - " dir3", - " 🔒 private_dir", - " user_four", - " dir2", - " user_three", - " dir1", - "v Offline", - " user_five", - ] - ); - - // Take a project online. It appears as loading, since the project - // isn't yet visible to other contacts. - project.update(cx, |project, cx| project.set_online(true, cx)); - cx.foreground().run_until_parked(); - assert_eq!( - cx.read(|cx| render_to_strings(&panel, cx)), - &[ - "v Requests", - " incoming user_one", - " outgoing user_two", - "v Online", - " the_current_user", - " dir3", - " 🔒 private_dir (going online...)", - " user_four", - " dir2", - " user_three", - " dir1", - "v Offline", - " user_five", - ] - ); - - // The server receives the project's metadata and updates the contact metadata - // for the current user. Now the project appears as online. - assert_eq!( - server - .receive::() - .await - .unwrap() - .payload, - proto::UpdateProject { - project_id: 200, - online: true, - worktrees: vec![proto::WorktreeMetadata { - id: worktree_id, - root_name: "private_dir".to_string(), - visible: true, - }] - }, - ); - server - .receive::() - .await - .unwrap(); - - server.send(proto::UpdateContacts { - contacts: vec![proto::Contact { - user_id: current_user_id, - online: true, - should_notify: false, - projects: vec![ - proto::ProjectMetadata { - id: 103, - visible_worktree_root_names: vec!["dir3".to_string()], - guests: vec![3], - }, - proto::ProjectMetadata { - id: 200, - visible_worktree_root_names: vec!["private_dir".to_string()], - guests: vec![3], - }, - ], - }], - ..Default::default() - }); - cx.foreground().run_until_parked(); - assert_eq!( - cx.read(|cx| render_to_strings(&panel, cx)), - &[ - "v Requests", - " incoming user_one", - " outgoing user_two", - "v Online", - " the_current_user", - " dir3", - " private_dir", - " user_four", - " dir2", - " user_three", - " dir1", - "v Offline", - " user_five", - ] - ); - - // Take the project offline. It appears as loading. - project.update(cx, |project, cx| project.set_online(false, cx)); - cx.foreground().run_until_parked(); - assert_eq!( - cx.read(|cx| render_to_strings(&panel, cx)), - &[ - "v Requests", - " incoming user_one", - " outgoing user_two", - "v Online", - " the_current_user", - " dir3", - " private_dir (going offline...)", - " user_four", - " dir2", - " user_three", - " dir1", - "v Offline", - " user_five", - ] - ); - - // The server receives the unregister request and updates the contact - // metadata for the current user. The project is now offline. - assert_eq!( - server - .receive::() - .await - .unwrap() - .payload, - proto::UpdateProject { - project_id: 200, - online: false, - worktrees: vec![] - }, - ); - - server.send(proto::UpdateContacts { - contacts: vec![proto::Contact { - user_id: current_user_id, - online: true, - should_notify: false, - projects: vec![proto::ProjectMetadata { - id: 103, - visible_worktree_root_names: vec!["dir3".to_string()], - guests: vec![3], - }], - }], - ..Default::default() - }); cx.foreground().run_until_parked(); assert_eq!( cx.read(|cx| render_to_strings(&panel, cx)), @@ -1529,12 +968,8 @@ mod tests { " outgoing user_two", "v Online", " the_current_user", - " dir3", - " 🔒 private_dir", " user_four", - " dir2", " user_three", - " dir1", "v Offline", " user_five", ] @@ -1551,7 +986,6 @@ mod tests { &[ "v Online", " user_four <=== selected", - " dir2", "v Offline", " user_five", ] @@ -1565,8 +999,7 @@ mod tests { &[ "v Online", " user_four", - " dir2 <=== selected", - "v Offline", + "v Offline <=== selected", " user_five", ] ); @@ -1579,9 +1012,8 @@ mod tests { &[ "v Online", " user_four", - " dir2", - "v Offline <=== selected", - " user_five", + "v Offline", + " user_five <=== selected", ] ); } @@ -1608,37 +1040,6 @@ mod tests { ContactEntry::Contact(contact) => { format!(" {}", contact.user.github_login) } - ContactEntry::ContactProject(contact, project_ix, project) => { - let project = project - .and_then(|p| p.upgrade(cx)) - .map(|project| project.read(cx)); - format!( - " {}{}", - contact.projects[*project_ix] - .visible_worktree_root_names - .join(", "), - if project.map_or(true, |project| project.is_online()) { - "" - } else { - " (going offline...)" - }, - ) - } - ContactEntry::OfflineProject(project) => { - let project = project.upgrade(cx).unwrap().read(cx); - format!( - " 🔒 {}{}", - project - .worktree_root_names(cx) - .collect::>() - .join(", "), - if project.is_online() { - " (going online...)" - } else { - "" - }, - ) - } }; if panel.selection == Some(ix) { diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 1125c2b3ad9dc89c7c6cd8d0f1b7c6dff4f0227c..751c41b209e552d4f2f2e5341809c583d60f9913 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -1048,15 +1048,8 @@ message ChannelMessage { message Contact { uint64 user_id = 1; - repeated ProjectMetadata projects = 2; - bool online = 3; - bool should_notify = 4; -} - -message ProjectMetadata { - uint64 id = 1; - repeated string visible_worktree_root_names = 3; - repeated uint64 guests = 4; + bool online = 2; + bool should_notify = 3; } message WorktreeMetadata { diff --git a/crates/workspace/src/waiting_room.rs b/crates/workspace/src/waiting_room.rs deleted file mode 100644 index bdced26c8b137a288be97621b22d2281714579f2..0000000000000000000000000000000000000000 --- a/crates/workspace/src/waiting_room.rs +++ /dev/null @@ -1,185 +0,0 @@ -use crate::{sidebar::SidebarSide, AppState, ToggleFollow, Workspace}; -use anyhow::Result; -use client::{proto, Client, Contact}; -use gpui::{ - elements::*, ElementBox, Entity, ImageData, MutableAppContext, RenderContext, Task, View, - ViewContext, -}; -use project::Project; -use settings::Settings; -use std::sync::Arc; -use util::ResultExt; - -pub struct WaitingRoom { - project_id: u64, - avatar: Option>, - message: String, - waiting: bool, - client: Arc, - _join_task: Task>, -} - -impl Entity for WaitingRoom { - type Event = (); - - fn release(&mut self, _: &mut MutableAppContext) { - if self.waiting { - self.client - .send(proto::LeaveProject { - project_id: self.project_id, - }) - .log_err(); - } - } -} - -impl View for WaitingRoom { - fn ui_name() -> &'static str { - "WaitingRoom" - } - - fn render(&mut self, cx: &mut RenderContext) -> ElementBox { - let theme = &cx.global::().theme.workspace; - - Flex::column() - .with_children(self.avatar.clone().map(|avatar| { - Image::new(avatar) - .with_style(theme.joining_project_avatar) - .aligned() - .boxed() - })) - .with_child( - Text::new( - self.message.clone(), - theme.joining_project_message.text.clone(), - ) - .contained() - .with_style(theme.joining_project_message.container) - .aligned() - .boxed(), - ) - .aligned() - .contained() - .with_background_color(theme.background) - .boxed() - } -} - -impl WaitingRoom { - pub fn new( - contact: Arc, - project_index: usize, - app_state: Arc, - cx: &mut ViewContext, - ) -> Self { - let project_id = contact.projects[project_index].id; - let client = app_state.client.clone(); - let _join_task = cx.spawn_weak({ - let contact = contact.clone(); - |this, mut cx| async move { - let project = Project::remote( - project_id, - app_state.client.clone(), - app_state.user_store.clone(), - app_state.project_store.clone(), - app_state.languages.clone(), - app_state.fs.clone(), - cx.clone(), - ) - .await; - - if let Some(this) = this.upgrade(&cx) { - this.update(&mut cx, |this, cx| { - this.waiting = false; - match project { - Ok(project) => { - cx.replace_root_view(|cx| { - let mut workspace = - Workspace::new(project, app_state.default_item_factory, cx); - (app_state.initialize_workspace)( - &mut workspace, - &app_state, - cx, - ); - workspace.toggle_sidebar(SidebarSide::Left, cx); - if let Some((host_peer_id, _)) = workspace - .project - .read(cx) - .collaborators() - .iter() - .find(|(_, collaborator)| collaborator.replica_id == 0) - { - if let Some(follow) = workspace - .toggle_follow(&ToggleFollow(*host_peer_id), cx) - { - follow.detach_and_log_err(cx); - } - } - workspace - }); - } - Err(error) => { - let login = &contact.user.github_login; - let message = match error { - project::JoinProjectError::HostDeclined => { - format!("@{} declined your request.", login) - } - project::JoinProjectError::HostClosedProject => { - format!( - "@{} closed their copy of {}.", - login, - humanize_list( - &contact.projects[project_index] - .visible_worktree_root_names - ) - ) - } - project::JoinProjectError::HostWentOffline => { - format!("@{} went offline.", login) - } - project::JoinProjectError::Other(error) => { - log::error!("error joining project: {}", error); - "An error occurred.".to_string() - } - }; - this.message = message; - cx.notify(); - } - } - }) - } - - Ok(()) - } - }); - - Self { - project_id, - avatar: contact.user.avatar.clone(), - message: format!( - "Asking to join @{}'s copy of {}...", - contact.user.github_login, - humanize_list(&contact.projects[project_index].visible_worktree_root_names) - ), - waiting: true, - client, - _join_task, - } - } -} - -fn humanize_list<'a>(items: impl IntoIterator) -> String { - let mut list = String::new(); - let mut items = items.into_iter().enumerate().peekable(); - while let Some((ix, item)) = items.next() { - if ix > 0 { - list.push_str(", "); - if items.peek().is_none() { - list.push_str("and "); - } - } - - list.push_str(item); - } - list -} diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 04bbc094b252e77d1b1a474814de4fa97eb4d3ef..90f01a3a5f24aafad8b25b5844e8b95ee3ff1188 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -10,7 +10,6 @@ pub mod searchable; pub mod sidebar; mod status_bar; mod toolbar; -mod waiting_room; use anyhow::{anyhow, Context, Result}; use client::{proto, Client, Contact, PeerId, Subscription, TypedEnvelope, UserStore}; @@ -58,7 +57,6 @@ use std::{ use theme::{Theme, ThemeRegistry}; pub use toolbar::{ToolbarItemLocation, ToolbarItemView}; use util::ResultExt; -use waiting_room::WaitingRoom; type ProjectItemBuilders = HashMap< TypeId, @@ -167,14 +165,6 @@ pub fn init(app_state: Arc, cx: &mut MutableAppContext) { } } }); - cx.add_global_action({ - let app_state = Arc::downgrade(&app_state); - move |action: &JoinProject, cx: &mut MutableAppContext| { - if let Some(app_state) = app_state.upgrade() { - join_project(action.contact.clone(), action.project_index, &app_state, cx); - } - } - }); cx.add_async_action(Workspace::toggle_follow); cx.add_async_action(Workspace::follow_next_collaborator); @@ -2663,28 +2653,6 @@ pub fn open_paths( }) } -pub fn join_project( - contact: Arc, - project_index: usize, - app_state: &Arc, - cx: &mut MutableAppContext, -) { - let project_id = contact.projects[project_index].id; - - for window_id in cx.window_ids().collect::>() { - if let Some(workspace) = cx.root_view::(window_id) { - if workspace.read(cx).project().read(cx).remote_id() == Some(project_id) { - cx.activate_window(window_id); - return; - } - } - } - - cx.add_window((app_state.build_window_options)(), |cx| { - WaitingRoom::new(contact, project_index, app_state.clone(), cx) - }); -} - fn open_new(app_state: &Arc, cx: &mut MutableAppContext) { let (window_id, workspace) = cx.add_window((app_state.build_window_options)(), |cx| { let mut workspace = Workspace::new( From be8990ea789cc4f6f45b920bf2e853008aada159 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 30 Sep 2022 11:35:50 +0200 Subject: [PATCH 033/112] Remove project join requests --- crates/collab/src/integration_tests.rs | 196 +-------- crates/collab/src/rpc.rs | 411 +++++------------- crates/collab/src/rpc/store.rs | 224 +++------- crates/contacts_panel/src/contacts_panel.rs | 67 +-- .../src/join_project_notification.rs | 80 ---- crates/project/src/project.rs | 341 +++------------ crates/rpc/proto/zed.proto | 53 +-- crates/rpc/src/proto.rs | 9 +- crates/workspace/src/workspace.rs | 32 +- crates/zed/src/main.rs | 2 +- crates/zed/src/zed.rs | 7 +- 11 files changed, 275 insertions(+), 1147 deletions(-) delete mode 100644 crates/contacts_panel/src/join_project_notification.rs diff --git a/crates/collab/src/integration_tests.rs b/crates/collab/src/integration_tests.rs index d000ebb309856733f7cbd238a990b6496f5ede65..ebdc952a0f93d0d7aa9cfd14dbfe2b8cf7c83f16 100644 --- a/crates/collab/src/integration_tests.rs +++ b/crates/collab/src/integration_tests.rs @@ -7,7 +7,7 @@ use ::rpc::Peer; use anyhow::anyhow; use call::Room; use client::{ - self, proto, test::FakeHttpClient, Channel, ChannelDetails, ChannelList, Client, Connection, + self, test::FakeHttpClient, Channel, ChannelDetails, ChannelList, Client, Connection, Credentials, EstablishConnectionError, UserStore, RECEIVE_TIMEOUT, }; use collections::{BTreeMap, HashMap, HashSet}; @@ -40,7 +40,6 @@ use serde_json::json; use settings::{Formatter, Settings}; use sqlx::types::time::OffsetDateTime; use std::{ - cell::RefCell, env, ops::Deref, path::{Path, PathBuf}, @@ -459,12 +458,15 @@ async fn test_unshare_project( .await .unwrap(); - // When client B leaves the project, it gets automatically unshared. - cx_b.update(|_| drop(project_b)); + // When client A unshares the project, client B's project becomes read-only. + project_a + .update(cx_a, |project, cx| project.unshare(cx)) + .unwrap(); deterministic.run_until_parked(); assert!(worktree_a.read_with(cx_a, |tree, _| !tree.as_local().unwrap().is_shared())); + assert!(project_b.read_with(cx_b, |project, _| project.is_read_only())); - // When client B joins again, the project gets re-shared. + // Client B can join again after client A re-shares. let project_b2 = client_b.build_remote_project(&project_a, cx_a, cx_b).await; assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared())); project_b2 @@ -515,7 +517,7 @@ async fn test_host_disconnect( let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; let worktree_a = project_a.read_with(cx_a, |project, cx| project.worktrees(cx).next().unwrap()); - let project_id = project_a.read_with(cx_a, |project, _| project.remote_id().unwrap()); + project_a.read_with(cx_a, |project, _| project.remote_id().unwrap()); let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await; assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared())); @@ -539,20 +541,6 @@ async fn test_host_disconnect( editor_b.update(cx_b, |editor, cx| editor.insert("X", cx)); assert!(cx_b.is_window_edited(workspace_b.window_id())); - // Request to join that project as client C - let project_c = cx_c.spawn(|cx| { - Project::remote( - project_id, - client_c.client.clone(), - client_c.user_store.clone(), - client_c.project_store.clone(), - client_c.language_registry.clone(), - FakeFs::new(cx.background()), - cx, - ) - }); - deterministic.run_until_parked(); - // Drop client A's connection. Collaborators should disappear and the project should not be shown as shared. server.disconnect_client(client_a.current_user_id(cx_a)); cx_a.foreground().advance_clock(rpc::RECEIVE_TIMEOUT); @@ -564,10 +552,6 @@ async fn test_host_disconnect( .condition(cx_b, |project, _| project.is_read_only()) .await; assert!(worktree_a.read_with(cx_a, |tree, _| !tree.as_local().unwrap().is_shared())); - assert!(matches!( - project_c.await.unwrap_err(), - project::JoinProjectError::HostWentOffline - )); // Ensure client B's edited state is reset and that the whole window is blurred. cx_b.read(|cx| { @@ -598,139 +582,6 @@ async fn test_host_disconnect( .unwrap(); } -#[gpui::test(iterations = 10)] -async fn test_decline_join_request( - deterministic: Arc, - cx_a: &mut TestAppContext, - cx_b: &mut TestAppContext, -) { - cx_a.foreground().forbid_parking(); - let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; - let client_a = server.create_client(cx_a, "user_a").await; - let client_b = server.create_client(cx_b, "user_b").await; - server - .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) - .await; - - client_a.fs.insert_tree("/a", json!({})).await; - - let (project_a, _) = client_a.build_local_project("/a", cx_a).await; - let project_id = project_a.read_with(cx_a, |project, _| project.remote_id().unwrap()); - - // Request to join that project as client B - let project_b = cx_b.spawn(|cx| { - Project::remote( - project_id, - client_b.client.clone(), - client_b.user_store.clone(), - client_b.project_store.clone(), - client_b.language_registry.clone(), - FakeFs::new(cx.background()), - cx, - ) - }); - deterministic.run_until_parked(); - project_a.update(cx_a, |project, cx| { - project.respond_to_join_request(client_b.user_id().unwrap(), false, cx) - }); - assert!(matches!( - project_b.await.unwrap_err(), - project::JoinProjectError::HostDeclined - )); - - // Request to join the project again as client B - let project_b = cx_b.spawn(|cx| { - Project::remote( - project_id, - client_b.client.clone(), - client_b.user_store.clone(), - client_b.project_store.clone(), - client_b.language_registry.clone(), - FakeFs::new(cx.background()), - cx, - ) - }); - - // Close the project on the host - deterministic.run_until_parked(); - cx_a.update(|_| drop(project_a)); - deterministic.run_until_parked(); - assert!(matches!( - project_b.await.unwrap_err(), - project::JoinProjectError::HostClosedProject - )); -} - -#[gpui::test(iterations = 10)] -async fn test_cancel_join_request( - deterministic: Arc, - cx_a: &mut TestAppContext, - cx_b: &mut TestAppContext, -) { - cx_a.foreground().forbid_parking(); - let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; - let client_a = server.create_client(cx_a, "user_a").await; - let client_b = server.create_client(cx_b, "user_b").await; - server - .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) - .await; - - client_a.fs.insert_tree("/a", json!({})).await; - let (project_a, _) = client_a.build_local_project("/a", cx_a).await; - let project_id = project_a.read_with(cx_a, |project, _| project.remote_id().unwrap()); - - let user_b = client_a - .user_store - .update(cx_a, |store, cx| { - store.get_user(client_b.user_id().unwrap(), cx) - }) - .await - .unwrap(); - - let project_a_events = Rc::new(RefCell::new(Vec::new())); - project_a.update(cx_a, { - let project_a_events = project_a_events.clone(); - move |_, cx| { - cx.subscribe(&cx.handle(), move |_, _, event, _| { - project_a_events.borrow_mut().push(event.clone()); - }) - .detach(); - } - }); - - // Request to join that project as client B - let project_b = cx_b.spawn(|cx| { - Project::remote( - project_id, - client_b.client.clone(), - client_b.user_store.clone(), - client_b.project_store.clone(), - client_b.language_registry.clone(), - FakeFs::new(cx.background()), - cx, - ) - }); - deterministic.run_until_parked(); - assert_eq!( - &*project_a_events.borrow(), - &[project::Event::ContactRequestedJoin(user_b.clone())] - ); - project_a_events.borrow_mut().clear(); - - // Cancel the join request by leaving the project - client_b - .client - .send(proto::LeaveProject { project_id }) - .unwrap(); - drop(project_b); - - deterministic.run_until_parked(); - assert_eq!( - &*project_a_events.borrow(), - &[project::Event::ContactCancelledJoinRequest(user_b)] - ); -} - #[gpui::test(iterations = 10)] async fn test_propagate_saves_and_fs_changes( cx_a: &mut TestAppContext, @@ -4586,7 +4437,6 @@ async fn test_random_collaboration( let host = server.create_client(&mut host_cx, "host").await; let host_project = host_cx.update(|cx| { Project::local( - true, host.client.clone(), host.user_store.clone(), host.project_store.clone(), @@ -4738,6 +4588,11 @@ async fn test_random_collaboration( .await; host_language_registry.add(Arc::new(language)); + host_project + .update(&mut host_cx, |project, cx| project.share(cx)) + .await + .unwrap(); + let op_start_signal = futures::channel::mpsc::unbounded(); user_ids.push(host.current_user_id(&host_cx)); op_start_signals.push(op_start_signal.0); @@ -5097,7 +4952,7 @@ impl TestServer { let fs = FakeFs::new(cx.background()); let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx)); - let project_store = cx.add_model(|_| ProjectStore::new(project::Db::open_fake())); + let project_store = cx.add_model(|_| ProjectStore::new()); let app_state = Arc::new(workspace::AppState { client: client.clone(), user_store: user_store.clone(), @@ -5283,7 +5138,6 @@ impl TestClient { ) -> (ModelHandle, WorktreeId) { let project = cx.update(|cx| { Project::local( - true, self.client.clone(), self.user_store.clone(), self.project_store.clone(), @@ -5316,7 +5170,10 @@ impl TestClient { let host_project_id = host_project .read_with(host_cx, |project, _| project.next_remote_id()) .await; - let guest_user_id = self.user_id().unwrap(); + host_project + .update(host_cx, |project, cx| project.share(cx)) + .await + .unwrap(); let languages = host_project.read_with(host_cx, |project, _| project.languages().clone()); let project_b = guest_cx.spawn(|cx| { Project::remote( @@ -5329,10 +5186,7 @@ impl TestClient { cx, ) }); - host_cx.foreground().run_until_parked(); - host_project.update(host_cx, |project, cx| { - project.respond_to_join_request(guest_user_id, true, cx) - }); + let project = project_b.await.unwrap(); project } @@ -5369,18 +5223,6 @@ impl TestClient { ) -> anyhow::Result<()> { let fs = project.read_with(cx, |project, _| project.fs().clone()); - cx.update(|cx| { - cx.subscribe(&project, move |project, event, cx| { - if let project::Event::ContactRequestedJoin(user) = event { - log::info!("Host: accepting join request from {}", user.github_login); - project.update(cx, |project, cx| { - project.respond_to_join_request(user.id, true, cx) - }); - } - }) - .detach(); - }); - while op_start_signal.next().await.is_some() { let distribution = rng.lock().gen_range::(0..100); let files = fs.as_fake().files().await; diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 55c3414d85672f0bc82626340be76ae40d9ff05d..192adb701c17b7f7b398707522825f071594a911 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -88,11 +88,6 @@ impl Response { self.server.peer.respond(self.receipt, payload)?; Ok(()) } - - fn into_receipt(self) -> Receipt { - self.responded.store(true, SeqCst); - self.receipt - } } pub struct Server { @@ -160,7 +155,7 @@ impl Server { .add_request_handler(Server::unregister_project) .add_request_handler(Server::join_project) .add_message_handler(Server::leave_project) - .add_message_handler(Server::respond_to_join_project_request) + .add_message_handler(Server::unshare_project) .add_message_handler(Server::update_project) .add_message_handler(Server::register_project_activity) .add_request_handler(Server::update_worktree) @@ -491,21 +486,6 @@ impl Server { }, ) }); - - for (_, receipts) in project.join_requests { - for receipt in receipts { - self.peer.respond( - receipt, - proto::JoinProjectResponse { - variant: Some(proto::join_project_response::Variant::Decline( - proto::join_project_response::Decline { - reason: proto::join_project_response::decline::Reason::WentOffline as i32 - }, - )), - }, - )?; - } - } } for project_id in removed_connection.guest_project_ids { @@ -519,16 +499,6 @@ impl Server { }, ) }); - if project.guests.is_empty() { - self.peer - .send( - project.host_connection_id, - proto::ProjectUnshared { - project_id: project_id.to_proto(), - }, - ) - .trace_err(); - } } } @@ -727,11 +697,9 @@ impl Server { .await .user_id_for_connection(request.sender_id)?; let project_id = self.app_state.db.register_project(user_id).await?; - self.store().await.register_project( - request.sender_id, - project_id, - request.payload.online, - )?; + self.store() + .await + .register_project(request.sender_id, project_id)?; response.send(proto::RegisterProjectResponse { project_id: project_id.to_proto(), @@ -746,11 +714,10 @@ impl Server { response: Response, ) -> Result<()> { let project_id = ProjectId::from_proto(request.payload.project_id); - let (user_id, project) = { - let mut state = self.store().await; - let project = state.unregister_project(project_id, request.sender_id)?; - (state.user_id_for_connection(request.sender_id)?, project) - }; + let project = self + .store() + .await + .unregister_project(project_id, request.sender_id)?; self.app_state.db.unregister_project(project_id).await?; broadcast( @@ -765,32 +732,27 @@ impl Server { ) }, ); - for (_, receipts) in project.join_requests { - for receipt in receipts { - self.peer.respond( - receipt, - proto::JoinProjectResponse { - variant: Some(proto::join_project_response::Variant::Decline( - proto::join_project_response::Decline { - reason: proto::join_project_response::decline::Reason::Closed - as i32, - }, - )), - }, - )?; - } - } - - // Send out the `UpdateContacts` message before responding to the unregister - // request. This way, when the project's host can keep track of the project's - // remote id until after they've received the `UpdateContacts` message for - // themself. - self.update_user_contacts(user_id).await?; response.send(proto::Ack {})?; Ok(()) } + async fn unshare_project( + self: Arc, + message: TypedEnvelope, + ) -> Result<()> { + let project_id = ProjectId::from_proto(message.payload.project_id); + let project = self + .store() + .await + .unshare_project(project_id, message.sender_id)?; + broadcast(message.sender_id, project.guest_connection_ids, |conn_id| { + self.peer.send(conn_id, message.payload.clone()) + }); + + Ok(()) + } + async fn update_user_contacts(self: &Arc, user_id: UserId) -> Result<()> { let contacts = self.app_state.db.get_contacts(user_id).await?; let store = self.store().await; @@ -849,167 +811,93 @@ impl Server { return Err(anyhow!("no such project"))?; } - self.store().await.request_join_project( - guest_user_id, - project_id, - response.into_receipt(), - )?; - self.peer.send( - host_connection_id, - proto::RequestJoinProject { - project_id: project_id.to_proto(), - requester_id: guest_user_id.to_proto(), - }, - )?; - Ok(()) - } - - async fn respond_to_join_project_request( - self: Arc, - request: TypedEnvelope, - ) -> Result<()> { - let host_user_id; + let mut store = self.store().await; + let (project, replica_id) = store.join_project(request.sender_id, project_id)?; + let peer_count = project.guests.len(); + let mut collaborators = Vec::with_capacity(peer_count); + collaborators.push(proto::Collaborator { + peer_id: project.host_connection_id.0, + replica_id: 0, + user_id: project.host.user_id.to_proto(), + }); + let worktrees = project + .worktrees + .iter() + .map(|(id, worktree)| proto::WorktreeMetadata { + id: *id, + root_name: worktree.root_name.clone(), + visible: worktree.visible, + }) + .collect::>(); - { - let mut state = self.store().await; - let project_id = ProjectId::from_proto(request.payload.project_id); - let project = state.project(project_id)?; - if project.host_connection_id != request.sender_id { - Err(anyhow!("no such connection"))?; + // Add all guests other than the requesting user's own connections as collaborators + for (guest_conn_id, guest) in &project.guests { + if request.sender_id != *guest_conn_id { + collaborators.push(proto::Collaborator { + peer_id: guest_conn_id.0, + replica_id: guest.replica_id as u32, + user_id: guest.user_id.to_proto(), + }); } + } - host_user_id = project.host.user_id; - let guest_user_id = UserId::from_proto(request.payload.requester_id); - - if !request.payload.allow { - let receipts = state - .deny_join_project_request(request.sender_id, guest_user_id, project_id) - .ok_or_else(|| anyhow!("no such request"))?; - for receipt in receipts { - self.peer.respond( - receipt, - proto::JoinProjectResponse { - variant: Some(proto::join_project_response::Variant::Decline( - proto::join_project_response::Decline { - reason: proto::join_project_response::decline::Reason::Declined - as i32, - }, - )), - }, - )?; - } - return Ok(()); + for conn_id in project.connection_ids() { + if conn_id != request.sender_id { + self.peer.send( + conn_id, + proto::AddProjectCollaborator { + project_id: project_id.to_proto(), + collaborator: Some(proto::Collaborator { + peer_id: request.sender_id.0, + replica_id: replica_id as u32, + user_id: guest_user_id.to_proto(), + }), + }, + )?; } + } - let (receipts_with_replica_ids, project) = state - .accept_join_project_request(request.sender_id, guest_user_id, project_id) - .ok_or_else(|| anyhow!("no such request"))?; + // First, we send the metadata associated with each worktree. + response.send(proto::JoinProjectResponse { + worktrees: worktrees.clone(), + replica_id: replica_id as u32, + collaborators: collaborators.clone(), + language_servers: project.language_servers.clone(), + })?; - let peer_count = project.guests.len(); - let mut collaborators = Vec::with_capacity(peer_count); - collaborators.push(proto::Collaborator { - peer_id: project.host_connection_id.0, - replica_id: 0, - user_id: project.host.user_id.to_proto(), - }); - let worktrees = project - .worktrees - .iter() - .map(|(id, worktree)| proto::WorktreeMetadata { - id: *id, - root_name: worktree.root_name.clone(), - visible: worktree.visible, - }) - .collect::>(); - - // Add all guests other than the requesting user's own connections as collaborators - for (guest_conn_id, guest) in &project.guests { - if receipts_with_replica_ids - .iter() - .all(|(receipt, _)| receipt.sender_id != *guest_conn_id) - { - collaborators.push(proto::Collaborator { - peer_id: guest_conn_id.0, - replica_id: guest.replica_id as u32, - user_id: guest.user_id.to_proto(), - }); - } - } + for (worktree_id, worktree) in &project.worktrees { + #[cfg(any(test, feature = "test-support"))] + const MAX_CHUNK_SIZE: usize = 2; + #[cfg(not(any(test, feature = "test-support")))] + const MAX_CHUNK_SIZE: usize = 256; - for conn_id in project.connection_ids() { - for (receipt, replica_id) in &receipts_with_replica_ids { - if conn_id != receipt.sender_id { - self.peer.send( - conn_id, - proto::AddProjectCollaborator { - project_id: project_id.to_proto(), - collaborator: Some(proto::Collaborator { - peer_id: receipt.sender_id.0, - replica_id: *replica_id as u32, - user_id: guest_user_id.to_proto(), - }), - }, - )?; - } - } + // Stream this worktree's entries. + let message = proto::UpdateWorktree { + project_id: project_id.to_proto(), + worktree_id: *worktree_id, + root_name: worktree.root_name.clone(), + updated_entries: worktree.entries.values().cloned().collect(), + removed_entries: Default::default(), + scan_id: worktree.scan_id, + is_last_update: worktree.is_complete, + }; + for update in proto::split_worktree_update(message, MAX_CHUNK_SIZE) { + self.peer.send(request.sender_id, update.clone())?; } - // First, we send the metadata associated with each worktree. - for (receipt, replica_id) in &receipts_with_replica_ids { - self.peer.respond( - *receipt, - proto::JoinProjectResponse { - variant: Some(proto::join_project_response::Variant::Accept( - proto::join_project_response::Accept { - worktrees: worktrees.clone(), - replica_id: *replica_id as u32, - collaborators: collaborators.clone(), - language_servers: project.language_servers.clone(), - }, - )), + // Stream this worktree's diagnostics. + for summary in worktree.diagnostic_summaries.values() { + self.peer.send( + request.sender_id, + proto::UpdateDiagnosticSummary { + project_id: project_id.to_proto(), + worktree_id: *worktree_id, + summary: Some(summary.clone()), }, )?; } - - for (worktree_id, worktree) in &project.worktrees { - #[cfg(any(test, feature = "test-support"))] - const MAX_CHUNK_SIZE: usize = 2; - #[cfg(not(any(test, feature = "test-support")))] - const MAX_CHUNK_SIZE: usize = 256; - - // Stream this worktree's entries. - let message = proto::UpdateWorktree { - project_id: project_id.to_proto(), - worktree_id: *worktree_id, - root_name: worktree.root_name.clone(), - updated_entries: worktree.entries.values().cloned().collect(), - removed_entries: Default::default(), - scan_id: worktree.scan_id, - is_last_update: worktree.is_complete, - }; - for update in proto::split_worktree_update(message, MAX_CHUNK_SIZE) { - for (receipt, _) in &receipts_with_replica_ids { - self.peer.send(receipt.sender_id, update.clone())?; - } - } - - // Stream this worktree's diagnostics. - for summary in worktree.diagnostic_summaries.values() { - for (receipt, _) in &receipts_with_replica_ids { - self.peer.send( - receipt.sender_id, - proto::UpdateDiagnosticSummary { - project_id: project_id.to_proto(), - worktree_id: *worktree_id, - summary: Some(summary.clone()), - }, - )?; - } - } - } } - self.update_user_contacts(host_user_id).await?; Ok(()) } @@ -1041,27 +929,8 @@ impl Server { ) }); } - - if let Some(requester_id) = project.cancel_request { - self.peer.send( - project.host_connection_id, - proto::JoinProjectRequestCancelled { - project_id: project_id.to_proto(), - requester_id: requester_id.to_proto(), - }, - )?; - } - - if project.unshare { - self.peer.send( - project.host_connection_id, - proto::ProjectUnshared { - project_id: project_id.to_proto(), - }, - )?; - } } - self.update_user_contacts(project.host_user_id).await?; + Ok(()) } @@ -1070,61 +939,18 @@ impl Server { request: TypedEnvelope, ) -> Result<()> { let project_id = ProjectId::from_proto(request.payload.project_id); - let user_id; { let mut state = self.store().await; - user_id = state.user_id_for_connection(request.sender_id)?; let guest_connection_ids = state .read_project(project_id, request.sender_id)? .guest_connection_ids(); - let unshared_project = state.update_project( - project_id, - &request.payload.worktrees, - request.payload.online, - request.sender_id, - )?; - - if let Some(unshared_project) = unshared_project { - broadcast( - request.sender_id, - unshared_project.guests.keys().copied(), - |conn_id| { - self.peer.send( - conn_id, - proto::UnregisterProject { - project_id: project_id.to_proto(), - }, - ) - }, - ); - for (_, receipts) in unshared_project.pending_join_requests { - for receipt in receipts { - self.peer.respond( - receipt, - proto::JoinProjectResponse { - variant: Some(proto::join_project_response::Variant::Decline( - proto::join_project_response::Decline { - reason: - proto::join_project_response::decline::Reason::Closed - as i32, - }, - )), - }, - )?; - } - } - } else { - broadcast(request.sender_id, guest_connection_ids, |connection_id| { - self.peer.forward_send( - request.sender_id, - connection_id, - request.payload.clone(), - ) - }); - } + state.update_project(project_id, &request.payload.worktrees, request.sender_id)?; + broadcast(request.sender_id, guest_connection_ids, |connection_id| { + self.peer + .forward_send(request.sender_id, connection_id, request.payload.clone()) + }); }; - self.update_user_contacts(user_id).await?; Ok(()) } @@ -1146,32 +972,21 @@ impl Server { ) -> Result<()> { let project_id = ProjectId::from_proto(request.payload.project_id); let worktree_id = request.payload.worktree_id; - let (connection_ids, metadata_changed) = { - let mut store = self.store().await; - let (connection_ids, metadata_changed) = store.update_worktree( - request.sender_id, - project_id, - worktree_id, - &request.payload.root_name, - &request.payload.removed_entries, - &request.payload.updated_entries, - request.payload.scan_id, - request.payload.is_last_update, - )?; - (connection_ids, metadata_changed) - }; + let connection_ids = self.store().await.update_worktree( + request.sender_id, + project_id, + worktree_id, + &request.payload.root_name, + &request.payload.removed_entries, + &request.payload.updated_entries, + request.payload.scan_id, + request.payload.is_last_update, + )?; broadcast(request.sender_id, connection_ids, |connection_id| { self.peer .forward_send(request.sender_id, connection_id, request.payload.clone()) }); - if metadata_changed { - let user_id = self - .store() - .await - .user_id_for_connection(request.sender_id)?; - self.update_user_contacts(user_id).await?; - } response.send(proto::Ack {})?; Ok(()) } diff --git a/crates/collab/src/rpc/store.rs b/crates/collab/src/rpc/store.rs index 54c3a25e27390c9425e16dc646a2e978130176ab..deb22301473c6ada22f8d4f65cb40e7482142407 100644 --- a/crates/collab/src/rpc/store.rs +++ b/crates/collab/src/rpc/store.rs @@ -1,7 +1,7 @@ use crate::db::{self, ChannelId, ProjectId, UserId}; use anyhow::{anyhow, Result}; -use collections::{btree_map, hash_map::Entry, BTreeMap, BTreeSet, HashMap, HashSet}; -use rpc::{proto, ConnectionId, Receipt}; +use collections::{btree_map, BTreeMap, BTreeSet, HashMap, HashSet}; +use rpc::{proto, ConnectionId}; use serde::Serialize; use std::{mem, path::PathBuf, str, time::Duration}; use time::OffsetDateTime; @@ -32,7 +32,6 @@ struct ConnectionState { user_id: UserId, admin: bool, projects: BTreeSet, - requested_projects: HashSet, channels: HashSet, } @@ -45,12 +44,9 @@ pub struct Call { #[derive(Serialize)] pub struct Project { - pub online: bool, pub host_connection_id: ConnectionId, pub host: Collaborator, pub guests: HashMap, - #[serde(skip)] - pub join_requests: HashMap>>, pub active_replica_ids: HashSet, pub worktrees: BTreeMap, pub language_servers: Vec, @@ -98,13 +94,10 @@ pub struct LeftProject { pub host_connection_id: ConnectionId, pub connection_ids: Vec, pub remove_collaborator: bool, - pub cancel_request: Option, - pub unshare: bool, } pub struct UnsharedProject { - pub guests: HashMap, - pub pending_join_requests: HashMap>>, + pub guest_connection_ids: Vec, } #[derive(Copy, Clone)] @@ -159,7 +152,6 @@ impl Store { user_id, admin, projects: Default::default(), - requested_projects: Default::default(), channels: Default::default(), }, ); @@ -578,7 +570,6 @@ impl Store { &mut self, host_connection_id: ConnectionId, project_id: ProjectId, - online: bool, ) -> Result<()> { let connection = self .connections @@ -588,7 +579,6 @@ impl Store { self.projects.insert( project_id, Project { - online, host_connection_id, host: Collaborator { user_id: connection.user_id, @@ -597,7 +587,6 @@ impl Store { admin: connection.admin, }, guests: Default::default(), - join_requests: Default::default(), active_replica_ids: Default::default(), worktrees: Default::default(), language_servers: Default::default(), @@ -610,9 +599,8 @@ impl Store { &mut self, project_id: ProjectId, worktrees: &[proto::WorktreeMetadata], - online: bool, connection_id: ConnectionId, - ) -> Result> { + ) -> Result<()> { let project = self .projects .get_mut(&project_id) @@ -634,32 +622,7 @@ impl Store { } } - if online != project.online { - project.online = online; - if project.online { - Ok(None) - } else { - for connection_id in project.guest_connection_ids() { - if let Some(connection) = self.connections.get_mut(&connection_id) { - connection.projects.remove(&project_id); - } - } - - project.active_replica_ids.clear(); - project.language_servers.clear(); - for worktree in project.worktrees.values_mut() { - worktree.diagnostic_summaries.clear(); - worktree.entries.clear(); - } - - Ok(Some(UnsharedProject { - guests: mem::take(&mut project.guests), - pending_join_requests: mem::take(&mut project.join_requests), - })) - } - } else { - Ok(None) - } + Ok(()) } else { Err(anyhow!("no such project"))? } @@ -685,22 +648,6 @@ impl Store { } } - for requester_user_id in project.join_requests.keys() { - if let Some(requester_user_connection_state) = - self.connected_users.get_mut(requester_user_id) - { - for requester_connection_id in - &requester_user_connection_state.connection_ids - { - if let Some(requester_connection) = - self.connections.get_mut(requester_connection_id) - { - requester_connection.requested_projects.remove(&project_id); - } - } - } - } - Ok(project) } else { Err(anyhow!("no such project"))? @@ -710,6 +657,37 @@ impl Store { } } + pub fn unshare_project( + &mut self, + project_id: ProjectId, + connection_id: ConnectionId, + ) -> Result { + let project = self + .projects + .get_mut(&project_id) + .ok_or_else(|| anyhow!("no such project"))?; + anyhow::ensure!( + project.host_connection_id == connection_id, + "no such project" + ); + + let guest_connection_ids = project.guest_connection_ids(); + project.active_replica_ids.clear(); + project.guests.clear(); + project.language_servers.clear(); + project.worktrees.clear(); + + for connection_id in &guest_connection_ids { + if let Some(connection) = self.connections.get_mut(connection_id) { + connection.projects.remove(&project_id); + } + } + + Ok(UnsharedProject { + guest_connection_ids, + }) + } + pub fn update_diagnostic_summary( &mut self, project_id: ProjectId, @@ -753,91 +731,37 @@ impl Store { Err(anyhow!("no such project"))? } - pub fn request_join_project( + pub fn join_project( &mut self, - requester_id: UserId, + requester_connection_id: ConnectionId, project_id: ProjectId, - receipt: Receipt, - ) -> Result<()> { + ) -> Result<(&Project, ReplicaId)> { let connection = self .connections - .get_mut(&receipt.sender_id) + .get_mut(&requester_connection_id) .ok_or_else(|| anyhow!("no such connection"))?; let project = self .projects .get_mut(&project_id) .ok_or_else(|| anyhow!("no such project"))?; - if project.online { - connection.requested_projects.insert(project_id); - project - .join_requests - .entry(requester_id) - .or_default() - .push(receipt); - Ok(()) - } else { - Err(anyhow!("no such project")) - } - } - - pub fn deny_join_project_request( - &mut self, - responder_connection_id: ConnectionId, - requester_id: UserId, - project_id: ProjectId, - ) -> Option>> { - let project = self.projects.get_mut(&project_id)?; - if responder_connection_id != project.host_connection_id { - return None; - } - - let receipts = project.join_requests.remove(&requester_id)?; - for receipt in &receipts { - let requester_connection = self.connections.get_mut(&receipt.sender_id)?; - requester_connection.requested_projects.remove(&project_id); - } - project.host.last_activity = Some(OffsetDateTime::now_utc()); - - Some(receipts) - } - - #[allow(clippy::type_complexity)] - pub fn accept_join_project_request( - &mut self, - responder_connection_id: ConnectionId, - requester_id: UserId, - project_id: ProjectId, - ) -> Option<(Vec<(Receipt, ReplicaId)>, &Project)> { - let project = self.projects.get_mut(&project_id)?; - if responder_connection_id != project.host_connection_id { - return None; - } - - let receipts = project.join_requests.remove(&requester_id)?; - let mut receipts_with_replica_ids = Vec::new(); - for receipt in receipts { - let requester_connection = self.connections.get_mut(&receipt.sender_id)?; - requester_connection.requested_projects.remove(&project_id); - requester_connection.projects.insert(project_id); - let mut replica_id = 1; - while project.active_replica_ids.contains(&replica_id) { - replica_id += 1; - } - project.active_replica_ids.insert(replica_id); - project.guests.insert( - receipt.sender_id, - Collaborator { - replica_id, - user_id: requester_id, - last_activity: Some(OffsetDateTime::now_utc()), - admin: requester_connection.admin, - }, - ); - receipts_with_replica_ids.push((receipt, replica_id)); - } + connection.projects.insert(project_id); + let mut replica_id = 1; + while project.active_replica_ids.contains(&replica_id) { + replica_id += 1; + } + project.active_replica_ids.insert(replica_id); + project.guests.insert( + requester_connection_id, + Collaborator { + replica_id, + user_id: connection.user_id, + last_activity: Some(OffsetDateTime::now_utc()), + admin: connection.admin, + }, + ); project.host.last_activity = Some(OffsetDateTime::now_utc()); - Some((receipts_with_replica_ids, project)) + Ok((project, replica_id)) } pub fn leave_project( @@ -845,7 +769,6 @@ impl Store { connection_id: ConnectionId, project_id: ProjectId, ) -> Result { - let user_id = self.user_id_for_connection(connection_id)?; let project = self .projects .get_mut(&project_id) @@ -859,39 +782,14 @@ impl Store { false }; - // If the connection leaving the project has a pending request, remove it. - // If that user has no other pending requests on other connections, indicate that the request should be cancelled. - let mut cancel_request = None; - if let Entry::Occupied(mut entry) = project.join_requests.entry(user_id) { - entry - .get_mut() - .retain(|receipt| receipt.sender_id != connection_id); - if entry.get().is_empty() { - entry.remove(); - cancel_request = Some(user_id); - } - } - if let Some(connection) = self.connections.get_mut(&connection_id) { connection.projects.remove(&project_id); } - let connection_ids = project.connection_ids(); - let unshare = connection_ids.len() <= 1 && project.join_requests.is_empty(); - if unshare { - project.language_servers.clear(); - for worktree in project.worktrees.values_mut() { - worktree.diagnostic_summaries.clear(); - worktree.entries.clear(); - } - } - Ok(LeftProject { host_connection_id: project.host_connection_id, host_user_id: project.host.user_id, - connection_ids, - cancel_request, - unshare, + connection_ids: project.connection_ids(), remove_collaborator, }) } @@ -907,15 +805,11 @@ impl Store { updated_entries: &[proto::Entry], scan_id: u64, is_last_update: bool, - ) -> Result<(Vec, bool)> { + ) -> Result> { let project = self.write_project(project_id, connection_id)?; - if !project.online { - return Err(anyhow!("project is not online")); - } let connection_ids = project.connection_ids(); let mut worktree = project.worktrees.entry(worktree_id).or_default(); - let metadata_changed = worktree_root_name != worktree.root_name; worktree.root_name = worktree_root_name.to_string(); for entry_id in removed_entries { @@ -928,7 +822,7 @@ impl Store { worktree.scan_id = scan_id; worktree.is_complete = is_last_update; - Ok((connection_ids, metadata_changed)) + Ok(connection_ids) } pub fn project_connection_ids( diff --git a/crates/contacts_panel/src/contacts_panel.rs b/crates/contacts_panel/src/contacts_panel.rs index a0259ab8c53ffd301cf7e7f888c212cad74246cd..eb1afc3810da7a1e050af84a3a3ded22cfe75811 100644 --- a/crates/contacts_panel/src/contacts_panel.rs +++ b/crates/contacts_panel/src/contacts_panel.rs @@ -1,6 +1,5 @@ mod contact_finder; mod contact_notification; -mod join_project_notification; mod notifications; use client::{Contact, ContactEventKind, User, UserStore}; @@ -13,9 +12,7 @@ use gpui::{ MouseButton, MutableAppContext, RenderContext, Subscription, View, ViewContext, ViewHandle, WeakViewHandle, }; -use join_project_notification::JoinProjectNotification; use menu::{Confirm, SelectNext, SelectPrev}; -use project::ProjectStore; use serde::Deserialize; use settings::Settings; use std::sync::Arc; @@ -54,7 +51,6 @@ pub struct ContactsPanel { match_candidates: Vec, list_state: ListState, user_store: ModelHandle, - project_store: ModelHandle, filter_editor: ViewHandle, collapsed_sections: Vec
, selection: Option, @@ -76,7 +72,6 @@ pub struct RespondToContactRequest { pub fn init(cx: &mut MutableAppContext) { contact_finder::init(cx); contact_notification::init(cx); - join_project_notification::init(cx); cx.add_action(ContactsPanel::request_contact); cx.add_action(ContactsPanel::remove_contact); cx.add_action(ContactsPanel::respond_to_contact_request); @@ -90,7 +85,6 @@ pub fn init(cx: &mut MutableAppContext) { impl ContactsPanel { pub fn new( user_store: ModelHandle, - project_store: ModelHandle, workspace: WeakViewHandle, cx: &mut ViewContext, ) -> Self { @@ -120,38 +114,6 @@ impl ContactsPanel { }) .detach(); - cx.defer({ - let workspace = workspace.clone(); - move |_, cx| { - if let Some(workspace_handle) = workspace.upgrade(cx) { - cx.subscribe(&workspace_handle.read(cx).project().clone(), { - let workspace = workspace; - move |_, project, event, cx| { - if let project::Event::ContactRequestedJoin(user) = event { - if let Some(workspace) = workspace.upgrade(cx) { - workspace.update(cx, |workspace, cx| { - workspace.show_notification(user.id as usize, cx, |cx| { - cx.add_view(|cx| { - JoinProjectNotification::new( - project, - user.clone(), - cx, - ) - }) - }) - }); - } - } - } - }) - .detach(); - } - } - }); - - cx.observe(&project_store, |this, _, cx| this.update_entries(cx)) - .detach(); - cx.subscribe(&user_store, move |_, user_store, event, cx| { if let Some(workspace) = workspace.upgrade(cx) { workspace.update(cx, |workspace, cx| { @@ -219,7 +181,6 @@ impl ContactsPanel { filter_editor, _maintain_contacts: cx.observe(&user_store, |this, _, cx| this.update_entries(cx)), user_store, - project_store, }; this.update_entries(cx); this @@ -841,7 +802,7 @@ mod tests { use collections::HashSet; use gpui::TestAppContext; use language::LanguageRegistry; - use project::{FakeFs, Project}; + use project::{FakeFs, Project, ProjectStore}; #[gpui::test] async fn test_contact_panel(cx: &mut TestAppContext) { @@ -852,12 +813,11 @@ mod tests { let http_client = FakeHttpClient::with_404_response(); let client = Client::new(http_client.clone()); let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx)); - let project_store = cx.add_model(|_| ProjectStore::new(project::Db::open_fake())); + let project_store = cx.add_model(|_| ProjectStore::new()); let server = FakeServer::for_client(current_user_id, &client, cx).await; let fs = FakeFs::new(cx.background()); let project = cx.update(|cx| { Project::local( - false, client.clone(), user_store.clone(), project_store.clone(), @@ -870,12 +830,7 @@ mod tests { let (_, workspace) = cx.add_window(|cx| Workspace::new(project.clone(), |_, _| unimplemented!(), cx)); let panel = cx.add_view(&workspace, |cx| { - ContactsPanel::new( - user_store.clone(), - project_store.clone(), - workspace.downgrade(), - cx, - ) + ContactsPanel::new(user_store.clone(), workspace.downgrade(), cx) }); workspace.update(cx, |_, cx| { @@ -890,6 +845,14 @@ mod tests { .detach(); }); + let request = server.receive::().await.unwrap(); + server + .respond( + request.receipt(), + proto::RegisterProjectResponse { project_id: 200 }, + ) + .await; + let get_users_request = server.receive::().await.unwrap(); server .respond( @@ -920,14 +883,6 @@ mod tests { ) .await; - let request = server.receive::().await.unwrap(); - server - .respond( - request.receipt(), - proto::RegisterProjectResponse { project_id: 200 }, - ) - .await; - server.send(proto::UpdateContacts { incoming_requests: vec![proto::IncomingContactRequest { requester_id: 1, diff --git a/crates/contacts_panel/src/join_project_notification.rs b/crates/contacts_panel/src/join_project_notification.rs deleted file mode 100644 index d8e8e670cff02da81a5b6c4afc6754d88661f2f2..0000000000000000000000000000000000000000 --- a/crates/contacts_panel/src/join_project_notification.rs +++ /dev/null @@ -1,80 +0,0 @@ -use client::User; -use gpui::{ - actions, ElementBox, Entity, ModelHandle, MutableAppContext, RenderContext, View, ViewContext, -}; -use project::Project; -use std::sync::Arc; -use workspace::Notification; - -use crate::notifications::render_user_notification; - -pub fn init(cx: &mut MutableAppContext) { - cx.add_action(JoinProjectNotification::decline); - cx.add_action(JoinProjectNotification::accept); -} - -pub enum Event { - Dismiss, -} - -actions!(contacts_panel, [Accept, Decline]); - -pub struct JoinProjectNotification { - project: ModelHandle, - user: Arc, -} - -impl JoinProjectNotification { - pub fn new(project: ModelHandle, user: Arc, cx: &mut ViewContext) -> Self { - cx.subscribe(&project, |this, _, event, cx| { - if let project::Event::ContactCancelledJoinRequest(user) = event { - if *user == this.user { - cx.emit(Event::Dismiss); - } - } - }) - .detach(); - Self { project, user } - } - - fn decline(&mut self, _: &Decline, cx: &mut ViewContext) { - self.project.update(cx, |project, cx| { - project.respond_to_join_request(self.user.id, false, cx) - }); - cx.emit(Event::Dismiss) - } - - fn accept(&mut self, _: &Accept, cx: &mut ViewContext) { - self.project.update(cx, |project, cx| { - project.respond_to_join_request(self.user.id, true, cx) - }); - cx.emit(Event::Dismiss) - } -} - -impl Entity for JoinProjectNotification { - type Event = Event; -} - -impl View for JoinProjectNotification { - fn ui_name() -> &'static str { - "JoinProjectNotification" - } - - fn render(&mut self, cx: &mut RenderContext) -> ElementBox { - render_user_notification( - self.user.clone(), - "wants to join your project", - None, - Decline, - vec![("Decline", Box::new(Decline)), ("Accept", Box::new(Accept))], - cx, - ) - } -} - -impl Notification for JoinProjectNotification { - fn should_dismiss_notification_on_event(&self, event: &::Event) -> bool { - matches!(event, Event::Dismiss) - } -} diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index f6c20ff837c5061de81d0293a413d30686925110..903e103d41b9bdbcdf330dbbd70d794e3f5c85f8 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -74,7 +74,6 @@ pub trait Item: Entity { } pub struct ProjectStore { - db: Arc, projects: Vec>, } @@ -126,7 +125,6 @@ pub struct Project { incomplete_buffers: HashMap>, buffer_snapshots: HashMap>, nonce: u128, - initialized_persistent_state: bool, _maintain_buffer_languages: Task<()>, } @@ -158,10 +156,7 @@ enum ProjectClientState { is_shared: bool, remote_id_tx: watch::Sender>, remote_id_rx: watch::Receiver>, - online_tx: watch::Sender, - online_rx: watch::Receiver, _maintain_remote_id: Task>, - _maintain_online_status: Task>, }, Remote { sharing_has_stopped: bool, @@ -196,8 +191,6 @@ pub enum Event { RemoteIdChanged(Option), DisconnectedFromHost, CollaboratorLeft(PeerId), - ContactRequestedJoin(Arc), - ContactCancelledJoinRequest(Arc), } pub enum LanguageServerState { @@ -382,17 +375,15 @@ impl FormatTrigger { impl Project { pub fn init(client: &Arc) { - client.add_model_message_handler(Self::handle_request_join_project); client.add_model_message_handler(Self::handle_add_collaborator); client.add_model_message_handler(Self::handle_buffer_reloaded); client.add_model_message_handler(Self::handle_buffer_saved); client.add_model_message_handler(Self::handle_start_language_server); client.add_model_message_handler(Self::handle_update_language_server); client.add_model_message_handler(Self::handle_remove_collaborator); - client.add_model_message_handler(Self::handle_join_project_request_cancelled); client.add_model_message_handler(Self::handle_update_project); client.add_model_message_handler(Self::handle_unregister_project); - client.add_model_message_handler(Self::handle_project_unshared); + client.add_model_message_handler(Self::handle_unshare_project); client.add_model_message_handler(Self::handle_create_buffer_for_peer); client.add_model_message_handler(Self::handle_update_buffer_file); client.add_model_message_handler(Self::handle_update_buffer); @@ -424,7 +415,6 @@ impl Project { } pub fn local( - online: bool, client: Arc, user_store: ModelHandle, project_store: ModelHandle, @@ -453,23 +443,6 @@ impl Project { } }); - let (online_tx, online_rx) = watch::channel_with(online); - let _maintain_online_status = cx.spawn_weak({ - let mut online_rx = online_rx.clone(); - move |this, mut cx| async move { - while let Some(online) = online_rx.recv().await { - let this = this.upgrade(&cx)?; - this.update(&mut cx, |this, cx| { - if !online { - this.unshared(cx); - } - this.metadata_changed(false, cx) - }); - } - None - } - }); - let handle = cx.weak_handle(); project_store.update(cx, |store, cx| store.add_project(handle, cx)); @@ -486,10 +459,7 @@ impl Project { is_shared: false, remote_id_tx, remote_id_rx, - online_tx, - online_rx, _maintain_remote_id, - _maintain_online_status, }, opened_buffer: watch::channel(), client_subscriptions: Vec::new(), @@ -510,7 +480,6 @@ impl Project { language_server_settings: Default::default(), next_language_server_id: 0, nonce: StdRng::from_entropy().gen(), - initialized_persistent_state: false, } }) } @@ -532,24 +501,6 @@ impl Project { }) .await?; - let response = match response.variant.ok_or_else(|| anyhow!("missing variant"))? { - proto::join_project_response::Variant::Accept(response) => response, - proto::join_project_response::Variant::Decline(decline) => { - match proto::join_project_response::decline::Reason::from_i32(decline.reason) { - Some(proto::join_project_response::decline::Reason::Declined) => { - Err(JoinProjectError::HostDeclined)? - } - Some(proto::join_project_response::decline::Reason::Closed) => { - Err(JoinProjectError::HostClosedProject)? - } - Some(proto::join_project_response::decline::Reason::WentOffline) => { - Err(JoinProjectError::HostWentOffline)? - } - None => Err(anyhow!("missing decline reason"))?, - } - } - }; - let replica_id = response.replica_id as ReplicaId; let mut worktrees = Vec::new(); @@ -625,7 +576,6 @@ impl Project { opened_buffers: Default::default(), buffer_snapshots: Default::default(), nonce: StdRng::from_entropy().gen(), - initialized_persistent_state: false, }; for worktree in worktrees { this.add_worktree(&worktree, cx); @@ -668,10 +618,9 @@ impl Project { let http_client = client::test::FakeHttpClient::with_404_response(); let client = client::Client::new(http_client.clone()); let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx)); - let project_store = cx.add_model(|_| ProjectStore::new(Db::open_fake())); - let project = cx.update(|cx| { - Project::local(true, client, user_store, project_store, languages, fs, cx) - }); + let project_store = cx.add_model(|_| ProjectStore::new()); + let project = + cx.update(|cx| Project::local(client, user_store, project_store, languages, fs, cx)); for path in root_paths { let (tree, _) = project .update(cx, |project, cx| { @@ -685,53 +634,6 @@ impl Project { project } - pub fn restore_state(&mut self, cx: &mut ModelContext) -> Task> { - if self.is_remote() { - return Task::ready(Ok(())); - } - - let db = self.project_store.read(cx).db.clone(); - let keys = self.db_keys_for_online_state(cx); - let online_by_default = cx.global::().projects_online_by_default; - let read_online = cx.background().spawn(async move { - let values = db.read(keys)?; - anyhow::Ok( - values - .into_iter() - .all(|e| e.map_or(online_by_default, |e| e == [true as u8])), - ) - }); - cx.spawn(|this, mut cx| async move { - let online = read_online.await.log_err().unwrap_or(false); - this.update(&mut cx, |this, cx| { - this.initialized_persistent_state = true; - if let ProjectClientState::Local { online_tx, .. } = &mut this.client_state { - let mut online_tx = online_tx.borrow_mut(); - if *online_tx != online { - *online_tx = online; - drop(online_tx); - this.metadata_changed(false, cx); - } - } - }); - Ok(()) - }) - } - - fn persist_state(&mut self, cx: &mut ModelContext) -> Task> { - if self.is_remote() || !self.initialized_persistent_state { - return Task::ready(Ok(())); - } - - let db = self.project_store.read(cx).db.clone(); - let keys = self.db_keys_for_online_state(cx); - let is_online = self.is_online(); - cx.background().spawn(async move { - let value = &[is_online as u8]; - db.write(keys.into_iter().map(|key| (key, value))) - }) - } - fn on_settings_changed(&mut self, cx: &mut ModelContext) { let settings = cx.global::(); @@ -860,24 +762,8 @@ impl Project { &self.fs } - pub fn set_online(&mut self, online: bool, _: &mut ModelContext) { - if let ProjectClientState::Local { online_tx, .. } = &mut self.client_state { - let mut online_tx = online_tx.borrow_mut(); - if *online_tx != online { - *online_tx = online; - } - } - } - - pub fn is_online(&self) -> bool { - match &self.client_state { - ProjectClientState::Local { online_rx, .. } => *online_rx.borrow(), - ProjectClientState::Remote { .. } => true, - } - } - fn unregister(&mut self, cx: &mut ModelContext) -> Task> { - self.unshared(cx); + self.unshare(cx).log_err(); if let ProjectClientState::Local { remote_id_rx, .. } = &mut self.client_state { if let Some(remote_id) = *remote_id_rx.borrow() { let request = self.client.request(proto::UnregisterProject { @@ -905,7 +791,7 @@ impl Project { *remote_id_tx.borrow_mut() = None; } this.client_subscriptions.clear(); - this.metadata_changed(false, cx); + this.metadata_changed(cx); }); response.map(drop) }); @@ -915,19 +801,12 @@ impl Project { } fn register(&mut self, cx: &mut ModelContext) -> Task> { - if let ProjectClientState::Local { - remote_id_rx, - online_rx, - .. - } = &self.client_state - { + if let ProjectClientState::Local { remote_id_rx, .. } = &self.client_state { if remote_id_rx.borrow().is_some() { return Task::ready(Ok(())); } - let response = self.client.request(proto::RegisterProject { - online: *online_rx.borrow(), - }); + let response = self.client.request(proto::RegisterProject {}); cx.spawn(|this, mut cx| async move { let remote_id = response.await?.project_id; this.update(&mut cx, |this, cx| { @@ -935,7 +814,7 @@ impl Project { *remote_id_tx.borrow_mut() = Some(remote_id); } - this.metadata_changed(false, cx); + this.metadata_changed(cx); cx.emit(Event::RemoteIdChanged(Some(remote_id))); this.client_subscriptions .push(this.client.add_model_for_remote_entity(remote_id, cx)); @@ -1001,65 +880,50 @@ impl Project { } } - fn metadata_changed(&mut self, persist: bool, cx: &mut ModelContext) { - if let ProjectClientState::Local { - remote_id_rx, - online_rx, - .. - } = &self.client_state - { + fn metadata_changed(&mut self, cx: &mut ModelContext) { + if let ProjectClientState::Local { remote_id_rx, .. } = &self.client_state { // Broadcast worktrees only if the project is online. - let worktrees = if *online_rx.borrow() { - self.worktrees - .iter() - .filter_map(|worktree| { - worktree - .upgrade(cx) - .map(|worktree| worktree.read(cx).as_local().unwrap().metadata_proto()) - }) - .collect() - } else { - Default::default() - }; + let worktrees = self + .worktrees + .iter() + .filter_map(|worktree| { + worktree + .upgrade(cx) + .map(|worktree| worktree.read(cx).as_local().unwrap().metadata_proto()) + }) + .collect(); if let Some(project_id) = *remote_id_rx.borrow() { - let online = *online_rx.borrow(); self.client .send(proto::UpdateProject { project_id, worktrees, - online, }) .log_err(); - if online { - let worktrees = self.visible_worktrees(cx).collect::>(); - let scans_complete = - futures::future::join_all(worktrees.iter().filter_map(|worktree| { - Some(worktree.read(cx).as_local()?.scan_complete()) - })); + let worktrees = self.visible_worktrees(cx).collect::>(); + let scans_complete = + futures::future::join_all(worktrees.iter().filter_map(|worktree| { + Some(worktree.read(cx).as_local()?.scan_complete()) + })); - let worktrees = worktrees.into_iter().map(|handle| handle.downgrade()); - cx.spawn_weak(move |_, cx| async move { - scans_complete.await; - cx.read(|cx| { - for worktree in worktrees { - if let Some(worktree) = worktree - .upgrade(cx) - .and_then(|worktree| worktree.read(cx).as_local()) - { - worktree.send_extension_counts(project_id); - } + let worktrees = worktrees.into_iter().map(|handle| handle.downgrade()); + cx.spawn_weak(move |_, cx| async move { + scans_complete.await; + cx.read(|cx| { + for worktree in worktrees { + if let Some(worktree) = worktree + .upgrade(cx) + .and_then(|worktree| worktree.read(cx).as_local()) + { + worktree.send_extension_counts(project_id); } - }) + } }) - .detach(); - } + }) + .detach(); } self.project_store.update(cx, |_, cx| cx.notify()); - if persist { - self.persist_state(cx).detach_and_log_err(cx); - } cx.notify(); } } @@ -1097,23 +961,6 @@ impl Project { .map(|tree| tree.read(cx).root_name()) } - fn db_keys_for_online_state(&self, cx: &AppContext) -> Vec { - self.worktrees - .iter() - .filter_map(|worktree| { - let worktree = worktree.upgrade(cx)?.read(cx); - if worktree.is_visible() { - Some(format!( - "project-path-online:{}", - worktree.as_local().unwrap().abs_path().to_string_lossy() - )) - } else { - None - } - }) - .collect::>() - } - pub fn worktree_for_id( &self, id: WorktreeId, @@ -1317,11 +1164,7 @@ impl Project { } } - fn share(&mut self, cx: &mut ModelContext) -> Task> { - if !self.is_online() { - return Task::ready(Err(anyhow!("can't share an offline project"))); - } - + pub fn share(&mut self, cx: &mut ModelContext) -> Task> { let project_id; if let ProjectClientState::Local { remote_id_rx, @@ -1394,10 +1237,15 @@ impl Project { }) } - fn unshared(&mut self, cx: &mut ModelContext) { - if let ProjectClientState::Local { is_shared, .. } = &mut self.client_state { + pub fn unshare(&mut self, cx: &mut ModelContext) -> Result<()> { + if let ProjectClientState::Local { + is_shared, + remote_id_rx, + .. + } = &mut self.client_state + { if !*is_shared { - return; + return Ok(()); } *is_shared = false; @@ -1422,37 +1270,13 @@ impl Project { } cx.notify(); - } else { - log::error!("attempted to unshare a remote project"); - } - } + if let Some(project_id) = *remote_id_rx.borrow() { + self.client.send(proto::UnshareProject { project_id })?; + } - pub fn respond_to_join_request( - &mut self, - requester_id: u64, - allow: bool, - cx: &mut ModelContext, - ) { - if let Some(project_id) = self.remote_id() { - let share = if self.is_online() && allow { - Some(self.share(cx)) - } else { - None - }; - let client = self.client.clone(); - cx.foreground() - .spawn(async move { - client.send(proto::RespondToJoinProjectRequest { - requester_id, - project_id, - allow, - })?; - if let Some(share) = share { - share.await?; - } - anyhow::Ok(()) - }) - .detach_and_log_err(cx); + Ok(()) + } else { + Err(anyhow!("attempted to unshare a remote project")) } } @@ -4527,7 +4351,7 @@ impl Project { false } }); - self.metadata_changed(true, cx); + self.metadata_changed(cx); cx.notify(); } @@ -4552,7 +4376,7 @@ impl Project { .push(WorktreeHandle::Weak(worktree.downgrade())); } - self.metadata_changed(true, cx); + self.metadata_changed(cx); cx.observe_release(worktree, |this, worktree, cx| { this.remove_worktree(worktree.id(), cx); cx.notify(); @@ -4728,29 +4552,6 @@ impl Project { // RPC message handlers - async fn handle_request_join_project( - this: ModelHandle, - message: TypedEnvelope, - _: Arc, - mut cx: AsyncAppContext, - ) -> Result<()> { - let user_id = message.payload.requester_id; - if this.read_with(&cx, |project, _| { - project.collaborators.values().any(|c| c.user.id == user_id) - }) { - this.update(&mut cx, |this, cx| { - this.respond_to_join_request(user_id, true, cx) - }); - } else { - let user_store = this.read_with(&cx, |this, _| this.user_store.clone()); - let user = user_store - .update(&mut cx, |store, cx| store.get_user(user_id, cx)) - .await?; - this.update(&mut cx, |_, cx| cx.emit(Event::ContactRequestedJoin(user))); - } - Ok(()) - } - async fn handle_unregister_project( this: ModelHandle, _: TypedEnvelope, @@ -4761,13 +4562,13 @@ impl Project { Ok(()) } - async fn handle_project_unshared( + async fn handle_unshare_project( this: ModelHandle, - _: TypedEnvelope, + _: TypedEnvelope, _: Arc, mut cx: AsyncAppContext, ) -> Result<()> { - this.update(&mut cx, |this, cx| this.unshared(cx)); + this.update(&mut cx, |this, cx| this.disconnected_from_host(cx)); Ok(()) } @@ -4819,27 +4620,6 @@ impl Project { }) } - async fn handle_join_project_request_cancelled( - this: ModelHandle, - envelope: TypedEnvelope, - _: Arc, - mut cx: AsyncAppContext, - ) -> Result<()> { - let user = this - .update(&mut cx, |this, cx| { - this.user_store.update(cx, |user_store, cx| { - user_store.get_user(envelope.payload.requester_id, cx) - }) - }) - .await?; - - this.update(&mut cx, |_, cx| { - cx.emit(Event::ContactCancelledJoinRequest(user)); - }); - - Ok(()) - } - async fn handle_update_project( this: ModelHandle, envelope: TypedEnvelope, @@ -4871,7 +4651,7 @@ impl Project { } } - this.metadata_changed(true, cx); + this.metadata_changed(cx); for (id, _) in old_worktrees_by_id { cx.emit(Event::WorktreeRemoved(id)); } @@ -6077,9 +5857,8 @@ impl Project { } impl ProjectStore { - pub fn new(db: Arc) -> Self { + pub fn new() -> Self { Self { - db, projects: Default::default(), } } diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 751c41b209e552d4f2f2e5341809c583d60f9913..2659ddb86d1895a524332c823aaa5fb8b73a42bb 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -25,15 +25,12 @@ message Envelope { RegisterProject register_project = 15; RegisterProjectResponse register_project_response = 16; UnregisterProject unregister_project = 17; - RequestJoinProject request_join_project = 18; - RespondToJoinProjectRequest respond_to_join_project_request = 19; - JoinProjectRequestCancelled join_project_request_cancelled = 20; JoinProject join_project = 21; JoinProjectResponse join_project_response = 22; LeaveProject leave_project = 23; AddProjectCollaborator add_project_collaborator = 24; RemoveProjectCollaborator remove_project_collaborator = 25; - ProjectUnshared project_unshared = 26; + UnshareProject unshare_project = 26; GetDefinition get_definition = 27; GetDefinitionResponse get_definition_response = 28; @@ -198,9 +195,7 @@ message RoomUpdated { Room room = 1; } -message RegisterProject { - bool online = 1; -} +message RegisterProject {} message RegisterProjectResponse { uint64 project_id = 1; @@ -213,55 +208,21 @@ message UnregisterProject { message UpdateProject { uint64 project_id = 1; repeated WorktreeMetadata worktrees = 2; - bool online = 3; } message RegisterProjectActivity { uint64 project_id = 1; } -message RequestJoinProject { - uint64 requester_id = 1; - uint64 project_id = 2; -} - -message RespondToJoinProjectRequest { - uint64 requester_id = 1; - uint64 project_id = 2; - bool allow = 3; -} - -message JoinProjectRequestCancelled { - uint64 requester_id = 1; - uint64 project_id = 2; -} - message JoinProject { uint64 project_id = 1; } message JoinProjectResponse { - oneof variant { - Accept accept = 1; - Decline decline = 2; - } - - message Accept { - uint32 replica_id = 1; - repeated WorktreeMetadata worktrees = 2; - repeated Collaborator collaborators = 3; - repeated LanguageServer language_servers = 4; - } - - message Decline { - Reason reason = 1; - - enum Reason { - Declined = 0; - Closed = 1; - WentOffline = 2; - } - } + uint32 replica_id = 1; + repeated WorktreeMetadata worktrees = 2; + repeated Collaborator collaborators = 3; + repeated LanguageServer language_servers = 4; } message LeaveProject { @@ -324,7 +285,7 @@ message RemoveProjectCollaborator { uint32 peer_id = 2; } -message ProjectUnshared { +message UnshareProject { uint64 project_id = 1; } diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index 814983938c35e412b21a84d9cad18ed449bca86f..c2d2d2b321b551171c7592c26c97a86014d5fe0d 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -126,7 +126,6 @@ messages!( (JoinChannelResponse, Foreground), (JoinProject, Foreground), (JoinProjectResponse, Foreground), - (JoinProjectRequestCancelled, Foreground), (JoinRoom, Foreground), (JoinRoomResponse, Foreground), (LeaveChannel, Foreground), @@ -142,7 +141,6 @@ messages!( (PrepareRename, Background), (PrepareRenameResponse, Background), (ProjectEntryResponse, Foreground), - (ProjectUnshared, Foreground), (RegisterProjectResponse, Foreground), (RemoveContact, Foreground), (Ping, Foreground), @@ -153,9 +151,7 @@ messages!( (RemoveProjectCollaborator, Foreground), (RenameProjectEntry, Foreground), (RequestContact, Foreground), - (RequestJoinProject, Foreground), (RespondToContactRequest, Foreground), - (RespondToJoinProjectRequest, Foreground), (RoomUpdated, Foreground), (SaveBuffer, Foreground), (SearchProject, Background), @@ -167,6 +163,7 @@ messages!( (Test, Foreground), (Unfollow, Foreground), (UnregisterProject, Foreground), + (UnshareProject, Foreground), (UpdateBuffer, Foreground), (UpdateBufferFile, Foreground), (UpdateContacts, Foreground), @@ -252,24 +249,22 @@ entity_messages!( GetReferences, GetProjectSymbols, JoinProject, - JoinProjectRequestCancelled, LeaveProject, OpenBufferById, OpenBufferByPath, OpenBufferForSymbol, PerformRename, PrepareRename, - ProjectUnshared, RegisterProjectActivity, ReloadBuffers, RemoveProjectCollaborator, RenameProjectEntry, - RequestJoinProject, SaveBuffer, SearchProject, StartLanguageServer, Unfollow, UnregisterProject, + UnshareProject, UpdateBuffer, UpdateBufferFile, UpdateDiagnosticSummary, diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 90f01a3a5f24aafad8b25b5844e8b95ee3ff1188..7aa93f47d965f57e64251ccaaeb1be7f3f08374d 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -107,12 +107,6 @@ pub struct OpenPaths { pub paths: Vec, } -#[derive(Clone, Deserialize, PartialEq)] -pub struct ToggleProjectOnline { - #[serde(skip_deserializing)] - pub project: Option>, -} - #[derive(Clone, Deserialize, PartialEq)] pub struct ActivatePane(pub usize); @@ -134,7 +128,7 @@ impl_internal_actions!( RemoveWorktreeFromProject ] ); -impl_actions!(workspace, [ToggleProjectOnline, ActivatePane]); +impl_actions!(workspace, [ActivatePane]); pub fn init(app_state: Arc, cx: &mut MutableAppContext) { pane::init(cx); @@ -172,7 +166,6 @@ pub fn init(app_state: Arc, cx: &mut MutableAppContext) { cx.add_async_action(Workspace::save_all); cx.add_action(Workspace::add_folder_to_project); cx.add_action(Workspace::remove_folder_from_project); - cx.add_action(Workspace::toggle_project_online); cx.add_action( |workspace: &mut Workspace, _: &Unfollow, cx: &mut ViewContext| { let pane = workspace.active_pane().clone(); @@ -840,7 +833,7 @@ impl AppState { let languages = Arc::new(LanguageRegistry::test()); let http_client = client::test::FakeHttpClient::with_404_response(); let client = Client::new(http_client.clone()); - let project_store = cx.add_model(|_| ProjectStore::new(project::Db::open_fake())); + let project_store = cx.add_model(|_| ProjectStore::new()); let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx)); let themes = ThemeRegistry::new((), cx.font_cache().clone()); Arc::new(Self { @@ -1086,7 +1079,6 @@ impl Workspace { let (_, workspace) = cx.add_window((app_state.build_window_options)(), |cx| { let mut workspace = Workspace::new( Project::local( - false, app_state.client.clone(), app_state.user_store.clone(), app_state.project_store.clone(), @@ -1291,17 +1283,6 @@ impl Workspace { .update(cx, |project, cx| project.remove_worktree(*worktree_id, cx)); } - fn toggle_project_online(&mut self, action: &ToggleProjectOnline, cx: &mut ViewContext) { - let project = action - .project - .clone() - .unwrap_or_else(|| self.project.clone()); - project.update(cx, |project, cx| { - let public = !project.is_online(); - project.set_online(public, cx); - }); - } - fn project_path_for_path( &self, abs_path: &Path, @@ -2617,7 +2598,6 @@ pub fn open_paths( cx.add_window((app_state.build_window_options)(), |cx| { let project = Project::local( - false, app_state.client.clone(), app_state.user_store.clone(), app_state.project_store.clone(), @@ -2642,13 +2622,6 @@ pub fn open_paths( }) .await; - if let Some(project) = new_project { - project - .update(&mut cx, |project, cx| project.restore_state(cx)) - .await - .log_err(); - } - (workspace, items) }) } @@ -2657,7 +2630,6 @@ fn open_new(app_state: &Arc, cx: &mut MutableAppContext) { let (window_id, workspace) = cx.add_window((app_state.build_window_options)(), |cx| { let mut workspace = Workspace::new( Project::local( - false, app_state.client.clone(), app_state.user_store.clone(), app_state.project_store.clone(), diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index de769a6e5ec24c262d4881a3b4aeea393967f342..ea42c61dfba1fd51c0646a0be5f671bbcf16b679 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -140,7 +140,7 @@ fn main() { }) .detach(); - let project_store = cx.add_model(|_| ProjectStore::new(db.clone())); + let project_store = cx.add_model(|_| ProjectStore::new()); let app_state = Arc::new(AppState { languages, themes, diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index fcb6f8f74e1fb6ea0b8e86ea270998a3069cccda..d41b9284c4d768b6f40d29cb6c4d3ece30355f03 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -286,12 +286,7 @@ pub fn initialize_workspace( let project_panel = ProjectPanel::new(workspace.project().clone(), cx); let contact_panel = cx.add_view(|cx| { - ContactsPanel::new( - app_state.user_store.clone(), - app_state.project_store.clone(), - workspace.weak_handle(), - cx, - ) + ContactsPanel::new(app_state.user_store.clone(), workspace.weak_handle(), cx) }); workspace.left_sidebar().update(cx, |sidebar, cx| { From 074b8f18d1922bdecf89afdef7934b44f2370395 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 30 Sep 2022 12:23:57 +0200 Subject: [PATCH 034/112] Rip out project registration and use sharing/unsharing instead --- crates/collab/src/integration_tests.rs | 306 +++++++++-------- crates/collab/src/rpc.rs | 60 +--- crates/collab/src/rpc/store.rs | 87 ++--- crates/contacts_panel/src/contacts_panel.rs | 8 - crates/project/src/project.rs | 351 ++++++-------------- crates/rpc/proto/zed.proto | 17 +- crates/rpc/src/proto.rs | 9 +- 7 files changed, 323 insertions(+), 515 deletions(-) diff --git a/crates/collab/src/integration_tests.rs b/crates/collab/src/integration_tests.rs index ebdc952a0f93d0d7aa9cfd14dbfe2b8cf7c83f16..8753ddee359c9b7f4a5047bcf78ae54e8242ee62 100644 --- a/crates/collab/src/integration_tests.rs +++ b/crates/collab/src/integration_tests.rs @@ -314,11 +314,14 @@ async fn test_share_project( .await; let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; - let project_id = project_a.read_with(cx_a, |project, _| project.remote_id().unwrap()); + let project_id = project_a + .update(cx_a, |project, cx| project.share(cx)) + .await + .unwrap(); // Join that project as client B let client_b_peer_id = client_b.peer_id; - let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await; + let project_b = client_b.build_remote_project(project_id, cx_b).await; let replica_id_b = project_b.read_with(cx_b, |project, _| { assert_eq!( project @@ -390,17 +393,7 @@ async fn test_share_project( // Client B can join again on a different window because they are already a participant. let client_b2 = server.create_client(cx_b2, "user_b").await; - let project_b2 = Project::remote( - project_id, - client_b2.client.clone(), - client_b2.user_store.clone(), - client_b2.project_store.clone(), - client_b2.language_registry.clone(), - FakeFs::new(cx_b2.background()), - cx_b2.to_async(), - ) - .await - .unwrap(); + let project_b2 = client_b2.build_remote_project(project_id, cx_b2).await; deterministic.run_until_parked(); project_a.read_with(cx_a, |project, _| { assert_eq!(project.collaborators().len(), 2); @@ -449,8 +442,12 @@ async fn test_unshare_project( .await; let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; + let project_id = project_a + .update(cx_a, |project, cx| project.share(cx)) + .await + .unwrap(); let worktree_a = project_a.read_with(cx_a, |project, cx| project.worktrees(cx).next().unwrap()); - let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await; + let project_b = client_b.build_remote_project(project_id, cx_b).await; assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared())); project_b @@ -467,7 +464,11 @@ async fn test_unshare_project( assert!(project_b.read_with(cx_b, |project, _| project.is_read_only())); // Client B can join again after client A re-shares. - let project_b2 = client_b.build_remote_project(&project_a, cx_a, cx_b).await; + let project_id = project_a + .update(cx_a, |project, cx| project.share(cx)) + .await + .unwrap(); + let project_b2 = client_b.build_remote_project(project_id, cx_b).await; assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared())); project_b2 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx)) @@ -517,9 +518,12 @@ async fn test_host_disconnect( let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; let worktree_a = project_a.read_with(cx_a, |project, cx| project.worktrees(cx).next().unwrap()); - project_a.read_with(cx_a, |project, _| project.remote_id().unwrap()); + let project_id = project_a + .update(cx_a, |project, cx| project.share(cx)) + .await + .unwrap(); - let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await; + let project_b = client_b.build_remote_project(project_id, cx_b).await; assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared())); let (_, workspace_b) = @@ -574,7 +578,11 @@ async fn test_host_disconnect( }); // Ensure guests can still join. - let project_b2 = client_b.build_remote_project(&project_a, cx_a, cx_b).await; + let project_id = project_a + .update(cx_a, |project, cx| project.share(cx)) + .await + .unwrap(); + let project_b2 = client_b.build_remote_project(project_id, cx_b).await; assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared())); project_b2 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx)) @@ -613,10 +621,14 @@ async fn test_propagate_saves_and_fs_changes( .await; let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; let worktree_a = project_a.read_with(cx_a, |p, cx| p.worktrees(cx).next().unwrap()); + let project_id = project_a + .update(cx_a, |project, cx| project.share(cx)) + .await + .unwrap(); // Join that worktree as clients B and C. - let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await; - let project_c = client_c.build_remote_project(&project_a, cx_a, cx_c).await; + let project_b = client_b.build_remote_project(project_id, cx_b).await; + let project_c = client_c.build_remote_project(project_id, cx_c).await; let worktree_b = project_b.read_with(cx_b, |p, cx| p.worktrees(cx).next().unwrap()); let worktree_c = project_c.read_with(cx_c, |p, cx| p.worktrees(cx).next().unwrap()); @@ -756,7 +768,11 @@ async fn test_fs_operations( ) .await; let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await; - let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await; + let project_id = project_a + .update(cx_a, |project, cx| project.share(cx)) + .await + .unwrap(); + let project_b = client_b.build_remote_project(project_id, cx_b).await; let worktree_a = project_a.read_with(cx_a, |project, cx| project.worktrees(cx).next().unwrap()); let worktree_b = project_b.read_with(cx_b, |project, cx| project.worktrees(cx).next().unwrap()); @@ -1016,7 +1032,11 @@ async fn test_buffer_conflict_after_save(cx_a: &mut TestAppContext, cx_b: &mut T ) .await; let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await; - let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await; + let project_id = project_a + .update(cx_a, |project, cx| project.share(cx)) + .await + .unwrap(); + let project_b = client_b.build_remote_project(project_id, cx_b).await; // Open a buffer as client B let buffer_b = project_b @@ -1065,7 +1085,11 @@ async fn test_buffer_reloading(cx_a: &mut TestAppContext, cx_b: &mut TestAppCont ) .await; let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await; - let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await; + let project_id = project_a + .update(cx_a, |project, cx| project.share(cx)) + .await + .unwrap(); + let project_b = client_b.build_remote_project(project_id, cx_b).await; // Open a buffer as client B let buffer_b = project_b @@ -1114,7 +1138,11 @@ async fn test_editing_while_guest_opens_buffer( .insert_tree("/dir", json!({ "a.txt": "a-contents" })) .await; let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await; - let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await; + let project_id = project_a + .update(cx_a, |project, cx| project.share(cx)) + .await + .unwrap(); + let project_b = client_b.build_remote_project(project_id, cx_b).await; // Open a buffer as client A let buffer_a = project_a @@ -1156,7 +1184,11 @@ async fn test_leaving_worktree_while_opening_buffer( .insert_tree("/dir", json!({ "a.txt": "a-contents" })) .await; let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await; - let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await; + let project_id = project_a + .update(cx_a, |project, cx| project.share(cx)) + .await + .unwrap(); + let project_b = client_b.build_remote_project(project_id, cx_b).await; // See that a guest has joined as client A. project_a @@ -1201,7 +1233,11 @@ async fn test_canceling_buffer_opening( ) .await; let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await; - let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await; + let project_id = project_a + .update(cx_a, |project, cx| project.share(cx)) + .await + .unwrap(); + let project_b = client_b.build_remote_project(project_id, cx_b).await; let buffer_a = project_a .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx)) @@ -1243,7 +1279,11 @@ async fn test_leaving_project(cx_a: &mut TestAppContext, cx_b: &mut TestAppConte ) .await; let (project_a, _) = client_a.build_local_project("/a", cx_a).await; - let _project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await; + let project_id = project_a + .update(cx_a, |project, cx| project.share(cx)) + .await + .unwrap(); + let _project_b = client_b.build_remote_project(project_id, cx_b).await; // Client A sees that a guest has joined. project_a @@ -1257,7 +1297,7 @@ async fn test_leaving_project(cx_a: &mut TestAppContext, cx_b: &mut TestAppConte .await; // Rejoin the project as client B - let _project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await; + let _project_b = client_b.build_remote_project(project_id, cx_b).await; // Client A sees that a guest has re-joined. project_a @@ -1317,7 +1357,10 @@ async fn test_collaborating_with_diagnostics( ) .await; let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; - let project_id = project_a.update(cx_a, |p, _| p.next_remote_id()).await; + let project_id = project_a + .update(cx_a, |project, cx| project.share(cx)) + .await + .unwrap(); // Cause the language server to start. let _buffer = cx_a @@ -1335,7 +1378,7 @@ async fn test_collaborating_with_diagnostics( .unwrap(); // Join the worktree as client B. - let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await; + let project_b = client_b.build_remote_project(project_id, cx_b).await; // Simulate a language server reporting errors for a file. let mut fake_language_server = fake_language_servers.next().await.unwrap(); @@ -1383,7 +1426,7 @@ async fn test_collaborating_with_diagnostics( }); // Join project as client C and observe the diagnostics. - let project_c = client_c.build_remote_project(&project_a, cx_a, cx_c).await; + let project_c = client_c.build_remote_project(project_id, cx_c).await; deterministic.run_until_parked(); project_c.read_with(cx_c, |project, cx| { assert_eq!( @@ -1561,7 +1604,11 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu ) .await; let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; - let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await; + let project_id = project_a + .update(cx_a, |project, cx| project.share(cx)) + .await + .unwrap(); + let project_b = client_b.build_remote_project(project_id, cx_b).await; // Open a file in an editor as the guest. let buffer_b = project_b @@ -1705,8 +1752,12 @@ async fn test_reloading_buffer_manually(cx_a: &mut TestAppContext, cx_b: &mut Te .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)) .await .unwrap(); + let project_id = project_a + .update(cx_a, |project, cx| project.share(cx)) + .await + .unwrap(); - let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await; + let project_b = client_b.build_remote_project(project_id, cx_b).await; let buffer_b = cx_b .background() @@ -1805,7 +1856,11 @@ async fn test_formatting_buffer(cx_a: &mut TestAppContext, cx_b: &mut TestAppCon .insert_tree(&directory, json!({ "a.rs": "let one = \"two\"" })) .await; let (project_a, worktree_id) = client_a.build_local_project(&directory, cx_a).await; - let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await; + let project_id = project_a + .update(cx_a, |project, cx| project.share(cx)) + .await + .unwrap(); + let project_b = client_b.build_remote_project(project_id, cx_b).await; let buffer_b = cx_b .background() @@ -1908,7 +1963,11 @@ async fn test_definition(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { ) .await; let (project_a, worktree_id) = client_a.build_local_project("/root/dir-1", cx_a).await; - let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await; + let project_id = project_a + .update(cx_a, |project, cx| project.share(cx)) + .await + .unwrap(); + let project_b = client_b.build_remote_project(project_id, cx_b).await; // Open the file on client B. let buffer_b = cx_b @@ -2047,7 +2106,11 @@ async fn test_references(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { ) .await; let (project_a, worktree_id) = client_a.build_local_project("/root/dir-1", cx_a).await; - let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await; + let project_id = project_a + .update(cx_a, |project, cx| project.share(cx)) + .await + .unwrap(); + let project_b = client_b.build_remote_project(project_id, cx_b).await; // Open the file on client B. let buffer_b = cx_b @@ -2142,8 +2205,12 @@ async fn test_project_search(cx_a: &mut TestAppContext, cx_b: &mut TestAppContex worktree_2 .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete()) .await; + let project_id = project_a + .update(cx_a, |project, cx| project.share(cx)) + .await + .unwrap(); - let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await; + let project_b = client_b.build_remote_project(project_id, cx_b).await; // Perform a search as the guest. let results = project_b @@ -2212,7 +2279,11 @@ async fn test_document_highlights(cx_a: &mut TestAppContext, cx_b: &mut TestAppC client_a.language_registry.add(Arc::new(language)); let (project_a, worktree_id) = client_a.build_local_project("/root-1", cx_a).await; - let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await; + let project_id = project_a + .update(cx_a, |project, cx| project.share(cx)) + .await + .unwrap(); + let project_b = client_b.build_remote_project(project_id, cx_b).await; // Open the file on client B. let buffer_b = cx_b @@ -2309,7 +2380,11 @@ async fn test_lsp_hover(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { client_a.language_registry.add(Arc::new(language)); let (project_a, worktree_id) = client_a.build_local_project("/root-1", cx_a).await; - let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await; + let project_id = project_a + .update(cx_a, |project, cx| project.share(cx)) + .await + .unwrap(); + let project_b = client_b.build_remote_project(project_id, cx_b).await; // Open the file as the guest let buffer_b = cx_b @@ -2414,7 +2489,11 @@ async fn test_project_symbols(cx_a: &mut TestAppContext, cx_b: &mut TestAppConte ) .await; let (project_a, worktree_id) = client_a.build_local_project("/code/crate-1", cx_a).await; - let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await; + let project_id = project_a + .update(cx_a, |project, cx| project.share(cx)) + .await + .unwrap(); + let project_b = client_b.build_remote_project(project_id, cx_b).await; // Cause the language server to start. let _buffer = cx_b @@ -2510,7 +2589,11 @@ async fn test_open_buffer_while_getting_definition_pointing_to_it( ) .await; let (project_a, worktree_id) = client_a.build_local_project("/root", cx_a).await; - let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await; + let project_id = project_a + .update(cx_a, |project, cx| project.share(cx)) + .await + .unwrap(); + let project_b = client_b.build_remote_project(project_id, cx_b).await; let buffer_b1 = cx_b .background() @@ -2581,9 +2664,13 @@ async fn test_collaborating_with_code_actions( ) .await; let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; + let project_id = project_a + .update(cx_a, |project, cx| project.share(cx)) + .await + .unwrap(); // Join the project as client B. - let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await; + let project_b = client_b.build_remote_project(project_id, cx_b).await; let (_window_b, workspace_b) = cx_b.add_window(|cx| Workspace::new(project_b.clone(), |_, _| unimplemented!(), cx)); let editor_b = workspace_b @@ -2798,7 +2885,11 @@ async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut T ) .await; let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await; - let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await; + let project_id = project_a + .update(cx_a, |project, cx| project.share(cx)) + .await + .unwrap(); + let project_b = client_b.build_remote_project(project_id, cx_b).await; let (_window_b, workspace_b) = cx_b.add_window(|cx| Workspace::new(project_b.clone(), |_, _| unimplemented!(), cx)); @@ -3006,7 +3097,11 @@ async fn test_language_server_statuses( ); }); - let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await; + let project_id = project_a + .update(cx_a, |project, cx| project.share(cx)) + .await + .unwrap(); + let project_b = client_b.build_remote_project(project_id, cx_b).await; project_b.read_with(cx_b, |project, _| { let status = project.language_server_statuses().next().unwrap(); assert_eq!(status.name, "the-language-server"); @@ -3485,79 +3580,6 @@ async fn test_contacts( [("user_a".to_string(), true), ("user_b".to_string(), true)] ); - // Share a project as client A. - client_a.fs.create_dir(Path::new("/a")).await.unwrap(); - let (project_a, _) = client_a.build_local_project("/a", cx_a).await; - - deterministic.run_until_parked(); - assert_eq!( - contacts(&client_a, cx_a), - [("user_b".to_string(), true), ("user_c".to_string(), true)] - ); - assert_eq!( - contacts(&client_b, cx_b), - [("user_a".to_string(), true), ("user_c".to_string(), true)] - ); - assert_eq!( - contacts(&client_c, cx_c), - [("user_a".to_string(), true), ("user_b".to_string(), true)] - ); - - let _project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await; - - deterministic.run_until_parked(); - assert_eq!( - contacts(&client_a, cx_a), - [("user_b".to_string(), true), ("user_c".to_string(), true)] - ); - assert_eq!( - contacts(&client_b, cx_b), - [("user_a".to_string(), true,), ("user_c".to_string(), true)] - ); - assert_eq!( - contacts(&client_c, cx_c), - [("user_a".to_string(), true,), ("user_b".to_string(), true)] - ); - - // Add a local project as client B - client_a.fs.create_dir("/b".as_ref()).await.unwrap(); - let (_project_b, _) = client_b.build_local_project("/b", cx_b).await; - - deterministic.run_until_parked(); - assert_eq!( - contacts(&client_a, cx_a), - [("user_b".to_string(), true), ("user_c".to_string(), true)] - ); - assert_eq!( - contacts(&client_b, cx_b), - [("user_a".to_string(), true), ("user_c".to_string(), true)] - ); - assert_eq!( - contacts(&client_c, cx_c), - [("user_a".to_string(), true,), ("user_b".to_string(), true)] - ); - - project_a - .condition(cx_a, |project, _| { - project.collaborators().contains_key(&client_b.peer_id) - }) - .await; - - cx_a.update(move |_| drop(project_a)); - deterministic.run_until_parked(); - assert_eq!( - contacts(&client_a, cx_a), - [("user_b".to_string(), true), ("user_c".to_string(), true)] - ); - assert_eq!( - contacts(&client_b, cx_b), - [("user_a".to_string(), true), ("user_c".to_string(), true)] - ); - assert_eq!( - contacts(&client_c, cx_c), - [("user_a".to_string(), true), ("user_b".to_string(), true)] - ); - server.disconnect_client(client_c.current_user_id(cx_c)); server.forbid_connections(); deterministic.advance_clock(rpc::RECEIVE_TIMEOUT); @@ -3811,8 +3833,11 @@ async fn test_following(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { ) .await; let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; - - let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await; + let project_id = project_a + .update(cx_a, |project, cx| project.share(cx)) + .await + .unwrap(); + let project_b = client_b.build_remote_project(project_id, cx_b).await; // Client A opens some editors. let workspace_a = client_a.build_workspace(&project_a, cx_a); @@ -4019,9 +4044,13 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T ) .await; let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; + let project_id = project_a + .update(cx_a, |project, cx| project.share(cx)) + .await + .unwrap(); // Client B joins the project. - let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await; + let project_b = client_b.build_remote_project(project_id, cx_b).await; // Client A opens some editors. let workspace_a = client_a.build_workspace(&project_a, cx_a); @@ -4182,7 +4211,11 @@ async fn test_auto_unfollowing(cx_a: &mut TestAppContext, cx_b: &mut TestAppCont ) .await; let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; - let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await; + let project_id = project_a + .update(cx_a, |project, cx| project.share(cx)) + .await + .unwrap(); + let project_b = client_b.build_remote_project(project_id, cx_b).await; // Client A opens some editors. let workspace_a = client_a.build_workspace(&project_a, cx_a); @@ -4331,8 +4364,12 @@ async fn test_peers_simultaneously_following_each_other( client_a.fs.insert_tree("/a", json!({})).await; let (project_a, _) = client_a.build_local_project("/a", cx_a).await; let workspace_a = client_a.build_workspace(&project_a, cx_a); + let project_id = project_a + .update(cx_a, |project, cx| project.share(cx)) + .await + .unwrap(); - let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await; + let project_b = client_b.build_remote_project(project_id, cx_b).await; let workspace_b = client_b.build_workspace(&project_b, cx_b); deterministic.run_until_parked(); @@ -4445,9 +4482,6 @@ async fn test_random_collaboration( cx, ) }); - let host_project_id = host_project - .update(&mut host_cx, |p, _| p.next_remote_id()) - .await; let (collab_worktree, _) = host_project .update(&mut host_cx, |project, cx| { @@ -4588,7 +4622,7 @@ async fn test_random_collaboration( .await; host_language_registry.add(Arc::new(language)); - host_project + let host_project_id = host_project .update(&mut host_cx, |project, cx| project.share(cx)) .await .unwrap(); @@ -5155,40 +5189,26 @@ impl TestClient { worktree .read_with(cx, |tree, _| tree.as_local().unwrap().scan_complete()) .await; - project - .update(cx, |project, _| project.next_remote_id()) - .await; (project, worktree.read_with(cx, |tree, _| tree.id())) } async fn build_remote_project( &self, - host_project: &ModelHandle, - host_cx: &mut TestAppContext, + host_project_id: u64, guest_cx: &mut TestAppContext, ) -> ModelHandle { - let host_project_id = host_project - .read_with(host_cx, |project, _| project.next_remote_id()) - .await; - host_project - .update(host_cx, |project, cx| project.share(cx)) - .await - .unwrap(); - let languages = host_project.read_with(host_cx, |project, _| project.languages().clone()); let project_b = guest_cx.spawn(|cx| { Project::remote( host_project_id, self.client.clone(), self.user_store.clone(), self.project_store.clone(), - languages, + self.language_registry.clone(), FakeFs::new(cx.background()), cx, ) }); - - let project = project_b.await.unwrap(); - project + project_b.await.unwrap() } fn build_workspace( diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 192adb701c17b7f7b398707522825f071594a911..f675ff29313535602d4eddadc573713d09431c59 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -151,11 +151,10 @@ impl Server { .add_message_handler(Server::leave_room) .add_request_handler(Server::call) .add_message_handler(Server::decline_call) - .add_request_handler(Server::register_project) - .add_request_handler(Server::unregister_project) + .add_request_handler(Server::share_project) + .add_message_handler(Server::unshare_project) .add_request_handler(Server::join_project) .add_message_handler(Server::leave_project) - .add_message_handler(Server::unshare_project) .add_message_handler(Server::update_project) .add_message_handler(Server::register_project_activity) .add_request_handler(Server::update_worktree) @@ -470,18 +469,18 @@ impl Server { async fn sign_out(self: &mut Arc, connection_id: ConnectionId) -> Result<()> { self.peer.disconnect(connection_id); - let mut projects_to_unregister = Vec::new(); + let mut projects_to_unshare = Vec::new(); let removed_user_id; { let mut store = self.store().await; let removed_connection = store.remove_connection(connection_id)?; for (project_id, project) in removed_connection.hosted_projects { - projects_to_unregister.push(project_id); + projects_to_unshare.push(project_id); broadcast(connection_id, project.guests.keys().copied(), |conn_id| { self.peer.send( conn_id, - proto::UnregisterProject { + proto::UnshareProject { project_id: project_id.to_proto(), }, ) @@ -514,7 +513,7 @@ impl Server { self.update_user_contacts(removed_user_id).await.trace_err(); - for project_id in projects_to_unregister { + for project_id in projects_to_unshare { self.app_state .db .unregister_project(project_id) @@ -687,10 +686,10 @@ impl Server { } } - async fn register_project( + async fn share_project( self: Arc, - request: TypedEnvelope, - response: Response, + request: TypedEnvelope, + response: Response, ) -> Result<()> { let user_id = self .store() @@ -699,44 +698,15 @@ impl Server { let project_id = self.app_state.db.register_project(user_id).await?; self.store() .await - .register_project(request.sender_id, project_id)?; + .share_project(request.sender_id, project_id)?; - response.send(proto::RegisterProjectResponse { + response.send(proto::ShareProjectResponse { project_id: project_id.to_proto(), })?; Ok(()) } - async fn unregister_project( - self: Arc, - request: TypedEnvelope, - response: Response, - ) -> Result<()> { - let project_id = ProjectId::from_proto(request.payload.project_id); - let project = self - .store() - .await - .unregister_project(project_id, request.sender_id)?; - self.app_state.db.unregister_project(project_id).await?; - - broadcast( - request.sender_id, - project.guests.keys().copied(), - |conn_id| { - self.peer.send( - conn_id, - proto::UnregisterProject { - project_id: project_id.to_proto(), - }, - ) - }, - ); - response.send(proto::Ack {})?; - - Ok(()) - } - async fn unshare_project( self: Arc, message: TypedEnvelope, @@ -746,9 +716,11 @@ impl Server { .store() .await .unshare_project(project_id, message.sender_id)?; - broadcast(message.sender_id, project.guest_connection_ids, |conn_id| { - self.peer.send(conn_id, message.payload.clone()) - }); + broadcast( + message.sender_id, + project.guest_connection_ids(), + |conn_id| self.peer.send(conn_id, message.payload.clone()), + ); Ok(()) } diff --git a/crates/collab/src/rpc/store.rs b/crates/collab/src/rpc/store.rs index deb22301473c6ada22f8d4f65cb40e7482142407..e73b2130c2b00ae4bc460d89361098572422fde9 100644 --- a/crates/collab/src/rpc/store.rs +++ b/crates/collab/src/rpc/store.rs @@ -96,10 +96,6 @@ pub struct LeftProject { pub remove_collaborator: bool, } -pub struct UnsharedProject { - pub guest_connection_ids: Vec, -} - #[derive(Copy, Clone)] pub struct Metrics { pub connections: usize, @@ -201,9 +197,9 @@ impl Store { self.leave_channel(connection_id, channel_id); } - // Unregister and leave all projects. + // Unshare and leave all projects. for project_id in connection_projects { - if let Ok(project) = self.unregister_project(project_id, connection_id) { + if let Ok(project) = self.unshare_project(project_id, connection_id) { result.hosted_projects.insert(project_id, project); } else if self.leave_project(connection_id, project_id).is_ok() { result.guest_project_ids.insert(project_id); @@ -566,7 +562,7 @@ impl Store { } } - pub fn register_project( + pub fn share_project( &mut self, host_connection_id: ConnectionId, project_id: ProjectId, @@ -595,40 +591,7 @@ impl Store { Ok(()) } - pub fn update_project( - &mut self, - project_id: ProjectId, - worktrees: &[proto::WorktreeMetadata], - connection_id: ConnectionId, - ) -> Result<()> { - let project = self - .projects - .get_mut(&project_id) - .ok_or_else(|| anyhow!("no such project"))?; - if project.host_connection_id == connection_id { - let mut old_worktrees = mem::take(&mut project.worktrees); - for worktree in worktrees { - if let Some(old_worktree) = old_worktrees.remove(&worktree.id) { - project.worktrees.insert(worktree.id, old_worktree); - } else { - project.worktrees.insert( - worktree.id, - Worktree { - root_name: worktree.root_name.clone(), - visible: worktree.visible, - ..Default::default() - }, - ); - } - } - - Ok(()) - } else { - Err(anyhow!("no such project"))? - } - } - - pub fn unregister_project( + pub fn unshare_project( &mut self, project_id: ProjectId, connection_id: ConnectionId, @@ -657,35 +620,37 @@ impl Store { } } - pub fn unshare_project( + pub fn update_project( &mut self, project_id: ProjectId, + worktrees: &[proto::WorktreeMetadata], connection_id: ConnectionId, - ) -> Result { + ) -> Result<()> { let project = self .projects .get_mut(&project_id) .ok_or_else(|| anyhow!("no such project"))?; - anyhow::ensure!( - project.host_connection_id == connection_id, - "no such project" - ); - - let guest_connection_ids = project.guest_connection_ids(); - project.active_replica_ids.clear(); - project.guests.clear(); - project.language_servers.clear(); - project.worktrees.clear(); - - for connection_id in &guest_connection_ids { - if let Some(connection) = self.connections.get_mut(connection_id) { - connection.projects.remove(&project_id); + if project.host_connection_id == connection_id { + let mut old_worktrees = mem::take(&mut project.worktrees); + for worktree in worktrees { + if let Some(old_worktree) = old_worktrees.remove(&worktree.id) { + project.worktrees.insert(worktree.id, old_worktree); + } else { + project.worktrees.insert( + worktree.id, + Worktree { + root_name: worktree.root_name.clone(), + visible: worktree.visible, + ..Default::default() + }, + ); + } } - } - Ok(UnsharedProject { - guest_connection_ids, - }) + Ok(()) + } else { + Err(anyhow!("no such project"))? + } } pub fn update_diagnostic_summary( diff --git a/crates/contacts_panel/src/contacts_panel.rs b/crates/contacts_panel/src/contacts_panel.rs index eb1afc3810da7a1e050af84a3a3ded22cfe75811..db6d3bd3b0fc623ccb7ebad67f7aa43eb5498911 100644 --- a/crates/contacts_panel/src/contacts_panel.rs +++ b/crates/contacts_panel/src/contacts_panel.rs @@ -845,14 +845,6 @@ mod tests { .detach(); }); - let request = server.receive::().await.unwrap(); - server - .respond( - request.receipt(), - proto::RegisterProjectResponse { project_id: 200 }, - ) - .await; - let get_users_request = server.receive::().await.unwrap(); server .respond( diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 903e103d41b9bdbcdf330dbbd70d794e3f5c85f8..279e2caaa37d70bae31f5507f5796940ee22cc90 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -35,7 +35,6 @@ use lsp::{ }; use lsp_command::*; use parking_lot::Mutex; -use postage::stream::Stream; use postage::watch; use rand::prelude::*; use search::SearchQuery; @@ -153,10 +152,8 @@ enum WorktreeHandle { enum ProjectClientState { Local { - is_shared: bool, - remote_id_tx: watch::Sender>, - remote_id_rx: watch::Receiver>, - _maintain_remote_id: Task>, + remote_id: Option, + _detect_unshare: Task>, }, Remote { sharing_has_stopped: bool, @@ -382,7 +379,6 @@ impl Project { client.add_model_message_handler(Self::handle_update_language_server); client.add_model_message_handler(Self::handle_remove_collaborator); client.add_model_message_handler(Self::handle_update_project); - client.add_model_message_handler(Self::handle_unregister_project); client.add_model_message_handler(Self::handle_unshare_project); client.add_model_message_handler(Self::handle_create_buffer_for_peer); client.add_model_message_handler(Self::handle_update_buffer_file); @@ -423,24 +419,19 @@ impl Project { cx: &mut MutableAppContext, ) -> ModelHandle { cx.add_model(|cx: &mut ModelContext| { - let (remote_id_tx, remote_id_rx) = watch::channel(); - let _maintain_remote_id = cx.spawn_weak({ - let mut status_rx = client.clone().status(); - move |this, mut cx| async move { - while let Some(status) = status_rx.recv().await { - let this = this.upgrade(&cx)?; - if status.is_connected() { - this.update(&mut cx, |this, cx| this.register(cx)) - .await - .log_err()?; - } else { - this.update(&mut cx, |this, cx| this.unregister(cx)) - .await - .log_err(); + let mut status = client.status(); + let _detect_unshare = cx.spawn_weak(move |this, mut cx| { + async move { + let is_connected = status.next().await.map_or(false, |s| s.is_connected()); + // Even if we're initially connected, any future change of the status means we momentarily disconnected. + if !is_connected || status.next().await.is_some() { + if let Some(this) = this.upgrade(&cx) { + let _ = this.update(&mut cx, |this, cx| this.unshare(cx)); } } - None + Ok(()) } + .log_err() }); let handle = cx.weak_handle(); @@ -456,10 +447,8 @@ impl Project { loading_local_worktrees: Default::default(), buffer_snapshots: Default::default(), client_state: ProjectClientState::Local { - is_shared: false, - remote_id_tx, - remote_id_rx, - _maintain_remote_id, + remote_id: None, + _detect_unshare, }, opened_buffer: watch::channel(), client_subscriptions: Vec::new(), @@ -762,113 +751,9 @@ impl Project { &self.fs } - fn unregister(&mut self, cx: &mut ModelContext) -> Task> { - self.unshare(cx).log_err(); - if let ProjectClientState::Local { remote_id_rx, .. } = &mut self.client_state { - if let Some(remote_id) = *remote_id_rx.borrow() { - let request = self.client.request(proto::UnregisterProject { - project_id: remote_id, - }); - return cx.spawn(|this, mut cx| async move { - let response = request.await; - - // Unregistering the project causes the server to send out a - // contact update removing this project from the host's list - // of online projects. Wait until this contact update has been - // processed before clearing out this project's remote id, so - // that there is no moment where this project appears in the - // contact metadata and *also* has no remote id. - this.update(&mut cx, |this, cx| { - this.user_store() - .update(cx, |store, _| store.contact_updates_done()) - }) - .await; - - this.update(&mut cx, |this, cx| { - if let ProjectClientState::Local { remote_id_tx, .. } = - &mut this.client_state - { - *remote_id_tx.borrow_mut() = None; - } - this.client_subscriptions.clear(); - this.metadata_changed(cx); - }); - response.map(drop) - }); - } - } - Task::ready(Ok(())) - } - - fn register(&mut self, cx: &mut ModelContext) -> Task> { - if let ProjectClientState::Local { remote_id_rx, .. } = &self.client_state { - if remote_id_rx.borrow().is_some() { - return Task::ready(Ok(())); - } - - let response = self.client.request(proto::RegisterProject {}); - cx.spawn(|this, mut cx| async move { - let remote_id = response.await?.project_id; - this.update(&mut cx, |this, cx| { - if let ProjectClientState::Local { remote_id_tx, .. } = &mut this.client_state { - *remote_id_tx.borrow_mut() = Some(remote_id); - } - - this.metadata_changed(cx); - cx.emit(Event::RemoteIdChanged(Some(remote_id))); - this.client_subscriptions - .push(this.client.add_model_for_remote_entity(remote_id, cx)); - Ok(()) - }) - }) - } else { - Task::ready(Err(anyhow!("can't register a remote project"))) - } - } - pub fn remote_id(&self) -> Option { match &self.client_state { - ProjectClientState::Local { remote_id_rx, .. } => *remote_id_rx.borrow(), - ProjectClientState::Remote { remote_id, .. } => Some(*remote_id), - } - } - - pub fn next_remote_id(&self) -> impl Future { - let mut id = None; - let mut watch = None; - match &self.client_state { - ProjectClientState::Local { remote_id_rx, .. } => watch = Some(remote_id_rx.clone()), - ProjectClientState::Remote { remote_id, .. } => id = Some(*remote_id), - } - - async move { - if let Some(id) = id { - return id; - } - let mut watch = watch.unwrap(); - loop { - let id = *watch.borrow(); - if let Some(id) = id { - return id; - } - watch.next().await; - } - } - } - - pub fn shared_remote_id(&self) -> Option { - match &self.client_state { - ProjectClientState::Local { - remote_id_rx, - is_shared, - .. - } => { - if *is_shared { - *remote_id_rx.borrow() - } else { - None - } - } + ProjectClientState::Local { remote_id, .. } => *remote_id, ProjectClientState::Remote { remote_id, .. } => Some(*remote_id), } } @@ -881,7 +766,7 @@ impl Project { } fn metadata_changed(&mut self, cx: &mut ModelContext) { - if let ProjectClientState::Local { remote_id_rx, .. } = &self.client_state { + if let ProjectClientState::Local { remote_id, .. } = &self.client_state { // Broadcast worktrees only if the project is online. let worktrees = self .worktrees @@ -892,7 +777,7 @@ impl Project { .map(|worktree| worktree.read(cx).as_local().unwrap().metadata_proto()) }) .collect(); - if let Some(project_id) = *remote_id_rx.borrow() { + if let Some(project_id) = *remote_id { self.client .send(proto::UpdateProject { project_id, @@ -1164,113 +1049,105 @@ impl Project { } } - pub fn share(&mut self, cx: &mut ModelContext) -> Task> { - let project_id; - if let ProjectClientState::Local { - remote_id_rx, - is_shared, - .. - } = &mut self.client_state - { - if *is_shared { - return Task::ready(Ok(())); - } - *is_shared = true; - if let Some(id) = *remote_id_rx.borrow() { - project_id = id; - } else { - return Task::ready(Err(anyhow!("project hasn't been registered"))); + pub fn share(&mut self, cx: &mut ModelContext) -> Task> { + if let ProjectClientState::Local { remote_id, .. } = &mut self.client_state { + if let Some(remote_id) = remote_id { + return Task::ready(Ok(*remote_id)); } - } else { - return Task::ready(Err(anyhow!("can't share a remote project"))); - }; - for open_buffer in self.opened_buffers.values_mut() { - match open_buffer { - OpenBuffer::Strong(_) => {} - OpenBuffer::Weak(buffer) => { - if let Some(buffer) = buffer.upgrade(cx) { - *open_buffer = OpenBuffer::Strong(buffer); + let response = self.client.request(proto::ShareProject {}); + cx.spawn(|this, mut cx| async move { + let project_id = response.await?.project_id; + let mut worktree_share_tasks = Vec::new(); + this.update(&mut cx, |this, cx| { + if let ProjectClientState::Local { remote_id, .. } = &mut this.client_state { + *remote_id = Some(project_id); } - } - OpenBuffer::Operations(_) => unreachable!(), - } - } - for worktree_handle in self.worktrees.iter_mut() { - match worktree_handle { - WorktreeHandle::Strong(_) => {} - WorktreeHandle::Weak(worktree) => { - if let Some(worktree) = worktree.upgrade(cx) { - *worktree_handle = WorktreeHandle::Strong(worktree); + for open_buffer in this.opened_buffers.values_mut() { + match open_buffer { + OpenBuffer::Strong(_) => {} + OpenBuffer::Weak(buffer) => { + if let Some(buffer) = buffer.upgrade(cx) { + *open_buffer = OpenBuffer::Strong(buffer); + } + } + OpenBuffer::Operations(_) => unreachable!(), + } } - } - } - } - let mut tasks = Vec::new(); - for worktree in self.worktrees(cx).collect::>() { - worktree.update(cx, |worktree, cx| { - let worktree = worktree.as_local_mut().unwrap(); - tasks.push(worktree.share(project_id, cx)); - }); - } + for worktree_handle in this.worktrees.iter_mut() { + match worktree_handle { + WorktreeHandle::Strong(_) => {} + WorktreeHandle::Weak(worktree) => { + if let Some(worktree) = worktree.upgrade(cx) { + *worktree_handle = WorktreeHandle::Strong(worktree); + } + } + } + } - for (server_id, status) in &self.language_server_statuses { - self.client - .send(proto::StartLanguageServer { - project_id, - server: Some(proto::LanguageServer { - id: *server_id as u64, - name: status.name.clone(), - }), - }) - .log_err(); - } + for worktree in this.worktrees(cx).collect::>() { + worktree.update(cx, |worktree, cx| { + let worktree = worktree.as_local_mut().unwrap(); + worktree_share_tasks.push(worktree.share(project_id, cx)); + }); + } - cx.spawn(|this, mut cx| async move { - for task in tasks { - task.await?; - } - this.update(&mut cx, |_, cx| cx.notify()); - Ok(()) - }) + for (server_id, status) in &this.language_server_statuses { + this.client + .send(proto::StartLanguageServer { + project_id, + server: Some(proto::LanguageServer { + id: *server_id as u64, + name: status.name.clone(), + }), + }) + .log_err(); + } + + this.client_subscriptions + .push(this.client.add_model_for_remote_entity(project_id, cx)); + this.metadata_changed(cx); + cx.emit(Event::RemoteIdChanged(Some(project_id))); + cx.notify(); + }); + + futures::future::try_join_all(worktree_share_tasks).await?; + Ok(project_id) + }) + } else { + Task::ready(Err(anyhow!("can't share a remote project"))) + } } pub fn unshare(&mut self, cx: &mut ModelContext) -> Result<()> { - if let ProjectClientState::Local { - is_shared, - remote_id_rx, - .. - } = &mut self.client_state - { - if !*is_shared { - return Ok(()); - } - - *is_shared = false; - self.collaborators.clear(); - self.shared_buffers.clear(); - for worktree_handle in self.worktrees.iter_mut() { - if let WorktreeHandle::Strong(worktree) = worktree_handle { - let is_visible = worktree.update(cx, |worktree, _| { - worktree.as_local_mut().unwrap().unshare(); - worktree.is_visible() - }); - if !is_visible { - *worktree_handle = WorktreeHandle::Weak(worktree.downgrade()); + if let ProjectClientState::Local { remote_id, .. } = &mut self.client_state { + if let Some(project_id) = remote_id.take() { + self.collaborators.clear(); + self.shared_buffers.clear(); + self.client_subscriptions.clear(); + + for worktree_handle in self.worktrees.iter_mut() { + if let WorktreeHandle::Strong(worktree) = worktree_handle { + let is_visible = worktree.update(cx, |worktree, _| { + worktree.as_local_mut().unwrap().unshare(); + worktree.is_visible() + }); + if !is_visible { + *worktree_handle = WorktreeHandle::Weak(worktree.downgrade()); + } } } - } - for open_buffer in self.opened_buffers.values_mut() { - if let OpenBuffer::Strong(buffer) = open_buffer { - *open_buffer = OpenBuffer::Weak(buffer.downgrade()); + for open_buffer in self.opened_buffers.values_mut() { + if let OpenBuffer::Strong(buffer) = open_buffer { + *open_buffer = OpenBuffer::Weak(buffer.downgrade()); + } } - } - cx.notify(); - if let Some(project_id) = *remote_id_rx.borrow() { + self.metadata_changed(cx); + cx.notify(); self.client.send(proto::UnshareProject { project_id })?; } @@ -1750,7 +1627,7 @@ impl Project { ) -> Option<()> { match event { BufferEvent::Operation(operation) => { - if let Some(project_id) = self.shared_remote_id() { + if let Some(project_id) = self.remote_id() { let request = self.client.request(proto::UpdateBuffer { project_id, buffer_id: buffer.read(cx).remote_id(), @@ -2155,7 +2032,7 @@ impl Project { ) .ok(); - if let Some(project_id) = this.shared_remote_id() { + if let Some(project_id) = this.remote_id() { this.client .send(proto::StartLanguageServer { project_id, @@ -2562,7 +2439,7 @@ impl Project { language_server_id: usize, event: proto::update_language_server::Variant, ) { - if let Some(project_id) = self.shared_remote_id() { + if let Some(project_id) = self.remote_id() { self.client .send(proto::UpdateLanguageServer { project_id, @@ -4273,7 +4150,7 @@ impl Project { pub fn is_shared(&self) -> bool { match &self.client_state { - ProjectClientState::Local { is_shared, .. } => *is_shared, + ProjectClientState::Local { remote_id, .. } => remote_id.is_some(), ProjectClientState::Remote { .. } => false, } } @@ -4310,7 +4187,7 @@ impl Project { let project_id = project.update(&mut cx, |project, cx| { project.add_worktree(&worktree, cx); - project.shared_remote_id() + project.remote_id() }); if let Some(project_id) = project_id { @@ -4439,7 +4316,7 @@ impl Project { renamed_buffers.push((cx.handle(), old_path)); } - if let Some(project_id) = self.shared_remote_id() { + if let Some(project_id) = self.remote_id() { self.client .send(proto::UpdateBufferFile { project_id, @@ -4552,16 +4429,6 @@ impl Project { // RPC message handlers - async fn handle_unregister_project( - this: ModelHandle, - _: TypedEnvelope, - _: Arc, - mut cx: AsyncAppContext, - ) -> Result<()> { - this.update(&mut cx, |this, cx| this.disconnected_from_host(cx)); - Ok(()) - } - async fn handle_unshare_project( this: ModelHandle, _: TypedEnvelope, @@ -5987,10 +5854,10 @@ impl Entity for Project { self.project_store.update(cx, ProjectStore::prune_projects); match &self.client_state { - ProjectClientState::Local { remote_id_rx, .. } => { - if let Some(project_id) = *remote_id_rx.borrow() { + ProjectClientState::Local { remote_id, .. } => { + if let Some(project_id) = *remote_id { self.client - .send(proto::UnregisterProject { project_id }) + .send(proto::UnshareProject { project_id }) .log_err(); } } diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 2659ddb86d1895a524332c823aaa5fb8b73a42bb..acb18878d9a7e170b04b2aa7a13f675681951c74 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -22,15 +22,14 @@ message Envelope { DeclineCall decline_call = 13; RoomUpdated room_updated = 14; - RegisterProject register_project = 15; - RegisterProjectResponse register_project_response = 16; - UnregisterProject unregister_project = 17; + ShareProject share_project = 15; + ShareProjectResponse share_project_response = 16; + UnshareProject unshare_project = 17; JoinProject join_project = 21; JoinProjectResponse join_project_response = 22; LeaveProject leave_project = 23; AddProjectCollaborator add_project_collaborator = 24; RemoveProjectCollaborator remove_project_collaborator = 25; - UnshareProject unshare_project = 26; GetDefinition get_definition = 27; GetDefinitionResponse get_definition_response = 28; @@ -195,13 +194,13 @@ message RoomUpdated { Room room = 1; } -message RegisterProject {} +message ShareProject {} -message RegisterProjectResponse { +message ShareProjectResponse { uint64 project_id = 1; } -message UnregisterProject { +message UnshareProject { uint64 project_id = 1; } @@ -285,10 +284,6 @@ message RemoveProjectCollaborator { uint32 peer_id = 2; } -message UnshareProject { - uint64 project_id = 1; -} - message GetDefinition { uint64 project_id = 1; uint64 buffer_id = 2; diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index c2d2d2b321b551171c7592c26c97a86014d5fe0d..822a50c3e4c61ce535c1e41360e3470af6477391 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -141,10 +141,8 @@ messages!( (PrepareRename, Background), (PrepareRenameResponse, Background), (ProjectEntryResponse, Foreground), - (RegisterProjectResponse, Foreground), (RemoveContact, Foreground), (Ping, Foreground), - (RegisterProject, Foreground), (RegisterProjectActivity, Foreground), (ReloadBuffers, Foreground), (ReloadBuffersResponse, Foreground), @@ -158,11 +156,12 @@ messages!( (SearchProjectResponse, Background), (SendChannelMessage, Foreground), (SendChannelMessageResponse, Foreground), + (ShareProject, Foreground), + (ShareProjectResponse, Foreground), (ShowContacts, Foreground), (StartLanguageServer, Foreground), (Test, Foreground), (Unfollow, Foreground), - (UnregisterProject, Foreground), (UnshareProject, Foreground), (UpdateBuffer, Foreground), (UpdateBufferFile, Foreground), @@ -212,7 +211,6 @@ request_messages!( (Ping, Ack), (PerformRename, PerformRenameResponse), (PrepareRename, PrepareRenameResponse), - (RegisterProject, RegisterProjectResponse), (ReloadBuffers, ReloadBuffersResponse), (RequestContact, Ack), (RemoveContact, Ack), @@ -221,8 +219,8 @@ request_messages!( (SaveBuffer, BufferSaved), (SearchProject, SearchProjectResponse), (SendChannelMessage, SendChannelMessageResponse), + (ShareProject, ShareProjectResponse), (Test, Test), - (UnregisterProject, Ack), (UpdateBuffer, Ack), (UpdateWorktree, Ack), ); @@ -263,7 +261,6 @@ entity_messages!( SearchProject, StartLanguageServer, Unfollow, - UnregisterProject, UnshareProject, UpdateBuffer, UpdateBufferFile, From 964a5d2db7cf84b76441adf8756724f03a94e4bc Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 30 Sep 2022 18:21:47 +0200 Subject: [PATCH 035/112] WIP: require sharing projects on a given `Room` --- crates/call/src/participant.rs | 4 -- crates/call/src/room.rs | 34 +-------------- crates/collab/src/rpc.rs | 46 +++++++++++++++----- crates/collab/src/rpc/store.rs | 78 ++++++++++++++++++++++++++++------ crates/project/src/project.rs | 4 +- crates/rpc/proto/zed.proto | 4 +- 6 files changed, 105 insertions(+), 65 deletions(-) diff --git a/crates/call/src/participant.rs b/crates/call/src/participant.rs index cde17b45c2b447af11b5e76bda7b80706ec476d0..c6257b28957d64e8e4c00e7cb9dac96be7e1ab13 100644 --- a/crates/call/src/participant.rs +++ b/crates/call/src/participant.rs @@ -20,10 +20,6 @@ impl ParticipantLocation { } } -pub struct LocalParticipant { - pub projects: Vec>, -} - pub struct RemoteParticipant { pub user_id: u64, pub projects: Vec>, diff --git a/crates/call/src/room.rs b/crates/call/src/room.rs index adf3a676aad22b40a29d38fe63421c917db9ee83..ca8d5ea95fb0e4d3b9fa49a825bd6a56a8218453 100644 --- a/crates/call/src/room.rs +++ b/crates/call/src/room.rs @@ -1,10 +1,9 @@ -use crate::participant::{LocalParticipant, ParticipantLocation, RemoteParticipant}; +use crate::participant::{ParticipantLocation, RemoteParticipant}; use anyhow::{anyhow, Result}; use client::{incoming_call::IncomingCall, proto, Client, PeerId, TypedEnvelope, User, UserStore}; use collections::HashMap; use futures::StreamExt; use gpui::{AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Task}; -use project::Project; use std::sync::Arc; use util::ResultExt; @@ -15,7 +14,6 @@ pub enum Event { pub struct Room { id: u64, status: RoomStatus, - local_participant: LocalParticipant, remote_participants: HashMap, pending_users: Vec>, client: Arc, @@ -53,9 +51,6 @@ impl Room { Self { id, status: RoomStatus::Online, - local_participant: LocalParticipant { - projects: Default::default(), - }, remote_participants: Default::default(), pending_users: Default::default(), _subscriptions: vec![client.add_message_handler(cx.handle(), Self::handle_room_updated)], @@ -179,33 +174,6 @@ impl Room { Ok(()) }) } - - pub fn publish_project(&mut self, project: ModelHandle) -> Task> { - if self.status.is_offline() { - return Task::ready(Err(anyhow!("room is offline"))); - } - - todo!() - } - - pub fn unpublish_project(&mut self, project: ModelHandle) -> Task> { - if self.status.is_offline() { - return Task::ready(Err(anyhow!("room is offline"))); - } - - todo!() - } - - pub fn set_active_project( - &mut self, - project: Option<&ModelHandle>, - ) -> Task> { - if self.status.is_offline() { - return Task::ready(Err(anyhow!("room is offline"))); - } - - todo!() - } } #[derive(Copy, Clone, PartialEq, Eq)] diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index f675ff29313535602d4eddadc573713d09431c59..5b89dc6bf6b6a6cb32b20cf9ebbfcacfb41be044 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -611,8 +611,34 @@ impl Server { async fn leave_room(self: Arc, message: TypedEnvelope) -> Result<()> { let room_id = message.payload.id; let mut store = self.store().await; - let room = store.leave_room(room_id, message.sender_id)?; - self.room_updated(room); + let left_room = store.leave_room(room_id, message.sender_id)?; + + for project in left_room.unshared_projects { + for connection_id in project.connection_ids() { + self.peer.send( + connection_id, + proto::UnshareProject { + project_id: project.id.to_proto(), + }, + )?; + } + } + + for project in left_room.left_projects { + if project.remove_collaborator { + for connection_id in project.connection_ids { + self.peer.send( + connection_id, + proto::RemoveProjectCollaborator { + project_id: project.id.to_proto(), + peer_id: message.sender_id.0, + }, + )?; + } + } + } + + self.room_updated(left_room.room); Ok(()) } @@ -696,13 +722,12 @@ impl Server { .await .user_id_for_connection(request.sender_id)?; let project_id = self.app_state.db.register_project(user_id).await?; - self.store() - .await - .share_project(request.sender_id, project_id)?; - + let mut store = self.store().await; + let room = store.share_project(request.payload.room_id, project_id, request.sender_id)?; response.send(proto::ShareProjectResponse { project_id: project_id.to_proto(), })?; + self.room_updated(room); Ok(()) } @@ -712,15 +737,14 @@ impl Server { message: TypedEnvelope, ) -> Result<()> { let project_id = ProjectId::from_proto(message.payload.project_id); - let project = self - .store() - .await - .unshare_project(project_id, message.sender_id)?; + let mut store = self.store().await; + let (room, project) = store.unshare_project(project_id, message.sender_id)?; broadcast( message.sender_id, project.guest_connection_ids(), |conn_id| self.peer.send(conn_id, message.payload.clone()), ); + self.room_updated(room); Ok(()) } @@ -882,7 +906,7 @@ impl Server { let project; { let mut store = self.store().await; - project = store.leave_project(sender_id, project_id)?; + project = store.leave_project(project_id, sender_id)?; tracing::info!( %project_id, host_user_id = %project.host_user_id, diff --git a/crates/collab/src/rpc/store.rs b/crates/collab/src/rpc/store.rs index e73b2130c2b00ae4bc460d89361098572422fde9..2ae52f7c2b11a9b20602be3a0594b219f5749e49 100644 --- a/crates/collab/src/rpc/store.rs +++ b/crates/collab/src/rpc/store.rs @@ -44,6 +44,8 @@ pub struct Call { #[derive(Serialize)] pub struct Project { + pub id: ProjectId, + pub room_id: RoomId, pub host_connection_id: ConnectionId, pub host: Collaborator, pub guests: HashMap, @@ -90,12 +92,19 @@ pub struct RemovedConnectionState { } pub struct LeftProject { + pub id: ProjectId, pub host_user_id: UserId, pub host_connection_id: ConnectionId, pub connection_ids: Vec, pub remove_collaborator: bool, } +pub struct LeftRoom<'a> { + pub room: &'a proto::Room, + pub unshared_projects: Vec, + pub left_projects: Vec, +} + #[derive(Copy, Clone)] pub struct Metrics { pub connections: usize, @@ -199,9 +208,9 @@ impl Store { // Unshare and leave all projects. for project_id in connection_projects { - if let Ok(project) = self.unshare_project(project_id, connection_id) { + if let Ok((_, project)) = self.unshare_project(project_id, connection_id) { result.hosted_projects.insert(project_id, project); - } else if self.leave_project(connection_id, project_id).is_ok() { + } else if self.leave_project(project_id, connection_id).is_ok() { result.guest_project_ids.insert(project_id); } } @@ -424,11 +433,7 @@ impl Store { Ok((room, recipient_connection_ids)) } - pub fn leave_room( - &mut self, - room_id: RoomId, - connection_id: ConnectionId, - ) -> Result<&proto::Room> { + pub fn leave_room(&mut self, room_id: RoomId, connection_id: ConnectionId) -> Result { let connection = self .connections .get_mut(&connection_id) @@ -454,7 +459,22 @@ impl Store { .retain(|participant| participant.peer_id != connection_id.0); connected_user.active_call = None; - Ok(room) + let mut unshared_projects = Vec::new(); + let mut left_projects = Vec::new(); + for project_id in connection.projects.clone() { + if let Ok((_, project)) = self.unshare_project(project_id, connection_id) { + unshared_projects.push(project); + } else if let Ok(project) = self.leave_project(project_id, connection_id) { + left_projects.push(project); + } + } + + let room = self.rooms.get(&room_id).unwrap(); + Ok(LeftRoom { + room, + unshared_projects, + left_projects, + }) } pub fn room(&self, room_id: RoomId) -> Option<&proto::Room> { @@ -564,17 +584,32 @@ impl Store { pub fn share_project( &mut self, - host_connection_id: ConnectionId, + room_id: RoomId, project_id: ProjectId, - ) -> Result<()> { + host_connection_id: ConnectionId, + ) -> Result<&proto::Room> { let connection = self .connections .get_mut(&host_connection_id) .ok_or_else(|| anyhow!("no such connection"))?; + + let room = self + .rooms + .get_mut(&room_id) + .ok_or_else(|| anyhow!("no such room"))?; + let participant = room + .participants + .iter_mut() + .find(|participant| participant.peer_id == host_connection_id.0) + .ok_or_else(|| anyhow!("no such room"))?; + participant.project_ids.push(project_id.to_proto()); + connection.projects.insert(project_id); self.projects.insert( project_id, Project { + id: project_id, + room_id, host_connection_id, host: Collaborator { user_id: connection.user_id, @@ -588,14 +623,15 @@ impl Store { language_servers: Default::default(), }, ); - Ok(()) + + Ok(room) } pub fn unshare_project( &mut self, project_id: ProjectId, connection_id: ConnectionId, - ) -> Result { + ) -> Result<(&proto::Room, Project)> { match self.projects.entry(project_id) { btree_map::Entry::Occupied(e) => { if e.get().host_connection_id == connection_id { @@ -611,7 +647,20 @@ impl Store { } } - Ok(project) + let room = self + .rooms + .get_mut(&project.room_id) + .ok_or_else(|| anyhow!("no such room"))?; + let participant = room + .participants + .iter_mut() + .find(|participant| participant.peer_id == connection_id.0) + .ok_or_else(|| anyhow!("no such room"))?; + participant + .project_ids + .retain(|id| *id != project_id.to_proto()); + + Ok((room, project)) } else { Err(anyhow!("no such project"))? } @@ -731,8 +780,8 @@ impl Store { pub fn leave_project( &mut self, - connection_id: ConnectionId, project_id: ProjectId, + connection_id: ConnectionId, ) -> Result { let project = self .projects @@ -752,6 +801,7 @@ impl Store { } Ok(LeftProject { + id: project.id, host_connection_id: project.host_connection_id, host_user_id: project.host.user_id, connection_ids: project.connection_ids(), diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 279e2caaa37d70bae31f5507f5796940ee22cc90..901e3b7d85857afb1bf7a467daff39289adb73b4 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1049,13 +1049,13 @@ impl Project { } } - pub fn share(&mut self, cx: &mut ModelContext) -> Task> { + pub fn share(&mut self, room_id: u64, cx: &mut ModelContext) -> Task> { if let ProjectClientState::Local { remote_id, .. } = &mut self.client_state { if let Some(remote_id) = remote_id { return Task::ready(Ok(*remote_id)); } - let response = self.client.request(proto::ShareProject {}); + let response = self.client.request(proto::ShareProject { room_id }); cx.spawn(|this, mut cx| async move { let project_id = response.await?.project_id; let mut worktree_share_tasks = Vec::new(); diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index acb18878d9a7e170b04b2aa7a13f675681951c74..cff10278b425742ed9f03510d60336035f6e2d85 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -194,7 +194,9 @@ message RoomUpdated { Room room = 1; } -message ShareProject {} +message ShareProject { + uint64 room_id = 1; +} message ShareProjectResponse { uint64 project_id = 1; From 64260376539b1929734371ddc12eea44bc6cd9d1 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 3 Oct 2022 15:44:11 +0200 Subject: [PATCH 036/112] Adapt integration tests to always pass a room id to `Project::share` Randomized test is failing, so we'll look into that next. --- crates/call/src/room.rs | 14 +- crates/collab/src/integration_tests.rs | 481 +++++++++++++++---------- crates/collab/src/rpc.rs | 15 +- crates/collab/src/rpc/store.rs | 53 ++- crates/project/src/project.rs | 10 +- 5 files changed, 349 insertions(+), 224 deletions(-) diff --git a/crates/call/src/room.rs b/crates/call/src/room.rs index ca8d5ea95fb0e4d3b9fa49a825bd6a56a8218453..f371384a52ea43f02cd38194ffba7dbb51507e2e 100644 --- a/crates/call/src/room.rs +++ b/crates/call/src/room.rs @@ -24,6 +24,10 @@ pub struct Room { impl Entity for Room { type Event = Event; + + fn release(&mut self, _: &mut MutableAppContext) { + self.client.send(proto::LeaveRoom { id: self.id }).log_err(); + } } impl Room { @@ -99,6 +103,14 @@ impl Room { Ok(()) } + pub fn id(&self) -> u64 { + self.id + } + + pub fn status(&self) -> RoomStatus { + self.status + } + pub fn remote_participants(&self) -> &HashMap { &self.remote_participants } @@ -183,7 +195,7 @@ pub enum RoomStatus { } impl RoomStatus { - fn is_offline(&self) -> bool { + pub fn is_offline(&self) -> bool { matches!(self, RoomStatus::Offline) } } diff --git a/crates/collab/src/integration_tests.rs b/crates/collab/src/integration_tests.rs index 8753ddee359c9b7f4a5047bcf78ae54e8242ee62..3bb63c0229d0b0a48d8ad00cc6d8a5ac302a14d4 100644 --- a/crates/collab/src/integration_tests.rs +++ b/crates/collab/src/integration_tests.rs @@ -74,11 +74,7 @@ async fn test_basic_calls( let client_b = server.create_client(cx_b, "user_b").await; let client_c = server.create_client(cx_c, "user_c").await; server - .make_contacts(vec![ - (&client_a, cx_a), - (&client_b, cx_b), - (&client_c, cx_c), - ]) + .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)]) .await; let room_a = cx_a @@ -224,7 +220,7 @@ async fn test_leaving_room_on_disconnection( let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; server - .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) + .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b)]) .await; let room_a = cx_a @@ -286,15 +282,14 @@ async fn test_share_project( deterministic: Arc, cx_a: &mut TestAppContext, cx_b: &mut TestAppContext, - cx_b2: &mut TestAppContext, ) { cx_a.foreground().forbid_parking(); let (_, window_b) = cx_b.add_window(|_| EmptyView); let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; - server - .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) + let (room_id, _rooms) = server + .create_rooms(&mut [(&client_a, cx_a), (&client_b, cx_b)]) .await; client_a @@ -315,7 +310,7 @@ async fn test_share_project( let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; let project_id = project_a - .update(cx_a, |project, cx| project.share(cx)) + .update(cx_a, |project, cx| project.share(room_id, cx)) .await .unwrap(); @@ -390,30 +385,6 @@ async fn test_share_project( // buffer_a // .condition(&cx_a, |buffer, _| buffer.selection_sets().count() == 0) // .await; - - // Client B can join again on a different window because they are already a participant. - let client_b2 = server.create_client(cx_b2, "user_b").await; - let project_b2 = client_b2.build_remote_project(project_id, cx_b2).await; - deterministic.run_until_parked(); - project_a.read_with(cx_a, |project, _| { - assert_eq!(project.collaborators().len(), 2); - }); - project_b.read_with(cx_b, |project, _| { - assert_eq!(project.collaborators().len(), 2); - }); - project_b2.read_with(cx_b2, |project, _| { - assert_eq!(project.collaborators().len(), 2); - }); - - // Dropping client B's first project removes only that from client A's collaborators. - cx_b.update(move |_| drop(project_b)); - deterministic.run_until_parked(); - project_a.read_with(cx_a, |project, _| { - assert_eq!(project.collaborators().len(), 1); - }); - project_b2.read_with(cx_b2, |project, _| { - assert_eq!(project.collaborators().len(), 1); - }); } #[gpui::test(iterations = 10)] @@ -421,13 +392,15 @@ async fn test_unshare_project( deterministic: Arc, cx_a: &mut TestAppContext, cx_b: &mut TestAppContext, + cx_c: &mut TestAppContext, ) { - cx_a.foreground().forbid_parking(); + deterministic.forbid_parking(); let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; - server - .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) + let client_c = server.create_client(cx_c, "user_c").await; + let (room_id, mut rooms) = server + .create_rooms(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)]) .await; client_a @@ -443,7 +416,7 @@ async fn test_unshare_project( let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; let project_id = project_a - .update(cx_a, |project, cx| project.share(cx)) + .update(cx_a, |project, cx| project.share(room_id, cx)) .await .unwrap(); let worktree_a = project_a.read_with(cx_a, |project, cx| project.worktrees(cx).next().unwrap()); @@ -455,30 +428,39 @@ async fn test_unshare_project( .await .unwrap(); - // When client A unshares the project, client B's project becomes read-only. + // When client B leaves the room, the project becomes read-only. + cx_b.update(|_| drop(rooms.remove(1))); + deterministic.run_until_parked(); + assert!(project_b.read_with(cx_b, |project, _| project.is_read_only())); + + // Client C opens the project. + let project_c = client_c.build_remote_project(project_id, cx_c).await; + + // When client A unshares the project, client C's project becomes read-only. project_a .update(cx_a, |project, cx| project.unshare(cx)) .unwrap(); deterministic.run_until_parked(); assert!(worktree_a.read_with(cx_a, |tree, _| !tree.as_local().unwrap().is_shared())); - assert!(project_b.read_with(cx_b, |project, _| project.is_read_only())); + assert!(project_c.read_with(cx_c, |project, _| project.is_read_only())); - // Client B can join again after client A re-shares. + // Client C can open the project again after client A re-shares. let project_id = project_a - .update(cx_a, |project, cx| project.share(cx)) + .update(cx_a, |project, cx| project.share(room_id, cx)) .await .unwrap(); - let project_b2 = client_b.build_remote_project(project_id, cx_b).await; + let project_c2 = client_c.build_remote_project(project_id, cx_c).await; assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared())); - project_b2 - .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx)) + project_c2 + .update(cx_c, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx)) .await .unwrap(); - // When client A (the host) leaves, the project gets unshared and guests are notified. - cx_a.update(|_| drop(project_a)); + // When client A (the host) leaves the room, the project gets unshared and guests are notified. + cx_a.update(|_| drop(rooms.remove(0))); deterministic.run_until_parked(); - project_b2.read_with(cx_b, |project, _| { + project_a.read_with(cx_a, |project, _| assert!(!project.is_shared())); + project_c2.read_with(cx_c, |project, _| { assert!(project.is_read_only()); assert!(project.collaborators().is_empty()); }); @@ -497,12 +479,8 @@ async fn test_host_disconnect( let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; let client_c = server.create_client(cx_c, "user_c").await; - server - .make_contacts(vec![ - (&client_a, cx_a), - (&client_b, cx_b), - (&client_c, cx_c), - ]) + let (room_id, _rooms) = server + .create_rooms(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)]) .await; client_a @@ -519,7 +497,7 @@ async fn test_host_disconnect( let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; let worktree_a = project_a.read_with(cx_a, |project, cx| project.worktrees(cx).next().unwrap()); let project_id = project_a - .update(cx_a, |project, cx| project.share(cx)) + .update(cx_a, |project, cx| project.share(room_id, cx)) .await .unwrap(); @@ -576,18 +554,6 @@ async fn test_host_disconnect( drop(workspace_b); drop(project_b); }); - - // Ensure guests can still join. - let project_id = project_a - .update(cx_a, |project, cx| project.share(cx)) - .await - .unwrap(); - let project_b2 = client_b.build_remote_project(project_id, cx_b).await; - assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared())); - project_b2 - .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx)) - .await - .unwrap(); } #[gpui::test(iterations = 10)] @@ -601,12 +567,8 @@ async fn test_propagate_saves_and_fs_changes( let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; let client_c = server.create_client(cx_c, "user_c").await; - server - .make_contacts(vec![ - (&client_a, cx_a), - (&client_b, cx_b), - (&client_c, cx_c), - ]) + let (room_id, _rooms) = server + .create_rooms(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)]) .await; client_a @@ -622,7 +584,7 @@ async fn test_propagate_saves_and_fs_changes( let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; let worktree_a = project_a.read_with(cx_a, |p, cx| p.worktrees(cx).next().unwrap()); let project_id = project_a - .update(cx_a, |project, cx| project.share(cx)) + .update(cx_a, |project, cx| project.share(room_id, cx)) .await .unwrap(); @@ -753,8 +715,8 @@ async fn test_fs_operations( let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; - server - .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) + let (room_id, _rooms) = server + .create_rooms(&mut [(&client_a, cx_a), (&client_b, cx_b)]) .await; client_a @@ -769,7 +731,7 @@ async fn test_fs_operations( .await; let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await; let project_id = project_a - .update(cx_a, |project, cx| project.share(cx)) + .update(cx_a, |project, cx| project.share(room_id, cx)) .await .unwrap(); let project_b = client_b.build_remote_project(project_id, cx_b).await; @@ -1018,8 +980,8 @@ async fn test_buffer_conflict_after_save(cx_a: &mut TestAppContext, cx_b: &mut T let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; - server - .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) + let (room_id, _rooms) = server + .create_rooms(&mut [(&client_a, cx_a), (&client_b, cx_b)]) .await; client_a @@ -1033,7 +995,7 @@ async fn test_buffer_conflict_after_save(cx_a: &mut TestAppContext, cx_b: &mut T .await; let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await; let project_id = project_a - .update(cx_a, |project, cx| project.share(cx)) + .update(cx_a, |project, cx| project.share(room_id, cx)) .await .unwrap(); let project_b = client_b.build_remote_project(project_id, cx_b).await; @@ -1071,8 +1033,8 @@ async fn test_buffer_reloading(cx_a: &mut TestAppContext, cx_b: &mut TestAppCont let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; - server - .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) + let (room_id, _rooms) = server + .create_rooms(&mut [(&client_a, cx_a), (&client_b, cx_b)]) .await; client_a @@ -1086,7 +1048,7 @@ async fn test_buffer_reloading(cx_a: &mut TestAppContext, cx_b: &mut TestAppCont .await; let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await; let project_id = project_a - .update(cx_a, |project, cx| project.share(cx)) + .update(cx_a, |project, cx| project.share(room_id, cx)) .await .unwrap(); let project_b = client_b.build_remote_project(project_id, cx_b).await; @@ -1129,8 +1091,8 @@ async fn test_editing_while_guest_opens_buffer( let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; - server - .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) + let (room_id, _rooms) = server + .create_rooms(&mut [(&client_a, cx_a), (&client_b, cx_b)]) .await; client_a @@ -1139,7 +1101,7 @@ async fn test_editing_while_guest_opens_buffer( .await; let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await; let project_id = project_a - .update(cx_a, |project, cx| project.share(cx)) + .update(cx_a, |project, cx| project.share(room_id, cx)) .await .unwrap(); let project_b = client_b.build_remote_project(project_id, cx_b).await; @@ -1175,8 +1137,8 @@ async fn test_leaving_worktree_while_opening_buffer( let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; - server - .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) + let (room_id, _rooms) = server + .create_rooms(&mut [(&client_a, cx_a), (&client_b, cx_b)]) .await; client_a @@ -1185,7 +1147,7 @@ async fn test_leaving_worktree_while_opening_buffer( .await; let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await; let project_id = project_a - .update(cx_a, |project, cx| project.share(cx)) + .update(cx_a, |project, cx| project.share(room_id, cx)) .await .unwrap(); let project_b = client_b.build_remote_project(project_id, cx_b).await; @@ -1219,8 +1181,8 @@ async fn test_canceling_buffer_opening( let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; - server - .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) + let (room_id, _rooms) = server + .create_rooms(&mut [(&client_a, cx_a), (&client_b, cx_b)]) .await; client_a @@ -1234,7 +1196,7 @@ async fn test_canceling_buffer_opening( .await; let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await; let project_id = project_a - .update(cx_a, |project, cx| project.share(cx)) + .update(cx_a, |project, cx| project.share(room_id, cx)) .await .unwrap(); let project_b = client_b.build_remote_project(project_id, cx_b).await; @@ -1259,13 +1221,19 @@ async fn test_canceling_buffer_opening( } #[gpui::test(iterations = 10)] -async fn test_leaving_project(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { - cx_a.foreground().forbid_parking(); +async fn test_leaving_project( + deterministic: Arc, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, + cx_c: &mut TestAppContext, +) { + deterministic.forbid_parking(); let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; - server - .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) + let client_c = server.create_client(cx_c, "user_c").await; + let (room_id, _rooms) = server + .create_rooms(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)]) .await; client_a @@ -1280,37 +1248,66 @@ async fn test_leaving_project(cx_a: &mut TestAppContext, cx_b: &mut TestAppConte .await; let (project_a, _) = client_a.build_local_project("/a", cx_a).await; let project_id = project_a - .update(cx_a, |project, cx| project.share(cx)) + .update(cx_a, |project, cx| project.share(room_id, cx)) .await .unwrap(); - let _project_b = client_b.build_remote_project(project_id, cx_b).await; + let project_b = client_b.build_remote_project(project_id, cx_b).await; + let project_c = client_c.build_remote_project(project_id, cx_c).await; // Client A sees that a guest has joined. - project_a - .condition(cx_a, |p, _| p.collaborators().len() == 1) - .await; + deterministic.run_until_parked(); + project_a.read_with(cx_a, |project, _| { + assert_eq!(project.collaborators().len(), 2); + }); + project_b.read_with(cx_b, |project, _| { + assert_eq!(project.collaborators().len(), 2); + }); + project_c.read_with(cx_c, |project, _| { + assert_eq!(project.collaborators().len(), 2); + }); - // Drop client B's connection and ensure client A observes client B leaving the project. + // Drop client B's connection and ensure client A and client C observe client B leaving the project. client_b.disconnect(&cx_b.to_async()).unwrap(); - project_a - .condition(cx_a, |p, _| p.collaborators().is_empty()) - .await; - - // Rejoin the project as client B - let _project_b = client_b.build_remote_project(project_id, cx_b).await; + deterministic.run_until_parked(); + project_a.read_with(cx_a, |project, _| { + assert_eq!(project.collaborators().len(), 1); + }); + project_b.read_with(cx_b, |project, _| { + assert!(project.is_read_only()); + }); + project_c.read_with(cx_c, |project, _| { + assert_eq!(project.collaborators().len(), 1); + }); - // Client A sees that a guest has re-joined. - project_a - .condition(cx_a, |p, _| p.collaborators().len() == 1) - .await; + // Client B can't join the project, unless they re-join the room. + cx_b.spawn(|cx| { + Project::remote( + project_id, + client_b.client.clone(), + client_b.user_store.clone(), + client_b.project_store.clone(), + client_b.language_registry.clone(), + FakeFs::new(cx.background()), + cx, + ) + }) + .await + .unwrap_err(); - // Simulate connection loss for client B and ensure client A observes client B leaving the project. - client_b.wait_for_current_user(cx_b).await; - server.disconnect_client(client_b.current_user_id(cx_b)); + // Simulate connection loss for client C and ensure client A observes client C leaving the project. + client_c.wait_for_current_user(cx_c).await; + server.disconnect_client(client_c.current_user_id(cx_c)); cx_a.foreground().advance_clock(rpc::RECEIVE_TIMEOUT); - project_a - .condition(cx_a, |p, _| p.collaborators().is_empty()) - .await; + deterministic.run_until_parked(); + project_a.read_with(cx_a, |project, _| { + assert_eq!(project.collaborators().len(), 0); + }); + project_b.read_with(cx_b, |project, _| { + assert!(project.is_read_only()); + }); + project_c.read_with(cx_c, |project, _| { + assert!(project.is_read_only()); + }); } #[gpui::test(iterations = 10)] @@ -1325,12 +1322,8 @@ async fn test_collaborating_with_diagnostics( let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; let client_c = server.create_client(cx_c, "user_c").await; - server - .make_contacts(vec![ - (&client_a, cx_a), - (&client_b, cx_b), - (&client_c, cx_c), - ]) + let (room_id, _rooms) = server + .create_rooms(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)]) .await; // Set up a fake language server. @@ -1358,7 +1351,7 @@ async fn test_collaborating_with_diagnostics( .await; let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; let project_id = project_a - .update(cx_a, |project, cx| project.share(cx)) + .update(cx_a, |project, cx| project.share(room_id, cx)) .await .unwrap(); @@ -1566,8 +1559,8 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; - server - .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) + let (room_id, _rooms) = server + .create_rooms(&mut [(&client_a, cx_a), (&client_b, cx_b)]) .await; // Set up a fake language server. @@ -1605,7 +1598,7 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu .await; let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; let project_id = project_a - .update(cx_a, |project, cx| project.share(cx)) + .update(cx_a, |project, cx| project.share(room_id, cx)) .await .unwrap(); let project_b = client_b.build_remote_project(project_id, cx_b).await; @@ -1739,8 +1732,8 @@ async fn test_reloading_buffer_manually(cx_a: &mut TestAppContext, cx_b: &mut Te let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; - server - .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) + let (room_id, _rooms) = server + .create_rooms(&mut [(&client_a, cx_a), (&client_b, cx_b)]) .await; client_a @@ -1753,7 +1746,7 @@ async fn test_reloading_buffer_manually(cx_a: &mut TestAppContext, cx_b: &mut Te .await .unwrap(); let project_id = project_a - .update(cx_a, |project, cx| project.share(cx)) + .update(cx_a, |project, cx| project.share(room_id, cx)) .await .unwrap(); @@ -1831,8 +1824,8 @@ async fn test_formatting_buffer(cx_a: &mut TestAppContext, cx_b: &mut TestAppCon let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; - server - .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) + let (room_id, _rooms) = server + .create_rooms(&mut [(&client_a, cx_a), (&client_b, cx_b)]) .await; // Set up a fake language server. @@ -1857,7 +1850,7 @@ async fn test_formatting_buffer(cx_a: &mut TestAppContext, cx_b: &mut TestAppCon .await; let (project_a, worktree_id) = client_a.build_local_project(&directory, cx_a).await; let project_id = project_a - .update(cx_a, |project, cx| project.share(cx)) + .update(cx_a, |project, cx| project.share(room_id, cx)) .await .unwrap(); let project_b = client_b.build_remote_project(project_id, cx_b).await; @@ -1931,8 +1924,8 @@ async fn test_definition(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; - server - .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) + let (room_id, _rooms) = server + .create_rooms(&mut [(&client_a, cx_a), (&client_b, cx_b)]) .await; // Set up a fake language server. @@ -1964,7 +1957,7 @@ async fn test_definition(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { .await; let (project_a, worktree_id) = client_a.build_local_project("/root/dir-1", cx_a).await; let project_id = project_a - .update(cx_a, |project, cx| project.share(cx)) + .update(cx_a, |project, cx| project.share(room_id, cx)) .await .unwrap(); let project_b = client_b.build_remote_project(project_id, cx_b).await; @@ -2074,8 +2067,8 @@ async fn test_references(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; - server - .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) + let (room_id, _rooms) = server + .create_rooms(&mut [(&client_a, cx_a), (&client_b, cx_b)]) .await; // Set up a fake language server. @@ -2107,7 +2100,7 @@ async fn test_references(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { .await; let (project_a, worktree_id) = client_a.build_local_project("/root/dir-1", cx_a).await; let project_id = project_a - .update(cx_a, |project, cx| project.share(cx)) + .update(cx_a, |project, cx| project.share(room_id, cx)) .await .unwrap(); let project_b = client_b.build_remote_project(project_id, cx_b).await; @@ -2174,8 +2167,8 @@ async fn test_project_search(cx_a: &mut TestAppContext, cx_b: &mut TestAppContex let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; - server - .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) + let (room_id, _rooms) = server + .create_rooms(&mut [(&client_a, cx_a), (&client_b, cx_b)]) .await; client_a @@ -2206,7 +2199,7 @@ async fn test_project_search(cx_a: &mut TestAppContext, cx_b: &mut TestAppContex .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete()) .await; let project_id = project_a - .update(cx_a, |project, cx| project.share(cx)) + .update(cx_a, |project, cx| project.share(room_id, cx)) .await .unwrap(); @@ -2252,8 +2245,8 @@ async fn test_document_highlights(cx_a: &mut TestAppContext, cx_b: &mut TestAppC let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; - server - .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) + let (room_id, _rooms) = server + .create_rooms(&mut [(&client_a, cx_a), (&client_b, cx_b)]) .await; client_a @@ -2280,7 +2273,7 @@ async fn test_document_highlights(cx_a: &mut TestAppContext, cx_b: &mut TestAppC let (project_a, worktree_id) = client_a.build_local_project("/root-1", cx_a).await; let project_id = project_a - .update(cx_a, |project, cx| project.share(cx)) + .update(cx_a, |project, cx| project.share(room_id, cx)) .await .unwrap(); let project_b = client_b.build_remote_project(project_id, cx_b).await; @@ -2353,8 +2346,8 @@ async fn test_lsp_hover(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; - server - .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) + let (room_id, _rooms) = server + .create_rooms(&mut [(&client_a, cx_a), (&client_b, cx_b)]) .await; client_a @@ -2381,7 +2374,7 @@ async fn test_lsp_hover(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { let (project_a, worktree_id) = client_a.build_local_project("/root-1", cx_a).await; let project_id = project_a - .update(cx_a, |project, cx| project.share(cx)) + .update(cx_a, |project, cx| project.share(room_id, cx)) .await .unwrap(); let project_b = client_b.build_remote_project(project_id, cx_b).await; @@ -2455,8 +2448,8 @@ async fn test_project_symbols(cx_a: &mut TestAppContext, cx_b: &mut TestAppConte let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; - server - .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) + let (room_id, _rooms) = server + .create_rooms(&mut [(&client_a, cx_a), (&client_b, cx_b)]) .await; // Set up a fake language server. @@ -2490,7 +2483,7 @@ async fn test_project_symbols(cx_a: &mut TestAppContext, cx_b: &mut TestAppConte .await; let (project_a, worktree_id) = client_a.build_local_project("/code/crate-1", cx_a).await; let project_id = project_a - .update(cx_a, |project, cx| project.share(cx)) + .update(cx_a, |project, cx| project.share(room_id, cx)) .await .unwrap(); let project_b = client_b.build_remote_project(project_id, cx_b).await; @@ -2562,8 +2555,8 @@ async fn test_open_buffer_while_getting_definition_pointing_to_it( let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; - server - .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) + let (room_id, _rooms) = server + .create_rooms(&mut [(&client_a, cx_a), (&client_b, cx_b)]) .await; // Set up a fake language server. @@ -2590,7 +2583,7 @@ async fn test_open_buffer_while_getting_definition_pointing_to_it( .await; let (project_a, worktree_id) = client_a.build_local_project("/root", cx_a).await; let project_id = project_a - .update(cx_a, |project, cx| project.share(cx)) + .update(cx_a, |project, cx| project.share(room_id, cx)) .await .unwrap(); let project_b = client_b.build_remote_project(project_id, cx_b).await; @@ -2637,8 +2630,8 @@ async fn test_collaborating_with_code_actions( let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; - server - .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) + let (room_id, _rooms) = server + .create_rooms(&mut [(&client_a, cx_a), (&client_b, cx_b)]) .await; // Set up a fake language server. @@ -2665,7 +2658,7 @@ async fn test_collaborating_with_code_actions( .await; let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; let project_id = project_a - .update(cx_a, |project, cx| project.share(cx)) + .update(cx_a, |project, cx| project.share(room_id, cx)) .await .unwrap(); @@ -2847,8 +2840,8 @@ async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut T let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; - server - .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) + let (room_id, _rooms) = server + .create_rooms(&mut [(&client_a, cx_a), (&client_b, cx_b)]) .await; // Set up a fake language server. @@ -2886,7 +2879,7 @@ async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut T .await; let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await; let project_id = project_a - .update(cx_a, |project, cx| project.share(cx)) + .update(cx_a, |project, cx| project.share(room_id, cx)) .await .unwrap(); let project_b = client_b.build_remote_project(project_id, cx_b).await; @@ -3038,8 +3031,8 @@ async fn test_language_server_statuses( let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; - server - .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) + let (room_id, _rooms) = server + .create_rooms(&mut [(&client_a, cx_a), (&client_b, cx_b)]) .await; // Set up a fake language server. @@ -3098,7 +3091,7 @@ async fn test_language_server_statuses( }); let project_id = project_a - .update(cx_a, |project, cx| project.share(cx)) + .update(cx_a, |project, cx| project.share(room_id, cx)) .await .unwrap(); let project_b = client_b.build_remote_project(project_id, cx_b).await; @@ -3559,11 +3552,7 @@ async fn test_contacts( let client_b = server.create_client(cx_b, "user_b").await; let client_c = server.create_client(cx_c, "user_c").await; server - .make_contacts(vec![ - (&client_a, cx_a), - (&client_b, cx_b), - (&client_c, cx_c), - ]) + .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)]) .await; deterministic.run_until_parked(); @@ -3815,8 +3804,8 @@ async fn test_following(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; - server - .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) + let (room_id, _rooms) = server + .create_rooms(&mut [(&client_a, cx_a), (&client_b, cx_b)]) .await; cx_a.update(editor::init); cx_b.update(editor::init); @@ -3834,7 +3823,7 @@ async fn test_following(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { .await; let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; let project_id = project_a - .update(cx_a, |project, cx| project.share(cx)) + .update(cx_a, |project, cx| project.share(room_id, cx)) .await .unwrap(); let project_b = client_b.build_remote_project(project_id, cx_b).await; @@ -4024,8 +4013,8 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; - server - .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) + let (room_id, _rooms) = server + .create_rooms(&mut [(&client_a, cx_a), (&client_b, cx_b)]) .await; cx_a.update(editor::init); cx_b.update(editor::init); @@ -4045,7 +4034,7 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T .await; let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; let project_id = project_a - .update(cx_a, |project, cx| project.share(cx)) + .update(cx_a, |project, cx| project.share(room_id, cx)) .await .unwrap(); @@ -4192,8 +4181,8 @@ async fn test_auto_unfollowing(cx_a: &mut TestAppContext, cx_b: &mut TestAppCont let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; - server - .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) + let (room_id, _rooms) = server + .create_rooms(&mut [(&client_a, cx_a), (&client_b, cx_b)]) .await; cx_a.update(editor::init); cx_b.update(editor::init); @@ -4212,7 +4201,7 @@ async fn test_auto_unfollowing(cx_a: &mut TestAppContext, cx_b: &mut TestAppCont .await; let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; let project_id = project_a - .update(cx_a, |project, cx| project.share(cx)) + .update(cx_a, |project, cx| project.share(room_id, cx)) .await .unwrap(); let project_b = client_b.build_remote_project(project_id, cx_b).await; @@ -4355,8 +4344,8 @@ async fn test_peers_simultaneously_following_each_other( let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; - server - .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) + let (room_id, _rooms) = server + .create_rooms(&mut [(&client_a, cx_a), (&client_b, cx_b)]) .await; cx_a.update(editor::init); cx_b.update(editor::init); @@ -4365,7 +4354,7 @@ async fn test_peers_simultaneously_following_each_other( let (project_a, _) = client_a.build_local_project("/a", cx_a).await; let workspace_a = client_a.build_workspace(&project_a, cx_a); let project_id = project_a - .update(cx_a, |project, cx| project.share(cx)) + .update(cx_a, |project, cx| project.share(room_id, cx)) .await .unwrap(); @@ -4432,7 +4421,8 @@ async fn test_random_collaboration( let mut server = TestServer::start(cx.foreground(), cx.background()).await; let db = server.app_state.db.clone(); - let host_user_id = db.create_user("host", None, false).await.unwrap(); + + let room_creator_user_id = db.create_user("room-creator", None, false).await.unwrap(); let mut available_guests = vec![ "guest-1".to_string(), "guest-2".to_string(), @@ -4440,23 +4430,32 @@ async fn test_random_collaboration( "guest-4".to_string(), ]; - for username in &available_guests { - let guest_user_id = db.create_user(username, None, false).await.unwrap(); - assert_eq!(*username, format!("guest-{}", guest_user_id)); + for username in Some(&"host".to_string()) + .into_iter() + .chain(&available_guests) + { + let user_id = db.create_user(username, None, false).await.unwrap(); server .app_state .db - .send_contact_request(guest_user_id, host_user_id) + .send_contact_request(user_id, room_creator_user_id) .await .unwrap(); server .app_state .db - .respond_to_contact_request(host_user_id, guest_user_id, true) + .respond_to_contact_request(room_creator_user_id, user_id, true) .await .unwrap(); } + let client = server.create_client(cx, "room-creator").await; + let room = cx + .update(|cx| Room::create(client.client.clone(), client.user_store.clone(), cx)) + .await + .unwrap(); + let room_id = room.read_with(cx, |room, _| room.id()); + let mut clients = Vec::new(); let mut user_ids = Vec::new(); let mut op_start_signals = Vec::new(); @@ -4622,15 +4621,36 @@ async fn test_random_collaboration( .await; host_language_registry.add(Arc::new(language)); + let host_user_id = host.current_user_id(&host_cx); + room.update(cx, |room, cx| room.call(host_user_id.to_proto(), cx)) + .await + .unwrap(); + deterministic.run_until_parked(); + let call = host + .user_store + .read_with(&host_cx, |user_store, _| user_store.incoming_call()); + let host_room = host_cx + .update(|cx| { + Room::join( + call.borrow().as_ref().unwrap(), + host.client.clone(), + host.user_store.clone(), + cx, + ) + }) + .await + .unwrap(); + let host_project_id = host_project - .update(&mut host_cx, |project, cx| project.share(cx)) + .update(&mut host_cx, |project, cx| project.share(room_id, cx)) .await .unwrap(); let op_start_signal = futures::channel::mpsc::unbounded(); - user_ids.push(host.current_user_id(&host_cx)); + user_ids.push(host_user_id); op_start_signals.push(op_start_signal.0); clients.push(host_cx.foreground().spawn(host.simulate_host( + host_room, host_project, op_start_signal.1, rng.clone(), @@ -4692,6 +4712,28 @@ async fn test_random_collaboration( deterministic.start_waiting(); let guest = server.create_client(&mut guest_cx, &guest_username).await; + let guest_user_id = guest.current_user_id(&guest_cx); + + room.update(cx, |room, cx| room.call(guest_user_id.to_proto(), cx)) + .await + .unwrap(); + deterministic.run_until_parked(); + let call = guest + .user_store + .read_with(&guest_cx, |user_store, _| user_store.incoming_call()); + + let guest_room = guest_cx + .update(|cx| { + Room::join( + call.borrow().as_ref().unwrap(), + guest.client.clone(), + guest.user_store.clone(), + cx, + ) + }) + .await + .unwrap(); + let guest_project = Project::remote( host_project_id, guest.client.clone(), @@ -4706,10 +4748,11 @@ async fn test_random_collaboration( deterministic.finish_waiting(); let op_start_signal = futures::channel::mpsc::unbounded(); - user_ids.push(guest.current_user_id(&guest_cx)); + user_ids.push(guest_user_id); op_start_signals.push(op_start_signal.0); clients.push(guest_cx.foreground().spawn(guest.simulate_guest( guest_username.clone(), + guest_room, guest_project, op_start_signal.1, rng.clone(), @@ -5039,12 +5082,14 @@ impl TestServer { self.forbid_connections.store(false, SeqCst); } - async fn make_contacts(&self, mut clients: Vec<(&TestClient, &mut TestAppContext)>) { - while let Some((client_a, cx_a)) = clients.pop() { - for (client_b, cx_b) in &mut clients { + async fn make_contacts(&self, clients: &mut [(&TestClient, &mut TestAppContext)]) { + for ix in 1..clients.len() { + let (left, right) = clients.split_at_mut(ix); + let (client_a, cx_a) = left.last_mut().unwrap(); + for (client_b, cx_b) in right { client_a .user_store - .update(cx_a, |store, cx| { + .update(*cx_a, |store, cx| { store.request_contact(client_b.user_id().unwrap(), cx) }) .await @@ -5061,6 +5106,52 @@ impl TestServer { } } + async fn create_rooms( + &self, + clients: &mut [(&TestClient, &mut TestAppContext)], + ) -> (u64, Vec>) { + self.make_contacts(clients).await; + + let mut rooms = Vec::new(); + + let (left, right) = clients.split_at_mut(1); + let (client_a, cx_a) = &mut left[0]; + + let room_a = cx_a + .update(|cx| Room::create(client_a.client.clone(), client_a.user_store.clone(), cx)) + .await + .unwrap(); + let room_id = room_a.read_with(*cx_a, |room, _| room.id()); + + for (client_b, cx_b) in right { + let user_id_b = client_b.current_user_id(*cx_b).to_proto(); + room_a + .update(*cx_a, |room, cx| room.call(user_id_b, cx)) + .await + .unwrap(); + + cx_b.foreground().run_until_parked(); + let incoming_call = client_b + .user_store + .read_with(*cx_b, |user_store, _| user_store.incoming_call()); + let room_b = cx_b + .update(|cx| { + Room::join( + incoming_call.borrow().as_ref().unwrap(), + client_b.client.clone(), + client_b.user_store.clone(), + cx, + ) + }) + .await + .unwrap(); + rooms.push(room_b); + } + + rooms.insert(0, room_a); + (room_id, rooms) + } + async fn build_app_state(test_db: &TestDb) -> Arc { Arc::new(AppState { db: test_db.db().clone(), @@ -5224,6 +5315,7 @@ impl TestClient { async fn simulate_host( mut self, + _room: ModelHandle, project: ModelHandle, op_start_signal: futures::channel::mpsc::UnboundedReceiver<()>, rng: Arc>, @@ -5361,6 +5453,7 @@ impl TestClient { pub async fn simulate_guest( mut self, guest_username: String, + _room: ModelHandle, project: ModelHandle, op_start_signal: futures::channel::mpsc::UnboundedReceiver<()>, rng: Arc>, diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 5b89dc6bf6b6a6cb32b20cf9ebbfcacfb41be044..31f99759cd12a8bdb816f7b54cdc7838ef8f71b4 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -635,6 +635,13 @@ impl Server { }, )?; } + + self.peer.send( + message.sender_id, + proto::UnshareProject { + project_id: project.id.to_proto(), + }, + )?; } } @@ -798,14 +805,6 @@ impl Server { }; tracing::info!(%project_id, %host_user_id, %host_connection_id, "join project"); - let has_contact = self - .app_state - .db - .has_contact(guest_user_id, host_user_id) - .await?; - if !has_contact { - return Err(anyhow!("no such project"))?; - } let mut store = self.store().await; let (project, replica_id) = store.join_project(request.sender_id, project_id)?; diff --git a/crates/collab/src/rpc/store.rs b/crates/collab/src/rpc/store.rs index 2ae52f7c2b11a9b20602be3a0594b219f5749e49..9b241446b52eaf6f21265243741dcf2502cd5f32 100644 --- a/crates/collab/src/rpc/store.rs +++ b/crates/collab/src/rpc/store.rs @@ -39,7 +39,7 @@ struct ConnectionState { pub struct Call { pub caller_user_id: UserId, pub room_id: RoomId, - pub joined: bool, + pub connection_id: Option, } #[derive(Serialize)] @@ -163,7 +163,7 @@ impl Store { let connected_user = self.connected_users.entry(user_id).or_default(); connected_user.connection_ids.insert(connection_id); if let Some(active_call) = connected_user.active_call { - if active_call.joined { + if active_call.connection_id.is_some() { None } else { let room = self.room(active_call.room_id)?; @@ -378,7 +378,7 @@ impl Store { connected_user.active_call = Some(Call { caller_user_id: connection.user_id, room_id, - joined: true, + connection_id: Some(creator_connection_id), }); Ok(room_id) } @@ -404,7 +404,7 @@ impl Store { .as_mut() .ok_or_else(|| anyhow!("not being called"))?; anyhow::ensure!( - active_call.room_id == room_id && !active_call.joined, + active_call.room_id == room_id && active_call.connection_id.is_none(), "not being called on this room" ); @@ -428,7 +428,7 @@ impl Store { )), }), }); - active_call.joined = true; + active_call.connection_id = Some(connection_id); Ok((room, recipient_connection_ids)) } @@ -440,25 +440,20 @@ impl Store { .ok_or_else(|| anyhow!("no such connection"))?; let user_id = connection.user_id; - let mut connected_user = self + let connected_user = self .connected_users - .get_mut(&user_id) + .get(&user_id) .ok_or_else(|| anyhow!("no such connection"))?; anyhow::ensure!( connected_user .active_call - .map_or(false, |call| call.room_id == room_id && call.joined), + .map_or(false, |call| call.room_id == room_id + && call.connection_id == Some(connection_id)), "cannot leave a room before joining it" ); - let room = self - .rooms - .get_mut(&room_id) - .ok_or_else(|| anyhow!("no such room"))?; - room.participants - .retain(|participant| participant.peer_id != connection_id.0); - connected_user.active_call = None; - + // Given that users can only join one room at a time, we can safely unshare + // and leave all projects associated with the connection. let mut unshared_projects = Vec::new(); let mut left_projects = Vec::new(); for project_id in connection.projects.clone() { @@ -468,8 +463,15 @@ impl Store { left_projects.push(project); } } + self.connected_users.get_mut(&user_id).unwrap().active_call = None; + + let room = self + .rooms + .get_mut(&room_id) + .ok_or_else(|| anyhow!("no such room"))?; + room.participants + .retain(|participant| participant.peer_id != connection_id.0); - let room = self.rooms.get(&room_id).unwrap(); Ok(LeftRoom { room, unshared_projects, @@ -521,7 +523,7 @@ impl Store { recipient.active_call = Some(Call { caller_user_id, room_id, - joined: false, + connection_id: None, }); Ok(( @@ -546,7 +548,8 @@ impl Store { .ok_or_else(|| anyhow!("no such connection"))?; anyhow::ensure!(recipient .active_call - .map_or(false, |call| call.room_id == room_id && !call.joined)); + .map_or(false, |call| call.room_id == room_id + && call.connection_id.is_none())); recipient.active_call = None; let room = self .rooms @@ -754,10 +757,22 @@ impl Store { .connections .get_mut(&requester_connection_id) .ok_or_else(|| anyhow!("no such connection"))?; + let user = self + .connected_users + .get(&connection.user_id) + .ok_or_else(|| anyhow!("no such connection"))?; + let active_call = user.active_call.ok_or_else(|| anyhow!("no such project"))?; + anyhow::ensure!( + active_call.connection_id == Some(requester_connection_id), + "no such project" + ); + let project = self .projects .get_mut(&project_id) .ok_or_else(|| anyhow!("no such project"))?; + anyhow::ensure!(project.room_id == active_call.room_id, "no such project"); + connection.projects.insert(project_id); let mut replica_id = 1; while project.active_replica_ids.contains(&replica_id) { diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 901e3b7d85857afb1bf7a467daff39289adb73b4..c0ed2e5ad4828ca6caec0e8082d80635d109faa4 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -4435,8 +4435,14 @@ impl Project { _: Arc, mut cx: AsyncAppContext, ) -> Result<()> { - this.update(&mut cx, |this, cx| this.disconnected_from_host(cx)); - Ok(()) + this.update(&mut cx, |this, cx| { + if this.is_local() { + this.unshare(cx)?; + } else { + this.disconnected_from_host(cx); + } + Ok(()) + }) } async fn handle_add_collaborator( From bec6b41448435ab458536d975421d6a918cbdc67 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 3 Oct 2022 15:50:47 +0200 Subject: [PATCH 037/112] Fix randomized integration test failure --- crates/collab/src/integration_tests.rs | 30 ++++++++++++++------------ 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/crates/collab/src/integration_tests.rs b/crates/collab/src/integration_tests.rs index 3bb63c0229d0b0a48d8ad00cc6d8a5ac302a14d4..0a789775be55f53e1952a32d45c5e6c887c3f3fb 100644 --- a/crates/collab/src/integration_tests.rs +++ b/crates/collab/src/integration_tests.rs @@ -4675,20 +4675,20 @@ async fn test_random_collaboration( deterministic.finish_waiting(); deterministic.run_until_parked(); - let (host, host_project, mut host_cx, host_err) = clients.remove(0); + let (host, host_room, host_project, mut host_cx, host_err) = clients.remove(0); if let Some(host_err) = host_err { log::error!("host error - {:?}", host_err); } host_project.read_with(&host_cx, |project, _| assert!(!project.is_shared())); - for (guest, guest_project, mut guest_cx, guest_err) in clients { + for (guest, guest_room, guest_project, mut guest_cx, guest_err) in clients { if let Some(guest_err) = guest_err { log::error!("{} error - {:?}", guest.username, guest_err); } guest_project.read_with(&guest_cx, |project, _| assert!(project.is_read_only())); - guest_cx.update(|_| drop((guest, guest_project))); + guest_cx.update(|_| drop((guest, guest_room, guest_project))); } - host_cx.update(|_| drop((host, host_project))); + host_cx.update(|_| drop((host, host_room, host_project))); return; } @@ -4773,7 +4773,7 @@ async fn test_random_collaboration( deterministic.advance_clock(RECEIVE_TIMEOUT); deterministic.start_waiting(); log::info!("Waiting for guest {} to exit...", removed_guest_id); - let (guest, guest_project, mut guest_cx, guest_err) = guest.await; + let (guest, guest_room, guest_project, mut guest_cx, guest_err) = guest.await; deterministic.finish_waiting(); server.allow_connections(); @@ -4801,7 +4801,7 @@ async fn test_random_collaboration( log::info!("{} removed", guest.username); available_guests.push(guest.username.clone()); - guest_cx.update(|_| drop((guest, guest_project))); + guest_cx.update(|_| drop((guest, guest_room, guest_project))); operations += 1; } @@ -4828,7 +4828,7 @@ async fn test_random_collaboration( deterministic.finish_waiting(); deterministic.run_until_parked(); - let (host_client, host_project, mut host_cx, host_err) = clients.remove(0); + let (host_client, host_room, host_project, mut host_cx, host_err) = clients.remove(0); if let Some(host_err) = host_err { panic!("host error - {:?}", host_err); } @@ -4844,7 +4844,7 @@ async fn test_random_collaboration( host_project.read_with(&host_cx, |project, cx| project.check_invariants(cx)); - for (guest_client, guest_project, mut guest_cx, guest_err) in clients.into_iter() { + for (guest_client, guest_room, guest_project, mut guest_cx, guest_err) in clients.into_iter() { if let Some(guest_err) = guest_err { panic!("{} error - {:?}", guest_client.username, guest_err); } @@ -4916,10 +4916,10 @@ async fn test_random_collaboration( ); } - guest_cx.update(|_| drop((guest_project, guest_client))); + guest_cx.update(|_| drop((guest_room, guest_project, guest_client))); } - host_cx.update(|_| drop((host_client, host_project))); + host_cx.update(|_| drop((host_client, host_room, host_project))); } struct TestServer { @@ -5315,13 +5315,14 @@ impl TestClient { async fn simulate_host( mut self, - _room: ModelHandle, + room: ModelHandle, project: ModelHandle, op_start_signal: futures::channel::mpsc::UnboundedReceiver<()>, rng: Arc>, mut cx: TestAppContext, ) -> ( Self, + ModelHandle, ModelHandle, TestAppContext, Option, @@ -5447,19 +5448,20 @@ impl TestClient { let result = simulate_host_internal(&mut self, project.clone(), op_start_signal, rng, &mut cx).await; log::info!("Host done"); - (self, project, cx, result.err()) + (self, room, project, cx, result.err()) } pub async fn simulate_guest( mut self, guest_username: String, - _room: ModelHandle, + room: ModelHandle, project: ModelHandle, op_start_signal: futures::channel::mpsc::UnboundedReceiver<()>, rng: Arc>, mut cx: TestAppContext, ) -> ( Self, + ModelHandle, ModelHandle, TestAppContext, Option, @@ -5778,7 +5780,7 @@ impl TestClient { .await; log::info!("{}: done", guest_username); - (self, project, cx, result.err()) + (self, room, project, cx, result.err()) } } From da6106db8e6abae46e172181e68f4713b8b675ae Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 3 Oct 2022 15:54:20 +0200 Subject: [PATCH 038/112] Prevent calls from users who aren't contacts --- crates/collab/src/rpc.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 31f99759cd12a8bdb816f7b54cdc7838ef8f71b4..64d81b51d7ffea4f20c96ddf65c7d0da33f7eab8 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -654,7 +654,20 @@ impl Server { request: TypedEnvelope, response: Response, ) -> Result<()> { + let caller_user_id = self + .store() + .await + .user_id_for_connection(request.sender_id)?; let recipient_user_id = UserId::from_proto(request.payload.recipient_user_id); + if !self + .app_state + .db + .has_contact(caller_user_id, recipient_user_id) + .await? + { + return Err(anyhow!("cannot call a user who isn't a contact"))?; + } + let room_id = request.payload.room_id; let mut calls = { let mut store = self.store().await; From ad323d6e3b079c8a4438083d64f140584216fd87 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 3 Oct 2022 16:09:49 +0200 Subject: [PATCH 039/112] Automatically fetch remote participant users in `Room` --- crates/call/src/participant.rs | 9 ++-- crates/call/src/room.rs | 69 ++++++++++++++++---------- crates/collab/src/integration_tests.rs | 65 +++++++++--------------- 3 files changed, 72 insertions(+), 71 deletions(-) diff --git a/crates/call/src/participant.rs b/crates/call/src/participant.rs index c6257b28957d64e8e4c00e7cb9dac96be7e1ab13..cf7c965816c3f1b455f2f993642f71aa394a7087 100644 --- a/crates/call/src/participant.rs +++ b/crates/call/src/participant.rs @@ -1,7 +1,6 @@ use anyhow::{anyhow, Result}; -use client::proto; -use gpui::ModelHandle; -use project::Project; +use client::{proto, User}; +use std::sync::Arc; pub enum ParticipantLocation { Project { project_id: u64 }, @@ -21,7 +20,7 @@ impl ParticipantLocation { } pub struct RemoteParticipant { - pub user_id: u64, - pub projects: Vec>, + pub user: Arc, + pub project_ids: Vec, pub location: ParticipantLocation, } diff --git a/crates/call/src/room.rs b/crates/call/src/room.rs index f371384a52ea43f02cd38194ffba7dbb51507e2e..d6a0012e3060ac4a0cc13717bd36e6319d31efc8 100644 --- a/crates/call/src/room.rs +++ b/crates/call/src/room.rs @@ -19,7 +19,7 @@ pub struct Room { client: Arc, user_store: ModelHandle, _subscriptions: Vec, - _load_pending_users: Option>, + _pending_room_update: Option>, } impl Entity for Room { @@ -58,7 +58,7 @@ impl Room { remote_participants: Default::default(), pending_users: Default::default(), _subscriptions: vec![client.add_message_handler(cx.handle(), Self::handle_room_updated)], - _load_pending_users: None, + _pending_room_update: None, client, user_store, } @@ -133,32 +133,51 @@ impl Room { Ok(()) } - fn apply_room_update(&mut self, room: proto::Room, cx: &mut ModelContext) -> Result<()> { - // TODO: compute diff instead of clearing participants - self.remote_participants.clear(); - for participant in room.participants { - if Some(participant.user_id) != self.client.user_id() { - self.remote_participants.insert( - PeerId(participant.peer_id), - RemoteParticipant { - user_id: participant.user_id, - projects: Default::default(), // TODO: populate projects - location: ParticipantLocation::from_proto(participant.location)?, - }, - ); - } - } - - let pending_users = self.user_store.update(cx, move |user_store, cx| { - user_store.get_users(room.pending_user_ids, cx) + fn apply_room_update( + &mut self, + mut room: proto::Room, + cx: &mut ModelContext, + ) -> Result<()> { + // Filter ourselves out from the room's participants. + room.participants + .retain(|participant| Some(participant.user_id) != self.client.user_id()); + + let participant_user_ids = room + .participants + .iter() + .map(|p| p.user_id) + .collect::>(); + let (participants, pending_users) = self.user_store.update(cx, move |user_store, cx| { + ( + user_store.get_users(participant_user_ids, cx), + user_store.get_users(room.pending_user_ids, cx), + ) }); - self._load_pending_users = Some(cx.spawn(|this, mut cx| async move { - if let Some(pending_users) = pending_users.await.log_err() { - this.update(&mut cx, |this, cx| { + self._pending_room_update = Some(cx.spawn(|this, mut cx| async move { + let (participants, pending_users) = futures::join!(participants, pending_users); + + this.update(&mut cx, |this, cx| { + if let Some(participants) = participants.log_err() { + // TODO: compute diff instead of clearing participants + this.remote_participants.clear(); + for (participant, user) in room.participants.into_iter().zip(participants) { + this.remote_participants.insert( + PeerId(participant.peer_id), + RemoteParticipant { + user, + project_ids: participant.project_ids, + location: ParticipantLocation::from_proto(participant.location) + .unwrap_or(ParticipantLocation::External), + }, + ); + } + } + + if let Some(pending_users) = pending_users.log_err() { this.pending_users = pending_users; cx.notify(); - }); - } + } + }); })); cx.notify(); diff --git a/crates/collab/src/integration_tests.rs b/crates/collab/src/integration_tests.rs index 0a789775be55f53e1952a32d45c5e6c887c3f3fb..0bc848f1072dc7dd54c0a0360433be3cf27afd47 100644 --- a/crates/collab/src/integration_tests.rs +++ b/crates/collab/src/integration_tests.rs @@ -82,7 +82,7 @@ async fn test_basic_calls( .await .unwrap(); assert_eq!( - room_participants(&room_a, &client_a, cx_a).await, + room_participants(&room_a, cx_a), RoomParticipants { remote: Default::default(), pending: Default::default() @@ -100,7 +100,7 @@ async fn test_basic_calls( deterministic.run_until_parked(); assert_eq!( - room_participants(&room_a, &client_a, cx_a).await, + room_participants(&room_a, cx_a), RoomParticipants { remote: Default::default(), pending: vec!["user_b".to_string()] @@ -127,14 +127,14 @@ async fn test_basic_calls( deterministic.run_until_parked(); assert_eq!( - room_participants(&room_a, &client_a, cx_a).await, + room_participants(&room_a, cx_a), RoomParticipants { remote: vec!["user_b".to_string()], pending: Default::default() } ); assert_eq!( - room_participants(&room_b, &client_b, cx_b).await, + room_participants(&room_b, cx_b), RoomParticipants { remote: vec!["user_a".to_string()], pending: Default::default() @@ -152,14 +152,14 @@ async fn test_basic_calls( deterministic.run_until_parked(); assert_eq!( - room_participants(&room_a, &client_a, cx_a).await, + room_participants(&room_a, cx_a), RoomParticipants { remote: vec!["user_b".to_string()], pending: vec!["user_c".to_string()] } ); assert_eq!( - room_participants(&room_b, &client_b, cx_b).await, + room_participants(&room_b, cx_b), RoomParticipants { remote: vec!["user_a".to_string()], pending: vec!["user_c".to_string()] @@ -176,14 +176,14 @@ async fn test_basic_calls( deterministic.run_until_parked(); assert_eq!( - room_participants(&room_a, &client_a, cx_a).await, + room_participants(&room_a, cx_a), RoomParticipants { remote: vec!["user_b".to_string()], pending: Default::default() } ); assert_eq!( - room_participants(&room_b, &client_b, cx_b).await, + room_participants(&room_b, cx_b), RoomParticipants { remote: vec!["user_a".to_string()], pending: Default::default() @@ -194,14 +194,14 @@ async fn test_basic_calls( room_a.update(cx_a, |room, cx| room.leave(cx)).unwrap(); deterministic.run_until_parked(); assert_eq!( - room_participants(&room_a, &client_a, cx_a).await, + room_participants(&room_a, cx_a), RoomParticipants { remote: Default::default(), pending: Default::default() } ); assert_eq!( - room_participants(&room_b, &client_b, cx_b).await, + room_participants(&room_b, cx_b), RoomParticipants { remote: Default::default(), pending: Default::default() @@ -245,14 +245,14 @@ async fn test_leaving_room_on_disconnection( .unwrap(); deterministic.run_until_parked(); assert_eq!( - room_participants(&room_a, &client_a, cx_a).await, + room_participants(&room_a, cx_a), RoomParticipants { remote: vec!["user_b".to_string()], pending: Default::default() } ); assert_eq!( - room_participants(&room_b, &client_b, cx_b).await, + room_participants(&room_b, cx_b), RoomParticipants { remote: vec!["user_a".to_string()], pending: Default::default() @@ -262,14 +262,14 @@ async fn test_leaving_room_on_disconnection( server.disconnect_client(client_a.current_user_id(cx_a)); cx_a.foreground().advance_clock(rpc::RECEIVE_TIMEOUT); assert_eq!( - room_participants(&room_a, &client_a, cx_a).await, + room_participants(&room_a, cx_a), RoomParticipants { remote: Default::default(), pending: Default::default() } ); assert_eq!( - room_participants(&room_b, &client_b, cx_b).await, + room_participants(&room_b, cx_b), RoomParticipants { remote: Default::default(), pending: Default::default() @@ -5822,34 +5822,17 @@ struct RoomParticipants { pending: Vec, } -async fn room_participants( - room: &ModelHandle, - client: &TestClient, - cx: &mut TestAppContext, -) -> RoomParticipants { - let remote_users = room.update(cx, |room, cx| { - room.remote_participants() - .values() - .map(|participant| { - client - .user_store - .update(cx, |users, cx| users.get_user(participant.user_id, cx)) - }) - .collect::>() - }); - let remote_users = futures::future::try_join_all(remote_users).await.unwrap(); - let pending_users = room.read_with(cx, |room, _| { - room.pending_users().iter().cloned().collect::>() - }); - - RoomParticipants { - remote: remote_users - .into_iter() - .map(|user| user.github_login.clone()) +fn room_participants(room: &ModelHandle, cx: &mut TestAppContext) -> RoomParticipants { + room.read_with(cx, |room, _| RoomParticipants { + remote: room + .remote_participants() + .iter() + .map(|(_, participant)| participant.user.github_login.clone()) .collect(), - pending: pending_users - .into_iter() + pending: room + .pending_users() + .iter() .map(|user| user.github_login.clone()) .collect(), - } + }) } From 1e45198b9fbf98739f346bcf29528e55677a430d Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 3 Oct 2022 17:12:07 +0200 Subject: [PATCH 040/112] Emit event on `Room` when a user shares a new project --- crates/call/src/call.rs | 2 +- crates/call/src/room.rs | 40 ++++++++++-- crates/client/src/user.rs | 2 +- crates/collab/src/integration_tests.rs | 87 +++++++++++++++++++++++++- 4 files changed, 120 insertions(+), 11 deletions(-) diff --git a/crates/call/src/call.rs b/crates/call/src/call.rs index 0fcf5d76986c2684750f028ad5bb21599c913a2f..01adf9e39d5c6023b0aef99b80bd66157f0884b4 100644 --- a/crates/call/src/call.rs +++ b/crates/call/src/call.rs @@ -1,5 +1,5 @@ mod participant; -mod room; +pub mod room; use anyhow::{anyhow, Result}; use client::{incoming_call::IncomingCall, Client, UserStore}; diff --git a/crates/call/src/room.rs b/crates/call/src/room.rs index d6a0012e3060ac4a0cc13717bd36e6319d31efc8..0e9ce95c2168d058b900c778e6a5313c0b87e7f1 100644 --- a/crates/call/src/room.rs +++ b/crates/call/src/room.rs @@ -1,14 +1,15 @@ use crate::participant::{ParticipantLocation, RemoteParticipant}; use anyhow::{anyhow, Result}; use client::{incoming_call::IncomingCall, proto, Client, PeerId, TypedEnvelope, User, UserStore}; -use collections::HashMap; +use collections::{HashMap, HashSet}; use futures::StreamExt; use gpui::{AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Task}; use std::sync::Arc; use util::ResultExt; +#[derive(Clone, Debug, PartialEq, Eq)] pub enum Event { - PeerChangedActiveProject, + RemoteProjectShared { owner: Arc, project_id: u64 }, } pub struct Room { @@ -158,19 +159,46 @@ impl Room { this.update(&mut cx, |this, cx| { if let Some(participants) = participants.log_err() { - // TODO: compute diff instead of clearing participants - this.remote_participants.clear(); + let mut seen_participants = HashSet::default(); + for (participant, user) in room.participants.into_iter().zip(participants) { + let peer_id = PeerId(participant.peer_id); + seen_participants.insert(peer_id); + + let existing_project_ids = this + .remote_participants + .get(&peer_id) + .map(|existing| existing.project_ids.clone()) + .unwrap_or_default(); + for project_id in &participant.project_ids { + if !existing_project_ids.contains(project_id) { + cx.emit(Event::RemoteProjectShared { + owner: user.clone(), + project_id: *project_id, + }); + } + } + this.remote_participants.insert( - PeerId(participant.peer_id), + peer_id, RemoteParticipant { - user, + user: user.clone(), project_ids: participant.project_ids, location: ParticipantLocation::from_proto(participant.location) .unwrap_or(ParticipantLocation::External), }, ); } + + for participant_peer_id in + this.remote_participants.keys().copied().collect::>() + { + if !seen_participants.contains(&participant_peer_id) { + this.remote_participants.remove(&participant_peer_id); + } + } + + cx.notify(); } if let Some(pending_users) = pending_users.log_err() { diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index 9f86020044b6ce43740b98054309b26a1b9052e8..0529045c77c4ece62a0cd0fc9729ba86b957c366 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -9,7 +9,7 @@ use rpc::proto::{RequestMessage, UsersResponse}; use std::sync::{Arc, Weak}; use util::TryFutureExt as _; -#[derive(Debug)] +#[derive(Default, Debug)] pub struct User { pub id: u64, pub github_login: String, diff --git a/crates/collab/src/integration_tests.rs b/crates/collab/src/integration_tests.rs index 0bc848f1072dc7dd54c0a0360433be3cf27afd47..5754513e1eafc40493888a7ea433079c3d5e2e51 100644 --- a/crates/collab/src/integration_tests.rs +++ b/crates/collab/src/integration_tests.rs @@ -5,10 +5,10 @@ use crate::{ }; use ::rpc::Peer; use anyhow::anyhow; -use call::Room; +use call::{room, Room}; use client::{ self, test::FakeHttpClient, Channel, ChannelDetails, ChannelList, Client, Connection, - Credentials, EstablishConnectionError, UserStore, RECEIVE_TIMEOUT, + Credentials, EstablishConnectionError, User, UserStore, RECEIVE_TIMEOUT, }; use collections::{BTreeMap, HashMap, HashSet}; use editor::{ @@ -40,7 +40,8 @@ use serde_json::json; use settings::{Formatter, Settings}; use sqlx::types::time::OffsetDateTime; use std::{ - env, + cell::RefCell, + env, mem, ops::Deref, path::{Path, PathBuf}, rc::Rc, @@ -556,6 +557,86 @@ async fn test_host_disconnect( }); } +#[gpui::test(iterations = 10)] +async fn test_room_events( + deterministic: Arc, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, +) { + deterministic.forbid_parking(); + let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + client_a.fs.insert_tree("/a", json!({})).await; + client_b.fs.insert_tree("/b", json!({})).await; + + let (project_a, _) = client_a.build_local_project("/a", cx_a).await; + let (project_b, _) = client_b.build_local_project("/b", cx_b).await; + + let (room_id, mut rooms) = server + .create_rooms(&mut [(&client_a, cx_a), (&client_b, cx_b)]) + .await; + + let room_a = rooms.remove(0); + let room_a_events = room_events(&room_a, cx_a); + + let room_b = rooms.remove(0); + let room_b_events = room_events(&room_b, cx_b); + + let project_a_id = project_a + .update(cx_a, |project, cx| project.share(room_id, cx)) + .await + .unwrap(); + deterministic.run_until_parked(); + assert_eq!(mem::take(&mut *room_a_events.borrow_mut()), vec![]); + assert_eq!( + mem::take(&mut *room_b_events.borrow_mut()), + vec![room::Event::RemoteProjectShared { + owner: Arc::new(User { + id: client_a.user_id().unwrap(), + github_login: "user_a".to_string(), + avatar: None, + }), + project_id: project_a_id, + }] + ); + + let project_b_id = project_b + .update(cx_b, |project, cx| project.share(room_id, cx)) + .await + .unwrap(); + deterministic.run_until_parked(); + assert_eq!( + mem::take(&mut *room_a_events.borrow_mut()), + vec![room::Event::RemoteProjectShared { + owner: Arc::new(User { + id: client_b.user_id().unwrap(), + github_login: "user_b".to_string(), + avatar: None, + }), + project_id: project_b_id, + }] + ); + assert_eq!(mem::take(&mut *room_b_events.borrow_mut()), vec![]); + + fn room_events( + room: &ModelHandle, + cx: &mut TestAppContext, + ) -> Rc>> { + let events = Rc::new(RefCell::new(Vec::new())); + cx.update({ + let events = events.clone(); + |cx| { + cx.subscribe(room, move |_, event, _| { + events.borrow_mut().push(event.clone()) + }) + .detach() + } + }); + events + } +} + #[gpui::test(iterations = 10)] async fn test_propagate_saves_and_fs_changes( cx_a: &mut TestAppContext, From 456dde200c7083c6bbd6dc6ba1d32c0aa1e86eb7 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 4 Oct 2022 11:46:01 +0200 Subject: [PATCH 041/112] Implement `Room::set_location` --- crates/call/src/call.rs | 1 + crates/call/src/participant.rs | 1 + crates/call/src/room.rs | 37 ++++++ crates/collab/src/integration_tests.rs | 162 ++++++++++++++++++++++++- crates/collab/src/rpc.rs | 18 +++ crates/collab/src/rpc/store.rs | 31 +++++ crates/rpc/proto/zed.proto | 6 + crates/rpc/src/proto.rs | 2 + 8 files changed, 256 insertions(+), 2 deletions(-) diff --git a/crates/call/src/call.rs b/crates/call/src/call.rs index 01adf9e39d5c6023b0aef99b80bd66157f0884b4..d800c3ac0657fbbe716ef1f1799b4e75b9d7adbd 100644 --- a/crates/call/src/call.rs +++ b/crates/call/src/call.rs @@ -4,6 +4,7 @@ pub mod room; use anyhow::{anyhow, Result}; use client::{incoming_call::IncomingCall, Client, UserStore}; use gpui::{Entity, ModelContext, ModelHandle, MutableAppContext, Task}; +pub use participant::ParticipantLocation; pub use room::Room; use std::sync::Arc; diff --git a/crates/call/src/participant.rs b/crates/call/src/participant.rs index cf7c965816c3f1b455f2f993642f71aa394a7087..e0e96bc590bbd1e8b270874e2afa595dba1d5544 100644 --- a/crates/call/src/participant.rs +++ b/crates/call/src/participant.rs @@ -2,6 +2,7 @@ use anyhow::{anyhow, Result}; use client::{proto, User}; use std::sync::Arc; +#[derive(Copy, Clone, Debug, Eq, PartialEq)] pub enum ParticipantLocation { Project { project_id: u64 }, External, diff --git a/crates/call/src/room.rs b/crates/call/src/room.rs index 0e9ce95c2168d058b900c778e6a5313c0b87e7f1..4cac210f8b66afe60d37488289ead4dc4f276386 100644 --- a/crates/call/src/room.rs +++ b/crates/call/src/room.rs @@ -4,6 +4,7 @@ use client::{incoming_call::IncomingCall, proto, Client, PeerId, TypedEnvelope, use collections::{HashMap, HashSet}; use futures::StreamExt; use gpui::{AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Task}; +use project::Project; use std::sync::Arc; use util::ResultExt; @@ -233,6 +234,42 @@ impl Room { Ok(()) }) } + + pub fn set_location( + &mut self, + project: Option<&ModelHandle>, + cx: &mut ModelContext, + ) -> Task> { + if self.status.is_offline() { + return Task::ready(Err(anyhow!("room is offline"))); + } + + let client = self.client.clone(); + let room_id = self.id; + let location = if let Some(project) = project { + if let Some(project_id) = project.read(cx).remote_id() { + proto::participant_location::Variant::Project( + proto::participant_location::Project { id: project_id }, + ) + } else { + return Task::ready(Err(anyhow!("project is not shared"))); + } + } else { + proto::participant_location::Variant::External(proto::participant_location::External {}) + }; + + cx.foreground().spawn(async move { + client + .request(proto::UpdateParticipantLocation { + room_id, + location: Some(proto::ParticipantLocation { + variant: Some(location), + }), + }) + .await?; + Ok(()) + }) + } } #[derive(Copy, Clone, PartialEq, Eq)] diff --git a/crates/collab/src/integration_tests.rs b/crates/collab/src/integration_tests.rs index 5754513e1eafc40493888a7ea433079c3d5e2e51..40af0a731eb578a1aab6814074b590188ce4d279 100644 --- a/crates/collab/src/integration_tests.rs +++ b/crates/collab/src/integration_tests.rs @@ -5,7 +5,7 @@ use crate::{ }; use ::rpc::Peer; use anyhow::anyhow; -use call::{room, Room}; +use call::{room, ParticipantLocation, Room}; use client::{ self, test::FakeHttpClient, Channel, ChannelDetails, ChannelList, Client, Connection, Credentials, EstablishConnectionError, User, UserStore, RECEIVE_TIMEOUT, @@ -40,7 +40,7 @@ use serde_json::json; use settings::{Formatter, Settings}; use sqlx::types::time::OffsetDateTime; use std::{ - cell::RefCell, + cell::{Cell, RefCell}, env, mem, ops::Deref, path::{Path, PathBuf}, @@ -637,6 +637,164 @@ async fn test_room_events( } } +#[gpui::test(iterations = 10)] +async fn test_room_location( + deterministic: Arc, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, +) { + deterministic.forbid_parking(); + let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + client_a.fs.insert_tree("/a", json!({})).await; + client_b.fs.insert_tree("/b", json!({})).await; + + let (project_a, _) = client_a.build_local_project("/a", cx_a).await; + let (project_b, _) = client_b.build_local_project("/b", cx_b).await; + + let (room_id, mut rooms) = server + .create_rooms(&mut [(&client_a, cx_a), (&client_b, cx_b)]) + .await; + + let room_a = rooms.remove(0); + let room_a_notified = Rc::new(Cell::new(false)); + cx_a.update({ + let room_a_notified = room_a_notified.clone(); + |cx| { + cx.observe(&room_a, move |_, _| room_a_notified.set(true)) + .detach() + } + }); + + let room_b = rooms.remove(0); + let room_b_notified = Rc::new(Cell::new(false)); + cx_b.update({ + let room_b_notified = room_b_notified.clone(); + |cx| { + cx.observe(&room_b, move |_, _| room_b_notified.set(true)) + .detach() + } + }); + + let project_a_id = project_a + .update(cx_a, |project, cx| project.share(room_id, cx)) + .await + .unwrap(); + deterministic.run_until_parked(); + assert!(room_a_notified.take()); + assert_eq!( + participant_locations(&room_a, cx_a), + vec![("user_b".to_string(), ParticipantLocation::External)] + ); + assert!(room_b_notified.take()); + assert_eq!( + participant_locations(&room_b, cx_b), + vec![("user_a".to_string(), ParticipantLocation::External)] + ); + + let project_b_id = project_b + .update(cx_b, |project, cx| project.share(room_id, cx)) + .await + .unwrap(); + deterministic.run_until_parked(); + assert!(room_a_notified.take()); + assert_eq!( + participant_locations(&room_a, cx_a), + vec![("user_b".to_string(), ParticipantLocation::External)] + ); + assert!(room_b_notified.take()); + assert_eq!( + participant_locations(&room_b, cx_b), + vec![("user_a".to_string(), ParticipantLocation::External)] + ); + + room_a + .update(cx_a, |room, cx| room.set_location(Some(&project_a), cx)) + .await + .unwrap(); + deterministic.run_until_parked(); + assert!(room_a_notified.take()); + assert_eq!( + participant_locations(&room_a, cx_a), + vec![("user_b".to_string(), ParticipantLocation::External)] + ); + assert!(room_b_notified.take()); + assert_eq!( + participant_locations(&room_b, cx_b), + vec![( + "user_a".to_string(), + ParticipantLocation::Project { + project_id: project_a_id + } + )] + ); + + room_b + .update(cx_b, |room, cx| room.set_location(Some(&project_b), cx)) + .await + .unwrap(); + deterministic.run_until_parked(); + assert!(room_a_notified.take()); + assert_eq!( + participant_locations(&room_a, cx_a), + vec![( + "user_b".to_string(), + ParticipantLocation::Project { + project_id: project_b_id + } + )] + ); + assert!(room_b_notified.take()); + assert_eq!( + participant_locations(&room_b, cx_b), + vec![( + "user_a".to_string(), + ParticipantLocation::Project { + project_id: project_a_id + } + )] + ); + + room_b + .update(cx_b, |room, cx| room.set_location(None, cx)) + .await + .unwrap(); + deterministic.run_until_parked(); + assert!(room_a_notified.take()); + assert_eq!( + participant_locations(&room_a, cx_a), + vec![("user_b".to_string(), ParticipantLocation::External)] + ); + assert!(room_b_notified.take()); + assert_eq!( + participant_locations(&room_b, cx_b), + vec![( + "user_a".to_string(), + ParticipantLocation::Project { + project_id: project_a_id + } + )] + ); + + fn participant_locations( + room: &ModelHandle, + cx: &TestAppContext, + ) -> Vec<(String, ParticipantLocation)> { + room.read_with(cx, |room, _| { + room.remote_participants() + .values() + .map(|participant| { + ( + participant.user.github_login.to_string(), + participant.location, + ) + }) + .collect() + }) + } +} + #[gpui::test(iterations = 10)] async fn test_propagate_saves_and_fs_changes( cx_a: &mut TestAppContext, diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 64d81b51d7ffea4f20c96ddf65c7d0da33f7eab8..c000400568648692f68651f15e568a981d3113f4 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -151,6 +151,7 @@ impl Server { .add_message_handler(Server::leave_room) .add_request_handler(Server::call) .add_message_handler(Server::decline_call) + .add_request_handler(Server::update_participant_location) .add_request_handler(Server::share_project) .add_message_handler(Server::unshare_project) .add_request_handler(Server::join_project) @@ -719,6 +720,23 @@ impl Server { Ok(()) } + async fn update_participant_location( + self: Arc, + request: TypedEnvelope, + response: Response, + ) -> Result<()> { + let room_id = request.payload.room_id; + let location = request + .payload + .location + .ok_or_else(|| anyhow!("invalid location"))?; + let mut store = self.store().await; + let room = store.update_participant_location(room_id, location, request.sender_id)?; + self.room_updated(room); + response.send(proto::Ack {})?; + Ok(()) + } + fn room_updated(&self, room: &proto::Room) { for participant in &room.participants { self.peer diff --git a/crates/collab/src/rpc/store.rs b/crates/collab/src/rpc/store.rs index 9b241446b52eaf6f21265243741dcf2502cd5f32..c917309cd21da201e7f6cb06b23857e298cb65ed 100644 --- a/crates/collab/src/rpc/store.rs +++ b/crates/collab/src/rpc/store.rs @@ -585,6 +585,37 @@ impl Store { } } + pub fn update_participant_location( + &mut self, + room_id: RoomId, + location: proto::ParticipantLocation, + connection_id: ConnectionId, + ) -> Result<&proto::Room> { + let room = self + .rooms + .get_mut(&room_id) + .ok_or_else(|| anyhow!("no such room"))?; + if let Some(proto::participant_location::Variant::Project(project)) = + location.variant.as_ref() + { + anyhow::ensure!( + room.participants + .iter() + .any(|participant| participant.project_ids.contains(&project.id)), + "no such project" + ); + } + + let participant = room + .participants + .iter_mut() + .find(|participant| participant.peer_id == connection_id.0) + .ok_or_else(|| anyhow!("no such room"))?; + participant.location = Some(location); + + Ok(room) + } + pub fn share_project( &mut self, room_id: RoomId, diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index cff10278b425742ed9f03510d60336035f6e2d85..ffec915269deb5f5d91e93e20cc772e94238e3d0 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -20,6 +20,7 @@ message Envelope { IncomingCall incoming_call = 1000; CancelCall cancel_call = 1001; DeclineCall decline_call = 13; + UpdateParticipantLocation update_participant_location = 1003; RoomUpdated room_updated = 14; ShareProject share_project = 15; @@ -190,6 +191,11 @@ message CancelCall {} message DeclineCall {} +message UpdateParticipantLocation { + uint64 room_id = 1; + ParticipantLocation location = 2; +} + message RoomUpdated { Room room = 1; } diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index 822a50c3e4c61ce535c1e41360e3470af6477391..25e04e6645823207fafee135726a26fc8fb0e440 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -170,6 +170,7 @@ messages!( (UpdateFollowers, Foreground), (UpdateInviteInfo, Foreground), (UpdateLanguageServer, Foreground), + (UpdateParticipantLocation, Foreground), (UpdateProject, Foreground), (UpdateWorktree, Foreground), (UpdateWorktreeExtensions, Background), @@ -222,6 +223,7 @@ request_messages!( (ShareProject, ShareProjectResponse), (Test, Test), (UpdateBuffer, Ack), + (UpdateParticipantLocation, Ack), (UpdateWorktree, Ack), ); From de917c4678395330e01c5649117bc761f0d55d8d Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 4 Oct 2022 14:50:41 +0200 Subject: [PATCH 042/112] Use a different style for inactive participants --- crates/call/src/participant.rs | 1 + crates/collab_ui/src/collab_titlebar_item.rs | 92 ++++++++++++++------ crates/collab_ui/src/contacts_popover.rs | 19 ++-- crates/theme/src/theme.rs | 1 + styles/src/styleTree/workspace.ts | 4 + 5 files changed, 82 insertions(+), 35 deletions(-) diff --git a/crates/call/src/participant.rs b/crates/call/src/participant.rs index e0e96bc590bbd1e8b270874e2afa595dba1d5544..15aaf2f13b9972e0043a0e2b199ee038fa0b3b54 100644 --- a/crates/call/src/participant.rs +++ b/crates/call/src/participant.rs @@ -20,6 +20,7 @@ impl ParticipantLocation { } } +#[derive(Clone)] pub struct RemoteParticipant { pub user: Arc, pub project_ids: Vec, diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index 770b9f29e653c61f3ee646f0519ae8d76cab1a77..4bbf322205297ade3acafe5eded9ec9bc1cda0fe 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -1,4 +1,5 @@ use crate::contacts_popover; +use call::{ActiveCall, ParticipantLocation}; use client::{Authenticate, PeerId}; use clock::ReplicaId; use contacts_popover::ContactsPopover; @@ -25,6 +26,7 @@ pub fn init(cx: &mut MutableAppContext) { pub struct CollabTitlebarItem { workspace: WeakViewHandle, contacts_popover: Option>, + room_subscription: Option, _subscriptions: Vec, } @@ -56,12 +58,27 @@ impl View for CollabTitlebarItem { impl CollabTitlebarItem { pub fn new(workspace: &ViewHandle, cx: &mut ViewContext) -> Self { - let observe_workspace = cx.observe(workspace, |_, _, cx| cx.notify()); - Self { + let active_call = ActiveCall::global(cx); + let mut subscriptions = Vec::new(); + subscriptions.push(cx.observe(workspace, |_, _, cx| cx.notify())); + subscriptions.push(cx.observe(&active_call, |this, _, cx| this.active_call_changed(cx))); + let mut this = Self { workspace: workspace.downgrade(), contacts_popover: None, - _subscriptions: vec![observe_workspace], + room_subscription: None, + _subscriptions: subscriptions, + }; + this.active_call_changed(cx); + this + } + + fn active_call_changed(&mut self, cx: &mut ViewContext) { + if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() { + self.room_subscription = Some(cx.observe(&room, |_, _, cx| cx.notify())); + } else { + self.room_subscription = None; } + cx.notify(); } fn toggle_contacts_popover(&mut self, _: &ToggleContactsPopover, cx: &mut ViewContext) { @@ -151,28 +168,40 @@ impl CollabTitlebarItem { theme: &Theme, cx: &mut RenderContext, ) -> Vec { - let mut collaborators = workspace - .read(cx) - .project() - .read(cx) - .collaborators() - .values() - .cloned() - .collect::>(); - collaborators.sort_unstable_by_key(|collaborator| collaborator.replica_id); - collaborators - .into_iter() - .filter_map(|collaborator| { - Some(self.render_avatar( - collaborator.user.avatar.clone()?, - collaborator.replica_id, - Some((collaborator.peer_id, &collaborator.user.github_login)), - workspace, - theme, - cx, - )) - }) - .collect() + let active_call = ActiveCall::global(cx); + if let Some(room) = active_call.read(cx).room().cloned() { + let project = workspace.read(cx).project().read(cx); + let project_id = project.remote_id(); + let mut collaborators = project + .collaborators() + .values() + .cloned() + .collect::>(); + collaborators.sort_by_key(|collaborator| collaborator.replica_id); + collaborators + .into_iter() + .filter_map(|collaborator| { + let participant = room + .read(cx) + .remote_participants() + .get(&collaborator.peer_id)?; + let is_active = project_id.map_or(false, |project_id| { + participant.location == ParticipantLocation::Project { project_id } + }); + Some(self.render_avatar( + collaborator.user.avatar.clone()?, + collaborator.replica_id, + Some((collaborator.peer_id, &collaborator.user.github_login)), + is_active, + workspace, + theme, + cx, + )) + }) + .collect() + } else { + Default::default() + } } fn render_current_user( @@ -185,7 +214,7 @@ impl CollabTitlebarItem { let replica_id = workspace.read(cx).project().read(cx).replica_id(); let status = *workspace.read(cx).client().status().borrow(); if let Some(avatar) = user.and_then(|user| user.avatar.clone()) { - Some(self.render_avatar(avatar, replica_id, None, workspace, theme, cx)) + Some(self.render_avatar(avatar, replica_id, None, true, workspace, theme, cx)) } else if matches!(status, client::Status::UpgradeRequired) { None } else { @@ -214,6 +243,7 @@ impl CollabTitlebarItem { avatar: Arc, replica_id: ReplicaId, peer: Option<(PeerId, &str)>, + is_active: bool, workspace: &ViewHandle, theme: &Theme, cx: &mut RenderContext, @@ -222,10 +252,18 @@ impl CollabTitlebarItem { let is_followed = peer.map_or(false, |(peer_id, _)| { workspace.read(cx).is_following(peer_id) }); - let mut avatar_style = theme.workspace.titlebar.avatar; + let mut avatar_style; + + if is_active { + avatar_style = theme.workspace.titlebar.avatar; + } else { + avatar_style = theme.workspace.titlebar.inactive_avatar; + } + if is_followed { avatar_style.border = Border::all(1.0, replica_color); } + let content = Stack::new() .with_child( Image::new(avatar) diff --git a/crates/collab_ui/src/contacts_popover.rs b/crates/collab_ui/src/contacts_popover.rs index aff159127f95d2e0d40c1657348e8ea99b8ae52a..7d0473bfc509f7a08eab11ae19fdd064b193c36f 100644 --- a/crates/collab_ui/src/contacts_popover.rs +++ b/crates/collab_ui/src/contacts_popover.rs @@ -159,14 +159,7 @@ impl ContactsPopover { let active_call = ActiveCall::global(cx); let mut subscriptions = Vec::new(); subscriptions.push(cx.observe(&user_store, |this, _, cx| this.update_entries(cx))); - subscriptions.push(cx.observe(&active_call, |this, active_call, cx| { - if let Some(room) = active_call.read(cx).room().cloned() { - this.room_subscription = Some(cx.observe(&room, |_, _, cx| cx.notify())); - } else { - this.room_subscription = None; - } - cx.notify(); - })); + subscriptions.push(cx.observe(&active_call, |this, _, cx| this.active_call_changed(cx))); let mut this = Self { room_subscription: None, @@ -180,9 +173,19 @@ impl ContactsPopover { user_store, }; this.update_entries(cx); + this.active_call_changed(cx); this } + fn active_call_changed(&mut self, cx: &mut ViewContext) { + if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() { + self.room_subscription = Some(cx.observe(&room, |_, _, cx| cx.notify())); + } else { + self.room_subscription = None; + } + cx.notify(); + } + fn clear_filter(&mut self, _: &Cancel, cx: &mut ViewContext) { let did_clear = self.filter_editor.update(cx, |editor, cx| { if editor.buffer().read(cx).len(cx) > 0 { diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 28c8eb30917ac581b5d6e2b82c4f723959d29a63..7e78f74c59e99e05dfa5fdee63f0e2e0be30d89d 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -71,6 +71,7 @@ pub struct Titlebar { pub avatar_ribbon: AvatarRibbon, pub offline_icon: OfflineIcon, pub avatar: ImageStyle, + pub inactive_avatar: ImageStyle, pub sign_in_prompt: Interactive, pub outdated_warning: ContainedText, pub toggle_contacts_button: Interactive, diff --git a/styles/src/styleTree/workspace.ts b/styles/src/styleTree/workspace.ts index 8bd1e3800fe5ff50c02b46c65c437242ef412fcd..4889ee3f10ee1fcd3504b162533fba5dc5d7a9df 100644 --- a/styles/src/styleTree/workspace.ts +++ b/styles/src/styleTree/workspace.ts @@ -69,6 +69,10 @@ export default function workspace(theme: Theme) { width: 1, }, }, + inactiveAvatar: { + cornerRadius: 10, + opacity: 0.65, + }, avatarRibbon: { height: 3, width: 12, From 57930cb88ad6a44f656249b199594ca320306b05 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 4 Oct 2022 15:56:20 +0200 Subject: [PATCH 043/112] Show `Share` button for unshared projects when inside of a room --- crates/collab_ui/src/collab_titlebar_item.rs | 146 ++++++++++++------- crates/collab_ui/src/contacts_popover.rs | 65 +++++---- crates/theme/src/theme.rs | 1 + styles/src/styleTree/workspace.ts | 42 +++--- 4 files changed, 154 insertions(+), 100 deletions(-) diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index 4bbf322205297ade3acafe5eded9ec9bc1cda0fe..8b467e6cb43cb2113bb11ac398091c278bac9c28 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -17,10 +17,14 @@ use std::{ops::Range, sync::Arc}; use theme::Theme; use workspace::{FollowNextCollaborator, ToggleFollow, Workspace}; -actions!(contacts_titlebar_item, [ToggleContactsPopover]); +actions!( + contacts_titlebar_item, + [ToggleContactsPopover, ShareProject] +); pub fn init(cx: &mut MutableAppContext) { cx.add_action(CollabTitlebarItem::toggle_contacts_popover); + cx.add_action(CollabTitlebarItem::share_project); } pub struct CollabTitlebarItem { @@ -47,12 +51,20 @@ impl View for CollabTitlebarItem { }; let theme = cx.global::().theme.clone(); - Flex::row() - .with_children(self.render_toggle_contacts_button(&workspace, &theme, cx)) - .with_children(self.render_collaborators(&workspace, &theme, cx)) - .with_children(self.render_current_user(&workspace, &theme, cx)) - .with_children(self.render_connection_status(&workspace, cx)) - .boxed() + let project = workspace.read(cx).project().read(cx); + + let mut container = Flex::row(); + if workspace.read(cx).client().status().borrow().is_connected() { + if project.is_shared() || ActiveCall::global(cx).read(cx).room().is_none() { + container.add_child(self.render_toggle_contacts_button(&theme, cx)); + } else { + container.add_child(self.render_share_button(&theme, cx)); + } + } + container.add_children(self.render_collaborators(&workspace, &theme, cx)); + container.add_children(self.render_current_user(&workspace, &theme, cx)); + container.add_children(self.render_connection_status(&workspace, cx)); + container.boxed() } } @@ -81,6 +93,18 @@ impl CollabTitlebarItem { cx.notify(); } + fn share_project(&mut self, _: &ShareProject, cx: &mut ViewContext) { + if let Some(workspace) = self.workspace.upgrade(cx) { + if let Some(room) = ActiveCall::global(cx).read(cx).room() { + let room_id = room.read(cx).id(); + let project = workspace.read(cx).project().clone(); + project + .update(cx, |project, cx| project.share(room_id, cx)) + .detach_and_log_err(cx); + } + } + } + fn toggle_contacts_popover(&mut self, _: &ToggleContactsPopover, cx: &mut ViewContext) { match self.contacts_popover.take() { Some(_) => {} @@ -108,58 +132,72 @@ impl CollabTitlebarItem { fn render_toggle_contacts_button( &self, - workspace: &ViewHandle, theme: &Theme, cx: &mut RenderContext, - ) -> Option { - if !workspace.read(cx).client().status().borrow().is_connected() { - return None; - } - + ) -> ElementBox { let titlebar = &theme.workspace.titlebar; - - Some( - Stack::new() - .with_child( - MouseEventHandler::::new(0, cx, |state, _| { - let style = titlebar - .toggle_contacts_button - .style_for(state, self.contacts_popover.is_some()); - Svg::new("icons/plus_8.svg") - .with_color(style.color) - .constrained() - .with_width(style.icon_width) - .aligned() - .constrained() - .with_width(style.button_width) - .with_height(style.button_width) - .contained() - .with_style(style.container) - .boxed() - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, |_, cx| { - cx.dispatch_action(ToggleContactsPopover); - }) - .aligned() - .boxed(), - ) - .with_children(self.contacts_popover.as_ref().map(|popover| { - Overlay::new( - ChildView::new(popover) - .contained() - .with_margin_top(titlebar.height) - .with_margin_right( - -titlebar.toggle_contacts_button.default.button_width, - ) - .boxed(), - ) - .with_fit_mode(OverlayFitMode::SwitchAnchor) - .with_anchor_corner(AnchorCorner::BottomLeft) - .boxed() - })) + Stack::new() + .with_child( + MouseEventHandler::::new(0, cx, |state, _| { + let style = titlebar + .toggle_contacts_button + .style_for(state, self.contacts_popover.is_some()); + Svg::new("icons/plus_8.svg") + .with_color(style.color) + .constrained() + .with_width(style.icon_width) + .aligned() + .constrained() + .with_width(style.button_width) + .with_height(style.button_width) + .contained() + .with_style(style.container) + .boxed() + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, |_, cx| { + cx.dispatch_action(ToggleContactsPopover); + }) + .aligned() .boxed(), + ) + .with_children(self.contacts_popover.as_ref().map(|popover| { + Overlay::new( + ChildView::new(popover) + .contained() + .with_margin_top(titlebar.height) + .with_margin_right(-titlebar.toggle_contacts_button.default.button_width) + .boxed(), + ) + .with_fit_mode(OverlayFitMode::SwitchAnchor) + .with_anchor_corner(AnchorCorner::BottomLeft) + .boxed() + })) + .boxed() + } + + fn render_share_button(&self, theme: &Theme, cx: &mut RenderContext) -> ElementBox { + enum Share {} + + let titlebar = &theme.workspace.titlebar; + MouseEventHandler::::new(0, cx, |state, _| { + let style = titlebar.share_button.style_for(state, false); + Label::new("Share".into(), style.text.clone()) + .contained() + .with_style(style.container) + .boxed() + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, |_, cx| cx.dispatch_action(ShareProject)) + .with_tooltip::( + 0, + "Share project with call participants".into(), + None, + theme.tooltip.clone(), + cx, ) + .aligned() + .boxed() } fn render_collaborators( diff --git a/crates/collab_ui/src/contacts_popover.rs b/crates/collab_ui/src/contacts_popover.rs index 7d0473bfc509f7a08eab11ae19fdd064b193c36f..742096104130f7cbb5aed3ed0f1465d102b1986b 100644 --- a/crates/collab_ui/src/contacts_popover.rs +++ b/crates/collab_ui/src/contacts_popover.rs @@ -479,40 +479,49 @@ impl ContactsPopover { is_selected: bool, cx: &mut RenderContext, ) -> ElementBox { + let online = contact.online; let user_id = contact.user.id; - MouseEventHandler::::new(contact.user.id as usize, cx, |_, _| { - Flex::row() - .with_children(contact.user.avatar.clone().map(|avatar| { - Image::new(avatar) - .with_style(theme.contact_avatar) + let mut element = + MouseEventHandler::::new(contact.user.id as usize, cx, |_, _| { + Flex::row() + .with_children(contact.user.avatar.clone().map(|avatar| { + Image::new(avatar) + .with_style(theme.contact_avatar) + .aligned() + .left() + .boxed() + })) + .with_child( + Label::new( + contact.user.github_login.clone(), + theme.contact_username.text.clone(), + ) + .contained() + .with_style(theme.contact_username.container) .aligned() .left() - .boxed() - })) - .with_child( - Label::new( - contact.user.github_login.clone(), - theme.contact_username.text.clone(), + .flex(1., true) + .boxed(), ) + .constrained() + .with_height(theme.row_height) .contained() - .with_style(theme.contact_username.container) - .aligned() - .left() - .flex(1., true) - .boxed(), - ) - .constrained() - .with_height(theme.row_height) - .contained() - .with_style(*theme.contact_row.style_for(Default::default(), is_selected)) - .boxed() - }) - .on_click(MouseButton::Left, move |_, cx| { - cx.dispatch_action(Call { - recipient_user_id: user_id, + .with_style(*theme.contact_row.style_for(Default::default(), is_selected)) + .boxed() }) - }) - .boxed() + .on_click(MouseButton::Left, move |_, cx| { + if online { + cx.dispatch_action(Call { + recipient_user_id: user_id, + }); + } + }); + + if online { + element = element.with_cursor_style(CursorStyle::PointingHand); + } + + element.boxed() } fn render_contact_request( diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 7e78f74c59e99e05dfa5fdee63f0e2e0be30d89d..52b34462aa992b11431ccd944f51e14e0d634e65 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -74,6 +74,7 @@ pub struct Titlebar { pub inactive_avatar: ImageStyle, pub sign_in_prompt: Interactive, pub outdated_warning: ContainedText, + pub share_button: Interactive, pub toggle_contacts_button: Interactive, pub contacts_popover: AddParticipantPopover, } diff --git a/styles/src/styleTree/workspace.ts b/styles/src/styleTree/workspace.ts index 4889ee3f10ee1fcd3504b162533fba5dc5d7a9df..40ed44e8dbc6e0bf634ec84b3fba67fe630d0ba8 100644 --- a/styles/src/styleTree/workspace.ts +++ b/styles/src/styleTree/workspace.ts @@ -17,6 +17,26 @@ export function workspaceBackground(theme: Theme) { export default function workspace(theme: Theme) { const titlebarPadding = 6; + const titlebarButton = { + background: backgroundColor(theme, 100), + border: border(theme, "secondary"), + cornerRadius: 6, + margin: { + top: 1, + }, + padding: { + top: 1, + bottom: 1, + left: 7, + right: 7, + }, + ...text(theme, "sans", "secondary", { size: "xs" }), + hover: { + ...text(theme, "sans", "active", { size: "xs" }), + background: backgroundColor(theme, "on300", "hovered"), + border: border(theme, "primary"), + }, + }; return { background: backgroundColor(theme, 300), @@ -81,24 +101,7 @@ export default function workspace(theme: Theme) { }, border: border(theme, "primary", { bottom: true, overlay: true }), signInPrompt: { - background: backgroundColor(theme, 100), - border: border(theme, "secondary"), - cornerRadius: 6, - margin: { - top: 1, - }, - padding: { - top: 1, - bottom: 1, - left: 7, - right: 7, - }, - ...text(theme, "sans", "secondary", { size: "xs" }), - hover: { - ...text(theme, "sans", "active", { size: "xs" }), - background: backgroundColor(theme, "on300", "hovered"), - border: border(theme, "primary"), - }, + ...titlebarButton }, offlineIcon: { color: iconColor(theme, "secondary"), @@ -137,6 +140,9 @@ export default function workspace(theme: Theme) { color: iconColor(theme, "active"), }, }, + shareButton: { + ...titlebarButton + }, contactsPopover: { background: backgroundColor(theme, 300, "base"), cornerRadius: 6, From debedaf004be2c14af5714afd3a7ef8ef8dc8489 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 4 Oct 2022 16:55:41 +0200 Subject: [PATCH 044/112] Show notification when a new project is shared and allow joining it --- crates/collab_ui/src/collab_ui.rs | 11 +- .../src/project_shared_notification.rs | 174 ++++++++++++++++++ crates/theme/src/theme.rs | 9 + crates/zed/src/main.rs | 2 +- styles/src/styleTree/app.ts | 2 + .../styleTree/projectSharedNotification.ts | 22 +++ 6 files changed, 215 insertions(+), 5 deletions(-) create mode 100644 crates/collab_ui/src/project_shared_notification.rs create mode 100644 styles/src/styleTree/projectSharedNotification.ts diff --git a/crates/collab_ui/src/collab_ui.rs b/crates/collab_ui/src/collab_ui.rs index 4bb08607047ed43507d86da53765579e5646faf6..ccf17974a4512c2d7778cc73fa119fcf14970160 100644 --- a/crates/collab_ui/src/collab_ui.rs +++ b/crates/collab_ui/src/collab_ui.rs @@ -1,13 +1,16 @@ mod collab_titlebar_item; mod contacts_popover; mod incoming_call_notification; +mod project_shared_notification; -use client::UserStore; pub use collab_titlebar_item::CollabTitlebarItem; -use gpui::{ModelHandle, MutableAppContext}; +use gpui::MutableAppContext; +use std::sync::Arc; +use workspace::AppState; -pub fn init(user_store: ModelHandle, cx: &mut MutableAppContext) { +pub fn init(app_state: Arc, cx: &mut MutableAppContext) { contacts_popover::init(cx); collab_titlebar_item::init(cx); - incoming_call_notification::init(user_store, cx); + incoming_call_notification::init(app_state.user_store.clone(), cx); + project_shared_notification::init(app_state, cx); } diff --git a/crates/collab_ui/src/project_shared_notification.rs b/crates/collab_ui/src/project_shared_notification.rs new file mode 100644 index 0000000000000000000000000000000000000000..879c3ca28cbcf2dae621d031b08ce5568bc88248 --- /dev/null +++ b/crates/collab_ui/src/project_shared_notification.rs @@ -0,0 +1,174 @@ +use call::{room, ActiveCall}; +use client::User; +use gpui::{ + actions, + elements::*, + geometry::{rect::RectF, vector::vec2f}, + Entity, MouseButton, MutableAppContext, RenderContext, View, ViewContext, WindowBounds, + WindowKind, WindowOptions, +}; +use project::Project; +use settings::Settings; +use std::sync::Arc; +use workspace::{AppState, Workspace}; + +actions!(project_shared_notification, [JoinProject, DismissProject]); + +pub fn init(app_state: Arc, cx: &mut MutableAppContext) { + cx.add_action(ProjectSharedNotification::join); + cx.add_action(ProjectSharedNotification::dismiss); + + let active_call = ActiveCall::global(cx); + let mut _room_subscription = None; + cx.observe(&active_call, move |active_call, cx| { + if let Some(room) = active_call.read(cx).room().cloned() { + let app_state = app_state.clone(); + _room_subscription = Some(cx.subscribe(&room, move |_, event, cx| match event { + room::Event::RemoteProjectShared { owner, project_id } => { + cx.add_window( + WindowOptions { + bounds: WindowBounds::Fixed(RectF::new( + vec2f(0., 0.), + vec2f(300., 400.), + )), + titlebar: None, + center: true, + kind: WindowKind::PopUp, + is_movable: false, + }, + |_| { + ProjectSharedNotification::new( + *project_id, + owner.clone(), + app_state.clone(), + ) + }, + ); + } + })); + } else { + _room_subscription = None; + } + }) + .detach(); +} + +pub struct ProjectSharedNotification { + project_id: u64, + owner: Arc, + app_state: Arc, +} + +impl ProjectSharedNotification { + fn new(project_id: u64, owner: Arc, app_state: Arc) -> Self { + Self { + project_id, + owner, + app_state, + } + } + + fn join(&mut self, _: &JoinProject, cx: &mut ViewContext) { + let project_id = self.project_id; + let app_state = self.app_state.clone(); + cx.spawn_weak(|_, mut cx| async move { + let project = Project::remote( + project_id, + app_state.client.clone(), + app_state.user_store.clone(), + app_state.project_store.clone(), + app_state.languages.clone(), + app_state.fs.clone(), + cx.clone(), + ) + .await?; + + cx.add_window((app_state.build_window_options)(), |cx| { + let mut workspace = Workspace::new(project, app_state.default_item_factory, cx); + (app_state.initialize_workspace)(&mut workspace, &app_state, cx); + workspace + }); + + anyhow::Ok(()) + }) + .detach_and_log_err(cx); + + let window_id = cx.window_id(); + cx.remove_window(window_id); + } + + fn dismiss(&mut self, _: &DismissProject, cx: &mut ViewContext) { + let window_id = cx.window_id(); + cx.remove_window(window_id); + } + + fn render_owner(&self, cx: &mut RenderContext) -> ElementBox { + let theme = &cx.global::().theme.project_shared_notification; + Flex::row() + .with_children( + self.owner + .avatar + .clone() + .map(|avatar| Image::new(avatar).with_style(theme.owner_avatar).boxed()), + ) + .with_child( + Label::new( + format!("{} has shared a new project", self.owner.github_login), + theme.message.text.clone(), + ) + .boxed(), + ) + .boxed() + } + + fn render_buttons(&self, cx: &mut RenderContext) -> ElementBox { + enum Join {} + enum Dismiss {} + + Flex::row() + .with_child( + MouseEventHandler::::new(0, cx, |_, cx| { + let theme = &cx.global::().theme.project_shared_notification; + Label::new("Join".to_string(), theme.join_button.text.clone()) + .contained() + .with_style(theme.join_button.container) + .boxed() + }) + .on_click(MouseButton::Left, |_, cx| { + cx.dispatch_action(JoinProject); + }) + .boxed(), + ) + .with_child( + MouseEventHandler::::new(0, cx, |_, cx| { + let theme = &cx.global::().theme.project_shared_notification; + Label::new("Dismiss".to_string(), theme.dismiss_button.text.clone()) + .contained() + .with_style(theme.dismiss_button.container) + .boxed() + }) + .on_click(MouseButton::Left, |_, cx| { + cx.dispatch_action(DismissProject); + }) + .boxed(), + ) + .boxed() + } +} + +impl Entity for ProjectSharedNotification { + type Event = (); +} + +impl View for ProjectSharedNotification { + fn ui_name() -> &'static str { + "ProjectSharedNotification" + } + + fn render(&mut self, cx: &mut RenderContext) -> gpui::ElementBox { + Flex::row() + .with_child(self.render_owner(cx)) + .with_child(self.render_buttons(cx)) + .boxed() + } +} diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 52b34462aa992b11431ccd944f51e14e0d634e65..6253b6dabbc129442097fbbb63c7fd3607f9a8aa 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -30,6 +30,7 @@ pub struct Theme { pub breadcrumbs: ContainedText, pub contact_notification: ContactNotification, pub update_notification: UpdateNotification, + pub project_shared_notification: ProjectSharedNotification, pub tooltip: TooltipStyle, pub terminal: TerminalStyle, } @@ -481,6 +482,14 @@ pub struct UpdateNotification { pub dismiss_button: Interactive, } +#[derive(Deserialize, Default)] +pub struct ProjectSharedNotification { + pub owner_avatar: ImageStyle, + pub message: ContainedText, + pub join_button: ContainedText, + pub dismiss_button: ContainedText, +} + #[derive(Clone, Deserialize, Default)] pub struct Editor { pub text_color: Color, diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index ea42c61dfba1fd51c0646a0be5f671bbcf16b679..580493f6d0f321c934c3c71b538fb579304f701e 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -107,7 +107,6 @@ fn main() { project::Project::init(&client); client::Channel::init(&client); client::init(client.clone(), cx); - collab_ui::init(user_store.clone(), cx); command_palette::init(cx); editor::init(cx); go_to_line::init(cx); @@ -157,6 +156,7 @@ fn main() { journal::init(app_state.clone(), cx); theme_selector::init(app_state.clone(), cx); zed::init(&app_state, cx); + collab_ui::init(app_state.clone(), cx); cx.set_menus(menus::menus()); diff --git a/styles/src/styleTree/app.ts b/styles/src/styleTree/app.ts index a3ab4b654c6f6d2835804c834dab6a7435cd75f3..1c0c81cfde950a7e89f3e9546955d2ea8cb3f6eb 100644 --- a/styles/src/styleTree/app.ts +++ b/styles/src/styleTree/app.ts @@ -14,6 +14,7 @@ import contextMenu from "./contextMenu"; import projectDiagnostics from "./projectDiagnostics"; import contactNotification from "./contactNotification"; import updateNotification from "./updateNotification"; +import projectSharedNotification from "./projectSharedNotification"; import tooltip from "./tooltip"; import terminal from "./terminal"; @@ -47,6 +48,7 @@ export default function app(theme: Theme): Object { }, contactNotification: contactNotification(theme), updateNotification: updateNotification(theme), + projectSharedNotification: projectSharedNotification(theme), tooltip: tooltip(theme), terminal: terminal(theme), }; diff --git a/styles/src/styleTree/projectSharedNotification.ts b/styles/src/styleTree/projectSharedNotification.ts new file mode 100644 index 0000000000000000000000000000000000000000..bc342651358fac5627ec71f718e5322f3b03d891 --- /dev/null +++ b/styles/src/styleTree/projectSharedNotification.ts @@ -0,0 +1,22 @@ +import Theme from "../themes/common/theme"; +import { text } from "./components"; + +export default function projectSharedNotification(theme: Theme): Object { + const avatarSize = 12; + return { + ownerAvatar: { + height: avatarSize, + width: avatarSize, + cornerRadius: 6, + }, + message: { + ...text(theme, "sans", "primary", { size: "xs" }), + }, + joinButton: { + ...text(theme, "sans", "primary", { size: "xs" }) + }, + dismissButton: { + ...text(theme, "sans", "primary", { size: "xs" }) + }, + }; +} From 41240351d351197392562c04b8c59a1336104c0f Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 4 Oct 2022 18:00:54 +0200 Subject: [PATCH 045/112] Simplify `Collaborator` to stop including the user It can be retrieved from the `Room` and we're guaranteed to have a room in order to have collaborators in a project. --- crates/collab/src/integration_tests.rs | 14 +--------- crates/collab_ui/src/collab_titlebar_item.rs | 5 ++-- crates/project/src/project.rs | 27 +++++--------------- 3 files changed, 11 insertions(+), 35 deletions(-) diff --git a/crates/collab/src/integration_tests.rs b/crates/collab/src/integration_tests.rs index 40af0a731eb578a1aab6814074b590188ce4d279..6cadadb4c5fe9e63bc16fde1c0a2772c2c0ce2c7 100644 --- a/crates/collab/src/integration_tests.rs +++ b/crates/collab/src/integration_tests.rs @@ -318,24 +318,12 @@ async fn test_share_project( // Join that project as client B let client_b_peer_id = client_b.peer_id; let project_b = client_b.build_remote_project(project_id, cx_b).await; - let replica_id_b = project_b.read_with(cx_b, |project, _| { - assert_eq!( - project - .collaborators() - .get(&client_a.peer_id) - .unwrap() - .user - .github_login, - "user_a" - ); - project.replica_id() - }); + let replica_id_b = project_b.read_with(cx_b, |project, _| project.replica_id()); deterministic.run_until_parked(); project_a.read_with(cx_a, |project, _| { let client_b_collaborator = project.collaborators().get(&client_b_peer_id).unwrap(); assert_eq!(client_b_collaborator.replica_id, replica_id_b); - assert_eq!(client_b_collaborator.user.github_login, "user_b"); }); project_b.read_with(cx_b, |project, cx| { let worktree = project.worktrees(cx).next().unwrap().read(cx); diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index 8b467e6cb43cb2113bb11ac398091c278bac9c28..a30ae68f9feaf07516a97c2a47cd5fedda393f2b 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -223,13 +223,14 @@ impl CollabTitlebarItem { .read(cx) .remote_participants() .get(&collaborator.peer_id)?; + let user = participant.user.clone(); let is_active = project_id.map_or(false, |project_id| { participant.location == ParticipantLocation::Project { project_id } }); Some(self.render_avatar( - collaborator.user.avatar.clone()?, + user.avatar.clone()?, collaborator.replica_id, - Some((collaborator.peer_id, &collaborator.user.github_login)), + Some((collaborator.peer_id, &user.github_login)), is_active, workspace, theme, diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index c0ed2e5ad4828ca6caec0e8082d80635d109faa4..40503297b35f9d73357251fa62ad17f08c4dd08c 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -9,7 +9,7 @@ pub mod worktree; mod project_tests; use anyhow::{anyhow, Context, Result}; -use client::{proto, Client, PeerId, TypedEnvelope, User, UserStore}; +use client::{proto, Client, PeerId, TypedEnvelope, UserStore}; use clock::ReplicaId; use collections::{hash_map, BTreeMap, HashMap, HashSet}; use futures::{future::Shared, AsyncWriteExt, Future, FutureExt, StreamExt, TryFutureExt}; @@ -165,7 +165,6 @@ enum ProjectClientState { #[derive(Clone, Debug)] pub struct Collaborator { - pub user: Arc, pub peer_id: PeerId, pub replica_id: ReplicaId, } @@ -582,7 +581,7 @@ impl Project { .await?; let mut collaborators = HashMap::default(); for message in response.collaborators { - let collaborator = Collaborator::from_proto(message, &user_store, &mut cx).await?; + let collaborator = Collaborator::from_proto(message); collaborators.insert(collaborator.peer_id, collaborator); } @@ -4451,14 +4450,13 @@ impl Project { _: Arc, mut cx: AsyncAppContext, ) -> Result<()> { - let user_store = this.read_with(&cx, |this, _| this.user_store.clone()); let collaborator = envelope .payload .collaborator .take() .ok_or_else(|| anyhow!("empty collaborator"))?; - let collaborator = Collaborator::from_proto(collaborator, &user_store, &mut cx).await?; + let collaborator = Collaborator::from_proto(collaborator); this.update(&mut cx, |this, cx| { this.collaborators .insert(collaborator.peer_id, collaborator); @@ -5904,21 +5902,10 @@ impl Entity for Project { } impl Collaborator { - fn from_proto( - message: proto::Collaborator, - user_store: &ModelHandle, - cx: &mut AsyncAppContext, - ) -> impl Future> { - let user = user_store.update(cx, |user_store, cx| { - user_store.get_user(message.user_id, cx) - }); - - async move { - Ok(Self { - peer_id: PeerId(message.peer_id), - user: user.await?, - replica_id: message.replica_id as ReplicaId, - }) + fn from_proto(message: proto::Collaborator) -> Self { + Self { + peer_id: PeerId(message.peer_id), + replica_id: message.replica_id as ReplicaId, } } } From ebee2168fc737a5534779a6f1ddea28563736b31 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 4 Oct 2022 18:15:56 +0200 Subject: [PATCH 046/112] Re-emit notifications and events from `ActiveCall` This lets us only observe and subscribe to the active call without needing to track the underlying `Room` if it changes, which implies writing the same boilerplate over and over. --- crates/call/src/call.rs | 34 ++++++++------- crates/collab_ui/src/collab_titlebar_item.rs | 17 +------- crates/collab_ui/src/contacts_popover.rs | 14 +------ .../src/project_shared_notification.rs | 41 ++++++------------- 4 files changed, 35 insertions(+), 71 deletions(-) diff --git a/crates/call/src/call.rs b/crates/call/src/call.rs index d800c3ac0657fbbe716ef1f1799b4e75b9d7adbd..19de06c1c40dd98f98badecdd7cf1fc186eb1b82 100644 --- a/crates/call/src/call.rs +++ b/crates/call/src/call.rs @@ -3,7 +3,7 @@ pub mod room; use anyhow::{anyhow, Result}; use client::{incoming_call::IncomingCall, Client, UserStore}; -use gpui::{Entity, ModelContext, ModelHandle, MutableAppContext, Task}; +use gpui::{Entity, ModelContext, ModelHandle, MutableAppContext, Subscription, Task}; pub use participant::ParticipantLocation; pub use room::Room; use std::sync::Arc; @@ -14,13 +14,13 @@ pub fn init(client: Arc, user_store: ModelHandle, cx: &mut Mu } pub struct ActiveCall { - room: Option>, + room: Option<(ModelHandle, Vec)>, client: Arc, user_store: ModelHandle, } impl Entity for ActiveCall { - type Event = (); + type Event = room::Event; } impl ActiveCall { @@ -41,8 +41,7 @@ impl ActiveCall { recipient_user_id: u64, cx: &mut ModelContext, ) -> Task> { - let room = self.room.clone(); - + let room = self.room.as_ref().map(|(room, _)| room.clone()); let client = self.client.clone(); let user_store = self.user_store.clone(); cx.spawn(|this, mut cx| async move { @@ -50,10 +49,7 @@ impl ActiveCall { room } else { let room = cx.update(|cx| Room::create(client, user_store, cx)).await?; - this.update(&mut cx, |this, cx| { - this.room = Some(room.clone()); - cx.notify(); - }); + this.update(&mut cx, |this, cx| this.set_room(Some(room.clone()), cx)); room }; room.update(&mut cx, |room, cx| room.call(recipient_user_id, cx)) @@ -71,15 +67,25 @@ impl ActiveCall { let join = Room::join(call, self.client.clone(), self.user_store.clone(), cx); cx.spawn(|this, mut cx| async move { let room = join.await?; - this.update(&mut cx, |this, cx| { - this.room = Some(room); - cx.notify(); - }); + this.update(&mut cx, |this, cx| this.set_room(Some(room.clone()), cx)); Ok(()) }) } + fn set_room(&mut self, room: Option>, cx: &mut ModelContext) { + if let Some(room) = room { + let subscriptions = vec![ + cx.observe(&room, |_, _, cx| cx.notify()), + cx.subscribe(&room, |_, _, event, cx| cx.emit(event.clone())), + ]; + self.room = Some((room, subscriptions)); + } else { + self.room = None; + } + cx.notify(); + } + pub fn room(&self) -> Option<&ModelHandle> { - self.room.as_ref() + self.room.as_ref().map(|(room, _)| room) } } diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index a30ae68f9feaf07516a97c2a47cd5fedda393f2b..81ec7973a5b139bc4040c2fbd4f4d4cdbc599b70 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -30,7 +30,6 @@ pub fn init(cx: &mut MutableAppContext) { pub struct CollabTitlebarItem { workspace: WeakViewHandle, contacts_popover: Option>, - room_subscription: Option, _subscriptions: Vec, } @@ -73,24 +72,12 @@ impl CollabTitlebarItem { let active_call = ActiveCall::global(cx); let mut subscriptions = Vec::new(); subscriptions.push(cx.observe(workspace, |_, _, cx| cx.notify())); - subscriptions.push(cx.observe(&active_call, |this, _, cx| this.active_call_changed(cx))); - let mut this = Self { + subscriptions.push(cx.observe(&active_call, |_, _, cx| cx.notify())); + Self { workspace: workspace.downgrade(), contacts_popover: None, - room_subscription: None, _subscriptions: subscriptions, - }; - this.active_call_changed(cx); - this - } - - fn active_call_changed(&mut self, cx: &mut ViewContext) { - if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() { - self.room_subscription = Some(cx.observe(&room, |_, _, cx| cx.notify())); - } else { - self.room_subscription = None; } - cx.notify(); } fn share_project(&mut self, _: &ShareProject, cx: &mut ViewContext) { diff --git a/crates/collab_ui/src/contacts_popover.rs b/crates/collab_ui/src/contacts_popover.rs index 742096104130f7cbb5aed3ed0f1465d102b1986b..1980d8a14fc4f3174d93f396177f440e969acf58 100644 --- a/crates/collab_ui/src/contacts_popover.rs +++ b/crates/collab_ui/src/contacts_popover.rs @@ -80,7 +80,6 @@ pub enum Event { } pub struct ContactsPopover { - room_subscription: Option, entries: Vec, match_candidates: Vec, list_state: ListState, @@ -159,10 +158,9 @@ impl ContactsPopover { let active_call = ActiveCall::global(cx); let mut subscriptions = Vec::new(); subscriptions.push(cx.observe(&user_store, |this, _, cx| this.update_entries(cx))); - subscriptions.push(cx.observe(&active_call, |this, _, cx| this.active_call_changed(cx))); + subscriptions.push(cx.observe(&active_call, |_, _, cx| cx.notify())); let mut this = Self { - room_subscription: None, list_state, selection: None, collapsed_sections: Default::default(), @@ -173,19 +171,9 @@ impl ContactsPopover { user_store, }; this.update_entries(cx); - this.active_call_changed(cx); this } - fn active_call_changed(&mut self, cx: &mut ViewContext) { - if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() { - self.room_subscription = Some(cx.observe(&room, |_, _, cx| cx.notify())); - } else { - self.room_subscription = None; - } - cx.notify(); - } - fn clear_filter(&mut self, _: &Cancel, cx: &mut ViewContext) { let did_clear = self.filter_editor.update(cx, |editor, cx| { if editor.buffer().read(cx).len(cx) > 0 { diff --git a/crates/collab_ui/src/project_shared_notification.rs b/crates/collab_ui/src/project_shared_notification.rs index 879c3ca28cbcf2dae621d031b08ce5568bc88248..53eb17684e9a0f53aecbf03efc201b9c5099768c 100644 --- a/crates/collab_ui/src/project_shared_notification.rs +++ b/crates/collab_ui/src/project_shared_notification.rs @@ -19,35 +19,18 @@ pub fn init(app_state: Arc, cx: &mut MutableAppContext) { cx.add_action(ProjectSharedNotification::dismiss); let active_call = ActiveCall::global(cx); - let mut _room_subscription = None; - cx.observe(&active_call, move |active_call, cx| { - if let Some(room) = active_call.read(cx).room().cloned() { - let app_state = app_state.clone(); - _room_subscription = Some(cx.subscribe(&room, move |_, event, cx| match event { - room::Event::RemoteProjectShared { owner, project_id } => { - cx.add_window( - WindowOptions { - bounds: WindowBounds::Fixed(RectF::new( - vec2f(0., 0.), - vec2f(300., 400.), - )), - titlebar: None, - center: true, - kind: WindowKind::PopUp, - is_movable: false, - }, - |_| { - ProjectSharedNotification::new( - *project_id, - owner.clone(), - app_state.clone(), - ) - }, - ); - } - })); - } else { - _room_subscription = None; + cx.subscribe(&active_call, move |_, event, cx| match event { + room::Event::RemoteProjectShared { owner, project_id } => { + cx.add_window( + WindowOptions { + bounds: WindowBounds::Fixed(RectF::new(vec2f(0., 0.), vec2f(300., 400.))), + titlebar: None, + center: true, + kind: WindowKind::PopUp, + is_movable: false, + }, + |_| ProjectSharedNotification::new(*project_id, owner.clone(), app_state.clone()), + ); } }) .detach(); From 678b013da6a0c604b362417c273ccda2c225b0c5 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 4 Oct 2022 18:35:54 +0200 Subject: [PATCH 047/112] Don't show share button for remote projects Co-Authored-By: Max Brunsfeld --- crates/collab_ui/src/collab_titlebar_item.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index 81ec7973a5b139bc4040c2fbd4f4d4cdbc599b70..94593d5dc53e154a3c57640da0152993e3639105 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -54,7 +54,10 @@ impl View for CollabTitlebarItem { let mut container = Flex::row(); if workspace.read(cx).client().status().borrow().is_connected() { - if project.is_shared() || ActiveCall::global(cx).read(cx).room().is_none() { + if project.is_shared() + || project.is_remote() + || ActiveCall::global(cx).read(cx).room().is_none() + { container.add_child(self.render_toggle_contacts_button(&theme, cx)); } else { container.add_child(self.render_share_button(&theme, cx)); From fceba6814ffda8e75647ff9957296b13567f5d8a Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 4 Oct 2022 19:25:48 +0200 Subject: [PATCH 048/112] Automatically share project when creating the room Co-Authored-By: Max Brunsfeld --- crates/call/src/call.rs | 19 ++++++- crates/call/src/room.rs | 2 + crates/client/src/incoming_call.rs | 1 + crates/client/src/user.rs | 1 + crates/collab/src/integration_tests.rs | 18 ++++-- crates/collab/src/rpc.rs | 12 +++- crates/collab/src/rpc/store.rs | 27 +++++++-- crates/collab_ui/src/collab_titlebar_item.rs | 3 +- crates/collab_ui/src/collab_ui.rs | 2 +- crates/collab_ui/src/contacts_popover.rs | 29 ++++++++-- .../src/incoming_call_notification.rs | 56 ++++++++++++++----- crates/rpc/proto/zed.proto | 2 + 12 files changed, 137 insertions(+), 35 deletions(-) diff --git a/crates/call/src/call.rs b/crates/call/src/call.rs index 19de06c1c40dd98f98badecdd7cf1fc186eb1b82..a861f94bd0fa05c92442d98f846743ead630724a 100644 --- a/crates/call/src/call.rs +++ b/crates/call/src/call.rs @@ -5,6 +5,7 @@ use anyhow::{anyhow, Result}; use client::{incoming_call::IncomingCall, Client, UserStore}; use gpui::{Entity, ModelContext, ModelHandle, MutableAppContext, Subscription, Task}; pub use participant::ParticipantLocation; +use project::Project; pub use room::Room; use std::sync::Arc; @@ -39,6 +40,7 @@ impl ActiveCall { pub fn invite( &mut self, recipient_user_id: u64, + initial_project: Option>, cx: &mut ModelContext, ) -> Task> { let room = self.room.as_ref().map(|(room, _)| room.clone()); @@ -52,8 +54,21 @@ impl ActiveCall { this.update(&mut cx, |this, cx| this.set_room(Some(room.clone()), cx)); room }; - room.update(&mut cx, |room, cx| room.call(recipient_user_id, cx)) - .await?; + + let initial_project_id = if let Some(initial_project) = initial_project { + let room_id = room.read_with(&cx, |room, _| room.id()); + Some( + initial_project + .update(&mut cx, |project, cx| project.share(room_id, cx)) + .await?, + ) + } else { + None + }; + room.update(&mut cx, |room, cx| { + room.call(recipient_user_id, initial_project_id, cx) + }) + .await?; Ok(()) }) diff --git a/crates/call/src/room.rs b/crates/call/src/room.rs index 4cac210f8b66afe60d37488289ead4dc4f276386..0237972167726bf392620bbf2d424828fb484f02 100644 --- a/crates/call/src/room.rs +++ b/crates/call/src/room.rs @@ -216,6 +216,7 @@ impl Room { pub fn call( &mut self, recipient_user_id: u64, + initial_project_id: Option, cx: &mut ModelContext, ) -> Task> { if self.status.is_offline() { @@ -229,6 +230,7 @@ impl Room { .request(proto::Call { room_id, recipient_user_id, + initial_project_id, }) .await?; Ok(()) diff --git a/crates/client/src/incoming_call.rs b/crates/client/src/incoming_call.rs index 75d8411ec3879af9d5c9d9d6ac666d7cd98e8f6a..80ba014061f97c2f44e5887f883b4dd34335e629 100644 --- a/crates/client/src/incoming_call.rs +++ b/crates/client/src/incoming_call.rs @@ -6,4 +6,5 @@ pub struct IncomingCall { pub room_id: u64, pub caller: Arc, pub participants: Vec>, + pub initial_project_id: Option, } diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index 0529045c77c4ece62a0cd0fc9729ba86b957c366..2d79b7be84592b9a91932c2dbe6545439f6c4b56 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -212,6 +212,7 @@ impl UserStore { this.get_user(envelope.payload.caller_user_id, cx) }) .await?, + initial_project_id: envelope.payload.initial_project_id, }; this.update(&mut cx, |this, _| { *this.incoming_call.0.borrow_mut() = Some(call); diff --git a/crates/collab/src/integration_tests.rs b/crates/collab/src/integration_tests.rs index 6cadadb4c5fe9e63bc16fde1c0a2772c2c0ce2c7..92f94b662150e1a3bb2ea100fccb08266dc4278b 100644 --- a/crates/collab/src/integration_tests.rs +++ b/crates/collab/src/integration_tests.rs @@ -95,7 +95,9 @@ async fn test_basic_calls( .user_store .update(cx_b, |user, _| user.incoming_call()); room_a - .update(cx_a, |room, cx| room.call(client_b.user_id().unwrap(), cx)) + .update(cx_a, |room, cx| { + room.call(client_b.user_id().unwrap(), None, cx) + }) .await .unwrap(); @@ -147,7 +149,9 @@ async fn test_basic_calls( .user_store .update(cx_c, |user, _| user.incoming_call()); room_b - .update(cx_b, |room, cx| room.call(client_c.user_id().unwrap(), cx)) + .update(cx_b, |room, cx| { + room.call(client_c.user_id().unwrap(), None, cx) + }) .await .unwrap(); @@ -234,7 +238,9 @@ async fn test_leaving_room_on_disconnection( .user_store .update(cx_b, |user, _| user.incoming_call()); room_a - .update(cx_a, |room, cx| room.call(client_b.user_id().unwrap(), cx)) + .update(cx_a, |room, cx| { + room.call(client_b.user_id().unwrap(), None, cx) + }) .await .unwrap(); @@ -4849,7 +4855,7 @@ async fn test_random_collaboration( host_language_registry.add(Arc::new(language)); let host_user_id = host.current_user_id(&host_cx); - room.update(cx, |room, cx| room.call(host_user_id.to_proto(), cx)) + room.update(cx, |room, cx| room.call(host_user_id.to_proto(), None, cx)) .await .unwrap(); deterministic.run_until_parked(); @@ -4941,7 +4947,7 @@ async fn test_random_collaboration( let guest = server.create_client(&mut guest_cx, &guest_username).await; let guest_user_id = guest.current_user_id(&guest_cx); - room.update(cx, |room, cx| room.call(guest_user_id.to_proto(), cx)) + room.update(cx, |room, cx| room.call(guest_user_id.to_proto(), None, cx)) .await .unwrap(); deterministic.run_until_parked(); @@ -5353,7 +5359,7 @@ impl TestServer { for (client_b, cx_b) in right { let user_id_b = client_b.current_user_id(*cx_b).to_proto(); room_a - .update(*cx_a, |room, cx| room.call(user_id_b, cx)) + .update(*cx_a, |room, cx| room.call(user_id_b, None, cx)) .await .unwrap(); diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index c000400568648692f68651f15e568a981d3113f4..4098e6522aa88df83afbe13a4454f50d727e0ede 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -660,6 +660,10 @@ impl Server { .await .user_id_for_connection(request.sender_id)?; let recipient_user_id = UserId::from_proto(request.payload.recipient_user_id); + let initial_project_id = request + .payload + .initial_project_id + .map(ProjectId::from_proto); if !self .app_state .db @@ -672,8 +676,12 @@ impl Server { let room_id = request.payload.room_id; let mut calls = { let mut store = self.store().await; - let (room, recipient_connection_ids, incoming_call) = - store.call(room_id, request.sender_id, recipient_user_id)?; + let (room, recipient_connection_ids, incoming_call) = store.call( + room_id, + recipient_user_id, + initial_project_id, + request.sender_id, + )?; self.room_updated(room); recipient_connection_ids .into_iter() diff --git a/crates/collab/src/rpc/store.rs b/crates/collab/src/rpc/store.rs index c917309cd21da201e7f6cb06b23857e298cb65ed..da9f242ac5110bf4c5017c3b646849e0f9cc00a6 100644 --- a/crates/collab/src/rpc/store.rs +++ b/crates/collab/src/rpc/store.rs @@ -40,6 +40,7 @@ pub struct Call { pub caller_user_id: UserId, pub room_id: RoomId, pub connection_id: Option, + pub initial_project_id: Option, } #[derive(Serialize)] @@ -175,6 +176,9 @@ impl Store { .iter() .map(|participant| participant.user_id) .collect(), + initial_project_id: active_call + .initial_project_id + .map(|project_id| project_id.to_proto()), }) } } else { @@ -379,6 +383,7 @@ impl Store { caller_user_id: connection.user_id, room_id, connection_id: Some(creator_connection_id), + initial_project_id: None, }); Ok(room_id) } @@ -486,17 +491,18 @@ impl Store { pub fn call( &mut self, room_id: RoomId, + recipient_user_id: UserId, + initial_project_id: Option, from_connection_id: ConnectionId, - recipient_id: UserId, ) -> Result<(&proto::Room, Vec, proto::IncomingCall)> { let caller_user_id = self.user_id_for_connection(from_connection_id)?; let recipient_connection_ids = self - .connection_ids_for_user(recipient_id) + .connection_ids_for_user(recipient_user_id) .collect::>(); let mut recipient = self .connected_users - .get_mut(&recipient_id) + .get_mut(&recipient_user_id) .ok_or_else(|| anyhow!("no such connection"))?; anyhow::ensure!( recipient.active_call.is_none(), @@ -516,14 +522,24 @@ impl Store { anyhow::ensure!( room.pending_user_ids .iter() - .all(|user_id| UserId::from_proto(*user_id) != recipient_id), + .all(|user_id| UserId::from_proto(*user_id) != recipient_user_id), "cannot call the same user more than once" ); - room.pending_user_ids.push(recipient_id.to_proto()); + room.pending_user_ids.push(recipient_user_id.to_proto()); + + if let Some(initial_project_id) = initial_project_id { + let project = self + .projects + .get(&initial_project_id) + .ok_or_else(|| anyhow!("no such project"))?; + anyhow::ensure!(project.room_id == room_id, "no such project"); + } + recipient.active_call = Some(Call { caller_user_id, room_id, connection_id: None, + initial_project_id, }); Ok(( @@ -537,6 +553,7 @@ impl Store { .iter() .map(|participant| participant.user_id) .collect(), + initial_project_id: initial_project_id.map(|project_id| project_id.to_proto()), }, )) } diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index 94593d5dc53e154a3c57640da0152993e3639105..8e4b8f9f322b48f441faffd91766f696d0f418a8 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -100,8 +100,9 @@ impl CollabTitlebarItem { Some(_) => {} None => { if let Some(workspace) = self.workspace.upgrade(cx) { + let project = workspace.read(cx).project().clone(); let user_store = workspace.read(cx).user_store().clone(); - let view = cx.add_view(|cx| ContactsPopover::new(user_store, cx)); + let view = cx.add_view(|cx| ContactsPopover::new(project, user_store, cx)); cx.focus(&view); cx.subscribe(&view, |this, _, event, cx| { match event { diff --git a/crates/collab_ui/src/collab_ui.rs b/crates/collab_ui/src/collab_ui.rs index ccf17974a4512c2d7778cc73fa119fcf14970160..607c1b50543b886f52efa9dafb02801e38f7d44e 100644 --- a/crates/collab_ui/src/collab_ui.rs +++ b/crates/collab_ui/src/collab_ui.rs @@ -11,6 +11,6 @@ use workspace::AppState; pub fn init(app_state: Arc, cx: &mut MutableAppContext) { contacts_popover::init(cx); collab_titlebar_item::init(cx); - incoming_call_notification::init(app_state.user_store.clone(), cx); + incoming_call_notification::init(app_state.clone(), cx); project_shared_notification::init(app_state, cx); } diff --git a/crates/collab_ui/src/contacts_popover.rs b/crates/collab_ui/src/contacts_popover.rs index 1980d8a14fc4f3174d93f396177f440e969acf58..c4eef3c9d8dcc781c3a343550c618b6bb828a62a 100644 --- a/crates/collab_ui/src/contacts_popover.rs +++ b/crates/collab_ui/src/contacts_popover.rs @@ -10,6 +10,7 @@ use gpui::{ ViewHandle, }; use menu::{Confirm, SelectNext, SelectPrev}; +use project::Project; use settings::Settings; use theme::IconButton; @@ -30,6 +31,7 @@ struct ToggleExpanded(Section); #[derive(Clone, PartialEq)] struct Call { recipient_user_id: u64, + initial_project: Option>, } #[derive(Clone, Copy, PartialEq, Eq, Debug, PartialOrd, Ord)] @@ -83,6 +85,7 @@ pub struct ContactsPopover { entries: Vec, match_candidates: Vec, list_state: ListState, + project: ModelHandle, user_store: ModelHandle, filter_editor: ViewHandle, collapsed_sections: Vec
, @@ -91,7 +94,11 @@ pub struct ContactsPopover { } impl ContactsPopover { - pub fn new(user_store: ModelHandle, cx: &mut ViewContext) -> Self { + pub fn new( + project: ModelHandle, + user_store: ModelHandle, + cx: &mut ViewContext, + ) -> Self { let filter_editor = cx.add_view(|cx| { let mut editor = Editor::single_line( Some(|theme| theme.contacts_panel.user_query_editor.clone()), @@ -149,9 +156,13 @@ impl ContactsPopover { is_selected, cx, ), - ContactEntry::Contact(contact) => { - Self::render_contact(contact, &theme.contacts_panel, is_selected, cx) - } + ContactEntry::Contact(contact) => Self::render_contact( + contact, + &this.project, + &theme.contacts_panel, + is_selected, + cx, + ), } }); @@ -168,6 +179,7 @@ impl ContactsPopover { match_candidates: Default::default(), filter_editor, _subscriptions: subscriptions, + project, user_store, }; this.update_entries(cx); @@ -463,12 +475,18 @@ impl ContactsPopover { fn render_contact( contact: &Contact, + project: &ModelHandle, theme: &theme::ContactsPanel, is_selected: bool, cx: &mut RenderContext, ) -> ElementBox { let online = contact.online; let user_id = contact.user.id; + let initial_project = if ActiveCall::global(cx).read(cx).room().is_none() { + Some(project.clone()) + } else { + None + }; let mut element = MouseEventHandler::::new(contact.user.id as usize, cx, |_, _| { Flex::row() @@ -501,6 +519,7 @@ impl ContactsPopover { if online { cx.dispatch_action(Call { recipient_user_id: user_id, + initial_project: initial_project.clone(), }); } }); @@ -629,7 +648,7 @@ impl ContactsPopover { fn call(&mut self, action: &Call, cx: &mut ViewContext) { ActiveCall::global(cx) .update(cx, |active_call, cx| { - active_call.invite(action.recipient_user_id, cx) + active_call.invite(action.recipient_user_id, action.initial_project.clone(), cx) }) .detach_and_log_err(cx); } diff --git a/crates/collab_ui/src/incoming_call_notification.rs b/crates/collab_ui/src/incoming_call_notification.rs index a239acc7e6faec47c137670c13ef29a626c48182..4630054c5e355bd41eb7dce4729376d5248ceeb7 100644 --- a/crates/collab_ui/src/incoming_call_notification.rs +++ b/crates/collab_ui/src/incoming_call_notification.rs @@ -1,21 +1,25 @@ +use std::sync::Arc; + use call::ActiveCall; -use client::{incoming_call::IncomingCall, UserStore}; +use client::incoming_call::IncomingCall; use futures::StreamExt; use gpui::{ elements::*, geometry::{rect::RectF, vector::vec2f}, - impl_internal_actions, Entity, ModelHandle, MouseButton, MutableAppContext, RenderContext, - View, ViewContext, WindowBounds, WindowKind, WindowOptions, + impl_internal_actions, Entity, MouseButton, MutableAppContext, RenderContext, View, + ViewContext, WindowBounds, WindowKind, WindowOptions, }; +use project::Project; use settings::Settings; use util::ResultExt; +use workspace::{AppState, Workspace}; impl_internal_actions!(incoming_call_notification, [RespondToCall]); -pub fn init(user_store: ModelHandle, cx: &mut MutableAppContext) { +pub fn init(app_state: Arc, cx: &mut MutableAppContext) { cx.add_action(IncomingCallNotification::respond_to_call); - let mut incoming_call = user_store.read(cx).incoming_call(); + let mut incoming_call = app_state.user_store.read(cx).incoming_call(); cx.spawn(|mut cx| async move { let mut notification_window = None; while let Some(incoming_call) = incoming_call.next().await { @@ -32,7 +36,7 @@ pub fn init(user_store: ModelHandle, cx: &mut MutableAppContext) { kind: WindowKind::PopUp, is_movable: false, }, - |_| IncomingCallNotification::new(incoming_call, user_store.clone()), + |_| IncomingCallNotification::new(incoming_call, app_state.clone()), ); notification_window = Some(window_id); } @@ -48,21 +52,47 @@ struct RespondToCall { pub struct IncomingCallNotification { call: IncomingCall, - user_store: ModelHandle, + app_state: Arc, } impl IncomingCallNotification { - pub fn new(call: IncomingCall, user_store: ModelHandle) -> Self { - Self { call, user_store } + pub fn new(call: IncomingCall, app_state: Arc) -> Self { + Self { call, app_state } } fn respond_to_call(&mut self, action: &RespondToCall, cx: &mut ViewContext) { if action.accept { - ActiveCall::global(cx) - .update(cx, |active_call, cx| active_call.join(&self.call, cx)) - .detach_and_log_err(cx); + let app_state = self.app_state.clone(); + let join = ActiveCall::global(cx) + .update(cx, |active_call, cx| active_call.join(&self.call, cx)); + let initial_project_id = self.call.initial_project_id; + cx.spawn_weak(|_, mut cx| async move { + join.await?; + if let Some(initial_project_id) = initial_project_id { + let project = Project::remote( + initial_project_id, + app_state.client.clone(), + app_state.user_store.clone(), + app_state.project_store.clone(), + app_state.languages.clone(), + app_state.fs.clone(), + cx.clone(), + ) + .await?; + + cx.add_window((app_state.build_window_options)(), |cx| { + let mut workspace = + Workspace::new(project, app_state.default_item_factory, cx); + (app_state.initialize_workspace)(&mut workspace, &app_state, cx); + workspace + }); + } + anyhow::Ok(()) + }) + .detach_and_log_err(cx); } else { - self.user_store + self.app_state + .user_store .update(cx, |user_store, _| user_store.decline_call().log_err()); } diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index ffec915269deb5f5d91e93e20cc772e94238e3d0..6c8ec72e86d30779fbc11d9cbf1c3296f2aaa57d 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -179,12 +179,14 @@ message ParticipantLocation { message Call { uint64 room_id = 1; uint64 recipient_user_id = 2; + optional uint64 initial_project_id = 3; } message IncomingCall { uint64 room_id = 1; uint64 caller_user_id = 2; repeated uint64 participant_user_ids = 3; + optional uint64 initial_project_id = 4; } message CancelCall {} From 087760dba0d9d5962a14dae2f4bcab33d85100bb Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 5 Oct 2022 10:51:51 +0200 Subject: [PATCH 049/112] Use AppContext instead of MutableAppContext for ActiveCall::global --- crates/call/src/call.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/call/src/call.rs b/crates/call/src/call.rs index a861f94bd0fa05c92442d98f846743ead630724a..4a662e42e084b07804f0e326ef5cd4efc6325715 100644 --- a/crates/call/src/call.rs +++ b/crates/call/src/call.rs @@ -3,7 +3,7 @@ pub mod room; use anyhow::{anyhow, Result}; use client::{incoming_call::IncomingCall, Client, UserStore}; -use gpui::{Entity, ModelContext, ModelHandle, MutableAppContext, Subscription, Task}; +use gpui::{AppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Subscription, Task}; pub use participant::ParticipantLocation; use project::Project; pub use room::Room; @@ -33,7 +33,7 @@ impl ActiveCall { } } - pub fn global(cx: &mut MutableAppContext) -> ModelHandle { + pub fn global(cx: &AppContext) -> ModelHandle { cx.global::>().clone() } From 84eebbe24a415d45bd3e9d2d035a800e2f5d3c57 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 5 Oct 2022 11:01:28 +0200 Subject: [PATCH 050/112] Always open project when added to a call via the `+` button --- crates/collab_ui/src/contacts_popover.rs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/crates/collab_ui/src/contacts_popover.rs b/crates/collab_ui/src/contacts_popover.rs index c4eef3c9d8dcc781c3a343550c618b6bb828a62a..389fe9fbd286aaaaaeb00e33b57a6396a242cee7 100644 --- a/crates/collab_ui/src/contacts_popover.rs +++ b/crates/collab_ui/src/contacts_popover.rs @@ -482,11 +482,7 @@ impl ContactsPopover { ) -> ElementBox { let online = contact.online; let user_id = contact.user.id; - let initial_project = if ActiveCall::global(cx).read(cx).room().is_none() { - Some(project.clone()) - } else { - None - }; + let initial_project = project.clone(); let mut element = MouseEventHandler::::new(contact.user.id as usize, cx, |_, _| { Flex::row() @@ -519,7 +515,7 @@ impl ContactsPopover { if online { cx.dispatch_action(Call { recipient_user_id: user_id, - initial_project: initial_project.clone(), + initial_project: Some(initial_project.clone()), }); } }); From 78e3370c1e701a8acbec8d8a06181002afc3ef6f Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 5 Oct 2022 11:19:44 +0200 Subject: [PATCH 051/112] Set room only after project has been shared to avoid flicker --- crates/call/src/call.rs | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/crates/call/src/call.rs b/crates/call/src/call.rs index 4a662e42e084b07804f0e326ef5cd4efc6325715..2f64115fb5c01cda6cb9e0b09a5b239270fe4d41 100644 --- a/crates/call/src/call.rs +++ b/crates/call/src/call.rs @@ -50,9 +50,7 @@ impl ActiveCall { let room = if let Some(room) = room { room } else { - let room = cx.update(|cx| Room::create(client, user_store, cx)).await?; - this.update(&mut cx, |this, cx| this.set_room(Some(room.clone()), cx)); - room + cx.update(|cx| Room::create(client, user_store, cx)).await? }; let initial_project_id = if let Some(initial_project) = initial_project { @@ -65,6 +63,8 @@ impl ActiveCall { } else { None }; + + this.update(&mut cx, |this, cx| this.set_room(Some(room.clone()), cx)); room.update(&mut cx, |room, cx| { room.call(recipient_user_id, initial_project_id, cx) }) @@ -88,16 +88,18 @@ impl ActiveCall { } fn set_room(&mut self, room: Option>, cx: &mut ModelContext) { - if let Some(room) = room { - let subscriptions = vec![ - cx.observe(&room, |_, _, cx| cx.notify()), - cx.subscribe(&room, |_, _, event, cx| cx.emit(event.clone())), - ]; - self.room = Some((room, subscriptions)); - } else { - self.room = None; + if room.as_ref() != self.room.as_ref().map(|room| &room.0) { + if let Some(room) = room { + let subscriptions = vec![ + cx.observe(&room, |_, _, cx| cx.notify()), + cx.subscribe(&room, |_, _, event, cx| cx.emit(event.clone())), + ]; + self.room = Some((room, subscriptions)); + } else { + self.room = None; + } + cx.notify(); } - cx.notify(); } pub fn room(&self) -> Option<&ModelHandle> { From 383c21046f1c1e924cae5db33650b0fbe5b0b24e Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 5 Oct 2022 14:27:59 +0200 Subject: [PATCH 052/112] Set room location when active workspace changes --- Cargo.lock | 1 + crates/collab_ui/src/collab_titlebar_item.rs | 41 +++++++++-- crates/theme/src/theme.rs | 1 + crates/workspace/Cargo.toml | 9 ++- crates/workspace/src/pane_group.rs | 74 ++++++++++++++++---- crates/workspace/src/workspace.rs | 9 ++- styles/src/styleTree/workspace.ts | 4 ++ 7 files changed, 119 insertions(+), 20 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0211f4d1e1e6fcd24a28cbb303d292b179833e47..2e700006731af2a19bf9989c9f3c8c63688992e9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7084,6 +7084,7 @@ name = "workspace" version = "0.1.0" dependencies = [ "anyhow", + "call", "client", "collections", "context_menu", diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index 8e4b8f9f322b48f441faffd91766f696d0f418a8..cea36548561c64d9aa7951ea2b8c9bb8ea40aea1 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -76,6 +76,10 @@ impl CollabTitlebarItem { let mut subscriptions = Vec::new(); subscriptions.push(cx.observe(workspace, |_, _, cx| cx.notify())); subscriptions.push(cx.observe(&active_call, |_, _, cx| cx.notify())); + subscriptions.push(cx.observe_window_activation(|this, active, cx| { + this.window_activation_changed(active, cx) + })); + Self { workspace: workspace.downgrade(), contacts_popover: None, @@ -83,14 +87,43 @@ impl CollabTitlebarItem { } } + fn window_activation_changed(&mut self, active: bool, cx: &mut ViewContext) { + let workspace = self.workspace.upgrade(cx); + let room = ActiveCall::global(cx).read(cx).room().cloned(); + if let Some((workspace, room)) = workspace.zip(room) { + let workspace = workspace.read(cx); + let project = if !active { + None + } else if workspace.project().read(cx).remote_id().is_some() { + Some(workspace.project().clone()) + } else { + None + }; + + room.update(cx, |room, cx| { + room.set_location(project.as_ref(), cx) + .detach_and_log_err(cx); + }); + } + } + fn share_project(&mut self, _: &ShareProject, cx: &mut ViewContext) { if let Some(workspace) = self.workspace.upgrade(cx) { - if let Some(room) = ActiveCall::global(cx).read(cx).room() { + if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() { + let window_id = cx.window_id(); let room_id = room.read(cx).id(); let project = workspace.read(cx).project().clone(); - project - .update(cx, |project, cx| project.share(room_id, cx)) - .detach_and_log_err(cx); + let share = project.update(cx, |project, cx| project.share(room_id, cx)); + cx.spawn_weak(|_, mut cx| async move { + share.await?; + if cx.update(|cx| cx.window_is_active(window_id)) { + room.update(&mut cx, |room, cx| { + room.set_location(Some(&project), cx).detach_and_log_err(cx); + }); + } + anyhow::Ok(()) + }) + .detach_and_log_err(cx); } } } diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 6253b6dabbc129442097fbbb63c7fd3607f9a8aa..bc7e6f09951257974290cc2ee3762b60995c62e3 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -58,6 +58,7 @@ pub struct Workspace { pub notifications: Notifications, pub joining_project_avatar: ImageStyle, pub joining_project_message: ContainedText, + pub external_location_message: ContainedText, pub dock: Dock, } diff --git a/crates/workspace/Cargo.toml b/crates/workspace/Cargo.toml index 759bff2cbd7c7e0a5e4184f89db28d72791f0c2c..2fd43b7bcb68267977e2152876a881dd3024be8e 100644 --- a/crates/workspace/Cargo.toml +++ b/crates/workspace/Cargo.toml @@ -8,9 +8,15 @@ path = "src/workspace.rs" doctest = false [features] -test-support = ["client/test-support", "project/test-support", "settings/test-support"] +test-support = [ + "call/test-support", + "client/test-support", + "project/test-support", + "settings/test-support" +] [dependencies] +call = { path = "../call" } client = { path = "../client" } collections = { path = "../collections" } context_menu = { path = "../context_menu" } @@ -32,6 +38,7 @@ serde_json = { version = "1.0", features = ["preserve_order"] } smallvec = { version = "1.6", features = ["union"] } [dev-dependencies] +call = { path = "../call", features = ["test-support"] } client = { path = "../client", features = ["test-support"] } gpui = { path = "../gpui", features = ["test-support"] } project = { path = "../project", features = ["test-support"] } diff --git a/crates/workspace/src/pane_group.rs b/crates/workspace/src/pane_group.rs index 94acf427e4fe0718c3cbe330d269af93a62567dc..fb04c4ead644f40731990e68e807f85c27fbe891 100644 --- a/crates/workspace/src/pane_group.rs +++ b/crates/workspace/src/pane_group.rs @@ -1,8 +1,9 @@ use crate::{FollowerStatesByLeader, Pane}; use anyhow::{anyhow, Result}; +use call::ActiveCall; use client::PeerId; use collections::HashMap; -use gpui::{elements::*, Axis, Border, ViewHandle}; +use gpui::{elements::*, AppContext, Axis, Border, ViewHandle}; use project::Collaborator; use serde::Deserialize; use theme::Theme; @@ -56,11 +57,14 @@ impl PaneGroup { pub(crate) fn render( &self, + project_id: Option, theme: &Theme, follower_states: &FollowerStatesByLeader, collaborators: &HashMap, + cx: &AppContext, ) -> ElementBox { - self.root.render(theme, follower_states, collaborators) + self.root + .render(project_id, theme, follower_states, collaborators, cx) } pub(crate) fn panes(&self) -> Vec<&ViewHandle> { @@ -100,13 +104,14 @@ impl Member { pub fn render( &self, + project_id: Option, theme: &Theme, follower_states: &FollowerStatesByLeader, collaborators: &HashMap, + cx: &AppContext, ) -> ElementBox { match self { Member::Pane(pane) => { - let mut border = Border::default(); let leader = follower_states .iter() .find_map(|(leader_id, follower_states)| { @@ -116,21 +121,61 @@ impl Member { None } }) - .and_then(|leader_id| collaborators.get(leader_id)); - if let Some(leader) = leader { - let leader_color = theme - .editor - .replica_selection_style(leader.replica_id) - .cursor; - border = Border::all(theme.workspace.leader_border_width, leader_color); + .and_then(|leader_id| { + let room = ActiveCall::global(cx).read(cx).room()?.read(cx); + let collaborator = collaborators.get(leader_id)?; + let participant = room.remote_participants().get(&leader_id)?; + Some((collaborator.replica_id, participant)) + }); + + if let Some((replica_id, leader)) = leader { + let view = match leader.location { + call::ParticipantLocation::Project { + project_id: leader_project_id, + } => { + if Some(leader_project_id) == project_id { + ChildView::new(pane).boxed() + } else { + Label::new( + format!( + "Follow {} on their currently active project", + leader.user.github_login, + ), + theme.workspace.external_location_message.text.clone(), + ) + .contained() + .with_style(theme.workspace.external_location_message.container) + .aligned() + .boxed() + } + } + call::ParticipantLocation::External => Label::new( + format!( + "{} is viewing a window outside of Zed", + leader.user.github_login + ), + theme.workspace.external_location_message.text.clone(), + ) + .contained() + .with_style(theme.workspace.external_location_message.container) + .aligned() + .boxed(), + }; + + let leader_color = theme.editor.replica_selection_style(replica_id).cursor; + let mut border = Border::all(theme.workspace.leader_border_width, leader_color); border .color .fade_out(1. - theme.workspace.leader_border_opacity); border.overlay = true; + Container::new(view).with_border(border).boxed() + } else { + ChildView::new(pane).boxed() } - ChildView::new(pane).contained().with_border(border).boxed() } - Member::Axis(axis) => axis.render(theme, follower_states, collaborators), + Member::Axis(axis) => { + axis.render(project_id, theme, follower_states, collaborators, cx) + } } } @@ -232,14 +277,17 @@ impl PaneAxis { fn render( &self, + project_id: Option, theme: &Theme, follower_state: &FollowerStatesByLeader, collaborators: &HashMap, + cx: &AppContext, ) -> ElementBox { let last_member_ix = self.members.len() - 1; Flex::new(self.axis) .with_children(self.members.iter().enumerate().map(|(ix, member)| { - let mut member = member.render(theme, follower_state, collaborators); + let mut member = + member.render(project_id, theme, follower_state, collaborators, cx); if ix < last_member_ix { let mut border = theme.workspace.pane_divider; border.left = false; diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 7aa93f47d965f57e64251ccaaeb1be7f3f08374d..5bec4b4c6dfd36f4f6088de7a5783522ab9c0e37 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -12,7 +12,8 @@ mod status_bar; mod toolbar; use anyhow::{anyhow, Context, Result}; -use client::{proto, Client, Contact, PeerId, Subscription, TypedEnvelope, UserStore}; +use call::ActiveCall; +use client::{proto, Client, Contact, PeerId, TypedEnvelope, UserStore}; use collections::{hash_map, HashMap, HashSet}; use dock::{DefaultItemFactory, Dock, ToggleDockButton}; use drag_and_drop::DragAndDrop; @@ -860,7 +861,7 @@ pub struct Workspace { weak_self: WeakViewHandle, client: Arc, user_store: ModelHandle, - remote_entity_subscription: Option, + remote_entity_subscription: Option, fs: Arc, modal: Option, center: PaneGroup, @@ -880,6 +881,7 @@ pub struct Workspace { last_leaders_by_pane: HashMap, PeerId>, window_edited: bool, _observe_current_user: Task<()>, + _active_call_observation: gpui::Subscription, } #[derive(Default)] @@ -1015,6 +1017,7 @@ impl Workspace { last_leaders_by_pane: Default::default(), window_edited: false, _observe_current_user, + _active_call_observation: cx.observe(&ActiveCall::global(cx), |_, _, cx| cx.notify()), }; this.project_remote_id_changed(this.project.read(cx).remote_id(), cx); cx.defer(|this, cx| this.update_window_title(cx)); @@ -2430,9 +2433,11 @@ impl View for Workspace { Flex::column() .with_child( FlexItem::new(self.center.render( + self.project.read(cx).remote_id(), &theme, &self.follower_states_by_leader, self.project.read(cx).collaborators(), + cx, )) .flex(1., true) .boxed(), diff --git a/styles/src/styleTree/workspace.ts b/styles/src/styleTree/workspace.ts index 40ed44e8dbc6e0bf634ec84b3fba67fe630d0ba8..0877f131c144a487cd6abe6d1b19dce321de54d7 100644 --- a/styles/src/styleTree/workspace.ts +++ b/styles/src/styleTree/workspace.ts @@ -48,6 +48,10 @@ export default function workspace(theme: Theme) { padding: 12, ...text(theme, "sans", "primary", { size: "lg" }), }, + externalLocationMessage: { + padding: 12, + ...text(theme, "sans", "primary", { size: "lg" }), + }, leaderBorderOpacity: 0.7, leaderBorderWidth: 2.0, tabBar: tabBar(theme), From 8f8843711ff80bb3afed3f29ed22eff41df4a790 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 5 Oct 2022 15:02:57 +0200 Subject: [PATCH 053/112] Move logic for joining project into a global action in `collab_ui` --- crates/collab_ui/src/collab_ui.rs | 79 ++++++++++++++++++- .../src/incoming_call_notification.rs | 51 +++++------- .../src/project_shared_notification.rs | 52 ++++-------- crates/gpui/src/app.rs | 4 + crates/workspace/src/workspace.rs | 6 +- 5 files changed, 115 insertions(+), 77 deletions(-) diff --git a/crates/collab_ui/src/collab_ui.rs b/crates/collab_ui/src/collab_ui.rs index 607c1b50543b886f52efa9dafb02801e38f7d44e..786d344df19c57bd520a434beac5957a45a871ad 100644 --- a/crates/collab_ui/src/collab_ui.rs +++ b/crates/collab_ui/src/collab_ui.rs @@ -3,14 +3,87 @@ mod contacts_popover; mod incoming_call_notification; mod project_shared_notification; +use call::ActiveCall; pub use collab_titlebar_item::CollabTitlebarItem; use gpui::MutableAppContext; +use project::Project; use std::sync::Arc; -use workspace::AppState; +use workspace::{AppState, JoinProject, ToggleFollow, Workspace}; pub fn init(app_state: Arc, cx: &mut MutableAppContext) { contacts_popover::init(cx); collab_titlebar_item::init(cx); - incoming_call_notification::init(app_state.clone(), cx); - project_shared_notification::init(app_state, cx); + incoming_call_notification::init(app_state.user_store.clone(), cx); + project_shared_notification::init(cx); + + cx.add_global_action(move |action: &JoinProject, cx| { + let project_id = action.project_id; + let follow_user_id = action.follow_user_id; + let app_state = app_state.clone(); + cx.spawn(|mut cx| async move { + let existing_workspace = cx.update(|cx| { + cx.window_ids() + .filter_map(|window_id| cx.root_view::(window_id)) + .find(|workspace| { + workspace.read(cx).project().read(cx).remote_id() == Some(project_id) + }) + }); + + let workspace = if let Some(existing_workspace) = existing_workspace { + existing_workspace + } else { + let project = Project::remote( + project_id, + app_state.client.clone(), + app_state.user_store.clone(), + app_state.project_store.clone(), + app_state.languages.clone(), + app_state.fs.clone(), + cx.clone(), + ) + .await?; + + let (_, workspace) = cx.add_window((app_state.build_window_options)(), |cx| { + let mut workspace = Workspace::new(project, app_state.default_item_factory, cx); + (app_state.initialize_workspace)(&mut workspace, &app_state, cx); + workspace + }); + workspace + }; + + cx.activate_window(workspace.window_id()); + + workspace.update(&mut 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(|(peer_id, _)| *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.replica_id == 0)?; + Some(collaborator.peer_id) + }); + + if let Some(follow_peer_id) = follow_peer_id { + if !workspace.is_following(follow_peer_id) { + workspace + .toggle_follow(&ToggleFollow(follow_peer_id), cx) + .map(|follow| follow.detach_and_log_err(cx)); + } + } + } + }); + + anyhow::Ok(()) + }) + .detach_and_log_err(cx); + }); } diff --git a/crates/collab_ui/src/incoming_call_notification.rs b/crates/collab_ui/src/incoming_call_notification.rs index 4630054c5e355bd41eb7dce4729376d5248ceeb7..e46e69522f2548e5683caf74a39ffb59f6c69207 100644 --- a/crates/collab_ui/src/incoming_call_notification.rs +++ b/crates/collab_ui/src/incoming_call_notification.rs @@ -1,25 +1,22 @@ -use std::sync::Arc; - use call::ActiveCall; -use client::incoming_call::IncomingCall; +use client::{incoming_call::IncomingCall, UserStore}; use futures::StreamExt; use gpui::{ elements::*, geometry::{rect::RectF, vector::vec2f}, - impl_internal_actions, Entity, MouseButton, MutableAppContext, RenderContext, View, - ViewContext, WindowBounds, WindowKind, WindowOptions, + impl_internal_actions, Entity, ModelHandle, MouseButton, MutableAppContext, RenderContext, + View, ViewContext, WindowBounds, WindowKind, WindowOptions, }; -use project::Project; use settings::Settings; use util::ResultExt; -use workspace::{AppState, Workspace}; +use workspace::JoinProject; impl_internal_actions!(incoming_call_notification, [RespondToCall]); -pub fn init(app_state: Arc, cx: &mut MutableAppContext) { +pub fn init(user_store: ModelHandle, cx: &mut MutableAppContext) { cx.add_action(IncomingCallNotification::respond_to_call); - let mut incoming_call = app_state.user_store.read(cx).incoming_call(); + let mut incoming_call = user_store.read(cx).incoming_call(); cx.spawn(|mut cx| async move { let mut notification_window = None; while let Some(incoming_call) = incoming_call.next().await { @@ -36,7 +33,7 @@ pub fn init(app_state: Arc, cx: &mut MutableAppContext) { kind: WindowKind::PopUp, is_movable: false, }, - |_| IncomingCallNotification::new(incoming_call, app_state.clone()), + |_| IncomingCallNotification::new(incoming_call, user_store.clone()), ); notification_window = Some(window_id); } @@ -52,47 +49,35 @@ struct RespondToCall { pub struct IncomingCallNotification { call: IncomingCall, - app_state: Arc, + user_store: ModelHandle, } impl IncomingCallNotification { - pub fn new(call: IncomingCall, app_state: Arc) -> Self { - Self { call, app_state } + pub fn new(call: IncomingCall, user_store: ModelHandle) -> Self { + Self { call, user_store } } fn respond_to_call(&mut self, action: &RespondToCall, cx: &mut ViewContext) { if action.accept { - let app_state = self.app_state.clone(); let join = ActiveCall::global(cx) .update(cx, |active_call, cx| active_call.join(&self.call, cx)); + let caller_user_id = self.call.caller.id; let initial_project_id = self.call.initial_project_id; cx.spawn_weak(|_, mut cx| async move { join.await?; - if let Some(initial_project_id) = initial_project_id { - let project = Project::remote( - initial_project_id, - app_state.client.clone(), - app_state.user_store.clone(), - app_state.project_store.clone(), - app_state.languages.clone(), - app_state.fs.clone(), - cx.clone(), - ) - .await?; - - cx.add_window((app_state.build_window_options)(), |cx| { - let mut workspace = - Workspace::new(project, app_state.default_item_factory, cx); - (app_state.initialize_workspace)(&mut workspace, &app_state, cx); - workspace + if let Some(project_id) = initial_project_id { + cx.update(|cx| { + cx.dispatch_global_action(JoinProject { + project_id, + follow_user_id: caller_user_id, + }) }); } anyhow::Ok(()) }) .detach_and_log_err(cx); } else { - self.app_state - .user_store + self.user_store .update(cx, |user_store, _| user_store.decline_call().log_err()); } diff --git a/crates/collab_ui/src/project_shared_notification.rs b/crates/collab_ui/src/project_shared_notification.rs index 53eb17684e9a0f53aecbf03efc201b9c5099768c..e0c196614450550d25ba9afc921f798b1be65031 100644 --- a/crates/collab_ui/src/project_shared_notification.rs +++ b/crates/collab_ui/src/project_shared_notification.rs @@ -7,14 +7,13 @@ use gpui::{ Entity, MouseButton, MutableAppContext, RenderContext, View, ViewContext, WindowBounds, WindowKind, WindowOptions, }; -use project::Project; use settings::Settings; use std::sync::Arc; -use workspace::{AppState, Workspace}; +use workspace::JoinProject; -actions!(project_shared_notification, [JoinProject, DismissProject]); +actions!(project_shared_notification, [DismissProject]); -pub fn init(app_state: Arc, cx: &mut MutableAppContext) { +pub fn init(cx: &mut MutableAppContext) { cx.add_action(ProjectSharedNotification::join); cx.add_action(ProjectSharedNotification::dismiss); @@ -29,7 +28,7 @@ pub fn init(app_state: Arc, cx: &mut MutableAppContext) { kind: WindowKind::PopUp, is_movable: false, }, - |_| ProjectSharedNotification::new(*project_id, owner.clone(), app_state.clone()), + |_| ProjectSharedNotification::new(*project_id, owner.clone()), ); } }) @@ -39,45 +38,17 @@ pub fn init(app_state: Arc, cx: &mut MutableAppContext) { pub struct ProjectSharedNotification { project_id: u64, owner: Arc, - app_state: Arc, } impl ProjectSharedNotification { - fn new(project_id: u64, owner: Arc, app_state: Arc) -> Self { - Self { - project_id, - owner, - app_state, - } + fn new(project_id: u64, owner: Arc) -> Self { + Self { project_id, owner } } fn join(&mut self, _: &JoinProject, cx: &mut ViewContext) { - let project_id = self.project_id; - let app_state = self.app_state.clone(); - cx.spawn_weak(|_, mut cx| async move { - let project = Project::remote( - project_id, - app_state.client.clone(), - app_state.user_store.clone(), - app_state.project_store.clone(), - app_state.languages.clone(), - app_state.fs.clone(), - cx.clone(), - ) - .await?; - - cx.add_window((app_state.build_window_options)(), |cx| { - let mut workspace = Workspace::new(project, app_state.default_item_factory, cx); - (app_state.initialize_workspace)(&mut workspace, &app_state, cx); - workspace - }); - - anyhow::Ok(()) - }) - .detach_and_log_err(cx); - let window_id = cx.window_id(); cx.remove_window(window_id); + cx.propagate_action(); } fn dismiss(&mut self, _: &DismissProject, cx: &mut ViewContext) { @@ -108,6 +79,8 @@ impl ProjectSharedNotification { enum Join {} enum Dismiss {} + let project_id = self.project_id; + let owner_user_id = self.owner.id; Flex::row() .with_child( MouseEventHandler::::new(0, cx, |_, cx| { @@ -117,8 +90,11 @@ impl ProjectSharedNotification { .with_style(theme.join_button.container) .boxed() }) - .on_click(MouseButton::Left, |_, cx| { - cx.dispatch_action(JoinProject); + .on_click(MouseButton::Left, move |_, cx| { + cx.dispatch_action(JoinProject { + project_id, + follow_user_id: owner_user_id, + }); }) .boxed(), ) diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 04e27a8279836a2bc4c7b4ea89fae6bf18d9dc48..002bd01377dd173a07b47850d6c16df045e8955f 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -790,6 +790,10 @@ impl AsyncAppContext { self.update(|cx| cx.remove_window(window_id)) } + pub fn activate_window(&mut self, window_id: usize) { + self.update(|cx| cx.activate_window(window_id)) + } + pub fn platform(&self) -> Arc { self.0.borrow().platform() } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 5bec4b4c6dfd36f4f6088de7a5783522ab9c0e37..1509c52887959e38caa8fdf71489c5c3aaf0e97b 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -13,7 +13,7 @@ mod toolbar; use anyhow::{anyhow, Context, Result}; use call::ActiveCall; -use client::{proto, Client, Contact, PeerId, TypedEnvelope, UserStore}; +use client::{proto, Client, PeerId, TypedEnvelope, UserStore}; use collections::{hash_map, HashMap, HashSet}; use dock::{DefaultItemFactory, Dock, ToggleDockButton}; use drag_and_drop::DragAndDrop; @@ -116,8 +116,8 @@ pub struct ToggleFollow(pub PeerId); #[derive(Clone, PartialEq)] pub struct JoinProject { - pub contact: Arc, - pub project_index: usize, + pub project_id: u64, + pub follow_user_id: u64, } impl_internal_actions!( From 183ca5da6f0931b05b52f213affc7e4221ca9e4e Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 5 Oct 2022 15:32:55 +0200 Subject: [PATCH 054/112] Allow following users into external projects --- crates/workspace/src/pane_group.rs | 75 ++++++++++++++++++------------ crates/workspace/src/workspace.rs | 4 +- 2 files changed, 46 insertions(+), 33 deletions(-) diff --git a/crates/workspace/src/pane_group.rs b/crates/workspace/src/pane_group.rs index fb04c4ead644f40731990e68e807f85c27fbe891..9fd27f78b5a4517f74091a52e1875eaecdbe2f80 100644 --- a/crates/workspace/src/pane_group.rs +++ b/crates/workspace/src/pane_group.rs @@ -1,10 +1,10 @@ -use crate::{FollowerStatesByLeader, Pane}; +use crate::{FollowerStatesByLeader, JoinProject, Pane, Workspace}; use anyhow::{anyhow, Result}; use call::ActiveCall; -use client::PeerId; -use collections::HashMap; -use gpui::{elements::*, AppContext, Axis, Border, ViewHandle}; -use project::Collaborator; +use gpui::{ + elements::*, Axis, Border, CursorStyle, ModelHandle, MouseButton, RenderContext, ViewHandle, +}; +use project::Project; use serde::Deserialize; use theme::Theme; @@ -57,14 +57,12 @@ impl PaneGroup { pub(crate) fn render( &self, - project_id: Option, + project: &ModelHandle, theme: &Theme, follower_states: &FollowerStatesByLeader, - collaborators: &HashMap, - cx: &AppContext, + cx: &mut RenderContext, ) -> ElementBox { - self.root - .render(project_id, theme, follower_states, collaborators, cx) + self.root.render(project, theme, follower_states, cx) } pub(crate) fn panes(&self) -> Vec<&ViewHandle> { @@ -104,12 +102,13 @@ impl Member { pub fn render( &self, - project_id: Option, + project: &ModelHandle, theme: &Theme, follower_states: &FollowerStatesByLeader, - collaborators: &HashMap, - cx: &AppContext, + cx: &mut RenderContext, ) -> ElementBox { + enum FollowIntoExternalProject {} + match self { Member::Pane(pane) => { let leader = follower_states @@ -123,7 +122,7 @@ impl Member { }) .and_then(|leader_id| { let room = ActiveCall::global(cx).read(cx).room()?.read(cx); - let collaborator = collaborators.get(leader_id)?; + let collaborator = project.read(cx).collaborators().get(leader_id)?; let participant = room.remote_participants().get(&leader_id)?; Some((collaborator.replica_id, participant)) }); @@ -133,18 +132,36 @@ impl Member { call::ParticipantLocation::Project { project_id: leader_project_id, } => { - if Some(leader_project_id) == project_id { + if Some(leader_project_id) == project.read(cx).remote_id() { ChildView::new(pane).boxed() } else { - Label::new( - format!( - "Follow {} on their currently active project", - leader.user.github_login, - ), - theme.workspace.external_location_message.text.clone(), + let leader_user = leader.user.clone(); + let leader_user_id = leader.user.id; + MouseEventHandler::::new( + pane.id(), + cx, + |_, _| { + Label::new( + format!( + "Follow {} on their currently active project", + leader_user.github_login, + ), + theme.workspace.external_location_message.text.clone(), + ) + .contained() + .with_style( + theme.workspace.external_location_message.container, + ) + .boxed() + }, ) - .contained() - .with_style(theme.workspace.external_location_message.container) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, move |_, cx| { + cx.dispatch_action(JoinProject { + project_id: leader_project_id, + follow_user_id: leader_user_id, + }) + }) .aligned() .boxed() } @@ -173,9 +190,7 @@ impl Member { ChildView::new(pane).boxed() } } - Member::Axis(axis) => { - axis.render(project_id, theme, follower_states, collaborators, cx) - } + Member::Axis(axis) => axis.render(project, theme, follower_states, cx), } } @@ -277,17 +292,15 @@ impl PaneAxis { fn render( &self, - project_id: Option, + project: &ModelHandle, theme: &Theme, follower_state: &FollowerStatesByLeader, - collaborators: &HashMap, - cx: &AppContext, + cx: &mut RenderContext, ) -> ElementBox { let last_member_ix = self.members.len() - 1; Flex::new(self.axis) .with_children(self.members.iter().enumerate().map(|(ix, member)| { - let mut member = - member.render(project_id, theme, follower_state, collaborators, cx); + let mut member = member.render(project, theme, follower_state, cx); if ix < last_member_ix { let mut border = theme.workspace.pane_divider; border.left = false; diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 1509c52887959e38caa8fdf71489c5c3aaf0e97b..00b237d1c4ad707d66a6c2ef57a2116cf0fc299c 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -2416,6 +2416,7 @@ impl View for Workspace { .with_child( Stack::new() .with_child({ + let project = self.project.clone(); Flex::row() .with_children( if self.left_sidebar.read(cx).active_item().is_some() { @@ -2433,10 +2434,9 @@ impl View for Workspace { Flex::column() .with_child( FlexItem::new(self.center.render( - self.project.read(cx).remote_id(), + &project, &theme, &self.follower_states_by_leader, - self.project.read(cx).collaborators(), cx, )) .flex(1., true) From 5b811e4304ae8824c2ace2511e6f02a17c492b40 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 5 Oct 2022 16:14:40 +0200 Subject: [PATCH 055/112] Add integration test verifying calls to busy users --- crates/collab/src/integration_tests.rs | 84 ++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/crates/collab/src/integration_tests.rs b/crates/collab/src/integration_tests.rs index 92f94b662150e1a3bb2ea100fccb08266dc4278b..7ab7126613357e30415648dc8b19f4fc0ad56ee6 100644 --- a/crates/collab/src/integration_tests.rs +++ b/crates/collab/src/integration_tests.rs @@ -214,6 +214,90 @@ async fn test_basic_calls( ); } +#[gpui::test(iterations = 10)] +async fn test_calling_busy_user( + deterministic: Arc, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, + cx_c: &mut TestAppContext, +) { + deterministic.forbid_parking(); + let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + let client_c = server.create_client(cx_c, "user_c").await; + server + .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)]) + .await; + + // Call user B from client A. + let room_a = cx_a + .update(|cx| Room::create(client_a.clone(), client_a.user_store.clone(), cx)) + .await + .unwrap(); + + let mut incoming_call_b = client_b + .user_store + .update(cx_b, |user, _| user.incoming_call()); + room_a + .update(cx_a, |room, cx| { + room.call(client_b.user_id().unwrap(), None, cx) + }) + .await + .unwrap(); + let call_b1 = incoming_call_b.next().await.unwrap().unwrap(); + assert_eq!(call_b1.caller.github_login, "user_a"); + + // Ensure calling users A and B from client C fails. + let room_c = cx_c + .update(|cx| Room::create(client_c.clone(), client_c.user_store.clone(), cx)) + .await + .unwrap(); + room_c + .update(cx_c, |room, cx| { + room.call(client_a.user_id().unwrap(), None, cx) + }) + .await + .unwrap_err(); + room_c + .update(cx_c, |room, cx| { + room.call(client_b.user_id().unwrap(), None, cx) + }) + .await + .unwrap_err(); + + // User B joins the room and calling them after they've joined still fails. + let room_b = cx_b + .update(|cx| { + Room::join( + &call_b1, + client_b.client.clone(), + client_b.user_store.clone(), + cx, + ) + }) + .await + .unwrap(); + room_c + .update(cx_c, |room, cx| { + room.call(client_b.user_id().unwrap(), None, cx) + }) + .await + .unwrap_err(); + + // Client C can successfully call client B after client B leaves the room. + cx_b.update(|_| drop(room_b)); + deterministic.run_until_parked(); + room_c + .update(cx_c, |room, cx| { + room.call(client_b.user_id().unwrap(), None, cx) + }) + .await + .unwrap(); + let call_b2 = incoming_call_b.next().await.unwrap().unwrap(); + assert_eq!(call_b2.caller.github_login, "user_c"); +} + #[gpui::test(iterations = 10)] async fn test_leaving_room_on_disconnection( deterministic: Arc, From 5ef342f8c442b19f7dc9205c98faef9494f08721 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 5 Oct 2022 16:20:01 +0200 Subject: [PATCH 056/112] Enhance integration test to verify creating rooms while busy --- crates/collab/src/integration_tests.rs | 28 ++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/crates/collab/src/integration_tests.rs b/crates/collab/src/integration_tests.rs index 7ab7126613357e30415648dc8b19f4fc0ad56ee6..79d167d0132ec8f43819effc3b56d7d192852588 100644 --- a/crates/collab/src/integration_tests.rs +++ b/crates/collab/src/integration_tests.rs @@ -215,7 +215,7 @@ async fn test_basic_calls( } #[gpui::test(iterations = 10)] -async fn test_calling_busy_user( +async fn test_room_uniqueness( deterministic: Arc, cx_a: &mut TestAppContext, cx_b: &mut TestAppContext, @@ -230,12 +230,16 @@ async fn test_calling_busy_user( .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)]) .await; - // Call user B from client A. let room_a = cx_a .update(|cx| Room::create(client_a.clone(), client_a.user_store.clone(), cx)) .await .unwrap(); + // Ensure room can't be created given we've just created one. + cx_a.update(|cx| Room::create(client_a.clone(), client_a.user_store.clone(), cx)) + .await + .unwrap_err(); + // Call user B from client A. let mut incoming_call_b = client_b .user_store .update(cx_b, |user, _| user.incoming_call()); @@ -266,6 +270,11 @@ async fn test_calling_busy_user( .await .unwrap_err(); + // Ensure User B can't create a room while they still have an incoming call. + cx_b.update(|cx| Room::create(client_b.clone(), client_b.user_store.clone(), cx)) + .await + .unwrap_err(); + // User B joins the room and calling them after they've joined still fails. let room_b = cx_b .update(|cx| { @@ -285,6 +294,11 @@ async fn test_calling_busy_user( .await .unwrap_err(); + // Ensure User B can't create a room while they belong to another room. + cx_b.update(|cx| Room::create(client_b.clone(), client_b.user_store.clone(), cx)) + .await + .unwrap_err(); + // Client C can successfully call client B after client B leaves the room. cx_b.update(|_| drop(room_b)); deterministic.run_until_parked(); @@ -296,6 +310,16 @@ async fn test_calling_busy_user( .unwrap(); let call_b2 = incoming_call_b.next().await.unwrap().unwrap(); assert_eq!(call_b2.caller.github_login, "user_c"); + + // Client B can successfully create a room after declining the call from client C. + client_b + .user_store + .update(cx_b, |user_store, _| user_store.decline_call()) + .unwrap(); + deterministic.run_until_parked(); + cx_b.update(|cx| Room::create(client_b.clone(), client_b.user_store.clone(), cx)) + .await + .unwrap(); } #[gpui::test(iterations = 10)] From fa31c9659bffd6beb8a8d3875d3a3e034dac2095 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 5 Oct 2022 16:29:22 +0200 Subject: [PATCH 057/112] Check room invariants in `Store::check_invariants` --- crates/collab/src/rpc/store.rs | 52 ++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/crates/collab/src/rpc/store.rs b/crates/collab/src/rpc/store.rs index da9f242ac5110bf4c5017c3b646849e0f9cc00a6..2f95851bf7aa9bd05f477e127ed1a2ff7200492d 100644 --- a/crates/collab/src/rpc/store.rs +++ b/crates/collab/src/rpc/store.rs @@ -1030,6 +1030,45 @@ impl Store { *user_id ); } + + if let Some(active_call) = state.active_call.as_ref() { + if let Some(active_call_connection_id) = active_call.connection_id { + assert!( + state.connection_ids.contains(&active_call_connection_id), + "call is active on a dead connection" + ); + assert!( + state.connection_ids.contains(&active_call_connection_id), + "call is active on a dead connection" + ); + } + } + } + + for (room_id, room) in &self.rooms { + for pending_user_id in &room.pending_user_ids { + assert!( + self.connected_users + .contains_key(&UserId::from_proto(*pending_user_id)), + "call is active on a user that has disconnected" + ); + } + + for participant in &room.participants { + assert!( + self.connections + .contains_key(&ConnectionId(participant.peer_id)), + "room contains participant that has disconnected" + ); + + for project_id in &participant.project_ids { + let project = &self.projects[&ProjectId::from_proto(*project_id)]; + assert_eq!( + project.room_id, *room_id, + "project was shared on a different room" + ); + } + } } for (project_id, project) in &self.projects { @@ -1049,6 +1088,19 @@ impl Store { .map(|guest| guest.replica_id) .collect::>(), ); + + let room = &self.rooms[&project.room_id]; + let room_participant = room + .participants + .iter() + .find(|participant| participant.peer_id == project.host_connection_id.0) + .unwrap(); + assert!( + room_participant + .project_ids + .contains(&project_id.to_proto()), + "project was not shared in room" + ); } for (channel_id, channel) in &self.channels { From 55cc142319a6c07997e33eccf4b3dd6887772288 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 6 Oct 2022 09:50:26 +0200 Subject: [PATCH 058/112] Move incoming calls into `ActiveCall` --- Cargo.lock | 1 + crates/call/Cargo.toml | 1 + crates/call/src/call.rs | 93 ++- crates/call/src/room.rs | 8 +- crates/client/src/user.rs | 58 -- crates/collab/src/integration_tests.rs | 529 ++++++++---------- crates/collab_ui/src/collab_ui.rs | 2 +- .../src/incoming_call_notification.rs | 29 +- crates/gpui/src/app.rs | 4 + crates/gpui/src/test.rs | 2 +- crates/gpui_macros/src/gpui_macros.rs | 2 +- 11 files changed, 359 insertions(+), 370 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2e700006731af2a19bf9989c9f3c8c63688992e9..a971570e2f33a7a3f78f1e4193478ea3f0c476fc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -693,6 +693,7 @@ dependencies = [ "collections", "futures", "gpui", + "postage", "project", "util", ] diff --git a/crates/call/Cargo.toml b/crates/call/Cargo.toml index cf5e7d6702152b3c1eb646e637f108fcf54e251a..e725c7cfe3b053d36f1b04b81d1d5476e68e7bed 100644 --- a/crates/call/Cargo.toml +++ b/crates/call/Cargo.toml @@ -25,6 +25,7 @@ util = { path = "../util" } anyhow = "1.0.38" futures = "0.3" +postage = { version = "0.4.1", features = ["futures-traits"] } [dev-dependencies] client = { path = "../client", features = ["test-support"] } diff --git a/crates/call/src/call.rs b/crates/call/src/call.rs index 2f64115fb5c01cda6cb9e0b09a5b239270fe4d41..607931fdc4710ee40aca8931a5078c7574227d59 100644 --- a/crates/call/src/call.rs +++ b/crates/call/src/call.rs @@ -2,22 +2,31 @@ mod participant; pub mod room; use anyhow::{anyhow, Result}; -use client::{incoming_call::IncomingCall, Client, UserStore}; -use gpui::{AppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Subscription, Task}; +use client::{incoming_call::IncomingCall, proto, Client, TypedEnvelope, UserStore}; +use gpui::{ + AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, + Subscription, Task, +}; pub use participant::ParticipantLocation; +use postage::watch; use project::Project; pub use room::Room; use std::sync::Arc; pub fn init(client: Arc, user_store: ModelHandle, cx: &mut MutableAppContext) { - let active_call = cx.add_model(|_| ActiveCall::new(client, user_store)); + let active_call = cx.add_model(|cx| ActiveCall::new(client, user_store, cx)); cx.set_global(active_call); } pub struct ActiveCall { room: Option<(ModelHandle, Vec)>, + incoming_call: ( + watch::Sender>, + watch::Receiver>, + ), client: Arc, user_store: ModelHandle, + _subscriptions: Vec, } impl Entity for ActiveCall { @@ -25,14 +34,63 @@ impl Entity for ActiveCall { } impl ActiveCall { - fn new(client: Arc, user_store: ModelHandle) -> Self { + fn new( + client: Arc, + user_store: ModelHandle, + cx: &mut ModelContext, + ) -> Self { Self { room: None, + incoming_call: watch::channel(), + _subscriptions: vec![ + client.add_request_handler(cx.handle(), Self::handle_incoming_call), + client.add_message_handler(cx.handle(), Self::handle_cancel_call), + ], client, user_store, } } + async fn handle_incoming_call( + this: ModelHandle, + envelope: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result { + let user_store = this.read_with(&cx, |this, _| this.user_store.clone()); + let call = IncomingCall { + room_id: envelope.payload.room_id, + participants: user_store + .update(&mut cx, |user_store, cx| { + user_store.get_users(envelope.payload.participant_user_ids, cx) + }) + .await?, + caller: user_store + .update(&mut cx, |user_store, cx| { + user_store.get_user(envelope.payload.caller_user_id, cx) + }) + .await?, + initial_project_id: envelope.payload.initial_project_id, + }; + this.update(&mut cx, |this, _| { + *this.incoming_call.0.borrow_mut() = Some(call); + }); + + Ok(proto::Ack {}) + } + + async fn handle_cancel_call( + this: ModelHandle, + _: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result<()> { + this.update(&mut cx, |this, _| { + *this.incoming_call.0.borrow_mut() = None; + }); + Ok(()) + } + pub fn global(cx: &AppContext) -> ModelHandle { cx.global::>().clone() } @@ -74,12 +132,22 @@ impl ActiveCall { }) } - pub fn join(&mut self, call: &IncomingCall, cx: &mut ModelContext) -> Task> { + pub fn incoming(&self) -> watch::Receiver> { + self.incoming_call.1.clone() + } + + pub fn accept_incoming(&mut self, cx: &mut ModelContext) -> Task> { if self.room.is_some() { return Task::ready(Err(anyhow!("cannot join while on another call"))); } - let join = Room::join(call, self.client.clone(), self.user_store.clone(), cx); + let call = if let Some(call) = self.incoming_call.1.borrow().clone() { + call + } else { + return Task::ready(Err(anyhow!("no incoming call"))); + }; + + let join = Room::join(&call, self.client.clone(), self.user_store.clone(), cx); cx.spawn(|this, mut cx| async move { let room = join.await?; this.update(&mut cx, |this, cx| this.set_room(Some(room.clone()), cx)); @@ -87,6 +155,19 @@ impl ActiveCall { }) } + pub fn decline_incoming(&mut self) -> Result<()> { + *self.incoming_call.0.borrow_mut() = None; + self.client.send(proto::DeclineCall {})?; + Ok(()) + } + + pub fn hang_up(&mut self, cx: &mut ModelContext) -> Result<()> { + if let Some((room, _)) = self.room.take() { + room.update(cx, |room, cx| room.leave(cx))?; + } + Ok(()) + } + fn set_room(&mut self, room: Option>, cx: &mut ModelContext) { if room.as_ref() != self.room.as_ref().map(|room| &room.0) { if let Some(room) = room { diff --git a/crates/call/src/room.rs b/crates/call/src/room.rs index 0237972167726bf392620bbf2d424828fb484f02..52f283dd03ecbef82e5cc90a7c900e88aac87272 100644 --- a/crates/call/src/room.rs +++ b/crates/call/src/room.rs @@ -66,7 +66,7 @@ impl Room { } } - pub fn create( + pub(crate) fn create( client: Arc, user_store: ModelHandle, cx: &mut MutableAppContext, @@ -77,7 +77,7 @@ impl Room { }) } - pub fn join( + pub(crate) fn join( call: &IncomingCall, client: Arc, user_store: ModelHandle, @@ -93,7 +93,7 @@ impl Room { }) } - pub fn leave(&mut self, cx: &mut ModelContext) -> Result<()> { + pub(crate) fn leave(&mut self, cx: &mut ModelContext) -> Result<()> { if self.status.is_offline() { return Err(anyhow!("room is offline")); } @@ -213,7 +213,7 @@ impl Room { Ok(()) } - pub fn call( + pub(crate) fn call( &mut self, recipient_user_id: u64, initial_project_id: Option, diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index 2d79b7be84592b9a91932c2dbe6545439f6c4b56..252fb4d455a270c6cd161ce6202bf6af337a1f0e 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -1,5 +1,4 @@ use super::{http::HttpClient, proto, Client, Status, TypedEnvelope}; -use crate::incoming_call::IncomingCall; use anyhow::{anyhow, Context, Result}; use collections::{hash_map::Entry, HashMap, HashSet}; use futures::{channel::mpsc, future, AsyncReadExt, Future, StreamExt}; @@ -59,10 +58,6 @@ pub struct UserStore { outgoing_contact_requests: Vec>, pending_contact_requests: HashMap, invite_info: Option, - incoming_call: ( - watch::Sender>, - watch::Receiver>, - ), client: Weak, http: Arc, _maintain_contacts: Task<()>, @@ -112,8 +107,6 @@ impl UserStore { client.add_message_handler(cx.handle(), Self::handle_update_contacts), client.add_message_handler(cx.handle(), Self::handle_update_invite_info), client.add_message_handler(cx.handle(), Self::handle_show_contacts), - client.add_request_handler(cx.handle(), Self::handle_incoming_call), - client.add_message_handler(cx.handle(), Self::handle_cancel_call), ]; Self { users: Default::default(), @@ -122,7 +115,6 @@ impl UserStore { incoming_contact_requests: Default::default(), outgoing_contact_requests: Default::default(), invite_info: None, - incoming_call: watch::channel(), client: Arc::downgrade(&client), update_contacts_tx, http, @@ -194,60 +186,10 @@ impl UserStore { Ok(()) } - async fn handle_incoming_call( - this: ModelHandle, - envelope: TypedEnvelope, - _: Arc, - mut cx: AsyncAppContext, - ) -> Result { - let call = IncomingCall { - room_id: envelope.payload.room_id, - participants: this - .update(&mut cx, |this, cx| { - this.get_users(envelope.payload.participant_user_ids, cx) - }) - .await?, - caller: this - .update(&mut cx, |this, cx| { - this.get_user(envelope.payload.caller_user_id, cx) - }) - .await?, - initial_project_id: envelope.payload.initial_project_id, - }; - this.update(&mut cx, |this, _| { - *this.incoming_call.0.borrow_mut() = Some(call); - }); - - Ok(proto::Ack {}) - } - - async fn handle_cancel_call( - this: ModelHandle, - _: TypedEnvelope, - _: Arc, - mut cx: AsyncAppContext, - ) -> Result<()> { - this.update(&mut cx, |this, _| { - *this.incoming_call.0.borrow_mut() = None; - }); - Ok(()) - } - pub fn invite_info(&self) -> Option<&InviteInfo> { self.invite_info.as_ref() } - pub fn incoming_call(&self) -> watch::Receiver> { - self.incoming_call.1.clone() - } - - pub fn decline_call(&mut self) -> Result<()> { - if let Some(client) = self.client.upgrade() { - client.send(proto::DeclineCall {})?; - } - Ok(()) - } - async fn handle_update_contacts( this: ModelHandle, message: TypedEnvelope, diff --git a/crates/collab/src/integration_tests.rs b/crates/collab/src/integration_tests.rs index 79d167d0132ec8f43819effc3b56d7d192852588..1adac8b28e7756393c4de321f38c64f84935e397 100644 --- a/crates/collab/src/integration_tests.rs +++ b/crates/collab/src/integration_tests.rs @@ -5,7 +5,7 @@ use crate::{ }; use ::rpc::Peer; use anyhow::anyhow; -use call::{room, ParticipantLocation, Room}; +use call::{room, ActiveCall, ParticipantLocation, Room}; use client::{ self, test::FakeHttpClient, Channel, ChannelDetails, ChannelList, Client, Connection, Credentials, EstablishConnectionError, User, UserStore, RECEIVE_TIMEOUT, @@ -78,29 +78,18 @@ async fn test_basic_calls( .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)]) .await; - let room_a = cx_a - .update(|cx| Room::create(client_a.clone(), client_a.user_store.clone(), cx)) - .await - .unwrap(); - assert_eq!( - room_participants(&room_a, cx_a), - RoomParticipants { - remote: Default::default(), - pending: Default::default() - } - ); + let active_call_a = cx_a.read(ActiveCall::global); + let active_call_b = cx_b.read(ActiveCall::global); + let active_call_c = cx_c.read(ActiveCall::global); // Call user B from client A. - let mut incoming_call_b = client_b - .user_store - .update(cx_b, |user, _| user.incoming_call()); - room_a - .update(cx_a, |room, cx| { - room.call(client_b.user_id().unwrap(), None, cx) + active_call_a + .update(cx_a, |call, cx| { + call.invite(client_b.user_id().unwrap(), None, cx) }) .await .unwrap(); - + let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone()); deterministic.run_until_parked(); assert_eq!( room_participants(&room_a, cx_a), @@ -111,21 +100,24 @@ async fn test_basic_calls( ); // User B receives the call. + let mut incoming_call_b = active_call_b.read_with(cx_b, |call, _| call.incoming()); let call_b = incoming_call_b.next().await.unwrap().unwrap(); + assert_eq!(call_b.caller.github_login, "user_a"); // User B connects via another client and also receives a ring on the newly-connected client. - let client_b2 = server.create_client(cx_b2, "user_b").await; - let mut incoming_call_b2 = client_b2 - .user_store - .update(cx_b2, |user, _| user.incoming_call()); + let _client_b2 = server.create_client(cx_b2, "user_b").await; + let active_call_b2 = cx_b2.read(ActiveCall::global); + let mut incoming_call_b2 = active_call_b2.read_with(cx_b2, |call, _| call.incoming()); deterministic.run_until_parked(); - let _call_b2 = incoming_call_b2.next().await.unwrap().unwrap(); + let call_b2 = incoming_call_b2.next().await.unwrap().unwrap(); + assert_eq!(call_b2.caller.github_login, "user_a"); // User B joins the room using the first client. - let room_b = cx_b - .update(|cx| Room::join(&call_b, client_b.clone(), client_b.user_store.clone(), cx)) + active_call_b + .update(cx_b, |call, cx| call.accept_incoming(cx)) .await .unwrap(); + let room_b = active_call_b.read_with(cx_b, |call, _| call.room().unwrap().clone()); assert!(incoming_call_b.next().await.unwrap().is_none()); deterministic.run_until_parked(); @@ -145,12 +137,10 @@ async fn test_basic_calls( ); // Call user C from client B. - let mut incoming_call_c = client_c - .user_store - .update(cx_c, |user, _| user.incoming_call()); - room_b - .update(cx_b, |room, cx| { - room.call(client_c.user_id().unwrap(), None, cx) + let mut incoming_call_c = active_call_c.read_with(cx_c, |call, _| call.incoming()); + active_call_b + .update(cx_b, |call, cx| { + call.invite(client_c.user_id().unwrap(), None, cx) }) .await .unwrap(); @@ -172,11 +162,9 @@ async fn test_basic_calls( ); // User C receives the call, but declines it. - let _call_c = incoming_call_c.next().await.unwrap().unwrap(); - client_c - .user_store - .update(cx_c, |user, _| user.decline_call()) - .unwrap(); + let call_c = incoming_call_c.next().await.unwrap().unwrap(); + assert_eq!(call_c.caller.github_login, "user_b"); + active_call_c.update(cx_c, |call, _| call.decline_incoming().unwrap()); assert!(incoming_call_c.next().await.unwrap().is_none()); deterministic.run_until_parked(); @@ -196,7 +184,10 @@ async fn test_basic_calls( ); // User A leaves the room. - room_a.update(cx_a, |room, cx| room.leave(cx)).unwrap(); + active_call_a.update(cx_a, |call, cx| { + call.hang_up(cx).unwrap(); + assert!(call.room().is_none()); + }); deterministic.run_until_parked(); assert_eq!( room_participants(&room_a, cx_a), @@ -218,108 +209,107 @@ async fn test_basic_calls( async fn test_room_uniqueness( deterministic: Arc, cx_a: &mut TestAppContext, + cx_a2: &mut TestAppContext, cx_b: &mut TestAppContext, + cx_b2: &mut TestAppContext, cx_c: &mut TestAppContext, ) { deterministic.forbid_parking(); let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; + let _client_a2 = server.create_client(cx_a2, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; + let _client_b2 = server.create_client(cx_b2, "user_b").await; let client_c = server.create_client(cx_c, "user_c").await; server .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)]) .await; - let room_a = cx_a - .update(|cx| Room::create(client_a.clone(), client_a.user_store.clone(), cx)) - .await - .unwrap(); - // Ensure room can't be created given we've just created one. - cx_a.update(|cx| Room::create(client_a.clone(), client_a.user_store.clone(), cx)) - .await - .unwrap_err(); + let active_call_a = cx_a.read(ActiveCall::global); + let active_call_a2 = cx_a2.read(ActiveCall::global); + let active_call_b = cx_b.read(ActiveCall::global); + let active_call_b2 = cx_b2.read(ActiveCall::global); + let active_call_c = cx_c.read(ActiveCall::global); // Call user B from client A. - let mut incoming_call_b = client_b - .user_store - .update(cx_b, |user, _| user.incoming_call()); - room_a - .update(cx_a, |room, cx| { - room.call(client_b.user_id().unwrap(), None, cx) + active_call_a + .update(cx_a, |call, cx| { + call.invite(client_b.user_id().unwrap(), None, cx) }) .await .unwrap(); + + // Ensure a new room can't be created given user A just created one. + active_call_a2 + .update(cx_a2, |call, cx| { + call.invite(client_c.user_id().unwrap(), None, cx) + }) + .await + .unwrap_err(); + active_call_a2.read_with(cx_a2, |call, _| assert!(call.room().is_none())); + + // User B receives the call from user A. + let mut incoming_call_b = active_call_b.read_with(cx_b, |call, _| call.incoming()); let call_b1 = incoming_call_b.next().await.unwrap().unwrap(); assert_eq!(call_b1.caller.github_login, "user_a"); // Ensure calling users A and B from client C fails. - let room_c = cx_c - .update(|cx| Room::create(client_c.clone(), client_c.user_store.clone(), cx)) - .await - .unwrap(); - room_c - .update(cx_c, |room, cx| { - room.call(client_a.user_id().unwrap(), None, cx) + active_call_c + .update(cx_c, |call, cx| { + call.invite(client_a.user_id().unwrap(), None, cx) }) .await .unwrap_err(); - room_c - .update(cx_c, |room, cx| { - room.call(client_b.user_id().unwrap(), None, cx) + active_call_c + .update(cx_c, |call, cx| { + call.invite(client_b.user_id().unwrap(), None, cx) }) .await .unwrap_err(); // Ensure User B can't create a room while they still have an incoming call. - cx_b.update(|cx| Room::create(client_b.clone(), client_b.user_store.clone(), cx)) + active_call_b2 + .update(cx_b2, |call, cx| { + call.invite(client_c.user_id().unwrap(), None, cx) + }) .await .unwrap_err(); + active_call_b2.read_with(cx_b2, |call, _| assert!(call.room().is_none())); // User B joins the room and calling them after they've joined still fails. - let room_b = cx_b - .update(|cx| { - Room::join( - &call_b1, - client_b.client.clone(), - client_b.user_store.clone(), - cx, - ) - }) + active_call_b + .update(cx_b, |call, cx| call.accept_incoming(cx)) .await .unwrap(); - room_c - .update(cx_c, |room, cx| { - room.call(client_b.user_id().unwrap(), None, cx) + active_call_c + .update(cx_c, |call, cx| { + call.invite(client_b.user_id().unwrap(), None, cx) }) .await .unwrap_err(); // Ensure User B can't create a room while they belong to another room. - cx_b.update(|cx| Room::create(client_b.clone(), client_b.user_store.clone(), cx)) + active_call_b2 + .update(cx_b2, |call, cx| { + call.invite(client_c.user_id().unwrap(), None, cx) + }) .await .unwrap_err(); + active_call_b2.read_with(cx_b2, |call, _| assert!(call.room().is_none())); // Client C can successfully call client B after client B leaves the room. - cx_b.update(|_| drop(room_b)); + active_call_b + .update(cx_b, |call, cx| call.hang_up(cx)) + .unwrap(); deterministic.run_until_parked(); - room_c - .update(cx_c, |room, cx| { - room.call(client_b.user_id().unwrap(), None, cx) + active_call_c + .update(cx_c, |call, cx| { + call.invite(client_b.user_id().unwrap(), None, cx) }) .await .unwrap(); let call_b2 = incoming_call_b.next().await.unwrap().unwrap(); assert_eq!(call_b2.caller.github_login, "user_c"); - - // Client B can successfully create a room after declining the call from client C. - client_b - .user_store - .update(cx_b, |user_store, _| user_store.decline_call()) - .unwrap(); - deterministic.run_until_parked(); - cx_b.update(|cx| Room::create(client_b.clone(), client_b.user_store.clone(), cx)) - .await - .unwrap(); } #[gpui::test(iterations = 10)] @@ -336,28 +326,26 @@ async fn test_leaving_room_on_disconnection( .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b)]) .await; - let room_a = cx_a - .update(|cx| Room::create(client_a.clone(), client_a.user_store.clone(), cx)) - .await - .unwrap(); + let active_call_a = cx_a.read(ActiveCall::global); + let active_call_b = cx_b.read(ActiveCall::global); // Call user B from client A. - let mut incoming_call_b = client_b - .user_store - .update(cx_b, |user, _| user.incoming_call()); - room_a - .update(cx_a, |room, cx| { - room.call(client_b.user_id().unwrap(), None, cx) + active_call_a + .update(cx_a, |call, cx| { + call.invite(client_b.user_id().unwrap(), None, cx) }) .await .unwrap(); + let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone()); // User B receives the call and joins the room. - let call_b = incoming_call_b.next().await.unwrap().unwrap(); - let room_b = cx_b - .update(|cx| Room::join(&call_b, client_b.clone(), client_b.user_store.clone(), cx)) + let mut incoming_call_b = active_call_b.read_with(cx_b, |call, _| call.incoming()); + incoming_call_b.next().await.unwrap().unwrap(); + active_call_b + .update(cx_b, |call, cx| call.accept_incoming(cx)) .await .unwrap(); + let room_b = active_call_b.read_with(cx_b, |call, _| call.room().unwrap().clone()); deterministic.run_until_parked(); assert_eq!( room_participants(&room_a, cx_a), @@ -398,13 +386,13 @@ async fn test_share_project( cx_a: &mut TestAppContext, cx_b: &mut TestAppContext, ) { - cx_a.foreground().forbid_parking(); + deterministic.forbid_parking(); let (_, window_b) = cx_b.add_window(|_| EmptyView); let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; - let (room_id, _rooms) = server - .create_rooms(&mut [(&client_a, cx_a), (&client_b, cx_b)]) + let room_id = server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) .await; client_a @@ -502,10 +490,13 @@ async fn test_unshare_project( let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; let client_c = server.create_client(cx_c, "user_c").await; - let (room_id, mut rooms) = server - .create_rooms(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)]) + let room_id = server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)]) .await; + let active_call_a = cx_a.read(ActiveCall::global); + let active_call_b = cx_b.read(ActiveCall::global); + client_a .fs .insert_tree( @@ -532,7 +523,7 @@ async fn test_unshare_project( .unwrap(); // When client B leaves the room, the project becomes read-only. - cx_b.update(|_| drop(rooms.remove(1))); + active_call_b.update(cx_b, |call, cx| call.hang_up(cx).unwrap()); deterministic.run_until_parked(); assert!(project_b.read_with(cx_b, |project, _| project.is_read_only())); @@ -560,7 +551,7 @@ async fn test_unshare_project( .unwrap(); // When client A (the host) leaves the room, the project gets unshared and guests are notified. - cx_a.update(|_| drop(rooms.remove(0))); + active_call_a.update(cx_a, |call, cx| call.hang_up(cx).unwrap()); deterministic.run_until_parked(); project_a.read_with(cx_a, |project, _| assert!(!project.is_shared())); project_c2.read_with(cx_c, |project, _| { @@ -582,8 +573,8 @@ async fn test_host_disconnect( let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; let client_c = server.create_client(cx_c, "user_c").await; - let (room_id, _rooms) = server - .create_rooms(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)]) + let room_id = server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)]) .await; client_a @@ -660,7 +651,7 @@ async fn test_host_disconnect( } #[gpui::test(iterations = 10)] -async fn test_room_events( +async fn test_active_call_events( deterministic: Arc, cx_a: &mut TestAppContext, cx_b: &mut TestAppContext, @@ -675,24 +666,21 @@ async fn test_room_events( let (project_a, _) = client_a.build_local_project("/a", cx_a).await; let (project_b, _) = client_b.build_local_project("/b", cx_b).await; - let (room_id, mut rooms) = server - .create_rooms(&mut [(&client_a, cx_a), (&client_b, cx_b)]) + let room_id = server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) .await; - let room_a = rooms.remove(0); - let room_a_events = room_events(&room_a, cx_a); - - let room_b = rooms.remove(0); - let room_b_events = room_events(&room_b, cx_b); + let events_a = active_call_events(cx_a); + let events_b = active_call_events(cx_b); let project_a_id = project_a .update(cx_a, |project, cx| project.share(room_id, cx)) .await .unwrap(); deterministic.run_until_parked(); - assert_eq!(mem::take(&mut *room_a_events.borrow_mut()), vec![]); + assert_eq!(mem::take(&mut *events_a.borrow_mut()), vec![]); assert_eq!( - mem::take(&mut *room_b_events.borrow_mut()), + mem::take(&mut *events_b.borrow_mut()), vec![room::Event::RemoteProjectShared { owner: Arc::new(User { id: client_a.user_id().unwrap(), @@ -709,7 +697,7 @@ async fn test_room_events( .unwrap(); deterministic.run_until_parked(); assert_eq!( - mem::take(&mut *room_a_events.borrow_mut()), + mem::take(&mut *events_a.borrow_mut()), vec![room::Event::RemoteProjectShared { owner: Arc::new(User { id: client_b.user_id().unwrap(), @@ -719,17 +707,15 @@ async fn test_room_events( project_id: project_b_id, }] ); - assert_eq!(mem::take(&mut *room_b_events.borrow_mut()), vec![]); + assert_eq!(mem::take(&mut *events_b.borrow_mut()), vec![]); - fn room_events( - room: &ModelHandle, - cx: &mut TestAppContext, - ) -> Rc>> { + fn active_call_events(cx: &mut TestAppContext) -> Rc>> { let events = Rc::new(RefCell::new(Vec::new())); + let active_call = cx.read(ActiveCall::global); cx.update({ let events = events.clone(); |cx| { - cx.subscribe(room, move |_, event, _| { + cx.subscribe(&active_call, move |_, event, _| { events.borrow_mut().push(event.clone()) }) .detach() @@ -755,26 +741,28 @@ async fn test_room_location( let (project_a, _) = client_a.build_local_project("/a", cx_a).await; let (project_b, _) = client_b.build_local_project("/b", cx_b).await; - let (room_id, mut rooms) = server - .create_rooms(&mut [(&client_a, cx_a), (&client_b, cx_b)]) + let room_id = server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) .await; - let room_a = rooms.remove(0); - let room_a_notified = Rc::new(Cell::new(false)); + let active_call_a = cx_a.read(ActiveCall::global); + let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone()); + let a_notified = Rc::new(Cell::new(false)); cx_a.update({ - let room_a_notified = room_a_notified.clone(); + let notified = a_notified.clone(); |cx| { - cx.observe(&room_a, move |_, _| room_a_notified.set(true)) + cx.observe(&active_call_a, move |_, _| notified.set(true)) .detach() } }); - let room_b = rooms.remove(0); - let room_b_notified = Rc::new(Cell::new(false)); + let active_call_b = cx_b.read(ActiveCall::global); + let room_b = active_call_b.read_with(cx_b, |call, _| call.room().unwrap().clone()); + let b_notified = Rc::new(Cell::new(false)); cx_b.update({ - let room_b_notified = room_b_notified.clone(); + let b_notified = b_notified.clone(); |cx| { - cx.observe(&room_b, move |_, _| room_b_notified.set(true)) + cx.observe(&active_call_b, move |_, _| b_notified.set(true)) .detach() } }); @@ -784,12 +772,12 @@ async fn test_room_location( .await .unwrap(); deterministic.run_until_parked(); - assert!(room_a_notified.take()); + assert!(a_notified.take()); assert_eq!( participant_locations(&room_a, cx_a), vec![("user_b".to_string(), ParticipantLocation::External)] ); - assert!(room_b_notified.take()); + assert!(b_notified.take()); assert_eq!( participant_locations(&room_b, cx_b), vec![("user_a".to_string(), ParticipantLocation::External)] @@ -800,12 +788,12 @@ async fn test_room_location( .await .unwrap(); deterministic.run_until_parked(); - assert!(room_a_notified.take()); + assert!(a_notified.take()); assert_eq!( participant_locations(&room_a, cx_a), vec![("user_b".to_string(), ParticipantLocation::External)] ); - assert!(room_b_notified.take()); + assert!(b_notified.take()); assert_eq!( participant_locations(&room_b, cx_b), vec![("user_a".to_string(), ParticipantLocation::External)] @@ -816,12 +804,12 @@ async fn test_room_location( .await .unwrap(); deterministic.run_until_parked(); - assert!(room_a_notified.take()); + assert!(a_notified.take()); assert_eq!( participant_locations(&room_a, cx_a), vec![("user_b".to_string(), ParticipantLocation::External)] ); - assert!(room_b_notified.take()); + assert!(b_notified.take()); assert_eq!( participant_locations(&room_b, cx_b), vec![( @@ -837,7 +825,7 @@ async fn test_room_location( .await .unwrap(); deterministic.run_until_parked(); - assert!(room_a_notified.take()); + assert!(a_notified.take()); assert_eq!( participant_locations(&room_a, cx_a), vec![( @@ -847,7 +835,7 @@ async fn test_room_location( } )] ); - assert!(room_b_notified.take()); + assert!(b_notified.take()); assert_eq!( participant_locations(&room_b, cx_b), vec![( @@ -863,12 +851,12 @@ async fn test_room_location( .await .unwrap(); deterministic.run_until_parked(); - assert!(room_a_notified.take()); + assert!(a_notified.take()); assert_eq!( participant_locations(&room_a, cx_a), vec![("user_b".to_string(), ParticipantLocation::External)] ); - assert!(room_b_notified.take()); + assert!(b_notified.take()); assert_eq!( participant_locations(&room_b, cx_b), vec![( @@ -908,8 +896,8 @@ async fn test_propagate_saves_and_fs_changes( let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; let client_c = server.create_client(cx_c, "user_c").await; - let (room_id, _rooms) = server - .create_rooms(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)]) + let room_id = server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)]) .await; client_a @@ -1056,8 +1044,8 @@ async fn test_fs_operations( let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; - let (room_id, _rooms) = server - .create_rooms(&mut [(&client_a, cx_a), (&client_b, cx_b)]) + let room_id = server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) .await; client_a @@ -1321,8 +1309,8 @@ async fn test_buffer_conflict_after_save(cx_a: &mut TestAppContext, cx_b: &mut T let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; - let (room_id, _rooms) = server - .create_rooms(&mut [(&client_a, cx_a), (&client_b, cx_b)]) + let room_id = server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) .await; client_a @@ -1374,8 +1362,8 @@ async fn test_buffer_reloading(cx_a: &mut TestAppContext, cx_b: &mut TestAppCont let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; - let (room_id, _rooms) = server - .create_rooms(&mut [(&client_a, cx_a), (&client_b, cx_b)]) + let room_id = server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) .await; client_a @@ -1432,8 +1420,8 @@ async fn test_editing_while_guest_opens_buffer( let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; - let (room_id, _rooms) = server - .create_rooms(&mut [(&client_a, cx_a), (&client_b, cx_b)]) + let room_id = server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) .await; client_a @@ -1478,8 +1466,8 @@ async fn test_leaving_worktree_while_opening_buffer( let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; - let (room_id, _rooms) = server - .create_rooms(&mut [(&client_a, cx_a), (&client_b, cx_b)]) + let room_id = server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) .await; client_a @@ -1522,8 +1510,8 @@ async fn test_canceling_buffer_opening( let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; - let (room_id, _rooms) = server - .create_rooms(&mut [(&client_a, cx_a), (&client_b, cx_b)]) + let room_id = server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) .await; client_a @@ -1573,8 +1561,8 @@ async fn test_leaving_project( let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; let client_c = server.create_client(cx_c, "user_c").await; - let (room_id, _rooms) = server - .create_rooms(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)]) + let room_id = server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)]) .await; client_a @@ -1663,8 +1651,8 @@ async fn test_collaborating_with_diagnostics( let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; let client_c = server.create_client(cx_c, "user_c").await; - let (room_id, _rooms) = server - .create_rooms(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)]) + let room_id = server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)]) .await; // Set up a fake language server. @@ -1900,8 +1888,8 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; - let (room_id, _rooms) = server - .create_rooms(&mut [(&client_a, cx_a), (&client_b, cx_b)]) + let room_id = server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) .await; // Set up a fake language server. @@ -2073,8 +2061,8 @@ async fn test_reloading_buffer_manually(cx_a: &mut TestAppContext, cx_b: &mut Te let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; - let (room_id, _rooms) = server - .create_rooms(&mut [(&client_a, cx_a), (&client_b, cx_b)]) + let room_id = server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) .await; client_a @@ -2165,8 +2153,8 @@ async fn test_formatting_buffer(cx_a: &mut TestAppContext, cx_b: &mut TestAppCon let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; - let (room_id, _rooms) = server - .create_rooms(&mut [(&client_a, cx_a), (&client_b, cx_b)]) + let room_id = server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) .await; // Set up a fake language server. @@ -2265,8 +2253,8 @@ async fn test_definition(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; - let (room_id, _rooms) = server - .create_rooms(&mut [(&client_a, cx_a), (&client_b, cx_b)]) + let room_id = server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) .await; // Set up a fake language server. @@ -2408,8 +2396,8 @@ async fn test_references(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; - let (room_id, _rooms) = server - .create_rooms(&mut [(&client_a, cx_a), (&client_b, cx_b)]) + let room_id = server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) .await; // Set up a fake language server. @@ -2508,8 +2496,8 @@ async fn test_project_search(cx_a: &mut TestAppContext, cx_b: &mut TestAppContex let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; - let (room_id, _rooms) = server - .create_rooms(&mut [(&client_a, cx_a), (&client_b, cx_b)]) + let room_id = server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) .await; client_a @@ -2586,8 +2574,8 @@ async fn test_document_highlights(cx_a: &mut TestAppContext, cx_b: &mut TestAppC let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; - let (room_id, _rooms) = server - .create_rooms(&mut [(&client_a, cx_a), (&client_b, cx_b)]) + let room_id = server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) .await; client_a @@ -2687,8 +2675,8 @@ async fn test_lsp_hover(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; - let (room_id, _rooms) = server - .create_rooms(&mut [(&client_a, cx_a), (&client_b, cx_b)]) + let room_id = server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) .await; client_a @@ -2789,8 +2777,8 @@ async fn test_project_symbols(cx_a: &mut TestAppContext, cx_b: &mut TestAppConte let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; - let (room_id, _rooms) = server - .create_rooms(&mut [(&client_a, cx_a), (&client_b, cx_b)]) + let room_id = server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) .await; // Set up a fake language server. @@ -2896,8 +2884,8 @@ async fn test_open_buffer_while_getting_definition_pointing_to_it( let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; - let (room_id, _rooms) = server - .create_rooms(&mut [(&client_a, cx_a), (&client_b, cx_b)]) + let room_id = server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) .await; // Set up a fake language server. @@ -2971,8 +2959,8 @@ async fn test_collaborating_with_code_actions( let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; - let (room_id, _rooms) = server - .create_rooms(&mut [(&client_a, cx_a), (&client_b, cx_b)]) + let room_id = server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) .await; // Set up a fake language server. @@ -3181,8 +3169,8 @@ async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut T let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; - let (room_id, _rooms) = server - .create_rooms(&mut [(&client_a, cx_a), (&client_b, cx_b)]) + let room_id = server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) .await; // Set up a fake language server. @@ -3372,8 +3360,8 @@ async fn test_language_server_statuses( let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; - let (room_id, _rooms) = server - .create_rooms(&mut [(&client_a, cx_a), (&client_b, cx_b)]) + let room_id = server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) .await; // Set up a fake language server. @@ -4145,8 +4133,8 @@ async fn test_following(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; - let (room_id, _rooms) = server - .create_rooms(&mut [(&client_a, cx_a), (&client_b, cx_b)]) + let room_id = server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) .await; cx_a.update(editor::init); cx_b.update(editor::init); @@ -4354,8 +4342,8 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; - let (room_id, _rooms) = server - .create_rooms(&mut [(&client_a, cx_a), (&client_b, cx_b)]) + let room_id = server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) .await; cx_a.update(editor::init); cx_b.update(editor::init); @@ -4522,8 +4510,8 @@ async fn test_auto_unfollowing(cx_a: &mut TestAppContext, cx_b: &mut TestAppCont let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; - let (room_id, _rooms) = server - .create_rooms(&mut [(&client_a, cx_a), (&client_b, cx_b)]) + let room_id = server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) .await; cx_a.update(editor::init); cx_b.update(editor::init); @@ -4685,8 +4673,8 @@ async fn test_peers_simultaneously_following_each_other( let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; - let (room_id, _rooms) = server - .create_rooms(&mut [(&client_a, cx_a), (&client_b, cx_b)]) + let room_id = server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) .await; cx_a.update(editor::init); cx_b.update(editor::init); @@ -4790,12 +4778,8 @@ async fn test_random_collaboration( .unwrap(); } - let client = server.create_client(cx, "room-creator").await; - let room = cx - .update(|cx| Room::create(client.client.clone(), client.user_store.clone(), cx)) - .await - .unwrap(); - let room_id = room.read_with(cx, |room, _| room.id()); + let _room_creator = server.create_client(cx, "room-creator").await; + let active_call = cx.read(ActiveCall::global); let mut clients = Vec::new(); let mut user_ids = Vec::new(); @@ -4963,22 +4947,17 @@ async fn test_random_collaboration( host_language_registry.add(Arc::new(language)); let host_user_id = host.current_user_id(&host_cx); - room.update(cx, |room, cx| room.call(host_user_id.to_proto(), None, cx)) + active_call + .update(cx, |call, cx| { + call.invite(host_user_id.to_proto(), None, cx) + }) .await .unwrap(); + let room_id = active_call.read_with(cx, |call, cx| call.room().unwrap().read(cx).id()); deterministic.run_until_parked(); - let call = host - .user_store - .read_with(&host_cx, |user_store, _| user_store.incoming_call()); - let host_room = host_cx - .update(|cx| { - Room::join( - call.borrow().as_ref().unwrap(), - host.client.clone(), - host.user_store.clone(), - cx, - ) - }) + host_cx + .read(ActiveCall::global) + .update(&mut host_cx, |call, cx| call.accept_incoming(cx)) .await .unwrap(); @@ -4991,7 +4970,6 @@ async fn test_random_collaboration( user_ids.push(host_user_id); op_start_signals.push(op_start_signal.0); clients.push(host_cx.foreground().spawn(host.simulate_host( - host_room, host_project, op_start_signal.1, rng.clone(), @@ -5016,20 +4994,26 @@ async fn test_random_collaboration( deterministic.finish_waiting(); deterministic.run_until_parked(); - let (host, host_room, host_project, mut host_cx, host_err) = clients.remove(0); + let (host, host_project, mut host_cx, host_err) = clients.remove(0); if let Some(host_err) = host_err { log::error!("host error - {:?}", host_err); } host_project.read_with(&host_cx, |project, _| assert!(!project.is_shared())); - for (guest, guest_room, guest_project, mut guest_cx, guest_err) in clients { + for (guest, guest_project, mut guest_cx, guest_err) in clients { if let Some(guest_err) = guest_err { log::error!("{} error - {:?}", guest.username, guest_err); } guest_project.read_with(&guest_cx, |project, _| assert!(project.is_read_only())); - guest_cx.update(|_| drop((guest, guest_room, guest_project))); + guest_cx.update(|cx| { + cx.clear_globals(); + drop((guest, guest_project)); + }); } - host_cx.update(|_| drop((host, host_room, host_project))); + host_cx.update(|cx| { + cx.clear_globals(); + drop((host, host_project)); + }); return; } @@ -5055,23 +5039,16 @@ async fn test_random_collaboration( let guest = server.create_client(&mut guest_cx, &guest_username).await; let guest_user_id = guest.current_user_id(&guest_cx); - room.update(cx, |room, cx| room.call(guest_user_id.to_proto(), None, cx)) + active_call + .update(cx, |call, cx| { + call.invite(guest_user_id.to_proto(), None, cx) + }) .await .unwrap(); deterministic.run_until_parked(); - let call = guest - .user_store - .read_with(&guest_cx, |user_store, _| user_store.incoming_call()); - - let guest_room = guest_cx - .update(|cx| { - Room::join( - call.borrow().as_ref().unwrap(), - guest.client.clone(), - guest.user_store.clone(), - cx, - ) - }) + guest_cx + .read(ActiveCall::global) + .update(&mut guest_cx, |call, cx| call.accept_incoming(cx)) .await .unwrap(); @@ -5093,7 +5070,6 @@ async fn test_random_collaboration( op_start_signals.push(op_start_signal.0); clients.push(guest_cx.foreground().spawn(guest.simulate_guest( guest_username.clone(), - guest_room, guest_project, op_start_signal.1, rng.clone(), @@ -5114,7 +5090,7 @@ async fn test_random_collaboration( deterministic.advance_clock(RECEIVE_TIMEOUT); deterministic.start_waiting(); log::info!("Waiting for guest {} to exit...", removed_guest_id); - let (guest, guest_room, guest_project, mut guest_cx, guest_err) = guest.await; + let (guest, guest_project, mut guest_cx, guest_err) = guest.await; deterministic.finish_waiting(); server.allow_connections(); @@ -5142,7 +5118,10 @@ async fn test_random_collaboration( log::info!("{} removed", guest.username); available_guests.push(guest.username.clone()); - guest_cx.update(|_| drop((guest, guest_room, guest_project))); + guest_cx.update(|cx| { + cx.clear_globals(); + drop((guest, guest_project)); + }); operations += 1; } @@ -5169,7 +5148,7 @@ async fn test_random_collaboration( deterministic.finish_waiting(); deterministic.run_until_parked(); - let (host_client, host_room, host_project, mut host_cx, host_err) = clients.remove(0); + let (host_client, host_project, mut host_cx, host_err) = clients.remove(0); if let Some(host_err) = host_err { panic!("host error - {:?}", host_err); } @@ -5185,7 +5164,7 @@ async fn test_random_collaboration( host_project.read_with(&host_cx, |project, cx| project.check_invariants(cx)); - for (guest_client, guest_room, guest_project, mut guest_cx, guest_err) in clients.into_iter() { + for (guest_client, guest_project, mut guest_cx, guest_err) in clients.into_iter() { if let Some(guest_err) = guest_err { panic!("{} error - {:?}", guest_client.username, guest_err); } @@ -5257,10 +5236,16 @@ async fn test_random_collaboration( ); } - guest_cx.update(|_| drop((guest_room, guest_project, guest_client))); + guest_cx.update(|cx| { + cx.clear_globals(); + drop((guest_project, guest_client)); + }); } - host_cx.update(|_| drop((host_client, host_room, host_project))); + host_cx.update(|cx| { + cx.clear_globals(); + drop((host_client, host_project)) + }); } struct TestServer { @@ -5385,7 +5370,10 @@ impl TestServer { Channel::init(&client); Project::init(&client); - cx.update(|cx| workspace::init(app_state.clone(), cx)); + cx.update(|cx| { + workspace::init(app_state.clone(), cx); + call::init(client.clone(), user_store.clone(), cx); + }); client .authenticate_and_connect(false, &cx.to_async()) @@ -5447,50 +5435,29 @@ impl TestServer { } } - async fn create_rooms( - &self, - clients: &mut [(&TestClient, &mut TestAppContext)], - ) -> (u64, Vec>) { + async fn create_room(&self, clients: &mut [(&TestClient, &mut TestAppContext)]) -> u64 { self.make_contacts(clients).await; - let mut rooms = Vec::new(); - let (left, right) = clients.split_at_mut(1); - let (client_a, cx_a) = &mut left[0]; - - let room_a = cx_a - .update(|cx| Room::create(client_a.client.clone(), client_a.user_store.clone(), cx)) - .await - .unwrap(); - let room_id = room_a.read_with(*cx_a, |room, _| room.id()); + let (_client_a, cx_a) = &mut left[0]; + let active_call_a = cx_a.read(ActiveCall::global); for (client_b, cx_b) in right { let user_id_b = client_b.current_user_id(*cx_b).to_proto(); - room_a - .update(*cx_a, |room, cx| room.call(user_id_b, None, cx)) + active_call_a + .update(*cx_a, |call, cx| call.invite(user_id_b, None, cx)) .await .unwrap(); cx_b.foreground().run_until_parked(); - let incoming_call = client_b - .user_store - .read_with(*cx_b, |user_store, _| user_store.incoming_call()); - let room_b = cx_b - .update(|cx| { - Room::join( - incoming_call.borrow().as_ref().unwrap(), - client_b.client.clone(), - client_b.user_store.clone(), - cx, - ) - }) + let active_call_b = cx_b.read(ActiveCall::global); + active_call_b + .update(*cx_b, |call, cx| call.accept_incoming(cx)) .await .unwrap(); - rooms.push(room_b); } - rooms.insert(0, room_a); - (room_id, rooms) + active_call_a.read_with(*cx_a, |call, cx| call.room().unwrap().read(cx).id()) } async fn build_app_state(test_db: &TestDb) -> Arc { @@ -5656,14 +5623,12 @@ impl TestClient { async fn simulate_host( mut self, - room: ModelHandle, project: ModelHandle, op_start_signal: futures::channel::mpsc::UnboundedReceiver<()>, rng: Arc>, mut cx: TestAppContext, ) -> ( Self, - ModelHandle, ModelHandle, TestAppContext, Option, @@ -5789,20 +5754,18 @@ impl TestClient { let result = simulate_host_internal(&mut self, project.clone(), op_start_signal, rng, &mut cx).await; log::info!("Host done"); - (self, room, project, cx, result.err()) + (self, project, cx, result.err()) } pub async fn simulate_guest( mut self, guest_username: String, - room: ModelHandle, project: ModelHandle, op_start_signal: futures::channel::mpsc::UnboundedReceiver<()>, rng: Arc>, mut cx: TestAppContext, ) -> ( Self, - ModelHandle, ModelHandle, TestAppContext, Option, @@ -6121,7 +6084,7 @@ impl TestClient { .await; log::info!("{}: done", guest_username); - (self, room, project, cx, result.err()) + (self, project, cx, result.err()) } } diff --git a/crates/collab_ui/src/collab_ui.rs b/crates/collab_ui/src/collab_ui.rs index 786d344df19c57bd520a434beac5957a45a871ad..03d1bf6672da10e48880861be07662f57bcf15e5 100644 --- a/crates/collab_ui/src/collab_ui.rs +++ b/crates/collab_ui/src/collab_ui.rs @@ -13,7 +13,7 @@ use workspace::{AppState, JoinProject, ToggleFollow, Workspace}; pub fn init(app_state: Arc, cx: &mut MutableAppContext) { contacts_popover::init(cx); collab_titlebar_item::init(cx); - incoming_call_notification::init(app_state.user_store.clone(), cx); + incoming_call_notification::init(cx); project_shared_notification::init(cx); cx.add_global_action(move |action: &JoinProject, cx| { diff --git a/crates/collab_ui/src/incoming_call_notification.rs b/crates/collab_ui/src/incoming_call_notification.rs index e46e69522f2548e5683caf74a39ffb59f6c69207..ae1240d0d9feef84b5e79d328e9569ed3030108b 100644 --- a/crates/collab_ui/src/incoming_call_notification.rs +++ b/crates/collab_ui/src/incoming_call_notification.rs @@ -1,11 +1,11 @@ use call::ActiveCall; -use client::{incoming_call::IncomingCall, UserStore}; +use client::incoming_call::IncomingCall; use futures::StreamExt; use gpui::{ elements::*, geometry::{rect::RectF, vector::vec2f}, - impl_internal_actions, Entity, ModelHandle, MouseButton, MutableAppContext, RenderContext, - View, ViewContext, WindowBounds, WindowKind, WindowOptions, + impl_internal_actions, Entity, MouseButton, MutableAppContext, RenderContext, View, + ViewContext, WindowBounds, WindowKind, WindowOptions, }; use settings::Settings; use util::ResultExt; @@ -13,10 +13,10 @@ use workspace::JoinProject; impl_internal_actions!(incoming_call_notification, [RespondToCall]); -pub fn init(user_store: ModelHandle, cx: &mut MutableAppContext) { +pub fn init(cx: &mut MutableAppContext) { cx.add_action(IncomingCallNotification::respond_to_call); - let mut incoming_call = user_store.read(cx).incoming_call(); + let mut incoming_call = ActiveCall::global(cx).read(cx).incoming(); cx.spawn(|mut cx| async move { let mut notification_window = None; while let Some(incoming_call) = incoming_call.next().await { @@ -33,7 +33,7 @@ pub fn init(user_store: ModelHandle, cx: &mut MutableAppContext) { kind: WindowKind::PopUp, is_movable: false, }, - |_| IncomingCallNotification::new(incoming_call, user_store.clone()), + |_| IncomingCallNotification::new(incoming_call), ); notification_window = Some(window_id); } @@ -49,18 +49,17 @@ struct RespondToCall { pub struct IncomingCallNotification { call: IncomingCall, - user_store: ModelHandle, } impl IncomingCallNotification { - pub fn new(call: IncomingCall, user_store: ModelHandle) -> Self { - Self { call, user_store } + pub fn new(call: IncomingCall) -> Self { + Self { call } } fn respond_to_call(&mut self, action: &RespondToCall, cx: &mut ViewContext) { + let active_call = ActiveCall::global(cx); if action.accept { - let join = ActiveCall::global(cx) - .update(cx, |active_call, cx| active_call.join(&self.call, cx)); + let join = active_call.update(cx, |active_call, cx| active_call.accept_incoming(cx)); let caller_user_id = self.call.caller.id; let initial_project_id = self.call.initial_project_id; cx.spawn_weak(|_, mut cx| async move { @@ -77,12 +76,10 @@ impl IncomingCallNotification { }) .detach_and_log_err(cx); } else { - self.user_store - .update(cx, |user_store, _| user_store.decline_call().log_err()); + active_call.update(cx, |active_call, _| { + active_call.decline_incoming().log_err(); + }); } - - let window_id = cx.window_id(); - cx.remove_window(window_id); } fn render_caller(&self, cx: &mut RenderContext) -> ElementBox { diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 002bd01377dd173a07b47850d6c16df045e8955f..668071d04670452cfba2c8a532a2a2a7ca6324ac 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -1906,6 +1906,10 @@ impl MutableAppContext { }) } + pub fn clear_globals(&mut self) { + self.cx.globals.clear(); + } + pub fn add_model(&mut self, build_model: F) -> ModelHandle where T: Entity, diff --git a/crates/gpui/src/test.rs b/crates/gpui/src/test.rs index 4122ad09b7480adcd647db98821a6bb3c533aa1f..6cfb4cf2b64f6518303599d9e8a3209d7dbcc09c 100644 --- a/crates/gpui/src/test.rs +++ b/crates/gpui/src/test.rs @@ -91,7 +91,7 @@ pub fn run_test( cx.update(|cx| cx.remove_all_windows()); deterministic.run_until_parked(); - cx.update(|_| {}); // flush effects + cx.update(|cx| cx.clear_globals()); leak_detector.lock().detect(); if is_last_iteration { diff --git a/crates/gpui_macros/src/gpui_macros.rs b/crates/gpui_macros/src/gpui_macros.rs index a60d385e8f5305d398503e2508958d089174fbfc..32a821f4c83f010355600814b2d589b87e7b7f59 100644 --- a/crates/gpui_macros/src/gpui_macros.rs +++ b/crates/gpui_macros/src/gpui_macros.rs @@ -122,7 +122,7 @@ pub fn test(args: TokenStream, function: TokenStream) -> TokenStream { cx_teardowns.extend(quote!( #cx_varname.update(|cx| cx.remove_all_windows()); deterministic.run_until_parked(); - #cx_varname.update(|_| {}); // flush effects + #cx_varname.update(|cx| cx.clear_globals()); )); inner_fn_args.extend(quote!(&mut #cx_varname,)); continue; From 7763acbdd5a2a7c55d9459b3231f79694e8e1820 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 6 Oct 2022 09:52:03 +0200 Subject: [PATCH 059/112] Move `IncomingCall` into `call` crate --- crates/call/src/call.rs | 10 +++++++++- crates/call/src/room.rs | 7 +++++-- crates/client/src/client.rs | 1 - crates/client/src/incoming_call.rs | 10 ---------- crates/collab_ui/src/incoming_call_notification.rs | 3 +-- 5 files changed, 15 insertions(+), 16 deletions(-) delete mode 100644 crates/client/src/incoming_call.rs diff --git a/crates/call/src/call.rs b/crates/call/src/call.rs index 607931fdc4710ee40aca8931a5078c7574227d59..7cd8896bf6ee5568f7acf0deea1d80a201b1c6bc 100644 --- a/crates/call/src/call.rs +++ b/crates/call/src/call.rs @@ -2,7 +2,7 @@ mod participant; pub mod room; use anyhow::{anyhow, Result}; -use client::{incoming_call::IncomingCall, proto, Client, TypedEnvelope, UserStore}; +use client::{proto, Client, TypedEnvelope, User, UserStore}; use gpui::{ AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Subscription, Task, @@ -18,6 +18,14 @@ pub fn init(client: Arc, user_store: ModelHandle, cx: &mut Mu cx.set_global(active_call); } +#[derive(Clone)] +pub struct IncomingCall { + pub room_id: u64, + pub caller: Arc, + pub participants: Vec>, + pub initial_project_id: Option, +} + pub struct ActiveCall { room: Option<(ModelHandle, Vec)>, incoming_call: ( diff --git a/crates/call/src/room.rs b/crates/call/src/room.rs index 52f283dd03ecbef82e5cc90a7c900e88aac87272..3d8697f1e0f1e7ebb7c4d03cbfb0c4e900daa58f 100644 --- a/crates/call/src/room.rs +++ b/crates/call/src/room.rs @@ -1,6 +1,9 @@ -use crate::participant::{ParticipantLocation, RemoteParticipant}; +use crate::{ + participant::{ParticipantLocation, RemoteParticipant}, + IncomingCall, +}; use anyhow::{anyhow, Result}; -use client::{incoming_call::IncomingCall, proto, Client, PeerId, TypedEnvelope, User, UserStore}; +use client::{proto, Client, PeerId, TypedEnvelope, User, UserStore}; use collections::{HashMap, HashSet}; use futures::StreamExt; use gpui::{AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Task}; diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 9c5b8e35c9c9f83dd389b628e767c5d23a9cd502..a762e263ea55d790bfbfa1c4af7bd4baad8b6f00 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -3,7 +3,6 @@ pub mod test; pub mod channel; pub mod http; -pub mod incoming_call; pub mod user; use anyhow::{anyhow, Context, Result}; diff --git a/crates/client/src/incoming_call.rs b/crates/client/src/incoming_call.rs deleted file mode 100644 index 80ba014061f97c2f44e5887f883b4dd34335e629..0000000000000000000000000000000000000000 --- a/crates/client/src/incoming_call.rs +++ /dev/null @@ -1,10 +0,0 @@ -use crate::User; -use std::sync::Arc; - -#[derive(Clone)] -pub struct IncomingCall { - pub room_id: u64, - pub caller: Arc, - pub participants: Vec>, - pub initial_project_id: Option, -} diff --git a/crates/collab_ui/src/incoming_call_notification.rs b/crates/collab_ui/src/incoming_call_notification.rs index ae1240d0d9feef84b5e79d328e9569ed3030108b..8860097a592f9af87ec136d25f6f756e827fd5bc 100644 --- a/crates/collab_ui/src/incoming_call_notification.rs +++ b/crates/collab_ui/src/incoming_call_notification.rs @@ -1,5 +1,4 @@ -use call::ActiveCall; -use client::incoming_call::IncomingCall; +use call::{ActiveCall, IncomingCall}; use futures::StreamExt; use gpui::{ elements::*, From 40163da679d9d5a5ca1cf6bca7824e91daa3233f Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 6 Oct 2022 14:00:14 +0200 Subject: [PATCH 060/112] Move contacts panel features into collab_ui --- Cargo.lock | 26 +- assets/keymaps/default.json | 1 - crates/collab_ui/Cargo.toml | 1 + crates/collab_ui/src/collab_titlebar_item.rs | 60 +- crates/collab_ui/src/collab_ui.rs | 5 + .../src/contact_finder.rs | 26 +- .../src/contact_notification.rs | 5 +- crates/collab_ui/src/contacts_popover.rs | 69 +- .../src/notifications.rs | 23 +- crates/contacts_panel/Cargo.toml | 32 - crates/contacts_panel/src/contacts_panel.rs | 1000 ----------------- crates/theme/src/theme.rs | 1 + crates/zed/Cargo.toml | 1 - crates/zed/src/main.rs | 1 - crates/zed/src/menus.rs | 4 - crates/zed/src/zed.rs | 24 +- styles/src/styleTree/workspace.ts | 7 + 17 files changed, 152 insertions(+), 1134 deletions(-) rename crates/{contacts_panel => collab_ui}/src/contact_finder.rs (88%) rename crates/{contacts_panel => collab_ui}/src/contact_notification.rs (96%) rename crates/{contacts_panel => collab_ui}/src/notifications.rs (83%) delete mode 100644 crates/contacts_panel/Cargo.toml delete mode 100644 crates/contacts_panel/src/contacts_panel.rs diff --git a/Cargo.lock b/Cargo.lock index a971570e2f33a7a3f78f1e4193478ea3f0c476fc..2d3ca1f78c68df542e4ef6395cc420ff8ad40b2d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1091,6 +1091,7 @@ dependencies = [ "gpui", "log", "menu", + "picker", "postage", "project", "serde", @@ -1141,30 +1142,6 @@ dependencies = [ "cache-padded", ] -[[package]] -name = "contacts_panel" -version = "0.1.0" -dependencies = [ - "anyhow", - "client", - "collections", - "editor", - "futures", - "fuzzy", - "gpui", - "language", - "log", - "menu", - "picker", - "postage", - "project", - "serde", - "settings", - "theme", - "util", - "workspace", -] - [[package]] name = "context_menu" version = "0.1.0" @@ -7165,7 +7142,6 @@ dependencies = [ "collab_ui", "collections", "command_palette", - "contacts_panel", "context_menu", "ctor", "diagnostics", diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index a0bc8c39e6b2d29089292daa2863705a32e05ea4..e2adfc0f81d09bc6226079dd83713cf1b0dc79b5 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -395,7 +395,6 @@ "context": "Workspace", "bindings": { "shift-escape": "dock::FocusDock", - "cmd-shift-c": "contacts_panel::ToggleFocus", "cmd-shift-b": "workspace::ToggleRightSidebar" } }, diff --git a/crates/collab_ui/Cargo.toml b/crates/collab_ui/Cargo.toml index cf3a78a0b5304c1a1c7f58715cef95410b29434c..20db066ce72a1c0c514c64cf6983f1ccdde43f6a 100644 --- a/crates/collab_ui/Cargo.toml +++ b/crates/collab_ui/Cargo.toml @@ -29,6 +29,7 @@ editor = { path = "../editor" } fuzzy = { path = "../fuzzy" } gpui = { path = "../gpui" } menu = { path = "../menu" } +picker = { path = "../picker" } project = { path = "../project" } settings = { path = "../settings" } theme = { path = "../theme" } diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index cea36548561c64d9aa7951ea2b8c9bb8ea40aea1..c98296204267b307a69c5c09bb4e505f13010acc 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -1,6 +1,6 @@ -use crate::contacts_popover; +use crate::{contact_notification::ContactNotification, contacts_popover}; use call::{ActiveCall, ParticipantLocation}; -use client::{Authenticate, PeerId}; +use client::{Authenticate, ContactEventKind, PeerId, UserStore}; use clock::ReplicaId; use contacts_popover::ContactsPopover; use gpui::{ @@ -9,8 +9,8 @@ use gpui::{ elements::*, geometry::{rect::RectF, vector::vec2f, PathBuilder}, json::{self, ToJson}, - Border, CursorStyle, Entity, ImageData, MouseButton, MutableAppContext, RenderContext, - Subscription, View, ViewContext, ViewHandle, WeakViewHandle, + Border, CursorStyle, Entity, ImageData, ModelHandle, MouseButton, MutableAppContext, + RenderContext, Subscription, View, ViewContext, ViewHandle, WeakViewHandle, }; use settings::Settings; use std::{ops::Range, sync::Arc}; @@ -29,6 +29,7 @@ pub fn init(cx: &mut MutableAppContext) { pub struct CollabTitlebarItem { workspace: WeakViewHandle, + user_store: ModelHandle, contacts_popover: Option>, _subscriptions: Vec, } @@ -71,7 +72,11 @@ impl View for CollabTitlebarItem { } impl CollabTitlebarItem { - pub fn new(workspace: &ViewHandle, cx: &mut ViewContext) -> Self { + pub fn new( + workspace: &ViewHandle, + user_store: &ModelHandle, + cx: &mut ViewContext, + ) -> Self { let active_call = ActiveCall::global(cx); let mut subscriptions = Vec::new(); subscriptions.push(cx.observe(workspace, |_, _, cx| cx.notify())); @@ -79,9 +84,33 @@ impl CollabTitlebarItem { subscriptions.push(cx.observe_window_activation(|this, active, cx| { this.window_activation_changed(active, cx) })); + subscriptions.push(cx.observe(user_store, |_, _, cx| cx.notify())); + subscriptions.push( + cx.subscribe(user_store, move |this, user_store, event, cx| { + if let Some(workspace) = this.workspace.upgrade(cx) { + workspace.update(cx, |workspace, cx| { + if let client::Event::Contact { user, kind } = event { + if let ContactEventKind::Requested | ContactEventKind::Accepted = kind { + workspace.show_notification(user.id as usize, cx, |cx| { + cx.add_view(|cx| { + ContactNotification::new( + user.clone(), + *kind, + user_store, + cx, + ) + }) + }) + } + } + }); + } + }), + ); Self { workspace: workspace.downgrade(), + user_store: user_store.clone(), contacts_popover: None, _subscriptions: subscriptions, } @@ -160,6 +189,26 @@ impl CollabTitlebarItem { cx: &mut RenderContext, ) -> ElementBox { let titlebar = &theme.workspace.titlebar; + let badge = if self + .user_store + .read(cx) + .incoming_contact_requests() + .is_empty() + { + None + } else { + Some( + Empty::new() + .collapsed() + .contained() + .with_style(titlebar.toggle_contacts_badge) + .contained() + .with_margin_left(titlebar.toggle_contacts_button.default.icon_width) + .with_margin_top(titlebar.toggle_contacts_button.default.icon_width) + .aligned() + .boxed(), + ) + }; Stack::new() .with_child( MouseEventHandler::::new(0, cx, |state, _| { @@ -185,6 +234,7 @@ impl CollabTitlebarItem { .aligned() .boxed(), ) + .with_children(badge) .with_children(self.contacts_popover.as_ref().map(|popover| { Overlay::new( ChildView::new(popover) diff --git a/crates/collab_ui/src/collab_ui.rs b/crates/collab_ui/src/collab_ui.rs index 03d1bf6672da10e48880861be07662f57bcf15e5..438a41ae7d1eb82c4c19bd3ea222bfcbfa7f08d5 100644 --- a/crates/collab_ui/src/collab_ui.rs +++ b/crates/collab_ui/src/collab_ui.rs @@ -1,6 +1,9 @@ mod collab_titlebar_item; +mod contact_finder; +mod contact_notification; mod contacts_popover; mod incoming_call_notification; +mod notifications; mod project_shared_notification; use call::ActiveCall; @@ -11,6 +14,8 @@ use std::sync::Arc; use workspace::{AppState, JoinProject, ToggleFollow, Workspace}; pub fn init(app_state: Arc, cx: &mut MutableAppContext) { + contact_notification::init(cx); + contact_finder::init(cx); contacts_popover::init(cx); collab_titlebar_item::init(cx); incoming_call_notification::init(cx); diff --git a/crates/contacts_panel/src/contact_finder.rs b/crates/collab_ui/src/contact_finder.rs similarity index 88% rename from crates/contacts_panel/src/contact_finder.rs rename to crates/collab_ui/src/contact_finder.rs index 1831c1ba72593f9570e1f87825042a2b2e4f2b78..6814b7479f58b432b9683e1abe8f43d4c1b7b1d8 100644 --- a/crates/contacts_panel/src/contact_finder.rs +++ b/crates/collab_ui/src/contact_finder.rs @@ -9,8 +9,6 @@ use std::sync::Arc; use util::TryFutureExt; use workspace::Workspace; -use crate::render_icon_button; - actions!(contact_finder, [Toggle]); pub fn init(cx: &mut MutableAppContext) { @@ -117,11 +115,10 @@ impl PickerDelegate for ContactFinder { let icon_path = match request_status { ContactRequestStatus::None | ContactRequestStatus::RequestReceived => { - "icons/check_8.svg" - } - ContactRequestStatus::RequestSent | ContactRequestStatus::RequestAccepted => { - "icons/x_mark_8.svg" + Some("icons/check_8.svg") } + ContactRequestStatus::RequestSent => Some("icons/x_mark_8.svg"), + ContactRequestStatus::RequestAccepted => None, }; let button_style = if self.user_store.read(cx).is_contact_request_pending(user) { &theme.contact_finder.disabled_contact_button @@ -145,12 +142,21 @@ impl PickerDelegate for ContactFinder { .left() .boxed(), ) - .with_child( - render_icon_button(button_style, icon_path) + .with_children(icon_path.map(|icon_path| { + Svg::new(icon_path) + .with_color(button_style.color) + .constrained() + .with_width(button_style.icon_width) + .aligned() + .contained() + .with_style(button_style.container) + .constrained() + .with_width(button_style.button_width) + .with_height(button_style.button_width) .aligned() .flex_float() - .boxed(), - ) + .boxed() + })) .contained() .with_style(style.container) .constrained() diff --git a/crates/contacts_panel/src/contact_notification.rs b/crates/collab_ui/src/contact_notification.rs similarity index 96% rename from crates/contacts_panel/src/contact_notification.rs rename to crates/collab_ui/src/contact_notification.rs index c608346d7991c974ee02647586f29c5a8b28eb28..f543a0144610f5fc1f64d568720a3bb19f70bed0 100644 --- a/crates/contacts_panel/src/contact_notification.rs +++ b/crates/collab_ui/src/contact_notification.rs @@ -49,10 +49,7 @@ impl View for ContactNotification { self.user.clone(), "wants to add you as a contact", Some("They won't know if you decline."), - RespondToContactRequest { - user_id: self.user.id, - accept: false, - }, + Dismiss(self.user.id), vec![ ( "Decline", diff --git a/crates/collab_ui/src/contacts_popover.rs b/crates/collab_ui/src/contacts_popover.rs index 389fe9fbd286aaaaaeb00e33b57a6396a242cee7..f3ebf3abea5dec93b94496f5cfe8b96150359fca 100644 --- a/crates/collab_ui/src/contacts_popover.rs +++ b/crates/collab_ui/src/contacts_popover.rs @@ -1,22 +1,27 @@ use std::sync::Arc; +use crate::contact_finder; use call::ActiveCall; use client::{Contact, User, UserStore}; use editor::{Cancel, Editor}; use fuzzy::{match_strings, StringMatchCandidate}; use gpui::{ - elements::*, impl_internal_actions, keymap, AppContext, ClipboardItem, CursorStyle, Entity, - ModelHandle, MouseButton, MutableAppContext, RenderContext, Subscription, View, ViewContext, - ViewHandle, + elements::*, impl_actions, impl_internal_actions, keymap, AppContext, ClipboardItem, + CursorStyle, Entity, ModelHandle, MouseButton, MutableAppContext, RenderContext, Subscription, + View, ViewContext, ViewHandle, }; use menu::{Confirm, SelectNext, SelectPrev}; use project::Project; +use serde::Deserialize; use settings::Settings; use theme::IconButton; -impl_internal_actions!(contacts_panel, [ToggleExpanded, Call]); +impl_actions!(contacts_popover, [RemoveContact, RespondToContactRequest]); +impl_internal_actions!(contacts_popover, [ToggleExpanded, Call]); pub fn init(cx: &mut MutableAppContext) { + cx.add_action(ContactsPopover::remove_contact); + cx.add_action(ContactsPopover::respond_to_contact_request); cx.add_action(ContactsPopover::clear_filter); cx.add_action(ContactsPopover::select_next); cx.add_action(ContactsPopover::select_prev); @@ -77,6 +82,18 @@ impl PartialEq for ContactEntry { } } +#[derive(Clone, Deserialize, PartialEq)] +pub struct RequestContact(pub u64); + +#[derive(Clone, Deserialize, PartialEq)] +pub struct RemoveContact(pub u64); + +#[derive(Clone, Deserialize, PartialEq)] +pub struct RespondToContactRequest { + pub user_id: u64, + pub accept: bool, +} + pub enum Event { Dismissed, } @@ -186,6 +203,24 @@ impl ContactsPopover { this } + fn remove_contact(&mut self, request: &RemoveContact, cx: &mut ViewContext) { + self.user_store + .update(cx, |store, cx| store.remove_contact(request.0, cx)) + .detach(); + } + + fn respond_to_contact_request( + &mut self, + action: &RespondToContactRequest, + cx: &mut ViewContext, + ) { + self.user_store + .update(cx, |store, cx| { + store.respond_to_contact_request(action.user_id, action.accept, cx) + }) + .detach(); + } + fn clear_filter(&mut self, _: &Cancel, cx: &mut ViewContext) { let did_clear = self.filter_editor.update(cx, |editor, cx| { if editor.buffer().read(cx).len(cx) > 0 { @@ -574,18 +609,15 @@ impl ContactsPopover { }; render_icon_button(button_style, "icons/x_mark_8.svg") .aligned() - // .flex_float() .boxed() }) .with_cursor_style(CursorStyle::PointingHand) .on_click(MouseButton::Left, move |_, cx| { - todo!(); - // cx.dispatch_action(RespondToContactRequest { - // user_id, - // accept: false, - // }) + cx.dispatch_action(RespondToContactRequest { + user_id, + accept: false, + }) }) - // .flex_float() .contained() .with_margin_right(button_spacing) .boxed(), @@ -602,11 +634,10 @@ impl ContactsPopover { }) .with_cursor_style(CursorStyle::PointingHand) .on_click(MouseButton::Left, move |_, cx| { - todo!() - // cx.dispatch_action(RespondToContactRequest { - // user_id, - // accept: true, - // }) + cx.dispatch_action(RespondToContactRequest { + user_id, + accept: true, + }) }) .boxed(), ]); @@ -626,8 +657,7 @@ impl ContactsPopover { .with_padding(Padding::uniform(2.)) .with_cursor_style(CursorStyle::PointingHand) .on_click(MouseButton::Left, move |_, cx| { - todo!() - // cx.dispatch_action(RemoveContact(user_id)) + cx.dispatch_action(RemoveContact(user_id)) }) .flex_float() .boxed(), @@ -692,8 +722,7 @@ impl View for ContactsPopover { }) .with_cursor_style(CursorStyle::PointingHand) .on_click(MouseButton::Left, |_, cx| { - todo!() - // cx.dispatch_action(contact_finder::Toggle) + cx.dispatch_action(contact_finder::Toggle) }) .boxed(), ) diff --git a/crates/contacts_panel/src/notifications.rs b/crates/collab_ui/src/notifications.rs similarity index 83% rename from crates/contacts_panel/src/notifications.rs rename to crates/collab_ui/src/notifications.rs index b9a6dba545b820e38fcaf55ae5767e498a3a52c4..dcb98940065e6611e9ebf39d255f43d3d8646af4 100644 --- a/crates/contacts_panel/src/notifications.rs +++ b/crates/collab_ui/src/notifications.rs @@ -1,9 +1,7 @@ -use crate::render_icon_button; use client::User; use gpui::{ - elements::{Flex, Image, Label, MouseEventHandler, Padding, ParentElement, Text}, - platform::CursorStyle, - Action, Element, ElementBox, MouseButton, RenderContext, View, + elements::*, platform::CursorStyle, Action, Element, ElementBox, MouseButton, RenderContext, + View, }; use settings::Settings; use std::sync::Arc; @@ -53,11 +51,18 @@ pub fn render_user_notification( ) .with_child( MouseEventHandler::::new(user.id as usize, cx, |state, _| { - render_icon_button( - theme.dismiss_button.style_for(state, false), - "icons/x_mark_thin_8.svg", - ) - .boxed() + let style = theme.dismiss_button.style_for(state, false); + Svg::new("icons/x_mark_thin_8.svg") + .with_color(style.color) + .constrained() + .with_width(style.icon_width) + .aligned() + .contained() + .with_style(style.container) + .constrained() + .with_width(style.button_width) + .with_height(style.button_width) + .boxed() }) .with_cursor_style(CursorStyle::PointingHand) .with_padding(Padding::uniform(5.)) diff --git a/crates/contacts_panel/Cargo.toml b/crates/contacts_panel/Cargo.toml deleted file mode 100644 index b68f48bb97ed908de9e454da0524601d8eb4bffe..0000000000000000000000000000000000000000 --- a/crates/contacts_panel/Cargo.toml +++ /dev/null @@ -1,32 +0,0 @@ -[package] -name = "contacts_panel" -version = "0.1.0" -edition = "2021" - -[lib] -path = "src/contacts_panel.rs" -doctest = false - -[dependencies] -client = { path = "../client" } -collections = { path = "../collections" } -editor = { path = "../editor" } -fuzzy = { path = "../fuzzy" } -gpui = { path = "../gpui" } -menu = { path = "../menu" } -picker = { path = "../picker" } -project = { path = "../project" } -settings = { path = "../settings" } -theme = { path = "../theme" } -util = { path = "../util" } -workspace = { path = "../workspace" } -anyhow = "1.0" -futures = "0.3" -log = "0.4" -postage = { version = "0.4.1", features = ["futures-traits"] } -serde = { version = "1.0", features = ["derive", "rc"] } - -[dev-dependencies] -language = { path = "../language", features = ["test-support"] } -project = { path = "../project", features = ["test-support"] } -workspace = { path = "../workspace", features = ["test-support"] } diff --git a/crates/contacts_panel/src/contacts_panel.rs b/crates/contacts_panel/src/contacts_panel.rs deleted file mode 100644 index db6d3bd3b0fc623ccb7ebad67f7aa43eb5498911..0000000000000000000000000000000000000000 --- a/crates/contacts_panel/src/contacts_panel.rs +++ /dev/null @@ -1,1000 +0,0 @@ -mod contact_finder; -mod contact_notification; -mod notifications; - -use client::{Contact, ContactEventKind, User, UserStore}; -use contact_notification::ContactNotification; -use editor::{Cancel, Editor}; -use fuzzy::{match_strings, StringMatchCandidate}; -use gpui::{ - actions, elements::*, impl_actions, impl_internal_actions, platform::CursorStyle, - AnyViewHandle, AppContext, ClipboardItem, Element, ElementBox, Entity, ModelHandle, - MouseButton, MutableAppContext, RenderContext, Subscription, View, ViewContext, ViewHandle, - WeakViewHandle, -}; -use menu::{Confirm, SelectNext, SelectPrev}; -use serde::Deserialize; -use settings::Settings; -use std::sync::Arc; -use theme::IconButton; -use workspace::{sidebar::SidebarItem, Workspace}; - -actions!(contacts_panel, [ToggleFocus]); - -impl_actions!( - contacts_panel, - [RequestContact, RemoveContact, RespondToContactRequest] -); - -impl_internal_actions!(contacts_panel, [ToggleExpanded]); - -#[derive(Clone, Copy, PartialEq, Eq, Debug, PartialOrd, Ord)] -enum Section { - Requests, - Online, - Offline, -} - -#[derive(Clone)] -enum ContactEntry { - Header(Section), - IncomingRequest(Arc), - OutgoingRequest(Arc), - Contact(Arc), -} - -#[derive(Clone, PartialEq)] -struct ToggleExpanded(Section); - -pub struct ContactsPanel { - entries: Vec, - match_candidates: Vec, - list_state: ListState, - user_store: ModelHandle, - filter_editor: ViewHandle, - collapsed_sections: Vec
, - selection: Option, - _maintain_contacts: Subscription, -} - -#[derive(Clone, Deserialize, PartialEq)] -pub struct RequestContact(pub u64); - -#[derive(Clone, Deserialize, PartialEq)] -pub struct RemoveContact(pub u64); - -#[derive(Clone, Deserialize, PartialEq)] -pub struct RespondToContactRequest { - pub user_id: u64, - pub accept: bool, -} - -pub fn init(cx: &mut MutableAppContext) { - contact_finder::init(cx); - contact_notification::init(cx); - cx.add_action(ContactsPanel::request_contact); - cx.add_action(ContactsPanel::remove_contact); - cx.add_action(ContactsPanel::respond_to_contact_request); - cx.add_action(ContactsPanel::clear_filter); - cx.add_action(ContactsPanel::select_next); - cx.add_action(ContactsPanel::select_prev); - cx.add_action(ContactsPanel::confirm); - cx.add_action(ContactsPanel::toggle_expanded); -} - -impl ContactsPanel { - pub fn new( - user_store: ModelHandle, - workspace: WeakViewHandle, - cx: &mut ViewContext, - ) -> Self { - let filter_editor = cx.add_view(|cx| { - let mut editor = Editor::single_line( - Some(|theme| theme.contacts_panel.user_query_editor.clone()), - cx, - ); - editor.set_placeholder_text("Filter contacts", cx); - editor - }); - - cx.subscribe(&filter_editor, |this, _, event, cx| { - if let editor::Event::BufferEdited = event { - let query = this.filter_editor.read(cx).text(cx); - if !query.is_empty() { - this.selection.take(); - } - this.update_entries(cx); - if !query.is_empty() { - this.selection = this - .entries - .iter() - .position(|entry| !matches!(entry, ContactEntry::Header(_))); - } - } - }) - .detach(); - - cx.subscribe(&user_store, move |_, user_store, event, cx| { - if let Some(workspace) = workspace.upgrade(cx) { - workspace.update(cx, |workspace, cx| { - if let client::Event::Contact { user, kind } = event { - if let ContactEventKind::Requested | ContactEventKind::Accepted = kind { - workspace.show_notification(user.id as usize, cx, |cx| { - cx.add_view(|cx| { - ContactNotification::new(user.clone(), *kind, user_store, cx) - }) - }) - } - } - }); - } - - if let client::Event::ShowContacts = event { - cx.emit(Event::Activate); - } - }) - .detach(); - - let list_state = ListState::new(0, Orientation::Top, 1000., cx, move |this, ix, cx| { - let theme = cx.global::().theme.clone(); - let is_selected = this.selection == Some(ix); - - match &this.entries[ix] { - ContactEntry::Header(section) => { - let is_collapsed = this.collapsed_sections.contains(section); - Self::render_header( - *section, - &theme.contacts_panel, - is_selected, - is_collapsed, - cx, - ) - } - ContactEntry::IncomingRequest(user) => Self::render_contact_request( - user.clone(), - this.user_store.clone(), - &theme.contacts_panel, - true, - is_selected, - cx, - ), - ContactEntry::OutgoingRequest(user) => Self::render_contact_request( - user.clone(), - this.user_store.clone(), - &theme.contacts_panel, - false, - is_selected, - cx, - ), - ContactEntry::Contact(contact) => { - Self::render_contact(&contact.user, &theme.contacts_panel, is_selected) - } - } - }); - - let mut this = Self { - list_state, - selection: None, - collapsed_sections: Default::default(), - entries: Default::default(), - match_candidates: Default::default(), - filter_editor, - _maintain_contacts: cx.observe(&user_store, |this, _, cx| this.update_entries(cx)), - user_store, - }; - this.update_entries(cx); - this - } - - fn render_header( - section: Section, - theme: &theme::ContactsPanel, - is_selected: bool, - is_collapsed: bool, - cx: &mut RenderContext, - ) -> ElementBox { - enum Header {} - - let header_style = theme.header_row.style_for(Default::default(), is_selected); - let text = match section { - Section::Requests => "Requests", - Section::Online => "Online", - Section::Offline => "Offline", - }; - let icon_size = theme.section_icon_size; - MouseEventHandler::
::new(section as usize, cx, |_, _| { - Flex::row() - .with_child( - Svg::new(if is_collapsed { - "icons/chevron_right_8.svg" - } else { - "icons/chevron_down_8.svg" - }) - .with_color(header_style.text.color) - .constrained() - .with_max_width(icon_size) - .with_max_height(icon_size) - .aligned() - .constrained() - .with_width(icon_size) - .boxed(), - ) - .with_child( - Label::new(text.to_string(), header_style.text.clone()) - .aligned() - .left() - .contained() - .with_margin_left(theme.contact_username.container.margin.left) - .flex(1., true) - .boxed(), - ) - .constrained() - .with_height(theme.row_height) - .contained() - .with_style(header_style.container) - .boxed() - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, move |_, cx| { - cx.dispatch_action(ToggleExpanded(section)) - }) - .boxed() - } - - fn render_contact(user: &User, theme: &theme::ContactsPanel, is_selected: bool) -> ElementBox { - Flex::row() - .with_children(user.avatar.clone().map(|avatar| { - Image::new(avatar) - .with_style(theme.contact_avatar) - .aligned() - .left() - .boxed() - })) - .with_child( - Label::new( - user.github_login.clone(), - theme.contact_username.text.clone(), - ) - .contained() - .with_style(theme.contact_username.container) - .aligned() - .left() - .flex(1., true) - .boxed(), - ) - .constrained() - .with_height(theme.row_height) - .contained() - .with_style(*theme.contact_row.style_for(Default::default(), is_selected)) - .boxed() - } - - fn render_contact_request( - user: Arc, - user_store: ModelHandle, - theme: &theme::ContactsPanel, - is_incoming: bool, - is_selected: bool, - cx: &mut RenderContext, - ) -> ElementBox { - enum Decline {} - enum Accept {} - enum Cancel {} - - let mut row = Flex::row() - .with_children(user.avatar.clone().map(|avatar| { - Image::new(avatar) - .with_style(theme.contact_avatar) - .aligned() - .left() - .boxed() - })) - .with_child( - Label::new( - user.github_login.clone(), - theme.contact_username.text.clone(), - ) - .contained() - .with_style(theme.contact_username.container) - .aligned() - .left() - .flex(1., true) - .boxed(), - ); - - let user_id = user.id; - let is_contact_request_pending = user_store.read(cx).is_contact_request_pending(&user); - let button_spacing = theme.contact_button_spacing; - - if is_incoming { - row.add_children([ - MouseEventHandler::::new(user.id as usize, cx, |mouse_state, _| { - let button_style = if is_contact_request_pending { - &theme.disabled_button - } else { - theme.contact_button.style_for(mouse_state, false) - }; - render_icon_button(button_style, "icons/x_mark_8.svg") - .aligned() - // .flex_float() - .boxed() - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, move |_, cx| { - cx.dispatch_action(RespondToContactRequest { - user_id, - accept: false, - }) - }) - // .flex_float() - .contained() - .with_margin_right(button_spacing) - .boxed(), - MouseEventHandler::::new(user.id as usize, cx, |mouse_state, _| { - let button_style = if is_contact_request_pending { - &theme.disabled_button - } else { - theme.contact_button.style_for(mouse_state, false) - }; - render_icon_button(button_style, "icons/check_8.svg") - .aligned() - .flex_float() - .boxed() - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, move |_, cx| { - cx.dispatch_action(RespondToContactRequest { - user_id, - accept: true, - }) - }) - .boxed(), - ]); - } else { - row.add_child( - MouseEventHandler::::new(user.id as usize, cx, |mouse_state, _| { - let button_style = if is_contact_request_pending { - &theme.disabled_button - } else { - theme.contact_button.style_for(mouse_state, false) - }; - render_icon_button(button_style, "icons/x_mark_8.svg") - .aligned() - .flex_float() - .boxed() - }) - .with_padding(Padding::uniform(2.)) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, move |_, cx| { - cx.dispatch_action(RemoveContact(user_id)) - }) - .flex_float() - .boxed(), - ); - } - - row.constrained() - .with_height(theme.row_height) - .contained() - .with_style(*theme.contact_row.style_for(Default::default(), is_selected)) - .boxed() - } - - fn update_entries(&mut self, cx: &mut ViewContext) { - let user_store = self.user_store.read(cx); - let query = self.filter_editor.read(cx).text(cx); - let executor = cx.background().clone(); - - let prev_selected_entry = self.selection.and_then(|ix| self.entries.get(ix).cloned()); - self.entries.clear(); - - let mut request_entries = Vec::new(); - let incoming = user_store.incoming_contact_requests(); - if !incoming.is_empty() { - self.match_candidates.clear(); - self.match_candidates - .extend( - incoming - .iter() - .enumerate() - .map(|(ix, user)| StringMatchCandidate { - id: ix, - string: user.github_login.clone(), - char_bag: user.github_login.chars().collect(), - }), - ); - let matches = executor.block(match_strings( - &self.match_candidates, - &query, - true, - usize::MAX, - &Default::default(), - executor.clone(), - )); - request_entries.extend( - matches - .iter() - .map(|mat| ContactEntry::IncomingRequest(incoming[mat.candidate_id].clone())), - ); - } - - let outgoing = user_store.outgoing_contact_requests(); - if !outgoing.is_empty() { - self.match_candidates.clear(); - self.match_candidates - .extend( - outgoing - .iter() - .enumerate() - .map(|(ix, user)| StringMatchCandidate { - id: ix, - string: user.github_login.clone(), - char_bag: user.github_login.chars().collect(), - }), - ); - let matches = executor.block(match_strings( - &self.match_candidates, - &query, - true, - usize::MAX, - &Default::default(), - executor.clone(), - )); - request_entries.extend( - matches - .iter() - .map(|mat| ContactEntry::OutgoingRequest(outgoing[mat.candidate_id].clone())), - ); - } - - if !request_entries.is_empty() { - self.entries.push(ContactEntry::Header(Section::Requests)); - if !self.collapsed_sections.contains(&Section::Requests) { - self.entries.append(&mut request_entries); - } - } - - let current_user = user_store.current_user(); - - let contacts = user_store.contacts(); - if !contacts.is_empty() { - // Always put the current user first. - self.match_candidates.clear(); - self.match_candidates.reserve(contacts.len()); - self.match_candidates.push(StringMatchCandidate { - id: 0, - string: Default::default(), - char_bag: Default::default(), - }); - for (ix, contact) in contacts.iter().enumerate() { - let candidate = StringMatchCandidate { - id: ix, - string: contact.user.github_login.clone(), - char_bag: contact.user.github_login.chars().collect(), - }; - if current_user - .as_ref() - .map_or(false, |current_user| current_user.id == contact.user.id) - { - self.match_candidates[0] = candidate; - } else { - self.match_candidates.push(candidate); - } - } - if self.match_candidates[0].string.is_empty() { - self.match_candidates.remove(0); - } - - let matches = executor.block(match_strings( - &self.match_candidates, - &query, - true, - usize::MAX, - &Default::default(), - executor.clone(), - )); - - let (online_contacts, offline_contacts) = matches - .iter() - .partition::, _>(|mat| contacts[mat.candidate_id].online); - - for (matches, section) in [ - (online_contacts, Section::Online), - (offline_contacts, Section::Offline), - ] { - if !matches.is_empty() { - self.entries.push(ContactEntry::Header(section)); - if !self.collapsed_sections.contains(§ion) { - for mat in matches { - let contact = &contacts[mat.candidate_id]; - self.entries.push(ContactEntry::Contact(contact.clone())); - } - } - } - } - } - - if let Some(prev_selected_entry) = prev_selected_entry { - self.selection.take(); - for (ix, entry) in self.entries.iter().enumerate() { - if *entry == prev_selected_entry { - self.selection = Some(ix); - break; - } - } - } - - self.list_state.reset(self.entries.len()); - cx.notify(); - } - - fn request_contact(&mut self, request: &RequestContact, cx: &mut ViewContext) { - self.user_store - .update(cx, |store, cx| store.request_contact(request.0, cx)) - .detach(); - } - - fn remove_contact(&mut self, request: &RemoveContact, cx: &mut ViewContext) { - self.user_store - .update(cx, |store, cx| store.remove_contact(request.0, cx)) - .detach(); - } - - fn respond_to_contact_request( - &mut self, - action: &RespondToContactRequest, - cx: &mut ViewContext, - ) { - self.user_store - .update(cx, |store, cx| { - store.respond_to_contact_request(action.user_id, action.accept, cx) - }) - .detach(); - } - - fn clear_filter(&mut self, _: &Cancel, cx: &mut ViewContext) { - let did_clear = self.filter_editor.update(cx, |editor, cx| { - if editor.buffer().read(cx).len(cx) > 0 { - editor.set_text("", cx); - true - } else { - false - } - }); - if !did_clear { - cx.propagate_action(); - } - } - - fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext) { - if let Some(ix) = self.selection { - if self.entries.len() > ix + 1 { - self.selection = Some(ix + 1); - } - } else if !self.entries.is_empty() { - self.selection = Some(0); - } - cx.notify(); - self.list_state.reset(self.entries.len()); - } - - fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext) { - if let Some(ix) = self.selection { - if ix > 0 { - self.selection = Some(ix - 1); - } else { - self.selection = None; - } - } - cx.notify(); - self.list_state.reset(self.entries.len()); - } - - fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext) { - if let Some(selection) = self.selection { - if let Some(entry) = self.entries.get(selection) { - match entry { - ContactEntry::Header(section) => { - let section = *section; - self.toggle_expanded(&ToggleExpanded(section), cx); - } - _ => {} - } - } - } - } - - fn toggle_expanded(&mut self, action: &ToggleExpanded, cx: &mut ViewContext) { - let section = action.0; - if let Some(ix) = self.collapsed_sections.iter().position(|s| *s == section) { - self.collapsed_sections.remove(ix); - } else { - self.collapsed_sections.push(section); - } - self.update_entries(cx); - } -} - -impl SidebarItem for ContactsPanel { - fn should_show_badge(&self, cx: &AppContext) -> bool { - !self - .user_store - .read(cx) - .incoming_contact_requests() - .is_empty() - } - - fn contains_focused_view(&self, cx: &AppContext) -> bool { - self.filter_editor.is_focused(cx) - } - - fn should_activate_item_on_event(&self, event: &Event, _: &AppContext) -> bool { - matches!(event, Event::Activate) - } -} - -fn render_icon_button(style: &IconButton, svg_path: &'static str) -> impl Element { - Svg::new(svg_path) - .with_color(style.color) - .constrained() - .with_width(style.icon_width) - .aligned() - .contained() - .with_style(style.container) - .constrained() - .with_width(style.button_width) - .with_height(style.button_width) -} - -pub enum Event { - Activate, -} - -impl Entity for ContactsPanel { - type Event = Event; -} - -impl View for ContactsPanel { - fn ui_name() -> &'static str { - "ContactsPanel" - } - - fn render(&mut self, cx: &mut RenderContext) -> ElementBox { - enum AddContact {} - - let theme = cx.global::().theme.clone(); - let theme = &theme.contacts_panel; - Container::new( - Flex::column() - .with_child( - Flex::row() - .with_child( - ChildView::new(self.filter_editor.clone()) - .contained() - .with_style(theme.user_query_editor.container) - .flex(1., true) - .boxed(), - ) - .with_child( - MouseEventHandler::::new(0, cx, |_, _| { - Svg::new("icons/user_plus_16.svg") - .with_color(theme.add_contact_button.color) - .constrained() - .with_height(16.) - .contained() - .with_style(theme.add_contact_button.container) - .aligned() - .boxed() - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, |_, cx| { - cx.dispatch_action(contact_finder::Toggle) - }) - .boxed(), - ) - .constrained() - .with_height(theme.user_query_editor_height) - .boxed(), - ) - .with_child(List::new(self.list_state.clone()).flex(1., false).boxed()) - .with_children( - self.user_store - .read(cx) - .invite_info() - .cloned() - .and_then(|info| { - enum InviteLink {} - - if info.count > 0 { - Some( - MouseEventHandler::::new(0, cx, |state, cx| { - let style = - theme.invite_row.style_for(state, false).clone(); - - let copied = - cx.read_from_clipboard().map_or(false, |item| { - item.text().as_str() == info.url.as_ref() - }); - - Label::new( - format!( - "{} invite link ({} left)", - if copied { "Copied" } else { "Copy" }, - info.count - ), - style.label.clone(), - ) - .aligned() - .left() - .constrained() - .with_height(theme.row_height) - .contained() - .with_style(style.container) - .boxed() - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, move |_, cx| { - cx.write_to_clipboard(ClipboardItem::new( - info.url.to_string(), - )); - cx.notify(); - }) - .boxed(), - ) - } else { - None - } - }), - ) - .boxed(), - ) - .with_style(theme.container) - .boxed() - } - - fn on_focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext) { - cx.focus(&self.filter_editor); - } - - fn keymap_context(&self, _: &gpui::AppContext) -> gpui::keymap::Context { - let mut cx = Self::default_keymap_context(); - cx.set.insert("menu".into()); - cx - } -} - -impl PartialEq for ContactEntry { - fn eq(&self, other: &Self) -> bool { - match self { - ContactEntry::Header(section_1) => { - if let ContactEntry::Header(section_2) = other { - return section_1 == section_2; - } - } - ContactEntry::IncomingRequest(user_1) => { - if let ContactEntry::IncomingRequest(user_2) = other { - return user_1.id == user_2.id; - } - } - ContactEntry::OutgoingRequest(user_1) => { - if let ContactEntry::OutgoingRequest(user_2) = other { - return user_1.id == user_2.id; - } - } - ContactEntry::Contact(contact_1) => { - if let ContactEntry::Contact(contact_2) = other { - return contact_1.user.id == contact_2.user.id; - } - } - } - false - } -} - -#[cfg(test)] -mod tests { - use super::*; - use client::{ - proto, - test::{FakeHttpClient, FakeServer}, - Client, - }; - use collections::HashSet; - use gpui::TestAppContext; - use language::LanguageRegistry; - use project::{FakeFs, Project, ProjectStore}; - - #[gpui::test] - async fn test_contact_panel(cx: &mut TestAppContext) { - Settings::test_async(cx); - let current_user_id = 100; - - let languages = Arc::new(LanguageRegistry::test()); - let http_client = FakeHttpClient::with_404_response(); - let client = Client::new(http_client.clone()); - let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx)); - let project_store = cx.add_model(|_| ProjectStore::new()); - let server = FakeServer::for_client(current_user_id, &client, cx).await; - let fs = FakeFs::new(cx.background()); - let project = cx.update(|cx| { - Project::local( - client.clone(), - user_store.clone(), - project_store.clone(), - languages, - fs, - cx, - ) - }); - - let (_, workspace) = - cx.add_window(|cx| Workspace::new(project.clone(), |_, _| unimplemented!(), cx)); - let panel = cx.add_view(&workspace, |cx| { - ContactsPanel::new(user_store.clone(), workspace.downgrade(), cx) - }); - - workspace.update(cx, |_, cx| { - cx.observe(&panel, |_, panel, cx| { - let entries = render_to_strings(&panel, cx); - assert!( - entries.iter().collect::>().len() == entries.len(), - "Duplicate contact panel entries {:?}", - entries - ) - }) - .detach(); - }); - - let get_users_request = server.receive::().await.unwrap(); - server - .respond( - get_users_request.receipt(), - proto::UsersResponse { - users: [ - "user_zero", - "user_one", - "user_two", - "user_three", - "user_four", - "user_five", - ] - .into_iter() - .enumerate() - .map(|(id, name)| proto::User { - id: id as u64, - github_login: name.to_string(), - ..Default::default() - }) - .chain([proto::User { - id: current_user_id, - github_login: "the_current_user".to_string(), - ..Default::default() - }]) - .collect(), - }, - ) - .await; - - server.send(proto::UpdateContacts { - incoming_requests: vec![proto::IncomingContactRequest { - requester_id: 1, - should_notify: false, - }], - outgoing_requests: vec![2], - contacts: vec![ - proto::Contact { - user_id: 3, - online: true, - should_notify: false, - }, - proto::Contact { - user_id: 4, - online: true, - should_notify: false, - }, - proto::Contact { - user_id: 5, - online: false, - should_notify: false, - }, - proto::Contact { - user_id: current_user_id, - online: true, - should_notify: false, - }, - ], - ..Default::default() - }); - - cx.foreground().run_until_parked(); - assert_eq!( - cx.read(|cx| render_to_strings(&panel, cx)), - &[ - "v Requests", - " incoming user_one", - " outgoing user_two", - "v Online", - " the_current_user", - " user_four", - " user_three", - "v Offline", - " user_five", - ] - ); - - panel.update(cx, |panel, cx| { - panel - .filter_editor - .update(cx, |editor, cx| editor.set_text("f", cx)) - }); - cx.foreground().run_until_parked(); - assert_eq!( - cx.read(|cx| render_to_strings(&panel, cx)), - &[ - "v Online", - " user_four <=== selected", - "v Offline", - " user_five", - ] - ); - - panel.update(cx, |panel, cx| { - panel.select_next(&Default::default(), cx); - }); - assert_eq!( - cx.read(|cx| render_to_strings(&panel, cx)), - &[ - "v Online", - " user_four", - "v Offline <=== selected", - " user_five", - ] - ); - - panel.update(cx, |panel, cx| { - panel.select_next(&Default::default(), cx); - }); - assert_eq!( - cx.read(|cx| render_to_strings(&panel, cx)), - &[ - "v Online", - " user_four", - "v Offline", - " user_five <=== selected", - ] - ); - } - - fn render_to_strings(panel: &ViewHandle, cx: &AppContext) -> Vec { - let panel = panel.read(cx); - let mut entries = Vec::new(); - entries.extend(panel.entries.iter().enumerate().map(|(ix, entry)| { - let mut string = match entry { - ContactEntry::Header(name) => { - let icon = if panel.collapsed_sections.contains(name) { - ">" - } else { - "v" - }; - format!("{} {:?}", icon, name) - } - ContactEntry::IncomingRequest(user) => { - format!(" incoming {}", user.github_login) - } - ContactEntry::OutgoingRequest(user) => { - format!(" outgoing {}", user.github_login) - } - ContactEntry::Contact(contact) => { - format!(" {}", contact.user.github_login) - } - }; - - if panel.selection == Some(ix) { - string.push_str(" <=== selected"); - } - - string - })); - entries - } -} diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index bc7e6f09951257974290cc2ee3762b60995c62e3..175c523e539431c51a1b920ad0689cf3ec1448df 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -78,6 +78,7 @@ pub struct Titlebar { pub outdated_warning: ContainedText, pub share_button: Interactive, pub toggle_contacts_button: Interactive, + pub toggle_contacts_badge: ContainerStyle, pub contacts_popover: AddParticipantPopover, } diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index f2eb7653533cd9a24579afd13ea636b0a915bc55..c0b43dca8e8ddd654a0a5cf6f992fee667c21333 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -28,7 +28,6 @@ command_palette = { path = "../command_palette" } context_menu = { path = "../context_menu" } client = { path = "../client" } clock = { path = "../clock" } -contacts_panel = { path = "../contacts_panel" } diagnostics = { path = "../diagnostics" } editor = { path = "../editor" } file_finder = { path = "../file_finder" } diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 580493f6d0f321c934c3c71b538fb579304f701e..dc953bae8c85d43ebc8f830a4f242f0600963081 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -112,7 +112,6 @@ fn main() { go_to_line::init(cx); file_finder::init(cx); chat_panel::init(cx); - contacts_panel::init(cx); outline::init(cx); project_symbols::init(cx); project_panel::init(cx); diff --git a/crates/zed/src/menus.rs b/crates/zed/src/menus.rs index 3a34166ba62c98aa145ef0daba0227c9dbd15796..835519fb5c5c1494c715abcae78ba7081fd99d1d 100644 --- a/crates/zed/src/menus.rs +++ b/crates/zed/src/menus.rs @@ -244,10 +244,6 @@ pub fn menus() -> Vec> { name: "Project Panel", action: Box::new(project_panel::ToggleFocus), }, - MenuItem::Action { - name: "Contacts Panel", - action: Box::new(contacts_panel::ToggleFocus), - }, MenuItem::Action { name: "Command Palette", action: Box::new(command_palette::Toggle), diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index d41b9284c4d768b6f40d29cb6c4d3ece30355f03..28a1249c12c601e48a82c58fd711c59d360ae9a4 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -12,8 +12,6 @@ use breadcrumbs::Breadcrumbs; pub use client; use collab_ui::CollabTitlebarItem; use collections::VecDeque; -pub use contacts_panel; -use contacts_panel::ContactsPanel; pub use editor; use editor::{Editor, MultiBuffer}; use gpui::{ @@ -208,13 +206,6 @@ pub fn init(app_state: &Arc, cx: &mut gpui::MutableAppContext) { workspace.toggle_sidebar_item_focus(SidebarSide::Left, 0, cx); }, ); - cx.add_action( - |workspace: &mut Workspace, - _: &contacts_panel::ToggleFocus, - cx: &mut ViewContext| { - workspace.toggle_sidebar_item_focus(SidebarSide::Right, 0, cx); - }, - ); activity_indicator::init(cx); call::init(app_state.client.clone(), app_state.user_store.clone(), cx); @@ -281,14 +272,11 @@ pub fn initialize_workspace( })); }); - let collab_titlebar_item = cx.add_view(|cx| CollabTitlebarItem::new(&workspace_handle, cx)); + let collab_titlebar_item = + cx.add_view(|cx| CollabTitlebarItem::new(&workspace_handle, &app_state.user_store, cx)); workspace.set_titlebar_item(collab_titlebar_item, cx); let project_panel = ProjectPanel::new(workspace.project().clone(), cx); - let contact_panel = cx.add_view(|cx| { - ContactsPanel::new(app_state.user_store.clone(), workspace.weak_handle(), cx) - }); - workspace.left_sidebar().update(cx, |sidebar, cx| { sidebar.add_item( "icons/folder_tree_16.svg", @@ -297,14 +285,6 @@ pub fn initialize_workspace( cx, ) }); - workspace.right_sidebar().update(cx, |sidebar, cx| { - sidebar.add_item( - "icons/user_group_16.svg", - "Contacts Panel".to_string(), - contact_panel, - cx, - ) - }); let diagnostic_summary = cx.add_view(|cx| diagnostics::items::DiagnosticIndicator::new(workspace.project(), cx)); diff --git a/styles/src/styleTree/workspace.ts b/styles/src/styleTree/workspace.ts index 0877f131c144a487cd6abe6d1b19dce321de54d7..65531e6ec937676776533ffe782353e86343d4e8 100644 --- a/styles/src/styleTree/workspace.ts +++ b/styles/src/styleTree/workspace.ts @@ -144,6 +144,13 @@ export default function workspace(theme: Theme) { color: iconColor(theme, "active"), }, }, + toggleContactsBadge: { + cornerRadius: 3, + padding: 2, + margin: { top: 3, left: 3 }, + border: { width: 1, color: workspaceBackground(theme) }, + background: iconColor(theme, "feature"), + }, shareButton: { ...titlebarButton }, From c43956d70a91a3c71345fbc6297aa84f781969b9 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 6 Oct 2022 14:07:21 +0200 Subject: [PATCH 061/112] Move contacts panel styles into contacts popover --- crates/collab_ui/src/contacts_popover.rs | 36 ++-- .../src/incoming_call_notification.rs | 6 +- crates/theme/src/theme.rs | 45 ++--- styles/src/styleTree/contactsPanel.ts | 165 ------------------ styles/src/styleTree/contactsPopover.ts | 161 ++++++++++++++++- styles/src/styleTree/workspace.ts | 10 -- 6 files changed, 198 insertions(+), 225 deletions(-) delete mode 100644 styles/src/styleTree/contactsPanel.ts diff --git a/crates/collab_ui/src/contacts_popover.rs b/crates/collab_ui/src/contacts_popover.rs index f3ebf3abea5dec93b94496f5cfe8b96150359fca..388b344879d02f4f38dc33e2216d5911b23a30d1 100644 --- a/crates/collab_ui/src/contacts_popover.rs +++ b/crates/collab_ui/src/contacts_popover.rs @@ -118,7 +118,7 @@ impl ContactsPopover { ) -> Self { let filter_editor = cx.add_view(|cx| { let mut editor = Editor::single_line( - Some(|theme| theme.contacts_panel.user_query_editor.clone()), + Some(|theme| theme.contacts_popover.user_query_editor.clone()), cx, ); editor.set_placeholder_text("Filter contacts", cx); @@ -151,7 +151,7 @@ impl ContactsPopover { let is_collapsed = this.collapsed_sections.contains(section); Self::render_header( *section, - &theme.contacts_panel, + &theme.contacts_popover, is_selected, is_collapsed, cx, @@ -160,7 +160,7 @@ impl ContactsPopover { ContactEntry::IncomingRequest(user) => Self::render_contact_request( user.clone(), this.user_store.clone(), - &theme.contacts_panel, + &theme.contacts_popover, true, is_selected, cx, @@ -168,7 +168,7 @@ impl ContactsPopover { ContactEntry::OutgoingRequest(user) => Self::render_contact_request( user.clone(), this.user_store.clone(), - &theme.contacts_panel, + &theme.contacts_popover, false, is_selected, cx, @@ -176,7 +176,7 @@ impl ContactsPopover { ContactEntry::Contact(contact) => Self::render_contact( contact, &this.project, - &theme.contacts_panel, + &theme.contacts_popover, is_selected, cx, ), @@ -418,7 +418,7 @@ impl ContactsPopover { fn render_active_call(&self, cx: &mut RenderContext) -> Option { let room = ActiveCall::global(cx).read(cx).room()?; - let theme = &cx.global::().theme.contacts_panel; + let theme = &cx.global::().theme.contacts_popover; Some( Flex::column() @@ -455,7 +455,7 @@ impl ContactsPopover { fn render_header( section: Section, - theme: &theme::ContactsPanel, + theme: &theme::ContactsPopover, is_selected: bool, is_collapsed: bool, cx: &mut RenderContext, @@ -511,7 +511,7 @@ impl ContactsPopover { fn render_contact( contact: &Contact, project: &ModelHandle, - theme: &theme::ContactsPanel, + theme: &theme::ContactsPopover, is_selected: bool, cx: &mut RenderContext, ) -> ElementBox { @@ -565,7 +565,7 @@ impl ContactsPopover { fn render_contact_request( user: Arc, user_store: ModelHandle, - theme: &theme::ContactsPanel, + theme: &theme::ContactsPopover, is_incoming: bool, is_selected: bool, cx: &mut RenderContext, @@ -705,18 +705,18 @@ impl View for ContactsPopover { .with_child( ChildView::new(self.filter_editor.clone()) .contained() - .with_style(theme.contacts_panel.user_query_editor.container) + .with_style(theme.contacts_popover.user_query_editor.container) .flex(1., true) .boxed(), ) .with_child( MouseEventHandler::::new(0, cx, |_, _| { Svg::new("icons/user_plus_16.svg") - .with_color(theme.contacts_panel.add_contact_button.color) + .with_color(theme.contacts_popover.add_contact_button.color) .constrained() .with_height(16.) .contained() - .with_style(theme.contacts_panel.add_contact_button.container) + .with_style(theme.contacts_popover.add_contact_button.container) .aligned() .boxed() }) @@ -727,7 +727,7 @@ impl View for ContactsPopover { .boxed(), ) .constrained() - .with_height(theme.contacts_panel.user_query_editor_height) + .with_height(theme.contacts_popover.user_query_editor_height) .boxed(), ) .with_children(self.render_active_call(cx)) @@ -744,7 +744,7 @@ impl View for ContactsPopover { Some( MouseEventHandler::::new(0, cx, |state, cx| { let style = theme - .contacts_panel + .contacts_popover .invite_row .style_for(state, false) .clone(); @@ -764,7 +764,7 @@ impl View for ContactsPopover { .aligned() .left() .constrained() - .with_height(theme.contacts_panel.row_height) + .with_height(theme.contacts_popover.row_height) .contained() .with_style(style.container) .boxed() @@ -782,10 +782,10 @@ impl View for ContactsPopover { }), ) .contained() - .with_style(theme.workspace.titlebar.contacts_popover.container) + .with_style(theme.contacts_popover.container) .constrained() - .with_width(theme.workspace.titlebar.contacts_popover.width) - .with_height(theme.workspace.titlebar.contacts_popover.height) + .with_width(theme.contacts_popover.width) + .with_height(theme.contacts_popover.height) .boxed() } diff --git a/crates/collab_ui/src/incoming_call_notification.rs b/crates/collab_ui/src/incoming_call_notification.rs index 8860097a592f9af87ec136d25f6f756e827fd5bc..0581859ea9ed8da03cce0f5320fe278cbdec943f 100644 --- a/crates/collab_ui/src/incoming_call_notification.rs +++ b/crates/collab_ui/src/incoming_call_notification.rs @@ -82,7 +82,7 @@ impl IncomingCallNotification { } fn render_caller(&self, cx: &mut RenderContext) -> ElementBox { - let theme = &cx.global::().theme.contacts_panel; + let theme = &cx.global::().theme.contacts_popover; Flex::row() .with_children( self.call @@ -108,7 +108,7 @@ impl IncomingCallNotification { Flex::row() .with_child( MouseEventHandler::::new(0, cx, |_, cx| { - let theme = &cx.global::().theme.contacts_panel; + let theme = &cx.global::().theme.contacts_popover; Label::new("Accept".to_string(), theme.contact_username.text.clone()).boxed() }) .on_click(MouseButton::Left, |_, cx| { @@ -118,7 +118,7 @@ impl IncomingCallNotification { ) .with_child( MouseEventHandler::::new(0, cx, |_, cx| { - let theme = &cx.global::().theme.contacts_panel; + let theme = &cx.global::().theme.contacts_popover; Label::new("Decline".to_string(), theme.contact_username.text.clone()).boxed() }) .on_click(MouseButton::Left, |_, cx| { diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 175c523e539431c51a1b920ad0689cf3ec1448df..b9de72065a54ea76c1350bc08703a614783a0cb4 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -19,7 +19,7 @@ pub struct Theme { pub workspace: Workspace, pub context_menu: ContextMenu, pub chat_panel: ChatPanel, - pub contacts_panel: ContactsPanel, + pub contacts_popover: ContactsPopover, pub contact_finder: ContactFinder, pub project_panel: ProjectPanel, pub command_palette: CommandPalette, @@ -79,15 +79,30 @@ pub struct Titlebar { pub share_button: Interactive, pub toggle_contacts_button: Interactive, pub toggle_contacts_badge: ContainerStyle, - pub contacts_popover: AddParticipantPopover, } -#[derive(Clone, Deserialize, Default)] -pub struct AddParticipantPopover { +#[derive(Deserialize, Default)] +pub struct ContactsPopover { #[serde(flatten)] pub container: ContainerStyle, pub height: f32, pub width: f32, + pub user_query_editor: FieldEditor, + pub user_query_editor_height: f32, + pub add_contact_button: IconButton, + pub header_row: Interactive, + pub contact_row: Interactive, + pub project_row: Interactive, + pub row_height: f32, + pub contact_avatar: ImageStyle, + pub contact_username: ContainedText, + pub contact_button: Interactive, + pub contact_button_spacing: f32, + pub disabled_button: IconButton, + pub tree_branch: Interactive, + pub private_button: Interactive, + pub section_icon_size: f32, + pub invite_row: Interactive, } #[derive(Clone, Deserialize, Default)] @@ -329,28 +344,6 @@ pub struct CommandPalette { pub keystroke_spacing: f32, } -#[derive(Deserialize, Default)] -pub struct ContactsPanel { - #[serde(flatten)] - pub container: ContainerStyle, - pub user_query_editor: FieldEditor, - pub user_query_editor_height: f32, - pub add_contact_button: IconButton, - pub header_row: Interactive, - pub contact_row: Interactive, - pub project_row: Interactive, - pub row_height: f32, - pub contact_avatar: ImageStyle, - pub contact_username: ContainedText, - pub contact_button: Interactive, - pub contact_button_spacing: f32, - pub disabled_button: IconButton, - pub tree_branch: Interactive, - pub private_button: Interactive, - pub section_icon_size: f32, - pub invite_row: Interactive, -} - #[derive(Deserialize, Default)] pub struct InviteLink { #[serde(flatten)] diff --git a/styles/src/styleTree/contactsPanel.ts b/styles/src/styleTree/contactsPanel.ts deleted file mode 100644 index 20fce729e434ac98d2a9959c1b453f5b2e6f8c37..0000000000000000000000000000000000000000 --- a/styles/src/styleTree/contactsPanel.ts +++ /dev/null @@ -1,165 +0,0 @@ -import Theme from "../themes/common/theme"; -import { panel } from "./app"; -import { - backgroundColor, - border, - borderColor, - iconColor, - player, - text, -} from "./components"; - -export default function contactsPanel(theme: Theme) { - const nameMargin = 8; - const sidePadding = 12; - - const projectRow = { - guestAvatarSpacing: 4, - height: 24, - guestAvatar: { - cornerRadius: 8, - width: 14, - }, - name: { - ...text(theme, "mono", "placeholder", { size: "sm" }), - margin: { - left: nameMargin, - right: 6, - }, - }, - guests: { - margin: { - left: nameMargin, - right: nameMargin, - }, - }, - padding: { - left: sidePadding, - right: sidePadding, - }, - }; - - const contactButton = { - background: backgroundColor(theme, 100), - color: iconColor(theme, "primary"), - iconWidth: 8, - buttonWidth: 16, - cornerRadius: 8, - }; - - return { - ...panel, - padding: { top: panel.padding.top, bottom: 0 }, - userQueryEditor: { - background: backgroundColor(theme, 500), - cornerRadius: 6, - text: text(theme, "mono", "primary"), - placeholderText: text(theme, "mono", "placeholder", { size: "sm" }), - selection: player(theme, 1).selection, - border: border(theme, "secondary"), - padding: { - bottom: 4, - left: 8, - right: 8, - top: 4, - }, - margin: { - left: sidePadding, - right: sidePadding, - }, - }, - userQueryEditorHeight: 32, - addContactButton: { - margin: { left: 6, right: 12 }, - color: iconColor(theme, "primary"), - buttonWidth: 16, - iconWidth: 16, - }, - privateButton: { - iconWidth: 12, - color: iconColor(theme, "primary"), - cornerRadius: 5, - buttonWidth: 12, - }, - rowHeight: 28, - sectionIconSize: 8, - headerRow: { - ...text(theme, "mono", "secondary", { size: "sm" }), - margin: { top: 14 }, - padding: { - left: sidePadding, - right: sidePadding, - }, - active: { - ...text(theme, "mono", "primary", { size: "sm" }), - background: backgroundColor(theme, 100, "active"), - }, - }, - contactRow: { - padding: { - left: sidePadding, - right: sidePadding, - }, - active: { - background: backgroundColor(theme, 100, "active"), - }, - }, - treeBranch: { - color: borderColor(theme, "active"), - width: 1, - hover: { - color: borderColor(theme, "active"), - }, - active: { - color: borderColor(theme, "active"), - }, - }, - contactAvatar: { - cornerRadius: 10, - width: 18, - }, - contactUsername: { - ...text(theme, "mono", "primary", { size: "sm" }), - margin: { - left: nameMargin, - }, - }, - contactButtonSpacing: nameMargin, - contactButton: { - ...contactButton, - hover: { - background: backgroundColor(theme, "on300", "hovered"), - }, - }, - disabledButton: { - ...contactButton, - background: backgroundColor(theme, 100), - color: iconColor(theme, "muted"), - }, - projectRow: { - ...projectRow, - background: backgroundColor(theme, 300), - name: { - ...projectRow.name, - ...text(theme, "mono", "secondary", { size: "sm" }), - }, - hover: { - background: backgroundColor(theme, 300, "hovered"), - }, - active: { - background: backgroundColor(theme, 300, "active"), - }, - }, - inviteRow: { - padding: { - left: sidePadding, - right: sidePadding, - }, - border: { top: true, width: 1, color: borderColor(theme, "primary") }, - text: text(theme, "sans", "secondary", { size: "sm" }), - hover: { - text: text(theme, "sans", "active", { size: "sm" }), - }, - }, - }; -} diff --git a/styles/src/styleTree/contactsPopover.ts b/styles/src/styleTree/contactsPopover.ts index e9de5dddaf627120d12ab452137fbd47508771a5..82174fd6726e6242f8f2db8aed35949c2f3089b2 100644 --- a/styles/src/styleTree/contactsPopover.ts +++ b/styles/src/styleTree/contactsPopover.ts @@ -1,8 +1,163 @@ import Theme from "../themes/common/theme"; -import { backgroundColor } from "./components"; +import { backgroundColor, border, borderColor, iconColor, player, popoverShadow, text } from "./components"; + +export default function contactsPopover(theme: Theme) { + const nameMargin = 8; + const sidePadding = 12; + + const projectRow = { + guestAvatarSpacing: 4, + height: 24, + guestAvatar: { + cornerRadius: 8, + width: 14, + }, + name: { + ...text(theme, "mono", "placeholder", { size: "sm" }), + margin: { + left: nameMargin, + right: 6, + }, + }, + guests: { + margin: { + left: nameMargin, + right: nameMargin, + }, + }, + padding: { + left: sidePadding, + right: sidePadding, + }, + }; + + const contactButton = { + background: backgroundColor(theme, 100), + color: iconColor(theme, "primary"), + iconWidth: 8, + buttonWidth: 16, + cornerRadius: 8, + }; -export default function workspace(theme: Theme) { return { - background: backgroundColor(theme, 300), + background: backgroundColor(theme, 300, "base"), + cornerRadius: 6, + padding: { top: 6 }, + shadow: popoverShadow(theme), + border: border(theme, "primary"), + margin: { top: -5 }, + width: 250, + height: 300, + userQueryEditor: { + background: backgroundColor(theme, 500), + cornerRadius: 6, + text: text(theme, "mono", "primary"), + placeholderText: text(theme, "mono", "placeholder", { size: "sm" }), + selection: player(theme, 1).selection, + border: border(theme, "secondary"), + padding: { + bottom: 4, + left: 8, + right: 8, + top: 4, + }, + margin: { + left: sidePadding, + right: sidePadding, + }, + }, + userQueryEditorHeight: 32, + addContactButton: { + margin: { left: 6, right: 12 }, + color: iconColor(theme, "primary"), + buttonWidth: 16, + iconWidth: 16, + }, + privateButton: { + iconWidth: 12, + color: iconColor(theme, "primary"), + cornerRadius: 5, + buttonWidth: 12, + }, + rowHeight: 28, + sectionIconSize: 8, + headerRow: { + ...text(theme, "mono", "secondary", { size: "sm" }), + margin: { top: 14 }, + padding: { + left: sidePadding, + right: sidePadding, + }, + active: { + ...text(theme, "mono", "primary", { size: "sm" }), + background: backgroundColor(theme, 100, "active"), + }, + }, + contactRow: { + padding: { + left: sidePadding, + right: sidePadding, + }, + active: { + background: backgroundColor(theme, 100, "active"), + }, + }, + treeBranch: { + color: borderColor(theme, "active"), + width: 1, + hover: { + color: borderColor(theme, "active"), + }, + active: { + color: borderColor(theme, "active"), + }, + }, + contactAvatar: { + cornerRadius: 10, + width: 18, + }, + contactUsername: { + ...text(theme, "mono", "primary", { size: "sm" }), + margin: { + left: nameMargin, + }, + }, + contactButtonSpacing: nameMargin, + contactButton: { + ...contactButton, + hover: { + background: backgroundColor(theme, "on300", "hovered"), + }, + }, + disabledButton: { + ...contactButton, + background: backgroundColor(theme, 100), + color: iconColor(theme, "muted"), + }, + projectRow: { + ...projectRow, + background: backgroundColor(theme, 300), + name: { + ...projectRow.name, + ...text(theme, "mono", "secondary", { size: "sm" }), + }, + hover: { + background: backgroundColor(theme, 300, "hovered"), + }, + active: { + background: backgroundColor(theme, 300, "active"), + }, + }, + inviteRow: { + padding: { + left: sidePadding, + right: sidePadding, + }, + border: { top: true, width: 1, color: borderColor(theme, "primary") }, + text: text(theme, "sans", "secondary", { size: "sm" }), + hover: { + text: text(theme, "sans", "active", { size: "sm" }), + }, + }, } } diff --git a/styles/src/styleTree/workspace.ts b/styles/src/styleTree/workspace.ts index 65531e6ec937676776533ffe782353e86343d4e8..7ad99ef6ab50d9c4c3b4e95b387f8012dc5e7cba 100644 --- a/styles/src/styleTree/workspace.ts +++ b/styles/src/styleTree/workspace.ts @@ -153,16 +153,6 @@ export default function workspace(theme: Theme) { }, shareButton: { ...titlebarButton - }, - contactsPopover: { - background: backgroundColor(theme, 300, "base"), - cornerRadius: 6, - padding: { top: 6 }, - shadow: popoverShadow(theme), - border: border(theme, "primary"), - margin: { top: -5 }, - width: 250, - height: 300 } }, toolbar: { From 2e84fc673798e5ab1e0ffc19f496598bec7d15e5 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 6 Oct 2022 14:20:40 +0200 Subject: [PATCH 062/112] Delete rooms without pending users or participants --- crates/collab/src/integration_tests.rs | 21 +++++++++++++++++++++ crates/collab/src/rpc.rs | 4 +++- crates/collab/src/rpc/store.rs | 23 ++++++++++++++++++----- 3 files changed, 42 insertions(+), 6 deletions(-) diff --git a/crates/collab/src/integration_tests.rs b/crates/collab/src/integration_tests.rs index 1adac8b28e7756393c4de321f38c64f84935e397..a11d908cdd77a17f85c30b665c78aa193407b5d1 100644 --- a/crates/collab/src/integration_tests.rs +++ b/crates/collab/src/integration_tests.rs @@ -203,6 +203,27 @@ async fn test_basic_calls( pending: Default::default() } ); + + // User B leaves the room. + active_call_b.update(cx_b, |call, cx| { + call.hang_up(cx).unwrap(); + assert!(call.room().is_none()); + }); + deterministic.run_until_parked(); + assert_eq!( + room_participants(&room_a, cx_a), + RoomParticipants { + remote: Default::default(), + pending: Default::default() + } + ); + assert_eq!( + room_participants(&room_b, cx_b), + RoomParticipants { + remote: Default::default(), + pending: Default::default() + } + ); } #[gpui::test(iterations = 10)] diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 4098e6522aa88df83afbe13a4454f50d727e0ede..46bf4bf3dcba9240a3f74aea26d491e8dc9b2a0d 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -646,7 +646,9 @@ impl Server { } } - self.room_updated(left_room.room); + if let Some(room) = left_room.room { + self.room_updated(room); + } Ok(()) } diff --git a/crates/collab/src/rpc/store.rs b/crates/collab/src/rpc/store.rs index 2f95851bf7aa9bd05f477e127ed1a2ff7200492d..a0d272ccc8b7ccf335a21d3cca1090315fdde0f3 100644 --- a/crates/collab/src/rpc/store.rs +++ b/crates/collab/src/rpc/store.rs @@ -101,7 +101,7 @@ pub struct LeftProject { } pub struct LeftRoom<'a> { - pub room: &'a proto::Room, + pub room: Option<&'a proto::Room>, pub unshared_projects: Vec, pub left_projects: Vec, } @@ -222,7 +222,8 @@ impl Store { let connected_user = self.connected_users.get_mut(&user_id).unwrap(); connected_user.connection_ids.remove(&connection_id); if let Some(active_call) = connected_user.active_call.as_ref() { - if let Some(room) = self.rooms.get_mut(&active_call.room_id) { + let room_id = active_call.room_id; + if let Some(room) = self.rooms.get_mut(&room_id) { let prev_participant_count = room.participants.len(); room.participants .retain(|participant| participant.peer_id != connection_id.0); @@ -230,13 +231,17 @@ impl Store { if connected_user.connection_ids.is_empty() { room.pending_user_ids .retain(|pending_user_id| *pending_user_id != user_id.to_proto()); - result.room_id = Some(active_call.room_id); + result.room_id = Some(room_id); connected_user.active_call = None; } } else { - result.room_id = Some(active_call.room_id); + result.room_id = Some(room_id); connected_user.active_call = None; } + + if room.participants.is_empty() && room.pending_user_ids.is_empty() { + self.rooms.remove(&room_id); + } } else { tracing::error!("disconnected user claims to be in a room that does not exist"); connected_user.active_call = None; @@ -476,9 +481,12 @@ impl Store { .ok_or_else(|| anyhow!("no such room"))?; room.participants .retain(|participant| participant.peer_id != connection_id.0); + if room.participants.is_empty() && room.pending_user_ids.is_empty() { + self.rooms.remove(&room_id); + } Ok(LeftRoom { - room, + room: self.rooms.get(&room_id), unshared_projects, left_projects, }) @@ -1069,6 +1077,11 @@ impl Store { ); } } + + assert!( + !room.pending_user_ids.is_empty() || !room.participants.is_empty(), + "room can't be empty" + ); } for (project_id, project) in &self.projects { From 4cb306fbf36bf28afd73725712332e69faedcea5 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 6 Oct 2022 15:12:27 +0200 Subject: [PATCH 063/112] Implement call cancellation --- crates/call/src/call.rs | 20 ++++++- crates/collab/src/integration_tests.rs | 82 ++++++++++++++++++++++++++ crates/collab/src/rpc.rs | 25 +++++++- crates/collab/src/rpc/store.rs | 45 ++++++++++++++ crates/rpc/proto/zed.proto | 9 ++- crates/rpc/src/proto.rs | 2 + 6 files changed, 176 insertions(+), 7 deletions(-) diff --git a/crates/call/src/call.rs b/crates/call/src/call.rs index 7cd8896bf6ee5568f7acf0deea1d80a201b1c6bc..1f4f7633e1f3696d1009c96daf0a59065b1aa587 100644 --- a/crates/call/src/call.rs +++ b/crates/call/src/call.rs @@ -52,7 +52,7 @@ impl ActiveCall { incoming_call: watch::channel(), _subscriptions: vec![ client.add_request_handler(cx.handle(), Self::handle_incoming_call), - client.add_message_handler(cx.handle(), Self::handle_cancel_call), + client.add_message_handler(cx.handle(), Self::handle_call_canceled), ], client, user_store, @@ -87,9 +87,9 @@ impl ActiveCall { Ok(proto::Ack {}) } - async fn handle_cancel_call( + async fn handle_call_canceled( this: ModelHandle, - _: TypedEnvelope, + _: TypedEnvelope, _: Arc, mut cx: AsyncAppContext, ) -> Result<()> { @@ -140,6 +140,20 @@ impl ActiveCall { }) } + pub fn cancel_invite( + &mut self, + recipient_user_id: u64, + cx: &mut ModelContext, + ) -> Task> { + let client = self.client.clone(); + cx.foreground().spawn(async move { + client + .request(proto::CancelCall { recipient_user_id }) + .await?; + anyhow::Ok(()) + }) + } + pub fn incoming(&self) -> watch::Receiver> { self.incoming_call.1.clone() } diff --git a/crates/collab/src/integration_tests.rs b/crates/collab/src/integration_tests.rs index a11d908cdd77a17f85c30b665c78aa193407b5d1..0767ee5ddbe57da3736f6c7e3833f1d6df9e3095 100644 --- a/crates/collab/src/integration_tests.rs +++ b/crates/collab/src/integration_tests.rs @@ -401,6 +401,88 @@ async fn test_leaving_room_on_disconnection( ); } +#[gpui::test(iterations = 10)] +async fn test_calls_on_multiple_connections( + deterministic: Arc, + cx_a: &mut TestAppContext, + cx_b1: &mut TestAppContext, + cx_b2: &mut TestAppContext, +) { + deterministic.forbid_parking(); + let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b1 = server.create_client(cx_b1, "user_b").await; + let _client_b2 = server.create_client(cx_b2, "user_b").await; + server + .make_contacts(&mut [(&client_a, cx_a), (&client_b1, cx_b1)]) + .await; + + let active_call_a = cx_a.read(ActiveCall::global); + let active_call_b1 = cx_b1.read(ActiveCall::global); + let active_call_b2 = cx_b2.read(ActiveCall::global); + let mut incoming_call_b1 = active_call_b1.read_with(cx_b1, |call, _| call.incoming()); + let mut incoming_call_b2 = active_call_b2.read_with(cx_b2, |call, _| call.incoming()); + assert!(incoming_call_b1.next().await.unwrap().is_none()); + assert!(incoming_call_b2.next().await.unwrap().is_none()); + + // Call user B from client A, ensuring both clients for user B ring. + active_call_a + .update(cx_a, |call, cx| { + call.invite(client_b1.user_id().unwrap(), None, cx) + }) + .await + .unwrap(); + assert!(incoming_call_b1.next().await.unwrap().is_some()); + assert!(incoming_call_b2.next().await.unwrap().is_some()); + + // User B declines the call on one of the two connections, causing both connections + // to stop ringing. + active_call_b2.update(cx_b2, |call, _| call.decline_incoming().unwrap()); + assert!(incoming_call_b1.next().await.unwrap().is_none()); + assert!(incoming_call_b2.next().await.unwrap().is_none()); + + // Call user B again from client A. + active_call_a + .update(cx_a, |call, cx| { + call.invite(client_b1.user_id().unwrap(), None, cx) + }) + .await + .unwrap(); + assert!(incoming_call_b1.next().await.unwrap().is_some()); + assert!(incoming_call_b2.next().await.unwrap().is_some()); + + // User B accepts the call on one of the two connections, causing both connections + // to stop ringing. + active_call_b2 + .update(cx_b2, |call, cx| call.accept_incoming(cx)) + .await + .unwrap(); + assert!(incoming_call_b1.next().await.unwrap().is_none()); + assert!(incoming_call_b2.next().await.unwrap().is_none()); + + // User B hangs up, and user A calls them again. + active_call_b2.update(cx_b2, |call, cx| call.hang_up(cx).unwrap()); + deterministic.run_until_parked(); + active_call_a + .update(cx_a, |call, cx| { + call.invite(client_b1.user_id().unwrap(), None, cx) + }) + .await + .unwrap(); + assert!(incoming_call_b1.next().await.unwrap().is_some()); + assert!(incoming_call_b2.next().await.unwrap().is_some()); + + // User A cancels the call, causing both connections to stop ringing. + active_call_a + .update(cx_a, |call, cx| { + call.cancel_invite(client_b1.user_id().unwrap(), cx) + }) + .await + .unwrap(); + assert!(incoming_call_b1.next().await.unwrap().is_none()); + assert!(incoming_call_b2.next().await.unwrap().is_none()); +} + #[gpui::test(iterations = 10)] async fn test_share_project( deterministic: Arc, diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 46bf4bf3dcba9240a3f74aea26d491e8dc9b2a0d..16bb7cfefd51a467dbc2b2377f4d16d0731e4938 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -150,6 +150,7 @@ impl Server { .add_request_handler(Server::join_room) .add_message_handler(Server::leave_room) .add_request_handler(Server::call) + .add_request_handler(Server::cancel_call) .add_message_handler(Server::decline_call) .add_request_handler(Server::update_participant_location) .add_request_handler(Server::share_project) @@ -599,7 +600,7 @@ impl Server { let (room, recipient_connection_ids) = store.join_room(room_id, request.sender_id)?; for recipient_id in recipient_connection_ids { self.peer - .send(recipient_id, proto::CancelCall {}) + .send(recipient_id, proto::CallCanceled {}) .trace_err(); } response.send(proto::JoinRoomResponse { @@ -715,6 +716,26 @@ impl Server { Err(anyhow!("failed to ring call recipient"))? } + async fn cancel_call( + self: Arc, + request: TypedEnvelope, + response: Response, + ) -> Result<()> { + let mut store = self.store().await; + let (room, recipient_connection_ids) = store.cancel_call( + UserId::from_proto(request.payload.recipient_user_id), + request.sender_id, + )?; + for recipient_id in recipient_connection_ids { + self.peer + .send(recipient_id, proto::CallCanceled {}) + .trace_err(); + } + self.room_updated(room); + response.send(proto::Ack {})?; + Ok(()) + } + async fn decline_call( self: Arc, message: TypedEnvelope, @@ -723,7 +744,7 @@ impl Server { let (room, recipient_connection_ids) = store.call_declined(message.sender_id)?; for recipient_id in recipient_connection_ids { self.peer - .send(recipient_id, proto::CancelCall {}) + .send(recipient_id, proto::CallCanceled {}) .trace_err(); } self.room_updated(room); diff --git a/crates/collab/src/rpc/store.rs b/crates/collab/src/rpc/store.rs index a0d272ccc8b7ccf335a21d3cca1090315fdde0f3..cb43c736741e29f12a9945727871d56a735b5ab9 100644 --- a/crates/collab/src/rpc/store.rs +++ b/crates/collab/src/rpc/store.rs @@ -585,6 +585,51 @@ impl Store { Ok(room) } + pub fn cancel_call( + &mut self, + recipient_user_id: UserId, + canceller_connection_id: ConnectionId, + ) -> Result<(&proto::Room, HashSet)> { + let canceller_user_id = self.user_id_for_connection(canceller_connection_id)?; + let canceller = self + .connected_users + .get(&canceller_user_id) + .ok_or_else(|| anyhow!("no such connection"))?; + let recipient = self + .connected_users + .get(&recipient_user_id) + .ok_or_else(|| anyhow!("no such connection"))?; + let canceller_active_call = canceller + .active_call + .as_ref() + .ok_or_else(|| anyhow!("no active call"))?; + let recipient_active_call = recipient + .active_call + .as_ref() + .ok_or_else(|| anyhow!("no active call for recipient"))?; + + anyhow::ensure!( + canceller_active_call.room_id == recipient_active_call.room_id, + "users are on different calls" + ); + anyhow::ensure!( + recipient_active_call.connection_id.is_none(), + "recipient has already answered" + ); + let room_id = recipient_active_call.room_id; + let room = self + .rooms + .get_mut(&room_id) + .ok_or_else(|| anyhow!("no such room"))?; + room.pending_user_ids + .retain(|user_id| UserId::from_proto(*user_id) != recipient_user_id); + + let recipient = self.connected_users.get_mut(&recipient_user_id).unwrap(); + recipient.active_call.take(); + + Ok((room, recipient.connection_ids.clone())) + } + pub fn call_declined( &mut self, recipient_connection_id: ConnectionId, diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 6c8ec72e86d30779fbc11d9cbf1c3296f2aaa57d..f938acb7bca70ea8fbad9a5bb9bdf515ddc48cfa 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -18,7 +18,8 @@ message Envelope { LeaveRoom leave_room = 1002; Call call = 12; IncomingCall incoming_call = 1000; - CancelCall cancel_call = 1001; + CallCanceled call_canceled = 1001; + CancelCall cancel_call = 1004; DeclineCall decline_call = 13; UpdateParticipantLocation update_participant_location = 1003; RoomUpdated room_updated = 14; @@ -189,7 +190,11 @@ message IncomingCall { optional uint64 initial_project_id = 4; } -message CancelCall {} +message CallCanceled {} + +message CancelCall { + uint64 recipient_user_id = 1; +} message DeclineCall {} diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index 25e04e6645823207fafee135726a26fc8fb0e440..7bfefc496a86a620b611b4c821d304f061afc82a 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -84,6 +84,7 @@ messages!( (BufferReloaded, Foreground), (BufferSaved, Foreground), (Call, Foreground), + (CallCanceled, Foreground), (CancelCall, Foreground), (ChannelMessageSent, Foreground), (CopyProjectEntry, Foreground), @@ -183,6 +184,7 @@ request_messages!( ApplyCompletionAdditionalEditsResponse ), (Call, Ack), + (CancelCall, Ack), (CopyProjectEntry, ProjectEntryResponse), (CreateProjectEntry, ProjectEntryResponse), (CreateRoom, CreateRoomResponse), From baf6097b4904ab3a05e78b0b61a3f4f7834f2514 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 6 Oct 2022 15:17:02 +0200 Subject: [PATCH 064/112] Remove stale contacts panel reference --- styles/src/styleTree/app.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/styles/src/styleTree/app.ts b/styles/src/styleTree/app.ts index 1c0c81cfde950a7e89f3e9546955d2ea8cb3f6eb..1b1aa2691657adc290dd2182ffae90e87e83d98a 100644 --- a/styles/src/styleTree/app.ts +++ b/styles/src/styleTree/app.ts @@ -2,7 +2,6 @@ import Theme from "../themes/common/theme"; import chatPanel from "./chatPanel"; import { text } from "./components"; import contactFinder from "./contactFinder"; -import contactsPanel from "./contactsPanel"; import contactsPopover from "./contactsPopover"; import commandPalette from "./commandPalette"; import editor from "./editor"; @@ -37,7 +36,6 @@ export default function app(theme: Theme): Object { projectPanel: projectPanel(theme), chatPanel: chatPanel(theme), contactsPopover: contactsPopover(theme), - contactsPanel: contactsPanel(theme), contactFinder: contactFinder(theme), search: search(theme), breadcrumbs: { From 95e08edbb82d82f3b9d08715b5ecd4bcf78efcb1 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 6 Oct 2022 15:20:49 +0200 Subject: [PATCH 065/112] Always include room id in protos This is redundant, but it futures-proof the ability to talk about multiple rooms at any given time and feels safer in terms of race conditions. --- crates/call/src/call.rs | 22 +++++++++++++++++++--- crates/collab/src/rpc.rs | 4 +++- crates/collab/src/rpc/store.rs | 11 +++++++++-- crates/rpc/proto/zed.proto | 7 +++++-- 4 files changed, 36 insertions(+), 8 deletions(-) diff --git a/crates/call/src/call.rs b/crates/call/src/call.rs index 1f4f7633e1f3696d1009c96daf0a59065b1aa587..8617b5391a23c2254a29471f752645160b5179db 100644 --- a/crates/call/src/call.rs +++ b/crates/call/src/call.rs @@ -145,10 +145,19 @@ impl ActiveCall { recipient_user_id: u64, cx: &mut ModelContext, ) -> Task> { + let room_id = if let Some(room) = self.room() { + room.read(cx).id() + } else { + return Task::ready(Err(anyhow!("no active call"))); + }; + let client = self.client.clone(); cx.foreground().spawn(async move { client - .request(proto::CancelCall { recipient_user_id }) + .request(proto::CancelCall { + room_id, + recipient_user_id, + }) .await?; anyhow::Ok(()) }) @@ -178,8 +187,15 @@ impl ActiveCall { } pub fn decline_incoming(&mut self) -> Result<()> { - *self.incoming_call.0.borrow_mut() = None; - self.client.send(proto::DeclineCall {})?; + let call = self + .incoming_call + .0 + .borrow_mut() + .take() + .ok_or_else(|| anyhow!("no incoming call"))?; + self.client.send(proto::DeclineCall { + room_id: call.room_id, + })?; Ok(()) } diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 16bb7cfefd51a467dbc2b2377f4d16d0731e4938..50d1c82fc866c087b3375afb95da034e3c1c6a1c 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -723,6 +723,7 @@ impl Server { ) -> Result<()> { let mut store = self.store().await; let (room, recipient_connection_ids) = store.cancel_call( + request.payload.room_id, UserId::from_proto(request.payload.recipient_user_id), request.sender_id, )?; @@ -741,7 +742,8 @@ impl Server { message: TypedEnvelope, ) -> Result<()> { let mut store = self.store().await; - let (room, recipient_connection_ids) = store.call_declined(message.sender_id)?; + let (room, recipient_connection_ids) = + store.decline_call(message.payload.room_id, message.sender_id)?; for recipient_id in recipient_connection_ids { self.peer .send(recipient_id, proto::CallCanceled {}) diff --git a/crates/collab/src/rpc/store.rs b/crates/collab/src/rpc/store.rs index cb43c736741e29f12a9945727871d56a735b5ab9..a9ae91aba0bcc8cf3e962e6c985f4bb6b5df18f6 100644 --- a/crates/collab/src/rpc/store.rs +++ b/crates/collab/src/rpc/store.rs @@ -587,6 +587,7 @@ impl Store { pub fn cancel_call( &mut self, + room_id: RoomId, recipient_user_id: UserId, canceller_connection_id: ConnectionId, ) -> Result<(&proto::Room, HashSet)> { @@ -609,7 +610,11 @@ impl Store { .ok_or_else(|| anyhow!("no active call for recipient"))?; anyhow::ensure!( - canceller_active_call.room_id == recipient_active_call.room_id, + canceller_active_call.room_id == room_id, + "users are on different calls" + ); + anyhow::ensure!( + recipient_active_call.room_id == room_id, "users are on different calls" ); anyhow::ensure!( @@ -630,8 +635,9 @@ impl Store { Ok((room, recipient.connection_ids.clone())) } - pub fn call_declined( + pub fn decline_call( &mut self, + room_id: RoomId, recipient_connection_id: ConnectionId, ) -> Result<(&proto::Room, Vec)> { let recipient_user_id = self.user_id_for_connection(recipient_connection_id)?; @@ -640,6 +646,7 @@ impl Store { .get_mut(&recipient_user_id) .ok_or_else(|| anyhow!("no such connection"))?; if let Some(active_call) = recipient.active_call.take() { + anyhow::ensure!(active_call.room_id == room_id, "no such room"); let recipient_connection_ids = self .connection_ids_for_user(recipient_user_id) .collect::>(); diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index f938acb7bca70ea8fbad9a5bb9bdf515ddc48cfa..334bcfbf90f4e072354fa440e0e3758eeeb0bb57 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -193,10 +193,13 @@ message IncomingCall { message CallCanceled {} message CancelCall { - uint64 recipient_user_id = 1; + uint64 room_id = 1; + uint64 recipient_user_id = 2; } -message DeclineCall {} +message DeclineCall { + uint64 room_id = 1; +} message UpdateParticipantLocation { uint64 room_id = 1; From 9f81699e0114da7a6818d2e3a00eacfa7dc3b48d Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 6 Oct 2022 16:10:45 +0200 Subject: [PATCH 066/112] WIP: start on menu bar extra --- crates/collab_ui/src/active_call_popover.rs | 40 +++++++ crates/collab_ui/src/collab_ui.rs | 5 +- crates/collab_ui/src/menu_bar_extra.rs | 110 ++++++++++++++++++++ crates/gpui/src/app.rs | 4 + 4 files changed, 158 insertions(+), 1 deletion(-) create mode 100644 crates/collab_ui/src/active_call_popover.rs create mode 100644 crates/collab_ui/src/menu_bar_extra.rs diff --git a/crates/collab_ui/src/active_call_popover.rs b/crates/collab_ui/src/active_call_popover.rs new file mode 100644 index 0000000000000000000000000000000000000000..01a4e4721d4f490696f0be3a97e7f75c09a67367 --- /dev/null +++ b/crates/collab_ui/src/active_call_popover.rs @@ -0,0 +1,40 @@ +use gpui::{color::Color, elements::*, Entity, RenderContext, View, ViewContext}; + +pub enum Event { + Deactivated, +} + +pub struct ActiveCallPopover { + _subscription: gpui::Subscription, +} + +impl Entity for ActiveCallPopover { + type Event = Event; +} + +impl View for ActiveCallPopover { + fn ui_name() -> &'static str { + "ActiveCallPopover" + } + + fn render(&mut self, _: &mut RenderContext) -> ElementBox { + Empty::new() + .contained() + .with_background_color(Color::red()) + .boxed() + } +} + +impl ActiveCallPopover { + pub fn new(cx: &mut ViewContext) -> Self { + Self { + _subscription: cx.observe_window_activation(Self::window_activation_changed), + } + } + + fn window_activation_changed(&mut self, is_active: bool, cx: &mut ViewContext) { + if !is_active { + cx.emit(Event::Deactivated); + } + } +} diff --git a/crates/collab_ui/src/collab_ui.rs b/crates/collab_ui/src/collab_ui.rs index 438a41ae7d1eb82c4c19bd3ea222bfcbfa7f08d5..421258114ef09593d7676b78315e3aea18ac26f0 100644 --- a/crates/collab_ui/src/collab_ui.rs +++ b/crates/collab_ui/src/collab_ui.rs @@ -1,8 +1,10 @@ +mod active_call_popover; mod collab_titlebar_item; mod contact_finder; mod contact_notification; mod contacts_popover; mod incoming_call_notification; +mod menu_bar_extra; mod notifications; mod project_shared_notification; @@ -14,11 +16,12 @@ use std::sync::Arc; use workspace::{AppState, JoinProject, ToggleFollow, Workspace}; pub fn init(app_state: Arc, cx: &mut MutableAppContext) { + collab_titlebar_item::init(cx); contact_notification::init(cx); contact_finder::init(cx); contacts_popover::init(cx); - collab_titlebar_item::init(cx); incoming_call_notification::init(cx); + menu_bar_extra::init(cx); project_shared_notification::init(cx); cx.add_global_action(move |action: &JoinProject, cx| { diff --git a/crates/collab_ui/src/menu_bar_extra.rs b/crates/collab_ui/src/menu_bar_extra.rs new file mode 100644 index 0000000000000000000000000000000000000000..8b2baa53ebdd02df4359ec82ffae8787dc5ec35c --- /dev/null +++ b/crates/collab_ui/src/menu_bar_extra.rs @@ -0,0 +1,110 @@ +use crate::active_call_popover::{self, ActiveCallPopover}; +use call::ActiveCall; +use gpui::{ + actions, + color::Color, + elements::*, + geometry::{rect::RectF, vector::vec2f}, + Appearance, Entity, MouseButton, MutableAppContext, RenderContext, View, ViewContext, + ViewHandle, WindowKind, +}; + +actions!(menu_bar_extra, [ToggleActiveCallPopover]); + +pub fn init(cx: &mut MutableAppContext) { + cx.add_action(MenuBarExtra::toggle_active_call_popover); + + let mut status_bar_item_id = None; + cx.observe(&ActiveCall::global(cx), move |call, cx| { + if let Some(status_bar_item_id) = status_bar_item_id.take() { + cx.remove_status_bar_item(status_bar_item_id); + } + + if call.read(cx).room().is_some() { + let (id, _) = cx.add_status_bar_item(|_| MenuBarExtra::new()); + status_bar_item_id = Some(id); + } + }) + .detach(); +} + +struct MenuBarExtra { + popover: Option>, +} + +impl Entity for MenuBarExtra { + type Event = (); +} + +impl View for MenuBarExtra { + fn ui_name() -> &'static str { + "MenuBarExtra" + } + + fn render(&mut self, cx: &mut RenderContext) -> ElementBox { + let color = match cx.appearance { + Appearance::Light | Appearance::VibrantLight => Color::black(), + Appearance::Dark | Appearance::VibrantDark => Color::white(), + }; + MouseEventHandler::::new(0, cx, |_, _| { + Svg::new("icons/zed_22.svg") + .with_color(color) + .aligned() + .boxed() + }) + .on_click(MouseButton::Left, |_, cx| { + cx.dispatch_action(ToggleActiveCallPopover); + }) + .boxed() + } +} + +impl MenuBarExtra { + fn new() -> Self { + Self { popover: None } + } + + fn toggle_active_call_popover( + &mut self, + _: &ToggleActiveCallPopover, + cx: &mut ViewContext, + ) { + match self.popover.take() { + Some(popover) => { + cx.remove_window(popover.window_id()); + } + None => { + let window_bounds = cx.window_bounds(); + let size = vec2f(360., 460.); + let origin = window_bounds.lower_left() + + vec2f(window_bounds.width() / 2. - size.x() / 2., 0.); + let (_, popover) = cx.add_window( + gpui::WindowOptions { + bounds: gpui::WindowBounds::Fixed(RectF::new(origin, size)), + titlebar: None, + center: false, + kind: WindowKind::PopUp, + is_movable: false, + }, + |cx| ActiveCallPopover::new(cx), + ); + cx.subscribe(&popover, Self::on_popover_event).detach(); + self.popover = Some(popover); + } + } + } + + fn on_popover_event( + &mut self, + popover: ViewHandle, + event: &active_call_popover::Event, + cx: &mut ViewContext, + ) { + match event { + active_call_popover::Event::Deactivated => { + self.popover.take(); + cx.remove_window(popover.window_id()); + } + } + } +} diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 668071d04670452cfba2c8a532a2a2a7ca6324ac..f55dd2b464f56e6d5f8ba35fd3996332de86563f 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -1990,6 +1990,10 @@ impl MutableAppContext { }) } + pub fn remove_status_bar_item(&mut self, id: usize) { + self.remove_window(id); + } + fn register_platform_window( &mut self, window_id: usize, From 3d467a949178022f29a9876be23a2bcb42318141 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 7 Oct 2022 09:23:25 +0200 Subject: [PATCH 067/112] Unset room on active call when disconnecting --- crates/call/src/call.rs | 8 +++++++- crates/collab/src/integration_tests.rs | 1 + 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/crates/call/src/call.rs b/crates/call/src/call.rs index 8617b5391a23c2254a29471f752645160b5179db..01787023be41ac68e957269edb22b2b33a5be017 100644 --- a/crates/call/src/call.rs +++ b/crates/call/src/call.rs @@ -210,7 +210,13 @@ impl ActiveCall { if room.as_ref() != self.room.as_ref().map(|room| &room.0) { if let Some(room) = room { let subscriptions = vec![ - cx.observe(&room, |_, _, cx| cx.notify()), + cx.observe(&room, |this, room, cx| { + if room.read(cx).status().is_offline() { + this.set_room(None, cx); + } + + cx.notify(); + }), cx.subscribe(&room, |_, _, event, cx| cx.emit(event.clone())), ]; self.room = Some((room, subscriptions)); diff --git a/crates/collab/src/integration_tests.rs b/crates/collab/src/integration_tests.rs index 0767ee5ddbe57da3736f6c7e3833f1d6df9e3095..665a9ca729aac3ac561370e569bba1102ec12142 100644 --- a/crates/collab/src/integration_tests.rs +++ b/crates/collab/src/integration_tests.rs @@ -385,6 +385,7 @@ async fn test_leaving_room_on_disconnection( server.disconnect_client(client_a.current_user_id(cx_a)); cx_a.foreground().advance_clock(rpc::RECEIVE_TIMEOUT); + active_call_a.read_with(cx_a, |call, _| assert!(call.room().is_none())); assert_eq!( room_participants(&room_a, cx_a), RoomParticipants { From b479c8c8ba616b9eece4d4be8bd45c8a2b82b77a Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 7 Oct 2022 10:14:17 +0200 Subject: [PATCH 068/112] Move project sharing into `Room` --- crates/call/src/call.rs | 28 +- crates/call/src/room.rs | 19 ++ crates/collab/src/integration_tests.rs | 262 +++++++++++-------- crates/collab_ui/src/collab_titlebar_item.rs | 31 ++- crates/project/src/project.rs | 10 +- 5 files changed, 210 insertions(+), 140 deletions(-) diff --git a/crates/call/src/call.rs b/crates/call/src/call.rs index 01787023be41ac68e957269edb22b2b33a5be017..91fee3ae875ffac73a0e5629278faa40fa10f2cc 100644 --- a/crates/call/src/call.rs +++ b/crates/call/src/call.rs @@ -120,10 +120,8 @@ impl ActiveCall { }; let initial_project_id = if let Some(initial_project) = initial_project { - let room_id = room.read_with(&cx, |room, _| room.id()); Some( - initial_project - .update(&mut cx, |project, cx| project.share(room_id, cx)) + room.update(&mut cx, |room, cx| room.share_project(initial_project, cx)) .await?, ) } else { @@ -206,6 +204,30 @@ impl ActiveCall { Ok(()) } + pub fn share_project( + &mut self, + project: ModelHandle, + cx: &mut ModelContext, + ) -> Task> { + if let Some((room, _)) = self.room.as_ref() { + room.update(cx, |room, cx| room.share_project(project, cx)) + } else { + Task::ready(Err(anyhow!("no active call"))) + } + } + + pub fn set_location( + &mut self, + project: Option<&ModelHandle>, + cx: &mut ModelContext, + ) -> Task> { + if let Some((room, _)) = self.room.as_ref() { + room.update(cx, |room, cx| room.set_location(project, cx)) + } else { + Task::ready(Err(anyhow!("no active call"))) + } + } + fn set_room(&mut self, room: Option>, cx: &mut ModelContext) { if room.as_ref() != self.room.as_ref().map(|room| &room.0) { if let Some(room) = room { diff --git a/crates/call/src/room.rs b/crates/call/src/room.rs index 3d8697f1e0f1e7ebb7c4d03cbfb0c4e900daa58f..ba2c51b39eda4650221ee1109a47655a4d67126e 100644 --- a/crates/call/src/room.rs +++ b/crates/call/src/room.rs @@ -240,6 +240,25 @@ impl Room { }) } + pub(crate) fn share_project( + &mut self, + project: ModelHandle, + cx: &mut ModelContext, + ) -> Task> { + let request = self + .client + .request(proto::ShareProject { room_id: self.id() }); + cx.spawn_weak(|_, mut cx| async move { + let response = request.await?; + project + .update(&mut cx, |project, cx| { + project.shared(response.project_id, cx) + }) + .await?; + Ok(response.project_id) + }) + } + pub fn set_location( &mut self, project: Option<&ModelHandle>, diff --git a/crates/collab/src/integration_tests.rs b/crates/collab/src/integration_tests.rs index 665a9ca729aac3ac561370e569bba1102ec12142..55f1267ba2355170ab8305a842fdb91dbf308fcd 100644 --- a/crates/collab/src/integration_tests.rs +++ b/crates/collab/src/integration_tests.rs @@ -495,9 +495,10 @@ async fn test_share_project( let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; - let room_id = server + server .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) .await; + let active_call_a = cx_a.read(ActiveCall::global); client_a .fs @@ -516,8 +517,8 @@ async fn test_share_project( .await; let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; - let project_id = project_a - .update(cx_a, |project, cx| project.share(room_id, cx)) + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) .await .unwrap(); @@ -594,7 +595,7 @@ async fn test_unshare_project( let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; let client_c = server.create_client(cx_c, "user_c").await; - let room_id = server + server .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)]) .await; @@ -613,8 +614,8 @@ async fn test_unshare_project( .await; let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; - let project_id = project_a - .update(cx_a, |project, cx| project.share(room_id, cx)) + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) .await .unwrap(); let worktree_a = project_a.read_with(cx_a, |project, cx| project.worktrees(cx).next().unwrap()); @@ -643,8 +644,8 @@ async fn test_unshare_project( assert!(project_c.read_with(cx_c, |project, _| project.is_read_only())); // Client C can open the project again after client A re-shares. - let project_id = project_a - .update(cx_a, |project, cx| project.share(room_id, cx)) + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) .await .unwrap(); let project_c2 = client_c.build_remote_project(project_id, cx_c).await; @@ -677,7 +678,7 @@ async fn test_host_disconnect( let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; let client_c = server.create_client(cx_c, "user_c").await; - let room_id = server + server .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)]) .await; @@ -692,10 +693,11 @@ async fn test_host_disconnect( ) .await; + let active_call_a = cx_a.read(ActiveCall::global); let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; let worktree_a = project_a.read_with(cx_a, |project, cx| project.worktrees(cx).next().unwrap()); - let project_id = project_a - .update(cx_a, |project, cx| project.share(room_id, cx)) + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) .await .unwrap(); @@ -770,15 +772,17 @@ async fn test_active_call_events( let (project_a, _) = client_a.build_local_project("/a", cx_a).await; let (project_b, _) = client_b.build_local_project("/b", cx_b).await; - let room_id = server + server .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) .await; + let active_call_a = cx_a.read(ActiveCall::global); + let active_call_b = cx_b.read(ActiveCall::global); let events_a = active_call_events(cx_a); let events_b = active_call_events(cx_b); - let project_a_id = project_a - .update(cx_a, |project, cx| project.share(room_id, cx)) + let project_a_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) .await .unwrap(); deterministic.run_until_parked(); @@ -795,8 +799,8 @@ async fn test_active_call_events( }] ); - let project_b_id = project_b - .update(cx_b, |project, cx| project.share(room_id, cx)) + let project_b_id = active_call_b + .update(cx_b, |call, cx| call.share_project(project_b.clone(), cx)) .await .unwrap(); deterministic.run_until_parked(); @@ -845,7 +849,7 @@ async fn test_room_location( let (project_a, _) = client_a.build_local_project("/a", cx_a).await; let (project_b, _) = client_b.build_local_project("/b", cx_b).await; - let room_id = server + server .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) .await; @@ -871,8 +875,8 @@ async fn test_room_location( } }); - let project_a_id = project_a - .update(cx_a, |project, cx| project.share(room_id, cx)) + let project_a_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) .await .unwrap(); deterministic.run_until_parked(); @@ -887,8 +891,8 @@ async fn test_room_location( vec![("user_a".to_string(), ParticipantLocation::External)] ); - let project_b_id = project_b - .update(cx_b, |project, cx| project.share(room_id, cx)) + let project_b_id = active_call_b + .update(cx_b, |call, cx| call.share_project(project_b.clone(), cx)) .await .unwrap(); deterministic.run_until_parked(); @@ -1000,9 +1004,10 @@ async fn test_propagate_saves_and_fs_changes( let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; let client_c = server.create_client(cx_c, "user_c").await; - let room_id = server + server .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)]) .await; + let active_call_a = cx_a.read(ActiveCall::global); client_a .fs @@ -1016,8 +1021,8 @@ async fn test_propagate_saves_and_fs_changes( .await; let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; let worktree_a = project_a.read_with(cx_a, |p, cx| p.worktrees(cx).next().unwrap()); - let project_id = project_a - .update(cx_a, |project, cx| project.share(room_id, cx)) + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) .await .unwrap(); @@ -1148,9 +1153,10 @@ async fn test_fs_operations( let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; - let room_id = server + server .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) .await; + let active_call_a = cx_a.read(ActiveCall::global); client_a .fs @@ -1163,8 +1169,8 @@ async fn test_fs_operations( ) .await; let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await; - let project_id = project_a - .update(cx_a, |project, cx| project.share(room_id, cx)) + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) .await .unwrap(); let project_b = client_b.build_remote_project(project_id, cx_b).await; @@ -1413,9 +1419,10 @@ async fn test_buffer_conflict_after_save(cx_a: &mut TestAppContext, cx_b: &mut T let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; - let room_id = server + server .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) .await; + let active_call_a = cx_a.read(ActiveCall::global); client_a .fs @@ -1427,8 +1434,8 @@ async fn test_buffer_conflict_after_save(cx_a: &mut TestAppContext, cx_b: &mut T ) .await; let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await; - let project_id = project_a - .update(cx_a, |project, cx| project.share(room_id, cx)) + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) .await .unwrap(); let project_b = client_b.build_remote_project(project_id, cx_b).await; @@ -1466,9 +1473,10 @@ async fn test_buffer_reloading(cx_a: &mut TestAppContext, cx_b: &mut TestAppCont let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; - let room_id = server + server .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) .await; + let active_call_a = cx_a.read(ActiveCall::global); client_a .fs @@ -1480,8 +1488,8 @@ async fn test_buffer_reloading(cx_a: &mut TestAppContext, cx_b: &mut TestAppCont ) .await; let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await; - let project_id = project_a - .update(cx_a, |project, cx| project.share(room_id, cx)) + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) .await .unwrap(); let project_b = client_b.build_remote_project(project_id, cx_b).await; @@ -1524,17 +1532,18 @@ async fn test_editing_while_guest_opens_buffer( let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; - let room_id = server + server .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) .await; + let active_call_a = cx_a.read(ActiveCall::global); client_a .fs .insert_tree("/dir", json!({ "a.txt": "a-contents" })) .await; let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await; - let project_id = project_a - .update(cx_a, |project, cx| project.share(room_id, cx)) + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) .await .unwrap(); let project_b = client_b.build_remote_project(project_id, cx_b).await; @@ -1570,17 +1579,18 @@ async fn test_leaving_worktree_while_opening_buffer( let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; - let room_id = server + server .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) .await; + let active_call_a = cx_a.read(ActiveCall::global); client_a .fs .insert_tree("/dir", json!({ "a.txt": "a-contents" })) .await; let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await; - let project_id = project_a - .update(cx_a, |project, cx| project.share(room_id, cx)) + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) .await .unwrap(); let project_b = client_b.build_remote_project(project_id, cx_b).await; @@ -1614,9 +1624,10 @@ async fn test_canceling_buffer_opening( let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; - let room_id = server + server .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) .await; + let active_call_a = cx_a.read(ActiveCall::global); client_a .fs @@ -1628,8 +1639,8 @@ async fn test_canceling_buffer_opening( ) .await; let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await; - let project_id = project_a - .update(cx_a, |project, cx| project.share(room_id, cx)) + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) .await .unwrap(); let project_b = client_b.build_remote_project(project_id, cx_b).await; @@ -1665,9 +1676,10 @@ async fn test_leaving_project( let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; let client_c = server.create_client(cx_c, "user_c").await; - let room_id = server + server .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)]) .await; + let active_call_a = cx_a.read(ActiveCall::global); client_a .fs @@ -1680,8 +1692,8 @@ async fn test_leaving_project( ) .await; let (project_a, _) = client_a.build_local_project("/a", cx_a).await; - let project_id = project_a - .update(cx_a, |project, cx| project.share(room_id, cx)) + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) .await .unwrap(); let project_b = client_b.build_remote_project(project_id, cx_b).await; @@ -1755,9 +1767,10 @@ async fn test_collaborating_with_diagnostics( let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; let client_c = server.create_client(cx_c, "user_c").await; - let room_id = server + server .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)]) .await; + let active_call_a = cx_a.read(ActiveCall::global); // Set up a fake language server. let mut language = Language::new( @@ -1783,8 +1796,8 @@ async fn test_collaborating_with_diagnostics( ) .await; let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; - let project_id = project_a - .update(cx_a, |project, cx| project.share(room_id, cx)) + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) .await .unwrap(); @@ -1992,9 +2005,10 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; - let room_id = server + server .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) .await; + let active_call_a = cx_a.read(ActiveCall::global); // Set up a fake language server. let mut language = Language::new( @@ -2030,8 +2044,8 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu ) .await; let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; - let project_id = project_a - .update(cx_a, |project, cx| project.share(room_id, cx)) + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) .await .unwrap(); let project_b = client_b.build_remote_project(project_id, cx_b).await; @@ -2165,9 +2179,10 @@ async fn test_reloading_buffer_manually(cx_a: &mut TestAppContext, cx_b: &mut Te let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; - let room_id = server + server .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) .await; + let active_call_a = cx_a.read(ActiveCall::global); client_a .fs @@ -2178,8 +2193,8 @@ async fn test_reloading_buffer_manually(cx_a: &mut TestAppContext, cx_b: &mut Te .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)) .await .unwrap(); - let project_id = project_a - .update(cx_a, |project, cx| project.share(room_id, cx)) + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) .await .unwrap(); @@ -2257,9 +2272,10 @@ async fn test_formatting_buffer(cx_a: &mut TestAppContext, cx_b: &mut TestAppCon let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; - let room_id = server + server .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) .await; + let active_call_a = cx_a.read(ActiveCall::global); // Set up a fake language server. let mut language = Language::new( @@ -2282,8 +2298,8 @@ async fn test_formatting_buffer(cx_a: &mut TestAppContext, cx_b: &mut TestAppCon .insert_tree(&directory, json!({ "a.rs": "let one = \"two\"" })) .await; let (project_a, worktree_id) = client_a.build_local_project(&directory, cx_a).await; - let project_id = project_a - .update(cx_a, |project, cx| project.share(room_id, cx)) + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) .await .unwrap(); let project_b = client_b.build_remote_project(project_id, cx_b).await; @@ -2357,9 +2373,10 @@ async fn test_definition(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; - let room_id = server + server .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) .await; + let active_call_a = cx_a.read(ActiveCall::global); // Set up a fake language server. let mut language = Language::new( @@ -2389,8 +2406,8 @@ async fn test_definition(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { ) .await; let (project_a, worktree_id) = client_a.build_local_project("/root/dir-1", cx_a).await; - let project_id = project_a - .update(cx_a, |project, cx| project.share(room_id, cx)) + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) .await .unwrap(); let project_b = client_b.build_remote_project(project_id, cx_b).await; @@ -2500,9 +2517,10 @@ async fn test_references(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; - let room_id = server + server .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) .await; + let active_call_a = cx_a.read(ActiveCall::global); // Set up a fake language server. let mut language = Language::new( @@ -2532,8 +2550,8 @@ async fn test_references(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { ) .await; let (project_a, worktree_id) = client_a.build_local_project("/root/dir-1", cx_a).await; - let project_id = project_a - .update(cx_a, |project, cx| project.share(room_id, cx)) + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) .await .unwrap(); let project_b = client_b.build_remote_project(project_id, cx_b).await; @@ -2600,9 +2618,10 @@ async fn test_project_search(cx_a: &mut TestAppContext, cx_b: &mut TestAppContex let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; - let room_id = server + server .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) .await; + let active_call_a = cx_a.read(ActiveCall::global); client_a .fs @@ -2631,8 +2650,8 @@ async fn test_project_search(cx_a: &mut TestAppContext, cx_b: &mut TestAppContex worktree_2 .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete()) .await; - let project_id = project_a - .update(cx_a, |project, cx| project.share(room_id, cx)) + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) .await .unwrap(); @@ -2678,9 +2697,10 @@ async fn test_document_highlights(cx_a: &mut TestAppContext, cx_b: &mut TestAppC let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; - let room_id = server + server .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) .await; + let active_call_a = cx_a.read(ActiveCall::global); client_a .fs @@ -2705,8 +2725,8 @@ async fn test_document_highlights(cx_a: &mut TestAppContext, cx_b: &mut TestAppC client_a.language_registry.add(Arc::new(language)); let (project_a, worktree_id) = client_a.build_local_project("/root-1", cx_a).await; - let project_id = project_a - .update(cx_a, |project, cx| project.share(room_id, cx)) + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) .await .unwrap(); let project_b = client_b.build_remote_project(project_id, cx_b).await; @@ -2779,9 +2799,10 @@ async fn test_lsp_hover(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; - let room_id = server + server .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) .await; + let active_call_a = cx_a.read(ActiveCall::global); client_a .fs @@ -2806,8 +2827,8 @@ async fn test_lsp_hover(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { client_a.language_registry.add(Arc::new(language)); let (project_a, worktree_id) = client_a.build_local_project("/root-1", cx_a).await; - let project_id = project_a - .update(cx_a, |project, cx| project.share(room_id, cx)) + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) .await .unwrap(); let project_b = client_b.build_remote_project(project_id, cx_b).await; @@ -2881,9 +2902,10 @@ async fn test_project_symbols(cx_a: &mut TestAppContext, cx_b: &mut TestAppConte let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; - let room_id = server + server .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) .await; + let active_call_a = cx_a.read(ActiveCall::global); // Set up a fake language server. let mut language = Language::new( @@ -2915,8 +2937,8 @@ async fn test_project_symbols(cx_a: &mut TestAppContext, cx_b: &mut TestAppConte ) .await; let (project_a, worktree_id) = client_a.build_local_project("/code/crate-1", cx_a).await; - let project_id = project_a - .update(cx_a, |project, cx| project.share(room_id, cx)) + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) .await .unwrap(); let project_b = client_b.build_remote_project(project_id, cx_b).await; @@ -2988,9 +3010,10 @@ async fn test_open_buffer_while_getting_definition_pointing_to_it( let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; - let room_id = server + server .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) .await; + let active_call_a = cx_a.read(ActiveCall::global); // Set up a fake language server. let mut language = Language::new( @@ -3015,8 +3038,8 @@ async fn test_open_buffer_while_getting_definition_pointing_to_it( ) .await; let (project_a, worktree_id) = client_a.build_local_project("/root", cx_a).await; - let project_id = project_a - .update(cx_a, |project, cx| project.share(room_id, cx)) + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) .await .unwrap(); let project_b = client_b.build_remote_project(project_id, cx_b).await; @@ -3063,9 +3086,10 @@ async fn test_collaborating_with_code_actions( let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; - let room_id = server + server .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) .await; + let active_call_a = cx_a.read(ActiveCall::global); // Set up a fake language server. let mut language = Language::new( @@ -3090,8 +3114,8 @@ async fn test_collaborating_with_code_actions( ) .await; let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; - let project_id = project_a - .update(cx_a, |project, cx| project.share(room_id, cx)) + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) .await .unwrap(); @@ -3273,9 +3297,10 @@ async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut T let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; - let room_id = server + server .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) .await; + let active_call_a = cx_a.read(ActiveCall::global); // Set up a fake language server. let mut language = Language::new( @@ -3311,8 +3336,8 @@ async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut T ) .await; let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await; - let project_id = project_a - .update(cx_a, |project, cx| project.share(room_id, cx)) + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) .await .unwrap(); let project_b = client_b.build_remote_project(project_id, cx_b).await; @@ -3464,9 +3489,10 @@ async fn test_language_server_statuses( let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; - let room_id = server + server .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) .await; + let active_call_a = cx_a.read(ActiveCall::global); // Set up a fake language server. let mut language = Language::new( @@ -3523,8 +3549,8 @@ async fn test_language_server_statuses( ); }); - let project_id = project_a - .update(cx_a, |project, cx| project.share(room_id, cx)) + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) .await .unwrap(); let project_b = client_b.build_remote_project(project_id, cx_b).await; @@ -4234,14 +4260,16 @@ async fn test_contact_requests( #[gpui::test(iterations = 10)] async fn test_following(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { cx_a.foreground().forbid_parking(); + cx_a.update(editor::init); + cx_b.update(editor::init); + let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; - let room_id = server + server .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) .await; - cx_a.update(editor::init); - cx_b.update(editor::init); + let active_call_a = cx_a.read(ActiveCall::global); client_a .fs @@ -4255,8 +4283,8 @@ async fn test_following(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { ) .await; let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; - let project_id = project_a - .update(cx_a, |project, cx| project.share(room_id, cx)) + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) .await .unwrap(); let project_b = client_b.build_remote_project(project_id, cx_b).await; @@ -4443,14 +4471,16 @@ async fn test_following(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { #[gpui::test(iterations = 10)] async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { cx_a.foreground().forbid_parking(); + cx_a.update(editor::init); + cx_b.update(editor::init); + let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; - let room_id = server + server .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) .await; - cx_a.update(editor::init); - cx_b.update(editor::init); + let active_call_a = cx_a.read(ActiveCall::global); // Client A shares a project. client_a @@ -4466,8 +4496,8 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T ) .await; let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; - let project_id = project_a - .update(cx_a, |project, cx| project.share(room_id, cx)) + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) .await .unwrap(); @@ -4609,16 +4639,17 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T #[gpui::test(iterations = 10)] async fn test_auto_unfollowing(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { cx_a.foreground().forbid_parking(); + cx_a.update(editor::init); + cx_b.update(editor::init); // 2 clients connect to a server. let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; - let room_id = server + server .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) .await; - cx_a.update(editor::init); - cx_b.update(editor::init); + let active_call_a = cx_a.read(ActiveCall::global); // Client A shares a project. client_a @@ -4633,8 +4664,8 @@ async fn test_auto_unfollowing(cx_a: &mut TestAppContext, cx_b: &mut TestAppCont ) .await; let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; - let project_id = project_a - .update(cx_a, |project, cx| project.share(room_id, cx)) + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) .await .unwrap(); let project_b = client_b.build_remote_project(project_id, cx_b).await; @@ -4773,21 +4804,22 @@ async fn test_peers_simultaneously_following_each_other( cx_b: &mut TestAppContext, ) { deterministic.forbid_parking(); + cx_a.update(editor::init); + cx_b.update(editor::init); let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; - let room_id = server + server .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) .await; - cx_a.update(editor::init); - cx_b.update(editor::init); + let active_call_a = cx_a.read(ActiveCall::global); client_a.fs.insert_tree("/a", json!({})).await; let (project_a, _) = client_a.build_local_project("/a", cx_a).await; let workspace_a = client_a.build_workspace(&project_a, cx_a); - let project_id = project_a - .update(cx_a, |project, cx| project.share(room_id, cx)) + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) .await .unwrap(); @@ -5057,16 +5089,18 @@ async fn test_random_collaboration( }) .await .unwrap(); - let room_id = active_call.read_with(cx, |call, cx| call.room().unwrap().read(cx).id()); + active_call.read_with(cx, |call, cx| call.room().unwrap().read(cx).id()); deterministic.run_until_parked(); - host_cx - .read(ActiveCall::global) + let host_active_call = host_cx.read(ActiveCall::global); + host_active_call .update(&mut host_cx, |call, cx| call.accept_incoming(cx)) .await .unwrap(); - let host_project_id = host_project - .update(&mut host_cx, |project, cx| project.share(room_id, cx)) + let host_project_id = host_active_call + .update(&mut host_cx, |call, cx| { + call.share_project(host_project.clone(), cx) + }) .await .unwrap(); @@ -5539,7 +5573,7 @@ impl TestServer { } } - async fn create_room(&self, clients: &mut [(&TestClient, &mut TestAppContext)]) -> u64 { + async fn create_room(&self, clients: &mut [(&TestClient, &mut TestAppContext)]) { self.make_contacts(clients).await; let (left, right) = clients.split_at_mut(1); @@ -5560,8 +5594,6 @@ impl TestServer { .await .unwrap(); } - - active_call_a.read_with(*cx_a, |call, cx| call.room().unwrap().read(cx).id()) } async fn build_app_state(test_db: &TestDb) -> Arc { diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index c98296204267b307a69c5c09bb4e505f13010acc..cc82acac5e7118018b6636c8d81fa1adb38b997d 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -138,22 +138,21 @@ impl CollabTitlebarItem { fn share_project(&mut self, _: &ShareProject, cx: &mut ViewContext) { if let Some(workspace) = self.workspace.upgrade(cx) { - if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() { - let window_id = cx.window_id(); - let room_id = room.read(cx).id(); - let project = workspace.read(cx).project().clone(); - let share = project.update(cx, |project, cx| project.share(room_id, cx)); - cx.spawn_weak(|_, mut cx| async move { - share.await?; - if cx.update(|cx| cx.window_is_active(window_id)) { - room.update(&mut cx, |room, cx| { - room.set_location(Some(&project), cx).detach_and_log_err(cx); - }); - } - anyhow::Ok(()) - }) - .detach_and_log_err(cx); - } + let active_call = ActiveCall::global(cx); + + let window_id = cx.window_id(); + let project = workspace.read(cx).project().clone(); + let share = active_call.update(cx, |call, cx| call.share_project(project.clone(), cx)); + cx.spawn_weak(|_, mut cx| async move { + share.await?; + if cx.update(|cx| cx.window_is_active(window_id)) { + active_call.update(&mut cx, |call, cx| { + call.set_location(Some(&project), cx).detach_and_log_err(cx); + }); + } + anyhow::Ok(()) + }) + .detach_and_log_err(cx); } } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 40503297b35f9d73357251fa62ad17f08c4dd08c..f984e16990061613127c612e699265a3118bca34 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1048,15 +1048,13 @@ impl Project { } } - pub fn share(&mut self, room_id: u64, cx: &mut ModelContext) -> Task> { + pub fn shared(&mut self, project_id: u64, cx: &mut ModelContext) -> Task> { if let ProjectClientState::Local { remote_id, .. } = &mut self.client_state { - if let Some(remote_id) = remote_id { - return Task::ready(Ok(*remote_id)); + if remote_id.is_some() { + return Task::ready(Err(anyhow!("project was already shared"))); } - let response = self.client.request(proto::ShareProject { room_id }); cx.spawn(|this, mut cx| async move { - let project_id = response.await?.project_id; let mut worktree_share_tasks = Vec::new(); this.update(&mut cx, |this, cx| { if let ProjectClientState::Local { remote_id, .. } = &mut this.client_state { @@ -1113,7 +1111,7 @@ impl Project { }); futures::future::try_join_all(worktree_share_tasks).await?; - Ok(project_id) + Ok(()) }) } else { Task::ready(Err(anyhow!("can't share a remote project"))) From 669406d5af5f29011939e5cdf31e7852296d523c Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 7 Oct 2022 11:56:50 +0200 Subject: [PATCH 069/112] Leave room when client is the only participant --- crates/call/src/call.rs | 37 ++++++++------- crates/call/src/room.rs | 66 ++++++++++++++++++++++---- crates/collab/src/integration_tests.rs | 2 + 3 files changed, 78 insertions(+), 27 deletions(-) diff --git a/crates/call/src/call.rs b/crates/call/src/call.rs index 91fee3ae875ffac73a0e5629278faa40fa10f2cc..2cfb155d111eea384d30e9d579d904dcdfd4485f 100644 --- a/crates/call/src/call.rs +++ b/crates/call/src/call.rs @@ -109,31 +109,32 @@ impl ActiveCall { initial_project: Option>, cx: &mut ModelContext, ) -> Task> { - let room = self.room.as_ref().map(|(room, _)| room.clone()); let client = self.client.clone(); let user_store = self.user_store.clone(); cx.spawn(|this, mut cx| async move { - let room = if let Some(room) = room { - room - } else { - cx.update(|cx| Room::create(client, user_store, cx)).await? - }; + if let Some(room) = this.read_with(&cx, |this, _| this.room().cloned()) { + let initial_project_id = if let Some(initial_project) = initial_project { + Some( + room.update(&mut cx, |room, cx| room.share_project(initial_project, cx)) + .await?, + ) + } else { + None + }; - let initial_project_id = if let Some(initial_project) = initial_project { - Some( - room.update(&mut cx, |room, cx| room.share_project(initial_project, cx)) - .await?, - ) + room.update(&mut cx, |room, cx| { + room.call(recipient_user_id, initial_project_id, cx) + }) + .await?; } else { - None + let room = cx + .update(|cx| { + Room::create(recipient_user_id, initial_project, client, user_store, cx) + }) + .await?; + this.update(&mut cx, |this, cx| this.set_room(Some(room), cx)); }; - this.update(&mut cx, |this, cx| this.set_room(Some(room.clone()), cx)); - room.update(&mut cx, |room, cx| { - room.call(recipient_user_id, initial_project_id, cx) - }) - .await?; - Ok(()) }) } diff --git a/crates/call/src/room.rs b/crates/call/src/room.rs index ba2c51b39eda4650221ee1109a47655a4d67126e..29e3c04259badd53b08cd12073f052d145dc1c63 100644 --- a/crates/call/src/room.rs +++ b/crates/call/src/room.rs @@ -21,9 +21,10 @@ pub struct Room { status: RoomStatus, remote_participants: HashMap, pending_users: Vec>, + pending_call_count: usize, client: Arc, user_store: ModelHandle, - _subscriptions: Vec, + subscriptions: Vec, _pending_room_update: Option>, } @@ -62,7 +63,8 @@ impl Room { status: RoomStatus::Online, remote_participants: Default::default(), pending_users: Default::default(), - _subscriptions: vec![client.add_message_handler(cx.handle(), Self::handle_room_updated)], + pending_call_count: 0, + subscriptions: vec![client.add_message_handler(cx.handle(), Self::handle_room_updated)], _pending_room_update: None, client, user_store, @@ -70,13 +72,40 @@ impl Room { } pub(crate) fn create( + recipient_user_id: u64, + initial_project: Option>, client: Arc, user_store: ModelHandle, cx: &mut MutableAppContext, ) -> Task>> { cx.spawn(|mut cx| async move { - let room = client.request(proto::CreateRoom {}).await?; - Ok(cx.add_model(|cx| Self::new(room.id, client, user_store, cx))) + let response = client.request(proto::CreateRoom {}).await?; + let room = cx.add_model(|cx| Self::new(response.id, client, user_store, cx)); + let initial_project_id = if let Some(initial_project) = initial_project { + let initial_project_id = room + .update(&mut cx, |room, cx| { + room.share_project(initial_project.clone(), cx) + }) + .await?; + initial_project + .update(&mut cx, |project, cx| { + project.shared(initial_project_id, cx) + }) + .await?; + Some(initial_project_id) + } else { + None + }; + + match room + .update(&mut cx, |room, cx| { + room.call(recipient_user_id, initial_project_id, cx) + }) + .await + { + Ok(()) => Ok(room), + Err(_) => Err(anyhow!("call failed")), + } }) } @@ -96,6 +125,12 @@ impl Room { }) } + fn should_leave(&self) -> bool { + self.pending_users.is_empty() + && self.remote_participants.is_empty() + && self.pending_call_count == 0 + } + pub(crate) fn leave(&mut self, cx: &mut ModelContext) -> Result<()> { if self.status.is_offline() { return Err(anyhow!("room is offline")); @@ -104,6 +139,7 @@ impl Room { cx.notify(); self.status = RoomStatus::Offline; self.remote_participants.clear(); + self.subscriptions.clear(); self.client.send(proto::LeaveRoom { id: self.id })?; Ok(()) } @@ -134,8 +170,7 @@ impl Room { .payload .room .ok_or_else(|| anyhow!("invalid room"))?; - this.update(&mut cx, |this, cx| this.apply_room_update(room, cx))?; - Ok(()) + this.update(&mut cx, |this, cx| this.apply_room_update(room, cx)) } fn apply_room_update( @@ -209,6 +244,10 @@ impl Room { this.pending_users = pending_users; cx.notify(); } + + if this.should_leave() { + let _ = this.leave(cx); + } }); })); @@ -226,16 +265,25 @@ impl Room { return Task::ready(Err(anyhow!("room is offline"))); } + cx.notify(); let client = self.client.clone(); let room_id = self.id; - cx.foreground().spawn(async move { - client + self.pending_call_count += 1; + cx.spawn(|this, mut cx| async move { + let result = client .request(proto::Call { room_id, recipient_user_id, initial_project_id, }) - .await?; + .await; + this.update(&mut cx, |this, cx| { + this.pending_call_count -= 1; + if this.should_leave() { + this.leave(cx)?; + } + result + })?; Ok(()) }) } diff --git a/crates/collab/src/integration_tests.rs b/crates/collab/src/integration_tests.rs index 55f1267ba2355170ab8305a842fdb91dbf308fcd..ba344d0aab9b927952aa2da366b661d11d7e4364 100644 --- a/crates/collab/src/integration_tests.rs +++ b/crates/collab/src/integration_tests.rs @@ -383,9 +383,11 @@ async fn test_leaving_room_on_disconnection( } ); + // When user A disconnects, both client A and B clear their room on the active call. server.disconnect_client(client_a.current_user_id(cx_a)); cx_a.foreground().advance_clock(rpc::RECEIVE_TIMEOUT); active_call_a.read_with(cx_a, |call, _| assert!(call.room().is_none())); + active_call_b.read_with(cx_b, |call, _| assert!(call.room().is_none())); assert_eq!( room_participants(&room_a, cx_a), RoomParticipants { From e82320cde8822c2d20fe5920a34d348b39ed911a Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 7 Oct 2022 12:00:23 +0200 Subject: [PATCH 070/112] Never set a room on active call if it is offline --- crates/call/src/call.rs | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/crates/call/src/call.rs b/crates/call/src/call.rs index 2cfb155d111eea384d30e9d579d904dcdfd4485f..7015173ce73523d94dd1414741c2f0578a1ab9a5 100644 --- a/crates/call/src/call.rs +++ b/crates/call/src/call.rs @@ -232,17 +232,21 @@ impl ActiveCall { fn set_room(&mut self, room: Option>, cx: &mut ModelContext) { if room.as_ref() != self.room.as_ref().map(|room| &room.0) { if let Some(room) = room { - let subscriptions = vec![ - cx.observe(&room, |this, room, cx| { - if room.read(cx).status().is_offline() { - this.set_room(None, cx); - } + if room.read(cx).status().is_offline() { + self.room = None; + } else { + let subscriptions = vec![ + cx.observe(&room, |this, room, cx| { + if room.read(cx).status().is_offline() { + this.set_room(None, cx); + } - cx.notify(); - }), - cx.subscribe(&room, |_, _, event, cx| cx.emit(event.clone())), - ]; - self.room = Some((room, subscriptions)); + cx.notify(); + }), + cx.subscribe(&room, |_, _, event, cx| cx.emit(event.clone())), + ]; + self.room = Some((room, subscriptions)); + } } else { self.room = None; } From d7cea646fc66e3fc97086d7f8b025a04fe2522d6 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 7 Oct 2022 12:21:56 +0200 Subject: [PATCH 071/112] Include a `busy` field in `proto::Contact` --- crates/client/src/user.rs | 2 + crates/collab/src/integration_tests.rs | 200 +++++++++++++++++++++++-- crates/collab/src/rpc.rs | 146 ++++++++++-------- crates/collab/src/rpc/store.rs | 9 ++ crates/rpc/proto/zed.proto | 3 +- 5 files changed, 290 insertions(+), 70 deletions(-) diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index 252fb4d455a270c6cd161ce6202bf6af337a1f0e..e0c5713dab4d97b00c67fb01e8a27a9594cca62a 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -39,6 +39,7 @@ impl Eq for User {} pub struct Contact { pub user: Arc, pub online: bool, + pub busy: bool, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -625,6 +626,7 @@ impl Contact { Ok(Self { user, online: contact.online, + busy: contact.busy, }) } } diff --git a/crates/collab/src/integration_tests.rs b/crates/collab/src/integration_tests.rs index ba344d0aab9b927952aa2da366b661d11d7e4364..72ebe937abc72a8c9f87d93e48f567bbd39656f8 100644 --- a/crates/collab/src/integration_tests.rs +++ b/crates/collab/src/integration_tests.rs @@ -435,12 +435,14 @@ async fn test_calls_on_multiple_connections( }) .await .unwrap(); + deterministic.run_until_parked(); assert!(incoming_call_b1.next().await.unwrap().is_some()); assert!(incoming_call_b2.next().await.unwrap().is_some()); // User B declines the call on one of the two connections, causing both connections // to stop ringing. active_call_b2.update(cx_b2, |call, _| call.decline_incoming().unwrap()); + deterministic.run_until_parked(); assert!(incoming_call_b1.next().await.unwrap().is_none()); assert!(incoming_call_b2.next().await.unwrap().is_none()); @@ -451,6 +453,7 @@ async fn test_calls_on_multiple_connections( }) .await .unwrap(); + deterministic.run_until_parked(); assert!(incoming_call_b1.next().await.unwrap().is_some()); assert!(incoming_call_b2.next().await.unwrap().is_some()); @@ -460,6 +463,7 @@ async fn test_calls_on_multiple_connections( .update(cx_b2, |call, cx| call.accept_incoming(cx)) .await .unwrap(); + deterministic.run_until_parked(); assert!(incoming_call_b1.next().await.unwrap().is_none()); assert!(incoming_call_b2.next().await.unwrap().is_none()); @@ -472,6 +476,7 @@ async fn test_calls_on_multiple_connections( }) .await .unwrap(); + deterministic.run_until_parked(); assert!(incoming_call_b1.next().await.unwrap().is_some()); assert!(incoming_call_b2.next().await.unwrap().is_some()); @@ -482,6 +487,7 @@ async fn test_calls_on_multiple_connections( }) .await .unwrap(); + deterministic.run_until_parked(); assert!(incoming_call_b1.next().await.unwrap().is_none()); assert!(incoming_call_b2.next().await.unwrap().is_none()); } @@ -4015,19 +4021,31 @@ async fn test_contacts( server .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)]) .await; + let active_call_a = cx_a.read(ActiveCall::global); + let active_call_b = cx_b.read(ActiveCall::global); + let active_call_c = cx_c.read(ActiveCall::global); deterministic.run_until_parked(); assert_eq!( contacts(&client_a, cx_a), - [("user_b".to_string(), true), ("user_c".to_string(), true)] + [ + ("user_b".to_string(), "online", "free"), + ("user_c".to_string(), "online", "free") + ] ); assert_eq!( contacts(&client_b, cx_b), - [("user_a".to_string(), true), ("user_c".to_string(), true)] + [ + ("user_a".to_string(), "online", "free"), + ("user_c".to_string(), "online", "free") + ] ); assert_eq!( contacts(&client_c, cx_c), - [("user_a".to_string(), true), ("user_b".to_string(), true)] + [ + ("user_a".to_string(), "online", "free"), + ("user_b".to_string(), "online", "free") + ] ); server.disconnect_client(client_c.current_user_id(cx_c)); @@ -4035,11 +4053,17 @@ async fn test_contacts( deterministic.advance_clock(rpc::RECEIVE_TIMEOUT); assert_eq!( contacts(&client_a, cx_a), - [("user_b".to_string(), true), ("user_c".to_string(), false)] + [ + ("user_b".to_string(), "online", "free"), + ("user_c".to_string(), "offline", "free") + ] ); assert_eq!( contacts(&client_b, cx_b), - [("user_a".to_string(), true), ("user_c".to_string(), false)] + [ + ("user_a".to_string(), "online", "free"), + ("user_c".to_string(), "offline", "free") + ] ); assert_eq!(contacts(&client_c, cx_c), []); @@ -4052,24 +4076,180 @@ async fn test_contacts( deterministic.run_until_parked(); assert_eq!( contacts(&client_a, cx_a), - [("user_b".to_string(), true), ("user_c".to_string(), true)] + [ + ("user_b".to_string(), "online", "free"), + ("user_c".to_string(), "online", "free") + ] + ); + assert_eq!( + contacts(&client_b, cx_b), + [ + ("user_a".to_string(), "online", "free"), + ("user_c".to_string(), "online", "free") + ] + ); + assert_eq!( + contacts(&client_c, cx_c), + [ + ("user_a".to_string(), "online", "free"), + ("user_b".to_string(), "online", "free") + ] + ); + + active_call_a + .update(cx_a, |call, cx| { + call.invite(client_b.user_id().unwrap(), None, cx) + }) + .await + .unwrap(); + deterministic.run_until_parked(); + assert_eq!( + contacts(&client_a, cx_a), + [ + ("user_b".to_string(), "online", "busy"), + ("user_c".to_string(), "online", "free") + ] + ); + assert_eq!( + contacts(&client_b, cx_b), + [ + ("user_a".to_string(), "online", "busy"), + ("user_c".to_string(), "online", "free") + ] + ); + assert_eq!( + contacts(&client_c, cx_c), + [ + ("user_a".to_string(), "online", "busy"), + ("user_b".to_string(), "online", "busy") + ] + ); + + active_call_b.update(cx_b, |call, _| call.decline_incoming().unwrap()); + deterministic.run_until_parked(); + assert_eq!( + contacts(&client_a, cx_a), + [ + ("user_b".to_string(), "online", "free"), + ("user_c".to_string(), "online", "free") + ] + ); + assert_eq!( + contacts(&client_b, cx_b), + [ + ("user_a".to_string(), "online", "free"), + ("user_c".to_string(), "online", "free") + ] + ); + assert_eq!( + contacts(&client_c, cx_c), + [ + ("user_a".to_string(), "online", "free"), + ("user_b".to_string(), "online", "free") + ] + ); + + active_call_c + .update(cx_c, |call, cx| { + call.invite(client_a.user_id().unwrap(), None, cx) + }) + .await + .unwrap(); + deterministic.run_until_parked(); + assert_eq!( + contacts(&client_a, cx_a), + [ + ("user_b".to_string(), "online", "free"), + ("user_c".to_string(), "online", "busy") + ] + ); + assert_eq!( + contacts(&client_b, cx_b), + [ + ("user_a".to_string(), "online", "busy"), + ("user_c".to_string(), "online", "busy") + ] + ); + assert_eq!( + contacts(&client_c, cx_c), + [ + ("user_a".to_string(), "online", "busy"), + ("user_b".to_string(), "online", "free") + ] + ); + + active_call_a + .update(cx_a, |call, cx| call.accept_incoming(cx)) + .await + .unwrap(); + deterministic.run_until_parked(); + assert_eq!( + contacts(&client_a, cx_a), + [ + ("user_b".to_string(), "online", "free"), + ("user_c".to_string(), "online", "busy") + ] + ); + assert_eq!( + contacts(&client_b, cx_b), + [ + ("user_a".to_string(), "online", "busy"), + ("user_c".to_string(), "online", "busy") + ] + ); + assert_eq!( + contacts(&client_c, cx_c), + [ + ("user_a".to_string(), "online", "busy"), + ("user_b".to_string(), "online", "free") + ] + ); + + active_call_a + .update(cx_a, |call, cx| { + call.invite(client_b.user_id().unwrap(), None, cx) + }) + .await + .unwrap(); + deterministic.run_until_parked(); + assert_eq!( + contacts(&client_a, cx_a), + [ + ("user_b".to_string(), "online", "busy"), + ("user_c".to_string(), "online", "busy") + ] ); assert_eq!( contacts(&client_b, cx_b), - [("user_a".to_string(), true), ("user_c".to_string(), true)] + [ + ("user_a".to_string(), "online", "busy"), + ("user_c".to_string(), "online", "busy") + ] ); assert_eq!( contacts(&client_c, cx_c), - [("user_a".to_string(), true), ("user_b".to_string(), true)] + [ + ("user_a".to_string(), "online", "busy"), + ("user_b".to_string(), "online", "busy") + ] ); #[allow(clippy::type_complexity)] - fn contacts(client: &TestClient, cx: &TestAppContext) -> Vec<(String, bool)> { + fn contacts( + client: &TestClient, + cx: &TestAppContext, + ) -> Vec<(String, &'static str, &'static str)> { client.user_store.read_with(cx, |store, _| { store .contacts() .iter() - .map(|contact| (contact.user.github_login.clone(), contact.online)) + .map(|contact| { + ( + contact.user.github_login.clone(), + if contact.online { "online" } else { "offline" }, + if contact.busy { "busy" } else { "free" }, + ) + }) .collect() }) } diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 50d1c82fc866c087b3375afb95da034e3c1c6a1c..bd7afba775e3867665fbf48aeb52c21faa06e033 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -585,8 +585,15 @@ impl Server { request: TypedEnvelope, response: Response, ) -> Result<()> { - let room_id = self.store().await.create_room(request.sender_id)?; + let user_id; + let room_id; + { + let mut store = self.store().await; + user_id = store.user_id_for_connection(request.sender_id)?; + room_id = store.create_room(request.sender_id)?; + } response.send(proto::CreateRoomResponse { id: room_id })?; + self.update_user_contacts(user_id).await?; Ok(()) } @@ -595,61 +602,71 @@ impl Server { request: TypedEnvelope, response: Response, ) -> Result<()> { - let room_id = request.payload.id; - let mut store = self.store().await; - let (room, recipient_connection_ids) = store.join_room(room_id, request.sender_id)?; - for recipient_id in recipient_connection_ids { - self.peer - .send(recipient_id, proto::CallCanceled {}) - .trace_err(); + let user_id; + { + let mut store = self.store().await; + user_id = store.user_id_for_connection(request.sender_id)?; + let (room, recipient_connection_ids) = + store.join_room(request.payload.id, request.sender_id)?; + for recipient_id in recipient_connection_ids { + self.peer + .send(recipient_id, proto::CallCanceled {}) + .trace_err(); + } + response.send(proto::JoinRoomResponse { + room: Some(room.clone()), + })?; + self.room_updated(room); } - response.send(proto::JoinRoomResponse { - room: Some(room.clone()), - })?; - self.room_updated(room); + self.update_user_contacts(user_id).await?; Ok(()) } async fn leave_room(self: Arc, message: TypedEnvelope) -> Result<()> { - let room_id = message.payload.id; - let mut store = self.store().await; - let left_room = store.leave_room(room_id, message.sender_id)?; + let user_id; + { + let mut store = self.store().await; + user_id = store.user_id_for_connection(message.sender_id)?; + let left_room = store.leave_room(message.payload.id, message.sender_id)?; - for project in left_room.unshared_projects { - for connection_id in project.connection_ids() { - self.peer.send( - connection_id, - proto::UnshareProject { - project_id: project.id.to_proto(), - }, - )?; + for project in left_room.unshared_projects { + for connection_id in project.connection_ids() { + self.peer.send( + connection_id, + proto::UnshareProject { + project_id: project.id.to_proto(), + }, + )?; + } } - } - for project in left_room.left_projects { - if project.remove_collaborator { - for connection_id in project.connection_ids { + for project in left_room.left_projects { + if project.remove_collaborator { + for connection_id in project.connection_ids { + self.peer.send( + connection_id, + proto::RemoveProjectCollaborator { + project_id: project.id.to_proto(), + peer_id: message.sender_id.0, + }, + )?; + } + self.peer.send( - connection_id, - proto::RemoveProjectCollaborator { + message.sender_id, + proto::UnshareProject { project_id: project.id.to_proto(), - peer_id: message.sender_id.0, }, )?; } + } - self.peer.send( - message.sender_id, - proto::UnshareProject { - project_id: project.id.to_proto(), - }, - )?; + if let Some(room) = left_room.room { + self.room_updated(room); } } + self.update_user_contacts(user_id).await?; - if let Some(room) = left_room.room { - self.room_updated(room); - } Ok(()) } @@ -694,6 +711,7 @@ impl Server { }) .collect::>() }; + self.update_user_contacts(recipient_user_id).await?; while let Some(call_response) = calls.next().await { match call_response.as_ref() { @@ -712,6 +730,7 @@ impl Server { let room = store.call_failed(room_id, recipient_user_id)?; self.room_updated(&room); } + self.update_user_contacts(recipient_user_id).await?; Err(anyhow!("failed to ring call recipient"))? } @@ -721,19 +740,23 @@ impl Server { request: TypedEnvelope, response: Response, ) -> Result<()> { - let mut store = self.store().await; - let (room, recipient_connection_ids) = store.cancel_call( - request.payload.room_id, - UserId::from_proto(request.payload.recipient_user_id), - request.sender_id, - )?; - for recipient_id in recipient_connection_ids { - self.peer - .send(recipient_id, proto::CallCanceled {}) - .trace_err(); + let recipient_user_id = UserId::from_proto(request.payload.recipient_user_id); + { + let mut store = self.store().await; + let (room, recipient_connection_ids) = store.cancel_call( + request.payload.room_id, + recipient_user_id, + request.sender_id, + )?; + for recipient_id in recipient_connection_ids { + self.peer + .send(recipient_id, proto::CallCanceled {}) + .trace_err(); + } + self.room_updated(room); + response.send(proto::Ack {})?; } - self.room_updated(room); - response.send(proto::Ack {})?; + self.update_user_contacts(recipient_user_id).await?; Ok(()) } @@ -741,15 +764,20 @@ impl Server { self: Arc, message: TypedEnvelope, ) -> Result<()> { - let mut store = self.store().await; - let (room, recipient_connection_ids) = - store.decline_call(message.payload.room_id, message.sender_id)?; - for recipient_id in recipient_connection_ids { - self.peer - .send(recipient_id, proto::CallCanceled {}) - .trace_err(); + let recipient_user_id; + { + let mut store = self.store().await; + recipient_user_id = store.user_id_for_connection(message.sender_id)?; + let (room, recipient_connection_ids) = + store.decline_call(message.payload.room_id, message.sender_id)?; + for recipient_id in recipient_connection_ids { + self.peer + .send(recipient_id, proto::CallCanceled {}) + .trace_err(); + } + self.room_updated(room); } - self.room_updated(room); + self.update_user_contacts(recipient_user_id).await?; Ok(()) } diff --git a/crates/collab/src/rpc/store.rs b/crates/collab/src/rpc/store.rs index a9ae91aba0bcc8cf3e962e6c985f4bb6b5df18f6..be7f798685baa120c7f98808bea02ee18f396c84 100644 --- a/crates/collab/src/rpc/store.rs +++ b/crates/collab/src/rpc/store.rs @@ -314,6 +314,14 @@ impl Store { .is_empty() } + fn is_user_busy(&self, user_id: UserId) -> bool { + self.connected_users + .get(&user_id) + .unwrap_or(&Default::default()) + .active_call + .is_some() + } + pub fn build_initial_contacts_update( &self, contacts: Vec, @@ -352,6 +360,7 @@ impl Store { proto::Contact { user_id: user_id.to_proto(), online: self.is_user_online(user_id), + busy: self.is_user_busy(user_id), should_notify, } } diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 334bcfbf90f4e072354fa440e0e3758eeeb0bb57..5f62e3585e2cd9063243761cbe4fefcec8cb8b29 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -1023,7 +1023,8 @@ message ChannelMessage { message Contact { uint64 user_id = 1; bool online = 2; - bool should_notify = 3; + bool busy = 3; + bool should_notify = 4; } message WorktreeMetadata { From 4aaf3df8c738ee83845b9de13dc7720b205c6342 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 7 Oct 2022 13:56:28 +0200 Subject: [PATCH 072/112] Show contact status --- crates/collab_ui/src/contacts_popover.rs | 29 ++++++++++++++++++++---- crates/theme/src/theme.rs | 2 ++ styles/src/styleTree/contactsPopover.ts | 12 ++++++++++ styles/src/styleTree/workspace.ts | 1 - styles/src/themes/common/base16.ts | 1 + styles/src/themes/common/theme.ts | 1 + 6 files changed, 41 insertions(+), 5 deletions(-) diff --git a/crates/collab_ui/src/contacts_popover.rs b/crates/collab_ui/src/contacts_popover.rs index 388b344879d02f4f38dc33e2216d5911b23a30d1..074e81616366724531e0a80ad7616b6121fbf493 100644 --- a/crates/collab_ui/src/contacts_popover.rs +++ b/crates/collab_ui/src/contacts_popover.rs @@ -522,10 +522,31 @@ impl ContactsPopover { MouseEventHandler::::new(contact.user.id as usize, cx, |_, _| { Flex::row() .with_children(contact.user.avatar.clone().map(|avatar| { - Image::new(avatar) - .with_style(theme.contact_avatar) - .aligned() - .left() + let status_badge = if contact.online { + Some( + Empty::new() + .collapsed() + .contained() + .with_style(if contact.busy { + theme.contact_status_busy + } else { + theme.contact_status_free + }) + .aligned() + .boxed(), + ) + } else { + None + }; + Stack::new() + .with_child( + Image::new(avatar) + .with_style(theme.contact_avatar) + .aligned() + .left() + .boxed(), + ) + .with_children(status_badge) .boxed() })) .with_child( diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index b9de72065a54ea76c1350bc08703a614783a0cb4..d70aaed189ad134507b0ea255cbced189f5db90d 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -95,6 +95,8 @@ pub struct ContactsPopover { pub project_row: Interactive, pub row_height: f32, pub contact_avatar: ImageStyle, + pub contact_status_free: ContainerStyle, + pub contact_status_busy: ContainerStyle, pub contact_username: ContainedText, pub contact_button: Interactive, pub contact_button_spacing: f32, diff --git a/styles/src/styleTree/contactsPopover.ts b/styles/src/styleTree/contactsPopover.ts index 82174fd6726e6242f8f2db8aed35949c2f3089b2..16656c3fcd7c9ec7cfca83a719b4d53a6024790e 100644 --- a/styles/src/styleTree/contactsPopover.ts +++ b/styles/src/styleTree/contactsPopover.ts @@ -116,6 +116,18 @@ export default function contactsPopover(theme: Theme) { cornerRadius: 10, width: 18, }, + contactStatusFree: { + cornerRadius: 4, + padding: 4, + margin: { top: 12, left: 12 }, + background: iconColor(theme, "success"), + }, + contactStatusBusy: { + cornerRadius: 3, + padding: 2, + margin: { top: 3, left: 3 }, + background: iconColor(theme, "feature"), + }, contactUsername: { ...text(theme, "mono", "primary", { size: "sm" }), margin: { diff --git a/styles/src/styleTree/workspace.ts b/styles/src/styleTree/workspace.ts index 7ad99ef6ab50d9c4c3b4e95b387f8012dc5e7cba..c970c382960cd776cc977ed1141e853ae740e863 100644 --- a/styles/src/styleTree/workspace.ts +++ b/styles/src/styleTree/workspace.ts @@ -5,7 +5,6 @@ import { border, iconColor, modalShadow, - popoverShadow, text, } from "./components"; import statusBar from "./statusBar"; diff --git a/styles/src/themes/common/base16.ts b/styles/src/themes/common/base16.ts index 7aa72ef1377ea40656a45e50bc4155f28ac7f8a5..36129880752692c910c60deea8e7806c1485d768 100644 --- a/styles/src/themes/common/base16.ts +++ b/styles/src/themes/common/base16.ts @@ -137,6 +137,7 @@ export function createTheme( ok: sample(ramps.green, 0.5), error: sample(ramps.red, 0.5), warning: sample(ramps.yellow, 0.5), + success: sample(ramps.green, 0.5), info: sample(ramps.blue, 0.5), onMedia: darkest, }; diff --git a/styles/src/themes/common/theme.ts b/styles/src/themes/common/theme.ts index e01435b846c4d4a5d1fdbe8366166c38de361f07..18ad8122e0cb96b84d58373cb2d3adb7c921ca8d 100644 --- a/styles/src/themes/common/theme.ts +++ b/styles/src/themes/common/theme.ts @@ -123,6 +123,7 @@ export default interface Theme { error: string; warning: string; info: string; + success: string; }; editor: { background: string; From 386de03f46a95bac42c97086894ebe684c63cbb2 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 7 Oct 2022 14:39:11 +0200 Subject: [PATCH 073/112] Fix room disconnection problems when creating room and sharing project --- crates/call/src/participant.rs | 2 +- crates/call/src/room.rs | 24 ++++--- crates/collab/src/integration_tests.rs | 19 +++++- crates/project/src/project.rs | 89 +++++++++++++------------- crates/rpc/src/peer.rs | 6 +- 5 files changed, 80 insertions(+), 60 deletions(-) diff --git a/crates/call/src/participant.rs b/crates/call/src/participant.rs index 15aaf2f13b9972e0043a0e2b199ee038fa0b3b54..db7a84e58427b51d37b8ddd634fda7800ebfd05b 100644 --- a/crates/call/src/participant.rs +++ b/crates/call/src/participant.rs @@ -20,7 +20,7 @@ impl ParticipantLocation { } } -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct RemoteParticipant { pub user: Arc, pub project_ids: Vec, diff --git a/crates/call/src/room.rs b/crates/call/src/room.rs index 29e3c04259badd53b08cd12073f052d145dc1c63..e08d814a3e79bc94fbee2f062dbc4c68efd36496 100644 --- a/crates/call/src/room.rs +++ b/crates/call/src/room.rs @@ -22,6 +22,7 @@ pub struct Room { remote_participants: HashMap, pending_users: Vec>, pending_call_count: usize, + leave_when_empty: bool, client: Arc, user_store: ModelHandle, subscriptions: Vec, @@ -65,6 +66,7 @@ impl Room { pending_users: Default::default(), pending_call_count: 0, subscriptions: vec![client.add_message_handler(cx.handle(), Self::handle_room_updated)], + leave_when_empty: false, _pending_room_update: None, client, user_store, @@ -81,17 +83,13 @@ impl Room { cx.spawn(|mut cx| async move { let response = client.request(proto::CreateRoom {}).await?; let room = cx.add_model(|cx| Self::new(response.id, client, user_store, cx)); + let initial_project_id = if let Some(initial_project) = initial_project { let initial_project_id = room .update(&mut cx, |room, cx| { room.share_project(initial_project.clone(), cx) }) .await?; - initial_project - .update(&mut cx, |project, cx| { - project.shared(initial_project_id, cx) - }) - .await?; Some(initial_project_id) } else { None @@ -103,8 +101,11 @@ impl Room { }) .await { - Ok(()) => Ok(room), - Err(_) => Err(anyhow!("call failed")), + Ok(()) => { + room.update(&mut cx, |room, _| room.leave_when_empty = true); + Ok(room) + } + Err(error) => Err(anyhow!("room creation failed: {:?}", error)), } }) } @@ -120,13 +121,18 @@ impl Room { let response = client.request(proto::JoinRoom { id: room_id }).await?; let room_proto = response.room.ok_or_else(|| anyhow!("invalid room"))?; let room = cx.add_model(|cx| Self::new(room_id, client, user_store, cx)); - room.update(&mut cx, |room, cx| room.apply_room_update(room_proto, cx))?; + room.update(&mut cx, |room, cx| { + room.leave_when_empty = true; + room.apply_room_update(room_proto, cx)?; + anyhow::Ok(()) + })?; Ok(room) }) } fn should_leave(&self) -> bool { - self.pending_users.is_empty() + self.leave_when_empty + && self.pending_users.is_empty() && self.remote_participants.is_empty() && self.pending_call_count == 0 } diff --git a/crates/collab/src/integration_tests.rs b/crates/collab/src/integration_tests.rs index 72ebe937abc72a8c9f87d93e48f567bbd39656f8..36b9d46d7b3379c13b00cb43ba87e7723013d967 100644 --- a/crates/collab/src/integration_tests.rs +++ b/crates/collab/src/integration_tests.rs @@ -504,9 +504,10 @@ async fn test_share_project( let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; server - .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) + .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b)]) .await; let active_call_a = cx_a.read(ActiveCall::global); + let active_call_b = cx_b.read(ActiveCall::global); client_a .fs @@ -524,13 +525,25 @@ async fn test_share_project( ) .await; + // Invite client B to collaborate on a project let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; - let project_id = active_call_a - .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) + active_call_a + .update(cx_a, |call, cx| { + call.invite(client_b.user_id().unwrap(), Some(project_a.clone()), cx) + }) .await .unwrap(); // Join that project as client B + let incoming_call_b = active_call_b.read_with(cx_b, |call, _| call.incoming()); + deterministic.run_until_parked(); + let call = incoming_call_b.borrow().clone().unwrap(); + assert_eq!(call.caller.github_login, "user_a"); + let project_id = call.initial_project_id.unwrap(); + active_call_b + .update(cx_b, |call, cx| call.accept_incoming(cx)) + .await + .unwrap(); let client_b_peer_id = client_b.peer_id; let project_b = client_b.build_remote_project(project_id, cx_b).await; let replica_id_b = project_b.read_with(cx_b, |project, _| project.replica_id()); diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index f984e16990061613127c612e699265a3118bca34..000751d417c0ddbed51c7131667a51a9a9841fba 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1054,62 +1054,59 @@ impl Project { return Task::ready(Err(anyhow!("project was already shared"))); } - cx.spawn(|this, mut cx| async move { - let mut worktree_share_tasks = Vec::new(); - this.update(&mut cx, |this, cx| { - if let ProjectClientState::Local { remote_id, .. } = &mut this.client_state { - *remote_id = Some(project_id); - } + *remote_id = Some(project_id); - for open_buffer in this.opened_buffers.values_mut() { - match open_buffer { - OpenBuffer::Strong(_) => {} - OpenBuffer::Weak(buffer) => { - if let Some(buffer) = buffer.upgrade(cx) { - *open_buffer = OpenBuffer::Strong(buffer); - } - } - OpenBuffer::Operations(_) => unreachable!(), + let mut worktree_share_tasks = Vec::new(); + + for open_buffer in self.opened_buffers.values_mut() { + match open_buffer { + OpenBuffer::Strong(_) => {} + OpenBuffer::Weak(buffer) => { + if let Some(buffer) = buffer.upgrade(cx) { + *open_buffer = OpenBuffer::Strong(buffer); } } + OpenBuffer::Operations(_) => unreachable!(), + } + } - for worktree_handle in this.worktrees.iter_mut() { - match worktree_handle { - WorktreeHandle::Strong(_) => {} - WorktreeHandle::Weak(worktree) => { - if let Some(worktree) = worktree.upgrade(cx) { - *worktree_handle = WorktreeHandle::Strong(worktree); - } - } + for worktree_handle in self.worktrees.iter_mut() { + match worktree_handle { + WorktreeHandle::Strong(_) => {} + WorktreeHandle::Weak(worktree) => { + if let Some(worktree) = worktree.upgrade(cx) { + *worktree_handle = WorktreeHandle::Strong(worktree); } } + } + } - for worktree in this.worktrees(cx).collect::>() { - worktree.update(cx, |worktree, cx| { - let worktree = worktree.as_local_mut().unwrap(); - worktree_share_tasks.push(worktree.share(project_id, cx)); - }); - } + for worktree in self.worktrees(cx).collect::>() { + worktree.update(cx, |worktree, cx| { + let worktree = worktree.as_local_mut().unwrap(); + worktree_share_tasks.push(worktree.share(project_id, cx)); + }); + } - for (server_id, status) in &this.language_server_statuses { - this.client - .send(proto::StartLanguageServer { - project_id, - server: Some(proto::LanguageServer { - id: *server_id as u64, - name: status.name.clone(), - }), - }) - .log_err(); - } + for (server_id, status) in &self.language_server_statuses { + self.client + .send(proto::StartLanguageServer { + project_id, + server: Some(proto::LanguageServer { + id: *server_id as u64, + name: status.name.clone(), + }), + }) + .log_err(); + } - this.client_subscriptions - .push(this.client.add_model_for_remote_entity(project_id, cx)); - this.metadata_changed(cx); - cx.emit(Event::RemoteIdChanged(Some(project_id))); - cx.notify(); - }); + self.client_subscriptions + .push(self.client.add_model_for_remote_entity(project_id, cx)); + self.metadata_changed(cx); + cx.emit(Event::RemoteIdChanged(Some(project_id))); + cx.notify(); + cx.foreground().spawn(async move { futures::future::try_join_all(worktree_share_tasks).await?; Ok(()) }) diff --git a/crates/rpc/src/peer.rs b/crates/rpc/src/peer.rs index 6c1c4f01da887e107468757fa12f48603cf74934..834acd0afaa96ab23a69c7322d59e89b54c4658d 100644 --- a/crates/rpc/src/peer.rs +++ b/crates/rpc/src/peer.rs @@ -394,7 +394,11 @@ impl Peer { send?; let (response, _barrier) = rx.await.map_err(|_| anyhow!("connection was closed"))?; if let Some(proto::envelope::Payload::Error(error)) = &response.payload { - Err(anyhow!("RPC request failed - {}", error.message)) + Err(anyhow!( + "RPC request {} failed - {}", + T::NAME, + error.message + )) } else { T::Response::from_envelope(response) .ok_or_else(|| anyhow!("received response of the wrong type")) From d3cddfdced0f5f6695249589de0dc24cf06a453f Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 7 Oct 2022 14:42:18 +0200 Subject: [PATCH 074/112] Fix styling for busy contacts --- styles/src/styleTree/contactsPopover.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/styles/src/styleTree/contactsPopover.ts b/styles/src/styleTree/contactsPopover.ts index 16656c3fcd7c9ec7cfca83a719b4d53a6024790e..7dfdb404d6436772c11062cd8e2d397e8a9f3ea8 100644 --- a/styles/src/styleTree/contactsPopover.ts +++ b/styles/src/styleTree/contactsPopover.ts @@ -123,10 +123,10 @@ export default function contactsPopover(theme: Theme) { background: iconColor(theme, "success"), }, contactStatusBusy: { - cornerRadius: 3, - padding: 2, - margin: { top: 3, left: 3 }, - background: iconColor(theme, "feature"), + cornerRadius: 4, + padding: 4, + margin: { top: 12, left: 12 }, + background: iconColor(theme, "warning"), }, contactUsername: { ...text(theme, "mono", "primary", { size: "sm" }), From 6fb5901d69054eadc881c104ff2b4220ff1dd05e Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 7 Oct 2022 14:47:06 +0200 Subject: [PATCH 075/112] Ensure sharing the same project twice is idempotent --- crates/call/src/room.rs | 6 ++++++ crates/collab/src/integration_tests.rs | 10 ++++++++++ 2 files changed, 16 insertions(+) diff --git a/crates/call/src/room.rs b/crates/call/src/room.rs index e08d814a3e79bc94fbee2f062dbc4c68efd36496..1630edb300096314df860ab24656a6bb7ff0f942 100644 --- a/crates/call/src/room.rs +++ b/crates/call/src/room.rs @@ -299,6 +299,12 @@ impl Room { project: ModelHandle, cx: &mut ModelContext, ) -> Task> { + if project.read(cx).is_remote() { + return Task::ready(Err(anyhow!("can't share remote project"))); + } else if let Some(project_id) = project.read(cx).remote_id() { + return Task::ready(Ok(project_id)); + } + let request = self .client .request(proto::ShareProject { room_id: self.id() }); diff --git a/crates/collab/src/integration_tests.rs b/crates/collab/src/integration_tests.rs index 36b9d46d7b3379c13b00cb43ba87e7723013d967..9cde2b2206c72212d3cd8adc9c4b8c02e237165c 100644 --- a/crates/collab/src/integration_tests.rs +++ b/crates/collab/src/integration_tests.rs @@ -838,6 +838,16 @@ async fn test_active_call_events( ); assert_eq!(mem::take(&mut *events_b.borrow_mut()), vec![]); + // Sharing a project twice is idempotent. + let project_b_id_2 = active_call_b + .update(cx_b, |call, cx| call.share_project(project_b.clone(), cx)) + .await + .unwrap(); + assert_eq!(project_b_id_2, project_b_id); + deterministic.run_until_parked(); + assert_eq!(mem::take(&mut *events_a.borrow_mut()), vec![]); + assert_eq!(mem::take(&mut *events_b.borrow_mut()), vec![]); + fn active_call_events(cx: &mut TestAppContext) -> Rc>> { let events = Rc::new(RefCell::new(Vec::new())); let active_call = cx.read(ActiveCall::global); From 251e06c50f3a7c88e1cd73465872629a5574a5e0 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 7 Oct 2022 14:51:04 +0200 Subject: [PATCH 076/112] :lipstick: --- crates/call/src/room.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/crates/call/src/room.rs b/crates/call/src/room.rs index 1630edb300096314df860ab24656a6bb7ff0f942..21e7c85c7fced3579324c9426a0bc89362a7d59e 100644 --- a/crates/call/src/room.rs +++ b/crates/call/src/room.rs @@ -97,14 +97,12 @@ impl Room { match room .update(&mut cx, |room, cx| { + room.leave_when_empty = true; room.call(recipient_user_id, initial_project_id, cx) }) .await { - Ok(()) => { - room.update(&mut cx, |room, _| room.leave_when_empty = true); - Ok(room) - } + Ok(()) => Ok(room), Err(error) => Err(anyhow!("room creation failed: {:?}", error)), } }) From 560d8a8004bc8614268d30640887ed486df0be9f Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 7 Oct 2022 14:52:39 +0200 Subject: [PATCH 077/112] Don't leave the room if there's a pending room update --- crates/call/src/room.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/crates/call/src/room.rs b/crates/call/src/room.rs index 21e7c85c7fced3579324c9426a0bc89362a7d59e..41648e6b24f323ce40a04fffd4af5ccc4d11d30c 100644 --- a/crates/call/src/room.rs +++ b/crates/call/src/room.rs @@ -26,7 +26,7 @@ pub struct Room { client: Arc, user_store: ModelHandle, subscriptions: Vec, - _pending_room_update: Option>, + pending_room_update: Option>, } impl Entity for Room { @@ -67,7 +67,7 @@ impl Room { pending_call_count: 0, subscriptions: vec![client.add_message_handler(cx.handle(), Self::handle_room_updated)], leave_when_empty: false, - _pending_room_update: None, + pending_room_update: None, client, user_store, } @@ -130,6 +130,7 @@ impl Room { fn should_leave(&self) -> bool { self.leave_when_empty + && self.pending_room_update.is_none() && self.pending_users.is_empty() && self.remote_participants.is_empty() && self.pending_call_count == 0 @@ -197,7 +198,7 @@ impl Room { user_store.get_users(room.pending_user_ids, cx), ) }); - self._pending_room_update = Some(cx.spawn(|this, mut cx| async move { + self.pending_room_update = Some(cx.spawn(|this, mut cx| async move { let (participants, pending_users) = futures::join!(participants, pending_users); this.update(&mut cx, |this, cx| { @@ -249,6 +250,7 @@ impl Room { cx.notify(); } + this.pending_room_update.take(); if this.should_leave() { let _ = this.leave(cx); } From 96c5bb8c396d0ac9a5fd7a9962783502cce2eca9 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 7 Oct 2022 15:07:09 +0200 Subject: [PATCH 078/112] Fix flicker due to adding and removing menu bar extra unnecessarily --- crates/collab_ui/src/menu_bar_extra.rs | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/crates/collab_ui/src/menu_bar_extra.rs b/crates/collab_ui/src/menu_bar_extra.rs index 8b2baa53ebdd02df4359ec82ffae8787dc5ec35c..814d51b1892756b729998b95a08d91e433995f55 100644 --- a/crates/collab_ui/src/menu_bar_extra.rs +++ b/crates/collab_ui/src/menu_bar_extra.rs @@ -16,13 +16,17 @@ pub fn init(cx: &mut MutableAppContext) { let mut status_bar_item_id = None; cx.observe(&ActiveCall::global(cx), move |call, cx| { - if let Some(status_bar_item_id) = status_bar_item_id.take() { - cx.remove_status_bar_item(status_bar_item_id); - } + let had_room = status_bar_item_id.is_some(); + let has_room = call.read(cx).room().is_some(); + if had_room != has_room { + if let Some(status_bar_item_id) = status_bar_item_id.take() { + cx.remove_status_bar_item(status_bar_item_id); + } - if call.read(cx).room().is_some() { - let (id, _) = cx.add_status_bar_item(|_| MenuBarExtra::new()); - status_bar_item_id = Some(id); + if has_room { + let (id, _) = cx.add_status_bar_item(|_| MenuBarExtra::new()); + status_bar_item_id = Some(id); + } } }) .detach(); @@ -34,6 +38,12 @@ struct MenuBarExtra { impl Entity for MenuBarExtra { type Event = (); + + fn release(&mut self, cx: &mut MutableAppContext) { + if let Some(popover) = self.popover.take() { + cx.remove_window(popover.window_id()); + } + } } impl View for MenuBarExtra { From f9fb3f78b2a63714387f46dd603f5f1194e3f8fb Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 7 Oct 2022 17:01:48 +0200 Subject: [PATCH 079/112] WIP: Render active call in contacts popover Co-Authored-By: Nathan Sobo --- crates/call/src/room.rs | 85 ++++++++---- crates/collab/src/integration_tests.rs | 2 +- crates/collab/src/rpc/store.rs | 26 ++-- crates/collab_ui/src/contacts_popover.rs | 159 +++++++++++++++++------ crates/rpc/proto/zed.proto | 2 +- crates/rpc/src/peer.rs | 2 +- crates/theme/src/theme.rs | 1 + styles/src/styleTree/contactsPopover.ts | 3 + 8 files changed, 198 insertions(+), 82 deletions(-) diff --git a/crates/call/src/room.rs b/crates/call/src/room.rs index 41648e6b24f323ce40a04fffd4af5ccc4d11d30c..9cad1ff211a4446eec3afde5fccb13df99858860 100644 --- a/crates/call/src/room.rs +++ b/crates/call/src/room.rs @@ -4,7 +4,7 @@ use crate::{ }; use anyhow::{anyhow, Result}; use client::{proto, Client, PeerId, TypedEnvelope, User, UserStore}; -use collections::{HashMap, HashSet}; +use collections::{BTreeMap, HashSet}; use futures::StreamExt; use gpui::{AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Task}; use project::Project; @@ -19,8 +19,9 @@ pub enum Event { pub struct Room { id: u64, status: RoomStatus, - remote_participants: HashMap, - pending_users: Vec>, + remote_participants: BTreeMap, + pending_participants: Vec>, + participant_user_ids: HashSet, pending_call_count: usize, leave_when_empty: bool, client: Arc, @@ -62,8 +63,9 @@ impl Room { Self { id, status: RoomStatus::Online, + participant_user_ids: Default::default(), remote_participants: Default::default(), - pending_users: Default::default(), + pending_participants: Default::default(), pending_call_count: 0, subscriptions: vec![client.add_message_handler(cx.handle(), Self::handle_room_updated)], leave_when_empty: false, @@ -131,7 +133,7 @@ impl Room { fn should_leave(&self) -> bool { self.leave_when_empty && self.pending_room_update.is_none() - && self.pending_users.is_empty() + && self.pending_participants.is_empty() && self.remote_participants.is_empty() && self.pending_call_count == 0 } @@ -144,6 +146,8 @@ impl Room { cx.notify(); self.status = RoomStatus::Offline; self.remote_participants.clear(); + self.pending_participants.clear(); + self.participant_user_ids.clear(); self.subscriptions.clear(); self.client.send(proto::LeaveRoom { id: self.id })?; Ok(()) @@ -157,12 +161,16 @@ impl Room { self.status } - pub fn remote_participants(&self) -> &HashMap { + pub fn remote_participants(&self) -> &BTreeMap { &self.remote_participants } - pub fn pending_users(&self) -> &[Arc] { - &self.pending_users + pub fn pending_participants(&self) -> &[Arc] { + &self.pending_participants + } + + pub fn contains_participant(&self, user_id: u64) -> bool { + self.participant_user_ids.contains(&user_id) } async fn handle_room_updated( @@ -187,27 +195,29 @@ impl Room { room.participants .retain(|participant| Some(participant.user_id) != self.client.user_id()); - let participant_user_ids = room + let remote_participant_user_ids = room .participants .iter() .map(|p| p.user_id) .collect::>(); - let (participants, pending_users) = self.user_store.update(cx, move |user_store, cx| { - ( - user_store.get_users(participant_user_ids, cx), - user_store.get_users(room.pending_user_ids, cx), - ) - }); + let (remote_participants, pending_participants) = + self.user_store.update(cx, move |user_store, cx| { + ( + user_store.get_users(remote_participant_user_ids, cx), + user_store.get_users(room.pending_participant_user_ids, cx), + ) + }); self.pending_room_update = Some(cx.spawn(|this, mut cx| async move { - let (participants, pending_users) = futures::join!(participants, pending_users); + let (remote_participants, pending_participants) = + futures::join!(remote_participants, pending_participants); this.update(&mut cx, |this, cx| { - if let Some(participants) = participants.log_err() { - let mut seen_participants = HashSet::default(); + this.participant_user_ids.clear(); + if let Some(participants) = remote_participants.log_err() { for (participant, user) in room.participants.into_iter().zip(participants) { let peer_id = PeerId(participant.peer_id); - seen_participants.insert(peer_id); + this.participant_user_ids.insert(participant.user_id); let existing_project_ids = this .remote_participants @@ -234,19 +244,18 @@ impl Room { ); } - for participant_peer_id in - this.remote_participants.keys().copied().collect::>() - { - if !seen_participants.contains(&participant_peer_id) { - this.remote_participants.remove(&participant_peer_id); - } - } + this.remote_participants.retain(|_, participant| { + this.participant_user_ids.contains(&participant.user.id) + }); cx.notify(); } - if let Some(pending_users) = pending_users.log_err() { - this.pending_users = pending_users; + if let Some(pending_participants) = pending_participants.log_err() { + this.pending_participants = pending_participants; + for participant in &this.pending_participants { + this.participant_user_ids.insert(participant.id); + } cx.notify(); } @@ -254,6 +263,8 @@ impl Room { if this.should_leave() { let _ = this.leave(cx); } + + this.check_invariants(); }); })); @@ -261,6 +272,24 @@ impl Room { Ok(()) } + fn check_invariants(&self) { + #[cfg(any(test, feature = "test-support"))] + { + for participant in self.remote_participants.values() { + assert!(self.participant_user_ids.contains(&participant.user.id)); + } + + for participant in &self.pending_participants { + assert!(self.participant_user_ids.contains(&participant.id)); + } + + assert_eq!( + self.participant_user_ids.len(), + self.remote_participants.len() + self.pending_participants.len() + ); + } + } + pub(crate) fn call( &mut self, recipient_user_id: u64, diff --git a/crates/collab/src/integration_tests.rs b/crates/collab/src/integration_tests.rs index 9cde2b2206c72212d3cd8adc9c4b8c02e237165c..c94d766f824c8f4ce2f235d70f65580f014da55a 100644 --- a/crates/collab/src/integration_tests.rs +++ b/crates/collab/src/integration_tests.rs @@ -6475,7 +6475,7 @@ fn room_participants(room: &ModelHandle, cx: &mut TestAppContext) -> RoomP .map(|(_, participant)| participant.user.github_login.clone()) .collect(), pending: room - .pending_users() + .pending_participants() .iter() .map(|user| user.github_login.clone()) .collect(), diff --git a/crates/collab/src/rpc/store.rs b/crates/collab/src/rpc/store.rs index be7f798685baa120c7f98808bea02ee18f396c84..5b23ac92d5d7af57268a7be97390ec67bc15e5dd 100644 --- a/crates/collab/src/rpc/store.rs +++ b/crates/collab/src/rpc/store.rs @@ -229,7 +229,7 @@ impl Store { .retain(|participant| participant.peer_id != connection_id.0); if prev_participant_count == room.participants.len() { if connected_user.connection_ids.is_empty() { - room.pending_user_ids + room.pending_participant_user_ids .retain(|pending_user_id| *pending_user_id != user_id.to_proto()); result.room_id = Some(room_id); connected_user.active_call = None; @@ -239,7 +239,7 @@ impl Store { connected_user.active_call = None; } - if room.participants.is_empty() && room.pending_user_ids.is_empty() { + if room.participants.is_empty() && room.pending_participant_user_ids.is_empty() { self.rooms.remove(&room_id); } } else { @@ -432,10 +432,11 @@ impl Store { .get_mut(&room_id) .ok_or_else(|| anyhow!("no such room"))?; anyhow::ensure!( - room.pending_user_ids.contains(&user_id.to_proto()), + room.pending_participant_user_ids + .contains(&user_id.to_proto()), anyhow!("no such room") ); - room.pending_user_ids + room.pending_participant_user_ids .retain(|pending| *pending != user_id.to_proto()); room.participants.push(proto::Participant { user_id: user_id.to_proto(), @@ -490,7 +491,7 @@ impl Store { .ok_or_else(|| anyhow!("no such room"))?; room.participants .retain(|participant| participant.peer_id != connection_id.0); - if room.participants.is_empty() && room.pending_user_ids.is_empty() { + if room.participants.is_empty() && room.pending_participant_user_ids.is_empty() { self.rooms.remove(&room_id); } @@ -537,12 +538,13 @@ impl Store { "no such room" ); anyhow::ensure!( - room.pending_user_ids + room.pending_participant_user_ids .iter() .all(|user_id| UserId::from_proto(*user_id) != recipient_user_id), "cannot call the same user more than once" ); - room.pending_user_ids.push(recipient_user_id.to_proto()); + room.pending_participant_user_ids + .push(recipient_user_id.to_proto()); if let Some(initial_project_id) = initial_project_id { let project = self @@ -589,7 +591,7 @@ impl Store { .rooms .get_mut(&room_id) .ok_or_else(|| anyhow!("no such room"))?; - room.pending_user_ids + room.pending_participant_user_ids .retain(|user_id| UserId::from_proto(*user_id) != to_user_id); Ok(room) } @@ -635,7 +637,7 @@ impl Store { .rooms .get_mut(&room_id) .ok_or_else(|| anyhow!("no such room"))?; - room.pending_user_ids + room.pending_participant_user_ids .retain(|user_id| UserId::from_proto(*user_id) != recipient_user_id); let recipient = self.connected_users.get_mut(&recipient_user_id).unwrap(); @@ -663,7 +665,7 @@ impl Store { .rooms .get_mut(&active_call.room_id) .ok_or_else(|| anyhow!("no such room"))?; - room.pending_user_ids + room.pending_participant_user_ids .retain(|user_id| UserId::from_proto(*user_id) != recipient_user_id); Ok((room, recipient_connection_ids)) } else { @@ -1115,7 +1117,7 @@ impl Store { } for (room_id, room) in &self.rooms { - for pending_user_id in &room.pending_user_ids { + for pending_user_id in &room.pending_participant_user_ids { assert!( self.connected_users .contains_key(&UserId::from_proto(*pending_user_id)), @@ -1140,7 +1142,7 @@ impl Store { } assert!( - !room.pending_user_ids.is_empty() || !room.participants.is_empty(), + !room.pending_participant_user_ids.is_empty() || !room.participants.is_empty(), "room can't be empty" ); } diff --git a/crates/collab_ui/src/contacts_popover.rs b/crates/collab_ui/src/contacts_popover.rs index 074e81616366724531e0a80ad7616b6121fbf493..090720c7a6e86ba553a530d89186d6d0af4b689a 100644 --- a/crates/collab_ui/src/contacts_popover.rs +++ b/crates/collab_ui/src/contacts_popover.rs @@ -2,7 +2,7 @@ use std::sync::Arc; use crate::contact_finder; use call::ActiveCall; -use client::{Contact, User, UserStore}; +use client::{Contact, PeerId, User, UserStore}; use editor::{Cancel, Editor}; use fuzzy::{match_strings, StringMatchCandidate}; use gpui::{ @@ -41,6 +41,7 @@ struct Call { #[derive(Clone, Copy, PartialEq, Eq, Debug, PartialOrd, Ord)] enum Section { + ActiveCall, Requests, Online, Offline, @@ -49,6 +50,7 @@ enum Section { #[derive(Clone)] enum ContactEntry { Header(Section), + CallParticipant { user: Arc, is_pending: bool }, IncomingRequest(Arc), OutgoingRequest(Arc), Contact(Arc), @@ -62,6 +64,11 @@ impl PartialEq for ContactEntry { return section_1 == section_2; } } + ContactEntry::CallParticipant { user: user_1, .. } => { + if let ContactEntry::CallParticipant { user: user_2, .. } = other { + return user_1.id == user_2.id; + } + } ContactEntry::IncomingRequest(user_1) => { if let ContactEntry::IncomingRequest(user_2) = other { return user_1.id == user_2.id; @@ -157,6 +164,9 @@ impl ContactsPopover { cx, ) } + ContactEntry::CallParticipant { user, is_pending } => { + Self::render_call_participant(user, *is_pending, &theme.contacts_popover) + } ContactEntry::IncomingRequest(user) => Self::render_contact_request( user.clone(), this.user_store.clone(), @@ -186,7 +196,7 @@ impl ContactsPopover { let active_call = ActiveCall::global(cx); let mut subscriptions = Vec::new(); subscriptions.push(cx.observe(&user_store, |this, _, cx| this.update_entries(cx))); - subscriptions.push(cx.observe(&active_call, |_, _, cx| cx.notify())); + subscriptions.push(cx.observe(&active_call, |this, _, cx| this.update_entries(cx))); let mut this = Self { list_state, @@ -291,6 +301,66 @@ impl ContactsPopover { let prev_selected_entry = self.selection.and_then(|ix| self.entries.get(ix).cloned()); self.entries.clear(); + if let Some(room) = ActiveCall::global(cx).read(cx).room() { + let room = room.read(cx); + + self.entries.push(ContactEntry::Header(Section::ActiveCall)); + if !self.collapsed_sections.contains(&Section::ActiveCall) { + // Populate remote participants. + self.match_candidates.clear(); + self.match_candidates + .extend( + room.remote_participants() + .iter() + .map(|(peer_id, participant)| StringMatchCandidate { + id: peer_id.0 as usize, + string: participant.user.github_login.clone(), + char_bag: participant.user.github_login.chars().collect(), + }), + ); + let matches = executor.block(match_strings( + &self.match_candidates, + &query, + true, + usize::MAX, + &Default::default(), + executor.clone(), + )); + self.entries.extend(matches.iter().map(|mat| { + ContactEntry::CallParticipant { + user: room.remote_participants()[&PeerId(mat.candidate_id as u32)] + .user + .clone(), + is_pending: false, + } + })); + + // Populate pending participants. + self.match_candidates.clear(); + self.match_candidates + .extend(room.pending_participants().iter().enumerate().map( + |(id, participant)| StringMatchCandidate { + id, + string: participant.github_login.clone(), + char_bag: participant.github_login.chars().collect(), + }, + )); + let matches = executor.block(match_strings( + &self.match_candidates, + &query, + true, + usize::MAX, + &Default::default(), + executor.clone(), + )); + self.entries + .extend(matches.iter().map(|mat| ContactEntry::CallParticipant { + user: room.pending_participants()[mat.candidate_id].clone(), + is_pending: true, + })); + } + } + let mut request_entries = Vec::new(); let incoming = user_store.incoming_contact_requests(); if !incoming.is_empty() { @@ -359,7 +429,6 @@ impl ContactsPopover { let contacts = user_store.contacts(); if !contacts.is_empty() { - // Always put the current user first. self.match_candidates.clear(); self.match_candidates .extend( @@ -382,9 +451,16 @@ impl ContactsPopover { executor.clone(), )); - let (online_contacts, offline_contacts) = matches + let (mut online_contacts, offline_contacts) = matches .iter() .partition::, _>(|mat| contacts[mat.candidate_id].online); + if let Some(room) = ActiveCall::global(cx).read(cx).room() { + let room = room.read(cx); + online_contacts.retain(|contact| { + let contact = &contacts[contact.candidate_id]; + !room.contains_participant(contact.user.id) + }); + } for (matches, section) in [ (online_contacts, Section::Online), @@ -416,41 +492,46 @@ impl ContactsPopover { cx.notify(); } - fn render_active_call(&self, cx: &mut RenderContext) -> Option { - let room = ActiveCall::global(cx).read(cx).room()?; - let theme = &cx.global::().theme.contacts_popover; - - Some( - Flex::column() - .with_children(room.read(cx).pending_users().iter().map(|user| { - Flex::row() - .with_children(user.avatar.clone().map(|avatar| { - Image::new(avatar) - .with_style(theme.contact_avatar) - .aligned() - .left() - .boxed() - })) - .with_child( - Label::new( - user.github_login.clone(), - theme.contact_username.text.clone(), - ) - .contained() - .with_style(theme.contact_username.container) - .aligned() - .left() - .flex(1., true) - .boxed(), - ) - .constrained() - .with_height(theme.row_height) - .contained() - .with_style(theme.contact_row.default) - .boxed() - })) + fn render_call_participant( + user: &User, + is_pending: bool, + theme: &theme::ContactsPopover, + ) -> ElementBox { + Flex::row() + .with_children(user.avatar.clone().map(|avatar| { + Image::new(avatar) + .with_style(theme.contact_avatar) + .aligned() + .left() + .boxed() + })) + .with_child( + Label::new( + user.github_login.clone(), + theme.contact_username.text.clone(), + ) + .contained() + .with_style(theme.contact_username.container) .boxed(), - ) + ) + .with_children(if is_pending { + Some( + Label::new( + "Calling...".to_string(), + theme.calling_indicator.text.clone(), + ) + .contained() + .with_style(theme.calling_indicator.container) + .aligned() + .flex_float() + .boxed(), + ) + } else { + None + }) + .constrained() + .with_height(theme.row_height) + .boxed() } fn render_header( @@ -464,6 +545,7 @@ impl ContactsPopover { let header_style = theme.header_row.style_for(Default::default(), is_selected); let text = match section { + Section::ActiveCall => "Call", Section::Requests => "Requests", Section::Online => "Online", Section::Offline => "Offline", @@ -751,7 +833,6 @@ impl View for ContactsPopover { .with_height(theme.contacts_popover.user_query_editor_height) .boxed(), ) - .with_children(self.render_active_call(cx)) .with_child(List::new(self.list_state.clone()).flex(1., false).boxed()) .with_children( self.user_store diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 5f62e3585e2cd9063243761cbe4fefcec8cb8b29..67f3afb46105e71036f333a3a5521c3c8459c1bf 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -154,7 +154,7 @@ message LeaveRoom { message Room { repeated Participant participants = 1; - repeated uint64 pending_user_ids = 2; + repeated uint64 pending_participant_user_ids = 2; } message Participant { diff --git a/crates/rpc/src/peer.rs b/crates/rpc/src/peer.rs index 834acd0afaa96ab23a69c7322d59e89b54c4658d..5b1ed6c2af19dd389197132a01ad540e5deb57fa 100644 --- a/crates/rpc/src/peer.rs +++ b/crates/rpc/src/peer.rs @@ -33,7 +33,7 @@ impl fmt::Display for ConnectionId { } } -#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] pub struct PeerId(pub u32); impl fmt::Display for PeerId { diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index d70aaed189ad134507b0ea255cbced189f5db90d..d66f30fe95368364123a7ddd779fcb141170edb5 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -105,6 +105,7 @@ pub struct ContactsPopover { pub private_button: Interactive, pub section_icon_size: f32, pub invite_row: Interactive, + pub calling_indicator: ContainedText, } #[derive(Clone, Deserialize, Default)] diff --git a/styles/src/styleTree/contactsPopover.ts b/styles/src/styleTree/contactsPopover.ts index 7dfdb404d6436772c11062cd8e2d397e8a9f3ea8..3e39746bdf3262626e28d05666cc3b5533970c44 100644 --- a/styles/src/styleTree/contactsPopover.ts +++ b/styles/src/styleTree/contactsPopover.ts @@ -171,5 +171,8 @@ export default function contactsPopover(theme: Theme) { text: text(theme, "sans", "active", { size: "sm" }), }, }, + callingIndicator: { + ...text(theme, "mono", "primary", { size: "xs" }), + } } } From d14744d02f030017b70eff2afb082fb276242ca8 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Sat, 8 Oct 2022 14:38:17 +0200 Subject: [PATCH 080/112] Show current user in active call --- crates/collab_ui/src/contacts_popover.rs | 138 +++++++++++++++-------- crates/theme/src/theme.rs | 19 ---- styles/src/styleTree/contactsPopover.ts | 52 +-------- 3 files changed, 89 insertions(+), 120 deletions(-) diff --git a/crates/collab_ui/src/contacts_popover.rs b/crates/collab_ui/src/contacts_popover.rs index 090720c7a6e86ba553a530d89186d6d0af4b689a..672201590a5adf0e3cddf14c424bf93b0b131936 100644 --- a/crates/collab_ui/src/contacts_popover.rs +++ b/crates/collab_ui/src/contacts_popover.rs @@ -165,7 +165,12 @@ impl ContactsPopover { ) } ContactEntry::CallParticipant { user, is_pending } => { - Self::render_call_participant(user, *is_pending, &theme.contacts_popover) + Self::render_call_participant( + user, + *is_pending, + is_selected, + &theme.contacts_popover, + ) } ContactEntry::IncomingRequest(user) => Self::render_contact_request( user.clone(), @@ -303,21 +308,16 @@ impl ContactsPopover { if let Some(room) = ActiveCall::global(cx).read(cx).room() { let room = room.read(cx); + let mut call_participants = Vec::new(); - self.entries.push(ContactEntry::Header(Section::ActiveCall)); - if !self.collapsed_sections.contains(&Section::ActiveCall) { - // Populate remote participants. + // Populate the active user. + if let Some(user) = user_store.current_user() { self.match_candidates.clear(); - self.match_candidates - .extend( - room.remote_participants() - .iter() - .map(|(peer_id, participant)| StringMatchCandidate { - id: peer_id.0 as usize, - string: participant.user.github_login.clone(), - char_bag: participant.user.github_login.chars().collect(), - }), - ); + self.match_candidates.push(StringMatchCandidate { + id: 0, + string: user.github_login.clone(), + char_bag: user.github_login.chars().collect(), + }); let matches = executor.block(match_strings( &self.match_candidates, &query, @@ -326,38 +326,74 @@ impl ContactsPopover { &Default::default(), executor.clone(), )); - self.entries.extend(matches.iter().map(|mat| { - ContactEntry::CallParticipant { - user: room.remote_participants()[&PeerId(mat.candidate_id as u32)] - .user - .clone(), + if !matches.is_empty() { + call_participants.push(ContactEntry::CallParticipant { + user, is_pending: false, - } - })); + }); + } + } - // Populate pending participants. - self.match_candidates.clear(); - self.match_candidates - .extend(room.pending_participants().iter().enumerate().map( - |(id, participant)| StringMatchCandidate { + // Populate remote participants. + self.match_candidates.clear(); + self.match_candidates + .extend( + room.remote_participants() + .iter() + .map(|(peer_id, participant)| StringMatchCandidate { + id: peer_id.0 as usize, + string: participant.user.github_login.clone(), + char_bag: participant.user.github_login.chars().collect(), + }), + ); + let matches = executor.block(match_strings( + &self.match_candidates, + &query, + true, + usize::MAX, + &Default::default(), + executor.clone(), + )); + call_participants.extend(matches.iter().map(|mat| { + ContactEntry::CallParticipant { + user: room.remote_participants()[&PeerId(mat.candidate_id as u32)] + .user + .clone(), + is_pending: false, + } + })); + + // Populate pending participants. + self.match_candidates.clear(); + self.match_candidates + .extend( + room.pending_participants() + .iter() + .enumerate() + .map(|(id, participant)| StringMatchCandidate { id, string: participant.github_login.clone(), char_bag: participant.github_login.chars().collect(), - }, - )); - let matches = executor.block(match_strings( - &self.match_candidates, - &query, - true, - usize::MAX, - &Default::default(), - executor.clone(), - )); - self.entries - .extend(matches.iter().map(|mat| ContactEntry::CallParticipant { - user: room.pending_participants()[mat.candidate_id].clone(), - is_pending: true, - })); + }), + ); + let matches = executor.block(match_strings( + &self.match_candidates, + &query, + true, + usize::MAX, + &Default::default(), + executor.clone(), + )); + call_participants.extend(matches.iter().map(|mat| ContactEntry::CallParticipant { + user: room.pending_participants()[mat.candidate_id].clone(), + is_pending: true, + })); + + if !call_participants.is_empty() { + self.entries.push(ContactEntry::Header(Section::ActiveCall)); + if !self.collapsed_sections.contains(&Section::ActiveCall) { + self.entries.extend(call_participants); + } } } @@ -495,6 +531,7 @@ impl ContactsPopover { fn render_call_participant( user: &User, is_pending: bool, + is_selected: bool, theme: &theme::ContactsPopover, ) -> ElementBox { Flex::row() @@ -512,25 +549,26 @@ impl ContactsPopover { ) .contained() .with_style(theme.contact_username.container) + .aligned() + .left() + .flex(1., true) .boxed(), ) .with_children(if is_pending { Some( - Label::new( - "Calling...".to_string(), - theme.calling_indicator.text.clone(), - ) - .contained() - .with_style(theme.calling_indicator.container) - .aligned() - .flex_float() - .boxed(), + Label::new("Calling".to_string(), theme.calling_indicator.text.clone()) + .contained() + .with_style(theme.calling_indicator.container) + .aligned() + .boxed(), ) } else { None }) .constrained() .with_height(theme.row_height) + .contained() + .with_style(*theme.contact_row.style_for(Default::default(), is_selected)) .boxed() } diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index d66f30fe95368364123a7ddd779fcb141170edb5..96d5b07582463ecdc1d6882c36b58ff43fcce943 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -92,7 +92,6 @@ pub struct ContactsPopover { pub add_contact_button: IconButton, pub header_row: Interactive, pub contact_row: Interactive, - pub project_row: Interactive, pub row_height: f32, pub contact_avatar: ImageStyle, pub contact_status_free: ContainerStyle, @@ -101,8 +100,6 @@ pub struct ContactsPopover { pub contact_button: Interactive, pub contact_button_spacing: f32, pub disabled_button: IconButton, - pub tree_branch: Interactive, - pub private_button: Interactive, pub section_icon_size: f32, pub invite_row: Interactive, pub calling_indicator: ContainedText, @@ -356,12 +353,6 @@ pub struct InviteLink { pub icon: Icon, } -#[derive(Deserialize, Default, Clone, Copy)] -pub struct TreeBranch { - pub width: f32, - pub color: Color, -} - #[derive(Deserialize, Default)] pub struct ContactFinder { pub row_height: f32, @@ -389,16 +380,6 @@ pub struct IconButton { pub button_width: f32, } -#[derive(Deserialize, Default)] -pub struct ProjectRow { - #[serde(flatten)] - pub container: ContainerStyle, - pub name: ContainedText, - pub guests: ContainerStyle, - pub guest_avatar: ImageStyle, - pub guest_avatar_spacing: f32, -} - #[derive(Deserialize, Default)] pub struct ChatMessage { #[serde(flatten)] diff --git a/styles/src/styleTree/contactsPopover.ts b/styles/src/styleTree/contactsPopover.ts index 3e39746bdf3262626e28d05666cc3b5533970c44..8786e81f5c25439c8fdf8e65e5fefe2fefea5231 100644 --- a/styles/src/styleTree/contactsPopover.ts +++ b/styles/src/styleTree/contactsPopover.ts @@ -5,32 +5,6 @@ export default function contactsPopover(theme: Theme) { const nameMargin = 8; const sidePadding = 12; - const projectRow = { - guestAvatarSpacing: 4, - height: 24, - guestAvatar: { - cornerRadius: 8, - width: 14, - }, - name: { - ...text(theme, "mono", "placeholder", { size: "sm" }), - margin: { - left: nameMargin, - right: 6, - }, - }, - guests: { - margin: { - left: nameMargin, - right: nameMargin, - }, - }, - padding: { - left: sidePadding, - right: sidePadding, - }, - }; - const contactButton = { background: backgroundColor(theme, 100), color: iconColor(theme, "primary"), @@ -102,16 +76,6 @@ export default function contactsPopover(theme: Theme) { background: backgroundColor(theme, 100, "active"), }, }, - treeBranch: { - color: borderColor(theme, "active"), - width: 1, - hover: { - color: borderColor(theme, "active"), - }, - active: { - color: borderColor(theme, "active"), - }, - }, contactAvatar: { cornerRadius: 10, width: 18, @@ -146,20 +110,6 @@ export default function contactsPopover(theme: Theme) { background: backgroundColor(theme, 100), color: iconColor(theme, "muted"), }, - projectRow: { - ...projectRow, - background: backgroundColor(theme, 300), - name: { - ...projectRow.name, - ...text(theme, "mono", "secondary", { size: "sm" }), - }, - hover: { - background: backgroundColor(theme, 300, "hovered"), - }, - active: { - background: backgroundColor(theme, 300, "active"), - }, - }, inviteRow: { padding: { left: sidePadding, @@ -172,7 +122,7 @@ export default function contactsPopover(theme: Theme) { }, }, callingIndicator: { - ...text(theme, "mono", "primary", { size: "xs" }), + ...text(theme, "mono", "muted", { size: "xs" }) } } } From 59aaf4ce1b69d80734af06e572235e165941c8e6 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Sat, 8 Oct 2022 14:43:41 +0200 Subject: [PATCH 081/112] Call contact on enter --- crates/collab_ui/src/contacts_popover.rs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/crates/collab_ui/src/contacts_popover.rs b/crates/collab_ui/src/contacts_popover.rs index 672201590a5adf0e3cddf14c424bf93b0b131936..700c7179624f603787ed0f91a86f11859201f0c3 100644 --- a/crates/collab_ui/src/contacts_popover.rs +++ b/crates/collab_ui/src/contacts_popover.rs @@ -282,6 +282,17 @@ impl ContactsPopover { let section = *section; self.toggle_expanded(&ToggleExpanded(section), cx); } + ContactEntry::Contact(contact) => { + if contact.online && !contact.busy { + self.call( + &Call { + recipient_user_id: contact.user.id, + initial_project: Some(self.project.clone()), + }, + cx, + ); + } + } _ => {} } } @@ -636,6 +647,7 @@ impl ContactsPopover { cx: &mut RenderContext, ) -> ElementBox { let online = contact.online; + let busy = contact.busy; let user_id = contact.user.id; let initial_project = project.clone(); let mut element = @@ -688,7 +700,7 @@ impl ContactsPopover { .boxed() }) .on_click(MouseButton::Left, move |_, cx| { - if online { + if online && !busy { cx.dispatch_action(Call { recipient_user_id: user_id, initial_project: Some(initial_project.clone()), From 34cb742db1cbdd24fdb6ac3e67a294b51e42d4a3 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Sat, 8 Oct 2022 14:47:40 +0200 Subject: [PATCH 082/112] Set current location after calling another user --- crates/collab_ui/src/contacts_popover.rs | 26 +++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/crates/collab_ui/src/contacts_popover.rs b/crates/collab_ui/src/contacts_popover.rs index 700c7179624f603787ed0f91a86f11859201f0c3..abc8db658b380e60cd67295e9ec112a3b4c9b90f 100644 --- a/crates/collab_ui/src/contacts_popover.rs +++ b/crates/collab_ui/src/contacts_popover.rs @@ -825,11 +825,27 @@ impl ContactsPopover { } fn call(&mut self, action: &Call, cx: &mut ViewContext) { - ActiveCall::global(cx) - .update(cx, |active_call, cx| { - active_call.invite(action.recipient_user_id, action.initial_project.clone(), cx) - }) - .detach_and_log_err(cx); + let recipient_user_id = action.recipient_user_id; + let initial_project = action.initial_project.clone(); + let window_id = cx.window_id(); + + let active_call = ActiveCall::global(cx); + cx.spawn_weak(|_, mut cx| async move { + active_call + .update(&mut cx, |active_call, cx| { + active_call.invite(recipient_user_id, initial_project.clone(), cx) + }) + .await?; + if cx.update(|cx| cx.window_is_active(window_id)) { + active_call + .update(&mut cx, |call, cx| { + call.set_location(initial_project.as_ref(), cx) + }) + .await?; + } + anyhow::Ok(()) + }) + .detach_and_log_err(cx); } } From 6f4edf6df58390a8ba63309951892f780ccfc7a5 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 10 Oct 2022 09:56:21 +0200 Subject: [PATCH 083/112] Move contact finder into contacts popover --- crates/collab_ui/src/collab_ui.rs | 2 + crates/collab_ui/src/contact_finder.rs | 34 +- crates/collab_ui/src/contacts_popover.rs | 966 +----------------- .../src/incoming_call_notification.rs | 22 +- crates/picker/src/picker.rs | 25 +- crates/theme/src/theme.rs | 33 +- styles/src/styleTree/app.ts | 4 + styles/src/styleTree/contactFinder.ts | 25 +- styles/src/styleTree/contactsPopover.ts | 1 - 9 files changed, 137 insertions(+), 975 deletions(-) diff --git a/crates/collab_ui/src/collab_ui.rs b/crates/collab_ui/src/collab_ui.rs index 421258114ef09593d7676b78315e3aea18ac26f0..da2cf775340974b062d90242d9a7fa7975f50886 100644 --- a/crates/collab_ui/src/collab_ui.rs +++ b/crates/collab_ui/src/collab_ui.rs @@ -1,6 +1,7 @@ mod active_call_popover; mod collab_titlebar_item; mod contact_finder; +mod contact_list; mod contact_notification; mod contacts_popover; mod incoming_call_notification; @@ -18,6 +19,7 @@ use workspace::{AppState, JoinProject, ToggleFollow, Workspace}; pub fn init(app_state: Arc, cx: &mut MutableAppContext) { collab_titlebar_item::init(cx); contact_notification::init(cx); + contact_list::init(cx); contact_finder::init(cx); contacts_popover::init(cx); incoming_call_notification::init(cx); diff --git a/crates/collab_ui/src/contact_finder.rs b/crates/collab_ui/src/contact_finder.rs index 6814b7479f58b432b9683e1abe8f43d4c1b7b1d8..65ebee2797cd8a0a0c6db468d75f1c84892f9a5e 100644 --- a/crates/collab_ui/src/contact_finder.rs +++ b/crates/collab_ui/src/contact_finder.rs @@ -1,19 +1,15 @@ use client::{ContactRequestStatus, User, UserStore}; use gpui::{ - actions, elements::*, AnyViewHandle, Entity, ModelHandle, MouseState, MutableAppContext, - RenderContext, Task, View, ViewContext, ViewHandle, + elements::*, AnyViewHandle, Entity, ModelHandle, MouseState, MutableAppContext, RenderContext, + Task, View, ViewContext, ViewHandle, }; use picker::{Picker, PickerDelegate}; use settings::Settings; use std::sync::Arc; use util::TryFutureExt; -use workspace::Workspace; - -actions!(contact_finder, [Toggle]); pub fn init(cx: &mut MutableAppContext) { Picker::::init(cx); - cx.add_action(ContactFinder::toggle); } pub struct ContactFinder { @@ -166,34 +162,16 @@ impl PickerDelegate for ContactFinder { } impl ContactFinder { - fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext) { - workspace.toggle_modal(cx, |workspace, cx| { - let finder = cx.add_view(|cx| Self::new(workspace.user_store().clone(), cx)); - cx.subscribe(&finder, Self::on_event).detach(); - finder - }); - } - pub fn new(user_store: ModelHandle, cx: &mut ViewContext) -> Self { let this = cx.weak_handle(); Self { - picker: cx.add_view(|cx| Picker::new(this, cx)), + picker: cx.add_view(|cx| { + Picker::new(this, cx) + .with_theme(|cx| &cx.global::().theme.contact_finder.picker) + }), potential_contacts: Arc::from([]), user_store, selected_index: 0, } } - - fn on_event( - workspace: &mut Workspace, - _: ViewHandle, - event: &Event, - cx: &mut ViewContext, - ) { - match event { - Event::Dismissed => { - workspace.dismiss_modal(cx); - } - } - } } diff --git a/crates/collab_ui/src/contacts_popover.rs b/crates/collab_ui/src/contacts_popover.rs index abc8db658b380e60cd67295e9ec112a3b4c9b90f..07ddc487a405412149c57aa78ad005a2732a7844 100644 --- a/crates/collab_ui/src/contacts_popover.rs +++ b/crates/collab_ui/src/contacts_popover.rs @@ -1,120 +1,32 @@ -use std::sync::Arc; - -use crate::contact_finder; -use call::ActiveCall; -use client::{Contact, PeerId, User, UserStore}; -use editor::{Cancel, Editor}; -use fuzzy::{match_strings, StringMatchCandidate}; +use crate::{contact_finder::ContactFinder, contact_list::ContactList}; +use client::UserStore; use gpui::{ - elements::*, impl_actions, impl_internal_actions, keymap, AppContext, ClipboardItem, - CursorStyle, Entity, ModelHandle, MouseButton, MutableAppContext, RenderContext, Subscription, - View, ViewContext, ViewHandle, + actions, elements::*, Entity, ModelHandle, MutableAppContext, RenderContext, View, ViewContext, + ViewHandle, }; -use menu::{Confirm, SelectNext, SelectPrev}; use project::Project; -use serde::Deserialize; use settings::Settings; -use theme::IconButton; -impl_actions!(contacts_popover, [RemoveContact, RespondToContactRequest]); -impl_internal_actions!(contacts_popover, [ToggleExpanded, Call]); +actions!(contacts_popover, [ToggleContactFinder]); pub fn init(cx: &mut MutableAppContext) { - cx.add_action(ContactsPopover::remove_contact); - cx.add_action(ContactsPopover::respond_to_contact_request); - cx.add_action(ContactsPopover::clear_filter); - cx.add_action(ContactsPopover::select_next); - cx.add_action(ContactsPopover::select_prev); - cx.add_action(ContactsPopover::confirm); - cx.add_action(ContactsPopover::toggle_expanded); - cx.add_action(ContactsPopover::call); -} - -#[derive(Clone, PartialEq)] -struct ToggleExpanded(Section); - -#[derive(Clone, PartialEq)] -struct Call { - recipient_user_id: u64, - initial_project: Option>, -} - -#[derive(Clone, Copy, PartialEq, Eq, Debug, PartialOrd, Ord)] -enum Section { - ActiveCall, - Requests, - Online, - Offline, -} - -#[derive(Clone)] -enum ContactEntry { - Header(Section), - CallParticipant { user: Arc, is_pending: bool }, - IncomingRequest(Arc), - OutgoingRequest(Arc), - Contact(Arc), -} - -impl PartialEq for ContactEntry { - fn eq(&self, other: &Self) -> bool { - match self { - ContactEntry::Header(section_1) => { - if let ContactEntry::Header(section_2) = other { - return section_1 == section_2; - } - } - ContactEntry::CallParticipant { user: user_1, .. } => { - if let ContactEntry::CallParticipant { user: user_2, .. } = other { - return user_1.id == user_2.id; - } - } - ContactEntry::IncomingRequest(user_1) => { - if let ContactEntry::IncomingRequest(user_2) = other { - return user_1.id == user_2.id; - } - } - ContactEntry::OutgoingRequest(user_1) => { - if let ContactEntry::OutgoingRequest(user_2) = other { - return user_1.id == user_2.id; - } - } - ContactEntry::Contact(contact_1) => { - if let ContactEntry::Contact(contact_2) = other { - return contact_1.user.id == contact_2.user.id; - } - } - } - false - } -} - -#[derive(Clone, Deserialize, PartialEq)] -pub struct RequestContact(pub u64); - -#[derive(Clone, Deserialize, PartialEq)] -pub struct RemoveContact(pub u64); - -#[derive(Clone, Deserialize, PartialEq)] -pub struct RespondToContactRequest { - pub user_id: u64, - pub accept: bool, + cx.add_action(ContactsPopover::toggle_contact_finder); } pub enum Event { Dismissed, } +enum Child { + ContactList(ViewHandle), + ContactFinder(ViewHandle), +} + pub struct ContactsPopover { - entries: Vec, - match_candidates: Vec, - list_state: ListState, + child: Child, project: ModelHandle, user_store: ModelHandle, - filter_editor: ViewHandle, - collapsed_sections: Vec
, - selection: Option, - _subscriptions: Vec, + _subscription: Option, } impl ContactsPopover { @@ -123,729 +35,44 @@ impl ContactsPopover { user_store: ModelHandle, cx: &mut ViewContext, ) -> Self { - let filter_editor = cx.add_view(|cx| { - let mut editor = Editor::single_line( - Some(|theme| theme.contacts_popover.user_query_editor.clone()), - cx, - ); - editor.set_placeholder_text("Filter contacts", cx); - editor - }); - - cx.subscribe(&filter_editor, |this, _, event, cx| { - if let editor::Event::BufferEdited = event { - let query = this.filter_editor.read(cx).text(cx); - if !query.is_empty() { - this.selection.take(); - } - this.update_entries(cx); - if !query.is_empty() { - this.selection = this - .entries - .iter() - .position(|entry| !matches!(entry, ContactEntry::Header(_))); - } - } - }) - .detach(); - - let list_state = ListState::new(0, Orientation::Top, 1000., cx, move |this, ix, cx| { - let theme = cx.global::().theme.clone(); - let is_selected = this.selection == Some(ix); - - match &this.entries[ix] { - ContactEntry::Header(section) => { - let is_collapsed = this.collapsed_sections.contains(section); - Self::render_header( - *section, - &theme.contacts_popover, - is_selected, - is_collapsed, - cx, - ) - } - ContactEntry::CallParticipant { user, is_pending } => { - Self::render_call_participant( - user, - *is_pending, - is_selected, - &theme.contacts_popover, - ) - } - ContactEntry::IncomingRequest(user) => Self::render_contact_request( - user.clone(), - this.user_store.clone(), - &theme.contacts_popover, - true, - is_selected, - cx, - ), - ContactEntry::OutgoingRequest(user) => Self::render_contact_request( - user.clone(), - this.user_store.clone(), - &theme.contacts_popover, - false, - is_selected, - cx, - ), - ContactEntry::Contact(contact) => Self::render_contact( - contact, - &this.project, - &theme.contacts_popover, - is_selected, - cx, - ), - } - }); - - let active_call = ActiveCall::global(cx); - let mut subscriptions = Vec::new(); - subscriptions.push(cx.observe(&user_store, |this, _, cx| this.update_entries(cx))); - subscriptions.push(cx.observe(&active_call, |this, _, cx| this.update_entries(cx))); - let mut this = Self { - list_state, - selection: None, - collapsed_sections: Default::default(), - entries: Default::default(), - match_candidates: Default::default(), - filter_editor, - _subscriptions: subscriptions, + child: Child::ContactList( + cx.add_view(|cx| ContactList::new(project.clone(), user_store.clone(), cx)), + ), project, user_store, + _subscription: None, }; - this.update_entries(cx); + this.show_contact_list(cx); this } - fn remove_contact(&mut self, request: &RemoveContact, cx: &mut ViewContext) { - self.user_store - .update(cx, |store, cx| store.remove_contact(request.0, cx)) - .detach(); - } - - fn respond_to_contact_request( - &mut self, - action: &RespondToContactRequest, - cx: &mut ViewContext, - ) { - self.user_store - .update(cx, |store, cx| { - store.respond_to_contact_request(action.user_id, action.accept, cx) - }) - .detach(); - } - - fn clear_filter(&mut self, _: &Cancel, cx: &mut ViewContext) { - let did_clear = self.filter_editor.update(cx, |editor, cx| { - if editor.buffer().read(cx).len(cx) > 0 { - editor.set_text("", cx); - true - } else { - false - } - }); - if !did_clear { - cx.emit(Event::Dismissed); + fn toggle_contact_finder(&mut self, _: &ToggleContactFinder, cx: &mut ViewContext) { + match &self.child { + Child::ContactList(_) => self.show_contact_finder(cx), + Child::ContactFinder(_) => self.show_contact_list(cx), } } - fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext) { - if let Some(ix) = self.selection { - if self.entries.len() > ix + 1 { - self.selection = Some(ix + 1); - } - } else if !self.entries.is_empty() { - self.selection = Some(0); - } + fn show_contact_finder(&mut self, cx: &mut ViewContext) { + let child = cx.add_view(|cx| ContactFinder::new(self.user_store.clone(), cx)); + cx.focus(&child); + self._subscription = Some(cx.subscribe(&child, |this, _, event, cx| match event { + crate::contact_finder::Event::Dismissed => this.show_contact_list(cx), + })); + self.child = Child::ContactFinder(child); cx.notify(); - self.list_state.reset(self.entries.len()); } - fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext) { - if let Some(ix) = self.selection { - if ix > 0 { - self.selection = Some(ix - 1); - } else { - self.selection = None; - } - } + fn show_contact_list(&mut self, cx: &mut ViewContext) { + let child = + cx.add_view(|cx| ContactList::new(self.project.clone(), self.user_store.clone(), cx)); + cx.focus(&child); + self._subscription = Some(cx.subscribe(&child, |_, _, event, cx| match event { + crate::contact_list::Event::Dismissed => cx.emit(Event::Dismissed), + })); + self.child = Child::ContactList(child); cx.notify(); - self.list_state.reset(self.entries.len()); - } - - fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext) { - if let Some(selection) = self.selection { - if let Some(entry) = self.entries.get(selection) { - match entry { - ContactEntry::Header(section) => { - let section = *section; - self.toggle_expanded(&ToggleExpanded(section), cx); - } - ContactEntry::Contact(contact) => { - if contact.online && !contact.busy { - self.call( - &Call { - recipient_user_id: contact.user.id, - initial_project: Some(self.project.clone()), - }, - cx, - ); - } - } - _ => {} - } - } - } - } - - fn toggle_expanded(&mut self, action: &ToggleExpanded, cx: &mut ViewContext) { - let section = action.0; - if let Some(ix) = self.collapsed_sections.iter().position(|s| *s == section) { - self.collapsed_sections.remove(ix); - } else { - self.collapsed_sections.push(section); - } - self.update_entries(cx); - } - - fn update_entries(&mut self, cx: &mut ViewContext) { - let user_store = self.user_store.read(cx); - let query = self.filter_editor.read(cx).text(cx); - let executor = cx.background().clone(); - - let prev_selected_entry = self.selection.and_then(|ix| self.entries.get(ix).cloned()); - self.entries.clear(); - - if let Some(room) = ActiveCall::global(cx).read(cx).room() { - let room = room.read(cx); - let mut call_participants = Vec::new(); - - // Populate the active user. - if let Some(user) = user_store.current_user() { - self.match_candidates.clear(); - self.match_candidates.push(StringMatchCandidate { - id: 0, - string: user.github_login.clone(), - char_bag: user.github_login.chars().collect(), - }); - let matches = executor.block(match_strings( - &self.match_candidates, - &query, - true, - usize::MAX, - &Default::default(), - executor.clone(), - )); - if !matches.is_empty() { - call_participants.push(ContactEntry::CallParticipant { - user, - is_pending: false, - }); - } - } - - // Populate remote participants. - self.match_candidates.clear(); - self.match_candidates - .extend( - room.remote_participants() - .iter() - .map(|(peer_id, participant)| StringMatchCandidate { - id: peer_id.0 as usize, - string: participant.user.github_login.clone(), - char_bag: participant.user.github_login.chars().collect(), - }), - ); - let matches = executor.block(match_strings( - &self.match_candidates, - &query, - true, - usize::MAX, - &Default::default(), - executor.clone(), - )); - call_participants.extend(matches.iter().map(|mat| { - ContactEntry::CallParticipant { - user: room.remote_participants()[&PeerId(mat.candidate_id as u32)] - .user - .clone(), - is_pending: false, - } - })); - - // Populate pending participants. - self.match_candidates.clear(); - self.match_candidates - .extend( - room.pending_participants() - .iter() - .enumerate() - .map(|(id, participant)| StringMatchCandidate { - id, - string: participant.github_login.clone(), - char_bag: participant.github_login.chars().collect(), - }), - ); - let matches = executor.block(match_strings( - &self.match_candidates, - &query, - true, - usize::MAX, - &Default::default(), - executor.clone(), - )); - call_participants.extend(matches.iter().map(|mat| ContactEntry::CallParticipant { - user: room.pending_participants()[mat.candidate_id].clone(), - is_pending: true, - })); - - if !call_participants.is_empty() { - self.entries.push(ContactEntry::Header(Section::ActiveCall)); - if !self.collapsed_sections.contains(&Section::ActiveCall) { - self.entries.extend(call_participants); - } - } - } - - let mut request_entries = Vec::new(); - let incoming = user_store.incoming_contact_requests(); - if !incoming.is_empty() { - self.match_candidates.clear(); - self.match_candidates - .extend( - incoming - .iter() - .enumerate() - .map(|(ix, user)| StringMatchCandidate { - id: ix, - string: user.github_login.clone(), - char_bag: user.github_login.chars().collect(), - }), - ); - let matches = executor.block(match_strings( - &self.match_candidates, - &query, - true, - usize::MAX, - &Default::default(), - executor.clone(), - )); - request_entries.extend( - matches - .iter() - .map(|mat| ContactEntry::IncomingRequest(incoming[mat.candidate_id].clone())), - ); - } - - let outgoing = user_store.outgoing_contact_requests(); - if !outgoing.is_empty() { - self.match_candidates.clear(); - self.match_candidates - .extend( - outgoing - .iter() - .enumerate() - .map(|(ix, user)| StringMatchCandidate { - id: ix, - string: user.github_login.clone(), - char_bag: user.github_login.chars().collect(), - }), - ); - let matches = executor.block(match_strings( - &self.match_candidates, - &query, - true, - usize::MAX, - &Default::default(), - executor.clone(), - )); - request_entries.extend( - matches - .iter() - .map(|mat| ContactEntry::OutgoingRequest(outgoing[mat.candidate_id].clone())), - ); - } - - if !request_entries.is_empty() { - self.entries.push(ContactEntry::Header(Section::Requests)); - if !self.collapsed_sections.contains(&Section::Requests) { - self.entries.append(&mut request_entries); - } - } - - let contacts = user_store.contacts(); - if !contacts.is_empty() { - self.match_candidates.clear(); - self.match_candidates - .extend( - contacts - .iter() - .enumerate() - .map(|(ix, contact)| StringMatchCandidate { - id: ix, - string: contact.user.github_login.clone(), - char_bag: contact.user.github_login.chars().collect(), - }), - ); - - let matches = executor.block(match_strings( - &self.match_candidates, - &query, - true, - usize::MAX, - &Default::default(), - executor.clone(), - )); - - let (mut online_contacts, offline_contacts) = matches - .iter() - .partition::, _>(|mat| contacts[mat.candidate_id].online); - if let Some(room) = ActiveCall::global(cx).read(cx).room() { - let room = room.read(cx); - online_contacts.retain(|contact| { - let contact = &contacts[contact.candidate_id]; - !room.contains_participant(contact.user.id) - }); - } - - for (matches, section) in [ - (online_contacts, Section::Online), - (offline_contacts, Section::Offline), - ] { - if !matches.is_empty() { - self.entries.push(ContactEntry::Header(section)); - if !self.collapsed_sections.contains(§ion) { - for mat in matches { - let contact = &contacts[mat.candidate_id]; - self.entries.push(ContactEntry::Contact(contact.clone())); - } - } - } - } - } - - if let Some(prev_selected_entry) = prev_selected_entry { - self.selection.take(); - for (ix, entry) in self.entries.iter().enumerate() { - if *entry == prev_selected_entry { - self.selection = Some(ix); - break; - } - } - } - - self.list_state.reset(self.entries.len()); - cx.notify(); - } - - fn render_call_participant( - user: &User, - is_pending: bool, - is_selected: bool, - theme: &theme::ContactsPopover, - ) -> ElementBox { - Flex::row() - .with_children(user.avatar.clone().map(|avatar| { - Image::new(avatar) - .with_style(theme.contact_avatar) - .aligned() - .left() - .boxed() - })) - .with_child( - Label::new( - user.github_login.clone(), - theme.contact_username.text.clone(), - ) - .contained() - .with_style(theme.contact_username.container) - .aligned() - .left() - .flex(1., true) - .boxed(), - ) - .with_children(if is_pending { - Some( - Label::new("Calling".to_string(), theme.calling_indicator.text.clone()) - .contained() - .with_style(theme.calling_indicator.container) - .aligned() - .boxed(), - ) - } else { - None - }) - .constrained() - .with_height(theme.row_height) - .contained() - .with_style(*theme.contact_row.style_for(Default::default(), is_selected)) - .boxed() - } - - fn render_header( - section: Section, - theme: &theme::ContactsPopover, - is_selected: bool, - is_collapsed: bool, - cx: &mut RenderContext, - ) -> ElementBox { - enum Header {} - - let header_style = theme.header_row.style_for(Default::default(), is_selected); - let text = match section { - Section::ActiveCall => "Call", - Section::Requests => "Requests", - Section::Online => "Online", - Section::Offline => "Offline", - }; - let icon_size = theme.section_icon_size; - MouseEventHandler::
::new(section as usize, cx, |_, _| { - Flex::row() - .with_child( - Svg::new(if is_collapsed { - "icons/chevron_right_8.svg" - } else { - "icons/chevron_down_8.svg" - }) - .with_color(header_style.text.color) - .constrained() - .with_max_width(icon_size) - .with_max_height(icon_size) - .aligned() - .constrained() - .with_width(icon_size) - .boxed(), - ) - .with_child( - Label::new(text.to_string(), header_style.text.clone()) - .aligned() - .left() - .contained() - .with_margin_left(theme.contact_username.container.margin.left) - .flex(1., true) - .boxed(), - ) - .constrained() - .with_height(theme.row_height) - .contained() - .with_style(header_style.container) - .boxed() - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, move |_, cx| { - cx.dispatch_action(ToggleExpanded(section)) - }) - .boxed() - } - - fn render_contact( - contact: &Contact, - project: &ModelHandle, - theme: &theme::ContactsPopover, - is_selected: bool, - cx: &mut RenderContext, - ) -> ElementBox { - let online = contact.online; - let busy = contact.busy; - let user_id = contact.user.id; - let initial_project = project.clone(); - let mut element = - MouseEventHandler::::new(contact.user.id as usize, cx, |_, _| { - Flex::row() - .with_children(contact.user.avatar.clone().map(|avatar| { - let status_badge = if contact.online { - Some( - Empty::new() - .collapsed() - .contained() - .with_style(if contact.busy { - theme.contact_status_busy - } else { - theme.contact_status_free - }) - .aligned() - .boxed(), - ) - } else { - None - }; - Stack::new() - .with_child( - Image::new(avatar) - .with_style(theme.contact_avatar) - .aligned() - .left() - .boxed(), - ) - .with_children(status_badge) - .boxed() - })) - .with_child( - Label::new( - contact.user.github_login.clone(), - theme.contact_username.text.clone(), - ) - .contained() - .with_style(theme.contact_username.container) - .aligned() - .left() - .flex(1., true) - .boxed(), - ) - .constrained() - .with_height(theme.row_height) - .contained() - .with_style(*theme.contact_row.style_for(Default::default(), is_selected)) - .boxed() - }) - .on_click(MouseButton::Left, move |_, cx| { - if online && !busy { - cx.dispatch_action(Call { - recipient_user_id: user_id, - initial_project: Some(initial_project.clone()), - }); - } - }); - - if online { - element = element.with_cursor_style(CursorStyle::PointingHand); - } - - element.boxed() - } - - fn render_contact_request( - user: Arc, - user_store: ModelHandle, - theme: &theme::ContactsPopover, - is_incoming: bool, - is_selected: bool, - cx: &mut RenderContext, - ) -> ElementBox { - enum Decline {} - enum Accept {} - enum Cancel {} - - let mut row = Flex::row() - .with_children(user.avatar.clone().map(|avatar| { - Image::new(avatar) - .with_style(theme.contact_avatar) - .aligned() - .left() - .boxed() - })) - .with_child( - Label::new( - user.github_login.clone(), - theme.contact_username.text.clone(), - ) - .contained() - .with_style(theme.contact_username.container) - .aligned() - .left() - .flex(1., true) - .boxed(), - ); - - let user_id = user.id; - let is_contact_request_pending = user_store.read(cx).is_contact_request_pending(&user); - let button_spacing = theme.contact_button_spacing; - - if is_incoming { - row.add_children([ - MouseEventHandler::::new(user.id as usize, cx, |mouse_state, _| { - let button_style = if is_contact_request_pending { - &theme.disabled_button - } else { - theme.contact_button.style_for(mouse_state, false) - }; - render_icon_button(button_style, "icons/x_mark_8.svg") - .aligned() - .boxed() - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, move |_, cx| { - cx.dispatch_action(RespondToContactRequest { - user_id, - accept: false, - }) - }) - .contained() - .with_margin_right(button_spacing) - .boxed(), - MouseEventHandler::::new(user.id as usize, cx, |mouse_state, _| { - let button_style = if is_contact_request_pending { - &theme.disabled_button - } else { - theme.contact_button.style_for(mouse_state, false) - }; - render_icon_button(button_style, "icons/check_8.svg") - .aligned() - .flex_float() - .boxed() - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, move |_, cx| { - cx.dispatch_action(RespondToContactRequest { - user_id, - accept: true, - }) - }) - .boxed(), - ]); - } else { - row.add_child( - MouseEventHandler::::new(user.id as usize, cx, |mouse_state, _| { - let button_style = if is_contact_request_pending { - &theme.disabled_button - } else { - theme.contact_button.style_for(mouse_state, false) - }; - render_icon_button(button_style, "icons/x_mark_8.svg") - .aligned() - .flex_float() - .boxed() - }) - .with_padding(Padding::uniform(2.)) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, move |_, cx| { - cx.dispatch_action(RemoveContact(user_id)) - }) - .flex_float() - .boxed(), - ); - } - - row.constrained() - .with_height(theme.row_height) - .contained() - .with_style(*theme.contact_row.style_for(Default::default(), is_selected)) - .boxed() - } - - fn call(&mut self, action: &Call, cx: &mut ViewContext) { - let recipient_user_id = action.recipient_user_id; - let initial_project = action.initial_project.clone(); - let window_id = cx.window_id(); - - let active_call = ActiveCall::global(cx); - cx.spawn_weak(|_, mut cx| async move { - active_call - .update(&mut cx, |active_call, cx| { - active_call.invite(recipient_user_id, initial_project.clone(), cx) - }) - .await?; - if cx.update(|cx| cx.window_is_active(window_id)) { - active_call - .update(&mut cx, |call, cx| { - call.set_location(initial_project.as_ref(), cx) - }) - .await?; - } - anyhow::Ok(()) - }) - .detach_and_log_err(cx); } } @@ -858,97 +85,14 @@ impl View for ContactsPopover { "ContactsPopover" } - fn keymap_context(&self, _: &AppContext) -> keymap::Context { - let mut cx = Self::default_keymap_context(); - cx.set.insert("menu".into()); - cx - } - fn render(&mut self, cx: &mut RenderContext) -> ElementBox { - enum AddContact {} let theme = cx.global::().theme.clone(); + let child = match &self.child { + Child::ContactList(child) => ChildView::new(child), + Child::ContactFinder(child) => ChildView::new(child), + }; - Flex::column() - .with_child( - Flex::row() - .with_child( - ChildView::new(self.filter_editor.clone()) - .contained() - .with_style(theme.contacts_popover.user_query_editor.container) - .flex(1., true) - .boxed(), - ) - .with_child( - MouseEventHandler::::new(0, cx, |_, _| { - Svg::new("icons/user_plus_16.svg") - .with_color(theme.contacts_popover.add_contact_button.color) - .constrained() - .with_height(16.) - .contained() - .with_style(theme.contacts_popover.add_contact_button.container) - .aligned() - .boxed() - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, |_, cx| { - cx.dispatch_action(contact_finder::Toggle) - }) - .boxed(), - ) - .constrained() - .with_height(theme.contacts_popover.user_query_editor_height) - .boxed(), - ) - .with_child(List::new(self.list_state.clone()).flex(1., false).boxed()) - .with_children( - self.user_store - .read(cx) - .invite_info() - .cloned() - .and_then(|info| { - enum InviteLink {} - - if info.count > 0 { - Some( - MouseEventHandler::::new(0, cx, |state, cx| { - let style = theme - .contacts_popover - .invite_row - .style_for(state, false) - .clone(); - - let copied = cx.read_from_clipboard().map_or(false, |item| { - item.text().as_str() == info.url.as_ref() - }); - - Label::new( - format!( - "{} invite link ({} left)", - if copied { "Copied" } else { "Copy" }, - info.count - ), - style.label.clone(), - ) - .aligned() - .left() - .constrained() - .with_height(theme.contacts_popover.row_height) - .contained() - .with_style(style.container) - .boxed() - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, move |_, cx| { - cx.write_to_clipboard(ClipboardItem::new(info.url.to_string())); - cx.notify(); - }) - .boxed(), - ) - } else { - None - } - }), - ) + child .contained() .with_style(theme.contacts_popover.container) .constrained() @@ -958,27 +102,11 @@ impl View for ContactsPopover { } fn on_focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext) { - if !self.filter_editor.is_focused(cx) { - cx.focus(&self.filter_editor); - } - } - - fn on_focus_out(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext) { - if !self.filter_editor.is_focused(cx) { - cx.emit(Event::Dismissed); + if cx.is_self_focused() { + match &self.child { + Child::ContactList(child) => cx.focus(child), + Child::ContactFinder(child) => cx.focus(child), + } } } } - -fn render_icon_button(style: &IconButton, svg_path: &'static str) -> impl Element { - Svg::new(svg_path) - .with_color(style.color) - .constrained() - .with_width(style.icon_width) - .aligned() - .contained() - .with_style(style.container) - .constrained() - .with_width(style.button_width) - .with_height(style.button_width) -} diff --git a/crates/collab_ui/src/incoming_call_notification.rs b/crates/collab_ui/src/incoming_call_notification.rs index 0581859ea9ed8da03cce0f5320fe278cbdec943f..47fe8cbbfbdc5976a144d0790603b2f1afb20850 100644 --- a/crates/collab_ui/src/incoming_call_notification.rs +++ b/crates/collab_ui/src/incoming_call_notification.rs @@ -82,20 +82,22 @@ impl IncomingCallNotification { } fn render_caller(&self, cx: &mut RenderContext) -> ElementBox { - let theme = &cx.global::().theme.contacts_popover; + let theme = &cx.global::().theme.incoming_call_notification; Flex::row() .with_children( self.call .caller .avatar .clone() - .map(|avatar| Image::new(avatar).with_style(theme.contact_avatar).boxed()), + .map(|avatar| Image::new(avatar).with_style(theme.caller_avatar).boxed()), ) .with_child( Label::new( self.call.caller.github_login.clone(), - theme.contact_username.text.clone(), + theme.caller_username.text.clone(), ) + .contained() + .with_style(theme.caller_username.container) .boxed(), ) .boxed() @@ -108,8 +110,11 @@ impl IncomingCallNotification { Flex::row() .with_child( MouseEventHandler::::new(0, cx, |_, cx| { - let theme = &cx.global::().theme.contacts_popover; - Label::new("Accept".to_string(), theme.contact_username.text.clone()).boxed() + let theme = &cx.global::().theme.incoming_call_notification; + Label::new("Accept".to_string(), theme.accept_button.text.clone()) + .contained() + .with_style(theme.accept_button.container) + .boxed() }) .on_click(MouseButton::Left, |_, cx| { cx.dispatch_action(RespondToCall { accept: true }); @@ -118,8 +123,11 @@ impl IncomingCallNotification { ) .with_child( MouseEventHandler::::new(0, cx, |_, cx| { - let theme = &cx.global::().theme.contacts_popover; - Label::new("Decline".to_string(), theme.contact_username.text.clone()).boxed() + let theme = &cx.global::().theme.incoming_call_notification; + Label::new("Decline".to_string(), theme.decline_button.text.clone()) + .contained() + .with_style(theme.decline_button.container) + .boxed() }) .on_click(MouseButton::Left, |_, cx| { cx.dispatch_action(RespondToCall { accept: false }); diff --git a/crates/picker/src/picker.rs b/crates/picker/src/picker.rs index c2fac6371e3d916c935ebaa158dbe06ae0ffea9c..622dc13309a9b41171d8c45053fe8a835d9270d5 100644 --- a/crates/picker/src/picker.rs +++ b/crates/picker/src/picker.rs @@ -19,6 +19,7 @@ pub struct Picker { query_editor: ViewHandle, list_state: UniformListState, max_size: Vector2F, + theme: Box &theme::Picker>, confirmed: bool, } @@ -51,8 +52,8 @@ impl View for Picker { } fn render(&mut self, cx: &mut RenderContext) -> gpui::ElementBox { - let settings = cx.global::(); - let container_style = settings.theme.picker.container; + let theme = (self.theme)(cx); + let container_style = theme.container; let delegate = self.delegate.clone(); let match_count = if let Some(delegate) = delegate.upgrade(cx.app) { delegate.read(cx).match_count() @@ -64,17 +65,14 @@ impl View for Picker { .with_child( ChildView::new(&self.query_editor) .contained() - .with_style(settings.theme.picker.input_editor.container) + .with_style(theme.input_editor.container) .boxed(), ) .with_child( if match_count == 0 { - Label::new( - "No matches".into(), - settings.theme.picker.empty.label.clone(), - ) - .contained() - .with_style(settings.theme.picker.empty.container) + Label::new("No matches".into(), theme.empty.label.clone()) + .contained() + .with_style(theme.empty.container) } else { UniformList::new( self.list_state.clone(), @@ -147,6 +145,7 @@ impl Picker { list_state: Default::default(), delegate, max_size: vec2f(540., 420.), + theme: Box::new(|cx| &cx.global::().theme.picker), confirmed: false, }; cx.defer(|this, cx| { @@ -163,6 +162,14 @@ impl Picker { self } + pub fn with_theme(mut self, theme: F) -> Self + where + F: 'static + FnMut(&AppContext) -> &theme::Picker, + { + self.theme = Box::new(theme); + self + } + pub fn query(&self, cx: &AppContext) -> String { self.query_editor.read(cx).text(cx) } diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 96d5b07582463ecdc1d6882c36b58ff43fcce943..268a655c230ace0afaa74b3db70ace7a2c85c8de 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -20,6 +20,7 @@ pub struct Theme { pub context_menu: ContextMenu, pub chat_panel: ChatPanel, pub contacts_popover: ContactsPopover, + pub contact_list: ContactList, pub contact_finder: ContactFinder, pub project_panel: ProjectPanel, pub command_palette: CommandPalette, @@ -31,6 +32,7 @@ pub struct Theme { pub contact_notification: ContactNotification, pub update_notification: UpdateNotification, pub project_shared_notification: ProjectSharedNotification, + pub incoming_call_notification: IncomingCallNotification, pub tooltip: TooltipStyle, pub terminal: TerminalStyle, } @@ -87,6 +89,10 @@ pub struct ContactsPopover { pub container: ContainerStyle, pub height: f32, pub width: f32, +} + +#[derive(Deserialize, Default)] +pub struct ContactList { pub user_query_editor: FieldEditor, pub user_query_editor_height: f32, pub add_contact_button: IconButton, @@ -105,6 +111,16 @@ pub struct ContactsPopover { pub calling_indicator: ContainedText, } +#[derive(Deserialize, Default)] +pub struct ContactFinder { + pub picker: Picker, + pub row_height: f32, + pub contact_avatar: ImageStyle, + pub contact_username: ContainerStyle, + pub contact_button: IconButton, + pub disabled_contact_button: IconButton, +} + #[derive(Clone, Deserialize, Default)] pub struct TabBar { #[serde(flatten)] @@ -353,15 +369,6 @@ pub struct InviteLink { pub icon: Icon, } -#[derive(Deserialize, Default)] -pub struct ContactFinder { - pub row_height: f32, - pub contact_avatar: ImageStyle, - pub contact_username: ContainerStyle, - pub contact_button: IconButton, - pub disabled_contact_button: IconButton, -} - #[derive(Deserialize, Default)] pub struct Icon { #[serde(flatten)] @@ -469,6 +476,14 @@ pub struct ProjectSharedNotification { pub dismiss_button: ContainedText, } +#[derive(Deserialize, Default)] +pub struct IncomingCallNotification { + pub caller_avatar: ImageStyle, + pub caller_username: ContainedText, + pub accept_button: ContainedText, + pub decline_button: ContainedText, +} + #[derive(Clone, Deserialize, Default)] pub struct Editor { pub text_color: Color, diff --git a/styles/src/styleTree/app.ts b/styles/src/styleTree/app.ts index 1b1aa2691657adc290dd2182ffae90e87e83d98a..f540074a70c47daaa0e57dbb56e13ed10596a78c 100644 --- a/styles/src/styleTree/app.ts +++ b/styles/src/styleTree/app.ts @@ -16,6 +16,8 @@ import updateNotification from "./updateNotification"; import projectSharedNotification from "./projectSharedNotification"; import tooltip from "./tooltip"; import terminal from "./terminal"; +import contactList from "./contactList"; +import incomingCallNotification from "./incomingCallNotification"; export const panel = { padding: { top: 12, bottom: 12 }, @@ -36,6 +38,7 @@ export default function app(theme: Theme): Object { projectPanel: projectPanel(theme), chatPanel: chatPanel(theme), contactsPopover: contactsPopover(theme), + contactList: contactList(theme), contactFinder: contactFinder(theme), search: search(theme), breadcrumbs: { @@ -47,6 +50,7 @@ export default function app(theme: Theme): Object { contactNotification: contactNotification(theme), updateNotification: updateNotification(theme), projectSharedNotification: projectSharedNotification(theme), + incomingCallNotification: incomingCallNotification(theme), tooltip: tooltip(theme), terminal: terminal(theme), }; diff --git a/styles/src/styleTree/contactFinder.ts b/styles/src/styleTree/contactFinder.ts index e34fac4b2d734734f6b42d5089a9d7decf852919..bf43a74666da6325676140c9a5c6720b77b0547a 100644 --- a/styles/src/styleTree/contactFinder.ts +++ b/styles/src/styleTree/contactFinder.ts @@ -1,6 +1,6 @@ import Theme from "../themes/common/theme"; import picker from "./picker"; -import { backgroundColor, iconColor } from "./components"; +import { backgroundColor, border, iconColor, player, text } from "./components"; export default function contactFinder(theme: Theme) { const contactButton = { @@ -12,7 +12,28 @@ export default function contactFinder(theme: Theme) { }; return { - ...picker(theme), + picker: { + item: picker(theme).item, + empty: picker(theme).empty, + inputEditor: { + background: backgroundColor(theme, 500), + cornerRadius: 6, + text: text(theme, "mono", "primary"), + placeholderText: text(theme, "mono", "placeholder", { size: "sm" }), + selection: player(theme, 1).selection, + border: border(theme, "secondary"), + padding: { + bottom: 4, + left: 8, + right: 8, + top: 4, + }, + margin: { + left: 12, + right: 12, + } + } + }, rowHeight: 28, contactAvatar: { cornerRadius: 10, diff --git a/styles/src/styleTree/contactsPopover.ts b/styles/src/styleTree/contactsPopover.ts index 8786e81f5c25439c8fdf8e65e5fefe2fefea5231..57af5a6d4d3cdb2b32aa6d73d5f824aeadf24162 100644 --- a/styles/src/styleTree/contactsPopover.ts +++ b/styles/src/styleTree/contactsPopover.ts @@ -19,7 +19,6 @@ export default function contactsPopover(theme: Theme) { padding: { top: 6 }, shadow: popoverShadow(theme), border: border(theme, "primary"), - margin: { top: -5 }, width: 250, height: 300, userQueryEditor: { From 79748803a90fb2418cc391ed8b7a3d710393c9cd Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 10 Oct 2022 10:30:51 +0200 Subject: [PATCH 084/112] Add leave button on active call header --- crates/call/src/call.rs | 1 + crates/collab_ui/src/contact_list.rs | 1008 +++++++++++++++++ crates/theme/src/theme.rs | 1 + styles/src/styleTree/contactList.ts | 134 +++ .../src/styleTree/incomingCallNotification.ts | 22 + 5 files changed, 1166 insertions(+) create mode 100644 crates/collab_ui/src/contact_list.rs create mode 100644 styles/src/styleTree/contactList.ts create mode 100644 styles/src/styleTree/incomingCallNotification.ts diff --git a/crates/call/src/call.rs b/crates/call/src/call.rs index 7015173ce73523d94dd1414741c2f0578a1ab9a5..99edb33b6eb2c14038e5e1e0e41314d322c917ed 100644 --- a/crates/call/src/call.rs +++ b/crates/call/src/call.rs @@ -201,6 +201,7 @@ impl ActiveCall { pub fn hang_up(&mut self, cx: &mut ModelContext) -> Result<()> { if let Some((room, _)) = self.room.take() { room.update(cx, |room, cx| room.leave(cx))?; + cx.notify(); } Ok(()) } diff --git a/crates/collab_ui/src/contact_list.rs b/crates/collab_ui/src/contact_list.rs new file mode 100644 index 0000000000000000000000000000000000000000..a539f8ffac9e0a3e88b30562b46d45ed5b521b87 --- /dev/null +++ b/crates/collab_ui/src/contact_list.rs @@ -0,0 +1,1008 @@ +use std::sync::Arc; + +use crate::contacts_popover; +use call::ActiveCall; +use client::{Contact, PeerId, User, UserStore}; +use editor::{Cancel, Editor}; +use fuzzy::{match_strings, StringMatchCandidate}; +use gpui::{ + elements::*, impl_actions, impl_internal_actions, keymap, AppContext, ClipboardItem, + CursorStyle, Entity, ModelHandle, MouseButton, MutableAppContext, RenderContext, Subscription, + View, ViewContext, ViewHandle, +}; +use menu::{Confirm, SelectNext, SelectPrev}; +use project::Project; +use serde::Deserialize; +use settings::Settings; +use theme::IconButton; +use util::ResultExt; + +impl_actions!(contact_list, [RemoveContact, RespondToContactRequest]); +impl_internal_actions!(contact_list, [ToggleExpanded, Call, LeaveCall]); + +pub fn init(cx: &mut MutableAppContext) { + cx.add_action(ContactList::remove_contact); + cx.add_action(ContactList::respond_to_contact_request); + cx.add_action(ContactList::clear_filter); + cx.add_action(ContactList::select_next); + cx.add_action(ContactList::select_prev); + cx.add_action(ContactList::confirm); + cx.add_action(ContactList::toggle_expanded); + cx.add_action(ContactList::call); + cx.add_action(ContactList::leave_call); +} + +#[derive(Clone, PartialEq)] +struct ToggleExpanded(Section); + +#[derive(Clone, PartialEq)] +struct Call { + recipient_user_id: u64, + initial_project: Option>, +} + +#[derive(Copy, Clone, PartialEq)] +struct LeaveCall; + +#[derive(Clone, Copy, PartialEq, Eq, Debug, PartialOrd, Ord)] +enum Section { + ActiveCall, + Requests, + Online, + Offline, +} + +#[derive(Clone)] +enum ContactEntry { + Header(Section), + CallParticipant { user: Arc, is_pending: bool }, + IncomingRequest(Arc), + OutgoingRequest(Arc), + Contact(Arc), +} + +impl PartialEq for ContactEntry { + fn eq(&self, other: &Self) -> bool { + match self { + ContactEntry::Header(section_1) => { + if let ContactEntry::Header(section_2) = other { + return section_1 == section_2; + } + } + ContactEntry::CallParticipant { user: user_1, .. } => { + if let ContactEntry::CallParticipant { user: user_2, .. } = other { + return user_1.id == user_2.id; + } + } + ContactEntry::IncomingRequest(user_1) => { + if let ContactEntry::IncomingRequest(user_2) = other { + return user_1.id == user_2.id; + } + } + ContactEntry::OutgoingRequest(user_1) => { + if let ContactEntry::OutgoingRequest(user_2) = other { + return user_1.id == user_2.id; + } + } + ContactEntry::Contact(contact_1) => { + if let ContactEntry::Contact(contact_2) = other { + return contact_1.user.id == contact_2.user.id; + } + } + } + false + } +} + +#[derive(Clone, Deserialize, PartialEq)] +pub struct RequestContact(pub u64); + +#[derive(Clone, Deserialize, PartialEq)] +pub struct RemoveContact(pub u64); + +#[derive(Clone, Deserialize, PartialEq)] +pub struct RespondToContactRequest { + pub user_id: u64, + pub accept: bool, +} + +pub enum Event { + Dismissed, +} + +pub struct ContactList { + entries: Vec, + match_candidates: Vec, + list_state: ListState, + project: ModelHandle, + user_store: ModelHandle, + filter_editor: ViewHandle, + collapsed_sections: Vec
, + selection: Option, + _subscriptions: Vec, +} + +impl ContactList { + pub fn new( + project: ModelHandle, + user_store: ModelHandle, + cx: &mut ViewContext, + ) -> Self { + let filter_editor = cx.add_view(|cx| { + let mut editor = Editor::single_line( + Some(|theme| theme.contact_list.user_query_editor.clone()), + cx, + ); + editor.set_placeholder_text("Filter contacts", cx); + editor + }); + + cx.subscribe(&filter_editor, |this, _, event, cx| { + if let editor::Event::BufferEdited = event { + let query = this.filter_editor.read(cx).text(cx); + if !query.is_empty() { + this.selection.take(); + } + this.update_entries(cx); + if !query.is_empty() { + this.selection = this + .entries + .iter() + .position(|entry| !matches!(entry, ContactEntry::Header(_))); + } + } + }) + .detach(); + + let list_state = ListState::new(0, Orientation::Top, 1000., cx, move |this, ix, cx| { + let theme = cx.global::().theme.clone(); + let is_selected = this.selection == Some(ix); + + match &this.entries[ix] { + ContactEntry::Header(section) => { + let is_collapsed = this.collapsed_sections.contains(section); + Self::render_header( + *section, + &theme.contact_list, + is_selected, + is_collapsed, + cx, + ) + } + ContactEntry::CallParticipant { user, is_pending } => { + Self::render_call_participant( + user, + *is_pending, + is_selected, + &theme.contact_list, + ) + } + ContactEntry::IncomingRequest(user) => Self::render_contact_request( + user.clone(), + this.user_store.clone(), + &theme.contact_list, + true, + is_selected, + cx, + ), + ContactEntry::OutgoingRequest(user) => Self::render_contact_request( + user.clone(), + this.user_store.clone(), + &theme.contact_list, + false, + is_selected, + cx, + ), + ContactEntry::Contact(contact) => Self::render_contact( + contact, + &this.project, + &theme.contact_list, + is_selected, + cx, + ), + } + }); + + let active_call = ActiveCall::global(cx); + let mut subscriptions = Vec::new(); + subscriptions.push(cx.observe(&user_store, |this, _, cx| this.update_entries(cx))); + subscriptions.push(cx.observe(&active_call, |this, _, cx| this.update_entries(cx))); + + let mut this = Self { + list_state, + selection: None, + collapsed_sections: Default::default(), + entries: Default::default(), + match_candidates: Default::default(), + filter_editor, + _subscriptions: subscriptions, + project, + user_store, + }; + this.update_entries(cx); + this + } + + fn remove_contact(&mut self, request: &RemoveContact, cx: &mut ViewContext) { + self.user_store + .update(cx, |store, cx| store.remove_contact(request.0, cx)) + .detach(); + } + + fn respond_to_contact_request( + &mut self, + action: &RespondToContactRequest, + cx: &mut ViewContext, + ) { + self.user_store + .update(cx, |store, cx| { + store.respond_to_contact_request(action.user_id, action.accept, cx) + }) + .detach(); + } + + fn clear_filter(&mut self, _: &Cancel, cx: &mut ViewContext) { + let did_clear = self.filter_editor.update(cx, |editor, cx| { + if editor.buffer().read(cx).len(cx) > 0 { + editor.set_text("", cx); + true + } else { + false + } + }); + if !did_clear { + cx.emit(Event::Dismissed); + } + } + + fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext) { + if let Some(ix) = self.selection { + if self.entries.len() > ix + 1 { + self.selection = Some(ix + 1); + } + } else if !self.entries.is_empty() { + self.selection = Some(0); + } + cx.notify(); + self.list_state.reset(self.entries.len()); + } + + fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext) { + if let Some(ix) = self.selection { + if ix > 0 { + self.selection = Some(ix - 1); + } else { + self.selection = None; + } + } + cx.notify(); + self.list_state.reset(self.entries.len()); + } + + fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext) { + if let Some(selection) = self.selection { + if let Some(entry) = self.entries.get(selection) { + match entry { + ContactEntry::Header(section) => { + let section = *section; + self.toggle_expanded(&ToggleExpanded(section), cx); + } + ContactEntry::Contact(contact) => { + if contact.online && !contact.busy { + self.call( + &Call { + recipient_user_id: contact.user.id, + initial_project: Some(self.project.clone()), + }, + cx, + ); + } + } + _ => {} + } + } + } + } + + fn toggle_expanded(&mut self, action: &ToggleExpanded, cx: &mut ViewContext) { + let section = action.0; + if let Some(ix) = self.collapsed_sections.iter().position(|s| *s == section) { + self.collapsed_sections.remove(ix); + } else { + self.collapsed_sections.push(section); + } + self.update_entries(cx); + } + + fn update_entries(&mut self, cx: &mut ViewContext) { + let user_store = self.user_store.read(cx); + let query = self.filter_editor.read(cx).text(cx); + let executor = cx.background().clone(); + + let prev_selected_entry = self.selection.and_then(|ix| self.entries.get(ix).cloned()); + self.entries.clear(); + + if let Some(room) = ActiveCall::global(cx).read(cx).room() { + let room = room.read(cx); + let mut call_participants = Vec::new(); + + // Populate the active user. + if let Some(user) = user_store.current_user() { + self.match_candidates.clear(); + self.match_candidates.push(StringMatchCandidate { + id: 0, + string: user.github_login.clone(), + char_bag: user.github_login.chars().collect(), + }); + let matches = executor.block(match_strings( + &self.match_candidates, + &query, + true, + usize::MAX, + &Default::default(), + executor.clone(), + )); + if !matches.is_empty() { + call_participants.push(ContactEntry::CallParticipant { + user, + is_pending: false, + }); + } + } + + // Populate remote participants. + self.match_candidates.clear(); + self.match_candidates + .extend( + room.remote_participants() + .iter() + .map(|(peer_id, participant)| StringMatchCandidate { + id: peer_id.0 as usize, + string: participant.user.github_login.clone(), + char_bag: participant.user.github_login.chars().collect(), + }), + ); + let matches = executor.block(match_strings( + &self.match_candidates, + &query, + true, + usize::MAX, + &Default::default(), + executor.clone(), + )); + call_participants.extend(matches.iter().map(|mat| { + ContactEntry::CallParticipant { + user: room.remote_participants()[&PeerId(mat.candidate_id as u32)] + .user + .clone(), + is_pending: false, + } + })); + + // Populate pending participants. + self.match_candidates.clear(); + self.match_candidates + .extend( + room.pending_participants() + .iter() + .enumerate() + .map(|(id, participant)| StringMatchCandidate { + id, + string: participant.github_login.clone(), + char_bag: participant.github_login.chars().collect(), + }), + ); + let matches = executor.block(match_strings( + &self.match_candidates, + &query, + true, + usize::MAX, + &Default::default(), + executor.clone(), + )); + call_participants.extend(matches.iter().map(|mat| ContactEntry::CallParticipant { + user: room.pending_participants()[mat.candidate_id].clone(), + is_pending: true, + })); + + if !call_participants.is_empty() { + self.entries.push(ContactEntry::Header(Section::ActiveCall)); + if !self.collapsed_sections.contains(&Section::ActiveCall) { + self.entries.extend(call_participants); + } + } + } + + let mut request_entries = Vec::new(); + let incoming = user_store.incoming_contact_requests(); + if !incoming.is_empty() { + self.match_candidates.clear(); + self.match_candidates + .extend( + incoming + .iter() + .enumerate() + .map(|(ix, user)| StringMatchCandidate { + id: ix, + string: user.github_login.clone(), + char_bag: user.github_login.chars().collect(), + }), + ); + let matches = executor.block(match_strings( + &self.match_candidates, + &query, + true, + usize::MAX, + &Default::default(), + executor.clone(), + )); + request_entries.extend( + matches + .iter() + .map(|mat| ContactEntry::IncomingRequest(incoming[mat.candidate_id].clone())), + ); + } + + let outgoing = user_store.outgoing_contact_requests(); + if !outgoing.is_empty() { + self.match_candidates.clear(); + self.match_candidates + .extend( + outgoing + .iter() + .enumerate() + .map(|(ix, user)| StringMatchCandidate { + id: ix, + string: user.github_login.clone(), + char_bag: user.github_login.chars().collect(), + }), + ); + let matches = executor.block(match_strings( + &self.match_candidates, + &query, + true, + usize::MAX, + &Default::default(), + executor.clone(), + )); + request_entries.extend( + matches + .iter() + .map(|mat| ContactEntry::OutgoingRequest(outgoing[mat.candidate_id].clone())), + ); + } + + if !request_entries.is_empty() { + self.entries.push(ContactEntry::Header(Section::Requests)); + if !self.collapsed_sections.contains(&Section::Requests) { + self.entries.append(&mut request_entries); + } + } + + let contacts = user_store.contacts(); + if !contacts.is_empty() { + self.match_candidates.clear(); + self.match_candidates + .extend( + contacts + .iter() + .enumerate() + .map(|(ix, contact)| StringMatchCandidate { + id: ix, + string: contact.user.github_login.clone(), + char_bag: contact.user.github_login.chars().collect(), + }), + ); + + let matches = executor.block(match_strings( + &self.match_candidates, + &query, + true, + usize::MAX, + &Default::default(), + executor.clone(), + )); + + let (mut online_contacts, offline_contacts) = matches + .iter() + .partition::, _>(|mat| contacts[mat.candidate_id].online); + if let Some(room) = ActiveCall::global(cx).read(cx).room() { + let room = room.read(cx); + online_contacts.retain(|contact| { + let contact = &contacts[contact.candidate_id]; + !room.contains_participant(contact.user.id) + }); + } + + for (matches, section) in [ + (online_contacts, Section::Online), + (offline_contacts, Section::Offline), + ] { + if !matches.is_empty() { + self.entries.push(ContactEntry::Header(section)); + if !self.collapsed_sections.contains(§ion) { + for mat in matches { + let contact = &contacts[mat.candidate_id]; + self.entries.push(ContactEntry::Contact(contact.clone())); + } + } + } + } + } + + if let Some(prev_selected_entry) = prev_selected_entry { + self.selection.take(); + for (ix, entry) in self.entries.iter().enumerate() { + if *entry == prev_selected_entry { + self.selection = Some(ix); + break; + } + } + } + + self.list_state.reset(self.entries.len()); + cx.notify(); + } + + fn render_call_participant( + user: &User, + is_pending: bool, + is_selected: bool, + theme: &theme::ContactList, + ) -> ElementBox { + Flex::row() + .with_children(user.avatar.clone().map(|avatar| { + Image::new(avatar) + .with_style(theme.contact_avatar) + .aligned() + .left() + .boxed() + })) + .with_child( + Label::new( + user.github_login.clone(), + theme.contact_username.text.clone(), + ) + .contained() + .with_style(theme.contact_username.container) + .aligned() + .left() + .flex(1., true) + .boxed(), + ) + .with_children(if is_pending { + Some( + Label::new("Calling".to_string(), theme.calling_indicator.text.clone()) + .contained() + .with_style(theme.calling_indicator.container) + .aligned() + .boxed(), + ) + } else { + None + }) + .constrained() + .with_height(theme.row_height) + .contained() + .with_style(*theme.contact_row.style_for(Default::default(), is_selected)) + .boxed() + } + + fn render_header( + section: Section, + theme: &theme::ContactList, + is_selected: bool, + is_collapsed: bool, + cx: &mut RenderContext, + ) -> ElementBox { + enum Header {} + + let header_style = theme.header_row.style_for(Default::default(), is_selected); + let text = match section { + Section::ActiveCall => "Call", + Section::Requests => "Requests", + Section::Online => "Online", + Section::Offline => "Offline", + }; + let leave_call = if section == Section::ActiveCall { + Some( + MouseEventHandler::::new(0, cx, |state, _| { + let style = theme.leave_call.style_for(state, false); + Label::new("Leave".into(), style.text.clone()) + .contained() + .with_style(style.container) + .boxed() + }) + .on_click(MouseButton::Left, |_, cx| cx.dispatch_action(LeaveCall)) + .aligned() + .boxed(), + ) + } else { + None + }; + + let icon_size = theme.section_icon_size; + MouseEventHandler::
::new(section as usize, cx, |_, _| { + Flex::row() + .with_child( + Svg::new(if is_collapsed { + "icons/chevron_right_8.svg" + } else { + "icons/chevron_down_8.svg" + }) + .with_color(header_style.text.color) + .constrained() + .with_max_width(icon_size) + .with_max_height(icon_size) + .aligned() + .constrained() + .with_width(icon_size) + .boxed(), + ) + .with_child( + Label::new(text.to_string(), header_style.text.clone()) + .aligned() + .left() + .contained() + .with_margin_left(theme.contact_username.container.margin.left) + .flex(1., true) + .boxed(), + ) + .with_children(leave_call) + .constrained() + .with_height(theme.row_height) + .contained() + .with_style(header_style.container) + .boxed() + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, move |_, cx| { + cx.dispatch_action(ToggleExpanded(section)) + }) + .boxed() + } + + fn render_contact( + contact: &Contact, + project: &ModelHandle, + theme: &theme::ContactList, + is_selected: bool, + cx: &mut RenderContext, + ) -> ElementBox { + let online = contact.online; + let busy = contact.busy; + let user_id = contact.user.id; + let initial_project = project.clone(); + let mut element = + MouseEventHandler::::new(contact.user.id as usize, cx, |_, _| { + Flex::row() + .with_children(contact.user.avatar.clone().map(|avatar| { + let status_badge = if contact.online { + Some( + Empty::new() + .collapsed() + .contained() + .with_style(if contact.busy { + theme.contact_status_busy + } else { + theme.contact_status_free + }) + .aligned() + .boxed(), + ) + } else { + None + }; + Stack::new() + .with_child( + Image::new(avatar) + .with_style(theme.contact_avatar) + .aligned() + .left() + .boxed(), + ) + .with_children(status_badge) + .boxed() + })) + .with_child( + Label::new( + contact.user.github_login.clone(), + theme.contact_username.text.clone(), + ) + .contained() + .with_style(theme.contact_username.container) + .aligned() + .left() + .flex(1., true) + .boxed(), + ) + .constrained() + .with_height(theme.row_height) + .contained() + .with_style(*theme.contact_row.style_for(Default::default(), is_selected)) + .boxed() + }) + .on_click(MouseButton::Left, move |_, cx| { + if online && !busy { + cx.dispatch_action(Call { + recipient_user_id: user_id, + initial_project: Some(initial_project.clone()), + }); + } + }); + + if online { + element = element.with_cursor_style(CursorStyle::PointingHand); + } + + element.boxed() + } + + fn render_contact_request( + user: Arc, + user_store: ModelHandle, + theme: &theme::ContactList, + is_incoming: bool, + is_selected: bool, + cx: &mut RenderContext, + ) -> ElementBox { + enum Decline {} + enum Accept {} + enum Cancel {} + + let mut row = Flex::row() + .with_children(user.avatar.clone().map(|avatar| { + Image::new(avatar) + .with_style(theme.contact_avatar) + .aligned() + .left() + .boxed() + })) + .with_child( + Label::new( + user.github_login.clone(), + theme.contact_username.text.clone(), + ) + .contained() + .with_style(theme.contact_username.container) + .aligned() + .left() + .flex(1., true) + .boxed(), + ); + + let user_id = user.id; + let is_contact_request_pending = user_store.read(cx).is_contact_request_pending(&user); + let button_spacing = theme.contact_button_spacing; + + if is_incoming { + row.add_children([ + MouseEventHandler::::new(user.id as usize, cx, |mouse_state, _| { + let button_style = if is_contact_request_pending { + &theme.disabled_button + } else { + theme.contact_button.style_for(mouse_state, false) + }; + render_icon_button(button_style, "icons/x_mark_8.svg") + .aligned() + .boxed() + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, move |_, cx| { + cx.dispatch_action(RespondToContactRequest { + user_id, + accept: false, + }) + }) + .contained() + .with_margin_right(button_spacing) + .boxed(), + MouseEventHandler::::new(user.id as usize, cx, |mouse_state, _| { + let button_style = if is_contact_request_pending { + &theme.disabled_button + } else { + theme.contact_button.style_for(mouse_state, false) + }; + render_icon_button(button_style, "icons/check_8.svg") + .aligned() + .flex_float() + .boxed() + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, move |_, cx| { + cx.dispatch_action(RespondToContactRequest { + user_id, + accept: true, + }) + }) + .boxed(), + ]); + } else { + row.add_child( + MouseEventHandler::::new(user.id as usize, cx, |mouse_state, _| { + let button_style = if is_contact_request_pending { + &theme.disabled_button + } else { + theme.contact_button.style_for(mouse_state, false) + }; + render_icon_button(button_style, "icons/x_mark_8.svg") + .aligned() + .flex_float() + .boxed() + }) + .with_padding(Padding::uniform(2.)) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, move |_, cx| { + cx.dispatch_action(RemoveContact(user_id)) + }) + .flex_float() + .boxed(), + ); + } + + row.constrained() + .with_height(theme.row_height) + .contained() + .with_style(*theme.contact_row.style_for(Default::default(), is_selected)) + .boxed() + } + + fn call(&mut self, action: &Call, cx: &mut ViewContext) { + let recipient_user_id = action.recipient_user_id; + let initial_project = action.initial_project.clone(); + let window_id = cx.window_id(); + + let active_call = ActiveCall::global(cx); + cx.spawn_weak(|_, mut cx| async move { + active_call + .update(&mut cx, |active_call, cx| { + active_call.invite(recipient_user_id, initial_project.clone(), cx) + }) + .await?; + if cx.update(|cx| cx.window_is_active(window_id)) { + active_call + .update(&mut cx, |call, cx| { + call.set_location(initial_project.as_ref(), cx) + }) + .await?; + } + anyhow::Ok(()) + }) + .detach_and_log_err(cx); + } + + fn leave_call(&mut self, _: &LeaveCall, cx: &mut ViewContext) { + ActiveCall::global(cx) + .update(cx, |call, cx| call.hang_up(cx)) + .log_err(); + } +} + +impl Entity for ContactList { + type Event = Event; +} + +impl View for ContactList { + fn ui_name() -> &'static str { + "ContactList" + } + + fn keymap_context(&self, _: &AppContext) -> keymap::Context { + let mut cx = Self::default_keymap_context(); + cx.set.insert("menu".into()); + cx + } + + fn render(&mut self, cx: &mut RenderContext) -> ElementBox { + enum AddContact {} + let theme = cx.global::().theme.clone(); + + Flex::column() + .with_child( + Flex::row() + .with_child( + ChildView::new(self.filter_editor.clone()) + .contained() + .with_style(theme.contact_list.user_query_editor.container) + .flex(1., true) + .boxed(), + ) + .with_child( + MouseEventHandler::::new(0, cx, |_, _| { + Svg::new("icons/user_plus_16.svg") + .with_color(theme.contact_list.add_contact_button.color) + .constrained() + .with_height(16.) + .contained() + .with_style(theme.contact_list.add_contact_button.container) + .aligned() + .boxed() + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, |_, cx| { + cx.dispatch_action(contacts_popover::ToggleContactFinder) + }) + .boxed(), + ) + .constrained() + .with_height(theme.contact_list.user_query_editor_height) + .boxed(), + ) + .with_child(List::new(self.list_state.clone()).flex(1., false).boxed()) + .with_children( + self.user_store + .read(cx) + .invite_info() + .cloned() + .and_then(|info| { + enum InviteLink {} + + if info.count > 0 { + Some( + MouseEventHandler::::new(0, cx, |state, cx| { + let style = theme + .contact_list + .invite_row + .style_for(state, false) + .clone(); + + let copied = cx.read_from_clipboard().map_or(false, |item| { + item.text().as_str() == info.url.as_ref() + }); + + Label::new( + format!( + "{} invite link ({} left)", + if copied { "Copied" } else { "Copy" }, + info.count + ), + style.label.clone(), + ) + .aligned() + .left() + .constrained() + .with_height(theme.contact_list.row_height) + .contained() + .with_style(style.container) + .boxed() + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, move |_, cx| { + cx.write_to_clipboard(ClipboardItem::new(info.url.to_string())); + cx.notify(); + }) + .boxed(), + ) + } else { + None + } + }), + ) + .boxed() + } + + fn on_focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext) { + if !self.filter_editor.is_focused(cx) { + cx.focus(&self.filter_editor); + } + } + + fn on_focus_out(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext) { + if !self.filter_editor.is_focused(cx) { + cx.emit(Event::Dismissed); + } + } +} + +fn render_icon_button(style: &IconButton, svg_path: &'static str) -> impl Element { + Svg::new(svg_path) + .with_color(style.color) + .constrained() + .with_width(style.icon_width) + .aligned() + .contained() + .with_style(style.container) + .constrained() + .with_width(style.button_width) + .with_height(style.button_width) +} diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 268a655c230ace0afaa74b3db70ace7a2c85c8de..d69720322eb80fa9edac6f12fe61bcff11ea072e 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -97,6 +97,7 @@ pub struct ContactList { pub user_query_editor_height: f32, pub add_contact_button: IconButton, pub header_row: Interactive, + pub leave_call: Interactive, pub contact_row: Interactive, pub row_height: f32, pub contact_avatar: ImageStyle, diff --git a/styles/src/styleTree/contactList.ts b/styles/src/styleTree/contactList.ts new file mode 100644 index 0000000000000000000000000000000000000000..f2430d31e6ccdf5f35cf07df972574b5dbb7c02b --- /dev/null +++ b/styles/src/styleTree/contactList.ts @@ -0,0 +1,134 @@ +import Theme from "../themes/common/theme"; +import { backgroundColor, border, borderColor, iconColor, player, text } from "./components"; + +export default function contactList(theme: Theme) { + const nameMargin = 8; + const sidePadding = 12; + + const contactButton = { + background: backgroundColor(theme, 100), + color: iconColor(theme, "primary"), + iconWidth: 8, + buttonWidth: 16, + cornerRadius: 8, + }; + + return { + userQueryEditor: { + background: backgroundColor(theme, 500), + cornerRadius: 6, + text: text(theme, "mono", "primary"), + placeholderText: text(theme, "mono", "placeholder", { size: "sm" }), + selection: player(theme, 1).selection, + border: border(theme, "secondary"), + padding: { + bottom: 4, + left: 8, + right: 8, + top: 4, + }, + margin: { + left: sidePadding, + right: sidePadding, + }, + }, + userQueryEditorHeight: 33, + addContactButton: { + margin: { left: 6, right: 12 }, + color: iconColor(theme, "primary"), + buttonWidth: 16, + iconWidth: 16, + }, + rowHeight: 28, + sectionIconSize: 8, + headerRow: { + ...text(theme, "mono", "secondary", { size: "sm" }), + margin: { top: 14 }, + padding: { + left: sidePadding, + right: sidePadding, + }, + active: { + ...text(theme, "mono", "primary", { size: "sm" }), + background: backgroundColor(theme, 100, "active"), + }, + }, + leaveCall: { + background: backgroundColor(theme, 100), + border: border(theme, "secondary"), + cornerRadius: 6, + margin: { + top: 1, + }, + padding: { + top: 1, + bottom: 1, + left: 7, + right: 7, + }, + ...text(theme, "sans", "secondary", { size: "xs" }), + hover: { + ...text(theme, "sans", "active", { size: "xs" }), + background: backgroundColor(theme, "on300", "hovered"), + border: border(theme, "primary"), + }, + }, + contactRow: { + padding: { + left: sidePadding, + right: sidePadding, + }, + active: { + background: backgroundColor(theme, 100, "active"), + }, + }, + contactAvatar: { + cornerRadius: 10, + width: 18, + }, + contactStatusFree: { + cornerRadius: 4, + padding: 4, + margin: { top: 12, left: 12 }, + background: iconColor(theme, "success"), + }, + contactStatusBusy: { + cornerRadius: 4, + padding: 4, + margin: { top: 12, left: 12 }, + background: iconColor(theme, "warning"), + }, + contactUsername: { + ...text(theme, "mono", "primary", { size: "sm" }), + margin: { + left: nameMargin, + }, + }, + contactButtonSpacing: nameMargin, + contactButton: { + ...contactButton, + hover: { + background: backgroundColor(theme, "on300", "hovered"), + }, + }, + disabledButton: { + ...contactButton, + background: backgroundColor(theme, 100), + color: iconColor(theme, "muted"), + }, + inviteRow: { + padding: { + left: sidePadding, + right: sidePadding, + }, + border: { top: true, width: 1, color: borderColor(theme, "primary") }, + text: text(theme, "sans", "secondary", { size: "sm" }), + hover: { + text: text(theme, "sans", "active", { size: "sm" }), + }, + }, + callingIndicator: { + ...text(theme, "mono", "muted", { size: "xs" }) + } + } +} diff --git a/styles/src/styleTree/incomingCallNotification.ts b/styles/src/styleTree/incomingCallNotification.ts new file mode 100644 index 0000000000000000000000000000000000000000..dcae47bff0b90ceb52809d99379ac97abf8e6964 --- /dev/null +++ b/styles/src/styleTree/incomingCallNotification.ts @@ -0,0 +1,22 @@ +import Theme from "../themes/common/theme"; +import { text } from "./components"; + +export default function incomingCallNotification(theme: Theme): Object { + const avatarSize = 12; + return { + callerAvatar: { + height: avatarSize, + width: avatarSize, + cornerRadius: 6, + }, + callerUsername: { + ...text(theme, "sans", "primary", { size: "xs" }), + }, + acceptButton: { + ...text(theme, "sans", "primary", { size: "xs" }) + }, + declineButton: { + ...text(theme, "sans", "primary", { size: "xs" }) + }, + }; +} From d7bac3cea6ec08aabccb866db1eb593b376cc1aa Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 10 Oct 2022 11:36:39 +0200 Subject: [PATCH 085/112] Style incoming call notification --- .../src/incoming_call_notification.rs | 85 ++++++++++++++----- crates/gpui/src/platform.rs | 2 + crates/gpui/src/platform/mac/platform.rs | 14 ++- crates/gpui/src/platform/test.rs | 4 + crates/theme/src/theme.rs | 6 ++ .../src/styleTree/incomingCallNotification.ts | 28 ++++-- 6 files changed, 111 insertions(+), 28 deletions(-) diff --git a/crates/collab_ui/src/incoming_call_notification.rs b/crates/collab_ui/src/incoming_call_notification.rs index 47fe8cbbfbdc5976a144d0790603b2f1afb20850..a396f8728a9d45460c19a4ea9615210c85a64dc5 100644 --- a/crates/collab_ui/src/incoming_call_notification.rs +++ b/crates/collab_ui/src/incoming_call_notification.rs @@ -3,8 +3,8 @@ use futures::StreamExt; use gpui::{ elements::*, geometry::{rect::RectF, vector::vec2f}, - impl_internal_actions, Entity, MouseButton, MutableAppContext, RenderContext, View, - ViewContext, WindowBounds, WindowKind, WindowOptions, + impl_internal_actions, CursorStyle, Entity, MouseButton, MutableAppContext, RenderContext, + View, ViewContext, WindowBounds, WindowKind, WindowOptions, }; use settings::Settings; use util::ResultExt; @@ -24,11 +24,17 @@ pub fn init(cx: &mut MutableAppContext) { } if let Some(incoming_call) = incoming_call { + const PADDING: f32 = 16.; + let screen_size = cx.platform().screen_size(); + let window_size = vec2f(304., 64.); let (window_id, _) = cx.add_window( WindowOptions { - bounds: WindowBounds::Fixed(RectF::new(vec2f(0., 0.), vec2f(300., 400.))), + bounds: WindowBounds::Fixed(RectF::new( + vec2f(screen_size.x() - window_size.x() - PADDING, PADDING), + window_size, + )), titlebar: None, - center: true, + center: false, kind: WindowKind::PopUp, is_movable: false, }, @@ -84,22 +90,40 @@ impl IncomingCallNotification { fn render_caller(&self, cx: &mut RenderContext) -> ElementBox { let theme = &cx.global::().theme.incoming_call_notification; Flex::row() - .with_children( - self.call - .caller - .avatar - .clone() - .map(|avatar| Image::new(avatar).with_style(theme.caller_avatar).boxed()), - ) + .with_children(self.call.caller.avatar.clone().map(|avatar| { + Image::new(avatar) + .with_style(theme.caller_avatar) + .aligned() + .boxed() + })) .with_child( - Label::new( - self.call.caller.github_login.clone(), - theme.caller_username.text.clone(), - ) - .contained() - .with_style(theme.caller_username.container) - .boxed(), + Flex::column() + .with_child( + Label::new( + self.call.caller.github_login.clone(), + theme.caller_username.text.clone(), + ) + .contained() + .with_style(theme.caller_username.container) + .boxed(), + ) + .with_child( + Label::new( + "Incoming Zed call...".into(), + theme.caller_message.text.clone(), + ) + .contained() + .with_style(theme.caller_message.container) + .boxed(), + ) + .contained() + .with_style(theme.caller_metadata) + .aligned() + .boxed(), ) + .contained() + .with_style(theme.caller_container) + .flex(1., true) .boxed() } @@ -107,33 +131,46 @@ impl IncomingCallNotification { enum Accept {} enum Decline {} - Flex::row() + Flex::column() .with_child( MouseEventHandler::::new(0, cx, |_, cx| { let theme = &cx.global::().theme.incoming_call_notification; Label::new("Accept".to_string(), theme.accept_button.text.clone()) + .aligned() .contained() .with_style(theme.accept_button.container) .boxed() }) + .with_cursor_style(CursorStyle::PointingHand) .on_click(MouseButton::Left, |_, cx| { cx.dispatch_action(RespondToCall { accept: true }); }) + .flex(1., true) .boxed(), ) .with_child( MouseEventHandler::::new(0, cx, |_, cx| { let theme = &cx.global::().theme.incoming_call_notification; Label::new("Decline".to_string(), theme.decline_button.text.clone()) + .aligned() .contained() .with_style(theme.decline_button.container) .boxed() }) + .with_cursor_style(CursorStyle::PointingHand) .on_click(MouseButton::Left, |_, cx| { cx.dispatch_action(RespondToCall { accept: false }); }) + .flex(1., true) .boxed(), ) + .constrained() + .with_width( + cx.global::() + .theme + .incoming_call_notification + .button_width, + ) .boxed() } } @@ -148,9 +185,17 @@ impl View for IncomingCallNotification { } fn render(&mut self, cx: &mut RenderContext) -> gpui::ElementBox { - Flex::column() + let background = cx + .global::() + .theme + .incoming_call_notification + .background; + Flex::row() .with_child(self.render_caller(cx)) .with_child(self.render_buttons(cx)) + .contained() + .with_background_color(background) + .expanded() .boxed() } } diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index a50698070c1ecd472041d1a2db6c7ee2ed600fcd..a0e8cedfe91ee2c79101b5ba1ee0762cb9af5632 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -44,6 +44,8 @@ pub trait Platform: Send + Sync { fn unhide_other_apps(&self); fn quit(&self); + fn screen_size(&self) -> Vector2F; + fn open_window( &self, id: usize, diff --git a/crates/gpui/src/platform/mac/platform.rs b/crates/gpui/src/platform/mac/platform.rs index 7732da2b3efe9da6e9c584e77d82c99cf85f0334..cf2aeff466d34bd2918af25dd4e67704fd444f9d 100644 --- a/crates/gpui/src/platform/mac/platform.rs +++ b/crates/gpui/src/platform/mac/platform.rs @@ -2,7 +2,9 @@ use super::{ event::key_to_native, status_item::StatusItem, BoolExt as _, Dispatcher, FontSystem, Window, }; use crate::{ - executor, keymap, + executor, + geometry::vector::{vec2f, Vector2F}, + keymap, platform::{self, CursorStyle}, Action, ClipboardItem, Event, Menu, MenuItem, }; @@ -12,7 +14,7 @@ use cocoa::{ appkit::{ NSApplication, NSApplicationActivationPolicy::NSApplicationActivationPolicyRegular, NSEventModifierFlags, NSMenu, NSMenuItem, NSModalResponse, NSOpenPanel, NSPasteboard, - NSPasteboardTypeString, NSSavePanel, NSWindow, + NSPasteboardTypeString, NSSavePanel, NSScreen, NSWindow, }, base::{id, nil, selector, YES}, foundation::{ @@ -485,6 +487,14 @@ impl platform::Platform for MacPlatform { } } + fn screen_size(&self) -> Vector2F { + unsafe { + let screen = NSScreen::mainScreen(nil); + let frame = NSScreen::frame(screen); + vec2f(frame.size.width as f32, frame.size.height as f32) + } + } + fn open_window( &self, id: usize, diff --git a/crates/gpui/src/platform/test.rs b/crates/gpui/src/platform/test.rs index 9a458a1dd96609c70c6acc4ffed2e69b6e44faa3..613a2117b9845559ff0ee29388636fc43b3f0fbe 100644 --- a/crates/gpui/src/platform/test.rs +++ b/crates/gpui/src/platform/test.rs @@ -131,6 +131,10 @@ impl super::Platform for Platform { fn quit(&self) {} + fn screen_size(&self) -> Vector2F { + vec2f(1024., 768.) + } + fn open_window( &self, _: usize, diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index d69720322eb80fa9edac6f12fe61bcff11ea072e..3f9b64ce565c181a06f8626c88a14733bb896ae3 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -479,8 +479,14 @@ pub struct ProjectSharedNotification { #[derive(Deserialize, Default)] pub struct IncomingCallNotification { + #[serde(default)] + pub background: Color, + pub caller_container: ContainerStyle, pub caller_avatar: ImageStyle, + pub caller_metadata: ContainerStyle, pub caller_username: ContainedText, + pub caller_message: ContainedText, + pub button_width: f32, pub accept_button: ContainedText, pub decline_button: ContainedText, } diff --git a/styles/src/styleTree/incomingCallNotification.ts b/styles/src/styleTree/incomingCallNotification.ts index dcae47bff0b90ceb52809d99379ac97abf8e6964..d8ea7dbad90ccf84b760a39e9ea7581de71ccc6c 100644 --- a/styles/src/styleTree/incomingCallNotification.ts +++ b/styles/src/styleTree/incomingCallNotification.ts @@ -1,22 +1,38 @@ import Theme from "../themes/common/theme"; -import { text } from "./components"; +import { backgroundColor, borderColor, text } from "./components"; export default function incomingCallNotification(theme: Theme): Object { - const avatarSize = 12; + const avatarSize = 32; return { + background: backgroundColor(theme, 300), + callerContainer: { + padding: 12, + }, callerAvatar: { height: avatarSize, width: avatarSize, - cornerRadius: 6, + cornerRadius: avatarSize / 2, + }, + callerMetadata: { + margin: { left: 10 }, }, callerUsername: { - ...text(theme, "sans", "primary", { size: "xs" }), + ...text(theme, "sans", "active", { size: "sm", weight: "bold" }), + margin: { top: -3 }, + }, + callerMessage: { + ...text(theme, "sans", "secondary", { size: "xs" }), + margin: { top: -3 }, }, + buttonWidth: 96, acceptButton: { - ...text(theme, "sans", "primary", { size: "xs" }) + background: backgroundColor(theme, "ok", "active"), + border: { left: true, bottom: true, width: 1, color: borderColor(theme, "primary") }, + ...text(theme, "sans", "ok", { size: "xs", weight: "extra_bold" }) }, declineButton: { - ...text(theme, "sans", "primary", { size: "xs" }) + border: { left: true, width: 1, color: borderColor(theme, "primary") }, + ...text(theme, "sans", "error", { size: "xs", weight: "extra_bold" }) }, }; } From 25ff5959fb72b7658ba0f080c62ba2238c13fbec Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 10 Oct 2022 12:23:50 +0200 Subject: [PATCH 086/112] Superimpose external location message on active view --- crates/workspace/src/pane_group.rs | 120 +++++++++++++++++------------ styles/src/styleTree/workspace.ts | 6 +- 2 files changed, 74 insertions(+), 52 deletions(-) diff --git a/crates/workspace/src/pane_group.rs b/crates/workspace/src/pane_group.rs index 9fd27f78b5a4517f74091a52e1875eaecdbe2f80..694f6a55eb44d42f5dacf6d9419aa013908d1b7b 100644 --- a/crates/workspace/src/pane_group.rs +++ b/crates/workspace/src/pane_group.rs @@ -127,68 +127,86 @@ impl Member { Some((collaborator.replica_id, participant)) }); - if let Some((replica_id, leader)) = leader { - let view = match leader.location { + let mut border = Border::default(); + + let prompt = if let Some((replica_id, leader)) = leader { + let leader_color = theme.editor.replica_selection_style(replica_id).cursor; + border = Border::all(theme.workspace.leader_border_width, leader_color); + border + .color + .fade_out(1. - theme.workspace.leader_border_opacity); + border.overlay = true; + + match leader.location { call::ParticipantLocation::Project { project_id: leader_project_id, } => { if Some(leader_project_id) == project.read(cx).remote_id() { - ChildView::new(pane).boxed() + None } else { let leader_user = leader.user.clone(); let leader_user_id = leader.user.id; - MouseEventHandler::::new( - pane.id(), - cx, - |_, _| { - Label::new( - format!( - "Follow {} on their currently active project", - leader_user.github_login, - ), - theme.workspace.external_location_message.text.clone(), - ) - .contained() - .with_style( - theme.workspace.external_location_message.container, - ) - .boxed() - }, - ) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, move |_, cx| { - cx.dispatch_action(JoinProject { - project_id: leader_project_id, - follow_user_id: leader_user_id, + Some( + MouseEventHandler::::new( + pane.id(), + cx, + |_, _| { + Label::new( + format!( + "Follow {} on their currently active project", + leader_user.github_login, + ), + theme + .workspace + .external_location_message + .text + .clone(), + ) + .contained() + .with_style( + theme.workspace.external_location_message.container, + ) + .boxed() + }, + ) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, move |_, cx| { + cx.dispatch_action(JoinProject { + project_id: leader_project_id, + follow_user_id: leader_user_id, + }) }) - }) - .aligned() - .boxed() + .aligned() + .bottom() + .right() + .boxed(), + ) } } - call::ParticipantLocation::External => Label::new( - format!( - "{} is viewing a window outside of Zed", - leader.user.github_login - ), - theme.workspace.external_location_message.text.clone(), - ) - .contained() - .with_style(theme.workspace.external_location_message.container) - .aligned() - .boxed(), - }; - - let leader_color = theme.editor.replica_selection_style(replica_id).cursor; - let mut border = Border::all(theme.workspace.leader_border_width, leader_color); - border - .color - .fade_out(1. - theme.workspace.leader_border_opacity); - border.overlay = true; - Container::new(view).with_border(border).boxed() + call::ParticipantLocation::External => Some( + Label::new( + format!( + "{} is viewing a window outside of Zed", + leader.user.github_login + ), + theme.workspace.external_location_message.text.clone(), + ) + .contained() + .with_style(theme.workspace.external_location_message.container) + .aligned() + .bottom() + .right() + .boxed(), + ), + } } else { - ChildView::new(pane).boxed() - } + None + }; + + Stack::new() + .with_child(ChildView::new(pane).contained().with_border(border).boxed()) + .with_children(prompt) + .boxed() } Member::Axis(axis) => axis.render(project, theme, follower_states, cx), } diff --git a/styles/src/styleTree/workspace.ts b/styles/src/styleTree/workspace.ts index c970c382960cd776cc977ed1141e853ae740e863..4deee046b4298ae3b98bda8b0d87d7c4ab766ef8 100644 --- a/styles/src/styleTree/workspace.ts +++ b/styles/src/styleTree/workspace.ts @@ -48,8 +48,12 @@ export default function workspace(theme: Theme) { ...text(theme, "sans", "primary", { size: "lg" }), }, externalLocationMessage: { + background: backgroundColor(theme, "info"), + border: border(theme, "secondary"), + cornerRadius: 6, padding: 12, - ...text(theme, "sans", "primary", { size: "lg" }), + margin: { bottom: 8, right: 8 }, + ...text(theme, "sans", "secondary", { size: "xs" }), }, leaderBorderOpacity: 0.7, leaderBorderWidth: 2.0, From 9d990ae3292132e9a53be1f7d387179084069c36 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 10 Oct 2022 14:19:40 +0200 Subject: [PATCH 087/112] Show all room participants in titlebar ...and allow following them into external projects. --- crates/collab_ui/src/collab_titlebar_item.rs | 157 +++++++++++------- crates/gpui/src/elements/image.rs | 3 + crates/gpui/src/platform/mac/renderer.rs | 2 + .../gpui/src/platform/mac/shaders/shaders.h | 1 + .../src/platform/mac/shaders/shaders.metal | 11 ++ crates/gpui/src/scene.rs | 1 + styles/src/styleTree/workspace.ts | 13 +- 7 files changed, 121 insertions(+), 67 deletions(-) diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index cc82acac5e7118018b6636c8d81fa1adb38b997d..c46861b5ab659224b3462b71c7c6ddaab725719a 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -1,6 +1,6 @@ use crate::{contact_notification::ContactNotification, contacts_popover}; use call::{ActiveCall, ParticipantLocation}; -use client::{Authenticate, ContactEventKind, PeerId, UserStore}; +use client::{Authenticate, ContactEventKind, PeerId, User, UserStore}; use clock::ReplicaId; use contacts_popover::ContactsPopover; use gpui::{ @@ -9,13 +9,13 @@ use gpui::{ elements::*, geometry::{rect::RectF, vector::vec2f, PathBuilder}, json::{self, ToJson}, - Border, CursorStyle, Entity, ImageData, ModelHandle, MouseButton, MutableAppContext, - RenderContext, Subscription, View, ViewContext, ViewHandle, WeakViewHandle, + Border, CursorStyle, Entity, ModelHandle, MouseButton, MutableAppContext, RenderContext, + Subscription, View, ViewContext, ViewHandle, WeakViewHandle, }; use settings::Settings; -use std::{ops::Range, sync::Arc}; +use std::ops::Range; use theme::Theme; -use workspace::{FollowNextCollaborator, ToggleFollow, Workspace}; +use workspace::{FollowNextCollaborator, JoinProject, ToggleFollow, Workspace}; actions!( contacts_titlebar_item, @@ -282,29 +282,27 @@ impl CollabTitlebarItem { let active_call = ActiveCall::global(cx); if let Some(room) = active_call.read(cx).room().cloned() { let project = workspace.read(cx).project().read(cx); - let project_id = project.remote_id(); - let mut collaborators = project - .collaborators() - .values() - .cloned() + let mut participants = room + .read(cx) + .remote_participants() + .iter() + .map(|(peer_id, collaborator)| (*peer_id, collaborator.clone())) .collect::>(); - collaborators.sort_by_key(|collaborator| collaborator.replica_id); - collaborators + participants + .sort_by_key(|(peer_id, _)| Some(project.collaborators().get(peer_id)?.replica_id)); + participants .into_iter() - .filter_map(|collaborator| { - let participant = room - .read(cx) - .remote_participants() - .get(&collaborator.peer_id)?; + .filter_map(|(peer_id, participant)| { + let project = workspace.read(cx).project().read(cx); + let replica_id = project + .collaborators() + .get(&peer_id) + .map(|collaborator| collaborator.replica_id); let user = participant.user.clone(); - let is_active = project_id.map_or(false, |project_id| { - participant.location == ParticipantLocation::Project { project_id } - }); Some(self.render_avatar( - user.avatar.clone()?, - collaborator.replica_id, - Some((collaborator.peer_id, &user.github_login)), - is_active, + &user, + replica_id, + Some((peer_id, &user.github_login, participant.location)), workspace, theme, cx, @@ -325,8 +323,8 @@ impl CollabTitlebarItem { let user = workspace.read(cx).user_store().read(cx).current_user(); let replica_id = workspace.read(cx).project().read(cx).replica_id(); let status = *workspace.read(cx).client().status().borrow(); - if let Some(avatar) = user.and_then(|user| user.avatar.clone()) { - Some(self.render_avatar(avatar, replica_id, None, true, workspace, theme, cx)) + if let Some(user) = user { + Some(self.render_avatar(&user, Some(replica_id), None, workspace, theme, cx)) } else if matches!(status, client::Status::UpgradeRequired) { None } else { @@ -352,72 +350,105 @@ impl CollabTitlebarItem { fn render_avatar( &self, - avatar: Arc, - replica_id: ReplicaId, - peer: Option<(PeerId, &str)>, - is_active: bool, + user: &User, + replica_id: Option, + peer: Option<(PeerId, &str, ParticipantLocation)>, workspace: &ViewHandle, theme: &Theme, cx: &mut RenderContext, ) -> ElementBox { - let replica_color = theme.editor.replica_selection_style(replica_id).cursor; - let is_followed = peer.map_or(false, |(peer_id, _)| { + let is_followed = peer.map_or(false, |(peer_id, _, _)| { workspace.read(cx).is_following(peer_id) }); - let mut avatar_style; - if is_active { - avatar_style = theme.workspace.titlebar.avatar; + let mut avatar_style; + if let Some((_, _, location)) = peer.as_ref() { + if let ParticipantLocation::Project { project_id } = *location { + if Some(project_id) == workspace.read(cx).project().read(cx).remote_id() { + avatar_style = theme.workspace.titlebar.avatar; + } else { + avatar_style = theme.workspace.titlebar.inactive_avatar; + } + } else { + avatar_style = theme.workspace.titlebar.inactive_avatar; + } } else { - avatar_style = theme.workspace.titlebar.inactive_avatar; + avatar_style = theme.workspace.titlebar.avatar; } - if is_followed { - avatar_style.border = Border::all(1.0, replica_color); + let mut replica_color = None; + if let Some(replica_id) = replica_id { + let color = theme.editor.replica_selection_style(replica_id).cursor; + replica_color = Some(color); + if is_followed { + avatar_style.border = Border::all(1.0, color); + } } let content = Stack::new() - .with_child( - Image::new(avatar) + .with_children(user.avatar.as_ref().map(|avatar| { + Image::new(avatar.clone()) .with_style(avatar_style) .constrained() .with_width(theme.workspace.titlebar.avatar_width) .aligned() - .boxed(), - ) - .with_child( + .boxed() + })) + .with_children(replica_color.map(|replica_color| { AvatarRibbon::new(replica_color) .constrained() .with_width(theme.workspace.titlebar.avatar_ribbon.width) .with_height(theme.workspace.titlebar.avatar_ribbon.height) .aligned() .bottom() - .boxed(), - ) + .boxed() + })) .constrained() .with_width(theme.workspace.titlebar.avatar_width) .contained() .with_margin_left(theme.workspace.titlebar.avatar_margin) .boxed(); - if let Some((peer_id, peer_github_login)) = peer { - MouseEventHandler::::new(replica_id.into(), cx, move |_, _| content) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, move |_, cx| { - cx.dispatch_action(ToggleFollow(peer_id)) - }) - .with_tooltip::( - peer_id.0 as usize, - if is_followed { - format!("Unfollow {}", peer_github_login) - } else { - format!("Follow {}", peer_github_login) - }, - Some(Box::new(FollowNextCollaborator)), - theme.tooltip.clone(), - cx, - ) - .boxed() + if let Some((peer_id, peer_github_login, location)) = peer { + if let Some(replica_id) = replica_id { + MouseEventHandler::::new(replica_id.into(), cx, move |_, _| content) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, move |_, cx| { + cx.dispatch_action(ToggleFollow(peer_id)) + }) + .with_tooltip::( + peer_id.0 as usize, + if is_followed { + format!("Unfollow {}", peer_github_login) + } else { + format!("Follow {}", peer_github_login) + }, + Some(Box::new(FollowNextCollaborator)), + theme.tooltip.clone(), + cx, + ) + .boxed() + } else if let ParticipantLocation::Project { project_id } = location { + let user_id = user.id; + MouseEventHandler::::new(peer_id.0 as usize, cx, move |_, _| content) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, move |_, cx| { + cx.dispatch_action(JoinProject { + project_id, + follow_user_id: user_id, + }) + }) + .with_tooltip::( + peer_id.0 as usize, + format!("Follow {} into external project", peer_github_login), + Some(Box::new(FollowNextCollaborator)), + theme.tooltip.clone(), + cx, + ) + .boxed() + } else { + content + } } else { content } diff --git a/crates/gpui/src/elements/image.rs b/crates/gpui/src/elements/image.rs index e0387e39bb1cadd340635ff0c0259d6486e49bad..7e231e5e293f8660b5dc60b9e8a6ebcf32264429 100644 --- a/crates/gpui/src/elements/image.rs +++ b/crates/gpui/src/elements/image.rs @@ -27,6 +27,8 @@ pub struct ImageStyle { pub height: Option, #[serde(default)] pub width: Option, + #[serde(default)] + pub grayscale: bool, } impl Image { @@ -74,6 +76,7 @@ impl Element for Image { bounds, border: self.style.border, corner_radius: self.style.corner_radius, + grayscale: self.style.grayscale, data: self.data.clone(), }); } diff --git a/crates/gpui/src/platform/mac/renderer.rs b/crates/gpui/src/platform/mac/renderer.rs index ea094e998c112b4b875f129b2aef0c5d67fd8002..6a70ff41f0a975cefa0fa2b3f9e28cda3dad61f3 100644 --- a/crates/gpui/src/platform/mac/renderer.rs +++ b/crates/gpui/src/platform/mac/renderer.rs @@ -747,6 +747,7 @@ impl Renderer { border_left: border_width * (image.border.left as usize as f32), border_color: image.border.color.to_uchar4(), corner_radius, + grayscale: image.grayscale as u8, }); } @@ -769,6 +770,7 @@ impl Renderer { border_left: 0., border_color: Default::default(), corner_radius: 0., + grayscale: false as u8, }); } else { log::warn!("could not render glyph with id {}", image_glyph.id); diff --git a/crates/gpui/src/platform/mac/shaders/shaders.h b/crates/gpui/src/platform/mac/shaders/shaders.h index 29be2c9e1e789ade67c4c05b53e8300779cdc884..6e0ed1a5f1b5ef89182e5fb46518b0801d5a9cb0 100644 --- a/crates/gpui/src/platform/mac/shaders/shaders.h +++ b/crates/gpui/src/platform/mac/shaders/shaders.h @@ -90,6 +90,7 @@ typedef struct { float border_left; vector_uchar4 border_color; float corner_radius; + uint8_t grayscale; } GPUIImage; typedef enum { diff --git a/crates/gpui/src/platform/mac/shaders/shaders.metal b/crates/gpui/src/platform/mac/shaders/shaders.metal index 795026e747e4ffa1f0083737c7d6b2158f79a1b9..397e7647c47107b5176880a0e0fd1ba2adbaecda 100644 --- a/crates/gpui/src/platform/mac/shaders/shaders.metal +++ b/crates/gpui/src/platform/mac/shaders/shaders.metal @@ -44,6 +44,7 @@ struct QuadFragmentInput { float border_left; float4 border_color; float corner_radius; + uchar grayscale; // only used in image shader }; float4 quad_sdf(QuadFragmentInput input) { @@ -110,6 +111,7 @@ vertex QuadFragmentInput quad_vertex( quad.border_left, coloru_to_colorf(quad.border_color), quad.corner_radius, + 0, }; } @@ -251,6 +253,7 @@ vertex QuadFragmentInput image_vertex( image.border_left, coloru_to_colorf(image.border_color), image.corner_radius, + image.grayscale, }; } @@ -260,6 +263,13 @@ fragment float4 image_fragment( ) { constexpr sampler atlas_sampler(mag_filter::linear, min_filter::linear); input.background_color = atlas.sample(atlas_sampler, input.atlas_position); + if (input.grayscale) { + float grayscale = + 0.2126 * input.background_color.r + + 0.7152 * input.background_color.g + + 0.0722 * input.background_color.b; + input.background_color = float4(grayscale, grayscale, grayscale, input.background_color.a); + } return quad_sdf(input); } @@ -289,6 +299,7 @@ vertex QuadFragmentInput surface_vertex( 0., float4(0.), 0., + 0, }; } diff --git a/crates/gpui/src/scene.rs b/crates/gpui/src/scene.rs index e676147fa90e72d0f9f36cd02e2356423f310be5..4ef17a3f8f5d384723ed1d94a13019bb495e4d0e 100644 --- a/crates/gpui/src/scene.rs +++ b/crates/gpui/src/scene.rs @@ -172,6 +172,7 @@ pub struct Image { pub bounds: RectF, pub border: Border, pub corner_radius: f32, + pub grayscale: bool, pub data: Arc, } diff --git a/styles/src/styleTree/workspace.ts b/styles/src/styleTree/workspace.ts index 4deee046b4298ae3b98bda8b0d87d7c4ab766ef8..cfbda49056da6635b6966bca5722b3a39d4a4e89 100644 --- a/styles/src/styleTree/workspace.ts +++ b/styles/src/styleTree/workspace.ts @@ -36,6 +36,7 @@ export default function workspace(theme: Theme) { border: border(theme, "primary"), }, }; + const avatarWidth = 18; return { background: backgroundColor(theme, 300), @@ -80,7 +81,7 @@ export default function workspace(theme: Theme) { }, statusBar: statusBar(theme), titlebar: { - avatarWidth: 18, + avatarWidth, avatarMargin: 8, height: 33, background: backgroundColor(theme, 100), @@ -90,15 +91,19 @@ export default function workspace(theme: Theme) { }, title: text(theme, "sans", "primary"), avatar: { - cornerRadius: 10, + cornerRadius: avatarWidth / 2, border: { color: "#00000088", width: 1, }, }, inactiveAvatar: { - cornerRadius: 10, - opacity: 0.65, + cornerRadius: avatarWidth / 2, + border: { + color: "#00000088", + width: 1, + }, + grayscale: true, }, avatarRibbon: { height: 3, From 7cfe435e627f4187683df6f736e7f095af264c25 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 10 Oct 2022 14:37:51 +0200 Subject: [PATCH 088/112] Style project shared notifications --- .../src/incoming_call_notification.rs | 13 ++- .../src/project_shared_notification.rs | 81 +++++++++++++++---- crates/theme/src/theme.rs | 6 ++ .../styleTree/projectSharedNotification.ts | 28 +++++-- 4 files changed, 98 insertions(+), 30 deletions(-) diff --git a/crates/collab_ui/src/incoming_call_notification.rs b/crates/collab_ui/src/incoming_call_notification.rs index a396f8728a9d45460c19a4ea9615210c85a64dc5..cbb23de2e695bca9b4e4aca73b5a7785acb56be8 100644 --- a/crates/collab_ui/src/incoming_call_notification.rs +++ b/crates/collab_ui/src/incoming_call_notification.rs @@ -26,7 +26,7 @@ pub fn init(cx: &mut MutableAppContext) { if let Some(incoming_call) = incoming_call { const PADDING: f32 = 16.; let screen_size = cx.platform().screen_size(); - let window_size = vec2f(304., 64.); + let window_size = vec2f(274., 64.); let (window_id, _) = cx.add_window( WindowOptions { bounds: WindowBounds::Fixed(RectF::new( @@ -108,13 +108,10 @@ impl IncomingCallNotification { .boxed(), ) .with_child( - Label::new( - "Incoming Zed call...".into(), - theme.caller_message.text.clone(), - ) - .contained() - .with_style(theme.caller_message.container) - .boxed(), + Label::new("is calling you".into(), theme.caller_message.text.clone()) + .contained() + .with_style(theme.caller_message.container) + .boxed(), ) .contained() .with_style(theme.caller_metadata) diff --git a/crates/collab_ui/src/project_shared_notification.rs b/crates/collab_ui/src/project_shared_notification.rs index e0c196614450550d25ba9afc921f798b1be65031..5597ea0a01467ba5267097b6e0dd83416eb83142 100644 --- a/crates/collab_ui/src/project_shared_notification.rs +++ b/crates/collab_ui/src/project_shared_notification.rs @@ -4,8 +4,8 @@ use gpui::{ actions, elements::*, geometry::{rect::RectF, vector::vec2f}, - Entity, MouseButton, MutableAppContext, RenderContext, View, ViewContext, WindowBounds, - WindowKind, WindowOptions, + CursorStyle, Entity, MouseButton, MutableAppContext, RenderContext, View, ViewContext, + WindowBounds, WindowKind, WindowOptions, }; use settings::Settings; use std::sync::Arc; @@ -20,11 +20,17 @@ pub fn init(cx: &mut MutableAppContext) { let active_call = ActiveCall::global(cx); cx.subscribe(&active_call, move |_, event, cx| match event { room::Event::RemoteProjectShared { owner, project_id } => { + const PADDING: f32 = 16.; + let screen_size = cx.platform().screen_size(); + let window_size = vec2f(366., 64.); cx.add_window( WindowOptions { - bounds: WindowBounds::Fixed(RectF::new(vec2f(0., 0.), vec2f(300., 400.))), + bounds: WindowBounds::Fixed(RectF::new( + vec2f(screen_size.x() - window_size.x() - PADDING, PADDING), + window_size, + )), titlebar: None, - center: true, + center: false, kind: WindowKind::PopUp, is_movable: false, }, @@ -59,19 +65,40 @@ impl ProjectSharedNotification { fn render_owner(&self, cx: &mut RenderContext) -> ElementBox { let theme = &cx.global::().theme.project_shared_notification; Flex::row() - .with_children( - self.owner - .avatar - .clone() - .map(|avatar| Image::new(avatar).with_style(theme.owner_avatar).boxed()), - ) + .with_children(self.owner.avatar.clone().map(|avatar| { + Image::new(avatar) + .with_style(theme.owner_avatar) + .aligned() + .boxed() + })) .with_child( - Label::new( - format!("{} has shared a new project", self.owner.github_login), - theme.message.text.clone(), - ) - .boxed(), + Flex::column() + .with_child( + Label::new( + self.owner.github_login.clone(), + theme.owner_username.text.clone(), + ) + .contained() + .with_style(theme.owner_username.container) + .boxed(), + ) + .with_child( + Label::new( + "has shared a project with you".into(), + theme.message.text.clone(), + ) + .contained() + .with_style(theme.message.container) + .boxed(), + ) + .contained() + .with_style(theme.owner_metadata) + .aligned() + .boxed(), ) + .contained() + .with_style(theme.owner_container) + .flex(1., true) .boxed() } @@ -81,36 +108,50 @@ impl ProjectSharedNotification { let project_id = self.project_id; let owner_user_id = self.owner.id; - Flex::row() + + Flex::column() .with_child( MouseEventHandler::::new(0, cx, |_, cx| { let theme = &cx.global::().theme.project_shared_notification; Label::new("Join".to_string(), theme.join_button.text.clone()) + .aligned() .contained() .with_style(theme.join_button.container) .boxed() }) + .with_cursor_style(CursorStyle::PointingHand) .on_click(MouseButton::Left, move |_, cx| { cx.dispatch_action(JoinProject { project_id, follow_user_id: owner_user_id, }); }) + .flex(1., true) .boxed(), ) .with_child( MouseEventHandler::::new(0, cx, |_, cx| { let theme = &cx.global::().theme.project_shared_notification; Label::new("Dismiss".to_string(), theme.dismiss_button.text.clone()) + .aligned() .contained() .with_style(theme.dismiss_button.container) .boxed() }) + .with_cursor_style(CursorStyle::PointingHand) .on_click(MouseButton::Left, |_, cx| { cx.dispatch_action(DismissProject); }) + .flex(1., true) .boxed(), ) + .constrained() + .with_width( + cx.global::() + .theme + .project_shared_notification + .button_width, + ) .boxed() } } @@ -125,9 +166,17 @@ impl View for ProjectSharedNotification { } fn render(&mut self, cx: &mut RenderContext) -> gpui::ElementBox { + let background = cx + .global::() + .theme + .project_shared_notification + .background; Flex::row() .with_child(self.render_owner(cx)) .with_child(self.render_buttons(cx)) + .contained() + .with_background_color(background) + .expanded() .boxed() } } diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 3f9b64ce565c181a06f8626c88a14733bb896ae3..a02bcfb1daaf0469f81ba8c0b2e6a4e78a68e52f 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -471,8 +471,14 @@ pub struct UpdateNotification { #[derive(Deserialize, Default)] pub struct ProjectSharedNotification { + #[serde(default)] + pub background: Color, + pub owner_container: ContainerStyle, pub owner_avatar: ImageStyle, + pub owner_metadata: ContainerStyle, + pub owner_username: ContainedText, pub message: ContainedText, + pub button_width: f32, pub join_button: ContainedText, pub dismiss_button: ContainedText, } diff --git a/styles/src/styleTree/projectSharedNotification.ts b/styles/src/styleTree/projectSharedNotification.ts index bc342651358fac5627ec71f718e5322f3b03d891..7a70f0f0d3577cdd378cd25604c731619689251e 100644 --- a/styles/src/styleTree/projectSharedNotification.ts +++ b/styles/src/styleTree/projectSharedNotification.ts @@ -1,22 +1,38 @@ import Theme from "../themes/common/theme"; -import { text } from "./components"; +import { backgroundColor, borderColor, text } from "./components"; export default function projectSharedNotification(theme: Theme): Object { - const avatarSize = 12; + const avatarSize = 32; return { + background: backgroundColor(theme, 300), + ownerContainer: { + padding: 12, + }, ownerAvatar: { height: avatarSize, width: avatarSize, - cornerRadius: 6, + cornerRadius: avatarSize / 2, + }, + ownerMetadata: { + margin: { left: 10 }, + }, + ownerUsername: { + ...text(theme, "sans", "active", { size: "sm", weight: "bold" }), + margin: { top: -3 }, }, message: { - ...text(theme, "sans", "primary", { size: "xs" }), + ...text(theme, "sans", "secondary", { size: "xs" }), + margin: { top: -3 }, }, + buttonWidth: 96, joinButton: { - ...text(theme, "sans", "primary", { size: "xs" }) + background: backgroundColor(theme, "info", "active"), + border: { left: true, bottom: true, width: 1, color: borderColor(theme, "primary") }, + ...text(theme, "sans", "info", { size: "xs", weight: "extra_bold" }) }, dismissButton: { - ...text(theme, "sans", "primary", { size: "xs" }) + border: { left: true, width: 1, color: borderColor(theme, "primary") }, + ...text(theme, "sans", "secondary", { size: "xs", weight: "extra_bold" }) }, }; } From 3396a98978ecdb6a9e3013e0ac26d587946efa61 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 10 Oct 2022 14:41:18 +0200 Subject: [PATCH 089/112] :lipstick: --- styles/src/styleTree/contactList.ts | 4 +- styles/src/styleTree/contactsPopover.ts | 115 +----------------------- styles/src/themes/common/base16.ts | 1 - styles/src/themes/common/theme.ts | 1 - 4 files changed, 3 insertions(+), 118 deletions(-) diff --git a/styles/src/styleTree/contactList.ts b/styles/src/styleTree/contactList.ts index f2430d31e6ccdf5f35cf07df972574b5dbb7c02b..8b8b6024cfed9f88db9c32e7095369f36bd4ac36 100644 --- a/styles/src/styleTree/contactList.ts +++ b/styles/src/styleTree/contactList.ts @@ -90,13 +90,13 @@ export default function contactList(theme: Theme) { cornerRadius: 4, padding: 4, margin: { top: 12, left: 12 }, - background: iconColor(theme, "success"), + background: iconColor(theme, "ok"), }, contactStatusBusy: { cornerRadius: 4, padding: 4, margin: { top: 12, left: 12 }, - background: iconColor(theme, "warning"), + background: iconColor(theme, "error"), }, contactUsername: { ...text(theme, "mono", "primary", { size: "sm" }), diff --git a/styles/src/styleTree/contactsPopover.ts b/styles/src/styleTree/contactsPopover.ts index 57af5a6d4d3cdb2b32aa6d73d5f824aeadf24162..7d699fa26b3e2d55d1e868073f413751d60ce382 100644 --- a/styles/src/styleTree/contactsPopover.ts +++ b/styles/src/styleTree/contactsPopover.ts @@ -1,18 +1,7 @@ import Theme from "../themes/common/theme"; -import { backgroundColor, border, borderColor, iconColor, player, popoverShadow, text } from "./components"; +import { backgroundColor, border, popoverShadow } from "./components"; export default function contactsPopover(theme: Theme) { - const nameMargin = 8; - const sidePadding = 12; - - const contactButton = { - background: backgroundColor(theme, 100), - color: iconColor(theme, "primary"), - iconWidth: 8, - buttonWidth: 16, - cornerRadius: 8, - }; - return { background: backgroundColor(theme, 300, "base"), cornerRadius: 6, @@ -21,107 +10,5 @@ export default function contactsPopover(theme: Theme) { border: border(theme, "primary"), width: 250, height: 300, - userQueryEditor: { - background: backgroundColor(theme, 500), - cornerRadius: 6, - text: text(theme, "mono", "primary"), - placeholderText: text(theme, "mono", "placeholder", { size: "sm" }), - selection: player(theme, 1).selection, - border: border(theme, "secondary"), - padding: { - bottom: 4, - left: 8, - right: 8, - top: 4, - }, - margin: { - left: sidePadding, - right: sidePadding, - }, - }, - userQueryEditorHeight: 32, - addContactButton: { - margin: { left: 6, right: 12 }, - color: iconColor(theme, "primary"), - buttonWidth: 16, - iconWidth: 16, - }, - privateButton: { - iconWidth: 12, - color: iconColor(theme, "primary"), - cornerRadius: 5, - buttonWidth: 12, - }, - rowHeight: 28, - sectionIconSize: 8, - headerRow: { - ...text(theme, "mono", "secondary", { size: "sm" }), - margin: { top: 14 }, - padding: { - left: sidePadding, - right: sidePadding, - }, - active: { - ...text(theme, "mono", "primary", { size: "sm" }), - background: backgroundColor(theme, 100, "active"), - }, - }, - contactRow: { - padding: { - left: sidePadding, - right: sidePadding, - }, - active: { - background: backgroundColor(theme, 100, "active"), - }, - }, - contactAvatar: { - cornerRadius: 10, - width: 18, - }, - contactStatusFree: { - cornerRadius: 4, - padding: 4, - margin: { top: 12, left: 12 }, - background: iconColor(theme, "success"), - }, - contactStatusBusy: { - cornerRadius: 4, - padding: 4, - margin: { top: 12, left: 12 }, - background: iconColor(theme, "warning"), - }, - contactUsername: { - ...text(theme, "mono", "primary", { size: "sm" }), - margin: { - left: nameMargin, - }, - }, - contactButtonSpacing: nameMargin, - contactButton: { - ...contactButton, - hover: { - background: backgroundColor(theme, "on300", "hovered"), - }, - }, - disabledButton: { - ...contactButton, - background: backgroundColor(theme, 100), - color: iconColor(theme, "muted"), - }, - inviteRow: { - padding: { - left: sidePadding, - right: sidePadding, - }, - border: { top: true, width: 1, color: borderColor(theme, "primary") }, - text: text(theme, "sans", "secondary", { size: "sm" }), - hover: { - text: text(theme, "sans", "active", { size: "sm" }), - }, - }, - callingIndicator: { - ...text(theme, "mono", "muted", { size: "xs" }) - } } } diff --git a/styles/src/themes/common/base16.ts b/styles/src/themes/common/base16.ts index 36129880752692c910c60deea8e7806c1485d768..7aa72ef1377ea40656a45e50bc4155f28ac7f8a5 100644 --- a/styles/src/themes/common/base16.ts +++ b/styles/src/themes/common/base16.ts @@ -137,7 +137,6 @@ export function createTheme( ok: sample(ramps.green, 0.5), error: sample(ramps.red, 0.5), warning: sample(ramps.yellow, 0.5), - success: sample(ramps.green, 0.5), info: sample(ramps.blue, 0.5), onMedia: darkest, }; diff --git a/styles/src/themes/common/theme.ts b/styles/src/themes/common/theme.ts index 18ad8122e0cb96b84d58373cb2d3adb7c921ca8d..e01435b846c4d4a5d1fdbe8366166c38de361f07 100644 --- a/styles/src/themes/common/theme.ts +++ b/styles/src/themes/common/theme.ts @@ -123,7 +123,6 @@ export default interface Theme { error: string; warning: string; info: string; - success: string; }; editor: { background: string; From 5f9cedad23d66a8c18ac59c26dd5b9f9cf11db9b Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 10 Oct 2022 16:05:09 +0200 Subject: [PATCH 090/112] Add margin to picker in contacts popover --- crates/collab_ui/src/contact_finder.rs | 6 +++++- styles/src/styleTree/contactFinder.ts | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/crates/collab_ui/src/contact_finder.rs b/crates/collab_ui/src/contact_finder.rs index 65ebee2797cd8a0a0c6db468d75f1c84892f9a5e..25726e381e6242496b83df734d75ef1b19b1b243 100644 --- a/crates/collab_ui/src/contact_finder.rs +++ b/crates/collab_ui/src/contact_finder.rs @@ -121,7 +121,11 @@ impl PickerDelegate for ContactFinder { } else { &theme.contact_finder.contact_button }; - let style = theme.picker.item.style_for(mouse_state, selected); + let style = theme + .contact_finder + .picker + .item + .style_for(mouse_state, selected); Flex::row() .with_children(user.avatar.clone().map(|avatar| { Image::new(avatar) diff --git a/styles/src/styleTree/contactFinder.ts b/styles/src/styleTree/contactFinder.ts index bf43a74666da6325676140c9a5c6720b77b0547a..2feb3d7e356c945824ae577a0ae5a136802b0fd9 100644 --- a/styles/src/styleTree/contactFinder.ts +++ b/styles/src/styleTree/contactFinder.ts @@ -3,6 +3,7 @@ import picker from "./picker"; import { backgroundColor, border, iconColor, player, text } from "./components"; export default function contactFinder(theme: Theme) { + const sideMargin = 12; const contactButton = { background: backgroundColor(theme, 100), color: iconColor(theme, "primary"), @@ -13,7 +14,10 @@ export default function contactFinder(theme: Theme) { return { picker: { - item: picker(theme).item, + item: { + ...picker(theme).item, + margin: { left: sideMargin, right: sideMargin } + }, empty: picker(theme).empty, inputEditor: { background: backgroundColor(theme, 500), From d9d99e5e0407f9641463ee38660db556e71e449b Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 10 Oct 2022 16:05:22 +0200 Subject: [PATCH 091/112] Fix seed script --- crates/collab/src/bin/seed.rs | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/crates/collab/src/bin/seed.rs b/crates/collab/src/bin/seed.rs index b7b3a967101f1cd9de3d29677127e03705113092..cabea7d013776d4f3cb248d1b0c8985a0f3090a2 100644 --- a/crates/collab/src/bin/seed.rs +++ b/crates/collab/src/bin/seed.rs @@ -84,7 +84,23 @@ async fn main() { }, ) .await - .expect("failed to insert user"), + .expect("failed to insert user") + .user_id, + ); + } else if admin { + zed_user_ids.push( + db.create_user( + &format!("{}@zed.dev", github_user.login), + admin, + db::NewUserParams { + github_login: github_user.login, + github_user_id: github_user.id, + invite_count: 5, + }, + ) + .await + .expect("failed to insert user") + .user_id, ); } } From 04fcd18c755c7e6f1ec9339fb1328617ad55c517 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 10 Oct 2022 16:30:02 +0200 Subject: [PATCH 092/112] Show contacts popover when clicking on menu bar extra --- crates/collab_ui/src/active_call_popover.rs | 40 -------------------- crates/collab_ui/src/collab_titlebar_item.rs | 3 +- crates/collab_ui/src/collab_ui.rs | 3 +- crates/collab_ui/src/contact_list.rs | 14 +++---- crates/collab_ui/src/contacts_popover.rs | 37 +++++++++++++----- crates/collab_ui/src/menu_bar_extra.rs | 31 ++++++++------- 6 files changed, 56 insertions(+), 72 deletions(-) delete mode 100644 crates/collab_ui/src/active_call_popover.rs diff --git a/crates/collab_ui/src/active_call_popover.rs b/crates/collab_ui/src/active_call_popover.rs deleted file mode 100644 index 01a4e4721d4f490696f0be3a97e7f75c09a67367..0000000000000000000000000000000000000000 --- a/crates/collab_ui/src/active_call_popover.rs +++ /dev/null @@ -1,40 +0,0 @@ -use gpui::{color::Color, elements::*, Entity, RenderContext, View, ViewContext}; - -pub enum Event { - Deactivated, -} - -pub struct ActiveCallPopover { - _subscription: gpui::Subscription, -} - -impl Entity for ActiveCallPopover { - type Event = Event; -} - -impl View for ActiveCallPopover { - fn ui_name() -> &'static str { - "ActiveCallPopover" - } - - fn render(&mut self, _: &mut RenderContext) -> ElementBox { - Empty::new() - .contained() - .with_background_color(Color::red()) - .boxed() - } -} - -impl ActiveCallPopover { - pub fn new(cx: &mut ViewContext) -> Self { - Self { - _subscription: cx.observe_window_activation(Self::window_activation_changed), - } - } - - fn window_activation_changed(&mut self, is_active: bool, cx: &mut ViewContext) { - if !is_active { - cx.emit(Event::Deactivated); - } - } -} diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index c46861b5ab659224b3462b71c7c6ddaab725719a..bbe27ce3a972976d8ede93fc6931420f6bcca11f 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -163,7 +163,8 @@ impl CollabTitlebarItem { if let Some(workspace) = self.workspace.upgrade(cx) { let project = workspace.read(cx).project().clone(); let user_store = workspace.read(cx).user_store().clone(); - let view = cx.add_view(|cx| ContactsPopover::new(project, user_store, cx)); + let view = cx + .add_view(|cx| ContactsPopover::new(false, Some(project), user_store, cx)); cx.focus(&view); cx.subscribe(&view, |this, _, event, cx| { match event { diff --git a/crates/collab_ui/src/collab_ui.rs b/crates/collab_ui/src/collab_ui.rs index da2cf775340974b062d90242d9a7fa7975f50886..221ee6068fc23e907fed2785303d41e328af0629 100644 --- a/crates/collab_ui/src/collab_ui.rs +++ b/crates/collab_ui/src/collab_ui.rs @@ -1,4 +1,3 @@ -mod active_call_popover; mod collab_titlebar_item; mod contact_finder; mod contact_list; @@ -23,7 +22,7 @@ pub fn init(app_state: Arc, cx: &mut MutableAppContext) { contact_finder::init(cx); contacts_popover::init(cx); incoming_call_notification::init(cx); - menu_bar_extra::init(cx); + menu_bar_extra::init(app_state.user_store.clone(), cx); project_shared_notification::init(cx); cx.add_global_action(move |action: &JoinProject, cx| { diff --git a/crates/collab_ui/src/contact_list.rs b/crates/collab_ui/src/contact_list.rs index a539f8ffac9e0a3e88b30562b46d45ed5b521b87..da48015b4e909cfdd29ce2d37a4776e04b28fe4c 100644 --- a/crates/collab_ui/src/contact_list.rs +++ b/crates/collab_ui/src/contact_list.rs @@ -114,7 +114,7 @@ pub struct ContactList { entries: Vec, match_candidates: Vec, list_state: ListState, - project: ModelHandle, + project: Option>, user_store: ModelHandle, filter_editor: ViewHandle, collapsed_sections: Vec
, @@ -124,7 +124,7 @@ pub struct ContactList { impl ContactList { pub fn new( - project: ModelHandle, + project: Option>, user_store: ModelHandle, cx: &mut ViewContext, ) -> Self { @@ -195,7 +195,7 @@ impl ContactList { ), ContactEntry::Contact(contact) => Self::render_contact( contact, - &this.project, + this.project.as_ref(), &theme.contact_list, is_selected, cx, @@ -292,7 +292,7 @@ impl ContactList { self.call( &Call { recipient_user_id: contact.user.id, - initial_project: Some(self.project.clone()), + initial_project: self.project.clone(), }, cx, ); @@ -664,7 +664,7 @@ impl ContactList { fn render_contact( contact: &Contact, - project: &ModelHandle, + project: Option<&ModelHandle>, theme: &theme::ContactList, is_selected: bool, cx: &mut RenderContext, @@ -672,7 +672,7 @@ impl ContactList { let online = contact.online; let busy = contact.busy; let user_id = contact.user.id; - let initial_project = project.clone(); + let initial_project = project.cloned(); let mut element = MouseEventHandler::::new(contact.user.id as usize, cx, |_, _| { Flex::row() @@ -726,7 +726,7 @@ impl ContactList { if online && !busy { cx.dispatch_action(Call { recipient_user_id: user_id, - initial_project: Some(initial_project.clone()), + initial_project: initial_project.clone(), }); } }); diff --git a/crates/collab_ui/src/contacts_popover.rs b/crates/collab_ui/src/contacts_popover.rs index 07ddc487a405412149c57aa78ad005a2732a7844..dd67cfe72409ca8da3abf79986799048a39dcdb9 100644 --- a/crates/collab_ui/src/contacts_popover.rs +++ b/crates/collab_ui/src/contacts_popover.rs @@ -23,30 +23,41 @@ enum Child { } pub struct ContactsPopover { + is_popup: bool, child: Child, - project: ModelHandle, + project: Option>, user_store: ModelHandle, _subscription: Option, + _window_subscription: gpui::Subscription, } impl ContactsPopover { pub fn new( - project: ModelHandle, + is_popup: bool, + project: Option>, user_store: ModelHandle, cx: &mut ViewContext, ) -> Self { let mut this = Self { + is_popup, child: Child::ContactList( cx.add_view(|cx| ContactList::new(project.clone(), user_store.clone(), cx)), ), project, user_store, _subscription: None, + _window_subscription: cx.observe_window_activation(Self::window_activation_changed), }; this.show_contact_list(cx); this } + fn window_activation_changed(&mut self, active: bool, cx: &mut ViewContext) { + if !active { + cx.emit(Event::Dismissed); + } + } + fn toggle_contact_finder(&mut self, _: &ToggleContactFinder, cx: &mut ViewContext) { match &self.child { Child::ContactList(_) => self.show_contact_finder(cx), @@ -92,13 +103,21 @@ impl View for ContactsPopover { Child::ContactFinder(child) => ChildView::new(child), }; - child - .contained() - .with_style(theme.contacts_popover.container) - .constrained() - .with_width(theme.contacts_popover.width) - .with_height(theme.contacts_popover.height) - .boxed() + let mut container_style = theme.contacts_popover.container; + if self.is_popup { + container_style.shadow = Default::default(); + container_style.border = Default::default(); + container_style.corner_radius = Default::default(); + child.contained().with_style(container_style).boxed() + } else { + child + .contained() + .with_style(container_style) + .constrained() + .with_width(theme.contacts_popover.width) + .with_height(theme.contacts_popover.height) + .boxed() + } } fn on_focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext) { diff --git a/crates/collab_ui/src/menu_bar_extra.rs b/crates/collab_ui/src/menu_bar_extra.rs index 814d51b1892756b729998b95a08d91e433995f55..1db9ceaabd325a8963c80102f54d48df66e7c29d 100644 --- a/crates/collab_ui/src/menu_bar_extra.rs +++ b/crates/collab_ui/src/menu_bar_extra.rs @@ -1,17 +1,18 @@ -use crate::active_call_popover::{self, ActiveCallPopover}; +use crate::contacts_popover::{self, ContactsPopover}; use call::ActiveCall; +use client::UserStore; use gpui::{ actions, color::Color, elements::*, geometry::{rect::RectF, vector::vec2f}, - Appearance, Entity, MouseButton, MutableAppContext, RenderContext, View, ViewContext, - ViewHandle, WindowKind, + Appearance, Entity, ModelHandle, MouseButton, MutableAppContext, RenderContext, View, + ViewContext, ViewHandle, WindowKind, }; actions!(menu_bar_extra, [ToggleActiveCallPopover]); -pub fn init(cx: &mut MutableAppContext) { +pub fn init(user_store: ModelHandle, cx: &mut MutableAppContext) { cx.add_action(MenuBarExtra::toggle_active_call_popover); let mut status_bar_item_id = None; @@ -24,7 +25,7 @@ pub fn init(cx: &mut MutableAppContext) { } if has_room { - let (id, _) = cx.add_status_bar_item(|_| MenuBarExtra::new()); + let (id, _) = cx.add_status_bar_item(|_| MenuBarExtra::new(user_store.clone())); status_bar_item_id = Some(id); } } @@ -33,7 +34,8 @@ pub fn init(cx: &mut MutableAppContext) { } struct MenuBarExtra { - popover: Option>, + popover: Option>, + user_store: ModelHandle, } impl Entity for MenuBarExtra { @@ -70,8 +72,11 @@ impl View for MenuBarExtra { } impl MenuBarExtra { - fn new() -> Self { - Self { popover: None } + fn new(user_store: ModelHandle) -> Self { + Self { + popover: None, + user_store, + } } fn toggle_active_call_popover( @@ -85,7 +90,7 @@ impl MenuBarExtra { } None => { let window_bounds = cx.window_bounds(); - let size = vec2f(360., 460.); + let size = vec2f(300., 350.); let origin = window_bounds.lower_left() + vec2f(window_bounds.width() / 2. - size.x() / 2., 0.); let (_, popover) = cx.add_window( @@ -96,7 +101,7 @@ impl MenuBarExtra { kind: WindowKind::PopUp, is_movable: false, }, - |cx| ActiveCallPopover::new(cx), + |cx| ContactsPopover::new(true, None, self.user_store.clone(), cx), ); cx.subscribe(&popover, Self::on_popover_event).detach(); self.popover = Some(popover); @@ -106,12 +111,12 @@ impl MenuBarExtra { fn on_popover_event( &mut self, - popover: ViewHandle, - event: &active_call_popover::Event, + popover: ViewHandle, + event: &contacts_popover::Event, cx: &mut ViewContext, ) { match event { - active_call_popover::Event::Deactivated => { + contacts_popover::Event::Dismissed => { self.popover.take(); cx.remove_window(popover.window_id()); } From 8dc99d42ff643a77ee1de2c087270ab7fd313e8f Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 10 Oct 2022 18:21:11 +0200 Subject: [PATCH 093/112] Remove menu bar extra --- assets/icons/zed_22.svg | 4 - crates/collab_ui/src/collab_titlebar_item.rs | 3 +- crates/collab_ui/src/collab_ui.rs | 2 - crates/collab_ui/src/contact_list.rs | 14 +-- crates/collab_ui/src/contacts_popover.rs | 29 ++--- crates/collab_ui/src/menu_bar_extra.rs | 125 ------------------- 6 files changed, 17 insertions(+), 160 deletions(-) delete mode 100644 assets/icons/zed_22.svg delete mode 100644 crates/collab_ui/src/menu_bar_extra.rs diff --git a/assets/icons/zed_22.svg b/assets/icons/zed_22.svg deleted file mode 100644 index 68e7dc8e57c966d5723920bc027c313161b95af8..0000000000000000000000000000000000000000 --- a/assets/icons/zed_22.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index bbe27ce3a972976d8ede93fc6931420f6bcca11f..c46861b5ab659224b3462b71c7c6ddaab725719a 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -163,8 +163,7 @@ impl CollabTitlebarItem { if let Some(workspace) = self.workspace.upgrade(cx) { let project = workspace.read(cx).project().clone(); let user_store = workspace.read(cx).user_store().clone(); - let view = cx - .add_view(|cx| ContactsPopover::new(false, Some(project), user_store, cx)); + let view = cx.add_view(|cx| ContactsPopover::new(project, user_store, cx)); cx.focus(&view); cx.subscribe(&view, |this, _, event, cx| { match event { diff --git a/crates/collab_ui/src/collab_ui.rs b/crates/collab_ui/src/collab_ui.rs index 221ee6068fc23e907fed2785303d41e328af0629..72595c2338efb7c403df422959e7a3deb9694972 100644 --- a/crates/collab_ui/src/collab_ui.rs +++ b/crates/collab_ui/src/collab_ui.rs @@ -4,7 +4,6 @@ mod contact_list; mod contact_notification; mod contacts_popover; mod incoming_call_notification; -mod menu_bar_extra; mod notifications; mod project_shared_notification; @@ -22,7 +21,6 @@ pub fn init(app_state: Arc, cx: &mut MutableAppContext) { contact_finder::init(cx); contacts_popover::init(cx); incoming_call_notification::init(cx); - menu_bar_extra::init(app_state.user_store.clone(), cx); project_shared_notification::init(cx); cx.add_global_action(move |action: &JoinProject, cx| { diff --git a/crates/collab_ui/src/contact_list.rs b/crates/collab_ui/src/contact_list.rs index da48015b4e909cfdd29ce2d37a4776e04b28fe4c..a539f8ffac9e0a3e88b30562b46d45ed5b521b87 100644 --- a/crates/collab_ui/src/contact_list.rs +++ b/crates/collab_ui/src/contact_list.rs @@ -114,7 +114,7 @@ pub struct ContactList { entries: Vec, match_candidates: Vec, list_state: ListState, - project: Option>, + project: ModelHandle, user_store: ModelHandle, filter_editor: ViewHandle, collapsed_sections: Vec
, @@ -124,7 +124,7 @@ pub struct ContactList { impl ContactList { pub fn new( - project: Option>, + project: ModelHandle, user_store: ModelHandle, cx: &mut ViewContext, ) -> Self { @@ -195,7 +195,7 @@ impl ContactList { ), ContactEntry::Contact(contact) => Self::render_contact( contact, - this.project.as_ref(), + &this.project, &theme.contact_list, is_selected, cx, @@ -292,7 +292,7 @@ impl ContactList { self.call( &Call { recipient_user_id: contact.user.id, - initial_project: self.project.clone(), + initial_project: Some(self.project.clone()), }, cx, ); @@ -664,7 +664,7 @@ impl ContactList { fn render_contact( contact: &Contact, - project: Option<&ModelHandle>, + project: &ModelHandle, theme: &theme::ContactList, is_selected: bool, cx: &mut RenderContext, @@ -672,7 +672,7 @@ impl ContactList { let online = contact.online; let busy = contact.busy; let user_id = contact.user.id; - let initial_project = project.cloned(); + let initial_project = project.clone(); let mut element = MouseEventHandler::::new(contact.user.id as usize, cx, |_, _| { Flex::row() @@ -726,7 +726,7 @@ impl ContactList { if online && !busy { cx.dispatch_action(Call { recipient_user_id: user_id, - initial_project: initial_project.clone(), + initial_project: Some(initial_project.clone()), }); } }); diff --git a/crates/collab_ui/src/contacts_popover.rs b/crates/collab_ui/src/contacts_popover.rs index dd67cfe72409ca8da3abf79986799048a39dcdb9..b9b1d254b6ce10c98792b965c24b55749534f223 100644 --- a/crates/collab_ui/src/contacts_popover.rs +++ b/crates/collab_ui/src/contacts_popover.rs @@ -23,9 +23,8 @@ enum Child { } pub struct ContactsPopover { - is_popup: bool, child: Child, - project: Option>, + project: ModelHandle, user_store: ModelHandle, _subscription: Option, _window_subscription: gpui::Subscription, @@ -33,13 +32,11 @@ pub struct ContactsPopover { impl ContactsPopover { pub fn new( - is_popup: bool, - project: Option>, + project: ModelHandle, user_store: ModelHandle, cx: &mut ViewContext, ) -> Self { let mut this = Self { - is_popup, child: Child::ContactList( cx.add_view(|cx| ContactList::new(project.clone(), user_store.clone(), cx)), ), @@ -103,21 +100,13 @@ impl View for ContactsPopover { Child::ContactFinder(child) => ChildView::new(child), }; - let mut container_style = theme.contacts_popover.container; - if self.is_popup { - container_style.shadow = Default::default(); - container_style.border = Default::default(); - container_style.corner_radius = Default::default(); - child.contained().with_style(container_style).boxed() - } else { - child - .contained() - .with_style(container_style) - .constrained() - .with_width(theme.contacts_popover.width) - .with_height(theme.contacts_popover.height) - .boxed() - } + child + .contained() + .with_style(theme.contacts_popover.container) + .constrained() + .with_width(theme.contacts_popover.width) + .with_height(theme.contacts_popover.height) + .boxed() } fn on_focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext) { diff --git a/crates/collab_ui/src/menu_bar_extra.rs b/crates/collab_ui/src/menu_bar_extra.rs deleted file mode 100644 index 1db9ceaabd325a8963c80102f54d48df66e7c29d..0000000000000000000000000000000000000000 --- a/crates/collab_ui/src/menu_bar_extra.rs +++ /dev/null @@ -1,125 +0,0 @@ -use crate::contacts_popover::{self, ContactsPopover}; -use call::ActiveCall; -use client::UserStore; -use gpui::{ - actions, - color::Color, - elements::*, - geometry::{rect::RectF, vector::vec2f}, - Appearance, Entity, ModelHandle, MouseButton, MutableAppContext, RenderContext, View, - ViewContext, ViewHandle, WindowKind, -}; - -actions!(menu_bar_extra, [ToggleActiveCallPopover]); - -pub fn init(user_store: ModelHandle, cx: &mut MutableAppContext) { - cx.add_action(MenuBarExtra::toggle_active_call_popover); - - let mut status_bar_item_id = None; - cx.observe(&ActiveCall::global(cx), move |call, cx| { - let had_room = status_bar_item_id.is_some(); - let has_room = call.read(cx).room().is_some(); - if had_room != has_room { - if let Some(status_bar_item_id) = status_bar_item_id.take() { - cx.remove_status_bar_item(status_bar_item_id); - } - - if has_room { - let (id, _) = cx.add_status_bar_item(|_| MenuBarExtra::new(user_store.clone())); - status_bar_item_id = Some(id); - } - } - }) - .detach(); -} - -struct MenuBarExtra { - popover: Option>, - user_store: ModelHandle, -} - -impl Entity for MenuBarExtra { - type Event = (); - - fn release(&mut self, cx: &mut MutableAppContext) { - if let Some(popover) = self.popover.take() { - cx.remove_window(popover.window_id()); - } - } -} - -impl View for MenuBarExtra { - fn ui_name() -> &'static str { - "MenuBarExtra" - } - - fn render(&mut self, cx: &mut RenderContext) -> ElementBox { - let color = match cx.appearance { - Appearance::Light | Appearance::VibrantLight => Color::black(), - Appearance::Dark | Appearance::VibrantDark => Color::white(), - }; - MouseEventHandler::::new(0, cx, |_, _| { - Svg::new("icons/zed_22.svg") - .with_color(color) - .aligned() - .boxed() - }) - .on_click(MouseButton::Left, |_, cx| { - cx.dispatch_action(ToggleActiveCallPopover); - }) - .boxed() - } -} - -impl MenuBarExtra { - fn new(user_store: ModelHandle) -> Self { - Self { - popover: None, - user_store, - } - } - - fn toggle_active_call_popover( - &mut self, - _: &ToggleActiveCallPopover, - cx: &mut ViewContext, - ) { - match self.popover.take() { - Some(popover) => { - cx.remove_window(popover.window_id()); - } - None => { - let window_bounds = cx.window_bounds(); - let size = vec2f(300., 350.); - let origin = window_bounds.lower_left() - + vec2f(window_bounds.width() / 2. - size.x() / 2., 0.); - let (_, popover) = cx.add_window( - gpui::WindowOptions { - bounds: gpui::WindowBounds::Fixed(RectF::new(origin, size)), - titlebar: None, - center: false, - kind: WindowKind::PopUp, - is_movable: false, - }, - |cx| ContactsPopover::new(true, None, self.user_store.clone(), cx), - ); - cx.subscribe(&popover, Self::on_popover_event).detach(); - self.popover = Some(popover); - } - } - } - - fn on_popover_event( - &mut self, - popover: ViewHandle, - event: &contacts_popover::Event, - cx: &mut ViewContext, - ) { - match event { - contacts_popover::Event::Dismissed => { - self.popover.take(); - cx.remove_window(popover.window_id()); - } - } - } -} From 94c68d246e1c009b481043ca15c94ba971f8cc16 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 10 Oct 2022 19:18:05 +0200 Subject: [PATCH 094/112] :memo: Co-Authored-By: Nathan Sobo --- crates/workspace/src/pane_group.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/workspace/src/pane_group.rs b/crates/workspace/src/pane_group.rs index 694f6a55eb44d42f5dacf6d9419aa013908d1b7b..83ce28fdf0b9420a27d174a603dca8b558a831c5 100644 --- a/crates/workspace/src/pane_group.rs +++ b/crates/workspace/src/pane_group.rs @@ -153,7 +153,7 @@ impl Member { |_, _| { Label::new( format!( - "Follow {} on their currently active project", + "Follow {} on their active project", leader_user.github_login, ), theme From b8c2acf0f2b87ee49c5f2a3e4404fb3aa62d1644 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 10 Oct 2022 17:56:03 -0600 Subject: [PATCH 095/112] Show worktree root names when sharing additional projects on a call Co-Authored-By: Antonio Scandurra --- crates/call/src/participant.rs | 2 +- crates/call/src/room.rs | 41 ++++++++---- crates/collab/src/integration_tests.rs | 2 + crates/collab/src/rpc.rs | 11 +++- crates/collab/src/rpc/store.rs | 64 +++++++++++++++---- .../src/project_shared_notification.rs | 49 ++++++++++++-- crates/rpc/proto/zed.proto | 8 ++- crates/theme/src/theme.rs | 3 + .../styleTree/projectSharedNotification.ts | 8 ++- 9 files changed, 153 insertions(+), 35 deletions(-) diff --git a/crates/call/src/participant.rs b/crates/call/src/participant.rs index db7a84e58427b51d37b8ddd634fda7800ebfd05b..b124d920a3937db910f384bf057fe475231d433d 100644 --- a/crates/call/src/participant.rs +++ b/crates/call/src/participant.rs @@ -23,6 +23,6 @@ impl ParticipantLocation { #[derive(Clone, Debug)] pub struct RemoteParticipant { pub user: Arc, - pub project_ids: Vec, + pub projects: Vec, pub location: ParticipantLocation, } diff --git a/crates/call/src/room.rs b/crates/call/src/room.rs index 9cad1ff211a4446eec3afde5fccb13df99858860..fb9ba09d814cda6cfaa551ce3fe926fb802ad7c4 100644 --- a/crates/call/src/room.rs +++ b/crates/call/src/room.rs @@ -13,7 +13,11 @@ use util::ResultExt; #[derive(Clone, Debug, PartialEq, Eq)] pub enum Event { - RemoteProjectShared { owner: Arc, project_id: u64 }, + RemoteProjectShared { + owner: Arc, + project_id: u64, + worktree_root_names: Vec, + }, } pub struct Room { @@ -219,16 +223,19 @@ impl Room { let peer_id = PeerId(participant.peer_id); this.participant_user_ids.insert(participant.user_id); - let existing_project_ids = this + let existing_projects = this .remote_participants .get(&peer_id) - .map(|existing| existing.project_ids.clone()) - .unwrap_or_default(); - for project_id in &participant.project_ids { - if !existing_project_ids.contains(project_id) { + .into_iter() + .flat_map(|existing| &existing.projects) + .map(|project| project.id) + .collect::>(); + for project in &participant.projects { + if !existing_projects.contains(&project.id) { cx.emit(Event::RemoteProjectShared { owner: user.clone(), - project_id: *project_id, + project_id: project.id, + worktree_root_names: project.worktree_root_names.clone(), }); } } @@ -237,7 +244,7 @@ impl Room { peer_id, RemoteParticipant { user: user.clone(), - project_ids: participant.project_ids, + projects: participant.projects, location: ParticipantLocation::from_proto(participant.location) .unwrap_or(ParticipantLocation::External), }, @@ -334,9 +341,21 @@ impl Room { return Task::ready(Ok(project_id)); } - let request = self - .client - .request(proto::ShareProject { room_id: self.id() }); + let request = self.client.request(proto::ShareProject { + room_id: self.id(), + worktrees: project + .read(cx) + .worktrees(cx) + .map(|worktree| { + let worktree = worktree.read(cx); + proto::WorktreeMetadata { + id: worktree.id().to_proto(), + root_name: worktree.root_name().into(), + visible: worktree.is_visible(), + } + }) + .collect(), + }); cx.spawn_weak(|_, mut cx| async move { let response = request.await?; project diff --git a/crates/collab/src/integration_tests.rs b/crates/collab/src/integration_tests.rs index 87ae24bb3da868d3374bcd3d9961f40cbfb40922..06e009d08bd6d04291ed65d47ba5899836e55233 100644 --- a/crates/collab/src/integration_tests.rs +++ b/crates/collab/src/integration_tests.rs @@ -819,6 +819,7 @@ async fn test_active_call_events( avatar: None, }), project_id: project_a_id, + worktree_root_names: vec!["a".to_string()], }] ); @@ -836,6 +837,7 @@ async fn test_active_call_events( avatar: None, }), project_id: project_b_id, + worktree_root_names: vec!["b".to_string()] }] ); assert_eq!(mem::take(&mut *events_b.borrow_mut()), vec![]); diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 05bcd6875d899f6863adeddb8fb5f7f5590f9d45..11219fb8b84975ecfc65f342cd9e1e9c9daf2133 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -827,7 +827,12 @@ impl Server { .user_id_for_connection(request.sender_id)?; let project_id = self.app_state.db.register_project(user_id).await?; let mut store = self.store().await; - let room = store.share_project(request.payload.room_id, project_id, request.sender_id)?; + let room = store.share_project( + request.payload.room_id, + project_id, + request.payload.worktrees, + request.sender_id, + )?; response.send(proto::ShareProjectResponse { project_id: project_id.to_proto(), })?; @@ -1036,11 +1041,13 @@ impl Server { let guest_connection_ids = state .read_project(project_id, request.sender_id)? .guest_connection_ids(); - state.update_project(project_id, &request.payload.worktrees, request.sender_id)?; + let room = + state.update_project(project_id, &request.payload.worktrees, request.sender_id)?; broadcast(request.sender_id, guest_connection_ids, |connection_id| { self.peer .forward_send(request.sender_id, connection_id, request.payload.clone()) }); + self.room_updated(room); }; Ok(()) diff --git a/crates/collab/src/rpc/store.rs b/crates/collab/src/rpc/store.rs index 5b23ac92d5d7af57268a7be97390ec67bc15e5dd..522438255a80928a12925fb8b1661bdc10744cbe 100644 --- a/crates/collab/src/rpc/store.rs +++ b/crates/collab/src/rpc/store.rs @@ -383,7 +383,7 @@ impl Store { room.participants.push(proto::Participant { user_id: connection.user_id.to_proto(), peer_id: creator_connection_id.0, - project_ids: Default::default(), + projects: Default::default(), location: Some(proto::ParticipantLocation { variant: Some(proto::participant_location::Variant::External( proto::participant_location::External {}, @@ -441,7 +441,7 @@ impl Store { room.participants.push(proto::Participant { user_id: user_id.to_proto(), peer_id: connection_id.0, - project_ids: Default::default(), + projects: Default::default(), location: Some(proto::ParticipantLocation { variant: Some(proto::participant_location::Variant::External( proto::participant_location::External {}, @@ -689,7 +689,8 @@ impl Store { anyhow::ensure!( room.participants .iter() - .any(|participant| participant.project_ids.contains(&project.id)), + .flat_map(|participant| &participant.projects) + .any(|participant_project| participant_project.id == project.id), "no such project" ); } @@ -708,6 +709,7 @@ impl Store { &mut self, room_id: RoomId, project_id: ProjectId, + worktrees: Vec, host_connection_id: ConnectionId, ) -> Result<&proto::Room> { let connection = self @@ -724,7 +726,14 @@ impl Store { .iter_mut() .find(|participant| participant.peer_id == host_connection_id.0) .ok_or_else(|| anyhow!("no such room"))?; - participant.project_ids.push(project_id.to_proto()); + participant.projects.push(proto::ParticipantProject { + id: project_id.to_proto(), + worktree_root_names: worktrees + .iter() + .filter(|worktree| worktree.visible) + .map(|worktree| worktree.root_name.clone()) + .collect(), + }); connection.projects.insert(project_id); self.projects.insert( @@ -741,7 +750,19 @@ impl Store { }, guests: Default::default(), active_replica_ids: Default::default(), - worktrees: Default::default(), + worktrees: worktrees + .into_iter() + .map(|worktree| { + ( + worktree.id, + Worktree { + root_name: worktree.root_name, + visible: worktree.visible, + ..Default::default() + }, + ) + }) + .collect(), language_servers: Default::default(), }, ); @@ -779,8 +800,8 @@ impl Store { .find(|participant| participant.peer_id == connection_id.0) .ok_or_else(|| anyhow!("no such room"))?; participant - .project_ids - .retain(|id| *id != project_id.to_proto()); + .projects + .retain(|project| project.id != project_id.to_proto()); Ok((room, project)) } else { @@ -796,7 +817,7 @@ impl Store { project_id: ProjectId, worktrees: &[proto::WorktreeMetadata], connection_id: ConnectionId, - ) -> Result<()> { + ) -> Result<&proto::Room> { let project = self .projects .get_mut(&project_id) @@ -818,7 +839,23 @@ impl Store { } } - Ok(()) + let room = self + .rooms + .get_mut(&project.room_id) + .ok_or_else(|| anyhow!("no such room"))?; + let participant_project = room + .participants + .iter_mut() + .flat_map(|participant| &mut participant.projects) + .find(|project| project.id == project_id.to_proto()) + .ok_or_else(|| anyhow!("no such project"))?; + participant_project.worktree_root_names = worktrees + .iter() + .filter(|worktree| worktree.visible) + .map(|worktree| worktree.root_name.clone()) + .collect(); + + Ok(room) } else { Err(anyhow!("no such project"))? } @@ -1132,8 +1169,8 @@ impl Store { "room contains participant that has disconnected" ); - for project_id in &participant.project_ids { - let project = &self.projects[&ProjectId::from_proto(*project_id)]; + for participant_project in &participant.projects { + let project = &self.projects[&ProjectId::from_proto(participant_project.id)]; assert_eq!( project.room_id, *room_id, "project was shared on a different room" @@ -1173,8 +1210,9 @@ impl Store { .unwrap(); assert!( room_participant - .project_ids - .contains(&project_id.to_proto()), + .projects + .iter() + .any(|project| project.id == project_id.to_proto()), "project was not shared in room" ); } diff --git a/crates/collab_ui/src/project_shared_notification.rs b/crates/collab_ui/src/project_shared_notification.rs index 5597ea0a01467ba5267097b6e0dd83416eb83142..e5ff27060a5f6299795e8679b508a40f2a9e92ca 100644 --- a/crates/collab_ui/src/project_shared_notification.rs +++ b/crates/collab_ui/src/project_shared_notification.rs @@ -19,10 +19,16 @@ pub fn init(cx: &mut MutableAppContext) { let active_call = ActiveCall::global(cx); cx.subscribe(&active_call, move |_, event, cx| match event { - room::Event::RemoteProjectShared { owner, project_id } => { + room::Event::RemoteProjectShared { + owner, + project_id, + worktree_root_names, + } => { const PADDING: f32 = 16.; let screen_size = cx.platform().screen_size(); - let window_size = vec2f(366., 64.); + + let theme = &cx.global::().theme.project_shared_notification; + let window_size = vec2f(theme.window_width, theme.window_height); cx.add_window( WindowOptions { bounds: WindowBounds::Fixed(RectF::new( @@ -34,7 +40,13 @@ pub fn init(cx: &mut MutableAppContext) { kind: WindowKind::PopUp, is_movable: false, }, - |_| ProjectSharedNotification::new(*project_id, owner.clone()), + |_| { + ProjectSharedNotification::new( + owner.clone(), + *project_id, + worktree_root_names.clone(), + ) + }, ); } }) @@ -43,12 +55,17 @@ pub fn init(cx: &mut MutableAppContext) { pub struct ProjectSharedNotification { project_id: u64, + worktree_root_names: Vec, owner: Arc, } impl ProjectSharedNotification { - fn new(project_id: u64, owner: Arc) -> Self { - Self { project_id, owner } + fn new(owner: Arc, project_id: u64, worktree_root_names: Vec) -> Self { + Self { + project_id, + worktree_root_names, + owner, + } } fn join(&mut self, _: &JoinProject, cx: &mut ViewContext) { @@ -84,13 +101,33 @@ impl ProjectSharedNotification { ) .with_child( Label::new( - "has shared a project with you".into(), + format!( + "shared a project in Zed{}", + if self.worktree_root_names.is_empty() { + "" + } else { + ":" + } + ), theme.message.text.clone(), ) .contained() .with_style(theme.message.container) .boxed(), ) + .with_children(if self.worktree_root_names.is_empty() { + None + } else { + Some( + Label::new( + self.worktree_root_names.join(", "), + theme.worktree_roots.text.clone(), + ) + .contained() + .with_style(theme.worktree_roots.container) + .boxed(), + ) + }) .contained() .with_style(theme.owner_metadata) .aligned() diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index f9a9fbd34e788e69f14f50c231585ceb0ffd3478..b1897653c91437364e9d6e206445549f9ee38299 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -163,10 +163,15 @@ message Room { message Participant { uint64 user_id = 1; uint32 peer_id = 2; - repeated uint64 project_ids = 3; + repeated ParticipantProject projects = 3; ParticipantLocation location = 4; } +message ParticipantProject { + uint64 id = 1; + repeated string worktree_root_names = 2; +} + message ParticipantLocation { oneof variant { Project project = 1; @@ -215,6 +220,7 @@ message RoomUpdated { message ShareProject { uint64 room_id = 1; + repeated WorktreeMetadata worktrees = 2; } message ShareProjectResponse { diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 2f193048292da351fa86aedddb341b246dede384..50e2462c159997636fe4e0129687355badf3d123 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -471,6 +471,8 @@ pub struct UpdateNotification { #[derive(Deserialize, Default)] pub struct ProjectSharedNotification { + pub window_height: f32, + pub window_width: f32, #[serde(default)] pub background: Color, pub owner_container: ContainerStyle, @@ -478,6 +480,7 @@ pub struct ProjectSharedNotification { pub owner_metadata: ContainerStyle, pub owner_username: ContainedText, pub message: ContainedText, + pub worktree_roots: ContainedText, pub button_width: f32, pub join_button: ContainedText, pub dismiss_button: ContainedText, diff --git a/styles/src/styleTree/projectSharedNotification.ts b/styles/src/styleTree/projectSharedNotification.ts index 7a70f0f0d3577cdd378cd25604c731619689251e..abe77e7d56ad4ecfbf6734e47d9ef891249cadd8 100644 --- a/styles/src/styleTree/projectSharedNotification.ts +++ b/styles/src/styleTree/projectSharedNotification.ts @@ -2,8 +2,10 @@ import Theme from "../themes/common/theme"; import { backgroundColor, borderColor, text } from "./components"; export default function projectSharedNotification(theme: Theme): Object { - const avatarSize = 32; + const avatarSize = 48; return { + windowHeight: 72, + windowWidth: 360, background: backgroundColor(theme, 300), ownerContainer: { padding: 12, @@ -24,6 +26,10 @@ export default function projectSharedNotification(theme: Theme): Object { ...text(theme, "sans", "secondary", { size: "xs" }), margin: { top: -3 }, }, + worktreeRoots: { + ...text(theme, "sans", "secondary", { size: "xs", weight: "bold" }), + margin: { top: -3 }, + }, buttonWidth: 96, joinButton: { background: backgroundColor(theme, "info", "active"), From e0b6b0df2aab8edd5502d7e7bc10df9ca4cbf3be Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 10 Oct 2022 18:12:00 -0600 Subject: [PATCH 096/112] Rename Join button to Open, rework message slightly --- crates/collab_ui/src/project_shared_notification.rs | 10 +++++----- crates/theme/src/theme.rs | 2 +- styles/src/styleTree/projectSharedNotification.ts | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/crates/collab_ui/src/project_shared_notification.rs b/crates/collab_ui/src/project_shared_notification.rs index e5ff27060a5f6299795e8679b508a40f2a9e92ca..22383feb20fc3da614654a078b34d2f32e11af9f 100644 --- a/crates/collab_ui/src/project_shared_notification.rs +++ b/crates/collab_ui/src/project_shared_notification.rs @@ -102,7 +102,7 @@ impl ProjectSharedNotification { .with_child( Label::new( format!( - "shared a project in Zed{}", + "is sharing a project in Zed{}", if self.worktree_root_names.is_empty() { "" } else { @@ -140,7 +140,7 @@ impl ProjectSharedNotification { } fn render_buttons(&self, cx: &mut RenderContext) -> ElementBox { - enum Join {} + enum Open {} enum Dismiss {} let project_id = self.project_id; @@ -148,12 +148,12 @@ impl ProjectSharedNotification { Flex::column() .with_child( - MouseEventHandler::::new(0, cx, |_, cx| { + MouseEventHandler::::new(0, cx, |_, cx| { let theme = &cx.global::().theme.project_shared_notification; - Label::new("Join".to_string(), theme.join_button.text.clone()) + Label::new("Open".to_string(), theme.open_button.text.clone()) .aligned() .contained() - .with_style(theme.join_button.container) + .with_style(theme.open_button.container) .boxed() }) .with_cursor_style(CursorStyle::PointingHand) diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 50e2462c159997636fe4e0129687355badf3d123..7f11ae6e03633a141a9303e18a5796448d04c528 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -482,7 +482,7 @@ pub struct ProjectSharedNotification { pub message: ContainedText, pub worktree_roots: ContainedText, pub button_width: f32, - pub join_button: ContainedText, + pub open_button: ContainedText, pub dismiss_button: ContainedText, } diff --git a/styles/src/styleTree/projectSharedNotification.ts b/styles/src/styleTree/projectSharedNotification.ts index abe77e7d56ad4ecfbf6734e47d9ef891249cadd8..f6ebf4781afe40ce2a0088524326e0d0d8c005a7 100644 --- a/styles/src/styleTree/projectSharedNotification.ts +++ b/styles/src/styleTree/projectSharedNotification.ts @@ -5,7 +5,7 @@ export default function projectSharedNotification(theme: Theme): Object { const avatarSize = 48; return { windowHeight: 72, - windowWidth: 360, + windowWidth: 380, background: backgroundColor(theme, 300), ownerContainer: { padding: 12, @@ -31,7 +31,7 @@ export default function projectSharedNotification(theme: Theme): Object { margin: { top: -3 }, }, buttonWidth: 96, - joinButton: { + openButton: { background: backgroundColor(theme, "info", "active"), border: { left: true, bottom: true, width: 1, color: borderColor(theme, "primary") }, ...text(theme, "sans", "info", { size: "xs", weight: "extra_bold" }) From bf488f2027a432d7cdd67771ae4bca0ba3393ed9 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 11 Oct 2022 10:59:36 +0200 Subject: [PATCH 097/112] Show project root names when displaying incoming call notification --- crates/call/src/call.rs | 4 +- crates/collab/src/integration_tests.rs | 6 ++- crates/collab/src/rpc/store.rs | 35 ++++++++++----- .../src/incoming_call_notification.rs | 44 ++++++++++++++++--- crates/rpc/proto/zed.proto | 2 +- crates/theme/src/theme.rs | 3 ++ .../src/styleTree/incomingCallNotification.ts | 6 +++ .../styleTree/projectSharedNotification.ts | 2 +- 8 files changed, 80 insertions(+), 22 deletions(-) diff --git a/crates/call/src/call.rs b/crates/call/src/call.rs index 99edb33b6eb2c14038e5e1e0e41314d322c917ed..6b06d04375b8a1fca563ef3be558f823ce45cd1c 100644 --- a/crates/call/src/call.rs +++ b/crates/call/src/call.rs @@ -23,7 +23,7 @@ pub struct IncomingCall { pub room_id: u64, pub caller: Arc, pub participants: Vec>, - pub initial_project_id: Option, + pub initial_project: Option, } pub struct ActiveCall { @@ -78,7 +78,7 @@ impl ActiveCall { user_store.get_user(envelope.payload.caller_user_id, cx) }) .await?, - initial_project_id: envelope.payload.initial_project_id, + initial_project: envelope.payload.initial_project, }; this.update(&mut cx, |this, _| { *this.incoming_call.0.borrow_mut() = Some(call); diff --git a/crates/collab/src/integration_tests.rs b/crates/collab/src/integration_tests.rs index 06e009d08bd6d04291ed65d47ba5899836e55233..ce9d01e3d314cc95119977fed7229046ff5e0a28 100644 --- a/crates/collab/src/integration_tests.rs +++ b/crates/collab/src/integration_tests.rs @@ -541,13 +541,15 @@ async fn test_share_project( deterministic.run_until_parked(); let call = incoming_call_b.borrow().clone().unwrap(); assert_eq!(call.caller.github_login, "user_a"); - let project_id = call.initial_project_id.unwrap(); + let initial_project = call.initial_project.unwrap(); active_call_b .update(cx_b, |call, cx| call.accept_incoming(cx)) .await .unwrap(); let client_b_peer_id = client_b.peer_id; - let project_b = client_b.build_remote_project(project_id, cx_b).await; + let project_b = client_b + .build_remote_project(initial_project.id, cx_b) + .await; let replica_id_b = project_b.read_with(cx_b, |project, _| project.replica_id()); deterministic.run_until_parked(); diff --git a/crates/collab/src/rpc/store.rs b/crates/collab/src/rpc/store.rs index 522438255a80928a12925fb8b1661bdc10744cbe..12f50db48e47795ac49cfd905aaec7f1b43afad9 100644 --- a/crates/collab/src/rpc/store.rs +++ b/crates/collab/src/rpc/store.rs @@ -176,9 +176,9 @@ impl Store { .iter() .map(|participant| participant.user_id) .collect(), - initial_project_id: active_call + initial_project: active_call .initial_project_id - .map(|project_id| project_id.to_proto()), + .and_then(|id| Self::build_participant_project(id, &self.projects)), }) } } else { @@ -572,7 +572,8 @@ impl Store { .iter() .map(|participant| participant.user_id) .collect(), - initial_project_id: initial_project_id.map(|project_id| project_id.to_proto()), + initial_project: initial_project_id + .and_then(|id| Self::build_participant_project(id, &self.projects)), }, )) } @@ -726,14 +727,6 @@ impl Store { .iter_mut() .find(|participant| participant.peer_id == host_connection_id.0) .ok_or_else(|| anyhow!("no such room"))?; - participant.projects.push(proto::ParticipantProject { - id: project_id.to_proto(), - worktree_root_names: worktrees - .iter() - .filter(|worktree| worktree.visible) - .map(|worktree| worktree.root_name.clone()) - .collect(), - }); connection.projects.insert(project_id); self.projects.insert( @@ -767,6 +760,10 @@ impl Store { }, ); + participant + .projects + .extend(Self::build_participant_project(project_id, &self.projects)); + Ok(room) } @@ -1011,6 +1008,22 @@ impl Store { Ok(connection_ids) } + fn build_participant_project( + project_id: ProjectId, + projects: &BTreeMap, + ) -> Option { + Some(proto::ParticipantProject { + id: project_id.to_proto(), + worktree_root_names: projects + .get(&project_id)? + .worktrees + .values() + .filter(|worktree| worktree.visible) + .map(|worktree| worktree.root_name.clone()) + .collect(), + }) + } + pub fn project_connection_ids( &self, project_id: ProjectId, diff --git a/crates/collab_ui/src/incoming_call_notification.rs b/crates/collab_ui/src/incoming_call_notification.rs index cbb23de2e695bca9b4e4aca73b5a7785acb56be8..ff359b9d9e087219cfabb95fd0c719b67183aa44 100644 --- a/crates/collab_ui/src/incoming_call_notification.rs +++ b/crates/collab_ui/src/incoming_call_notification.rs @@ -1,4 +1,5 @@ use call::{ActiveCall, IncomingCall}; +use client::proto; use futures::StreamExt; use gpui::{ elements::*, @@ -26,7 +27,11 @@ pub fn init(cx: &mut MutableAppContext) { if let Some(incoming_call) = incoming_call { const PADDING: f32 = 16.; let screen_size = cx.platform().screen_size(); - let window_size = vec2f(274., 64.); + + let window_size = cx.read(|cx| { + let theme = &cx.global::().theme.incoming_call_notification; + vec2f(theme.window_width, theme.window_height) + }); let (window_id, _) = cx.add_window( WindowOptions { bounds: WindowBounds::Fixed(RectF::new( @@ -66,7 +71,7 @@ impl IncomingCallNotification { if action.accept { let join = active_call.update(cx, |active_call, cx| active_call.accept_incoming(cx)); let caller_user_id = self.call.caller.id; - let initial_project_id = self.call.initial_project_id; + let initial_project_id = self.call.initial_project.as_ref().map(|project| project.id); cx.spawn_weak(|_, mut cx| async move { join.await?; if let Some(project_id) = initial_project_id { @@ -89,6 +94,12 @@ impl IncomingCallNotification { fn render_caller(&self, cx: &mut RenderContext) -> ElementBox { let theme = &cx.global::().theme.incoming_call_notification; + let default_project = proto::ParticipantProject::default(); + let initial_project = self + .call + .initial_project + .as_ref() + .unwrap_or(&default_project); Flex::row() .with_children(self.call.caller.avatar.clone().map(|avatar| { Image::new(avatar) @@ -108,11 +119,34 @@ impl IncomingCallNotification { .boxed(), ) .with_child( - Label::new("is calling you".into(), theme.caller_message.text.clone()) + Label::new( + format!( + "is sharing a project in Zed{}", + if initial_project.worktree_root_names.is_empty() { + "" + } else { + ":" + } + ), + theme.caller_message.text.clone(), + ) + .contained() + .with_style(theme.caller_message.container) + .boxed(), + ) + .with_children(if initial_project.worktree_root_names.is_empty() { + None + } else { + Some( + Label::new( + initial_project.worktree_root_names.join(", "), + theme.worktree_roots.text.clone(), + ) .contained() - .with_style(theme.caller_message.container) + .with_style(theme.worktree_roots.container) .boxed(), - ) + ) + }) .contained() .with_style(theme.caller_metadata) .aligned() diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index b1897653c91437364e9d6e206445549f9ee38299..2a358113e9238616119e0d3450457d883825d704 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -195,7 +195,7 @@ message IncomingCall { uint64 room_id = 1; uint64 caller_user_id = 2; repeated uint64 participant_user_ids = 3; - optional uint64 initial_project_id = 4; + optional ParticipantProject initial_project = 4; } message CallCanceled {} diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 7f11ae6e03633a141a9303e18a5796448d04c528..9a836864bff66377f7806e0d6f9a512c9c8ab2f5 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -488,6 +488,8 @@ pub struct ProjectSharedNotification { #[derive(Deserialize, Default)] pub struct IncomingCallNotification { + pub window_height: f32, + pub window_width: f32, #[serde(default)] pub background: Color, pub caller_container: ContainerStyle, @@ -495,6 +497,7 @@ pub struct IncomingCallNotification { pub caller_metadata: ContainerStyle, pub caller_username: ContainedText, pub caller_message: ContainedText, + pub worktree_roots: ContainedText, pub button_width: f32, pub accept_button: ContainedText, pub decline_button: ContainedText, diff --git a/styles/src/styleTree/incomingCallNotification.ts b/styles/src/styleTree/incomingCallNotification.ts index d8ea7dbad90ccf84b760a39e9ea7581de71ccc6c..30b8ae90cad05c3cdb5fd08af2b4554f58694039 100644 --- a/styles/src/styleTree/incomingCallNotification.ts +++ b/styles/src/styleTree/incomingCallNotification.ts @@ -4,6 +4,8 @@ import { backgroundColor, borderColor, text } from "./components"; export default function incomingCallNotification(theme: Theme): Object { const avatarSize = 32; return { + windowHeight: 74, + windowWidth: 380, background: backgroundColor(theme, 300), callerContainer: { padding: 12, @@ -24,6 +26,10 @@ export default function incomingCallNotification(theme: Theme): Object { ...text(theme, "sans", "secondary", { size: "xs" }), margin: { top: -3 }, }, + worktreeRoots: { + ...text(theme, "sans", "secondary", { size: "xs", weight: "bold" }), + margin: { top: -3 }, + }, buttonWidth: 96, acceptButton: { background: backgroundColor(theme, "ok", "active"), diff --git a/styles/src/styleTree/projectSharedNotification.ts b/styles/src/styleTree/projectSharedNotification.ts index f6ebf4781afe40ce2a0088524326e0d0d8c005a7..e3cb29bbf1ea539b2a26040936ad8f85601da23e 100644 --- a/styles/src/styleTree/projectSharedNotification.ts +++ b/styles/src/styleTree/projectSharedNotification.ts @@ -4,7 +4,7 @@ import { backgroundColor, borderColor, text } from "./components"; export default function projectSharedNotification(theme: Theme): Object { const avatarSize = 48; return { - windowHeight: 72, + windowHeight: 74, windowWidth: 380, background: backgroundColor(theme, 300), ownerContainer: { From bf0a04ab50f8a11071e67ddb0d2db4085d91d6cc Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 11 Oct 2022 11:01:38 +0200 Subject: [PATCH 098/112] Dismiss popover when contact finder is unfocused --- crates/collab_ui/src/contacts_popover.rs | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/crates/collab_ui/src/contacts_popover.rs b/crates/collab_ui/src/contacts_popover.rs index b9b1d254b6ce10c98792b965c24b55749534f223..0758cf735d786665d891323ac426c283974e1af6 100644 --- a/crates/collab_ui/src/contacts_popover.rs +++ b/crates/collab_ui/src/contacts_popover.rs @@ -27,7 +27,6 @@ pub struct ContactsPopover { project: ModelHandle, user_store: ModelHandle, _subscription: Option, - _window_subscription: gpui::Subscription, } impl ContactsPopover { @@ -43,18 +42,11 @@ impl ContactsPopover { project, user_store, _subscription: None, - _window_subscription: cx.observe_window_activation(Self::window_activation_changed), }; this.show_contact_list(cx); this } - fn window_activation_changed(&mut self, active: bool, cx: &mut ViewContext) { - if !active { - cx.emit(Event::Dismissed); - } - } - fn toggle_contact_finder(&mut self, _: &ToggleContactFinder, cx: &mut ViewContext) { match &self.child { Child::ContactList(_) => self.show_contact_finder(cx), @@ -65,8 +57,8 @@ impl ContactsPopover { fn show_contact_finder(&mut self, cx: &mut ViewContext) { let child = cx.add_view(|cx| ContactFinder::new(self.user_store.clone(), cx)); cx.focus(&child); - self._subscription = Some(cx.subscribe(&child, |this, _, event, cx| match event { - crate::contact_finder::Event::Dismissed => this.show_contact_list(cx), + self._subscription = Some(cx.subscribe(&child, |_, _, event, cx| match event { + crate::contact_finder::Event::Dismissed => cx.emit(Event::Dismissed), })); self.child = Child::ContactFinder(child); cx.notify(); From 9ec62d4c1fdb7636a4f0b61aafb4fae44e6cf1dd Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 11 Oct 2022 11:03:49 +0200 Subject: [PATCH 099/112] Foreground app when accepting calls and project shares --- crates/collab_ui/src/collab_ui.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/collab_ui/src/collab_ui.rs b/crates/collab_ui/src/collab_ui.rs index 72595c2338efb7c403df422959e7a3deb9694972..4b7e3dae01b7a02e5187386a2ce9c6ed2c051f1f 100644 --- a/crates/collab_ui/src/collab_ui.rs +++ b/crates/collab_ui/src/collab_ui.rs @@ -59,6 +59,7 @@ pub fn init(app_state: Arc, cx: &mut MutableAppContext) { }; cx.activate_window(workspace.window_id()); + cx.platform().activate(true); workspace.update(&mut cx, |workspace, cx| { if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() { From 1d4bdfc4a18ba8073ba5bffb94cad1b4942eb95d Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 11 Oct 2022 11:28:27 +0200 Subject: [PATCH 100/112] Cancel calls automatically when caller hangs up or disconnects --- crates/collab/src/integration_tests.rs | 34 ++++++++++++ crates/collab/src/rpc.rs | 40 +++++++++----- crates/collab/src/rpc/store.rs | 76 +++++++++++++------------- 3 files changed, 97 insertions(+), 53 deletions(-) diff --git a/crates/collab/src/integration_tests.rs b/crates/collab/src/integration_tests.rs index ce9d01e3d314cc95119977fed7229046ff5e0a28..42c95c4b2e5090d73f103023f7dd00eb4c7343d8 100644 --- a/crates/collab/src/integration_tests.rs +++ b/crates/collab/src/integration_tests.rs @@ -492,6 +492,40 @@ async fn test_calls_on_multiple_connections( deterministic.run_until_parked(); assert!(incoming_call_b1.next().await.unwrap().is_none()); assert!(incoming_call_b2.next().await.unwrap().is_none()); + + // User A calls user B again. + active_call_a + .update(cx_a, |call, cx| { + call.invite(client_b1.user_id().unwrap(), None, cx) + }) + .await + .unwrap(); + deterministic.run_until_parked(); + assert!(incoming_call_b1.next().await.unwrap().is_some()); + assert!(incoming_call_b2.next().await.unwrap().is_some()); + + // User A hangs up, causing both connections to stop ringing. + active_call_a.update(cx_a, |call, cx| call.hang_up(cx).unwrap()); + deterministic.run_until_parked(); + assert!(incoming_call_b1.next().await.unwrap().is_none()); + assert!(incoming_call_b2.next().await.unwrap().is_none()); + + // User A calls user B again. + active_call_a + .update(cx_a, |call, cx| { + call.invite(client_b1.user_id().unwrap(), None, cx) + }) + .await + .unwrap(); + deterministic.run_until_parked(); + assert!(incoming_call_b1.next().await.unwrap().is_some()); + assert!(incoming_call_b2.next().await.unwrap().is_some()); + + // User A disconnects up, causing both connections to stop ringing. + server.disconnect_client(client_a.current_user_id(cx_a)); + cx_a.foreground().advance_clock(rpc::RECEIVE_TIMEOUT); + assert!(incoming_call_b1.next().await.unwrap().is_none()); + assert!(incoming_call_b2.next().await.unwrap().is_none()); } #[gpui::test(iterations = 10)] diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 11219fb8b84975ecfc65f342cd9e1e9c9daf2133..febdfe4434d0b02539c56d49a3f00ebf2ec119b7 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -479,30 +479,34 @@ impl Server { let mut store = self.store().await; let removed_connection = store.remove_connection(connection_id)?; - for (project_id, project) in removed_connection.hosted_projects { - projects_to_unshare.push(project_id); + for project in removed_connection.hosted_projects { + projects_to_unshare.push(project.id); broadcast(connection_id, project.guests.keys().copied(), |conn_id| { self.peer.send( conn_id, proto::UnshareProject { - project_id: project_id.to_proto(), + project_id: project.id.to_proto(), }, ) }); } - for project_id in removed_connection.guest_project_ids { - if let Some(project) = store.project(project_id).trace_err() { - broadcast(connection_id, project.connection_ids(), |conn_id| { - self.peer.send( - conn_id, - proto::RemoveProjectCollaborator { - project_id: project_id.to_proto(), - peer_id: connection_id.0, - }, - ) - }); - } + for project in removed_connection.guest_projects { + broadcast(connection_id, project.connection_ids, |conn_id| { + self.peer.send( + conn_id, + proto::RemoveProjectCollaborator { + project_id: project.id.to_proto(), + peer_id: connection_id.0, + }, + ) + }); + } + + for connection_id in removed_connection.canceled_call_connection_ids { + self.peer + .send(connection_id, proto::CallCanceled {}) + .trace_err(); } if let Some(room) = removed_connection @@ -666,6 +670,12 @@ impl Server { } } + for connection_id in left_room.canceled_call_connection_ids { + self.peer + .send(connection_id, proto::CallCanceled {}) + .trace_err(); + } + if let Some(room) = left_room.room { self.room_updated(room); } diff --git a/crates/collab/src/rpc/store.rs b/crates/collab/src/rpc/store.rs index 12f50db48e47795ac49cfd905aaec7f1b43afad9..9b2661abca8324e936e10bc3a449dca09a4e0341 100644 --- a/crates/collab/src/rpc/store.rs +++ b/crates/collab/src/rpc/store.rs @@ -86,10 +86,11 @@ pub type ReplicaId = u16; #[derive(Default)] pub struct RemovedConnectionState { pub user_id: UserId, - pub hosted_projects: HashMap, - pub guest_project_ids: HashSet, + pub hosted_projects: Vec, + pub guest_projects: Vec, pub contact_ids: HashSet, pub room_id: Option, + pub canceled_call_connection_ids: Vec, } pub struct LeftProject { @@ -104,6 +105,7 @@ pub struct LeftRoom<'a> { pub room: Option<&'a proto::Room>, pub unshared_projects: Vec, pub left_projects: Vec, + pub canceled_call_connection_ids: Vec, } #[derive(Copy, Clone)] @@ -197,7 +199,6 @@ impl Store { .ok_or_else(|| anyhow!("no such connection"))?; let user_id = connection.user_id; - let connection_projects = mem::take(&mut connection.projects); let connection_channels = mem::take(&mut connection.channels); let mut result = RemovedConnectionState { @@ -210,48 +211,21 @@ impl Store { self.leave_channel(connection_id, channel_id); } - // Unshare and leave all projects. - for project_id in connection_projects { - if let Ok((_, project)) = self.unshare_project(project_id, connection_id) { - result.hosted_projects.insert(project_id, project); - } else if self.leave_project(project_id, connection_id).is_ok() { - result.guest_project_ids.insert(project_id); - } - } - - let connected_user = self.connected_users.get_mut(&user_id).unwrap(); - connected_user.connection_ids.remove(&connection_id); + let connected_user = self.connected_users.get(&user_id).unwrap(); if let Some(active_call) = connected_user.active_call.as_ref() { let room_id = active_call.room_id; - if let Some(room) = self.rooms.get_mut(&room_id) { - let prev_participant_count = room.participants.len(); - room.participants - .retain(|participant| participant.peer_id != connection_id.0); - if prev_participant_count == room.participants.len() { - if connected_user.connection_ids.is_empty() { - room.pending_participant_user_ids - .retain(|pending_user_id| *pending_user_id != user_id.to_proto()); - result.room_id = Some(room_id); - connected_user.active_call = None; - } - } else { - result.room_id = Some(room_id); - connected_user.active_call = None; - } - - if room.participants.is_empty() && room.pending_participant_user_ids.is_empty() { - self.rooms.remove(&room_id); - } - } else { - tracing::error!("disconnected user claims to be in a room that does not exist"); - connected_user.active_call = None; - } + let left_room = self.leave_room(room_id, connection_id)?; + result.hosted_projects = left_room.unshared_projects; + result.guest_projects = left_room.left_projects; + result.room_id = Some(room_id); + result.canceled_call_connection_ids = left_room.canceled_call_connection_ids; } + let connected_user = self.connected_users.get_mut(&user_id).unwrap(); + connected_user.connection_ids.remove(&connection_id); if connected_user.connection_ids.is_empty() { self.connected_users.remove(&user_id); } - self.connections.remove(&connection_id).unwrap(); Ok(result) @@ -491,6 +465,31 @@ impl Store { .ok_or_else(|| anyhow!("no such room"))?; room.participants .retain(|participant| participant.peer_id != connection_id.0); + + let mut canceled_call_connection_ids = Vec::new(); + room.pending_participant_user_ids + .retain(|pending_participant_user_id| { + if let Some(connected_user) = self + .connected_users + .get_mut(&UserId::from_proto(*pending_participant_user_id)) + { + if let Some(call) = connected_user.active_call.as_ref() { + if call.caller_user_id == user_id { + connected_user.active_call.take(); + canceled_call_connection_ids + .extend(connected_user.connection_ids.iter().copied()); + false + } else { + true + } + } else { + true + } + } else { + true + } + }); + if room.participants.is_empty() && room.pending_participant_user_ids.is_empty() { self.rooms.remove(&room_id); } @@ -499,6 +498,7 @@ impl Store { room: self.rooms.get(&room_id), unshared_projects, left_projects, + canceled_call_connection_ids, }) } From 0a306808dacbe65672e243d5474128d561fda97a Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 11 Oct 2022 11:44:31 +0200 Subject: [PATCH 101/112] Dismiss project shared notifications when a project was unshared --- crates/call/src/room.rs | 32 +++++++++++++++++-- .../src/project_shared_notification.rs | 15 ++++++++- 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/crates/call/src/room.rs b/crates/call/src/room.rs index fb9ba09d814cda6cfaa551ce3fe926fb802ad7c4..ebe2d4284be1e4caf60ae3da62a025d2688f12b0 100644 --- a/crates/call/src/room.rs +++ b/crates/call/src/room.rs @@ -18,6 +18,10 @@ pub enum Event { project_id: u64, worktree_root_names: Vec, }, + RemoteProjectUnshared { + project_id: u64, + }, + Left, } pub struct Room { @@ -148,6 +152,7 @@ impl Room { } cx.notify(); + cx.emit(Event::Left); self.status = RoomStatus::Offline; self.remote_participants.clear(); self.pending_participants.clear(); @@ -223,15 +228,21 @@ impl Room { let peer_id = PeerId(participant.peer_id); this.participant_user_ids.insert(participant.user_id); - let existing_projects = this + let old_projects = this .remote_participants .get(&peer_id) .into_iter() .flat_map(|existing| &existing.projects) .map(|project| project.id) .collect::>(); + let new_projects = participant + .projects + .iter() + .map(|project| project.id) + .collect::>(); + for project in &participant.projects { - if !existing_projects.contains(&project.id) { + if !old_projects.contains(&project.id) { cx.emit(Event::RemoteProjectShared { owner: user.clone(), project_id: project.id, @@ -240,6 +251,12 @@ impl Room { } } + for unshared_project_id in old_projects.difference(&new_projects) { + cx.emit(Event::RemoteProjectUnshared { + project_id: *unshared_project_id, + }); + } + this.remote_participants.insert( peer_id, RemoteParticipant { @@ -252,7 +269,16 @@ impl Room { } this.remote_participants.retain(|_, participant| { - this.participant_user_ids.contains(&participant.user.id) + if this.participant_user_ids.contains(&participant.user.id) { + true + } else { + for project in &participant.projects { + cx.emit(Event::RemoteProjectUnshared { + project_id: project.id, + }); + } + false + } }); cx.notify(); diff --git a/crates/collab_ui/src/project_shared_notification.rs b/crates/collab_ui/src/project_shared_notification.rs index 22383feb20fc3da614654a078b34d2f32e11af9f..a17e11b079f42c2bb863b0dee8cc8f734fcc0aca 100644 --- a/crates/collab_ui/src/project_shared_notification.rs +++ b/crates/collab_ui/src/project_shared_notification.rs @@ -1,5 +1,6 @@ use call::{room, ActiveCall}; use client::User; +use collections::HashMap; use gpui::{ actions, elements::*, @@ -18,6 +19,7 @@ pub fn init(cx: &mut MutableAppContext) { cx.add_action(ProjectSharedNotification::dismiss); let active_call = ActiveCall::global(cx); + let mut notification_windows = HashMap::default(); cx.subscribe(&active_call, move |_, event, cx| match event { room::Event::RemoteProjectShared { owner, @@ -29,7 +31,7 @@ pub fn init(cx: &mut MutableAppContext) { let theme = &cx.global::().theme.project_shared_notification; let window_size = vec2f(theme.window_width, theme.window_height); - cx.add_window( + let (window_id, _) = cx.add_window( WindowOptions { bounds: WindowBounds::Fixed(RectF::new( vec2f(screen_size.x() - window_size.x() - PADDING, PADDING), @@ -48,6 +50,17 @@ pub fn init(cx: &mut MutableAppContext) { ) }, ); + notification_windows.insert(*project_id, window_id); + } + room::Event::RemoteProjectUnshared { project_id } => { + if let Some(window_id) = notification_windows.remove(&project_id) { + cx.remove_window(window_id); + } + } + room::Event::Left => { + for (_, window_id) in notification_windows.drain() { + cx.remove_window(window_id); + } } }) .detach(); From 8e7f96cebc027960ed92a3e7051401d77732415a Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 11 Oct 2022 11:54:32 +0200 Subject: [PATCH 102/112] Update contacts when automatically canceling calls --- crates/collab/src/integration_tests.rs | 72 ++++++++++++++++++++++++++ crates/collab/src/rpc.rs | 28 ++++++---- 2 files changed, 90 insertions(+), 10 deletions(-) diff --git a/crates/collab/src/integration_tests.rs b/crates/collab/src/integration_tests.rs index 42c95c4b2e5090d73f103023f7dd00eb4c7343d8..27fb1a9bfbcb48723732f953ed43e32e561396b7 100644 --- a/crates/collab/src/integration_tests.rs +++ b/crates/collab/src/integration_tests.rs @@ -4549,6 +4549,78 @@ async fn test_contacts( ] ); + active_call_a.update(cx_a, |call, cx| call.hang_up(cx).unwrap()); + deterministic.run_until_parked(); + assert_eq!( + contacts(&client_a, cx_a), + [ + ("user_b".to_string(), "online", "free"), + ("user_c".to_string(), "online", "free") + ] + ); + assert_eq!( + contacts(&client_b, cx_b), + [ + ("user_a".to_string(), "online", "free"), + ("user_c".to_string(), "online", "free") + ] + ); + assert_eq!( + contacts(&client_c, cx_c), + [ + ("user_a".to_string(), "online", "free"), + ("user_b".to_string(), "online", "free") + ] + ); + + active_call_a + .update(cx_a, |call, cx| { + call.invite(client_b.user_id().unwrap(), None, cx) + }) + .await + .unwrap(); + deterministic.run_until_parked(); + assert_eq!( + contacts(&client_a, cx_a), + [ + ("user_b".to_string(), "online", "busy"), + ("user_c".to_string(), "online", "free") + ] + ); + assert_eq!( + contacts(&client_b, cx_b), + [ + ("user_a".to_string(), "online", "busy"), + ("user_c".to_string(), "online", "free") + ] + ); + assert_eq!( + contacts(&client_c, cx_c), + [ + ("user_a".to_string(), "online", "busy"), + ("user_b".to_string(), "online", "busy") + ] + ); + + server.forbid_connections(); + server.disconnect_client(client_a.current_user_id(cx_a)); + deterministic.advance_clock(rpc::RECEIVE_TIMEOUT); + assert_eq!(contacts(&client_a, cx_a), []); + assert_eq!( + contacts(&client_b, cx_b), + [ + ("user_a".to_string(), "offline", "free"), + ("user_c".to_string(), "online", "free") + ] + ); + assert_eq!( + contacts(&client_c, cx_c), + [ + ("user_a".to_string(), "offline", "free"), + ("user_b".to_string(), "online", "free") + ] + ); + #[allow(clippy::type_complexity)] fn contacts( client: &TestClient, diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index febdfe4434d0b02539c56d49a3f00ebf2ec119b7..84449e79d5a2e134469bce2b656fa0446aeaf03c 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -22,7 +22,7 @@ use axum::{ routing::get, Extension, Router, TypedHeader, }; -use collections::HashMap; +use collections::{HashMap, HashSet}; use futures::{ channel::mpsc, future::{self, BoxFuture}, @@ -474,7 +474,7 @@ impl Server { self.peer.disconnect(connection_id); let mut projects_to_unshare = Vec::new(); - let removed_user_id; + let mut contacts_to_update = HashSet::default(); { let mut store = self.store().await; let removed_connection = store.remove_connection(connection_id)?; @@ -507,6 +507,7 @@ impl Server { self.peer .send(connection_id, proto::CallCanceled {}) .trace_err(); + contacts_to_update.extend(store.user_id_for_connection(connection_id).ok()); } if let Some(room) = removed_connection @@ -516,10 +517,12 @@ impl Server { self.room_updated(room); } - removed_user_id = removed_connection.user_id; + contacts_to_update.insert(removed_connection.user_id); }; - self.update_user_contacts(removed_user_id).await.trace_err(); + for user_id in contacts_to_update { + self.update_user_contacts(user_id).await.trace_err(); + } for project_id in projects_to_unshare { self.app_state @@ -632,11 +635,12 @@ impl Server { } async fn leave_room(self: Arc, message: TypedEnvelope) -> Result<()> { - let user_id; + let mut contacts_to_update = HashSet::default(); { let mut store = self.store().await; - user_id = store.user_id_for_connection(message.sender_id)?; + let user_id = store.user_id_for_connection(message.sender_id)?; let left_room = store.leave_room(message.payload.id, message.sender_id)?; + contacts_to_update.insert(user_id); for project in left_room.unshared_projects { for connection_id in project.connection_ids() { @@ -670,17 +674,21 @@ impl Server { } } + if let Some(room) = left_room.room { + self.room_updated(room); + } + for connection_id in left_room.canceled_call_connection_ids { self.peer .send(connection_id, proto::CallCanceled {}) .trace_err(); + contacts_to_update.extend(store.user_id_for_connection(connection_id).ok()); } + } - if let Some(room) = left_room.room { - self.room_updated(room); - } + for user_id in contacts_to_update { + self.update_user_contacts(user_id).await?; } - self.update_user_contacts(user_id).await?; Ok(()) } From feb17c29ec1e9fd041918ddff34c6f8c4b95c3d9 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 11 Oct 2022 12:23:15 +0200 Subject: [PATCH 103/112] Show participant projects in contacts popover --- crates/call/src/participant.rs | 5 + crates/call/src/room.rs | 25 +++- crates/collab_ui/src/contact_list.rs | 202 +++++++++++++++++++++++++-- crates/theme/src/theme.rs | 15 ++ styles/src/styleTree/contactList.ts | 51 ++++++- 5 files changed, 279 insertions(+), 19 deletions(-) diff --git a/crates/call/src/participant.rs b/crates/call/src/participant.rs index b124d920a3937db910f384bf057fe475231d433d..7e031f907ff26710fcd243c64330931306920932 100644 --- a/crates/call/src/participant.rs +++ b/crates/call/src/participant.rs @@ -20,6 +20,11 @@ impl ParticipantLocation { } } +#[derive(Clone, Debug, Default)] +pub struct LocalParticipant { + pub projects: Vec, +} + #[derive(Clone, Debug)] pub struct RemoteParticipant { pub user: Arc, diff --git a/crates/call/src/room.rs b/crates/call/src/room.rs index ebe2d4284be1e4caf60ae3da62a025d2688f12b0..a3b49c2a4f8090a383e14c2673dbe51e5af7f6ff 100644 --- a/crates/call/src/room.rs +++ b/crates/call/src/room.rs @@ -1,5 +1,5 @@ use crate::{ - participant::{ParticipantLocation, RemoteParticipant}, + participant::{LocalParticipant, ParticipantLocation, RemoteParticipant}, IncomingCall, }; use anyhow::{anyhow, Result}; @@ -27,6 +27,7 @@ pub enum Event { pub struct Room { id: u64, status: RoomStatus, + local_participant: LocalParticipant, remote_participants: BTreeMap, pending_participants: Vec>, participant_user_ids: HashSet, @@ -72,6 +73,7 @@ impl Room { id, status: RoomStatus::Online, participant_user_ids: Default::default(), + local_participant: Default::default(), remote_participants: Default::default(), pending_participants: Default::default(), pending_call_count: 0, @@ -170,6 +172,10 @@ impl Room { self.status } + pub fn local_participant(&self) -> &LocalParticipant { + &self.local_participant + } + pub fn remote_participants(&self) -> &BTreeMap { &self.remote_participants } @@ -201,8 +207,11 @@ impl Room { cx: &mut ModelContext, ) -> Result<()> { // Filter ourselves out from the room's participants. - room.participants - .retain(|participant| Some(participant.user_id) != self.client.user_id()); + let local_participant_ix = room + .participants + .iter() + .position(|participant| Some(participant.user_id) == self.client.user_id()); + let local_participant = local_participant_ix.map(|ix| room.participants.swap_remove(ix)); let remote_participant_user_ids = room .participants @@ -223,6 +232,12 @@ impl Room { this.update(&mut cx, |this, cx| { this.participant_user_ids.clear(); + if let Some(participant) = local_participant { + this.local_participant.projects = participant.projects; + } else { + this.local_participant.projects.clear(); + } + if let Some(participants) = remote_participants.log_err() { for (participant, user) in room.participants.into_iter().zip(participants) { let peer_id = PeerId(participant.peer_id); @@ -280,8 +295,6 @@ impl Room { false } }); - - cx.notify(); } if let Some(pending_participants) = pending_participants.log_err() { @@ -289,7 +302,6 @@ impl Room { for participant in &this.pending_participants { this.participant_user_ids.insert(participant.id); } - cx.notify(); } this.pending_room_update.take(); @@ -298,6 +310,7 @@ impl Room { } this.check_invariants(); + cx.notify(); }); })); diff --git a/crates/collab_ui/src/contact_list.rs b/crates/collab_ui/src/contact_list.rs index a539f8ffac9e0a3e88b30562b46d45ed5b521b87..357b3c65e0a4ca735cc0fcf2317b303b5c16479a 100644 --- a/crates/collab_ui/src/contact_list.rs +++ b/crates/collab_ui/src/contact_list.rs @@ -6,9 +6,11 @@ use client::{Contact, PeerId, User, UserStore}; use editor::{Cancel, Editor}; use fuzzy::{match_strings, StringMatchCandidate}; use gpui::{ - elements::*, impl_actions, impl_internal_actions, keymap, AppContext, ClipboardItem, - CursorStyle, Entity, ModelHandle, MouseButton, MutableAppContext, RenderContext, Subscription, - View, ViewContext, ViewHandle, + elements::*, + geometry::{rect::RectF, vector::vec2f}, + impl_actions, impl_internal_actions, keymap, AppContext, ClipboardItem, CursorStyle, Entity, + ModelHandle, MouseButton, MutableAppContext, RenderContext, Subscription, View, ViewContext, + ViewHandle, }; use menu::{Confirm, SelectNext, SelectPrev}; use project::Project; @@ -16,6 +18,7 @@ use serde::Deserialize; use settings::Settings; use theme::IconButton; use util::ResultExt; +use workspace::JoinProject; impl_actions!(contact_list, [RemoveContact, RespondToContactRequest]); impl_internal_actions!(contact_list, [ToggleExpanded, Call, LeaveCall]); @@ -55,7 +58,17 @@ enum Section { #[derive(Clone)] enum ContactEntry { Header(Section), - CallParticipant { user: Arc, is_pending: bool }, + CallParticipant { + user: Arc, + is_pending: bool, + }, + ParticipantProject { + project_id: u64, + worktree_root_names: Vec, + host_user_id: u64, + is_host: bool, + is_last: bool, + }, IncomingRequest(Arc), OutgoingRequest(Arc), Contact(Arc), @@ -74,6 +87,18 @@ impl PartialEq for ContactEntry { return user_1.id == user_2.id; } } + ContactEntry::ParticipantProject { + project_id: project_id_1, + .. + } => { + if let ContactEntry::ParticipantProject { + project_id: project_id_2, + .. + } = other + { + return project_id_1 == project_id_2; + } + } ContactEntry::IncomingRequest(user_1) => { if let ContactEntry::IncomingRequest(user_2) = other { return user_1.id == user_2.id; @@ -177,6 +202,22 @@ impl ContactList { &theme.contact_list, ) } + ContactEntry::ParticipantProject { + project_id, + worktree_root_names, + host_user_id, + is_host, + is_last, + } => Self::render_participant_project( + *project_id, + worktree_root_names, + *host_user_id, + *is_host, + *is_last, + is_selected, + &theme.contact_list, + cx, + ), ContactEntry::IncomingRequest(user) => Self::render_contact_request( user.clone(), this.user_store.clone(), @@ -298,6 +339,19 @@ impl ContactList { ); } } + ContactEntry::ParticipantProject { + project_id, + host_user_id, + is_host, + .. + } => { + if !is_host { + cx.dispatch_global_action(JoinProject { + project_id: *project_id, + follow_user_id: *host_user_id, + }); + } + } _ => {} } } @@ -324,7 +378,7 @@ impl ContactList { if let Some(room) = ActiveCall::global(cx).read(cx).room() { let room = room.read(cx); - let mut call_participants = Vec::new(); + let mut participant_entries = Vec::new(); // Populate the active user. if let Some(user) = user_store.current_user() { @@ -343,10 +397,21 @@ impl ContactList { executor.clone(), )); if !matches.is_empty() { - call_participants.push(ContactEntry::CallParticipant { + let user_id = user.id; + participant_entries.push(ContactEntry::CallParticipant { user, is_pending: false, }); + let mut projects = room.local_participant().projects.iter().peekable(); + while let Some(project) = projects.next() { + participant_entries.push(ContactEntry::ParticipantProject { + project_id: project.id, + worktree_root_names: project.worktree_root_names.clone(), + host_user_id: user_id, + is_host: true, + is_last: projects.peek().is_none(), + }); + } } } @@ -370,14 +435,25 @@ impl ContactList { &Default::default(), executor.clone(), )); - call_participants.extend(matches.iter().map(|mat| { - ContactEntry::CallParticipant { + for mat in matches { + let participant = &room.remote_participants()[&PeerId(mat.candidate_id as u32)]; + participant_entries.push(ContactEntry::CallParticipant { user: room.remote_participants()[&PeerId(mat.candidate_id as u32)] .user .clone(), is_pending: false, + }); + let mut projects = participant.projects.iter().peekable(); + while let Some(project) = projects.next() { + participant_entries.push(ContactEntry::ParticipantProject { + project_id: project.id, + worktree_root_names: project.worktree_root_names.clone(), + host_user_id: participant.user.id, + is_host: false, + is_last: projects.peek().is_none(), + }); } - })); + } // Populate pending participants. self.match_candidates.clear(); @@ -400,15 +476,15 @@ impl ContactList { &Default::default(), executor.clone(), )); - call_participants.extend(matches.iter().map(|mat| ContactEntry::CallParticipant { + participant_entries.extend(matches.iter().map(|mat| ContactEntry::CallParticipant { user: room.pending_participants()[mat.candidate_id].clone(), is_pending: true, })); - if !call_participants.is_empty() { + if !participant_entries.is_empty() { self.entries.push(ContactEntry::Header(Section::ActiveCall)); if !self.collapsed_sections.contains(&Section::ActiveCall) { - self.entries.extend(call_participants); + self.entries.extend(participant_entries); } } } @@ -588,6 +664,108 @@ impl ContactList { .boxed() } + fn render_participant_project( + project_id: u64, + worktree_root_names: &[String], + host_user_id: u64, + is_host: bool, + is_last: bool, + is_selected: bool, + theme: &theme::ContactList, + cx: &mut RenderContext, + ) -> ElementBox { + let font_cache = cx.font_cache(); + let host_avatar_height = theme + .contact_avatar + .width + .or(theme.contact_avatar.height) + .unwrap_or(0.); + let row = &theme.project_row.default; + let tree_branch = theme.tree_branch; + let line_height = row.name.text.line_height(font_cache); + let cap_height = row.name.text.cap_height(font_cache); + let baseline_offset = + row.name.text.baseline_offset(font_cache) + (theme.row_height - line_height) / 2.; + let project_name = if worktree_root_names.is_empty() { + "untitled".to_string() + } else { + worktree_root_names.join(", ") + }; + + MouseEventHandler::::new(project_id as usize, cx, |mouse_state, _| { + let tree_branch = *tree_branch.style_for(mouse_state, is_selected); + let row = theme.project_row.style_for(mouse_state, is_selected); + + Flex::row() + .with_child( + Stack::new() + .with_child( + Canvas::new(move |bounds, _, cx| { + let start_x = bounds.min_x() + (bounds.width() / 2.) + - (tree_branch.width / 2.); + let end_x = bounds.max_x(); + let start_y = bounds.min_y(); + let end_y = bounds.min_y() + baseline_offset - (cap_height / 2.); + + cx.scene.push_quad(gpui::Quad { + bounds: RectF::from_points( + vec2f(start_x, start_y), + vec2f( + start_x + tree_branch.width, + if is_last { end_y } else { bounds.max_y() }, + ), + ), + background: Some(tree_branch.color), + border: gpui::Border::default(), + corner_radius: 0., + }); + cx.scene.push_quad(gpui::Quad { + bounds: RectF::from_points( + vec2f(start_x, end_y), + vec2f(end_x, end_y + tree_branch.width), + ), + background: Some(tree_branch.color), + border: gpui::Border::default(), + corner_radius: 0., + }); + }) + .boxed(), + ) + .constrained() + .with_width(host_avatar_height) + .boxed(), + ) + .with_child( + Label::new(project_name, row.name.text.clone()) + .aligned() + .left() + .contained() + .with_style(row.name.container) + .flex(1., false) + .boxed(), + ) + .constrained() + .with_height(theme.row_height) + .contained() + .with_style(row.container) + .boxed() + }) + .with_cursor_style(if !is_host { + CursorStyle::PointingHand + } else { + CursorStyle::Arrow + }) + .on_click(MouseButton::Left, move |_, cx| { + if !is_host { + cx.dispatch_global_action(JoinProject { + project_id, + follow_user_id: host_user_id, + }); + } + }) + .boxed() + } + fn render_header( section: Section, theme: &theme::ContactList, diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 9a836864bff66377f7806e0d6f9a512c9c8ab2f5..503645d6bc453b7d8406aa884733b2a7dfac2841 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -100,6 +100,8 @@ pub struct ContactList { pub leave_call: Interactive, pub contact_row: Interactive, pub row_height: f32, + pub project_row: Interactive, + pub tree_branch: Interactive, pub contact_avatar: ImageStyle, pub contact_status_free: ContainerStyle, pub contact_status_busy: ContainerStyle, @@ -112,6 +114,19 @@ pub struct ContactList { pub calling_indicator: ContainedText, } +#[derive(Deserialize, Default)] +pub struct ProjectRow { + #[serde(flatten)] + pub container: ContainerStyle, + pub name: ContainedText, +} + +#[derive(Deserialize, Default, Clone, Copy)] +pub struct TreeBranch { + pub width: f32, + pub color: Color, +} + #[derive(Deserialize, Default)] pub struct ContactFinder { pub picker: Picker, diff --git a/styles/src/styleTree/contactList.ts b/styles/src/styleTree/contactList.ts index 8b8b6024cfed9f88db9c32e7095369f36bd4ac36..633d6341967bb2c46ba308dfc3df44f984f785e3 100644 --- a/styles/src/styleTree/contactList.ts +++ b/styles/src/styleTree/contactList.ts @@ -12,6 +12,31 @@ export default function contactList(theme: Theme) { buttonWidth: 16, cornerRadius: 8, }; + const projectRow = { + guestAvatarSpacing: 4, + height: 24, + guestAvatar: { + cornerRadius: 8, + width: 14, + }, + name: { + ...text(theme, "mono", "placeholder", { size: "sm" }), + margin: { + left: nameMargin, + right: 6, + }, + }, + guests: { + margin: { + left: nameMargin, + right: nameMargin, + }, + }, + padding: { + left: sidePadding, + right: sidePadding, + }, + }; return { userQueryEditor: { @@ -129,6 +154,30 @@ export default function contactList(theme: Theme) { }, callingIndicator: { ...text(theme, "mono", "muted", { size: "xs" }) - } + }, + treeBranch: { + color: borderColor(theme, "active"), + width: 1, + hover: { + color: borderColor(theme, "active"), + }, + active: { + color: borderColor(theme, "active"), + }, + }, + projectRow: { + ...projectRow, + background: backgroundColor(theme, 300), + name: { + ...projectRow.name, + ...text(theme, "mono", "secondary", { size: "sm" }), + }, + hover: { + background: backgroundColor(theme, 300, "hovered"), + }, + active: { + background: backgroundColor(theme, 300, "active"), + }, + }, } } From 29c3b81a0a6d8fd1994d827ed979a5ee5f83809d Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 11 Oct 2022 14:52:47 +0200 Subject: [PATCH 104/112] Show prompt when closing last window while there's an active call Co-Authored-By: Nathan Sobo --- crates/gpui/src/app.rs | 10 ++++ crates/workspace/src/workspace.rs | 46 ++++++++++++++++--- crates/zed/src/zed.rs | 4 +- .../src/styleTree/incomingCallNotification.ts | 2 +- 4 files changed, 54 insertions(+), 8 deletions(-) diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index f55dd2b464f56e6d5f8ba35fd3996332de86563f..765501368c229535b689a2baacdad965701b6eb3 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -794,6 +794,16 @@ impl AsyncAppContext { self.update(|cx| cx.activate_window(window_id)) } + pub fn prompt( + &mut self, + window_id: usize, + level: PromptLevel, + msg: &str, + answers: &[&str], + ) -> oneshot::Receiver { + self.update(|cx| cx.prompt(window_id, level, msg, answers)) + } + pub fn platform(&self) -> Arc { self.0.borrow().platform() } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index db9ac3a0c6edb1e27776256e3129cd68a0d490cd..6472a6f6971d8229d6c2b36f613fa2b8d4d8db10 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -17,7 +17,7 @@ use client::{proto, Client, PeerId, TypedEnvelope, UserStore}; use collections::{hash_map, HashMap, HashSet}; use dock::{DefaultItemFactory, Dock, ToggleDockButton}; use drag_and_drop::DragAndDrop; -use futures::{channel::oneshot, FutureExt}; +use futures::{channel::oneshot, FutureExt, StreamExt}; use gpui::{ actions, elements::*, @@ -1231,7 +1231,7 @@ impl Workspace { _: &CloseWindow, cx: &mut ViewContext, ) -> Option>> { - let prepare = self.prepare_to_close(cx); + let prepare = self.prepare_to_close(false, cx); Some(cx.spawn(|this, mut cx| async move { if prepare.await? { this.update(&mut cx, |_, cx| { @@ -1243,8 +1243,42 @@ impl Workspace { })) } - pub fn prepare_to_close(&mut self, cx: &mut ViewContext) -> Task> { - self.save_all_internal(true, cx) + pub fn prepare_to_close( + &mut self, + quitting: bool, + cx: &mut ViewContext, + ) -> Task> { + let active_call = ActiveCall::global(cx); + let window_id = cx.window_id(); + let workspace_count = cx + .window_ids() + .flat_map(|window_id| cx.root_view::(window_id)) + .count(); + cx.spawn(|this, mut cx| async move { + if !quitting + && workspace_count == 1 + && active_call.read_with(&cx, |call, _| call.room().is_some()) + { + let answer = cx + .prompt( + window_id, + PromptLevel::Warning, + "Do you want to leave the current call?", + &["Close window and hang up", "Cancel"], + ) + .next() + .await; + if answer == Some(1) { + return anyhow::Ok(false); + } else { + active_call.update(&mut cx, |call, cx| call.hang_up(cx))?; + } + } + + Ok(this + .update(&mut cx, |this, cx| this.save_all_internal(true, cx)) + .await?) + }) } fn save_all(&mut self, _: &SaveAll, cx: &mut ViewContext) -> Option>> { @@ -2944,7 +2978,7 @@ mod tests { // When there are no dirty items, there's nothing to do. let item1 = cx.add_view(&workspace, |_| TestItem::new()); workspace.update(cx, |w, cx| w.add_item(Box::new(item1.clone()), cx)); - let task = workspace.update(cx, |w, cx| w.prepare_to_close(cx)); + let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx)); assert!(task.await.unwrap()); // When there are dirty untitled items, prompt to save each one. If the user @@ -2964,7 +2998,7 @@ mod tests { w.add_item(Box::new(item2.clone()), cx); w.add_item(Box::new(item3.clone()), cx); }); - let task = workspace.update(cx, |w, cx| w.prepare_to_close(cx)); + let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx)); cx.foreground().run_until_parked(); cx.simulate_prompt_answer(window_id, 2 /* cancel */); cx.foreground().run_until_parked(); diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index be3d0cfb8877a340f1af9f4d814e60548c5130e4..d032e661d75aa2dcf2a12f92dcfe155dbe2402e7 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -344,7 +344,9 @@ fn quit(_: &Quit, cx: &mut gpui::MutableAppContext) { // If the user cancels any save prompt, then keep the app open. for workspace in workspaces { if !workspace - .update(&mut cx, |workspace, cx| workspace.prepare_to_close(cx)) + .update(&mut cx, |workspace, cx| { + workspace.prepare_to_close(true, cx) + }) .await? { return Ok(()); diff --git a/styles/src/styleTree/incomingCallNotification.ts b/styles/src/styleTree/incomingCallNotification.ts index 30b8ae90cad05c3cdb5fd08af2b4554f58694039..55f5cc80fd6781486fdb38df71a3a7f69603150c 100644 --- a/styles/src/styleTree/incomingCallNotification.ts +++ b/styles/src/styleTree/incomingCallNotification.ts @@ -2,7 +2,7 @@ import Theme from "../themes/common/theme"; import { backgroundColor, borderColor, text } from "./components"; export default function incomingCallNotification(theme: Theme): Object { - const avatarSize = 32; + const avatarSize = 48; return { windowHeight: 74, windowWidth: 380, From 4504b36c8f4f9257ff19fc3f6d7172002071cf31 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 11 Oct 2022 15:24:31 +0200 Subject: [PATCH 105/112] Show a different message when participant is active on unshared project Co-Authored-By: Nathan Sobo --- crates/call/src/participant.rs | 19 +++++++++---- crates/call/src/room.rs | 26 +++++++++++++++--- crates/collab/src/integration_tests.rs | 29 ++++++++++++-------- crates/collab/src/rpc/store.rs | 2 +- crates/collab_ui/src/collab_titlebar_item.rs | 25 ++++------------- crates/gpui/src/app.rs | 6 ++++ crates/rpc/proto/zed.proto | 9 ++++-- crates/workspace/src/pane_group.rs | 17 +++++++++++- 8 files changed, 88 insertions(+), 45 deletions(-) diff --git a/crates/call/src/participant.rs b/crates/call/src/participant.rs index 7e031f907ff26710fcd243c64330931306920932..a5be5b4af2779a369f0e0707b8144c873a6e61d5 100644 --- a/crates/call/src/participant.rs +++ b/crates/call/src/participant.rs @@ -1,28 +1,37 @@ use anyhow::{anyhow, Result}; use client::{proto, User}; +use gpui::WeakModelHandle; +use project::Project; use std::sync::Arc; #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub enum ParticipantLocation { - Project { project_id: u64 }, + SharedProject { project_id: u64 }, + UnsharedProject, External, } impl ParticipantLocation { pub fn from_proto(location: Option) -> Result { match location.and_then(|l| l.variant) { - Some(proto::participant_location::Variant::Project(project)) => Ok(Self::Project { - project_id: project.id, - }), + Some(proto::participant_location::Variant::SharedProject(project)) => { + Ok(Self::SharedProject { + project_id: project.id, + }) + } + Some(proto::participant_location::Variant::UnsharedProject(_)) => { + Ok(Self::UnsharedProject) + } Some(proto::participant_location::Variant::External(_)) => Ok(Self::External), None => Err(anyhow!("participant location was not provided")), } } } -#[derive(Clone, Debug, Default)] +#[derive(Clone, Default)] pub struct LocalParticipant { pub projects: Vec, + pub active_project: Option>, } #[derive(Clone, Debug)] diff --git a/crates/call/src/room.rs b/crates/call/src/room.rs index a3b49c2a4f8090a383e14c2673dbe51e5af7f6ff..572f512d1c28841ef41f7eeb49dbbc073495c9b8 100644 --- a/crates/call/src/room.rs +++ b/crates/call/src/room.rs @@ -395,13 +395,26 @@ impl Room { }) .collect(), }); - cx.spawn_weak(|_, mut cx| async move { + cx.spawn(|this, mut cx| async move { let response = request.await?; + project .update(&mut cx, |project, cx| { project.shared(response.project_id, cx) }) .await?; + + // If the user's location is in this project, it changes from UnsharedProject to SharedProject. + this.update(&mut cx, |this, cx| { + let active_project = this.local_participant.active_project.as_ref(); + if active_project.map_or(false, |location| *location == project) { + this.set_location(Some(&project), cx) + } else { + Task::ready(Ok(())) + } + }) + .await?; + Ok(response.project_id) }) } @@ -418,17 +431,22 @@ impl Room { let client = self.client.clone(); let room_id = self.id; let location = if let Some(project) = project { + self.local_participant.active_project = Some(project.downgrade()); if let Some(project_id) = project.read(cx).remote_id() { - proto::participant_location::Variant::Project( - proto::participant_location::Project { id: project_id }, + proto::participant_location::Variant::SharedProject( + proto::participant_location::SharedProject { id: project_id }, ) } else { - return Task::ready(Err(anyhow!("project is not shared"))); + proto::participant_location::Variant::UnsharedProject( + proto::participant_location::UnsharedProject {}, + ) } } else { + self.local_participant.active_project = None; proto::participant_location::Variant::External(proto::participant_location::External {}) }; + cx.notify(); cx.foreground().spawn(async move { client .request(proto::UpdateParticipantLocation { diff --git a/crates/collab/src/integration_tests.rs b/crates/collab/src/integration_tests.rs index 27fb1a9bfbcb48723732f953ed43e32e561396b7..29d1c1b8330398bb22a3fa4edf373de2857424a5 100644 --- a/crates/collab/src/integration_tests.rs +++ b/crates/collab/src/integration_tests.rs @@ -946,8 +946,8 @@ async fn test_room_location( } }); - let project_a_id = active_call_a - .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) + room_a + .update(cx_a, |room, cx| room.set_location(Some(&project_a), cx)) .await .unwrap(); deterministic.run_until_parked(); @@ -959,11 +959,11 @@ async fn test_room_location( assert!(b_notified.take()); assert_eq!( participant_locations(&room_b, cx_b), - vec![("user_a".to_string(), ParticipantLocation::External)] + vec![("user_a".to_string(), ParticipantLocation::UnsharedProject)] ); - let project_b_id = active_call_b - .update(cx_b, |call, cx| call.share_project(project_b.clone(), cx)) + let project_a_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) .await .unwrap(); deterministic.run_until_parked(); @@ -975,11 +975,16 @@ async fn test_room_location( assert!(b_notified.take()); assert_eq!( participant_locations(&room_b, cx_b), - vec![("user_a".to_string(), ParticipantLocation::External)] + vec![( + "user_a".to_string(), + ParticipantLocation::SharedProject { + project_id: project_a_id + } + )] ); - room_a - .update(cx_a, |room, cx| room.set_location(Some(&project_a), cx)) + let project_b_id = active_call_b + .update(cx_b, |call, cx| call.share_project(project_b.clone(), cx)) .await .unwrap(); deterministic.run_until_parked(); @@ -993,7 +998,7 @@ async fn test_room_location( participant_locations(&room_b, cx_b), vec![( "user_a".to_string(), - ParticipantLocation::Project { + ParticipantLocation::SharedProject { project_id: project_a_id } )] @@ -1009,7 +1014,7 @@ async fn test_room_location( participant_locations(&room_a, cx_a), vec![( "user_b".to_string(), - ParticipantLocation::Project { + ParticipantLocation::SharedProject { project_id: project_b_id } )] @@ -1019,7 +1024,7 @@ async fn test_room_location( participant_locations(&room_b, cx_b), vec![( "user_a".to_string(), - ParticipantLocation::Project { + ParticipantLocation::SharedProject { project_id: project_a_id } )] @@ -1040,7 +1045,7 @@ async fn test_room_location( participant_locations(&room_b, cx_b), vec![( "user_a".to_string(), - ParticipantLocation::Project { + ParticipantLocation::SharedProject { project_id: project_a_id } )] diff --git a/crates/collab/src/rpc/store.rs b/crates/collab/src/rpc/store.rs index 9b2661abca8324e936e10bc3a449dca09a4e0341..cc34094782fe5ecc47074d3cbaee5ad12ebb460e 100644 --- a/crates/collab/src/rpc/store.rs +++ b/crates/collab/src/rpc/store.rs @@ -684,7 +684,7 @@ impl Store { .rooms .get_mut(&room_id) .ok_or_else(|| anyhow!("no such room"))?; - if let Some(proto::participant_location::Variant::Project(project)) = + if let Some(proto::participant_location::Variant::SharedProject(project)) = location.variant.as_ref() { anyhow::ensure!( diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index c46861b5ab659224b3462b71c7c6ddaab725719a..a2d7249b57f6b9ae12b76b54b7fc5c5cebe38268 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -121,14 +121,11 @@ impl CollabTitlebarItem { let room = ActiveCall::global(cx).read(cx).room().cloned(); if let Some((workspace, room)) = workspace.zip(room) { let workspace = workspace.read(cx); - let project = if !active { - None - } else if workspace.project().read(cx).remote_id().is_some() { + let project = if active { Some(workspace.project().clone()) } else { None }; - room.update(cx, |room, cx| { room.set_location(project.as_ref(), cx) .detach_and_log_err(cx); @@ -139,20 +136,10 @@ impl CollabTitlebarItem { fn share_project(&mut self, _: &ShareProject, cx: &mut ViewContext) { if let Some(workspace) = self.workspace.upgrade(cx) { let active_call = ActiveCall::global(cx); - - let window_id = cx.window_id(); let project = workspace.read(cx).project().clone(); - let share = active_call.update(cx, |call, cx| call.share_project(project.clone(), cx)); - cx.spawn_weak(|_, mut cx| async move { - share.await?; - if cx.update(|cx| cx.window_is_active(window_id)) { - active_call.update(&mut cx, |call, cx| { - call.set_location(Some(&project), cx).detach_and_log_err(cx); - }); - } - anyhow::Ok(()) - }) - .detach_and_log_err(cx); + active_call + .update(cx, |call, cx| call.share_project(project, cx)) + .detach_and_log_err(cx); } } @@ -363,7 +350,7 @@ impl CollabTitlebarItem { let mut avatar_style; if let Some((_, _, location)) = peer.as_ref() { - if let ParticipantLocation::Project { project_id } = *location { + if let ParticipantLocation::SharedProject { project_id } = *location { if Some(project_id) == workspace.read(cx).project().read(cx).remote_id() { avatar_style = theme.workspace.titlebar.avatar; } else { @@ -428,7 +415,7 @@ impl CollabTitlebarItem { cx, ) .boxed() - } else if let ParticipantLocation::Project { project_id } = location { + } else if let ParticipantLocation::SharedProject { project_id } = location { let user_id = user.id; MouseEventHandler::::new(peer_id.0 as usize, cx, move |_, _| content) .with_cursor_style(CursorStyle::PointingHand) diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 765501368c229535b689a2baacdad965701b6eb3..ed351cdefefe165d93d21933f3822c796681bb19 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -4687,6 +4687,12 @@ impl PartialEq for WeakModelHandle { impl Eq for WeakModelHandle {} +impl PartialEq> for WeakModelHandle { + fn eq(&self, other: &ModelHandle) -> bool { + self.model_id == other.model_id + } +} + impl Clone for WeakModelHandle { fn clone(&self) -> Self { Self { diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 2a358113e9238616119e0d3450457d883825d704..283b11fd788b904d8b98d1ff85dc3d08bc8034de 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -174,14 +174,17 @@ message ParticipantProject { message ParticipantLocation { oneof variant { - Project project = 1; - External external = 2; + SharedProject shared_project = 1; + UnsharedProject unshared_project = 2; + External external = 3; } - message Project { + message SharedProject { uint64 id = 1; } + message UnsharedProject {} + message External {} } diff --git a/crates/workspace/src/pane_group.rs b/crates/workspace/src/pane_group.rs index 83ce28fdf0b9420a27d174a603dca8b558a831c5..707526c1d6412b5c28de82d78edd4ac39480ca41 100644 --- a/crates/workspace/src/pane_group.rs +++ b/crates/workspace/src/pane_group.rs @@ -138,7 +138,7 @@ impl Member { border.overlay = true; match leader.location { - call::ParticipantLocation::Project { + call::ParticipantLocation::SharedProject { project_id: leader_project_id, } => { if Some(leader_project_id) == project.read(cx).remote_id() { @@ -183,6 +183,21 @@ impl Member { ) } } + call::ParticipantLocation::UnsharedProject => Some( + Label::new( + format!( + "{} is viewing an unshared Zed project", + leader.user.github_login + ), + theme.workspace.external_location_message.text.clone(), + ) + .contained() + .with_style(theme.workspace.external_location_message.container) + .aligned() + .bottom() + .right() + .boxed(), + ), call::ParticipantLocation::External => Some( Label::new( format!( From eb711cde5381681df11f1ea14411209252a6cf50 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 11 Oct 2022 16:52:20 +0200 Subject: [PATCH 106/112] Polish styling of contacts popover Co-Authored-By: Nathan Sobo --- crates/collab_ui/src/contact_list.rs | 13 +++++-------- styles/src/styleTree/contactFinder.ts | 6 +++--- styles/src/styleTree/contactList.ts | 8 +++----- styles/src/styleTree/contactsPopover.ts | 4 ++-- 4 files changed, 13 insertions(+), 18 deletions(-) diff --git a/crates/collab_ui/src/contact_list.rs b/crates/collab_ui/src/contact_list.rs index 357b3c65e0a4ca735cc0fcf2317b303b5c16479a..ba7083d60427f4b5faa10b9c93496a5fd75c1fd7 100644 --- a/crates/collab_ui/src/contact_list.rs +++ b/crates/collab_ui/src/contact_list.rs @@ -1087,14 +1087,11 @@ impl View for ContactList { ) .with_child( MouseEventHandler::::new(0, cx, |_, _| { - Svg::new("icons/user_plus_16.svg") - .with_color(theme.contact_list.add_contact_button.color) - .constrained() - .with_height(16.) - .contained() - .with_style(theme.contact_list.add_contact_button.container) - .aligned() - .boxed() + render_icon_button( + &theme.contact_list.add_contact_button, + "icons/user_plus_16.svg", + ) + .boxed() }) .with_cursor_style(CursorStyle::PointingHand) .on_click(MouseButton::Left, |_, cx| { diff --git a/styles/src/styleTree/contactFinder.ts b/styles/src/styleTree/contactFinder.ts index 2feb3d7e356c945824ae577a0ae5a136802b0fd9..103d669df1ea26c7937b9bf9b4a1d3c8fee8fa75 100644 --- a/styles/src/styleTree/contactFinder.ts +++ b/styles/src/styleTree/contactFinder.ts @@ -3,7 +3,7 @@ import picker from "./picker"; import { backgroundColor, border, iconColor, player, text } from "./components"; export default function contactFinder(theme: Theme) { - const sideMargin = 12; + const sideMargin = 6; const contactButton = { background: backgroundColor(theme, 100), color: iconColor(theme, "primary"), @@ -33,8 +33,8 @@ export default function contactFinder(theme: Theme) { top: 4, }, margin: { - left: 12, - right: 12, + left: sideMargin, + right: sideMargin, } } }, diff --git a/styles/src/styleTree/contactList.ts b/styles/src/styleTree/contactList.ts index 633d6341967bb2c46ba308dfc3df44f984f785e3..52d5a25c447988f30fc704335c026cae2b1b7a71 100644 --- a/styles/src/styleTree/contactList.ts +++ b/styles/src/styleTree/contactList.ts @@ -53,22 +53,20 @@ export default function contactList(theme: Theme) { top: 4, }, margin: { - left: sidePadding, - right: sidePadding, + left: 6 }, }, userQueryEditorHeight: 33, addContactButton: { - margin: { left: 6, right: 12 }, color: iconColor(theme, "primary"), - buttonWidth: 16, + buttonWidth: 28, iconWidth: 16, }, rowHeight: 28, sectionIconSize: 8, headerRow: { ...text(theme, "mono", "secondary", { size: "sm" }), - margin: { top: 14 }, + margin: { top: 6 }, padding: { left: sidePadding, right: sidePadding, diff --git a/styles/src/styleTree/contactsPopover.ts b/styles/src/styleTree/contactsPopover.ts index 7d699fa26b3e2d55d1e868073f413751d60ce382..957f3d6c8d95e4293b80b8d5c1ea1df11abcb5ea 100644 --- a/styles/src/styleTree/contactsPopover.ts +++ b/styles/src/styleTree/contactsPopover.ts @@ -8,7 +8,7 @@ export default function contactsPopover(theme: Theme) { padding: { top: 6 }, shadow: popoverShadow(theme), border: border(theme, "primary"), - width: 250, - height: 300, + width: 300, + height: 400, } } From 45d118f96f17b550e3a77dfb0108eb16081bcd5d Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 11 Oct 2022 17:05:13 +0200 Subject: [PATCH 107/112] Decide whether to clip to visible bounds on a per-element basis Co-Authored-By: Nathan Sobo --- crates/editor/src/element.rs | 3 ++- crates/gpui/src/elements.rs | 6 ------ crates/gpui/src/elements/flex.rs | 5 +++-- crates/gpui/src/elements/list.rs | 3 ++- crates/gpui/src/elements/mouse_event_handler.rs | 1 + crates/gpui/src/elements/overlay.rs | 6 +++++- crates/gpui/src/elements/uniform_list.rs | 4 +++- crates/terminal/src/terminal_element.rs | 2 ++ 8 files changed, 18 insertions(+), 12 deletions(-) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index acf2e5887ccae44c826921174a404736e398bfe3..3b6033cefa066bed4be1f450830b923fc37ee81d 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -1729,7 +1729,8 @@ impl Element for EditorElement { layout: &mut Self::LayoutState, cx: &mut PaintContext, ) -> Self::PaintState { - cx.scene.push_layer(Some(bounds)); + let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default(); + cx.scene.push_layer(Some(visible_bounds)); let gutter_bounds = RectF::new(bounds.origin(), layout.gutter_size); let text_bounds = RectF::new( diff --git a/crates/gpui/src/elements.rs b/crates/gpui/src/elements.rs index b22f0b3250d7a94dabaa58bc84fbe22688077d3f..59269f8af68ebc2819da7a63be996663e4379982 100644 --- a/crates/gpui/src/elements.rs +++ b/crates/gpui/src/elements.rs @@ -271,9 +271,6 @@ impl AnyElement for Lifecycle { mut layout, } => { let bounds = RectF::new(origin, size); - let visible_bounds = visible_bounds - .intersection(bounds) - .unwrap_or_else(|| RectF::new(bounds.origin(), Vector2F::default())); let paint = element.paint(bounds, visible_bounds, &mut layout, cx); Lifecycle::PostPaint { element, @@ -292,9 +289,6 @@ impl AnyElement for Lifecycle { .. } => { let bounds = RectF::new(origin, bounds.size()); - let visible_bounds = visible_bounds - .intersection(bounds) - .unwrap_or_else(|| RectF::new(bounds.origin(), Vector2F::default())); let paint = element.paint(bounds, visible_bounds, &mut layout, cx); Lifecycle::PostPaint { element, diff --git a/crates/gpui/src/elements/flex.rs b/crates/gpui/src/elements/flex.rs index 227f946ac6529e9d4e33f8acb5c535cc1805699d..fd37b001feb7dbc5c209a8f369cc9e24caabf040 100644 --- a/crates/gpui/src/elements/flex.rs +++ b/crates/gpui/src/elements/flex.rs @@ -241,11 +241,12 @@ impl Element for Flex { remaining_space: &mut Self::LayoutState, cx: &mut PaintContext, ) -> Self::PaintState { - let mut remaining_space = *remaining_space; + let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default(); + let mut remaining_space = *remaining_space; let overflowing = remaining_space < 0.; if overflowing { - cx.scene.push_layer(Some(bounds)); + cx.scene.push_layer(Some(visible_bounds)); } if let Some(scroll_state) = &self.scroll_state { diff --git a/crates/gpui/src/elements/list.rs b/crates/gpui/src/elements/list.rs index d752a52a1666110a7fc1cc2160988aac86429212..a6c76cf643302d21819c2368172f755e032de573 100644 --- a/crates/gpui/src/elements/list.rs +++ b/crates/gpui/src/elements/list.rs @@ -261,7 +261,8 @@ impl Element for List { scroll_top: &mut ListOffset, cx: &mut PaintContext, ) { - cx.scene.push_layer(Some(bounds)); + let visible_bounds = visible_bounds.intersection(bounds).unwrap_or_default(); + cx.scene.push_layer(Some(visible_bounds)); cx.scene .push_mouse_region(MouseRegion::new::(10, 0, bounds).on_scroll({ diff --git a/crates/gpui/src/elements/mouse_event_handler.rs b/crates/gpui/src/elements/mouse_event_handler.rs index 5b3b9b13f6c77ae2f3fe2b517a3136e83729e4e0..e809c0080f878e50bc62821fd6df2385156ac0d5 100644 --- a/crates/gpui/src/elements/mouse_event_handler.rs +++ b/crates/gpui/src/elements/mouse_event_handler.rs @@ -169,6 +169,7 @@ impl Element for MouseEventHandler { _: &mut Self::LayoutState, cx: &mut PaintContext, ) -> Self::PaintState { + let visible_bounds = visible_bounds.intersection(bounds).unwrap_or_default(); let hit_bounds = self.hit_bounds(visible_bounds); if let Some(style) = self.cursor_style { cx.scene.push_cursor_region(CursorRegion { diff --git a/crates/gpui/src/elements/overlay.rs b/crates/gpui/src/elements/overlay.rs index d47a39e958b8cc18ac12e5b32fa52c602ad0d714..07442d1140c2be8f992641bf447e0ede2aa157d3 100644 --- a/crates/gpui/src/elements/overlay.rs +++ b/crates/gpui/src/elements/overlay.rs @@ -217,7 +217,11 @@ impl Element for Overlay { )); } - self.child.paint(bounds.origin(), bounds, cx); + self.child.paint( + bounds.origin(), + RectF::new(Vector2F::zero(), cx.window_size), + cx, + ); cx.scene.pop_stacking_context(); } diff --git a/crates/gpui/src/elements/uniform_list.rs b/crates/gpui/src/elements/uniform_list.rs index 6bc35c06922638c8221805d7a945b0c5f747ec65..c9cdbc1b2c90f76f8ac9ba3c6014d2d68e3e06f6 100644 --- a/crates/gpui/src/elements/uniform_list.rs +++ b/crates/gpui/src/elements/uniform_list.rs @@ -284,7 +284,9 @@ impl Element for UniformList { layout: &mut Self::LayoutState, cx: &mut PaintContext, ) -> Self::PaintState { - cx.scene.push_layer(Some(bounds)); + let visible_bounds = visible_bounds.intersection(bounds).unwrap_or_default(); + + cx.scene.push_layer(Some(visible_bounds)); cx.scene.push_mouse_region( MouseRegion::new::(self.view_id, 0, visible_bounds).on_scroll({ diff --git a/crates/terminal/src/terminal_element.rs b/crates/terminal/src/terminal_element.rs index 0f037863af38dd50317fd2bf09c315e11a1971e0..099de23c6d4e23adfa5702c02ad4716b7819e538 100644 --- a/crates/terminal/src/terminal_element.rs +++ b/crates/terminal/src/terminal_element.rs @@ -726,6 +726,8 @@ impl Element for TerminalElement { layout: &mut Self::LayoutState, cx: &mut gpui::PaintContext, ) -> Self::PaintState { + let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default(); + //Setup element stuff let clip_bounds = Some(visible_bounds); From ee2587d3e57225c41650370c377ed661a353fba7 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 11 Oct 2022 17:09:54 +0200 Subject: [PATCH 108/112] Fix integration tests --- crates/collab/src/integration_tests.rs | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/crates/collab/src/integration_tests.rs b/crates/collab/src/integration_tests.rs index 29d1c1b8330398bb22a3fa4edf373de2857424a5..bdd240fda8e60c2e4479119f6af36a204d221240 100644 --- a/crates/collab/src/integration_tests.rs +++ b/crates/collab/src/integration_tests.rs @@ -801,18 +801,11 @@ async fn test_host_disconnect( assert!(!cx_b.is_window_edited(workspace_b.window_id())); // Ensure client B is not prompted to save edits when closing window after disconnecting. - workspace_b - .update(cx_b, |workspace, cx| { - workspace.close(&Default::default(), cx) - }) - .unwrap() + let can_close = workspace_b + .update(cx_b, |workspace, cx| workspace.prepare_to_close(true, cx)) .await .unwrap(); - assert_eq!(cx_b.window_ids().len(), 0); - cx_b.update(|_| { - drop(workspace_b); - drop(project_b); - }); + assert!(can_close); } #[gpui::test(iterations = 10)] From 4c07a0782b25d9fd65c6918c40bc1e7dd8501d8b Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 11 Oct 2022 17:25:35 +0200 Subject: [PATCH 109/112] Allow active call to be optional on workspace This prepares us for a future where the workspace is unaware of the active call and doesn't require all tests to invoke `call::init`. --- crates/workspace/src/pane_group.rs | 12 ++++--- crates/workspace/src/workspace.rs | 53 +++++++++++++++++++----------- crates/zed/src/zed.rs | 1 + 3 files changed, 42 insertions(+), 24 deletions(-) diff --git a/crates/workspace/src/pane_group.rs b/crates/workspace/src/pane_group.rs index 707526c1d6412b5c28de82d78edd4ac39480ca41..f09c31741ef2a60e5475057d0d0e9394d121d3c4 100644 --- a/crates/workspace/src/pane_group.rs +++ b/crates/workspace/src/pane_group.rs @@ -60,9 +60,11 @@ impl PaneGroup { project: &ModelHandle, theme: &Theme, follower_states: &FollowerStatesByLeader, + active_call: Option<&ModelHandle>, cx: &mut RenderContext, ) -> ElementBox { - self.root.render(project, theme, follower_states, cx) + self.root + .render(project, theme, follower_states, active_call, cx) } pub(crate) fn panes(&self) -> Vec<&ViewHandle> { @@ -105,6 +107,7 @@ impl Member { project: &ModelHandle, theme: &Theme, follower_states: &FollowerStatesByLeader, + active_call: Option<&ModelHandle>, cx: &mut RenderContext, ) -> ElementBox { enum FollowIntoExternalProject {} @@ -121,7 +124,7 @@ impl Member { } }) .and_then(|leader_id| { - let room = ActiveCall::global(cx).read(cx).room()?.read(cx); + let room = active_call?.read(cx).room()?.read(cx); let collaborator = project.read(cx).collaborators().get(leader_id)?; let participant = room.remote_participants().get(&leader_id)?; Some((collaborator.replica_id, participant)) @@ -223,7 +226,7 @@ impl Member { .with_children(prompt) .boxed() } - Member::Axis(axis) => axis.render(project, theme, follower_states, cx), + Member::Axis(axis) => axis.render(project, theme, follower_states, active_call, cx), } } @@ -328,12 +331,13 @@ impl PaneAxis { project: &ModelHandle, theme: &Theme, follower_state: &FollowerStatesByLeader, + active_call: Option<&ModelHandle>, cx: &mut RenderContext, ) -> ElementBox { let last_member_ix = self.members.len() - 1; Flex::new(self.axis) .with_children(self.members.iter().enumerate().map(|(ix, member)| { - let mut member = member.render(project, theme, follower_state, cx); + let mut member = member.render(project, theme, follower_state, active_call, cx); if ix < last_member_ix { let mut border = theme.workspace.pane_divider; border.left = false; diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 6472a6f6971d8229d6c2b36f613fa2b8d4d8db10..705823003f43c737f714253353131b8a92cb3e10 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -980,8 +980,9 @@ pub struct Workspace { follower_states_by_leader: FollowerStatesByLeader, last_leaders_by_pane: HashMap, PeerId>, window_edited: bool, + active_call: Option>, _observe_current_user: Task<()>, - _active_call_observation: gpui::Subscription, + _active_call_observation: Option, } #[derive(Default)] @@ -1090,6 +1091,14 @@ impl Workspace { drag_and_drop.register_container(weak_handle.clone()); }); + let mut active_call = None; + let mut active_call_observation = None; + if cx.has_global::>() { + let call = cx.global::>().clone(); + active_call_observation = Some(cx.observe(&call, |_, _, cx| cx.notify())); + active_call = Some(call); + } + let mut this = Workspace { modal: None, weak_self: weak_handle, @@ -1116,8 +1125,9 @@ impl Workspace { follower_states_by_leader: Default::default(), last_leaders_by_pane: Default::default(), window_edited: false, + active_call, _observe_current_user, - _active_call_observation: cx.observe(&ActiveCall::global(cx), |_, _, cx| cx.notify()), + _active_call_observation: active_call_observation, }; this.project_remote_id_changed(this.project.read(cx).remote_id(), cx); cx.defer(|this, cx| this.update_window_title(cx)); @@ -1248,30 +1258,32 @@ impl Workspace { quitting: bool, cx: &mut ViewContext, ) -> Task> { - let active_call = ActiveCall::global(cx); + let active_call = self.active_call.clone(); let window_id = cx.window_id(); let workspace_count = cx .window_ids() .flat_map(|window_id| cx.root_view::(window_id)) .count(); cx.spawn(|this, mut cx| async move { - if !quitting - && workspace_count == 1 - && active_call.read_with(&cx, |call, _| call.room().is_some()) - { - let answer = cx - .prompt( - window_id, - PromptLevel::Warning, - "Do you want to leave the current call?", - &["Close window and hang up", "Cancel"], - ) - .next() - .await; - if answer == Some(1) { - return anyhow::Ok(false); - } else { - active_call.update(&mut cx, |call, cx| call.hang_up(cx))?; + if let Some(active_call) = active_call { + if !quitting + && workspace_count == 1 + && active_call.read_with(&cx, |call, _| call.room().is_some()) + { + let answer = cx + .prompt( + window_id, + PromptLevel::Warning, + "Do you want to leave the current call?", + &["Close window and hang up", "Cancel"], + ) + .next() + .await; + if answer == Some(1) { + return anyhow::Ok(false); + } else { + active_call.update(&mut cx, |call, cx| call.hang_up(cx))?; + } } } @@ -2571,6 +2583,7 @@ impl View for Workspace { &project, &theme, &self.follower_states_by_leader, + self.active_call.as_ref(), cx, )) .flex(1., true) diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index d032e661d75aa2dcf2a12f92dcfe155dbe2402e7..f6f3a34242ac82dc143457aa3946c32d36eee67a 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -1755,6 +1755,7 @@ mod tests { let state = Arc::get_mut(&mut app_state).unwrap(); state.initialize_workspace = initialize_workspace; state.build_window_options = build_window_options; + call::init(app_state.client.clone(), app_state.user_store.clone(), cx); workspace::init(app_state.clone(), cx); editor::init(cx); pane::init(cx); From f83de0a91cfba22609f0784fdff1e13b2a7f1355 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 11 Oct 2022 17:30:17 +0200 Subject: [PATCH 110/112] Respect contacts popover size --- crates/collab_ui/src/collab_titlebar_item.rs | 1 + styles/src/styleTree/contactsPopover.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index a2d7249b57f6b9ae12b76b54b7fc5c5cebe38268..9faea76a107795a81eca61c77e7916ac1d62725f 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -226,6 +226,7 @@ impl CollabTitlebarItem { ChildView::new(popover) .contained() .with_margin_top(titlebar.height) + .with_margin_left(titlebar.toggle_contacts_button.default.button_width) .with_margin_right(-titlebar.toggle_contacts_button.default.button_width) .boxed(), ) diff --git a/styles/src/styleTree/contactsPopover.ts b/styles/src/styleTree/contactsPopover.ts index 957f3d6c8d95e4293b80b8d5c1ea1df11abcb5ea..2e70b3daea9f8951ba410475b5c69726bb8db109 100644 --- a/styles/src/styleTree/contactsPopover.ts +++ b/styles/src/styleTree/contactsPopover.ts @@ -6,6 +6,7 @@ export default function contactsPopover(theme: Theme) { background: backgroundColor(theme, 300, "base"), cornerRadius: 6, padding: { top: 6 }, + margin: { top: -6 }, shadow: popoverShadow(theme), border: border(theme, "primary"), width: 300, From ba6c5441c0eb1edfccc29e453f33d68cb6f3b965 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 11 Oct 2022 18:22:00 +0200 Subject: [PATCH 111/112] Always show invite link in contacts popover --- crates/collab_ui/src/contact_list.rs | 54 +---------------------- crates/collab_ui/src/contacts_popover.rs | 56 ++++++++++++++++++++++-- crates/theme/src/theme.rs | 3 +- styles/src/styleTree/contactList.ts | 11 ----- styles/src/styleTree/contactsPopover.ts | 15 ++++++- 5 files changed, 71 insertions(+), 68 deletions(-) diff --git a/crates/collab_ui/src/contact_list.rs b/crates/collab_ui/src/contact_list.rs index ba7083d60427f4b5faa10b9c93496a5fd75c1fd7..c2736ea9d37131b293c5ec2e666c782b813bea01 100644 --- a/crates/collab_ui/src/contact_list.rs +++ b/crates/collab_ui/src/contact_list.rs @@ -8,9 +8,8 @@ use fuzzy::{match_strings, StringMatchCandidate}; use gpui::{ elements::*, geometry::{rect::RectF, vector::vec2f}, - impl_actions, impl_internal_actions, keymap, AppContext, ClipboardItem, CursorStyle, Entity, - ModelHandle, MouseButton, MutableAppContext, RenderContext, Subscription, View, ViewContext, - ViewHandle, + impl_actions, impl_internal_actions, keymap, AppContext, CursorStyle, Entity, ModelHandle, + MouseButton, MutableAppContext, RenderContext, Subscription, View, ViewContext, ViewHandle, }; use menu::{Confirm, SelectNext, SelectPrev}; use project::Project; @@ -1104,55 +1103,6 @@ impl View for ContactList { .boxed(), ) .with_child(List::new(self.list_state.clone()).flex(1., false).boxed()) - .with_children( - self.user_store - .read(cx) - .invite_info() - .cloned() - .and_then(|info| { - enum InviteLink {} - - if info.count > 0 { - Some( - MouseEventHandler::::new(0, cx, |state, cx| { - let style = theme - .contact_list - .invite_row - .style_for(state, false) - .clone(); - - let copied = cx.read_from_clipboard().map_or(false, |item| { - item.text().as_str() == info.url.as_ref() - }); - - Label::new( - format!( - "{} invite link ({} left)", - if copied { "Copied" } else { "Copy" }, - info.count - ), - style.label.clone(), - ) - .aligned() - .left() - .constrained() - .with_height(theme.contact_list.row_height) - .contained() - .with_style(style.container) - .boxed() - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, move |_, cx| { - cx.write_to_clipboard(ClipboardItem::new(info.url.to_string())); - cx.notify(); - }) - .boxed(), - ) - } else { - None - } - }), - ) .boxed() } diff --git a/crates/collab_ui/src/contacts_popover.rs b/crates/collab_ui/src/contacts_popover.rs index 0758cf735d786665d891323ac426c283974e1af6..deedac9f9816a4d971251651388478326630a46e 100644 --- a/crates/collab_ui/src/contacts_popover.rs +++ b/crates/collab_ui/src/contacts_popover.rs @@ -1,8 +1,8 @@ use crate::{contact_finder::ContactFinder, contact_list::ContactList}; use client::UserStore; use gpui::{ - actions, elements::*, Entity, ModelHandle, MutableAppContext, RenderContext, View, ViewContext, - ViewHandle, + actions, elements::*, ClipboardItem, CursorStyle, Entity, ModelHandle, MouseButton, + MutableAppContext, RenderContext, View, ViewContext, ViewHandle, }; use project::Project; use settings::Settings; @@ -92,7 +92,57 @@ impl View for ContactsPopover { Child::ContactFinder(child) => ChildView::new(child), }; - child + Flex::column() + .with_child(child.flex(1., true).boxed()) + .with_children( + self.user_store + .read(cx) + .invite_info() + .cloned() + .and_then(|info| { + enum InviteLink {} + + if info.count > 0 { + Some( + MouseEventHandler::::new(0, cx, |state, cx| { + let style = theme + .contacts_popover + .invite_row + .style_for(state, false) + .clone(); + + let copied = cx.read_from_clipboard().map_or(false, |item| { + item.text().as_str() == info.url.as_ref() + }); + + Label::new( + format!( + "{} invite link ({} left)", + if copied { "Copied" } else { "Copy" }, + info.count + ), + style.label.clone(), + ) + .aligned() + .left() + .constrained() + .with_height(theme.contacts_popover.invite_row_height) + .contained() + .with_style(style.container) + .boxed() + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, move |_, cx| { + cx.write_to_clipboard(ClipboardItem::new(info.url.to_string())); + cx.notify(); + }) + .boxed(), + ) + } else { + None + } + }), + ) .contained() .with_style(theme.contacts_popover.container) .constrained() diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 503645d6bc453b7d8406aa884733b2a7dfac2841..37ec279d021b14d135fe1b09720d606b04e8402f 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -89,6 +89,8 @@ pub struct ContactsPopover { pub container: ContainerStyle, pub height: f32, pub width: f32, + pub invite_row_height: f32, + pub invite_row: Interactive, } #[derive(Deserialize, Default)] @@ -110,7 +112,6 @@ pub struct ContactList { pub contact_button_spacing: f32, pub disabled_button: IconButton, pub section_icon_size: f32, - pub invite_row: Interactive, pub calling_indicator: ContainedText, } diff --git a/styles/src/styleTree/contactList.ts b/styles/src/styleTree/contactList.ts index 52d5a25c447988f30fc704335c026cae2b1b7a71..ecf0eaa0c7dc925ffc0fa0fb1bf63275ab35a77b 100644 --- a/styles/src/styleTree/contactList.ts +++ b/styles/src/styleTree/contactList.ts @@ -139,17 +139,6 @@ export default function contactList(theme: Theme) { background: backgroundColor(theme, 100), color: iconColor(theme, "muted"), }, - inviteRow: { - padding: { - left: sidePadding, - right: sidePadding, - }, - border: { top: true, width: 1, color: borderColor(theme, "primary") }, - text: text(theme, "sans", "secondary", { size: "sm" }), - hover: { - text: text(theme, "sans", "active", { size: "sm" }), - }, - }, callingIndicator: { ...text(theme, "mono", "muted", { size: "xs" }) }, diff --git a/styles/src/styleTree/contactsPopover.ts b/styles/src/styleTree/contactsPopover.ts index 2e70b3daea9f8951ba410475b5c69726bb8db109..0f82c7c1759e40b1a42abbfb03f8a2794cc89ba7 100644 --- a/styles/src/styleTree/contactsPopover.ts +++ b/styles/src/styleTree/contactsPopover.ts @@ -1,7 +1,8 @@ import Theme from "../themes/common/theme"; -import { backgroundColor, border, popoverShadow } from "./components"; +import { backgroundColor, border, borderColor, popoverShadow, text } from "./components"; export default function contactsPopover(theme: Theme) { + const sidePadding = 12; return { background: backgroundColor(theme, 300, "base"), cornerRadius: 6, @@ -11,5 +12,17 @@ export default function contactsPopover(theme: Theme) { border: border(theme, "primary"), width: 300, height: 400, + inviteRowHeight: 28, + inviteRow: { + padding: { + left: sidePadding, + right: sidePadding, + }, + border: { top: true, width: 1, color: borderColor(theme, "primary") }, + text: text(theme, "sans", "secondary", { size: "sm" }), + hover: { + text: text(theme, "sans", "active", { size: "sm" }), + }, + }, } } From f26695ea8cd8f8211cd989abdd005e5fc3d6c829 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 11 Oct 2022 18:34:04 +0200 Subject: [PATCH 112/112] :lipstick: --- crates/collab_ui/src/contact_list.rs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/crates/collab_ui/src/contact_list.rs b/crates/collab_ui/src/contact_list.rs index c2736ea9d37131b293c5ec2e666c782b813bea01..7b773240cf0d6f410823cb164b52d427ffeca6a5 100644 --- a/crates/collab_ui/src/contact_list.rs +++ b/crates/collab_ui/src/contact_list.rs @@ -776,8 +776,8 @@ impl ContactList { let header_style = theme.header_row.style_for(Default::default(), is_selected); let text = match section { - Section::ActiveCall => "Call", - Section::Requests => "Requests", + Section::ActiveCall => "Collaborators", + Section::Requests => "Contact Requests", Section::Online => "Online", Section::Offline => "Offline", }; @@ -785,7 +785,7 @@ impl ContactList { Some( MouseEventHandler::::new(0, cx, |state, _| { let style = theme.leave_call.style_for(state, false); - Label::new("Leave".into(), style.text.clone()) + Label::new("Leave Session".into(), style.text.clone()) .contained() .with_style(style.container) .boxed() @@ -1096,6 +1096,13 @@ impl View for ContactList { .on_click(MouseButton::Left, |_, cx| { cx.dispatch_action(contacts_popover::ToggleContactFinder) }) + .with_tooltip::( + 0, + "Add contact".into(), + None, + theme.tooltip.clone(), + cx, + ) .boxed(), ) .constrained()