Show the reason why a join request was declined
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
This commit is contained in:
parent
740ec3d192
commit
ed6ed99d8f
6 changed files with 203 additions and 17 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -3394,6 +3394,7 @@ dependencies = [
|
||||||
"sum_tree",
|
"sum_tree",
|
||||||
"tempdir",
|
"tempdir",
|
||||||
"text",
|
"text",
|
||||||
|
"thiserror",
|
||||||
"toml",
|
"toml",
|
||||||
"unindent",
|
"unindent",
|
||||||
"util",
|
"util",
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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)?
|
||||||
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue