diff --git a/assets/themes/cave-dark.json b/assets/themes/cave-dark.json index 13eb8f2473..347a8d90b9 100644 --- a/assets/themes/cave-dark.json +++ b/assets/themes/cave-dark.json @@ -1359,41 +1359,7 @@ "button_width": 16, "corner_radius": 8 }, - "shared_project_row": { - "guest_avatar_spacing": 4, - "height": 24, - "guest_avatar": { - "corner_radius": 8, - "width": 14 - }, - "name": { - "family": "Zed Mono", - "color": "#8b8792", - "size": 14, - "margin": { - "left": 8, - "right": 6 - } - }, - "guests": { - "margin": { - "left": 8, - "right": 8 - } - }, - "padding": { - "left": 12, - "right": 12 - }, - "background": "#26232a", - "hover": { - "background": "#5852603d" - }, - "active": { - "background": "#5852605c" - } - }, - "unshared_project_row": { + "project_row": { "guest_avatar_spacing": 4, "height": 24, "guest_avatar": { diff --git a/assets/themes/cave-light.json b/assets/themes/cave-light.json index 097d2755aa..89acf414ab 100644 --- a/assets/themes/cave-light.json +++ b/assets/themes/cave-light.json @@ -1359,41 +1359,7 @@ "button_width": 16, "corner_radius": 8 }, - "shared_project_row": { - "guest_avatar_spacing": 4, - "height": 24, - "guest_avatar": { - "corner_radius": 8, - "width": 14 - }, - "name": { - "family": "Zed Mono", - "color": "#585260", - "size": 14, - "margin": { - "left": 8, - "right": 6 - } - }, - "guests": { - "margin": { - "left": 8, - "right": 8 - } - }, - "padding": { - "left": 12, - "right": 12 - }, - "background": "#e2dfe7", - "hover": { - "background": "#8b87921f" - }, - "active": { - "background": "#8b87922e" - } - }, - "unshared_project_row": { + "project_row": { "guest_avatar_spacing": 4, "height": 24, "guest_avatar": { diff --git a/assets/themes/dark.json b/assets/themes/dark.json index c4e33765ce..e91642bb25 100644 --- a/assets/themes/dark.json +++ b/assets/themes/dark.json @@ -1359,41 +1359,7 @@ "button_width": 16, "corner_radius": 8 }, - "shared_project_row": { - "guest_avatar_spacing": 4, - "height": 24, - "guest_avatar": { - "corner_radius": 8, - "width": 14 - }, - "name": { - "family": "Zed Mono", - "color": "#9c9c9c", - "size": 14, - "margin": { - "left": 8, - "right": 6 - } - }, - "guests": { - "margin": { - "left": 8, - "right": 8 - } - }, - "padding": { - "left": 12, - "right": 12 - }, - "background": "#1c1c1c", - "hover": { - "background": "#232323" - }, - "active": { - "background": "#2b2b2b" - } - }, - "unshared_project_row": { + "project_row": { "guest_avatar_spacing": 4, "height": 24, "guest_avatar": { diff --git a/assets/themes/light.json b/assets/themes/light.json index 1cff4df3ad..99c1bab730 100644 --- a/assets/themes/light.json +++ b/assets/themes/light.json @@ -1359,41 +1359,7 @@ "button_width": 16, "corner_radius": 8 }, - "shared_project_row": { - "guest_avatar_spacing": 4, - "height": 24, - "guest_avatar": { - "corner_radius": 8, - "width": 14 - }, - "name": { - "family": "Zed Mono", - "color": "#474747", - "size": 14, - "margin": { - "left": 8, - "right": 6 - } - }, - "guests": { - "margin": { - "left": 8, - "right": 8 - } - }, - "padding": { - "left": 12, - "right": 12 - }, - "background": "#f8f8f8", - "hover": { - "background": "#eaeaea" - }, - "active": { - "background": "#e3e3e3" - } - }, - "unshared_project_row": { + "project_row": { "guest_avatar_spacing": 4, "height": 24, "guest_avatar": { diff --git a/assets/themes/solarized-dark.json b/assets/themes/solarized-dark.json index 8606c78ca0..8a75aa02dd 100644 --- a/assets/themes/solarized-dark.json +++ b/assets/themes/solarized-dark.json @@ -1359,41 +1359,7 @@ "button_width": 16, "corner_radius": 8 }, - "shared_project_row": { - "guest_avatar_spacing": 4, - "height": 24, - "guest_avatar": { - "corner_radius": 8, - "width": 14 - }, - "name": { - "family": "Zed Mono", - "color": "#93a1a1", - "size": 14, - "margin": { - "left": 8, - "right": 6 - } - }, - "guests": { - "margin": { - "left": 8, - "right": 8 - } - }, - "padding": { - "left": 12, - "right": 12 - }, - "background": "#073642", - "hover": { - "background": "#586e753d" - }, - "active": { - "background": "#586e755c" - } - }, - "unshared_project_row": { + "project_row": { "guest_avatar_spacing": 4, "height": 24, "guest_avatar": { diff --git a/assets/themes/solarized-light.json b/assets/themes/solarized-light.json index fb8e781ea0..84bd0762ec 100644 --- a/assets/themes/solarized-light.json +++ b/assets/themes/solarized-light.json @@ -1359,41 +1359,7 @@ "button_width": 16, "corner_radius": 8 }, - "shared_project_row": { - "guest_avatar_spacing": 4, - "height": 24, - "guest_avatar": { - "corner_radius": 8, - "width": 14 - }, - "name": { - "family": "Zed Mono", - "color": "#586e75", - "size": 14, - "margin": { - "left": 8, - "right": 6 - } - }, - "guests": { - "margin": { - "left": 8, - "right": 8 - } - }, - "padding": { - "left": 12, - "right": 12 - }, - "background": "#eee8d5", - "hover": { - "background": "#93a1a11f" - }, - "active": { - "background": "#93a1a12e" - } - }, - "unshared_project_row": { + "project_row": { "guest_avatar_spacing": 4, "height": 24, "guest_avatar": { diff --git a/assets/themes/sulphurpool-dark.json b/assets/themes/sulphurpool-dark.json index 607cecf990..716e81d6a2 100644 --- a/assets/themes/sulphurpool-dark.json +++ b/assets/themes/sulphurpool-dark.json @@ -1359,41 +1359,7 @@ "button_width": 16, "corner_radius": 8 }, - "shared_project_row": { - "guest_avatar_spacing": 4, - "height": 24, - "guest_avatar": { - "corner_radius": 8, - "width": 14 - }, - "name": { - "family": "Zed Mono", - "color": "#979db4", - "size": 14, - "margin": { - "left": 8, - "right": 6 - } - }, - "guests": { - "margin": { - "left": 8, - "right": 8 - } - }, - "padding": { - "left": 12, - "right": 12 - }, - "background": "#293256", - "hover": { - "background": "#5e66873d" - }, - "active": { - "background": "#5e66875c" - } - }, - "unshared_project_row": { + "project_row": { "guest_avatar_spacing": 4, "height": 24, "guest_avatar": { diff --git a/assets/themes/sulphurpool-light.json b/assets/themes/sulphurpool-light.json index d66382331a..09bb2127df 100644 --- a/assets/themes/sulphurpool-light.json +++ b/assets/themes/sulphurpool-light.json @@ -1359,41 +1359,7 @@ "button_width": 16, "corner_radius": 8 }, - "shared_project_row": { - "guest_avatar_spacing": 4, - "height": 24, - "guest_avatar": { - "corner_radius": 8, - "width": 14 - }, - "name": { - "family": "Zed Mono", - "color": "#5e6687", - "size": 14, - "margin": { - "left": 8, - "right": 6 - } - }, - "guests": { - "margin": { - "left": 8, - "right": 8 - } - }, - "padding": { - "left": 12, - "right": 12 - }, - "background": "#dfe2f1", - "hover": { - "background": "#979db41f" - }, - "active": { - "background": "#979db42e" - } - }, - "unshared_project_row": { + "project_row": { "guest_avatar_spacing": 4, "height": 24, "guest_avatar": { diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 75d5b459e1..0fc0f97949 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -1106,7 +1106,7 @@ mod tests { let (done_tx1, mut done_rx1) = smol::channel::unbounded(); let (done_tx2, mut done_rx2) = smol::channel::unbounded(); client.add_model_message_handler( - move |model: ModelHandle, _: TypedEnvelope, _, cx| { + move |model: ModelHandle, _: TypedEnvelope, _, cx| { match model.read_with(&cx, |model, _| model.id) { 1 => done_tx1.try_send(()).unwrap(), 2 => done_tx2.try_send(()).unwrap(), @@ -1135,8 +1135,8 @@ mod tests { let subscription3 = model3.update(cx, |_, cx| client.add_model_for_remote_entity(3, cx)); drop(subscription3); - server.send(proto::UnshareProject { project_id: 1 }); - server.send(proto::UnshareProject { project_id: 2 }); + server.send(proto::JoinProject { project_id: 1 }); + server.send(proto::JoinProject { project_id: 2 }); done_rx1.next().await.unwrap(); done_rx2.next().await.unwrap(); } diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index 84254da73a..97cf225b5f 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -17,6 +17,12 @@ pub struct User { pub avatar: Option>, } +impl PartialEq for User { + fn eq(&self, other: &Self) -> bool { + self.id == other.id && self.github_login == other.github_login + } +} + #[derive(Debug)] pub struct Contact { pub user: Arc, @@ -27,7 +33,6 @@ pub struct Contact { #[derive(Debug)] pub struct ProjectMetadata { pub id: u64, - pub is_shared: bool, pub worktree_root_names: Vec, pub guests: Vec>, } @@ -560,7 +565,6 @@ impl Contact { projects.push(ProjectMetadata { id: project.id, worktree_root_names: project.worktree_root_names.clone(), - is_shared: project.is_shared, guests, }); } diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 1e7384c2c3..19d8ebc779 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -66,6 +66,11 @@ 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 { @@ -115,10 +120,9 @@ impl Server { .add_request_handler(Server::ping) .add_request_handler(Server::register_project) .add_message_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::respond_to_join_project_request) .add_request_handler(Server::register_worktree) .add_message_handler(Server::unregister_worktree) .add_request_handler(Server::update_worktree) @@ -336,12 +340,10 @@ impl Server { let removed_connection = self.store_mut().await.remove_connection(connection_id)?; for (project_id, project) in removed_connection.hosted_projects { - if let Some(share) = project.share { - broadcast(connection_id, share.guests.keys().copied(), |conn_id| { - self.peer - .send(conn_id, proto::UnshareProject { project_id }) - }); - } + broadcast(connection_id, project.guests.keys().copied(), |conn_id| { + self.peer + .send(conn_id, proto::UnregisterProject { project_id }) + }); } for (project_id, peer_ids) in removed_connection.guest_project_ids { @@ -402,20 +404,20 @@ impl Server { Ok(()) } - async fn share_project( - self: Arc, - request: TypedEnvelope, - response: Response, - ) -> Result<()> { - let user_id = { - let mut state = self.store_mut().await; - state.share_project(request.payload.project_id, request.sender_id)?; - state.user_id_for_connection(request.sender_id)? - }; - self.update_user_contacts(user_id).await?; - response.send(proto::Ack {})?; - Ok(()) - } + // async fn share_project( + // self: Arc, + // request: TypedEnvelope, + // response: Response, + // ) -> Result<()> { + // let user_id = { + // let mut state = self.store_mut().await; + // state.share_project(request.payload.project_id, request.sender_id)?; + // state.user_id_for_connection(request.sender_id)? + // }; + // self.update_user_contacts(user_id).await?; + // response.send(proto::Ack {})?; + // Ok(()) + // } async fn update_user_contacts(self: &Arc, user_id: UserId) -> Result<()> { let contacts = self.app_state.db.get_contacts(user_id).await?; @@ -447,24 +449,6 @@ impl Server { Ok(()) } - async fn unshare_project( - self: Arc, - request: TypedEnvelope, - ) -> Result<()> { - let project_id = request.payload.project_id; - let project; - { - let mut state = self.store_mut().await; - project = state.unshare_project(project_id, request.sender_id)?; - broadcast(request.sender_id, project.connection_ids, |conn_id| { - self.peer - .send(conn_id, proto::UnshareProject { project_id }) - }); - } - self.update_user_contacts(project.host_user_id).await?; - Ok(()) - } - async fn join_project( self: Arc, request: TypedEnvelope, @@ -473,9 +457,12 @@ impl Server { let project_id = request.payload.project_id; let host_user_id; let guest_user_id; + let host_connection_id; { let state = self.store().await; - host_user_id = state.project(project_id)?.host_user_id; + let project = state.project(project_id)?; + host_user_id = project.host_user_id; + host_connection_id = project.host_connection_id; guest_user_id = state.user_id_for_connection(request.sender_id)?; }; @@ -488,22 +475,71 @@ impl Server { return Err(anyhow!("no such project"))?; } + self.store_mut().await.request_join_project( + guest_user_id, + project_id, + response.into_receipt(), + )?; + self.peer.send( + host_connection_id, + proto::RequestJoinProject { + project_id, + 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 state = &mut *self.store_mut().await; - let joined = state.join_project(request.sender_id, guest_user_id, project_id)?; - let share = joined.project.share()?; - let peer_count = share.guests.len(); + let mut state = self.store_mut().await; + let project_id = request.payload.project_id; + let project = state.project(project_id)?; + if project.host_connection_id != request.sender_id { + Err(anyhow!("no such connection"))?; + } + + 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 {}, + )), + }, + )?; + } + return Ok(()); + } + + 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"))?; + + let peer_count = project.guests.len(); let mut collaborators = Vec::with_capacity(peer_count); collaborators.push(proto::Collaborator { - peer_id: joined.project.host_connection_id.0, + peer_id: project.host_connection_id.0, replica_id: 0, - user_id: joined.project.host_user_id.to_proto(), + user_id: project.host_user_id.to_proto(), }); - let worktrees = share + let worktrees = project .worktrees .iter() .filter_map(|(id, shared_worktree)| { - let worktree = joined.project.worktrees.get(&id)?; + let worktree = project.worktrees.get(&id)?; Some(proto::Worktree { id: *id, root_name: worktree.root_name.clone(), @@ -517,8 +553,8 @@ impl Server { scan_id: shared_worktree.scan_id, }) }) - .collect(); - for (peer_conn_id, (peer_replica_id, peer_user_id)) in &share.guests { + .collect::>(); + for (peer_conn_id, (peer_replica_id, peer_user_id)) in &project.guests { if *peer_conn_id != request.sender_id { collaborators.push(proto::Collaborator { peer_id: peer_conn_id.0, @@ -527,30 +563,41 @@ impl Server { }); } } - broadcast( - request.sender_id, - joined.project.connection_ids(), - |conn_id| { - self.peer.send( - conn_id, - proto::AddProjectCollaborator { - project_id, - collaborator: Some(proto::Collaborator { - peer_id: request.sender_id.0, - replica_id: joined.replica_id as u32, - user_id: guest_user_id.to_proto(), - }), - }, - ) - }, - ); - response.send(proto::JoinProjectResponse { - worktrees, - replica_id: joined.replica_id as u32, - collaborators, - language_servers: joined.project.language_servers.clone(), - })?; + 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, + collaborator: Some(proto::Collaborator { + peer_id: receipt.sender_id.0, + replica_id: *replica_id as u32, + user_id: guest_user_id.to_proto(), + }), + }, + )?; + } + } + } + + 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(), + }, + )), + }, + )?; + } } + self.update_user_contacts(host_user_id).await?; Ok(()) } @@ -599,6 +646,7 @@ impl Server { Worktree { root_name: request.payload.root_name.clone(), visible: request.payload.visible, + ..Default::default() }, )?; @@ -2782,9 +2830,6 @@ mod tests { let worktree = store .project(project_id) .unwrap() - .share - .as_ref() - .unwrap() .worktrees .get(&worktree_id.to_proto()) .unwrap(); @@ -5055,7 +5100,7 @@ mod tests { assert_eq!( contacts(store), [ - ("user_a", true, vec![("a", false, vec![])]), + ("user_a", true, vec![("a", vec![])]), ("user_b", true, vec![]), ("user_c", true, vec![]) ] @@ -5077,7 +5122,7 @@ mod tests { assert_eq!( contacts(store), [ - ("user_a", true, vec![("a", true, vec![])]), + ("user_a", true, vec![("a", vec![])]), ("user_b", true, vec![]), ("user_c", true, vec![]) ] @@ -5093,7 +5138,7 @@ mod tests { assert_eq!( contacts(store), [ - ("user_a", true, vec![("a", true, vec!["user_b"])]), + ("user_a", true, vec![("a", vec!["user_b"])]), ("user_b", true, vec![]), ("user_c", true, vec![]) ] @@ -5112,8 +5157,8 @@ mod tests { assert_eq!( contacts(store), [ - ("user_a", true, vec![("a", true, vec!["user_b"])]), - ("user_b", true, vec![("b", false, vec![])]), + ("user_a", true, vec![("a", vec!["user_b"])]), + ("user_b", true, vec![("b", vec![])]), ("user_c", true, vec![]) ] ) @@ -5135,7 +5180,7 @@ mod tests { contacts(store), [ ("user_a", true, vec![]), - ("user_b", true, vec![("b", false, vec![])]), + ("user_b", true, vec![("b", vec![])]), ("user_c", true, vec![]) ] ) @@ -5151,7 +5196,7 @@ mod tests { contacts(store), [ ("user_a", true, vec![]), - ("user_b", true, vec![("b", false, vec![])]), + ("user_b", true, vec![("b", vec![])]), ("user_c", false, vec![]) ] ) @@ -5174,14 +5219,14 @@ mod tests { contacts(store), [ ("user_a", true, vec![]), - ("user_b", true, vec![("b", false, vec![])]), + ("user_b", true, vec![("b", vec![])]), ("user_c", true, vec![]) ] ) }); } - fn contacts(user_store: &UserStore) -> Vec<(&str, bool, Vec<(&str, bool, Vec<&str>)>)> { + fn contacts(user_store: &UserStore) -> Vec<(&str, bool, Vec<(&str, Vec<&str>)>)> { user_store .contacts() .iter() @@ -5192,7 +5237,6 @@ mod tests { .map(|p| { ( p.worktree_root_names[0].as_str(), - p.is_shared, p.guests.iter().map(|p| p.github_login.as_str()).collect(), ) }) diff --git a/crates/collab/src/rpc/store.rs b/crates/collab/src/rpc/store.rs index 07103204e5..765162936c 100644 --- a/crates/collab/src/rpc/store.rs +++ b/crates/collab/src/rpc/store.rs @@ -1,7 +1,7 @@ use crate::db::{self, ChannelId, UserId}; use anyhow::{anyhow, Result}; use collections::{BTreeMap, HashMap, HashSet}; -use rpc::{proto, ConnectionId}; +use rpc::{proto, ConnectionId, Receipt}; use std::{collections::hash_map, path::PathBuf}; use tracing::instrument; @@ -17,31 +17,24 @@ pub struct Store { struct ConnectionState { user_id: UserId, projects: HashSet, + requested_projects: HashSet, channels: HashSet, } pub struct Project { pub host_connection_id: ConnectionId, pub host_user_id: UserId, - pub share: Option, + pub guests: HashMap, + pub join_requests: HashMap>>, + pub active_replica_ids: HashSet, pub worktrees: HashMap, pub language_servers: Vec, } +#[derive(Default)] pub struct Worktree { pub root_name: String, pub visible: bool, -} - -#[derive(Default)] -pub struct ProjectShare { - pub guests: HashMap, - pub active_replica_ids: HashSet, - pub worktrees: HashMap, -} - -#[derive(Default)] -pub struct WorktreeShare { pub entries: HashMap, pub diagnostic_summaries: BTreeMap, pub scan_id: u64, @@ -62,18 +55,6 @@ pub struct RemovedConnectionState { pub contact_ids: HashSet, } -pub struct JoinedProject<'a> { - pub replica_id: ReplicaId, - pub project: &'a Project, -} - -pub struct SharedProject {} - -pub struct UnsharedProject { - pub connection_ids: Vec, - pub host_user_id: UserId, -} - pub struct LeftProject { pub connection_ids: Vec, pub host_user_id: UserId, @@ -93,7 +74,7 @@ impl Store { let mut shared_projects = 0; for project in self.projects.values() { registered_projects += 1; - if project.share.is_some() { + if !project.guests.is_empty() { shared_projects += 1; } } @@ -112,6 +93,7 @@ impl Store { ConnectionState { user_id, projects: Default::default(), + requested_projects: Default::default(), channels: Default::default(), }, ); @@ -275,18 +257,15 @@ impl Store { if project.host_user_id == user_id { metadata.push(proto::ProjectMetadata { id: project_id, - is_shared: project.share.is_some(), worktree_root_names: project .worktrees .values() .map(|worktree| worktree.root_name.clone()) .collect(), guests: project - .share - .iter() - .flat_map(|share| { - share.guests.values().map(|(_, user_id)| user_id.to_proto()) - }) + .guests + .values() + .map(|(_, user_id)| user_id.to_proto()) .collect(), }); } @@ -307,7 +286,9 @@ impl Store { Project { host_connection_id, host_user_id, - share: None, + guests: Default::default(), + join_requests: Default::default(), + active_replica_ids: Default::default(), worktrees: Default::default(), language_servers: Default::default(), }, @@ -332,10 +313,6 @@ impl Store { .ok_or_else(|| anyhow!("no such project"))?; if project.host_connection_id == connection_id { project.worktrees.insert(worktree_id, worktree); - if let Ok(share) = project.share_mut() { - share.worktrees.insert(worktree_id, Default::default()); - } - Ok(()) } else { Err(anyhow!("no such project"))? @@ -356,11 +333,9 @@ impl Store { host_connection.projects.remove(&project_id); } - if let Some(share) = &project.share { - for guest_connection in share.guests.keys() { - if let Some(connection) = self.connections.get_mut(&guest_connection) { - connection.projects.remove(&project_id); - } + for guest_connection in project.guests.keys() { + if let Some(connection) = self.connections.get_mut(&guest_connection) { + connection.projects.remove(&project_id); } } @@ -391,64 +366,7 @@ impl Store { .worktrees .remove(&worktree_id) .ok_or_else(|| anyhow!("no such worktree"))?; - - let mut guest_connection_ids = Vec::new(); - if let Ok(share) = project.share_mut() { - guest_connection_ids.extend(share.guests.keys()); - share.worktrees.remove(&worktree_id); - } - - Ok((worktree, guest_connection_ids)) - } - - pub fn share_project( - &mut self, - project_id: u64, - connection_id: ConnectionId, - ) -> Result { - if let Some(project) = self.projects.get_mut(&project_id) { - if project.host_connection_id == connection_id { - let mut share = ProjectShare::default(); - for worktree_id in project.worktrees.keys() { - share.worktrees.insert(*worktree_id, Default::default()); - } - project.share = Some(share); - return Ok(SharedProject {}); - } - } - Err(anyhow!("no such project"))? - } - - pub fn unshare_project( - &mut self, - project_id: u64, - acting_connection_id: ConnectionId, - ) -> Result { - let project = if let Some(project) = self.projects.get_mut(&project_id) { - project - } else { - return Err(anyhow!("no such project"))?; - }; - - if project.host_connection_id != acting_connection_id { - return Err(anyhow!("not your project"))?; - } - - let connection_ids = project.connection_ids(); - if let Some(share) = project.share.take() { - for connection_id in share.guests.into_keys() { - if let Some(connection) = self.connections.get_mut(&connection_id) { - connection.projects.remove(&project_id); - } - } - - Ok(UnsharedProject { - connection_ids, - host_user_id: project.host_user_id, - }) - } else { - Err(anyhow!("project is not shared"))? - } + Ok((worktree, project.guest_connection_ids())) } pub fn update_diagnostic_summary( @@ -464,7 +382,6 @@ impl Store { .ok_or_else(|| anyhow!("no such project"))?; if project.host_connection_id == connection_id { let worktree = project - .share_mut()? .worktrees .get_mut(&worktree_id) .ok_or_else(|| anyhow!("no such worktree"))?; @@ -495,35 +412,77 @@ impl Store { Err(anyhow!("no such project"))? } - pub fn join_project( + pub fn request_join_project( &mut self, - connection_id: ConnectionId, - user_id: UserId, + requester_id: UserId, project_id: u64, - ) -> Result { + receipt: Receipt, + ) -> Result<()> { let connection = self .connections - .get_mut(&connection_id) + .get_mut(&receipt.sender_id) .ok_or_else(|| anyhow!("no such connection"))?; let project = self .projects .get_mut(&project_id) .ok_or_else(|| anyhow!("no such project"))?; + connection.requested_projects.insert(project_id); + project + .join_requests + .entry(requester_id) + .or_default() + .push(receipt); + Ok(()) + } - let share = project.share_mut()?; - connection.projects.insert(project_id); - - let mut replica_id = 1; - while share.active_replica_ids.contains(&replica_id) { - replica_id += 1; + pub fn deny_join_project_request( + &mut self, + responder_connection_id: ConnectionId, + requester_id: UserId, + project_id: u64, + ) -> Option>> { + let project = self.projects.get_mut(&project_id)?; + if responder_connection_id != project.host_connection_id { + return None; } - share.active_replica_ids.insert(replica_id); - share.guests.insert(connection_id, (replica_id, user_id)); - Ok(JoinedProject { - replica_id, - project: &self.projects[&project_id], - }) + 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); + } + Some(receipts) + } + + pub fn accept_join_project_request( + &mut self, + responder_connection_id: ConnectionId, + requester_id: UserId, + project_id: u64, + ) -> 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, (replica_id, requester_id)); + receipts_with_replica_ids.push((receipt, replica_id)); + } + + Some((receipts_with_replica_ids, project)) } pub fn leave_project( @@ -535,15 +494,11 @@ impl Store { .projects .get_mut(&project_id) .ok_or_else(|| anyhow!("no such project"))?; - let share = project - .share - .as_mut() - .ok_or_else(|| anyhow!("project is not shared"))?; - let (replica_id, _) = share + let (replica_id, _) = project .guests .remove(&connection_id) .ok_or_else(|| anyhow!("cannot leave a project before joining it"))?; - share.active_replica_ids.remove(&replica_id); + project.active_replica_ids.remove(&replica_id); if let Some(connection) = self.connections.get_mut(&connection_id) { connection.projects.remove(&project_id); @@ -566,7 +521,6 @@ impl Store { ) -> Result> { let project = self.write_project(project_id, connection_id)?; let worktree = project - .share_mut()? .worktrees .get_mut(&worktree_id) .ok_or_else(|| anyhow!("no such worktree"))?; @@ -611,12 +565,7 @@ impl Store { .get(&project_id) .ok_or_else(|| anyhow!("no such project"))?; if project.host_connection_id == connection_id - || project - .share - .as_ref() - .ok_or_else(|| anyhow!("project is not shared"))? - .guests - .contains_key(&connection_id) + || project.guests.contains_key(&connection_id) { Ok(project) } else { @@ -634,12 +583,7 @@ impl Store { .get_mut(&project_id) .ok_or_else(|| anyhow!("no such project"))?; if project.host_connection_id == connection_id - || project - .share - .as_ref() - .ok_or_else(|| anyhow!("project is not shared"))? - .guests - .contains_key(&connection_id) + || project.guests.contains_key(&connection_id) { Ok(project) } else { @@ -653,28 +597,21 @@ impl Store { for project_id in &connection.projects { let project = &self.projects.get(&project_id).unwrap(); if project.host_connection_id != *connection_id { - assert!(project - .share - .as_ref() - .unwrap() - .guests - .contains_key(connection_id)); + assert!(project.guests.contains_key(connection_id)); } - if let Some(share) = project.share.as_ref() { - for (worktree_id, worktree) in share.worktrees.iter() { - let mut paths = HashMap::default(); - for entry in worktree.entries.values() { - let prev_entry = paths.insert(&entry.path, entry); - assert_eq!( - prev_entry, - None, - "worktree {:?}, duplicate path for entries {:?} and {:?}", - worktree_id, - prev_entry.unwrap(), - entry - ); - } + for (worktree_id, worktree) in project.worktrees.iter() { + let mut paths = HashMap::default(); + for entry in worktree.entries.values() { + let prev_entry = paths.insert(&entry.path, entry); + assert_eq!( + prev_entry, + None, + "worktree {:?}, duplicate path for entries {:?} and {:?}", + worktree_id, + prev_entry.unwrap(), + entry + ); } } } @@ -702,21 +639,19 @@ impl Store { let host_connection = self.connections.get(&project.host_connection_id).unwrap(); assert!(host_connection.projects.contains(project_id)); - if let Some(share) = &project.share { - for guest_connection_id in share.guests.keys() { - let guest_connection = self.connections.get(guest_connection_id).unwrap(); - assert!(guest_connection.projects.contains(project_id)); - } - assert_eq!(share.active_replica_ids.len(), share.guests.len(),); - assert_eq!( - share.active_replica_ids, - share - .guests - .values() - .map(|(replica_id, _)| *replica_id) - .collect::>(), - ); + for guest_connection_id in project.guests.keys() { + let guest_connection = self.connections.get(guest_connection_id).unwrap(); + assert!(guest_connection.projects.contains(project_id)); } + assert_eq!(project.active_replica_ids.len(), project.guests.len(),); + assert_eq!( + project.active_replica_ids, + project + .guests + .values() + .map(|(replica_id, _)| *replica_id) + .collect::>(), + ); } for (channel_id, channel) in &self.channels { @@ -730,38 +665,15 @@ impl Store { impl Project { pub fn guest_connection_ids(&self) -> Vec { - if let Some(share) = &self.share { - share.guests.keys().copied().collect() - } else { - Vec::new() - } + self.guests.keys().copied().collect() } pub fn connection_ids(&self) -> Vec { - if let Some(share) = &self.share { - share - .guests - .keys() - .copied() - .chain(Some(self.host_connection_id)) - .collect() - } else { - vec![self.host_connection_id] - } - } - - pub fn share(&self) -> Result<&ProjectShare> { - Ok(self - .share - .as_ref() - .ok_or_else(|| anyhow!("worktree is not shared"))?) - } - - fn share_mut(&mut self) -> Result<&mut ProjectShare> { - Ok(self - .share - .as_mut() - .ok_or_else(|| anyhow!("worktree is not shared"))?) + self.guests + .keys() + .copied() + .chain(Some(self.host_connection_id)) + .collect() } } diff --git a/crates/contacts_panel/Cargo.toml b/crates/contacts_panel/Cargo.toml index de49f070b9..b6d7bf63fc 100644 --- a/crates/contacts_panel/Cargo.toml +++ b/crates/contacts_panel/Cargo.toml @@ -13,6 +13,7 @@ editor = { path = "../editor" } fuzzy = { path = "../fuzzy" } gpui = { path = "../gpui" } picker = { path = "../picker" } +project = { path = "../project" } settings = { path = "../settings" } theme = { path = "../theme" } util = { path = "../util" } diff --git a/crates/contacts_panel/src/contact_notification.rs b/crates/contacts_panel/src/contact_notification.rs index 6369f70ce0..7df0f7cad9 100644 --- a/crates/contacts_panel/src/contact_notification.rs +++ b/crates/contacts_panel/src/contact_notification.rs @@ -1,13 +1,11 @@ +use crate::notifications::render_user_notification; use client::{ContactEvent, ContactEventKind, UserStore}; use gpui::{ - elements::*, impl_internal_actions, platform::CursorStyle, Entity, ModelHandle, - MutableAppContext, RenderContext, View, ViewContext, + elements::*, impl_internal_actions, Entity, ModelHandle, MutableAppContext, RenderContext, + View, ViewContext, }; -use settings::Settings; use workspace::Notification; -use crate::render_icon_button; - impl_internal_actions!(contact_notifications, [Dismiss, RespondToContactRequest]); pub fn init(cx: &mut MutableAppContext) { @@ -33,9 +31,6 @@ pub enum Event { Dismiss, } -enum Decline {} -enum Accept {} - impl Entity for ContactNotification { type Event = Event; } @@ -47,8 +42,38 @@ impl View for ContactNotification { fn render(&mut self, cx: &mut RenderContext) -> ElementBox { match self.event.kind { - ContactEventKind::Requested => self.render_incoming_request(cx), - ContactEventKind::Accepted => self.render_acceptance(cx), + ContactEventKind::Requested => render_user_notification( + self.event.user.clone(), + "wants to add you as a contact", + RespondToContactRequest { + user_id: self.event.user.id, + accept: false, + }, + vec![ + ( + "Decline", + Box::new(RespondToContactRequest { + user_id: self.event.user.id, + accept: false, + }), + ), + ( + "Accept", + Box::new(RespondToContactRequest { + user_id: self.event.user.id, + accept: true, + }), + ), + ], + cx, + ), + ContactEventKind::Accepted => render_user_notification( + self.event.user.clone(), + "accepted your contact request", + Dismiss(self.event.user.id), + vec![], + cx, + ), _ => unreachable!(), } } @@ -82,138 +107,6 @@ impl ContactNotification { Self { event, user_store } } - fn render_incoming_request(&mut self, cx: &mut RenderContext) -> ElementBox { - let theme = cx.global::().theme.clone(); - let theme = &theme.contact_notification; - let user = &self.event.user; - let user_id = user.id; - - Flex::column() - .with_child(self.render_header("wants to add you as a contact.", theme, cx)) - .with_child( - Label::new( - "They won't know if you decline.".to_string(), - theme.body_message.text.clone(), - ) - .contained() - .with_style(theme.body_message.container) - .boxed(), - ) - .with_child( - Flex::row() - .with_child( - MouseEventHandler::new::( - self.event.user.id as usize, - cx, - |state, _| { - let button = theme.button.style_for(state, false); - Label::new("Decline".to_string(), button.text.clone()) - .contained() - .with_style(button.container) - .boxed() - }, - ) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(move |_, cx| { - cx.dispatch_action(RespondToContactRequest { - user_id, - accept: false, - }); - }) - .boxed(), - ) - .with_child( - MouseEventHandler::new::(user.id as usize, cx, |state, _| { - let button = theme.button.style_for(state, false); - Label::new("Accept".to_string(), button.text.clone()) - .contained() - .with_style(button.container) - .boxed() - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(move |_, cx| { - cx.dispatch_action(RespondToContactRequest { - user_id, - accept: true, - }); - }) - .boxed(), - ) - .aligned() - .right() - .boxed(), - ) - .contained() - .boxed() - } - - fn render_acceptance(&mut self, cx: &mut RenderContext) -> ElementBox { - let theme = cx.global::().theme.clone(); - let theme = &theme.contact_notification; - - self.render_header("accepted your contact request", theme, cx) - } - - fn render_header( - &self, - message: &'static str, - theme: &theme::ContactNotification, - cx: &mut RenderContext, - ) -> ElementBox { - let user = &self.event.user; - let user_id = user.id; - Flex::row() - .with_children(user.avatar.clone().map(|avatar| { - Image::new(avatar) - .with_style(theme.header_avatar) - .aligned() - .constrained() - .with_height( - cx.font_cache() - .line_height(theme.header_message.text.font_size), - ) - .aligned() - .top() - .boxed() - })) - .with_child( - Text::new( - format!("{} {}", user.github_login, message), - theme.header_message.text.clone(), - ) - .contained() - .with_style(theme.header_message.container) - .aligned() - .top() - .left() - .flex(1., true) - .boxed(), - ) - .with_child( - MouseEventHandler::new::(user.id as usize, cx, |state, _| { - render_icon_button( - theme.dismiss_button.style_for(state, false), - "icons/decline.svg", - ) - .boxed() - }) - .with_cursor_style(CursorStyle::PointingHand) - .with_padding(Padding::uniform(5.)) - .on_click(move |_, cx| cx.dispatch_action(Dismiss(user_id))) - .aligned() - .constrained() - .with_height( - cx.font_cache() - .line_height(theme.header_message.text.font_size), - ) - .aligned() - .top() - .flex_float() - .boxed(), - ) - .named("contact notification header") - } - fn dismiss(&mut self, _: &Dismiss, cx: &mut ViewContext) { self.user_store.update(cx, |store, cx| { store diff --git a/crates/contacts_panel/src/contacts_panel.rs b/crates/contacts_panel/src/contacts_panel.rs index 03968d5aaa..9ea62f0831 100644 --- a/crates/contacts_panel/src/contacts_panel.rs +++ b/crates/contacts_panel/src/contacts_panel.rs @@ -1,5 +1,7 @@ mod contact_finder; mod contact_notification; +mod join_project_notification; +mod notifications; use client::{Contact, ContactEventKind, User, UserStore}; use contact_notification::ContactNotification; @@ -13,6 +15,7 @@ use gpui::{ AppContext, Element, ElementBox, Entity, LayoutContext, ModelHandle, MutableAppContext, RenderContext, Subscription, View, ViewContext, ViewHandle, WeakViewHandle, }; +use join_project_notification::JoinProjectNotification; use serde::Deserialize; use settings::Settings; use std::sync::Arc; @@ -76,6 +79,7 @@ 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); @@ -118,6 +122,33 @@ 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.clone(); + move |_, project, event, cx| match event { + project::Event::ContactRequestedJoin(user) => { + if let Some(workspace) = workspace.upgrade(cx) { + workspace.update(cx, |workspace, cx| { + workspace.show_notification( + cx.add_view(|_| { + JoinProjectNotification::new(project, user.clone()) + }), + cx, + ) + }); + } + } + _ => {} + } + }) + .detach(); + } + } + }); + cx.subscribe(&app_state.user_store, { let user_store = app_state.user_store.downgrade(); move |_, _, event, cx| { @@ -320,7 +351,6 @@ impl ContactsPanel { .guests .iter() .any(|guest| Some(guest.id) == current_user_id); - let is_shared = project.is_shared; let font_cache = cx.font_cache(); let host_avatar_height = theme @@ -328,7 +358,7 @@ impl ContactsPanel { .width .or(theme.contact_avatar.height) .unwrap_or(0.); - let row = &theme.unshared_project_row.default; + let row = &theme.project_row.default; let tree_branch = theme.tree_branch.clone(); let line_height = row.name.text.line_height(font_cache); let cap_height = row.name.text.cap_height(font_cache); @@ -337,12 +367,7 @@ impl ContactsPanel { MouseEventHandler::new::(project_id as usize, cx, |mouse_state, _| { let tree_branch = *tree_branch.style_for(mouse_state, is_selected); - let row = if project.is_shared { - &theme.shared_project_row - } else { - &theme.unshared_project_row - } - .style_for(mouse_state, is_selected); + let row = theme.project_row.style_for(mouse_state, is_selected); Flex::row() .with_child( @@ -412,7 +437,7 @@ impl ContactsPanel { .with_style(row.container) .boxed() }) - .with_cursor_style(if !is_host && is_shared { + .with_cursor_style(if !is_host { CursorStyle::PointingHand } else { CursorStyle::Arrow @@ -947,7 +972,6 @@ mod tests { projects: vec![proto::ProjectMetadata { id: 101, worktree_root_names: vec!["dir1".to_string()], - is_shared: true, guests: vec![2], }], }, @@ -958,7 +982,6 @@ mod tests { projects: vec![proto::ProjectMetadata { id: 102, worktree_root_names: vec!["dir2".to_string()], - is_shared: true, guests: vec![2], }], }, diff --git a/crates/contacts_panel/src/join_project_notification.rs b/crates/contacts_panel/src/join_project_notification.rs new file mode 100644 index 0000000000..e3cd1c6c9e --- /dev/null +++ b/crates/contacts_panel/src/join_project_notification.rs @@ -0,0 +1,71 @@ +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) -> Self { + 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", + 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/contacts_panel/src/notifications.rs b/crates/contacts_panel/src/notifications.rs new file mode 100644 index 0000000000..21eac2a3c4 --- /dev/null +++ b/crates/contacts_panel/src/notifications.rs @@ -0,0 +1,112 @@ +use crate::render_icon_button; +use client::User; +use gpui::{ + elements::{Flex, Image, Label, MouseEventHandler, Padding, ParentElement, Text}, + platform::CursorStyle, + Action, Element, ElementBox, RenderContext, View, +}; +use settings::Settings; +use std::sync::Arc; + +enum Dismiss {} +enum Button {} + +pub fn render_user_notification( + user: Arc, + message: &str, + dismiss_action: A, + buttons: Vec<(&'static str, Box)>, + cx: &mut RenderContext, +) -> ElementBox { + let theme = cx.global::().theme.clone(); + let theme = &theme.contact_notification; + + Flex::column() + .with_child( + Flex::row() + .with_children(user.avatar.clone().map(|avatar| { + Image::new(avatar) + .with_style(theme.header_avatar) + .aligned() + .constrained() + .with_height( + cx.font_cache() + .line_height(theme.header_message.text.font_size), + ) + .aligned() + .top() + .boxed() + })) + .with_child( + Text::new( + format!("{} {}", user.github_login, message), + theme.header_message.text.clone(), + ) + .contained() + .with_style(theme.header_message.container) + .aligned() + .top() + .left() + .flex(1., true) + .boxed(), + ) + .with_child( + MouseEventHandler::new::(user.id as usize, cx, |state, _| { + render_icon_button( + theme.dismiss_button.style_for(state, false), + "icons/decline.svg", + ) + .boxed() + }) + .with_cursor_style(CursorStyle::PointingHand) + .with_padding(Padding::uniform(5.)) + .on_click(move |_, cx| cx.dispatch_any_action(dismiss_action.boxed_clone())) + .aligned() + .constrained() + .with_height( + cx.font_cache() + .line_height(theme.header_message.text.font_size), + ) + .aligned() + .top() + .flex_float() + .boxed(), + ) + .named("contact notification header"), + ) + .with_child( + Label::new( + "They won't know if you decline.".to_string(), + theme.body_message.text.clone(), + ) + .contained() + .with_style(theme.body_message.container) + .boxed(), + ) + .with_children(if buttons.is_empty() { + None + } else { + Some( + Flex::row() + .with_children(buttons.into_iter().enumerate().map( + |(ix, (message, action))| { + MouseEventHandler::new::(ix, cx, |state, _| { + let button = theme.button.style_for(state, false); + Label::new(message.to_string(), button.text.clone()) + .contained() + .with_style(button.container) + .boxed() + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(move |_, cx| cx.dispatch_any_action(action.boxed_clone())) + .boxed() + }, + )) + .aligned() + .right() + .boxed(), + ) + }) + .contained() + .boxed() +} diff --git a/crates/gpui/src/presenter.rs b/crates/gpui/src/presenter.rs index a7d715fadf..7b16ad4869 100644 --- a/crates/gpui/src/presenter.rs +++ b/crates/gpui/src/presenter.rs @@ -386,13 +386,17 @@ impl<'a> EventContext<'a> { } } - pub fn dispatch_action(&mut self, action: A) { + pub fn dispatch_any_action(&mut self, action: Box) { self.dispatched_actions.push(DispatchDirective { path: self.view_stack.clone(), - action: Box::new(action), + action, }); } + pub fn dispatch_action(&mut self, action: A) { + self.dispatch_any_action(Box::new(action)); + } + pub fn notify(&mut self) { self.notify_count += 1; if let Some(view_id) = self.view_stack.last() { diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index d23122f45b..cc73aadb29 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -133,6 +133,7 @@ pub enum Event { DiagnosticsUpdated(ProjectPath), RemoteIdChanged(Option), CollaboratorLeft(PeerId), + ContactRequestedJoin(Arc), } #[derive(Serialize)] @@ -248,6 +249,7 @@ impl ProjectEntryId { 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); @@ -256,7 +258,7 @@ impl Project { client.add_model_message_handler(Self::handle_remove_collaborator); client.add_model_message_handler(Self::handle_register_worktree); client.add_model_message_handler(Self::handle_unregister_worktree); - client.add_model_message_handler(Self::handle_unshare_project); + client.add_model_message_handler(Self::handle_unregister_project); client.add_model_message_handler(Self::handle_update_buffer_file); client.add_model_message_handler(Self::handle_update_buffer); client.add_model_message_handler(Self::handle_update_diagnostic_summary); @@ -362,6 +364,11 @@ 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(_) => Err(anyhow!("rejected"))?, + }; + let replica_id = response.replica_id as ReplicaId; let mut worktrees = Vec::new(); @@ -400,7 +407,7 @@ impl Project { // 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) { - this.update(&mut cx, |this, cx| this.project_unshared(cx)) + this.update(&mut cx, |this, cx| this.removed_from_project(cx)) } } Ok(()) @@ -814,60 +821,59 @@ impl Project { self.is_local() && self.visible_worktrees(cx).next().is_some() } - pub fn share(&self, cx: &mut ModelContext) -> Task> { - let rpc = self.client.clone(); - cx.spawn(|this, mut cx| async move { - let project_id = this.update(&mut cx, |this, cx| { - if let ProjectClientState::Local { - is_shared, - remote_id_rx, - .. - } = &mut this.client_state - { - *is_shared = true; + 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"))); + } + } else { + return Task::ready(Err(anyhow!("can't share a remote project"))); + }; - 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::Loading(_) => unreachable!(), - } + 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::Loading(_) => 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); } - - remote_id_rx - .borrow() - .ok_or_else(|| anyhow!("no project id")) - } else { - Err(anyhow!("can't share a remote project")) } - })?; + } + } - rpc.request(proto::ShareProject { project_id }).await?; - - let mut tasks = Vec::new(); - this.update(&mut cx, |this, cx| { - for worktree in this.worktrees(cx).collect::>() { - worktree.update(cx, |worktree, cx| { - let worktree = worktree.as_local_mut().unwrap(); - tasks.push(worktree.share(project_id, cx)); - }); - } + 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)); }); + } + + cx.spawn(|this, mut cx| async move { for task in tasks { task.await?; } @@ -877,14 +883,7 @@ impl Project { } pub fn unshare(&mut self, cx: &mut ModelContext) { - let rpc = self.client.clone(); - - if let ProjectClientState::Local { - is_shared, - remote_id_rx, - .. - } = &mut self.client_state - { + if let ProjectClientState::Local { is_shared, .. } = &mut self.client_state { if !*is_shared { return; } @@ -913,17 +912,35 @@ impl Project { } } - if let Some(project_id) = *remote_id_rx.borrow() { - rpc.send(proto::UnshareProject { project_id }).log_err(); - } - cx.notify(); } else { log::error!("attempted to unshare a remote project"); } } - fn project_unshared(&mut self, cx: &mut ModelContext) { + 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 = self.share(cx); + let client = self.client.clone(); + cx.foreground() + .spawn(async move { + share.await?; + client.send(proto::RespondToJoinProjectRequest { + requester_id, + project_id, + allow, + }) + }) + .detach_and_log_err(cx); + } + } + + fn removed_from_project(&mut self, cx: &mut ModelContext) { if let ProjectClientState::Remote { sharing_has_stopped, .. @@ -3745,13 +3762,28 @@ impl Project { // RPC message handlers - async fn handle_unshare_project( + async fn handle_request_join_project( this: ModelHandle, - _: TypedEnvelope, + message: TypedEnvelope, _: Arc, mut cx: AsyncAppContext, ) -> Result<()> { - this.update(&mut cx, |this, cx| this.project_unshared(cx)); + let user_id = message.payload.requester_id; + 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)) + .await?; + this.update(&mut cx, |_, cx| cx.emit(Event::ContactRequestedJoin(user))); + Ok(()) + } + + async fn handle_unregister_project( + this: ModelHandle, + _: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result<()> { + this.update(&mut cx, |this, cx| this.removed_from_project(cx)); Ok(()) } @@ -3796,6 +3828,9 @@ impl Project { buffer.update(cx, |buffer, cx| buffer.remove_peer(replica_id, cx)); } } + if this.collaborators.is_empty() { + this.unshare(cx); + } cx.emit(Event::CollaboratorLeft(peer_id)); cx.notify(); Ok(()) diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 12ff05c757..f81fabadae 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -14,8 +14,8 @@ message Envelope { RegisterProject register_project = 8; RegisterProjectResponse register_project_response = 9; UnregisterProject unregister_project = 10; - ShareProject share_project = 11; - UnshareProject unshare_project = 12; + RequestJoinProject request_join_project = 11; + RespondToJoinProjectRequest respond_to_join_project_request = 12; JoinProject join_project = 13; JoinProjectResponse join_project_response = 14; LeaveProject leave_project = 15; @@ -124,12 +124,15 @@ message UnregisterProject { uint64 project_id = 1; } -message ShareProject { - uint64 project_id = 1; +message RequestJoinProject { + uint64 requester_id = 1; + uint64 project_id = 2; } -message UnshareProject { - uint64 project_id = 1; +message RespondToJoinProjectRequest { + uint64 requester_id = 1; + uint64 project_id = 2; + bool allow = 3; } message JoinProject { @@ -137,10 +140,19 @@ message JoinProject { } message JoinProjectResponse { - uint32 replica_id = 1; - repeated Worktree worktrees = 2; - repeated Collaborator collaborators = 3; - repeated LanguageServer language_servers = 4; + oneof variant { + Accept accept = 1; + Decline decline = 2; + } + + message Accept { + uint32 replica_id = 1; + repeated Worktree worktrees = 2; + repeated Collaborator collaborators = 3; + repeated LanguageServer language_servers = 4; + } + + message Decline {} } message LeaveProject { @@ -882,7 +894,6 @@ message Contact { message ProjectMetadata { uint64 id = 1; - bool is_shared = 2; repeated string worktree_root_names = 3; repeated uint64 guests = 4; } diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index 0b7ba21c4a..7d12136255 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -135,19 +135,19 @@ messages!( (RemoveProjectCollaborator, Foreground), (RenameProjectEntry, Foreground), (RequestContact, Foreground), + (RequestJoinProject, Foreground), (RespondToContactRequest, Foreground), + (RespondToJoinProjectRequest, Foreground), (SaveBuffer, Foreground), (SearchProject, Background), (SearchProjectResponse, Background), (SendChannelMessage, Foreground), (SendChannelMessageResponse, Foreground), - (ShareProject, Foreground), (StartLanguageServer, Foreground), (Test, Foreground), (Unfollow, Foreground), (UnregisterProject, Foreground), (UnregisterWorktree, Foreground), - (UnshareProject, Foreground), (UpdateBuffer, Foreground), (UpdateBufferFile, Foreground), (UpdateContacts, Foreground), @@ -195,7 +195,6 @@ request_messages!( (SaveBuffer, BufferSaved), (SearchProject, SearchProjectResponse), (SendChannelMessage, SendChannelMessageResponse), - (ShareProject, Ack), (Test, Test), (UpdateBuffer, Ack), (UpdateWorktree, Ack), @@ -228,12 +227,13 @@ entity_messages!( PrepareRename, ReloadBuffers, RemoveProjectCollaborator, + RequestJoinProject, SaveBuffer, SearchProject, StartLanguageServer, Unfollow, + UnregisterProject, UnregisterWorktree, - UnshareProject, UpdateBuffer, UpdateBufferFile, UpdateDiagnosticSummary, diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index c6ad68ace6..8ca7ce9ae2 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -251,8 +251,7 @@ pub struct ContactsPanel { pub add_contact_button: IconButton, pub header_row: Interactive, pub contact_row: Interactive, - pub shared_project_row: Interactive, - pub unshared_project_row: Interactive, + pub project_row: Interactive, pub row_height: f32, pub contact_avatar: ImageStyle, pub contact_username: ContainedText, diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index fada690bb5..26b355ae0a 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -72,7 +72,6 @@ type FollowableItemBuilders = HashMap< actions!( workspace, [ - ToggleShare, Unfollow, Save, ActivatePreviousPane, @@ -121,7 +120,6 @@ pub fn init(client: &Arc, cx: &mut MutableAppContext) { join_project(action.project_id, &action.app_state, cx).detach(); }); - cx.add_action(Workspace::toggle_share); cx.add_async_action(Workspace::toggle_follow); cx.add_async_action(Workspace::follow_next_collaborator); cx.add_action( @@ -692,6 +690,7 @@ impl WorkspaceParams { pub enum Event { PaneAdded(ViewHandle), + ContactRequestedJoin(u64), } pub struct Workspace { @@ -1366,18 +1365,6 @@ impl Workspace { &self.active_pane } - fn toggle_share(&mut self, _: &ToggleShare, cx: &mut ViewContext) { - self.project.update(cx, |project, cx| { - if project.is_local() { - if project.is_shared() { - project.unshare(cx); - } else if project.can_share(cx) { - project.share(cx).detach(); - } - } - }); - } - fn project_remote_id_changed(&mut self, remote_id: Option, cx: &mut ViewContext) { if let Some(remote_id) = remote_id { self.remote_entity_subscription = @@ -1580,7 +1567,6 @@ impl Workspace { cx, )) .with_children(self.render_connection_status(cx)) - .with_children(self.render_share_icon(theme, cx)) .boxed(), ) .right() @@ -1701,39 +1687,6 @@ impl Workspace { } } - fn render_share_icon(&self, theme: &Theme, cx: &mut RenderContext) -> Option { - if self.project().read(cx).is_local() - && self.client.user_id().is_some() - && self.project().read(cx).can_share(cx) - { - Some( - MouseEventHandler::new::(0, cx, |state, cx| { - let style = &theme - .workspace - .titlebar - .share_icon - .style_for(state, self.project().read(cx).is_shared()); - Svg::new("icons/share.svg") - .with_color(style.color) - .constrained() - .with_height(14.) - .aligned() - .contained() - .with_style(style.container) - .constrained() - .with_width(24.) - .aligned() - .boxed() - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(|_, cx| cx.dispatch_action(ToggleShare)) - .boxed(), - ) - } else { - None - } - } - fn render_disconnected_overlay(&self, cx: &AppContext) -> Option { if self.project.read(cx).is_read_only() { let theme = &cx.global::().theme; diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index d4938501b8..2a1fd0a7f2 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -123,17 +123,18 @@ pub fn build_workspace( cx.subscribe(&cx.handle(), { let project = project.clone(); move |_, _, event, cx| { - let workspace::Event::PaneAdded(pane) = event; - pane.update(cx, |pane, cx| { - pane.toolbar().update(cx, |toolbar, cx| { - let breadcrumbs = cx.add_view(|_| Breadcrumbs::new(project.clone())); - toolbar.add_item(breadcrumbs, cx); - let buffer_search_bar = cx.add_view(|cx| BufferSearchBar::new(cx)); - toolbar.add_item(buffer_search_bar, cx); - let project_search_bar = cx.add_view(|_| ProjectSearchBar::new()); - toolbar.add_item(project_search_bar, cx); - }) - }); + if let workspace::Event::PaneAdded(pane) = event { + pane.update(cx, |pane, cx| { + pane.toolbar().update(cx, |toolbar, cx| { + let breadcrumbs = cx.add_view(|_| Breadcrumbs::new(project.clone())); + toolbar.add_item(breadcrumbs, cx); + let buffer_search_bar = cx.add_view(|cx| BufferSearchBar::new(cx)); + toolbar.add_item(buffer_search_bar, cx); + let project_search_bar = cx.add_view(|_| ProjectSearchBar::new()); + toolbar.add_item(project_search_bar, cx); + }) + }); + } } }) .detach(); diff --git a/styles/src/styleTree/contactsPanel.ts b/styles/src/styleTree/contactsPanel.ts index a2caafadec..421afd1967 100644 --- a/styles/src/styleTree/contactsPanel.ts +++ b/styles/src/styleTree/contactsPanel.ts @@ -122,7 +122,7 @@ export default function contactsPanel(theme: Theme) { background: backgroundColor(theme, 100), color: iconColor(theme, "muted"), }, - sharedProjectRow: { + projectRow: { ...projectRow, background: backgroundColor(theme, 300), name: { @@ -136,19 +136,5 @@ export default function contactsPanel(theme: Theme) { background: backgroundColor(theme, 300, "active"), } }, - unsharedProjectRow: { - ...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"), - } - } } }