@@ -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<PeerId, RemoteParticipant>,
pending_participants: Vec<Arc<User>>,
participant_user_ids: HashSet<u64>,
@@ -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<PeerId, RemoteParticipant> {
&self.remote_participants
}
@@ -201,8 +207,11 @@ impl Room {
cx: &mut ModelContext<Self>,
) -> 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();
});
}));
@@ -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<User>, is_pending: bool },
+ CallParticipant {
+ user: Arc<User>,
+ is_pending: bool,
+ },
+ ParticipantProject {
+ project_id: u64,
+ worktree_root_names: Vec<String>,
+ host_user_id: u64,
+ is_host: bool,
+ is_last: bool,
+ },
IncomingRequest(Arc<User>),
OutgoingRequest(Arc<User>),
Contact(Arc<Contact>),
@@ -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<Self>,
+ ) -> 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::<JoinProject>::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,
@@ -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"),
+ },
+ },
}
}