Remove projects from contact updates
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
This commit is contained in:
parent
1898e813f5
commit
b35e8f0164
7 changed files with 45 additions and 1339 deletions
|
@ -1,7 +1,7 @@
|
||||||
use super::{http::HttpClient, proto, Client, Status, TypedEnvelope};
|
use super::{http::HttpClient, proto, Client, Status, TypedEnvelope};
|
||||||
use crate::incoming_call::IncomingCall;
|
use crate::incoming_call::IncomingCall;
|
||||||
use anyhow::{anyhow, Context, Result};
|
use anyhow::{anyhow, Context, Result};
|
||||||
use collections::{hash_map::Entry, BTreeSet, HashMap, HashSet};
|
use collections::{hash_map::Entry, HashMap, HashSet};
|
||||||
use futures::{channel::mpsc, future, AsyncReadExt, Future, StreamExt};
|
use futures::{channel::mpsc, future, AsyncReadExt, Future, StreamExt};
|
||||||
use gpui::{AsyncAppContext, Entity, ImageData, ModelContext, ModelHandle, Task};
|
use gpui::{AsyncAppContext, Entity, ImageData, ModelContext, ModelHandle, Task};
|
||||||
use postage::{sink::Sink, watch};
|
use postage::{sink::Sink, watch};
|
||||||
|
@ -40,14 +40,6 @@ impl Eq for User {}
|
||||||
pub struct Contact {
|
pub struct Contact {
|
||||||
pub user: Arc<User>,
|
pub user: Arc<User>,
|
||||||
pub online: bool,
|
pub online: bool,
|
||||||
pub projects: Vec<ProjectMetadata>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
|
||||||
pub struct ProjectMetadata {
|
|
||||||
pub id: u64,
|
|
||||||
pub visible_worktree_root_names: Vec<String>,
|
|
||||||
pub guests: BTreeSet<Arc<User>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
@ -290,7 +282,6 @@ impl UserStore {
|
||||||
let mut user_ids = HashSet::default();
|
let mut user_ids = HashSet::default();
|
||||||
for contact in &message.contacts {
|
for contact in &message.contacts {
|
||||||
user_ids.insert(contact.user_id);
|
user_ids.insert(contact.user_id);
|
||||||
user_ids.extend(contact.projects.iter().flat_map(|w| &w.guests).copied());
|
|
||||||
}
|
}
|
||||||
user_ids.extend(message.incoming_requests.iter().map(|req| req.requester_id));
|
user_ids.extend(message.incoming_requests.iter().map(|req| req.requester_id));
|
||||||
user_ids.extend(message.outgoing_requests.iter());
|
user_ids.extend(message.outgoing_requests.iter());
|
||||||
|
@ -688,34 +679,11 @@ impl Contact {
|
||||||
user_store.get_user(contact.user_id, cx)
|
user_store.get_user(contact.user_id, cx)
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
let mut projects = Vec::new();
|
|
||||||
for project in contact.projects {
|
|
||||||
let mut guests = BTreeSet::new();
|
|
||||||
for participant_id in project.guests {
|
|
||||||
guests.insert(
|
|
||||||
user_store
|
|
||||||
.update(cx, |user_store, cx| user_store.get_user(participant_id, cx))
|
|
||||||
.await?,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
projects.push(ProjectMetadata {
|
|
||||||
id: project.id,
|
|
||||||
visible_worktree_root_names: project.visible_worktree_root_names.clone(),
|
|
||||||
guests,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
user,
|
user,
|
||||||
online: contact.online,
|
online: contact.online,
|
||||||
projects,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn non_empty_projects(&self) -> impl Iterator<Item = &ProjectMetadata> {
|
|
||||||
self.projects
|
|
||||||
.iter()
|
|
||||||
.filter(|project| !project.visible_worktree_root_names.is_empty())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn fetch_avatar(http: &dyn HttpClient, url: &str) -> Result<Arc<ImageData>> {
|
async fn fetch_avatar(http: &dyn HttpClient, url: &str) -> Result<Arc<ImageData>> {
|
||||||
|
|
|
@ -8,7 +8,7 @@ use anyhow::anyhow;
|
||||||
use call::Room;
|
use call::Room;
|
||||||
use client::{
|
use client::{
|
||||||
self, proto, test::FakeHttpClient, Channel, ChannelDetails, ChannelList, Client, Connection,
|
self, proto, test::FakeHttpClient, Channel, ChannelDetails, ChannelList, Client, Connection,
|
||||||
Credentials, EstablishConnectionError, ProjectMetadata, UserStore, RECEIVE_TIMEOUT,
|
Credentials, EstablishConnectionError, UserStore, RECEIVE_TIMEOUT,
|
||||||
};
|
};
|
||||||
use collections::{BTreeMap, HashMap, HashSet};
|
use collections::{BTreeMap, HashMap, HashSet};
|
||||||
use editor::{
|
use editor::{
|
||||||
|
@ -731,294 +731,6 @@ async fn test_cancel_join_request(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test(iterations = 10)]
|
|
||||||
async fn test_offline_projects(
|
|
||||||
deterministic: Arc<Deterministic>,
|
|
||||||
cx_a: &mut TestAppContext,
|
|
||||||
cx_b: &mut TestAppContext,
|
|
||||||
cx_c: &mut TestAppContext,
|
|
||||||
) {
|
|
||||||
cx_a.foreground().forbid_parking();
|
|
||||||
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
|
|
||||||
let client_a = server.create_client(cx_a, "user_a").await;
|
|
||||||
let client_b = server.create_client(cx_b, "user_b").await;
|
|
||||||
let client_c = server.create_client(cx_c, "user_c").await;
|
|
||||||
let user_a = UserId::from_proto(client_a.user_id().unwrap());
|
|
||||||
server
|
|
||||||
.make_contacts(vec![
|
|
||||||
(&client_a, cx_a),
|
|
||||||
(&client_b, cx_b),
|
|
||||||
(&client_c, cx_c),
|
|
||||||
])
|
|
||||||
.await;
|
|
||||||
|
|
||||||
// Set up observers of the project and user stores. Any time either of
|
|
||||||
// these models update, they should be in a consistent state with each
|
|
||||||
// other. There should not be an observable moment where the current
|
|
||||||
// user's contact entry contains a project that does not match one of
|
|
||||||
// the current open projects. That would cause a duplicate entry to be
|
|
||||||
// shown in the contacts panel.
|
|
||||||
let mut subscriptions = vec![];
|
|
||||||
let (window_id, view) = cx_a.add_window(|cx| {
|
|
||||||
subscriptions.push(cx.observe(&client_a.user_store, {
|
|
||||||
let project_store = client_a.project_store.clone();
|
|
||||||
let user_store = client_a.user_store.clone();
|
|
||||||
move |_, _, cx| check_project_list(project_store.clone(), user_store.clone(), cx)
|
|
||||||
}));
|
|
||||||
|
|
||||||
subscriptions.push(cx.observe(&client_a.project_store, {
|
|
||||||
let project_store = client_a.project_store.clone();
|
|
||||||
let user_store = client_a.user_store.clone();
|
|
||||||
move |_, _, cx| check_project_list(project_store.clone(), user_store.clone(), cx)
|
|
||||||
}));
|
|
||||||
|
|
||||||
fn check_project_list(
|
|
||||||
project_store: ModelHandle<ProjectStore>,
|
|
||||||
user_store: ModelHandle<UserStore>,
|
|
||||||
cx: &mut gpui::MutableAppContext,
|
|
||||||
) {
|
|
||||||
let user_store = user_store.read(cx);
|
|
||||||
for contact in user_store.contacts() {
|
|
||||||
if contact.user.id == user_store.current_user().unwrap().id {
|
|
||||||
for project in &contact.projects {
|
|
||||||
let store_contains_project = project_store
|
|
||||||
.read(cx)
|
|
||||||
.projects(cx)
|
|
||||||
.filter_map(|project| project.read(cx).remote_id())
|
|
||||||
.any(|x| x == project.id);
|
|
||||||
|
|
||||||
if !store_contains_project {
|
|
||||||
panic!(
|
|
||||||
concat!(
|
|
||||||
"current user's contact data has a project",
|
|
||||||
"that doesn't match any open project {:?}",
|
|
||||||
),
|
|
||||||
project
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
EmptyView
|
|
||||||
});
|
|
||||||
|
|
||||||
// Build an offline project with two worktrees.
|
|
||||||
client_a
|
|
||||||
.fs
|
|
||||||
.insert_tree(
|
|
||||||
"/code",
|
|
||||||
json!({
|
|
||||||
"crate1": { "a.rs": "" },
|
|
||||||
"crate2": { "b.rs": "" },
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
let project = cx_a.update(|cx| {
|
|
||||||
Project::local(
|
|
||||||
false,
|
|
||||||
client_a.client.clone(),
|
|
||||||
client_a.user_store.clone(),
|
|
||||||
client_a.project_store.clone(),
|
|
||||||
client_a.language_registry.clone(),
|
|
||||||
client_a.fs.clone(),
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
});
|
|
||||||
project
|
|
||||||
.update(cx_a, |p, cx| {
|
|
||||||
p.find_or_create_local_worktree("/code/crate1", true, cx)
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
project
|
|
||||||
.update(cx_a, |p, cx| {
|
|
||||||
p.find_or_create_local_worktree("/code/crate2", true, cx)
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
project
|
|
||||||
.update(cx_a, |p, cx| p.restore_state(cx))
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
// When a project is offline, we still create it on the server but is invisible
|
|
||||||
// to other users.
|
|
||||||
deterministic.run_until_parked();
|
|
||||||
assert!(server
|
|
||||||
.store
|
|
||||||
.lock()
|
|
||||||
.await
|
|
||||||
.project_metadata_for_user(user_a)
|
|
||||||
.is_empty());
|
|
||||||
project.read_with(cx_a, |project, _| {
|
|
||||||
assert!(project.remote_id().is_some());
|
|
||||||
assert!(!project.is_online());
|
|
||||||
});
|
|
||||||
assert!(client_b
|
|
||||||
.user_store
|
|
||||||
.read_with(cx_b, |store, _| { store.contacts()[0].projects.is_empty() }));
|
|
||||||
|
|
||||||
// When the project is taken online, its metadata is sent to the server
|
|
||||||
// and broadcasted to other users.
|
|
||||||
project.update(cx_a, |p, cx| p.set_online(true, cx));
|
|
||||||
deterministic.run_until_parked();
|
|
||||||
let project_id = project.read_with(cx_a, |p, _| p.remote_id()).unwrap();
|
|
||||||
client_b.user_store.read_with(cx_b, |store, _| {
|
|
||||||
assert_eq!(
|
|
||||||
store.contacts()[0].projects,
|
|
||||||
&[ProjectMetadata {
|
|
||||||
id: project_id,
|
|
||||||
visible_worktree_root_names: vec!["crate1".into(), "crate2".into()],
|
|
||||||
guests: Default::default(),
|
|
||||||
}]
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
// The project is registered again when the host loses and regains connection.
|
|
||||||
server.disconnect_client(user_a);
|
|
||||||
server.forbid_connections();
|
|
||||||
cx_a.foreground().advance_clock(rpc::RECEIVE_TIMEOUT);
|
|
||||||
assert!(server
|
|
||||||
.store
|
|
||||||
.lock()
|
|
||||||
.await
|
|
||||||
.project_metadata_for_user(user_a)
|
|
||||||
.is_empty());
|
|
||||||
assert!(project.read_with(cx_a, |p, _| p.remote_id().is_none()));
|
|
||||||
assert!(client_b
|
|
||||||
.user_store
|
|
||||||
.read_with(cx_b, |store, _| { store.contacts()[0].projects.is_empty() }));
|
|
||||||
|
|
||||||
server.allow_connections();
|
|
||||||
cx_b.foreground().advance_clock(Duration::from_secs(10));
|
|
||||||
let project_id = project.read_with(cx_a, |p, _| p.remote_id()).unwrap();
|
|
||||||
client_b.user_store.read_with(cx_b, |store, _| {
|
|
||||||
assert_eq!(
|
|
||||||
store.contacts()[0].projects,
|
|
||||||
&[ProjectMetadata {
|
|
||||||
id: project_id,
|
|
||||||
visible_worktree_root_names: vec!["crate1".into(), "crate2".into()],
|
|
||||||
guests: Default::default(),
|
|
||||||
}]
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
project
|
|
||||||
.update(cx_a, |p, cx| {
|
|
||||||
p.find_or_create_local_worktree("/code/crate3", true, cx)
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
deterministic.run_until_parked();
|
|
||||||
client_b.user_store.read_with(cx_b, |store, _| {
|
|
||||||
assert_eq!(
|
|
||||||
store.contacts()[0].projects,
|
|
||||||
&[ProjectMetadata {
|
|
||||||
id: project_id,
|
|
||||||
visible_worktree_root_names: vec![
|
|
||||||
"crate1".into(),
|
|
||||||
"crate2".into(),
|
|
||||||
"crate3".into()
|
|
||||||
],
|
|
||||||
guests: Default::default(),
|
|
||||||
}]
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Build another project using a directory which was previously part of
|
|
||||||
// an online project. Restore the project's state from the host's database.
|
|
||||||
let project2_a = cx_a.update(|cx| {
|
|
||||||
Project::local(
|
|
||||||
false,
|
|
||||||
client_a.client.clone(),
|
|
||||||
client_a.user_store.clone(),
|
|
||||||
client_a.project_store.clone(),
|
|
||||||
client_a.language_registry.clone(),
|
|
||||||
client_a.fs.clone(),
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
});
|
|
||||||
project2_a
|
|
||||||
.update(cx_a, |p, cx| {
|
|
||||||
p.find_or_create_local_worktree("/code/crate3", true, cx)
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
project2_a
|
|
||||||
.update(cx_a, |project, cx| project.restore_state(cx))
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
// This project is now online, because its directory was previously online.
|
|
||||||
project2_a.read_with(cx_a, |project, _| assert!(project.is_online()));
|
|
||||||
deterministic.run_until_parked();
|
|
||||||
let project2_id = project2_a.read_with(cx_a, |p, _| p.remote_id()).unwrap();
|
|
||||||
client_b.user_store.read_with(cx_b, |store, _| {
|
|
||||||
assert_eq!(
|
|
||||||
store.contacts()[0].projects,
|
|
||||||
&[
|
|
||||||
ProjectMetadata {
|
|
||||||
id: project_id,
|
|
||||||
visible_worktree_root_names: vec![
|
|
||||||
"crate1".into(),
|
|
||||||
"crate2".into(),
|
|
||||||
"crate3".into()
|
|
||||||
],
|
|
||||||
guests: Default::default(),
|
|
||||||
},
|
|
||||||
ProjectMetadata {
|
|
||||||
id: project2_id,
|
|
||||||
visible_worktree_root_names: vec!["crate3".into()],
|
|
||||||
guests: Default::default(),
|
|
||||||
}
|
|
||||||
]
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
let project2_b = client_b.build_remote_project(&project2_a, cx_a, cx_b).await;
|
|
||||||
let project2_c = cx_c.foreground().spawn(Project::remote(
|
|
||||||
project2_id,
|
|
||||||
client_c.client.clone(),
|
|
||||||
client_c.user_store.clone(),
|
|
||||||
client_c.project_store.clone(),
|
|
||||||
client_c.language_registry.clone(),
|
|
||||||
FakeFs::new(cx_c.background()),
|
|
||||||
cx_c.to_async(),
|
|
||||||
));
|
|
||||||
deterministic.run_until_parked();
|
|
||||||
|
|
||||||
// Taking a project offline unshares the project, rejects any pending join request and
|
|
||||||
// disconnects existing guests.
|
|
||||||
project2_a.update(cx_a, |project, cx| project.set_online(false, cx));
|
|
||||||
deterministic.run_until_parked();
|
|
||||||
project2_a.read_with(cx_a, |project, _| assert!(!project.is_shared()));
|
|
||||||
project2_b.read_with(cx_b, |project, _| assert!(project.is_read_only()));
|
|
||||||
project2_c.await.unwrap_err();
|
|
||||||
|
|
||||||
client_b.user_store.read_with(cx_b, |store, _| {
|
|
||||||
assert_eq!(
|
|
||||||
store.contacts()[0].projects,
|
|
||||||
&[ProjectMetadata {
|
|
||||||
id: project_id,
|
|
||||||
visible_worktree_root_names: vec![
|
|
||||||
"crate1".into(),
|
|
||||||
"crate2".into(),
|
|
||||||
"crate3".into()
|
|
||||||
],
|
|
||||||
guests: Default::default(),
|
|
||||||
},]
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
cx_a.update(|cx| {
|
|
||||||
drop(subscriptions);
|
|
||||||
drop(view);
|
|
||||||
cx.remove_window(window_id);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#[gpui::test(iterations = 10)]
|
#[gpui::test(iterations = 10)]
|
||||||
async fn test_propagate_saves_and_fs_changes(
|
async fn test_propagate_saves_and_fs_changes(
|
||||||
cx_a: &mut TestAppContext,
|
cx_a: &mut TestAppContext,
|
||||||
|
@ -3911,24 +3623,15 @@ async fn test_contacts(
|
||||||
deterministic.run_until_parked();
|
deterministic.run_until_parked();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
contacts(&client_a, cx_a),
|
contacts(&client_a, cx_a),
|
||||||
[
|
[("user_b".to_string(), true), ("user_c".to_string(), true)]
|
||||||
("user_b".to_string(), true, vec![]),
|
|
||||||
("user_c".to_string(), true, vec![])
|
|
||||||
]
|
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
contacts(&client_b, cx_b),
|
contacts(&client_b, cx_b),
|
||||||
[
|
[("user_a".to_string(), true), ("user_c".to_string(), true)]
|
||||||
("user_a".to_string(), true, vec![]),
|
|
||||||
("user_c".to_string(), true, vec![])
|
|
||||||
]
|
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
contacts(&client_c, cx_c),
|
contacts(&client_c, cx_c),
|
||||||
[
|
[("user_a".to_string(), true), ("user_b".to_string(), true)]
|
||||||
("user_a".to_string(), true, vec![]),
|
|
||||||
("user_b".to_string(), true, vec![])
|
|
||||||
]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Share a project as client A.
|
// Share a project as client A.
|
||||||
|
@ -3938,24 +3641,15 @@ async fn test_contacts(
|
||||||
deterministic.run_until_parked();
|
deterministic.run_until_parked();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
contacts(&client_a, cx_a),
|
contacts(&client_a, cx_a),
|
||||||
[
|
[("user_b".to_string(), true), ("user_c".to_string(), true)]
|
||||||
("user_b".to_string(), true, vec![]),
|
|
||||||
("user_c".to_string(), true, vec![])
|
|
||||||
]
|
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
contacts(&client_b, cx_b),
|
contacts(&client_b, cx_b),
|
||||||
[
|
[("user_a".to_string(), true), ("user_c".to_string(), true)]
|
||||||
("user_a".to_string(), true, vec![("a".to_string(), vec![])]),
|
|
||||||
("user_c".to_string(), true, vec![])
|
|
||||||
]
|
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
contacts(&client_c, cx_c),
|
contacts(&client_c, cx_c),
|
||||||
[
|
[("user_a".to_string(), true), ("user_b".to_string(), true)]
|
||||||
("user_a".to_string(), true, vec![("a".to_string(), vec![])]),
|
|
||||||
("user_b".to_string(), true, vec![])
|
|
||||||
]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
let _project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
|
let _project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
|
||||||
|
@ -3963,32 +3657,15 @@ async fn test_contacts(
|
||||||
deterministic.run_until_parked();
|
deterministic.run_until_parked();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
contacts(&client_a, cx_a),
|
contacts(&client_a, cx_a),
|
||||||
[
|
[("user_b".to_string(), true), ("user_c".to_string(), true)]
|
||||||
("user_b".to_string(), true, vec![]),
|
|
||||||
("user_c".to_string(), true, vec![])
|
|
||||||
]
|
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
contacts(&client_b, cx_b),
|
contacts(&client_b, cx_b),
|
||||||
[
|
[("user_a".to_string(), true,), ("user_c".to_string(), true)]
|
||||||
(
|
|
||||||
"user_a".to_string(),
|
|
||||||
true,
|
|
||||||
vec![("a".to_string(), vec!["user_b".to_string()])]
|
|
||||||
),
|
|
||||||
("user_c".to_string(), true, vec![])
|
|
||||||
]
|
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
contacts(&client_c, cx_c),
|
contacts(&client_c, cx_c),
|
||||||
[
|
[("user_a".to_string(), true,), ("user_b".to_string(), true)]
|
||||||
(
|
|
||||||
"user_a".to_string(),
|
|
||||||
true,
|
|
||||||
vec![("a".to_string(), vec!["user_b".to_string()])]
|
|
||||||
),
|
|
||||||
("user_b".to_string(), true, vec![])
|
|
||||||
]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Add a local project as client B
|
// Add a local project as client B
|
||||||
|
@ -3998,32 +3675,15 @@ async fn test_contacts(
|
||||||
deterministic.run_until_parked();
|
deterministic.run_until_parked();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
contacts(&client_a, cx_a),
|
contacts(&client_a, cx_a),
|
||||||
[
|
[("user_b".to_string(), true), ("user_c".to_string(), true)]
|
||||||
("user_b".to_string(), true, vec![("b".to_string(), vec![])]),
|
|
||||||
("user_c".to_string(), true, vec![])
|
|
||||||
]
|
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
contacts(&client_b, cx_b),
|
contacts(&client_b, cx_b),
|
||||||
[
|
[("user_a".to_string(), true), ("user_c".to_string(), true)]
|
||||||
(
|
|
||||||
"user_a".to_string(),
|
|
||||||
true,
|
|
||||||
vec![("a".to_string(), vec!["user_b".to_string()])]
|
|
||||||
),
|
|
||||||
("user_c".to_string(), true, vec![])
|
|
||||||
]
|
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
contacts(&client_c, cx_c),
|
contacts(&client_c, cx_c),
|
||||||
[
|
[("user_a".to_string(), true,), ("user_b".to_string(), true)]
|
||||||
(
|
|
||||||
"user_a".to_string(),
|
|
||||||
true,
|
|
||||||
vec![("a".to_string(), vec!["user_b".to_string()])]
|
|
||||||
),
|
|
||||||
("user_b".to_string(), true, vec![("b".to_string(), vec![])])
|
|
||||||
]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
project_a
|
project_a
|
||||||
|
@ -4036,24 +3696,15 @@ async fn test_contacts(
|
||||||
deterministic.run_until_parked();
|
deterministic.run_until_parked();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
contacts(&client_a, cx_a),
|
contacts(&client_a, cx_a),
|
||||||
[
|
[("user_b".to_string(), true), ("user_c".to_string(), true)]
|
||||||
("user_b".to_string(), true, vec![("b".to_string(), vec![])]),
|
|
||||||
("user_c".to_string(), true, vec![])
|
|
||||||
]
|
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
contacts(&client_b, cx_b),
|
contacts(&client_b, cx_b),
|
||||||
[
|
[("user_a".to_string(), true), ("user_c".to_string(), true)]
|
||||||
("user_a".to_string(), true, vec![]),
|
|
||||||
("user_c".to_string(), true, vec![])
|
|
||||||
]
|
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
contacts(&client_c, cx_c),
|
contacts(&client_c, cx_c),
|
||||||
[
|
[("user_a".to_string(), true), ("user_b".to_string(), true)]
|
||||||
("user_a".to_string(), true, vec![]),
|
|
||||||
("user_b".to_string(), true, vec![("b".to_string(), vec![])])
|
|
||||||
]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
server.disconnect_client(client_c.current_user_id(cx_c));
|
server.disconnect_client(client_c.current_user_id(cx_c));
|
||||||
|
@ -4061,17 +3712,11 @@ async fn test_contacts(
|
||||||
deterministic.advance_clock(rpc::RECEIVE_TIMEOUT);
|
deterministic.advance_clock(rpc::RECEIVE_TIMEOUT);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
contacts(&client_a, cx_a),
|
contacts(&client_a, cx_a),
|
||||||
[
|
[("user_b".to_string(), true), ("user_c".to_string(), false)]
|
||||||
("user_b".to_string(), true, vec![("b".to_string(), vec![])]),
|
|
||||||
("user_c".to_string(), false, vec![])
|
|
||||||
]
|
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
contacts(&client_b, cx_b),
|
contacts(&client_b, cx_b),
|
||||||
[
|
[("user_a".to_string(), true), ("user_c".to_string(), false)]
|
||||||
("user_a".to_string(), true, vec![]),
|
|
||||||
("user_c".to_string(), false, vec![])
|
|
||||||
]
|
|
||||||
);
|
);
|
||||||
assert_eq!(contacts(&client_c, cx_c), []);
|
assert_eq!(contacts(&client_c, cx_c), []);
|
||||||
|
|
||||||
|
@ -4084,48 +3729,24 @@ async fn test_contacts(
|
||||||
deterministic.run_until_parked();
|
deterministic.run_until_parked();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
contacts(&client_a, cx_a),
|
contacts(&client_a, cx_a),
|
||||||
[
|
[("user_b".to_string(), true), ("user_c".to_string(), true)]
|
||||||
("user_b".to_string(), true, vec![("b".to_string(), vec![])]),
|
|
||||||
("user_c".to_string(), true, vec![])
|
|
||||||
]
|
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
contacts(&client_b, cx_b),
|
contacts(&client_b, cx_b),
|
||||||
[
|
[("user_a".to_string(), true), ("user_c".to_string(), true)]
|
||||||
("user_a".to_string(), true, vec![]),
|
|
||||||
("user_c".to_string(), true, vec![])
|
|
||||||
]
|
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
contacts(&client_c, cx_c),
|
contacts(&client_c, cx_c),
|
||||||
[
|
[("user_a".to_string(), true), ("user_b".to_string(), true)]
|
||||||
("user_a".to_string(), true, vec![]),
|
|
||||||
("user_b".to_string(), true, vec![("b".to_string(), vec![])])
|
|
||||||
]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
#[allow(clippy::type_complexity)]
|
#[allow(clippy::type_complexity)]
|
||||||
fn contacts(
|
fn contacts(client: &TestClient, cx: &TestAppContext) -> Vec<(String, bool)> {
|
||||||
client: &TestClient,
|
|
||||||
cx: &TestAppContext,
|
|
||||||
) -> Vec<(String, bool, Vec<(String, Vec<String>)>)> {
|
|
||||||
client.user_store.read_with(cx, |store, _| {
|
client.user_store.read_with(cx, |store, _| {
|
||||||
store
|
store
|
||||||
.contacts()
|
.contacts()
|
||||||
.iter()
|
.iter()
|
||||||
.map(|contact| {
|
.map(|contact| (contact.user.github_login.clone(), contact.online))
|
||||||
let projects = contact
|
|
||||||
.projects
|
|
||||||
.iter()
|
|
||||||
.map(|p| {
|
|
||||||
(
|
|
||||||
p.visible_worktree_root_names[0].clone(),
|
|
||||||
p.guests.iter().map(|p| p.github_login.clone()).collect(),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
(contact.user.github_login.clone(), contact.online, projects)
|
|
||||||
})
|
|
||||||
.collect()
|
.collect()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -5155,22 +4776,6 @@ async fn test_random_collaboration(
|
||||||
log::error!("{} error - {:?}", guest.username, guest_err);
|
log::error!("{} error - {:?}", guest.username, guest_err);
|
||||||
}
|
}
|
||||||
|
|
||||||
let contacts = server
|
|
||||||
.app_state
|
|
||||||
.db
|
|
||||||
.get_contacts(guest.current_user_id(&guest_cx))
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
let contacts = server
|
|
||||||
.store
|
|
||||||
.lock()
|
|
||||||
.await
|
|
||||||
.build_initial_contacts_update(contacts)
|
|
||||||
.contacts;
|
|
||||||
assert!(!contacts
|
|
||||||
.iter()
|
|
||||||
.flat_map(|contact| &contact.projects)
|
|
||||||
.any(|project| project.id == host_project_id));
|
|
||||||
guest_project.read_with(&guest_cx, |project, _| assert!(project.is_read_only()));
|
guest_project.read_with(&guest_cx, |project, _| assert!(project.is_read_only()));
|
||||||
guest_cx.update(|_| drop((guest, guest_project)));
|
guest_cx.update(|_| drop((guest, guest_project)));
|
||||||
}
|
}
|
||||||
|
@ -5259,14 +4864,6 @@ async fn test_random_collaboration(
|
||||||
"removed guest is still a contact of another peer"
|
"removed guest is still a contact of another peer"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
for project in contact.projects {
|
|
||||||
for project_guest_id in project.guests {
|
|
||||||
assert_ne!(
|
|
||||||
project_guest_id, removed_guest_id.0 as u64,
|
|
||||||
"removed guest appears as still participating on a project"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -345,47 +345,11 @@ impl Store {
|
||||||
pub fn contact_for_user(&self, user_id: UserId, should_notify: bool) -> proto::Contact {
|
pub fn contact_for_user(&self, user_id: UserId, should_notify: bool) -> proto::Contact {
|
||||||
proto::Contact {
|
proto::Contact {
|
||||||
user_id: user_id.to_proto(),
|
user_id: user_id.to_proto(),
|
||||||
projects: self.project_metadata_for_user(user_id),
|
|
||||||
online: self.is_user_online(user_id),
|
online: self.is_user_online(user_id),
|
||||||
should_notify,
|
should_notify,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn project_metadata_for_user(&self, user_id: UserId) -> Vec<proto::ProjectMetadata> {
|
|
||||||
let user_connection_state = self.connected_users.get(&user_id);
|
|
||||||
let project_ids = user_connection_state.iter().flat_map(|state| {
|
|
||||||
state
|
|
||||||
.connection_ids
|
|
||||||
.iter()
|
|
||||||
.filter_map(|connection_id| self.connections.get(connection_id))
|
|
||||||
.flat_map(|connection| connection.projects.iter().copied())
|
|
||||||
});
|
|
||||||
|
|
||||||
let mut metadata = Vec::new();
|
|
||||||
for project_id in project_ids {
|
|
||||||
if let Some(project) = self.projects.get(&project_id) {
|
|
||||||
if project.host.user_id == user_id && project.online {
|
|
||||||
metadata.push(proto::ProjectMetadata {
|
|
||||||
id: project_id.to_proto(),
|
|
||||||
visible_worktree_root_names: project
|
|
||||||
.worktrees
|
|
||||||
.values()
|
|
||||||
.filter(|worktree| worktree.visible)
|
|
||||||
.map(|worktree| worktree.root_name.clone())
|
|
||||||
.collect(),
|
|
||||||
guests: project
|
|
||||||
.guests
|
|
||||||
.values()
|
|
||||||
.map(|guest| guest.user_id.to_proto())
|
|
||||||
.collect(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
metadata
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn create_room(&mut self, creator_connection_id: ConnectionId) -> Result<RoomId> {
|
pub fn create_room(&mut self, creator_connection_id: ConnectionId) -> Result<RoomId> {
|
||||||
let connection = self
|
let connection = self
|
||||||
.connections
|
.connections
|
||||||
|
|
|
@ -8,23 +8,19 @@ use contact_notification::ContactNotification;
|
||||||
use editor::{Cancel, Editor};
|
use editor::{Cancel, Editor};
|
||||||
use fuzzy::{match_strings, StringMatchCandidate};
|
use fuzzy::{match_strings, StringMatchCandidate};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions,
|
actions, elements::*, impl_actions, impl_internal_actions, platform::CursorStyle,
|
||||||
elements::*,
|
|
||||||
geometry::{rect::RectF, vector::vec2f},
|
|
||||||
impl_actions, impl_internal_actions,
|
|
||||||
platform::CursorStyle,
|
|
||||||
AnyViewHandle, AppContext, ClipboardItem, Element, ElementBox, Entity, ModelHandle,
|
AnyViewHandle, AppContext, ClipboardItem, Element, ElementBox, Entity, ModelHandle,
|
||||||
MouseButton, MutableAppContext, RenderContext, Subscription, View, ViewContext, ViewHandle,
|
MouseButton, MutableAppContext, RenderContext, Subscription, View, ViewContext, ViewHandle,
|
||||||
WeakModelHandle, WeakViewHandle,
|
WeakViewHandle,
|
||||||
};
|
};
|
||||||
use join_project_notification::JoinProjectNotification;
|
use join_project_notification::JoinProjectNotification;
|
||||||
use menu::{Confirm, SelectNext, SelectPrev};
|
use menu::{Confirm, SelectNext, SelectPrev};
|
||||||
use project::{Project, ProjectStore};
|
use project::ProjectStore;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
use std::{ops::DerefMut, sync::Arc};
|
use std::sync::Arc;
|
||||||
use theme::IconButton;
|
use theme::IconButton;
|
||||||
use workspace::{sidebar::SidebarItem, JoinProject, ToggleProjectOnline, Workspace};
|
use workspace::{sidebar::SidebarItem, Workspace};
|
||||||
|
|
||||||
actions!(contacts_panel, [ToggleFocus]);
|
actions!(contacts_panel, [ToggleFocus]);
|
||||||
|
|
||||||
|
@ -48,8 +44,6 @@ enum ContactEntry {
|
||||||
IncomingRequest(Arc<User>),
|
IncomingRequest(Arc<User>),
|
||||||
OutgoingRequest(Arc<User>),
|
OutgoingRequest(Arc<User>),
|
||||||
Contact(Arc<Contact>),
|
Contact(Arc<Contact>),
|
||||||
ContactProject(Arc<Contact>, usize, Option<WeakModelHandle<Project>>),
|
|
||||||
OfflineProject(WeakModelHandle<Project>),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, PartialEq)]
|
#[derive(Clone, PartialEq)]
|
||||||
|
@ -181,7 +175,6 @@ impl ContactsPanel {
|
||||||
|
|
||||||
let list_state = ListState::new(0, Orientation::Top, 1000., cx, move |this, ix, cx| {
|
let list_state = ListState::new(0, Orientation::Top, 1000., cx, move |this, ix, cx| {
|
||||||
let theme = cx.global::<Settings>().theme.clone();
|
let theme = cx.global::<Settings>().theme.clone();
|
||||||
let current_user_id = this.user_store.read(cx).current_user().map(|user| user.id);
|
|
||||||
let is_selected = this.selection == Some(ix);
|
let is_selected = this.selection == Some(ix);
|
||||||
|
|
||||||
match &this.entries[ix] {
|
match &this.entries[ix] {
|
||||||
|
@ -214,34 +207,6 @@ impl ContactsPanel {
|
||||||
ContactEntry::Contact(contact) => {
|
ContactEntry::Contact(contact) => {
|
||||||
Self::render_contact(&contact.user, &theme.contacts_panel, is_selected)
|
Self::render_contact(&contact.user, &theme.contacts_panel, is_selected)
|
||||||
}
|
}
|
||||||
ContactEntry::ContactProject(contact, project_ix, open_project) => {
|
|
||||||
let is_last_project_for_contact =
|
|
||||||
this.entries.get(ix + 1).map_or(true, |next| {
|
|
||||||
if let ContactEntry::ContactProject(next_contact, _, _) = next {
|
|
||||||
next_contact.user.id != contact.user.id
|
|
||||||
} else {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
});
|
|
||||||
Self::render_project(
|
|
||||||
contact.clone(),
|
|
||||||
current_user_id,
|
|
||||||
*project_ix,
|
|
||||||
*open_project,
|
|
||||||
&theme.contacts_panel,
|
|
||||||
&theme.tooltip,
|
|
||||||
is_last_project_for_contact,
|
|
||||||
is_selected,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
ContactEntry::OfflineProject(project) => Self::render_offline_project(
|
|
||||||
*project,
|
|
||||||
&theme.contacts_panel,
|
|
||||||
&theme.tooltip,
|
|
||||||
is_selected,
|
|
||||||
cx,
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -343,260 +308,6 @@ impl ContactsPanel {
|
||||||
.boxed()
|
.boxed()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
fn render_project(
|
|
||||||
contact: Arc<Contact>,
|
|
||||||
current_user_id: Option<u64>,
|
|
||||||
project_index: usize,
|
|
||||||
open_project: Option<WeakModelHandle<Project>>,
|
|
||||||
theme: &theme::ContactsPanel,
|
|
||||||
tooltip_style: &TooltipStyle,
|
|
||||||
is_last_project: bool,
|
|
||||||
is_selected: bool,
|
|
||||||
cx: &mut RenderContext<Self>,
|
|
||||||
) -> ElementBox {
|
|
||||||
enum ToggleOnline {}
|
|
||||||
|
|
||||||
let project = &contact.projects[project_index];
|
|
||||||
let project_id = project.id;
|
|
||||||
let is_host = Some(contact.user.id) == current_user_id;
|
|
||||||
let open_project = open_project.and_then(|p| p.upgrade(cx.deref_mut()));
|
|
||||||
|
|
||||||
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.;
|
|
||||||
|
|
||||||
MouseEventHandler::<JoinProject>::new(project_id as usize, cx, |mouse_state, cx| {
|
|
||||||
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_project {
|
|
||||||
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(),
|
|
||||||
)
|
|
||||||
.with_children(open_project.and_then(|open_project| {
|
|
||||||
let is_going_offline = !open_project.read(cx).is_online();
|
|
||||||
if !mouse_state.hovered && !is_going_offline {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let button = MouseEventHandler::<ToggleProjectOnline>::new(
|
|
||||||
project_id as usize,
|
|
||||||
cx,
|
|
||||||
|state, _| {
|
|
||||||
let mut icon_style =
|
|
||||||
*theme.private_button.style_for(state, false);
|
|
||||||
icon_style.container.background_color =
|
|
||||||
row.container.background_color;
|
|
||||||
if is_going_offline {
|
|
||||||
icon_style.color = theme.disabled_button.color;
|
|
||||||
}
|
|
||||||
render_icon_button(&icon_style, "icons/lock_8.svg")
|
|
||||||
.aligned()
|
|
||||||
.boxed()
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
if is_going_offline {
|
|
||||||
Some(button.boxed())
|
|
||||||
} else {
|
|
||||||
Some(
|
|
||||||
button
|
|
||||||
.with_cursor_style(CursorStyle::PointingHand)
|
|
||||||
.on_click(MouseButton::Left, move |_, cx| {
|
|
||||||
cx.dispatch_action(ToggleProjectOnline {
|
|
||||||
project: Some(open_project.clone()),
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.with_tooltip::<ToggleOnline, _>(
|
|
||||||
project_id as usize,
|
|
||||||
"Take project offline".to_string(),
|
|
||||||
None,
|
|
||||||
tooltip_style.clone(),
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
.boxed(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
.constrained()
|
|
||||||
.with_width(host_avatar_height)
|
|
||||||
.boxed(),
|
|
||||||
)
|
|
||||||
.with_child(
|
|
||||||
Label::new(
|
|
||||||
project.visible_worktree_root_names.join(", "),
|
|
||||||
row.name.text.clone(),
|
|
||||||
)
|
|
||||||
.aligned()
|
|
||||||
.left()
|
|
||||||
.contained()
|
|
||||||
.with_style(row.name.container)
|
|
||||||
.flex(1., false)
|
|
||||||
.boxed(),
|
|
||||||
)
|
|
||||||
.with_children(project.guests.iter().filter_map(|participant| {
|
|
||||||
participant.avatar.clone().map(|avatar| {
|
|
||||||
Image::new(avatar)
|
|
||||||
.with_style(row.guest_avatar)
|
|
||||||
.aligned()
|
|
||||||
.left()
|
|
||||||
.contained()
|
|
||||||
.with_margin_right(row.guest_avatar_spacing)
|
|
||||||
.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 {
|
|
||||||
contact: contact.clone(),
|
|
||||||
project_index,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.boxed()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_offline_project(
|
|
||||||
project_handle: WeakModelHandle<Project>,
|
|
||||||
theme: &theme::ContactsPanel,
|
|
||||||
tooltip_style: &TooltipStyle,
|
|
||||||
is_selected: bool,
|
|
||||||
cx: &mut RenderContext<Self>,
|
|
||||||
) -> ElementBox {
|
|
||||||
let host_avatar_height = theme
|
|
||||||
.contact_avatar
|
|
||||||
.width
|
|
||||||
.or(theme.contact_avatar.height)
|
|
||||||
.unwrap_or(0.);
|
|
||||||
|
|
||||||
enum LocalProject {}
|
|
||||||
enum ToggleOnline {}
|
|
||||||
|
|
||||||
let project_id = project_handle.id();
|
|
||||||
MouseEventHandler::<LocalProject>::new(project_id, cx, |state, cx| {
|
|
||||||
let row = theme.project_row.style_for(state, is_selected);
|
|
||||||
let mut worktree_root_names = String::new();
|
|
||||||
let project = if let Some(project) = project_handle.upgrade(cx.deref_mut()) {
|
|
||||||
project.read(cx)
|
|
||||||
} else {
|
|
||||||
return Empty::new().boxed();
|
|
||||||
};
|
|
||||||
let is_going_online = project.is_online();
|
|
||||||
for tree in project.visible_worktrees(cx) {
|
|
||||||
if !worktree_root_names.is_empty() {
|
|
||||||
worktree_root_names.push_str(", ");
|
|
||||||
}
|
|
||||||
worktree_root_names.push_str(tree.read(cx).root_name());
|
|
||||||
}
|
|
||||||
|
|
||||||
Flex::row()
|
|
||||||
.with_child({
|
|
||||||
let button =
|
|
||||||
MouseEventHandler::<ToggleOnline>::new(project_id, cx, |state, _| {
|
|
||||||
let mut style = *theme.private_button.style_for(state, false);
|
|
||||||
if is_going_online {
|
|
||||||
style.color = theme.disabled_button.color;
|
|
||||||
}
|
|
||||||
render_icon_button(&style, "icons/lock_8.svg")
|
|
||||||
.aligned()
|
|
||||||
.constrained()
|
|
||||||
.with_width(host_avatar_height)
|
|
||||||
.boxed()
|
|
||||||
});
|
|
||||||
|
|
||||||
if is_going_online {
|
|
||||||
button.boxed()
|
|
||||||
} else {
|
|
||||||
button
|
|
||||||
.with_cursor_style(CursorStyle::PointingHand)
|
|
||||||
.on_click(MouseButton::Left, move |_, cx| {
|
|
||||||
let project = project_handle.upgrade(cx.app);
|
|
||||||
cx.dispatch_action(ToggleProjectOnline { project })
|
|
||||||
})
|
|
||||||
.with_tooltip::<ToggleOnline, _>(
|
|
||||||
project_id,
|
|
||||||
"Take project online".to_string(),
|
|
||||||
None,
|
|
||||||
tooltip_style.clone(),
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
.boxed()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.with_child(
|
|
||||||
Label::new(worktree_root_names, 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()
|
|
||||||
})
|
|
||||||
.boxed()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_contact_request(
|
fn render_contact_request(
|
||||||
user: Arc<User>,
|
user: Arc<User>,
|
||||||
user_store: ModelHandle<UserStore>,
|
user_store: ModelHandle<UserStore>,
|
||||||
|
@ -710,7 +421,6 @@ impl ContactsPanel {
|
||||||
|
|
||||||
fn update_entries(&mut self, cx: &mut ViewContext<Self>) {
|
fn update_entries(&mut self, cx: &mut ViewContext<Self>) {
|
||||||
let user_store = self.user_store.read(cx);
|
let user_store = self.user_store.read(cx);
|
||||||
let project_store = self.project_store.read(cx);
|
|
||||||
let query = self.filter_editor.read(cx).text(cx);
|
let query = self.filter_editor.read(cx).text(cx);
|
||||||
let executor = cx.background().clone();
|
let executor = cx.background().clone();
|
||||||
|
|
||||||
|
@ -837,60 +547,6 @@ impl ContactsPanel {
|
||||||
for mat in matches {
|
for mat in matches {
|
||||||
let contact = &contacts[mat.candidate_id];
|
let contact = &contacts[mat.candidate_id];
|
||||||
self.entries.push(ContactEntry::Contact(contact.clone()));
|
self.entries.push(ContactEntry::Contact(contact.clone()));
|
||||||
|
|
||||||
let is_current_user = current_user
|
|
||||||
.as_ref()
|
|
||||||
.map_or(false, |user| user.id == contact.user.id);
|
|
||||||
if is_current_user {
|
|
||||||
let mut open_projects =
|
|
||||||
project_store.projects(cx).collect::<Vec<_>>();
|
|
||||||
self.entries.extend(
|
|
||||||
contact.projects.iter().enumerate().filter_map(
|
|
||||||
|(ix, project)| {
|
|
||||||
let open_project = open_projects
|
|
||||||
.iter()
|
|
||||||
.position(|p| {
|
|
||||||
p.read(cx).remote_id() == Some(project.id)
|
|
||||||
})
|
|
||||||
.map(|ix| open_projects.remove(ix).downgrade());
|
|
||||||
if project.visible_worktree_root_names.is_empty() {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(ContactEntry::ContactProject(
|
|
||||||
contact.clone(),
|
|
||||||
ix,
|
|
||||||
open_project,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
self.entries.extend(open_projects.into_iter().filter_map(
|
|
||||||
|project| {
|
|
||||||
if project.read(cx).visible_worktrees(cx).next().is_none() {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(ContactEntry::OfflineProject(project.downgrade()))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
));
|
|
||||||
} else {
|
|
||||||
self.entries.extend(
|
|
||||||
contact.projects.iter().enumerate().filter_map(
|
|
||||||
|(ix, project)| {
|
|
||||||
if project.visible_worktree_root_names.is_empty() {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(ContactEntry::ContactProject(
|
|
||||||
contact.clone(),
|
|
||||||
ix,
|
|
||||||
None,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -981,18 +637,6 @@ impl ContactsPanel {
|
||||||
let section = *section;
|
let section = *section;
|
||||||
self.toggle_expanded(&ToggleExpanded(section), cx);
|
self.toggle_expanded(&ToggleExpanded(section), cx);
|
||||||
}
|
}
|
||||||
ContactEntry::ContactProject(contact, project_index, open_project) => {
|
|
||||||
if let Some(open_project) = open_project {
|
|
||||||
workspace::activate_workspace_for_project(cx, |_, cx| {
|
|
||||||
cx.model_id() == open_project.id()
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
cx.dispatch_global_action(JoinProject {
|
|
||||||
contact: contact.clone(),
|
|
||||||
project_index: *project_index,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1181,16 +825,6 @@ impl PartialEq for ContactEntry {
|
||||||
return contact_1.user.id == contact_2.user.id;
|
return contact_1.user.id == contact_2.user.id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ContactEntry::ContactProject(contact_1, ix_1, _) => {
|
|
||||||
if let ContactEntry::ContactProject(contact_2, ix_2, _) = other {
|
|
||||||
return contact_1.user.id == contact_2.user.id && ix_1 == ix_2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ContactEntry::OfflineProject(project_1) => {
|
|
||||||
if let ContactEntry::OfflineProject(project_2) = other {
|
|
||||||
return project_1.id() == project_2.id();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
@ -1205,7 +839,7 @@ mod tests {
|
||||||
Client,
|
Client,
|
||||||
};
|
};
|
||||||
use collections::HashSet;
|
use collections::HashSet;
|
||||||
use gpui::{serde_json::json, TestAppContext};
|
use gpui::TestAppContext;
|
||||||
use language::LanguageRegistry;
|
use language::LanguageRegistry;
|
||||||
use project::{FakeFs, Project};
|
use project::{FakeFs, Project};
|
||||||
|
|
||||||
|
@ -1221,8 +855,6 @@ mod tests {
|
||||||
let project_store = cx.add_model(|_| ProjectStore::new(project::Db::open_fake()));
|
let project_store = cx.add_model(|_| ProjectStore::new(project::Db::open_fake()));
|
||||||
let server = FakeServer::for_client(current_user_id, &client, cx).await;
|
let server = FakeServer::for_client(current_user_id, &client, cx).await;
|
||||||
let fs = FakeFs::new(cx.background());
|
let fs = FakeFs::new(cx.background());
|
||||||
fs.insert_tree("/private_dir", json!({ "one.rs": "" }))
|
|
||||||
.await;
|
|
||||||
let project = cx.update(|cx| {
|
let project = cx.update(|cx| {
|
||||||
Project::local(
|
Project::local(
|
||||||
false,
|
false,
|
||||||
|
@ -1234,14 +866,6 @@ mod tests {
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
let worktree_id = project
|
|
||||||
.update(cx, |project, cx| {
|
|
||||||
project.find_or_create_local_worktree("/private_dir", true, cx)
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.0
|
|
||||||
.read_with(cx, |worktree, _| worktree.id().to_proto());
|
|
||||||
|
|
||||||
let (_, workspace) =
|
let (_, workspace) =
|
||||||
cx.add_window(|cx| Workspace::new(project.clone(), |_, _| unimplemented!(), cx));
|
cx.add_window(|cx| Workspace::new(project.clone(), |_, _| unimplemented!(), cx));
|
||||||
|
@ -1315,55 +939,26 @@ mod tests {
|
||||||
user_id: 3,
|
user_id: 3,
|
||||||
online: true,
|
online: true,
|
||||||
should_notify: false,
|
should_notify: false,
|
||||||
projects: vec![proto::ProjectMetadata {
|
|
||||||
id: 101,
|
|
||||||
visible_worktree_root_names: vec!["dir1".to_string()],
|
|
||||||
guests: vec![2],
|
|
||||||
}],
|
|
||||||
},
|
},
|
||||||
proto::Contact {
|
proto::Contact {
|
||||||
user_id: 4,
|
user_id: 4,
|
||||||
online: true,
|
online: true,
|
||||||
should_notify: false,
|
should_notify: false,
|
||||||
projects: vec![proto::ProjectMetadata {
|
|
||||||
id: 102,
|
|
||||||
visible_worktree_root_names: vec!["dir2".to_string()],
|
|
||||||
guests: vec![2],
|
|
||||||
}],
|
|
||||||
},
|
},
|
||||||
proto::Contact {
|
proto::Contact {
|
||||||
user_id: 5,
|
user_id: 5,
|
||||||
online: false,
|
online: false,
|
||||||
should_notify: false,
|
should_notify: false,
|
||||||
projects: vec![],
|
|
||||||
},
|
},
|
||||||
proto::Contact {
|
proto::Contact {
|
||||||
user_id: current_user_id,
|
user_id: current_user_id,
|
||||||
online: true,
|
online: true,
|
||||||
should_notify: false,
|
should_notify: false,
|
||||||
projects: vec![proto::ProjectMetadata {
|
|
||||||
id: 103,
|
|
||||||
visible_worktree_root_names: vec!["dir3".to_string()],
|
|
||||||
guests: vec![3],
|
|
||||||
}],
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
..Default::default()
|
..Default::default()
|
||||||
});
|
});
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
server
|
|
||||||
.receive::<proto::UpdateProject>()
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.payload,
|
|
||||||
proto::UpdateProject {
|
|
||||||
project_id: 200,
|
|
||||||
online: false,
|
|
||||||
worktrees: vec![]
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
cx.foreground().run_until_parked();
|
cx.foreground().run_until_parked();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
cx.read(|cx| render_to_strings(&panel, cx)),
|
cx.read(|cx| render_to_strings(&panel, cx)),
|
||||||
|
@ -1373,168 +968,8 @@ mod tests {
|
||||||
" outgoing user_two",
|
" outgoing user_two",
|
||||||
"v Online",
|
"v Online",
|
||||||
" the_current_user",
|
" the_current_user",
|
||||||
" dir3",
|
|
||||||
" 🔒 private_dir",
|
|
||||||
" user_four",
|
" user_four",
|
||||||
" dir2",
|
|
||||||
" user_three",
|
" user_three",
|
||||||
" dir1",
|
|
||||||
"v Offline",
|
|
||||||
" user_five",
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Take a project online. It appears as loading, since the project
|
|
||||||
// isn't yet visible to other contacts.
|
|
||||||
project.update(cx, |project, cx| project.set_online(true, cx));
|
|
||||||
cx.foreground().run_until_parked();
|
|
||||||
assert_eq!(
|
|
||||||
cx.read(|cx| render_to_strings(&panel, cx)),
|
|
||||||
&[
|
|
||||||
"v Requests",
|
|
||||||
" incoming user_one",
|
|
||||||
" outgoing user_two",
|
|
||||||
"v Online",
|
|
||||||
" the_current_user",
|
|
||||||
" dir3",
|
|
||||||
" 🔒 private_dir (going online...)",
|
|
||||||
" user_four",
|
|
||||||
" dir2",
|
|
||||||
" user_three",
|
|
||||||
" dir1",
|
|
||||||
"v Offline",
|
|
||||||
" user_five",
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
// The server receives the project's metadata and updates the contact metadata
|
|
||||||
// for the current user. Now the project appears as online.
|
|
||||||
assert_eq!(
|
|
||||||
server
|
|
||||||
.receive::<proto::UpdateProject>()
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.payload,
|
|
||||||
proto::UpdateProject {
|
|
||||||
project_id: 200,
|
|
||||||
online: true,
|
|
||||||
worktrees: vec![proto::WorktreeMetadata {
|
|
||||||
id: worktree_id,
|
|
||||||
root_name: "private_dir".to_string(),
|
|
||||||
visible: true,
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
);
|
|
||||||
server
|
|
||||||
.receive::<proto::UpdateWorktreeExtensions>()
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
server.send(proto::UpdateContacts {
|
|
||||||
contacts: vec![proto::Contact {
|
|
||||||
user_id: current_user_id,
|
|
||||||
online: true,
|
|
||||||
should_notify: false,
|
|
||||||
projects: vec![
|
|
||||||
proto::ProjectMetadata {
|
|
||||||
id: 103,
|
|
||||||
visible_worktree_root_names: vec!["dir3".to_string()],
|
|
||||||
guests: vec![3],
|
|
||||||
},
|
|
||||||
proto::ProjectMetadata {
|
|
||||||
id: 200,
|
|
||||||
visible_worktree_root_names: vec!["private_dir".to_string()],
|
|
||||||
guests: vec![3],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}],
|
|
||||||
..Default::default()
|
|
||||||
});
|
|
||||||
cx.foreground().run_until_parked();
|
|
||||||
assert_eq!(
|
|
||||||
cx.read(|cx| render_to_strings(&panel, cx)),
|
|
||||||
&[
|
|
||||||
"v Requests",
|
|
||||||
" incoming user_one",
|
|
||||||
" outgoing user_two",
|
|
||||||
"v Online",
|
|
||||||
" the_current_user",
|
|
||||||
" dir3",
|
|
||||||
" private_dir",
|
|
||||||
" user_four",
|
|
||||||
" dir2",
|
|
||||||
" user_three",
|
|
||||||
" dir1",
|
|
||||||
"v Offline",
|
|
||||||
" user_five",
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Take the project offline. It appears as loading.
|
|
||||||
project.update(cx, |project, cx| project.set_online(false, cx));
|
|
||||||
cx.foreground().run_until_parked();
|
|
||||||
assert_eq!(
|
|
||||||
cx.read(|cx| render_to_strings(&panel, cx)),
|
|
||||||
&[
|
|
||||||
"v Requests",
|
|
||||||
" incoming user_one",
|
|
||||||
" outgoing user_two",
|
|
||||||
"v Online",
|
|
||||||
" the_current_user",
|
|
||||||
" dir3",
|
|
||||||
" private_dir (going offline...)",
|
|
||||||
" user_four",
|
|
||||||
" dir2",
|
|
||||||
" user_three",
|
|
||||||
" dir1",
|
|
||||||
"v Offline",
|
|
||||||
" user_five",
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
// The server receives the unregister request and updates the contact
|
|
||||||
// metadata for the current user. The project is now offline.
|
|
||||||
assert_eq!(
|
|
||||||
server
|
|
||||||
.receive::<proto::UpdateProject>()
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.payload,
|
|
||||||
proto::UpdateProject {
|
|
||||||
project_id: 200,
|
|
||||||
online: false,
|
|
||||||
worktrees: vec![]
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
server.send(proto::UpdateContacts {
|
|
||||||
contacts: vec![proto::Contact {
|
|
||||||
user_id: current_user_id,
|
|
||||||
online: true,
|
|
||||||
should_notify: false,
|
|
||||||
projects: vec![proto::ProjectMetadata {
|
|
||||||
id: 103,
|
|
||||||
visible_worktree_root_names: vec!["dir3".to_string()],
|
|
||||||
guests: vec![3],
|
|
||||||
}],
|
|
||||||
}],
|
|
||||||
..Default::default()
|
|
||||||
});
|
|
||||||
cx.foreground().run_until_parked();
|
|
||||||
assert_eq!(
|
|
||||||
cx.read(|cx| render_to_strings(&panel, cx)),
|
|
||||||
&[
|
|
||||||
"v Requests",
|
|
||||||
" incoming user_one",
|
|
||||||
" outgoing user_two",
|
|
||||||
"v Online",
|
|
||||||
" the_current_user",
|
|
||||||
" dir3",
|
|
||||||
" 🔒 private_dir",
|
|
||||||
" user_four",
|
|
||||||
" dir2",
|
|
||||||
" user_three",
|
|
||||||
" dir1",
|
|
||||||
"v Offline",
|
"v Offline",
|
||||||
" user_five",
|
" user_five",
|
||||||
]
|
]
|
||||||
|
@ -1551,7 +986,6 @@ mod tests {
|
||||||
&[
|
&[
|
||||||
"v Online",
|
"v Online",
|
||||||
" user_four <=== selected",
|
" user_four <=== selected",
|
||||||
" dir2",
|
|
||||||
"v Offline",
|
"v Offline",
|
||||||
" user_five",
|
" user_five",
|
||||||
]
|
]
|
||||||
|
@ -1565,25 +999,23 @@ mod tests {
|
||||||
&[
|
&[
|
||||||
"v Online",
|
"v Online",
|
||||||
" user_four",
|
" user_four",
|
||||||
" dir2 <=== selected",
|
|
||||||
"v Offline",
|
|
||||||
" user_five",
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
panel.update(cx, |panel, cx| {
|
|
||||||
panel.select_next(&Default::default(), cx);
|
|
||||||
});
|
|
||||||
assert_eq!(
|
|
||||||
cx.read(|cx| render_to_strings(&panel, cx)),
|
|
||||||
&[
|
|
||||||
"v Online",
|
|
||||||
" user_four",
|
|
||||||
" dir2",
|
|
||||||
"v Offline <=== selected",
|
"v Offline <=== selected",
|
||||||
" user_five",
|
" user_five",
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
panel.update(cx, |panel, cx| {
|
||||||
|
panel.select_next(&Default::default(), cx);
|
||||||
|
});
|
||||||
|
assert_eq!(
|
||||||
|
cx.read(|cx| render_to_strings(&panel, cx)),
|
||||||
|
&[
|
||||||
|
"v Online",
|
||||||
|
" user_four",
|
||||||
|
"v Offline",
|
||||||
|
" user_five <=== selected",
|
||||||
|
]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_to_strings(panel: &ViewHandle<ContactsPanel>, cx: &AppContext) -> Vec<String> {
|
fn render_to_strings(panel: &ViewHandle<ContactsPanel>, cx: &AppContext) -> Vec<String> {
|
||||||
|
@ -1608,37 +1040,6 @@ mod tests {
|
||||||
ContactEntry::Contact(contact) => {
|
ContactEntry::Contact(contact) => {
|
||||||
format!(" {}", contact.user.github_login)
|
format!(" {}", contact.user.github_login)
|
||||||
}
|
}
|
||||||
ContactEntry::ContactProject(contact, project_ix, project) => {
|
|
||||||
let project = project
|
|
||||||
.and_then(|p| p.upgrade(cx))
|
|
||||||
.map(|project| project.read(cx));
|
|
||||||
format!(
|
|
||||||
" {}{}",
|
|
||||||
contact.projects[*project_ix]
|
|
||||||
.visible_worktree_root_names
|
|
||||||
.join(", "),
|
|
||||||
if project.map_or(true, |project| project.is_online()) {
|
|
||||||
""
|
|
||||||
} else {
|
|
||||||
" (going offline...)"
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
ContactEntry::OfflineProject(project) => {
|
|
||||||
let project = project.upgrade(cx).unwrap().read(cx);
|
|
||||||
format!(
|
|
||||||
" 🔒 {}{}",
|
|
||||||
project
|
|
||||||
.worktree_root_names(cx)
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join(", "),
|
|
||||||
if project.is_online() {
|
|
||||||
" (going online...)"
|
|
||||||
} else {
|
|
||||||
""
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if panel.selection == Some(ix) {
|
if panel.selection == Some(ix) {
|
||||||
|
|
|
@ -1048,15 +1048,8 @@ message ChannelMessage {
|
||||||
|
|
||||||
message Contact {
|
message Contact {
|
||||||
uint64 user_id = 1;
|
uint64 user_id = 1;
|
||||||
repeated ProjectMetadata projects = 2;
|
bool online = 2;
|
||||||
bool online = 3;
|
bool should_notify = 3;
|
||||||
bool should_notify = 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
message ProjectMetadata {
|
|
||||||
uint64 id = 1;
|
|
||||||
repeated string visible_worktree_root_names = 3;
|
|
||||||
repeated uint64 guests = 4;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
message WorktreeMetadata {
|
message WorktreeMetadata {
|
||||||
|
|
|
@ -1,185 +0,0 @@
|
||||||
use crate::{sidebar::SidebarSide, AppState, ToggleFollow, Workspace};
|
|
||||||
use anyhow::Result;
|
|
||||||
use client::{proto, Client, Contact};
|
|
||||||
use gpui::{
|
|
||||||
elements::*, ElementBox, Entity, ImageData, MutableAppContext, RenderContext, Task, View,
|
|
||||||
ViewContext,
|
|
||||||
};
|
|
||||||
use project::Project;
|
|
||||||
use settings::Settings;
|
|
||||||
use std::sync::Arc;
|
|
||||||
use util::ResultExt;
|
|
||||||
|
|
||||||
pub struct WaitingRoom {
|
|
||||||
project_id: u64,
|
|
||||||
avatar: Option<Arc<ImageData>>,
|
|
||||||
message: String,
|
|
||||||
waiting: bool,
|
|
||||||
client: Arc<Client>,
|
|
||||||
_join_task: Task<Result<()>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Entity for WaitingRoom {
|
|
||||||
type Event = ();
|
|
||||||
|
|
||||||
fn release(&mut self, _: &mut MutableAppContext) {
|
|
||||||
if self.waiting {
|
|
||||||
self.client
|
|
||||||
.send(proto::LeaveProject {
|
|
||||||
project_id: self.project_id,
|
|
||||||
})
|
|
||||||
.log_err();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl View for WaitingRoom {
|
|
||||||
fn ui_name() -> &'static str {
|
|
||||||
"WaitingRoom"
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
|
|
||||||
let theme = &cx.global::<Settings>().theme.workspace;
|
|
||||||
|
|
||||||
Flex::column()
|
|
||||||
.with_children(self.avatar.clone().map(|avatar| {
|
|
||||||
Image::new(avatar)
|
|
||||||
.with_style(theme.joining_project_avatar)
|
|
||||||
.aligned()
|
|
||||||
.boxed()
|
|
||||||
}))
|
|
||||||
.with_child(
|
|
||||||
Text::new(
|
|
||||||
self.message.clone(),
|
|
||||||
theme.joining_project_message.text.clone(),
|
|
||||||
)
|
|
||||||
.contained()
|
|
||||||
.with_style(theme.joining_project_message.container)
|
|
||||||
.aligned()
|
|
||||||
.boxed(),
|
|
||||||
)
|
|
||||||
.aligned()
|
|
||||||
.contained()
|
|
||||||
.with_background_color(theme.background)
|
|
||||||
.boxed()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WaitingRoom {
|
|
||||||
pub fn new(
|
|
||||||
contact: Arc<Contact>,
|
|
||||||
project_index: usize,
|
|
||||||
app_state: Arc<AppState>,
|
|
||||||
cx: &mut ViewContext<Self>,
|
|
||||||
) -> Self {
|
|
||||||
let project_id = contact.projects[project_index].id;
|
|
||||||
let client = app_state.client.clone();
|
|
||||||
let _join_task = cx.spawn_weak({
|
|
||||||
let contact = contact.clone();
|
|
||||||
|this, mut cx| async move {
|
|
||||||
let project = Project::remote(
|
|
||||||
project_id,
|
|
||||||
app_state.client.clone(),
|
|
||||||
app_state.user_store.clone(),
|
|
||||||
app_state.project_store.clone(),
|
|
||||||
app_state.languages.clone(),
|
|
||||||
app_state.fs.clone(),
|
|
||||||
cx.clone(),
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
if let Some(this) = this.upgrade(&cx) {
|
|
||||||
this.update(&mut cx, |this, cx| {
|
|
||||||
this.waiting = false;
|
|
||||||
match project {
|
|
||||||
Ok(project) => {
|
|
||||||
cx.replace_root_view(|cx| {
|
|
||||||
let mut workspace =
|
|
||||||
Workspace::new(project, app_state.default_item_factory, cx);
|
|
||||||
(app_state.initialize_workspace)(
|
|
||||||
&mut workspace,
|
|
||||||
&app_state,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
workspace.toggle_sidebar(SidebarSide::Left, cx);
|
|
||||||
if let Some((host_peer_id, _)) = workspace
|
|
||||||
.project
|
|
||||||
.read(cx)
|
|
||||||
.collaborators()
|
|
||||||
.iter()
|
|
||||||
.find(|(_, collaborator)| collaborator.replica_id == 0)
|
|
||||||
{
|
|
||||||
if let Some(follow) = workspace
|
|
||||||
.toggle_follow(&ToggleFollow(*host_peer_id), cx)
|
|
||||||
{
|
|
||||||
follow.detach_and_log_err(cx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
workspace
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Err(error) => {
|
|
||||||
let login = &contact.user.github_login;
|
|
||||||
let message = match error {
|
|
||||||
project::JoinProjectError::HostDeclined => {
|
|
||||||
format!("@{} declined your request.", login)
|
|
||||||
}
|
|
||||||
project::JoinProjectError::HostClosedProject => {
|
|
||||||
format!(
|
|
||||||
"@{} closed their copy of {}.",
|
|
||||||
login,
|
|
||||||
humanize_list(
|
|
||||||
&contact.projects[project_index]
|
|
||||||
.visible_worktree_root_names
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
project::JoinProjectError::HostWentOffline => {
|
|
||||||
format!("@{} went offline.", login)
|
|
||||||
}
|
|
||||||
project::JoinProjectError::Other(error) => {
|
|
||||||
log::error!("error joining project: {}", error);
|
|
||||||
"An error occurred.".to_string()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
this.message = message;
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
Self {
|
|
||||||
project_id,
|
|
||||||
avatar: contact.user.avatar.clone(),
|
|
||||||
message: format!(
|
|
||||||
"Asking to join @{}'s copy of {}...",
|
|
||||||
contact.user.github_login,
|
|
||||||
humanize_list(&contact.projects[project_index].visible_worktree_root_names)
|
|
||||||
),
|
|
||||||
waiting: true,
|
|
||||||
client,
|
|
||||||
_join_task,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn humanize_list<'a>(items: impl IntoIterator<Item = &'a String>) -> String {
|
|
||||||
let mut list = String::new();
|
|
||||||
let mut items = items.into_iter().enumerate().peekable();
|
|
||||||
while let Some((ix, item)) = items.next() {
|
|
||||||
if ix > 0 {
|
|
||||||
list.push_str(", ");
|
|
||||||
if items.peek().is_none() {
|
|
||||||
list.push_str("and ");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
list.push_str(item);
|
|
||||||
}
|
|
||||||
list
|
|
||||||
}
|
|
|
@ -10,7 +10,6 @@ pub mod searchable;
|
||||||
pub mod sidebar;
|
pub mod sidebar;
|
||||||
mod status_bar;
|
mod status_bar;
|
||||||
mod toolbar;
|
mod toolbar;
|
||||||
mod waiting_room;
|
|
||||||
|
|
||||||
use anyhow::{anyhow, Context, Result};
|
use anyhow::{anyhow, Context, Result};
|
||||||
use client::{proto, Client, Contact, PeerId, Subscription, TypedEnvelope, UserStore};
|
use client::{proto, Client, Contact, PeerId, Subscription, TypedEnvelope, UserStore};
|
||||||
|
@ -58,7 +57,6 @@ use std::{
|
||||||
use theme::{Theme, ThemeRegistry};
|
use theme::{Theme, ThemeRegistry};
|
||||||
pub use toolbar::{ToolbarItemLocation, ToolbarItemView};
|
pub use toolbar::{ToolbarItemLocation, ToolbarItemView};
|
||||||
use util::ResultExt;
|
use util::ResultExt;
|
||||||
use waiting_room::WaitingRoom;
|
|
||||||
|
|
||||||
type ProjectItemBuilders = HashMap<
|
type ProjectItemBuilders = HashMap<
|
||||||
TypeId,
|
TypeId,
|
||||||
|
@ -167,14 +165,6 @@ pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
cx.add_global_action({
|
|
||||||
let app_state = Arc::downgrade(&app_state);
|
|
||||||
move |action: &JoinProject, cx: &mut MutableAppContext| {
|
|
||||||
if let Some(app_state) = app_state.upgrade() {
|
|
||||||
join_project(action.contact.clone(), action.project_index, &app_state, cx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
cx.add_async_action(Workspace::toggle_follow);
|
cx.add_async_action(Workspace::toggle_follow);
|
||||||
cx.add_async_action(Workspace::follow_next_collaborator);
|
cx.add_async_action(Workspace::follow_next_collaborator);
|
||||||
|
@ -2663,28 +2653,6 @@ pub fn open_paths(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn join_project(
|
|
||||||
contact: Arc<Contact>,
|
|
||||||
project_index: usize,
|
|
||||||
app_state: &Arc<AppState>,
|
|
||||||
cx: &mut MutableAppContext,
|
|
||||||
) {
|
|
||||||
let project_id = contact.projects[project_index].id;
|
|
||||||
|
|
||||||
for window_id in cx.window_ids().collect::<Vec<_>>() {
|
|
||||||
if let Some(workspace) = cx.root_view::<Workspace>(window_id) {
|
|
||||||
if workspace.read(cx).project().read(cx).remote_id() == Some(project_id) {
|
|
||||||
cx.activate_window(window_id);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cx.add_window((app_state.build_window_options)(), |cx| {
|
|
||||||
WaitingRoom::new(contact, project_index, app_state.clone(), cx)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn open_new(app_state: &Arc<AppState>, cx: &mut MutableAppContext) {
|
fn open_new(app_state: &Arc<AppState>, cx: &mut MutableAppContext) {
|
||||||
let (window_id, workspace) = cx.add_window((app_state.build_window_options)(), |cx| {
|
let (window_id, workspace) = cx.add_window((app_state.build_window_options)(), |cx| {
|
||||||
let mut workspace = Workspace::new(
|
let mut workspace = Workspace::new(
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue