show host in titlebar (#3072)

Release Notes:

- show host in the titlebar of shared projects
- clicking on faces in the titlebar will now always follow the person
(it used to toggle)
- clicking on someone in the channel panel will follow that person
- highlight the currently open project in the channel panel

- fixes a bug where sometimes following between workspaces would not
work
This commit is contained in:
Conrad Irwin 2023-10-02 21:02:02 -06:00 committed by GitHub
commit d9813a5bec
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 1713 additions and 1256 deletions

View file

@ -1,4 +1,4 @@
web: cd ../zed.dev && PORT=3000 npm run dev web: cd ../zed.dev && PORT=3000 npm run dev
collab: cd crates/collab && cargo run serve collab: cd crates/collab && RUST_LOG=${RUST_LOG:-collab=info} cargo run serve
livekit: livekit-server --dev livekit: livekit-server --dev
postgrest: postgrest crates/collab/admin_api.conf postgrest: postgrest crates/collab/admin_api.conf

View file

@ -44,6 +44,12 @@ pub enum Event {
RemoteProjectUnshared { RemoteProjectUnshared {
project_id: u64, project_id: u64,
}, },
RemoteProjectJoined {
project_id: u64,
},
RemoteProjectInvitationDiscarded {
project_id: u64,
},
Left, Left,
} }
@ -1015,6 +1021,7 @@ impl Room {
) -> Task<Result<ModelHandle<Project>>> { ) -> Task<Result<ModelHandle<Project>>> {
let client = self.client.clone(); let client = self.client.clone();
let user_store = self.user_store.clone(); let user_store = self.user_store.clone();
cx.emit(Event::RemoteProjectJoined { project_id: id });
cx.spawn(|this, mut cx| async move { cx.spawn(|this, mut cx| async move {
let project = let project =
Project::remote(id, client, user_store, language_registry, fs, cx.clone()).await?; Project::remote(id, client, user_store, language_registry, fs, cx.clone()).await?;

View file

@ -595,6 +595,10 @@ impl UserStore {
self.load_users(proto::FuzzySearchUsers { query }, cx) self.load_users(proto::FuzzySearchUsers { query }, cx)
} }
pub fn get_cached_user(&self, user_id: u64) -> Option<Arc<User>> {
self.users.get(&user_id).cloned()
}
pub fn get_user( pub fn get_user(
&mut self, &mut self,
user_id: u64, user_id: u64,

View file

@ -4,6 +4,7 @@ use gpui::{ModelHandle, TestAppContext};
mod channel_buffer_tests; mod channel_buffer_tests;
mod channel_message_tests; mod channel_message_tests;
mod channel_tests; mod channel_tests;
mod following_tests;
mod integration_tests; mod integration_tests;
mod random_channel_buffer_tests; mod random_channel_buffer_tests;
mod random_project_collaboration_tests; mod random_project_collaboration_tests;

View file

@ -702,9 +702,7 @@ async fn test_following_to_channel_notes_without_a_shared_project(
// Client B follows client A. // Client B follows client A.
workspace_b workspace_b
.update(cx_b, |workspace, cx| { .update(cx_b, |workspace, cx| {
workspace workspace.follow(client_a.peer_id().unwrap(), cx).unwrap()
.toggle_follow(client_a.peer_id().unwrap(), cx)
.unwrap()
}) })
.await .await
.unwrap(); .unwrap();

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -47,7 +47,7 @@ use util::{iife, ResultExt, TryFutureExt};
use workspace::{ use workspace::{
dock::{DockPosition, Panel}, dock::{DockPosition, Panel},
item::ItemHandle, item::ItemHandle,
Workspace, FollowNextCollaborator, Workspace,
}; };
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
@ -404,6 +404,7 @@ enum ListEntry {
Header(Section), Header(Section),
CallParticipant { CallParticipant {
user: Arc<User>, user: Arc<User>,
peer_id: Option<PeerId>,
is_pending: bool, is_pending: bool,
}, },
ParticipantProject { ParticipantProject {
@ -508,14 +509,19 @@ impl CollabPanel {
let is_collapsed = this.collapsed_sections.contains(section); let is_collapsed = this.collapsed_sections.contains(section);
this.render_header(*section, &theme, is_selected, is_collapsed, cx) this.render_header(*section, &theme, is_selected, is_collapsed, cx)
} }
ListEntry::CallParticipant { user, is_pending } => { ListEntry::CallParticipant {
Self::render_call_participant( user,
user, peer_id,
*is_pending, is_pending,
is_selected, } => Self::render_call_participant(
&theme.collab_panel, user,
) *peer_id,
} this.user_store.clone(),
*is_pending,
is_selected,
&theme,
cx,
),
ListEntry::ParticipantProject { ListEntry::ParticipantProject {
project_id, project_id,
worktree_root_names, worktree_root_names,
@ -528,7 +534,7 @@ impl CollabPanel {
Some(*project_id) == current_project_id, Some(*project_id) == current_project_id,
*is_last, *is_last,
is_selected, is_selected,
&theme.collab_panel, &theme,
cx, cx,
), ),
ListEntry::ParticipantScreen { peer_id, is_last } => { ListEntry::ParticipantScreen { peer_id, is_last } => {
@ -793,6 +799,7 @@ impl CollabPanel {
let user_id = user.id; let user_id = user.id;
self.entries.push(ListEntry::CallParticipant { self.entries.push(ListEntry::CallParticipant {
user, user,
peer_id: None,
is_pending: false, is_pending: false,
}); });
let mut projects = room.local_participant().projects.iter().peekable(); let mut projects = room.local_participant().projects.iter().peekable();
@ -830,6 +837,7 @@ impl CollabPanel {
let participant = &room.remote_participants()[&user_id]; let participant = &room.remote_participants()[&user_id];
self.entries.push(ListEntry::CallParticipant { self.entries.push(ListEntry::CallParticipant {
user: participant.user.clone(), user: participant.user.clone(),
peer_id: Some(participant.peer_id),
is_pending: false, is_pending: false,
}); });
let mut projects = participant.projects.iter().peekable(); let mut projects = participant.projects.iter().peekable();
@ -871,6 +879,7 @@ impl CollabPanel {
self.entries self.entries
.extend(matches.iter().map(|mat| ListEntry::CallParticipant { .extend(matches.iter().map(|mat| ListEntry::CallParticipant {
user: room.pending_participants()[mat.candidate_id].clone(), user: room.pending_participants()[mat.candidate_id].clone(),
peer_id: None,
is_pending: true, is_pending: true,
})); }));
} }
@ -1174,46 +1183,97 @@ impl CollabPanel {
fn render_call_participant( fn render_call_participant(
user: &User, user: &User,
peer_id: Option<PeerId>,
user_store: ModelHandle<UserStore>,
is_pending: bool, is_pending: bool,
is_selected: bool, is_selected: bool,
theme: &theme::CollabPanel, theme: &theme::Theme,
cx: &mut ViewContext<Self>,
) -> AnyElement<Self> { ) -> AnyElement<Self> {
Flex::row() enum CallParticipant {}
.with_children(user.avatar.clone().map(|avatar| { enum CallParticipantTooltip {}
Image::from_data(avatar)
.with_style(theme.contact_avatar) let collab_theme = &theme.collab_panel;
.aligned()
.left() let is_current_user =
})) user_store.read(cx).current_user().map(|user| user.id) == Some(user.id);
.with_child(
Label::new( let content =
user.github_login.clone(), MouseEventHandler::new::<CallParticipant, _>(user.id as usize, cx, |mouse_state, _| {
theme.contact_username.text.clone(), let style = if is_current_user {
) *collab_theme
.contained() .contact_row
.with_style(theme.contact_username.container) .in_state(is_selected)
.aligned() .style_for(&mut Default::default())
.left() } else {
.flex(1., true), *collab_theme
) .contact_row
.with_children(if is_pending { .in_state(is_selected)
Some( .style_for(mouse_state)
Label::new("Calling", theme.calling_indicator.text.clone()) };
Flex::row()
.with_children(user.avatar.clone().map(|avatar| {
Image::from_data(avatar)
.with_style(collab_theme.contact_avatar)
.aligned()
.left()
}))
.with_child(
Label::new(
user.github_login.clone(),
collab_theme.contact_username.text.clone(),
)
.contained() .contained()
.with_style(theme.calling_indicator.container) .with_style(collab_theme.contact_username.container)
.aligned(), .aligned()
) .left()
} else { .flex(1., true),
None )
.with_children(if is_pending {
Some(
Label::new("Calling", collab_theme.calling_indicator.text.clone())
.contained()
.with_style(collab_theme.calling_indicator.container)
.aligned(),
)
} else if is_current_user {
Some(
Label::new("You", collab_theme.calling_indicator.text.clone())
.contained()
.with_style(collab_theme.calling_indicator.container)
.aligned(),
)
} else {
None
})
.constrained()
.with_height(collab_theme.row_height)
.contained()
.with_style(style)
});
if is_current_user || is_pending || peer_id.is_none() {
return content.into_any();
}
let tooltip = format!("Follow {}", user.github_login);
content
.on_click(MouseButton::Left, move |_, this, cx| {
if let Some(workspace) = this.workspace.upgrade(cx) {
workspace
.update(cx, |workspace, cx| workspace.follow(peer_id.unwrap(), cx))
.map(|task| task.detach_and_log_err(cx));
}
}) })
.constrained() .with_cursor_style(CursorStyle::PointingHand)
.with_height(theme.row_height) .with_tooltip::<CallParticipantTooltip>(
.contained() user.id as usize,
.with_style( tooltip,
*theme Some(Box::new(FollowNextCollaborator)),
.contact_row theme.tooltip.clone(),
.in_state(is_selected) cx,
.style_for(&mut Default::default()),
) )
.into_any() .into_any()
} }
@ -1225,74 +1285,91 @@ impl CollabPanel {
is_current: bool, is_current: bool,
is_last: bool, is_last: bool,
is_selected: bool, is_selected: bool,
theme: &theme::CollabPanel, theme: &theme::Theme,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) -> AnyElement<Self> { ) -> AnyElement<Self> {
enum JoinProject {} enum JoinProject {}
enum JoinProjectTooltip {}
let host_avatar_width = theme let collab_theme = &theme.collab_panel;
let host_avatar_width = collab_theme
.contact_avatar .contact_avatar
.width .width
.or(theme.contact_avatar.height) .or(collab_theme.contact_avatar.height)
.unwrap_or(0.); .unwrap_or(0.);
let tree_branch = theme.tree_branch; let tree_branch = collab_theme.tree_branch;
let project_name = if worktree_root_names.is_empty() { let project_name = if worktree_root_names.is_empty() {
"untitled".to_string() "untitled".to_string()
} else { } else {
worktree_root_names.join(", ") worktree_root_names.join(", ")
}; };
MouseEventHandler::new::<JoinProject, _>(project_id as usize, cx, |mouse_state, cx| { let content =
let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state); MouseEventHandler::new::<JoinProject, _>(project_id as usize, cx, |mouse_state, cx| {
let row = theme let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state);
.project_row let row = if is_current {
.in_state(is_selected) collab_theme
.style_for(mouse_state); .project_row
.in_state(true)
.style_for(&mut Default::default())
} else {
collab_theme
.project_row
.in_state(is_selected)
.style_for(mouse_state)
};
Flex::row() Flex::row()
.with_child(render_tree_branch( .with_child(render_tree_branch(
tree_branch, tree_branch,
&row.name.text, &row.name.text,
is_last, is_last,
vec2f(host_avatar_width, theme.row_height), vec2f(host_avatar_width, collab_theme.row_height),
cx.font_cache(), cx.font_cache(),
)) ))
.with_child( .with_child(
Svg::new("icons/file_icons/folder.svg") Svg::new("icons/file_icons/folder.svg")
.with_color(theme.channel_hash.color) .with_color(collab_theme.channel_hash.color)
.constrained() .constrained()
.with_width(theme.channel_hash.width) .with_width(collab_theme.channel_hash.width)
.aligned() .aligned()
.left(), .left(),
) )
.with_child( .with_child(
Label::new(project_name, row.name.text.clone()) Label::new(project_name.clone(), row.name.text.clone())
.aligned() .aligned()
.left() .left()
.contained() .contained()
.with_style(row.name.container) .with_style(row.name.container)
.flex(1., false), .flex(1., false),
) )
.constrained() .constrained()
.with_height(theme.row_height) .with_height(collab_theme.row_height)
.contained() .contained()
.with_style(row.container) .with_style(row.container)
}) });
.with_cursor_style(if !is_current {
CursorStyle::PointingHand if is_current {
} else { return content.into_any();
CursorStyle::Arrow }
})
.on_click(MouseButton::Left, move |_, this, cx| { content
if !is_current { .with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, this, cx| {
if let Some(workspace) = this.workspace.upgrade(cx) { if let Some(workspace) = this.workspace.upgrade(cx) {
let app_state = workspace.read(cx).app_state().clone(); let app_state = workspace.read(cx).app_state().clone();
workspace::join_remote_project(project_id, host_user_id, app_state, cx) workspace::join_remote_project(project_id, host_user_id, app_state, cx)
.detach_and_log_err(cx); .detach_and_log_err(cx);
} }
} })
}) .with_tooltip::<JoinProjectTooltip>(
.into_any() project_id as usize,
format!("Open {}", project_name),
None,
theme.tooltip.clone(),
cx,
)
.into_any()
} }
fn render_participant_screen( fn render_participant_screen(

View file

@ -215,7 +215,13 @@ impl CollabTitlebarItem {
let git_style = theme.titlebar.git_menu_button.clone(); let git_style = theme.titlebar.git_menu_button.clone();
let item_spacing = theme.titlebar.item_spacing; let item_spacing = theme.titlebar.item_spacing;
let mut ret = Flex::row().with_child( let mut ret = Flex::row();
if let Some(project_host) = self.collect_project_host(theme.clone(), cx) {
ret = ret.with_child(project_host)
}
ret = ret.with_child(
Stack::new() Stack::new()
.with_child( .with_child(
MouseEventHandler::new::<ToggleProjectMenu, _>(0, cx, |mouse_state, cx| { MouseEventHandler::new::<ToggleProjectMenu, _>(0, cx, |mouse_state, cx| {
@ -283,6 +289,71 @@ impl CollabTitlebarItem {
ret.into_any() ret.into_any()
} }
fn collect_project_host(
&self,
theme: Arc<Theme>,
cx: &mut ViewContext<Self>,
) -> Option<AnyElement<Self>> {
if ActiveCall::global(cx).read(cx).room().is_none() {
return None;
}
let project = self.project.read(cx);
let user_store = self.user_store.read(cx);
if project.is_local() {
return None;
}
let Some(host) = project.host() else {
return None;
};
let (Some(host_user), Some(participant_index)) = (
user_store.get_cached_user(host.user_id),
user_store.participant_indices().get(&host.user_id),
) else {
return None;
};
enum ProjectHost {}
enum ProjectHostTooltip {}
let host_style = theme.titlebar.project_host.clone();
let selection_style = theme
.editor
.selection_style_for_room_participant(participant_index.0);
let peer_id = host.peer_id.clone();
Some(
MouseEventHandler::new::<ProjectHost, _>(0, cx, |mouse_state, _| {
let mut host_style = host_style.style_for(mouse_state).clone();
host_style.text.color = selection_style.cursor;
Label::new(host_user.github_login.clone(), host_style.text)
.contained()
.with_style(host_style.container)
.aligned()
.left()
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, this, cx| {
if let Some(workspace) = this.workspace.upgrade(cx) {
if let Some(task) =
workspace.update(cx, |workspace, cx| workspace.follow(peer_id, cx))
{
task.detach_and_log_err(cx);
}
}
})
.with_tooltip::<ProjectHostTooltip>(
0,
host_user.github_login.clone() + " is sharing this project. Click to follow.",
None,
theme.tooltip.clone(),
cx,
)
.into_any_named("project-host"),
)
}
fn window_activation_changed(&mut self, active: bool, cx: &mut ViewContext<Self>) { fn window_activation_changed(&mut self, active: bool, cx: &mut ViewContext<Self>) {
let project = if active { let project = if active {
Some(self.project.clone()) Some(self.project.clone())
@ -877,7 +948,7 @@ impl CollabTitlebarItem {
fn render_face_pile( fn render_face_pile(
&self, &self,
user: &User, user: &User,
replica_id: Option<ReplicaId>, _replica_id: Option<ReplicaId>,
peer_id: PeerId, peer_id: PeerId,
location: Option<ParticipantLocation>, location: Option<ParticipantLocation>,
muted: bool, muted: bool,
@ -1019,55 +1090,30 @@ impl CollabTitlebarItem {
}, },
); );
match (replica_id, location) { if Some(peer_id) == self_peer_id {
// If the user's location isn't known, do nothing. return content.into_any();
(_, None) => content.into_any(),
// If the user is not in this project, but is in another share project,
// join that project.
(None, Some(ParticipantLocation::SharedProject { project_id })) => content
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, this, cx| {
if let Some(workspace) = this.workspace.upgrade(cx) {
let app_state = workspace.read(cx).app_state().clone();
workspace::join_remote_project(project_id, user_id, app_state, cx)
.detach_and_log_err(cx);
}
})
.with_tooltip::<TitlebarParticipant>(
peer_id.as_u64() as usize,
format!("Follow {} into external project", user.github_login),
Some(Box::new(FollowNextCollaborator)),
theme.tooltip.clone(),
cx,
)
.into_any(),
// Otherwise, follow the user in the current window.
_ => content
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, item, cx| {
if let Some(workspace) = item.workspace.upgrade(cx) {
if let Some(task) = workspace
.update(cx, |workspace, cx| workspace.toggle_follow(peer_id, cx))
{
task.detach_and_log_err(cx);
}
}
})
.with_tooltip::<TitlebarParticipant>(
peer_id.as_u64() as usize,
if self_following {
format!("Unfollow {}", user.github_login)
} else {
format!("Follow {}", user.github_login)
},
Some(Box::new(FollowNextCollaborator)),
theme.tooltip.clone(),
cx,
)
.into_any(),
} }
content
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, this, cx| {
let Some(workspace) = this.workspace.upgrade(cx) else {
return;
};
if let Some(task) =
workspace.update(cx, |workspace, cx| workspace.follow(peer_id, cx))
{
task.detach_and_log_err(cx);
}
})
.with_tooltip::<TitlebarParticipant>(
peer_id.as_u64() as usize,
format!("Follow {}", user.github_login),
Some(Box::new(FollowNextCollaborator)),
theme.tooltip.clone(),
cx,
)
.into_any()
} }
fn location_style( fn location_style(

View file

@ -7,7 +7,7 @@ mod face_pile;
mod incoming_call_notification; mod incoming_call_notification;
mod notifications; mod notifications;
mod panel_settings; mod panel_settings;
mod project_shared_notification; pub mod project_shared_notification;
mod sharing_status_indicator; mod sharing_status_indicator;
use call::{report_call_event_for_room, ActiveCall, Room}; use call::{report_call_event_for_room, ActiveCall, Room};

View file

@ -40,7 +40,9 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
.push(window); .push(window);
} }
} }
room::Event::RemoteProjectUnshared { project_id } => { room::Event::RemoteProjectUnshared { project_id }
| room::Event::RemoteProjectJoined { project_id }
| room::Event::RemoteProjectInvitationDiscarded { project_id } => {
if let Some(windows) = notification_windows.remove(&project_id) { if let Some(windows) = notification_windows.remove(&project_id) {
for window in windows { for window in windows {
window.remove(cx); window.remove(cx);
@ -82,7 +84,6 @@ impl ProjectSharedNotification {
} }
fn join(&mut self, cx: &mut ViewContext<Self>) { fn join(&mut self, cx: &mut ViewContext<Self>) {
cx.remove_window();
if let Some(app_state) = self.app_state.upgrade() { if let Some(app_state) = self.app_state.upgrade() {
workspace::join_remote_project(self.project_id, self.owner.id, app_state, cx) workspace::join_remote_project(self.project_id, self.owner.id, app_state, cx)
.detach_and_log_err(cx); .detach_and_log_err(cx);
@ -90,7 +91,15 @@ impl ProjectSharedNotification {
} }
fn dismiss(&mut self, cx: &mut ViewContext<Self>) { fn dismiss(&mut self, cx: &mut ViewContext<Self>) {
cx.remove_window(); if let Some(active_room) =
ActiveCall::global(cx).read_with(cx, |call, _| call.room().cloned())
{
active_room.update(cx, |_, cx| {
cx.emit(room::Event::RemoteProjectInvitationDiscarded {
project_id: self.project_id,
});
});
}
} }
fn render_owner(&self, cx: &mut ViewContext<Self>) -> AnyElement<Self> { fn render_owner(&self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {

View file

@ -103,6 +103,7 @@ pub struct Platform {
current_clipboard_item: Mutex<Option<ClipboardItem>>, current_clipboard_item: Mutex<Option<ClipboardItem>>,
cursor: Mutex<CursorStyle>, cursor: Mutex<CursorStyle>,
active_window: Arc<Mutex<Option<AnyWindowHandle>>>, active_window: Arc<Mutex<Option<AnyWindowHandle>>>,
active_screen: Screen,
} }
impl Platform { impl Platform {
@ -113,6 +114,7 @@ impl Platform {
current_clipboard_item: Default::default(), current_clipboard_item: Default::default(),
cursor: Mutex::new(CursorStyle::Arrow), cursor: Mutex::new(CursorStyle::Arrow),
active_window: Default::default(), active_window: Default::default(),
active_screen: Screen::new(),
} }
} }
} }
@ -136,12 +138,16 @@ impl super::Platform for Platform {
fn quit(&self) {} fn quit(&self) {}
fn screen_by_id(&self, _id: uuid::Uuid) -> Option<Rc<dyn crate::platform::Screen>> { fn screen_by_id(&self, uuid: uuid::Uuid) -> Option<Rc<dyn crate::platform::Screen>> {
None if self.active_screen.uuid == uuid {
Some(Rc::new(self.active_screen.clone()))
} else {
None
}
} }
fn screens(&self) -> Vec<Rc<dyn crate::platform::Screen>> { fn screens(&self) -> Vec<Rc<dyn crate::platform::Screen>> {
Default::default() vec![Rc::new(self.active_screen.clone())]
} }
fn open_window( fn open_window(
@ -158,6 +164,7 @@ impl super::Platform for Platform {
WindowBounds::Fixed(rect) => rect.size(), WindowBounds::Fixed(rect) => rect.size(),
}, },
self.active_window.clone(), self.active_window.clone(),
Rc::new(self.active_screen.clone()),
)) ))
} }
@ -170,6 +177,7 @@ impl super::Platform for Platform {
handle, handle,
vec2f(24., 24.), vec2f(24., 24.),
self.active_window.clone(), self.active_window.clone(),
Rc::new(self.active_screen.clone()),
)) ))
} }
@ -238,8 +246,18 @@ impl super::Platform for Platform {
fn restart(&self) {} fn restart(&self) {}
} }
#[derive(Debug)] #[derive(Debug, Clone)]
pub struct Screen; pub struct Screen {
uuid: uuid::Uuid,
}
impl Screen {
fn new() -> Self {
Self {
uuid: uuid::Uuid::new_v4(),
}
}
}
impl super::Screen for Screen { impl super::Screen for Screen {
fn as_any(&self) -> &dyn Any { fn as_any(&self) -> &dyn Any {
@ -255,7 +273,7 @@ impl super::Screen for Screen {
} }
fn display_uuid(&self) -> Option<uuid::Uuid> { fn display_uuid(&self) -> Option<uuid::Uuid> {
Some(uuid::Uuid::new_v4()) Some(self.uuid)
} }
} }
@ -275,6 +293,7 @@ pub struct Window {
pub(crate) edited: bool, pub(crate) edited: bool,
pub(crate) pending_prompts: RefCell<VecDeque<oneshot::Sender<usize>>>, pub(crate) pending_prompts: RefCell<VecDeque<oneshot::Sender<usize>>>,
active_window: Arc<Mutex<Option<AnyWindowHandle>>>, active_window: Arc<Mutex<Option<AnyWindowHandle>>>,
screen: Rc<Screen>,
} }
impl Window { impl Window {
@ -282,6 +301,7 @@ impl Window {
handle: AnyWindowHandle, handle: AnyWindowHandle,
size: Vector2F, size: Vector2F,
active_window: Arc<Mutex<Option<AnyWindowHandle>>>, active_window: Arc<Mutex<Option<AnyWindowHandle>>>,
screen: Rc<Screen>,
) -> Self { ) -> Self {
Self { Self {
handle, handle,
@ -299,6 +319,7 @@ impl Window {
edited: false, edited: false,
pending_prompts: Default::default(), pending_prompts: Default::default(),
active_window, active_window,
screen,
} }
} }
@ -329,7 +350,7 @@ impl super::Window for Window {
} }
fn screen(&self) -> Rc<dyn crate::platform::Screen> { fn screen(&self) -> Rc<dyn crate::platform::Screen> {
Rc::new(Screen) self.screen.clone()
} }
fn mouse_position(&self) -> Vector2F { fn mouse_position(&self) -> Vector2F {

View file

@ -975,6 +975,10 @@ impl Project {
&self.collaborators &self.collaborators
} }
pub fn host(&self) -> Option<&Collaborator> {
self.collaborators.values().find(|c| c.replica_id == 0)
}
/// Collect all worktrees, including ones that don't appear in the project panel /// Collect all worktrees, including ones that don't appear in the project panel
pub fn worktrees<'a>( pub fn worktrees<'a>(
&'a self, &'a self,

View file

@ -131,6 +131,7 @@ pub struct Titlebar {
pub menu: TitlebarMenu, pub menu: TitlebarMenu,
pub project_menu_button: Toggleable<Interactive<ContainedText>>, pub project_menu_button: Toggleable<Interactive<ContainedText>>,
pub git_menu_button: Toggleable<Interactive<ContainedText>>, pub git_menu_button: Toggleable<Interactive<ContainedText>>,
pub project_host: Interactive<ContainedText>,
pub item_spacing: f32, pub item_spacing: f32,
pub face_pile_spacing: f32, pub face_pile_spacing: f32,
pub avatar_ribbon: AvatarRibbon, pub avatar_ribbon: AvatarRibbon,

View file

@ -222,7 +222,7 @@ impl Member {
|_, _| { |_, _| {
Label::new( Label::new(
format!( format!(
"Follow {} on their active project", "Follow {} to their active project",
leader_user.github_login, leader_user.github_login,
), ),
theme theme

View file

@ -2520,19 +2520,13 @@ impl Workspace {
cx.notify(); cx.notify();
} }
pub fn toggle_follow( fn start_following(
&mut self, &mut self,
leader_id: PeerId, leader_id: PeerId,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) -> Option<Task<Result<()>>> { ) -> Option<Task<Result<()>>> {
let pane = self.active_pane().clone(); let pane = self.active_pane().clone();
if let Some(prev_leader_id) = self.unfollow(&pane, cx) {
if leader_id == prev_leader_id {
return None;
}
}
self.last_leaders_by_pane self.last_leaders_by_pane
.insert(pane.downgrade(), leader_id); .insert(pane.downgrade(), leader_id);
self.follower_states_by_leader self.follower_states_by_leader
@ -2603,9 +2597,64 @@ impl Workspace {
None None
}; };
next_leader_id let pane = self.active_pane.clone();
.or_else(|| collaborators.keys().copied().next()) let Some(leader_id) = next_leader_id.or_else(|| collaborators.keys().copied().next())
.and_then(|leader_id| self.toggle_follow(leader_id, cx)) else {
return None;
};
if Some(leader_id) == self.unfollow(&pane, cx) {
return None;
}
self.follow(leader_id, cx)
}
pub fn follow(
&mut self,
leader_id: PeerId,
cx: &mut ViewContext<Self>,
) -> Option<Task<Result<()>>> {
let room = ActiveCall::global(cx).read(cx).room()?.read(cx);
let project = self.project.read(cx);
let Some(remote_participant) = room.remote_participant_for_peer_id(leader_id) else {
return None;
};
let other_project_id = match remote_participant.location {
call::ParticipantLocation::External => None,
call::ParticipantLocation::UnsharedProject => None,
call::ParticipantLocation::SharedProject { project_id } => {
if Some(project_id) == project.remote_id() {
None
} else {
Some(project_id)
}
}
};
// if they are active in another project, follow there.
if let Some(project_id) = other_project_id {
let app_state = self.app_state.clone();
return Some(crate::join_remote_project(
project_id,
remote_participant.user.id,
app_state,
cx,
));
}
// if you're already following, find the right pane and focus it.
for (existing_leader_id, states_by_pane) in &mut self.follower_states_by_leader {
if leader_id == *existing_leader_id {
for (pane, _) in states_by_pane {
cx.focus(pane);
return None;
}
}
}
// Otherwise, follow.
self.start_following(leader_id, cx)
} }
pub fn unfollow( pub fn unfollow(
@ -4197,21 +4246,20 @@ pub fn join_remote_project(
cx: &mut AppContext, cx: &mut AppContext,
) -> Task<Result<()>> { ) -> Task<Result<()>> {
cx.spawn(|mut cx| async move { cx.spawn(|mut cx| async move {
let existing_workspace = cx let windows = cx.windows();
.windows() let existing_workspace = windows.into_iter().find_map(|window| {
.into_iter() window.downcast::<Workspace>().and_then(|window| {
.find_map(|window| { window
window.downcast::<Workspace>().and_then(|window| { .read_root_with(&cx, |workspace, cx| {
window.read_root_with(&cx, |workspace, cx| {
if workspace.project().read(cx).remote_id() == Some(project_id) { if workspace.project().read(cx).remote_id() == Some(project_id) {
Some(cx.handle().downgrade()) Some(cx.handle().downgrade())
} else { } else {
None None
} }
}) })
}) .unwrap_or(None)
}) })
.flatten(); });
let workspace = if let Some(existing_workspace) = existing_workspace { let workspace = if let Some(existing_workspace) = existing_workspace {
existing_workspace existing_workspace
@ -4276,11 +4324,9 @@ pub fn join_remote_project(
}); });
if let Some(follow_peer_id) = follow_peer_id { if let Some(follow_peer_id) = follow_peer_id {
if !workspace.is_being_followed(follow_peer_id) { workspace
workspace .follow(follow_peer_id, cx)
.toggle_follow(follow_peer_id, cx) .map(|follow| follow.detach_and_log_err(cx));
.map(|follow| follow.detach_and_log_err(cx));
}
} }
} }
})?; })?;

View file

@ -1,4 +1,4 @@
import { icon_button, toggleable_icon_button, toggleable_text_button } from "../component" import { icon_button, text_button, toggleable_icon_button, toggleable_text_button } from "../component"
import { interactive, toggleable } from "../element" import { interactive, toggleable } from "../element"
import { useTheme, with_opacity } from "../theme" import { useTheme, with_opacity } from "../theme"
import { background, border, foreground, text } from "./components" import { background, border, foreground, text } from "./components"
@ -191,6 +191,12 @@ export function titlebar(): any {
color: "variant", color: "variant",
}), }),
project_host: text_button({
text_properties: {
weight: "bold"
}
}),
// Collaborators // Collaborators
leader_avatar: { leader_avatar: {
width: avatar_width, width: avatar_width,