Allow making projects private

This commit is contained in:
Max Brunsfeld 2022-05-31 16:47:06 -07:00
parent 8f676e76b3
commit 3ea061a11e
7 changed files with 131 additions and 82 deletions

View file

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

View file

@ -207,7 +207,7 @@ impl ContactsPanel {
ContactEntry::Contact(contact) => { ContactEntry::Contact(contact) => {
Self::render_contact(&contact.user, theme, is_selected) 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 = let is_last_project_for_contact =
this.entries.get(ix + 1).map_or(true, |next| { this.entries.get(ix + 1).map_or(true, |next| {
if let ContactEntry::ContactProject(next_contact, _, _) = next { if let ContactEntry::ContactProject(next_contact, _, _) = next {
@ -216,10 +216,11 @@ impl ContactsPanel {
true true
} }
}); });
Self::render_contact_project( Self::render_project(
contact.clone(), contact.clone(),
current_user_id, current_user_id,
*project_ix, *project_ix,
open_project.clone(),
theme, theme,
is_last_project_for_contact, is_last_project_for_contact,
is_selected, is_selected,
@ -328,10 +329,11 @@ impl ContactsPanel {
.boxed() .boxed()
} }
fn render_contact_project( fn render_project(
contact: Arc<Contact>, contact: Arc<Contact>,
current_user_id: Option<u64>, current_user_id: Option<u64>,
project_index: usize, project_index: usize,
open_project: Option<WeakModelHandle<Project>>,
theme: &theme::ContactsPanel, theme: &theme::ContactsPanel,
is_last_project: bool, is_last_project: bool,
is_selected: bool, is_selected: bool,
@ -340,6 +342,7 @@ impl ContactsPanel {
let project = &contact.projects[project_index]; let project = &contact.projects[project_index];
let project_id = project.id; let project_id = project.id;
let is_host = Some(contact.user.id) == current_user_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 font_cache = cx.font_cache();
let host_avatar_height = theme let host_avatar_height = theme
@ -354,48 +357,78 @@ impl ContactsPanel {
let baseline_offset = let baseline_offset =
row.name.text.baseline_offset(font_cache) + (theme.row_height - line_height) / 2.; 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 tree_branch = *tree_branch.style_for(mouse_state, is_selected);
let row = theme.project_row.style_for(mouse_state, is_selected); let row = theme.project_row.style_for(mouse_state, is_selected);
Flex::row() Flex::row()
.with_child( .with_child(
Canvas::new(move |bounds, _, cx| { Stack::new()
let start_x = .with_child(
bounds.min_x() + (bounds.width() / 2.) - (tree_branch.width / 2.); Canvas::new(move |bounds, _, cx| {
let end_x = bounds.max_x(); let start_x = bounds.min_x() + (bounds.width() / 2.)
let start_y = bounds.min_y(); - (tree_branch.width / 2.);
let end_y = bounds.min_y() + baseline_offset - (cap_height / 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 { cx.scene.push_quad(gpui::Quad {
bounds: RectF::from_points( bounds: RectF::from_points(
vec2f(start_x, start_y), vec2f(start_x, start_y),
vec2f( vec2f(
start_x + tree_branch.width, start_x + tree_branch.width,
if is_last_project { if is_last_project {
end_y end_y
} else { } else {
bounds.max_y() 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()
}, },
), )
), .with_cursor_style(CursorStyle::PointingHand)
background: Some(tree_branch.color), .on_click(move |_, _, cx| {
border: gpui::Border::default(), cx.dispatch_action(ToggleProjectPublic {
corner_radius: 0., project: open_project.clone(),
}); })
cx.scene.push_quad(gpui::Quad { })
bounds: RectF::from_points( .boxed(),
vec2f(start_x, end_y), )
vec2f(end_x, end_y + tree_branch.width), } else {
), None
background: Some(tree_branch.color), })
border: gpui::Border::default(), .constrained()
corner_radius: 0., .with_width(host_avatar_height)
}); .boxed(),
})
.constrained()
.with_width(host_avatar_height)
.boxed(),
) )
.with_child( .with_child(
Label::new( Label::new(
@ -467,9 +500,9 @@ impl ContactsPanel {
MouseEventHandler::new::<LocalProject, _, _>(project_id, cx, |state, cx| { MouseEventHandler::new::<LocalProject, _, _>(project_id, cx, |state, cx| {
let row = theme.project_row.style_for(state, is_selected); let row = theme.project_row.style_for(state, is_selected);
let mut worktree_root_names = String::new(); let mut worktree_root_names = String::new();
let project = project.read(cx); let project_ = project.read(cx);
let is_public = project.is_public(); let is_public = project_.is_public();
for tree in project.visible_worktrees(cx) { for tree in project_.visible_worktrees(cx) {
if !worktree_root_names.is_empty() { if !worktree_root_names.is_empty() {
worktree_root_names.push_str(", "); worktree_root_names.push_str(", ");
} }
@ -498,7 +531,9 @@ impl ContactsPanel {
CursorStyle::PointingHand CursorStyle::PointingHand
}) })
.on_click(move |_, _, cx| { .on_click(move |_, _, cx| {
cx.dispatch_action(ToggleProjectPublic { project: None }) cx.dispatch_action(ToggleProjectPublic {
project: Some(project.clone()),
})
}) })
.boxed(), .boxed(),
) )

View file

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

View file

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

View file

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

View file

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

View file

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