diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index ff5f03d5ef44e0835591312bf0fe8a47127dddfa..9f86020044b6ce43740b98054309b26a1b9052e8 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -1,7 +1,7 @@ use super::{http::HttpClient, proto, Client, Status, TypedEnvelope}; use crate::incoming_call::IncomingCall; use anyhow::{anyhow, Context, Result}; -use collections::{hash_map::Entry, BTreeSet, HashMap, HashSet}; +use collections::{hash_map::Entry, HashMap, HashSet}; use futures::{channel::mpsc, future, AsyncReadExt, Future, StreamExt}; use gpui::{AsyncAppContext, Entity, ImageData, ModelContext, ModelHandle, Task}; use postage::{sink::Sink, watch}; @@ -40,14 +40,6 @@ impl Eq for User {} pub struct Contact { pub user: Arc, pub online: bool, - pub projects: Vec, -} - -#[derive(Clone, Debug, PartialEq)] -pub struct ProjectMetadata { - pub id: u64, - pub visible_worktree_root_names: Vec, - pub guests: BTreeSet>, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -290,7 +282,6 @@ impl UserStore { let mut user_ids = HashSet::default(); for contact in &message.contacts { user_ids.insert(contact.user_id); - user_ids.extend(contact.projects.iter().flat_map(|w| &w.guests).copied()); } user_ids.extend(message.incoming_requests.iter().map(|req| req.requester_id)); user_ids.extend(message.outgoing_requests.iter()); @@ -688,34 +679,11 @@ impl Contact { user_store.get_user(contact.user_id, cx) }) .await?; - let mut projects = Vec::new(); - for project in contact.projects { - let mut guests = BTreeSet::new(); - for participant_id in project.guests { - guests.insert( - user_store - .update(cx, |user_store, cx| user_store.get_user(participant_id, cx)) - .await?, - ); - } - projects.push(ProjectMetadata { - id: project.id, - visible_worktree_root_names: project.visible_worktree_root_names.clone(), - guests, - }); - } Ok(Self { user, online: contact.online, - projects, }) } - - pub fn non_empty_projects(&self) -> impl Iterator { - self.projects - .iter() - .filter(|project| !project.visible_worktree_root_names.is_empty()) - } } async fn fetch_avatar(http: &dyn HttpClient, url: &str) -> Result> { diff --git a/crates/collab/src/integration_tests.rs b/crates/collab/src/integration_tests.rs index d16bff2f37ba046db62532f4e35961efb64ee7bb..d000ebb309856733f7cbd238a990b6496f5ede65 100644 --- a/crates/collab/src/integration_tests.rs +++ b/crates/collab/src/integration_tests.rs @@ -8,7 +8,7 @@ use anyhow::anyhow; use call::Room; use client::{ self, proto, test::FakeHttpClient, Channel, ChannelDetails, ChannelList, Client, Connection, - Credentials, EstablishConnectionError, ProjectMetadata, UserStore, RECEIVE_TIMEOUT, + Credentials, EstablishConnectionError, UserStore, RECEIVE_TIMEOUT, }; use collections::{BTreeMap, HashMap, HashSet}; use editor::{ @@ -731,294 +731,6 @@ async fn test_cancel_join_request( ); } -#[gpui::test(iterations = 10)] -async fn test_offline_projects( - deterministic: Arc, - cx_a: &mut TestAppContext, - cx_b: &mut TestAppContext, - cx_c: &mut TestAppContext, -) { - cx_a.foreground().forbid_parking(); - 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; - let client_c = server.create_client(cx_c, "user_c").await; - let user_a = UserId::from_proto(client_a.user_id().unwrap()); - server - .make_contacts(vec![ - (&client_a, cx_a), - (&client_b, cx_b), - (&client_c, cx_c), - ]) - .await; - - // Set up observers of the project and user stores. Any time either of - // these models update, they should be in a consistent state with each - // other. There should not be an observable moment where the current - // user's contact entry contains a project that does not match one of - // the current open projects. That would cause a duplicate entry to be - // shown in the contacts panel. - let mut subscriptions = vec![]; - let (window_id, view) = cx_a.add_window(|cx| { - subscriptions.push(cx.observe(&client_a.user_store, { - let project_store = client_a.project_store.clone(); - let user_store = client_a.user_store.clone(); - move |_, _, cx| check_project_list(project_store.clone(), user_store.clone(), cx) - })); - - subscriptions.push(cx.observe(&client_a.project_store, { - let project_store = client_a.project_store.clone(); - let user_store = client_a.user_store.clone(); - move |_, _, cx| check_project_list(project_store.clone(), user_store.clone(), cx) - })); - - fn check_project_list( - project_store: ModelHandle, - user_store: ModelHandle, - cx: &mut gpui::MutableAppContext, - ) { - let user_store = user_store.read(cx); - for contact in user_store.contacts() { - if contact.user.id == user_store.current_user().unwrap().id { - for project in &contact.projects { - let store_contains_project = project_store - .read(cx) - .projects(cx) - .filter_map(|project| project.read(cx).remote_id()) - .any(|x| x == project.id); - - if !store_contains_project { - panic!( - concat!( - "current user's contact data has a project", - "that doesn't match any open project {:?}", - ), - project - ); - } - } - } - } - } - - EmptyView - }); - - // Build an offline project with two worktrees. - client_a - .fs - .insert_tree( - "/code", - json!({ - "crate1": { "a.rs": "" }, - "crate2": { "b.rs": "" }, - }), - ) - .await; - let project = cx_a.update(|cx| { - Project::local( - false, - client_a.client.clone(), - client_a.user_store.clone(), - client_a.project_store.clone(), - client_a.language_registry.clone(), - client_a.fs.clone(), - cx, - ) - }); - project - .update(cx_a, |p, cx| { - p.find_or_create_local_worktree("/code/crate1", true, cx) - }) - .await - .unwrap(); - project - .update(cx_a, |p, cx| { - p.find_or_create_local_worktree("/code/crate2", true, cx) - }) - .await - .unwrap(); - project - .update(cx_a, |p, cx| p.restore_state(cx)) - .await - .unwrap(); - - // When a project is offline, we still create it on the server but is invisible - // to other users. - deterministic.run_until_parked(); - assert!(server - .store - .lock() - .await - .project_metadata_for_user(user_a) - .is_empty()); - project.read_with(cx_a, |project, _| { - assert!(project.remote_id().is_some()); - assert!(!project.is_online()); - }); - assert!(client_b - .user_store - .read_with(cx_b, |store, _| { store.contacts()[0].projects.is_empty() })); - - // When the project is taken online, its metadata is sent to the server - // and broadcasted to other users. - project.update(cx_a, |p, cx| p.set_online(true, cx)); - deterministic.run_until_parked(); - let project_id = project.read_with(cx_a, |p, _| p.remote_id()).unwrap(); - client_b.user_store.read_with(cx_b, |store, _| { - assert_eq!( - store.contacts()[0].projects, - &[ProjectMetadata { - id: project_id, - visible_worktree_root_names: vec!["crate1".into(), "crate2".into()], - guests: Default::default(), - }] - ); - }); - - // The project is registered again when the host loses and regains connection. - server.disconnect_client(user_a); - server.forbid_connections(); - cx_a.foreground().advance_clock(rpc::RECEIVE_TIMEOUT); - assert!(server - .store - .lock() - .await - .project_metadata_for_user(user_a) - .is_empty()); - assert!(project.read_with(cx_a, |p, _| p.remote_id().is_none())); - assert!(client_b - .user_store - .read_with(cx_b, |store, _| { store.contacts()[0].projects.is_empty() })); - - server.allow_connections(); - cx_b.foreground().advance_clock(Duration::from_secs(10)); - let project_id = project.read_with(cx_a, |p, _| p.remote_id()).unwrap(); - client_b.user_store.read_with(cx_b, |store, _| { - assert_eq!( - store.contacts()[0].projects, - &[ProjectMetadata { - id: project_id, - visible_worktree_root_names: vec!["crate1".into(), "crate2".into()], - guests: Default::default(), - }] - ); - }); - - project - .update(cx_a, |p, cx| { - p.find_or_create_local_worktree("/code/crate3", true, cx) - }) - .await - .unwrap(); - deterministic.run_until_parked(); - client_b.user_store.read_with(cx_b, |store, _| { - assert_eq!( - store.contacts()[0].projects, - &[ProjectMetadata { - id: project_id, - visible_worktree_root_names: vec![ - "crate1".into(), - "crate2".into(), - "crate3".into() - ], - guests: Default::default(), - }] - ); - }); - - // Build another project using a directory which was previously part of - // an online project. Restore the project's state from the host's database. - let project2_a = cx_a.update(|cx| { - Project::local( - false, - client_a.client.clone(), - client_a.user_store.clone(), - client_a.project_store.clone(), - client_a.language_registry.clone(), - client_a.fs.clone(), - cx, - ) - }); - project2_a - .update(cx_a, |p, cx| { - p.find_or_create_local_worktree("/code/crate3", true, cx) - }) - .await - .unwrap(); - project2_a - .update(cx_a, |project, cx| project.restore_state(cx)) - .await - .unwrap(); - - // This project is now online, because its directory was previously online. - project2_a.read_with(cx_a, |project, _| assert!(project.is_online())); - deterministic.run_until_parked(); - let project2_id = project2_a.read_with(cx_a, |p, _| p.remote_id()).unwrap(); - client_b.user_store.read_with(cx_b, |store, _| { - assert_eq!( - store.contacts()[0].projects, - &[ - ProjectMetadata { - id: project_id, - visible_worktree_root_names: vec![ - "crate1".into(), - "crate2".into(), - "crate3".into() - ], - guests: Default::default(), - }, - ProjectMetadata { - id: project2_id, - visible_worktree_root_names: vec!["crate3".into()], - guests: Default::default(), - } - ] - ); - }); - - let project2_b = client_b.build_remote_project(&project2_a, cx_a, cx_b).await; - let project2_c = cx_c.foreground().spawn(Project::remote( - project2_id, - client_c.client.clone(), - client_c.user_store.clone(), - client_c.project_store.clone(), - client_c.language_registry.clone(), - FakeFs::new(cx_c.background()), - cx_c.to_async(), - )); - deterministic.run_until_parked(); - - // Taking a project offline unshares the project, rejects any pending join request and - // disconnects existing guests. - project2_a.update(cx_a, |project, cx| project.set_online(false, cx)); - deterministic.run_until_parked(); - project2_a.read_with(cx_a, |project, _| assert!(!project.is_shared())); - project2_b.read_with(cx_b, |project, _| assert!(project.is_read_only())); - project2_c.await.unwrap_err(); - - client_b.user_store.read_with(cx_b, |store, _| { - assert_eq!( - store.contacts()[0].projects, - &[ProjectMetadata { - id: project_id, - visible_worktree_root_names: vec![ - "crate1".into(), - "crate2".into(), - "crate3".into() - ], - guests: Default::default(), - },] - ); - }); - - cx_a.update(|cx| { - drop(subscriptions); - drop(view); - cx.remove_window(window_id); - }); -} - #[gpui::test(iterations = 10)] async fn test_propagate_saves_and_fs_changes( cx_a: &mut TestAppContext, @@ -3911,24 +3623,15 @@ async fn test_contacts( deterministic.run_until_parked(); assert_eq!( contacts(&client_a, cx_a), - [ - ("user_b".to_string(), true, vec![]), - ("user_c".to_string(), true, vec![]) - ] + [("user_b".to_string(), true), ("user_c".to_string(), true)] ); assert_eq!( contacts(&client_b, cx_b), - [ - ("user_a".to_string(), true, vec![]), - ("user_c".to_string(), true, vec![]) - ] + [("user_a".to_string(), true), ("user_c".to_string(), true)] ); assert_eq!( contacts(&client_c, cx_c), - [ - ("user_a".to_string(), true, vec![]), - ("user_b".to_string(), true, vec![]) - ] + [("user_a".to_string(), true), ("user_b".to_string(), true)] ); // Share a project as client A. @@ -3938,24 +3641,15 @@ async fn test_contacts( deterministic.run_until_parked(); assert_eq!( contacts(&client_a, cx_a), - [ - ("user_b".to_string(), true, vec![]), - ("user_c".to_string(), true, vec![]) - ] + [("user_b".to_string(), true), ("user_c".to_string(), true)] ); assert_eq!( contacts(&client_b, cx_b), - [ - ("user_a".to_string(), true, vec![("a".to_string(), vec![])]), - ("user_c".to_string(), true, vec![]) - ] + [("user_a".to_string(), true), ("user_c".to_string(), true)] ); assert_eq!( contacts(&client_c, cx_c), - [ - ("user_a".to_string(), true, vec![("a".to_string(), vec![])]), - ("user_b".to_string(), true, vec![]) - ] + [("user_a".to_string(), true), ("user_b".to_string(), true)] ); let _project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await; @@ -3963,32 +3657,15 @@ async fn test_contacts( deterministic.run_until_parked(); assert_eq!( contacts(&client_a, cx_a), - [ - ("user_b".to_string(), true, vec![]), - ("user_c".to_string(), true, vec![]) - ] + [("user_b".to_string(), true), ("user_c".to_string(), true)] ); assert_eq!( contacts(&client_b, cx_b), - [ - ( - "user_a".to_string(), - true, - vec![("a".to_string(), vec!["user_b".to_string()])] - ), - ("user_c".to_string(), true, vec![]) - ] + [("user_a".to_string(), true,), ("user_c".to_string(), true)] ); assert_eq!( contacts(&client_c, cx_c), - [ - ( - "user_a".to_string(), - true, - vec![("a".to_string(), vec!["user_b".to_string()])] - ), - ("user_b".to_string(), true, vec![]) - ] + [("user_a".to_string(), true,), ("user_b".to_string(), true)] ); // Add a local project as client B @@ -3998,32 +3675,15 @@ async fn test_contacts( deterministic.run_until_parked(); assert_eq!( contacts(&client_a, cx_a), - [ - ("user_b".to_string(), true, vec![("b".to_string(), vec![])]), - ("user_c".to_string(), true, vec![]) - ] + [("user_b".to_string(), true), ("user_c".to_string(), true)] ); assert_eq!( contacts(&client_b, cx_b), - [ - ( - "user_a".to_string(), - true, - vec![("a".to_string(), vec!["user_b".to_string()])] - ), - ("user_c".to_string(), true, vec![]) - ] + [("user_a".to_string(), true), ("user_c".to_string(), true)] ); assert_eq!( contacts(&client_c, cx_c), - [ - ( - "user_a".to_string(), - true, - vec![("a".to_string(), vec!["user_b".to_string()])] - ), - ("user_b".to_string(), true, vec![("b".to_string(), vec![])]) - ] + [("user_a".to_string(), true,), ("user_b".to_string(), true)] ); project_a @@ -4036,24 +3696,15 @@ async fn test_contacts( deterministic.run_until_parked(); assert_eq!( contacts(&client_a, cx_a), - [ - ("user_b".to_string(), true, vec![("b".to_string(), vec![])]), - ("user_c".to_string(), true, vec![]) - ] + [("user_b".to_string(), true), ("user_c".to_string(), true)] ); assert_eq!( contacts(&client_b, cx_b), - [ - ("user_a".to_string(), true, vec![]), - ("user_c".to_string(), true, vec![]) - ] + [("user_a".to_string(), true), ("user_c".to_string(), true)] ); assert_eq!( contacts(&client_c, cx_c), - [ - ("user_a".to_string(), true, vec![]), - ("user_b".to_string(), true, vec![("b".to_string(), vec![])]) - ] + [("user_a".to_string(), true), ("user_b".to_string(), true)] ); server.disconnect_client(client_c.current_user_id(cx_c)); @@ -4061,17 +3712,11 @@ async fn test_contacts( deterministic.advance_clock(rpc::RECEIVE_TIMEOUT); assert_eq!( contacts(&client_a, cx_a), - [ - ("user_b".to_string(), true, vec![("b".to_string(), vec![])]), - ("user_c".to_string(), false, vec![]) - ] + [("user_b".to_string(), true), ("user_c".to_string(), false)] ); assert_eq!( contacts(&client_b, cx_b), - [ - ("user_a".to_string(), true, vec![]), - ("user_c".to_string(), false, vec![]) - ] + [("user_a".to_string(), true), ("user_c".to_string(), false)] ); assert_eq!(contacts(&client_c, cx_c), []); @@ -4084,48 +3729,24 @@ async fn test_contacts( deterministic.run_until_parked(); assert_eq!( contacts(&client_a, cx_a), - [ - ("user_b".to_string(), true, vec![("b".to_string(), vec![])]), - ("user_c".to_string(), true, vec![]) - ] + [("user_b".to_string(), true), ("user_c".to_string(), true)] ); assert_eq!( contacts(&client_b, cx_b), - [ - ("user_a".to_string(), true, vec![]), - ("user_c".to_string(), true, vec![]) - ] + [("user_a".to_string(), true), ("user_c".to_string(), true)] ); assert_eq!( contacts(&client_c, cx_c), - [ - ("user_a".to_string(), true, vec![]), - ("user_b".to_string(), true, vec![("b".to_string(), vec![])]) - ] + [("user_a".to_string(), true), ("user_b".to_string(), true)] ); #[allow(clippy::type_complexity)] - fn contacts( - client: &TestClient, - cx: &TestAppContext, - ) -> Vec<(String, bool, Vec<(String, Vec)>)> { + fn contacts(client: &TestClient, cx: &TestAppContext) -> Vec<(String, bool)> { client.user_store.read_with(cx, |store, _| { store .contacts() .iter() - .map(|contact| { - let projects = contact - .projects - .iter() - .map(|p| { - ( - p.visible_worktree_root_names[0].clone(), - p.guests.iter().map(|p| p.github_login.clone()).collect(), - ) - }) - .collect(); - (contact.user.github_login.clone(), contact.online, projects) - }) + .map(|contact| (contact.user.github_login.clone(), contact.online)) .collect() }) } @@ -5155,22 +4776,6 @@ async fn test_random_collaboration( log::error!("{} error - {:?}", guest.username, guest_err); } - let contacts = server - .app_state - .db - .get_contacts(guest.current_user_id(&guest_cx)) - .await - .unwrap(); - let contacts = server - .store - .lock() - .await - .build_initial_contacts_update(contacts) - .contacts; - assert!(!contacts - .iter() - .flat_map(|contact| &contact.projects) - .any(|project| project.id == host_project_id)); guest_project.read_with(&guest_cx, |project, _| assert!(project.is_read_only())); guest_cx.update(|_| drop((guest, guest_project))); } @@ -5259,14 +4864,6 @@ async fn test_random_collaboration( "removed guest is still a contact of another peer" ); } - for project in contact.projects { - for project_guest_id in project.guests { - assert_ne!( - project_guest_id, removed_guest_id.0 as u64, - "removed guest appears as still participating on a project" - ); - } - } } } diff --git a/crates/collab/src/rpc/store.rs b/crates/collab/src/rpc/store.rs index 1c69a7c2f801f73f766d4d418e1febaeab9ce2d4..54c3a25e27390c9425e16dc646a2e978130176ab 100644 --- a/crates/collab/src/rpc/store.rs +++ b/crates/collab/src/rpc/store.rs @@ -345,47 +345,11 @@ impl Store { pub fn contact_for_user(&self, user_id: UserId, should_notify: bool) -> proto::Contact { proto::Contact { user_id: user_id.to_proto(), - projects: self.project_metadata_for_user(user_id), online: self.is_user_online(user_id), should_notify, } } - pub fn project_metadata_for_user(&self, user_id: UserId) -> Vec { - let user_connection_state = self.connected_users.get(&user_id); - let project_ids = user_connection_state.iter().flat_map(|state| { - state - .connection_ids - .iter() - .filter_map(|connection_id| self.connections.get(connection_id)) - .flat_map(|connection| connection.projects.iter().copied()) - }); - - let mut metadata = Vec::new(); - for project_id in project_ids { - if let Some(project) = self.projects.get(&project_id) { - if project.host.user_id == user_id && project.online { - metadata.push(proto::ProjectMetadata { - id: project_id.to_proto(), - visible_worktree_root_names: project - .worktrees - .values() - .filter(|worktree| worktree.visible) - .map(|worktree| worktree.root_name.clone()) - .collect(), - guests: project - .guests - .values() - .map(|guest| guest.user_id.to_proto()) - .collect(), - }); - } - } - } - - metadata - } - pub fn create_room(&mut self, creator_connection_id: ConnectionId) -> Result { let connection = self .connections diff --git a/crates/contacts_panel/src/contacts_panel.rs b/crates/contacts_panel/src/contacts_panel.rs index b5460f4d063714f2107d56e94f04f0d61d8b159b..a0259ab8c53ffd301cf7e7f888c212cad74246cd 100644 --- a/crates/contacts_panel/src/contacts_panel.rs +++ b/crates/contacts_panel/src/contacts_panel.rs @@ -8,23 +8,19 @@ use contact_notification::ContactNotification; use editor::{Cancel, Editor}; use fuzzy::{match_strings, StringMatchCandidate}; use gpui::{ - actions, - elements::*, - geometry::{rect::RectF, vector::vec2f}, - impl_actions, impl_internal_actions, - platform::CursorStyle, + actions, elements::*, impl_actions, impl_internal_actions, platform::CursorStyle, AnyViewHandle, AppContext, ClipboardItem, Element, ElementBox, Entity, ModelHandle, MouseButton, MutableAppContext, RenderContext, Subscription, View, ViewContext, ViewHandle, - WeakModelHandle, WeakViewHandle, + WeakViewHandle, }; use join_project_notification::JoinProjectNotification; use menu::{Confirm, SelectNext, SelectPrev}; -use project::{Project, ProjectStore}; +use project::ProjectStore; use serde::Deserialize; use settings::Settings; -use std::{ops::DerefMut, sync::Arc}; +use std::sync::Arc; use theme::IconButton; -use workspace::{sidebar::SidebarItem, JoinProject, ToggleProjectOnline, Workspace}; +use workspace::{sidebar::SidebarItem, Workspace}; actions!(contacts_panel, [ToggleFocus]); @@ -48,8 +44,6 @@ enum ContactEntry { IncomingRequest(Arc), OutgoingRequest(Arc), Contact(Arc), - ContactProject(Arc, usize, Option>), - OfflineProject(WeakModelHandle), } #[derive(Clone, PartialEq)] @@ -181,7 +175,6 @@ impl ContactsPanel { let list_state = ListState::new(0, Orientation::Top, 1000., cx, move |this, ix, cx| { let theme = cx.global::().theme.clone(); - let current_user_id = this.user_store.read(cx).current_user().map(|user| user.id); let is_selected = this.selection == Some(ix); match &this.entries[ix] { @@ -214,34 +207,6 @@ impl ContactsPanel { ContactEntry::Contact(contact) => { Self::render_contact(&contact.user, &theme.contacts_panel, is_selected) } - ContactEntry::ContactProject(contact, project_ix, open_project) => { - let is_last_project_for_contact = - this.entries.get(ix + 1).map_or(true, |next| { - if let ContactEntry::ContactProject(next_contact, _, _) = next { - next_contact.user.id != contact.user.id - } else { - true - } - }); - Self::render_project( - contact.clone(), - current_user_id, - *project_ix, - *open_project, - &theme.contacts_panel, - &theme.tooltip, - is_last_project_for_contact, - is_selected, - cx, - ) - } - ContactEntry::OfflineProject(project) => Self::render_offline_project( - *project, - &theme.contacts_panel, - &theme.tooltip, - is_selected, - cx, - ), } }); @@ -343,260 +308,6 @@ impl ContactsPanel { .boxed() } - #[allow(clippy::too_many_arguments)] - fn render_project( - contact: Arc, - current_user_id: Option, - project_index: usize, - open_project: Option>, - theme: &theme::ContactsPanel, - tooltip_style: &TooltipStyle, - is_last_project: bool, - is_selected: bool, - cx: &mut RenderContext, - ) -> ElementBox { - enum ToggleOnline {} - - let project = &contact.projects[project_index]; - let project_id = project.id; - let is_host = Some(contact.user.id) == current_user_id; - let open_project = open_project.and_then(|p| p.upgrade(cx.deref_mut())); - - let font_cache = cx.font_cache(); - let host_avatar_height = theme - .contact_avatar - .width - .or(theme.contact_avatar.height) - .unwrap_or(0.); - let row = &theme.project_row.default; - let tree_branch = theme.tree_branch; - let line_height = row.name.text.line_height(font_cache); - let cap_height = row.name.text.cap_height(font_cache); - let baseline_offset = - row.name.text.baseline_offset(font_cache) + (theme.row_height - line_height) / 2.; - - MouseEventHandler::::new(project_id as usize, cx, |mouse_state, cx| { - let tree_branch = *tree_branch.style_for(mouse_state, is_selected); - let row = theme.project_row.style_for(mouse_state, is_selected); - - Flex::row() - .with_child( - Stack::new() - .with_child( - Canvas::new(move |bounds, _, cx| { - let start_x = bounds.min_x() + (bounds.width() / 2.) - - (tree_branch.width / 2.); - let end_x = bounds.max_x(); - let start_y = bounds.min_y(); - let end_y = bounds.min_y() + baseline_offset - (cap_height / 2.); - - cx.scene.push_quad(gpui::Quad { - bounds: RectF::from_points( - vec2f(start_x, start_y), - vec2f( - start_x + tree_branch.width, - if is_last_project { - end_y - } else { - bounds.max_y() - }, - ), - ), - background: Some(tree_branch.color), - border: gpui::Border::default(), - corner_radius: 0., - }); - cx.scene.push_quad(gpui::Quad { - bounds: RectF::from_points( - vec2f(start_x, end_y), - vec2f(end_x, end_y + tree_branch.width), - ), - background: Some(tree_branch.color), - border: gpui::Border::default(), - corner_radius: 0., - }); - }) - .boxed(), - ) - .with_children(open_project.and_then(|open_project| { - let is_going_offline = !open_project.read(cx).is_online(); - if !mouse_state.hovered && !is_going_offline { - return None; - } - - let button = MouseEventHandler::::new( - project_id as usize, - cx, - |state, _| { - let mut icon_style = - *theme.private_button.style_for(state, false); - icon_style.container.background_color = - row.container.background_color; - if is_going_offline { - icon_style.color = theme.disabled_button.color; - } - render_icon_button(&icon_style, "icons/lock_8.svg") - .aligned() - .boxed() - }, - ); - - if is_going_offline { - Some(button.boxed()) - } else { - Some( - button - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, move |_, cx| { - cx.dispatch_action(ToggleProjectOnline { - project: Some(open_project.clone()), - }) - }) - .with_tooltip::( - project_id as usize, - "Take project offline".to_string(), - None, - tooltip_style.clone(), - cx, - ) - .boxed(), - ) - } - })) - .constrained() - .with_width(host_avatar_height) - .boxed(), - ) - .with_child( - Label::new( - project.visible_worktree_root_names.join(", "), - row.name.text.clone(), - ) - .aligned() - .left() - .contained() - .with_style(row.name.container) - .flex(1., false) - .boxed(), - ) - .with_children(project.guests.iter().filter_map(|participant| { - participant.avatar.clone().map(|avatar| { - Image::new(avatar) - .with_style(row.guest_avatar) - .aligned() - .left() - .contained() - .with_margin_right(row.guest_avatar_spacing) - .boxed() - }) - })) - .constrained() - .with_height(theme.row_height) - .contained() - .with_style(row.container) - .boxed() - }) - .with_cursor_style(if !is_host { - CursorStyle::PointingHand - } else { - CursorStyle::Arrow - }) - .on_click(MouseButton::Left, move |_, cx| { - if !is_host { - cx.dispatch_global_action(JoinProject { - contact: contact.clone(), - project_index, - }); - } - }) - .boxed() - } - - fn render_offline_project( - project_handle: WeakModelHandle, - theme: &theme::ContactsPanel, - tooltip_style: &TooltipStyle, - is_selected: bool, - cx: &mut RenderContext, - ) -> ElementBox { - let host_avatar_height = theme - .contact_avatar - .width - .or(theme.contact_avatar.height) - .unwrap_or(0.); - - enum LocalProject {} - enum ToggleOnline {} - - let project_id = project_handle.id(); - MouseEventHandler::::new(project_id, cx, |state, cx| { - let row = theme.project_row.style_for(state, is_selected); - let mut worktree_root_names = String::new(); - let project = if let Some(project) = project_handle.upgrade(cx.deref_mut()) { - project.read(cx) - } else { - return Empty::new().boxed(); - }; - let is_going_online = project.is_online(); - for tree in project.visible_worktrees(cx) { - if !worktree_root_names.is_empty() { - worktree_root_names.push_str(", "); - } - worktree_root_names.push_str(tree.read(cx).root_name()); - } - - Flex::row() - .with_child({ - let button = - MouseEventHandler::::new(project_id, cx, |state, _| { - let mut style = *theme.private_button.style_for(state, false); - if is_going_online { - style.color = theme.disabled_button.color; - } - render_icon_button(&style, "icons/lock_8.svg") - .aligned() - .constrained() - .with_width(host_avatar_height) - .boxed() - }); - - if is_going_online { - button.boxed() - } else { - button - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, move |_, cx| { - let project = project_handle.upgrade(cx.app); - cx.dispatch_action(ToggleProjectOnline { project }) - }) - .with_tooltip::( - project_id, - "Take project online".to_string(), - None, - tooltip_style.clone(), - cx, - ) - .boxed() - } - }) - .with_child( - Label::new(worktree_root_names, row.name.text.clone()) - .aligned() - .left() - .contained() - .with_style(row.name.container) - .flex(1., false) - .boxed(), - ) - .constrained() - .with_height(theme.row_height) - .contained() - .with_style(row.container) - .boxed() - }) - .boxed() - } - fn render_contact_request( user: Arc, user_store: ModelHandle, @@ -710,7 +421,6 @@ impl ContactsPanel { fn update_entries(&mut self, cx: &mut ViewContext) { let user_store = self.user_store.read(cx); - let project_store = self.project_store.read(cx); let query = self.filter_editor.read(cx).text(cx); let executor = cx.background().clone(); @@ -837,60 +547,6 @@ impl ContactsPanel { for mat in matches { let contact = &contacts[mat.candidate_id]; self.entries.push(ContactEntry::Contact(contact.clone())); - - let is_current_user = current_user - .as_ref() - .map_or(false, |user| user.id == contact.user.id); - if is_current_user { - let mut open_projects = - project_store.projects(cx).collect::>(); - self.entries.extend( - contact.projects.iter().enumerate().filter_map( - |(ix, project)| { - let open_project = open_projects - .iter() - .position(|p| { - p.read(cx).remote_id() == Some(project.id) - }) - .map(|ix| open_projects.remove(ix).downgrade()); - if project.visible_worktree_root_names.is_empty() { - None - } else { - Some(ContactEntry::ContactProject( - contact.clone(), - ix, - open_project, - )) - } - }, - ), - ); - self.entries.extend(open_projects.into_iter().filter_map( - |project| { - if project.read(cx).visible_worktrees(cx).next().is_none() { - None - } else { - Some(ContactEntry::OfflineProject(project.downgrade())) - } - }, - )); - } else { - self.entries.extend( - contact.projects.iter().enumerate().filter_map( - |(ix, project)| { - if project.visible_worktree_root_names.is_empty() { - None - } else { - Some(ContactEntry::ContactProject( - contact.clone(), - ix, - None, - )) - } - }, - ), - ); - } } } } @@ -981,18 +637,6 @@ impl ContactsPanel { let section = *section; self.toggle_expanded(&ToggleExpanded(section), cx); } - ContactEntry::ContactProject(contact, project_index, open_project) => { - if let Some(open_project) = open_project { - workspace::activate_workspace_for_project(cx, |_, cx| { - cx.model_id() == open_project.id() - }); - } else { - cx.dispatch_global_action(JoinProject { - contact: contact.clone(), - project_index: *project_index, - }) - } - } _ => {} } } @@ -1181,16 +825,6 @@ impl PartialEq for ContactEntry { return contact_1.user.id == contact_2.user.id; } } - ContactEntry::ContactProject(contact_1, ix_1, _) => { - if let ContactEntry::ContactProject(contact_2, ix_2, _) = other { - return contact_1.user.id == contact_2.user.id && ix_1 == ix_2; - } - } - ContactEntry::OfflineProject(project_1) => { - if let ContactEntry::OfflineProject(project_2) = other { - return project_1.id() == project_2.id(); - } - } } false } @@ -1205,7 +839,7 @@ mod tests { Client, }; use collections::HashSet; - use gpui::{serde_json::json, TestAppContext}; + use gpui::TestAppContext; use language::LanguageRegistry; use project::{FakeFs, Project}; @@ -1221,8 +855,6 @@ mod tests { let project_store = cx.add_model(|_| ProjectStore::new(project::Db::open_fake())); let server = FakeServer::for_client(current_user_id, &client, cx).await; let fs = FakeFs::new(cx.background()); - fs.insert_tree("/private_dir", json!({ "one.rs": "" })) - .await; let project = cx.update(|cx| { Project::local( false, @@ -1234,14 +866,6 @@ mod tests { cx, ) }); - let worktree_id = project - .update(cx, |project, cx| { - project.find_or_create_local_worktree("/private_dir", true, cx) - }) - .await - .unwrap() - .0 - .read_with(cx, |worktree, _| worktree.id().to_proto()); let (_, workspace) = cx.add_window(|cx| Workspace::new(project.clone(), |_, _| unimplemented!(), cx)); @@ -1315,211 +939,26 @@ mod tests { user_id: 3, online: true, should_notify: false, - projects: vec![proto::ProjectMetadata { - id: 101, - visible_worktree_root_names: vec!["dir1".to_string()], - guests: vec![2], - }], }, proto::Contact { user_id: 4, online: true, should_notify: false, - projects: vec![proto::ProjectMetadata { - id: 102, - visible_worktree_root_names: vec!["dir2".to_string()], - guests: vec![2], - }], }, proto::Contact { user_id: 5, online: false, should_notify: false, - projects: vec![], }, proto::Contact { user_id: current_user_id, online: true, should_notify: false, - projects: vec![proto::ProjectMetadata { - id: 103, - visible_worktree_root_names: vec!["dir3".to_string()], - guests: vec![3], - }], }, ], ..Default::default() }); - assert_eq!( - server - .receive::() - .await - .unwrap() - .payload, - proto::UpdateProject { - project_id: 200, - online: false, - worktrees: vec![] - }, - ); - - cx.foreground().run_until_parked(); - assert_eq!( - cx.read(|cx| render_to_strings(&panel, cx)), - &[ - "v Requests", - " incoming user_one", - " outgoing user_two", - "v Online", - " the_current_user", - " dir3", - " 🔒 private_dir", - " user_four", - " dir2", - " user_three", - " dir1", - "v Offline", - " user_five", - ] - ); - - // Take a project online. It appears as loading, since the project - // isn't yet visible to other contacts. - project.update(cx, |project, cx| project.set_online(true, cx)); - cx.foreground().run_until_parked(); - assert_eq!( - cx.read(|cx| render_to_strings(&panel, cx)), - &[ - "v Requests", - " incoming user_one", - " outgoing user_two", - "v Online", - " the_current_user", - " dir3", - " 🔒 private_dir (going online...)", - " user_four", - " dir2", - " user_three", - " dir1", - "v Offline", - " user_five", - ] - ); - - // The server receives the project's metadata and updates the contact metadata - // for the current user. Now the project appears as online. - assert_eq!( - server - .receive::() - .await - .unwrap() - .payload, - proto::UpdateProject { - project_id: 200, - online: true, - worktrees: vec![proto::WorktreeMetadata { - id: worktree_id, - root_name: "private_dir".to_string(), - visible: true, - }] - }, - ); - server - .receive::() - .await - .unwrap(); - - server.send(proto::UpdateContacts { - contacts: vec![proto::Contact { - user_id: current_user_id, - online: true, - should_notify: false, - projects: vec![ - proto::ProjectMetadata { - id: 103, - visible_worktree_root_names: vec!["dir3".to_string()], - guests: vec![3], - }, - proto::ProjectMetadata { - id: 200, - visible_worktree_root_names: vec!["private_dir".to_string()], - guests: vec![3], - }, - ], - }], - ..Default::default() - }); - cx.foreground().run_until_parked(); - assert_eq!( - cx.read(|cx| render_to_strings(&panel, cx)), - &[ - "v Requests", - " incoming user_one", - " outgoing user_two", - "v Online", - " the_current_user", - " dir3", - " private_dir", - " user_four", - " dir2", - " user_three", - " dir1", - "v Offline", - " user_five", - ] - ); - - // Take the project offline. It appears as loading. - project.update(cx, |project, cx| project.set_online(false, cx)); - cx.foreground().run_until_parked(); - assert_eq!( - cx.read(|cx| render_to_strings(&panel, cx)), - &[ - "v Requests", - " incoming user_one", - " outgoing user_two", - "v Online", - " the_current_user", - " dir3", - " private_dir (going offline...)", - " user_four", - " dir2", - " user_three", - " dir1", - "v Offline", - " user_five", - ] - ); - - // The server receives the unregister request and updates the contact - // metadata for the current user. The project is now offline. - assert_eq!( - server - .receive::() - .await - .unwrap() - .payload, - proto::UpdateProject { - project_id: 200, - online: false, - worktrees: vec![] - }, - ); - - server.send(proto::UpdateContacts { - contacts: vec![proto::Contact { - user_id: current_user_id, - online: true, - should_notify: false, - projects: vec![proto::ProjectMetadata { - id: 103, - visible_worktree_root_names: vec!["dir3".to_string()], - guests: vec![3], - }], - }], - ..Default::default() - }); cx.foreground().run_until_parked(); assert_eq!( cx.read(|cx| render_to_strings(&panel, cx)), @@ -1529,12 +968,8 @@ mod tests { " outgoing user_two", "v Online", " the_current_user", - " dir3", - " 🔒 private_dir", " user_four", - " dir2", " user_three", - " dir1", "v Offline", " user_five", ] @@ -1551,7 +986,6 @@ mod tests { &[ "v Online", " user_four <=== selected", - " dir2", "v Offline", " user_five", ] @@ -1565,8 +999,7 @@ mod tests { &[ "v Online", " user_four", - " dir2 <=== selected", - "v Offline", + "v Offline <=== selected", " user_five", ] ); @@ -1579,9 +1012,8 @@ mod tests { &[ "v Online", " user_four", - " dir2", - "v Offline <=== selected", - " user_five", + "v Offline", + " user_five <=== selected", ] ); } @@ -1608,37 +1040,6 @@ mod tests { ContactEntry::Contact(contact) => { format!(" {}", contact.user.github_login) } - ContactEntry::ContactProject(contact, project_ix, project) => { - let project = project - .and_then(|p| p.upgrade(cx)) - .map(|project| project.read(cx)); - format!( - " {}{}", - contact.projects[*project_ix] - .visible_worktree_root_names - .join(", "), - if project.map_or(true, |project| project.is_online()) { - "" - } else { - " (going offline...)" - }, - ) - } - ContactEntry::OfflineProject(project) => { - let project = project.upgrade(cx).unwrap().read(cx); - format!( - " 🔒 {}{}", - project - .worktree_root_names(cx) - .collect::>() - .join(", "), - if project.is_online() { - " (going online...)" - } else { - "" - }, - ) - } }; if panel.selection == Some(ix) { diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 1125c2b3ad9dc89c7c6cd8d0f1b7c6dff4f0227c..751c41b209e552d4f2f2e5341809c583d60f9913 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -1048,15 +1048,8 @@ message ChannelMessage { message Contact { uint64 user_id = 1; - repeated ProjectMetadata projects = 2; - bool online = 3; - bool should_notify = 4; -} - -message ProjectMetadata { - uint64 id = 1; - repeated string visible_worktree_root_names = 3; - repeated uint64 guests = 4; + bool online = 2; + bool should_notify = 3; } message WorktreeMetadata { diff --git a/crates/workspace/src/waiting_room.rs b/crates/workspace/src/waiting_room.rs deleted file mode 100644 index bdced26c8b137a288be97621b22d2281714579f2..0000000000000000000000000000000000000000 --- a/crates/workspace/src/waiting_room.rs +++ /dev/null @@ -1,185 +0,0 @@ -use crate::{sidebar::SidebarSide, AppState, ToggleFollow, Workspace}; -use anyhow::Result; -use client::{proto, Client, Contact}; -use gpui::{ - elements::*, ElementBox, Entity, ImageData, MutableAppContext, RenderContext, Task, View, - ViewContext, -}; -use project::Project; -use settings::Settings; -use std::sync::Arc; -use util::ResultExt; - -pub struct WaitingRoom { - project_id: u64, - avatar: Option>, - message: String, - waiting: bool, - client: Arc, - _join_task: Task>, -} - -impl Entity for WaitingRoom { - type Event = (); - - fn release(&mut self, _: &mut MutableAppContext) { - if self.waiting { - self.client - .send(proto::LeaveProject { - project_id: self.project_id, - }) - .log_err(); - } - } -} - -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 client = app_state.client.clone(); - 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.project_store.clone(), - app_state.languages.clone(), - app_state.fs.clone(), - cx.clone(), - ) - .await; - - if let Some(this) = this.upgrade(&cx) { - this.update(&mut cx, |this, cx| { - this.waiting = false; - match project { - Ok(project) => { - cx.replace_root_view(|cx| { - let mut workspace = - Workspace::new(project, app_state.default_item_factory, cx); - (app_state.initialize_workspace)( - &mut workspace, - &app_state, - cx, - ); - workspace.toggle_sidebar(SidebarSide::Left, cx); - if let Some((host_peer_id, _)) = workspace - .project - .read(cx) - .collaborators() - .iter() - .find(|(_, collaborator)| collaborator.replica_id == 0) - { - if let Some(follow) = workspace - .toggle_follow(&ToggleFollow(*host_peer_id), cx) - { - follow.detach_and_log_err(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] - .visible_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 { - project_id, - avatar: contact.user.avatar.clone(), - message: format!( - "Asking to join @{}'s copy of {}...", - contact.user.github_login, - humanize_list(&contact.projects[project_index].visible_worktree_root_names) - ), - waiting: true, - client, - _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 04bbc094b252e77d1b1a474814de4fa97eb4d3ef..90f01a3a5f24aafad8b25b5844e8b95ee3ff1188 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -10,7 +10,6 @@ pub mod searchable; pub mod sidebar; mod status_bar; mod toolbar; -mod waiting_room; use anyhow::{anyhow, Context, Result}; use client::{proto, Client, Contact, PeerId, Subscription, TypedEnvelope, UserStore}; @@ -58,7 +57,6 @@ use std::{ use theme::{Theme, ThemeRegistry}; pub use toolbar::{ToolbarItemLocation, ToolbarItemView}; use util::ResultExt; -use waiting_room::WaitingRoom; type ProjectItemBuilders = HashMap< TypeId, @@ -167,14 +165,6 @@ pub fn init(app_state: Arc, cx: &mut MutableAppContext) { } } }); - cx.add_global_action({ - let app_state = Arc::downgrade(&app_state); - move |action: &JoinProject, cx: &mut MutableAppContext| { - if let Some(app_state) = app_state.upgrade() { - join_project(action.contact.clone(), action.project_index, &app_state, cx); - } - } - }); cx.add_async_action(Workspace::toggle_follow); cx.add_async_action(Workspace::follow_next_collaborator); @@ -2663,28 +2653,6 @@ pub fn open_paths( }) } -pub fn join_project( - contact: Arc, - project_index: usize, - app_state: &Arc, - cx: &mut MutableAppContext, -) { - let project_id = contact.projects[project_index].id; - - 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) { - cx.activate_window(window_id); - return; - } - } - } - - 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) { let (window_id, workspace) = cx.add_window((app_state.build_window_options)(), |cx| { let mut workspace = Workspace::new(