Show the reason why a join request was declined

Co-Authored-By: Nathan Sobo <nathan@zed.dev>
This commit is contained in:
Antonio Scandurra 2022-05-16 19:56:10 +02:00
parent 740ec3d192
commit ed6ed99d8f
6 changed files with 203 additions and 17 deletions

1
Cargo.lock generated
View file

@ -3394,6 +3394,7 @@ dependencies = [
"sum_tree", "sum_tree",
"tempdir", "tempdir",
"text", "text",
"thiserror",
"toml", "toml",
"unindent", "unindent",
"util", "util",

View file

@ -354,7 +354,9 @@ impl Server {
receipt, receipt,
proto::JoinProjectResponse { proto::JoinProjectResponse {
variant: Some(proto::join_project_response::Variant::Decline( variant: Some(proto::join_project_response::Variant::Decline(
proto::join_project_response::Decline {}, proto::join_project_response::Decline {
reason: proto::join_project_response::decline::Reason::WentOffline as i32
},
)), )),
}, },
)?; )?;
@ -434,7 +436,10 @@ impl Server {
receipt, receipt,
proto::JoinProjectResponse { proto::JoinProjectResponse {
variant: Some(proto::join_project_response::Variant::Decline( variant: Some(proto::join_project_response::Variant::Decline(
proto::join_project_response::Decline {}, proto::join_project_response::Decline {
reason: proto::join_project_response::decline::Reason::Closed
as i32,
},
)), )),
}, },
)?; )?;
@ -542,7 +547,10 @@ impl Server {
receipt, receipt,
proto::JoinProjectResponse { proto::JoinProjectResponse {
variant: Some(proto::join_project_response::Variant::Decline( variant: Some(proto::join_project_response::Variant::Decline(
proto::join_project_response::Decline {}, proto::join_project_response::Decline {
reason: proto::join_project_response::decline::Reason::Declined
as i32,
},
)), )),
}, },
)?; )?;
@ -1837,17 +1845,26 @@ mod tests {
} }
#[gpui::test(iterations = 10)] #[gpui::test(iterations = 10)]
async fn test_host_disconnect(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { async fn test_host_disconnect(
cx_a: &mut TestAppContext,
cx_b: &mut TestAppContext,
cx_c: &mut TestAppContext,
) {
let lang_registry = Arc::new(LanguageRegistry::test()); let lang_registry = Arc::new(LanguageRegistry::test());
let fs = FakeFs::new(cx_a.background()); let fs = FakeFs::new(cx_a.background());
cx_a.foreground().forbid_parking(); cx_a.foreground().forbid_parking();
// Connect to a server as 2 clients. // Connect to a server as 3 clients.
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; 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_a = server.create_client(cx_a, "user_a").await;
let mut client_b = server.create_client(cx_b, "user_b").await; let mut client_b = server.create_client(cx_b, "user_b").await;
let client_c = server.create_client(cx_c, "user_c").await;
server server
.make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) .make_contacts(vec![
(&client_a, cx_a),
(&client_b, cx_b),
(&client_c, cx_c),
])
.await; .await;
// Share a project as client A // Share a project as client A
@ -1868,6 +1885,9 @@ mod tests {
cx, cx,
) )
}); });
let project_id = project_a
.read_with(cx_a, |project, _| project.next_remote_id())
.await;
let (worktree_a, _) = project_a let (worktree_a, _) = project_a
.update(cx_a, |p, cx| { .update(cx_a, |p, cx| {
p.find_or_create_local_worktree("/a", true, cx) p.find_or_create_local_worktree("/a", true, cx)
@ -1887,6 +1907,24 @@ mod tests {
.await .await
.unwrap(); .unwrap();
// Request to join that project as client C
let project_c = cx_c.spawn(|mut cx| {
let client = client_c.client.clone();
let user_store = client_c.user_store.clone();
let lang_registry = lang_registry.clone();
async move {
Project::remote(
project_id,
client,
user_store,
lang_registry.clone(),
FakeFs::new(cx.background()),
&mut cx,
)
.await
}
});
// Drop client A's connection. Collaborators should disappear and the project should not be shown as shared. // Drop client A's connection. Collaborators should disappear and the project should not be shown as shared.
server.disconnect_client(client_a.current_user_id(cx_a)); server.disconnect_client(client_a.current_user_id(cx_a));
cx_a.foreground().advance_clock(rpc::RECEIVE_TIMEOUT); cx_a.foreground().advance_clock(rpc::RECEIVE_TIMEOUT);
@ -1901,6 +1939,10 @@ mod tests {
cx_b.update(|_| { cx_b.update(|_| {
drop(project_b); drop(project_b);
}); });
assert!(matches!(
project_c.await.unwrap_err(),
project::JoinProjectError::HostWentOffline
));
// Ensure guests can still join. // Ensure guests can still join.
let project_b2 = client_b.build_remote_project(&project_a, cx_a, cx_b).await; let project_b2 = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
@ -1911,6 +1953,102 @@ mod tests {
.unwrap(); .unwrap();
} }
#[gpui::test(iterations = 10)]
async fn test_decline_join_request(
deterministic: Arc<Deterministic>,
cx_a: &mut TestAppContext,
cx_b: &mut TestAppContext,
) {
let lang_registry = Arc::new(LanguageRegistry::test());
let fs = FakeFs::new(cx_a.background());
cx_a.foreground().forbid_parking();
// Connect to a server as 2 clients.
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;
server
.make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
.await;
// Share a project as client A
fs.insert_tree("/a", json!({})).await;
let project_a = cx_a.update(|cx| {
Project::local(
client_a.clone(),
client_a.user_store.clone(),
lang_registry.clone(),
fs.clone(),
cx,
)
});
let project_id = project_a
.read_with(cx_a, |project, _| project.next_remote_id())
.await;
let (worktree_a, _) = project_a
.update(cx_a, |p, cx| {
p.find_or_create_local_worktree("/a", true, cx)
})
.await
.unwrap();
worktree_a
.read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete())
.await;
// Request to join that project as client B
let project_b = cx_b.spawn(|mut cx| {
let client = client_b.client.clone();
let user_store = client_b.user_store.clone();
let lang_registry = lang_registry.clone();
async move {
Project::remote(
project_id,
client,
user_store,
lang_registry.clone(),
FakeFs::new(cx.background()),
&mut cx,
)
.await
}
});
deterministic.run_until_parked();
project_a.update(cx_a, |project, cx| {
project.respond_to_join_request(client_b.user_id().unwrap(), false, cx)
});
assert!(matches!(
project_b.await.unwrap_err(),
project::JoinProjectError::HostDeclined
));
// Request to join the project again as client B
let project_b = cx_b.spawn(|mut cx| {
let client = client_b.client.clone();
let user_store = client_b.user_store.clone();
let lang_registry = lang_registry.clone();
async move {
Project::remote(
project_id,
client,
user_store,
lang_registry.clone(),
FakeFs::new(cx.background()),
&mut cx,
)
.await
}
});
// Close the project on the host
deterministic.run_until_parked();
cx_a.update(|_| drop(project_a));
deterministic.run_until_parked();
assert!(matches!(
project_b.await.unwrap_err(),
project::JoinProjectError::HostClosedProject
));
}
#[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,

View file

@ -45,6 +45,7 @@ serde_json = { version = "1.0.64", features = ["preserve_order"] }
sha2 = "0.10" sha2 = "0.10"
similar = "1.3" similar = "1.3"
smol = "1.2.5" smol = "1.2.5"
thiserror = "1.0.29"
toml = "0.5" toml = "0.5"
[dev-dependencies] [dev-dependencies]

View file

@ -49,6 +49,7 @@ use std::{
}, },
time::Instant, time::Instant,
}; };
use thiserror::Error;
use util::{post_inc, ResultExt, TryFutureExt as _}; use util::{post_inc, ResultExt, TryFutureExt as _};
pub use fs::*; pub use fs::*;
@ -90,6 +91,18 @@ pub struct Project {
nonce: u128, nonce: u128,
} }
#[derive(Error, Debug)]
pub enum JoinProjectError {
#[error("host declined join request")]
HostDeclined,
#[error("host closed the project")]
HostClosedProject,
#[error("host went offline")]
HostWentOffline,
#[error("{0}")]
Other(#[from] anyhow::Error),
}
enum OpenBuffer { enum OpenBuffer {
Strong(ModelHandle<Buffer>), Strong(ModelHandle<Buffer>),
Weak(WeakModelHandle<Buffer>), Weak(WeakModelHandle<Buffer>),
@ -356,7 +369,7 @@ impl Project {
languages: Arc<LanguageRegistry>, languages: Arc<LanguageRegistry>,
fs: Arc<dyn Fs>, fs: Arc<dyn Fs>,
cx: &mut AsyncAppContext, cx: &mut AsyncAppContext,
) -> Result<ModelHandle<Self>> { ) -> Result<ModelHandle<Self>, JoinProjectError> {
client.authenticate_and_connect(true, &cx).await?; client.authenticate_and_connect(true, &cx).await?;
let response = client let response = client
@ -367,7 +380,20 @@ impl Project {
let response = match response.variant.ok_or_else(|| anyhow!("missing variant"))? { let response = match response.variant.ok_or_else(|| anyhow!("missing variant"))? {
proto::join_project_response::Variant::Accept(response) => response, proto::join_project_response::Variant::Accept(response) => response,
proto::join_project_response::Variant::Decline(_) => Err(anyhow!("rejected"))?, proto::join_project_response::Variant::Decline(decline) => {
match proto::join_project_response::decline::Reason::from_i32(decline.reason) {
Some(proto::join_project_response::decline::Reason::Declined) => {
Err(JoinProjectError::HostDeclined)?
}
Some(proto::join_project_response::decline::Reason::Closed) => {
Err(JoinProjectError::HostClosedProject)?
}
Some(proto::join_project_response::decline::Reason::WentOffline) => {
Err(JoinProjectError::HostWentOffline)?
}
None => Err(anyhow!("missing decline reason"))?,
}
}
}; };
let replica_id = response.replica_id as ReplicaId; let replica_id = response.replica_id as ReplicaId;

View file

@ -153,7 +153,15 @@ message JoinProjectResponse {
repeated LanguageServer language_servers = 4; repeated LanguageServer language_servers = 4;
} }
message Decline {} message Decline {
Reason reason = 1;
enum Reason {
Declined = 0;
Closed = 1;
WentOffline = 2;
}
}
} }
message LeaveProject { message LeaveProject {

View file

@ -2310,10 +2310,11 @@ pub fn join_project(
let app_state = app_state.clone(); let app_state = app_state.clone();
cx.spawn(|mut cx| async move { cx.spawn(|mut cx| async move {
let (window, joining_notice) = let (window, joining_notice) = cx.update(|cx| {
cx.update(|cx| cx.add_window((app_state.build_window_options)(), |_| JoiningNotice { cx.add_window((app_state.build_window_options)(), |_| JoiningNotice {
message: "Loading remote project...", message: "Loading remote project...",
})); })
});
let project = Project::remote( let project = Project::remote(
project_id, project_id,
app_state.client.clone(), app_state.client.clone(),
@ -2336,13 +2337,24 @@ pub fn join_project(
); );
workspace workspace
})), })),
Err(error) => { Err(error @ _) => {
joining_notice.update(cx, |joining_notice, cx| { let message = match error {
joining_notice.message = "An error occurred trying to join the project. Please, close this window and retry."; project::JoinProjectError::HostDeclined => {
"The host declined your request to join."
}
project::JoinProjectError::HostClosedProject => "The host closed the project.",
project::JoinProjectError::HostWentOffline => "The host went offline.",
project::JoinProjectError::Other(_) => {
"An error occurred when attempting to join the project."
}
};
joining_notice.update(cx, |notice, cx| {
notice.message = message;
cx.notify(); cx.notify();
}); });
Err(error)
}, Err(error)?
}
}) })
}) })
} }