From 6050e0ead7c103dfee1b941c2c06e7f9eb6cc77a Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 5 May 2022 09:58:18 -0600 Subject: [PATCH 01/53] Add fuzzy_search_users to Db trait, PostgresDb --- ...20505144506_add_trigram_index_to_users.sql | 2 + crates/collab/src/db.rs | 45 +++++++++++++++++++ 2 files changed, 47 insertions(+) create mode 100644 crates/collab/migrations/20220505144506_add_trigram_index_to_users.sql diff --git a/crates/collab/migrations/20220505144506_add_trigram_index_to_users.sql b/crates/collab/migrations/20220505144506_add_trigram_index_to_users.sql new file mode 100644 index 0000000000000000000000000000000000000000..3d6fd3179a236bf8407464f69f1e67469eb31d27 --- /dev/null +++ b/crates/collab/migrations/20220505144506_add_trigram_index_to_users.sql @@ -0,0 +1,2 @@ +CREATE EXTENSION IF NOT EXISTS pg_trgm; +CREATE INDEX trigram_index_users_on_github_login ON users USING GIN(github_login gin_trgm_ops); diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index 28375b6685f62cec951b0d34860ed5a920a2fed2..6356715eaef774aabfd19776a6c04bfb00c99857 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -10,6 +10,7 @@ use time::OffsetDateTime; pub trait Db: Send + Sync { async fn create_user(&self, github_login: &str, admin: bool) -> Result; async fn get_all_users(&self) -> Result>; + async fn fuzzy_search_users(&self, query: &str, limit: u32) -> Result>; async fn get_user_by_id(&self, id: UserId) -> Result>; async fn get_users_by_ids(&self, ids: Vec) -> Result>; async fn get_user_by_github_login(&self, github_login: &str) -> Result>; @@ -99,6 +100,21 @@ impl Db for PostgresDb { Ok(sqlx::query_as(query).fetch_all(&self.pool).await?) } + async fn fuzzy_search_users(&self, name_query: &str, limit: u32) -> Result> { + let query = " + SELECT users.* + FROM users + WHERE github_login % $1 + ORDER BY github_login <-> $1 + LIMIT $2 + "; + Ok(sqlx::query_as(query) + .bind(name_query) + .bind(limit) + .fetch_all(&self.pool) + .await?) + } + async fn get_user_by_id(&self, id: UserId) -> Result> { let users = self.get_users_by_ids(vec![id]).await?; Ok(users.into_iter().next()) @@ -640,6 +656,31 @@ pub mod tests { ); } + #[tokio::test(flavor = "multi_thread")] + async fn test_fuzzy_search_users() { + let test_db = TestDb::postgres().await; + let db = test_db.db(); + for github_login in [ + "nathansobo", + "nathansobot", + "nathanszabo", + "maxbrunsfeld", + "as-cii", + ] { + db.create_user(github_login, false).await.unwrap(); + } + + let results = db + .fuzzy_search_users("nathasbo", 10) + .await + .unwrap() + .into_iter() + .map(|user| user.github_login) + .collect::>(); + + assert_eq!(results, &["nathansobo", "nathanszabo", "nathansobot"]); + } + pub struct TestDb { pub db: Option>, pub url: String, @@ -749,6 +790,10 @@ pub mod tests { unimplemented!() } + async fn fuzzy_search_users(&self, _: &str, _: u32) -> Result> { + unimplemented!() + } + async fn get_user_by_id(&self, id: UserId) -> Result> { Ok(self.get_users_by_ids(vec![id]).await?.into_iter().next()) } From 342bdfc7e03a45dc3f3b02609002731def0a84ae Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 5 May 2022 10:24:21 -0600 Subject: [PATCH 02/53] Render a user query editor in the contacts panel --- Cargo.lock | 1 + assets/themes/cave-dark.json | 28 +++++++++++++++++++++ assets/themes/cave-light.json | 28 +++++++++++++++++++++ assets/themes/dark.json | 28 +++++++++++++++++++++ assets/themes/light.json | 28 +++++++++++++++++++++ assets/themes/solarized-dark.json | 28 +++++++++++++++++++++ assets/themes/solarized-light.json | 28 +++++++++++++++++++++ assets/themes/sulphurpool-dark.json | 28 +++++++++++++++++++++ assets/themes/sulphurpool-light.json | 28 +++++++++++++++++++++ crates/contacts_panel/Cargo.toml | 1 + crates/contacts_panel/src/contacts_panel.rs | 25 +++++++++++++++--- crates/theme/src/theme.rs | 1 + styles/src/styleTree/contactsPanel.ts | 16 +++++++++++- 13 files changed, 263 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6d7ce98341f0dab435d1148710f2134ebae582d2..0277e1b47680c2572d3ef95ea866c5974953404e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -930,6 +930,7 @@ name = "contacts_panel" version = "0.1.0" dependencies = [ "client", + "editor", "gpui", "postage", "settings", diff --git a/assets/themes/cave-dark.json b/assets/themes/cave-dark.json index 3b19c2e63eef855981632972fe933324dd677902..28d0e771ad46d3df2ff776a569a2059d7a09b8f9 100644 --- a/assets/themes/cave-dark.json +++ b/assets/themes/cave-dark.json @@ -1212,6 +1212,34 @@ "bottom": 12, "right": 12 }, + "user_query_editor": { + "background": "#19171c", + "corner_radius": 6, + "text": { + "family": "Zed Mono", + "color": "#e2dfe7", + "size": 14 + }, + "placeholder_text": { + "family": "Zed Mono", + "color": "#7e7887", + "size": 14 + }, + "selection": { + "cursor": "#576ddb", + "selection": "#576ddb3d" + }, + "border": { + "color": "#26232a", + "width": 1 + }, + "padding": { + "bottom": 7, + "left": 8, + "right": 8, + "top": 7 + } + }, "host_row_height": 28, "tree_branch_color": "#655f6d", "tree_branch_width": 1, diff --git a/assets/themes/cave-light.json b/assets/themes/cave-light.json index 2e33fb774fe95b06c4da7d8d7d6209c20673b592..505888f2b390742a14e732a890f8f7714801d833 100644 --- a/assets/themes/cave-light.json +++ b/assets/themes/cave-light.json @@ -1212,6 +1212,34 @@ "bottom": 12, "right": 12 }, + "user_query_editor": { + "background": "#efecf4", + "corner_radius": 6, + "text": { + "family": "Zed Mono", + "color": "#26232a", + "size": 14 + }, + "placeholder_text": { + "family": "Zed Mono", + "color": "#655f6d", + "size": 14 + }, + "selection": { + "cursor": "#576ddb", + "selection": "#576ddb3d" + }, + "border": { + "color": "#e2dfe7", + "width": 1 + }, + "padding": { + "bottom": 7, + "left": 8, + "right": 8, + "top": 7 + } + }, "host_row_height": 28, "tree_branch_color": "#7e7887", "tree_branch_width": 1, diff --git a/assets/themes/dark.json b/assets/themes/dark.json index ba9b7189d35fada8abcc2b42bace6c9f5a81fd19..060b8a12789de5068f5df2aeb2a25cdcb83b6d6c 100644 --- a/assets/themes/dark.json +++ b/assets/themes/dark.json @@ -1212,6 +1212,34 @@ "bottom": 12, "right": 12 }, + "user_query_editor": { + "background": "#000000", + "corner_radius": 6, + "text": { + "family": "Zed Mono", + "color": "#f1f1f1", + "size": 14 + }, + "placeholder_text": { + "family": "Zed Mono", + "color": "#474747", + "size": 14 + }, + "selection": { + "cursor": "#2472f2", + "selection": "#2472f23d" + }, + "border": { + "color": "#232323", + "width": 1 + }, + "padding": { + "bottom": 7, + "left": 8, + "right": 8, + "top": 7 + } + }, "host_row_height": 28, "tree_branch_color": "#404040", "tree_branch_width": 1, diff --git a/assets/themes/light.json b/assets/themes/light.json index 7cbd315c8a41ff3572cb414727618607ae3e8875..f15f232b92058fcb6b5ff7f36f4945013ab7d88b 100644 --- a/assets/themes/light.json +++ b/assets/themes/light.json @@ -1212,6 +1212,34 @@ "bottom": 12, "right": 12 }, + "user_query_editor": { + "background": "#ffffff", + "corner_radius": 6, + "text": { + "family": "Zed Mono", + "color": "#2b2b2b", + "size": 14 + }, + "placeholder_text": { + "family": "Zed Mono", + "color": "#808080", + "size": 14 + }, + "selection": { + "cursor": "#2472f2", + "selection": "#2472f23d" + }, + "border": { + "color": "#d5d5d5", + "width": 1 + }, + "padding": { + "bottom": 7, + "left": 8, + "right": 8, + "top": 7 + } + }, "host_row_height": 28, "tree_branch_color": "#e3e3e3", "tree_branch_width": 1, diff --git a/assets/themes/solarized-dark.json b/assets/themes/solarized-dark.json index 8672518b4cf3ef8a93cc9ad4793a6ba0106c2faa..f4e323edf9d1cae57356fe3805bf22a77109369b 100644 --- a/assets/themes/solarized-dark.json +++ b/assets/themes/solarized-dark.json @@ -1212,6 +1212,34 @@ "bottom": 12, "right": 12 }, + "user_query_editor": { + "background": "#002b36", + "corner_radius": 6, + "text": { + "family": "Zed Mono", + "color": "#eee8d5", + "size": 14 + }, + "placeholder_text": { + "family": "Zed Mono", + "color": "#839496", + "size": 14 + }, + "selection": { + "cursor": "#268bd2", + "selection": "#268bd23d" + }, + "border": { + "color": "#073642", + "width": 1 + }, + "padding": { + "bottom": 7, + "left": 8, + "right": 8, + "top": 7 + } + }, "host_row_height": 28, "tree_branch_color": "#657b83", "tree_branch_width": 1, diff --git a/assets/themes/solarized-light.json b/assets/themes/solarized-light.json index 66b43e613dc725687d3f34c3ab427d7a1311ae61..84eb135c9aa282b4c5e95ec473557ff7c82a7a92 100644 --- a/assets/themes/solarized-light.json +++ b/assets/themes/solarized-light.json @@ -1212,6 +1212,34 @@ "bottom": 12, "right": 12 }, + "user_query_editor": { + "background": "#fdf6e3", + "corner_radius": 6, + "text": { + "family": "Zed Mono", + "color": "#073642", + "size": 14 + }, + "placeholder_text": { + "family": "Zed Mono", + "color": "#657b83", + "size": 14 + }, + "selection": { + "cursor": "#268bd2", + "selection": "#268bd23d" + }, + "border": { + "color": "#eee8d5", + "width": 1 + }, + "padding": { + "bottom": 7, + "left": 8, + "right": 8, + "top": 7 + } + }, "host_row_height": 28, "tree_branch_color": "#839496", "tree_branch_width": 1, diff --git a/assets/themes/sulphurpool-dark.json b/assets/themes/sulphurpool-dark.json index 66f5182172e01e14902e28566600bbd7084de1de..63da853e8b887c072a1a2e67b21a5aae7f829d84 100644 --- a/assets/themes/sulphurpool-dark.json +++ b/assets/themes/sulphurpool-dark.json @@ -1212,6 +1212,34 @@ "bottom": 12, "right": 12 }, + "user_query_editor": { + "background": "#202746", + "corner_radius": 6, + "text": { + "family": "Zed Mono", + "color": "#dfe2f1", + "size": 14 + }, + "placeholder_text": { + "family": "Zed Mono", + "color": "#898ea4", + "size": 14 + }, + "selection": { + "cursor": "#3d8fd1", + "selection": "#3d8fd13d" + }, + "border": { + "color": "#293256", + "width": 1 + }, + "padding": { + "bottom": 7, + "left": 8, + "right": 8, + "top": 7 + } + }, "host_row_height": 28, "tree_branch_color": "#6b7394", "tree_branch_width": 1, diff --git a/assets/themes/sulphurpool-light.json b/assets/themes/sulphurpool-light.json index 34a33897288143e9e5c8492d5c753232ac1422b9..cc6bc705a07e3e616f275b172fd7f3a238d382a9 100644 --- a/assets/themes/sulphurpool-light.json +++ b/assets/themes/sulphurpool-light.json @@ -1212,6 +1212,34 @@ "bottom": 12, "right": 12 }, + "user_query_editor": { + "background": "#f5f7ff", + "corner_radius": 6, + "text": { + "family": "Zed Mono", + "color": "#293256", + "size": 14 + }, + "placeholder_text": { + "family": "Zed Mono", + "color": "#6b7394", + "size": 14 + }, + "selection": { + "cursor": "#3d8fd1", + "selection": "#3d8fd13d" + }, + "border": { + "color": "#dfe2f1", + "width": 1 + }, + "padding": { + "bottom": 7, + "left": 8, + "right": 8, + "top": 7 + } + }, "host_row_height": 28, "tree_branch_color": "#898ea4", "tree_branch_width": 1, diff --git a/crates/contacts_panel/Cargo.toml b/crates/contacts_panel/Cargo.toml index 6a4dbf653dbb1751ca1a41303cf2fec08fb6f09e..8e843b0f39941593c26a90daa6fc493eb29c7b15 100644 --- a/crates/contacts_panel/Cargo.toml +++ b/crates/contacts_panel/Cargo.toml @@ -9,6 +9,7 @@ doctest = false [dependencies] client = { path = "../client" } +editor = { path = "../editor" } gpui = { path = "../gpui" } settings = { path = "../settings" } theme = { path = "../theme" } diff --git a/crates/contacts_panel/src/contacts_panel.rs b/crates/contacts_panel/src/contacts_panel.rs index 171b4194960fc54f7c61b5d50e5e46d2a4b69c81..087f169da2f491ab41ed72c37d54cd271b933f54 100644 --- a/crates/contacts_panel/src/contacts_panel.rs +++ b/crates/contacts_panel/src/contacts_panel.rs @@ -1,10 +1,11 @@ use client::{Contact, UserStore}; +use editor::Editor; use gpui::{ elements::*, geometry::{rect::RectF, vector::vec2f}, platform::CursorStyle, Element, ElementBox, Entity, LayoutContext, ModelHandle, RenderContext, Subscription, View, - ViewContext, + ViewContext, ViewHandle, }; use settings::Settings; use std::sync::Arc; @@ -13,6 +14,7 @@ use workspace::{AppState, JoinProject}; pub struct ContactsPanel { contacts: ListState, user_store: ModelHandle, + user_query_editor: ViewHandle, _maintain_contacts: Subscription, } @@ -38,6 +40,12 @@ impl ContactsPanel { } }, ), + user_query_editor: cx.add_view(|cx| { + Editor::single_line( + Some(|theme| theme.contacts_panel.user_query_editor.clone()), + cx, + ) + }), _maintain_contacts: cx.observe(&app_state.user_store, Self::update_contacts), user_store: app_state.user_store.clone(), } @@ -237,8 +245,17 @@ impl View for ContactsPanel { fn render(&mut self, cx: &mut RenderContext) -> ElementBox { let theme = &cx.global::().theme.contacts_panel; - Container::new(List::new(self.contacts.clone()).boxed()) - .with_style(theme.container) - .boxed() + Container::new( + Flex::column() + .with_child( + Container::new(ChildView::new(self.user_query_editor.clone()).boxed()) + .with_style(theme.user_query_editor.container) + .boxed(), + ) + .with_child(List::new(self.contacts.clone()).flex(1., false).boxed()) + .boxed(), + ) + .with_style(theme.container) + .boxed() } } diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index d64c093144d752c1cb6a88105c3bd7cfea18be7c..d9fa771c433cb358aec0486e48a359b9eb52b0ba 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -234,6 +234,7 @@ pub struct CommandPalette { pub struct ContactsPanel { #[serde(flatten)] pub container: ContainerStyle, + pub user_query_editor: FieldEditor, pub host_row_height: f32, pub host_avatar: ImageStyle, pub host_username: ContainedText, diff --git a/styles/src/styleTree/contactsPanel.ts b/styles/src/styleTree/contactsPanel.ts index e22a09e25f5db3c3ab5fb027b563d834913a3180..23ad7926454f09358f51035303ae5829e25aa1cd 100644 --- a/styles/src/styleTree/contactsPanel.ts +++ b/styles/src/styleTree/contactsPanel.ts @@ -1,6 +1,6 @@ import Theme from "../themes/theme"; import { panel } from "./app"; -import { backgroundColor, borderColor, text } from "./components"; +import { backgroundColor, border, borderColor, player, text } from "./components"; export default function(theme: Theme) { const project = { @@ -33,6 +33,20 @@ export default function(theme: Theme) { return { ...panel, + userQueryEditor: { + background: backgroundColor(theme, 500), + cornerRadius: 6, + text: text(theme, "mono", "primary"), + placeholderText: text(theme, "mono", "placeholder", { size: "sm" }), + selection: player(theme, 1).selection, + border: border(theme, "secondary"), + padding: { + bottom: 7, + left: 8, + right: 8, + top: 7, + }, + }, hostRowHeight: 28, treeBranchColor: borderColor(theme, "muted"), treeBranchWidth: 1, From 35fea43089beb625e529adb9ba66dd2d46eaf5dd Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 5 May 2022 12:43:38 -0700 Subject: [PATCH 03/53] Adjust fuzzy search to avoid filtering based on edit distance threshold --- crates/collab/src/db.rs | 63 ++++++++++++++++++++++++++++++----------- 1 file changed, 47 insertions(+), 16 deletions(-) diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index 6356715eaef774aabfd19776a6c04bfb00c99857..3eb6fd0240ebe32c09d341ba60dc037c7ae3ab03 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -101,14 +101,16 @@ impl Db for PostgresDb { } async fn fuzzy_search_users(&self, name_query: &str, limit: u32) -> Result> { + let like_string = fuzzy_like_string(name_query); let query = " SELECT users.* FROM users - WHERE github_login % $1 - ORDER BY github_login <-> $1 - LIMIT $2 + WHERE github_login like $1 + ORDER BY github_login <-> $2 + LIMIT $3 "; Ok(sqlx::query_as(query) + .bind(like_string) .bind(name_query) .bind(limit) .fetch_all(&self.pool) @@ -492,6 +494,18 @@ pub struct ChannelMessage { pub nonce: Uuid, } +fn fuzzy_like_string(string: &str) -> String { + let mut result = String::with_capacity(string.len() * 2 + 1); + for c in string.chars() { + if c.is_alphanumeric() { + result.push('%'); + result.push(c); + } + } + result.push('%'); + result +} + #[cfg(test)] pub mod tests { use super::*; @@ -656,29 +670,46 @@ pub mod tests { ); } + #[test] + fn test_fuzzy_like_string() { + assert_eq!(fuzzy_like_string("abcd"), "%a%b%c%d%"); + assert_eq!(fuzzy_like_string("x y"), "%x%y%"); + assert_eq!(fuzzy_like_string(" z "), "%z%"); + } + #[tokio::test(flavor = "multi_thread")] async fn test_fuzzy_search_users() { let test_db = TestDb::postgres().await; let db = test_db.db(); for github_login in [ - "nathansobo", - "nathansobot", - "nathanszabo", - "maxbrunsfeld", - "as-cii", + "california", + "colorado", + "oregon", + "washington", + "florida", + "delaware", + "rhode-island", ] { db.create_user(github_login, false).await.unwrap(); } - let results = db - .fuzzy_search_users("nathasbo", 10) - .await - .unwrap() - .into_iter() - .map(|user| user.github_login) - .collect::>(); + assert_eq!( + fuzzy_search_user_names(db, "clr").await, + &["colorado", "california"] + ); + assert_eq!( + fuzzy_search_user_names(db, "ro").await, + &["rhode-island", "colorado", "oregon"], + ); - assert_eq!(results, &["nathansobo", "nathanszabo", "nathansobot"]); + async fn fuzzy_search_user_names(db: &Arc, query: &str) -> Vec { + db.fuzzy_search_users(query, 10) + .await + .unwrap() + .into_iter() + .map(|user| user.github_login) + .collect::>() + } } pub struct TestDb { From ea81737a8813fe9474de11d05f37c9bd863a36a2 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 5 May 2022 14:14:44 -0700 Subject: [PATCH 04/53] Allow fuzzy-search for potential contacts in the contacts panel Co-authored-by: Nathan Sobo --- Cargo.lock | 1 + assets/themes/cave-dark.json | 11 +- assets/themes/cave-light.json | 11 +- assets/themes/dark.json | 11 +- assets/themes/light.json | 11 +- assets/themes/solarized-dark.json | 11 +- assets/themes/solarized-light.json | 11 +- assets/themes/sulphurpool-dark.json | 11 +- assets/themes/sulphurpool-light.json | 11 +- crates/client/src/channel.rs | 8 +- crates/client/src/user.rs | 85 +++-- crates/collab/src/rpc.rs | 31 +- crates/contacts_panel/Cargo.toml | 1 + crates/contacts_panel/src/contacts_panel.rs | 403 ++++++++++++-------- crates/project/src/project.rs | 2 +- crates/rpc/proto/zed.proto | 17 +- crates/rpc/src/proto.rs | 6 +- crates/theme/src/theme.rs | 17 +- styles/src/styleTree/contactsPanel.ts | 12 +- 19 files changed, 435 insertions(+), 236 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0277e1b47680c2572d3ef95ea866c5974953404e..1e85073d501a07cf5534dd78003ea797784aa56b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -935,6 +935,7 @@ dependencies = [ "postage", "settings", "theme", + "util", "workspace", ] diff --git a/assets/themes/cave-dark.json b/assets/themes/cave-dark.json index 28d0e771ad46d3df2ff776a569a2059d7a09b8f9..a2e224ed028dab93ce616f56547f04a3c6832b36 100644 --- a/assets/themes/cave-dark.json +++ b/assets/themes/cave-dark.json @@ -1240,14 +1240,14 @@ "top": 7 } }, - "host_row_height": 28, + "row_height": 28, "tree_branch_color": "#655f6d", "tree_branch_width": 1, - "host_avatar": { + "contact_avatar": { "corner_radius": 10, "width": 18 }, - "host_username": { + "contact_username": { "family": "Zed Mono", "color": "#e2dfe7", "size": 14, @@ -1255,6 +1255,11 @@ "left": 8 } }, + "header": { + "family": "Zed Mono", + "color": "#8b8792", + "size": 14 + }, "project": { "guest_avatar_spacing": 4, "height": 24, diff --git a/assets/themes/cave-light.json b/assets/themes/cave-light.json index 505888f2b390742a14e732a890f8f7714801d833..6ec03294ec841481e0166a8cbe27c904859ac6a4 100644 --- a/assets/themes/cave-light.json +++ b/assets/themes/cave-light.json @@ -1240,14 +1240,14 @@ "top": 7 } }, - "host_row_height": 28, + "row_height": 28, "tree_branch_color": "#7e7887", "tree_branch_width": 1, - "host_avatar": { + "contact_avatar": { "corner_radius": 10, "width": 18 }, - "host_username": { + "contact_username": { "family": "Zed Mono", "color": "#26232a", "size": 14, @@ -1255,6 +1255,11 @@ "left": 8 } }, + "header": { + "family": "Zed Mono", + "color": "#585260", + "size": 14 + }, "project": { "guest_avatar_spacing": 4, "height": 24, diff --git a/assets/themes/dark.json b/assets/themes/dark.json index 060b8a12789de5068f5df2aeb2a25cdcb83b6d6c..db72fd965cdfba2794a5a1c10d609755f946548f 100644 --- a/assets/themes/dark.json +++ b/assets/themes/dark.json @@ -1240,14 +1240,14 @@ "top": 7 } }, - "host_row_height": 28, + "row_height": 28, "tree_branch_color": "#404040", "tree_branch_width": 1, - "host_avatar": { + "contact_avatar": { "corner_radius": 10, "width": 18 }, - "host_username": { + "contact_username": { "family": "Zed Mono", "color": "#f1f1f1", "size": 14, @@ -1255,6 +1255,11 @@ "left": 8 } }, + "header": { + "family": "Zed Mono", + "color": "#9c9c9c", + "size": 14 + }, "project": { "guest_avatar_spacing": 4, "height": 24, diff --git a/assets/themes/light.json b/assets/themes/light.json index f15f232b92058fcb6b5ff7f36f4945013ab7d88b..f8754b7ede1cf33c179ab49cebba4b96aa27dbc6 100644 --- a/assets/themes/light.json +++ b/assets/themes/light.json @@ -1240,14 +1240,14 @@ "top": 7 } }, - "host_row_height": 28, + "row_height": 28, "tree_branch_color": "#e3e3e3", "tree_branch_width": 1, - "host_avatar": { + "contact_avatar": { "corner_radius": 10, "width": 18 }, - "host_username": { + "contact_username": { "family": "Zed Mono", "color": "#2b2b2b", "size": 14, @@ -1255,6 +1255,11 @@ "left": 8 } }, + "header": { + "family": "Zed Mono", + "color": "#474747", + "size": 14 + }, "project": { "guest_avatar_spacing": 4, "height": 24, diff --git a/assets/themes/solarized-dark.json b/assets/themes/solarized-dark.json index f4e323edf9d1cae57356fe3805bf22a77109369b..c6c9951779b601e9c763abf1d23021e27fcfc046 100644 --- a/assets/themes/solarized-dark.json +++ b/assets/themes/solarized-dark.json @@ -1240,14 +1240,14 @@ "top": 7 } }, - "host_row_height": 28, + "row_height": 28, "tree_branch_color": "#657b83", "tree_branch_width": 1, - "host_avatar": { + "contact_avatar": { "corner_radius": 10, "width": 18 }, - "host_username": { + "contact_username": { "family": "Zed Mono", "color": "#eee8d5", "size": 14, @@ -1255,6 +1255,11 @@ "left": 8 } }, + "header": { + "family": "Zed Mono", + "color": "#93a1a1", + "size": 14 + }, "project": { "guest_avatar_spacing": 4, "height": 24, diff --git a/assets/themes/solarized-light.json b/assets/themes/solarized-light.json index 84eb135c9aa282b4c5e95ec473557ff7c82a7a92..677bba00703e6e772ebde4cebab18bb9c201f6ef 100644 --- a/assets/themes/solarized-light.json +++ b/assets/themes/solarized-light.json @@ -1240,14 +1240,14 @@ "top": 7 } }, - "host_row_height": 28, + "row_height": 28, "tree_branch_color": "#839496", "tree_branch_width": 1, - "host_avatar": { + "contact_avatar": { "corner_radius": 10, "width": 18 }, - "host_username": { + "contact_username": { "family": "Zed Mono", "color": "#073642", "size": 14, @@ -1255,6 +1255,11 @@ "left": 8 } }, + "header": { + "family": "Zed Mono", + "color": "#586e75", + "size": 14 + }, "project": { "guest_avatar_spacing": 4, "height": 24, diff --git a/assets/themes/sulphurpool-dark.json b/assets/themes/sulphurpool-dark.json index 63da853e8b887c072a1a2e67b21a5aae7f829d84..b603dccb8d100cf56269df1fa4dacde9d77798b4 100644 --- a/assets/themes/sulphurpool-dark.json +++ b/assets/themes/sulphurpool-dark.json @@ -1240,14 +1240,14 @@ "top": 7 } }, - "host_row_height": 28, + "row_height": 28, "tree_branch_color": "#6b7394", "tree_branch_width": 1, - "host_avatar": { + "contact_avatar": { "corner_radius": 10, "width": 18 }, - "host_username": { + "contact_username": { "family": "Zed Mono", "color": "#dfe2f1", "size": 14, @@ -1255,6 +1255,11 @@ "left": 8 } }, + "header": { + "family": "Zed Mono", + "color": "#979db4", + "size": 14 + }, "project": { "guest_avatar_spacing": 4, "height": 24, diff --git a/assets/themes/sulphurpool-light.json b/assets/themes/sulphurpool-light.json index cc6bc705a07e3e616f275b172fd7f3a238d382a9..3f4983c083f32502184d87d1b23fb651b1e3af87 100644 --- a/assets/themes/sulphurpool-light.json +++ b/assets/themes/sulphurpool-light.json @@ -1240,14 +1240,14 @@ "top": 7 } }, - "host_row_height": 28, + "row_height": 28, "tree_branch_color": "#898ea4", "tree_branch_width": 1, - "host_avatar": { + "contact_avatar": { "corner_radius": 10, "width": 18 }, - "host_username": { + "contact_username": { "family": "Zed Mono", "color": "#293256", "size": 14, @@ -1255,6 +1255,11 @@ "left": 8 } }, + "header": { + "family": "Zed Mono", + "color": "#5e6687", + "size": 14 + }, "project": { "guest_avatar_spacing": 4, "height": 24, diff --git a/crates/client/src/channel.rs b/crates/client/src/channel.rs index 51d4007ff75dc546008dbc8e1ca8ac33e9d2ff5c..0d44c6719139d3b4ed1344295841480f6df2f1f9 100644 --- a/crates/client/src/channel.rs +++ b/crates/client/src/channel.rs @@ -500,7 +500,7 @@ async fn messages_from_proto( .collect(); user_store .update(cx, |user_store, cx| { - user_store.load_users(unique_user_ids, cx) + user_store.get_users(unique_user_ids, cx) }) .await?; @@ -639,7 +639,7 @@ mod tests { server .respond( get_users.receipt(), - proto::GetUsersResponse { + proto::UsersResponse { users: vec![proto::User { id: 5, github_login: "nathansobo".into(), @@ -690,7 +690,7 @@ mod tests { server .respond( get_users.receipt(), - proto::GetUsersResponse { + proto::UsersResponse { users: vec![proto::User { id: 6, github_login: "maxbrunsfeld".into(), @@ -738,7 +738,7 @@ mod tests { server .respond( get_users.receipt(), - proto::GetUsersResponse { + proto::UsersResponse { users: vec![proto::User { id: 7, github_login: "as-cii".into(), diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index c8432d92d1ff38c66550ee69b997e0cc2748d8af..a28acdc63c573dfba9af6ed1ab239e11f7b4a7c4 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -3,6 +3,7 @@ use anyhow::{anyhow, Result}; use futures::{future, AsyncReadExt}; use gpui::{AsyncAppContext, Entity, ImageData, ModelContext, ModelHandle, Task}; use postage::{prelude::Stream, sink::Sink, watch}; +use rpc::proto::{RequestMessage, UsersResponse}; use std::{ collections::{HashMap, HashSet}, sync::{Arc, Weak}, @@ -121,7 +122,7 @@ impl UserStore { user_ids.extend(contact.projects.iter().flat_map(|w| &w.guests).copied()); } - let load_users = self.load_users(user_ids.into_iter().collect(), cx); + let load_users = self.get_users(user_ids.into_iter().collect(), cx); cx.spawn(|this, mut cx| async move { load_users.await?; @@ -144,37 +145,27 @@ impl UserStore { &self.contacts } - pub fn load_users( + pub fn has_contact(&self, user: &Arc) -> bool { + self.contacts + .binary_search_by_key(&&user.github_login, |contact| &contact.user.github_login) + .is_ok() + } + + pub fn get_users( &mut self, mut user_ids: Vec, cx: &mut ModelContext, - ) -> Task> { - let rpc = self.client.clone(); - let http = self.http.clone(); + ) -> Task>>> { user_ids.retain(|id| !self.users.contains_key(id)); - cx.spawn_weak(|this, mut cx| async move { - if let Some(rpc) = rpc.upgrade() { - if !user_ids.is_empty() { - let response = rpc.request(proto::GetUsers { user_ids }).await?; - let new_users = future::join_all( - response - .users - .into_iter() - .map(|user| User::new(user, http.as_ref())), - ) - .await; + self.load_users(proto::GetUsers { user_ids }, cx) + } - if let Some(this) = this.upgrade(&cx) { - this.update(&mut cx, |this, _| { - for user in new_users { - this.users.insert(user.id, Arc::new(user)); - } - }); - } - } - } - Ok(()) - }) + pub fn fuzzy_search_users( + &mut self, + query: String, + cx: &mut ModelContext, + ) -> Task>>> { + self.load_users(proto::FuzzySearchUsers { query }, cx) } pub fn fetch_user( @@ -186,7 +177,7 @@ impl UserStore { return cx.foreground().spawn(async move { Ok(user) }); } - let load_users = self.load_users(vec![user_id], cx); + let load_users = self.get_users(vec![user_id], cx); cx.spawn(|this, mut cx| async move { load_users.await?; this.update(&mut cx, |this, _| { @@ -205,15 +196,47 @@ impl UserStore { pub fn watch_current_user(&self) -> watch::Receiver>> { self.current_user.clone() } + + fn load_users( + &mut self, + request: impl RequestMessage, + cx: &mut ModelContext, + ) -> Task>>> { + let client = self.client.clone(); + let http = self.http.clone(); + cx.spawn_weak(|this, mut cx| async move { + if let Some(rpc) = client.upgrade() { + let response = rpc.request(request).await?; + let users = future::join_all( + response + .users + .into_iter() + .map(|user| User::new(user, http.as_ref())), + ) + .await; + + if let Some(this) = this.upgrade(&cx) { + this.update(&mut cx, |this, _| { + for user in &users { + this.users.insert(user.id, user.clone()); + } + }); + } + Ok(users) + } else { + Ok(Vec::new()) + } + }) + } } impl User { - async fn new(message: proto::User, http: &dyn HttpClient) -> Self { - User { + async fn new(message: proto::User, http: &dyn HttpClient) -> Arc { + Arc::new(User { id: message.id, github_login: message.github_login, avatar: fetch_avatar(http, &message.avatar_url).warn_on_err().await, - } + }) } } diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 827ac564f827746700ea6456f35bd54289e64fe2..46ca033ee2ae43a289c35b3146f17161ed864e9a 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -136,6 +136,7 @@ impl Server { .add_request_handler(Server::save_buffer) .add_request_handler(Server::get_channels) .add_request_handler(Server::get_users) + .add_request_handler(Server::fuzzy_search_users) .add_request_handler(Server::join_channel) .add_message_handler(Server::leave_channel) .add_request_handler(Server::send_channel_message) @@ -842,7 +843,7 @@ impl Server { async fn get_users( self: Arc, request: TypedEnvelope, - ) -> Result { + ) -> Result { let user_ids = request .payload .user_ids @@ -861,7 +862,33 @@ impl Server { github_login: user.github_login, }) .collect(); - Ok(proto::GetUsersResponse { users }) + Ok(proto::UsersResponse { users }) + } + + async fn fuzzy_search_users( + self: Arc, + request: TypedEnvelope, + ) -> Result { + let query = request.payload.query; + let db = &self.app_state.db; + let users = match query.len() { + 0 => vec![], + 1 | 2 => db + .get_user_by_github_login(&query) + .await? + .into_iter() + .collect(), + _ => db.fuzzy_search_users(&query, 10).await?, + }; + let users = users + .into_iter() + .map(|user| proto::User { + id: user.id.to_proto(), + avatar_url: format!("https://github.com/{}.png?size=128", user.github_login), + github_login: user.github_login, + }) + .collect(); + Ok(proto::UsersResponse { users }) } #[instrument(skip(self, state, user_ids))] diff --git a/crates/contacts_panel/Cargo.toml b/crates/contacts_panel/Cargo.toml index 8e843b0f39941593c26a90daa6fc493eb29c7b15..24ee22ed21f563f01c6095aaad2fbdcf1162f498 100644 --- a/crates/contacts_panel/Cargo.toml +++ b/crates/contacts_panel/Cargo.toml @@ -13,5 +13,6 @@ editor = { path = "../editor" } gpui = { path = "../gpui" } settings = { path = "../settings" } theme = { path = "../theme" } +util = { path = "../util" } workspace = { path = "../workspace" } postage = { version = "0.4.1", features = ["futures-traits"] } diff --git a/crates/contacts_panel/src/contacts_panel.rs b/crates/contacts_panel/src/contacts_panel.rs index 087f169da2f491ab41ed72c37d54cd271b933f54..d9460f265e1f7ab70bc7f307d47ba60b29b03519 100644 --- a/crates/contacts_panel/src/contacts_panel.rs +++ b/crates/contacts_panel/src/contacts_panel.rs @@ -1,71 +1,124 @@ -use client::{Contact, UserStore}; +use client::{Contact, User, UserStore}; use editor::Editor; use gpui::{ elements::*, geometry::{rect::RectF, vector::vec2f}, platform::CursorStyle, - Element, ElementBox, Entity, LayoutContext, ModelHandle, RenderContext, Subscription, View, - ViewContext, ViewHandle, + Element, ElementBox, Entity, LayoutContext, ModelHandle, RenderContext, Subscription, Task, + View, ViewContext, ViewHandle, }; use settings::Settings; use std::sync::Arc; +use util::ResultExt; use workspace::{AppState, JoinProject}; pub struct ContactsPanel { - contacts: ListState, + list_state: ListState, + potential_contacts: Vec>, user_store: ModelHandle, + contacts_search_task: Option>>, user_query_editor: ViewHandle, _maintain_contacts: Subscription, } impl ContactsPanel { pub fn new(app_state: Arc, cx: &mut ViewContext) -> Self { + let user_query_editor = cx.add_view(|cx| { + Editor::single_line( + Some(|theme| theme.contacts_panel.user_query_editor.clone()), + cx, + ) + }); + + cx.subscribe(&user_query_editor, |this, _, event, cx| { + if let editor::Event::BufferEdited = event { + this.user_query_changed(cx) + } + }) + .detach(); + Self { - contacts: ListState::new( - app_state.user_store.read(cx).contacts().len(), + list_state: ListState::new( + 1 + app_state.user_store.read(cx).contacts().len(), // Add 1 for the "Contacts" header Orientation::Top, 1000., { + let this = cx.weak_handle(); let app_state = app_state.clone(); move |ix, cx| { - let user_store = app_state.user_store.read(cx); + let this = this.upgrade(cx).unwrap(); + let this = this.read(cx); + let user_store = this.user_store.read(cx); let contacts = user_store.contacts().clone(); let current_user_id = user_store.current_user().map(|user| user.id); - Self::render_collaborator( - &contacts[ix], - current_user_id, - app_state.clone(), - cx, - ) + let theme = cx.global::().theme.clone(); + let theme = &theme.contacts_panel; + + if ix == 0 { + Label::new("contacts".to_string(), theme.header.text.clone()) + .contained() + .with_style(theme.header.container) + .aligned() + .left() + .constrained() + .with_height(theme.row_height) + .boxed() + } else if ix < contacts.len() + 1 { + let contact_ix = ix - 1; + Self::render_contact( + &contacts[contact_ix], + current_user_id, + app_state.clone(), + theme, + cx, + ) + } else if ix == contacts.len() + 1 { + Label::new("add contacts".to_string(), theme.header.text.clone()) + .contained() + .with_style(theme.header.container) + .aligned() + .left() + .constrained() + .with_height(theme.row_height) + .boxed() + } else { + let potential_contact_ix = ix - 2 - contacts.len(); + Self::render_potential_contact( + &this.potential_contacts[potential_contact_ix], + theme, + ) + } } }, ), - user_query_editor: cx.add_view(|cx| { - Editor::single_line( - Some(|theme| theme.contacts_panel.user_query_editor.clone()), - cx, - ) + potential_contacts: Default::default(), + user_query_editor, + _maintain_contacts: cx.observe(&app_state.user_store, |this, _, cx| { + this.update_contacts(cx) }), - _maintain_contacts: cx.observe(&app_state.user_store, Self::update_contacts), + contacts_search_task: None, user_store: app_state.user_store.clone(), } } - fn update_contacts(&mut self, _: ModelHandle, cx: &mut ViewContext) { - self.contacts - .reset(self.user_store.read(cx).contacts().len()); + fn update_contacts(&mut self, cx: &mut ViewContext) { + let mut list_len = 1 + self.user_store.read(cx).contacts().len(); + if !self.potential_contacts.is_empty() { + list_len += 1 + self.potential_contacts.len(); + } + + self.list_state.reset(list_len); cx.notify(); } - fn render_collaborator( - collaborator: &Contact, + fn render_contact( + contact: &Contact, current_user_id: Option, app_state: Arc, + theme: &theme::ContactsPanel, cx: &mut LayoutContext, ) -> ElementBox { - let theme = cx.global::().theme.clone(); - let theme = &theme.contacts_panel; - let project_count = collaborator.projects.len(); + let project_count = contact.projects.len(); let font_cache = cx.font_cache(); let line_height = theme.unshared_project.name.text.line_height(font_cache); let cap_height = theme.unshared_project.name.text.cap_height(font_cache); @@ -74,162 +127,202 @@ impl ContactsPanel { let tree_branch_width = theme.tree_branch_width; let tree_branch_color = theme.tree_branch_color; let host_avatar_height = theme - .host_avatar + .contact_avatar .width - .or(theme.host_avatar.height) + .or(theme.contact_avatar.height) .unwrap_or(0.); Flex::column() .with_child( Flex::row() - .with_children(collaborator.user.avatar.clone().map(|avatar| { + .with_children(contact.user.avatar.clone().map(|avatar| { Image::new(avatar) - .with_style(theme.host_avatar) + .with_style(theme.contact_avatar) .aligned() .left() .boxed() })) .with_child( Label::new( - collaborator.user.github_login.clone(), - theme.host_username.text.clone(), + contact.user.github_login.clone(), + theme.contact_username.text.clone(), ) .contained() - .with_style(theme.host_username.container) + .with_style(theme.contact_username.container) .aligned() .left() .boxed(), ) .constrained() - .with_height(theme.host_row_height) + .with_height(theme.row_height) .boxed(), ) - .with_children( - collaborator - .projects - .iter() - .enumerate() - .map(|(ix, project)| { - let project_id = project.id; + .with_children(contact.projects.iter().enumerate().map(|(ix, project)| { + let project_id = project.id; - 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.); + 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 ix + 1 == project_count { - 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., - }); - }) - .constrained() - .with_width(host_avatar_height) - .boxed(), - ) - .with_child({ - let is_host = Some(collaborator.user.id) == current_user_id; - let is_guest = !is_host - && project - .guests - .iter() - .any(|guest| Some(guest.id) == current_user_id); - let is_shared = project.is_shared; - let app_state = app_state.clone(); + cx.scene.push_quad(gpui::Quad { + bounds: RectF::from_points( + vec2f(start_x, start_y), + vec2f( + start_x + tree_branch_width, + if ix + 1 == project_count { + 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., + }); + }) + .constrained() + .with_width(host_avatar_height) + .boxed(), + ) + .with_child({ + let is_host = Some(contact.user.id) == current_user_id; + let is_guest = !is_host + && project + .guests + .iter() + .any(|guest| Some(guest.id) == current_user_id); + let is_shared = project.is_shared; + let app_state = app_state.clone(); - MouseEventHandler::new::( - project_id as usize, - cx, - |mouse_state, _| { - let style = match (project.is_shared, mouse_state.hovered) { - (false, false) => &theme.unshared_project, - (false, true) => &theme.hovered_unshared_project, - (true, false) => &theme.shared_project, - (true, true) => &theme.hovered_shared_project, - }; + MouseEventHandler::new::( + project_id as usize, + cx, + |mouse_state, _| { + let style = match (project.is_shared, mouse_state.hovered) { + (false, false) => &theme.unshared_project, + (false, true) => &theme.hovered_unshared_project, + (true, false) => &theme.shared_project, + (true, true) => &theme.hovered_shared_project, + }; - Flex::row() - .with_child( - Label::new( - project.worktree_root_names.join(", "), - style.name.text.clone(), - ) - .aligned() - .left() - .contained() - .with_style(style.name.container) - .boxed(), - ) - .with_children(project.guests.iter().filter_map( - |participant| { - participant.avatar.clone().map(|avatar| { - Image::new(avatar) - .with_style(style.guest_avatar) - .aligned() - .left() - .contained() - .with_margin_right( - style.guest_avatar_spacing, - ) - .boxed() - }) - }, - )) - .contained() - .with_style(style.container) - .constrained() - .with_height(style.height) - .boxed() - }, - ) - .with_cursor_style(if is_host || is_shared { - CursorStyle::PointingHand - } else { - CursorStyle::Arrow - }) - .on_click(move |_, cx| { - if !is_host && !is_guest { - cx.dispatch_global_action(JoinProject { - project_id, - app_state: app_state.clone(), - }); - } - }) - .flex(1., true) - .boxed() - }) - .constrained() - .with_height(theme.unshared_project.height) - .boxed() - }), + Flex::row() + .with_child( + Label::new( + project.worktree_root_names.join(", "), + style.name.text.clone(), + ) + .aligned() + .left() + .contained() + .with_style(style.name.container) + .boxed(), + ) + .with_children(project.guests.iter().filter_map( + |participant| { + participant.avatar.clone().map(|avatar| { + Image::new(avatar) + .with_style(style.guest_avatar) + .aligned() + .left() + .contained() + .with_margin_right(style.guest_avatar_spacing) + .boxed() + }) + }, + )) + .contained() + .with_style(style.container) + .constrained() + .with_height(style.height) + .boxed() + }, + ) + .with_cursor_style(if is_host || is_shared { + CursorStyle::PointingHand + } else { + CursorStyle::Arrow + }) + .on_click(move |_, cx| { + if !is_host && !is_guest { + cx.dispatch_global_action(JoinProject { + project_id, + app_state: app_state.clone(), + }); + } + }) + .flex(1., true) + .boxed() + }) + .constrained() + .with_height(theme.unshared_project.height) + .boxed() + })) + .boxed() + } + + fn render_potential_contact(contact: &User, theme: &theme::ContactsPanel) -> ElementBox { + Flex::row() + .with_children(contact.avatar.clone().map(|avatar| { + Image::new(avatar) + .with_style(theme.contact_avatar) + .aligned() + .left() + .boxed() + })) + .with_child( + Label::new( + contact.github_login.clone(), + theme.contact_username.text.clone(), + ) + .contained() + .with_style(theme.contact_username.container) + .aligned() + .left() + .boxed(), ) + .constrained() + .with_height(theme.row_height) .boxed() } + + fn user_query_changed(&mut self, cx: &mut ViewContext) { + let query = self.user_query_editor.read(cx).text(cx); + if query.is_empty() { + self.potential_contacts.clear(); + self.update_contacts(cx); + return; + } + + let search = self + .user_store + .update(cx, |store, cx| store.fuzzy_search_users(query, cx)); + self.contacts_search_task = Some(cx.spawn(|this, mut cx| async move { + let users = search.await.log_err()?; + this.update(&mut cx, |this, cx| { + let user_store = this.user_store.read(cx); + this.potential_contacts = users; + this.potential_contacts + .retain(|user| !user_store.has_contact(&user)); + this.update_contacts(cx); + }); + None + })); + } } pub enum Event {} @@ -252,7 +345,7 @@ impl View for ContactsPanel { .with_style(theme.user_query_editor.container) .boxed(), ) - .with_child(List::new(self.contacts.clone()).flex(1., false).boxed()) + .with_child(List::new(self.list_state.clone()).flex(1., false).boxed()) .boxed(), ) .with_style(theme.container) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 1fcd89fcde90bee62b0a3831b4e2df3d491ed956..b54c3677fa34096171a5c0d81ef72a72a3f7c6ec 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -443,7 +443,7 @@ impl Project { .map(|peer| peer.user_id) .collect(); user_store - .update(cx, |user_store, cx| user_store.load_users(user_ids, cx)) + .update(cx, |user_store, cx| user_store.get_users(user_ids, cx)) .await?; let mut collaborators = HashMap::default(); for message in response.collaborators { diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index fa0b587df486957d65b87c97caaf5b9a3eb5ed8f..267dcb0ba99be66bed2967592600fc8474c1f44c 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -87,12 +87,13 @@ message Envelope { UpdateContacts update_contacts = 75; GetUsers get_users = 76; - GetUsersResponse get_users_response = 77; + FuzzySearchUsers fuzzy_search_users = 77; + UsersResponse users_response = 78; - Follow follow = 78; - FollowResponse follow_response = 79; - UpdateFollowers update_followers = 80; - Unfollow unfollow = 81; + Follow follow = 79; + FollowResponse follow_response = 80; + UpdateFollowers update_followers = 81; + Unfollow unfollow = 82; } } @@ -538,7 +539,11 @@ message GetUsers { repeated uint64 user_ids = 1; } -message GetUsersResponse { +message FuzzySearchUsers { + string query = 1; +} + +message UsersResponse { repeated User users = 1; } diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index c505869c554744e1c89911b0a9c5f556ac3dd8b0..0935dc6265fbd19cb17f6f57ca1466a80b589122 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -155,6 +155,7 @@ messages!( (FollowResponse, Foreground), (FormatBuffers, Foreground), (FormatBuffersResponse, Foreground), + (FuzzySearchUsers, Foreground), (GetChannelMessages, Foreground), (GetChannelMessagesResponse, Foreground), (GetChannels, Foreground), @@ -172,7 +173,7 @@ messages!( (GetProjectSymbols, Background), (GetProjectSymbolsResponse, Background), (GetUsers, Foreground), - (GetUsersResponse, Foreground), + (UsersResponse, Foreground), (JoinChannel, Foreground), (JoinChannelResponse, Foreground), (JoinProject, Foreground), @@ -236,7 +237,8 @@ request_messages!( (GetDocumentHighlights, GetDocumentHighlightsResponse), (GetReferences, GetReferencesResponse), (GetProjectSymbols, GetProjectSymbolsResponse), - (GetUsers, GetUsersResponse), + (FuzzySearchUsers, UsersResponse), + (GetUsers, UsersResponse), (JoinChannel, JoinChannelResponse), (JoinProject, JoinProjectResponse), (OpenBufferById, OpenBufferResponse), diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index d9fa771c433cb358aec0486e48a359b9eb52b0ba..241cbc86738c30c11a34d0e88995ed95b2df83bb 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -234,20 +234,21 @@ pub struct CommandPalette { pub struct ContactsPanel { #[serde(flatten)] pub container: ContainerStyle, + pub header: ContainedText, pub user_query_editor: FieldEditor, - pub host_row_height: f32, - pub host_avatar: ImageStyle, - pub host_username: ContainedText, + pub row_height: f32, + pub contact_avatar: ImageStyle, + pub contact_username: ContainedText, pub tree_branch_width: f32, pub tree_branch_color: Color, - pub shared_project: WorktreeRow, - pub hovered_shared_project: WorktreeRow, - pub unshared_project: WorktreeRow, - pub hovered_unshared_project: WorktreeRow, + pub shared_project: ProjectRow, + pub hovered_shared_project: ProjectRow, + pub unshared_project: ProjectRow, + pub hovered_unshared_project: ProjectRow, } #[derive(Deserialize, Default)] -pub struct WorktreeRow { +pub struct ProjectRow { #[serde(flatten)] pub container: ContainerStyle, pub height: f32, diff --git a/styles/src/styleTree/contactsPanel.ts b/styles/src/styleTree/contactsPanel.ts index 23ad7926454f09358f51035303ae5829e25aa1cd..dbcc9e4ccaa15380a5ca713c668a95eafa65ec75 100644 --- a/styles/src/styleTree/contactsPanel.ts +++ b/styles/src/styleTree/contactsPanel.ts @@ -47,19 +47,25 @@ export default function(theme: Theme) { top: 7, }, }, - hostRowHeight: 28, + rowHeight: 28, treeBranchColor: borderColor(theme, "muted"), treeBranchWidth: 1, - hostAvatar: { + contactAvatar: { cornerRadius: 10, width: 18, }, - hostUsername: { + contactUsername: { ...text(theme, "mono", "primary", { size: "sm" }), padding: { left: 8, }, }, + header: { + ...text(theme, "mono", "secondary", { size: "sm" }), + // padding: { + // left: 8, + // } + }, project, sharedProject, hoveredSharedProject: { From 4620c7a1e5ccb721b7879eceb34f0dab2d13db74 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 5 May 2022 15:04:50 -0700 Subject: [PATCH 05/53] Filter existing contacts when searching in the contacts panel --- Cargo.lock | 2 + crates/client/src/user.rs | 12 +-- crates/contacts_panel/Cargo.toml | 2 + crates/contacts_panel/src/contacts_panel.rs | 92 ++++++++++++++++----- 4 files changed, 83 insertions(+), 25 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1e85073d501a07cf5534dd78003ea797784aa56b..bca506d7e3550aa20e21e300bc0998ab67f5a2f0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -931,6 +931,8 @@ version = "0.1.0" dependencies = [ "client", "editor", + "futures", + "fuzzy", "gpui", "postage", "settings", diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index a28acdc63c573dfba9af6ed1ab239e11f7b4a7c4..e6bcf14bdad1012a9102e76b06b24f8c56411e0a 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -35,7 +35,7 @@ pub struct UserStore { users: HashMap>, update_contacts_tx: watch::Sender>, current_user: watch::Receiver>>, - contacts: Arc<[Contact]>, + contacts: Vec>, client: Weak, http: Arc, _maintain_contacts: Task<()>, @@ -62,7 +62,7 @@ impl UserStore { Self { users: Default::default(), current_user: current_user_rx, - contacts: Arc::from([]), + contacts: Default::default(), client: Arc::downgrade(&client), update_contacts_tx, http, @@ -128,12 +128,14 @@ impl UserStore { let mut contacts = Vec::new(); for contact in message.contacts { - contacts.push(Contact::from_proto(contact, &this, &mut cx).await?); + contacts.push(Arc::new( + Contact::from_proto(contact, &this, &mut cx).await?, + )); } this.update(&mut cx, |this, cx| { contacts.sort_by(|a, b| a.user.github_login.cmp(&b.user.github_login)); - this.contacts = contacts.into(); + this.contacts = contacts; cx.notify(); }); @@ -141,7 +143,7 @@ impl UserStore { }) } - pub fn contacts(&self) -> &Arc<[Contact]> { + pub fn contacts(&self) -> &[Arc] { &self.contacts } diff --git a/crates/contacts_panel/Cargo.toml b/crates/contacts_panel/Cargo.toml index 24ee22ed21f563f01c6095aaad2fbdcf1162f498..8e76bce22b2e98c5beed4b643dd16c56c5129a34 100644 --- a/crates/contacts_panel/Cargo.toml +++ b/crates/contacts_panel/Cargo.toml @@ -10,9 +10,11 @@ doctest = false [dependencies] client = { path = "../client" } editor = { path = "../editor" } +fuzzy = { path = "../fuzzy" } gpui = { path = "../gpui" } settings = { path = "../settings" } theme = { path = "../theme" } util = { path = "../util" } workspace = { path = "../workspace" } +futures = "0.3" postage = { version = "0.4.1", features = ["futures-traits"] } diff --git a/crates/contacts_panel/src/contacts_panel.rs b/crates/contacts_panel/src/contacts_panel.rs index d9460f265e1f7ab70bc7f307d47ba60b29b03519..4923e1aa8005f5812e5e625f07276992d606eee8 100644 --- a/crates/contacts_panel/src/contacts_panel.rs +++ b/crates/contacts_panel/src/contacts_panel.rs @@ -1,6 +1,8 @@ use client::{Contact, User, UserStore}; use editor::Editor; +use fuzzy::StringMatchCandidate; use gpui::{ + anyhow, elements::*, geometry::{rect::RectF, vector::vec2f}, platform::CursorStyle, @@ -14,6 +16,7 @@ use workspace::{AppState, JoinProject}; pub struct ContactsPanel { list_state: ListState, + contacts: Vec>, potential_contacts: Vec>, user_store: ModelHandle, contacts_search_task: Option>>, @@ -32,7 +35,7 @@ impl ContactsPanel { cx.subscribe(&user_query_editor, |this, _, event, cx| { if let editor::Event::BufferEdited = event { - this.user_query_changed(cx) + this.filter_contacts(true, cx) } }) .detach(); @@ -48,9 +51,8 @@ impl ContactsPanel { move |ix, cx| { let this = this.upgrade(cx).unwrap(); let this = this.read(cx); - let user_store = this.user_store.read(cx); - let contacts = user_store.contacts().clone(); - let current_user_id = user_store.current_user().map(|user| user.id); + let current_user_id = + this.user_store.read(cx).current_user().map(|user| user.id); let theme = cx.global::().theme.clone(); let theme = &theme.contacts_panel; @@ -63,16 +65,16 @@ impl ContactsPanel { .constrained() .with_height(theme.row_height) .boxed() - } else if ix < contacts.len() + 1 { + } else if ix < this.contacts.len() + 1 { let contact_ix = ix - 1; Self::render_contact( - &contacts[contact_ix], + this.contacts[contact_ix].clone(), current_user_id, app_state.clone(), theme, cx, ) - } else if ix == contacts.len() + 1 { + } else if ix == this.contacts.len() + 1 { Label::new("add contacts".to_string(), theme.header.text.clone()) .contained() .with_style(theme.header.container) @@ -82,7 +84,7 @@ impl ContactsPanel { .with_height(theme.row_height) .boxed() } else { - let potential_contact_ix = ix - 2 - contacts.len(); + let potential_contact_ix = ix - 2 - this.contacts.len(); Self::render_potential_contact( &this.potential_contacts[potential_contact_ix], theme, @@ -91,18 +93,19 @@ impl ContactsPanel { } }, ), + contacts: app_state.user_store.read(cx).contacts().into(), potential_contacts: Default::default(), user_query_editor, _maintain_contacts: cx.observe(&app_state.user_store, |this, _, cx| { - this.update_contacts(cx) + this.filter_contacts(false, cx) }), contacts_search_task: None, user_store: app_state.user_store.clone(), } } - fn update_contacts(&mut self, cx: &mut ViewContext) { - let mut list_len = 1 + self.user_store.read(cx).contacts().len(); + fn update_list_state(&mut self, cx: &mut ViewContext) { + let mut list_len = 1 + self.contacts.len(); if !self.potential_contacts.is_empty() { list_len += 1 + self.potential_contacts.len(); } @@ -112,7 +115,7 @@ impl ContactsPanel { } fn render_contact( - contact: &Contact, + contact: Arc, current_user_id: Option, app_state: Arc, theme: &theme::ContactsPanel, @@ -300,25 +303,74 @@ impl ContactsPanel { .boxed() } - fn user_query_changed(&mut self, cx: &mut ViewContext) { + fn filter_contacts(&mut self, query_changed: bool, cx: &mut ViewContext) { let query = self.user_query_editor.read(cx).text(cx); + if query.is_empty() { - self.potential_contacts.clear(); - self.update_contacts(cx); + self.contacts.clear(); + self.contacts + .extend_from_slice(self.user_store.read(cx).contacts()); + + if query_changed { + self.potential_contacts.clear(); + } + + self.update_list_state(cx); return; } - let search = self - .user_store - .update(cx, |store, cx| store.fuzzy_search_users(query, cx)); + let contacts = self.user_store.read(cx).contacts().to_vec(); + let candidates = contacts + .iter() + .enumerate() + .map(|(ix, contact)| StringMatchCandidate { + id: ix, + string: contact.user.github_login.clone(), + char_bag: contact.user.github_login.chars().collect(), + }) + .collect::>(); + let cancel_flag = Default::default(); + let background = cx.background().clone(); + + let search_users = if query_changed { + self.user_store + .update(cx, |store, cx| store.fuzzy_search_users(query.clone(), cx)) + } else { + Task::ready(Ok(self.potential_contacts.clone())) + }; + + let match_contacts = async move { + anyhow::Ok( + fuzzy::match_strings( + &candidates, + query.as_str(), + false, + 100, + &cancel_flag, + background, + ) + .await, + ) + }; + self.contacts_search_task = Some(cx.spawn(|this, mut cx| async move { - let users = search.await.log_err()?; + let (contact_matches, users) = + futures::future::join(match_contacts, search_users).await; + let contact_matches = contact_matches.log_err()?; + let users = users.log_err()?; + this.update(&mut cx, |this, cx| { let user_store = this.user_store.read(cx); + this.contacts.clear(); + this.contacts.extend( + contact_matches + .iter() + .map(|mat| contacts[mat.candidate_id].clone()), + ); this.potential_contacts = users; this.potential_contacts .retain(|user| !user_store.has_contact(&user)); - this.update_contacts(cx); + this.update_list_state(cx); }); None })); From 8445eaab85bd4ab23f4bb729fd46567b6b8b7017 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 5 May 2022 17:52:47 -0700 Subject: [PATCH 06/53] Fix crash when emptying atlases Previously, when an atlas was emptied, we would move it into a different vector: free_atlases. This removal could cause existing atlas ids to refer to the wrong atlases. --- crates/gpui/src/platform/mac/atlas.rs | 103 +++++++++++--------------- 1 file changed, 44 insertions(+), 59 deletions(-) diff --git a/crates/gpui/src/platform/mac/atlas.rs b/crates/gpui/src/platform/mac/atlas.rs index a7a4de10006a7118469346e94e67898da1a8d0ca..a529513ef5ef38faab25cf983e4820066ce9d28b 100644 --- a/crates/gpui/src/platform/mac/atlas.rs +++ b/crates/gpui/src/platform/mac/atlas.rs @@ -12,10 +12,10 @@ pub struct AtlasAllocator { device: Device, texture_descriptor: TextureDescriptor, atlases: Vec, - free_atlases: Vec, + last_used_atlas_id: usize, } -#[derive(Copy, Clone)] +#[derive(Copy, Clone, Debug)] pub struct AllocId { pub atlas_id: usize, alloc_id: etagere::AllocId, @@ -23,15 +23,15 @@ pub struct AllocId { impl AtlasAllocator { pub fn new(device: Device, texture_descriptor: TextureDescriptor) -> Self { - let mut me = Self { + let mut this = Self { device, texture_descriptor, - atlases: Vec::new(), - free_atlases: Vec::new(), + atlases: vec![], + last_used_atlas_id: 0, }; - let atlas = me.new_atlas(Vector2I::zero()); - me.atlases.push(atlas); - me + let atlas = this.new_atlas(Vector2I::zero()); + this.atlases.push(atlas); + this } pub fn default_atlas_size(&self) -> Vector2I { @@ -42,17 +42,27 @@ impl AtlasAllocator { } pub fn allocate(&mut self, requested_size: Vector2I) -> Option<(AllocId, Vector2I)> { - let allocation = self - .atlases - .last_mut() - .unwrap() + let atlas_id = self.last_used_atlas_id; + if let Some((alloc_id, origin)) = self.atlases[atlas_id].allocate(requested_size) { + return Some((AllocId { atlas_id, alloc_id }, origin)); + } + + for (atlas_id, atlas) in self.atlases.iter_mut().enumerate() { + if atlas_id == self.last_used_atlas_id { + continue; + } + if let Some((alloc_id, origin)) = atlas.allocate(requested_size) { + self.last_used_atlas_id = atlas_id; + return Some((AllocId { atlas_id, alloc_id }, origin)); + } + } + + let atlas_id = self.atlases.len(); + let mut atlas = self.new_atlas(requested_size); + let allocation = atlas .allocate(requested_size) - .or_else(|| { - let mut atlas = self.new_atlas(requested_size); - let (id, origin) = atlas.allocate(requested_size)?; - self.atlases.push(atlas); - Some((id, origin)) - }); + .map(|(alloc_id, origin)| (AllocId { atlas_id, alloc_id }, origin)); + self.atlases.push(atlas); if allocation.is_none() { warn!( @@ -61,13 +71,7 @@ impl AtlasAllocator { ); } - let (alloc_id, origin) = allocation?; - - let id = AllocId { - atlas_id: self.atlases.len() - 1, - alloc_id, - }; - Some((id, origin)) + allocation } pub fn upload(&mut self, size: Vector2I, bytes: &[u8]) -> Option<(AllocId, RectI)> { @@ -80,9 +84,6 @@ impl AtlasAllocator { pub fn deallocate(&mut self, id: AllocId) { if let Some(atlas) = self.atlases.get_mut(id.atlas_id) { atlas.deallocate(id.alloc_id); - if atlas.is_empty() { - self.free_atlases.push(self.atlases.remove(id.atlas_id)); - } } } @@ -90,7 +91,6 @@ impl AtlasAllocator { for atlas in &mut self.atlases { atlas.clear(); } - self.free_atlases.extend(self.atlases.drain(1..)); } pub fn texture(&self, atlas_id: usize) -> Option<&metal::TextureRef> { @@ -98,28 +98,22 @@ impl AtlasAllocator { } fn new_atlas(&mut self, required_size: Vector2I) -> Atlas { - if let Some(i) = self.free_atlases.iter().rposition(|atlas| { - atlas.size().x() >= required_size.x() && atlas.size().y() >= required_size.y() - }) { - self.free_atlases.remove(i) - } else { - let size = self.default_atlas_size().max(required_size); - let texture = if size.x() as u64 > self.texture_descriptor.width() - || size.y() as u64 > self.texture_descriptor.height() - { - let descriptor = unsafe { - let descriptor_ptr: *mut metal::MTLTextureDescriptor = - msg_send![self.texture_descriptor, copy]; - metal::TextureDescriptor::from_ptr(descriptor_ptr) - }; - descriptor.set_width(size.x() as u64); - descriptor.set_height(size.y() as u64); - self.device.new_texture(&descriptor) - } else { - self.device.new_texture(&self.texture_descriptor) + let size = self.default_atlas_size().max(required_size); + let texture = if size.x() as u64 > self.texture_descriptor.width() + || size.y() as u64 > self.texture_descriptor.height() + { + let descriptor = unsafe { + let descriptor_ptr: *mut metal::MTLTextureDescriptor = + msg_send![self.texture_descriptor, copy]; + metal::TextureDescriptor::from_ptr(descriptor_ptr) }; - Atlas::new(size, texture) - } + descriptor.set_width(size.x() as u64); + descriptor.set_height(size.y() as u64); + self.device.new_texture(&descriptor) + } else { + self.device.new_texture(&self.texture_descriptor) + }; + Atlas::new(size, texture) } } @@ -136,11 +130,6 @@ impl Atlas { } } - fn size(&self) -> Vector2I { - let size = self.allocator.size(); - vec2i(size.width, size.height) - } - fn allocate(&mut self, size: Vector2I) -> Option<(etagere::AllocId, Vector2I)> { let alloc = self .allocator @@ -177,10 +166,6 @@ impl Atlas { self.allocator.deallocate(id); } - fn is_empty(&self) -> bool { - self.allocator.is_empty() - } - fn clear(&mut self) { self.allocator.clear(); } From 95d848fe1e038a652a8331028cff249df920fd18 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 6 May 2022 10:20:03 +0200 Subject: [PATCH 07/53] Ensure `flex_float` works even when re-painting without layout Previously, we were mutating the remaining space stored on the layout state, which would cause re-paints to always have a `remaining_space` of 0 and therefore not align `flex_float` elements to the right/bottom. --- crates/gpui/src/elements/flex.rs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/crates/gpui/src/elements/flex.rs b/crates/gpui/src/elements/flex.rs index 4f930dfb46679cacc7d627374cf7ed0dd3dc1cd6..3384b200199a2aa0d43fc6fe4f360f8951f18af5 100644 --- a/crates/gpui/src/elements/flex.rs +++ b/crates/gpui/src/elements/flex.rs @@ -225,7 +225,9 @@ impl Element for Flex { remaining_space: &mut Self::LayoutState, cx: &mut PaintContext, ) -> Self::PaintState { - let overflowing = *remaining_space < 0.; + let mut remaining_space = *remaining_space; + + let overflowing = remaining_space < 0.; if overflowing { cx.scene.push_layer(Some(bounds)); } @@ -240,14 +242,14 @@ impl Element for Flex { } for child in &mut self.children { - if *remaining_space > 0. { + if remaining_space > 0. { if let Some(metadata) = child.metadata::() { if metadata.float { match self.axis { - Axis::Horizontal => child_origin += vec2f(*remaining_space, 0.0), - Axis::Vertical => child_origin += vec2f(0.0, *remaining_space), + Axis::Horizontal => child_origin += vec2f(remaining_space, 0.0), + Axis::Vertical => child_origin += vec2f(0.0, remaining_space), } - *remaining_space = 0.; + remaining_space = 0.; } } } @@ -257,6 +259,7 @@ impl Element for Flex { Axis::Vertical => child_origin += vec2f(0.0, child.size().y()), } } + if overflowing { cx.scene.pop_layer(); } From 44f37afa95a30c4cdfdb7ba22323bf2978bc535b Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 6 May 2022 15:38:02 +0200 Subject: [PATCH 08/53] Define data types for the new contacts model Co-Authored-By: Nathan Sobo --- assets/themes/cave-dark.json | 11 +++++++ assets/themes/cave-light.json | 11 +++++++ assets/themes/dark.json | 11 +++++++ assets/themes/light.json | 11 +++++++ assets/themes/solarized-dark.json | 11 +++++++ assets/themes/solarized-light.json | 11 +++++++ assets/themes/sulphurpool-dark.json | 11 +++++++ assets/themes/sulphurpool-light.json | 11 +++++++ .../20220506130724_create_contacts.sql | 10 ++++++ crates/collab/src/rpc.rs | 2 ++ crates/contacts_panel/src/contacts_panel.rs | 8 +++++ crates/rpc/proto/zed.proto | 32 ++++++++++++++++--- crates/theme/src/theme.rs | 1 + styles/src/styleTree/contactsPanel.ts | 6 ++++ 14 files changed, 143 insertions(+), 4 deletions(-) create mode 100644 crates/collab/migrations/20220506130724_create_contacts.sql diff --git a/assets/themes/cave-dark.json b/assets/themes/cave-dark.json index a2e224ed028dab93ce616f56547f04a3c6832b36..92319af4968f4db4fbb6ea6a23e263b624ddce66 100644 --- a/assets/themes/cave-dark.json +++ b/assets/themes/cave-dark.json @@ -1255,6 +1255,17 @@ "left": 8 } }, + "edit_contact": { + "family": "Zed Mono", + "color": "#e2dfe7", + "size": 14, + "background": "#26232a", + "corner_radius": 12, + "padding": { + "left": 7, + "right": 7 + } + }, "header": { "family": "Zed Mono", "color": "#8b8792", diff --git a/assets/themes/cave-light.json b/assets/themes/cave-light.json index 6ec03294ec841481e0166a8cbe27c904859ac6a4..f44069e49f19e6ca55cda82d3224da4caf940ce3 100644 --- a/assets/themes/cave-light.json +++ b/assets/themes/cave-light.json @@ -1255,6 +1255,17 @@ "left": 8 } }, + "edit_contact": { + "family": "Zed Mono", + "color": "#26232a", + "size": 14, + "background": "#e2dfe7", + "corner_radius": 12, + "padding": { + "left": 7, + "right": 7 + } + }, "header": { "family": "Zed Mono", "color": "#585260", diff --git a/assets/themes/dark.json b/assets/themes/dark.json index db72fd965cdfba2794a5a1c10d609755f946548f..f50e590fc42e3c92604c07d8e045bea48e52ac22 100644 --- a/assets/themes/dark.json +++ b/assets/themes/dark.json @@ -1255,6 +1255,17 @@ "left": 8 } }, + "edit_contact": { + "family": "Zed Mono", + "color": "#f1f1f1", + "size": 14, + "background": "#2b2b2b", + "corner_radius": 12, + "padding": { + "left": 7, + "right": 7 + } + }, "header": { "family": "Zed Mono", "color": "#9c9c9c", diff --git a/assets/themes/light.json b/assets/themes/light.json index f8754b7ede1cf33c179ab49cebba4b96aa27dbc6..08d4326414489f132cd78036edb65756c3d810ee 100644 --- a/assets/themes/light.json +++ b/assets/themes/light.json @@ -1255,6 +1255,17 @@ "left": 8 } }, + "edit_contact": { + "family": "Zed Mono", + "color": "#2b2b2b", + "size": 14, + "background": "#eaeaea", + "corner_radius": 12, + "padding": { + "left": 7, + "right": 7 + } + }, "header": { "family": "Zed Mono", "color": "#474747", diff --git a/assets/themes/solarized-dark.json b/assets/themes/solarized-dark.json index c6c9951779b601e9c763abf1d23021e27fcfc046..2b75c4364b124d315fbaed2ad3294e90ee7e6138 100644 --- a/assets/themes/solarized-dark.json +++ b/assets/themes/solarized-dark.json @@ -1255,6 +1255,17 @@ "left": 8 } }, + "edit_contact": { + "family": "Zed Mono", + "color": "#eee8d5", + "size": 14, + "background": "#073642", + "corner_radius": 12, + "padding": { + "left": 7, + "right": 7 + } + }, "header": { "family": "Zed Mono", "color": "#93a1a1", diff --git a/assets/themes/solarized-light.json b/assets/themes/solarized-light.json index 677bba00703e6e772ebde4cebab18bb9c201f6ef..47758db9986751375885d78ed85454129b01d00b 100644 --- a/assets/themes/solarized-light.json +++ b/assets/themes/solarized-light.json @@ -1255,6 +1255,17 @@ "left": 8 } }, + "edit_contact": { + "family": "Zed Mono", + "color": "#073642", + "size": 14, + "background": "#eee8d5", + "corner_radius": 12, + "padding": { + "left": 7, + "right": 7 + } + }, "header": { "family": "Zed Mono", "color": "#586e75", diff --git a/assets/themes/sulphurpool-dark.json b/assets/themes/sulphurpool-dark.json index b603dccb8d100cf56269df1fa4dacde9d77798b4..38761d606eb156fe3ea3043ee26f9c10108df09b 100644 --- a/assets/themes/sulphurpool-dark.json +++ b/assets/themes/sulphurpool-dark.json @@ -1255,6 +1255,17 @@ "left": 8 } }, + "edit_contact": { + "family": "Zed Mono", + "color": "#dfe2f1", + "size": 14, + "background": "#293256", + "corner_radius": 12, + "padding": { + "left": 7, + "right": 7 + } + }, "header": { "family": "Zed Mono", "color": "#979db4", diff --git a/assets/themes/sulphurpool-light.json b/assets/themes/sulphurpool-light.json index 3f4983c083f32502184d87d1b23fb651b1e3af87..6d3aeadd5fea179c37130af69adff503b1a06cb3 100644 --- a/assets/themes/sulphurpool-light.json +++ b/assets/themes/sulphurpool-light.json @@ -1255,6 +1255,17 @@ "left": 8 } }, + "edit_contact": { + "family": "Zed Mono", + "color": "#293256", + "size": 14, + "background": "#dfe2f1", + "corner_radius": 12, + "padding": { + "left": 7, + "right": 7 + } + }, "header": { "family": "Zed Mono", "color": "#5e6687", diff --git a/crates/collab/migrations/20220506130724_create_contacts.sql b/crates/collab/migrations/20220506130724_create_contacts.sql new file mode 100644 index 0000000000000000000000000000000000000000..a292674ac47b0c3cb8536f68d53fe25d3364b2ac --- /dev/null +++ b/crates/collab/migrations/20220506130724_create_contacts.sql @@ -0,0 +1,10 @@ +CREATE TABLE IF NOT EXISTS "contacts" ( + "id" SERIAL PRIMARY KEY, + "requesting_user_id" INTEGER REFERENCES users (id) NOT NULL, + "receiving_user_id" INTEGER REFERENCES users (id) NOT NULL, + "accepted" BOOLEAN NOT NULL, + "blocked" BOOLEAN NOT NULL +); + +CREATE UNIQUE INDEX "index_org_contacts_requesting_user_id_and_receiving_user_id" ON "contacts" ("requesting_user_id", "receiving_user_id"); +CREATE UNIQUE INDEX "index_org_contacts_receiving_user" ON "contacts" ("receiving_user_id"); diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 46ca033ee2ae43a289c35b3146f17161ed864e9a..9813de7aad69b8f73e792968333d94f074c552f6 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -905,6 +905,8 @@ impl Server { connection_id, proto::UpdateContacts { contacts: contacts.clone(), + pending_requests_from_user_ids: Default::default(), + pending_requests_to_user_ids: Default::default(), }, ) .trace_err(); diff --git a/crates/contacts_panel/src/contacts_panel.rs b/crates/contacts_panel/src/contacts_panel.rs index 4923e1aa8005f5812e5e625f07276992d606eee8..7c187ba7eeb8e6562d928c6fd1c3992c12f64d20 100644 --- a/crates/contacts_panel/src/contacts_panel.rs +++ b/crates/contacts_panel/src/contacts_panel.rs @@ -298,6 +298,14 @@ impl ContactsPanel { .left() .boxed(), ) + .with_child( + Label::new("+".to_string(), theme.edit_contact.text.clone()) + .contained() + .with_style(theme.edit_contact.container) + .aligned() + .flex_float() + .boxed(), + ) .constrained() .with_height(theme.row_height) .boxed() diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 267dcb0ba99be66bed2967592600fc8474c1f44c..73f08345f6fd1c4c3d083b4751bfac8563ae0822 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -89,11 +89,13 @@ message Envelope { GetUsers get_users = 76; FuzzySearchUsers fuzzy_search_users = 77; UsersResponse users_response = 78; + RequestContact request_contact = 79; + RespondToContactRequest respond_to_contact_request = 80; - Follow follow = 79; - FollowResponse follow_response = 80; - UpdateFollowers update_followers = 81; - Unfollow unfollow = 82; + Follow follow = 81; + FollowResponse follow_response = 82; + UpdateFollowers update_followers = 83; + Unfollow unfollow = 84; } } @@ -547,6 +549,21 @@ message UsersResponse { repeated User users = 1; } +message RequestContact { + uint64 to_user_id = 1; +} + +message RespondToContactRequest { + uint64 requesting_user_id = 1; + ContactRequestResponse response = 2; +} + +enum ContactRequestResponse { + Accept = 0; + Reject = 1; + Block = 2; +} + message SendChannelMessage { uint64 channel_id = 1; string body = 2; @@ -574,6 +591,13 @@ message GetChannelMessagesResponse { message UpdateContacts { repeated Contact contacts = 1; + repeated IncomingContactRequest pending_requests_from_user_ids = 2; + repeated uint64 pending_requests_to_user_ids = 3; +} + +message IncomingContactRequest { + uint64 user_id = 1; + bool show_notification = 2; } message UpdateDiagnostics { diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 241cbc86738c30c11a34d0e88995ed95b2df83bb..1f320bd24ce173151b76d76d97ab326e18f536e9 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -239,6 +239,7 @@ pub struct ContactsPanel { pub row_height: f32, pub contact_avatar: ImageStyle, pub contact_username: ContainedText, + pub edit_contact: ContainedText, pub tree_branch_width: f32, pub tree_branch_color: Color, pub shared_project: ProjectRow, diff --git a/styles/src/styleTree/contactsPanel.ts b/styles/src/styleTree/contactsPanel.ts index dbcc9e4ccaa15380a5ca713c668a95eafa65ec75..237913c37e91494c72002d3a73785871f23ace5f 100644 --- a/styles/src/styleTree/contactsPanel.ts +++ b/styles/src/styleTree/contactsPanel.ts @@ -60,6 +60,12 @@ export default function(theme: Theme) { left: 8, }, }, + editContact: { + ...text(theme, "mono", "primary", { size: "sm" }), + background: backgroundColor(theme, 100), + cornerRadius: 12, + padding: { left: 7, right: 7 } + }, header: { ...text(theme, "mono", "secondary", { size: "sm" }), // padding: { From 9555b93bca0e9cd265c4cd50d267cd9855a99d39 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 6 May 2022 16:47:08 +0200 Subject: [PATCH 09/53] Wait on `RECEIVE_TIMEOUT` in tests when testing disconnection We were waiting for 3 seconds, but the timeout had changed in the meantime, making some iterations of the tests fail. Co-Authored-By: Nathan Sobo --- crates/collab/src/rpc.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 9813de7aad69b8f73e792968333d94f074c552f6..730879c0d10e941140b3c68705b7313f9c3955be 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -2415,7 +2415,7 @@ mod tests { // Simulate connection loss for client B and ensure client A observes client B leaving the project. client_b.wait_for_current_user(cx_b).await; server.disconnect_client(client_b.current_user_id(cx_b)); - cx_a.foreground().advance_clock(Duration::from_secs(3)); + cx_a.foreground().advance_clock(rpc::RECEIVE_TIMEOUT); project_a .condition(cx_a, |p, _| p.collaborators().len() == 0) .await; @@ -4602,7 +4602,7 @@ mod tests { // Disconnect client B, ensuring we can still access its cached channel data. server.forbid_connections(); server.disconnect_client(client_b.current_user_id(&cx_b)); - cx_b.foreground().advance_clock(Duration::from_secs(3)); + cx_b.foreground().advance_clock(rpc::RECEIVE_TIMEOUT); while !matches!( status_b.next().await, Some(client::Status::ReconnectionError { .. }) From 989b82d664fe1e388ac6ee056e8ef4ed4dc21a0b Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 6 May 2022 17:01:27 +0200 Subject: [PATCH 10/53] Refactor `add_request_handler` to respond via a `Response` struct This also removes `add_sync_request_handler`. Co-Authored-By: Nathan Sobo --- crates/collab/src/rpc.rs | 218 ++++++++++++++++++++++----------------- 1 file changed, 123 insertions(+), 95 deletions(-) diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 730879c0d10e941140b3c68705b7313f9c3955be..184592f0330097bae51f0fc72ee7e2adc91cacae 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -18,7 +18,7 @@ use axum::{ headers::{Header, HeaderName}, http::StatusCode, middleware, - response::{IntoResponse, Response}, + response::IntoResponse, routing::get, Extension, Router, TypedHeader, }; @@ -27,7 +27,7 @@ use futures::{channel::mpsc, future::BoxFuture, FutureExt, SinkExt, StreamExt, T use lazy_static::lazy_static; use rpc::{ proto::{self, AnyTypedEnvelope, EntityMessage, EnvelopedMessage, RequestMessage}, - Connection, ConnectionId, Peer, TypedEnvelope, + Connection, ConnectionId, Peer, Receipt, TypedEnvelope, }; use std::{ any::TypeId, @@ -36,7 +36,10 @@ use std::{ net::SocketAddr, ops::{Deref, DerefMut}, rc::Rc, - sync::Arc, + sync::{ + atomic::{AtomicBool, Ordering::SeqCst}, + Arc, + }, time::Duration, }; use store::{Store, Worktree}; @@ -51,6 +54,20 @@ use tracing::{info_span, instrument, Instrument}; type MessageHandler = Box, Box) -> BoxFuture<'static, ()>>; +struct Response { + server: Arc, + receipt: Receipt, + responded: Arc, +} + +impl Response { + fn send(self, payload: R::Response) -> Result<()> { + self.responded.store(true, SeqCst); + self.server.peer.respond(self.receipt, payload)?; + Ok(()) + } +} + pub struct Server { peer: Arc, store: RwLock, @@ -100,7 +117,7 @@ impl Server { .add_message_handler(Server::unregister_project) .add_request_handler(Server::share_project) .add_message_handler(Server::unshare_project) - .add_sync_request_handler(Server::join_project) + .add_request_handler(Server::join_project) .add_message_handler(Server::leave_project) .add_request_handler(Server::register_worktree) .add_message_handler(Server::unregister_worktree) @@ -179,43 +196,12 @@ impl Server { self } - fn add_request_handler(&mut self, handler: F) -> &mut Self - where - F: 'static + Send + Sync + Fn(Arc, TypedEnvelope) -> Fut, - Fut: 'static + Send + Future>, - M: RequestMessage, - { - self.add_message_handler(move |server, envelope| { - let receipt = envelope.receipt(); - let response = (handler)(server.clone(), envelope); - async move { - match response.await { - Ok(response) => { - server.peer.respond(receipt, response)?; - Ok(()) - } - Err(error) => { - server.peer.respond_with_error( - receipt, - proto::Error { - message: error.to_string(), - }, - )?; - Err(error) - } - } - } - }) - } - /// Handle a request while holding a lock to the store. This is useful when we're registering /// a connection but we want to respond on the connection before anybody else can send on it. - fn add_sync_request_handler(&mut self, handler: F) -> &mut Self + fn add_request_handler(&mut self, handler: F) -> &mut Self where - F: 'static - + Send - + Sync - + Fn(Arc, &mut Store, TypedEnvelope) -> Result, + F: 'static + Send + Sync + Fn(Arc, TypedEnvelope, Response) -> Fut, + Fut: Send + Future>, M: RequestMessage, { let handler = Arc::new(handler); @@ -223,12 +209,19 @@ impl Server { let receipt = envelope.receipt(); let handler = handler.clone(); async move { - let mut store = server.state_mut().await; - let response = (handler)(server.clone(), &mut *store, envelope); - match response { - Ok(response) => { - server.peer.respond(receipt, response)?; - Ok(()) + let responded = Arc::new(AtomicBool::default()); + let response = Response { + server: server.clone(), + responded: responded.clone(), + receipt: envelope.receipt(), + }; + match (handler)(server.clone(), envelope, response).await { + Ok(()) => { + if responded.load(std::sync::atomic::Ordering::SeqCst) { + Ok(()) + } else { + Err(anyhow!("handler did not send a response"))? + } } Err(error) => { server.peer.respond_with_error( @@ -364,20 +357,27 @@ impl Server { Ok(()) } - async fn ping(self: Arc, _: TypedEnvelope) -> Result { - Ok(proto::Ack {}) + async fn ping( + self: Arc, + _: TypedEnvelope, + response: Response, + ) -> Result<()> { + response.send(proto::Ack {})?; + Ok(()) } async fn register_project( self: Arc, request: TypedEnvelope, - ) -> Result { + response: Response, + ) -> Result<()> { let project_id = { let mut state = self.state_mut().await; let user_id = state.user_id_for_connection(request.sender_id)?; state.register_project(request.sender_id, user_id) }; - Ok(proto::RegisterProjectResponse { project_id }) + response.send(proto::RegisterProjectResponse { project_id })?; + Ok(()) } async fn unregister_project( @@ -393,11 +393,13 @@ impl Server { async fn share_project( self: Arc, request: TypedEnvelope, - ) -> Result { + response: Response, + ) -> Result<()> { let mut state = self.state_mut().await; let project = state.share_project(request.payload.project_id, request.sender_id)?; self.update_contacts_for_users(&mut *state, &project.authorized_user_ids); - Ok(proto::Ack {}) + response.send(proto::Ack {})?; + Ok(()) } async fn unshare_project( @@ -415,15 +417,16 @@ impl Server { Ok(()) } - fn join_project( + async fn join_project( self: Arc, - state: &mut Store, request: TypedEnvelope, - ) -> Result { + response: Response, + ) -> Result<()> { let project_id = request.payload.project_id; + let state = &mut *self.state_mut().await; let user_id = state.user_id_for_connection(request.sender_id)?; - let (response, connection_ids, contact_user_ids) = state + let (response_payload, connection_ids, contact_user_ids) = state .join_project(request.sender_id, user_id, project_id) .and_then(|joined| { let share = joined.project.share()?; @@ -480,14 +483,15 @@ impl Server { project_id, collaborator: Some(proto::Collaborator { peer_id: request.sender_id.0, - replica_id: response.replica_id, + replica_id: response_payload.replica_id, user_id: user_id.to_proto(), }), }, ) }); self.update_contacts_for_users(state, &contact_user_ids); - Ok(response) + response.send(response_payload)?; + Ok(()) } async fn leave_project( @@ -514,7 +518,8 @@ impl Server { async fn register_worktree( self: Arc, request: TypedEnvelope, - ) -> Result { + response: Response, + ) -> Result<()> { let mut contact_user_ids = HashSet::default(); for github_login in &request.payload.authorized_logins { let contact_user_id = self.app_state.db.create_user(github_login, false).await?; @@ -545,7 +550,8 @@ impl Server { .forward_send(request.sender_id, connection_id, request.payload.clone()) }); self.update_contacts_for_users(&*state, &contact_user_ids); - Ok(proto::Ack {}) + response.send(proto::Ack {})?; + Ok(()) } async fn unregister_worktree( @@ -573,7 +579,8 @@ impl Server { async fn update_worktree( self: Arc, request: TypedEnvelope, - ) -> Result { + response: Response, + ) -> Result<()> { let connection_ids = self.state_mut().await.update_worktree( request.sender_id, request.payload.project_id, @@ -587,8 +594,8 @@ impl Server { self.peer .forward_send(request.sender_id, connection_id, request.payload.clone()) }); - - Ok(proto::Ack {}) + response.send(proto::Ack {})?; + Ok(()) } async fn update_diagnostic_summary( @@ -652,7 +659,8 @@ impl Server { async fn forward_project_request( self: Arc, request: TypedEnvelope, - ) -> Result + response: Response, + ) -> Result<()> where T: EntityMessage + RequestMessage, { @@ -661,22 +669,26 @@ impl Server { .await .read_project(request.payload.remote_entity_id(), request.sender_id)? .host_connection_id; - Ok(self - .peer - .forward_request(request.sender_id, host_connection_id, request.payload) - .await?) + + response.send( + self.peer + .forward_request(request.sender_id, host_connection_id, request.payload) + .await?, + )?; + Ok(()) } async fn save_buffer( self: Arc, request: TypedEnvelope, - ) -> Result { + response: Response, + ) -> Result<()> { let host = self .state() .await .read_project(request.payload.project_id, request.sender_id)? .host_connection_id; - let response = self + let response_payload = self .peer .forward_request(request.sender_id, host, request.payload.clone()) .await?; @@ -688,16 +700,18 @@ impl Server { .connection_ids(); guests.retain(|guest_connection_id| *guest_connection_id != request.sender_id); broadcast(host, guests, |conn_id| { - self.peer.forward_send(host, conn_id, response.clone()) + self.peer + .forward_send(host, conn_id, response_payload.clone()) }); - - Ok(response) + response.send(response_payload)?; + Ok(()) } async fn update_buffer( self: Arc, request: TypedEnvelope, - ) -> Result { + response: Response, + ) -> Result<()> { let receiver_ids = self .state() .await @@ -706,7 +720,8 @@ impl Server { self.peer .forward_send(request.sender_id, connection_id, request.payload.clone()) }); - Ok(proto::Ack {}) + response.send(proto::Ack {})?; + Ok(()) } async fn update_buffer_file( @@ -757,7 +772,8 @@ impl Server { async fn follow( self: Arc, request: TypedEnvelope, - ) -> Result { + response: Response, + ) -> Result<()> { let leader_id = ConnectionId(request.payload.leader_id); let follower_id = request.sender_id; if !self @@ -768,14 +784,15 @@ impl Server { { Err(anyhow!("no such peer"))?; } - let mut response = self + let mut response_payload = self .peer .forward_request(request.sender_id, leader_id, request.payload) .await?; - response + response_payload .views .retain(|view| view.leader_id != Some(follower_id.0)); - Ok(response) + response.send(response_payload)?; + Ok(()) } async fn unfollow(self: Arc, request: TypedEnvelope) -> Result<()> { @@ -823,13 +840,14 @@ impl Server { async fn get_channels( self: Arc, request: TypedEnvelope, - ) -> Result { + response: Response, + ) -> Result<()> { let user_id = self .state() .await .user_id_for_connection(request.sender_id)?; let channels = self.app_state.db.get_accessible_channels(user_id).await?; - Ok(proto::GetChannelsResponse { + response.send(proto::GetChannelsResponse { channels: channels .into_iter() .map(|chan| proto::Channel { @@ -837,13 +855,15 @@ impl Server { name: chan.name, }) .collect(), - }) + })?; + Ok(()) } async fn get_users( self: Arc, request: TypedEnvelope, - ) -> Result { + response: Response, + ) -> Result<()> { let user_ids = request .payload .user_ids @@ -862,13 +882,15 @@ impl Server { github_login: user.github_login, }) .collect(); - Ok(proto::UsersResponse { users }) + response.send(proto::UsersResponse { users })?; + Ok(()) } async fn fuzzy_search_users( self: Arc, request: TypedEnvelope, - ) -> Result { + response: Response, + ) -> Result<()> { let query = request.payload.query; let db = &self.app_state.db; let users = match query.len() { @@ -888,7 +910,8 @@ impl Server { github_login: user.github_login, }) .collect(); - Ok(proto::UsersResponse { users }) + response.send(proto::UsersResponse { users })?; + Ok(()) } #[instrument(skip(self, state, user_ids))] @@ -917,7 +940,8 @@ impl Server { async fn join_channel( self: Arc, request: TypedEnvelope, - ) -> Result { + response: Response, + ) -> Result<()> { let user_id = self .state() .await @@ -949,10 +973,11 @@ impl Server { nonce: Some(msg.nonce.as_u128().into()), }) .collect::>(); - Ok(proto::JoinChannelResponse { + response.send(proto::JoinChannelResponse { done: messages.len() < MESSAGE_COUNT_PER_PAGE, messages, - }) + })?; + Ok(()) } async fn leave_channel( @@ -983,7 +1008,8 @@ impl Server { async fn send_channel_message( self: Arc, request: TypedEnvelope, - ) -> Result { + response: Response, + ) -> Result<()> { let channel_id = ChannelId::from_proto(request.payload.channel_id); let user_id; let connection_ids; @@ -1030,15 +1056,17 @@ impl Server { }, ) }); - Ok(proto::SendChannelMessageResponse { + response.send(proto::SendChannelMessageResponse { message: Some(message), - }) + })?; + Ok(()) } async fn get_channel_messages( self: Arc, request: TypedEnvelope, - ) -> Result { + response: Response, + ) -> Result<()> { let user_id = self .state() .await @@ -1071,11 +1099,11 @@ impl Server { nonce: Some(msg.nonce.as_u128().into()), }) .collect::>(); - - Ok(proto::GetChannelMessagesResponse { + response.send(proto::GetChannelMessagesResponse { done: messages.len() < MESSAGE_COUNT_PER_PAGE, messages, - }) + })?; + Ok(()) } async fn state<'a>(self: &'a Arc) -> StoreReadGuard<'a> { @@ -1213,7 +1241,7 @@ pub async fn handle_websocket_request( Extension(server): Extension>, Extension(user_id): Extension, ws: WebSocketUpgrade, -) -> Response { +) -> axum::response::Response { if protocol_version != rpc::PROTOCOL_VERSION { return ( StatusCode::UPGRADE_REQUIRED, From 274c4c244cba6155884001492a46ba1277a19251 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 6 May 2022 13:33:23 -0700 Subject: [PATCH 11/53] Implement persistence for contacts Co-authored-by: Nathan Sobo --- .../20220506130724_create_contacts.sql | 12 +- crates/collab/src/db.rs | 346 +++++++++++++++++- 2 files changed, 350 insertions(+), 8 deletions(-) diff --git a/crates/collab/migrations/20220506130724_create_contacts.sql b/crates/collab/migrations/20220506130724_create_contacts.sql index a292674ac47b0c3cb8536f68d53fe25d3364b2ac..216635b3195283c8a8a6cbf5319b572f45b7b04b 100644 --- a/crates/collab/migrations/20220506130724_create_contacts.sql +++ b/crates/collab/migrations/20220506130724_create_contacts.sql @@ -1,10 +1,10 @@ CREATE TABLE IF NOT EXISTS "contacts" ( "id" SERIAL PRIMARY KEY, - "requesting_user_id" INTEGER REFERENCES users (id) NOT NULL, - "receiving_user_id" INTEGER REFERENCES users (id) NOT NULL, - "accepted" BOOLEAN NOT NULL, - "blocked" BOOLEAN NOT NULL + "user_id_a" INTEGER REFERENCES users (id) NOT NULL, + "user_id_b" INTEGER REFERENCES users (id) NOT NULL, + "a_to_b" BOOLEAN NOT NULL, + "accepted" BOOLEAN NOT NULL ); -CREATE UNIQUE INDEX "index_org_contacts_requesting_user_id_and_receiving_user_id" ON "contacts" ("requesting_user_id", "receiving_user_id"); -CREATE UNIQUE INDEX "index_org_contacts_receiving_user" ON "contacts" ("receiving_user_id"); +CREATE UNIQUE INDEX "index_contacts_user_ids" ON "contacts" ("user_id_a", "user_id_b"); +CREATE INDEX "index_contacts_user_id_b" ON "contacts" ("user_id_b"); diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index 3eb6fd0240ebe32c09d341ba60dc037c7ae3ab03..0f2c700c2cc5a9fd540dc51a19f2af01412b5d33 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -1,6 +1,6 @@ -use anyhow::Context; -use anyhow::Result; +use anyhow::{anyhow, Context, Result}; use async_trait::async_trait; +use futures::StreamExt; use serde::Serialize; pub use sqlx::postgres::PgPoolOptions as DbOptions; use sqlx::{types::Uuid, FromRow}; @@ -16,6 +16,16 @@ pub trait Db: Send + Sync { async fn get_user_by_github_login(&self, github_login: &str) -> Result>; async fn set_user_is_admin(&self, id: UserId, is_admin: bool) -> Result<()>; async fn destroy_user(&self, id: UserId) -> Result<()>; + + async fn get_contacts(&self, id: UserId) -> Result; + async fn send_contact_request(&self, requester_id: UserId, responder_id: UserId) -> Result<()>; + async fn respond_to_contact_request( + &self, + responder_id: UserId, + requester_id: UserId, + accept: bool, + ) -> Result<()>; + async fn create_access_token_hash( &self, user_id: UserId, @@ -24,6 +34,7 @@ pub trait Db: Send + Sync { ) -> Result<()>; async fn get_access_token_hashes(&self, user_id: UserId) -> Result>; #[cfg(any(test, feature = "seed-support"))] + async fn find_org_by_slug(&self, slug: &str) -> Result>; #[cfg(any(test, feature = "seed-support"))] async fn create_org(&self, name: &str, slug: &str) -> Result; @@ -32,6 +43,7 @@ pub trait Db: Send + Sync { #[cfg(any(test, feature = "seed-support"))] async fn create_org_channel(&self, org_id: OrgId, name: &str) -> Result; #[cfg(any(test, feature = "seed-support"))] + async fn get_org_channels(&self, org_id: OrgId) -> Result>; async fn get_accessible_channels(&self, user_id: UserId) -> Result>; async fn can_user_access_channel(&self, user_id: UserId, channel_id: ChannelId) @@ -168,6 +180,124 @@ impl Db for PostgresDb { .map(drop)?) } + // contacts + + async fn get_contacts(&self, user_id: UserId) -> Result { + let query = " + SELECT user_id_a, user_id_b, a_to_b, accepted + FROM contacts + WHERE user_id_a = $1 OR user_id_b = $1; + "; + + let mut rows = sqlx::query_as::<_, (UserId, UserId, bool, bool)>(query) + .bind(user_id) + .fetch(&self.pool); + + let mut current = Vec::new(); + let mut requests_sent = Vec::new(); + let mut requests_received = Vec::new(); + while let Some(row) = rows.next().await { + let (user_id_a, user_id_b, a_to_b, accepted) = row?; + + if user_id_a == user_id { + if accepted { + current.push(user_id_b); + } else if a_to_b { + requests_sent.push(user_id_b); + } else { + requests_received.push(user_id_b); + } + } else { + if accepted { + current.push(user_id_a); + } else if a_to_b { + requests_received.push(user_id_a); + } else { + requests_sent.push(user_id_a); + } + } + } + + Ok(Contacts { + current, + requests_sent, + requests_received, + }) + } + + async fn send_contact_request(&self, sender_id: UserId, receiver_id: UserId) -> Result<()> { + let (id_a, id_b, a_to_b) = if sender_id < receiver_id { + (sender_id, receiver_id, true) + } else { + (receiver_id, sender_id, false) + }; + let query = " + INSERT into contacts (user_id_a, user_id_b, a_to_b, accepted) + VALUES ($1, $2, $3, 'f') + ON CONFLICT (user_id_a, user_id_b) DO UPDATE + SET + accepted = 't' + WHERE + NOT contacts.accepted AND + ((contacts.a_to_b = excluded.a_to_b AND contacts.user_id_a = excluded.user_id_b) OR + (contacts.a_to_b != excluded.a_to_b AND contacts.user_id_a = excluded.user_id_a)); + "; + let result = sqlx::query(query) + .bind(id_a.0) + .bind(id_b.0) + .bind(a_to_b) + .execute(&self.pool) + .await?; + + if result.rows_affected() == 1 { + Ok(()) + } else { + Err(anyhow!("contact already requested")) + } + } + + async fn respond_to_contact_request( + &self, + responder_id: UserId, + requester_id: UserId, + accept: bool, + ) -> Result<()> { + let (id_a, id_b, a_to_b) = if responder_id < requester_id { + (responder_id, requester_id, false) + } else { + (requester_id, responder_id, true) + }; + let result = if accept { + let query = " + UPDATE contacts + SET accepted = 't' + WHERE user_id_a = $1 AND user_id_b = $2 AND a_to_b = $3; + "; + sqlx::query(query) + .bind(id_a.0) + .bind(id_b.0) + .bind(a_to_b) + .execute(&self.pool) + .await? + } else { + let query = " + DELETE FROM contacts + WHERE user_id_a = $1 AND user_id_b = $2 AND a_to_b = $3 AND NOT accepted; + "; + sqlx::query(query) + .bind(id_a.0) + .bind(id_b.0) + .bind(a_to_b) + .execute(&self.pool) + .await? + }; + if result.rows_affected() == 1 { + Ok(()) + } else { + Err(anyhow!("no such contact request")) + } + } + // access tokens async fn create_access_token_hash( @@ -494,6 +624,13 @@ pub struct ChannelMessage { pub nonce: Uuid, } +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Contacts { + pub current: Vec, + pub requests_sent: Vec, + pub requests_received: Vec, +} + fn fuzzy_like_string(string: &str) -> String { let mut result = String::with_capacity(string.len() * 2 + 1); for c in string.chars() { @@ -712,6 +849,122 @@ pub mod tests { } } + #[tokio::test(flavor = "multi_thread")] + async fn test_add_contacts() { + for test_db in [ + TestDb::postgres().await, + TestDb::fake(Arc::new(gpui::executor::Background::new())), + ] { + let db = test_db.db(); + + let user_1 = db.create_user("user1", false).await.unwrap(); + let user_2 = db.create_user("user2", false).await.unwrap(); + let user_3 = db.create_user("user3", false).await.unwrap(); + + // User starts with no contacts + assert_eq!( + db.get_contacts(user_1).await.unwrap(), + Contacts { + current: vec![], + requests_sent: vec![], + requests_received: vec![], + }, + ); + + // User requests a contact. Both users see the pending request. + db.send_contact_request(user_1, user_2).await.unwrap(); + assert_eq!( + db.get_contacts(user_1).await.unwrap(), + Contacts { + current: vec![], + requests_sent: vec![user_2], + requests_received: vec![], + }, + ); + assert_eq!( + db.get_contacts(user_2).await.unwrap(), + Contacts { + current: vec![], + requests_sent: vec![], + requests_received: vec![user_1], + }, + ); + + // User can't accept their own contact request + db.respond_to_contact_request(user_1, user_2, true) + .await + .unwrap_err(); + + // User accepts a contact request. Both users see the contact. + db.respond_to_contact_request(user_2, user_1, true) + .await + .unwrap(); + assert_eq!( + db.get_contacts(user_1).await.unwrap(), + Contacts { + current: vec![user_2], + requests_sent: vec![], + requests_received: vec![], + }, + ); + assert_eq!( + db.get_contacts(user_2).await.unwrap(), + Contacts { + current: vec![user_1], + requests_sent: vec![], + requests_received: vec![], + }, + ); + + // Users cannot re-request existing contacts. + db.send_contact_request(user_1, user_2).await.unwrap_err(); + db.send_contact_request(user_2, user_1).await.unwrap_err(); + + // Users send each other concurrent contact requests and + // see that they are immediately accepted. + db.send_contact_request(user_1, user_3).await.unwrap(); + db.send_contact_request(user_3, user_1).await.unwrap(); + assert_eq!( + db.get_contacts(user_1).await.unwrap(), + Contacts { + current: vec![user_2, user_3], + requests_sent: vec![], + requests_received: vec![], + }, + ); + assert_eq!( + db.get_contacts(user_3).await.unwrap(), + Contacts { + current: vec![user_1], + requests_sent: vec![], + requests_received: vec![], + }, + ); + + // User declines a contact request. Both users see that it is gone. + db.send_contact_request(user_2, user_3).await.unwrap(); + db.respond_to_contact_request(user_3, user_2, false) + .await + .unwrap(); + assert_eq!( + db.get_contacts(user_2).await.unwrap(), + Contacts { + current: vec![user_1], + requests_sent: vec![], + requests_received: vec![], + }, + ); + assert_eq!( + db.get_contacts(user_3).await.unwrap(), + Contacts { + current: vec![user_1], + requests_sent: vec![], + requests_received: vec![], + }, + ); + } + } + pub struct TestDb { pub db: Option>, pub url: String, @@ -772,6 +1025,13 @@ pub mod tests { channel_memberships: Mutex>, channel_messages: Mutex>, next_channel_message_id: Mutex, + contacts: Mutex>, + } + + struct FakeContact { + requester_id: UserId, + responder_id: UserId, + accepted: bool, } impl FakeDb { @@ -788,6 +1048,7 @@ pub mod tests { channel_memberships: Default::default(), channel_messages: Default::default(), next_channel_message_id: Mutex::new(1), + contacts: Default::default(), } } } @@ -847,6 +1108,87 @@ pub mod tests { unimplemented!() } + async fn get_contacts(&self, id: UserId) -> Result { + self.background.simulate_random_delay().await; + let mut current = Vec::new(); + let mut requests_sent = Vec::new(); + let mut requests_received = Vec::new(); + for contact in self.contacts.lock().iter() { + if contact.requester_id == id { + if contact.accepted { + current.push(contact.responder_id); + } else { + requests_sent.push(contact.responder_id); + } + } else if contact.responder_id == id { + if contact.accepted { + current.push(contact.requester_id); + } else { + requests_received.push(contact.requester_id); + } + } + } + Ok(Contacts { + current, + requests_sent, + requests_received, + }) + } + + async fn send_contact_request( + &self, + requester_id: UserId, + responder_id: UserId, + ) -> Result<()> { + let mut contacts = self.contacts.lock(); + for contact in contacts.iter_mut() { + if contact.requester_id == requester_id && contact.responder_id == responder_id { + if contact.accepted { + Err(anyhow!("contact already exists"))?; + } else { + Err(anyhow!("contact already requested"))?; + } + } + if contact.responder_id == requester_id && contact.requester_id == responder_id { + if contact.accepted { + Err(anyhow!("contact already exists"))?; + } else { + contact.accepted = true; + return Ok(()); + } + } + } + contacts.push(FakeContact { + requester_id, + responder_id, + accepted: false, + }); + Ok(()) + } + + async fn respond_to_contact_request( + &self, + responder_id: UserId, + requester_id: UserId, + accept: bool, + ) -> Result<()> { + let mut contacts = self.contacts.lock(); + for (ix, contact) in contacts.iter_mut().enumerate() { + if contact.requester_id == requester_id && contact.responder_id == responder_id { + if contact.accepted { + return Err(anyhow!("contact already confirmed")); + } + if accept { + contact.accepted = true; + } else { + contacts.remove(ix); + } + return Ok(()); + } + } + Err(anyhow!("no such contact request")) + } + async fn create_access_token_hash( &self, _user_id: UserId, From 8a3425477f0bf74ec387baf2a951a4cf70746e93 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 6 May 2022 15:44:47 -0700 Subject: [PATCH 12/53] Start work on RPC endpoints for dealing with contact requests Co-authored-by: Nathan Sobo --- crates/client/src/user.rs | 42 ++++++- .../20220506130724_create_contacts.sql | 1 + crates/collab/src/db.rs | 114 ++++++++++++++++-- crates/collab/src/rpc.rs | 71 +++++++++++ crates/rpc/src/macros.rs | 67 ++++++++++ crates/rpc/src/proto.rs | 71 +---------- crates/rpc/src/rpc.rs | 1 + 7 files changed, 289 insertions(+), 78 deletions(-) create mode 100644 crates/rpc/src/macros.rs diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index e6bcf14bdad1012a9102e76b06b24f8c56411e0a..41a40617eef48178d4a81e22a2dcfb40c17fa0d5 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -1,6 +1,6 @@ use super::{http::HttpClient, proto, Client, Status, TypedEnvelope}; use anyhow::{anyhow, Result}; -use futures::{future, AsyncReadExt}; +use futures::{future, AsyncReadExt, Future}; use gpui::{AsyncAppContext, Entity, ImageData, ModelContext, ModelHandle, Task}; use postage::{prelude::Stream, sink::Sink, watch}; use rpc::proto::{RequestMessage, UsersResponse}; @@ -121,6 +121,13 @@ impl UserStore { user_ids.insert(contact.user_id); user_ids.extend(contact.projects.iter().flat_map(|w| &w.guests).copied()); } + user_ids.extend(message.pending_requests_to_user_ids.iter()); + user_ids.extend( + message + .pending_requests_from_user_ids + .iter() + .map(|req| req.user_id), + ); let load_users = self.get_users(user_ids.into_iter().collect(), cx); cx.spawn(|this, mut cx| async move { @@ -153,6 +160,39 @@ impl UserStore { .is_ok() } + pub fn request_contact(&self, to_user_id: u64) -> impl Future> { + let client = self.client.upgrade(); + async move { + client + .ok_or_else(|| anyhow!("not logged in"))? + .request(proto::RequestContact { to_user_id }) + .await?; + Ok(()) + } + } + + pub fn respond_to_contact_request( + &self, + from_user_id: u64, + accept: bool, + ) -> impl Future> { + let client = self.client.upgrade(); + async move { + client + .ok_or_else(|| anyhow!("not logged in"))? + .request(proto::RespondToContactRequest { + requesting_user_id: from_user_id, + response: if accept { + proto::ContactRequestResponse::Accept + } else { + proto::ContactRequestResponse::Reject + } as i32, + }) + .await?; + Ok(()) + } + } + pub fn get_users( &mut self, mut user_ids: Vec, diff --git a/crates/collab/migrations/20220506130724_create_contacts.sql b/crates/collab/migrations/20220506130724_create_contacts.sql index 216635b3195283c8a8a6cbf5319b572f45b7b04b..56beb70fd06ce8a3b7bb00d2f0ada2e465906c69 100644 --- a/crates/collab/migrations/20220506130724_create_contacts.sql +++ b/crates/collab/migrations/20220506130724_create_contacts.sql @@ -3,6 +3,7 @@ CREATE TABLE IF NOT EXISTS "contacts" ( "user_id_a" INTEGER REFERENCES users (id) NOT NULL, "user_id_b" INTEGER REFERENCES users (id) NOT NULL, "a_to_b" BOOLEAN NOT NULL, + "should_notify" BOOLEAN NOT NULL, "accepted" BOOLEAN NOT NULL ); diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index 0f2c700c2cc5a9fd540dc51a19f2af01412b5d33..5d5f55fc92edf38b9bd69d27fd55ab9ba7c66486 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -19,6 +19,11 @@ pub trait Db: Send + Sync { async fn get_contacts(&self, id: UserId) -> Result; async fn send_contact_request(&self, requester_id: UserId, responder_id: UserId) -> Result<()>; + async fn dismiss_contact_request( + &self, + responder_id: UserId, + requester_id: UserId, + ) -> Result<()>; async fn respond_to_contact_request( &self, responder_id: UserId, @@ -184,12 +189,12 @@ impl Db for PostgresDb { async fn get_contacts(&self, user_id: UserId) -> Result { let query = " - SELECT user_id_a, user_id_b, a_to_b, accepted + SELECT user_id_a, user_id_b, a_to_b, accepted, should_notify FROM contacts WHERE user_id_a = $1 OR user_id_b = $1; "; - let mut rows = sqlx::query_as::<_, (UserId, UserId, bool, bool)>(query) + let mut rows = sqlx::query_as::<_, (UserId, UserId, bool, bool, bool)>(query) .bind(user_id) .fetch(&self.pool); @@ -197,7 +202,7 @@ impl Db for PostgresDb { let mut requests_sent = Vec::new(); let mut requests_received = Vec::new(); while let Some(row) = rows.next().await { - let (user_id_a, user_id_b, a_to_b, accepted) = row?; + let (user_id_a, user_id_b, a_to_b, accepted, should_notify) = row?; if user_id_a == user_id { if accepted { @@ -205,13 +210,19 @@ impl Db for PostgresDb { } else if a_to_b { requests_sent.push(user_id_b); } else { - requests_received.push(user_id_b); + requests_received.push(IncomingContactRequest { + requesting_user_id: user_id_b, + should_notify, + }); } } else { if accepted { current.push(user_id_a); } else if a_to_b { - requests_received.push(user_id_a); + requests_received.push(IncomingContactRequest { + requesting_user_id: user_id_a, + should_notify, + }); } else { requests_sent.push(user_id_a); } @@ -232,8 +243,8 @@ impl Db for PostgresDb { (receiver_id, sender_id, false) }; let query = " - INSERT into contacts (user_id_a, user_id_b, a_to_b, accepted) - VALUES ($1, $2, $3, 'f') + INSERT into contacts (user_id_a, user_id_b, a_to_b, accepted, should_notify) + VALUES ($1, $2, $3, 'f', 't') ON CONFLICT (user_id_a, user_id_b) DO UPDATE SET accepted = 't' @@ -270,7 +281,7 @@ impl Db for PostgresDb { let result = if accept { let query = " UPDATE contacts - SET accepted = 't' + SET accepted = 't', should_notify = 'f' WHERE user_id_a = $1 AND user_id_b = $2 AND a_to_b = $3; "; sqlx::query(query) @@ -298,6 +309,37 @@ impl Db for PostgresDb { } } + async fn dismiss_contact_request( + &self, + responder_id: UserId, + requester_id: UserId, + ) -> Result<()> { + let (id_a, id_b, a_to_b) = if responder_id < requester_id { + (responder_id, requester_id, false) + } else { + (requester_id, responder_id, true) + }; + + let query = " + UPDATE contacts + SET should_notify = 'f' + WHERE user_id_a = $1 AND user_id_b = $2 AND a_to_b = $3; + "; + + let result = sqlx::query(query) + .bind(id_a.0) + .bind(id_b.0) + .bind(a_to_b) + .execute(&self.pool) + .await?; + + if result.rows_affected() == 0 { + Err(anyhow!("no such contact request"))?; + } + + Ok(()) + } + // access tokens async fn create_access_token_hash( @@ -628,7 +670,13 @@ pub struct ChannelMessage { pub struct Contacts { pub current: Vec, pub requests_sent: Vec, - pub requests_received: Vec, + pub requests_received: Vec, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct IncomingContactRequest { + pub requesting_user_id: UserId, + pub should_notify: bool, } fn fuzzy_like_string(string: &str) -> String { @@ -886,7 +934,28 @@ pub mod tests { Contacts { current: vec![], requests_sent: vec![], - requests_received: vec![user_1], + requests_received: vec![IncomingContactRequest { + requesting_user_id: user_1, + should_notify: true + }], + }, + ); + + // User 2 dismisses the contact request notification without accepting or rejecting. + // We shouldn't notify them again. + db.dismiss_contact_request(user_1, user_2) + .await + .unwrap_err(); + db.dismiss_contact_request(user_2, user_1).await.unwrap(); + assert_eq!( + db.get_contacts(user_2).await.unwrap(), + Contacts { + current: vec![], + requests_sent: vec![], + requests_received: vec![IncomingContactRequest { + requesting_user_id: user_1, + should_notify: false + }], }, ); @@ -1032,6 +1101,7 @@ pub mod tests { requester_id: UserId, responder_id: UserId, accepted: bool, + should_notify: bool, } impl FakeDb { @@ -1124,7 +1194,10 @@ pub mod tests { if contact.accepted { current.push(contact.requester_id); } else { - requests_received.push(contact.requester_id); + requests_received.push(IncomingContactRequest { + requesting_user_id: contact.requester_id, + should_notify: contact.should_notify, + }); } } } @@ -1162,10 +1235,29 @@ pub mod tests { requester_id, responder_id, accepted: false, + should_notify: true, }); Ok(()) } + async fn dismiss_contact_request( + &self, + responder_id: UserId, + requester_id: UserId, + ) -> Result<()> { + let mut contacts = self.contacts.lock(); + for contact in contacts.iter_mut() { + if contact.requester_id == requester_id && contact.responder_id == responder_id { + if contact.accepted { + return Err(anyhow!("contact already confirmed")); + } + contact.should_notify = false; + return Ok(()); + } + } + Err(anyhow!("no such contact request")) + } + async fn respond_to_contact_request( &self, responder_id: UserId, diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 184592f0330097bae51f0fc72ee7e2adc91cacae..0097d2580ab635fcb494b4804d8c48d0a06b9efa 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -154,6 +154,8 @@ impl Server { .add_request_handler(Server::get_channels) .add_request_handler(Server::get_users) .add_request_handler(Server::fuzzy_search_users) + .add_request_handler(Server::request_contact) + .add_request_handler(Server::respond_to_contact_request) .add_request_handler(Server::join_channel) .add_message_handler(Server::leave_channel) .add_request_handler(Server::send_channel_message) @@ -914,6 +916,48 @@ impl Server { Ok(()) } + async fn request_contact( + self: Arc, + request: TypedEnvelope, + response: Response, + ) -> Result<()> { + let requester_id = self + .store + .read() + .await + .user_id_for_connection(request.sender_id)?; + let responder_id = UserId::from_proto(request.payload.to_user_id); + self.app_state + .db + .send_contact_request(requester_id, responder_id) + .await?; + response.send(proto::Ack {})?; + Ok(()) + } + + async fn respond_to_contact_request( + self: Arc, + request: TypedEnvelope, + response: Response, + ) -> Result<()> { + let responder_id = self + .store + .read() + .await + .user_id_for_connection(request.sender_id)?; + let requester_id = UserId::from_proto(request.payload.requesting_user_id); + self.app_state + .db + .respond_to_contact_request( + responder_id, + requester_id, + request.payload.response == proto::ContactRequestResponse::Accept as i32, + ) + .await?; + response.send(proto::Ack {})?; + Ok(()) + } + #[instrument(skip(self, state, user_ids))] fn update_contacts_for_users<'a>( self: &Arc, @@ -4911,6 +4955,33 @@ mod tests { } } + #[gpui::test(iterations = 10)] + async fn test_contacts_requests(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { + cx_a.foreground().forbid_parking(); + + // Connect to a server as 3 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; + + client_a + .user_store + .read_with(cx_a, |store, _| { + store.request_contact(client_b.user_id().unwrap()) + }) + .await + .unwrap(); + + client_a.user_store.read_with(cx_a, |store, _| { + let contacts = store + .contacts() + .iter() + .map(|contact| contact.user.github_login.clone()) + .collect::>(); + assert_eq!(contacts, &["user_b"]) + }); + } + #[gpui::test(iterations = 10)] async fn test_following(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { cx_a.foreground().forbid_parking(); diff --git a/crates/rpc/src/macros.rs b/crates/rpc/src/macros.rs new file mode 100644 index 0000000000000000000000000000000000000000..38d35893ee70c34615da14aabb561c417f83170b --- /dev/null +++ b/crates/rpc/src/macros.rs @@ -0,0 +1,67 @@ +#[macro_export] +macro_rules! messages { + ($(($name:ident, $priority:ident)),* $(,)?) => { + pub fn build_typed_envelope(sender_id: ConnectionId, envelope: Envelope) -> Option> { + match envelope.payload { + $(Some(envelope::Payload::$name(payload)) => { + Some(Box::new(TypedEnvelope { + sender_id, + original_sender_id: envelope.original_sender_id.map(PeerId), + message_id: envelope.id, + payload, + })) + }, )* + _ => None + } + } + + $( + impl EnvelopedMessage for $name { + const NAME: &'static str = std::stringify!($name); + const PRIORITY: MessagePriority = MessagePriority::$priority; + + fn into_envelope( + self, + id: u32, + responding_to: Option, + original_sender_id: Option, + ) -> Envelope { + Envelope { + id, + responding_to, + original_sender_id, + payload: Some(envelope::Payload::$name(self)), + } + } + + fn from_envelope(envelope: Envelope) -> Option { + if let Some(envelope::Payload::$name(msg)) = envelope.payload { + Some(msg) + } else { + None + } + } + } + )* + }; +} + +#[macro_export] +macro_rules! request_messages { + ($(($request_name:ident, $response_name:ident)),* $(,)?) => { + $(impl RequestMessage for $request_name { + type Response = $response_name; + })* + }; +} + +#[macro_export] +macro_rules! entity_messages { + ($id_field:ident, $($name:ident),* $(,)?) => { + $(impl EntityMessage for $name { + fn remote_entity_id(&self) -> u64 { + self.$id_field + } + })* + }; +} diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index 0935dc6265fbd19cb17f6f57ca1466a80b589122..2674e8a0d8e2f36715339f5779219187388200e1 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -1,4 +1,4 @@ -use super::{ConnectionId, PeerId, TypedEnvelope}; +use super::{entity_messages, messages, request_messages, ConnectionId, PeerId, TypedEnvelope}; use anyhow::{anyhow, Result}; use async_tungstenite::tungstenite::Message as WebSocketMessage; use futures::{SinkExt as _, StreamExt as _}; @@ -73,71 +73,6 @@ impl AnyTypedEnvelope for TypedEnvelope { } } -macro_rules! messages { - ($(($name:ident, $priority:ident)),* $(,)?) => { - pub fn build_typed_envelope(sender_id: ConnectionId, envelope: Envelope) -> Option> { - match envelope.payload { - $(Some(envelope::Payload::$name(payload)) => { - Some(Box::new(TypedEnvelope { - sender_id, - original_sender_id: envelope.original_sender_id.map(PeerId), - message_id: envelope.id, - payload, - })) - }, )* - _ => None - } - } - - $( - impl EnvelopedMessage for $name { - const NAME: &'static str = std::stringify!($name); - const PRIORITY: MessagePriority = MessagePriority::$priority; - - fn into_envelope( - self, - id: u32, - responding_to: Option, - original_sender_id: Option, - ) -> Envelope { - Envelope { - id, - responding_to, - original_sender_id, - payload: Some(envelope::Payload::$name(self)), - } - } - - fn from_envelope(envelope: Envelope) -> Option { - if let Some(envelope::Payload::$name(msg)) = envelope.payload { - Some(msg) - } else { - None - } - } - } - )* - }; -} - -macro_rules! request_messages { - ($(($request_name:ident, $response_name:ident)),* $(,)?) => { - $(impl RequestMessage for $request_name { - type Response = $response_name; - })* - }; -} - -macro_rules! entity_messages { - ($id_field:ident, $($name:ident),* $(,)?) => { - $(impl EntityMessage for $name { - fn remote_entity_id(&self) -> u64 { - self.$id_field - } - })* - }; -} - messages!( (Ack, Foreground), (AddProjectCollaborator, Foreground), @@ -198,6 +133,8 @@ messages!( (ReloadBuffersResponse, Foreground), (RemoveProjectCollaborator, Foreground), (RenameProjectEntry, Foreground), + (RequestContact, Foreground), + (RespondToContactRequest, Foreground), (SaveBuffer, Foreground), (SearchProject, Background), (SearchProjectResponse, Background), @@ -250,6 +187,8 @@ request_messages!( (RegisterProject, RegisterProjectResponse), (RegisterWorktree, Ack), (ReloadBuffers, ReloadBuffersResponse), + (RequestContact, Ack), + (RespondToContactRequest, Ack), (RenameProjectEntry, ProjectEntryResponse), (SaveBuffer, BufferSaved), (SearchProject, SearchProjectResponse), diff --git a/crates/rpc/src/rpc.rs b/crates/rpc/src/rpc.rs index ffddcb9cd3ce1ac232b8eacd6b4ebeb08580c1b4..f21a0ba76e4d27cc82066ed4e5faa837789fcb15 100644 --- a/crates/rpc/src/rpc.rs +++ b/crates/rpc/src/rpc.rs @@ -4,5 +4,6 @@ mod peer; pub mod proto; pub use conn::Connection; pub use peer::*; +mod macros; pub const PROTOCOL_VERSION: u32 = 16; From 4f06dca78b4618a6280199d958d62a96c06dac0c Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 6 May 2022 20:50:59 -0600 Subject: [PATCH 13/53] WIP: Update contacts based on deltas rather than snapshots --- crates/client/src/user.rs | 61 +++++++++++--- crates/collab/src/db.rs | 64 +++++++-------- crates/collab/src/rpc.rs | 144 ++++++++++++++++++-------------- crates/collab/src/rpc/store.rs | 146 ++++++++++++++++++++++++--------- crates/rpc/proto/zed.proto | 10 ++- 5 files changed, 277 insertions(+), 148 deletions(-) diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index 41a40617eef48178d4a81e22a2dcfb40c17fa0d5..d98f813dbe6818fdf2fb9ec9052cf1b03e0ab535 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -36,6 +36,8 @@ pub struct UserStore { update_contacts_tx: watch::Sender>, current_user: watch::Receiver>>, contacts: Vec>, + incoming_contact_requests: Vec>, + outgoing_contact_requests: Vec>, client: Weak, http: Arc, _maintain_contacts: Task<()>, @@ -63,6 +65,8 @@ impl UserStore { users: Default::default(), current_user: current_user_rx, contacts: Default::default(), + incoming_contact_requests: Default::default(), + outgoing_contact_requests: Default::default(), client: Arc::downgrade(&client), update_contacts_tx, http, @@ -121,29 +125,64 @@ impl UserStore { user_ids.insert(contact.user_id); user_ids.extend(contact.projects.iter().flat_map(|w| &w.guests).copied()); } - user_ids.extend(message.pending_requests_to_user_ids.iter()); - user_ids.extend( - message - .pending_requests_from_user_ids - .iter() - .map(|req| req.user_id), - ); + user_ids.extend(message.incoming_requests.iter().map(|req| req.user_id)); + user_ids.extend(message.outgoing_requests.iter()); let load_users = self.get_users(user_ids.into_iter().collect(), cx); cx.spawn(|this, mut cx| async move { load_users.await?; - let mut contacts = Vec::new(); + // Users are fetched in parallel above and cached in call to get_users + // No need to paralellize here + let mut updated_contacts = Vec::new(); for contact in message.contacts { - contacts.push(Arc::new( + updated_contacts.push(Arc::new( Contact::from_proto(contact, &this, &mut cx).await?, )); } + let mut incoming_requests = Vec::new(); + for request in message.incoming_requests { + incoming_requests.push( + this.update(&mut cx, |this, cx| this.fetch_user(request.user_id, cx)) + .await?, + ); + } + + let mut outgoing_requests = Vec::new(); + for requested_user_id in message.outgoing_requests { + outgoing_requests.push( + this.update(&mut cx, |this, cx| this.fetch_user(requested_user_id, cx)) + .await?, + ); + } + + let removed_contacts = + HashSet::::from_iter(message.remove_contacts.iter().copied()); + let removed_incoming_requests = + HashSet::::from_iter(message.remove_incoming_requests.iter().copied()); + let removed_outgoing_requests = + HashSet::::from_iter(message.remove_outgoing_requests.iter().copied()); + this.update(&mut cx, |this, cx| { - contacts.sort_by(|a, b| a.user.github_login.cmp(&b.user.github_login)); - this.contacts = contacts; + this.contacts + .retain(|contact| !removed_contacts.contains(&contact.user.id)); + this.contacts.extend(updated_contacts); + this.contacts + .sort_by(|a, b| a.user.github_login.cmp(&b.user.github_login)); cx.notify(); + + this.incoming_contact_requests + .retain(|user| !removed_incoming_requests.contains(&user.id)); + this.incoming_contact_requests.extend(incoming_requests); + this.incoming_contact_requests + .sort_by(|a, b| a.github_login.cmp(&b.github_login)); + + this.outgoing_contact_requests + .retain(|user| !removed_outgoing_requests.contains(&user.id)); + this.outgoing_contact_requests.extend(outgoing_requests); + this.outgoing_contact_requests + .sort_by(|a, b| a.github_login.cmp(&b.github_login)); }); Ok(()) diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index 5d5f55fc92edf38b9bd69d27fd55ab9ba7c66486..a63369428b6f214756222a0692e47c383d347b14 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -199,8 +199,8 @@ impl Db for PostgresDb { .fetch(&self.pool); let mut current = Vec::new(); - let mut requests_sent = Vec::new(); - let mut requests_received = Vec::new(); + let mut outgoing_requests = Vec::new(); + let mut incoming_requests = Vec::new(); while let Some(row) = rows.next().await { let (user_id_a, user_id_b, a_to_b, accepted, should_notify) = row?; @@ -208,9 +208,9 @@ impl Db for PostgresDb { if accepted { current.push(user_id_b); } else if a_to_b { - requests_sent.push(user_id_b); + outgoing_requests.push(user_id_b); } else { - requests_received.push(IncomingContactRequest { + incoming_requests.push(IncomingContactRequest { requesting_user_id: user_id_b, should_notify, }); @@ -219,20 +219,20 @@ impl Db for PostgresDb { if accepted { current.push(user_id_a); } else if a_to_b { - requests_received.push(IncomingContactRequest { + incoming_requests.push(IncomingContactRequest { requesting_user_id: user_id_a, should_notify, }); } else { - requests_sent.push(user_id_a); + outgoing_requests.push(user_id_a); } } } Ok(Contacts { current, - requests_sent, - requests_received, + outgoing_requests, + incoming_requests, }) } @@ -669,8 +669,8 @@ pub struct ChannelMessage { #[derive(Clone, Debug, PartialEq, Eq)] pub struct Contacts { pub current: Vec, - pub requests_sent: Vec, - pub requests_received: Vec, + pub incoming_requests: Vec, + pub outgoing_requests: Vec, } #[derive(Clone, Debug, PartialEq, Eq)] @@ -914,8 +914,8 @@ pub mod tests { db.get_contacts(user_1).await.unwrap(), Contacts { current: vec![], - requests_sent: vec![], - requests_received: vec![], + outgoing_requests: vec![], + incoming_requests: vec![], }, ); @@ -925,16 +925,16 @@ pub mod tests { db.get_contacts(user_1).await.unwrap(), Contacts { current: vec![], - requests_sent: vec![user_2], - requests_received: vec![], + outgoing_requests: vec![user_2], + incoming_requests: vec![], }, ); assert_eq!( db.get_contacts(user_2).await.unwrap(), Contacts { current: vec![], - requests_sent: vec![], - requests_received: vec![IncomingContactRequest { + outgoing_requests: vec![], + incoming_requests: vec![IncomingContactRequest { requesting_user_id: user_1, should_notify: true }], @@ -951,8 +951,8 @@ pub mod tests { db.get_contacts(user_2).await.unwrap(), Contacts { current: vec![], - requests_sent: vec![], - requests_received: vec![IncomingContactRequest { + outgoing_requests: vec![], + incoming_requests: vec![IncomingContactRequest { requesting_user_id: user_1, should_notify: false }], @@ -972,16 +972,16 @@ pub mod tests { db.get_contacts(user_1).await.unwrap(), Contacts { current: vec![user_2], - requests_sent: vec![], - requests_received: vec![], + outgoing_requests: vec![], + incoming_requests: vec![], }, ); assert_eq!( db.get_contacts(user_2).await.unwrap(), Contacts { current: vec![user_1], - requests_sent: vec![], - requests_received: vec![], + outgoing_requests: vec![], + incoming_requests: vec![], }, ); @@ -997,16 +997,16 @@ pub mod tests { db.get_contacts(user_1).await.unwrap(), Contacts { current: vec![user_2, user_3], - requests_sent: vec![], - requests_received: vec![], + outgoing_requests: vec![], + incoming_requests: vec![], }, ); assert_eq!( db.get_contacts(user_3).await.unwrap(), Contacts { current: vec![user_1], - requests_sent: vec![], - requests_received: vec![], + outgoing_requests: vec![], + incoming_requests: vec![], }, ); @@ -1019,16 +1019,16 @@ pub mod tests { db.get_contacts(user_2).await.unwrap(), Contacts { current: vec![user_1], - requests_sent: vec![], - requests_received: vec![], + outgoing_requests: vec![], + incoming_requests: vec![], }, ); assert_eq!( db.get_contacts(user_3).await.unwrap(), Contacts { current: vec![user_1], - requests_sent: vec![], - requests_received: vec![], + outgoing_requests: vec![], + incoming_requests: vec![], }, ); } @@ -1203,8 +1203,8 @@ pub mod tests { } Ok(Contacts { current, - requests_sent, - requests_received, + outgoing_requests, + incoming_requests, }) } diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 0097d2580ab635fcb494b4804d8c48d0a06b9efa..4a0d3138f53d19d690732673d286c92059726e46 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -246,7 +246,7 @@ impl Server { user_id: UserId, mut send_connection_id: Option>, executor: E, - ) -> impl Future { + ) -> impl Future> { let mut this = self.clone(); let span = info_span!("handle connection", %user_id, %address); async move { @@ -269,10 +269,15 @@ impl Server { let _ = send_connection_id.send(connection_id).await; } + let contacts = this.app_state.db.get_contacts(user_id).await?; + { - let mut state = this.state_mut().await; - state.add_connection(connection_id, user_id); - this.update_contacts_for_users(&*state, &[user_id]); + let mut store = this.store_mut().await; + store.add_connection(connection_id, user_id); + let update_contacts = store.build_initial_contacts_update(contacts); + for connection_id in store.connection_ids_for_user(user_id) { + this.peer.send(connection_id, update_contacts.clone()); + } } let handle_io = handle_io.fuse(); @@ -322,14 +327,15 @@ impl Server { if let Err(error) = this.sign_out(connection_id).await { tracing::error!(%error, "error signing out"); } + + Ok(()) }.instrument(span) } async fn sign_out(self: &mut Arc, connection_id: ConnectionId) -> Result<()> { self.peer.disconnect(connection_id); - let mut state = self.state_mut().await; - let removed_connection = state.remove_connection(connection_id)?; - + let removed_connection = self.store_mut().await.remove_connection(connection_id)?; + for (project_id, project) in removed_connection.hosted_projects { if let Some(share) = project.share { broadcast( @@ -354,8 +360,22 @@ impl Server { ) }); } - - self.update_contacts_for_users(&*state, removed_connection.contact_ids.iter()); + + let contacts_to_update = self.app_state.db.get_contacts(removed_connection.user_id).await?; + let mut update = proto::UpdateContacts::default(); + update.contacts.push(proto::Contact { + user_id: removed_connection.user_id.to_proto(), + projects: Default::default(), + online: false, + }); + + let store = self.store().await; + for user_id in contacts_to_update.current { + for connection_id in store.connection_ids_for_user(user_id) { + self.peer.send(connection_id, update.clone()); + } + } + Ok(()) } @@ -374,7 +394,7 @@ impl Server { response: Response, ) -> Result<()> { let project_id = { - let mut state = self.state_mut().await; + let mut state = self.store_mut().await; let user_id = state.user_id_for_connection(request.sender_id)?; state.register_project(request.sender_id, user_id) }; @@ -386,7 +406,7 @@ impl Server { self: Arc, request: TypedEnvelope, ) -> Result<()> { - let mut state = self.state_mut().await; + let mut state = self.store_mut().await; let project = state.unregister_project(request.payload.project_id, request.sender_id)?; self.update_contacts_for_users(&*state, &project.authorized_user_ids()); Ok(()) @@ -397,7 +417,7 @@ impl Server { request: TypedEnvelope, response: Response, ) -> Result<()> { - let mut state = self.state_mut().await; + let mut state = self.store_mut().await; let project = state.share_project(request.payload.project_id, request.sender_id)?; self.update_contacts_for_users(&mut *state, &project.authorized_user_ids); response.send(proto::Ack {})?; @@ -409,7 +429,7 @@ impl Server { request: TypedEnvelope, ) -> Result<()> { let project_id = request.payload.project_id; - let mut state = self.state_mut().await; + let mut state = self.store_mut().await; let project = state.unshare_project(project_id, request.sender_id)?; broadcast(request.sender_id, project.connection_ids, |conn_id| { self.peer @@ -426,7 +446,7 @@ impl Server { ) -> Result<()> { let project_id = request.payload.project_id; - let state = &mut *self.state_mut().await; + let state = &mut *self.store_mut().await; let user_id = state.user_id_for_connection(request.sender_id)?; let (response_payload, connection_ids, contact_user_ids) = state .join_project(request.sender_id, user_id, project_id) @@ -502,7 +522,7 @@ impl Server { ) -> Result<()> { let sender_id = request.sender_id; let project_id = request.payload.project_id; - let mut state = self.state_mut().await; + let mut state = self.store_mut().await; let worktree = state.leave_project(sender_id, project_id)?; broadcast(sender_id, worktree.connection_ids, |conn_id| { self.peer.send( @@ -528,7 +548,7 @@ impl Server { contact_user_ids.insert(contact_user_id); } - let mut state = self.state_mut().await; + let mut state = self.store_mut().await; let host_user_id = state.user_id_for_connection(request.sender_id)?; contact_user_ids.insert(host_user_id); @@ -562,7 +582,7 @@ impl Server { ) -> Result<()> { let project_id = request.payload.project_id; let worktree_id = request.payload.worktree_id; - let mut state = self.state_mut().await; + let mut state = self.store_mut().await; let (worktree, guest_connection_ids) = state.unregister_worktree(project_id, worktree_id, request.sender_id)?; broadcast(request.sender_id, guest_connection_ids, |conn_id| { @@ -583,7 +603,7 @@ impl Server { request: TypedEnvelope, response: Response, ) -> Result<()> { - let connection_ids = self.state_mut().await.update_worktree( + let connection_ids = self.store_mut().await.update_worktree( request.sender_id, request.payload.project_id, request.payload.worktree_id, @@ -609,7 +629,7 @@ impl Server { .summary .clone() .ok_or_else(|| anyhow!("invalid summary"))?; - let receiver_ids = self.state_mut().await.update_diagnostic_summary( + let receiver_ids = self.store_mut().await.update_diagnostic_summary( request.payload.project_id, request.payload.worktree_id, request.sender_id, @@ -627,7 +647,7 @@ impl Server { self: Arc, request: TypedEnvelope, ) -> Result<()> { - let receiver_ids = self.state_mut().await.start_language_server( + let receiver_ids = self.store_mut().await.start_language_server( request.payload.project_id, request.sender_id, request @@ -648,7 +668,7 @@ impl Server { request: TypedEnvelope, ) -> Result<()> { let receiver_ids = self - .state() + .store() .await .project_connection_ids(request.payload.project_id, request.sender_id)?; broadcast(request.sender_id, receiver_ids, |connection_id| { @@ -667,7 +687,7 @@ impl Server { T: EntityMessage + RequestMessage, { let host_connection_id = self - .state() + .store() .await .read_project(request.payload.remote_entity_id(), request.sender_id)? .host_connection_id; @@ -686,7 +706,7 @@ impl Server { response: Response, ) -> Result<()> { let host = self - .state() + .store() .await .read_project(request.payload.project_id, request.sender_id)? .host_connection_id; @@ -696,7 +716,7 @@ impl Server { .await?; let mut guests = self - .state() + .store() .await .read_project(request.payload.project_id, request.sender_id)? .connection_ids(); @@ -715,7 +735,7 @@ impl Server { response: Response, ) -> Result<()> { let receiver_ids = self - .state() + .store() .await .project_connection_ids(request.payload.project_id, request.sender_id)?; broadcast(request.sender_id, receiver_ids, |connection_id| { @@ -731,7 +751,7 @@ impl Server { request: TypedEnvelope, ) -> Result<()> { let receiver_ids = self - .state() + .store() .await .project_connection_ids(request.payload.project_id, request.sender_id)?; broadcast(request.sender_id, receiver_ids, |connection_id| { @@ -746,7 +766,7 @@ impl Server { request: TypedEnvelope, ) -> Result<()> { let receiver_ids = self - .state() + .store() .await .project_connection_ids(request.payload.project_id, request.sender_id)?; broadcast(request.sender_id, receiver_ids, |connection_id| { @@ -761,7 +781,7 @@ impl Server { request: TypedEnvelope, ) -> Result<()> { let receiver_ids = self - .state() + .store() .await .project_connection_ids(request.payload.project_id, request.sender_id)?; broadcast(request.sender_id, receiver_ids, |connection_id| { @@ -779,7 +799,7 @@ impl Server { let leader_id = ConnectionId(request.payload.leader_id); let follower_id = request.sender_id; if !self - .state() + .store() .await .project_connection_ids(request.payload.project_id, follower_id)? .contains(&leader_id) @@ -800,7 +820,7 @@ impl Server { async fn unfollow(self: Arc, request: TypedEnvelope) -> Result<()> { let leader_id = ConnectionId(request.payload.leader_id); if !self - .state() + .store() .await .project_connection_ids(request.payload.project_id, request.sender_id)? .contains(&leader_id) @@ -817,7 +837,7 @@ impl Server { request: TypedEnvelope, ) -> Result<()> { let connection_ids = self - .state() + .store() .await .project_connection_ids(request.payload.project_id, request.sender_id)?; let leader_id = request @@ -845,7 +865,7 @@ impl Server { response: Response, ) -> Result<()> { let user_id = self - .state() + .store() .await .user_id_for_connection(request.sender_id)?; let channels = self.app_state.db.get_accessible_channels(user_id).await?; @@ -958,28 +978,28 @@ impl Server { Ok(()) } - #[instrument(skip(self, state, user_ids))] - fn update_contacts_for_users<'a>( - self: &Arc, - state: &Store, - user_ids: impl IntoIterator, - ) { - for user_id in user_ids { - let contacts = state.contacts_for_user(*user_id); - for connection_id in state.connection_ids_for_user(*user_id) { - self.peer - .send( - connection_id, - proto::UpdateContacts { - contacts: contacts.clone(), - pending_requests_from_user_ids: Default::default(), - pending_requests_to_user_ids: Default::default(), - }, - ) - .trace_err(); - } - } - } + // #[instrument(skip(self, state, user_ids))] + // fn update_contacts_for_users<'a>( + // self: &Arc, + // state: &Store, + // user_ids: impl IntoIterator, + // ) { + // for user_id in user_ids { + // let contacts = state.contacts_for_user(*user_id); + // for connection_id in state.connection_ids_for_user(*user_id) { + // self.peer + // .send( + // connection_id, + // proto::UpdateContacts { + // contacts: contacts.clone(), + // pending_requests_from_user_ids: Default::default(), + // pending_requests_to_user_ids: Default::default(), + // }, + // ) + // .trace_err(); + // } + // } + // } async fn join_channel( self: Arc, @@ -987,7 +1007,7 @@ impl Server { response: Response, ) -> Result<()> { let user_id = self - .state() + .store() .await .user_id_for_connection(request.sender_id)?; let channel_id = ChannelId::from_proto(request.payload.channel_id); @@ -1000,7 +1020,7 @@ impl Server { Err(anyhow!("access denied"))?; } - self.state_mut() + self.store_mut() .await .join_channel(request.sender_id, channel_id); let messages = self @@ -1029,7 +1049,7 @@ impl Server { request: TypedEnvelope, ) -> Result<()> { let user_id = self - .state() + .store() .await .user_id_for_connection(request.sender_id)?; let channel_id = ChannelId::from_proto(request.payload.channel_id); @@ -1042,7 +1062,7 @@ impl Server { Err(anyhow!("access denied"))?; } - self.state_mut() + self.store_mut() .await .leave_channel(request.sender_id, channel_id); @@ -1058,7 +1078,7 @@ impl Server { let user_id; let connection_ids; { - let state = self.state().await; + let state = self.store().await; user_id = state.user_id_for_connection(request.sender_id)?; connection_ids = state.channel_connection_ids(channel_id)?; } @@ -1112,7 +1132,7 @@ impl Server { response: Response, ) -> Result<()> { let user_id = self - .state() + .store() .await .user_id_for_connection(request.sender_id)?; let channel_id = ChannelId::from_proto(request.payload.channel_id); @@ -1150,7 +1170,7 @@ impl Server { Ok(()) } - async fn state<'a>(self: &'a Arc) -> StoreReadGuard<'a> { + async fn store<'a>(self: &'a Arc) -> StoreReadGuard<'a> { #[cfg(test)] tokio::task::yield_now().await; let guard = self.store.read().await; @@ -1162,7 +1182,7 @@ impl Server { } } - async fn state_mut<'a>(self: &'a Arc) -> StoreWriteGuard<'a> { + async fn store_mut<'a>(self: &'a Arc) -> StoreWriteGuard<'a> { #[cfg(test)] tokio::task::yield_now().await; let guard = self.store.write().await; diff --git a/crates/collab/src/rpc/store.rs b/crates/collab/src/rpc/store.rs index 4737dd2c804ded463841948413d404d09d0858c0..8e430150c9eba4b5f410bb29a53d85bd8f86f8cd 100644 --- a/crates/collab/src/rpc/store.rs +++ b/crates/collab/src/rpc/store.rs @@ -1,4 +1,4 @@ -use crate::db::{ChannelId, UserId}; +use crate::db::{self, ChannelId, UserId}; use anyhow::{anyhow, Result}; use collections::{BTreeMap, HashMap, HashSet}; use rpc::{proto, ConnectionId}; @@ -58,6 +58,7 @@ pub type ReplicaId = u16; #[derive(Default)] pub struct RemovedConnectionState { + pub user_id: UserId, pub hosted_projects: HashMap, pub guest_project_ids: HashMap>, pub contact_ids: HashSet, @@ -151,6 +152,7 @@ impl Store { } let mut result = RemovedConnectionState::default(); + result.user_id = connection.user_id; for project_id in connection.projects.clone() { if let Ok(project) = self.unregister_project(project_id, connection_id) { result.contact_ids.extend(project.authorized_user_ids()); @@ -213,51 +215,115 @@ impl Store { .copied() } - pub fn contacts_for_user(&self, user_id: UserId) -> Vec { - let mut contacts = HashMap::default(); - for project_id in self - .visible_projects_by_user_id - .get(&user_id) - .unwrap_or(&HashSet::default()) - { - let project = &self.projects[project_id]; + pub fn build_initial_contacts_update(&self, contacts: db::Contacts) -> proto::UpdateContacts { + let mut update = proto::UpdateContacts::default(); + for user_id in contacts.current { + update.contacts.push(self.contact_for_user(user_id)); + } - let mut guests = HashSet::default(); - if let Ok(share) = project.share() { - for guest_connection_id in share.guests.keys() { - if let Ok(user_id) = self.user_id_for_connection(*guest_connection_id) { - guests.insert(user_id.to_proto()); - } - } - } + for request in contacts.incoming_requests { + update + .incoming_requests + .push(proto::IncomingContactRequest { + user_id: request.requesting_user_id.to_proto(), + should_notify: request.should_notify, + }) + } - if let Ok(host_user_id) = self.user_id_for_connection(project.host_connection_id) { - let mut worktree_root_names = project - .worktrees - .values() - .filter(|worktree| worktree.visible) - .map(|worktree| worktree.root_name.clone()) - .collect::>(); - worktree_root_names.sort_unstable(); - contacts - .entry(host_user_id) - .or_insert_with(|| proto::Contact { - user_id: host_user_id.to_proto(), - projects: Vec::new(), - }) - .projects - .push(proto::ProjectMetadata { - id: *project_id, - worktree_root_names, - is_shared: project.share.is_some(), - guests: guests.into_iter().collect(), - }); - } + for requested_user_id in contacts.outgoing_requests { + update.outgoing_requests.push(requested_user_id.to_proto()) } - contacts.into_values().collect() + update } + pub fn contact_for_user(&self, user_id: UserId) -> proto::Contact { + proto::Contact { + user_id: user_id.to_proto(), + projects: self.project_metadata_for_user(user_id), + online: self.connection_ids_for_user(user_id).next().is_some(), + } + } + + pub fn project_metadata_for_user(&self, user_id: UserId) -> Vec { + let project_ids = self + .connections_by_user_id + .get(&user_id) + .unwrap_or_else(|| &HashSet::default()) + .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) { + metadata.push(proto::ProjectMetadata { + id: project_id, + is_shared: project.share.is_some(), + worktree_root_names: project + .worktrees + .values() + .map(|worktree| worktree.root_name) + .collect(), + guests: project + .share + .iter() + .flat_map(|share| { + share.guests.values().map(|(_, user_id)| user_id.to_proto()) + }) + .collect(), + }); + } + } + + metadata + } + + // pub fn contacts_for_user(&self, user_id: UserId) -> Vec { + // let mut contacts = HashMap::default(); + // for project_id in self + // .visible_projects_by_user_id + // .get(&user_id) + // .unwrap_or(&HashSet::default()) + // { + // let project = &self.projects[project_id]; + + // let mut guests = HashSet::default(); + // if let Ok(share) = project.share() { + // for guest_connection_id in share.guests.keys() { + // if let Ok(user_id) = self.user_id_for_connection(*guest_connection_id) { + // guests.insert(user_id.to_proto()); + // } + // } + // } + + // if let Ok(host_user_id) = self.user_id_for_connection(project.host_connection_id) { + // let mut worktree_root_names = project + // .worktrees + // .values() + // .filter(|worktree| worktree.visible) + // .map(|worktree| worktree.root_name.clone()) + // .collect::>(); + // worktree_root_names.sort_unstable(); + // contacts + // .entry(host_user_id) + // .or_insert_with(|| proto::Contact { + // user_id: host_user_id.to_proto(), + // projects: Vec::new(), + // }) + // .projects + // .push(proto::ProjectMetadata { + // id: *project_id, + // worktree_root_names, + // is_shared: project.share.is_some(), + // guests: guests.into_iter().collect(), + // }); + // } + // } + + // contacts.into_values().collect() + // } + pub fn register_project( &mut self, host_connection_id: ConnectionId, diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 73f08345f6fd1c4c3d083b4751bfac8563ae0822..5716be9d154820c14320f9e2a81d698e68bd1140 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -591,13 +591,16 @@ message GetChannelMessagesResponse { message UpdateContacts { repeated Contact contacts = 1; - repeated IncomingContactRequest pending_requests_from_user_ids = 2; - repeated uint64 pending_requests_to_user_ids = 3; + repeated uint64 remove_contacts = 2; + repeated IncomingContactRequest incoming_requests = 3; + repeated uint64 remove_incoming_requests = 4; + repeated uint64 outgoing_requests = 5; + repeated uint64 remove_outgoing_requests = 6; } message IncomingContactRequest { uint64 user_id = 1; - bool show_notification = 2; + bool should_notify = 2; } message UpdateDiagnostics { @@ -868,6 +871,7 @@ message ChannelMessage { message Contact { uint64 user_id = 1; repeated ProjectMetadata projects = 2; + bool online = 3; } message ProjectMetadata { From 93dae88cac3132aa401f8151eeb30e12149a0f09 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Sat, 7 May 2022 13:59:21 -0600 Subject: [PATCH 14/53] WIP: Fix compile errors by commenting stuff out --- crates/collab/src/db.rs | 10 ++--- crates/collab/src/rpc.rs | 80 ++++++++++++++++++++-------------- crates/collab/src/rpc/store.rs | 16 +++---- 3 files changed, 60 insertions(+), 46 deletions(-) diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index a63369428b6f214756222a0692e47c383d347b14..6d65fd307d2d328cb499db76b8c32d9dfcdf0591 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -601,7 +601,7 @@ impl Db for PostgresDb { macro_rules! id_type { ($name:ident) => { #[derive( - Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, sqlx::Type, Serialize, + Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, sqlx::Type, Serialize, )] #[sqlx(transparent)] #[serde(transparent)] @@ -1181,20 +1181,20 @@ pub mod tests { async fn get_contacts(&self, id: UserId) -> Result { self.background.simulate_random_delay().await; let mut current = Vec::new(); - let mut requests_sent = Vec::new(); - let mut requests_received = Vec::new(); + let mut outgoing_requests = Vec::new(); + let mut incoming_requests = Vec::new(); for contact in self.contacts.lock().iter() { if contact.requester_id == id { if contact.accepted { current.push(contact.responder_id); } else { - requests_sent.push(contact.responder_id); + outgoing_requests.push(contact.responder_id); } } else if contact.responder_id == id { if contact.accepted { current.push(contact.requester_id); } else { - requests_received.push(IncomingContactRequest { + incoming_requests.push(IncomingContactRequest { requesting_user_id: contact.requester_id, should_notify: contact.should_notify, }); diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 4a0d3138f53d19d690732673d286c92059726e46..6c345ce3183a12a3c2b016075e34425144d399af 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -408,7 +408,8 @@ impl Server { ) -> Result<()> { let mut state = self.store_mut().await; let project = state.unregister_project(request.payload.project_id, request.sender_id)?; - self.update_contacts_for_users(&*state, &project.authorized_user_ids()); + // TODO + // self.update_contacts_for_users(&*state, &project.authorized_user_ids()); Ok(()) } @@ -419,7 +420,8 @@ impl Server { ) -> Result<()> { let mut state = self.store_mut().await; let project = state.share_project(request.payload.project_id, request.sender_id)?; - self.update_contacts_for_users(&mut *state, &project.authorized_user_ids); + // TODO + // self.update_contacts_for_users(&mut *state, &project.authorized_user_ids); response.send(proto::Ack {})?; Ok(()) } @@ -435,7 +437,8 @@ impl Server { self.peer .send(conn_id, proto::UnshareProject { project_id }) }); - self.update_contacts_for_users(&mut *state, &project.authorized_user_ids); + // TODO + // self.update_contacts_for_users(&mut *state, &project.authorized_user_ids); Ok(()) } @@ -511,7 +514,8 @@ impl Server { }, ) }); - self.update_contacts_for_users(state, &contact_user_ids); + // TODO + // self.update_contacts_for_users(state, &contact_user_ids); response.send(response_payload)?; Ok(()) } @@ -533,7 +537,8 @@ impl Server { }, ) }); - self.update_contacts_for_users(&*state, &worktree.authorized_user_ids); + // TODO + // self.update_contacts_for_users(&*state, &worktree.authorized_user_ids); Ok(()) } @@ -571,7 +576,8 @@ impl Server { self.peer .forward_send(request.sender_id, connection_id, request.payload.clone()) }); - self.update_contacts_for_users(&*state, &contact_user_ids); + // TODO + // self.update_contacts_for_users(&*state, &contact_user_ids); response.send(proto::Ack {})?; Ok(()) } @@ -594,7 +600,8 @@ impl Server { }, ) }); - self.update_contacts_for_users(&*state, &worktree.authorized_user_ids); + // TODO + // self.update_contacts_for_users(&*state, &worktree.authorized_user_ids); Ok(()) } @@ -1315,12 +1322,17 @@ pub async fn handle_websocket_request( } let socket_address = socket_address.to_string(); ws.on_upgrade(move |socket| { + use util::ResultExt; let socket = socket .map_ok(to_tungstenite_message) .err_into() .with(|message| async move { Ok(to_axum_message(message)) }); let connection = Connection::new(Box::pin(socket)); - server.handle_connection(connection, socket_address, user_id, None, RealExecutor) + async move { + server.handle_connection(connection, socket_address, user_id, None, RealExecutor) + .await + .log_err(); + } }) } @@ -5769,15 +5781,16 @@ mod tests { if let Some(guest_err) = guest_err { log::error!("{} error - {}", guest.username, guest_err); } - let contacts = server - .store - .read() - .await - .contacts_for_user(guest.current_user_id(&guest_cx)); - assert!(!contacts - .iter() - .flat_map(|contact| &contact.projects) - .any(|project| project.id == host_project_id)); + // TODO + // let contacts = server + // .store + // .read() + // .await + // .contacts_for_user(guest.current_user_id(&guest_cx)); + // assert!(!contacts + // .iter() + // .flat_map(|contact| &contact.projects) + // .any(|project| project.id == host_project_id)); guest .project .as_ref() @@ -5848,22 +5861,23 @@ mod tests { .as_ref() .unwrap() .read_with(&guest_cx, |project, _| assert!(project.is_read_only())); - for user_id in &user_ids { - for contact in server.store.read().await.contacts_for_user(*user_id) { - assert_ne!( - contact.user_id, removed_guest_id.0 as u64, - "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" - ); - } - } - } - } + // TODO + // for user_id in &user_ids { + // for contact in server.store.read().await.contacts_for_user(*user_id) { + // assert_ne!( + // contact.user_id, removed_guest_id.0 as u64, + // "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" + // ); + // } + // } + // } + // } log::info!("{} removed", guest.username); available_guests.push(guest.username.clone()); diff --git a/crates/collab/src/rpc/store.rs b/crates/collab/src/rpc/store.rs index 8e430150c9eba4b5f410bb29a53d85bd8f86f8cd..3ae9e09916ed6a9d312e42bf2ae90a85096a4fa1 100644 --- a/crates/collab/src/rpc/store.rs +++ b/crates/collab/src/rpc/store.rs @@ -246,13 +246,13 @@ impl Store { } pub fn project_metadata_for_user(&self, user_id: UserId) -> Vec { - let project_ids = self - .connections_by_user_id - .get(&user_id) - .unwrap_or_else(|| &HashSet::default()) - .iter() - .filter_map(|connection_id| self.connections.get(connection_id)) - .flat_map(|connection| connection.projects.iter().copied()); + let connection_ids = self.connections_by_user_id.get(&user_id); + let project_ids = connection_ids.iter().flat_map(|connection_ids| { + 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 { @@ -263,7 +263,7 @@ impl Store { worktree_root_names: project .worktrees .values() - .map(|worktree| worktree.root_name) + .map(|worktree| worktree.root_name.clone()) .collect(), guests: project .share From 5d20338f69b44830770848476ae29b870fdddb64 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Sat, 7 May 2022 15:09:27 -0600 Subject: [PATCH 15/53] Get basic test of accepting a contact request passing --- crates/client/src/user.rs | 24 +++++--- crates/collab/src/db.rs | 12 ++-- crates/collab/src/rpc.rs | 105 ++++++++++++++++++++++++++++++--- crates/collab/src/rpc/store.rs | 2 +- crates/rpc/proto/zed.proto | 6 +- 5 files changed, 125 insertions(+), 24 deletions(-) diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index d98f813dbe6818fdf2fb9ec9052cf1b03e0ab535..013d11d4dd0d50827085daea95758febfd827fb3 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -125,7 +125,7 @@ impl UserStore { 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.user_id)); + user_ids.extend(message.incoming_requests.iter().map(|req| req.requester_id)); user_ids.extend(message.outgoing_requests.iter()); let load_users = self.get_users(user_ids.into_iter().collect(), cx); @@ -144,8 +144,10 @@ impl UserStore { let mut incoming_requests = Vec::new(); for request in message.incoming_requests { incoming_requests.push( - this.update(&mut cx, |this, cx| this.fetch_user(request.user_id, cx)) - .await?, + this.update(&mut cx, |this, cx| { + this.fetch_user(request.requester_id, cx) + }) + .await?, ); } @@ -199,12 +201,20 @@ impl UserStore { .is_ok() } - pub fn request_contact(&self, to_user_id: u64) -> impl Future> { + pub fn incoming_contact_requests(&self) -> &[Arc] { + &self.incoming_contact_requests + } + + pub fn outgoing_contact_requests(&self) -> &[Arc] { + &self.outgoing_contact_requests + } + + pub fn request_contact(&self, responder_id: u64) -> impl Future> { let client = self.client.upgrade(); async move { client .ok_or_else(|| anyhow!("not logged in"))? - .request(proto::RequestContact { to_user_id }) + .request(proto::RequestContact { responder_id }) .await?; Ok(()) } @@ -212,7 +222,7 @@ impl UserStore { pub fn respond_to_contact_request( &self, - from_user_id: u64, + requester_id: u64, accept: bool, ) -> impl Future> { let client = self.client.upgrade(); @@ -220,7 +230,7 @@ impl UserStore { client .ok_or_else(|| anyhow!("not logged in"))? .request(proto::RespondToContactRequest { - requesting_user_id: from_user_id, + requester_id, response: if accept { proto::ContactRequestResponse::Accept } else { diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index 6d65fd307d2d328cb499db76b8c32d9dfcdf0591..8a13d4fef4d183e3799384d3b25b03917ecb2c1e 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -211,7 +211,7 @@ impl Db for PostgresDb { outgoing_requests.push(user_id_b); } else { incoming_requests.push(IncomingContactRequest { - requesting_user_id: user_id_b, + requester_id: user_id_b, should_notify, }); } @@ -220,7 +220,7 @@ impl Db for PostgresDb { current.push(user_id_a); } else if a_to_b { incoming_requests.push(IncomingContactRequest { - requesting_user_id: user_id_a, + requester_id: user_id_a, should_notify, }); } else { @@ -675,7 +675,7 @@ pub struct Contacts { #[derive(Clone, Debug, PartialEq, Eq)] pub struct IncomingContactRequest { - pub requesting_user_id: UserId, + pub requester_id: UserId, pub should_notify: bool, } @@ -935,7 +935,7 @@ pub mod tests { current: vec![], outgoing_requests: vec![], incoming_requests: vec![IncomingContactRequest { - requesting_user_id: user_1, + requester_id: user_1, should_notify: true }], }, @@ -953,7 +953,7 @@ pub mod tests { current: vec![], outgoing_requests: vec![], incoming_requests: vec![IncomingContactRequest { - requesting_user_id: user_1, + requester_id: user_1, should_notify: false }], }, @@ -1195,7 +1195,7 @@ pub mod tests { current.push(contact.requester_id); } else { incoming_requests.push(IncomingContactRequest { - requesting_user_id: contact.requester_id, + requester_id: contact.requester_id, should_notify: contact.should_notify, }); } diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 6c345ce3183a12a3c2b016075e34425144d399af..a9639d82f234196f791902594e78569af5f991e5 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -276,7 +276,7 @@ impl Server { store.add_connection(connection_id, user_id); let update_contacts = store.build_initial_contacts_update(contacts); for connection_id in store.connection_ids_for_user(user_id) { - this.peer.send(connection_id, update_contacts.clone()); + this.peer.send(connection_id, update_contacts.clone())?; } } @@ -953,11 +953,30 @@ impl Server { .read() .await .user_id_for_connection(request.sender_id)?; - let responder_id = UserId::from_proto(request.payload.to_user_id); + let responder_id = UserId::from_proto(request.payload.responder_id); self.app_state .db .send_contact_request(requester_id, responder_id) .await?; + + + // Update outgoing contact requests of requester + let mut update = proto::UpdateContacts::default(); + update.outgoing_requests.push(responder_id.to_proto()); + for connection_id in self.store().await.connection_ids_for_user(requester_id) { + self.peer.send(connection_id, update.clone())?; + } + + // Update incoming contact requests of responder + let mut update = proto::UpdateContacts::default(); + update.incoming_requests.push(proto::IncomingContactRequest { + requester_id: requester_id.to_proto(), + should_notify: true, + }); + for connection_id in self.store().await.connection_ids_for_user(responder_id) { + self.peer.send(connection_id, update.clone())?; + } + response.send(proto::Ack {})?; Ok(()) } @@ -972,15 +991,45 @@ impl Server { .read() .await .user_id_for_connection(request.sender_id)?; - let requester_id = UserId::from_proto(request.payload.requesting_user_id); + let requester_id = UserId::from_proto(request.payload.requester_id); + let accept = request.payload.response == proto::ContactRequestResponse::Accept as i32; self.app_state .db .respond_to_contact_request( responder_id, requester_id, - request.payload.response == proto::ContactRequestResponse::Accept as i32, + accept, ) .await?; + + if accept { + // Update responder with new contact + let mut update = proto::UpdateContacts::default(); + update.contacts.push(proto::Contact { + user_id: requester_id.to_proto(), + projects: Default::default(), // TODO + online: true, // TODO + }); + update.remove_incoming_requests.push(requester_id.to_proto()); + for connection_id in self.store.read().await.connection_ids_for_user(responder_id) { + self.peer.send(connection_id, update.clone())?; + } + + // Update requester with new contact + let mut update = proto::UpdateContacts::default(); + update.contacts.push(proto::Contact { + user_id: responder_id.to_proto(), + projects: Default::default(), // TODO + online: true, // TODO + }); + update.remove_outgoing_requests.push(responder_id.to_proto()); + for connection_id in self.store.read().await.connection_ids_for_user(requester_id) { + self.peer.send(connection_id, update.clone())?; + } + } else { + todo!() + } + response.send(proto::Ack {})?; Ok(()) } @@ -4987,8 +5036,8 @@ mod tests { } } - #[gpui::test(iterations = 10)] - async fn test_contacts_requests(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { + #[gpui::test(iterations = 1)] // TODO: More iterations + async fn test_contacts_requests(executor: Arc, cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { cx_a.foreground().forbid_parking(); // Connect to a server as 3 clients. @@ -4996,6 +5045,8 @@ mod tests { let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; + // User A requests that user B become their contact + client_a .user_store .read_with(cx_a, |store, _| { @@ -5003,15 +5054,55 @@ mod tests { }) .await .unwrap(); + + executor.run_until_parked(); + + // Both parties see the pending request appear. User B accepts the request. + + client_a.user_store.read_with(cx_a, |store, _| { + let contacts = store + .outgoing_contact_requests() + .iter() + .map(|contact| contact.github_login.clone()) + .collect::>(); + assert_eq!(contacts, &["user_b"]); + }); + + client_b.user_store.read_with(cx_b, |store, _| { + let contacts = store + .incoming_contact_requests() + .iter() + .map(|contact| contact.github_login.clone()) + .collect::>(); + assert_eq!(contacts, &["user_a"]); + + store.respond_to_contact_request(client_a.user_id().unwrap(), true) + }).await.unwrap(); + + executor.run_until_parked(); + + // User B sees user A as their contact now, and the incoming request from them is removed + client_b.user_store.read_with(cx_b, |store, _| { + let contacts = store + .contacts() + .iter() + .map(|contact| contact.user.github_login.clone()) + .collect::>(); + assert_eq!(contacts, &["user_a"]); + assert!(store.incoming_contact_requests().is_empty()); + }); + // User A sees user B as their contact now, and the outgoing request to them is removed client_a.user_store.read_with(cx_a, |store, _| { let contacts = store .contacts() .iter() .map(|contact| contact.user.github_login.clone()) .collect::>(); - assert_eq!(contacts, &["user_b"]) + assert_eq!(contacts, &["user_b"]); + assert!(store.outgoing_contact_requests().is_empty()); }); + } #[gpui::test(iterations = 10)] diff --git a/crates/collab/src/rpc/store.rs b/crates/collab/src/rpc/store.rs index 3ae9e09916ed6a9d312e42bf2ae90a85096a4fa1..a115db762a1c3955c26295171d2a94351adbc667 100644 --- a/crates/collab/src/rpc/store.rs +++ b/crates/collab/src/rpc/store.rs @@ -225,7 +225,7 @@ impl Store { update .incoming_requests .push(proto::IncomingContactRequest { - user_id: request.requesting_user_id.to_proto(), + requester_id: request.requester_id.to_proto(), should_notify: request.should_notify, }) } diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 5716be9d154820c14320f9e2a81d698e68bd1140..97e0930f4fa2055d812652469f3745ba54ee42c6 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -550,11 +550,11 @@ message UsersResponse { } message RequestContact { - uint64 to_user_id = 1; + uint64 responder_id = 1; } message RespondToContactRequest { - uint64 requesting_user_id = 1; + uint64 requester_id = 1; ContactRequestResponse response = 2; } @@ -599,7 +599,7 @@ message UpdateContacts { } message IncomingContactRequest { - uint64 user_id = 1; + uint64 requester_id = 1; bool should_notify = 2; } From 9b1b61355a29c2d132372cb8148c4886490002a5 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Sun, 8 May 2022 15:19:56 -0600 Subject: [PATCH 16/53] Fully test contact request acceptance * Be sure we send updates to multiple clients for the same user * Be sure we send a full contacts update on initial connection As part of this commit, I fixed an issue where we couldn't disconnect and reconnect in tests. The first disconnect would cause the I/O future to terminate asynchronously, which caused us to sign out even though the active connection didn't belong to that future. I added a guard to ensure that we only sign out if the I/O future is associated with the current connection. --- crates/client/src/client.rs | 14 ++-- crates/client/src/user.rs | 57 +++++++++++++---- crates/collab/src/db.rs | 12 +++- crates/collab/src/rpc.rs | 124 ++++++++++++++++++++++-------------- crates/rpc/src/peer.rs | 10 ++- 5 files changed, 149 insertions(+), 68 deletions(-) diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index e6fc1bf19ad5fe0a886f1e077fdb335af19e5f40..75d5b459e134d97b61d01e949e164ab80baf9d89 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -117,7 +117,7 @@ impl EstablishConnectionError { } } -#[derive(Copy, Clone, Debug)] +#[derive(Copy, Clone, Debug, Eq, PartialEq)] pub enum Status { SignedOut, UpgradeRequired, @@ -293,6 +293,7 @@ impl Client { } fn set_status(self: &Arc, status: Status, cx: &AsyncAppContext) { + log::info!("set status on client {}: {:?}", self.id, status); let mut state = self.state.write(); *state.status.0.borrow_mut() = status; @@ -629,10 +630,13 @@ impl Client { async fn set_connection(self: &Arc, conn: Connection, cx: &AsyncAppContext) { let executor = cx.background(); + log::info!("add connection to peer"); let (connection_id, handle_io, mut incoming) = self .peer .add_connection(conn, move |duration| executor.timer(duration)) .await; + log::info!("set status to connected {}", connection_id); + self.set_status(Status::Connected { connection_id }, cx); cx.foreground() .spawn({ let cx = cx.clone(); @@ -730,15 +734,17 @@ impl Client { }) .detach(); - self.set_status(Status::Connected { connection_id }, cx); - let handle_io = cx.background().spawn(handle_io); let this = self.clone(); let cx = cx.clone(); cx.foreground() .spawn(async move { match handle_io.await { - Ok(()) => this.set_status(Status::SignedOut, &cx), + Ok(()) => { + if *this.status().borrow() == (Status::Connected { connection_id }) { + this.set_status(Status::SignedOut, &cx); + } + } Err(err) => { log::error!("connection error: {:?}", err); this.set_status(Status::ConnectionLost, &cx); diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index 013d11d4dd0d50827085daea95758febfd827fb3..984fe4126c996813ca34d03328d485872532d8e8 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -1,5 +1,5 @@ use super::{http::HttpClient, proto, Client, Status, TypedEnvelope}; -use anyhow::{anyhow, Result}; +use anyhow::{anyhow, Context, Result}; use futures::{future, AsyncReadExt, Future}; use gpui::{AsyncAppContext, Entity, ImageData, ModelContext, ModelHandle, Task}; use postage::{prelude::Stream, sink::Sink, watch}; @@ -120,6 +120,7 @@ impl UserStore { message: proto::UpdateContacts, cx: &mut ModelContext, ) -> Task> { + log::info!("update contacts on client {:?}", message); let mut user_ids = HashSet::new(); for contact in &message.contacts { user_ids.insert(contact.user_id); @@ -167,24 +168,51 @@ impl UserStore { HashSet::::from_iter(message.remove_outgoing_requests.iter().copied()); this.update(&mut cx, |this, cx| { + // Remove contacts this.contacts .retain(|contact| !removed_contacts.contains(&contact.user.id)); - this.contacts.extend(updated_contacts); - this.contacts - .sort_by(|a, b| a.user.github_login.cmp(&b.user.github_login)); + // Update existing contacts and insert new ones + for updated_contact in updated_contacts { + match this + .contacts + .binary_search_by_key(&&updated_contact.user.github_login, |contact| { + &contact.user.github_login + }) { + Ok(ix) => this.contacts[ix] = updated_contact, + Err(ix) => this.contacts.insert(ix, updated_contact), + } + } cx.notify(); + // Remove incoming contact requests this.incoming_contact_requests .retain(|user| !removed_incoming_requests.contains(&user.id)); - this.incoming_contact_requests.extend(incoming_requests); - this.incoming_contact_requests - .sort_by(|a, b| a.github_login.cmp(&b.github_login)); + // Update existing incoming requests and insert new ones + for request in incoming_requests { + match this + .incoming_contact_requests + .binary_search_by_key(&&request.github_login, |contact| { + &contact.github_login + }) { + Ok(ix) => this.incoming_contact_requests[ix] = request, + Err(ix) => this.incoming_contact_requests.insert(ix, request), + } + } + // Remove outgoing contact requests this.outgoing_contact_requests .retain(|user| !removed_outgoing_requests.contains(&user.id)); - this.outgoing_contact_requests.extend(outgoing_requests); - this.outgoing_contact_requests - .sort_by(|a, b| a.github_login.cmp(&b.github_login)); + // Update existing incoming requests and insert new ones + for request in outgoing_requests { + match this + .outgoing_contact_requests + .binary_search_by_key(&&request.github_login, |contact| { + &contact.github_login + }) { + Ok(ix) => this.outgoing_contact_requests[ix] = request, + Err(ix) => this.outgoing_contact_requests.insert(ix, request), + } + } }); Ok(()) @@ -242,6 +270,13 @@ impl UserStore { } } + #[cfg(any(test, feature = "test-support"))] + pub fn clear_contacts(&mut self) { + self.contacts.clear(); + self.incoming_contact_requests.clear(); + self.outgoing_contact_requests.clear(); + } + pub fn get_users( &mut self, mut user_ids: Vec, @@ -297,7 +332,7 @@ impl UserStore { let http = self.http.clone(); cx.spawn_weak(|this, mut cx| async move { if let Some(rpc) = client.upgrade() { - let response = rpc.request(request).await?; + let response = rpc.request(request).await.context("error loading users")?; let users = future::join_all( response .users diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index 8a13d4fef4d183e3799384d3b25b03917ecb2c1e..fd84f39bab9041ece9fbc16fc535255c0c18b945 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -1097,6 +1097,7 @@ pub mod tests { contacts: Mutex>, } + #[derive(Debug)] struct FakeContact { requester_id: UserId, responder_id: UserId, @@ -1166,8 +1167,13 @@ pub mod tests { Ok(ids.iter().filter_map(|id| users.get(id).cloned()).collect()) } - async fn get_user_by_github_login(&self, _github_login: &str) -> Result> { - unimplemented!() + async fn get_user_by_github_login(&self, github_login: &str) -> Result> { + Ok(self + .users + .lock() + .values() + .find(|user| user.github_login == github_login) + .cloned()) } async fn set_user_is_admin(&self, _id: UserId, _is_admin: bool) -> Result<()> { @@ -1183,6 +1189,7 @@ pub mod tests { let mut current = Vec::new(); let mut outgoing_requests = Vec::new(); let mut incoming_requests = Vec::new(); + for contact in self.contacts.lock().iter() { if contact.requester_id == id { if contact.accepted { @@ -1201,6 +1208,7 @@ pub mod tests { } } } + Ok(Contacts { current, outgoing_requests, diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index a9639d82f234196f791902594e78569af5f991e5..cd4868e7c64610d1b050d4f05d97f893b5bf3471 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -274,10 +274,7 @@ impl Server { { let mut store = this.store_mut().await; store.add_connection(connection_id, user_id); - let update_contacts = store.build_initial_contacts_update(contacts); - for connection_id in store.connection_ids_for_user(user_id) { - this.peer.send(connection_id, update_contacts.clone())?; - } + this.peer.send(connection_id, store.build_initial_contacts_update(contacts))?; } let handle_io = handle_io.fuse(); @@ -959,7 +956,6 @@ impl Server { .send_contact_request(requester_id, responder_id) .await?; - // Update outgoing contact requests of requester let mut update = proto::UpdateContacts::default(); update.outgoing_requests.push(responder_id.to_proto()); @@ -5035,18 +5031,21 @@ mod tests { .collect() } } - - #[gpui::test(iterations = 1)] // TODO: More iterations - async fn test_contacts_requests(executor: Arc, cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { + + #[gpui::test(iterations = 10)] + async fn test_contact_requests(executor: Arc, cx_a: &mut TestAppContext, cx_a2: &mut TestAppContext, cx_b: &mut TestAppContext, cx_b2: &mut TestAppContext) { cx_a.foreground().forbid_parking(); // Connect to a server as 3 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_a2 = server.create_client(cx_a2, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; + let client_b2 = server.create_client(cx_b2, "user_b").await; + + assert_eq!(client_a.user_id().unwrap(), client_a2.user_id().unwrap()); // User A requests that user B become their contact - client_a .user_store .read_with(cx_a, |store, _| { @@ -5054,55 +5053,56 @@ mod tests { }) .await .unwrap(); - executor.run_until_parked(); - // Both parties see the pending request appear. User B accepts the request. + // Both users see the pending request appear in all their clients. + assert_eq!(client_a.summarize_contacts(&cx_a).outgoing_requests, &["user_b"]); + assert_eq!(client_a2.summarize_contacts(&cx_a2).outgoing_requests, &["user_b"]); + assert_eq!(client_b.summarize_contacts(&cx_b).incoming_requests, &["user_a"]); + assert_eq!(client_b2.summarize_contacts(&cx_b2).incoming_requests, &["user_a"]); - client_a.user_store.read_with(cx_a, |store, _| { - let contacts = store - .outgoing_contact_requests() - .iter() - .map(|contact| contact.github_login.clone()) - .collect::>(); - assert_eq!(contacts, &["user_b"]); - }); - + // Contact requests are present upon connecting (tested here via disconnect/reconnect) + disconnect_and_reconnect(&client_a, cx_a).await; + disconnect_and_reconnect(&client_b, cx_b).await; + executor.run_until_parked(); + assert_eq!(client_a.summarize_contacts(&cx_a).outgoing_requests, &["user_b"]); + assert_eq!(client_b.summarize_contacts(&cx_b).incoming_requests, &["user_a"]); + + // User B accepts the request. client_b.user_store.read_with(cx_b, |store, _| { - let contacts = store - .incoming_contact_requests() - .iter() - .map(|contact| contact.github_login.clone()) - .collect::>(); - assert_eq!(contacts, &["user_a"]); - store.respond_to_contact_request(client_a.user_id().unwrap(), true) }).await.unwrap(); executor.run_until_parked(); - // User B sees user A as their contact now, and the incoming request from them is removed - client_b.user_store.read_with(cx_b, |store, _| { - let contacts = store - .contacts() - .iter() - .map(|contact| contact.user.github_login.clone()) - .collect::>(); - assert_eq!(contacts, &["user_a"]); - assert!(store.incoming_contact_requests().is_empty()); - }); - - // User A sees user B as their contact now, and the outgoing request to them is removed - client_a.user_store.read_with(cx_a, |store, _| { - let contacts = store - .contacts() - .iter() - .map(|contact| contact.user.github_login.clone()) - .collect::>(); - assert_eq!(contacts, &["user_b"]); - assert!(store.outgoing_contact_requests().is_empty()); - }); + // User B sees user A as their contact now in all client, and the incoming request from them is removed. + let contacts_b = client_b.summarize_contacts(&cx_b); + assert_eq!(contacts_b.current, &["user_a"]); + assert!(contacts_b.incoming_requests.is_empty()); + let contacts_b2 = client_b2.summarize_contacts(&cx_b2); + assert_eq!(contacts_b2.current, &["user_a"]); + assert!(contacts_b2.incoming_requests.is_empty()); + // User A sees user B as their contact now in all clients, and the outgoing request to them is removed. + let contacts_a = client_a.summarize_contacts(&cx_a); + assert_eq!(contacts_a.current, &["user_b"]); + assert!(contacts_a.outgoing_requests.is_empty()); + let contacts_a2 = client_a2.summarize_contacts(&cx_a2); + assert_eq!(contacts_a2.current, &["user_b"]); + assert!(contacts_a2.outgoing_requests.is_empty()); + + // Contacts are present upon connecting (tested here via disconnect/reconnect) + disconnect_and_reconnect(&client_a, cx_a).await; + disconnect_and_reconnect(&client_b, cx_b).await; + executor.run_until_parked(); + assert_eq!(client_a.summarize_contacts(&cx_a).current, &["user_b"]); + // assert_eq!(client_b.summarize_contacts(&cx_b).current, &["user_a"]); + + async fn disconnect_and_reconnect(client: &TestClient, cx: &mut TestAppContext) { + client.disconnect(&cx.to_async()).unwrap(); + client.clear_contacts(cx); + client.authenticate_and_connect(false, &cx.to_async()).await.unwrap(); + } } #[gpui::test(iterations = 10)] @@ -6143,7 +6143,11 @@ mod tests { }); let http = FakeHttpClient::with_404_response(); - let user_id = self.app_state.db.create_user(name, false).await.unwrap(); + let user_id = if let Ok(Some(user)) = self.app_state.db.get_user_by_github_login(name).await { + user.id + } else { + self.app_state.db.create_user(name, false).await.unwrap() + }; let client_name = name.to_string(); let mut client = Client::new(http.clone()); let server = self.server.clone(); @@ -6295,6 +6299,12 @@ mod tests { &self.client } } + + struct ContactsSummary { + pub current: Vec, + pub outgoing_requests: Vec, + pub incoming_requests: Vec, + } impl TestClient { pub fn current_user_id(&self, cx: &TestAppContext) -> UserId { @@ -6310,6 +6320,22 @@ mod tests { .read_with(cx, |user_store, _| user_store.watch_current_user()); while authed_user.next().await.unwrap().is_none() {} } + + fn clear_contacts(&self, cx: &mut TestAppContext) { + self.user_store.update(cx, |store, _| { + store.clear_contacts(); + }); + } + + fn summarize_contacts(&self, cx: &TestAppContext) -> ContactsSummary { + self.user_store.read_with(cx, |store, cx| { + ContactsSummary { + current: store.contacts().iter().map(|contact| contact.user.github_login.clone()).collect(), + outgoing_requests: store.outgoing_contact_requests().iter().map(|user| user.github_login.clone()).collect(), + incoming_requests: store.incoming_contact_requests().iter().map(|user| user.github_login.clone()).collect(), + } + }) + } async fn build_local_project( &mut self, diff --git a/crates/rpc/src/peer.rs b/crates/rpc/src/peer.rs index a2b88f795c69396770e1775473aef68d3e61f339..7d7d1c719495260845391f265770fdde3d76e6a3 100644 --- a/crates/rpc/src/peer.rs +++ b/crates/rpc/src/peer.rs @@ -173,7 +173,10 @@ impl Peer { Err(anyhow!("timed out writing message"))?; } } - None => return Ok(()), + None => { + log::info!("outgoing channel closed"); + return Ok(()) + }, }, incoming = read_message => { let incoming = incoming.context("received invalid RPC message")?; @@ -181,7 +184,10 @@ impl Peer { if let proto::Message::Envelope(incoming) = incoming { match incoming_tx.send(incoming).timeout(RECEIVE_TIMEOUT).await { Some(Ok(_)) => {}, - Some(Err(_)) => return Ok(()), + Some(Err(_)) => { + log::info!("incoming channel closed"); + return Ok(()) + }, None => Err(anyhow!("timed out processing incoming message"))?, } } From 3319e0a61374dd47570c2e5682a1c0561d61c346 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 9 May 2022 10:02:14 +0200 Subject: [PATCH 17/53] Implement contact rejection --- crates/collab/src/rpc.rs | 297 ++++++++++++++++++++++++++++----------- 1 file changed, 218 insertions(+), 79 deletions(-) diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index cd4868e7c64610d1b050d4f05d97f893b5bf3471..b5a26ab47a12ad51fc25c9b6d8da683ba7fb18ac 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -270,7 +270,7 @@ impl Server { } let contacts = this.app_state.db.get_contacts(user_id).await?; - + { let mut store = this.store_mut().await; store.add_connection(connection_id, user_id); @@ -324,7 +324,7 @@ impl Server { if let Err(error) = this.sign_out(connection_id).await { tracing::error!(%error, "error signing out"); } - + Ok(()) }.instrument(span) } @@ -332,7 +332,7 @@ impl Server { async fn sign_out(self: &mut Arc, connection_id: ConnectionId) -> Result<()> { self.peer.disconnect(connection_id); let removed_connection = self.store_mut().await.remove_connection(connection_id)?; - + for (project_id, project) in removed_connection.hosted_projects { if let Some(share) = project.share { broadcast( @@ -357,22 +357,26 @@ impl Server { ) }); } - - let contacts_to_update = self.app_state.db.get_contacts(removed_connection.user_id).await?; + + let contacts_to_update = self + .app_state + .db + .get_contacts(removed_connection.user_id) + .await?; let mut update = proto::UpdateContacts::default(); update.contacts.push(proto::Contact { user_id: removed_connection.user_id.to_proto(), projects: Default::default(), online: false, }); - + let store = self.store().await; for user_id in contacts_to_update.current { for connection_id in store.connection_ids_for_user(user_id) { - self.peer.send(connection_id, update.clone()); + self.peer.send(connection_id, update.clone()).trace_err(); } - } - + } + Ok(()) } @@ -955,24 +959,26 @@ impl Server { .db .send_contact_request(requester_id, responder_id) .await?; - + // Update outgoing contact requests of requester let mut update = proto::UpdateContacts::default(); update.outgoing_requests.push(responder_id.to_proto()); for connection_id in self.store().await.connection_ids_for_user(requester_id) { self.peer.send(connection_id, update.clone())?; } - + // Update incoming contact requests of responder let mut update = proto::UpdateContacts::default(); - update.incoming_requests.push(proto::IncomingContactRequest { - requester_id: requester_id.to_proto(), - should_notify: true, - }); + update + .incoming_requests + .push(proto::IncomingContactRequest { + requester_id: requester_id.to_proto(), + should_notify: true, + }); for connection_id in self.store().await.connection_ids_for_user(responder_id) { self.peer.send(connection_id, update.clone())?; } - + response.send(proto::Ack {})?; Ok(()) } @@ -991,41 +997,51 @@ impl Server { let accept = request.payload.response == proto::ContactRequestResponse::Accept as i32; self.app_state .db - .respond_to_contact_request( - responder_id, - requester_id, - accept, - ) + .respond_to_contact_request(responder_id, requester_id, accept) .await?; - + + // Update responder with new contact + let mut update = proto::UpdateContacts::default(); if accept { - // Update responder with new contact - let mut update = proto::UpdateContacts::default(); update.contacts.push(proto::Contact { user_id: requester_id.to_proto(), projects: Default::default(), // TODO - online: true, // TODO + online: true, // TODO }); - update.remove_incoming_requests.push(requester_id.to_proto()); - for connection_id in self.store.read().await.connection_ids_for_user(responder_id) { - self.peer.send(connection_id, update.clone())?; - } - - // Update requester with new contact - let mut update = proto::UpdateContacts::default(); + } + update + .remove_incoming_requests + .push(requester_id.to_proto()); + for connection_id in self + .store + .read() + .await + .connection_ids_for_user(responder_id) + { + self.peer.send(connection_id, update.clone())?; + } + + // Update requester with new contact + let mut update = proto::UpdateContacts::default(); + if accept { update.contacts.push(proto::Contact { user_id: responder_id.to_proto(), projects: Default::default(), // TODO - online: true, // TODO + online: true, // TODO }); - update.remove_outgoing_requests.push(responder_id.to_proto()); - for connection_id in self.store.read().await.connection_ids_for_user(requester_id) { - self.peer.send(connection_id, update.clone())?; - } - } else { - todo!() } - + update + .remove_outgoing_requests + .push(responder_id.to_proto()); + for connection_id in self + .store + .read() + .await + .connection_ids_for_user(requester_id) + { + self.peer.send(connection_id, update.clone())?; + } + response.send(proto::Ack {})?; Ok(()) } @@ -1374,7 +1390,8 @@ pub async fn handle_websocket_request( .with(|message| async move { Ok(to_axum_message(message)) }); let connection = Connection::new(Box::pin(socket)); async move { - server.handle_connection(connection, socket_address, user_id, None, RealExecutor) + server + .handle_connection(connection, socket_address, user_id, None, RealExecutor) .await .log_err(); } @@ -5031,9 +5048,17 @@ mod tests { .collect() } } - + #[gpui::test(iterations = 10)] - async fn test_contact_requests(executor: Arc, cx_a: &mut TestAppContext, cx_a2: &mut TestAppContext, cx_b: &mut TestAppContext, cx_b2: &mut TestAppContext) { + async fn test_contact_requests( + executor: Arc, + cx_a: &mut TestAppContext, + cx_a2: &mut TestAppContext, + cx_b: &mut TestAppContext, + cx_b2: &mut TestAppContext, + cx_c: &mut TestAppContext, + cx_c2: &mut TestAppContext, + ) { cx_a.foreground().forbid_parking(); // Connect to a server as 3 clients. @@ -5042,10 +5067,14 @@ mod tests { let client_a2 = server.create_client(cx_a2, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; let client_b2 = server.create_client(cx_b2, "user_b").await; - + let client_c = server.create_client(cx_c, "user_c").await; + let client_c2 = server.create_client(cx_c2, "user_c").await; + assert_eq!(client_a.user_id().unwrap(), client_a2.user_id().unwrap()); + assert_eq!(client_b.user_id().unwrap(), client_b2.user_id().unwrap()); + assert_eq!(client_c.user_id().unwrap(), client_c2.user_id().unwrap()); - // User A requests that user B become their contact + // User A and User C request that user B become their contact. client_a .user_store .read_with(cx_a, |store, _| { @@ -5053,36 +5082,78 @@ mod tests { }) .await .unwrap(); + client_c + .user_store + .read_with(cx_c, |store, _| { + store.request_contact(client_b.user_id().unwrap()) + }) + .await + .unwrap(); executor.run_until_parked(); - - // Both users see the pending request appear in all their clients. - assert_eq!(client_a.summarize_contacts(&cx_a).outgoing_requests, &["user_b"]); - assert_eq!(client_a2.summarize_contacts(&cx_a2).outgoing_requests, &["user_b"]); - assert_eq!(client_b.summarize_contacts(&cx_b).incoming_requests, &["user_a"]); - assert_eq!(client_b2.summarize_contacts(&cx_b2).incoming_requests, &["user_a"]); - + + // All users see the pending request appear in all their clients. + assert_eq!( + client_a.summarize_contacts(&cx_a).outgoing_requests, + &["user_b"] + ); + assert_eq!( + client_a2.summarize_contacts(&cx_a2).outgoing_requests, + &["user_b"] + ); + assert_eq!( + client_b.summarize_contacts(&cx_b).incoming_requests, + &["user_a", "user_c"] + ); + assert_eq!( + client_b2.summarize_contacts(&cx_b2).incoming_requests, + &["user_a", "user_c"] + ); + assert_eq!( + client_c.summarize_contacts(&cx_c).outgoing_requests, + &["user_b"] + ); + assert_eq!( + client_c2.summarize_contacts(&cx_c2).outgoing_requests, + &["user_b"] + ); + // Contact requests are present upon connecting (tested here via disconnect/reconnect) disconnect_and_reconnect(&client_a, cx_a).await; disconnect_and_reconnect(&client_b, cx_b).await; + disconnect_and_reconnect(&client_c, cx_c).await; executor.run_until_parked(); - assert_eq!(client_a.summarize_contacts(&cx_a).outgoing_requests, &["user_b"]); - assert_eq!(client_b.summarize_contacts(&cx_b).incoming_requests, &["user_a"]); - - // User B accepts the request. - client_b.user_store.read_with(cx_b, |store, _| { - store.respond_to_contact_request(client_a.user_id().unwrap(), true) - }).await.unwrap(); + assert_eq!( + client_a.summarize_contacts(&cx_a).outgoing_requests, + &["user_b"] + ); + assert_eq!( + client_b.summarize_contacts(&cx_b).incoming_requests, + &["user_a", "user_c"] + ); + assert_eq!( + client_c.summarize_contacts(&cx_c).outgoing_requests, + &["user_b"] + ); + + // User B accepts the request from user A. + client_b + .user_store + .read_with(cx_b, |store, _| { + store.respond_to_contact_request(client_a.user_id().unwrap(), true) + }) + .await + .unwrap(); executor.run_until_parked(); // User B sees user A as their contact now in all client, and the incoming request from them is removed. let contacts_b = client_b.summarize_contacts(&cx_b); assert_eq!(contacts_b.current, &["user_a"]); - assert!(contacts_b.incoming_requests.is_empty()); + assert_eq!(contacts_b.incoming_requests, &["user_c"]); let contacts_b2 = client_b2.summarize_contacts(&cx_b2); assert_eq!(contacts_b2.current, &["user_a"]); - assert!(contacts_b2.incoming_requests.is_empty()); - + assert_eq!(contacts_b2.incoming_requests, &["user_c"]); + // User A sees user B as their contact now in all clients, and the outgoing request to them is removed. let contacts_a = client_a.summarize_contacts(&cx_a); assert_eq!(contacts_a.current, &["user_b"]); @@ -5094,14 +5165,71 @@ mod tests { // Contacts are present upon connecting (tested here via disconnect/reconnect) disconnect_and_reconnect(&client_a, cx_a).await; disconnect_and_reconnect(&client_b, cx_b).await; + disconnect_and_reconnect(&client_c, cx_c).await; + executor.run_until_parked(); + assert_eq!(client_a.summarize_contacts(&cx_a).current, &["user_b"]); + assert_eq!(client_b.summarize_contacts(&cx_b).current, &["user_a"]); + assert_eq!( + client_b.summarize_contacts(&cx_b).incoming_requests, + &["user_c"] + ); + assert!(client_c.summarize_contacts(&cx_c).current.is_empty()); + assert_eq!( + client_c.summarize_contacts(&cx_c).outgoing_requests, + &["user_b"] + ); + + // User B rejects the request from user C. + client_b + .user_store + .read_with(cx_b, |store, _| { + store.respond_to_contact_request(client_c.user_id().unwrap(), false) + }) + .await + .unwrap(); + + executor.run_until_parked(); + + // User B doesn't see user C as their contact, and the incoming request from them is removed. + let contacts_b = client_b.summarize_contacts(&cx_b); + assert_eq!(contacts_b.current, &["user_a"]); + assert!(contacts_b.incoming_requests.is_empty()); + let contacts_b2 = client_b2.summarize_contacts(&cx_b2); + assert_eq!(contacts_b2.current, &["user_a"]); + assert!(contacts_b2.incoming_requests.is_empty()); + + // User C doesn't see user B as their contact, and the outgoing request to them is removed. + let contacts_c = client_c.summarize_contacts(&cx_c); + assert!(contacts_c.current.is_empty()); + assert!(contacts_c.outgoing_requests.is_empty()); + let contacts_c2 = client_c2.summarize_contacts(&cx_c2); + assert!(contacts_c2.current.is_empty()); + assert!(contacts_c2.outgoing_requests.is_empty()); + + // Incoming/outgoing requests are not present upon connecting (tested here via disconnect/reconnect) + disconnect_and_reconnect(&client_a, cx_a).await; + disconnect_and_reconnect(&client_b, cx_b).await; + disconnect_and_reconnect(&client_c, cx_c).await; executor.run_until_parked(); assert_eq!(client_a.summarize_contacts(&cx_a).current, &["user_b"]); - // assert_eq!(client_b.summarize_contacts(&cx_b).current, &["user_a"]); - + assert_eq!(client_b.summarize_contacts(&cx_b).current, &["user_a"]); + assert!(client_b + .summarize_contacts(&cx_b) + .incoming_requests + .is_empty()); + assert!(client_c.summarize_contacts(&cx_c).current.is_empty()); + assert!(client_c + .summarize_contacts(&cx_c) + .outgoing_requests + .is_empty()); + async fn disconnect_and_reconnect(client: &TestClient, cx: &mut TestAppContext) { client.disconnect(&cx.to_async()).unwrap(); client.clear_contacts(cx); - client.authenticate_and_connect(false, &cx.to_async()).await.unwrap(); + client + .authenticate_and_connect(false, &cx.to_async()) + .await + .unwrap(); } } @@ -6143,11 +6271,12 @@ mod tests { }); let http = FakeHttpClient::with_404_response(); - let user_id = if let Ok(Some(user)) = self.app_state.db.get_user_by_github_login(name).await { - user.id - } else { - self.app_state.db.create_user(name, false).await.unwrap() - }; + let user_id = + if let Ok(Some(user)) = self.app_state.db.get_user_by_github_login(name).await { + user.id + } else { + self.app_state.db.create_user(name, false).await.unwrap() + }; let client_name = name.to_string(); let mut client = Client::new(http.clone()); let server = self.server.clone(); @@ -6299,7 +6428,7 @@ mod tests { &self.client } } - + struct ContactsSummary { pub current: Vec, pub outgoing_requests: Vec, @@ -6320,20 +6449,30 @@ mod tests { .read_with(cx, |user_store, _| user_store.watch_current_user()); while authed_user.next().await.unwrap().is_none() {} } - + fn clear_contacts(&self, cx: &mut TestAppContext) { self.user_store.update(cx, |store, _| { store.clear_contacts(); }); } - + fn summarize_contacts(&self, cx: &TestAppContext) -> ContactsSummary { - self.user_store.read_with(cx, |store, cx| { - ContactsSummary { - current: store.contacts().iter().map(|contact| contact.user.github_login.clone()).collect(), - outgoing_requests: store.outgoing_contact_requests().iter().map(|user| user.github_login.clone()).collect(), - incoming_requests: store.incoming_contact_requests().iter().map(|user| user.github_login.clone()).collect(), - } + self.user_store.read_with(cx, |store, cx| ContactsSummary { + current: store + .contacts() + .iter() + .map(|contact| contact.user.github_login.clone()) + .collect(), + outgoing_requests: store + .outgoing_contact_requests() + .iter() + .map(|user| user.github_login.clone()) + .collect(), + incoming_requests: store + .incoming_contact_requests() + .iter() + .map(|user| user.github_login.clone()) + .collect(), }) } From 95d29c4a7b2faa3d390de6d736b1fd34fdfd7afb Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 9 May 2022 15:05:30 +0200 Subject: [PATCH 18/53] Update contacts when peers join/leave and when project status changes --- crates/collab/src/rpc.rs | 555 ++++++++++++++++++--------------- crates/collab/src/rpc/store.rs | 101 +----- 2 files changed, 312 insertions(+), 344 deletions(-) diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index b5a26ab47a12ad51fc25c9b6d8da683ba7fb18ac..2ae12ef59e33206d55229848cafbce211f37127b 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -22,7 +22,7 @@ use axum::{ routing::get, Extension, Router, TypedHeader, }; -use collections::{HashMap, HashSet}; +use collections::HashMap; use futures::{channel::mpsc, future::BoxFuture, FutureExt, SinkExt, StreamExt, TryStreamExt}; use lazy_static::lazy_static; use rpc::{ @@ -49,7 +49,7 @@ use tokio::{ time::Sleep, }; use tower::ServiceBuilder; -use tracing::{info_span, instrument, Instrument}; +use tracing::{info_span, Instrument}; type MessageHandler = Box, Box) -> BoxFuture<'static, ()>>; @@ -335,14 +335,10 @@ impl Server { for (project_id, project) in removed_connection.hosted_projects { if let Some(share) = project.share { - broadcast( - connection_id, - share.guests.keys().copied().collect(), - |conn_id| { - self.peer - .send(conn_id, proto::UnshareProject { project_id }) - }, - ); + broadcast(connection_id, share.guests.keys().copied(), |conn_id| { + self.peer + .send(conn_id, proto::UnshareProject { project_id }) + }); } } @@ -363,14 +359,12 @@ impl Server { .db .get_contacts(removed_connection.user_id) .await?; + let store = self.store().await; let mut update = proto::UpdateContacts::default(); - update.contacts.push(proto::Contact { - user_id: removed_connection.user_id.to_proto(), - projects: Default::default(), - online: false, - }); + update + .contacts + .push(store.contact_for_user(removed_connection.user_id)); - let store = self.store().await; for user_id in contacts_to_update.current { for connection_id in store.connection_ids_for_user(user_id) { self.peer.send(connection_id, update.clone()).trace_err(); @@ -407,10 +401,13 @@ impl Server { self: Arc, request: TypedEnvelope, ) -> Result<()> { - let mut state = self.store_mut().await; - let project = state.unregister_project(request.payload.project_id, request.sender_id)?; - // TODO - // self.update_contacts_for_users(&*state, &project.authorized_user_ids()); + let user_id = { + let mut state = self.store_mut().await; + state.unregister_project(request.payload.project_id, request.sender_id)?; + state.user_id_for_connection(request.sender_id)? + }; + + self.update_user_contacts(user_id).await?; Ok(()) } @@ -419,27 +416,55 @@ impl Server { request: TypedEnvelope, response: Response, ) -> Result<()> { - let mut state = self.store_mut().await; - let project = state.share_project(request.payload.project_id, request.sender_id)?; - // TODO - // self.update_contacts_for_users(&mut *state, &project.authorized_user_ids); + let user_id = { + let mut state = self.store_mut().await; + state.share_project(request.payload.project_id, request.sender_id)?; + state.user_id_for_connection(request.sender_id)? + }; + self.update_user_contacts(user_id).await?; response.send(proto::Ack {})?; Ok(()) } + async fn update_user_contacts(self: &Arc, user_id: UserId) -> Result<()> { + let contacts = self.app_state.db.get_contacts(user_id).await?; + let store = self.store().await; + let updated_contact = store.contact_for_user(user_id); + for contact_user_id in contacts.current { + for contact_conn_id in store.connection_ids_for_user(contact_user_id) { + self.peer + .send( + contact_conn_id, + proto::UpdateContacts { + contacts: vec![updated_contact.clone()], + remove_contacts: Default::default(), + incoming_requests: Default::default(), + remove_incoming_requests: Default::default(), + outgoing_requests: Default::default(), + remove_outgoing_requests: Default::default(), + }, + ) + .trace_err(); + } + } + Ok(()) + } + async fn unshare_project( self: Arc, request: TypedEnvelope, ) -> Result<()> { let project_id = request.payload.project_id; - let mut state = self.store_mut().await; - let project = state.unshare_project(project_id, request.sender_id)?; - broadcast(request.sender_id, project.connection_ids, |conn_id| { - self.peer - .send(conn_id, proto::UnshareProject { project_id }) - }); - // TODO - // self.update_contacts_for_users(&mut *state, &project.authorized_user_ids); + let project; + { + let mut state = self.store_mut().await; + project = state.unshare_project(project_id, request.sender_id)?; + broadcast(request.sender_id, project.connection_ids, |conn_id| { + self.peer + .send(conn_id, proto::UnshareProject { project_id }) + }); + } + self.update_user_contacts(project.host_user_id).await?; Ok(()) } @@ -449,74 +474,74 @@ impl Server { response: Response, ) -> Result<()> { let project_id = request.payload.project_id; - - let state = &mut *self.store_mut().await; - let user_id = state.user_id_for_connection(request.sender_id)?; - let (response_payload, connection_ids, contact_user_ids) = state - .join_project(request.sender_id, user_id, project_id) - .and_then(|joined| { - let share = joined.project.share()?; - let peer_count = share.guests.len(); - let mut collaborators = Vec::with_capacity(peer_count); - collaborators.push(proto::Collaborator { - peer_id: joined.project.host_connection_id.0, - replica_id: 0, - user_id: joined.project.host_user_id.to_proto(), - }); - let worktrees = share - .worktrees - .iter() - .filter_map(|(id, shared_worktree)| { - let worktree = joined.project.worktrees.get(&id)?; - Some(proto::Worktree { - id: *id, - root_name: worktree.root_name.clone(), - entries: shared_worktree.entries.values().cloned().collect(), - diagnostic_summaries: shared_worktree - .diagnostic_summaries - .values() - .cloned() - .collect(), - visible: worktree.visible, - scan_id: shared_worktree.scan_id, - }) + let response_payload; + let host_user_id; + { + let state = &mut *self.store_mut().await; + let user_id = state.user_id_for_connection(request.sender_id)?; + let joined = state.join_project(request.sender_id, user_id, project_id)?; + let share = joined.project.share()?; + let peer_count = share.guests.len(); + let mut collaborators = Vec::with_capacity(peer_count); + collaborators.push(proto::Collaborator { + peer_id: joined.project.host_connection_id.0, + replica_id: 0, + user_id: joined.project.host_user_id.to_proto(), + }); + let worktrees = share + .worktrees + .iter() + .filter_map(|(id, shared_worktree)| { + let worktree = joined.project.worktrees.get(&id)?; + Some(proto::Worktree { + id: *id, + root_name: worktree.root_name.clone(), + entries: shared_worktree.entries.values().cloned().collect(), + diagnostic_summaries: shared_worktree + .diagnostic_summaries + .values() + .cloned() + .collect(), + visible: worktree.visible, + scan_id: shared_worktree.scan_id, }) - .collect(); - for (peer_conn_id, (peer_replica_id, peer_user_id)) in &share.guests { - if *peer_conn_id != request.sender_id { - collaborators.push(proto::Collaborator { - peer_id: peer_conn_id.0, - replica_id: *peer_replica_id as u32, - user_id: peer_user_id.to_proto(), - }); - } + }) + .collect(); + for (peer_conn_id, (peer_replica_id, peer_user_id)) in &share.guests { + if *peer_conn_id != request.sender_id { + collaborators.push(proto::Collaborator { + peer_id: peer_conn_id.0, + replica_id: *peer_replica_id as u32, + user_id: peer_user_id.to_proto(), + }); } - let response = proto::JoinProjectResponse { - worktrees, - replica_id: joined.replica_id as u32, - collaborators, - language_servers: joined.project.language_servers.clone(), - }; - let connection_ids = joined.project.connection_ids(); - let contact_user_ids = joined.project.authorized_user_ids(); - Ok((response, connection_ids, contact_user_ids)) - })?; - - broadcast(request.sender_id, connection_ids, |conn_id| { - self.peer.send( - conn_id, - proto::AddProjectCollaborator { - project_id, - collaborator: Some(proto::Collaborator { - peer_id: request.sender_id.0, - replica_id: response_payload.replica_id, - user_id: user_id.to_proto(), - }), + } + response_payload = proto::JoinProjectResponse { + worktrees, + replica_id: joined.replica_id as u32, + collaborators, + language_servers: joined.project.language_servers.clone(), + }; + host_user_id = joined.project.host_user_id; + broadcast( + request.sender_id, + joined.project.connection_ids(), + |conn_id| { + self.peer.send( + conn_id, + proto::AddProjectCollaborator { + project_id, + collaborator: Some(proto::Collaborator { + peer_id: request.sender_id.0, + replica_id: response_payload.replica_id, + user_id: user_id.to_proto(), + }), + }, + ) }, - ) - }); - // TODO - // self.update_contacts_for_users(state, &contact_user_ids); + ); + } + self.update_user_contacts(host_user_id).await?; response.send(response_payload)?; Ok(()) } @@ -527,19 +552,21 @@ impl Server { ) -> Result<()> { let sender_id = request.sender_id; let project_id = request.payload.project_id; - let mut state = self.store_mut().await; - let worktree = state.leave_project(sender_id, project_id)?; - broadcast(sender_id, worktree.connection_ids, |conn_id| { - self.peer.send( - conn_id, - proto::RemoveProjectCollaborator { - project_id, - peer_id: sender_id.0, - }, - ) - }); - // TODO - // self.update_contacts_for_users(&*state, &worktree.authorized_user_ids); + let project; + { + let mut state = self.store_mut().await; + project = state.leave_project(sender_id, project_id)?; + broadcast(sender_id, project.connection_ids, |conn_id| { + self.peer.send( + conn_id, + proto::RemoveProjectCollaborator { + project_id, + peer_id: sender_id.0, + }, + ) + }); + } + self.update_user_contacts(project.host_user_id).await?; Ok(()) } @@ -548,37 +575,30 @@ impl Server { request: TypedEnvelope, response: Response, ) -> Result<()> { - let mut contact_user_ids = HashSet::default(); - for github_login in &request.payload.authorized_logins { - let contact_user_id = self.app_state.db.create_user(github_login, false).await?; - contact_user_ids.insert(contact_user_id); - } - - let mut state = self.store_mut().await; - let host_user_id = state.user_id_for_connection(request.sender_id)?; - contact_user_ids.insert(host_user_id); - - let contact_user_ids = contact_user_ids.into_iter().collect::>(); - let guest_connection_ids = state - .read_project(request.payload.project_id, request.sender_id)? - .guest_connection_ids(); - state.register_worktree( - request.payload.project_id, - request.payload.worktree_id, - request.sender_id, - Worktree { - authorized_user_ids: contact_user_ids.clone(), - root_name: request.payload.root_name.clone(), - visible: request.payload.visible, - }, - )?; + let host_user_id; + { + let mut state = self.store_mut().await; + host_user_id = state.user_id_for_connection(request.sender_id)?; + + let guest_connection_ids = state + .read_project(request.payload.project_id, request.sender_id)? + .guest_connection_ids(); + state.register_worktree( + request.payload.project_id, + request.payload.worktree_id, + request.sender_id, + Worktree { + root_name: request.payload.root_name.clone(), + visible: request.payload.visible, + }, + )?; - broadcast(request.sender_id, guest_connection_ids, |connection_id| { - self.peer - .forward_send(request.sender_id, connection_id, request.payload.clone()) - }); - // TODO - // self.update_contacts_for_users(&*state, &contact_user_ids); + broadcast(request.sender_id, guest_connection_ids, |connection_id| { + self.peer + .forward_send(request.sender_id, connection_id, request.payload.clone()) + }); + } + self.update_user_contacts(host_user_id).await?; response.send(proto::Ack {})?; Ok(()) } @@ -587,22 +607,25 @@ impl Server { self: Arc, request: TypedEnvelope, ) -> Result<()> { + let host_user_id; let project_id = request.payload.project_id; let worktree_id = request.payload.worktree_id; - let mut state = self.store_mut().await; - let (worktree, guest_connection_ids) = - state.unregister_worktree(project_id, worktree_id, request.sender_id)?; - broadcast(request.sender_id, guest_connection_ids, |conn_id| { - self.peer.send( - conn_id, - proto::UnregisterWorktree { - project_id, - worktree_id, - }, - ) - }); - // TODO - // self.update_contacts_for_users(&*state, &worktree.authorized_user_ids); + { + let mut state = self.store_mut().await; + let (_, guest_connection_ids) = + state.unregister_worktree(project_id, worktree_id, request.sender_id)?; + host_user_id = state.user_id_for_connection(request.sender_id)?; + broadcast(request.sender_id, guest_connection_ids, |conn_id| { + self.peer.send( + conn_id, + proto::UnregisterWorktree { + project_id, + worktree_id, + }, + ) + }); + } + self.update_user_contacts(host_user_id).await?; Ok(()) } @@ -950,8 +973,7 @@ impl Server { response: Response, ) -> Result<()> { let requester_id = self - .store - .read() + .store() .await .user_id_for_connection(request.sender_id)?; let responder_id = UserId::from_proto(request.payload.responder_id); @@ -989,8 +1011,7 @@ impl Server { response: Response, ) -> Result<()> { let responder_id = self - .store - .read() + .store() .await .user_id_for_connection(request.sender_id)?; let requester_id = UserId::from_proto(request.payload.requester_id); @@ -1000,45 +1021,28 @@ impl Server { .respond_to_contact_request(responder_id, requester_id, accept) .await?; + let store = self.store().await; // Update responder with new contact let mut update = proto::UpdateContacts::default(); if accept { - update.contacts.push(proto::Contact { - user_id: requester_id.to_proto(), - projects: Default::default(), // TODO - online: true, // TODO - }); + update.contacts.push(store.contact_for_user(requester_id)); } update .remove_incoming_requests .push(requester_id.to_proto()); - for connection_id in self - .store - .read() - .await - .connection_ids_for_user(responder_id) - { + for connection_id in store.connection_ids_for_user(responder_id) { self.peer.send(connection_id, update.clone())?; } // Update requester with new contact let mut update = proto::UpdateContacts::default(); if accept { - update.contacts.push(proto::Contact { - user_id: responder_id.to_proto(), - projects: Default::default(), // TODO - online: true, // TODO - }); + update.contacts.push(store.contact_for_user(responder_id)); } update .remove_outgoing_requests .push(responder_id.to_proto()); - for connection_id in self - .store - .read() - .await - .connection_ids_for_user(requester_id) - { + for connection_id in store.connection_ids_for_user(requester_id) { self.peer.send(connection_id, update.clone())?; } @@ -1312,9 +1316,11 @@ impl Executor for RealExecutor { } } -#[instrument(skip(f))] -fn broadcast(sender_id: ConnectionId, receiver_ids: Vec, mut f: F) -where +fn broadcast( + sender_id: ConnectionId, + receiver_ids: impl IntoIterator, + mut f: F, +) where F: FnMut(ConnectionId) -> anyhow::Result<()>, { for receiver_id in receiver_ids { @@ -1461,7 +1467,7 @@ mod tests { self, test::FakeHttpClient, Channel, ChannelDetails, ChannelList, Client, Credentials, EstablishConnectionError, UserStore, RECEIVE_TIMEOUT, }; - use collections::BTreeMap; + use collections::{BTreeMap, HashSet}; use editor::{ self, ConfirmCodeAction, ConfirmCompletion, ConfirmRename, Editor, Input, Redo, Rename, ToOffset, ToggleCodeActions, Undo, @@ -4890,6 +4896,7 @@ mod tests { #[gpui::test(iterations = 10)] async fn test_contacts( + deterministic: Arc, cx_a: &mut TestAppContext, cx_b: &mut TestAppContext, cx_c: &mut TestAppContext, @@ -4903,15 +4910,26 @@ mod tests { 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; + server + .make_contacts(vec![ + (&client_a, cx_a), + (&client_b, cx_b), + (&client_c, cx_c), + ]) + .await; + deterministic.run_until_parked(); + client_a.user_store.read_with(cx_a, |store, _| { + assert_eq!(contacts(store), [("user_b", vec![]), ("user_c", vec![])]) + }); + client_b.user_store.read_with(cx_b, |store, _| { + assert_eq!(contacts(store), [("user_a", vec![]), ("user_c", vec![])]) + }); + client_c.user_store.read_with(cx_c, |store, _| { + assert_eq!(contacts(store), [("user_a", vec![]), ("user_b", vec![])]) + }); // Share a worktree as client A. - fs.insert_tree( - "/a", - json!({ - ".zed.toml": r#"collaborators = ["user_b", "user_c"]"#, - }), - ) - .await; + fs.create_dir(Path::new("/a")).await.unwrap(); let project_a = cx_a.update(|cx| { Project::local( @@ -4932,24 +4950,22 @@ mod tests { .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete()) .await; - client_a - .user_store - .condition(&cx_a, |user_store, _| { - contacts(user_store) == vec![("user_a", vec![("a", false, vec![])])] - }) - .await; - client_b - .user_store - .condition(&cx_b, |user_store, _| { - contacts(user_store) == vec![("user_a", vec![("a", false, vec![])])] - }) - .await; - client_c - .user_store - .condition(&cx_c, |user_store, _| { - contacts(user_store) == vec![("user_a", vec![("a", false, vec![])])] - }) - .await; + deterministic.run_until_parked(); + client_a.user_store.read_with(cx_a, |store, _| { + assert_eq!(contacts(store), [("user_b", vec![]), ("user_c", vec![])]) + }); + client_b.user_store.read_with(cx_b, |store, _| { + assert_eq!( + contacts(store), + [("user_a", vec![("a", false, vec![])]), ("user_c", vec![])] + ) + }); + client_c.user_store.read_with(cx_c, |store, _| { + assert_eq!( + contacts(store), + [("user_a", vec![("a", false, vec![])]), ("user_b", vec![])] + ) + }); let project_id = project_a .update(cx_a, |project, _| project.next_remote_id()) @@ -4958,24 +4974,22 @@ mod tests { .update(cx_a, |project, cx| project.share(cx)) .await .unwrap(); - client_a - .user_store - .condition(&cx_a, |user_store, _| { - contacts(user_store) == vec![("user_a", vec![("a", true, vec![])])] - }) - .await; - client_b - .user_store - .condition(&cx_b, |user_store, _| { - contacts(user_store) == vec![("user_a", vec![("a", true, vec![])])] - }) - .await; - client_c - .user_store - .condition(&cx_c, |user_store, _| { - contacts(user_store) == vec![("user_a", vec![("a", true, vec![])])] - }) - .await; + deterministic.run_until_parked(); + client_a.user_store.read_with(cx_a, |store, _| { + assert_eq!(contacts(store), [("user_b", vec![]), ("user_c", vec![])]) + }); + client_b.user_store.read_with(cx_b, |store, _| { + assert_eq!( + contacts(store), + [("user_a", vec![("a", true, vec![])]), ("user_c", vec![])] + ) + }); + client_c.user_store.read_with(cx_c, |store, _| { + assert_eq!( + contacts(store), + [("user_a", vec![("a", true, vec![])]), ("user_b", vec![])] + ) + }); let _project_b = Project::remote( project_id, @@ -4987,25 +5001,28 @@ mod tests { ) .await .unwrap(); - - client_a - .user_store - .condition(&cx_a, |user_store, _| { - contacts(user_store) == vec![("user_a", vec![("a", true, vec!["user_b"])])] - }) - .await; - client_b - .user_store - .condition(&cx_b, |user_store, _| { - contacts(user_store) == vec![("user_a", vec![("a", true, vec!["user_b"])])] - }) - .await; - client_c - .user_store - .condition(&cx_c, |user_store, _| { - contacts(user_store) == vec![("user_a", vec![("a", true, vec!["user_b"])])] - }) - .await; + deterministic.run_until_parked(); + client_a.user_store.read_with(cx_a, |store, _| { + assert_eq!(contacts(store), [("user_b", vec![]), ("user_c", vec![])]) + }); + client_b.user_store.read_with(cx_b, |store, _| { + assert_eq!( + contacts(store), + [ + ("user_a", vec![("a", true, vec!["user_b"])]), + ("user_c", vec![]) + ] + ) + }); + client_c.user_store.read_with(cx_c, |store, _| { + assert_eq!( + contacts(store), + [ + ("user_a", vec![("a", true, vec!["user_b"])]), + ("user_b", vec![]) + ] + ) + }); project_a .condition(&cx_a, |project, _| { @@ -5014,18 +5031,16 @@ mod tests { .await; cx_a.update(move |_| drop(project_a)); - client_a - .user_store - .condition(&cx_a, |user_store, _| contacts(user_store) == vec![]) - .await; - client_b - .user_store - .condition(&cx_b, |user_store, _| contacts(user_store) == vec![]) - .await; - client_c - .user_store - .condition(&cx_c, |user_store, _| contacts(user_store) == vec![]) - .await; + deterministic.run_until_parked(); + client_a.user_store.read_with(cx_a, |store, _| { + assert_eq!(contacts(store), [("user_b", vec![]), ("user_c", vec![])]) + }); + client_b.user_store.read_with(cx_b, |store, _| { + assert_eq!(contacts(store), [("user_a", vec![]), ("user_c", vec![])]) + }); + client_c.user_store.read_with(cx_c, |store, _| { + assert_eq!(contacts(store), [("user_a", vec![]), ("user_b", vec![])]) + }); fn contacts(user_store: &UserStore) -> Vec<(&str, Vec<(&str, bool, Vec<&str>)>)> { user_store @@ -6370,6 +6385,28 @@ mod tests { self.forbid_connections.store(false, SeqCst); } + async fn make_contacts(&self, mut clients: Vec<(&TestClient, &mut TestAppContext)>) { + while let Some((client_a, cx_a)) = clients.pop() { + for (client_b, cx_b) in &mut clients { + client_a + .user_store + .update(cx_a, |store, _| { + store.request_contact(client_b.user_id().unwrap()) + }) + .await + .unwrap(); + cx_a.foreground().run_until_parked(); + client_b + .user_store + .update(*cx_b, |store, _| { + store.respond_to_contact_request(client_a.user_id().unwrap(), true) + }) + .await + .unwrap(); + } + } + } + async fn build_app_state(test_db: &TestDb) -> Arc { Arc::new(AppState { db: test_db.db().clone(), @@ -6457,7 +6494,7 @@ mod tests { } fn summarize_contacts(&self, cx: &TestAppContext) -> ContactsSummary { - self.user_store.read_with(cx, |store, cx| ContactsSummary { + self.user_store.read_with(cx, |store, _| ContactsSummary { current: store .contacts() .iter() diff --git a/crates/collab/src/rpc/store.rs b/crates/collab/src/rpc/store.rs index a115db762a1c3955c26295171d2a94351adbc667..e655af79012dd035c3944df1ad9fc57dd1decffc 100644 --- a/crates/collab/src/rpc/store.rs +++ b/crates/collab/src/rpc/store.rs @@ -10,7 +10,6 @@ pub struct Store { connections: HashMap, connections_by_user_id: HashMap>, projects: HashMap, - visible_projects_by_user_id: HashMap>, channels: HashMap, next_project_id: u64, } @@ -30,7 +29,6 @@ pub struct Project { } pub struct Worktree { - pub authorized_user_ids: Vec, pub root_name: String, pub visible: bool, } @@ -69,18 +67,16 @@ pub struct JoinedProject<'a> { pub project: &'a Project, } -pub struct SharedProject { - pub authorized_user_ids: Vec, -} +pub struct SharedProject {} pub struct UnsharedProject { pub connection_ids: Vec, - pub authorized_user_ids: Vec, + pub host_user_id: UserId, } pub struct LeftProject { pub connection_ids: Vec, - pub authorized_user_ids: Vec, + pub host_user_id: UserId, } #[derive(Copy, Clone)] @@ -155,13 +151,11 @@ impl Store { result.user_id = connection.user_id; for project_id in connection.projects.clone() { if let Ok(project) = self.unregister_project(project_id, connection_id) { - result.contact_ids.extend(project.authorized_user_ids()); result.hosted_projects.insert(project_id, project); } else if let Ok(project) = self.leave_project(connection_id, project_id) { result .guest_project_ids .insert(project_id, project.connection_ids); - result.contact_ids.extend(project.authorized_user_ids); } } @@ -215,6 +209,14 @@ impl Store { .copied() } + pub fn is_user_online(&self, user_id: UserId) -> bool { + !self + .connections_by_user_id + .get(&user_id) + .unwrap_or(&Default::default()) + .is_empty() + } + pub fn build_initial_contacts_update(&self, contacts: db::Contacts) -> proto::UpdateContacts { let mut update = proto::UpdateContacts::default(); for user_id in contacts.current { @@ -241,7 +243,7 @@ impl Store { proto::Contact { user_id: user_id.to_proto(), projects: self.project_metadata_for_user(user_id), - online: self.connection_ids_for_user(user_id).next().is_some(), + online: self.is_user_online(user_id), } } @@ -359,13 +361,6 @@ impl Store { .get_mut(&project_id) .ok_or_else(|| anyhow!("no such project"))?; if project.host_connection_id == connection_id { - for authorized_user_id in &worktree.authorized_user_ids { - self.visible_projects_by_user_id - .entry(*authorized_user_id) - .or_default() - .insert(project_id); - } - project.worktrees.insert(worktree_id, worktree); if let Ok(share) = project.share_mut() { share.worktrees.insert(worktree_id, Default::default()); @@ -385,14 +380,6 @@ impl Store { match self.projects.entry(project_id) { hash_map::Entry::Occupied(e) => { if e.get().host_connection_id == connection_id { - for user_id in e.get().authorized_user_ids() { - if let hash_map::Entry::Occupied(mut projects) = - self.visible_projects_by_user_id.entry(user_id) - { - projects.get_mut().remove(&project_id); - } - } - let project = e.remove(); if let Some(host_connection) = self.connections.get_mut(&connection_id) { @@ -441,16 +428,6 @@ impl Store { share.worktrees.remove(&worktree_id); } - for authorized_user_id in &worktree.authorized_user_ids { - if let Some(visible_projects) = - self.visible_projects_by_user_id.get_mut(authorized_user_id) - { - if !project.has_authorized_user_id(*authorized_user_id) { - visible_projects.remove(&project_id); - } - } - } - Ok((worktree, guest_connection_ids)) } @@ -466,9 +443,7 @@ impl Store { share.worktrees.insert(*worktree_id, Default::default()); } project.share = Some(share); - return Ok(SharedProject { - authorized_user_ids: project.authorized_user_ids(), - }); + return Ok(SharedProject {}); } } Err(anyhow!("no such project"))? @@ -490,7 +465,6 @@ impl Store { } let connection_ids = project.connection_ids(); - let authorized_user_ids = project.authorized_user_ids(); if let Some(share) = project.share.take() { for connection_id in share.guests.into_keys() { if let Some(connection) = self.connections.get_mut(&connection_id) { @@ -500,7 +474,7 @@ impl Store { Ok(UnsharedProject { connection_ids, - authorized_user_ids, + host_user_id: project.host_user_id, }) } else { Err(anyhow!("project is not shared"))? @@ -564,13 +538,6 @@ impl Store { let project = self .projects .get_mut(&project_id) - .and_then(|project| { - if project.has_authorized_user_id(user_id) { - Some(project) - } else { - None - } - }) .ok_or_else(|| anyhow!("no such project"))?; let share = project.share_mut()?; @@ -612,12 +579,9 @@ impl Store { connection.projects.remove(&project_id); } - let connection_ids = project.connection_ids(); - let authorized_user_ids = project.authorized_user_ids(); - Ok(LeftProject { - connection_ids, - authorized_user_ids, + connection_ids: project.connection_ids(), + host_user_id: project.host_user_id, }) } @@ -767,14 +731,6 @@ impl Store { let host_connection = self.connections.get(&project.host_connection_id).unwrap(); assert!(host_connection.projects.contains(project_id)); - for authorized_user_ids in project.authorized_user_ids() { - let visible_project_ids = self - .visible_projects_by_user_id - .get(&authorized_user_ids) - .unwrap(); - assert!(visible_project_ids.contains(project_id)); - } - if let Some(share) = &project.share { for guest_connection_id in share.guests.keys() { let guest_connection = self.connections.get(guest_connection_id).unwrap(); @@ -792,13 +748,6 @@ impl Store { } } - for (user_id, visible_project_ids) in &self.visible_projects_by_user_id { - for project_id in visible_project_ids { - let project = self.projects.get(project_id).unwrap(); - assert!(project.authorized_user_ids().contains(user_id)); - } - } - for (channel_id, channel) in &self.channels { for connection_id in &channel.connection_ids { let connection = self.connections.get(connection_id).unwrap(); @@ -809,24 +758,6 @@ impl Store { } impl Project { - pub fn has_authorized_user_id(&self, user_id: UserId) -> bool { - self.worktrees - .values() - .any(|worktree| worktree.authorized_user_ids.contains(&user_id)) - } - - pub fn authorized_user_ids(&self) -> Vec { - let mut ids = self - .worktrees - .values() - .flat_map(|worktree| worktree.authorized_user_ids.iter()) - .copied() - .collect::>(); - ids.sort_unstable(); - ids.dedup(); - ids - } - pub fn guest_connection_ids(&self) -> Vec { if let Some(share) = &self.share { share.guests.keys().copied().collect() From ca56b0d6d552f2acf204761f070867d57022af77 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 9 May 2022 15:51:54 +0200 Subject: [PATCH 19/53] Forbid joining projects if users are not contacts --- crates/collab/src/rpc.rs | 169 ++++++++++++++++++++++++--------- crates/collab/src/rpc/store.rs | 7 +- 2 files changed, 130 insertions(+), 46 deletions(-) diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 2ae12ef59e33206d55229848cafbce211f37127b..e976984d85bf706749f136a20a4d4fc2de3b0a5d 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -474,12 +474,23 @@ impl Server { response: Response, ) -> Result<()> { let project_id = request.payload.project_id; - let response_payload; let host_user_id; + let guest_user_id; + { + let state = self.store().await; + host_user_id = state.project(project_id)?.host_user_id; + guest_user_id = state.user_id_for_connection(request.sender_id)?; + }; + + let guest_contacts = self.app_state.db.get_contacts(guest_user_id).await?; + if !guest_contacts.current.contains(&host_user_id) { + return Err(anyhow!("no such project"))?; + } + + let response_payload; { let state = &mut *self.store_mut().await; - let user_id = state.user_id_for_connection(request.sender_id)?; - let joined = state.join_project(request.sender_id, user_id, project_id)?; + let joined = state.join_project(request.sender_id, guest_user_id, project_id)?; let share = joined.project.share()?; let peer_count = share.guests.len(); let mut collaborators = Vec::with_capacity(peer_count); @@ -522,7 +533,7 @@ impl Server { collaborators, language_servers: joined.project.language_servers.clone(), }; - host_user_id = joined.project.host_user_id; + broadcast( request.sender_id, joined.project.connection_ids(), @@ -534,7 +545,7 @@ impl Server { collaborator: Some(proto::Collaborator { peer_id: request.sender_id.0, replica_id: response_payload.replica_id, - user_id: user_id.to_proto(), + user_id: guest_user_id.to_proto(), }), }, ) @@ -1527,12 +1538,14 @@ mod tests { 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!({ - ".zed.toml": r#"collaborators = ["user_b"]"#, "a.txt": "a-contents", "b.txt": "b-contents", }), @@ -1649,12 +1662,14 @@ mod tests { 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!({ - ".zed.toml": r#"collaborators = ["user_b"]"#, "a.txt": "a-contents", "b.txt": "b-contents", }), @@ -1742,12 +1757,14 @@ mod tests { 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!({ - ".zed.toml": r#"collaborators = ["user_b"]"#, "a.txt": "a-contents", "b.txt": "b-contents", }), @@ -1848,12 +1865,18 @@ mod tests { 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; + server + .make_contacts(vec![ + (&client_a, cx_a), + (&client_b, cx_b), + (&client_c, cx_c), + ]) + .await; // Share a worktree as client A. fs.insert_tree( "/a", json!({ - ".zed.toml": r#"collaborators = ["user_b", "user_c"]"#, "file1": "", "file2": "" }), @@ -1974,7 +1997,7 @@ mod tests { tree.paths() .map(|p| p.to_string_lossy()) .collect::>() - == [".zed.toml", "file1-renamed", "file3", "file4"] + == ["file1-renamed", "file3", "file4"] }) .await; worktree_b @@ -1982,7 +2005,7 @@ mod tests { tree.paths() .map(|p| p.to_string_lossy()) .collect::>() - == [".zed.toml", "file1-renamed", "file3", "file4"] + == ["file1-renamed", "file3", "file4"] }) .await; worktree_c @@ -1990,7 +2013,7 @@ mod tests { tree.paths() .map(|p| p.to_string_lossy()) .collect::>() - == [".zed.toml", "file1-renamed", "file3", "file4"] + == ["file1-renamed", "file3", "file4"] }) .await; @@ -2025,12 +2048,14 @@ mod tests { let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let mut client_a = server.create_client(cx_a, "user_a").await; let mut 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( "/dir", json!({ - ".zed.toml": r#"collaborators = ["user_b"]"#, "a.txt": "a-contents", "b.txt": "b-contents", }), @@ -2065,7 +2090,7 @@ mod tests { .paths() .map(|p| p.to_string_lossy()) .collect::>(), - [".zed.toml", "a.txt", "b.txt", "c.txt"] + ["a.txt", "b.txt", "c.txt"] ); }); worktree_b.read_with(cx_b, |worktree, _| { @@ -2074,7 +2099,7 @@ mod tests { .paths() .map(|p| p.to_string_lossy()) .collect::>(), - [".zed.toml", "a.txt", "b.txt", "c.txt"] + ["a.txt", "b.txt", "c.txt"] ); }); @@ -2091,7 +2116,7 @@ mod tests { .paths() .map(|p| p.to_string_lossy()) .collect::>(), - [".zed.toml", "a.txt", "b.txt", "d.txt"] + ["a.txt", "b.txt", "d.txt"] ); }); worktree_b.read_with(cx_b, |worktree, _| { @@ -2100,7 +2125,7 @@ mod tests { .paths() .map(|p| p.to_string_lossy()) .collect::>(), - [".zed.toml", "a.txt", "b.txt", "d.txt"] + ["a.txt", "b.txt", "d.txt"] ); }); @@ -2118,7 +2143,7 @@ mod tests { .paths() .map(|p| p.to_string_lossy()) .collect::>(), - [".zed.toml", "DIR", "a.txt", "b.txt", "d.txt"] + ["DIR", "a.txt", "b.txt", "d.txt"] ); }); worktree_b.read_with(cx_b, |worktree, _| { @@ -2127,7 +2152,7 @@ mod tests { .paths() .map(|p| p.to_string_lossy()) .collect::>(), - [".zed.toml", "DIR", "a.txt", "b.txt", "d.txt"] + ["DIR", "a.txt", "b.txt", "d.txt"] ); }); @@ -2143,7 +2168,7 @@ mod tests { .paths() .map(|p| p.to_string_lossy()) .collect::>(), - [".zed.toml", "a.txt", "b.txt", "d.txt"] + ["a.txt", "b.txt", "d.txt"] ); }); worktree_b.read_with(cx_b, |worktree, _| { @@ -2152,7 +2177,7 @@ mod tests { .paths() .map(|p| p.to_string_lossy()) .collect::>(), - [".zed.toml", "a.txt", "b.txt", "d.txt"] + ["a.txt", "b.txt", "d.txt"] ); }); @@ -2168,7 +2193,7 @@ mod tests { .paths() .map(|p| p.to_string_lossy()) .collect::>(), - [".zed.toml", "a.txt", "b.txt"] + ["a.txt", "b.txt"] ); }); worktree_b.read_with(cx_b, |worktree, _| { @@ -2177,7 +2202,7 @@ mod tests { .paths() .map(|p| p.to_string_lossy()) .collect::>(), - [".zed.toml", "a.txt", "b.txt"] + ["a.txt", "b.txt"] ); }); } @@ -2192,12 +2217,14 @@ mod tests { 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( "/dir", json!({ - ".zed.toml": r#"collaborators = ["user_b", "user_c"]"#, "a.txt": "a-contents", }), ) @@ -2274,12 +2301,14 @@ mod tests { 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( "/dir", json!({ - ".zed.toml": r#"collaborators = ["user_b", "user_c"]"#, "a.txt": "a-contents", }), ) @@ -2356,12 +2385,14 @@ mod tests { 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( "/dir", json!({ - ".zed.toml": r#"collaborators = ["user_b"]"#, "a.txt": "a-contents", }), ) @@ -2435,12 +2466,14 @@ mod tests { 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( "/dir", json!({ - ".zed.toml": r#"collaborators = ["user_b"]"#, "a.txt": "a-contents", }), ) @@ -2507,12 +2540,14 @@ mod tests { 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!({ - ".zed.toml": r#"collaborators = ["user_b"]"#, "a.txt": "a-contents", "b.txt": "b-contents", }), @@ -2618,12 +2653,14 @@ mod tests { 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!({ - ".zed.toml": r#"collaborators = ["user_b"]"#, "a.rs": "let one = two", "other.rs": "", }), @@ -2866,12 +2903,14 @@ mod tests { 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!({ - ".zed.toml": r#"collaborators = ["user_b"]"#, "main.rs": "fn main() { a }", "other.rs": "", }), @@ -3047,12 +3086,14 @@ mod tests { 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!({ - ".zed.toml": r#"collaborators = ["user_b"]"#, "a.rs": "let one = 1;", }), ) @@ -3176,12 +3217,14 @@ mod tests { 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!({ - ".zed.toml": r#"collaborators = ["user_b"]"#, "a.rs": "let one = two", }), ) @@ -3260,7 +3303,6 @@ mod tests { fs.insert_tree( "/root-1", json!({ - ".zed.toml": r#"collaborators = ["user_b"]"#, "a.rs": "const ONE: usize = b::TWO + b::THREE;", }), ) @@ -3289,6 +3331,9 @@ mod tests { 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 let project_a = cx_a.update(|cx| { @@ -3404,7 +3449,6 @@ mod tests { fs.insert_tree( "/root-1", json!({ - ".zed.toml": r#"collaborators = ["user_b"]"#, "one.rs": "const ONE: usize = 1;", "two.rs": "const TWO: usize = one::ONE + one::ONE;", }), @@ -3434,6 +3478,9 @@ mod tests { 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 let project_a = cx_a.update(|cx| { @@ -3545,7 +3592,6 @@ mod tests { fs.insert_tree( "/root-1", json!({ - ".zed.toml": r#"collaborators = ["user_b"]"#, "a": "hello world", "b": "goodnight moon", "c": "a world of goo", @@ -3565,6 +3611,9 @@ mod tests { 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 let project_a = cx_a.update(|cx| { @@ -3652,7 +3701,6 @@ mod tests { fs.insert_tree( "/root-1", json!({ - ".zed.toml": r#"collaborators = ["user_b"]"#, "main.rs": "fn double(number: i32) -> i32 { number + number }", }), ) @@ -3674,6 +3722,9 @@ mod tests { 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 let project_a = cx_a.update(|cx| { @@ -3790,7 +3841,6 @@ mod tests { "/code", json!({ "crate-1": { - ".zed.toml": r#"collaborators = ["user_b"]"#, "one.rs": "const ONE: usize = 1;", }, "crate-2": { @@ -3819,6 +3869,9 @@ mod tests { 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 let project_a = cx_a.update(|cx| { @@ -3926,7 +3979,6 @@ mod tests { fs.insert_tree( "/root", json!({ - ".zed.toml": r#"collaborators = ["user_b"]"#, "a.rs": "const ONE: usize = b::TWO;", "b.rs": "const TWO: usize = 2", }), @@ -3949,6 +4001,9 @@ mod tests { 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 let project_a = cx_a.update(|cx| { @@ -4046,12 +4101,14 @@ mod tests { 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!({ - ".zed.toml": r#"collaborators = ["user_b"]"#, "main.rs": "mod other;\nfn main() { let foo = other::foo(); }", "other.rs": "pub fn foo() -> usize { 4 }", }), @@ -4294,12 +4351,14 @@ mod tests { 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( "/dir", json!({ - ".zed.toml": r#"collaborators = ["user_b"]"#, "one.rs": "const ONE: usize = 1;", "two.rs": "const TWO: usize = one::ONE + one::ONE;" }), @@ -5257,6 +5316,9 @@ mod tests { let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let mut client_a = server.create_client(cx_a, "user_a").await; let mut client_b = server.create_client(cx_b, "user_b").await; + server + .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) + .await; cx_a.update(editor::init); cx_b.update(editor::init); @@ -5264,7 +5326,6 @@ mod tests { fs.insert_tree( "/a", json!({ - ".zed.toml": r#"collaborators = ["user_b"]"#, "1.txt": "one", "2.txt": "two", "3.txt": "three", @@ -5468,6 +5529,9 @@ mod tests { let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let mut client_a = server.create_client(cx_a, "user_a").await; let mut client_b = server.create_client(cx_b, "user_b").await; + server + .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) + .await; cx_a.update(editor::init); cx_b.update(editor::init); @@ -5475,7 +5539,6 @@ mod tests { fs.insert_tree( "/a", json!({ - ".zed.toml": r#"collaborators = ["user_b"]"#, "1.txt": "one", "2.txt": "two", "3.txt": "three", @@ -5614,6 +5677,9 @@ mod tests { let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let mut client_a = server.create_client(cx_a, "user_a").await; let mut client_b = server.create_client(cx_b, "user_b").await; + server + .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) + .await; cx_a.update(editor::init); cx_b.update(editor::init); @@ -5621,7 +5687,6 @@ mod tests { fs.insert_tree( "/a", json!({ - ".zed.toml": r#"collaborators = ["user_b"]"#, "1.txt": "one", "2.txt": "two", "3.txt": "three", @@ -5802,6 +5867,24 @@ mod tests { .await; let mut server = TestServer::start(cx.foreground(), cx.background()).await; + let db = server.app_state.db.clone(); + let host_user_id = db.create_user("host", false).await.unwrap(); + for username in ["guest-1", "guest-2", "guest-3", "guest-4"] { + let guest_user_id = db.create_user(username, false).await.unwrap(); + server + .app_state + .db + .send_contact_request(guest_user_id, host_user_id) + .await + .unwrap(); + server + .app_state + .db + .respond_to_contact_request(host_user_id, guest_user_id, true) + .await + .unwrap(); + } + let mut clients = Vec::new(); let mut user_ids = Vec::new(); let mut op_start_signals = Vec::new(); diff --git a/crates/collab/src/rpc/store.rs b/crates/collab/src/rpc/store.rs index e655af79012dd035c3944df1ad9fc57dd1decffc..8ca270622832b4fc23151508d3df4a4fa9f013c8 100644 --- a/crates/collab/src/rpc/store.rs +++ b/crates/collab/src/rpc/store.rs @@ -629,9 +629,10 @@ impl Store { .connection_ids()) } - #[cfg(test)] - pub fn project(&self, project_id: u64) -> Option<&Project> { - self.projects.get(&project_id) + pub fn project(&self, project_id: u64) -> Result<&Project> { + self.projects + .get(&project_id) + .ok_or_else(|| anyhow!("no such project")) } pub fn read_project(&self, project_id: u64, connection_id: ConnectionId) -> Result<&Project> { From e4f1952657d5f3652761a71fc7ba4b644521e934 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 9 May 2022 17:06:21 +0200 Subject: [PATCH 20/53] WIP --- Cargo.lock | 1 + crates/client/src/user.rs | 8 ++++ crates/collab/src/rpc.rs | 17 +------- crates/contacts_panel/Cargo.toml | 1 + crates/contacts_panel/src/contacts_panel.rs | 48 +++++++++++++++++---- 5 files changed, 52 insertions(+), 23 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bca506d7e3550aa20e21e300bc0998ab67f5a2f0..98bcfe5a91dd5e8c1ccf30523b18729eff00cb5d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -935,6 +935,7 @@ dependencies = [ "fuzzy", "gpui", "postage", + "serde", "settings", "theme", "util", diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index 984fe4126c996813ca34d03328d485872532d8e8..a32a4b179a70b60766157e761b863e14ea461898 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -237,6 +237,14 @@ impl UserStore { &self.outgoing_contact_requests } + pub fn has_outgoing_contact_request(&self, user: &User) -> bool { + self.outgoing_contact_requests + .binary_search_by_key(&&user.github_login, |requested_user| { + &requested_user.github_login + }) + .is_ok() + } + pub fn request_contact(&self, responder_id: u64) -> impl Future> { let client = self.client.upgrade(); async move { diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index e976984d85bf706749f136a20a4d4fc2de3b0a5d..548744ca767777fe954404b64fd32273af5a7ee7 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -276,6 +276,7 @@ impl Server { store.add_connection(connection_id, user_id); this.peer.send(connection_id, store.build_initial_contacts_update(contacts))?; } + // this.update_user_contacts(user_id).await?; let handle_io = handle_io.fuse(); futures::pin_mut!(handle_io); @@ -354,22 +355,8 @@ impl Server { }); } - let contacts_to_update = self - .app_state - .db - .get_contacts(removed_connection.user_id) + self.update_user_contacts(removed_connection.user_id) .await?; - let store = self.store().await; - let mut update = proto::UpdateContacts::default(); - update - .contacts - .push(store.contact_for_user(removed_connection.user_id)); - - for user_id in contacts_to_update.current { - for connection_id in store.connection_ids_for_user(user_id) { - self.peer.send(connection_id, update.clone()).trace_err(); - } - } Ok(()) } diff --git a/crates/contacts_panel/Cargo.toml b/crates/contacts_panel/Cargo.toml index 8e76bce22b2e98c5beed4b643dd16c56c5129a34..e511b9d030a94372025953dae6c56be17538e8e0 100644 --- a/crates/contacts_panel/Cargo.toml +++ b/crates/contacts_panel/Cargo.toml @@ -18,3 +18,4 @@ util = { path = "../util" } workspace = { path = "../workspace" } futures = "0.3" postage = { version = "0.4.1", features = ["futures-traits"] } +serde = { version = "1", features = ["derive"] } diff --git a/crates/contacts_panel/src/contacts_panel.rs b/crates/contacts_panel/src/contacts_panel.rs index 7c187ba7eeb8e6562d928c6fd1c3992c12f64d20..affdded6e5ce57c3dfa08a117f8398a71d149b68 100644 --- a/crates/contacts_panel/src/contacts_panel.rs +++ b/crates/contacts_panel/src/contacts_panel.rs @@ -5,15 +5,19 @@ use gpui::{ anyhow, elements::*, geometry::{rect::RectF, vector::vec2f}, + impl_actions, platform::CursorStyle, Element, ElementBox, Entity, LayoutContext, ModelHandle, RenderContext, Subscription, Task, View, ViewContext, ViewHandle, }; +use serde::Deserialize; use settings::Settings; use std::sync::Arc; use util::ResultExt; use workspace::{AppState, JoinProject}; +impl_actions!(contacts_panel, [RequestContact]); + pub struct ContactsPanel { list_state: ListState, contacts: Vec>, @@ -24,6 +28,9 @@ pub struct ContactsPanel { _maintain_contacts: Subscription, } +#[derive(Clone, Deserialize)] +pub struct RequestContact(pub u64); + impl ContactsPanel { pub fn new(app_state: Arc, cx: &mut ViewContext) -> Self { let user_query_editor = cx.add_view(|cx| { @@ -86,8 +93,10 @@ impl ContactsPanel { } else { let potential_contact_ix = ix - 2 - this.contacts.len(); Self::render_potential_contact( - &this.potential_contacts[potential_contact_ix], + this.potential_contacts[potential_contact_ix].clone(), + this.user_store.clone(), theme, + cx, ) } } @@ -278,7 +287,16 @@ impl ContactsPanel { .boxed() } - fn render_potential_contact(contact: &User, theme: &theme::ContactsPanel) -> ElementBox { + fn render_potential_contact( + contact: Arc, + user_store: ModelHandle, + theme: &theme::ContactsPanel, + cx: &mut LayoutContext, + ) -> ElementBox { + enum RequestContactButton {} + + let requested_contact = user_store.read(cx).has_outgoing_contact_request(&contact); + Flex::row() .with_children(contact.avatar.clone().map(|avatar| { Image::new(avatar) @@ -299,12 +317,26 @@ impl ContactsPanel { .boxed(), ) .with_child( - Label::new("+".to_string(), theme.edit_contact.text.clone()) - .contained() - .with_style(theme.edit_contact.container) - .aligned() - .flex_float() - .boxed(), + MouseEventHandler::new::( + contact.id as usize, + cx, + |_, _| { + let label = if requested_contact { "-" } else { "+" }; + Label::new(label.to_string(), theme.edit_contact.text.clone()) + .contained() + .with_style(theme.edit_contact.container) + .aligned() + .flex_float() + .boxed() + }, + ) + .on_click(move |_, cx| { + if requested_contact { + } else { + cx.dispatch_action(RequestContact(contact.id)); + } + }) + .boxed(), ) .constrained() .with_height(theme.row_height) From e3ee19b123b919fe360f422fe407595f5fbaee5a Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 9 May 2022 11:24:05 -0600 Subject: [PATCH 21/53] Wire up UI for requesting contacts and cancelling requests Co-Authored-By: Max Brunsfeld --- crates/client/src/user.rs | 98 ++++++++++++++++++--- crates/collab/src/db.rs | 32 +++++++ crates/collab/src/rpc.rs | 50 +++++++++-- crates/contacts_panel/src/contacts_panel.rs | 48 ++++++++-- crates/rpc/proto/zed.proto | 13 ++- crates/rpc/src/proto.rs | 2 + crates/zed/src/main.rs | 1 + 7 files changed, 212 insertions(+), 32 deletions(-) diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index a32a4b179a70b60766157e761b863e14ea461898..ef38c6e2dafc3536918d4f2fdc780b90bec34142 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -5,7 +5,7 @@ use gpui::{AsyncAppContext, Entity, ImageData, ModelContext, ModelHandle, Task}; use postage::{prelude::Stream, sink::Sink, watch}; use rpc::proto::{RequestMessage, UsersResponse}; use std::{ - collections::{HashMap, HashSet}, + collections::{hash_map::Entry, HashMap, HashSet}, sync::{Arc, Weak}, }; use util::TryFutureExt as _; @@ -31,6 +31,14 @@ pub struct ProjectMetadata { pub guests: Vec>, } +#[derive(Debug, Clone, Copy)] +pub enum ContactRequestStatus { + None, + SendingRequest, + Requested, + RequestAccepted, +} + pub struct UserStore { users: HashMap>, update_contacts_tx: watch::Sender>, @@ -38,6 +46,7 @@ pub struct UserStore { contacts: Vec>, incoming_contact_requests: Vec>, outgoing_contact_requests: Vec>, + pending_contact_requests: HashMap, client: Weak, http: Arc, _maintain_contacts: Task<()>, @@ -100,6 +109,7 @@ impl UserStore { } } }), + pending_contact_requests: Default::default(), } } @@ -237,23 +247,85 @@ impl UserStore { &self.outgoing_contact_requests } - pub fn has_outgoing_contact_request(&self, user: &User) -> bool { - self.outgoing_contact_requests - .binary_search_by_key(&&user.github_login, |requested_user| { - &requested_user.github_login - }) + pub fn contact_request_status(&self, user: &User) -> ContactRequestStatus { + if self + .contacts + .binary_search_by_key(&&user.id, |contact| &contact.user.id) .is_ok() + { + ContactRequestStatus::RequestAccepted + } else if self + .outgoing_contact_requests + .binary_search_by_key(&&user.id, |user| &user.id) + .is_ok() + { + ContactRequestStatus::Requested + } else if self.pending_contact_requests.contains_key(&user.id) { + ContactRequestStatus::SendingRequest + } else { + ContactRequestStatus::None + } } - pub fn request_contact(&self, responder_id: u64) -> impl Future> { + pub fn request_contact( + &mut self, + responder_id: u64, + cx: &mut ModelContext, + ) -> Task> { let client = self.client.upgrade(); - async move { - client - .ok_or_else(|| anyhow!("not logged in"))? - .request(proto::RequestContact { responder_id }) - .await?; + *self + .pending_contact_requests + .entry(responder_id) + .or_insert(0) += 1; + cx.notify(); + + cx.spawn(|this, mut cx| async move { + let request = client + .ok_or_else(|| anyhow!("can't upgrade client reference"))? + .request(proto::RequestContact { responder_id }); + request.await?; + this.update(&mut cx, |this, cx| { + if let Entry::Occupied(mut request_count) = + this.pending_contact_requests.entry(responder_id) + { + *request_count.get_mut() -= 1; + if *request_count.get() == 0 { + request_count.remove(); + } + } + cx.notify(); + }); Ok(()) - } + }) + } + + pub fn remove_contact( + &mut self, + user_id: u64, + cx: &mut ModelContext, + ) -> Task> { + let client = self.client.upgrade(); + *self.pending_contact_requests.entry(user_id).or_insert(0) += 1; + cx.notify(); + + cx.spawn(|this, mut cx| async move { + let request = client + .ok_or_else(|| anyhow!("can't upgrade client reference"))? + .request(proto::RemoveContact { user_id }); + request.await?; + this.update(&mut cx, |this, cx| { + if let Entry::Occupied(mut request_count) = + this.pending_contact_requests.entry(user_id) + { + *request_count.get_mut() -= 1; + if *request_count.get() == 0 { + request_count.remove(); + } + } + cx.notify(); + }); + Ok(()) + }) } pub fn respond_to_contact_request( diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index fd84f39bab9041ece9fbc16fc535255c0c18b945..e5b3b31571bd2d3457aec0e92bfdc76640a071a4 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -19,6 +19,7 @@ pub trait Db: Send + Sync { async fn get_contacts(&self, id: UserId) -> Result; async fn send_contact_request(&self, requester_id: UserId, responder_id: UserId) -> Result<()>; + async fn remove_contact(&self, requester_id: UserId, responder_id: UserId) -> Result<()>; async fn dismiss_contact_request( &self, responder_id: UserId, @@ -267,6 +268,30 @@ impl Db for PostgresDb { } } + async fn remove_contact(&self, requester_id: UserId, responder_id: UserId) -> Result<()> { + let (id_a, id_b, a_to_b) = if responder_id < requester_id { + (responder_id, requester_id, false) + } else { + (requester_id, responder_id, true) + }; + let query = " + DELETE FROM contacts + WHERE user_id_a = $1 AND user_id_b = $2 AND a_to_b = $3; + "; + let result = sqlx::query(query) + .bind(id_a.0) + .bind(id_b.0) + .bind(a_to_b) + .execute(&self.pool) + .await?; + + if result.rows_affected() == 1 { + Ok(()) + } else { + Err(anyhow!("no such contact")) + } + } + async fn respond_to_contact_request( &self, responder_id: UserId, @@ -1248,6 +1273,13 @@ pub mod tests { Ok(()) } + async fn remove_contact(&self, requester_id: UserId, responder_id: UserId) -> Result<()> { + self.contacts.lock().retain(|contact| { + !(contact.requester_id == requester_id && contact.responder_id == responder_id) + }); + Ok(()) + } + async fn dismiss_contact_request( &self, responder_id: UserId, diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 548744ca767777fe954404b64fd32273af5a7ee7..6dabc63eaa103511946791efa405c77a0be00603 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -155,6 +155,7 @@ impl Server { .add_request_handler(Server::get_users) .add_request_handler(Server::fuzzy_search_users) .add_request_handler(Server::request_contact) + .add_request_handler(Server::remove_contact) .add_request_handler(Server::respond_to_contact_request) .add_request_handler(Server::join_channel) .add_message_handler(Server::leave_channel) @@ -1048,6 +1049,43 @@ impl Server { Ok(()) } + async fn remove_contact( + self: Arc, + request: TypedEnvelope, + response: Response, + ) -> Result<()> { + let requester_id = self + .store() + .await + .user_id_for_connection(request.sender_id)?; + let responder_id = UserId::from_proto(request.payload.user_id); + self.app_state + .db + .remove_contact(requester_id, responder_id) + .await?; + + // Update outgoing contact requests of requester + let mut update = proto::UpdateContacts::default(); + update + .remove_outgoing_requests + .push(responder_id.to_proto()); + for connection_id in self.store().await.connection_ids_for_user(requester_id) { + self.peer.send(connection_id, update.clone())?; + } + + // Update incoming contact requests of responder + let mut update = proto::UpdateContacts::default(); + update + .remove_incoming_requests + .push(requester_id.to_proto()); + for connection_id in self.store().await.connection_ids_for_user(responder_id) { + self.peer.send(connection_id, update.clone())?; + } + + response.send(proto::Ack {})?; + Ok(()) + } + // #[instrument(skip(self, state, user_ids))] // fn update_contacts_for_users<'a>( // self: &Arc, @@ -5138,15 +5176,15 @@ mod tests { // User A and User C request that user B become their contact. client_a .user_store - .read_with(cx_a, |store, _| { - store.request_contact(client_b.user_id().unwrap()) + .update(cx_a, |store, cx| { + store.request_contact(client_b.user_id().unwrap(), cx) }) .await .unwrap(); client_c .user_store - .read_with(cx_c, |store, _| { - store.request_contact(client_b.user_id().unwrap()) + .update(cx_c, |store, cx| { + store.request_contact(client_b.user_id().unwrap(), cx) }) .await .unwrap(); @@ -6460,8 +6498,8 @@ mod tests { for (client_b, cx_b) in &mut clients { client_a .user_store - .update(cx_a, |store, _| { - store.request_contact(client_b.user_id().unwrap()) + .update(cx_a, |store, cx| { + store.request_contact(client_b.user_id().unwrap(), cx) }) .await .unwrap(); diff --git a/crates/contacts_panel/src/contacts_panel.rs b/crates/contacts_panel/src/contacts_panel.rs index affdded6e5ce57c3dfa08a117f8398a71d149b68..eb64afb2d57b21ea0c9b8210616da4ad5496e9bd 100644 --- a/crates/contacts_panel/src/contacts_panel.rs +++ b/crates/contacts_panel/src/contacts_panel.rs @@ -1,4 +1,4 @@ -use client::{Contact, User, UserStore}; +use client::{Contact, ContactRequestStatus, User, UserStore}; use editor::Editor; use fuzzy::StringMatchCandidate; use gpui::{ @@ -7,8 +7,8 @@ use gpui::{ geometry::{rect::RectF, vector::vec2f}, impl_actions, platform::CursorStyle, - Element, ElementBox, Entity, LayoutContext, ModelHandle, RenderContext, Subscription, Task, - View, ViewContext, ViewHandle, + Element, ElementBox, Entity, LayoutContext, ModelHandle, MutableAppContext, RenderContext, + Subscription, Task, View, ViewContext, ViewHandle, }; use serde::Deserialize; use settings::Settings; @@ -16,7 +16,7 @@ use std::sync::Arc; use util::ResultExt; use workspace::{AppState, JoinProject}; -impl_actions!(contacts_panel, [RequestContact]); +impl_actions!(contacts_panel, [RequestContact, RemoveContact]); pub struct ContactsPanel { list_state: ListState, @@ -31,6 +31,14 @@ pub struct ContactsPanel { #[derive(Clone, Deserialize)] pub struct RequestContact(pub u64); +#[derive(Clone, Deserialize)] +pub struct RemoveContact(pub u64); + +pub fn init(cx: &mut MutableAppContext) { + cx.add_action(ContactsPanel::request_contact); + cx.add_action(ContactsPanel::remove_contact); +} + impl ContactsPanel { pub fn new(app_state: Arc, cx: &mut ViewContext) -> Self { let user_query_editor = cx.add_view(|cx| { @@ -295,7 +303,7 @@ impl ContactsPanel { ) -> ElementBox { enum RequestContactButton {} - let requested_contact = user_store.read(cx).has_outgoing_contact_request(&contact); + let request_status = user_store.read(cx).contact_request_status(&contact); Flex::row() .with_children(contact.avatar.clone().map(|avatar| { @@ -321,7 +329,13 @@ impl ContactsPanel { contact.id as usize, cx, |_, _| { - let label = if requested_contact { "-" } else { "+" }; + let label = match request_status { + ContactRequestStatus::None => "+", + ContactRequestStatus::SendingRequest => "…", + ContactRequestStatus::Requested => "-", + ContactRequestStatus::RequestAccepted => unreachable!(), + }; + Label::new(label.to_string(), theme.edit_contact.text.clone()) .contained() .with_style(theme.edit_contact.container) @@ -330,12 +344,16 @@ impl ContactsPanel { .boxed() }, ) - .on_click(move |_, cx| { - if requested_contact { - } else { + .on_click(move |_, cx| match request_status { + ContactRequestStatus::None => { cx.dispatch_action(RequestContact(contact.id)); } + ContactRequestStatus::Requested => { + cx.dispatch_action(RemoveContact(contact.id)); + } + _ => {} }) + .with_cursor_style(CursorStyle::PointingHand) .boxed(), ) .constrained() @@ -415,6 +433,18 @@ impl ContactsPanel { None })); } + + fn request_contact(&mut self, request: &RequestContact, cx: &mut ViewContext) { + self.user_store + .update(cx, |store, cx| store.request_contact(request.0, cx)) + .detach(); + } + + fn remove_contact(&mut self, request: &RemoveContact, cx: &mut ViewContext) { + self.user_store + .update(cx, |store, cx| store.remove_contact(request.0, cx)) + .detach(); + } } pub enum Event {} diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 97e0930f4fa2055d812652469f3745ba54ee42c6..55bdba975159bb5b912d849ce10afaa5fb617128 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -91,11 +91,12 @@ message Envelope { UsersResponse users_response = 78; RequestContact request_contact = 79; RespondToContactRequest respond_to_contact_request = 80; + RemoveContact remove_contact = 81; - Follow follow = 81; - FollowResponse follow_response = 82; - UpdateFollowers update_followers = 83; - Unfollow unfollow = 84; + Follow follow = 82; + FollowResponse follow_response = 83; + UpdateFollowers update_followers = 84; + Unfollow unfollow = 85; } } @@ -553,6 +554,10 @@ message RequestContact { uint64 responder_id = 1; } +message RemoveContact { + uint64 user_id = 1; +} + message RespondToContactRequest { uint64 requester_id = 1; ContactRequestResponse response = 2; diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index 2674e8a0d8e2f36715339f5779219187388200e1..0b7ba21c4a22419d1f13ba909bbaeab7e24c512b 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -82,6 +82,7 @@ messages!( (ApplyCompletionAdditionalEditsResponse, Background), (BufferReloaded, Foreground), (BufferSaved, Foreground), + (RemoveContact, Foreground), (ChannelMessageSent, Foreground), (CreateProjectEntry, Foreground), (DeleteProjectEntry, Foreground), @@ -188,6 +189,7 @@ request_messages!( (RegisterWorktree, Ack), (ReloadBuffers, ReloadBuffersResponse), (RequestContact, Ack), + (RemoveContact, Ack), (RespondToContactRequest, Ack), (RenameProjectEntry, ProjectEntryResponse), (SaveBuffer, BufferSaved), diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 53a0a92a2534ed091cb24638f2fb3e9b0269b86f..a4f85ab9bc7467b108092c01db7a26d5920098d9 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -146,6 +146,7 @@ fn main() { go_to_line::init(cx); file_finder::init(cx); chat_panel::init(cx); + contacts_panel::init(cx); outline::init(cx); project_symbols::init(cx); project_panel::init(cx); From e9d8cc94cc0cd75b3a38ccf84076d224d2df6b9d Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 9 May 2022 11:24:16 -0600 Subject: [PATCH 22/53] Rename script to match others (dashes) Co-Authored-By: Max Brunsfeld --- script/{zed_with_local_servers => zed-with-local-servers} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename script/{zed_with_local_servers => zed-with-local-servers} (100%) diff --git a/script/zed_with_local_servers b/script/zed-with-local-servers similarity index 100% rename from script/zed_with_local_servers rename to script/zed-with-local-servers From 40f1427885aded911401fabd657522ff2670e738 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 9 May 2022 12:48:07 -0600 Subject: [PATCH 23/53] Show requests in contacts panel Co-Authored-By: Max Brunsfeld --- Cargo.lock | 1 + crates/client/src/user.rs | 105 ++--- crates/collab/src/rpc.rs | 12 +- crates/contacts_panel/Cargo.toml | 1 + crates/contacts_panel/src/contacts_panel.rs | 489 +++++++++++++++----- crates/fuzzy/src/fuzzy.rs | 14 +- 6 files changed, 432 insertions(+), 190 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 98bcfe5a91dd5e8c1ccf30523b18729eff00cb5d..0d3271c0662eaa689cb7af97ff748b51e9df60fd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -934,6 +934,7 @@ dependencies = [ "futures", "fuzzy", "gpui", + "log", "postage", "serde", "settings", diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index ef38c6e2dafc3536918d4f2fdc780b90bec34142..6fd9b1ded1625d889b5d30be46874c31cbe75d06 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -1,6 +1,6 @@ use super::{http::HttpClient, proto, Client, Status, TypedEnvelope}; use anyhow::{anyhow, Context, Result}; -use futures::{future, AsyncReadExt, Future}; +use futures::{future, AsyncReadExt}; use gpui::{AsyncAppContext, Entity, ImageData, ModelContext, ModelHandle, Task}; use postage::{prelude::Stream, sink::Sink, watch}; use rpc::proto::{RequestMessage, UsersResponse}; @@ -31,11 +31,12 @@ pub struct ProjectMetadata { pub guests: Vec>, } -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ContactRequestStatus { None, - SendingRequest, - Requested, + Pending, + RequestSent, + RequestReceived, RequestAccepted, } @@ -192,7 +193,6 @@ impl UserStore { Err(ix) => this.contacts.insert(ix, updated_contact), } } - cx.notify(); // Remove incoming contact requests this.incoming_contact_requests @@ -223,6 +223,8 @@ impl UserStore { Err(ix) => this.outgoing_contact_requests.insert(ix, request), } } + + cx.notify(); }); Ok(()) @@ -248,7 +250,9 @@ impl UserStore { } pub fn contact_request_status(&self, user: &User) -> ContactRequestStatus { - if self + if self.pending_contact_requests.contains_key(&user.id) { + ContactRequestStatus::Pending + } else if self .contacts .binary_search_by_key(&&user.id, |contact| &contact.user.id) .is_ok() @@ -259,9 +263,13 @@ impl UserStore { .binary_search_by_key(&&user.id, |user| &user.id) .is_ok() { - ContactRequestStatus::Requested - } else if self.pending_contact_requests.contains_key(&user.id) { - ContactRequestStatus::SendingRequest + ContactRequestStatus::RequestSent + } else if self + .incoming_contact_requests + .binary_search_by_key(&&user.id, |user| &user.id) + .is_ok() + { + ContactRequestStatus::RequestReceived } else { ContactRequestStatus::None } @@ -272,37 +280,42 @@ impl UserStore { responder_id: u64, cx: &mut ModelContext, ) -> Task> { - let client = self.client.upgrade(); - *self - .pending_contact_requests - .entry(responder_id) - .or_insert(0) += 1; - cx.notify(); - - cx.spawn(|this, mut cx| async move { - let request = client - .ok_or_else(|| anyhow!("can't upgrade client reference"))? - .request(proto::RequestContact { responder_id }); - request.await?; - this.update(&mut cx, |this, cx| { - if let Entry::Occupied(mut request_count) = - this.pending_contact_requests.entry(responder_id) - { - *request_count.get_mut() -= 1; - if *request_count.get() == 0 { - request_count.remove(); - } - } - cx.notify(); - }); - Ok(()) - }) + self.perform_contact_request(responder_id, proto::RequestContact { responder_id }, cx) } pub fn remove_contact( &mut self, user_id: u64, cx: &mut ModelContext, + ) -> Task> { + self.perform_contact_request(user_id, proto::RemoveContact { user_id }, cx) + } + + pub fn respond_to_contact_request( + &mut self, + requester_id: u64, + accept: bool, + cx: &mut ModelContext, + ) -> Task> { + self.perform_contact_request( + requester_id, + proto::RespondToContactRequest { + requester_id, + response: if accept { + proto::ContactRequestResponse::Accept + } else { + proto::ContactRequestResponse::Reject + } as i32, + }, + cx, + ) + } + + fn perform_contact_request( + &mut self, + user_id: u64, + request: T, + cx: &mut ModelContext, ) -> Task> { let client = self.client.upgrade(); *self.pending_contact_requests.entry(user_id).or_insert(0) += 1; @@ -311,7 +324,7 @@ impl UserStore { cx.spawn(|this, mut cx| async move { let request = client .ok_or_else(|| anyhow!("can't upgrade client reference"))? - .request(proto::RemoveContact { user_id }); + .request(request); request.await?; this.update(&mut cx, |this, cx| { if let Entry::Occupied(mut request_count) = @@ -328,28 +341,6 @@ impl UserStore { }) } - pub fn respond_to_contact_request( - &self, - requester_id: u64, - accept: bool, - ) -> impl Future> { - let client = self.client.upgrade(); - async move { - client - .ok_or_else(|| anyhow!("not logged in"))? - .request(proto::RespondToContactRequest { - requester_id, - response: if accept { - proto::ContactRequestResponse::Accept - } else { - proto::ContactRequestResponse::Reject - } as i32, - }) - .await?; - Ok(()) - } - } - #[cfg(any(test, feature = "test-support"))] pub fn clear_contacts(&mut self) { self.contacts.clear(); diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 6dabc63eaa103511946791efa405c77a0be00603..4ccc332e8cc66e5fed72de8e5b45570eee437afa 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -5237,8 +5237,8 @@ mod tests { // User B accepts the request from user A. client_b .user_store - .read_with(cx_b, |store, _| { - store.respond_to_contact_request(client_a.user_id().unwrap(), true) + .update(cx_b, |store, cx| { + store.respond_to_contact_request(client_a.user_id().unwrap(), true, cx) }) .await .unwrap(); @@ -5281,8 +5281,8 @@ mod tests { // User B rejects the request from user C. client_b .user_store - .read_with(cx_b, |store, _| { - store.respond_to_contact_request(client_c.user_id().unwrap(), false) + .update(cx_b, |store, cx| { + store.respond_to_contact_request(client_c.user_id().unwrap(), false, cx) }) .await .unwrap(); @@ -6506,8 +6506,8 @@ mod tests { cx_a.foreground().run_until_parked(); client_b .user_store - .update(*cx_b, |store, _| { - store.respond_to_contact_request(client_a.user_id().unwrap(), true) + .update(*cx_b, |store, cx| { + store.respond_to_contact_request(client_a.user_id().unwrap(), true, cx) }) .await .unwrap(); diff --git a/crates/contacts_panel/Cargo.toml b/crates/contacts_panel/Cargo.toml index e511b9d030a94372025953dae6c56be17538e8e0..69cef3177f099bc0901f109f6d0429a6cbf35090 100644 --- a/crates/contacts_panel/Cargo.toml +++ b/crates/contacts_panel/Cargo.toml @@ -17,5 +17,6 @@ theme = { path = "../theme" } util = { path = "../util" } workspace = { path = "../workspace" } futures = "0.3" +log = "0.4" postage = { version = "0.4.1", features = ["futures-traits"] } serde = { version = "1", features = ["derive"] } diff --git a/crates/contacts_panel/src/contacts_panel.rs b/crates/contacts_panel/src/contacts_panel.rs index eb64afb2d57b21ea0c9b8210616da4ad5496e9bd..11883e88379fb3280dff0606b7213ddab003376b 100644 --- a/crates/contacts_panel/src/contacts_panel.rs +++ b/crates/contacts_panel/src/contacts_panel.rs @@ -1,8 +1,7 @@ use client::{Contact, ContactRequestStatus, User, UserStore}; use editor::Editor; -use fuzzy::StringMatchCandidate; +use fuzzy::{match_strings, StringMatchCandidate}; use gpui::{ - anyhow, elements::*, geometry::{rect::RectF, vector::vec2f}, impl_actions, @@ -13,15 +12,28 @@ use gpui::{ use serde::Deserialize; use settings::Settings; use std::sync::Arc; -use util::ResultExt; +use util::TryFutureExt; use workspace::{AppState, JoinProject}; -impl_actions!(contacts_panel, [RequestContact, RemoveContact]); +impl_actions!( + contacts_panel, + [RequestContact, RemoveContact, RespondToContactRequest] +); + +#[derive(Debug)] +enum ContactEntry { + Header(&'static str), + IncomingRequest(Arc), + OutgoingRequest(Arc), + Contact(Arc), + PotentialContact(Arc), +} pub struct ContactsPanel { - list_state: ListState, - contacts: Vec>, + entries: Vec, + match_candidates: Vec, potential_contacts: Vec>, + list_state: ListState, user_store: ModelHandle, contacts_search_task: Option>>, user_query_editor: ViewHandle, @@ -34,9 +46,16 @@ pub struct RequestContact(pub u64); #[derive(Clone, Deserialize)] pub struct RemoveContact(pub u64); +#[derive(Clone, Deserialize)] +pub struct RespondToContactRequest { + pub user_id: u64, + pub accept: bool, +} + pub fn init(cx: &mut MutableAppContext) { cx.add_action(ContactsPanel::request_contact); cx.add_action(ContactsPanel::remove_contact); + cx.add_action(ContactsPanel::respond_to_contact_request); } impl ContactsPanel { @@ -50,29 +69,26 @@ impl ContactsPanel { cx.subscribe(&user_query_editor, |this, _, event, cx| { if let editor::Event::BufferEdited = event { - this.filter_contacts(true, cx) + this.query_changed(cx) } }) .detach(); - Self { - list_state: ListState::new( - 1 + app_state.user_store.read(cx).contacts().len(), // Add 1 for the "Contacts" header - Orientation::Top, - 1000., - { - let this = cx.weak_handle(); - let app_state = app_state.clone(); - move |ix, cx| { - let this = this.upgrade(cx).unwrap(); - let this = this.read(cx); - let current_user_id = - this.user_store.read(cx).current_user().map(|user| user.id); - let theme = cx.global::().theme.clone(); - let theme = &theme.contacts_panel; - - if ix == 0 { - Label::new("contacts".to_string(), theme.header.text.clone()) + let mut this = Self { + list_state: ListState::new(0, Orientation::Top, 1000., { + let this = cx.weak_handle(); + let app_state = app_state.clone(); + move |ix, cx| { + let this = this.upgrade(cx).unwrap(); + let this = this.read(cx); + let theme = cx.global::().theme.clone(); + let theme = &theme.contacts_panel; + let current_user_id = + this.user_store.read(cx).current_user().map(|user| user.id); + + match &this.entries[ix] { + ContactEntry::Header(text) => { + Label::new(text.to_string(), theme.header.text.clone()) .contained() .with_style(theme.header.container) .aligned() @@ -80,55 +96,50 @@ impl ContactsPanel { .constrained() .with_height(theme.row_height) .boxed() - } else if ix < this.contacts.len() + 1 { - let contact_ix = ix - 1; - Self::render_contact( - this.contacts[contact_ix].clone(), - current_user_id, - app_state.clone(), + } + ContactEntry::IncomingRequest(user) => { + Self::render_incoming_contact_request( + user.clone(), + this.user_store.clone(), theme, cx, ) - } else if ix == this.contacts.len() + 1 { - Label::new("add contacts".to_string(), theme.header.text.clone()) - .contained() - .with_style(theme.header.container) - .aligned() - .left() - .constrained() - .with_height(theme.row_height) - .boxed() - } else { - let potential_contact_ix = ix - 2 - this.contacts.len(); - Self::render_potential_contact( - this.potential_contacts[potential_contact_ix].clone(), + } + ContactEntry::OutgoingRequest(user) => { + Self::render_outgoing_contact_request( + user.clone(), this.user_store.clone(), theme, cx, ) } + ContactEntry::Contact(contact) => Self::render_contact( + contact.clone(), + current_user_id, + app_state.clone(), + theme, + cx, + ), + ContactEntry::PotentialContact(user) => Self::render_potential_contact( + user.clone(), + this.user_store.clone(), + theme, + cx, + ), } - }, - ), - contacts: app_state.user_store.read(cx).contacts().into(), + } + }), + entries: Default::default(), potential_contacts: Default::default(), + match_candidates: Default::default(), user_query_editor, - _maintain_contacts: cx.observe(&app_state.user_store, |this, _, cx| { - this.filter_contacts(false, cx) - }), + _maintain_contacts: cx + .observe(&app_state.user_store, |this, _, cx| this.update_entries(cx)), contacts_search_task: None, user_store: app_state.user_store.clone(), - } - } - - fn update_list_state(&mut self, cx: &mut ViewContext) { - let mut list_len = 1 + self.contacts.len(); - if !self.potential_contacts.is_empty() { - list_len += 1 + self.potential_contacts.len(); - } - - self.list_state.reset(list_len); - cx.notify(); + }; + this.update_entries(cx); + this } fn render_contact( @@ -295,6 +306,150 @@ impl ContactsPanel { .boxed() } + fn render_incoming_contact_request( + user: Arc, + user_store: ModelHandle, + theme: &theme::ContactsPanel, + cx: &mut LayoutContext, + ) -> ElementBox { + enum Reject {} + enum Accept {} + + let user_id = user.id; + let request_status = user_store.read(cx).contact_request_status(&user); + + let mut row = Flex::row() + .with_children(user.avatar.clone().map(|avatar| { + Image::new(avatar) + .with_style(theme.contact_avatar) + .aligned() + .left() + .boxed() + })) + .with_child( + Label::new( + user.github_login.clone(), + theme.contact_username.text.clone(), + ) + .contained() + .with_style(theme.contact_username.container) + .aligned() + .left() + .boxed(), + ); + + if request_status == ContactRequestStatus::Pending { + row.add_child( + Label::new("…".to_string(), theme.edit_contact.text.clone()) + .contained() + .with_style(theme.edit_contact.container) + .aligned() + .flex_float() + .boxed(), + ); + } else { + row.add_children([ + MouseEventHandler::new::(user.id as usize, cx, |_, _| { + Label::new("Reject".to_string(), theme.edit_contact.text.clone()) + .contained() + .with_style(theme.edit_contact.container) + .aligned() + .flex_float() + .boxed() + }) + .on_click(move |_, cx| { + cx.dispatch_action(RespondToContactRequest { + user_id, + accept: false, + }); + }) + .with_cursor_style(CursorStyle::PointingHand) + .flex_float() + .boxed(), + MouseEventHandler::new::(user.id as usize, cx, |_, _| { + Label::new("Accept".to_string(), theme.edit_contact.text.clone()) + .contained() + .with_style(theme.edit_contact.container) + .aligned() + .flex_float() + .boxed() + }) + .on_click(move |_, cx| { + cx.dispatch_action(RespondToContactRequest { + user_id, + accept: true, + }); + }) + .with_cursor_style(CursorStyle::PointingHand) + .boxed(), + ]); + } + + row.constrained().with_height(theme.row_height).boxed() + } + + fn render_outgoing_contact_request( + user: Arc, + user_store: ModelHandle, + theme: &theme::ContactsPanel, + cx: &mut LayoutContext, + ) -> ElementBox { + enum Cancel {} + + let user_id = user.id; + let request_status = user_store.read(cx).contact_request_status(&user); + + let mut row = Flex::row() + .with_children(user.avatar.clone().map(|avatar| { + Image::new(avatar) + .with_style(theme.contact_avatar) + .aligned() + .left() + .boxed() + })) + .with_child( + Label::new( + user.github_login.clone(), + theme.contact_username.text.clone(), + ) + .contained() + .with_style(theme.contact_username.container) + .aligned() + .left() + .boxed(), + ); + + if request_status == ContactRequestStatus::Pending { + row.add_child( + Label::new("…".to_string(), theme.edit_contact.text.clone()) + .contained() + .with_style(theme.edit_contact.container) + .aligned() + .flex_float() + .boxed(), + ); + } else { + row.add_child( + MouseEventHandler::new::(user.id as usize, cx, |_, _| { + Label::new("Cancel".to_string(), theme.edit_contact.text.clone()) + .contained() + .with_style(theme.edit_contact.container) + .aligned() + .flex_float() + .boxed() + }) + .on_click(move |_, cx| { + cx.dispatch_action(RemoveContact(user_id)); + }) + .with_cursor_style(CursorStyle::PointingHand) + .flex_float() + .boxed(), + ); + } + + row.constrained().with_height(theme.row_height).boxed() + } + fn render_potential_contact( contact: Arc, user_store: ModelHandle, @@ -330,9 +485,11 @@ impl ContactsPanel { cx, |_, _| { let label = match request_status { - ContactRequestStatus::None => "+", - ContactRequestStatus::SendingRequest => "…", - ContactRequestStatus::Requested => "-", + ContactRequestStatus::None | ContactRequestStatus::RequestReceived => { + "+" + } + ContactRequestStatus::Pending => "…", + ContactRequestStatus::RequestSent => "-", ContactRequestStatus::RequestAccepted => unreachable!(), }; @@ -348,7 +505,7 @@ impl ContactsPanel { ContactRequestStatus::None => { cx.dispatch_action(RequestContact(contact.id)); } - ContactRequestStatus::Requested => { + ContactRequestStatus::RequestSent => { cx.dispatch_action(RemoveContact(contact.id)); } _ => {} @@ -361,77 +518,145 @@ impl ContactsPanel { .boxed() } - fn filter_contacts(&mut self, query_changed: bool, cx: &mut ViewContext) { + fn query_changed(&mut self, cx: &mut ViewContext) { + self.update_entries(cx); + let query = self.user_query_editor.read(cx).text(cx); + let search_users = self + .user_store + .update(cx, |store, cx| store.fuzzy_search_users(query, cx)); + + self.contacts_search_task = Some(cx.spawn(|this, mut cx| { + async move { + let potential_contacts = search_users.await?; + this.update(&mut cx, |this, cx| { + this.potential_contacts = potential_contacts; + this.update_entries(cx); + }); + Ok(()) + } + .log_err() + })); + } - if query.is_empty() { - self.contacts.clear(); - self.contacts - .extend_from_slice(self.user_store.read(cx).contacts()); + fn update_entries(&mut self, cx: &mut ViewContext) { + let user_store = self.user_store.read(cx); + let query = self.user_query_editor.read(cx).text(cx); + let executor = cx.background().clone(); - if query_changed { - self.potential_contacts.clear(); + self.entries.clear(); + + let incoming = user_store.incoming_contact_requests(); + if !incoming.is_empty() { + self.match_candidates.clear(); + self.match_candidates + .extend( + incoming + .iter() + .enumerate() + .map(|(ix, user)| StringMatchCandidate { + id: ix, + string: user.github_login.clone(), + char_bag: user.github_login.chars().collect(), + }), + ); + let matches = executor.block(match_strings( + &self.match_candidates, + &query, + true, + usize::MAX, + &Default::default(), + executor.clone(), + )); + if !matches.is_empty() { + self.entries.push(ContactEntry::Header("Requests Received")); + self.entries.extend( + matches.iter().map(|mat| { + ContactEntry::IncomingRequest(incoming[mat.candidate_id].clone()) + }), + ); } + } - self.update_list_state(cx); - return; + let outgoing = user_store.outgoing_contact_requests(); + if !outgoing.is_empty() { + self.match_candidates.clear(); + self.match_candidates + .extend( + outgoing + .iter() + .enumerate() + .map(|(ix, user)| StringMatchCandidate { + id: ix, + string: user.github_login.clone(), + char_bag: user.github_login.chars().collect(), + }), + ); + let matches = executor.block(match_strings( + &self.match_candidates, + &query, + true, + usize::MAX, + &Default::default(), + executor.clone(), + )); + if !matches.is_empty() { + self.entries.push(ContactEntry::Header("Requests Sent")); + self.entries.extend( + matches.iter().map(|mat| { + ContactEntry::OutgoingRequest(outgoing[mat.candidate_id].clone()) + }), + ); + } } - let contacts = self.user_store.read(cx).contacts().to_vec(); - let candidates = contacts - .iter() - .enumerate() - .map(|(ix, contact)| StringMatchCandidate { - id: ix, - string: contact.user.github_login.clone(), - char_bag: contact.user.github_login.chars().collect(), - }) - .collect::>(); - let cancel_flag = Default::default(); - let background = cx.background().clone(); + let contacts = user_store.contacts(); + if !contacts.is_empty() { + self.match_candidates.clear(); + self.match_candidates + .extend( + contacts + .iter() + .enumerate() + .map(|(ix, contact)| StringMatchCandidate { + id: ix, + string: contact.user.github_login.clone(), + char_bag: contact.user.github_login.chars().collect(), + }), + ); + let matches = executor.block(match_strings( + &self.match_candidates, + &query, + true, + usize::MAX, + &Default::default(), + executor.clone(), + )); + if !matches.is_empty() { + self.entries.push(ContactEntry::Header("Contacts")); + self.entries.extend( + matches + .iter() + .map(|mat| ContactEntry::Contact(contacts[mat.candidate_id].clone())), + ); + } + } - let search_users = if query_changed { - self.user_store - .update(cx, |store, cx| store.fuzzy_search_users(query.clone(), cx)) - } else { - Task::ready(Ok(self.potential_contacts.clone())) - }; + if !self.potential_contacts.is_empty() { + self.entries.push(ContactEntry::Header("Add Contacts")); + self.entries.extend( + self.potential_contacts + .iter() + .map(|user| ContactEntry::PotentialContact(user.clone())), + ); + } - let match_contacts = async move { - anyhow::Ok( - fuzzy::match_strings( - &candidates, - query.as_str(), - false, - 100, - &cancel_flag, - background, - ) - .await, - ) - }; + self.list_state.reset(self.entries.len()); - self.contacts_search_task = Some(cx.spawn(|this, mut cx| async move { - let (contact_matches, users) = - futures::future::join(match_contacts, search_users).await; - let contact_matches = contact_matches.log_err()?; - let users = users.log_err()?; - - this.update(&mut cx, |this, cx| { - let user_store = this.user_store.read(cx); - this.contacts.clear(); - this.contacts.extend( - contact_matches - .iter() - .map(|mat| contacts[mat.candidate_id].clone()), - ); - this.potential_contacts = users; - this.potential_contacts - .retain(|user| !user_store.has_contact(&user)); - this.update_list_state(cx); - }); - None - })); + log::info!("UPDATE ENTRIES"); + dbg!(&self.entries); + + cx.notify(); } fn request_contact(&mut self, request: &RequestContact, cx: &mut ViewContext) { @@ -445,6 +670,18 @@ impl ContactsPanel { .update(cx, |store, cx| store.remove_contact(request.0, cx)) .detach(); } + + fn respond_to_contact_request( + &mut self, + action: &RespondToContactRequest, + cx: &mut ViewContext, + ) { + self.user_store + .update(cx, |store, cx| { + store.respond_to_contact_request(action.user_id, action.accept, cx) + }) + .detach(); + } } pub enum Event {} diff --git a/crates/fuzzy/src/fuzzy.rs b/crates/fuzzy/src/fuzzy.rs index 7458f27c91521e24f7ff68478deba1c592659b66..f6abb22ddc4312f22f8d68013dab4082c7cea0c9 100644 --- a/crates/fuzzy/src/fuzzy.rs +++ b/crates/fuzzy/src/fuzzy.rs @@ -185,6 +185,18 @@ pub async fn match_strings( return Default::default(); } + if query.is_empty() { + return candidates + .iter() + .map(|candidate| StringMatch { + candidate_id: candidate.id, + score: 0., + positions: Default::default(), + string: candidate.string.clone(), + }) + .collect(); + } + let lowercase_query = query.to_lowercase().chars().collect::>(); let query = query.chars().collect::>(); @@ -195,7 +207,7 @@ pub async fn match_strings( let num_cpus = background.num_cpus().min(candidates.len()); let segment_size = (candidates.len() + num_cpus - 1) / num_cpus; let mut segment_results = (0..num_cpus) - .map(|_| Vec::with_capacity(max_results)) + .map(|_| Vec::with_capacity(max_results.min(candidates.len()))) .collect::>(); background From 3d6db9083d51522882740319458722843b0c9f14 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 9 May 2022 15:57:16 -0600 Subject: [PATCH 24/53] Update a user's contacts when they connect; fix test failures The test failure we fixed doesn't seem directly related to the contact update. Maybe it just caused a failure to occur earlier than it would have in the sequence of seeds. We fixed the test failure by responding to a user joining the project while holding the lock on the Store. This ensures that we don't send messages related to the project to that user until they've had a chance to setup event handlers after receiving the response. Co-Authored-By: Max Brunsfeld --- crates/collab/src/rpc.rs | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 4ccc332e8cc66e5fed72de8e5b45570eee437afa..0efd60d1864532c42febe895faaf6fc5999e322d 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -277,7 +277,7 @@ impl Server { store.add_connection(connection_id, user_id); this.peer.send(connection_id, store.build_initial_contacts_update(contacts))?; } - // this.update_user_contacts(user_id).await?; + this.update_user_contacts(user_id).await?; let handle_io = handle_io.fuse(); futures::pin_mut!(handle_io); @@ -475,7 +475,6 @@ impl Server { return Err(anyhow!("no such project"))?; } - let response_payload; { let state = &mut *self.store_mut().await; let joined = state.join_project(request.sender_id, guest_user_id, project_id)?; @@ -515,13 +514,6 @@ impl Server { }); } } - response_payload = proto::JoinProjectResponse { - worktrees, - replica_id: joined.replica_id as u32, - collaborators, - language_servers: joined.project.language_servers.clone(), - }; - broadcast( request.sender_id, joined.project.connection_ids(), @@ -532,16 +524,21 @@ impl Server { project_id, collaborator: Some(proto::Collaborator { peer_id: request.sender_id.0, - replica_id: response_payload.replica_id, + replica_id: joined.replica_id as u32, user_id: guest_user_id.to_proto(), }), }, ) }, ); + response.send(proto::JoinProjectResponse { + worktrees, + replica_id: joined.replica_id as u32, + collaborators, + language_servers: joined.project.language_servers.clone(), + })?; } self.update_user_contacts(host_user_id).await?; - response.send(response_payload)?; Ok(()) } @@ -6187,8 +6184,8 @@ mod tests { operations += 1; } 20..=29 if clients.len() > 1 => { - log::info!("Removing guest"); let guest_ix = rng.lock().gen_range(1..clients.len()); + log::info!("Removing guest {}", user_ids[guest_ix]); let removed_guest_id = user_ids.remove(guest_ix); let guest = clients.remove(guest_ix); op_start_signals.remove(guest_ix); From 2aec4ff234af2823b1f24e29531e6dd7b55d3b8a Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 9 May 2022 15:39:45 -0700 Subject: [PATCH 25/53] Pick files directly from the Fs in simulate_host Previously, the list of all existing files was maintained separately, but it was not updated when a guest created a file. --- crates/collab/src/rpc.rs | 34 ++++++++++++---------------------- crates/project/src/fs.rs | 10 ++++++++++ 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 0efd60d1864532c42febe895faaf6fc5999e322d..4978b2fca4d2e5e123a7f47f3adb1bcb743a06f0 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -5910,7 +5910,6 @@ mod tests { let mut clients = Vec::new(); let mut user_ids = Vec::new(); let mut op_start_signals = Vec::new(); - let files = Arc::new(Mutex::new(Vec::new())); let mut next_entity_id = 100000; let mut host_cx = TestAppContext::new( @@ -5964,7 +5963,7 @@ mod tests { capabilities: lsp::LanguageServer::full_capabilities(), initializer: Some(Box::new({ let rng = rng.clone(); - let files = files.clone(); + let fs = fs.clone(); let project = host_project.downgrade(); move |fake_server: &mut FakeLanguageServer| { fake_server.handle_request::( @@ -6005,13 +6004,13 @@ mod tests { ); fake_server.handle_request::({ - let files = files.clone(); + let fs = fs.clone(); let rng = rng.clone(); move |_, _| { - let files = files.clone(); + let fs = fs.clone(); let rng = rng.clone(); async move { - let files = files.lock(); + let files = fs.files().await; let mut rng = rng.lock(); let count = rng.gen_range::(1..3); let files = (0..count) @@ -6082,7 +6081,6 @@ mod tests { op_start_signals.push(op_start_signal.0); clients.push(host_cx.foreground().spawn(host.simulate_host( host_project, - files, op_start_signal.1, rng.clone(), host_cx, @@ -6696,7 +6694,6 @@ mod tests { async fn simulate_host( mut self, project: ModelHandle, - files: Arc>>, op_start_signal: futures::channel::mpsc::UnboundedReceiver<()>, rng: Arc>, mut cx: TestAppContext, @@ -6704,7 +6701,6 @@ mod tests { async fn simulate_host_internal( client: &mut TestClient, project: ModelHandle, - files: Arc>>, mut op_start_signal: futures::channel::mpsc::UnboundedReceiver<()>, rng: Arc>, cx: &mut TestAppContext, @@ -6713,9 +6709,10 @@ mod tests { while op_start_signal.next().await.is_some() { let distribution = rng.lock().gen_range::(0..100); + let files = fs.as_fake().files().await; match distribution { - 0..=20 if !files.lock().is_empty() => { - let path = files.lock().choose(&mut *rng.lock()).unwrap().clone(); + 0..=20 if !files.is_empty() => { + let path = files.choose(&mut *rng.lock()).unwrap(); let mut path = path.as_path(); while let Some(parent_path) = path.parent() { path = parent_path; @@ -6734,9 +6731,9 @@ mod tests { find_or_create_worktree.await?; } } - 10..=80 if !files.lock().is_empty() => { + 10..=80 if !files.is_empty() => { let buffer = if client.buffers.is_empty() || rng.lock().gen() { - let file = files.lock().choose(&mut *rng.lock()).unwrap().clone(); + let file = files.choose(&mut *rng.lock()).unwrap(); let (worktree, path) = project .update(cx, |project, cx| { project.find_or_create_local_worktree( @@ -6810,7 +6807,6 @@ mod tests { if fs.create_dir(&parent_path).await.is_ok() && fs.create_file(&path, Default::default()).await.is_ok() { - files.lock().push(path); break; } else { log::info!("Host: cannot create file"); @@ -6824,15 +6820,9 @@ mod tests { Ok(()) } - let result = simulate_host_internal( - &mut self, - project.clone(), - files, - op_start_signal, - rng, - &mut cx, - ) - .await; + let result = + simulate_host_internal(&mut self, project.clone(), op_start_signal, rng, &mut cx) + .await; log::info!("Host done"); self.project = Some(project); (self, cx, result.err()) diff --git a/crates/project/src/fs.rs b/crates/project/src/fs.rs index 912dc65afeae289e4283a44f9d0a6d2bdc281956..7da2a38a83bc2d5a15a1d369024314508ff29f2c 100644 --- a/crates/project/src/fs.rs +++ b/crates/project/src/fs.rs @@ -376,6 +376,16 @@ impl FakeFs { .boxed() } + pub async fn files(&self) -> Vec { + self.state + .lock() + .await + .entries + .iter() + .filter_map(|(path, entry)| entry.content.as_ref().map(|_| path.clone())) + .collect() + } + async fn simulate_random_delay(&self) { self.executor .upgrade() From d7cba73ead03d1eab272bff90b6e056a5abf9933 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 9 May 2022 16:19:00 -0700 Subject: [PATCH 26/53] Decrement pending_contact_requests even if a request fails --- crates/client/src/user.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index 6fd9b1ded1625d889b5d30be46874c31cbe75d06..b4743d35675a8fce9c5efa0ac6d263afca687694 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -322,10 +322,10 @@ impl UserStore { cx.notify(); cx.spawn(|this, mut cx| async move { - let request = client + let response = client .ok_or_else(|| anyhow!("can't upgrade client reference"))? - .request(request); - request.await?; + .request(request) + .await; this.update(&mut cx, |this, cx| { if let Entry::Occupied(mut request_count) = this.pending_contact_requests.entry(user_id) @@ -337,6 +337,7 @@ impl UserStore { } cx.notify(); }); + response?; Ok(()) }) } From 054d697fb74f21f9c8dc1b674b87e8f8a80678a2 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 9 May 2022 16:23:27 -0700 Subject: [PATCH 27/53] Remove remaining code associated with .zed.toml files --- crates/collab/src/rpc.rs | 8 +------- crates/project/src/worktree.rs | 20 -------------------- crates/rpc/proto/zed.proto | 3 +-- 3 files changed, 2 insertions(+), 29 deletions(-) diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 4978b2fca4d2e5e123a7f47f3adb1bcb743a06f0..98342f1be3064dfb37e397c21d31ca823b60c0ca 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -5880,13 +5880,7 @@ mod tests { let host_language_registry = Arc::new(LanguageRegistry::test()); let fs = FakeFs::new(cx.background()); - fs.insert_tree( - "/_collab", - json!({ - ".zed.toml": r#"collaborators = ["guest-1", "guest-2", "guest-3", "guest-4"]"# - }), - ) - .await; + fs.insert_tree("/_collab", json!({"init": ""})).await; let mut server = TestServer::start(cx.foreground(), cx.background()).await; let db = server.app_state.db.clone(); diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index bab41bbe278e21330b1f695a09feb9375efc105f..84fedbbde7facc527500c5137a8bdf2a8bf6bbe0 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -32,7 +32,6 @@ use postage::{ prelude::{Sink as _, Stream as _}, watch, }; -use serde::Deserialize; use smol::channel::{self, Sender}; use std::{ any::Any, @@ -64,7 +63,6 @@ pub enum Worktree { pub struct LocalWorktree { snapshot: LocalSnapshot, - config: WorktreeConfig, background_snapshot: Arc>, last_scan_state_rx: watch::Receiver, _background_scanner_task: Option>, @@ -143,11 +141,6 @@ struct ShareState { _maintain_remote_snapshot: Option>>, } -#[derive(Default, Deserialize)] -struct WorktreeConfig { - collaborators: Vec, -} - pub enum Event { UpdatedEntries, } @@ -460,13 +453,6 @@ impl LocalWorktree { .await .context("failed to stat worktree path")?; - let mut config = WorktreeConfig::default(); - if let Ok(zed_toml) = fs.load(&abs_path.join(".zed.toml")).await { - if let Ok(parsed) = toml::from_str(&zed_toml) { - config = parsed; - } - } - let (scan_states_tx, mut scan_states_rx) = mpsc::unbounded(); let (mut last_scan_state_tx, last_scan_state_rx) = watch::channel_with(ScanState::Scanning); let tree = cx.add_model(move |cx: &mut ModelContext| { @@ -496,7 +482,6 @@ impl LocalWorktree { let tree = Self { snapshot: snapshot.clone(), - config, background_snapshot: Arc::new(Mutex::new(snapshot)), last_scan_state_rx, _background_scanner_task: None, @@ -544,10 +529,6 @@ impl LocalWorktree { } } - pub fn authorized_logins(&self) -> Vec { - self.config.collaborators.clone() - } - pub(crate) fn load_buffer( &mut self, path: &Path, @@ -879,7 +860,6 @@ impl LocalWorktree { project_id, worktree_id: self.id().to_proto(), root_name: self.root_name().to_string(), - authorized_logins: self.authorized_logins(), visible: self.visible, }; let request = client.request(register_message); diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 55bdba975159bb5b912d849ce10afaa5fb617128..8adba5fc80c10dbac3413e803750e086a9ba8563 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -151,8 +151,7 @@ message RegisterWorktree { uint64 project_id = 1; uint64 worktree_id = 2; string root_name = 3; - repeated string authorized_logins = 4; - bool visible = 5; + bool visible = 4; } message UnregisterWorktree { From 0533a0bd3c2ebb8c13a64f473eb89b0621514363 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 9 May 2022 16:26:44 -0700 Subject: [PATCH 28/53] Allow users to remove contact relationships that they initiated --- crates/collab/src/db.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index e5b3b31571bd2d3457aec0e92bfdc76640a071a4..57619941a0cc387e86a22f2362b6e54f837d2284 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -269,19 +269,18 @@ impl Db for PostgresDb { } async fn remove_contact(&self, requester_id: UserId, responder_id: UserId) -> Result<()> { - let (id_a, id_b, a_to_b) = if responder_id < requester_id { - (responder_id, requester_id, false) + let (id_a, id_b) = if responder_id < requester_id { + (responder_id, requester_id) } else { - (requester_id, responder_id, true) + (requester_id, responder_id) }; let query = " DELETE FROM contacts - WHERE user_id_a = $1 AND user_id_b = $2 AND a_to_b = $3; + WHERE user_id_a = $1 AND user_id_b = $2; "; let result = sqlx::query(query) .bind(id_a.0) .bind(id_b.0) - .bind(a_to_b) .execute(&self.pool) .await?; From 2a2698b8db4483d65c10c3b7bd8b839321b7487f Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 9 May 2022 16:27:08 -0700 Subject: [PATCH 29/53] Remove stray printing in contacts panel --- crates/contacts_panel/src/contacts_panel.rs | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/crates/contacts_panel/src/contacts_panel.rs b/crates/contacts_panel/src/contacts_panel.rs index 11883e88379fb3280dff0606b7213ddab003376b..e87888816e78b27fd92af818e542872b659ce781 100644 --- a/crates/contacts_panel/src/contacts_panel.rs +++ b/crates/contacts_panel/src/contacts_panel.rs @@ -361,7 +361,7 @@ impl ContactsPanel { cx.dispatch_action(RespondToContactRequest { user_id, accept: false, - }); + }) }) .with_cursor_style(CursorStyle::PointingHand) .flex_float() @@ -378,7 +378,7 @@ impl ContactsPanel { cx.dispatch_action(RespondToContactRequest { user_id, accept: true, - }); + }) }) .with_cursor_style(CursorStyle::PointingHand) .boxed(), @@ -438,9 +438,7 @@ impl ContactsPanel { .flex_float() .boxed() }) - .on_click(move |_, cx| { - cx.dispatch_action(RemoveContact(user_id)); - }) + .on_click(move |_, cx| cx.dispatch_action(RemoveContact(user_id))) .with_cursor_style(CursorStyle::PointingHand) .flex_float() .boxed(), @@ -488,9 +486,9 @@ impl ContactsPanel { ContactRequestStatus::None | ContactRequestStatus::RequestReceived => { "+" } - ContactRequestStatus::Pending => "…", ContactRequestStatus::RequestSent => "-", - ContactRequestStatus::RequestAccepted => unreachable!(), + ContactRequestStatus::Pending + | ContactRequestStatus::RequestAccepted => "…", }; Label::new(label.to_string(), theme.edit_contact.text.clone()) @@ -652,10 +650,6 @@ impl ContactsPanel { } self.list_state.reset(self.entries.len()); - - log::info!("UPDATE ENTRIES"); - dbg!(&self.entries); - cx.notify(); } From b8aba0972d19b09b24e5ee2e209649df5c533151 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 9 May 2022 17:23:39 -0700 Subject: [PATCH 30/53] Wait until contacts have been cleared when disconnecting Also, use an mpsc for UpdateContacts messages, not a watch, since the messages now represent changes instead of snapshots. Co-authored-by: Nathan Sobo --- crates/client/src/user.rs | 227 +++++++++++++++++++++----------------- crates/collab/src/db.rs | 100 +++++++++-------- crates/collab/src/rpc.rs | 10 +- 3 files changed, 186 insertions(+), 151 deletions(-) diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index b4743d35675a8fce9c5efa0ac6d263afca687694..a8de7a082d4df79a116b8cc303d87cff49ca8127 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -1,6 +1,6 @@ use super::{http::HttpClient, proto, Client, Status, TypedEnvelope}; use anyhow::{anyhow, Context, Result}; -use futures::{future, AsyncReadExt}; +use futures::{channel::mpsc, future, AsyncReadExt, Future, StreamExt}; use gpui::{AsyncAppContext, Entity, ImageData, ModelContext, ModelHandle, Task}; use postage::{prelude::Stream, sink::Sink, watch}; use rpc::proto::{RequestMessage, UsersResponse}; @@ -42,7 +42,7 @@ pub enum ContactRequestStatus { pub struct UserStore { users: HashMap>, - update_contacts_tx: watch::Sender>, + update_contacts_tx: mpsc::UnboundedSender, current_user: watch::Receiver>>, contacts: Vec>, incoming_contact_requests: Vec>, @@ -60,6 +60,11 @@ impl Entity for UserStore { type Event = Event; } +enum UpdateContacts { + Update(proto::UpdateContacts), + Clear(postage::barrier::Sender), +} + impl UserStore { pub fn new( client: Arc, @@ -67,8 +72,7 @@ impl UserStore { cx: &mut ModelContext, ) -> Self { let (mut current_user_tx, current_user_rx) = watch::channel(); - let (update_contacts_tx, mut update_contacts_rx) = - watch::channel::>(); + let (update_contacts_tx, mut update_contacts_rx) = mpsc::unbounded(); let rpc_subscription = client.add_message_handler(cx.handle(), Self::handle_update_contacts); Self { @@ -82,8 +86,8 @@ impl UserStore { http, _maintain_contacts: cx.spawn_weak(|this, mut cx| async move { let _subscription = rpc_subscription; - while let Some(message) = update_contacts_rx.recv().await { - if let Some((message, this)) = message.zip(this.upgrade(&cx)) { + while let Some(message) = update_contacts_rx.next().await { + if let Some(this) = this.upgrade(&cx) { this.update(&mut cx, |this, cx| this.update_contacts(message, cx)) .log_err() .await; @@ -121,114 +125,130 @@ impl UserStore { mut cx: AsyncAppContext, ) -> Result<()> { this.update(&mut cx, |this, _| { - *this.update_contacts_tx.borrow_mut() = Some(msg.payload); + this.update_contacts_tx + .unbounded_send(UpdateContacts::Update(msg.payload)) + .unwrap(); }); Ok(()) } fn update_contacts( &mut self, - message: proto::UpdateContacts, + message: UpdateContacts, cx: &mut ModelContext, ) -> Task> { - log::info!("update contacts on client {:?}", message); - let mut user_ids = HashSet::new(); - 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()); - - let load_users = self.get_users(user_ids.into_iter().collect(), cx); - cx.spawn(|this, mut cx| async move { - load_users.await?; - - // Users are fetched in parallel above and cached in call to get_users - // No need to paralellize here - let mut updated_contacts = Vec::new(); - for contact in message.contacts { - updated_contacts.push(Arc::new( - Contact::from_proto(contact, &this, &mut cx).await?, - )); - } - - let mut incoming_requests = Vec::new(); - for request in message.incoming_requests { - incoming_requests.push( - this.update(&mut cx, |this, cx| { - this.fetch_user(request.requester_id, cx) - }) - .await?, - ); + match message { + UpdateContacts::Clear(barrier) => { + self.contacts.clear(); + self.incoming_contact_requests.clear(); + self.outgoing_contact_requests.clear(); + drop(barrier); + Task::ready(Ok(())) } - - let mut outgoing_requests = Vec::new(); - for requested_user_id in message.outgoing_requests { - outgoing_requests.push( - this.update(&mut cx, |this, cx| this.fetch_user(requested_user_id, cx)) - .await?, + UpdateContacts::Update(message) => { + log::info!( + "update contacts on client {}: {:?}", + self.client.upgrade().unwrap().id, + message ); - } - - let removed_contacts = - HashSet::::from_iter(message.remove_contacts.iter().copied()); - let removed_incoming_requests = - HashSet::::from_iter(message.remove_incoming_requests.iter().copied()); - let removed_outgoing_requests = - HashSet::::from_iter(message.remove_outgoing_requests.iter().copied()); - - this.update(&mut cx, |this, cx| { - // Remove contacts - this.contacts - .retain(|contact| !removed_contacts.contains(&contact.user.id)); - // Update existing contacts and insert new ones - for updated_contact in updated_contacts { - match this - .contacts - .binary_search_by_key(&&updated_contact.user.github_login, |contact| { - &contact.user.github_login - }) { - Ok(ix) => this.contacts[ix] = updated_contact, - Err(ix) => this.contacts.insert(ix, updated_contact), - } + let mut user_ids = HashSet::new(); + 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()); + + let load_users = self.get_users(user_ids.into_iter().collect(), cx); + cx.spawn(|this, mut cx| async move { + load_users.await?; + + // Users are fetched in parallel above and cached in call to get_users + // No need to paralellize here + let mut updated_contacts = Vec::new(); + for contact in message.contacts { + updated_contacts.push(Arc::new( + Contact::from_proto(contact, &this, &mut cx).await?, + )); + } - // Remove incoming contact requests - this.incoming_contact_requests - .retain(|user| !removed_incoming_requests.contains(&user.id)); - // Update existing incoming requests and insert new ones - for request in incoming_requests { - match this - .incoming_contact_requests - .binary_search_by_key(&&request.github_login, |contact| { - &contact.github_login - }) { - Ok(ix) => this.incoming_contact_requests[ix] = request, - Err(ix) => this.incoming_contact_requests.insert(ix, request), + let mut incoming_requests = Vec::new(); + for request in message.incoming_requests { + incoming_requests.push( + this.update(&mut cx, |this, cx| { + this.fetch_user(request.requester_id, cx) + }) + .await?, + ); } - } - // Remove outgoing contact requests - this.outgoing_contact_requests - .retain(|user| !removed_outgoing_requests.contains(&user.id)); - // Update existing incoming requests and insert new ones - for request in outgoing_requests { - match this - .outgoing_contact_requests - .binary_search_by_key(&&request.github_login, |contact| { - &contact.github_login - }) { - Ok(ix) => this.outgoing_contact_requests[ix] = request, - Err(ix) => this.outgoing_contact_requests.insert(ix, request), + let mut outgoing_requests = Vec::new(); + for requested_user_id in message.outgoing_requests { + outgoing_requests.push( + this.update(&mut cx, |this, cx| this.fetch_user(requested_user_id, cx)) + .await?, + ); } - } - cx.notify(); - }); + let removed_contacts = + HashSet::::from_iter(message.remove_contacts.iter().copied()); + let removed_incoming_requests = + HashSet::::from_iter(message.remove_incoming_requests.iter().copied()); + let removed_outgoing_requests = + HashSet::::from_iter(message.remove_outgoing_requests.iter().copied()); - Ok(()) - }) + this.update(&mut cx, |this, cx| { + // Remove contacts + this.contacts + .retain(|contact| !removed_contacts.contains(&contact.user.id)); + // Update existing contacts and insert new ones + for updated_contact in updated_contacts { + match this.contacts.binary_search_by_key( + &&updated_contact.user.github_login, + |contact| &contact.user.github_login, + ) { + Ok(ix) => this.contacts[ix] = updated_contact, + Err(ix) => this.contacts.insert(ix, updated_contact), + } + } + + // Remove incoming contact requests + this.incoming_contact_requests + .retain(|user| !removed_incoming_requests.contains(&user.id)); + // Update existing incoming requests and insert new ones + for request in incoming_requests { + match this + .incoming_contact_requests + .binary_search_by_key(&&request.github_login, |contact| { + &contact.github_login + }) { + Ok(ix) => this.incoming_contact_requests[ix] = request, + Err(ix) => this.incoming_contact_requests.insert(ix, request), + } + } + + // Remove outgoing contact requests + this.outgoing_contact_requests + .retain(|user| !removed_outgoing_requests.contains(&user.id)); + // Update existing incoming requests and insert new ones + for request in outgoing_requests { + match this + .outgoing_contact_requests + .binary_search_by_key(&&request.github_login, |contact| { + &contact.github_login + }) { + Ok(ix) => this.outgoing_contact_requests[ix] = request, + Err(ix) => this.outgoing_contact_requests.insert(ix, request), + } + } + + cx.notify(); + }); + + Ok(()) + }) + } + } } pub fn contacts(&self) -> &[Arc] { @@ -342,11 +362,14 @@ impl UserStore { }) } - #[cfg(any(test, feature = "test-support"))] - pub fn clear_contacts(&mut self) { - self.contacts.clear(); - self.incoming_contact_requests.clear(); - self.outgoing_contact_requests.clear(); + pub fn clear_contacts(&mut self) -> impl Future { + let (tx, mut rx) = postage::barrier::channel(); + self.update_contacts_tx + .unbounded_send(UpdateContacts::Clear(tx)) + .unwrap(); + async move { + rx.recv().await; + } } pub fn get_users( diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index 57619941a0cc387e86a22f2362b6e54f837d2284..3e8ec6b3221d01a95494d053344501e0aabd7557 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -77,6 +77,8 @@ pub trait Db: Send + Sync { ) -> Result>; #[cfg(test)] async fn teardown(&self, url: &str); + #[cfg(test)] + fn as_fake<'a>(&'a self) -> Option<&'a tests::FakeDb>; } pub struct PostgresDb { @@ -291,6 +293,37 @@ impl Db for PostgresDb { } } + async fn dismiss_contact_request( + &self, + responder_id: UserId, + requester_id: UserId, + ) -> Result<()> { + let (id_a, id_b, a_to_b) = if responder_id < requester_id { + (responder_id, requester_id, false) + } else { + (requester_id, responder_id, true) + }; + + let query = " + UPDATE contacts + SET should_notify = 'f' + WHERE user_id_a = $1 AND user_id_b = $2 AND a_to_b = $3; + "; + + let result = sqlx::query(query) + .bind(id_a.0) + .bind(id_b.0) + .bind(a_to_b) + .execute(&self.pool) + .await?; + + if result.rows_affected() == 0 { + Err(anyhow!("no such contact request"))?; + } + + Ok(()) + } + async fn respond_to_contact_request( &self, responder_id: UserId, @@ -333,37 +366,6 @@ impl Db for PostgresDb { } } - async fn dismiss_contact_request( - &self, - responder_id: UserId, - requester_id: UserId, - ) -> Result<()> { - let (id_a, id_b, a_to_b) = if responder_id < requester_id { - (responder_id, requester_id, false) - } else { - (requester_id, responder_id, true) - }; - - let query = " - UPDATE contacts - SET should_notify = 'f' - WHERE user_id_a = $1 AND user_id_b = $2 AND a_to_b = $3; - "; - - let result = sqlx::query(query) - .bind(id_a.0) - .bind(id_b.0) - .bind(a_to_b) - .execute(&self.pool) - .await?; - - if result.rows_affected() == 0 { - Err(anyhow!("no such contact request"))?; - } - - Ok(()) - } - // access tokens async fn create_access_token_hash( @@ -620,6 +622,11 @@ impl Db for PostgresDb { .await .log_err(); } + + #[cfg(test)] + fn as_fake(&self) -> Option<&tests::FakeDb> { + None + } } macro_rules! id_type { @@ -1108,25 +1115,25 @@ pub mod tests { pub struct FakeDb { background: Arc, - users: Mutex>, + pub users: Mutex>, + pub orgs: Mutex>, + pub org_memberships: Mutex>, + pub channels: Mutex>, + pub channel_memberships: Mutex>, + pub channel_messages: Mutex>, + pub contacts: Mutex>, + next_channel_message_id: Mutex, next_user_id: Mutex, - orgs: Mutex>, next_org_id: Mutex, - org_memberships: Mutex>, - channels: Mutex>, next_channel_id: Mutex, - channel_memberships: Mutex>, - channel_messages: Mutex>, - next_channel_message_id: Mutex, - contacts: Mutex>, } #[derive(Debug)] - struct FakeContact { - requester_id: UserId, - responder_id: UserId, - accepted: bool, - should_notify: bool, + pub struct FakeContact { + pub requester_id: UserId, + pub responder_id: UserId, + pub accepted: bool, + pub should_notify: bool, } impl FakeDb { @@ -1514,5 +1521,10 @@ pub mod tests { } async fn teardown(&self, _: &str) {} + + #[cfg(test)] + fn as_fake(&self) -> Option<&FakeDb> { + Some(self) + } } } diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 98342f1be3064dfb37e397c21d31ca823b60c0ca..ea71ab5f0049b3a11c15d1c380a638a4506eef65 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -5321,7 +5321,7 @@ mod tests { async fn disconnect_and_reconnect(client: &TestClient, cx: &mut TestAppContext) { client.disconnect(&cx.to_async()).unwrap(); - client.clear_contacts(cx); + client.clear_contacts(cx).await; client .authenticate_and_connect(false, &cx.to_async()) .await @@ -6584,10 +6584,10 @@ mod tests { while authed_user.next().await.unwrap().is_none() {} } - fn clear_contacts(&self, cx: &mut TestAppContext) { - self.user_store.update(cx, |store, _| { - store.clear_contacts(); - }); + async fn clear_contacts(&self, cx: &mut TestAppContext) { + self.user_store + .update(cx, |store, _| store.clear_contacts()) + .await; } fn summarize_contacts(&self, cx: &TestAppContext) -> ContactsSummary { From 45b6a9df367195650124d05f78ec626f22ac9141 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 9 May 2022 18:03:24 -0700 Subject: [PATCH 31/53] Avoid sending a GetUsers request for an emptly list of user ids We don't actually need to return the users at this time. We just call this for its side effect. Co-authored-by: Nathan Sobo --- crates/client/src/user.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index a8de7a082d4df79a116b8cc303d87cff49ca8127..13439c6893a11f1bb0545e3d39b923b780a6f255 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -376,9 +376,17 @@ impl UserStore { &mut self, mut user_ids: Vec, cx: &mut ModelContext, - ) -> Task>>> { + ) -> Task> { user_ids.retain(|id| !self.users.contains_key(id)); - self.load_users(proto::GetUsers { user_ids }, cx) + if user_ids.is_empty() { + Task::ready(Ok(())) + } else { + let load = self.load_users(proto::GetUsers { user_ids }, cx); + cx.foreground().spawn(async move { + load.await?; + Ok(()) + }) + } } pub fn fuzzy_search_users( From 3dee656490310e9ab24f162f80c096462aa02ce7 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 9 May 2022 18:05:10 -0700 Subject: [PATCH 32/53] Avoid panic when language server is dropped before being initialized in tests Co-authored-by: Nathan Sobo --- crates/language/src/language.rs | 11 +++++++---- crates/lsp/src/lsp.rs | 10 ++++++++-- crates/project/src/project.rs | 2 +- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 14b5a288e1e08f2f9f333b3782bdf1c4b975da42..b9d28c76a1355044b2d12d44f38781f9087b4ad6 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -289,10 +289,13 @@ impl LanguageRegistry { let servers_tx = servers_tx.clone(); cx.background() .spawn(async move { - fake_server - .receive_notification::() - .await; - servers_tx.unbounded_send(fake_server).ok(); + if fake_server + .try_receive_notification::() + .await + .is_some() + { + servers_tx.unbounded_send(fake_server).ok(); + } }) .detach(); Ok(server) diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index 02bc0486f1aaddae3fd5eed037a2461d8b8a139a..d5af202516c4134d02213bcb7841422edcaa1606 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -647,12 +647,18 @@ impl FakeLanguageServer { } pub async fn receive_notification(&mut self) -> T::Params { + self.try_receive_notification::().await.unwrap() + } + + pub async fn try_receive_notification( + &mut self, + ) -> Option { use futures::StreamExt as _; loop { - let (method, params) = self.notifications_rx.next().await.unwrap(); + let (method, params) = self.notifications_rx.next().await?; if &method == T::METHOD { - return serde_json::from_str::(¶ms).unwrap(); + return Some(serde_json::from_str::(¶ms).unwrap()); } else { log::info!("skipping message in fake language server {:?}", params); } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index b54c3677fa34096171a5c0d81ef72a72a3f7c6ec..d23122f45b337d5bc7152fef6cbec9abd3e4623e 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -6550,7 +6550,7 @@ mod tests { assert!(results.is_empty()); } - #[gpui::test] + #[gpui::test(iterations = 10)] async fn test_definition(cx: &mut gpui::TestAppContext) { let mut language = Language::new( LanguageConfig { From ef868ff0235ea867be1770f43dad2e004e0240c2 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 9 May 2022 20:41:18 -0600 Subject: [PATCH 33/53] Fix test after changing fuzzy matching for empty queries --- crates/project_symbols/src/project_symbols.rs | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/crates/project_symbols/src/project_symbols.rs b/crates/project_symbols/src/project_symbols.rs index 4f083888d9d04d55e8c35f8ecea3afe1446c9491..157ea8ef7380795d318430bffbd2dc989a3b6596 100644 --- a/crates/project_symbols/src/project_symbols.rs +++ b/crates/project_symbols/src/project_symbols.rs @@ -319,15 +319,20 @@ mod tests { .into_iter() .map(|name| StringMatchCandidate::new(0, name.into())) .collect::>(); - let matches = fuzzy::match_strings( - &candidates, - ¶ms.query, - true, - 100, - &Default::default(), - executor.clone(), - ) - .await; + let matches = if params.query.is_empty() { + Vec::new() + } else { + fuzzy::match_strings( + &candidates, + ¶ms.query, + true, + 100, + &Default::default(), + executor.clone(), + ) + .await + }; + Ok(Some( matches.into_iter().map(|mat| symbol(&mat.string)).collect(), )) From 4e9924c717d6eaabd38ebfed5f3c3b0308545365 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 9 May 2022 20:57:41 -0600 Subject: [PATCH 34/53] Filter out empty projects in contacts panel --- crates/client/src/user.rs | 6 + crates/contacts_panel/src/contacts_panel.rs | 239 ++++++++++---------- 2 files changed, 129 insertions(+), 116 deletions(-) diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index 13439c6893a11f1bb0545e3d39b923b780a6f255..a885b3b1ada8e121cd8933fc569bdd01a3733b89 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -501,6 +501,12 @@ impl Contact { } Ok(Self { user, projects }) } + + pub fn non_empty_projects(&self) -> impl Iterator { + self.projects + .iter() + .filter(|project| !project.worktree_root_names.is_empty()) + } } async fn fetch_avatar(http: &dyn HttpClient, url: &str) -> Result> { diff --git a/crates/contacts_panel/src/contacts_panel.rs b/crates/contacts_panel/src/contacts_panel.rs index e87888816e78b27fd92af818e542872b659ce781..4c354c56442a831b4d48229426ca2ab0bea1b853 100644 --- a/crates/contacts_panel/src/contacts_panel.rs +++ b/crates/contacts_panel/src/contacts_panel.rs @@ -149,7 +149,7 @@ impl ContactsPanel { theme: &theme::ContactsPanel, cx: &mut LayoutContext, ) -> ElementBox { - let project_count = contact.projects.len(); + let project_count = contact.non_empty_projects().count(); let font_cache = cx.font_cache(); let line_height = theme.unshared_project.name.text.line_height(font_cache); let cap_height = theme.unshared_project.name.text.cap_height(font_cache); @@ -188,121 +188,128 @@ impl ContactsPanel { .with_height(theme.row_height) .boxed(), ) - .with_children(contact.projects.iter().enumerate().map(|(ix, project)| { - let project_id = project.id; - - 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 ix + 1 == project_count { - 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., - }); - }) - .constrained() - .with_width(host_avatar_height) - .boxed(), - ) - .with_child({ - let is_host = Some(contact.user.id) == current_user_id; - let is_guest = !is_host - && project - .guests - .iter() - .any(|guest| Some(guest.id) == current_user_id); - let is_shared = project.is_shared; - let app_state = app_state.clone(); - - MouseEventHandler::new::( - project_id as usize, - cx, - |mouse_state, _| { - let style = match (project.is_shared, mouse_state.hovered) { - (false, false) => &theme.unshared_project, - (false, true) => &theme.hovered_unshared_project, - (true, false) => &theme.shared_project, - (true, true) => &theme.hovered_shared_project, - }; - - Flex::row() - .with_child( - Label::new( - project.worktree_root_names.join(", "), - style.name.text.clone(), - ) - .aligned() - .left() - .contained() - .with_style(style.name.container) - .boxed(), - ) - .with_children(project.guests.iter().filter_map( - |participant| { - participant.avatar.clone().map(|avatar| { - Image::new(avatar) - .with_style(style.guest_avatar) - .aligned() - .left() - .contained() - .with_margin_right(style.guest_avatar_spacing) - .boxed() - }) - }, - )) - .contained() - .with_style(style.container) - .constrained() - .with_height(style.height) - .boxed() - }, - ) - .with_cursor_style(if is_host || is_shared { - CursorStyle::PointingHand - } else { - CursorStyle::Arrow - }) - .on_click(move |_, cx| { - if !is_host && !is_guest { - cx.dispatch_global_action(JoinProject { - project_id, - app_state: app_state.clone(), - }); - } - }) - .flex(1., true) - .boxed() - }) - .constrained() - .with_height(theme.unshared_project.height) - .boxed() - })) + .with_children( + contact + .non_empty_projects() + .enumerate() + .map(|(ix, project)| { + let project_id = project.id; + 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 ix + 1 == project_count { + 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., + }); + }) + .constrained() + .with_width(host_avatar_height) + .boxed(), + ) + .with_child({ + let is_host = Some(contact.user.id) == current_user_id; + let is_guest = !is_host + && project + .guests + .iter() + .any(|guest| Some(guest.id) == current_user_id); + let is_shared = project.is_shared; + let app_state = app_state.clone(); + + MouseEventHandler::new::( + project_id as usize, + cx, + |mouse_state, _| { + let style = match (project.is_shared, mouse_state.hovered) { + (false, false) => &theme.unshared_project, + (false, true) => &theme.hovered_unshared_project, + (true, false) => &theme.shared_project, + (true, true) => &theme.hovered_shared_project, + }; + + Flex::row() + .with_child( + Label::new( + project.worktree_root_names.join(", "), + style.name.text.clone(), + ) + .aligned() + .left() + .contained() + .with_style(style.name.container) + .boxed(), + ) + .with_children(project.guests.iter().filter_map( + |participant| { + participant.avatar.clone().map(|avatar| { + Image::new(avatar) + .with_style(style.guest_avatar) + .aligned() + .left() + .contained() + .with_margin_right( + style.guest_avatar_spacing, + ) + .boxed() + }) + }, + )) + .contained() + .with_style(style.container) + .constrained() + .with_height(style.height) + .boxed() + }, + ) + .with_cursor_style(if is_host || is_shared { + CursorStyle::PointingHand + } else { + CursorStyle::Arrow + }) + .on_click(move |_, cx| { + if !is_host && !is_guest { + cx.dispatch_global_action(JoinProject { + project_id, + app_state: app_state.clone(), + }); + } + }) + .flex(1., true) + .boxed() + }) + .constrained() + .with_height(theme.unshared_project.height) + .boxed() + }), + ) .boxed() } From 52c36d3e3d3484e05add7f0099b5546c64fee018 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 10 May 2022 10:33:57 +0200 Subject: [PATCH 35/53] Maintain online status in contacts panel --- crates/client/src/user.rs | 15 ++- crates/collab/src/rpc.rs | 126 +++++++++++++++++--- crates/contacts_panel/src/contacts_panel.rs | 16 ++- 3 files changed, 134 insertions(+), 23 deletions(-) diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index a885b3b1ada8e121cd8933fc569bdd01a3733b89..3f52b2b6d909a0f87f5b18b64f4380b7650c1695 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -20,6 +20,7 @@ pub struct User { #[derive(Debug)] pub struct Contact { pub user: Arc, + pub online: bool, pub projects: Vec, } @@ -109,6 +110,14 @@ impl UserStore { } Status::SignedOut => { current_user_tx.send(None).await.ok(); + if let Some(this) = this.upgrade(&cx) { + this.update(&mut cx, |this, _| this.clear_contacts()).await; + } + } + Status::ConnectionLost => { + if let Some(this) = this.upgrade(&cx) { + this.update(&mut cx, |this, _| this.clear_contacts()).await; + } } _ => {} } @@ -499,7 +508,11 @@ impl Contact { guests, }); } - Ok(Self { user, projects }) + Ok(Self { + user, + online: contact.online, + projects, + }) } pub fn non_empty_projects(&self) -> impl Iterator { diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index ea71ab5f0049b3a11c15d1c380a638a4506eef65..51aec065cd5cc5675d9a8f947837f265d91c62c7 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -5000,13 +5000,22 @@ mod tests { .await; deterministic.run_until_parked(); client_a.user_store.read_with(cx_a, |store, _| { - assert_eq!(contacts(store), [("user_b", vec![]), ("user_c", vec![])]) + assert_eq!( + contacts(store), + [("user_b", true, vec![]), ("user_c", true, vec![])] + ) }); client_b.user_store.read_with(cx_b, |store, _| { - assert_eq!(contacts(store), [("user_a", vec![]), ("user_c", vec![])]) + assert_eq!( + contacts(store), + [("user_a", true, vec![]), ("user_c", true, vec![])] + ) }); client_c.user_store.read_with(cx_c, |store, _| { - assert_eq!(contacts(store), [("user_a", vec![]), ("user_b", vec![])]) + assert_eq!( + contacts(store), + [("user_a", true, vec![]), ("user_b", true, vec![])] + ) }); // Share a worktree as client A. @@ -5033,18 +5042,27 @@ mod tests { deterministic.run_until_parked(); client_a.user_store.read_with(cx_a, |store, _| { - assert_eq!(contacts(store), [("user_b", vec![]), ("user_c", vec![])]) + assert_eq!( + contacts(store), + [("user_b", true, vec![]), ("user_c", true, vec![])] + ) }); client_b.user_store.read_with(cx_b, |store, _| { assert_eq!( contacts(store), - [("user_a", vec![("a", false, vec![])]), ("user_c", vec![])] + [ + ("user_a", true, vec![("a", false, vec![])]), + ("user_c", true, vec![]) + ] ) }); client_c.user_store.read_with(cx_c, |store, _| { assert_eq!( contacts(store), - [("user_a", vec![("a", false, vec![])]), ("user_b", vec![])] + [ + ("user_a", true, vec![("a", false, vec![])]), + ("user_b", true, vec![]) + ] ) }); @@ -5057,18 +5075,27 @@ mod tests { .unwrap(); deterministic.run_until_parked(); client_a.user_store.read_with(cx_a, |store, _| { - assert_eq!(contacts(store), [("user_b", vec![]), ("user_c", vec![])]) + assert_eq!( + contacts(store), + [("user_b", true, vec![]), ("user_c", true, vec![])] + ) }); client_b.user_store.read_with(cx_b, |store, _| { assert_eq!( contacts(store), - [("user_a", vec![("a", true, vec![])]), ("user_c", vec![])] + [ + ("user_a", true, vec![("a", true, vec![])]), + ("user_c", true, vec![]) + ] ) }); client_c.user_store.read_with(cx_c, |store, _| { assert_eq!( contacts(store), - [("user_a", vec![("a", true, vec![])]), ("user_b", vec![])] + [ + ("user_a", true, vec![("a", true, vec![])]), + ("user_b", true, vec![]) + ] ) }); @@ -5084,14 +5111,17 @@ mod tests { .unwrap(); deterministic.run_until_parked(); client_a.user_store.read_with(cx_a, |store, _| { - assert_eq!(contacts(store), [("user_b", vec![]), ("user_c", vec![])]) + assert_eq!( + contacts(store), + [("user_b", true, vec![]), ("user_c", true, vec![])] + ) }); client_b.user_store.read_with(cx_b, |store, _| { assert_eq!( contacts(store), [ - ("user_a", vec![("a", true, vec!["user_b"])]), - ("user_c", vec![]) + ("user_a", true, vec![("a", true, vec!["user_b"])]), + ("user_c", true, vec![]) ] ) }); @@ -5099,8 +5129,8 @@ mod tests { assert_eq!( contacts(store), [ - ("user_a", vec![("a", true, vec!["user_b"])]), - ("user_b", vec![]) + ("user_a", true, vec![("a", true, vec!["user_b"])]), + ("user_b", true, vec![]) ] ) }); @@ -5114,16 +5144,70 @@ mod tests { cx_a.update(move |_| drop(project_a)); deterministic.run_until_parked(); client_a.user_store.read_with(cx_a, |store, _| { - assert_eq!(contacts(store), [("user_b", vec![]), ("user_c", vec![])]) + assert_eq!( + contacts(store), + [("user_b", true, vec![]), ("user_c", true, vec![])] + ) + }); + client_b.user_store.read_with(cx_b, |store, _| { + assert_eq!( + contacts(store), + [("user_a", true, vec![]), ("user_c", true, vec![])] + ) + }); + client_c.user_store.read_with(cx_c, |store, _| { + assert_eq!( + contacts(store), + [("user_a", true, vec![]), ("user_b", true, vec![])] + ) + }); + + server.disconnect_client(client_c.current_user_id(cx_c)); + server.forbid_connections(); + deterministic.advance_clock(rpc::RECEIVE_TIMEOUT); + + client_a.user_store.read_with(cx_a, |store, _| { + assert_eq!( + contacts(store), + [("user_b", true, vec![]), ("user_c", false, vec![])] + ) + }); + client_b.user_store.read_with(cx_b, |store, _| { + assert_eq!( + contacts(store), + [("user_a", true, vec![]), ("user_c", false, vec![])] + ) + }); + client_c + .user_store + .read_with(cx_c, |store, _| assert_eq!(contacts(store), [])); + + server.allow_connections(); + client_c + .authenticate_and_connect(false, &cx_c.to_async()) + .await + .unwrap(); + deterministic.run_until_parked(); + client_a.user_store.read_with(cx_a, |store, _| { + assert_eq!( + contacts(store), + [("user_b", true, vec![]), ("user_c", true, vec![])] + ) }); client_b.user_store.read_with(cx_b, |store, _| { - assert_eq!(contacts(store), [("user_a", vec![]), ("user_c", vec![])]) + assert_eq!( + contacts(store), + [("user_a", true, vec![]), ("user_c", true, vec![])] + ) }); client_c.user_store.read_with(cx_c, |store, _| { - assert_eq!(contacts(store), [("user_a", vec![]), ("user_b", vec![])]) + assert_eq!( + contacts(store), + [("user_a", true, vec![]), ("user_b", true, vec![])] + ) }); - fn contacts(user_store: &UserStore) -> Vec<(&str, Vec<(&str, bool, Vec<&str>)>)> { + fn contacts(user_store: &UserStore) -> Vec<(&str, bool, Vec<(&str, bool, Vec<&str>)>)> { user_store .contacts() .iter() @@ -5139,7 +5223,11 @@ mod tests { ) }) .collect(); - (contact.user.github_login.as_str(), worktrees) + ( + contact.user.github_login.as_str(), + contact.online, + worktrees, + ) }) .collect() } diff --git a/crates/contacts_panel/src/contacts_panel.rs b/crates/contacts_panel/src/contacts_panel.rs index 4c354c56442a831b4d48229426ca2ab0bea1b853..48386749ef5053e3fffdd148e13db4a1c386e56c 100644 --- a/crates/contacts_panel/src/contacts_panel.rs +++ b/crates/contacts_panel/src/contacts_panel.rs @@ -638,10 +638,20 @@ impl ContactsPanel { executor.clone(), )); if !matches.is_empty() { - self.entries.push(ContactEntry::Header("Contacts")); + let (online_contacts, offline_contacts) = matches + .iter() + .partition::, _>(|mat| contacts[mat.candidate_id].online); + + self.entries.push(ContactEntry::Header("Online")); self.entries.extend( - matches - .iter() + online_contacts + .into_iter() + .map(|mat| ContactEntry::Contact(contacts[mat.candidate_id].clone())), + ); + self.entries.push(ContactEntry::Header("Offline")); + self.entries.extend( + offline_contacts + .into_iter() .map(|mat| ContactEntry::Contact(contacts[mat.candidate_id].clone())), ); } From 09580516a0783b982ea5bec748938f8e57f7e3e1 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 10 May 2022 12:09:24 +0200 Subject: [PATCH 36/53] Start on adding icon for requesting contacts --- assets/icons/add-contact.svg | 3 +++ assets/themes/cave-dark.json | 10 ++++++++-- assets/themes/cave-light.json | 10 ++++++++-- assets/themes/dark.json | 10 ++++++++-- assets/themes/light.json | 10 ++++++++-- assets/themes/solarized-dark.json | 10 ++++++++-- assets/themes/solarized-light.json | 10 ++++++++-- assets/themes/sulphurpool-dark.json | 10 ++++++++-- assets/themes/sulphurpool-light.json | 10 ++++++++-- crates/contacts_panel/src/contacts_panel.rs | 22 +++++++++++++++++++-- crates/theme/src/theme.rs | 8 ++++++++ styles/src/styleTree/contactsPanel.ts | 10 +++++++--- 12 files changed, 102 insertions(+), 21 deletions(-) create mode 100644 assets/icons/add-contact.svg diff --git a/assets/icons/add-contact.svg b/assets/icons/add-contact.svg new file mode 100644 index 0000000000000000000000000000000000000000..4fc7790b9dc0d204997cff308d93d2bf4497c678 --- /dev/null +++ b/assets/icons/add-contact.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/themes/cave-dark.json b/assets/themes/cave-dark.json index 92319af4968f4db4fbb6ea6a23e263b624ddce66..1eac826b5436f5888e0f1a658d795bbc86f6e6c6 100644 --- a/assets/themes/cave-dark.json +++ b/assets/themes/cave-dark.json @@ -1234,12 +1234,18 @@ "width": 1 }, "padding": { - "bottom": 7, + "bottom": 4, "left": 8, "right": 8, - "top": 7 + "top": 4 } }, + "add_contact_icon": { + "margin": { + "left": 6 + }, + "color": "#e2dfe7" + }, "row_height": 28, "tree_branch_color": "#655f6d", "tree_branch_width": 1, diff --git a/assets/themes/cave-light.json b/assets/themes/cave-light.json index f44069e49f19e6ca55cda82d3224da4caf940ce3..4fd9e0865d034ee68d248ace4f88faa1fac9260e 100644 --- a/assets/themes/cave-light.json +++ b/assets/themes/cave-light.json @@ -1234,12 +1234,18 @@ "width": 1 }, "padding": { - "bottom": 7, + "bottom": 4, "left": 8, "right": 8, - "top": 7 + "top": 4 } }, + "add_contact_icon": { + "margin": { + "left": 6 + }, + "color": "#26232a" + }, "row_height": 28, "tree_branch_color": "#7e7887", "tree_branch_width": 1, diff --git a/assets/themes/dark.json b/assets/themes/dark.json index f50e590fc42e3c92604c07d8e045bea48e52ac22..50e2958b40c669b43f1d2c76e0f9d0695c8c20a6 100644 --- a/assets/themes/dark.json +++ b/assets/themes/dark.json @@ -1234,12 +1234,18 @@ "width": 1 }, "padding": { - "bottom": 7, + "bottom": 4, "left": 8, "right": 8, - "top": 7 + "top": 4 } }, + "add_contact_icon": { + "margin": { + "left": 6 + }, + "color": "#c6c6c6" + }, "row_height": 28, "tree_branch_color": "#404040", "tree_branch_width": 1, diff --git a/assets/themes/light.json b/assets/themes/light.json index 08d4326414489f132cd78036edb65756c3d810ee..a929dab7aae978240db2022c84b45c328d828e36 100644 --- a/assets/themes/light.json +++ b/assets/themes/light.json @@ -1234,12 +1234,18 @@ "width": 1 }, "padding": { - "bottom": 7, + "bottom": 4, "left": 8, "right": 8, - "top": 7 + "top": 4 } }, + "add_contact_icon": { + "margin": { + "left": 6 + }, + "color": "#393939" + }, "row_height": 28, "tree_branch_color": "#e3e3e3", "tree_branch_width": 1, diff --git a/assets/themes/solarized-dark.json b/assets/themes/solarized-dark.json index 2b75c4364b124d315fbaed2ad3294e90ee7e6138..3cfa8e701252d58970f362223007be96eb21d55d 100644 --- a/assets/themes/solarized-dark.json +++ b/assets/themes/solarized-dark.json @@ -1234,12 +1234,18 @@ "width": 1 }, "padding": { - "bottom": 7, + "bottom": 4, "left": 8, "right": 8, - "top": 7 + "top": 4 } }, + "add_contact_icon": { + "margin": { + "left": 6 + }, + "color": "#eee8d5" + }, "row_height": 28, "tree_branch_color": "#657b83", "tree_branch_width": 1, diff --git a/assets/themes/solarized-light.json b/assets/themes/solarized-light.json index 47758db9986751375885d78ed85454129b01d00b..4aaa7567e89a28f361964d88af58f00cb8101b91 100644 --- a/assets/themes/solarized-light.json +++ b/assets/themes/solarized-light.json @@ -1234,12 +1234,18 @@ "width": 1 }, "padding": { - "bottom": 7, + "bottom": 4, "left": 8, "right": 8, - "top": 7 + "top": 4 } }, + "add_contact_icon": { + "margin": { + "left": 6 + }, + "color": "#073642" + }, "row_height": 28, "tree_branch_color": "#839496", "tree_branch_width": 1, diff --git a/assets/themes/sulphurpool-dark.json b/assets/themes/sulphurpool-dark.json index 38761d606eb156fe3ea3043ee26f9c10108df09b..8151dea442d41ad5876eabd3be8cd1893323d595 100644 --- a/assets/themes/sulphurpool-dark.json +++ b/assets/themes/sulphurpool-dark.json @@ -1234,12 +1234,18 @@ "width": 1 }, "padding": { - "bottom": 7, + "bottom": 4, "left": 8, "right": 8, - "top": 7 + "top": 4 } }, + "add_contact_icon": { + "margin": { + "left": 6 + }, + "color": "#dfe2f1" + }, "row_height": 28, "tree_branch_color": "#6b7394", "tree_branch_width": 1, diff --git a/assets/themes/sulphurpool-light.json b/assets/themes/sulphurpool-light.json index 6d3aeadd5fea179c37130af69adff503b1a06cb3..b42f8b4b2e456183c125e01e7587e9bbbae809cb 100644 --- a/assets/themes/sulphurpool-light.json +++ b/assets/themes/sulphurpool-light.json @@ -1234,12 +1234,18 @@ "width": 1 }, "padding": { - "bottom": 7, + "bottom": 4, "left": 8, "right": 8, - "top": 7 + "top": 4 } }, + "add_contact_icon": { + "margin": { + "left": 6 + }, + "color": "#293256" + }, "row_height": 28, "tree_branch_color": "#898ea4", "tree_branch_width": 1, diff --git a/crates/contacts_panel/src/contacts_panel.rs b/crates/contacts_panel/src/contacts_panel.rs index 48386749ef5053e3fffdd148e13db4a1c386e56c..16daa1a7deb18a064d268e306f8b2ae17ae7f580 100644 --- a/crates/contacts_panel/src/contacts_panel.rs +++ b/crates/contacts_panel/src/contacts_panel.rs @@ -711,8 +711,26 @@ impl View for ContactsPanel { Container::new( Flex::column() .with_child( - Container::new(ChildView::new(self.user_query_editor.clone()).boxed()) - .with_style(theme.user_query_editor.container) + Flex::row() + .with_child( + ChildView::new(self.user_query_editor.clone()) + .contained() + .with_style(theme.user_query_editor.container) + .flex(1., true) + .boxed(), + ) + .with_child( + Svg::new("icons/add-contact.svg") + .with_color(theme.add_contact_icon.color) + .constrained() + .with_height(12.) + .contained() + .with_style(theme.add_contact_icon.container) + .aligned() + .boxed(), + ) + .constrained() + .with_height(32.) .boxed(), ) .with_child(List::new(self.list_state.clone()).flex(1., false).boxed()) diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 1f320bd24ce173151b76d76d97ab326e18f536e9..f0fdcfc29fe4c63c16d1a781052df7a7842aabae 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -236,6 +236,7 @@ pub struct ContactsPanel { pub container: ContainerStyle, pub header: ContainedText, pub user_query_editor: FieldEditor, + pub add_contact_icon: AddContactIcon, pub row_height: f32, pub contact_avatar: ImageStyle, pub contact_username: ContainedText, @@ -248,6 +249,13 @@ pub struct ContactsPanel { pub hovered_unshared_project: ProjectRow, } +#[derive(Deserialize, Default)] +pub struct AddContactIcon { + #[serde(flatten)] + pub container: ContainerStyle, + pub color: Color, +} + #[derive(Deserialize, Default)] pub struct ProjectRow { #[serde(flatten)] diff --git a/styles/src/styleTree/contactsPanel.ts b/styles/src/styleTree/contactsPanel.ts index 237913c37e91494c72002d3a73785871f23ace5f..aeae2fca6270fc05bf7e7d436f64f4cfe2f2a4a5 100644 --- a/styles/src/styleTree/contactsPanel.ts +++ b/styles/src/styleTree/contactsPanel.ts @@ -1,6 +1,6 @@ import Theme from "../themes/theme"; import { panel } from "./app"; -import { backgroundColor, border, borderColor, player, text } from "./components"; +import { backgroundColor, border, borderColor, iconColor, player, text } from "./components"; export default function(theme: Theme) { const project = { @@ -41,12 +41,16 @@ export default function(theme: Theme) { selection: player(theme, 1).selection, border: border(theme, "secondary"), padding: { - bottom: 7, + bottom: 4, left: 8, right: 8, - top: 7, + top: 4, }, }, + addContactIcon: { + margin: { left: 6 }, + color: iconColor(theme, "primary") + }, rowHeight: 28, treeBranchColor: borderColor(theme, "muted"), treeBranchWidth: 1, From 12783a588c6e24e4708c3e28a47744206401907e Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 10 May 2022 12:21:41 +0200 Subject: [PATCH 37/53] Prevent users from fuzzy searching and adding themselves as contacts --- crates/collab/src/rpc.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 51aec065cd5cc5675d9a8f947837f265d91c62c7..7e881f5d56a974a2e942e12d0b11227feb04a16f 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -940,6 +940,10 @@ impl Server { request: TypedEnvelope, response: Response, ) -> Result<()> { + let user_id = self + .store() + .await + .user_id_for_connection(request.sender_id)?; let query = request.payload.query; let db = &self.app_state.db; let users = match query.len() { @@ -953,6 +957,7 @@ impl Server { }; let users = users .into_iter() + .filter(|user| user.id != user_id) .map(|user| proto::User { id: user.id.to_proto(), avatar_url: format!("https://github.com/{}.png?size=128", user.github_login), @@ -973,6 +978,10 @@ impl Server { .await .user_id_for_connection(request.sender_id)?; let responder_id = UserId::from_proto(request.payload.responder_id); + if requester_id == responder_id { + return Err(anyhow!("cannot add yourself as a contact"))?; + } + self.app_state .db .send_contact_request(requester_id, responder_id) From eef99f059d3ef53acbbeaeff4b0d93a62d1be4bb Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 10 May 2022 06:14:30 -0600 Subject: [PATCH 38/53] Make flex elements fill available space when they contain a float Co-Authored-By: Antonio Scandurra --- crates/contacts_panel/src/contacts_panel.rs | 1 - crates/gpui/src/elements/flex.rs | 16 ++++++++++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/crates/contacts_panel/src/contacts_panel.rs b/crates/contacts_panel/src/contacts_panel.rs index 16daa1a7deb18a064d268e306f8b2ae17ae7f580..b75444785b0f23b6c13683455ce921c481eb1170 100644 --- a/crates/contacts_panel/src/contacts_panel.rs +++ b/crates/contacts_panel/src/contacts_panel.rs @@ -378,7 +378,6 @@ impl ContactsPanel { .contained() .with_style(theme.edit_contact.container) .aligned() - .flex_float() .boxed() }) .on_click(move |_, cx| { diff --git a/crates/gpui/src/elements/flex.rs b/crates/gpui/src/elements/flex.rs index 3384b200199a2aa0d43fc6fe4f360f8951f18af5..3f42f984075a16589de9256aa5f414a97bf7bf05 100644 --- a/crates/gpui/src/elements/flex.rs +++ b/crates/gpui/src/elements/flex.rs @@ -117,14 +117,15 @@ impl Element for Flex { ) -> (Vector2F, Self::LayoutState) { let mut total_flex = None; let mut fixed_space = 0.0; + let mut contains_float = false; let cross_axis = self.axis.invert(); let mut cross_axis_max: f32 = 0.0; for child in &mut self.children { - if let Some(flex) = child - .metadata::() - .and_then(|metadata| metadata.flex.map(|(flex, _)| flex)) - { + let metadata = child.metadata::(); + contains_float |= metadata.map_or(false, |metadata| metadata.float); + + if let Some(flex) = metadata.and_then(|metadata| metadata.flex.map(|(flex, _)| flex)) { *total_flex.get_or_insert(0.) += flex; } else { let child_constraint = match self.axis { @@ -177,6 +178,13 @@ impl Element for Flex { } }; + if contains_float { + match self.axis { + Axis::Horizontal => size.set_x(size.x().max(constraint.max.x())), + Axis::Vertical => size.set_y(size.y().max(constraint.max.y())), + } + } + if constraint.min.x().is_finite() { size.set_x(size.x().max(constraint.min.x())); } From b721f0064ac227870958725cef32e97a781d496b Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 10 May 2022 07:24:14 -0600 Subject: [PATCH 39/53] Start on contact finder modal Co-Authored-By: Antonio Scandurra --- assets/themes/cave-dark.json | 145 +++++++++++++++- assets/themes/cave-light.json | 145 +++++++++++++++- assets/themes/dark.json | 145 +++++++++++++++- assets/themes/light.json | 145 +++++++++++++++- assets/themes/solarized-dark.json | 145 +++++++++++++++- assets/themes/solarized-light.json | 145 +++++++++++++++- assets/themes/sulphurpool-dark.json | 145 +++++++++++++++- assets/themes/sulphurpool-light.json | 145 +++++++++++++++- crates/contacts_panel/src/contact_finder.rs | 175 +++++++++++++++++++ crates/contacts_panel/src/contacts_panel.rs | 177 +++++--------------- crates/theme/src/theme.rs | 16 +- crates/workspace/src/workspace.rs | 6 +- styles/src/styleTree/app.ts | 2 + styles/src/styleTree/contactFinder.ts | 42 +++++ styles/src/styleTree/contactsPanel.ts | 4 +- 15 files changed, 1438 insertions(+), 144 deletions(-) create mode 100644 crates/contacts_panel/src/contact_finder.rs create mode 100644 styles/src/styleTree/contactFinder.ts diff --git a/assets/themes/cave-dark.json b/assets/themes/cave-dark.json index 1eac826b5436f5888e0f1a658d795bbc86f6e6c6..68ea7ee996617bf316a7f7feeddf7f32a993fb6f 100644 --- a/assets/themes/cave-dark.json +++ b/assets/themes/cave-dark.json @@ -1261,7 +1261,7 @@ "left": 8 } }, - "edit_contact": { + "contact_button": { "family": "Zed Mono", "color": "#e2dfe7", "size": 14, @@ -1378,6 +1378,149 @@ "corner_radius": 6 } }, + "contact_finder": { + "background": "#26232a", + "corner_radius": 8, + "padding": 8, + "item": { + "padding": { + "bottom": 4, + "left": 12, + "right": 12, + "top": 4 + }, + "corner_radius": 8, + "text": { + "family": "Zed Sans", + "color": "#8b8792", + "size": 14 + }, + "highlight_text": { + "family": "Zed Sans", + "color": "#576ddb", + "weight": "bold", + "size": 14 + }, + "active": { + "background": "#5852605c", + "text": { + "family": "Zed Sans", + "color": "#e2dfe7", + "size": 14 + } + }, + "hover": { + "background": "#5852603d" + } + }, + "border": { + "color": "#19171c", + "width": 1 + }, + "empty": { + "text": { + "family": "Zed Sans", + "color": "#7e7887", + "size": 14 + }, + "padding": { + "bottom": 4, + "left": 12, + "right": 12, + "top": 8 + } + }, + "input_editor": { + "background": "#19171c", + "corner_radius": 8, + "placeholder_text": { + "family": "Zed Sans", + "color": "#7e7887", + "size": 14 + }, + "selection": { + "cursor": "#576ddb", + "selection": "#576ddb3d" + }, + "text": { + "family": "Zed Mono", + "color": "#e2dfe7", + "size": 14 + }, + "border": { + "color": "#26232a", + "width": 1 + }, + "padding": { + "bottom": 7, + "left": 16, + "right": 16, + "top": 7 + } + }, + "shadow": { + "blur": 16, + "color": "#0000003d", + "offset": [ + 0, + 2 + ] + }, + "max_width": 540, + "max_height": 420, + "query_editor": { + "background": "#19171c", + "corner_radius": 6, + "text": { + "family": "Zed Mono", + "color": "#e2dfe7", + "size": 14 + }, + "placeholder_text": { + "family": "Zed Mono", + "color": "#7e7887", + "size": 14 + }, + "selection": { + "cursor": "#576ddb", + "selection": "#576ddb3d" + }, + "border": { + "color": "#26232a", + "width": 1 + }, + "padding": { + "bottom": 4, + "left": 8, + "right": 8, + "top": 4 + } + }, + "row_height": 28, + "contact_avatar": { + "corner_radius": 10, + "width": 18 + }, + "contact_username": { + "family": "Zed Mono", + "color": "#e2dfe7", + "size": 14, + "padding": { + "left": 8 + } + }, + "contact_button": { + "family": "Zed Mono", + "color": "#e2dfe7", + "size": 14, + "background": "#26232a", + "corner_radius": 12, + "padding": { + "left": 7, + "right": 7 + } + } + }, "search": { "match_background": "#955ae77a", "tab_icon_spacing": 8, diff --git a/assets/themes/cave-light.json b/assets/themes/cave-light.json index 4fd9e0865d034ee68d248ace4f88faa1fac9260e..a7954938dcc3e4806d4305440a783b033b806ade 100644 --- a/assets/themes/cave-light.json +++ b/assets/themes/cave-light.json @@ -1261,7 +1261,7 @@ "left": 8 } }, - "edit_contact": { + "contact_button": { "family": "Zed Mono", "color": "#26232a", "size": 14, @@ -1378,6 +1378,149 @@ "corner_radius": 6 } }, + "contact_finder": { + "background": "#e2dfe7", + "corner_radius": 8, + "padding": 8, + "item": { + "padding": { + "bottom": 4, + "left": 12, + "right": 12, + "top": 4 + }, + "corner_radius": 8, + "text": { + "family": "Zed Sans", + "color": "#585260", + "size": 14 + }, + "highlight_text": { + "family": "Zed Sans", + "color": "#576ddb", + "weight": "bold", + "size": 14 + }, + "active": { + "background": "#8b87922e", + "text": { + "family": "Zed Sans", + "color": "#26232a", + "size": 14 + } + }, + "hover": { + "background": "#8b87921f" + } + }, + "border": { + "color": "#efecf4", + "width": 1 + }, + "empty": { + "text": { + "family": "Zed Sans", + "color": "#655f6d", + "size": 14 + }, + "padding": { + "bottom": 4, + "left": 12, + "right": 12, + "top": 8 + } + }, + "input_editor": { + "background": "#efecf4", + "corner_radius": 8, + "placeholder_text": { + "family": "Zed Sans", + "color": "#655f6d", + "size": 14 + }, + "selection": { + "cursor": "#576ddb", + "selection": "#576ddb3d" + }, + "text": { + "family": "Zed Mono", + "color": "#26232a", + "size": 14 + }, + "border": { + "color": "#e2dfe7", + "width": 1 + }, + "padding": { + "bottom": 7, + "left": 16, + "right": 16, + "top": 7 + } + }, + "shadow": { + "blur": 16, + "color": "#0000001f", + "offset": [ + 0, + 2 + ] + }, + "max_width": 540, + "max_height": 420, + "query_editor": { + "background": "#efecf4", + "corner_radius": 6, + "text": { + "family": "Zed Mono", + "color": "#26232a", + "size": 14 + }, + "placeholder_text": { + "family": "Zed Mono", + "color": "#655f6d", + "size": 14 + }, + "selection": { + "cursor": "#576ddb", + "selection": "#576ddb3d" + }, + "border": { + "color": "#e2dfe7", + "width": 1 + }, + "padding": { + "bottom": 4, + "left": 8, + "right": 8, + "top": 4 + } + }, + "row_height": 28, + "contact_avatar": { + "corner_radius": 10, + "width": 18 + }, + "contact_username": { + "family": "Zed Mono", + "color": "#26232a", + "size": 14, + "padding": { + "left": 8 + } + }, + "contact_button": { + "family": "Zed Mono", + "color": "#26232a", + "size": 14, + "background": "#e2dfe7", + "corner_radius": 12, + "padding": { + "left": 7, + "right": 7 + } + } + }, "search": { "match_background": "#955ae73d", "tab_icon_spacing": 8, diff --git a/assets/themes/dark.json b/assets/themes/dark.json index 50e2958b40c669b43f1d2c76e0f9d0695c8c20a6..d1070d6cadbcd2cb740b265d5684226e33ba73eb 100644 --- a/assets/themes/dark.json +++ b/assets/themes/dark.json @@ -1261,7 +1261,7 @@ "left": 8 } }, - "edit_contact": { + "contact_button": { "family": "Zed Mono", "color": "#f1f1f1", "size": 14, @@ -1378,6 +1378,149 @@ "corner_radius": 6 } }, + "contact_finder": { + "background": "#1c1c1c", + "corner_radius": 8, + "padding": 8, + "item": { + "padding": { + "bottom": 4, + "left": 12, + "right": 12, + "top": 4 + }, + "corner_radius": 8, + "text": { + "family": "Zed Sans", + "color": "#9c9c9c", + "size": 14 + }, + "highlight_text": { + "family": "Zed Sans", + "color": "#4f8ff7", + "weight": "bold", + "size": 14 + }, + "active": { + "background": "#2b2b2b", + "text": { + "family": "Zed Sans", + "color": "#f1f1f1", + "size": 14 + } + }, + "hover": { + "background": "#232323" + } + }, + "border": { + "color": "#070707", + "width": 1 + }, + "empty": { + "text": { + "family": "Zed Sans", + "color": "#474747", + "size": 14 + }, + "padding": { + "bottom": 4, + "left": 12, + "right": 12, + "top": 8 + } + }, + "input_editor": { + "background": "#000000", + "corner_radius": 8, + "placeholder_text": { + "family": "Zed Sans", + "color": "#474747", + "size": 14 + }, + "selection": { + "cursor": "#2472f2", + "selection": "#2472f23d" + }, + "text": { + "family": "Zed Mono", + "color": "#f1f1f1", + "size": 14 + }, + "border": { + "color": "#232323", + "width": 1 + }, + "padding": { + "bottom": 7, + "left": 16, + "right": 16, + "top": 7 + } + }, + "shadow": { + "blur": 16, + "color": "#00000052", + "offset": [ + 0, + 2 + ] + }, + "max_width": 540, + "max_height": 420, + "query_editor": { + "background": "#000000", + "corner_radius": 6, + "text": { + "family": "Zed Mono", + "color": "#f1f1f1", + "size": 14 + }, + "placeholder_text": { + "family": "Zed Mono", + "color": "#474747", + "size": 14 + }, + "selection": { + "cursor": "#2472f2", + "selection": "#2472f23d" + }, + "border": { + "color": "#232323", + "width": 1 + }, + "padding": { + "bottom": 4, + "left": 8, + "right": 8, + "top": 4 + } + }, + "row_height": 28, + "contact_avatar": { + "corner_radius": 10, + "width": 18 + }, + "contact_username": { + "family": "Zed Mono", + "color": "#f1f1f1", + "size": 14, + "padding": { + "left": 8 + } + }, + "contact_button": { + "family": "Zed Mono", + "color": "#f1f1f1", + "size": 14, + "background": "#2b2b2b", + "corner_radius": 12, + "padding": { + "left": 7, + "right": 7 + } + } + }, "search": { "match_background": "#3f15a380", "tab_icon_spacing": 8, diff --git a/assets/themes/light.json b/assets/themes/light.json index a929dab7aae978240db2022c84b45c328d828e36..d79bc79f386389746b8bb3674ec12dd969c5b4b3 100644 --- a/assets/themes/light.json +++ b/assets/themes/light.json @@ -1261,7 +1261,7 @@ "left": 8 } }, - "edit_contact": { + "contact_button": { "family": "Zed Mono", "color": "#2b2b2b", "size": 14, @@ -1378,6 +1378,149 @@ "corner_radius": 6 } }, + "contact_finder": { + "background": "#f8f8f8", + "corner_radius": 8, + "padding": 8, + "item": { + "padding": { + "bottom": 4, + "left": 12, + "right": 12, + "top": 4 + }, + "corner_radius": 8, + "text": { + "family": "Zed Sans", + "color": "#474747", + "size": 14 + }, + "highlight_text": { + "family": "Zed Sans", + "color": "#484bed", + "weight": "bold", + "size": 14 + }, + "active": { + "background": "#e3e3e3", + "text": { + "family": "Zed Sans", + "color": "#2b2b2b", + "size": 14 + } + }, + "hover": { + "background": "#eaeaea" + } + }, + "border": { + "color": "#d5d5d5", + "width": 1 + }, + "empty": { + "text": { + "family": "Zed Sans", + "color": "#808080", + "size": 14 + }, + "padding": { + "bottom": 4, + "left": 12, + "right": 12, + "top": 8 + } + }, + "input_editor": { + "background": "#ffffff", + "corner_radius": 8, + "placeholder_text": { + "family": "Zed Sans", + "color": "#808080", + "size": 14 + }, + "selection": { + "cursor": "#2472f2", + "selection": "#2472f23d" + }, + "text": { + "family": "Zed Mono", + "color": "#2b2b2b", + "size": 14 + }, + "border": { + "color": "#d5d5d5", + "width": 1 + }, + "padding": { + "bottom": 7, + "left": 16, + "right": 16, + "top": 7 + } + }, + "shadow": { + "blur": 16, + "color": "#0000001f", + "offset": [ + 0, + 2 + ] + }, + "max_width": 540, + "max_height": 420, + "query_editor": { + "background": "#ffffff", + "corner_radius": 6, + "text": { + "family": "Zed Mono", + "color": "#2b2b2b", + "size": 14 + }, + "placeholder_text": { + "family": "Zed Mono", + "color": "#808080", + "size": 14 + }, + "selection": { + "cursor": "#2472f2", + "selection": "#2472f23d" + }, + "border": { + "color": "#d5d5d5", + "width": 1 + }, + "padding": { + "bottom": 4, + "left": 8, + "right": 8, + "top": 4 + } + }, + "row_height": 28, + "contact_avatar": { + "corner_radius": 10, + "width": 18 + }, + "contact_username": { + "family": "Zed Mono", + "color": "#2b2b2b", + "size": 14, + "padding": { + "left": 8 + } + }, + "contact_button": { + "family": "Zed Mono", + "color": "#2b2b2b", + "size": 14, + "background": "#eaeaea", + "corner_radius": 12, + "padding": { + "left": 7, + "right": 7 + } + } + }, "search": { "match_background": "#fce9b7", "tab_icon_spacing": 8, diff --git a/assets/themes/solarized-dark.json b/assets/themes/solarized-dark.json index 3cfa8e701252d58970f362223007be96eb21d55d..01b217e8e5fc6c381f69f745d394e0f3a2ee8101 100644 --- a/assets/themes/solarized-dark.json +++ b/assets/themes/solarized-dark.json @@ -1261,7 +1261,7 @@ "left": 8 } }, - "edit_contact": { + "contact_button": { "family": "Zed Mono", "color": "#eee8d5", "size": 14, @@ -1378,6 +1378,149 @@ "corner_radius": 6 } }, + "contact_finder": { + "background": "#073642", + "corner_radius": 8, + "padding": 8, + "item": { + "padding": { + "bottom": 4, + "left": 12, + "right": 12, + "top": 4 + }, + "corner_radius": 8, + "text": { + "family": "Zed Sans", + "color": "#93a1a1", + "size": 14 + }, + "highlight_text": { + "family": "Zed Sans", + "color": "#268bd2", + "weight": "bold", + "size": 14 + }, + "active": { + "background": "#586e755c", + "text": { + "family": "Zed Sans", + "color": "#eee8d5", + "size": 14 + } + }, + "hover": { + "background": "#586e753d" + } + }, + "border": { + "color": "#002b36", + "width": 1 + }, + "empty": { + "text": { + "family": "Zed Sans", + "color": "#839496", + "size": 14 + }, + "padding": { + "bottom": 4, + "left": 12, + "right": 12, + "top": 8 + } + }, + "input_editor": { + "background": "#002b36", + "corner_radius": 8, + "placeholder_text": { + "family": "Zed Sans", + "color": "#839496", + "size": 14 + }, + "selection": { + "cursor": "#268bd2", + "selection": "#268bd23d" + }, + "text": { + "family": "Zed Mono", + "color": "#eee8d5", + "size": 14 + }, + "border": { + "color": "#073642", + "width": 1 + }, + "padding": { + "bottom": 7, + "left": 16, + "right": 16, + "top": 7 + } + }, + "shadow": { + "blur": 16, + "color": "#0000003d", + "offset": [ + 0, + 2 + ] + }, + "max_width": 540, + "max_height": 420, + "query_editor": { + "background": "#002b36", + "corner_radius": 6, + "text": { + "family": "Zed Mono", + "color": "#eee8d5", + "size": 14 + }, + "placeholder_text": { + "family": "Zed Mono", + "color": "#839496", + "size": 14 + }, + "selection": { + "cursor": "#268bd2", + "selection": "#268bd23d" + }, + "border": { + "color": "#073642", + "width": 1 + }, + "padding": { + "bottom": 4, + "left": 8, + "right": 8, + "top": 4 + } + }, + "row_height": 28, + "contact_avatar": { + "corner_radius": 10, + "width": 18 + }, + "contact_username": { + "family": "Zed Mono", + "color": "#eee8d5", + "size": 14, + "padding": { + "left": 8 + } + }, + "contact_button": { + "family": "Zed Mono", + "color": "#eee8d5", + "size": 14, + "background": "#073642", + "corner_radius": 12, + "padding": { + "left": 7, + "right": 7 + } + } + }, "search": { "match_background": "#6c71c47a", "tab_icon_spacing": 8, diff --git a/assets/themes/solarized-light.json b/assets/themes/solarized-light.json index 4aaa7567e89a28f361964d88af58f00cb8101b91..6d21fda5dbdd6778145cda0b92d3b5f3ec1aa7d1 100644 --- a/assets/themes/solarized-light.json +++ b/assets/themes/solarized-light.json @@ -1261,7 +1261,7 @@ "left": 8 } }, - "edit_contact": { + "contact_button": { "family": "Zed Mono", "color": "#073642", "size": 14, @@ -1378,6 +1378,149 @@ "corner_radius": 6 } }, + "contact_finder": { + "background": "#eee8d5", + "corner_radius": 8, + "padding": 8, + "item": { + "padding": { + "bottom": 4, + "left": 12, + "right": 12, + "top": 4 + }, + "corner_radius": 8, + "text": { + "family": "Zed Sans", + "color": "#586e75", + "size": 14 + }, + "highlight_text": { + "family": "Zed Sans", + "color": "#268bd2", + "weight": "bold", + "size": 14 + }, + "active": { + "background": "#93a1a12e", + "text": { + "family": "Zed Sans", + "color": "#073642", + "size": 14 + } + }, + "hover": { + "background": "#93a1a11f" + } + }, + "border": { + "color": "#fdf6e3", + "width": 1 + }, + "empty": { + "text": { + "family": "Zed Sans", + "color": "#657b83", + "size": 14 + }, + "padding": { + "bottom": 4, + "left": 12, + "right": 12, + "top": 8 + } + }, + "input_editor": { + "background": "#fdf6e3", + "corner_radius": 8, + "placeholder_text": { + "family": "Zed Sans", + "color": "#657b83", + "size": 14 + }, + "selection": { + "cursor": "#268bd2", + "selection": "#268bd23d" + }, + "text": { + "family": "Zed Mono", + "color": "#073642", + "size": 14 + }, + "border": { + "color": "#eee8d5", + "width": 1 + }, + "padding": { + "bottom": 7, + "left": 16, + "right": 16, + "top": 7 + } + }, + "shadow": { + "blur": 16, + "color": "#0000001f", + "offset": [ + 0, + 2 + ] + }, + "max_width": 540, + "max_height": 420, + "query_editor": { + "background": "#fdf6e3", + "corner_radius": 6, + "text": { + "family": "Zed Mono", + "color": "#073642", + "size": 14 + }, + "placeholder_text": { + "family": "Zed Mono", + "color": "#657b83", + "size": 14 + }, + "selection": { + "cursor": "#268bd2", + "selection": "#268bd23d" + }, + "border": { + "color": "#eee8d5", + "width": 1 + }, + "padding": { + "bottom": 4, + "left": 8, + "right": 8, + "top": 4 + } + }, + "row_height": 28, + "contact_avatar": { + "corner_radius": 10, + "width": 18 + }, + "contact_username": { + "family": "Zed Mono", + "color": "#073642", + "size": 14, + "padding": { + "left": 8 + } + }, + "contact_button": { + "family": "Zed Mono", + "color": "#073642", + "size": 14, + "background": "#eee8d5", + "corner_radius": 12, + "padding": { + "left": 7, + "right": 7 + } + } + }, "search": { "match_background": "#6c71c43d", "tab_icon_spacing": 8, diff --git a/assets/themes/sulphurpool-dark.json b/assets/themes/sulphurpool-dark.json index 8151dea442d41ad5876eabd3be8cd1893323d595..9711ff8f9f5c69ff272fc7351f3757bda7fbc597 100644 --- a/assets/themes/sulphurpool-dark.json +++ b/assets/themes/sulphurpool-dark.json @@ -1261,7 +1261,7 @@ "left": 8 } }, - "edit_contact": { + "contact_button": { "family": "Zed Mono", "color": "#dfe2f1", "size": 14, @@ -1378,6 +1378,149 @@ "corner_radius": 6 } }, + "contact_finder": { + "background": "#293256", + "corner_radius": 8, + "padding": 8, + "item": { + "padding": { + "bottom": 4, + "left": 12, + "right": 12, + "top": 4 + }, + "corner_radius": 8, + "text": { + "family": "Zed Sans", + "color": "#979db4", + "size": 14 + }, + "highlight_text": { + "family": "Zed Sans", + "color": "#3d8fd1", + "weight": "bold", + "size": 14 + }, + "active": { + "background": "#5e66875c", + "text": { + "family": "Zed Sans", + "color": "#dfe2f1", + "size": 14 + } + }, + "hover": { + "background": "#5e66873d" + } + }, + "border": { + "color": "#202746", + "width": 1 + }, + "empty": { + "text": { + "family": "Zed Sans", + "color": "#898ea4", + "size": 14 + }, + "padding": { + "bottom": 4, + "left": 12, + "right": 12, + "top": 8 + } + }, + "input_editor": { + "background": "#202746", + "corner_radius": 8, + "placeholder_text": { + "family": "Zed Sans", + "color": "#898ea4", + "size": 14 + }, + "selection": { + "cursor": "#3d8fd1", + "selection": "#3d8fd13d" + }, + "text": { + "family": "Zed Mono", + "color": "#dfe2f1", + "size": 14 + }, + "border": { + "color": "#293256", + "width": 1 + }, + "padding": { + "bottom": 7, + "left": 16, + "right": 16, + "top": 7 + } + }, + "shadow": { + "blur": 16, + "color": "#0000003d", + "offset": [ + 0, + 2 + ] + }, + "max_width": 540, + "max_height": 420, + "query_editor": { + "background": "#202746", + "corner_radius": 6, + "text": { + "family": "Zed Mono", + "color": "#dfe2f1", + "size": 14 + }, + "placeholder_text": { + "family": "Zed Mono", + "color": "#898ea4", + "size": 14 + }, + "selection": { + "cursor": "#3d8fd1", + "selection": "#3d8fd13d" + }, + "border": { + "color": "#293256", + "width": 1 + }, + "padding": { + "bottom": 4, + "left": 8, + "right": 8, + "top": 4 + } + }, + "row_height": 28, + "contact_avatar": { + "corner_radius": 10, + "width": 18 + }, + "contact_username": { + "family": "Zed Mono", + "color": "#dfe2f1", + "size": 14, + "padding": { + "left": 8 + } + }, + "contact_button": { + "family": "Zed Mono", + "color": "#dfe2f1", + "size": 14, + "background": "#293256", + "corner_radius": 12, + "padding": { + "left": 7, + "right": 7 + } + } + }, "search": { "match_background": "#6679cc7a", "tab_icon_spacing": 8, diff --git a/assets/themes/sulphurpool-light.json b/assets/themes/sulphurpool-light.json index b42f8b4b2e456183c125e01e7587e9bbbae809cb..17ec4fe62a5fac523a7169435d46c3696be3e745 100644 --- a/assets/themes/sulphurpool-light.json +++ b/assets/themes/sulphurpool-light.json @@ -1261,7 +1261,7 @@ "left": 8 } }, - "edit_contact": { + "contact_button": { "family": "Zed Mono", "color": "#293256", "size": 14, @@ -1378,6 +1378,149 @@ "corner_radius": 6 } }, + "contact_finder": { + "background": "#dfe2f1", + "corner_radius": 8, + "padding": 8, + "item": { + "padding": { + "bottom": 4, + "left": 12, + "right": 12, + "top": 4 + }, + "corner_radius": 8, + "text": { + "family": "Zed Sans", + "color": "#5e6687", + "size": 14 + }, + "highlight_text": { + "family": "Zed Sans", + "color": "#3d8fd1", + "weight": "bold", + "size": 14 + }, + "active": { + "background": "#979db42e", + "text": { + "family": "Zed Sans", + "color": "#293256", + "size": 14 + } + }, + "hover": { + "background": "#979db41f" + } + }, + "border": { + "color": "#f5f7ff", + "width": 1 + }, + "empty": { + "text": { + "family": "Zed Sans", + "color": "#6b7394", + "size": 14 + }, + "padding": { + "bottom": 4, + "left": 12, + "right": 12, + "top": 8 + } + }, + "input_editor": { + "background": "#f5f7ff", + "corner_radius": 8, + "placeholder_text": { + "family": "Zed Sans", + "color": "#6b7394", + "size": 14 + }, + "selection": { + "cursor": "#3d8fd1", + "selection": "#3d8fd13d" + }, + "text": { + "family": "Zed Mono", + "color": "#293256", + "size": 14 + }, + "border": { + "color": "#dfe2f1", + "width": 1 + }, + "padding": { + "bottom": 7, + "left": 16, + "right": 16, + "top": 7 + } + }, + "shadow": { + "blur": 16, + "color": "#0000001f", + "offset": [ + 0, + 2 + ] + }, + "max_width": 540, + "max_height": 420, + "query_editor": { + "background": "#f5f7ff", + "corner_radius": 6, + "text": { + "family": "Zed Mono", + "color": "#293256", + "size": 14 + }, + "placeholder_text": { + "family": "Zed Mono", + "color": "#6b7394", + "size": 14 + }, + "selection": { + "cursor": "#3d8fd1", + "selection": "#3d8fd13d" + }, + "border": { + "color": "#dfe2f1", + "width": 1 + }, + "padding": { + "bottom": 4, + "left": 8, + "right": 8, + "top": 4 + } + }, + "row_height": 28, + "contact_avatar": { + "corner_radius": 10, + "width": 18 + }, + "contact_username": { + "family": "Zed Mono", + "color": "#293256", + "size": 14, + "padding": { + "left": 8 + } + }, + "contact_button": { + "family": "Zed Mono", + "color": "#293256", + "size": 14, + "background": "#dfe2f1", + "corner_radius": 12, + "padding": { + "left": 7, + "right": 7 + } + } + }, "search": { "match_background": "#6679cc3d", "tab_icon_spacing": 8, diff --git a/crates/contacts_panel/src/contact_finder.rs b/crates/contacts_panel/src/contact_finder.rs new file mode 100644 index 0000000000000000000000000000000000000000..ed5281380c9f9ab77b5d2e8c3d57a28bc9646a79 --- /dev/null +++ b/crates/contacts_panel/src/contact_finder.rs @@ -0,0 +1,175 @@ +use client::{ContactRequestStatus, User, UserStore}; +use editor::Editor; +use gpui::{ + color::Color, elements::*, platform::CursorStyle, Entity, LayoutContext, ModelHandle, + RenderContext, Task, View, ViewContext, ViewHandle, +}; +use settings::Settings; +use std::sync::Arc; +use util::TryFutureExt; + +use crate::{RemoveContact, RequestContact}; + +pub struct ContactFinder { + query_editor: ViewHandle, + list_state: UniformListState, + potential_contacts: Arc<[Arc]>, + user_store: ModelHandle, + contacts_search_task: Option>>, +} + +impl Entity for ContactFinder { + type Event = (); +} + +impl View for ContactFinder { + fn ui_name() -> &'static str { + "ContactFinder" + } + + fn render(&mut self, cx: &mut RenderContext) -> ElementBox { + let theme = cx.global::().theme.clone(); + let user_store = self.user_store.clone(); + let potential_contacts = self.potential_contacts.clone(); + Flex::column() + .with_child( + ChildView::new(self.query_editor.clone()) + .contained() + .with_style(theme.contact_finder.query_editor.container) + .boxed(), + ) + .with_child( + UniformList::new(self.list_state.clone(), self.potential_contacts.len(), { + let theme = theme.clone(); + move |range, items, cx| { + items.extend(range.map(|ix| { + Self::render_potential_contact( + &potential_contacts[ix], + &user_store, + &theme.contact_finder, + cx, + ) + })) + } + }) + .flex(1., false) + .boxed(), + ) + .contained() + .with_style(theme.contact_finder.container) + .constrained() + .with_max_width(theme.contact_finder.max_width) + .with_max_height(theme.contact_finder.max_height) + .boxed() + } +} + +impl ContactFinder { + pub fn new(user_store: ModelHandle, cx: &mut ViewContext) -> Self { + let query_editor = cx.add_view(|cx| { + Editor::single_line(Some(|theme| theme.contact_finder.query_editor.clone()), cx) + }); + + cx.subscribe(&query_editor, |this, _, event, cx| { + if let editor::Event::BufferEdited = event { + this.query_changed(cx) + } + }) + .detach(); + Self { + query_editor, + list_state: Default::default(), + potential_contacts: Arc::from([]), + user_store, + contacts_search_task: None, + } + } + + fn render_potential_contact( + contact: &User, + user_store: &ModelHandle, + theme: &theme::ContactFinder, + cx: &mut LayoutContext, + ) -> ElementBox { + enum RequestContactButton {} + + let contact_id = contact.id; + let request_status = user_store.read(cx).contact_request_status(&contact); + + Flex::row() + .with_children(contact.avatar.clone().map(|avatar| { + Image::new(avatar) + .with_style(theme.contact_avatar) + .aligned() + .left() + .boxed() + })) + .with_child( + Label::new( + contact.github_login.clone(), + theme.contact_username.text.clone(), + ) + .contained() + .with_style(theme.contact_username.container) + .aligned() + .left() + .boxed(), + ) + .with_child( + MouseEventHandler::new::( + contact.id as usize, + cx, + |_, _| { + let label = match request_status { + ContactRequestStatus::None | ContactRequestStatus::RequestReceived => { + "+" + } + ContactRequestStatus::RequestSent => "-", + ContactRequestStatus::Pending + | ContactRequestStatus::RequestAccepted => "…", + }; + + Label::new(label.to_string(), theme.contact_button.text.clone()) + .contained() + .with_style(theme.contact_button.container) + .aligned() + .flex_float() + .boxed() + }, + ) + .on_click(move |_, cx| match request_status { + ContactRequestStatus::None => { + cx.dispatch_action(RequestContact(contact_id)); + } + ContactRequestStatus::RequestSent => { + cx.dispatch_action(RemoveContact(contact_id)); + } + _ => {} + }) + .with_cursor_style(CursorStyle::PointingHand) + .boxed(), + ) + .constrained() + .with_height(theme.row_height) + .boxed() + } + + fn query_changed(&mut self, cx: &mut ViewContext) { + let query = self.query_editor.read(cx).text(cx); + let search_users = self + .user_store + .update(cx, |store, cx| store.fuzzy_search_users(query, cx)); + + self.contacts_search_task = Some(cx.spawn(|this, mut cx| { + async move { + let potential_contacts = search_users.await?; + this.update(&mut cx, |this, cx| { + this.potential_contacts = potential_contacts.into(); + cx.notify(); + }); + Ok(()) + } + .log_err() + })); + } +} diff --git a/crates/contacts_panel/src/contacts_panel.rs b/crates/contacts_panel/src/contacts_panel.rs index b75444785b0f23b6c13683455ce921c481eb1170..6ff2ea7aff8827e2e1dd0404131edcc5ee460989 100644 --- a/crates/contacts_panel/src/contacts_panel.rs +++ b/crates/contacts_panel/src/contacts_panel.rs @@ -1,20 +1,24 @@ +mod contact_finder; + use client::{Contact, ContactRequestStatus, User, UserStore}; +use contact_finder::ContactFinder; use editor::Editor; use fuzzy::{match_strings, StringMatchCandidate}; use gpui::{ + actions, elements::*, geometry::{rect::RectF, vector::vec2f}, impl_actions, platform::CursorStyle, Element, ElementBox, Entity, LayoutContext, ModelHandle, MutableAppContext, RenderContext, - Subscription, Task, View, ViewContext, ViewHandle, + Subscription, View, ViewContext, ViewHandle, }; use serde::Deserialize; use settings::Settings; use std::sync::Arc; -use util::TryFutureExt; -use workspace::{AppState, JoinProject}; +use workspace::{AppState, JoinProject, Workspace}; +actions!(contacts_panel, [FindNewContacts]); impl_actions!( contacts_panel, [RequestContact, RemoveContact, RespondToContactRequest] @@ -26,16 +30,13 @@ enum ContactEntry { IncomingRequest(Arc), OutgoingRequest(Arc), Contact(Arc), - PotentialContact(Arc), } pub struct ContactsPanel { entries: Vec, match_candidates: Vec, - potential_contacts: Vec>, list_state: ListState, user_store: ModelHandle, - contacts_search_task: Option>>, user_query_editor: ViewHandle, _maintain_contacts: Subscription, } @@ -56,6 +57,7 @@ pub fn init(cx: &mut MutableAppContext) { cx.add_action(ContactsPanel::request_contact); cx.add_action(ContactsPanel::remove_contact); cx.add_action(ContactsPanel::respond_to_contact_request); + cx.add_action(ContactsPanel::find_new_contacts); } impl ContactsPanel { @@ -69,7 +71,7 @@ impl ContactsPanel { cx.subscribe(&user_query_editor, |this, _, event, cx| { if let editor::Event::BufferEdited = event { - this.query_changed(cx) + this.update_entries(cx) } }) .detach(); @@ -120,22 +122,14 @@ impl ContactsPanel { theme, cx, ), - ContactEntry::PotentialContact(user) => Self::render_potential_contact( - user.clone(), - this.user_store.clone(), - theme, - cx, - ), } } }), entries: Default::default(), - potential_contacts: Default::default(), match_candidates: Default::default(), user_query_editor, _maintain_contacts: cx .observe(&app_state.user_store, |this, _, cx| this.update_entries(cx)), - contacts_search_task: None, user_store: app_state.user_store.clone(), }; this.update_entries(cx); @@ -347,9 +341,9 @@ impl ContactsPanel { if request_status == ContactRequestStatus::Pending { row.add_child( - Label::new("…".to_string(), theme.edit_contact.text.clone()) + Label::new("…".to_string(), theme.contact_button.text.clone()) .contained() - .with_style(theme.edit_contact.container) + .with_style(theme.contact_button.container) .aligned() .flex_float() .boxed(), @@ -357,9 +351,9 @@ impl ContactsPanel { } else { row.add_children([ MouseEventHandler::new::(user.id as usize, cx, |_, _| { - Label::new("Reject".to_string(), theme.edit_contact.text.clone()) + Label::new("Reject".to_string(), theme.contact_button.text.clone()) .contained() - .with_style(theme.edit_contact.container) + .with_style(theme.contact_button.container) .aligned() .flex_float() .boxed() @@ -374,9 +368,9 @@ impl ContactsPanel { .flex_float() .boxed(), MouseEventHandler::new::(user.id as usize, cx, |_, _| { - Label::new("Accept".to_string(), theme.edit_contact.text.clone()) + Label::new("Accept".to_string(), theme.contact_button.text.clone()) .contained() - .with_style(theme.edit_contact.container) + .with_style(theme.contact_button.container) .aligned() .boxed() }) @@ -427,9 +421,9 @@ impl ContactsPanel { if request_status == ContactRequestStatus::Pending { row.add_child( - Label::new("…".to_string(), theme.edit_contact.text.clone()) + Label::new("…".to_string(), theme.contact_button.text.clone()) .contained() - .with_style(theme.edit_contact.container) + .with_style(theme.contact_button.container) .aligned() .flex_float() .boxed(), @@ -437,9 +431,9 @@ impl ContactsPanel { } else { row.add_child( MouseEventHandler::new::(user.id as usize, cx, |_, _| { - Label::new("Cancel".to_string(), theme.edit_contact.text.clone()) + Label::new("Cancel".to_string(), theme.contact_button.text.clone()) .contained() - .with_style(theme.edit_contact.container) + .with_style(theme.contact_button.container) .aligned() .flex_float() .boxed() @@ -454,95 +448,6 @@ impl ContactsPanel { row.constrained().with_height(theme.row_height).boxed() } - fn render_potential_contact( - contact: Arc, - user_store: ModelHandle, - theme: &theme::ContactsPanel, - cx: &mut LayoutContext, - ) -> ElementBox { - enum RequestContactButton {} - - let request_status = user_store.read(cx).contact_request_status(&contact); - - Flex::row() - .with_children(contact.avatar.clone().map(|avatar| { - Image::new(avatar) - .with_style(theme.contact_avatar) - .aligned() - .left() - .boxed() - })) - .with_child( - Label::new( - contact.github_login.clone(), - theme.contact_username.text.clone(), - ) - .contained() - .with_style(theme.contact_username.container) - .aligned() - .left() - .boxed(), - ) - .with_child( - MouseEventHandler::new::( - contact.id as usize, - cx, - |_, _| { - let label = match request_status { - ContactRequestStatus::None | ContactRequestStatus::RequestReceived => { - "+" - } - ContactRequestStatus::RequestSent => "-", - ContactRequestStatus::Pending - | ContactRequestStatus::RequestAccepted => "…", - }; - - Label::new(label.to_string(), theme.edit_contact.text.clone()) - .contained() - .with_style(theme.edit_contact.container) - .aligned() - .flex_float() - .boxed() - }, - ) - .on_click(move |_, cx| match request_status { - ContactRequestStatus::None => { - cx.dispatch_action(RequestContact(contact.id)); - } - ContactRequestStatus::RequestSent => { - cx.dispatch_action(RemoveContact(contact.id)); - } - _ => {} - }) - .with_cursor_style(CursorStyle::PointingHand) - .boxed(), - ) - .constrained() - .with_height(theme.row_height) - .boxed() - } - - fn query_changed(&mut self, cx: &mut ViewContext) { - self.update_entries(cx); - - let query = self.user_query_editor.read(cx).text(cx); - let search_users = self - .user_store - .update(cx, |store, cx| store.fuzzy_search_users(query, cx)); - - self.contacts_search_task = Some(cx.spawn(|this, mut cx| { - async move { - let potential_contacts = search_users.await?; - this.update(&mut cx, |this, cx| { - this.potential_contacts = potential_contacts; - this.update_entries(cx); - }); - Ok(()) - } - .log_err() - })); - } - fn update_entries(&mut self, cx: &mut ViewContext) { let user_store = self.user_store.read(cx); let query = self.user_query_editor.read(cx).text(cx); @@ -656,15 +561,6 @@ impl ContactsPanel { } } - if !self.potential_contacts.is_empty() { - self.entries.push(ContactEntry::Header("Add Contacts")); - self.entries.extend( - self.potential_contacts - .iter() - .map(|user| ContactEntry::PotentialContact(user.clone())), - ); - } - self.list_state.reset(self.entries.len()); cx.notify(); } @@ -692,6 +588,16 @@ impl ContactsPanel { }) .detach(); } + + fn find_new_contacts( + workspace: &mut Workspace, + _: &FindNewContacts, + cx: &mut ViewContext, + ) { + workspace.toggle_modal(cx, |cx, workspace| { + cx.add_view(|cx| ContactFinder::new(workspace.user_store().clone(), cx)) + }); + } } pub enum Event {} @@ -706,7 +612,10 @@ impl View for ContactsPanel { } fn render(&mut self, cx: &mut RenderContext) -> ElementBox { - let theme = &cx.global::().theme.contacts_panel; + enum AddContact {} + + let theme = cx.global::().theme.clone(); + let theme = &theme.contacts_panel; Container::new( Flex::column() .with_child( @@ -719,14 +628,18 @@ impl View for ContactsPanel { .boxed(), ) .with_child( - Svg::new("icons/add-contact.svg") - .with_color(theme.add_contact_icon.color) - .constrained() - .with_height(12.) - .contained() - .with_style(theme.add_contact_icon.container) - .aligned() - .boxed(), + MouseEventHandler::new::(0, cx, |_, _| { + Svg::new("icons/add-contact.svg") + .with_color(theme.add_contact_icon.color) + .constrained() + .with_height(12.) + .contained() + .with_style(theme.add_contact_icon.container) + .aligned() + .boxed() + }) + .on_click(|_, cx| cx.dispatch_action(FindNewContacts)) + .boxed(), ) .constrained() .with_height(32.) diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index f0fdcfc29fe4c63c16d1a781052df7a7842aabae..5a47bc504df76476ab06fc1a7554080db83ca052 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -21,6 +21,7 @@ pub struct Theme { pub workspace: Workspace, pub chat_panel: ChatPanel, pub contacts_panel: ContactsPanel, + pub contact_finder: ContactFinder, pub project_panel: ProjectPanel, pub command_palette: CommandPalette, pub picker: Picker, @@ -240,7 +241,7 @@ pub struct ContactsPanel { pub row_height: f32, pub contact_avatar: ImageStyle, pub contact_username: ContainedText, - pub edit_contact: ContainedText, + pub contact_button: ContainedText, pub tree_branch_width: f32, pub tree_branch_color: Color, pub shared_project: ProjectRow, @@ -249,6 +250,19 @@ pub struct ContactsPanel { pub hovered_unshared_project: ProjectRow, } +#[derive(Deserialize, Default)] +pub struct ContactFinder { + #[serde(flatten)] + pub container: ContainerStyle, + pub max_width: f32, + pub max_height: f32, + pub query_editor: FieldEditor, + pub row_height: f32, + pub contact_avatar: ImageStyle, + pub contact_username: ContainedText, + pub contact_button: ContainedText, +} + #[derive(Deserialize, Default)] pub struct AddContactIcon { #[serde(flatten)] diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index b23710834dfd38f00edcaa3ae7cf454613b79d6b..d5b0bf2ed54f8c999fa669e72a44dc8d1c7595eb 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -824,6 +824,10 @@ impl Workspace { &self.status_bar } + pub fn user_store(&self) -> &ModelHandle { + &self.user_store + } + pub fn project(&self) -> &ModelHandle { &self.project } @@ -931,7 +935,7 @@ impl Workspace { }) } - // Returns the model that was toggled closed if it was open + /// Returns the modal that was toggled closed if it was open. pub fn toggle_modal( &mut self, cx: &mut ViewContext, diff --git a/styles/src/styleTree/app.ts b/styles/src/styleTree/app.ts index 1f1dc74c308baa21f566044c9a30c0a38afed68a..0da6ada222d77ba1ec2cdbe7b1446ef76535cf05 100644 --- a/styles/src/styleTree/app.ts +++ b/styles/src/styleTree/app.ts @@ -1,6 +1,7 @@ import Theme from "../themes/theme"; import chatPanel from "./chatPanel"; import { text } from "./components"; +import contactFinder from "./contactFinder"; import contactsPanel from "./contactsPanel"; import commandPalette from "./commandPalette"; import editor from "./editor"; @@ -24,6 +25,7 @@ export default function app(theme: Theme): Object { projectPanel: projectPanel(theme), chatPanel: chatPanel(theme), contactsPanel: contactsPanel(theme), + contactFinder: contactFinder(theme), search: search(theme), breadcrumbs: { ...text(theme, "sans", "secondary"), diff --git a/styles/src/styleTree/contactFinder.ts b/styles/src/styleTree/contactFinder.ts new file mode 100644 index 0000000000000000000000000000000000000000..1f0fb65776cc634092ffbf39e4207241d0689a67 --- /dev/null +++ b/styles/src/styleTree/contactFinder.ts @@ -0,0 +1,42 @@ +import Theme from "../themes/theme"; +import picker from "./picker"; +import { backgroundColor, border, player, text } from "./components"; + +export default function contactFinder(theme: Theme) { + return { + ...picker(theme), + maxWidth: 540., + maxHeight: 420., + queryEditor: { + background: backgroundColor(theme, 500), + cornerRadius: 6, + text: text(theme, "mono", "primary"), + placeholderText: text(theme, "mono", "placeholder", { size: "sm" }), + selection: player(theme, 1).selection, + border: border(theme, "secondary"), + padding: { + bottom: 4, + left: 8, + right: 8, + top: 4, + }, + }, + rowHeight: 28, + contactAvatar: { + cornerRadius: 10, + width: 18, + }, + contactUsername: { + ...text(theme, "mono", "primary", { size: "sm" }), + padding: { + left: 8, + }, + }, + contactButton: { + ...text(theme, "mono", "primary", { size: "sm" }), + background: backgroundColor(theme, 100), + cornerRadius: 12, + padding: { left: 7, right: 7 } + }, + } +} diff --git a/styles/src/styleTree/contactsPanel.ts b/styles/src/styleTree/contactsPanel.ts index aeae2fca6270fc05bf7e7d436f64f4cfe2f2a4a5..202b818d14b21943af8a5b08fe00a4e7ba86e3bd 100644 --- a/styles/src/styleTree/contactsPanel.ts +++ b/styles/src/styleTree/contactsPanel.ts @@ -2,7 +2,7 @@ import Theme from "../themes/theme"; import { panel } from "./app"; import { backgroundColor, border, borderColor, iconColor, player, text } from "./components"; -export default function(theme: Theme) { +export default function contactsPanel(theme: Theme) { const project = { guestAvatarSpacing: 4, height: 24, @@ -64,7 +64,7 @@ export default function(theme: Theme) { left: 8, }, }, - editContact: { + contactButton: { ...text(theme, "mono", "primary", { size: "sm" }), background: backgroundColor(theme, 100), cornerRadius: 12, From f81edb88fe7368a1f7c6ed6876725414f97b0980 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 10 May 2022 16:43:51 +0200 Subject: [PATCH 40/53] Pull out contact finder as a picker Co-Authored-By: Nathan Sobo --- Cargo.lock | 1 + assets/themes/cave-dark.json | 33 --- assets/themes/cave-light.json | 33 --- assets/themes/dark.json | 33 --- assets/themes/light.json | 33 --- assets/themes/solarized-dark.json | 33 --- assets/themes/solarized-light.json | 33 --- assets/themes/sulphurpool-dark.json | 33 --- assets/themes/sulphurpool-light.json | 33 --- crates/contacts_panel/Cargo.toml | 1 + crates/contacts_panel/src/contact_finder.rs | 254 ++++++++++---------- crates/contacts_panel/src/contacts_panel.rs | 20 +- crates/theme/src/theme.rs | 7 +- styles/src/styleTree/contactFinder.ts | 19 +- 14 files changed, 140 insertions(+), 426 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0d3271c0662eaa689cb7af97ff748b51e9df60fd..35e48ed20039fead4d5555dcedbf41489fbeae03 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -935,6 +935,7 @@ dependencies = [ "fuzzy", "gpui", "log", + "picker", "postage", "serde", "settings", diff --git a/assets/themes/cave-dark.json b/assets/themes/cave-dark.json index 68ea7ee996617bf316a7f7feeddf7f32a993fb6f..9e10180e806860fbb3eb9d72de9288cd91e005ef 100644 --- a/assets/themes/cave-dark.json +++ b/assets/themes/cave-dark.json @@ -1466,45 +1466,12 @@ 2 ] }, - "max_width": 540, - "max_height": 420, - "query_editor": { - "background": "#19171c", - "corner_radius": 6, - "text": { - "family": "Zed Mono", - "color": "#e2dfe7", - "size": 14 - }, - "placeholder_text": { - "family": "Zed Mono", - "color": "#7e7887", - "size": 14 - }, - "selection": { - "cursor": "#576ddb", - "selection": "#576ddb3d" - }, - "border": { - "color": "#26232a", - "width": 1 - }, - "padding": { - "bottom": 4, - "left": 8, - "right": 8, - "top": 4 - } - }, "row_height": 28, "contact_avatar": { "corner_radius": 10, "width": 18 }, "contact_username": { - "family": "Zed Mono", - "color": "#e2dfe7", - "size": 14, "padding": { "left": 8 } diff --git a/assets/themes/cave-light.json b/assets/themes/cave-light.json index a7954938dcc3e4806d4305440a783b033b806ade..85edbbc23f609ea7ac5ed3a5986994b311308130 100644 --- a/assets/themes/cave-light.json +++ b/assets/themes/cave-light.json @@ -1466,45 +1466,12 @@ 2 ] }, - "max_width": 540, - "max_height": 420, - "query_editor": { - "background": "#efecf4", - "corner_radius": 6, - "text": { - "family": "Zed Mono", - "color": "#26232a", - "size": 14 - }, - "placeholder_text": { - "family": "Zed Mono", - "color": "#655f6d", - "size": 14 - }, - "selection": { - "cursor": "#576ddb", - "selection": "#576ddb3d" - }, - "border": { - "color": "#e2dfe7", - "width": 1 - }, - "padding": { - "bottom": 4, - "left": 8, - "right": 8, - "top": 4 - } - }, "row_height": 28, "contact_avatar": { "corner_radius": 10, "width": 18 }, "contact_username": { - "family": "Zed Mono", - "color": "#26232a", - "size": 14, "padding": { "left": 8 } diff --git a/assets/themes/dark.json b/assets/themes/dark.json index d1070d6cadbcd2cb740b265d5684226e33ba73eb..da9cff04d0f2d598c824b79376f898d78181d0be 100644 --- a/assets/themes/dark.json +++ b/assets/themes/dark.json @@ -1466,45 +1466,12 @@ 2 ] }, - "max_width": 540, - "max_height": 420, - "query_editor": { - "background": "#000000", - "corner_radius": 6, - "text": { - "family": "Zed Mono", - "color": "#f1f1f1", - "size": 14 - }, - "placeholder_text": { - "family": "Zed Mono", - "color": "#474747", - "size": 14 - }, - "selection": { - "cursor": "#2472f2", - "selection": "#2472f23d" - }, - "border": { - "color": "#232323", - "width": 1 - }, - "padding": { - "bottom": 4, - "left": 8, - "right": 8, - "top": 4 - } - }, "row_height": 28, "contact_avatar": { "corner_radius": 10, "width": 18 }, "contact_username": { - "family": "Zed Mono", - "color": "#f1f1f1", - "size": 14, "padding": { "left": 8 } diff --git a/assets/themes/light.json b/assets/themes/light.json index d79bc79f386389746b8bb3674ec12dd969c5b4b3..c4c0a72bfae2e82d5a929a13ab8e053cacf2e7c1 100644 --- a/assets/themes/light.json +++ b/assets/themes/light.json @@ -1466,45 +1466,12 @@ 2 ] }, - "max_width": 540, - "max_height": 420, - "query_editor": { - "background": "#ffffff", - "corner_radius": 6, - "text": { - "family": "Zed Mono", - "color": "#2b2b2b", - "size": 14 - }, - "placeholder_text": { - "family": "Zed Mono", - "color": "#808080", - "size": 14 - }, - "selection": { - "cursor": "#2472f2", - "selection": "#2472f23d" - }, - "border": { - "color": "#d5d5d5", - "width": 1 - }, - "padding": { - "bottom": 4, - "left": 8, - "right": 8, - "top": 4 - } - }, "row_height": 28, "contact_avatar": { "corner_radius": 10, "width": 18 }, "contact_username": { - "family": "Zed Mono", - "color": "#2b2b2b", - "size": 14, "padding": { "left": 8 } diff --git a/assets/themes/solarized-dark.json b/assets/themes/solarized-dark.json index 01b217e8e5fc6c381f69f745d394e0f3a2ee8101..fbaf1fc5b5ebd11b3ca1fe5d20ddfc961f9ce863 100644 --- a/assets/themes/solarized-dark.json +++ b/assets/themes/solarized-dark.json @@ -1466,45 +1466,12 @@ 2 ] }, - "max_width": 540, - "max_height": 420, - "query_editor": { - "background": "#002b36", - "corner_radius": 6, - "text": { - "family": "Zed Mono", - "color": "#eee8d5", - "size": 14 - }, - "placeholder_text": { - "family": "Zed Mono", - "color": "#839496", - "size": 14 - }, - "selection": { - "cursor": "#268bd2", - "selection": "#268bd23d" - }, - "border": { - "color": "#073642", - "width": 1 - }, - "padding": { - "bottom": 4, - "left": 8, - "right": 8, - "top": 4 - } - }, "row_height": 28, "contact_avatar": { "corner_radius": 10, "width": 18 }, "contact_username": { - "family": "Zed Mono", - "color": "#eee8d5", - "size": 14, "padding": { "left": 8 } diff --git a/assets/themes/solarized-light.json b/assets/themes/solarized-light.json index 6d21fda5dbdd6778145cda0b92d3b5f3ec1aa7d1..f04c8832c5a5a1a5c94189b6833ad992267cdb16 100644 --- a/assets/themes/solarized-light.json +++ b/assets/themes/solarized-light.json @@ -1466,45 +1466,12 @@ 2 ] }, - "max_width": 540, - "max_height": 420, - "query_editor": { - "background": "#fdf6e3", - "corner_radius": 6, - "text": { - "family": "Zed Mono", - "color": "#073642", - "size": 14 - }, - "placeholder_text": { - "family": "Zed Mono", - "color": "#657b83", - "size": 14 - }, - "selection": { - "cursor": "#268bd2", - "selection": "#268bd23d" - }, - "border": { - "color": "#eee8d5", - "width": 1 - }, - "padding": { - "bottom": 4, - "left": 8, - "right": 8, - "top": 4 - } - }, "row_height": 28, "contact_avatar": { "corner_radius": 10, "width": 18 }, "contact_username": { - "family": "Zed Mono", - "color": "#073642", - "size": 14, "padding": { "left": 8 } diff --git a/assets/themes/sulphurpool-dark.json b/assets/themes/sulphurpool-dark.json index 9711ff8f9f5c69ff272fc7351f3757bda7fbc597..58764eb7ec55ada69fc7292d9b35640d909ce813 100644 --- a/assets/themes/sulphurpool-dark.json +++ b/assets/themes/sulphurpool-dark.json @@ -1466,45 +1466,12 @@ 2 ] }, - "max_width": 540, - "max_height": 420, - "query_editor": { - "background": "#202746", - "corner_radius": 6, - "text": { - "family": "Zed Mono", - "color": "#dfe2f1", - "size": 14 - }, - "placeholder_text": { - "family": "Zed Mono", - "color": "#898ea4", - "size": 14 - }, - "selection": { - "cursor": "#3d8fd1", - "selection": "#3d8fd13d" - }, - "border": { - "color": "#293256", - "width": 1 - }, - "padding": { - "bottom": 4, - "left": 8, - "right": 8, - "top": 4 - } - }, "row_height": 28, "contact_avatar": { "corner_radius": 10, "width": 18 }, "contact_username": { - "family": "Zed Mono", - "color": "#dfe2f1", - "size": 14, "padding": { "left": 8 } diff --git a/assets/themes/sulphurpool-light.json b/assets/themes/sulphurpool-light.json index 17ec4fe62a5fac523a7169435d46c3696be3e745..9a2aeb1b05334573ff912792382ae6abb64f5ae2 100644 --- a/assets/themes/sulphurpool-light.json +++ b/assets/themes/sulphurpool-light.json @@ -1466,45 +1466,12 @@ 2 ] }, - "max_width": 540, - "max_height": 420, - "query_editor": { - "background": "#f5f7ff", - "corner_radius": 6, - "text": { - "family": "Zed Mono", - "color": "#293256", - "size": 14 - }, - "placeholder_text": { - "family": "Zed Mono", - "color": "#6b7394", - "size": 14 - }, - "selection": { - "cursor": "#3d8fd1", - "selection": "#3d8fd13d" - }, - "border": { - "color": "#dfe2f1", - "width": 1 - }, - "padding": { - "bottom": 4, - "left": 8, - "right": 8, - "top": 4 - } - }, "row_height": 28, "contact_avatar": { "corner_radius": 10, "width": 18 }, "contact_username": { - "family": "Zed Mono", - "color": "#293256", - "size": 14, "padding": { "left": 8 } diff --git a/crates/contacts_panel/Cargo.toml b/crates/contacts_panel/Cargo.toml index 69cef3177f099bc0901f109f6d0429a6cbf35090..619bcad3385255a232103efad30b954170e3f0da 100644 --- a/crates/contacts_panel/Cargo.toml +++ b/crates/contacts_panel/Cargo.toml @@ -12,6 +12,7 @@ client = { path = "../client" } editor = { path = "../editor" } fuzzy = { path = "../fuzzy" } gpui = { path = "../gpui" } +picker = { path = "../picker" } settings = { path = "../settings" } theme = { path = "../theme" } util = { path = "../util" } diff --git a/crates/contacts_panel/src/contact_finder.rs b/crates/contacts_panel/src/contact_finder.rs index ed5281380c9f9ab77b5d2e8c3d57a28bc9646a79..7626971eaf832e9e76cdde78eced238279de6501 100644 --- a/crates/contacts_panel/src/contact_finder.rs +++ b/crates/contacts_panel/src/contact_finder.rs @@ -1,25 +1,34 @@ use client::{ContactRequestStatus, User, UserStore}; -use editor::Editor; use gpui::{ - color::Color, elements::*, platform::CursorStyle, Entity, LayoutContext, ModelHandle, - RenderContext, Task, View, ViewContext, ViewHandle, + actions, elements::*, Entity, ModelHandle, MutableAppContext, RenderContext, Task, View, + ViewContext, ViewHandle, }; +use picker::{Picker, PickerDelegate}; use settings::Settings; use std::sync::Arc; use util::TryFutureExt; +use workspace::Workspace; -use crate::{RemoveContact, RequestContact}; +actions!(contact_finder, [Toggle]); + +pub fn init(cx: &mut MutableAppContext) { + Picker::::init(cx); + cx.add_action(ContactFinder::toggle); +} pub struct ContactFinder { - query_editor: ViewHandle, - list_state: UniformListState, + picker: ViewHandle>, potential_contacts: Arc<[Arc]>, user_store: ModelHandle, - contacts_search_task: Option>>, + selected_index: usize, +} + +pub enum Event { + Dismissed, } impl Entity for ContactFinder { - type Event = (); + type Event = Event; } impl View for ContactFinder { @@ -27,149 +36,150 @@ impl View for ContactFinder { "ContactFinder" } - fn render(&mut self, cx: &mut RenderContext) -> ElementBox { - let theme = cx.global::().theme.clone(); - let user_store = self.user_store.clone(); - let potential_contacts = self.potential_contacts.clone(); - Flex::column() - .with_child( - ChildView::new(self.query_editor.clone()) - .contained() - .with_style(theme.contact_finder.query_editor.container) - .boxed(), - ) - .with_child( - UniformList::new(self.list_state.clone(), self.potential_contacts.len(), { - let theme = theme.clone(); - move |range, items, cx| { - items.extend(range.map(|ix| { - Self::render_potential_contact( - &potential_contacts[ix], - &user_store, - &theme.contact_finder, - cx, - ) - })) - } - }) - .flex(1., false) - .boxed(), - ) - .contained() - .with_style(theme.contact_finder.container) - .constrained() - .with_max_width(theme.contact_finder.max_width) - .with_max_height(theme.contact_finder.max_height) - .boxed() + fn render(&mut self, _: &mut RenderContext) -> ElementBox { + ChildView::new(self.picker.clone()).boxed() + } + + fn on_focus(&mut self, cx: &mut ViewContext) { + cx.focus(&self.picker); } } -impl ContactFinder { - pub fn new(user_store: ModelHandle, cx: &mut ViewContext) -> Self { - let query_editor = cx.add_view(|cx| { - Editor::single_line(Some(|theme| theme.contact_finder.query_editor.clone()), cx) - }); +impl PickerDelegate for ContactFinder { + fn match_count(&self) -> usize { + self.potential_contacts.len() + } - cx.subscribe(&query_editor, |this, _, event, cx| { - if let editor::Event::BufferEdited = event { - this.query_changed(cx) + fn selected_index(&self) -> usize { + self.selected_index + } + + fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext) { + self.selected_index = ix; + } + + fn update_matches(&mut self, query: String, cx: &mut ViewContext) -> Task<()> { + let search_users = self + .user_store + .update(cx, |store, cx| store.fuzzy_search_users(query, cx)); + + cx.spawn(|this, mut cx| async move { + async { + let potential_contacts = search_users.await?; + this.update(&mut cx, |this, cx| { + this.potential_contacts = potential_contacts.into(); + cx.notify(); + }); + Ok(()) } + .log_err() + .await; }) - .detach(); - Self { - query_editor, - list_state: Default::default(), - potential_contacts: Arc::from([]), - user_store, - contacts_search_task: None, - } } - fn render_potential_contact( - contact: &User, - user_store: &ModelHandle, - theme: &theme::ContactFinder, - cx: &mut LayoutContext, - ) -> ElementBox { - enum RequestContactButton {} + fn confirm(&mut self, cx: &mut ViewContext) { + if let Some(user) = self.potential_contacts.get(self.selected_index) { + let user_store = self.user_store.read(cx); + match user_store.contact_request_status(user) { + ContactRequestStatus::None | ContactRequestStatus::RequestReceived => { + self.user_store + .update(cx, |store, cx| store.request_contact(user.id, cx)) + .detach(); + } + ContactRequestStatus::RequestSent => { + self.user_store + .update(cx, |store, cx| store.remove_contact(user.id, cx)) + .detach(); + } + _ => {} + } + } + } - let contact_id = contact.id; - let request_status = user_store.read(cx).contact_request_status(&contact); + fn dismiss(&mut self, cx: &mut ViewContext) { + cx.emit(Event::Dismissed); + } + fn render_match( + &self, + ix: usize, + mouse_state: &MouseState, + selected: bool, + cx: &gpui::AppContext, + ) -> ElementBox { + let theme = &cx.global::().theme; + let contact = &self.potential_contacts[ix]; + let request_status = self.user_store.read(cx).contact_request_status(&contact); + let label = match request_status { + ContactRequestStatus::None | ContactRequestStatus::RequestReceived => "+", + ContactRequestStatus::RequestSent => "-", + ContactRequestStatus::Pending | ContactRequestStatus::RequestAccepted => "…", + }; + let style = theme.picker.item.style_for(mouse_state, selected); Flex::row() .with_children(contact.avatar.clone().map(|avatar| { Image::new(avatar) - .with_style(theme.contact_avatar) + .with_style(theme.contact_finder.contact_avatar) .aligned() .left() .boxed() })) + .with_child( + Label::new(contact.github_login.clone(), style.label.clone()) + .contained() + .with_style(theme.contact_finder.contact_username) + .aligned() + .left() + .boxed(), + ) .with_child( Label::new( - contact.github_login.clone(), - theme.contact_username.text.clone(), + label.to_string(), + theme.contact_finder.contact_button.text.clone(), ) .contained() - .with_style(theme.contact_username.container) + .with_style(theme.contact_finder.contact_button.container) .aligned() - .left() - .boxed(), - ) - .with_child( - MouseEventHandler::new::( - contact.id as usize, - cx, - |_, _| { - let label = match request_status { - ContactRequestStatus::None | ContactRequestStatus::RequestReceived => { - "+" - } - ContactRequestStatus::RequestSent => "-", - ContactRequestStatus::Pending - | ContactRequestStatus::RequestAccepted => "…", - }; - - Label::new(label.to_string(), theme.contact_button.text.clone()) - .contained() - .with_style(theme.contact_button.container) - .aligned() - .flex_float() - .boxed() - }, - ) - .on_click(move |_, cx| match request_status { - ContactRequestStatus::None => { - cx.dispatch_action(RequestContact(contact_id)); - } - ContactRequestStatus::RequestSent => { - cx.dispatch_action(RemoveContact(contact_id)); - } - _ => {} - }) - .with_cursor_style(CursorStyle::PointingHand) + .flex_float() .boxed(), ) + .contained() + .with_style(style.container) .constrained() - .with_height(theme.row_height) + .with_height(theme.contact_finder.row_height) .boxed() } +} - fn query_changed(&mut self, cx: &mut ViewContext) { - let query = self.query_editor.read(cx).text(cx); - let search_users = self - .user_store - .update(cx, |store, cx| store.fuzzy_search_users(query, cx)); +impl ContactFinder { + fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext) { + workspace.toggle_modal(cx, |cx, workspace| { + let finder = cx.add_view(|cx| Self::new(workspace.user_store().clone(), cx)); + cx.subscribe(&finder, Self::on_event).detach(); + finder + }); + } - self.contacts_search_task = Some(cx.spawn(|this, mut cx| { - async move { - let potential_contacts = search_users.await?; - this.update(&mut cx, |this, cx| { - this.potential_contacts = potential_contacts.into(); - cx.notify(); - }); - Ok(()) + pub fn new(user_store: ModelHandle, cx: &mut ViewContext) -> Self { + let this = cx.weak_handle(); + Self { + picker: cx.add_view(|cx| Picker::new(this, cx)), + potential_contacts: Arc::from([]), + user_store, + selected_index: 0, + } + } + + fn on_event( + workspace: &mut Workspace, + _: ViewHandle, + event: &Event, + cx: &mut ViewContext, + ) { + match event { + Event::Dismissed => { + workspace.dismiss_modal(cx); } - .log_err() - })); + } } } diff --git a/crates/contacts_panel/src/contacts_panel.rs b/crates/contacts_panel/src/contacts_panel.rs index 6ff2ea7aff8827e2e1dd0404131edcc5ee460989..c86d0f45a3ecc1adbcce895eccdb828174b851f9 100644 --- a/crates/contacts_panel/src/contacts_panel.rs +++ b/crates/contacts_panel/src/contacts_panel.rs @@ -1,11 +1,9 @@ mod contact_finder; use client::{Contact, ContactRequestStatus, User, UserStore}; -use contact_finder::ContactFinder; use editor::Editor; use fuzzy::{match_strings, StringMatchCandidate}; use gpui::{ - actions, elements::*, geometry::{rect::RectF, vector::vec2f}, impl_actions, @@ -16,9 +14,8 @@ use gpui::{ use serde::Deserialize; use settings::Settings; use std::sync::Arc; -use workspace::{AppState, JoinProject, Workspace}; +use workspace::{AppState, JoinProject}; -actions!(contacts_panel, [FindNewContacts]); impl_actions!( contacts_panel, [RequestContact, RemoveContact, RespondToContactRequest] @@ -54,10 +51,10 @@ pub struct RespondToContactRequest { } pub fn init(cx: &mut MutableAppContext) { + contact_finder::init(cx); cx.add_action(ContactsPanel::request_contact); cx.add_action(ContactsPanel::remove_contact); cx.add_action(ContactsPanel::respond_to_contact_request); - cx.add_action(ContactsPanel::find_new_contacts); } impl ContactsPanel { @@ -588,16 +585,6 @@ impl ContactsPanel { }) .detach(); } - - fn find_new_contacts( - workspace: &mut Workspace, - _: &FindNewContacts, - cx: &mut ViewContext, - ) { - workspace.toggle_modal(cx, |cx, workspace| { - cx.add_view(|cx| ContactFinder::new(workspace.user_store().clone(), cx)) - }); - } } pub enum Event {} @@ -638,7 +625,8 @@ impl View for ContactsPanel { .aligned() .boxed() }) - .on_click(|_, cx| cx.dispatch_action(FindNewContacts)) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(|_, cx| cx.dispatch_action(contact_finder::Toggle)) .boxed(), ) .constrained() diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 5a47bc504df76476ab06fc1a7554080db83ca052..0c44a95ba9f8b217afe44852980721d7c05d18c5 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -252,14 +252,9 @@ pub struct ContactsPanel { #[derive(Deserialize, Default)] pub struct ContactFinder { - #[serde(flatten)] - pub container: ContainerStyle, - pub max_width: f32, - pub max_height: f32, - pub query_editor: FieldEditor, pub row_height: f32, pub contact_avatar: ImageStyle, - pub contact_username: ContainedText, + pub contact_username: ContainerStyle, pub contact_button: ContainedText, } diff --git a/styles/src/styleTree/contactFinder.ts b/styles/src/styleTree/contactFinder.ts index 1f0fb65776cc634092ffbf39e4207241d0689a67..668363076c0e59e9203e17de2f4023cf12791ebb 100644 --- a/styles/src/styleTree/contactFinder.ts +++ b/styles/src/styleTree/contactFinder.ts @@ -1,33 +1,16 @@ import Theme from "../themes/theme"; import picker from "./picker"; -import { backgroundColor, border, player, text } from "./components"; +import { backgroundColor, text } from "./components"; export default function contactFinder(theme: Theme) { return { ...picker(theme), - maxWidth: 540., - maxHeight: 420., - queryEditor: { - background: backgroundColor(theme, 500), - cornerRadius: 6, - text: text(theme, "mono", "primary"), - placeholderText: text(theme, "mono", "placeholder", { size: "sm" }), - selection: player(theme, 1).selection, - border: border(theme, "secondary"), - padding: { - bottom: 4, - left: 8, - right: 8, - top: 4, - }, - }, rowHeight: 28, contactAvatar: { cornerRadius: 10, width: 18, }, contactUsername: { - ...text(theme, "mono", "primary", { size: "sm" }), padding: { left: 8, }, From d4e6ab49754e155a2bdd27b0b6923393bf04d6f9 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 10 May 2022 17:19:39 +0200 Subject: [PATCH 41/53] Seed first users from GitHub when running script/seed-db --github-users Co-Authored-By: Nathan Sobo --- Cargo.lock | 136 +++++++++++++++++++++++++++++++++- crates/collab/Cargo.toml | 4 +- crates/collab/src/bin/seed.rs | 62 +++++++++++++++- script/seed-db | 2 +- 4 files changed, 197 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 35e48ed20039fead4d5555dcedbf41489fbeae03..6f3f8fed459c1f35d31e8e5456c2c86a44811c6d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -841,6 +841,7 @@ dependencies = [ "async-tungstenite", "axum", "base64 0.13.0", + "clap 3.1.12", "client", "collections", "ctor", @@ -859,6 +860,7 @@ dependencies = [ "parking_lot", "project", "rand 0.8.3", + "reqwest", "rpc", "scrypt", "serde", @@ -2140,6 +2142,19 @@ dependencies = [ "tokio-io-timeout", ] +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper", + "native-tls", + "tokio", + "tokio-native-tls", +] + [[package]] name = "idna" version = "0.2.3" @@ -2244,6 +2259,12 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "ipnet" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879d54834c8c76457ef4293a689b2a8c59b076067ad77b15efafbb05f92a592b" + [[package]] name = "isahc" version = "1.7.0" @@ -2727,6 +2748,24 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a" +[[package]] +name = "native-tls" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd7e2f3618557f980e0b17e8856252eee3c97fa12c54dff0ca290fb6266ca4a9" +dependencies = [ + "lazy_static", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + [[package]] name = "nb-connect" version = "1.0.3" @@ -2910,6 +2949,32 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" +[[package]] +name = "openssl" +version = "0.10.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb81a6430ac911acb25fe5ac8f1d2af1b4ea8a4fdfda0f1ee4292af2e2d8eb0e" +dependencies = [ + "bitflags", + "cfg-if 1.0.0", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b501e44f11665960c7e7fcf062c7d96a14ade4aa98116c004b2e37b5be7d736c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "openssl-probe" version = "0.1.4" @@ -2918,9 +2983,9 @@ checksum = "28988d872ab76095a6e6ac88d99b54fd267702734fd7ffe610ca27f533ddb95a" [[package]] name = "openssl-sys" -version = "0.9.65" +version = "0.9.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a7907e3bfa08bb85105209cdfcb6c63d109f8f6c1ed6ca318fff5c1853fbc1d" +checksum = "9d5fd19fb3e0a8191c1e34935718976a3e70c112ab9a24af6d7cadccd9d90bc0" dependencies = [ "autocfg 1.0.1", "cc", @@ -3684,6 +3749,42 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "reqwest" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87f242f1488a539a79bac6dbe7c8609ae43b7914b7736210f239a37cccb32525" +dependencies = [ + "base64 0.13.0", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "hyper-tls", + "ipnet", + "js-sys", + "lazy_static", + "log", + "mime", + "native-tls", + "percent-encoding", + "pin-project-lite 0.2.9", + "serde", + "serde_json", + "serde_urlencoded", + "tokio", + "tokio-native-tls", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg", +] + [[package]] name = "resvg" version = "0.14.0" @@ -4964,6 +5065,16 @@ dependencies = [ "syn", ] +[[package]] +name = "tokio-native-tls" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d995660bd2b7f8c1568414c1126076c13fbb725c40112dc0120b78eb9b717b" +dependencies = [ + "native-tls", + "tokio", +] + [[package]] name = "tokio-rustls" version = "0.22.0" @@ -5638,6 +5749,18 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fba7978c679d53ce2d0ac80c8c175840feb849a161664365d1287b41f2e67f1" +dependencies = [ + "cfg-if 1.0.0", + "js-sys", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.74" @@ -5783,6 +5906,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "winreg" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0120db82e8a1e0b9fb3345a539c478767c0048d842860994d96113d5b667bd69" +dependencies = [ + "winapi 0.3.9", +] + [[package]] name = "wio" version = "0.2.2" diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index feeed1ba6c7d65573322a1441e2116060a70caae..a5541990d302919956a67bf5cf5f815f794a58de 100644 --- a/crates/collab/Cargo.toml +++ b/crates/collab/Cargo.toml @@ -22,6 +22,7 @@ async-trait = "0.1.50" async-tungstenite = "0.16" axum = { version = "0.5", features = ["json", "headers", "ws"] } base64 = "0.13" +clap = { version = "3.1", features = ["derive"], optional = true } envy = "0.4.2" env_logger = "0.8" futures = "0.3" @@ -32,6 +33,7 @@ opentelemetry = { version = "0.17", features = ["rt-tokio"] } opentelemetry-otlp = { version = "0.10", features = ["tls-roots"] } parking_lot = "0.11.1" rand = "0.8" +reqwest = { version = "0.11", features = ["json"], optional = true } scrypt = "0.7" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" @@ -69,4 +71,4 @@ lazy_static = "1.4" serde_json = { version = "1.0.64", features = ["preserve_order"] } [features] -seed-support = ["lipsum"] +seed-support = ["clap", "lipsum", "reqwest"] diff --git a/crates/collab/src/bin/seed.rs b/crates/collab/src/bin/seed.rs index 7201fd5113a1bf1679862324242b1d0d74cc5b82..ee202ee4a84b128e7fcc91b03a589bb309641a86 100644 --- a/crates/collab/src/bin/seed.rs +++ b/crates/collab/src/bin/seed.rs @@ -1,31 +1,87 @@ +use clap::Parser; use db::{Db, PostgresDb, UserId}; use rand::prelude::*; +use serde::Deserialize; +use std::fmt::Write; use time::{Duration, OffsetDateTime}; #[allow(unused)] #[path = "../db.rs"] mod db; +#[derive(Parser)] +struct Args { + /// Seed users from GitHub. + #[clap(short, long)] + github_users: bool, +} + +#[derive(Debug, Deserialize)] +struct GitHubUser { + id: usize, + login: String, +} + #[tokio::main] async fn main() { + let args = Args::parse(); let mut rng = StdRng::from_entropy(); let database_url = std::env::var("DATABASE_URL").expect("missing DATABASE_URL env var"); let db = PostgresDb::new(&database_url, 5) .await .expect("failed to connect to postgres database"); - let zed_users = ["nathansobo", "maxbrunsfeld", "as-cii", "iamnbutler"]; + let mut zed_users = vec![ + "nathansobo".to_string(), + "maxbrunsfeld".to_string(), + "as-cii".to_string(), + "iamnbutler".to_string(), + "gibusu".to_string(), + "Kethku".to_string(), + ]; + + if args.github_users { + let github_token = std::env::var("GITHUB_TOKEN").expect("missing GITHUB_TOKEN env var"); + let client = reqwest::Client::new(); + let mut last_user_id = None; + for page in 0..20 { + println!("Downloading users from GitHub, page {}", page); + let mut uri = "https://api.github.com/users?per_page=100".to_string(); + if let Some(last_user_id) = last_user_id { + write!(&mut uri, "&since={}", last_user_id).unwrap(); + } + let response = client + .get(uri) + .bearer_auth(&github_token) + .header("user-agent", "zed") + .send() + .await + .expect("failed to fetch github users"); + let users = response + .json::>() + .await + .expect("failed to deserialize github user"); + zed_users.extend(users.iter().map(|user| user.login.clone())); + + if let Some(last_user) = users.last() { + last_user_id = Some(last_user.id); + } else { + break; + } + } + } + let mut zed_user_ids = Vec::::new(); for zed_user in zed_users { if let Some(user) = db - .get_user_by_github_login(zed_user) + .get_user_by_github_login(&zed_user) .await .expect("failed to fetch user") { zed_user_ids.push(user.id); } else { zed_user_ids.push( - db.create_user(zed_user, true) + db.create_user(&zed_user, true) .await .expect("failed to insert user"), ); diff --git a/script/seed-db b/script/seed-db index c69af799dd9ed32ebbac054584a499db7eededc9..2b12e0f480637ed583c51d516ac831e20125c135 100755 --- a/script/seed-db +++ b/script/seed-db @@ -6,4 +6,4 @@ cd crates/collab # Export contents of .env.toml eval "$(cargo run --bin dotenv)" -cargo run --package=collab --features seed-support --bin seed +cargo run --package=collab --features seed-support --bin seed -- $@ From 93688cbe22ee4d158f0bfbd09ac46bd9855e4570 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 10 May 2022 17:26:53 +0200 Subject: [PATCH 42/53] Fix bug when determining contact status Users are sorted by login but we were binary-searching them by id. Co-Authored-By: Nathan Sobo --- crates/client/src/user.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index 3f52b2b6d909a0f87f5b18b64f4380b7650c1695..8a4f723dfe03df23c61795edf5a937e685d08594 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -283,19 +283,19 @@ impl UserStore { ContactRequestStatus::Pending } else if self .contacts - .binary_search_by_key(&&user.id, |contact| &contact.user.id) + .binary_search_by_key(&&user.github_login, |contact| &contact.user.github_login) .is_ok() { ContactRequestStatus::RequestAccepted } else if self .outgoing_contact_requests - .binary_search_by_key(&&user.id, |user| &user.id) + .binary_search_by_key(&&user.github_login, |user| &user.github_login) .is_ok() { ContactRequestStatus::RequestSent } else if self .incoming_contact_requests - .binary_search_by_key(&&user.id, |user| &user.id) + .binary_search_by_key(&&user.github_login, |user| &user.github_login) .is_ok() { ContactRequestStatus::RequestReceived From b00338195ee0bb4801395f857c1d1061b0198fb9 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 10 May 2022 17:30:09 +0200 Subject: [PATCH 43/53] Make user fuzzy search case-insensitive Co-Authored-By: Nathan Sobo --- crates/collab/src/db.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index 3e8ec6b3221d01a95494d053344501e0aabd7557..cd7cbb981fc5a21ce076ae747278e344deff2046 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -125,7 +125,7 @@ impl Db for PostgresDb { let query = " SELECT users.* FROM users - WHERE github_login like $1 + WHERE github_login ILIKE $1 ORDER BY github_login <-> $2 LIMIT $3 "; @@ -898,7 +898,7 @@ pub mod tests { let test_db = TestDb::postgres().await; let db = test_db.db(); for github_login in [ - "california", + "California", "colorado", "oregon", "washington", @@ -911,7 +911,7 @@ pub mod tests { assert_eq!( fuzzy_search_user_names(db, "clr").await, - &["colorado", "california"] + &["colorado", "California"] ); assert_eq!( fuzzy_search_user_names(db, "ro").await, From 6c3e3c84ec8e7a6b6893ddcead2e12fda76c394d Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 10 May 2022 17:32:57 +0200 Subject: [PATCH 44/53] Eliminate flicker when contact status is pending We do this by using a bullet. When we have animations, a spinner would be better. Co-Authored-By: Nathan Sobo --- crates/contacts_panel/src/contact_finder.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/contacts_panel/src/contact_finder.rs b/crates/contacts_panel/src/contact_finder.rs index 7626971eaf832e9e76cdde78eced238279de6501..0d502b51ebb524c840f952b1a7ba53ee8766430f 100644 --- a/crates/contacts_panel/src/contact_finder.rs +++ b/crates/contacts_panel/src/contact_finder.rs @@ -113,7 +113,7 @@ impl PickerDelegate for ContactFinder { let label = match request_status { ContactRequestStatus::None | ContactRequestStatus::RequestReceived => "+", ContactRequestStatus::RequestSent => "-", - ContactRequestStatus::Pending | ContactRequestStatus::RequestAccepted => "…", + ContactRequestStatus::Pending | ContactRequestStatus::RequestAccepted => "•", }; let style = theme.picker.item.style_for(mouse_state, selected); Flex::row() From a12157654519fcd22ad32cedac1b1a0fbec0fc41 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 10 May 2022 18:25:47 +0200 Subject: [PATCH 45/53] WIP --- assets/icons/accept.svg | 3 + assets/icons/reject.svg | 3 + assets/themes/cave-dark.json | 39 +++-- assets/themes/cave-light.json | 39 +++-- assets/themes/dark.json | 43 +++--- assets/themes/light.json | 43 +++--- assets/themes/solarized-dark.json | 39 +++-- assets/themes/solarized-light.json | 39 +++-- assets/themes/sulphurpool-dark.json | 39 +++-- assets/themes/sulphurpool-light.json | 39 +++-- crates/client/src/user.rs | 9 +- crates/contacts_panel/src/contact_finder.rs | 47 +++--- crates/contacts_panel/src/contacts_panel.rs | 154 ++++++++++---------- crates/theme/src/theme.rs | 12 +- styles/src/styleTree/contactFinder.ts | 18 ++- styles/src/styleTree/contactsPanel.ts | 21 ++- 16 files changed, 346 insertions(+), 241 deletions(-) create mode 100644 assets/icons/accept.svg create mode 100644 assets/icons/reject.svg diff --git a/assets/icons/accept.svg b/assets/icons/accept.svg new file mode 100644 index 0000000000000000000000000000000000000000..6a8a7d67a08a3215966942430fe0d528374eee82 --- /dev/null +++ b/assets/icons/accept.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/reject.svg b/assets/icons/reject.svg new file mode 100644 index 0000000000000000000000000000000000000000..e78f49a22894055f22cf5af1fede318e50af8963 --- /dev/null +++ b/assets/icons/reject.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/themes/cave-dark.json b/assets/themes/cave-dark.json index 9e10180e806860fbb3eb9d72de9288cd91e005ef..226db3d0aebfd1f38b7774805232402e2671637a 100644 --- a/assets/themes/cave-dark.json +++ b/assets/themes/cave-dark.json @@ -1240,11 +1240,12 @@ "top": 4 } }, - "add_contact_icon": { + "add_contact_button": { "margin": { "left": 6 }, - "color": "#e2dfe7" + "color": "#e2dfe7", + "width": 8 }, "row_height": 28, "tree_branch_color": "#655f6d", @@ -1262,15 +1263,18 @@ } }, "contact_button": { - "family": "Zed Mono", + "background": "#26232a", + "corner_radius": 8, + "padding": 4, "color": "#e2dfe7", - "size": 14, + "width": 8 + }, + "disabled_contact_button": { "background": "#26232a", - "corner_radius": 12, - "padding": { - "left": 7, - "right": 7 - } + "corner_radius": 8, + "padding": 4, + "color": "#8b8792", + "width": 8 }, "header": { "family": "Zed Mono", @@ -1477,15 +1481,18 @@ } }, "contact_button": { - "family": "Zed Mono", + "background": "#26232a", + "corner_radius": 9, + "padding": 4, "color": "#e2dfe7", - "size": 14, + "width": 10 + }, + "disabled_contact_button": { "background": "#26232a", - "corner_radius": 12, - "padding": { - "left": 7, - "right": 7 - } + "corner_radius": 9, + "padding": 4, + "color": "#8b8792", + "width": 10 } }, "search": { diff --git a/assets/themes/cave-light.json b/assets/themes/cave-light.json index 85edbbc23f609ea7ac5ed3a5986994b311308130..f0b72c37a7b771b532bdae0f7cd46cde69116862 100644 --- a/assets/themes/cave-light.json +++ b/assets/themes/cave-light.json @@ -1240,11 +1240,12 @@ "top": 4 } }, - "add_contact_icon": { + "add_contact_button": { "margin": { "left": 6 }, - "color": "#26232a" + "color": "#26232a", + "width": 8 }, "row_height": 28, "tree_branch_color": "#7e7887", @@ -1262,15 +1263,18 @@ } }, "contact_button": { - "family": "Zed Mono", + "background": "#e2dfe7", + "corner_radius": 8, + "padding": 4, "color": "#26232a", - "size": 14, + "width": 8 + }, + "disabled_contact_button": { "background": "#e2dfe7", - "corner_radius": 12, - "padding": { - "left": 7, - "right": 7 - } + "corner_radius": 8, + "padding": 4, + "color": "#585260", + "width": 8 }, "header": { "family": "Zed Mono", @@ -1477,15 +1481,18 @@ } }, "contact_button": { - "family": "Zed Mono", + "background": "#e2dfe7", + "corner_radius": 9, + "padding": 4, "color": "#26232a", - "size": 14, + "width": 10 + }, + "disabled_contact_button": { "background": "#e2dfe7", - "corner_radius": 12, - "padding": { - "left": 7, - "right": 7 - } + "corner_radius": 9, + "padding": 4, + "color": "#585260", + "width": 10 } }, "search": { diff --git a/assets/themes/dark.json b/assets/themes/dark.json index da9cff04d0f2d598c824b79376f898d78181d0be..46871119cba46c740d4e6c9405a107595a66034d 100644 --- a/assets/themes/dark.json +++ b/assets/themes/dark.json @@ -1240,11 +1240,12 @@ "top": 4 } }, - "add_contact_icon": { + "add_contact_button": { "margin": { "left": 6 }, - "color": "#c6c6c6" + "color": "#c6c6c6", + "width": 8 }, "row_height": 28, "tree_branch_color": "#404040", @@ -1262,15 +1263,18 @@ } }, "contact_button": { - "family": "Zed Mono", - "color": "#f1f1f1", - "size": 14, "background": "#2b2b2b", - "corner_radius": 12, - "padding": { - "left": 7, - "right": 7 - } + "corner_radius": 8, + "padding": 4, + "color": "#c6c6c6", + "width": 8 + }, + "disabled_contact_button": { + "background": "#2b2b2b", + "corner_radius": 8, + "padding": 4, + "color": "#555555", + "width": 8 }, "header": { "family": "Zed Mono", @@ -1477,15 +1481,18 @@ } }, "contact_button": { - "family": "Zed Mono", - "color": "#f1f1f1", - "size": 14, "background": "#2b2b2b", - "corner_radius": 12, - "padding": { - "left": 7, - "right": 7 - } + "corner_radius": 9, + "padding": 4, + "color": "#c6c6c6", + "width": 10 + }, + "disabled_contact_button": { + "background": "#2b2b2b", + "corner_radius": 9, + "padding": 4, + "color": "#555555", + "width": 10 } }, "search": { diff --git a/assets/themes/light.json b/assets/themes/light.json index c4c0a72bfae2e82d5a929a13ab8e053cacf2e7c1..2f7422ec9c20d034afd46893c1e48334b6421478 100644 --- a/assets/themes/light.json +++ b/assets/themes/light.json @@ -1240,11 +1240,12 @@ "top": 4 } }, - "add_contact_icon": { + "add_contact_button": { "margin": { "left": 6 }, - "color": "#393939" + "color": "#393939", + "width": 8 }, "row_height": 28, "tree_branch_color": "#e3e3e3", @@ -1262,15 +1263,18 @@ } }, "contact_button": { - "family": "Zed Mono", - "color": "#2b2b2b", - "size": 14, "background": "#eaeaea", - "corner_radius": 12, - "padding": { - "left": 7, - "right": 7 - } + "corner_radius": 8, + "padding": 4, + "color": "#393939", + "width": 8 + }, + "disabled_contact_button": { + "background": "#eaeaea", + "corner_radius": 8, + "padding": 4, + "color": "#9c9c9c", + "width": 8 }, "header": { "family": "Zed Mono", @@ -1477,15 +1481,18 @@ } }, "contact_button": { - "family": "Zed Mono", - "color": "#2b2b2b", - "size": 14, "background": "#eaeaea", - "corner_radius": 12, - "padding": { - "left": 7, - "right": 7 - } + "corner_radius": 9, + "padding": 4, + "color": "#393939", + "width": 10 + }, + "disabled_contact_button": { + "background": "#eaeaea", + "corner_radius": 9, + "padding": 4, + "color": "#9c9c9c", + "width": 10 } }, "search": { diff --git a/assets/themes/solarized-dark.json b/assets/themes/solarized-dark.json index fbaf1fc5b5ebd11b3ca1fe5d20ddfc961f9ce863..d31d7368d89ded236a35bb0c031dd2349dc6d6fc 100644 --- a/assets/themes/solarized-dark.json +++ b/assets/themes/solarized-dark.json @@ -1240,11 +1240,12 @@ "top": 4 } }, - "add_contact_icon": { + "add_contact_button": { "margin": { "left": 6 }, - "color": "#eee8d5" + "color": "#eee8d5", + "width": 8 }, "row_height": 28, "tree_branch_color": "#657b83", @@ -1262,15 +1263,18 @@ } }, "contact_button": { - "family": "Zed Mono", + "background": "#073642", + "corner_radius": 8, + "padding": 4, "color": "#eee8d5", - "size": 14, + "width": 8 + }, + "disabled_contact_button": { "background": "#073642", - "corner_radius": 12, - "padding": { - "left": 7, - "right": 7 - } + "corner_radius": 8, + "padding": 4, + "color": "#93a1a1", + "width": 8 }, "header": { "family": "Zed Mono", @@ -1477,15 +1481,18 @@ } }, "contact_button": { - "family": "Zed Mono", + "background": "#073642", + "corner_radius": 9, + "padding": 4, "color": "#eee8d5", - "size": 14, + "width": 10 + }, + "disabled_contact_button": { "background": "#073642", - "corner_radius": 12, - "padding": { - "left": 7, - "right": 7 - } + "corner_radius": 9, + "padding": 4, + "color": "#93a1a1", + "width": 10 } }, "search": { diff --git a/assets/themes/solarized-light.json b/assets/themes/solarized-light.json index f04c8832c5a5a1a5c94189b6833ad992267cdb16..8f0749a8e503ba009c37ff1fe00f9e0a68986419 100644 --- a/assets/themes/solarized-light.json +++ b/assets/themes/solarized-light.json @@ -1240,11 +1240,12 @@ "top": 4 } }, - "add_contact_icon": { + "add_contact_button": { "margin": { "left": 6 }, - "color": "#073642" + "color": "#073642", + "width": 8 }, "row_height": 28, "tree_branch_color": "#839496", @@ -1262,15 +1263,18 @@ } }, "contact_button": { - "family": "Zed Mono", + "background": "#eee8d5", + "corner_radius": 8, + "padding": 4, "color": "#073642", - "size": 14, + "width": 8 + }, + "disabled_contact_button": { "background": "#eee8d5", - "corner_radius": 12, - "padding": { - "left": 7, - "right": 7 - } + "corner_radius": 8, + "padding": 4, + "color": "#586e75", + "width": 8 }, "header": { "family": "Zed Mono", @@ -1477,15 +1481,18 @@ } }, "contact_button": { - "family": "Zed Mono", + "background": "#eee8d5", + "corner_radius": 9, + "padding": 4, "color": "#073642", - "size": 14, + "width": 10 + }, + "disabled_contact_button": { "background": "#eee8d5", - "corner_radius": 12, - "padding": { - "left": 7, - "right": 7 - } + "corner_radius": 9, + "padding": 4, + "color": "#586e75", + "width": 10 } }, "search": { diff --git a/assets/themes/sulphurpool-dark.json b/assets/themes/sulphurpool-dark.json index 58764eb7ec55ada69fc7292d9b35640d909ce813..4eef98392411e36fd406cccd9b307ec056bf84e4 100644 --- a/assets/themes/sulphurpool-dark.json +++ b/assets/themes/sulphurpool-dark.json @@ -1240,11 +1240,12 @@ "top": 4 } }, - "add_contact_icon": { + "add_contact_button": { "margin": { "left": 6 }, - "color": "#dfe2f1" + "color": "#dfe2f1", + "width": 8 }, "row_height": 28, "tree_branch_color": "#6b7394", @@ -1262,15 +1263,18 @@ } }, "contact_button": { - "family": "Zed Mono", + "background": "#293256", + "corner_radius": 8, + "padding": 4, "color": "#dfe2f1", - "size": 14, + "width": 8 + }, + "disabled_contact_button": { "background": "#293256", - "corner_radius": 12, - "padding": { - "left": 7, - "right": 7 - } + "corner_radius": 8, + "padding": 4, + "color": "#979db4", + "width": 8 }, "header": { "family": "Zed Mono", @@ -1477,15 +1481,18 @@ } }, "contact_button": { - "family": "Zed Mono", + "background": "#293256", + "corner_radius": 9, + "padding": 4, "color": "#dfe2f1", - "size": 14, + "width": 10 + }, + "disabled_contact_button": { "background": "#293256", - "corner_radius": 12, - "padding": { - "left": 7, - "right": 7 - } + "corner_radius": 9, + "padding": 4, + "color": "#979db4", + "width": 10 } }, "search": { diff --git a/assets/themes/sulphurpool-light.json b/assets/themes/sulphurpool-light.json index 9a2aeb1b05334573ff912792382ae6abb64f5ae2..5f8badbf9144e98ca94bfa4ef3af1dbcb088ca2c 100644 --- a/assets/themes/sulphurpool-light.json +++ b/assets/themes/sulphurpool-light.json @@ -1240,11 +1240,12 @@ "top": 4 } }, - "add_contact_icon": { + "add_contact_button": { "margin": { "left": 6 }, - "color": "#293256" + "color": "#293256", + "width": 8 }, "row_height": 28, "tree_branch_color": "#898ea4", @@ -1262,15 +1263,18 @@ } }, "contact_button": { - "family": "Zed Mono", + "background": "#dfe2f1", + "corner_radius": 8, + "padding": 4, "color": "#293256", - "size": 14, + "width": 8 + }, + "disabled_contact_button": { "background": "#dfe2f1", - "corner_radius": 12, - "padding": { - "left": 7, - "right": 7 - } + "corner_radius": 8, + "padding": 4, + "color": "#5e6687", + "width": 8 }, "header": { "family": "Zed Mono", @@ -1477,15 +1481,18 @@ } }, "contact_button": { - "family": "Zed Mono", + "background": "#dfe2f1", + "corner_radius": 9, + "padding": 4, "color": "#293256", - "size": 14, + "width": 10 + }, + "disabled_contact_button": { "background": "#dfe2f1", - "corner_radius": 12, - "padding": { - "left": 7, - "right": 7 - } + "corner_radius": 9, + "padding": 4, + "color": "#5e6687", + "width": 10 } }, "search": { diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index 8a4f723dfe03df23c61795edf5a937e685d08594..1874822774a36ef2c7c777bd5e7d91f2d5e72b71 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -35,7 +35,6 @@ pub struct ProjectMetadata { #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ContactRequestStatus { None, - Pending, RequestSent, RequestReceived, RequestAccepted, @@ -278,10 +277,12 @@ impl UserStore { &self.outgoing_contact_requests } + pub fn is_contact_request_pending(&self, user: &User) -> bool { + self.pending_contact_requests.contains_key(&user.id) + } + pub fn contact_request_status(&self, user: &User) -> ContactRequestStatus { - if self.pending_contact_requests.contains_key(&user.id) { - ContactRequestStatus::Pending - } else if self + if self .contacts .binary_search_by_key(&&user.github_login, |contact| &contact.user.github_login) .is_ok() diff --git a/crates/contacts_panel/src/contact_finder.rs b/crates/contacts_panel/src/contact_finder.rs index 0d502b51ebb524c840f952b1a7ba53ee8766430f..6de61fdc3b398c86fc3b332fbb7772fb7db031a6 100644 --- a/crates/contacts_panel/src/contact_finder.rs +++ b/crates/contacts_panel/src/contact_finder.rs @@ -108,16 +108,25 @@ impl PickerDelegate for ContactFinder { cx: &gpui::AppContext, ) -> ElementBox { let theme = &cx.global::().theme; - let contact = &self.potential_contacts[ix]; - let request_status = self.user_store.read(cx).contact_request_status(&contact); - let label = match request_status { - ContactRequestStatus::None | ContactRequestStatus::RequestReceived => "+", - ContactRequestStatus::RequestSent => "-", - ContactRequestStatus::Pending | ContactRequestStatus::RequestAccepted => "•", + let user = &self.potential_contacts[ix]; + let request_status = self.user_store.read(cx).contact_request_status(&user); + + let icon_path = match request_status { + ContactRequestStatus::None | ContactRequestStatus::RequestReceived => { + "icons/accept.svg" + } + ContactRequestStatus::RequestSent | ContactRequestStatus::RequestAccepted => { + "icons/reject.svg" + } + }; + let button_style = if self.user_store.read(cx).is_contact_request_pending(&user) { + &theme.contact_finder.disabled_contact_button + } else { + &theme.contact_finder.contact_button }; let style = theme.picker.item.style_for(mouse_state, selected); Flex::row() - .with_children(contact.avatar.clone().map(|avatar| { + .with_children(user.avatar.clone().map(|avatar| { Image::new(avatar) .with_style(theme.contact_finder.contact_avatar) .aligned() @@ -125,7 +134,7 @@ impl PickerDelegate for ContactFinder { .boxed() })) .with_child( - Label::new(contact.github_login.clone(), style.label.clone()) + Label::new(user.github_login.clone(), style.label.clone()) .contained() .with_style(theme.contact_finder.contact_username) .aligned() @@ -133,15 +142,19 @@ impl PickerDelegate for ContactFinder { .boxed(), ) .with_child( - Label::new( - label.to_string(), - theme.contact_finder.contact_button.text.clone(), - ) - .contained() - .with_style(theme.contact_finder.contact_button.container) - .aligned() - .flex_float() - .boxed(), + Svg::new(icon_path) + .with_color(button_style.color) + .constrained() + .with_width(button_style.icon_width) + .aligned() + .constrained() + .with_width(button_style.button_width) + .with_height(button_style.button_width) + .contained() + .with_style(button_style.container) + .aligned() + .flex_float() + .boxed(), ) .contained() .with_style(style.container) diff --git a/crates/contacts_panel/src/contacts_panel.rs b/crates/contacts_panel/src/contacts_panel.rs index c86d0f45a3ecc1adbcce895eccdb828174b851f9..c771ea7ffd7fa6e7b823aa162b4c2a9f76da4b8a 100644 --- a/crates/contacts_panel/src/contacts_panel.rs +++ b/crates/contacts_panel/src/contacts_panel.rs @@ -1,6 +1,6 @@ mod contact_finder; -use client::{Contact, ContactRequestStatus, User, UserStore}; +use client::{Contact, User, UserStore}; use editor::Editor; use fuzzy::{match_strings, StringMatchCandidate}; use gpui::{ @@ -314,7 +314,6 @@ impl ContactsPanel { enum Accept {} let user_id = user.id; - let request_status = user_store.read(cx).contact_request_status(&user); let mut row = Flex::row() .with_children(user.avatar.clone().map(|avatar| { @@ -336,51 +335,60 @@ impl ContactsPanel { .boxed(), ); - if request_status == ContactRequestStatus::Pending { - row.add_child( - Label::new("…".to_string(), theme.contact_button.text.clone()) + let button_style = if user_store.read(cx).is_contact_request_pending(&user) { + &theme.disabled_contact_button + } else { + &theme.contact_button + }; + + row.add_children([ + MouseEventHandler::new::(user.id as usize, cx, |_, _| { + Svg::new("icons/reject.svg") + .with_color(button_style.color) + .constrained() + .with_width(button_style.icon_width) + .aligned() + .constrained() + .with_width(button_style.button_width) + .with_height(button_style.button_width) .contained() - .with_style(theme.contact_button.container) + .with_style(button_style.container) .aligned() - .flex_float() - .boxed(), - ); - } else { - row.add_children([ - MouseEventHandler::new::(user.id as usize, cx, |_, _| { - Label::new("Reject".to_string(), theme.contact_button.text.clone()) - .contained() - .with_style(theme.contact_button.container) - .aligned() - .flex_float() - .boxed() - }) - .on_click(move |_, cx| { - cx.dispatch_action(RespondToContactRequest { - user_id, - accept: false, - }) - }) - .with_cursor_style(CursorStyle::PointingHand) - .flex_float() - .boxed(), - MouseEventHandler::new::(user.id as usize, cx, |_, _| { - Label::new("Accept".to_string(), theme.contact_button.text.clone()) - .contained() - .with_style(theme.contact_button.container) - .aligned() - .boxed() + .boxed() + }) + .on_click(move |_, cx| { + cx.dispatch_action(RespondToContactRequest { + user_id, + accept: false, }) - .on_click(move |_, cx| { - cx.dispatch_action(RespondToContactRequest { - user_id, - accept: true, - }) + }) + .with_cursor_style(CursorStyle::PointingHand) + .flex_float() + .boxed(), + MouseEventHandler::new::(user.id as usize, cx, |_, _| { + Svg::new("icons/accept.svg") + .with_color(button_style.color) + .constrained() + .with_width(button_style.icon_width) + .with_height(button_style.icon_width) + .aligned() + .constrained() + .with_width(button_style.button_width) + .with_height(button_style.button_width) + .contained() + .with_style(button_style.container) + .aligned() + .boxed() + }) + .on_click(move |_, cx| { + cx.dispatch_action(RespondToContactRequest { + user_id, + accept: true, }) - .with_cursor_style(CursorStyle::PointingHand) - .boxed(), - ]); - } + }) + .with_cursor_style(CursorStyle::PointingHand) + .boxed(), + ]); row.constrained().with_height(theme.row_height).boxed() } @@ -394,7 +402,11 @@ impl ContactsPanel { enum Cancel {} let user_id = user.id; - let request_status = user_store.read(cx).contact_request_status(&user); + let button_style = if user_store.read(cx).is_contact_request_pending(&user) { + &theme.disabled_contact_button + } else { + &theme.contact_button + }; let mut row = Flex::row() .with_children(user.avatar.clone().map(|avatar| { @@ -416,31 +428,23 @@ impl ContactsPanel { .boxed(), ); - if request_status == ContactRequestStatus::Pending { - row.add_child( - Label::new("…".to_string(), theme.contact_button.text.clone()) + row.add_child( + MouseEventHandler::new::(user.id as usize, cx, |_, _| { + Svg::new("icons/reject.svg") + .with_color(button_style.color) + .constrained() + .with_width(button_style.icon_width) + .with_height(button_style.icon_width) .contained() - .with_style(theme.contact_button.container) + .with_style(button_style.container) .aligned() - .flex_float() - .boxed(), - ); - } else { - row.add_child( - MouseEventHandler::new::(user.id as usize, cx, |_, _| { - Label::new("Cancel".to_string(), theme.contact_button.text.clone()) - .contained() - .with_style(theme.contact_button.container) - .aligned() - .flex_float() - .boxed() - }) - .on_click(move |_, cx| cx.dispatch_action(RemoveContact(user_id))) - .with_cursor_style(CursorStyle::PointingHand) - .flex_float() - .boxed(), - ); - } + .boxed() + }) + .on_click(move |_, cx| cx.dispatch_action(RemoveContact(user_id))) + .with_cursor_style(CursorStyle::PointingHand) + .flex_float() + .boxed(), + ); row.constrained().with_height(theme.row_height).boxed() } @@ -452,6 +456,7 @@ impl ContactsPanel { self.entries.clear(); + let mut request_entries = Vec::new(); let incoming = user_store.incoming_contact_requests(); if !incoming.is_empty() { self.match_candidates.clear(); @@ -475,8 +480,7 @@ impl ContactsPanel { executor.clone(), )); if !matches.is_empty() { - self.entries.push(ContactEntry::Header("Requests Received")); - self.entries.extend( + request_entries.extend( matches.iter().map(|mat| { ContactEntry::IncomingRequest(incoming[mat.candidate_id].clone()) }), @@ -507,8 +511,7 @@ impl ContactsPanel { executor.clone(), )); if !matches.is_empty() { - self.entries.push(ContactEntry::Header("Requests Sent")); - self.entries.extend( + request_entries.extend( matches.iter().map(|mat| { ContactEntry::OutgoingRequest(outgoing[mat.candidate_id].clone()) }), @@ -516,6 +519,11 @@ impl ContactsPanel { } } + if !request_entries.is_empty() { + self.entries.push(ContactEntry::Header("Requests")); + self.entries.append(&mut request_entries); + } + let contacts = user_store.contacts(); if !contacts.is_empty() { self.match_candidates.clear(); @@ -617,11 +625,11 @@ impl View for ContactsPanel { .with_child( MouseEventHandler::new::(0, cx, |_, _| { Svg::new("icons/add-contact.svg") - .with_color(theme.add_contact_icon.color) + .with_color(theme.add_contact_button.color) .constrained() .with_height(12.) .contained() - .with_style(theme.add_contact_icon.container) + .with_style(theme.add_contact_button.container) .aligned() .boxed() }) diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 0c44a95ba9f8b217afe44852980721d7c05d18c5..427123e6294e283de3eaa7ef506ec7eed30f353d 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -237,11 +237,12 @@ pub struct ContactsPanel { pub container: ContainerStyle, pub header: ContainedText, pub user_query_editor: FieldEditor, - pub add_contact_icon: AddContactIcon, + pub add_contact_button: IconButton, pub row_height: f32, pub contact_avatar: ImageStyle, pub contact_username: ContainedText, - pub contact_button: ContainedText, + pub contact_button: IconButton, + pub disabled_contact_button: IconButton, pub tree_branch_width: f32, pub tree_branch_color: Color, pub shared_project: ProjectRow, @@ -255,14 +256,17 @@ pub struct ContactFinder { pub row_height: f32, pub contact_avatar: ImageStyle, pub contact_username: ContainerStyle, - pub contact_button: ContainedText, + pub contact_button: IconButton, + pub disabled_contact_button: IconButton, } #[derive(Deserialize, Default)] -pub struct AddContactIcon { +pub struct IconButton { #[serde(flatten)] pub container: ContainerStyle, pub color: Color, + pub icon_width: f32, + pub button_width: f32, } #[derive(Deserialize, Default)] diff --git a/styles/src/styleTree/contactFinder.ts b/styles/src/styleTree/contactFinder.ts index 668363076c0e59e9203e17de2f4023cf12791ebb..f79f55a237f9a22314a50e4ab086902b9afbd5ab 100644 --- a/styles/src/styleTree/contactFinder.ts +++ b/styles/src/styleTree/contactFinder.ts @@ -1,8 +1,16 @@ import Theme from "../themes/theme"; import picker from "./picker"; -import { backgroundColor, text } from "./components"; +import { backgroundColor, iconColor } from "./components"; export default function contactFinder(theme: Theme) { + const contactButton = { + background: backgroundColor(theme, 100), + color: iconColor(theme, "primary"), + iconWidth: 8, + buttonWidth: 16, + cornerRadius: 8, + }; + return { ...picker(theme), rowHeight: 28, @@ -15,11 +23,11 @@ export default function contactFinder(theme: Theme) { left: 8, }, }, - contactButton: { - ...text(theme, "mono", "primary", { size: "sm" }), + contactButton, + disabledContactButton: { + ...contactButton, background: backgroundColor(theme, 100), - cornerRadius: 12, - padding: { left: 7, right: 7 } + color: iconColor(theme, "muted"), }, } } diff --git a/styles/src/styleTree/contactsPanel.ts b/styles/src/styleTree/contactsPanel.ts index 202b818d14b21943af8a5b08fe00a4e7ba86e3bd..9aa9109964cb0aaf37eb6c4896da5ef410ebb823 100644 --- a/styles/src/styleTree/contactsPanel.ts +++ b/styles/src/styleTree/contactsPanel.ts @@ -31,6 +31,14 @@ export default function contactsPanel(theme: Theme) { }, }; + const contactButton = { + background: backgroundColor(theme, 100), + color: iconColor(theme, "primary"), + iconWidth: 8, + buttonWidth: 16, + cornerRadius: 8, + }; + return { ...panel, userQueryEditor: { @@ -47,9 +55,10 @@ export default function contactsPanel(theme: Theme) { top: 4, }, }, - addContactIcon: { + addContactButton: { margin: { left: 6 }, - color: iconColor(theme, "primary") + color: iconColor(theme, "primary"), + width: 8, }, rowHeight: 28, treeBranchColor: borderColor(theme, "muted"), @@ -64,11 +73,11 @@ export default function contactsPanel(theme: Theme) { left: 8, }, }, - contactButton: { - ...text(theme, "mono", "primary", { size: "sm" }), + contactButton, + disabledContactButton: { + ...contactButton, background: backgroundColor(theme, 100), - cornerRadius: 12, - padding: { left: 7, right: 7 } + color: iconColor(theme, "muted"), }, header: { ...text(theme, "mono", "secondary", { size: "sm" }), From 2cf9659f889356c01e13cdca976fc4a044791b30 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 10 May 2022 10:47:25 -0700 Subject: [PATCH 46/53] Style the buttons in the contact panel and contact finder Co-authored-by: Nathan Sobo Co-authored-by: Antonio Scandurra --- assets/themes/cave-dark.json | 33 ++++--- assets/themes/cave-light.json | 33 ++++--- assets/themes/dark.json | 33 ++++--- assets/themes/light.json | 33 ++++--- assets/themes/solarized-dark.json | 33 ++++--- assets/themes/solarized-light.json | 33 ++++--- assets/themes/sulphurpool-dark.json | 33 ++++--- assets/themes/sulphurpool-light.json | 33 ++++--- crates/contacts_panel/src/contact_finder.rs | 13 +-- crates/contacts_panel/src/contacts_panel.rs | 99 +++++++++++---------- crates/theme/src/theme.rs | 2 +- styles/src/styleTree/contactFinder.ts | 7 +- styles/src/styleTree/contactsPanel.ts | 10 ++- 13 files changed, 230 insertions(+), 165 deletions(-) diff --git a/assets/themes/cave-dark.json b/assets/themes/cave-dark.json index 226db3d0aebfd1f38b7774805232402e2671637a..55e255aa0b10755efb236653fea6c9058ba4dde1 100644 --- a/assets/themes/cave-dark.json +++ b/assets/themes/cave-dark.json @@ -1245,7 +1245,8 @@ "left": 6 }, "color": "#e2dfe7", - "width": 8 + "button_width": 8, + "icon_width": 8 }, "row_height": 28, "tree_branch_color": "#655f6d", @@ -1264,17 +1265,20 @@ }, "contact_button": { "background": "#26232a", - "corner_radius": 8, - "padding": 4, "color": "#e2dfe7", - "width": 8 + "icon_width": 8, + "button_width": 16, + "corner_radius": 8, + "hover": { + "background": "#5852603d" + } }, "disabled_contact_button": { "background": "#26232a", - "corner_radius": 8, - "padding": 4, "color": "#8b8792", - "width": 8 + "icon_width": 8, + "button_width": 16, + "corner_radius": 8 }, "header": { "family": "Zed Mono", @@ -1482,17 +1486,20 @@ }, "contact_button": { "background": "#26232a", - "corner_radius": 9, - "padding": 4, "color": "#e2dfe7", - "width": 10 + "icon_width": 8, + "button_width": 16, + "corner_radius": 8, + "hover": { + "background": "#5852603d" + } }, "disabled_contact_button": { "background": "#26232a", - "corner_radius": 9, - "padding": 4, "color": "#8b8792", - "width": 10 + "icon_width": 8, + "button_width": 16, + "corner_radius": 8 } }, "search": { diff --git a/assets/themes/cave-light.json b/assets/themes/cave-light.json index f0b72c37a7b771b532bdae0f7cd46cde69116862..626cc6f7541a6b77ef5121c45a02b40d6e2e8826 100644 --- a/assets/themes/cave-light.json +++ b/assets/themes/cave-light.json @@ -1245,7 +1245,8 @@ "left": 6 }, "color": "#26232a", - "width": 8 + "button_width": 8, + "icon_width": 8 }, "row_height": 28, "tree_branch_color": "#7e7887", @@ -1264,17 +1265,20 @@ }, "contact_button": { "background": "#e2dfe7", - "corner_radius": 8, - "padding": 4, "color": "#26232a", - "width": 8 + "icon_width": 8, + "button_width": 16, + "corner_radius": 8, + "hover": { + "background": "#8b87921f" + } }, "disabled_contact_button": { "background": "#e2dfe7", - "corner_radius": 8, - "padding": 4, "color": "#585260", - "width": 8 + "icon_width": 8, + "button_width": 16, + "corner_radius": 8 }, "header": { "family": "Zed Mono", @@ -1482,17 +1486,20 @@ }, "contact_button": { "background": "#e2dfe7", - "corner_radius": 9, - "padding": 4, "color": "#26232a", - "width": 10 + "icon_width": 8, + "button_width": 16, + "corner_radius": 8, + "hover": { + "background": "#8b87921f" + } }, "disabled_contact_button": { "background": "#e2dfe7", - "corner_radius": 9, - "padding": 4, "color": "#585260", - "width": 10 + "icon_width": 8, + "button_width": 16, + "corner_radius": 8 } }, "search": { diff --git a/assets/themes/dark.json b/assets/themes/dark.json index 46871119cba46c740d4e6c9405a107595a66034d..877985f7dbd55a94dab3ffe40661222a5b2d87e2 100644 --- a/assets/themes/dark.json +++ b/assets/themes/dark.json @@ -1245,7 +1245,8 @@ "left": 6 }, "color": "#c6c6c6", - "width": 8 + "button_width": 8, + "icon_width": 8 }, "row_height": 28, "tree_branch_color": "#404040", @@ -1264,17 +1265,20 @@ }, "contact_button": { "background": "#2b2b2b", - "corner_radius": 8, - "padding": 4, "color": "#c6c6c6", - "width": 8 + "icon_width": 8, + "button_width": 16, + "corner_radius": 8, + "hover": { + "background": "#323232" + } }, "disabled_contact_button": { "background": "#2b2b2b", - "corner_radius": 8, - "padding": 4, "color": "#555555", - "width": 8 + "icon_width": 8, + "button_width": 16, + "corner_radius": 8 }, "header": { "family": "Zed Mono", @@ -1482,17 +1486,20 @@ }, "contact_button": { "background": "#2b2b2b", - "corner_radius": 9, - "padding": 4, "color": "#c6c6c6", - "width": 10 + "icon_width": 8, + "button_width": 16, + "corner_radius": 8, + "hover": { + "background": "#323232" + } }, "disabled_contact_button": { "background": "#2b2b2b", - "corner_radius": 9, - "padding": 4, "color": "#555555", - "width": 10 + "icon_width": 8, + "button_width": 16, + "corner_radius": 8 } }, "search": { diff --git a/assets/themes/light.json b/assets/themes/light.json index 2f7422ec9c20d034afd46893c1e48334b6421478..51a48642fe7385a04601122c10820d48bbfb2a92 100644 --- a/assets/themes/light.json +++ b/assets/themes/light.json @@ -1245,7 +1245,8 @@ "left": 6 }, "color": "#393939", - "width": 8 + "button_width": 8, + "icon_width": 8 }, "row_height": 28, "tree_branch_color": "#e3e3e3", @@ -1264,17 +1265,20 @@ }, "contact_button": { "background": "#eaeaea", - "corner_radius": 8, - "padding": 4, "color": "#393939", - "width": 8 + "icon_width": 8, + "button_width": 16, + "corner_radius": 8, + "hover": { + "background": "#e3e3e3" + } }, "disabled_contact_button": { "background": "#eaeaea", - "corner_radius": 8, - "padding": 4, "color": "#9c9c9c", - "width": 8 + "icon_width": 8, + "button_width": 16, + "corner_radius": 8 }, "header": { "family": "Zed Mono", @@ -1482,17 +1486,20 @@ }, "contact_button": { "background": "#eaeaea", - "corner_radius": 9, - "padding": 4, "color": "#393939", - "width": 10 + "icon_width": 8, + "button_width": 16, + "corner_radius": 8, + "hover": { + "background": "#e3e3e3" + } }, "disabled_contact_button": { "background": "#eaeaea", - "corner_radius": 9, - "padding": 4, "color": "#9c9c9c", - "width": 10 + "icon_width": 8, + "button_width": 16, + "corner_radius": 8 } }, "search": { diff --git a/assets/themes/solarized-dark.json b/assets/themes/solarized-dark.json index d31d7368d89ded236a35bb0c031dd2349dc6d6fc..acf04db255ab2c6a60f655e79c83cef4ba94f2a8 100644 --- a/assets/themes/solarized-dark.json +++ b/assets/themes/solarized-dark.json @@ -1245,7 +1245,8 @@ "left": 6 }, "color": "#eee8d5", - "width": 8 + "button_width": 8, + "icon_width": 8 }, "row_height": 28, "tree_branch_color": "#657b83", @@ -1264,17 +1265,20 @@ }, "contact_button": { "background": "#073642", - "corner_radius": 8, - "padding": 4, "color": "#eee8d5", - "width": 8 + "icon_width": 8, + "button_width": 16, + "corner_radius": 8, + "hover": { + "background": "#586e753d" + } }, "disabled_contact_button": { "background": "#073642", - "corner_radius": 8, - "padding": 4, "color": "#93a1a1", - "width": 8 + "icon_width": 8, + "button_width": 16, + "corner_radius": 8 }, "header": { "family": "Zed Mono", @@ -1482,17 +1486,20 @@ }, "contact_button": { "background": "#073642", - "corner_radius": 9, - "padding": 4, "color": "#eee8d5", - "width": 10 + "icon_width": 8, + "button_width": 16, + "corner_radius": 8, + "hover": { + "background": "#586e753d" + } }, "disabled_contact_button": { "background": "#073642", - "corner_radius": 9, - "padding": 4, "color": "#93a1a1", - "width": 10 + "icon_width": 8, + "button_width": 16, + "corner_radius": 8 } }, "search": { diff --git a/assets/themes/solarized-light.json b/assets/themes/solarized-light.json index 8f0749a8e503ba009c37ff1fe00f9e0a68986419..a85463a0d9cda9236ee3450e674d9f2dc4118958 100644 --- a/assets/themes/solarized-light.json +++ b/assets/themes/solarized-light.json @@ -1245,7 +1245,8 @@ "left": 6 }, "color": "#073642", - "width": 8 + "button_width": 8, + "icon_width": 8 }, "row_height": 28, "tree_branch_color": "#839496", @@ -1264,17 +1265,20 @@ }, "contact_button": { "background": "#eee8d5", - "corner_radius": 8, - "padding": 4, "color": "#073642", - "width": 8 + "icon_width": 8, + "button_width": 16, + "corner_radius": 8, + "hover": { + "background": "#93a1a11f" + } }, "disabled_contact_button": { "background": "#eee8d5", - "corner_radius": 8, - "padding": 4, "color": "#586e75", - "width": 8 + "icon_width": 8, + "button_width": 16, + "corner_radius": 8 }, "header": { "family": "Zed Mono", @@ -1482,17 +1486,20 @@ }, "contact_button": { "background": "#eee8d5", - "corner_radius": 9, - "padding": 4, "color": "#073642", - "width": 10 + "icon_width": 8, + "button_width": 16, + "corner_radius": 8, + "hover": { + "background": "#93a1a11f" + } }, "disabled_contact_button": { "background": "#eee8d5", - "corner_radius": 9, - "padding": 4, "color": "#586e75", - "width": 10 + "icon_width": 8, + "button_width": 16, + "corner_radius": 8 } }, "search": { diff --git a/assets/themes/sulphurpool-dark.json b/assets/themes/sulphurpool-dark.json index 4eef98392411e36fd406cccd9b307ec056bf84e4..abf15762576f15aaa876775b527e5247df265cda 100644 --- a/assets/themes/sulphurpool-dark.json +++ b/assets/themes/sulphurpool-dark.json @@ -1245,7 +1245,8 @@ "left": 6 }, "color": "#dfe2f1", - "width": 8 + "button_width": 8, + "icon_width": 8 }, "row_height": 28, "tree_branch_color": "#6b7394", @@ -1264,17 +1265,20 @@ }, "contact_button": { "background": "#293256", - "corner_radius": 8, - "padding": 4, "color": "#dfe2f1", - "width": 8 + "icon_width": 8, + "button_width": 16, + "corner_radius": 8, + "hover": { + "background": "#5e66873d" + } }, "disabled_contact_button": { "background": "#293256", - "corner_radius": 8, - "padding": 4, "color": "#979db4", - "width": 8 + "icon_width": 8, + "button_width": 16, + "corner_radius": 8 }, "header": { "family": "Zed Mono", @@ -1482,17 +1486,20 @@ }, "contact_button": { "background": "#293256", - "corner_radius": 9, - "padding": 4, "color": "#dfe2f1", - "width": 10 + "icon_width": 8, + "button_width": 16, + "corner_radius": 8, + "hover": { + "background": "#5e66873d" + } }, "disabled_contact_button": { "background": "#293256", - "corner_radius": 9, - "padding": 4, "color": "#979db4", - "width": 10 + "icon_width": 8, + "button_width": 16, + "corner_radius": 8 } }, "search": { diff --git a/assets/themes/sulphurpool-light.json b/assets/themes/sulphurpool-light.json index 5f8badbf9144e98ca94bfa4ef3af1dbcb088ca2c..0a528438708c9eb837c1e3b5c48d7797a5f2d396 100644 --- a/assets/themes/sulphurpool-light.json +++ b/assets/themes/sulphurpool-light.json @@ -1245,7 +1245,8 @@ "left": 6 }, "color": "#293256", - "width": 8 + "button_width": 8, + "icon_width": 8 }, "row_height": 28, "tree_branch_color": "#898ea4", @@ -1264,17 +1265,20 @@ }, "contact_button": { "background": "#dfe2f1", - "corner_radius": 8, - "padding": 4, "color": "#293256", - "width": 8 + "icon_width": 8, + "button_width": 16, + "corner_radius": 8, + "hover": { + "background": "#979db41f" + } }, "disabled_contact_button": { "background": "#dfe2f1", - "corner_radius": 8, - "padding": 4, "color": "#5e6687", - "width": 8 + "icon_width": 8, + "button_width": 16, + "corner_radius": 8 }, "header": { "family": "Zed Mono", @@ -1482,17 +1486,20 @@ }, "contact_button": { "background": "#dfe2f1", - "corner_radius": 9, - "padding": 4, "color": "#293256", - "width": 10 + "icon_width": 8, + "button_width": 16, + "corner_radius": 8, + "hover": { + "background": "#979db41f" + } }, "disabled_contact_button": { "background": "#dfe2f1", - "corner_radius": 9, - "padding": 4, "color": "#5e6687", - "width": 10 + "icon_width": 8, + "button_width": 16, + "corner_radius": 8 } }, "search": { diff --git a/crates/contacts_panel/src/contact_finder.rs b/crates/contacts_panel/src/contact_finder.rs index 6de61fdc3b398c86fc3b332fbb7772fb7db031a6..5a480911d4a5ae3f6f2855da23bffdba29bf7216 100644 --- a/crates/contacts_panel/src/contact_finder.rs +++ b/crates/contacts_panel/src/contact_finder.rs @@ -9,6 +9,8 @@ use std::sync::Arc; use util::TryFutureExt; use workspace::Workspace; +use crate::render_icon_button; + actions!(contact_finder, [Toggle]); pub fn init(cx: &mut MutableAppContext) { @@ -142,16 +144,7 @@ impl PickerDelegate for ContactFinder { .boxed(), ) .with_child( - Svg::new(icon_path) - .with_color(button_style.color) - .constrained() - .with_width(button_style.icon_width) - .aligned() - .constrained() - .with_width(button_style.button_width) - .with_height(button_style.button_width) - .contained() - .with_style(button_style.container) + render_icon_button(button_style, icon_path) .aligned() .flex_float() .boxed(), diff --git a/crates/contacts_panel/src/contacts_panel.rs b/crates/contacts_panel/src/contacts_panel.rs index c771ea7ffd7fa6e7b823aa162b4c2a9f76da4b8a..d33a2ed3ed516a37722c209b9604ea58fe67cbc4 100644 --- a/crates/contacts_panel/src/contacts_panel.rs +++ b/crates/contacts_panel/src/contacts_panel.rs @@ -14,6 +14,7 @@ use gpui::{ use serde::Deserialize; use settings::Settings; use std::sync::Arc; +use theme::IconButton; use workspace::{AppState, JoinProject}; impl_actions!( @@ -335,49 +336,38 @@ impl ContactsPanel { .boxed(), ); - let button_style = if user_store.read(cx).is_contact_request_pending(&user) { - &theme.disabled_contact_button - } else { - &theme.contact_button - }; + let is_contact_request_pending = user_store.read(cx).is_contact_request_pending(&user); row.add_children([ - MouseEventHandler::new::(user.id as usize, cx, |_, _| { - Svg::new("icons/reject.svg") - .with_color(button_style.color) - .constrained() - .with_width(button_style.icon_width) - .aligned() - .constrained() - .with_width(button_style.button_width) - .with_height(button_style.button_width) - .contained() - .with_style(button_style.container) + MouseEventHandler::new::(user.id as usize, cx, |mouse_state, _| { + let button_style = if is_contact_request_pending { + &theme.disabled_contact_button + } else { + &theme.contact_button.style_for(mouse_state, false) + }; + render_icon_button(button_style, "icons/reject.svg") .aligned() + .flex_float() .boxed() }) + .with_cursor_style(CursorStyle::PointingHand) .on_click(move |_, cx| { cx.dispatch_action(RespondToContactRequest { user_id, accept: false, }) }) - .with_cursor_style(CursorStyle::PointingHand) .flex_float() .boxed(), - MouseEventHandler::new::(user.id as usize, cx, |_, _| { - Svg::new("icons/accept.svg") - .with_color(button_style.color) - .constrained() - .with_width(button_style.icon_width) - .with_height(button_style.icon_width) - .aligned() - .constrained() - .with_width(button_style.button_width) - .with_height(button_style.button_width) - .contained() - .with_style(button_style.container) + MouseEventHandler::new::(user.id as usize, cx, |mouse_state, _| { + let button_style = if is_contact_request_pending { + &theme.disabled_contact_button + } else { + &theme.contact_button.style_for(mouse_state, false) + }; + render_icon_button(button_style, "icons/accept.svg") .aligned() + .flex_float() .boxed() }) .on_click(move |_, cx| { @@ -402,11 +392,7 @@ impl ContactsPanel { enum Cancel {} let user_id = user.id; - let button_style = if user_store.read(cx).is_contact_request_pending(&user) { - &theme.disabled_contact_button - } else { - &theme.contact_button - }; + let is_contact_request_pending = user_store.read(cx).is_contact_request_pending(&user); let mut row = Flex::row() .with_children(user.avatar.clone().map(|avatar| { @@ -429,19 +415,21 @@ impl ContactsPanel { ); row.add_child( - MouseEventHandler::new::(user.id as usize, cx, |_, _| { - Svg::new("icons/reject.svg") - .with_color(button_style.color) - .constrained() - .with_width(button_style.icon_width) - .with_height(button_style.icon_width) - .contained() - .with_style(button_style.container) + MouseEventHandler::new::(user.id as usize, cx, |mouse_state, _| { + let button_style = if is_contact_request_pending { + &theme.disabled_contact_button + } else { + &theme.contact_button.style_for(mouse_state, false) + }; + + render_icon_button(button_style, "icons/reject.svg") .aligned() + .flex_float() .boxed() }) - .on_click(move |_, cx| cx.dispatch_action(RemoveContact(user_id))) + .with_padding(Padding::uniform(2.)) .with_cursor_style(CursorStyle::PointingHand) + .on_click(move |_, cx| cx.dispatch_action(RemoveContact(user_id))) .flex_float() .boxed(), ); @@ -546,17 +534,21 @@ impl ContactsPanel { &Default::default(), executor.clone(), )); - if !matches.is_empty() { - let (online_contacts, offline_contacts) = matches - .iter() - .partition::, _>(|mat| contacts[mat.candidate_id].online); + let (online_contacts, offline_contacts) = matches + .iter() + .partition::, _>(|mat| contacts[mat.candidate_id].online); + + if !online_contacts.is_empty() { self.entries.push(ContactEntry::Header("Online")); self.entries.extend( online_contacts .into_iter() .map(|mat| ContactEntry::Contact(contacts[mat.candidate_id].clone())), ); + } + + if !offline_contacts.is_empty() { self.entries.push(ContactEntry::Header("Offline")); self.entries.extend( offline_contacts @@ -595,6 +587,19 @@ impl ContactsPanel { } } +fn render_icon_button(style: &IconButton, svg_path: &'static str) -> impl Element { + Svg::new(svg_path) + .with_color(style.color) + .constrained() + .with_width(style.icon_width) + .aligned() + .contained() + .with_style(style.container) + .constrained() + .with_width(style.button_width) + .with_height(style.button_width) +} + pub enum Event {} impl Entity for ContactsPanel { diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 427123e6294e283de3eaa7ef506ec7eed30f353d..7689205b2fdd7b1874d6bf47e70926981b1c90ac 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -241,7 +241,7 @@ pub struct ContactsPanel { pub row_height: f32, pub contact_avatar: ImageStyle, pub contact_username: ContainedText, - pub contact_button: IconButton, + pub contact_button: Interactive, pub disabled_contact_button: IconButton, pub tree_branch_width: f32, pub tree_branch_color: Color, diff --git a/styles/src/styleTree/contactFinder.ts b/styles/src/styleTree/contactFinder.ts index f79f55a237f9a22314a50e4ab086902b9afbd5ab..853f87ca5e654d8824309158b0e97360e1ebc1fc 100644 --- a/styles/src/styleTree/contactFinder.ts +++ b/styles/src/styleTree/contactFinder.ts @@ -23,7 +23,12 @@ export default function contactFinder(theme: Theme) { left: 8, }, }, - contactButton, + contactButton: { + ...contactButton, + hover: { + background: backgroundColor(theme, 100, "hovered") + } + }, disabledContactButton: { ...contactButton, background: backgroundColor(theme, 100), diff --git a/styles/src/styleTree/contactsPanel.ts b/styles/src/styleTree/contactsPanel.ts index 9aa9109964cb0aaf37eb6c4896da5ef410ebb823..6f7b96476041ac9470b5a4c303a87e7488d8fdec 100644 --- a/styles/src/styleTree/contactsPanel.ts +++ b/styles/src/styleTree/contactsPanel.ts @@ -58,7 +58,8 @@ export default function contactsPanel(theme: Theme) { addContactButton: { margin: { left: 6 }, color: iconColor(theme, "primary"), - width: 8, + buttonWidth: 8, + iconWidth: 8, }, rowHeight: 28, treeBranchColor: borderColor(theme, "muted"), @@ -73,7 +74,12 @@ export default function contactsPanel(theme: Theme) { left: 8, }, }, - contactButton, + contactButton: { + ...contactButton, + hover: { + background: backgroundColor(theme, 100, "hovered"), + }, + }, disabledContactButton: { ...contactButton, background: backgroundColor(theme, 100), From b33cbccc31cb3efa7321c939f1ab5dffc087b786 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 10 May 2022 11:14:31 -0700 Subject: [PATCH 47/53] Improve layout of contact panel rows Co-authored-by: Nathan Sobo --- assets/themes/cave-dark.json | 19 ++++++++++++++----- assets/themes/cave-light.json | 19 ++++++++++++++----- assets/themes/dark.json | 19 ++++++++++++++----- assets/themes/light.json | 19 ++++++++++++++----- assets/themes/solarized-dark.json | 19 ++++++++++++++----- assets/themes/solarized-light.json | 19 ++++++++++++++----- assets/themes/sulphurpool-dark.json | 19 ++++++++++++++----- assets/themes/sulphurpool-light.json | 19 ++++++++++++++----- crates/contacts_panel/src/contacts_panel.rs | 19 +++++++++++++++---- crates/theme/src/theme.rs | 2 ++ styles/src/styleTree/contactsPanel.ts | 14 ++++++++------ 11 files changed, 137 insertions(+), 50 deletions(-) diff --git a/assets/themes/cave-dark.json b/assets/themes/cave-dark.json index 55e255aa0b10755efb236653fea6c9058ba4dde1..cb1208a1db764bbb540dc73540eb723bfded2d91 100644 --- a/assets/themes/cave-dark.json +++ b/assets/themes/cave-dark.json @@ -1240,6 +1240,7 @@ "top": 4 } }, + "user_query_editor_height": 32, "add_contact_button": { "margin": { "left": 6 @@ -1248,7 +1249,20 @@ "button_width": 8, "icon_width": 8 }, + "row": { + "padding": { + "left": 8 + } + }, "row_height": 28, + "header": { + "family": "Zed Mono", + "color": "#8b8792", + "size": 14, + "margin": { + "top": 8 + } + }, "tree_branch_color": "#655f6d", "tree_branch_width": 1, "contact_avatar": { @@ -1280,11 +1294,6 @@ "button_width": 16, "corner_radius": 8 }, - "header": { - "family": "Zed Mono", - "color": "#8b8792", - "size": 14 - }, "project": { "guest_avatar_spacing": 4, "height": 24, diff --git a/assets/themes/cave-light.json b/assets/themes/cave-light.json index 626cc6f7541a6b77ef5121c45a02b40d6e2e8826..f0b3f5bd438a53178d9f56f88e8fbf1dbd41472d 100644 --- a/assets/themes/cave-light.json +++ b/assets/themes/cave-light.json @@ -1240,6 +1240,7 @@ "top": 4 } }, + "user_query_editor_height": 32, "add_contact_button": { "margin": { "left": 6 @@ -1248,7 +1249,20 @@ "button_width": 8, "icon_width": 8 }, + "row": { + "padding": { + "left": 8 + } + }, "row_height": 28, + "header": { + "family": "Zed Mono", + "color": "#585260", + "size": 14, + "margin": { + "top": 8 + } + }, "tree_branch_color": "#7e7887", "tree_branch_width": 1, "contact_avatar": { @@ -1280,11 +1294,6 @@ "button_width": 16, "corner_radius": 8 }, - "header": { - "family": "Zed Mono", - "color": "#585260", - "size": 14 - }, "project": { "guest_avatar_spacing": 4, "height": 24, diff --git a/assets/themes/dark.json b/assets/themes/dark.json index 877985f7dbd55a94dab3ffe40661222a5b2d87e2..9cc3badc8104dbee88d2862ad7feeedba22383e8 100644 --- a/assets/themes/dark.json +++ b/assets/themes/dark.json @@ -1240,6 +1240,7 @@ "top": 4 } }, + "user_query_editor_height": 32, "add_contact_button": { "margin": { "left": 6 @@ -1248,7 +1249,20 @@ "button_width": 8, "icon_width": 8 }, + "row": { + "padding": { + "left": 8 + } + }, "row_height": 28, + "header": { + "family": "Zed Mono", + "color": "#9c9c9c", + "size": 14, + "margin": { + "top": 8 + } + }, "tree_branch_color": "#404040", "tree_branch_width": 1, "contact_avatar": { @@ -1280,11 +1294,6 @@ "button_width": 16, "corner_radius": 8 }, - "header": { - "family": "Zed Mono", - "color": "#9c9c9c", - "size": 14 - }, "project": { "guest_avatar_spacing": 4, "height": 24, diff --git a/assets/themes/light.json b/assets/themes/light.json index 51a48642fe7385a04601122c10820d48bbfb2a92..e2563fadad64d74d0b7add301cbfb2fc0969be6d 100644 --- a/assets/themes/light.json +++ b/assets/themes/light.json @@ -1240,6 +1240,7 @@ "top": 4 } }, + "user_query_editor_height": 32, "add_contact_button": { "margin": { "left": 6 @@ -1248,7 +1249,20 @@ "button_width": 8, "icon_width": 8 }, + "row": { + "padding": { + "left": 8 + } + }, "row_height": 28, + "header": { + "family": "Zed Mono", + "color": "#474747", + "size": 14, + "margin": { + "top": 8 + } + }, "tree_branch_color": "#e3e3e3", "tree_branch_width": 1, "contact_avatar": { @@ -1280,11 +1294,6 @@ "button_width": 16, "corner_radius": 8 }, - "header": { - "family": "Zed Mono", - "color": "#474747", - "size": 14 - }, "project": { "guest_avatar_spacing": 4, "height": 24, diff --git a/assets/themes/solarized-dark.json b/assets/themes/solarized-dark.json index acf04db255ab2c6a60f655e79c83cef4ba94f2a8..6e8c405b6c212bf77fc99c99ea3c2a6dcf5a2f07 100644 --- a/assets/themes/solarized-dark.json +++ b/assets/themes/solarized-dark.json @@ -1240,6 +1240,7 @@ "top": 4 } }, + "user_query_editor_height": 32, "add_contact_button": { "margin": { "left": 6 @@ -1248,7 +1249,20 @@ "button_width": 8, "icon_width": 8 }, + "row": { + "padding": { + "left": 8 + } + }, "row_height": 28, + "header": { + "family": "Zed Mono", + "color": "#93a1a1", + "size": 14, + "margin": { + "top": 8 + } + }, "tree_branch_color": "#657b83", "tree_branch_width": 1, "contact_avatar": { @@ -1280,11 +1294,6 @@ "button_width": 16, "corner_radius": 8 }, - "header": { - "family": "Zed Mono", - "color": "#93a1a1", - "size": 14 - }, "project": { "guest_avatar_spacing": 4, "height": 24, diff --git a/assets/themes/solarized-light.json b/assets/themes/solarized-light.json index a85463a0d9cda9236ee3450e674d9f2dc4118958..3f5b26ee56a24a883fc21c45a9de60bec20b9a5d 100644 --- a/assets/themes/solarized-light.json +++ b/assets/themes/solarized-light.json @@ -1240,6 +1240,7 @@ "top": 4 } }, + "user_query_editor_height": 32, "add_contact_button": { "margin": { "left": 6 @@ -1248,7 +1249,20 @@ "button_width": 8, "icon_width": 8 }, + "row": { + "padding": { + "left": 8 + } + }, "row_height": 28, + "header": { + "family": "Zed Mono", + "color": "#586e75", + "size": 14, + "margin": { + "top": 8 + } + }, "tree_branch_color": "#839496", "tree_branch_width": 1, "contact_avatar": { @@ -1280,11 +1294,6 @@ "button_width": 16, "corner_radius": 8 }, - "header": { - "family": "Zed Mono", - "color": "#586e75", - "size": 14 - }, "project": { "guest_avatar_spacing": 4, "height": 24, diff --git a/assets/themes/sulphurpool-dark.json b/assets/themes/sulphurpool-dark.json index abf15762576f15aaa876775b527e5247df265cda..0f2a868f24d028be4c960db27b39b36b540a3e5d 100644 --- a/assets/themes/sulphurpool-dark.json +++ b/assets/themes/sulphurpool-dark.json @@ -1240,6 +1240,7 @@ "top": 4 } }, + "user_query_editor_height": 32, "add_contact_button": { "margin": { "left": 6 @@ -1248,7 +1249,20 @@ "button_width": 8, "icon_width": 8 }, + "row": { + "padding": { + "left": 8 + } + }, "row_height": 28, + "header": { + "family": "Zed Mono", + "color": "#979db4", + "size": 14, + "margin": { + "top": 8 + } + }, "tree_branch_color": "#6b7394", "tree_branch_width": 1, "contact_avatar": { @@ -1280,11 +1294,6 @@ "button_width": 16, "corner_radius": 8 }, - "header": { - "family": "Zed Mono", - "color": "#979db4", - "size": 14 - }, "project": { "guest_avatar_spacing": 4, "height": 24, diff --git a/assets/themes/sulphurpool-light.json b/assets/themes/sulphurpool-light.json index 0a528438708c9eb837c1e3b5c48d7797a5f2d396..b9106c62f3d273a537616f0cbd38090b8c854411 100644 --- a/assets/themes/sulphurpool-light.json +++ b/assets/themes/sulphurpool-light.json @@ -1240,6 +1240,7 @@ "top": 4 } }, + "user_query_editor_height": 32, "add_contact_button": { "margin": { "left": 6 @@ -1248,7 +1249,20 @@ "button_width": 8, "icon_width": 8 }, + "row": { + "padding": { + "left": 8 + } + }, "row_height": 28, + "header": { + "family": "Zed Mono", + "color": "#5e6687", + "size": 14, + "margin": { + "top": 8 + } + }, "tree_branch_color": "#898ea4", "tree_branch_width": 1, "contact_avatar": { @@ -1280,11 +1294,6 @@ "button_width": 16, "corner_radius": 8 }, - "header": { - "family": "Zed Mono", - "color": "#5e6687", - "size": 14 - }, "project": { "guest_avatar_spacing": 4, "height": 24, diff --git a/crates/contacts_panel/src/contacts_panel.rs b/crates/contacts_panel/src/contacts_panel.rs index d33a2ed3ed516a37722c209b9604ea58fe67cbc4..9150c966e5bf1513121ca04e1e3fbc90ca393c17 100644 --- a/crates/contacts_panel/src/contacts_panel.rs +++ b/crates/contacts_panel/src/contacts_panel.rs @@ -90,11 +90,12 @@ impl ContactsPanel { ContactEntry::Header(text) => { Label::new(text.to_string(), theme.header.text.clone()) .contained() - .with_style(theme.header.container) .aligned() .left() .constrained() .with_height(theme.row_height) + .contained() + .with_style(theme.header.container) .boxed() } ContactEntry::IncomingRequest(user) => { @@ -302,6 +303,8 @@ impl ContactsPanel { .boxed() }), ) + .contained() + .with_style(theme.row.clone()) .boxed() } @@ -380,7 +383,11 @@ impl ContactsPanel { .boxed(), ]); - row.constrained().with_height(theme.row_height).boxed() + row.constrained() + .with_height(theme.row_height) + .contained() + .with_style(theme.row.clone()) + .boxed() } fn render_outgoing_contact_request( @@ -434,7 +441,11 @@ impl ContactsPanel { .boxed(), ); - row.constrained().with_height(theme.row_height).boxed() + row.constrained() + .with_height(theme.row_height) + .contained() + .with_style(theme.row) + .boxed() } fn update_entries(&mut self, cx: &mut ViewContext) { @@ -643,7 +654,7 @@ impl View for ContactsPanel { .boxed(), ) .constrained() - .with_height(32.) + .with_height(theme.user_query_editor_height) .boxed(), ) .with_child(List::new(self.list_state.clone()).flex(1., false).boxed()) diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 7689205b2fdd7b1874d6bf47e70926981b1c90ac..72db11c4931436ad6bf90650561d3baa5fe1eef1 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -237,7 +237,9 @@ pub struct ContactsPanel { pub container: ContainerStyle, pub header: ContainedText, pub user_query_editor: FieldEditor, + pub user_query_editor_height: f32, pub add_contact_button: IconButton, + pub row: ContainerStyle, pub row_height: f32, pub contact_avatar: ImageStyle, pub contact_username: ContainedText, diff --git a/styles/src/styleTree/contactsPanel.ts b/styles/src/styleTree/contactsPanel.ts index 6f7b96476041ac9470b5a4c303a87e7488d8fdec..3cc0f35c3eb176911699741591042d4f34177293 100644 --- a/styles/src/styleTree/contactsPanel.ts +++ b/styles/src/styleTree/contactsPanel.ts @@ -55,13 +55,21 @@ export default function contactsPanel(theme: Theme) { top: 4, }, }, + userQueryEditorHeight: 32, addContactButton: { margin: { left: 6 }, color: iconColor(theme, "primary"), buttonWidth: 8, iconWidth: 8, }, + row: { + padding: { left: 8 }, + }, rowHeight: 28, + header: { + ...text(theme, "mono", "secondary", { size: "sm" }), + margin: { top: 8 }, + }, treeBranchColor: borderColor(theme, "muted"), treeBranchWidth: 1, contactAvatar: { @@ -85,12 +93,6 @@ export default function contactsPanel(theme: Theme) { background: backgroundColor(theme, 100), color: iconColor(theme, "muted"), }, - header: { - ...text(theme, "mono", "secondary", { size: "sm" }), - // padding: { - // left: 8, - // } - }, project, sharedProject, hoveredSharedProject: { From 14ec3c86e5010173779c55a341113cbad510f5fe Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 10 May 2022 11:18:30 -0700 Subject: [PATCH 48/53] Clear contact panel filter editor on escape Co-authored-by: Nathan Sobo --- crates/contacts_panel/src/contacts_panel.rs | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/crates/contacts_panel/src/contacts_panel.rs b/crates/contacts_panel/src/contacts_panel.rs index 9150c966e5bf1513121ca04e1e3fbc90ca393c17..f2e3c6414388afc752ec142b6be147fbbe231ae6 100644 --- a/crates/contacts_panel/src/contacts_panel.rs +++ b/crates/contacts_panel/src/contacts_panel.rs @@ -1,7 +1,7 @@ mod contact_finder; use client::{Contact, User, UserStore}; -use editor::Editor; +use editor::{Cancel, Editor}; use fuzzy::{match_strings, StringMatchCandidate}; use gpui::{ elements::*, @@ -35,7 +35,7 @@ pub struct ContactsPanel { match_candidates: Vec, list_state: ListState, user_store: ModelHandle, - user_query_editor: ViewHandle, + filter_editor: ViewHandle, _maintain_contacts: Subscription, } @@ -56,6 +56,7 @@ pub fn init(cx: &mut MutableAppContext) { cx.add_action(ContactsPanel::request_contact); cx.add_action(ContactsPanel::remove_contact); cx.add_action(ContactsPanel::respond_to_contact_request); + cx.add_action(ContactsPanel::clear_filter); } impl ContactsPanel { @@ -126,7 +127,7 @@ impl ContactsPanel { }), entries: Default::default(), match_candidates: Default::default(), - user_query_editor, + filter_editor: user_query_editor, _maintain_contacts: cx .observe(&app_state.user_store, |this, _, cx| this.update_entries(cx)), user_store: app_state.user_store.clone(), @@ -450,7 +451,7 @@ impl ContactsPanel { fn update_entries(&mut self, cx: &mut ViewContext) { let user_store = self.user_store.read(cx); - let query = self.user_query_editor.read(cx).text(cx); + let query = self.filter_editor.read(cx).text(cx); let executor = cx.background().clone(); self.entries.clear(); @@ -596,6 +597,11 @@ impl ContactsPanel { }) .detach(); } + + fn clear_filter(&mut self, _: &Cancel, cx: &mut ViewContext) { + self.filter_editor + .update(cx, |editor, cx| editor.set_text("", cx)); + } } fn render_icon_button(style: &IconButton, svg_path: &'static str) -> impl Element { @@ -632,7 +638,7 @@ impl View for ContactsPanel { .with_child( Flex::row() .with_child( - ChildView::new(self.user_query_editor.clone()) + ChildView::new(self.filter_editor.clone()) .contained() .with_style(theme.user_query_editor.container) .flex(1., true) From de9a7b1927b2ac78d9b53228c097fa6670e1ac4d Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 10 May 2022 11:25:12 -0700 Subject: [PATCH 49/53] Give the contact panel's filter editor some placeholder text Co-authored-by: Nathan Sobo --- crates/contacts_panel/src/contacts_panel.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/contacts_panel/src/contacts_panel.rs b/crates/contacts_panel/src/contacts_panel.rs index f2e3c6414388afc752ec142b6be147fbbe231ae6..499d4d879c6bcb2ce74b6d09cbc14c9414ca6f5c 100644 --- a/crates/contacts_panel/src/contacts_panel.rs +++ b/crates/contacts_panel/src/contacts_panel.rs @@ -62,10 +62,12 @@ pub fn init(cx: &mut MutableAppContext) { impl ContactsPanel { pub fn new(app_state: Arc, cx: &mut ViewContext) -> Self { let user_query_editor = cx.add_view(|cx| { - Editor::single_line( + let mut editor = Editor::single_line( Some(|theme| theme.contacts_panel.user_query_editor.clone()), cx, - ) + ); + editor.set_placeholder_text("Filter contacts", cx); + editor }); cx.subscribe(&user_query_editor, |this, _, event, cx| { From dc465839e1d300a0aeeecaf31a64d1f1177738b8 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 10 May 2022 11:25:51 -0700 Subject: [PATCH 50/53] Round sidebar panels' widths to whole numbers of pixels Co-authored-by: Nathan Sobo --- crates/workspace/src/sidebar.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/crates/workspace/src/sidebar.rs b/crates/workspace/src/sidebar.rs index bc7314e73286b48ed7f2ca95bb56cd7e93f71de8..c9cbcbb4fb073367c04ca63b9d951310a0ecaed4 100644 --- a/crates/workspace/src/sidebar.rs +++ b/crates/workspace/src/sidebar.rs @@ -106,10 +106,12 @@ impl Sidebar { .with_cursor_style(CursorStyle::ResizeLeftRight) .on_drag(move |delta, cx| { let prev_width = *actual_width.borrow(); - match side { - Side::Left => *custom_width.borrow_mut() = 0f32.max(prev_width + delta.x()), - Side::Right => *custom_width.borrow_mut() = 0f32.max(prev_width - delta.x()), - } + *custom_width.borrow_mut() = 0f32 + .max(match side { + Side::Left => prev_width + delta.x(), + Side::Right => prev_width - delta.x(), + }) + .round(); cx.notify(); }) From 334f246df357a2de6b05f99f87e3e6feaf94753f Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 10 May 2022 14:05:07 -0700 Subject: [PATCH 51/53] Include every user in their own list of contacts --- crates/collab/src/db.rs | 34 +++-- crates/collab/src/rpc.rs | 276 +++++++++++++++++---------------------- 2 files changed, 141 insertions(+), 169 deletions(-) diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index cd7cbb981fc5a21ce076ae747278e344deff2046..4bb61c34046df885acb4a97960f0392da275f093 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -201,7 +201,7 @@ impl Db for PostgresDb { .bind(user_id) .fetch(&self.pool); - let mut current = Vec::new(); + let mut current = vec![user_id]; let mut outgoing_requests = Vec::new(); let mut incoming_requests = Vec::new(); while let Some(row) = rows.next().await { @@ -232,6 +232,10 @@ impl Db for PostgresDb { } } + current.sort_unstable(); + outgoing_requests.sort_unstable(); + incoming_requests.sort_unstable(); + Ok(Contacts { current, outgoing_requests, @@ -704,7 +708,7 @@ pub struct Contacts { pub outgoing_requests: Vec, } -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] pub struct IncomingContactRequest { pub requester_id: UserId, pub should_notify: bool, @@ -944,7 +948,7 @@ pub mod tests { assert_eq!( db.get_contacts(user_1).await.unwrap(), Contacts { - current: vec![], + current: vec![user_1], outgoing_requests: vec![], incoming_requests: vec![], }, @@ -955,7 +959,7 @@ pub mod tests { assert_eq!( db.get_contacts(user_1).await.unwrap(), Contacts { - current: vec![], + current: vec![user_1], outgoing_requests: vec![user_2], incoming_requests: vec![], }, @@ -963,7 +967,7 @@ pub mod tests { assert_eq!( db.get_contacts(user_2).await.unwrap(), Contacts { - current: vec![], + current: vec![user_2], outgoing_requests: vec![], incoming_requests: vec![IncomingContactRequest { requester_id: user_1, @@ -981,7 +985,7 @@ pub mod tests { assert_eq!( db.get_contacts(user_2).await.unwrap(), Contacts { - current: vec![], + current: vec![user_2], outgoing_requests: vec![], incoming_requests: vec![IncomingContactRequest { requester_id: user_1, @@ -1002,7 +1006,7 @@ pub mod tests { assert_eq!( db.get_contacts(user_1).await.unwrap(), Contacts { - current: vec![user_2], + current: vec![user_1, user_2], outgoing_requests: vec![], incoming_requests: vec![], }, @@ -1010,7 +1014,7 @@ pub mod tests { assert_eq!( db.get_contacts(user_2).await.unwrap(), Contacts { - current: vec![user_1], + current: vec![user_1, user_2], outgoing_requests: vec![], incoming_requests: vec![], }, @@ -1027,7 +1031,7 @@ pub mod tests { assert_eq!( db.get_contacts(user_1).await.unwrap(), Contacts { - current: vec![user_2, user_3], + current: vec![user_1, user_2, user_3], outgoing_requests: vec![], incoming_requests: vec![], }, @@ -1035,7 +1039,7 @@ pub mod tests { assert_eq!( db.get_contacts(user_3).await.unwrap(), Contacts { - current: vec![user_1], + current: vec![user_1, user_3], outgoing_requests: vec![], incoming_requests: vec![], }, @@ -1049,7 +1053,7 @@ pub mod tests { assert_eq!( db.get_contacts(user_2).await.unwrap(), Contacts { - current: vec![user_1], + current: vec![user_1, user_2], outgoing_requests: vec![], incoming_requests: vec![], }, @@ -1057,7 +1061,7 @@ pub mod tests { assert_eq!( db.get_contacts(user_3).await.unwrap(), Contacts { - current: vec![user_1], + current: vec![user_1, user_3], outgoing_requests: vec![], incoming_requests: vec![], }, @@ -1217,7 +1221,7 @@ pub mod tests { async fn get_contacts(&self, id: UserId) -> Result { self.background.simulate_random_delay().await; - let mut current = Vec::new(); + let mut current = vec![id]; let mut outgoing_requests = Vec::new(); let mut incoming_requests = Vec::new(); @@ -1240,6 +1244,10 @@ pub mod tests { } } + current.sort_unstable(); + outgoing_requests.sort_unstable(); + incoming_requests.sort_unstable(); + Ok(Contacts { current, outgoing_requests, diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 7e881f5d56a974a2e942e12d0b11227feb04a16f..33d1d526775f841e98e24e154d1ae29dcf54292e 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -376,11 +376,14 @@ impl Server { request: TypedEnvelope, response: Response, ) -> Result<()> { - let project_id = { + let user_id; + let project_id; + { let mut state = self.store_mut().await; - let user_id = state.user_id_for_connection(request.sender_id)?; - state.register_project(request.sender_id, user_id) + user_id = state.user_id_for_connection(request.sender_id)?; + project_id = state.register_project(request.sender_id, user_id); }; + self.update_user_contacts(user_id).await?; response.send(proto::RegisterProjectResponse { project_id })?; Ok(()) } @@ -5007,25 +5010,20 @@ mod tests { (&client_c, cx_c), ]) .await; + deterministic.run_until_parked(); - client_a.user_store.read_with(cx_a, |store, _| { - assert_eq!( - contacts(store), - [("user_b", true, vec![]), ("user_c", true, vec![])] - ) - }); - client_b.user_store.read_with(cx_b, |store, _| { - assert_eq!( - contacts(store), - [("user_a", true, vec![]), ("user_c", true, vec![])] - ) - }); - client_c.user_store.read_with(cx_c, |store, _| { - assert_eq!( - contacts(store), - [("user_a", true, vec![]), ("user_b", true, vec![])] - ) - }); + for (client, cx) in [(&client_a, &cx_a), (&client_b, &cx_b), (&client_c, &cx_c)] { + client.user_store.read_with(*cx, |store, _| { + assert_eq!( + contacts(store), + [ + ("user_a", true, vec![]), + ("user_b", true, vec![]), + ("user_c", true, vec![]) + ] + ) + }); + } // Share a worktree as client A. fs.create_dir(Path::new("/a")).await.unwrap(); @@ -5050,30 +5048,18 @@ mod tests { .await; deterministic.run_until_parked(); - client_a.user_store.read_with(cx_a, |store, _| { - assert_eq!( - contacts(store), - [("user_b", true, vec![]), ("user_c", true, vec![])] - ) - }); - client_b.user_store.read_with(cx_b, |store, _| { - assert_eq!( - contacts(store), - [ - ("user_a", true, vec![("a", false, vec![])]), - ("user_c", true, vec![]) - ] - ) - }); - client_c.user_store.read_with(cx_c, |store, _| { - assert_eq!( - contacts(store), - [ - ("user_a", true, vec![("a", false, vec![])]), - ("user_b", true, vec![]) - ] - ) - }); + for (client, cx) in [(&client_a, &cx_a), (&client_b, &cx_b), (&client_c, &cx_c)] { + client.user_store.read_with(*cx, |store, _| { + assert_eq!( + contacts(store), + [ + ("user_a", true, vec![("a", false, vec![])]), + ("user_b", true, vec![]), + ("user_c", true, vec![]) + ] + ) + }); + } let project_id = project_a .update(cx_a, |project, _| project.next_remote_id()) @@ -5082,31 +5068,20 @@ mod tests { .update(cx_a, |project, cx| project.share(cx)) .await .unwrap(); + deterministic.run_until_parked(); - client_a.user_store.read_with(cx_a, |store, _| { - assert_eq!( - contacts(store), - [("user_b", true, vec![]), ("user_c", true, vec![])] - ) - }); - client_b.user_store.read_with(cx_b, |store, _| { - assert_eq!( - contacts(store), - [ - ("user_a", true, vec![("a", true, vec![])]), - ("user_c", true, vec![]) - ] - ) - }); - client_c.user_store.read_with(cx_c, |store, _| { - assert_eq!( - contacts(store), - [ - ("user_a", true, vec![("a", true, vec![])]), - ("user_b", true, vec![]) - ] - ) - }); + for (client, cx) in [(&client_a, &cx_a), (&client_b, &cx_b), (&client_c, &cx_c)] { + client.user_store.read_with(*cx, |store, _| { + assert_eq!( + contacts(store), + [ + ("user_a", true, vec![("a", true, vec![])]), + ("user_b", true, vec![]), + ("user_c", true, vec![]) + ] + ) + }); + } let _project_b = Project::remote( project_id, @@ -5118,31 +5093,20 @@ mod tests { ) .await .unwrap(); + deterministic.run_until_parked(); - client_a.user_store.read_with(cx_a, |store, _| { - assert_eq!( - contacts(store), - [("user_b", true, vec![]), ("user_c", true, vec![])] - ) - }); - client_b.user_store.read_with(cx_b, |store, _| { - assert_eq!( - contacts(store), - [ - ("user_a", true, vec![("a", true, vec!["user_b"])]), - ("user_c", true, vec![]) - ] - ) - }); - client_c.user_store.read_with(cx_c, |store, _| { - assert_eq!( - contacts(store), - [ - ("user_a", true, vec![("a", true, vec!["user_b"])]), - ("user_b", true, vec![]) - ] - ) - }); + for (client, cx) in [(&client_a, &cx_a), (&client_b, &cx_b), (&client_c, &cx_c)] { + client.user_store.read_with(*cx, |store, _| { + assert_eq!( + contacts(store), + [ + ("user_a", true, vec![("a", true, vec!["user_b"])]), + ("user_b", true, vec![]), + ("user_c", true, vec![]) + ] + ) + }); + } project_a .condition(&cx_a, |project, _| { @@ -5152,41 +5116,34 @@ mod tests { cx_a.update(move |_| drop(project_a)); deterministic.run_until_parked(); - client_a.user_store.read_with(cx_a, |store, _| { - assert_eq!( - contacts(store), - [("user_b", true, vec![]), ("user_c", true, vec![])] - ) - }); - client_b.user_store.read_with(cx_b, |store, _| { - assert_eq!( - contacts(store), - [("user_a", true, vec![]), ("user_c", true, vec![])] - ) - }); - client_c.user_store.read_with(cx_c, |store, _| { - assert_eq!( - contacts(store), - [("user_a", true, vec![]), ("user_b", true, vec![])] - ) - }); + for (client, cx) in [(&client_a, &cx_a), (&client_b, &cx_b), (&client_c, &cx_c)] { + client.user_store.read_with(*cx, |store, _| { + assert_eq!( + contacts(store), + [ + ("user_a", true, vec![]), + ("user_b", true, vec![]), + ("user_c", true, vec![]) + ] + ) + }); + } server.disconnect_client(client_c.current_user_id(cx_c)); server.forbid_connections(); deterministic.advance_clock(rpc::RECEIVE_TIMEOUT); - - client_a.user_store.read_with(cx_a, |store, _| { - assert_eq!( - contacts(store), - [("user_b", true, vec![]), ("user_c", false, vec![])] - ) - }); - client_b.user_store.read_with(cx_b, |store, _| { - assert_eq!( - contacts(store), - [("user_a", true, vec![]), ("user_c", false, vec![])] - ) - }); + for (client, cx) in [(&client_a, &cx_a), (&client_b, &cx_b)] { + client.user_store.read_with(*cx, |store, _| { + assert_eq!( + contacts(store), + [ + ("user_a", true, vec![]), + ("user_b", true, vec![]), + ("user_c", false, vec![]) + ] + ) + }); + } client_c .user_store .read_with(cx_c, |store, _| assert_eq!(contacts(store), [])); @@ -5196,25 +5153,20 @@ mod tests { .authenticate_and_connect(false, &cx_c.to_async()) .await .unwrap(); + deterministic.run_until_parked(); - client_a.user_store.read_with(cx_a, |store, _| { - assert_eq!( - contacts(store), - [("user_b", true, vec![]), ("user_c", true, vec![])] - ) - }); - client_b.user_store.read_with(cx_b, |store, _| { - assert_eq!( - contacts(store), - [("user_a", true, vec![]), ("user_c", true, vec![])] - ) - }); - client_c.user_store.read_with(cx_c, |store, _| { - assert_eq!( - contacts(store), - [("user_a", true, vec![]), ("user_b", true, vec![])] - ) - }); + for (client, cx) in [(&client_a, &cx_a), (&client_b, &cx_b), (&client_c, &cx_c)] { + client.user_store.read_with(*cx, |store, _| { + assert_eq!( + contacts(store), + [ + ("user_a", true, vec![]), + ("user_b", true, vec![]), + ("user_c", true, vec![]) + ] + ) + }); + } fn contacts(user_store: &UserStore) -> Vec<(&str, bool, Vec<(&str, bool, Vec<&str>)>)> { user_store @@ -5341,18 +5293,18 @@ mod tests { // User B sees user A as their contact now in all client, and the incoming request from them is removed. let contacts_b = client_b.summarize_contacts(&cx_b); - assert_eq!(contacts_b.current, &["user_a"]); + assert_eq!(contacts_b.current, &["user_a", "user_b"]); assert_eq!(contacts_b.incoming_requests, &["user_c"]); let contacts_b2 = client_b2.summarize_contacts(&cx_b2); - assert_eq!(contacts_b2.current, &["user_a"]); + assert_eq!(contacts_b2.current, &["user_a", "user_b"]); assert_eq!(contacts_b2.incoming_requests, &["user_c"]); // User A sees user B as their contact now in all clients, and the outgoing request to them is removed. let contacts_a = client_a.summarize_contacts(&cx_a); - assert_eq!(contacts_a.current, &["user_b"]); + assert_eq!(contacts_a.current, &["user_a", "user_b"]); assert!(contacts_a.outgoing_requests.is_empty()); let contacts_a2 = client_a2.summarize_contacts(&cx_a2); - assert_eq!(contacts_a2.current, &["user_b"]); + assert_eq!(contacts_a2.current, &["user_a", "user_b"]); assert!(contacts_a2.outgoing_requests.is_empty()); // Contacts are present upon connecting (tested here via disconnect/reconnect) @@ -5360,13 +5312,19 @@ mod tests { disconnect_and_reconnect(&client_b, cx_b).await; disconnect_and_reconnect(&client_c, cx_c).await; executor.run_until_parked(); - assert_eq!(client_a.summarize_contacts(&cx_a).current, &["user_b"]); - assert_eq!(client_b.summarize_contacts(&cx_b).current, &["user_a"]); + assert_eq!( + client_a.summarize_contacts(&cx_a).current, + &["user_a", "user_b"] + ); + assert_eq!( + client_b.summarize_contacts(&cx_b).current, + &["user_a", "user_b"] + ); assert_eq!( client_b.summarize_contacts(&cx_b).incoming_requests, &["user_c"] ); - assert!(client_c.summarize_contacts(&cx_c).current.is_empty()); + assert_eq!(client_c.summarize_contacts(&cx_c).current, &["user_c"]); assert_eq!( client_c.summarize_contacts(&cx_c).outgoing_requests, &["user_b"] @@ -5385,18 +5343,18 @@ mod tests { // User B doesn't see user C as their contact, and the incoming request from them is removed. let contacts_b = client_b.summarize_contacts(&cx_b); - assert_eq!(contacts_b.current, &["user_a"]); + assert_eq!(contacts_b.current, &["user_a", "user_b"]); assert!(contacts_b.incoming_requests.is_empty()); let contacts_b2 = client_b2.summarize_contacts(&cx_b2); - assert_eq!(contacts_b2.current, &["user_a"]); + assert_eq!(contacts_b2.current, &["user_a", "user_b"]); assert!(contacts_b2.incoming_requests.is_empty()); // User C doesn't see user B as their contact, and the outgoing request to them is removed. let contacts_c = client_c.summarize_contacts(&cx_c); - assert!(contacts_c.current.is_empty()); + assert_eq!(contacts_c.current, &["user_c"]); assert!(contacts_c.outgoing_requests.is_empty()); let contacts_c2 = client_c2.summarize_contacts(&cx_c2); - assert!(contacts_c2.current.is_empty()); + assert_eq!(contacts_c2.current, &["user_c"]); assert!(contacts_c2.outgoing_requests.is_empty()); // Incoming/outgoing requests are not present upon connecting (tested here via disconnect/reconnect) @@ -5404,13 +5362,19 @@ mod tests { disconnect_and_reconnect(&client_b, cx_b).await; disconnect_and_reconnect(&client_c, cx_c).await; executor.run_until_parked(); - assert_eq!(client_a.summarize_contacts(&cx_a).current, &["user_b"]); - assert_eq!(client_b.summarize_contacts(&cx_b).current, &["user_a"]); + assert_eq!( + client_a.summarize_contacts(&cx_a).current, + &["user_a", "user_b"] + ); + assert_eq!( + client_b.summarize_contacts(&cx_b).current, + &["user_a", "user_b"] + ); assert!(client_b .summarize_contacts(&cx_b) .incoming_requests .is_empty()); - assert!(client_c.summarize_contacts(&cx_c).current.is_empty()); + assert_eq!(client_c.summarize_contacts(&cx_c).current, &["user_c"]); assert!(client_c .summarize_contacts(&cx_c) .outgoing_requests From 834c485300619362b90ef204e92c8697b31eddcf Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 10 May 2022 14:46:42 -0700 Subject: [PATCH 52/53] Don't use pointing hand cursor for the user's own projects --- crates/contacts_panel/src/contacts_panel.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/contacts_panel/src/contacts_panel.rs b/crates/contacts_panel/src/contacts_panel.rs index 499d4d879c6bcb2ce74b6d09cbc14c9414ca6f5c..d5ff31f27d690c9e53b207ce77d02a5fbe692d12 100644 --- a/crates/contacts_panel/src/contacts_panel.rs +++ b/crates/contacts_panel/src/contacts_panel.rs @@ -285,7 +285,7 @@ impl ContactsPanel { .boxed() }, ) - .with_cursor_style(if is_host || is_shared { + .with_cursor_style(if !is_host && is_shared { CursorStyle::PointingHand } else { CursorStyle::Arrow From b1a75805cc158fe0ddb5fb8667d01e10f58a4ff1 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 10 May 2022 14:52:13 -0700 Subject: [PATCH 53/53] Consolidate logic for rendering contact requests --- crates/contacts_panel/src/contacts_panel.rs | 190 ++++++++------------ 1 file changed, 76 insertions(+), 114 deletions(-) diff --git a/crates/contacts_panel/src/contacts_panel.rs b/crates/contacts_panel/src/contacts_panel.rs index d5ff31f27d690c9e53b207ce77d02a5fbe692d12..5d96a1b0c20f351c659e83256eb30610cde9ba10 100644 --- a/crates/contacts_panel/src/contacts_panel.rs +++ b/crates/contacts_panel/src/contacts_panel.rs @@ -101,22 +101,20 @@ impl ContactsPanel { .with_style(theme.header.container) .boxed() } - ContactEntry::IncomingRequest(user) => { - Self::render_incoming_contact_request( - user.clone(), - this.user_store.clone(), - theme, - cx, - ) - } - ContactEntry::OutgoingRequest(user) => { - Self::render_outgoing_contact_request( - user.clone(), - this.user_store.clone(), - theme, - cx, - ) - } + ContactEntry::IncomingRequest(user) => Self::render_contact_request( + user.clone(), + this.user_store.clone(), + theme, + true, + cx, + ), + ContactEntry::OutgoingRequest(user) => Self::render_contact_request( + user.clone(), + this.user_store.clone(), + theme, + false, + cx, + ), ContactEntry::Contact(contact) => Self::render_contact( contact.clone(), current_user_id, @@ -311,16 +309,16 @@ impl ContactsPanel { .boxed() } - fn render_incoming_contact_request( + fn render_contact_request( user: Arc, user_store: ModelHandle, theme: &theme::ContactsPanel, + is_incoming: bool, cx: &mut LayoutContext, ) -> ElementBox { enum Reject {} enum Accept {} - - let user_id = user.id; + enum Cancel {} let mut row = Flex::row() .with_children(user.avatar.clone().map(|avatar| { @@ -342,107 +340,71 @@ impl ContactsPanel { .boxed(), ); + let user_id = user.id; let is_contact_request_pending = user_store.read(cx).is_contact_request_pending(&user); - row.add_children([ - MouseEventHandler::new::(user.id as usize, cx, |mouse_state, _| { - let button_style = if is_contact_request_pending { - &theme.disabled_contact_button - } else { - &theme.contact_button.style_for(mouse_state, false) - }; - render_icon_button(button_style, "icons/reject.svg") - .aligned() - .flex_float() - .boxed() - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(move |_, cx| { - cx.dispatch_action(RespondToContactRequest { - user_id, - accept: false, + if is_incoming { + row.add_children([ + MouseEventHandler::new::(user.id as usize, cx, |mouse_state, _| { + let button_style = if is_contact_request_pending { + &theme.disabled_contact_button + } else { + &theme.contact_button.style_for(mouse_state, false) + }; + render_icon_button(button_style, "icons/reject.svg") + .aligned() + .flex_float() + .boxed() }) - }) - .flex_float() - .boxed(), - MouseEventHandler::new::(user.id as usize, cx, |mouse_state, _| { - let button_style = if is_contact_request_pending { - &theme.disabled_contact_button - } else { - &theme.contact_button.style_for(mouse_state, false) - }; - render_icon_button(button_style, "icons/accept.svg") - .aligned() - .flex_float() - .boxed() - }) - .on_click(move |_, cx| { - cx.dispatch_action(RespondToContactRequest { - user_id, - accept: true, + .with_cursor_style(CursorStyle::PointingHand) + .on_click(move |_, cx| { + cx.dispatch_action(RespondToContactRequest { + user_id, + accept: false, + }) }) - }) - .with_cursor_style(CursorStyle::PointingHand) - .boxed(), - ]); - - row.constrained() - .with_height(theme.row_height) - .contained() - .with_style(theme.row.clone()) - .boxed() - } - - fn render_outgoing_contact_request( - user: Arc, - user_store: ModelHandle, - theme: &theme::ContactsPanel, - cx: &mut LayoutContext, - ) -> ElementBox { - enum Cancel {} - - let user_id = user.id; - let is_contact_request_pending = user_store.read(cx).is_contact_request_pending(&user); - - let mut row = Flex::row() - .with_children(user.avatar.clone().map(|avatar| { - Image::new(avatar) - .with_style(theme.contact_avatar) - .aligned() - .left() - .boxed() - })) - .with_child( - Label::new( - user.github_login.clone(), - theme.contact_username.text.clone(), - ) - .contained() - .with_style(theme.contact_username.container) - .aligned() - .left() + .flex_float() + .boxed(), + MouseEventHandler::new::(user.id as usize, cx, |mouse_state, _| { + let button_style = if is_contact_request_pending { + &theme.disabled_contact_button + } else { + &theme.contact_button.style_for(mouse_state, false) + }; + render_icon_button(button_style, "icons/accept.svg") + .aligned() + .flex_float() + .boxed() + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(move |_, cx| { + cx.dispatch_action(RespondToContactRequest { + user_id, + accept: true, + }) + }) + .boxed(), + ]); + } else { + row.add_child( + MouseEventHandler::new::(user.id as usize, cx, |mouse_state, _| { + let button_style = if is_contact_request_pending { + &theme.disabled_contact_button + } else { + &theme.contact_button.style_for(mouse_state, false) + }; + render_icon_button(button_style, "icons/reject.svg") + .aligned() + .flex_float() + .boxed() + }) + .with_padding(Padding::uniform(2.)) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(move |_, cx| cx.dispatch_action(RemoveContact(user_id))) + .flex_float() .boxed(), ); - - row.add_child( - MouseEventHandler::new::(user.id as usize, cx, |mouse_state, _| { - let button_style = if is_contact_request_pending { - &theme.disabled_contact_button - } else { - &theme.contact_button.style_for(mouse_state, false) - }; - - render_icon_button(button_style, "icons/reject.svg") - .aligned() - .flex_float() - .boxed() - }) - .with_padding(Padding::uniform(2.)) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(move |_, cx| cx.dispatch_action(RemoveContact(user_id))) - .flex_float() - .boxed(), - ); + } row.constrained() .with_height(theme.row_height)