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<User>,
Nathan Sobo created
crates/client/src/user.rs | 2
crates/collab/src/rpc.rs | 133 +++++++++++++++++++++-
crates/collab/src/rpc/store.rs | 43 ++++++-
crates/gpui/src/app.rs | 15 ++
crates/project/src/project.rs | 25 ++++
crates/rpc/proto/zed.proto | 170 +++++++++++++++--------------
crates/rpc/src/proto.rs | 2
crates/rpc/src/rpc.rs | 2
crates/workspace/src/waiting_room.rs | 159 ++++++++++++++++++++++++++++
crates/workspace/src/workspace.rs | 130 +---------------------
10 files changed, 458 insertions(+), 223 deletions(-)
@@ -23,6 +23,8 @@ impl PartialEq for User {
}
}
+impl Eq for User {}
+
#[derive(Debug)]
pub struct Contact {
pub user: Arc<User>,
@@ -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,
@@ -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,
})
}
@@ -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<V, F>(&mut self, build_root_view: F) -> ViewHandle<V>
+ where
+ V: View,
+ F: FnOnce(&mut ViewContext<V>) -> 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<E, H, F>(&mut self, handle: &H, mut callback: F) -> Subscription
where
E: Entity,
@@ -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<ProjectEntryId>),
WorktreeRemoved(WorktreeId),
@@ -147,6 +147,7 @@ pub enum Event {
RemoteIdChanged(Option<u64>),
CollaboratorLeft(PeerId),
ContactRequestedJoin(Arc<User>),
+ ContactCancelledJoinRequest(Arc<User>),
}
#[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<Self>,
+ envelope: TypedEnvelope<proto::JoinProjectRequestCancelled>,
+ _: Arc<Client>,
+ 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<Self>,
envelope: TypedEnvelope<proto::RegisterWorktree>,
@@ -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;
-
- 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;
-
- RegisterWorktree register_worktree = 29;
- UnregisterWorktree unregister_worktree = 30;
- UpdateWorktree update_worktree = 31;
-
- CreateProjectEntry create_project_entry = 32;
- RenameProjectEntry rename_project_entry = 33;
- DeleteProjectEntry delete_project_entry = 34;
- ProjectEntryResponse project_entry_response = 35;
-
- UpdateDiagnosticSummary update_diagnostic_summary = 36;
- StartLanguageServer start_language_server = 37;
- UpdateLanguageServer update_language_server = 38;
-
- 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;
-
- 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;
-
- UpdateContacts update_contacts = 75;
-
- 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;
-
- Follow follow = 82;
- FollowResponse follow_response = 83;
- UpdateFollowers update_followers = 84;
- Unfollow unfollow = 85;
+ 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 = 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 = 30;
+ UnregisterWorktree unregister_worktree = 31;
+ UpdateWorktree update_worktree = 32;
+
+ CreateProjectEntry create_project_entry = 33;
+ RenameProjectEntry rename_project_entry = 34;
+ DeleteProjectEntry delete_project_entry = 35;
+ ProjectEntryResponse project_entry_response = 36;
+
+ UpdateDiagnosticSummary update_diagnostic_summary = 37;
+ StartLanguageServer start_language_server = 38;
+ UpdateLanguageServer update_language_server = 39;
+
+ 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 = 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 = 76;
+
+ 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 = 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;
}
@@ -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,
@@ -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;
@@ -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<Arc<ImageData>>,
+ message: String,
+ joined: bool,
+ _join_task: Task<Result<()>>,
+}
+
+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<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 _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<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
+}
@@ -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<Client>, 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<AppState>,
cx: &mut MutableAppContext,
-) -> Task<Result<ViewHandle<Workspace>>> {
+) {
let project_id = contact.projects[project_index].id;
- struct JoiningNotice {
- avatar: Option<Arc<ImageData>>,
- 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<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()
- }
- }
-
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) {
- 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<AppState>, cx: &mut MutableAppContext) {
@@ -2408,18 +2311,3 @@ fn open_new(app_state: &Arc<AppState>, cx: &mut MutableAppContext) {
});
cx.dispatch_action(window_id, vec![workspace.id()], &OpenNew(app_state.clone()));
}
-
-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
-}