mirror of
https://github.com/zed-industries/zed.git
synced 2024-12-24 17:28:40 +00:00
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:
commit
d9813a5bec
17 changed files with 1713 additions and 1256 deletions
2
Procfile
2
Procfile
|
@ -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
|
||||
|
|
|
@ -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?;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
|
|
1306
crates/collab/src/tests/following_tests.rs
Normal file
1306
crates/collab/src/tests/following_tests.rs
Normal file
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
@ -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(
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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};
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -222,7 +222,7 @@ impl Member {
|
|||
|_, _| {
|
||||
Label::new(
|
||||
format!(
|
||||
"Follow {} on their active project",
|
||||
"Follow {} to their active project",
|
||||
leader_user.github_login,
|
||||
),
|
||||
theme
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
})?;
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in a new issue