Allow making projects private

Max Brunsfeld created

Change summary

crates/collab/src/integration_tests.rs      |   2 
crates/contacts_panel/src/contacts_panel.rs | 121 ++++++++++++++--------
crates/project/src/project.rs               |  47 ++++++--
crates/project/src/worktree.rs              |  18 +-
crates/theme/src/theme.rs                   |   2 
crates/workspace/src/workspace.rs           |  22 +--
styles/src/styleTree/contactsPanel.ts       |   3 
7 files changed, 132 insertions(+), 83 deletions(-)

Detailed changes

crates/collab/src/integration_tests.rs 🔗

@@ -532,7 +532,7 @@ async fn test_private_projects(
         .read_with(cx_b, |store, _| { store.contacts()[0].projects.is_empty() }));
 
     // The project is registered when it is made public.
-    project_a.update(cx_a, |project, _| project.set_public(true));
+    project_a.update(cx_a, |project, cx| project.set_public(true, cx));
     deterministic.run_until_parked();
     assert!(project_a.read_with(cx_a, |project, _| project.remote_id().is_some()));
     assert!(!client_b

crates/contacts_panel/src/contacts_panel.rs 🔗

@@ -207,7 +207,7 @@ impl ContactsPanel {
                 ContactEntry::Contact(contact) => {
                     Self::render_contact(&contact.user, theme, is_selected)
                 }
-                ContactEntry::ContactProject(contact, project_ix, _) => {
+                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 {
@@ -216,10 +216,11 @@ impl ContactsPanel {
                                 true
                             }
                         });
-                    Self::render_contact_project(
+                    Self::render_project(
                         contact.clone(),
                         current_user_id,
                         *project_ix,
+                        open_project.clone(),
                         theme,
                         is_last_project_for_contact,
                         is_selected,
@@ -328,10 +329,11 @@ impl ContactsPanel {
             .boxed()
     }
 
-    fn render_contact_project(
+    fn render_project(
         contact: Arc<Contact>,
         current_user_id: Option<u64>,
         project_index: usize,
+        open_project: Option<WeakModelHandle<Project>>,
         theme: &theme::ContactsPanel,
         is_last_project: bool,
         is_selected: bool,
@@ -340,6 +342,7 @@ impl ContactsPanel {
         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
@@ -354,48 +357,78 @@ impl ContactsPanel {
         let baseline_offset =
             row.name.text.baseline_offset(font_cache) + (theme.row_height - line_height) / 2.;
 
-        MouseEventHandler::new::<JoinProject, _, _>(project_id as usize, cx, |mouse_state, _| {
+        MouseEventHandler::new::<JoinProject, _, _>(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(
-                    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()
+                    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(if mouse_state.hovered && open_project.is_some() {
+                            Some(
+                                MouseEventHandler::new::<ToggleProjectPublic, _, _>(
+                                    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;
+                                        render_icon_button(&icon_style, "icons/lock-8.svg")
+                                            .aligned()
+                                            .boxed()
                                     },
-                                ),
-                            ),
-                            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.,
-                        });
-                    })
-                    .constrained()
-                    .with_width(host_avatar_height)
-                    .boxed(),
+                                )
+                                .with_cursor_style(CursorStyle::PointingHand)
+                                .on_click(move |_, _, cx| {
+                                    cx.dispatch_action(ToggleProjectPublic {
+                                        project: open_project.clone(),
+                                    })
+                                })
+                                .boxed(),
+                            )
+                        } else {
+                            None
+                        })
+                        .constrained()
+                        .with_width(host_avatar_height)
+                        .boxed(),
                 )
                 .with_child(
                     Label::new(
@@ -467,9 +500,9 @@ impl ContactsPanel {
         MouseEventHandler::new::<LocalProject, _, _>(project_id, cx, |state, cx| {
             let row = theme.project_row.style_for(state, is_selected);
             let mut worktree_root_names = String::new();
-            let project = project.read(cx);
-            let is_public = project.is_public();
-            for tree in project.visible_worktrees(cx) {
+            let project_ = project.read(cx);
+            let is_public = project_.is_public();
+            for tree in project_.visible_worktrees(cx) {
                 if !worktree_root_names.is_empty() {
                     worktree_root_names.push_str(", ");
                 }
@@ -498,7 +531,9 @@ impl ContactsPanel {
                         CursorStyle::PointingHand
                     })
                     .on_click(move |_, _, cx| {
-                        cx.dispatch_action(ToggleProjectPublic { project: None })
+                        cx.dispatch_action(ToggleProjectPublic {
+                            project: Some(project.clone()),
+                        })
                     })
                     .boxed(),
                 )

crates/project/src/project.rs 🔗

@@ -8,7 +8,7 @@ use anyhow::{anyhow, Context, Result};
 use client::{proto, Client, PeerId, TypedEnvelope, User, UserStore};
 use clock::ReplicaId;
 use collections::{hash_map, BTreeMap, HashMap, HashSet};
-use futures::{future::Shared, select_biased, Future, FutureExt, StreamExt, TryFutureExt};
+use futures::{future::Shared, Future, FutureExt, StreamExt, TryFutureExt};
 use fuzzy::{PathMatch, PathMatchCandidate, PathMatchCandidateSet};
 use gpui::{
     AnyModelHandle, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle,
@@ -25,6 +25,7 @@ use language::{
 use lsp::{DiagnosticSeverity, DiagnosticTag, DocumentHighlightKind, LanguageServer};
 use lsp_command::*;
 use parking_lot::Mutex;
+use postage::stream::Stream;
 use postage::watch;
 use rand::prelude::*;
 use search::SearchQuery;
@@ -325,14 +326,12 @@ impl Project {
             let (public_tx, public_rx) = watch::channel_with(public);
             let (remote_id_tx, remote_id_rx) = watch::channel();
             let _maintain_remote_id_task = cx.spawn_weak({
-                let mut status_rx = client.clone().status();
-                let mut public_rx = public_rx.clone();
+                let status_rx = client.clone().status();
+                let public_rx = public_rx.clone();
                 move |this, mut cx| async move {
-                    loop {
-                        select_biased! {
-                            value = status_rx.next().fuse() => { value?; }
-                            value = public_rx.next().fuse() => { value?; }
-                        };
+                    let mut stream = Stream::map(status_rx.clone(), drop)
+                        .merge(Stream::map(public_rx.clone(), drop));
+                    while stream.recv().await.is_some() {
                         let this = this.upgrade(&cx)?;
                         if status_rx.borrow().is_connected() && *public_rx.borrow() {
                             this.update(&mut cx, |this, cx| this.register(cx))
@@ -342,11 +341,12 @@ impl Project {
                             this.update(&mut cx, |this, cx| this.unregister(cx));
                         }
                     }
+                    None
                 }
             });
 
             let handle = cx.weak_handle();
-            project_store.update(cx, |store, cx| store.add(handle, cx));
+            project_store.update(cx, |store, cx| store.add_project(handle, cx));
 
             let (opened_buffer_tx, opened_buffer_rx) = watch::channel();
             Self {
@@ -434,7 +434,7 @@ impl Project {
         let (opened_buffer_tx, opened_buffer_rx) = watch::channel();
         let this = cx.add_model(|cx: &mut ModelContext<Self>| {
             let handle = cx.weak_handle();
-            project_store.update(cx, |store, cx| store.add(handle, cx));
+            project_store.update(cx, |store, cx| store.add_project(handle, cx));
 
             let mut this = Self {
                 worktrees: Vec::new(),
@@ -624,9 +624,10 @@ impl Project {
         &self.fs
     }
 
-    pub fn set_public(&mut self, is_public: bool) {
+    pub fn set_public(&mut self, is_public: bool, cx: &mut ModelContext<Self>) {
         if let ProjectClientState::Local { public_tx, .. } = &mut self.client_state {
             *public_tx.borrow_mut() = is_public;
+            self.metadata_changed(cx);
         }
     }
 
@@ -648,10 +649,19 @@ impl Project {
         }
 
         if let ProjectClientState::Local { remote_id_tx, .. } = &mut self.client_state {
-            *remote_id_tx.borrow_mut() = None;
+            let mut remote_id = remote_id_tx.borrow_mut();
+            if let Some(remote_id) = *remote_id {
+                self.client
+                    .send(proto::UnregisterProject {
+                        project_id: remote_id,
+                    })
+                    .log_err();
+            }
+            *remote_id = None;
         }
 
         self.subscriptions.clear();
+        self.metadata_changed(cx);
     }
 
     fn register(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
@@ -671,6 +681,7 @@ impl Project {
                     *remote_id_tx.borrow_mut() = Some(remote_id);
                 }
 
+                this.metadata_changed(cx);
                 cx.emit(Event::RemoteIdChanged(Some(remote_id)));
 
                 this.subscriptions
@@ -745,6 +756,10 @@ impl Project {
         }
     }
 
+    fn metadata_changed(&mut self, cx: &mut ModelContext<Self>) {
+        self.project_store.update(cx, |_, cx| cx.notify());
+    }
+
     pub fn collaborators(&self) -> &HashMap<PeerId, Collaborator> {
         &self.collaborators
     }
@@ -3743,6 +3758,7 @@ impl Project {
                 false
             }
         });
+        self.metadata_changed(cx);
         cx.notify();
     }
 
@@ -3772,6 +3788,7 @@ impl Project {
             self.worktrees
                 .push(WorktreeHandle::Weak(worktree.downgrade()));
         }
+        self.metadata_changed(cx);
         cx.emit(Event::WorktreeAdded);
         cx.notify();
     }
@@ -5204,7 +5221,7 @@ impl ProjectStore {
             .filter_map(|project| project.upgrade(cx))
     }
 
-    fn add(&mut self, project: WeakModelHandle<Project>, cx: &mut ModelContext<Self>) {
+    fn add_project(&mut self, project: WeakModelHandle<Project>, cx: &mut ModelContext<Self>) {
         if let Err(ix) = self
             .projects
             .binary_search_by_key(&project.id(), WeakModelHandle::id)
@@ -5214,7 +5231,7 @@ impl ProjectStore {
         cx.notify();
     }
 
-    fn prune(&mut self, cx: &mut ModelContext<Self>) {
+    fn prune_projects(&mut self, cx: &mut ModelContext<Self>) {
         let mut did_change = false;
         self.projects.retain(|project| {
             if project.is_upgradable(cx) {
@@ -5316,7 +5333,7 @@ impl Entity for Project {
     type Event = Event;
 
     fn release(&mut self, cx: &mut gpui::MutableAppContext) {
-        self.project_store.update(cx, ProjectStore::prune);
+        self.project_store.update(cx, ProjectStore::prune_projects);
 
         match &self.client_state {
             ProjectClientState::Local { remote_id_rx, .. } => {

crates/project/src/worktree.rs 🔗

@@ -151,14 +151,7 @@ impl Entity for Worktree {
 
     fn release(&mut self, _: &mut MutableAppContext) {
         if let Some(worktree) = self.as_local_mut() {
-            if let Registration::Done { project_id } = worktree.registration {
-                let client = worktree.client.clone();
-                let unregister_message = proto::UnregisterWorktree {
-                    project_id,
-                    worktree_id: worktree.id().to_proto(),
-                };
-                client.send(unregister_message).log_err();
-            }
+            worktree.unregister();
         }
     }
 }
@@ -1063,6 +1056,15 @@ impl LocalWorktree {
 
     pub fn unregister(&mut self) {
         self.unshare();
+        if let Registration::Done { project_id } = self.registration {
+            self.client
+                .clone()
+                .send(proto::UnregisterWorktree {
+                    project_id,
+                    worktree_id: self.id().to_proto(),
+                })
+                .log_err();
+        }
         self.registration = Registration::None;
     }
 

crates/theme/src/theme.rs 🔗

@@ -319,7 +319,7 @@ pub struct Icon {
     pub path: String,
 }
 
-#[derive(Clone, Deserialize, Default)]
+#[derive(Deserialize, Clone, Copy, Default)]
 pub struct IconButton {
     #[serde(flatten)]
     pub container: ContainerStyle,

crates/workspace/src/workspace.rs 🔗

@@ -22,7 +22,7 @@ use gpui::{
     platform::{CursorStyle, WindowOptions},
     AnyModelHandle, AnyViewHandle, AppContext, AsyncAppContext, Border, Entity, ImageData,
     ModelContext, ModelHandle, MutableAppContext, PathPromptOptions, PromptLevel, RenderContext,
-    Task, View, ViewContext, ViewHandle, WeakModelHandle, WeakViewHandle,
+    Task, View, ViewContext, ViewHandle, WeakViewHandle,
 };
 use language::LanguageRegistry;
 use log::error;
@@ -102,7 +102,7 @@ pub struct OpenPaths {
 #[derive(Clone, Deserialize)]
 pub struct ToggleProjectPublic {
     #[serde(skip_deserializing)]
-    pub project: Option<WeakModelHandle<Project>>,
+    pub project: Option<ModelHandle<Project>>,
 }
 
 #[derive(Clone)]
@@ -1050,19 +1050,13 @@ impl Workspace {
     }
 
     fn toggle_project_public(&mut self, action: &ToggleProjectPublic, cx: &mut ViewContext<Self>) {
-        let project = if let Some(project) = action.project {
-            if let Some(project) = project.upgrade(cx) {
-                project
-            } else {
-                return;
-            }
-        } else {
-            self.project.clone()
-        };
-
-        project.update(cx, |project, _| {
+        let project = action
+            .project
+            .clone()
+            .unwrap_or_else(|| self.project.clone());
+        project.update(cx, |project, cx| {
             let is_public = project.is_public();
-            project.set_public(!is_public);
+            project.set_public(!is_public, cx);
         });
     }
 

styles/src/styleTree/contactsPanel.ts 🔗

@@ -71,7 +71,8 @@ export default function contactsPanel(theme: Theme) {
     privateButton: {
       iconWidth: 8,
       color: iconColor(theme, "primary"),
-      buttonWidth: 8,
+      cornerRadius: 5,
+      buttonWidth: 12,
     },
     rowHeight: 28,
     sectionIconSize: 8,