diff --git a/Cargo.lock b/Cargo.lock index 0277e1b476..1e85073d50 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 28d0e771ad..a2e224ed02 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 505888f2b3..6ec03294ec 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 060b8a1278..db72fd965c 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 f15f232b92..f8754b7ede 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 f4e323edf9..c6c9951779 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 84eb135c9a..677bba0070 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 63da853e8b..b603dccb8d 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 cc6bc705a0..3f4983c083 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 51d4007ff7..0d44c67191 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 c8432d92d1..a28acdc63c 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 827ac564f8..46ca033ee2 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 8e843b0f39..24ee22ed21 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 087f169da2..d9460f265e 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 1fcd89fcde..b54c3677fa 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 fa0b587df4..267dcb0ba9 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 c505869c55..0935dc6265 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 d9fa771c43..241cbc8673 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 23ad792645..dbcc9e4cca 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: {