diff --git a/crates/call/src/participant.rs b/crates/call/src/participant.rs index db7a84e584..b124d920a3 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 9cad1ff211..fb9ba09d81 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 87ae24bb3d..06e009d08b 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 05bcd6875d..11219fb8b8 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 5b23ac92d5..522438255a 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 5597ea0a01..e5ff27060a 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 f9a9fbd34e..b1897653c9 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 2f19304829..50e2462c15 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 7a70f0f0d3..abe77e7d56 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"),