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
collab: cd crates/collab && cargo run serve
collab: cd crates/collab && RUST_LOG=${RUST_LOG:-collab=info} cargo run serve
livekit: livekit-server --dev
postgrest: postgrest crates/collab/admin_api.conf

View file

@ -44,6 +44,12 @@ pub enum Event {
RemoteProjectUnshared {
project_id: u64,
},
RemoteProjectJoined {
project_id: u64,
},
RemoteProjectInvitationDiscarded {
project_id: u64,
},
Left,
}
@ -1015,6 +1021,7 @@ impl Room {
) -> Task<Result<ModelHandle<Project>>> {
let client = self.client.clone();
let user_store = self.user_store.clone();
cx.emit(Event::RemoteProjectJoined { project_id: id });
cx.spawn(|this, mut cx| async move {
let project =
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)
}
pub fn get_cached_user(&self, user_id: u64) -> Option<Arc<User>> {
self.users.get(&user_id).cloned()
}
pub fn get_user(
&mut self,
user_id: u64,

View file

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

View file

@ -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::<ToggleProjectMenu, _>(0, cx, |mouse_state, cx| {
@ -283,6 +289,71 @@ impl CollabTitlebarItem {
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>) {
let project = if active {
Some(self.project.clone())
@ -877,7 +948,7 @@ impl CollabTitlebarItem {
fn render_face_pile(
&self,
user: &User,
replica_id: Option<ReplicaId>,
_replica_id: Option<ReplicaId>,
peer_id: PeerId,
location: Option<ParticipantLocation>,
muted: bool,
@ -1019,55 +1090,30 @@ impl CollabTitlebarItem {
},
);
match (replica_id, location) {
// If the user's location isn't known, do nothing.
(_, 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(),
if Some(peer_id) == self_peer_id {
return content.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(

View file

@ -7,7 +7,7 @@ mod face_pile;
mod incoming_call_notification;
mod notifications;
mod panel_settings;
mod project_shared_notification;
pub mod project_shared_notification;
mod sharing_status_indicator;
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);
}
}
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) {
for window in windows {
window.remove(cx);
@ -82,7 +84,6 @@ impl ProjectSharedNotification {
}
fn join(&mut self, cx: &mut ViewContext<Self>) {
cx.remove_window();
if let Some(app_state) = self.app_state.upgrade() {
workspace::join_remote_project(self.project_id, self.owner.id, app_state, cx)
.detach_and_log_err(cx);
@ -90,7 +91,15 @@ impl ProjectSharedNotification {
}
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> {

View file

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

View file

@ -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,

View file

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

View file

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

View file

@ -2520,19 +2520,13 @@ impl Workspace {
cx.notify();
}
pub fn toggle_follow(
fn start_following(
&mut self,
leader_id: PeerId,
cx: &mut ViewContext<Self>,
) -> Option<Task<Result<()>>> {
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
.insert(pane.downgrade(), leader_id);
self.follower_states_by_leader
@ -2603,9 +2597,64 @@ impl Workspace {
None
};
next_leader_id
.or_else(|| collaborators.keys().copied().next())
.and_then(|leader_id| self.toggle_follow(leader_id, cx))
let pane = self.active_pane.clone();
let Some(leader_id) = next_leader_id.or_else(|| collaborators.keys().copied().next())
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(
@ -4197,21 +4246,20 @@ pub fn join_remote_project(
cx: &mut AppContext,
) -> Task<Result<()>> {
cx.spawn(|mut cx| async move {
let existing_workspace = cx
.windows()
.into_iter()
.find_map(|window| {
window.downcast::<Workspace>().and_then(|window| {
window.read_root_with(&cx, |workspace, cx| {
let windows = cx.windows();
let existing_workspace = windows.into_iter().find_map(|window| {
window.downcast::<Workspace>().and_then(|window| {
window
.read_root_with(&cx, |workspace, cx| {
if workspace.project().read(cx).remote_id() == Some(project_id) {
Some(cx.handle().downgrade())
} else {
None
}
})
})
.unwrap_or(None)
})
.flatten();
});
let workspace = if let Some(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 !workspace.is_being_followed(follow_peer_id) {
workspace
.toggle_follow(follow_peer_id, cx)
.map(|follow| follow.detach_and_log_err(cx));
}
workspace
.follow(follow_peer_id, 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 { 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,