Cancel join requests when the requester closes the window

This commit is contained in:
Nathan Sobo 2022-05-16 19:02:23 -06:00
parent 7c3eebf93e
commit d821e7a4c1
10 changed files with 449 additions and 214 deletions

View file

@ -650,19 +650,32 @@ impl Server {
let project_id = request.payload.project_id;
let project;
{
let mut state = self.store_mut().await;
project = state.leave_project(sender_id, project_id)?;
let unshare = project.connection_ids.len() <= 1;
broadcast(sender_id, project.connection_ids, |conn_id| {
let mut store = self.store_mut().await;
project = store.leave_project(sender_id, project_id)?;
if project.remove_collaborator {
broadcast(sender_id, project.connection_ids, |conn_id| {
self.peer.send(
conn_id,
proto::RemoveProjectCollaborator {
project_id,
peer_id: sender_id.0,
},
)
});
}
if let Some(requester_id) = project.cancel_request {
self.peer.send(
conn_id,
proto::RemoveProjectCollaborator {
project.host_connection_id,
proto::JoinProjectRequestCancelled {
project_id,
peer_id: sender_id.0,
requester_id: requester_id.to_proto(),
},
)
});
if unshare {
)?;
}
if project.unshare {
self.peer.send(
project.host_connection_id,
proto::ProjectUnshared { project_id },
@ -1633,6 +1646,7 @@ mod tests {
use settings::Settings;
use sqlx::types::time::OffsetDateTime;
use std::{
cell::RefCell,
env,
ops::Deref,
path::{Path, PathBuf},
@ -2049,6 +2063,105 @@ mod tests {
));
}
#[gpui::test(iterations = 10)]
async fn test_cancel_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 project_a_events = Rc::new(RefCell::new(Vec::new()));
let user_b = client_a
.user_store
.update(cx_a, |store, cx| {
store.fetch_user(client_b.user_id().unwrap(), cx)
})
.await
.unwrap();
project_a.update(cx_a, {
let project_a_events = project_a_events.clone();
move |_, cx| {
cx.subscribe(&cx.handle(), move |_, _, event, _| {
project_a_events.borrow_mut().push(event.clone());
})
.detach();
}
});
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();
assert_eq!(
&*project_a_events.borrow(),
&[project::Event::ContactRequestedJoin(user_b.clone())]
);
project_a_events.borrow_mut().clear();
// Cancel the join request by leaving the project
client_b
.client
.send(proto::LeaveProject { project_id })
.unwrap();
drop(project_b);
deterministic.run_until_parked();
assert_eq!(
&*project_a_events.borrow(),
&[project::Event::ContactCancelledJoinRequest(user_b.clone())]
);
}
#[gpui::test(iterations = 10)]
async fn test_propagate_saves_and_fs_changes(
cx_a: &mut TestAppContext,

View file

@ -1,6 +1,6 @@
use crate::db::{self, ChannelId, UserId};
use anyhow::{anyhow, Result};
use collections::{BTreeMap, HashMap, HashSet};
use collections::{hash_map::Entry, BTreeMap, HashMap, HashSet};
use rpc::{proto, ConnectionId, Receipt};
use std::{collections::hash_map, path::PathBuf};
use tracing::instrument;
@ -56,9 +56,12 @@ pub struct RemovedConnectionState {
}
pub struct LeftProject {
pub connection_ids: Vec<ConnectionId>,
pub host_user_id: UserId,
pub host_connection_id: ConnectionId,
pub connection_ids: Vec<ConnectionId>,
pub remove_collaborator: bool,
pub cancel_request: Option<UserId>,
pub unshare: bool,
}
#[derive(Copy, Clone)]
@ -503,24 +506,48 @@ impl Store {
connection_id: ConnectionId,
project_id: u64,
) -> Result<LeftProject> {
let user_id = self.user_id_for_connection(connection_id)?;
let project = self
.projects
.get_mut(&project_id)
.ok_or_else(|| anyhow!("no such project"))?;
let (replica_id, _) = project
.guests
.remove(&connection_id)
.ok_or_else(|| anyhow!("cannot leave a project before joining it"))?;
project.active_replica_ids.remove(&replica_id);
// If the connection leaving the project is a collaborator, remove it.
let remove_collaborator =
if let Some((replica_id, _)) = project.guests.remove(&connection_id) {
project.active_replica_ids.remove(&replica_id);
true
} else {
false
};
// If the connection leaving the project has a pending request, remove it.
// If that user has no other pending requests on other connections, indicate that the request should be cancelled.
let mut cancel_request = None;
if let Entry::Occupied(mut entry) = project.join_requests.entry(user_id) {
entry
.get_mut()
.retain(|receipt| receipt.sender_id != connection_id);
if entry.get().is_empty() {
entry.remove();
cancel_request = Some(user_id);
}
}
if let Some(connection) = self.connections.get_mut(&connection_id) {
connection.projects.remove(&project_id);
}
let connection_ids = project.connection_ids();
let unshare = connection_ids.len() <= 1 && project.join_requests.is_empty();
Ok(LeftProject {
connection_ids: project.connection_ids(),
host_connection_id: project.host_connection_id,
host_user_id: project.host_user_id,
connection_ids,
cancel_request,
unshare,
remove_collaborator,
})
}