Show worktree root names when sharing additional projects on a call

Co-Authored-By: Antonio Scandurra <antonio@zed.dev>
This commit is contained in:
Nathan Sobo 2022-10-10 17:56:03 -06:00
parent 94c68d246e
commit b8c2acf0f2
9 changed files with 153 additions and 35 deletions

View file

@ -23,6 +23,6 @@ impl ParticipantLocation {
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct RemoteParticipant { pub struct RemoteParticipant {
pub user: Arc<User>, pub user: Arc<User>,
pub project_ids: Vec<u64>, pub projects: Vec<proto::ParticipantProject>,
pub location: ParticipantLocation, pub location: ParticipantLocation,
} }

View file

@ -13,7 +13,11 @@ use util::ResultExt;
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
pub enum Event { pub enum Event {
RemoteProjectShared { owner: Arc<User>, project_id: u64 }, RemoteProjectShared {
owner: Arc<User>,
project_id: u64,
worktree_root_names: Vec<String>,
},
} }
pub struct Room { pub struct Room {
@ -219,16 +223,19 @@ impl Room {
let peer_id = PeerId(participant.peer_id); let peer_id = PeerId(participant.peer_id);
this.participant_user_ids.insert(participant.user_id); this.participant_user_ids.insert(participant.user_id);
let existing_project_ids = this let existing_projects = this
.remote_participants .remote_participants
.get(&peer_id) .get(&peer_id)
.map(|existing| existing.project_ids.clone()) .into_iter()
.unwrap_or_default(); .flat_map(|existing| &existing.projects)
for project_id in &participant.project_ids { .map(|project| project.id)
if !existing_project_ids.contains(project_id) { .collect::<HashSet<_>>();
for project in &participant.projects {
if !existing_projects.contains(&project.id) {
cx.emit(Event::RemoteProjectShared { cx.emit(Event::RemoteProjectShared {
owner: user.clone(), 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, peer_id,
RemoteParticipant { RemoteParticipant {
user: user.clone(), user: user.clone(),
project_ids: participant.project_ids, projects: participant.projects,
location: ParticipantLocation::from_proto(participant.location) location: ParticipantLocation::from_proto(participant.location)
.unwrap_or(ParticipantLocation::External), .unwrap_or(ParticipantLocation::External),
}, },
@ -334,9 +341,21 @@ impl Room {
return Task::ready(Ok(project_id)); return Task::ready(Ok(project_id));
} }
let request = self let request = self.client.request(proto::ShareProject {
.client room_id: self.id(),
.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 { cx.spawn_weak(|_, mut cx| async move {
let response = request.await?; let response = request.await?;
project project

View file

@ -819,6 +819,7 @@ async fn test_active_call_events(
avatar: None, avatar: None,
}), }),
project_id: project_a_id, project_id: project_a_id,
worktree_root_names: vec!["a".to_string()],
}] }]
); );
@ -836,6 +837,7 @@ async fn test_active_call_events(
avatar: None, avatar: None,
}), }),
project_id: project_b_id, project_id: project_b_id,
worktree_root_names: vec!["b".to_string()]
}] }]
); );
assert_eq!(mem::take(&mut *events_b.borrow_mut()), vec![]); assert_eq!(mem::take(&mut *events_b.borrow_mut()), vec![]);

View file

@ -827,7 +827,12 @@ impl Server {
.user_id_for_connection(request.sender_id)?; .user_id_for_connection(request.sender_id)?;
let project_id = self.app_state.db.register_project(user_id).await?; let project_id = self.app_state.db.register_project(user_id).await?;
let mut store = self.store().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 { response.send(proto::ShareProjectResponse {
project_id: project_id.to_proto(), project_id: project_id.to_proto(),
})?; })?;
@ -1036,11 +1041,13 @@ impl Server {
let guest_connection_ids = state let guest_connection_ids = state
.read_project(project_id, request.sender_id)? .read_project(project_id, request.sender_id)?
.guest_connection_ids(); .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| { broadcast(request.sender_id, guest_connection_ids, |connection_id| {
self.peer self.peer
.forward_send(request.sender_id, connection_id, request.payload.clone()) .forward_send(request.sender_id, connection_id, request.payload.clone())
}); });
self.room_updated(room);
}; };
Ok(()) Ok(())

View file

@ -383,7 +383,7 @@ impl Store {
room.participants.push(proto::Participant { room.participants.push(proto::Participant {
user_id: connection.user_id.to_proto(), user_id: connection.user_id.to_proto(),
peer_id: creator_connection_id.0, peer_id: creator_connection_id.0,
project_ids: Default::default(), projects: Default::default(),
location: Some(proto::ParticipantLocation { location: Some(proto::ParticipantLocation {
variant: Some(proto::participant_location::Variant::External( variant: Some(proto::participant_location::Variant::External(
proto::participant_location::External {}, proto::participant_location::External {},
@ -441,7 +441,7 @@ impl Store {
room.participants.push(proto::Participant { room.participants.push(proto::Participant {
user_id: user_id.to_proto(), user_id: user_id.to_proto(),
peer_id: connection_id.0, peer_id: connection_id.0,
project_ids: Default::default(), projects: Default::default(),
location: Some(proto::ParticipantLocation { location: Some(proto::ParticipantLocation {
variant: Some(proto::participant_location::Variant::External( variant: Some(proto::participant_location::Variant::External(
proto::participant_location::External {}, proto::participant_location::External {},
@ -689,7 +689,8 @@ impl Store {
anyhow::ensure!( anyhow::ensure!(
room.participants room.participants
.iter() .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" "no such project"
); );
} }
@ -708,6 +709,7 @@ impl Store {
&mut self, &mut self,
room_id: RoomId, room_id: RoomId,
project_id: ProjectId, project_id: ProjectId,
worktrees: Vec<proto::WorktreeMetadata>,
host_connection_id: ConnectionId, host_connection_id: ConnectionId,
) -> Result<&proto::Room> { ) -> Result<&proto::Room> {
let connection = self let connection = self
@ -724,7 +726,14 @@ impl Store {
.iter_mut() .iter_mut()
.find(|participant| participant.peer_id == host_connection_id.0) .find(|participant| participant.peer_id == host_connection_id.0)
.ok_or_else(|| anyhow!("no such room"))?; .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); connection.projects.insert(project_id);
self.projects.insert( self.projects.insert(
@ -741,7 +750,19 @@ impl Store {
}, },
guests: Default::default(), guests: Default::default(),
active_replica_ids: 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(), language_servers: Default::default(),
}, },
); );
@ -779,8 +800,8 @@ impl Store {
.find(|participant| participant.peer_id == connection_id.0) .find(|participant| participant.peer_id == connection_id.0)
.ok_or_else(|| anyhow!("no such room"))?; .ok_or_else(|| anyhow!("no such room"))?;
participant participant
.project_ids .projects
.retain(|id| *id != project_id.to_proto()); .retain(|project| project.id != project_id.to_proto());
Ok((room, project)) Ok((room, project))
} else { } else {
@ -796,7 +817,7 @@ impl Store {
project_id: ProjectId, project_id: ProjectId,
worktrees: &[proto::WorktreeMetadata], worktrees: &[proto::WorktreeMetadata],
connection_id: ConnectionId, connection_id: ConnectionId,
) -> Result<()> { ) -> Result<&proto::Room> {
let project = self let project = self
.projects .projects
.get_mut(&project_id) .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 { } else {
Err(anyhow!("no such project"))? Err(anyhow!("no such project"))?
} }
@ -1132,8 +1169,8 @@ impl Store {
"room contains participant that has disconnected" "room contains participant that has disconnected"
); );
for project_id in &participant.project_ids { for participant_project in &participant.projects {
let project = &self.projects[&ProjectId::from_proto(*project_id)]; let project = &self.projects[&ProjectId::from_proto(participant_project.id)];
assert_eq!( assert_eq!(
project.room_id, *room_id, project.room_id, *room_id,
"project was shared on a different room" "project was shared on a different room"
@ -1173,8 +1210,9 @@ impl Store {
.unwrap(); .unwrap();
assert!( assert!(
room_participant room_participant
.project_ids .projects
.contains(&project_id.to_proto()), .iter()
.any(|project| project.id == project_id.to_proto()),
"project was not shared in room" "project was not shared in room"
); );
} }

View file

@ -19,10 +19,16 @@ pub fn init(cx: &mut MutableAppContext) {
let active_call = ActiveCall::global(cx); let active_call = ActiveCall::global(cx);
cx.subscribe(&active_call, move |_, event, cx| match event { 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.; const PADDING: f32 = 16.;
let screen_size = cx.platform().screen_size(); let screen_size = cx.platform().screen_size();
let window_size = vec2f(366., 64.);
let theme = &cx.global::<Settings>().theme.project_shared_notification;
let window_size = vec2f(theme.window_width, theme.window_height);
cx.add_window( cx.add_window(
WindowOptions { WindowOptions {
bounds: WindowBounds::Fixed(RectF::new( bounds: WindowBounds::Fixed(RectF::new(
@ -34,7 +40,13 @@ pub fn init(cx: &mut MutableAppContext) {
kind: WindowKind::PopUp, kind: WindowKind::PopUp,
is_movable: false, 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 { pub struct ProjectSharedNotification {
project_id: u64, project_id: u64,
worktree_root_names: Vec<String>,
owner: Arc<User>, owner: Arc<User>,
} }
impl ProjectSharedNotification { impl ProjectSharedNotification {
fn new(project_id: u64, owner: Arc<User>) -> Self { fn new(owner: Arc<User>, project_id: u64, worktree_root_names: Vec<String>) -> Self {
Self { project_id, owner } Self {
project_id,
worktree_root_names,
owner,
}
} }
fn join(&mut self, _: &JoinProject, cx: &mut ViewContext<Self>) { fn join(&mut self, _: &JoinProject, cx: &mut ViewContext<Self>) {
@ -84,13 +101,33 @@ impl ProjectSharedNotification {
) )
.with_child( .with_child(
Label::new( 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(), theme.message.text.clone(),
) )
.contained() .contained()
.with_style(theme.message.container) .with_style(theme.message.container)
.boxed(), .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() .contained()
.with_style(theme.owner_metadata) .with_style(theme.owner_metadata)
.aligned() .aligned()

View file

@ -163,10 +163,15 @@ message Room {
message Participant { message Participant {
uint64 user_id = 1; uint64 user_id = 1;
uint32 peer_id = 2; uint32 peer_id = 2;
repeated uint64 project_ids = 3; repeated ParticipantProject projects = 3;
ParticipantLocation location = 4; ParticipantLocation location = 4;
} }
message ParticipantProject {
uint64 id = 1;
repeated string worktree_root_names = 2;
}
message ParticipantLocation { message ParticipantLocation {
oneof variant { oneof variant {
Project project = 1; Project project = 1;
@ -215,6 +220,7 @@ message RoomUpdated {
message ShareProject { message ShareProject {
uint64 room_id = 1; uint64 room_id = 1;
repeated WorktreeMetadata worktrees = 2;
} }
message ShareProjectResponse { message ShareProjectResponse {

View file

@ -471,6 +471,8 @@ pub struct UpdateNotification {
#[derive(Deserialize, Default)] #[derive(Deserialize, Default)]
pub struct ProjectSharedNotification { pub struct ProjectSharedNotification {
pub window_height: f32,
pub window_width: f32,
#[serde(default)] #[serde(default)]
pub background: Color, pub background: Color,
pub owner_container: ContainerStyle, pub owner_container: ContainerStyle,
@ -478,6 +480,7 @@ pub struct ProjectSharedNotification {
pub owner_metadata: ContainerStyle, pub owner_metadata: ContainerStyle,
pub owner_username: ContainedText, pub owner_username: ContainedText,
pub message: ContainedText, pub message: ContainedText,
pub worktree_roots: ContainedText,
pub button_width: f32, pub button_width: f32,
pub join_button: ContainedText, pub join_button: ContainedText,
pub dismiss_button: ContainedText, pub dismiss_button: ContainedText,

View file

@ -2,8 +2,10 @@ import Theme from "../themes/common/theme";
import { backgroundColor, borderColor, text } from "./components"; import { backgroundColor, borderColor, text } from "./components";
export default function projectSharedNotification(theme: Theme): Object { export default function projectSharedNotification(theme: Theme): Object {
const avatarSize = 32; const avatarSize = 48;
return { return {
windowHeight: 72,
windowWidth: 360,
background: backgroundColor(theme, 300), background: backgroundColor(theme, 300),
ownerContainer: { ownerContainer: {
padding: 12, padding: 12,
@ -24,6 +26,10 @@ export default function projectSharedNotification(theme: Theme): Object {
...text(theme, "sans", "secondary", { size: "xs" }), ...text(theme, "sans", "secondary", { size: "xs" }),
margin: { top: -3 }, margin: { top: -3 },
}, },
worktreeRoots: {
...text(theme, "sans", "secondary", { size: "xs", weight: "bold" }),
margin: { top: -3 },
},
buttonWidth: 96, buttonWidth: 96,
joinButton: { joinButton: {
background: backgroundColor(theme, "info", "active"), background: backgroundColor(theme, "info", "active"),