diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 5eae700404..4ddfbc5a34 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -453,6 +453,10 @@ impl Client { self.state.read().status.1.clone() } + pub fn is_connected(&self) -> bool { + matches!(&*self.status().borrow(), Status::Connected { .. }) + } + fn set_status(self: &Arc, status: Status, cx: &AsyncAppContext) { log::info!("set status on client {}: {:?}", self.id, status); let mut state = self.state.write(); diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index b8cc8fb1b8..6aa41708e3 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -595,6 +595,10 @@ impl UserStore { self.load_users(proto::FuzzySearchUsers { query }, cx) } + pub fn get_cached_user(&self, user_id: u64) -> Option> { + self.users.get(&user_id).cloned() + } + pub fn get_user( &mut self, user_id: u64, diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index 9f3b7d2f30..546b8ef407 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -215,7 +215,13 @@ impl CollabTitlebarItem { let git_style = theme.titlebar.git_menu_button.clone(); 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() .with_child( MouseEventHandler::new::(0, cx, |mouse_state, cx| { @@ -283,6 +289,71 @@ impl CollabTitlebarItem { ret.into_any() } + fn collect_project_host( + &self, + theme: Arc, + cx: &mut ViewContext, + ) -> Option> { + 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::(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::( + 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) { let project = if active { Some(self.project.clone()) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index ee8690ea70..1ddf1a1f66 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -975,6 +975,10 @@ impl Project { &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 pub fn worktrees<'a>( &'a self, diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 1ca2d839c0..b1595fb0d9 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -131,6 +131,7 @@ pub struct Titlebar { pub menu: TitlebarMenu, pub project_menu_button: Toggleable>, pub git_menu_button: Toggleable>, + pub project_host: Interactive, pub item_spacing: f32, pub face_pile_spacing: f32, pub avatar_ribbon: AvatarRibbon, diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 44a70f9a08..8d9a4c1550 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -2608,6 +2608,24 @@ impl Workspace { .and_then(|leader_id| self.toggle_follow(leader_id, cx)) } + pub fn follow( + &mut self, + leader_id: PeerId, + cx: &mut ViewContext, + ) -> Option>> { + 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; + } + } + } + + // not currently following, so follow. + self.toggle_follow(leader_id, cx) + } + pub fn unfollow( &mut self, pane: &ViewHandle, diff --git a/styles/src/style_tree/titlebar.ts b/styles/src/style_tree/titlebar.ts index 672907b22c..63c057a8eb 100644 --- a/styles/src/style_tree/titlebar.ts +++ b/styles/src/style_tree/titlebar.ts @@ -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 { useTheme, with_opacity } from "../theme" import { background, border, foreground, text } from "./components" @@ -191,6 +191,12 @@ export function titlebar(): any { color: "variant", }), + project_host: text_button({ + text_properties: { + weight: "bold" + } + }), + // Collaborators leader_avatar: { width: avatar_width,