diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index 97cf225b5f..296c0cf9ae 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -23,6 +23,8 @@ impl PartialEq for User { } } +impl Eq for User {} + #[derive(Debug)] pub struct Contact { pub user: Arc, diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 7fdaaf5d9f..28c863ef0b 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -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, + 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, diff --git a/crates/collab/src/rpc/store.rs b/crates/collab/src/rpc/store.rs index 9af324e5d8..80dc9f74e9 100644 --- a/crates/collab/src/rpc/store.rs +++ b/crates/collab/src/rpc/store.rs @@ -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, pub host_user_id: UserId, pub host_connection_id: ConnectionId, + pub connection_ids: Vec, + pub remove_collaborator: bool, + pub cancel_request: Option, + pub unshare: bool, } #[derive(Copy, Clone)] @@ -503,24 +506,48 @@ impl Store { connection_id: ConnectionId, project_id: u64, ) -> Result { + 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, }) } diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 4f937b0448..fc19b1422a 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -3231,6 +3231,21 @@ impl<'a, T: View> ViewContext<'a, T> { self.app.add_option_view(self.window_id, build_view) } + pub fn replace_root_view(&mut self, build_root_view: F) -> ViewHandle + where + V: View, + F: FnOnce(&mut ViewContext) -> V, + { + let window_id = self.window_id; + self.update(|this| { + let root_view = this.add_view(window_id, build_root_view); + let window = this.cx.windows.get_mut(&window_id).unwrap(); + window.root_view = root_view.clone().into(); + window.focused_view_id = Some(root_view.id()); + root_view + }) + } + pub fn subscribe(&mut self, handle: &H, mut callback: F) -> Subscription where E: Entity, diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index d2856f876d..33e37dc900 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -136,7 +136,7 @@ pub struct Collaborator { pub replica_id: ReplicaId, } -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, Eq)] pub enum Event { ActiveEntryChanged(Option), WorktreeRemoved(WorktreeId), @@ -147,6 +147,7 @@ pub enum Event { RemoteIdChanged(Option), CollaboratorLeft(PeerId), ContactRequestedJoin(Arc), + ContactCancelledJoinRequest(Arc), } #[derive(Serialize)] @@ -269,6 +270,7 @@ impl Project { client.add_model_message_handler(Self::handle_start_language_server); client.add_model_message_handler(Self::handle_update_language_server); client.add_model_message_handler(Self::handle_remove_collaborator); + client.add_model_message_handler(Self::handle_join_project_request_cancelled); client.add_model_message_handler(Self::handle_register_worktree); client.add_model_message_handler(Self::handle_unregister_worktree); client.add_model_message_handler(Self::handle_unregister_project); @@ -3879,6 +3881,27 @@ impl Project { }) } + async fn handle_join_project_request_cancelled( + this: ModelHandle, + envelope: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result<()> { + let user = this + .update(&mut cx, |this, cx| { + this.user_store.update(cx, |user_store, cx| { + user_store.fetch_user(envelope.payload.requester_id, cx) + }) + }) + .await?; + + this.update(&mut cx, |_, cx| { + cx.emit(Event::ContactCancelledJoinRequest(user)); + }); + + Ok(()) + } + async fn handle_register_worktree( this: ModelHandle, envelope: TypedEnvelope, diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 511348c9d0..d2ab7d99ac 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -16,88 +16,89 @@ message Envelope { UnregisterProject unregister_project = 10; RequestJoinProject request_join_project = 11; RespondToJoinProjectRequest respond_to_join_project_request = 12; - JoinProject join_project = 13; - JoinProjectResponse join_project_response = 14; - LeaveProject leave_project = 15; - AddProjectCollaborator add_project_collaborator = 16; - RemoveProjectCollaborator remove_project_collaborator = 17; - ProjectUnshared project_unshared = 18; + JoinProjectRequestCancelled join_project_request_cancelled = 13; + JoinProject join_project = 14; + JoinProjectResponse join_project_response = 15; + LeaveProject leave_project = 16; + AddProjectCollaborator add_project_collaborator = 17; + RemoveProjectCollaborator remove_project_collaborator = 18; + ProjectUnshared project_unshared = 19; - GetDefinition get_definition = 19; - GetDefinitionResponse get_definition_response = 20; - GetReferences get_references = 21; - GetReferencesResponse get_references_response = 22; - GetDocumentHighlights get_document_highlights = 23; - GetDocumentHighlightsResponse get_document_highlights_response = 24; - GetProjectSymbols get_project_symbols = 25; - GetProjectSymbolsResponse get_project_symbols_response = 26; - OpenBufferForSymbol open_buffer_for_symbol = 27; - OpenBufferForSymbolResponse open_buffer_for_symbol_response = 28; + GetDefinition get_definition = 20; + GetDefinitionResponse get_definition_response = 21; + GetReferences get_references = 22; + GetReferencesResponse get_references_response = 23; + GetDocumentHighlights get_document_highlights = 24; + GetDocumentHighlightsResponse get_document_highlights_response = 25; + GetProjectSymbols get_project_symbols = 26; + GetProjectSymbolsResponse get_project_symbols_response = 27; + OpenBufferForSymbol open_buffer_for_symbol = 28; + OpenBufferForSymbolResponse open_buffer_for_symbol_response = 29; - RegisterWorktree register_worktree = 29; - UnregisterWorktree unregister_worktree = 30; - UpdateWorktree update_worktree = 31; + RegisterWorktree register_worktree = 30; + UnregisterWorktree unregister_worktree = 31; + UpdateWorktree update_worktree = 32; - CreateProjectEntry create_project_entry = 32; - RenameProjectEntry rename_project_entry = 33; - DeleteProjectEntry delete_project_entry = 34; - ProjectEntryResponse project_entry_response = 35; + CreateProjectEntry create_project_entry = 33; + RenameProjectEntry rename_project_entry = 34; + DeleteProjectEntry delete_project_entry = 35; + ProjectEntryResponse project_entry_response = 36; - UpdateDiagnosticSummary update_diagnostic_summary = 36; - StartLanguageServer start_language_server = 37; - UpdateLanguageServer update_language_server = 38; + UpdateDiagnosticSummary update_diagnostic_summary = 37; + StartLanguageServer start_language_server = 38; + UpdateLanguageServer update_language_server = 39; - OpenBufferById open_buffer_by_id = 39; - OpenBufferByPath open_buffer_by_path = 40; - OpenBufferResponse open_buffer_response = 41; - UpdateBuffer update_buffer = 42; - UpdateBufferFile update_buffer_file = 43; - SaveBuffer save_buffer = 44; - BufferSaved buffer_saved = 45; - BufferReloaded buffer_reloaded = 46; - ReloadBuffers reload_buffers = 47; - ReloadBuffersResponse reload_buffers_response = 48; - FormatBuffers format_buffers = 49; - FormatBuffersResponse format_buffers_response = 50; - GetCompletions get_completions = 51; - GetCompletionsResponse get_completions_response = 52; - ApplyCompletionAdditionalEdits apply_completion_additional_edits = 53; - ApplyCompletionAdditionalEditsResponse apply_completion_additional_edits_response = 54; - GetCodeActions get_code_actions = 55; - GetCodeActionsResponse get_code_actions_response = 56; - ApplyCodeAction apply_code_action = 57; - ApplyCodeActionResponse apply_code_action_response = 58; - PrepareRename prepare_rename = 59; - PrepareRenameResponse prepare_rename_response = 60; - PerformRename perform_rename = 61; - PerformRenameResponse perform_rename_response = 62; - SearchProject search_project = 63; - SearchProjectResponse search_project_response = 64; + OpenBufferById open_buffer_by_id = 40; + OpenBufferByPath open_buffer_by_path = 41; + OpenBufferResponse open_buffer_response = 42; + UpdateBuffer update_buffer = 43; + UpdateBufferFile update_buffer_file = 44; + SaveBuffer save_buffer = 45; + BufferSaved buffer_saved = 46; + BufferReloaded buffer_reloaded = 47; + ReloadBuffers reload_buffers = 48; + ReloadBuffersResponse reload_buffers_response = 49; + FormatBuffers format_buffers = 50; + FormatBuffersResponse format_buffers_response = 51; + GetCompletions get_completions = 52; + GetCompletionsResponse get_completions_response = 53; + ApplyCompletionAdditionalEdits apply_completion_additional_edits = 54; + ApplyCompletionAdditionalEditsResponse apply_completion_additional_edits_response = 55; + GetCodeActions get_code_actions = 56; + GetCodeActionsResponse get_code_actions_response = 57; + ApplyCodeAction apply_code_action = 58; + ApplyCodeActionResponse apply_code_action_response = 59; + PrepareRename prepare_rename = 60; + PrepareRenameResponse prepare_rename_response = 61; + PerformRename perform_rename = 62; + PerformRenameResponse perform_rename_response = 63; + SearchProject search_project = 64; + SearchProjectResponse search_project_response = 65; - GetChannels get_channels = 65; - GetChannelsResponse get_channels_response = 66; - JoinChannel join_channel = 67; - JoinChannelResponse join_channel_response = 68; - LeaveChannel leave_channel = 69; - SendChannelMessage send_channel_message = 70; - SendChannelMessageResponse send_channel_message_response = 71; - ChannelMessageSent channel_message_sent = 72; - GetChannelMessages get_channel_messages = 73; - GetChannelMessagesResponse get_channel_messages_response = 74; + GetChannels get_channels = 66; + GetChannelsResponse get_channels_response = 67; + JoinChannel join_channel = 68; + JoinChannelResponse join_channel_response = 69; + LeaveChannel leave_channel = 70; + SendChannelMessage send_channel_message = 71; + SendChannelMessageResponse send_channel_message_response = 72; + ChannelMessageSent channel_message_sent = 73; + GetChannelMessages get_channel_messages = 74; + GetChannelMessagesResponse get_channel_messages_response = 75; - UpdateContacts update_contacts = 75; + UpdateContacts update_contacts = 76; - GetUsers get_users = 76; - FuzzySearchUsers fuzzy_search_users = 77; - UsersResponse users_response = 78; - RequestContact request_contact = 79; - RespondToContactRequest respond_to_contact_request = 80; - RemoveContact remove_contact = 81; + GetUsers get_users = 77; + FuzzySearchUsers fuzzy_search_users = 78; + UsersResponse users_response = 79; + RequestContact request_contact = 80; + RespondToContactRequest respond_to_contact_request = 81; + RemoveContact remove_contact = 82; - Follow follow = 82; - FollowResponse follow_response = 83; - UpdateFollowers update_followers = 84; - Unfollow unfollow = 85; + Follow follow = 83; + FollowResponse follow_response = 84; + UpdateFollowers update_followers = 85; + Unfollow unfollow = 86; } } @@ -136,6 +137,11 @@ message RespondToJoinProjectRequest { bool allow = 3; } +message JoinProjectRequestCancelled { + uint64 requester_id = 1; + uint64 project_id = 2; +} + message JoinProject { uint64 project_id = 1; } diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index 1a87cb6a0c..a1b7425b69 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -114,6 +114,7 @@ messages!( (JoinChannelResponse, Foreground), (JoinProject, Foreground), (JoinProjectResponse, Foreground), + (JoinProjectRequestCancelled, Foreground), (LeaveChannel, Foreground), (LeaveProject, Foreground), (OpenBufferById, Background), @@ -220,6 +221,7 @@ entity_messages!( GetReferences, GetProjectSymbols, JoinProject, + JoinProjectRequestCancelled, LeaveProject, OpenBufferById, OpenBufferByPath, diff --git a/crates/rpc/src/rpc.rs b/crates/rpc/src/rpc.rs index f238c6792d..1d4416d775 100644 --- a/crates/rpc/src/rpc.rs +++ b/crates/rpc/src/rpc.rs @@ -6,4 +6,4 @@ pub use conn::Connection; pub use peer::*; mod macros; -pub const PROTOCOL_VERSION: u32 = 17; +pub const PROTOCOL_VERSION: u32 = 18; diff --git a/crates/workspace/src/waiting_room.rs b/crates/workspace/src/waiting_room.rs new file mode 100644 index 0000000000..17efebce39 --- /dev/null +++ b/crates/workspace/src/waiting_room.rs @@ -0,0 +1,159 @@ +use crate::{ + sidebar::{Side, ToggleSidebarItem}, + AppState, +}; +use anyhow::Result; +use client::Contact; +use gpui::{elements::*, ElementBox, Entity, ImageData, RenderContext, Task, View, ViewContext}; +use project::Project; +use settings::Settings; +use std::sync::Arc; + +pub struct WaitingRoom { + avatar: Option>, + message: String, + joined: bool, + _join_task: Task>, +} + +impl Entity for WaitingRoom { + type Event = (); + + fn release(&mut self, _: &mut gpui::MutableAppContext) { + if !self.joined { + // TODO: Cancel the join request + } + } +} + +impl View for WaitingRoom { + fn ui_name() -> &'static str { + "WaitingRoom" + } + + fn render(&mut self, cx: &mut RenderContext) -> ElementBox { + let theme = &cx.global::().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, + project_index: usize, + app_state: Arc, + cx: &mut ViewContext, + ) -> Self { + let project_id = contact.projects[project_index].id; + + 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.languages.clone(), + app_state.fs.clone(), + &mut cx, + ) + .await; + + if let Some(this) = this.upgrade(&cx) { + this.update(&mut cx, |this, cx| match project { + Ok(project) => { + this.joined = true; + cx.replace_root_view(|cx| { + let mut workspace = + (app_state.build_workspace)(project, &app_state, cx); + workspace.toggle_sidebar_item( + &ToggleSidebarItem { + side: Side::Left, + item_index: 0, + }, + 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].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 { + avatar: contact.user.avatar.clone(), + message: format!( + "Asking to join @{}'s copy of {}...", + contact.user.github_login, + humanize_list(&contact.projects[project_index].worktree_root_names) + ), + joined: false, + _join_task, + } + } +} + +fn humanize_list<'a>(items: impl IntoIterator) -> 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 +} diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 9849e14bba..d27cb9bfba 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -5,6 +5,7 @@ pub mod pane_group; pub mod sidebar; mod status_bar; mod toolbar; +mod waiting_room; use anyhow::{anyhow, Context, Result}; use client::{ @@ -50,6 +51,7 @@ use std::{ use theme::{Theme, ThemeRegistry}; pub use toolbar::{ToolbarItemLocation, ToolbarItemView}; use util::ResultExt; +use waiting_room::WaitingRoom; type ProjectItemBuilders = HashMap< TypeId, @@ -124,8 +126,7 @@ pub fn init(client: &Arc, cx: &mut MutableAppContext) { action.project_index, &action.app_state, cx, - ) - .detach(); + ); }); cx.add_async_action(Workspace::toggle_follow); @@ -2280,119 +2281,21 @@ pub fn join_project( project_index: usize, app_state: &Arc, cx: &mut MutableAppContext, -) -> Task>> { +) { let project_id = contact.projects[project_index].id; - struct JoiningNotice { - avatar: Option>, - message: String, - } - - impl Entity for JoiningNotice { - type Event = (); - } - - impl View for JoiningNotice { - fn ui_name() -> &'static str { - "JoiningProjectWindow" - } - - fn render(&mut self, cx: &mut RenderContext) -> ElementBox { - let theme = &cx.global::().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() - } - } - for window_id in cx.window_ids().collect::>() { if let Some(workspace) = cx.root_view::(window_id) { if workspace.read(cx).project().read(cx).remote_id() == Some(project_id) { - return Task::ready(Ok(workspace)); + cx.activate_window(window_id); + return; } } } - let app_state = app_state.clone(); - cx.spawn(|mut cx| async move { - let (window, joining_notice) = cx.update(|cx| { - cx.add_window((app_state.build_window_options)(), |_| JoiningNotice { - avatar: contact.user.avatar.clone(), - message: format!( - "Asking to join @{}'s copy of {}...", - contact.user.github_login, - humanize_list(&contact.projects[project_index].worktree_root_names) - ), - }) - }); - let project = Project::remote( - project_id, - app_state.client.clone(), - app_state.user_store.clone(), - app_state.languages.clone(), - app_state.fs.clone(), - &mut cx, - ) - .await; - - cx.update(|cx| match project { - Ok(project) => Ok(cx.replace_root_view(window, |cx| { - let mut workspace = (app_state.build_workspace)(project, &app_state, cx); - workspace.toggle_sidebar_item( - &ToggleSidebarItem { - side: Side::Left, - item_index: 0, - }, - 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].worktree_root_names) - ) - } - project::JoinProjectError::HostWentOffline => { - format!("@{} went offline.", login) - } - project::JoinProjectError::Other(_) => "An error occurred.".to_string(), - }; - joining_notice.update(cx, |notice, cx| { - notice.message = message; - cx.notify(); - }); - - Err(error)? - } - }) - }) + cx.add_window((app_state.build_window_options)(), |cx| { + WaitingRoom::new(contact, project_index, app_state.clone(), cx) + }); } fn open_new(app_state: &Arc, cx: &mut MutableAppContext) { @@ -2408,18 +2311,3 @@ fn open_new(app_state: &Arc, cx: &mut MutableAppContext) { }); cx.dispatch_action(window_id, vec![workspace.id()], &OpenNew(app_state.clone())); } - -fn humanize_list<'a>(items: impl IntoIterator) -> 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 -}