Allow fuzzy-search for potential contacts in the contacts panel

Co-authored-by: Nathan Sobo <nathan@zed.dev>
This commit is contained in:
Max Brunsfeld 2022-05-05 14:14:44 -07:00
parent 35fea43089
commit ea81737a88
19 changed files with 436 additions and 237 deletions

1
Cargo.lock generated
View File

@ -935,6 +935,7 @@ dependencies = [
"postage", "postage",
"settings", "settings",
"theme", "theme",
"util",
"workspace", "workspace",
] ]

View File

@ -1240,14 +1240,14 @@
"top": 7 "top": 7
} }
}, },
"host_row_height": 28, "row_height": 28,
"tree_branch_color": "#655f6d", "tree_branch_color": "#655f6d",
"tree_branch_width": 1, "tree_branch_width": 1,
"host_avatar": { "contact_avatar": {
"corner_radius": 10, "corner_radius": 10,
"width": 18 "width": 18
}, },
"host_username": { "contact_username": {
"family": "Zed Mono", "family": "Zed Mono",
"color": "#e2dfe7", "color": "#e2dfe7",
"size": 14, "size": 14,
@ -1255,6 +1255,11 @@
"left": 8 "left": 8
} }
}, },
"header": {
"family": "Zed Mono",
"color": "#8b8792",
"size": 14
},
"project": { "project": {
"guest_avatar_spacing": 4, "guest_avatar_spacing": 4,
"height": 24, "height": 24,

View File

@ -1240,14 +1240,14 @@
"top": 7 "top": 7
} }
}, },
"host_row_height": 28, "row_height": 28,
"tree_branch_color": "#7e7887", "tree_branch_color": "#7e7887",
"tree_branch_width": 1, "tree_branch_width": 1,
"host_avatar": { "contact_avatar": {
"corner_radius": 10, "corner_radius": 10,
"width": 18 "width": 18
}, },
"host_username": { "contact_username": {
"family": "Zed Mono", "family": "Zed Mono",
"color": "#26232a", "color": "#26232a",
"size": 14, "size": 14,
@ -1255,6 +1255,11 @@
"left": 8 "left": 8
} }
}, },
"header": {
"family": "Zed Mono",
"color": "#585260",
"size": 14
},
"project": { "project": {
"guest_avatar_spacing": 4, "guest_avatar_spacing": 4,
"height": 24, "height": 24,

View File

@ -1240,14 +1240,14 @@
"top": 7 "top": 7
} }
}, },
"host_row_height": 28, "row_height": 28,
"tree_branch_color": "#404040", "tree_branch_color": "#404040",
"tree_branch_width": 1, "tree_branch_width": 1,
"host_avatar": { "contact_avatar": {
"corner_radius": 10, "corner_radius": 10,
"width": 18 "width": 18
}, },
"host_username": { "contact_username": {
"family": "Zed Mono", "family": "Zed Mono",
"color": "#f1f1f1", "color": "#f1f1f1",
"size": 14, "size": 14,
@ -1255,6 +1255,11 @@
"left": 8 "left": 8
} }
}, },
"header": {
"family": "Zed Mono",
"color": "#9c9c9c",
"size": 14
},
"project": { "project": {
"guest_avatar_spacing": 4, "guest_avatar_spacing": 4,
"height": 24, "height": 24,

View File

@ -1240,14 +1240,14 @@
"top": 7 "top": 7
} }
}, },
"host_row_height": 28, "row_height": 28,
"tree_branch_color": "#e3e3e3", "tree_branch_color": "#e3e3e3",
"tree_branch_width": 1, "tree_branch_width": 1,
"host_avatar": { "contact_avatar": {
"corner_radius": 10, "corner_radius": 10,
"width": 18 "width": 18
}, },
"host_username": { "contact_username": {
"family": "Zed Mono", "family": "Zed Mono",
"color": "#2b2b2b", "color": "#2b2b2b",
"size": 14, "size": 14,
@ -1255,6 +1255,11 @@
"left": 8 "left": 8
} }
}, },
"header": {
"family": "Zed Mono",
"color": "#474747",
"size": 14
},
"project": { "project": {
"guest_avatar_spacing": 4, "guest_avatar_spacing": 4,
"height": 24, "height": 24,

View File

@ -1240,14 +1240,14 @@
"top": 7 "top": 7
} }
}, },
"host_row_height": 28, "row_height": 28,
"tree_branch_color": "#657b83", "tree_branch_color": "#657b83",
"tree_branch_width": 1, "tree_branch_width": 1,
"host_avatar": { "contact_avatar": {
"corner_radius": 10, "corner_radius": 10,
"width": 18 "width": 18
}, },
"host_username": { "contact_username": {
"family": "Zed Mono", "family": "Zed Mono",
"color": "#eee8d5", "color": "#eee8d5",
"size": 14, "size": 14,
@ -1255,6 +1255,11 @@
"left": 8 "left": 8
} }
}, },
"header": {
"family": "Zed Mono",
"color": "#93a1a1",
"size": 14
},
"project": { "project": {
"guest_avatar_spacing": 4, "guest_avatar_spacing": 4,
"height": 24, "height": 24,

View File

@ -1240,14 +1240,14 @@
"top": 7 "top": 7
} }
}, },
"host_row_height": 28, "row_height": 28,
"tree_branch_color": "#839496", "tree_branch_color": "#839496",
"tree_branch_width": 1, "tree_branch_width": 1,
"host_avatar": { "contact_avatar": {
"corner_radius": 10, "corner_radius": 10,
"width": 18 "width": 18
}, },
"host_username": { "contact_username": {
"family": "Zed Mono", "family": "Zed Mono",
"color": "#073642", "color": "#073642",
"size": 14, "size": 14,
@ -1255,6 +1255,11 @@
"left": 8 "left": 8
} }
}, },
"header": {
"family": "Zed Mono",
"color": "#586e75",
"size": 14
},
"project": { "project": {
"guest_avatar_spacing": 4, "guest_avatar_spacing": 4,
"height": 24, "height": 24,

View File

@ -1240,14 +1240,14 @@
"top": 7 "top": 7
} }
}, },
"host_row_height": 28, "row_height": 28,
"tree_branch_color": "#6b7394", "tree_branch_color": "#6b7394",
"tree_branch_width": 1, "tree_branch_width": 1,
"host_avatar": { "contact_avatar": {
"corner_radius": 10, "corner_radius": 10,
"width": 18 "width": 18
}, },
"host_username": { "contact_username": {
"family": "Zed Mono", "family": "Zed Mono",
"color": "#dfe2f1", "color": "#dfe2f1",
"size": 14, "size": 14,
@ -1255,6 +1255,11 @@
"left": 8 "left": 8
} }
}, },
"header": {
"family": "Zed Mono",
"color": "#979db4",
"size": 14
},
"project": { "project": {
"guest_avatar_spacing": 4, "guest_avatar_spacing": 4,
"height": 24, "height": 24,

View File

@ -1240,14 +1240,14 @@
"top": 7 "top": 7
} }
}, },
"host_row_height": 28, "row_height": 28,
"tree_branch_color": "#898ea4", "tree_branch_color": "#898ea4",
"tree_branch_width": 1, "tree_branch_width": 1,
"host_avatar": { "contact_avatar": {
"corner_radius": 10, "corner_radius": 10,
"width": 18 "width": 18
}, },
"host_username": { "contact_username": {
"family": "Zed Mono", "family": "Zed Mono",
"color": "#293256", "color": "#293256",
"size": 14, "size": 14,
@ -1255,6 +1255,11 @@
"left": 8 "left": 8
} }
}, },
"header": {
"family": "Zed Mono",
"color": "#5e6687",
"size": 14
},
"project": { "project": {
"guest_avatar_spacing": 4, "guest_avatar_spacing": 4,
"height": 24, "height": 24,

View File

@ -500,7 +500,7 @@ async fn messages_from_proto(
.collect(); .collect();
user_store user_store
.update(cx, |user_store, cx| { .update(cx, |user_store, cx| {
user_store.load_users(unique_user_ids, cx) user_store.get_users(unique_user_ids, cx)
}) })
.await?; .await?;
@ -639,7 +639,7 @@ mod tests {
server server
.respond( .respond(
get_users.receipt(), get_users.receipt(),
proto::GetUsersResponse { proto::UsersResponse {
users: vec![proto::User { users: vec![proto::User {
id: 5, id: 5,
github_login: "nathansobo".into(), github_login: "nathansobo".into(),
@ -690,7 +690,7 @@ mod tests {
server server
.respond( .respond(
get_users.receipt(), get_users.receipt(),
proto::GetUsersResponse { proto::UsersResponse {
users: vec![proto::User { users: vec![proto::User {
id: 6, id: 6,
github_login: "maxbrunsfeld".into(), github_login: "maxbrunsfeld".into(),
@ -738,7 +738,7 @@ mod tests {
server server
.respond( .respond(
get_users.receipt(), get_users.receipt(),
proto::GetUsersResponse { proto::UsersResponse {
users: vec![proto::User { users: vec![proto::User {
id: 7, id: 7,
github_login: "as-cii".into(), github_login: "as-cii".into(),

View File

@ -3,6 +3,7 @@ use anyhow::{anyhow, Result};
use futures::{future, AsyncReadExt}; use futures::{future, AsyncReadExt};
use gpui::{AsyncAppContext, Entity, ImageData, ModelContext, ModelHandle, Task}; use gpui::{AsyncAppContext, Entity, ImageData, ModelContext, ModelHandle, Task};
use postage::{prelude::Stream, sink::Sink, watch}; use postage::{prelude::Stream, sink::Sink, watch};
use rpc::proto::{RequestMessage, UsersResponse};
use std::{ use std::{
collections::{HashMap, HashSet}, collections::{HashMap, HashSet},
sync::{Arc, Weak}, sync::{Arc, Weak},
@ -121,7 +122,7 @@ impl UserStore {
user_ids.extend(contact.projects.iter().flat_map(|w| &w.guests).copied()); 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 { cx.spawn(|this, mut cx| async move {
load_users.await?; load_users.await?;
@ -144,37 +145,27 @@ impl UserStore {
&self.contacts &self.contacts
} }
pub fn load_users( pub fn has_contact(&self, user: &Arc<User>) -> bool {
self.contacts
.binary_search_by_key(&&user.github_login, |contact| &contact.user.github_login)
.is_ok()
}
pub fn get_users(
&mut self, &mut self,
mut user_ids: Vec<u64>, mut user_ids: Vec<u64>,
cx: &mut ModelContext<Self>, cx: &mut ModelContext<Self>,
) -> Task<Result<()>> { ) -> Task<Result<Vec<Arc<User>>>> {
let rpc = self.client.clone();
let http = self.http.clone();
user_ids.retain(|id| !self.users.contains_key(id)); user_ids.retain(|id| !self.users.contains_key(id));
cx.spawn_weak(|this, mut cx| async move { self.load_users(proto::GetUsers { user_ids }, cx)
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;
if let Some(this) = this.upgrade(&cx) { pub fn fuzzy_search_users(
this.update(&mut cx, |this, _| { &mut self,
for user in new_users { query: String,
this.users.insert(user.id, Arc::new(user)); cx: &mut ModelContext<Self>,
} ) -> Task<Result<Vec<Arc<User>>>> {
}); self.load_users(proto::FuzzySearchUsers { query }, cx)
}
}
}
Ok(())
})
} }
pub fn fetch_user( pub fn fetch_user(
@ -186,7 +177,7 @@ impl UserStore {
return cx.foreground().spawn(async move { Ok(user) }); 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 { cx.spawn(|this, mut cx| async move {
load_users.await?; load_users.await?;
this.update(&mut cx, |this, _| { this.update(&mut cx, |this, _| {
@ -205,15 +196,47 @@ impl UserStore {
pub fn watch_current_user(&self) -> watch::Receiver<Option<Arc<User>>> { pub fn watch_current_user(&self) -> watch::Receiver<Option<Arc<User>>> {
self.current_user.clone() self.current_user.clone()
} }
fn load_users(
&mut self,
request: impl RequestMessage<Response = UsersResponse>,
cx: &mut ModelContext<Self>,
) -> Task<Result<Vec<Arc<User>>>> {
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 { impl User {
async fn new(message: proto::User, http: &dyn HttpClient) -> Self { async fn new(message: proto::User, http: &dyn HttpClient) -> Arc<Self> {
User { Arc::new(User {
id: message.id, id: message.id,
github_login: message.github_login, github_login: message.github_login,
avatar: fetch_avatar(http, &message.avatar_url).warn_on_err().await, avatar: fetch_avatar(http, &message.avatar_url).warn_on_err().await,
} })
} }
} }

View File

@ -136,6 +136,7 @@ impl Server {
.add_request_handler(Server::save_buffer) .add_request_handler(Server::save_buffer)
.add_request_handler(Server::get_channels) .add_request_handler(Server::get_channels)
.add_request_handler(Server::get_users) .add_request_handler(Server::get_users)
.add_request_handler(Server::fuzzy_search_users)
.add_request_handler(Server::join_channel) .add_request_handler(Server::join_channel)
.add_message_handler(Server::leave_channel) .add_message_handler(Server::leave_channel)
.add_request_handler(Server::send_channel_message) .add_request_handler(Server::send_channel_message)
@ -842,7 +843,7 @@ impl Server {
async fn get_users( async fn get_users(
self: Arc<Server>, self: Arc<Server>,
request: TypedEnvelope<proto::GetUsers>, request: TypedEnvelope<proto::GetUsers>,
) -> Result<proto::GetUsersResponse> { ) -> Result<proto::UsersResponse> {
let user_ids = request let user_ids = request
.payload .payload
.user_ids .user_ids
@ -861,7 +862,33 @@ impl Server {
github_login: user.github_login, github_login: user.github_login,
}) })
.collect(); .collect();
Ok(proto::GetUsersResponse { users }) Ok(proto::UsersResponse { users })
}
async fn fuzzy_search_users(
self: Arc<Server>,
request: TypedEnvelope<proto::FuzzySearchUsers>,
) -> Result<proto::UsersResponse> {
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))] #[instrument(skip(self, state, user_ids))]

View File

@ -13,5 +13,6 @@ editor = { path = "../editor" }
gpui = { path = "../gpui" } gpui = { path = "../gpui" }
settings = { path = "../settings" } settings = { path = "../settings" }
theme = { path = "../theme" } theme = { path = "../theme" }
util = { path = "../util" }
workspace = { path = "../workspace" } workspace = { path = "../workspace" }
postage = { version = "0.4.1", features = ["futures-traits"] } postage = { version = "0.4.1", features = ["futures-traits"] }

View File

@ -1,71 +1,124 @@
use client::{Contact, UserStore}; use client::{Contact, User, UserStore};
use editor::Editor; use editor::Editor;
use gpui::{ use gpui::{
elements::*, elements::*,
geometry::{rect::RectF, vector::vec2f}, geometry::{rect::RectF, vector::vec2f},
platform::CursorStyle, platform::CursorStyle,
Element, ElementBox, Entity, LayoutContext, ModelHandle, RenderContext, Subscription, View, Element, ElementBox, Entity, LayoutContext, ModelHandle, RenderContext, Subscription, Task,
ViewContext, ViewHandle, View, ViewContext, ViewHandle,
}; };
use settings::Settings; use settings::Settings;
use std::sync::Arc; use std::sync::Arc;
use util::ResultExt;
use workspace::{AppState, JoinProject}; use workspace::{AppState, JoinProject};
pub struct ContactsPanel { pub struct ContactsPanel {
contacts: ListState, list_state: ListState,
potential_contacts: Vec<Arc<User>>,
user_store: ModelHandle<UserStore>, user_store: ModelHandle<UserStore>,
contacts_search_task: Option<Task<Option<()>>>,
user_query_editor: ViewHandle<Editor>, user_query_editor: ViewHandle<Editor>,
_maintain_contacts: Subscription, _maintain_contacts: Subscription,
} }
impl ContactsPanel { impl ContactsPanel {
pub fn new(app_state: Arc<AppState>, cx: &mut ViewContext<Self>) -> Self { pub fn new(app_state: Arc<AppState>, cx: &mut ViewContext<Self>) -> 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 { Self {
contacts: ListState::new( list_state: ListState::new(
app_state.user_store.read(cx).contacts().len(), 1 + app_state.user_store.read(cx).contacts().len(), // Add 1 for the "Contacts" header
Orientation::Top, Orientation::Top,
1000., 1000.,
{ {
let this = cx.weak_handle();
let app_state = app_state.clone(); let app_state = app_state.clone();
move |ix, cx| { 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 contacts = user_store.contacts().clone();
let current_user_id = user_store.current_user().map(|user| user.id); let current_user_id = user_store.current_user().map(|user| user.id);
Self::render_collaborator( let theme = cx.global::<Settings>().theme.clone();
&contacts[ix], let theme = &theme.contacts_panel;
current_user_id,
app_state.clone(), if ix == 0 {
cx, 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| { potential_contacts: Default::default(),
Editor::single_line( user_query_editor,
Some(|theme| theme.contacts_panel.user_query_editor.clone()), _maintain_contacts: cx.observe(&app_state.user_store, |this, _, cx| {
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(), user_store: app_state.user_store.clone(),
} }
} }
fn update_contacts(&mut self, _: ModelHandle<UserStore>, cx: &mut ViewContext<Self>) { fn update_contacts(&mut self, cx: &mut ViewContext<Self>) {
self.contacts let mut list_len = 1 + self.user_store.read(cx).contacts().len();
.reset(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(); cx.notify();
} }
fn render_collaborator( fn render_contact(
collaborator: &Contact, contact: &Contact,
current_user_id: Option<u64>, current_user_id: Option<u64>,
app_state: Arc<AppState>, app_state: Arc<AppState>,
theme: &theme::ContactsPanel,
cx: &mut LayoutContext, cx: &mut LayoutContext,
) -> ElementBox { ) -> ElementBox {
let theme = cx.global::<Settings>().theme.clone(); let project_count = contact.projects.len();
let theme = &theme.contacts_panel;
let project_count = collaborator.projects.len();
let font_cache = cx.font_cache(); let font_cache = cx.font_cache();
let line_height = theme.unshared_project.name.text.line_height(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); 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_width = theme.tree_branch_width;
let tree_branch_color = theme.tree_branch_color; let tree_branch_color = theme.tree_branch_color;
let host_avatar_height = theme let host_avatar_height = theme
.host_avatar .contact_avatar
.width .width
.or(theme.host_avatar.height) .or(theme.contact_avatar.height)
.unwrap_or(0.); .unwrap_or(0.);
Flex::column() Flex::column()
.with_child( .with_child(
Flex::row() Flex::row()
.with_children(collaborator.user.avatar.clone().map(|avatar| { .with_children(contact.user.avatar.clone().map(|avatar| {
Image::new(avatar) Image::new(avatar)
.with_style(theme.host_avatar) .with_style(theme.contact_avatar)
.aligned() .aligned()
.left() .left()
.boxed() .boxed()
})) }))
.with_child( .with_child(
Label::new( Label::new(
collaborator.user.github_login.clone(), contact.user.github_login.clone(),
theme.host_username.text.clone(), theme.contact_username.text.clone(),
) )
.contained() .contained()
.with_style(theme.host_username.container) .with_style(theme.contact_username.container)
.aligned() .aligned()
.left() .left()
.boxed(), .boxed(),
) )
.constrained() .constrained()
.with_height(theme.host_row_height) .with_height(theme.row_height)
.boxed(), .boxed(),
) )
.with_children( .with_children(contact.projects.iter().enumerate().map(|(ix, project)| {
collaborator let project_id = project.id;
.projects
.iter()
.enumerate()
.map(|(ix, project)| {
let project_id = project.id;
Flex::row() Flex::row()
.with_child( .with_child(
Canvas::new(move |bounds, _, cx| { Canvas::new(move |bounds, _, cx| {
let start_x = bounds.min_x() + (bounds.width() / 2.) let start_x =
- (tree_branch_width / 2.); bounds.min_x() + (bounds.width() / 2.) - (tree_branch_width / 2.);
let end_x = bounds.max_x(); let end_x = bounds.max_x();
let start_y = bounds.min_y(); let start_y = bounds.min_y();
let end_y = let end_y = bounds.min_y() + baseline_offset - (cap_height / 2.);
bounds.min_y() + baseline_offset - (cap_height / 2.);
cx.scene.push_quad(gpui::Quad { cx.scene.push_quad(gpui::Quad {
bounds: RectF::from_points( bounds: RectF::from_points(
vec2f(start_x, start_y), vec2f(start_x, start_y),
vec2f( vec2f(
start_x + tree_branch_width, start_x + tree_branch_width,
if ix + 1 == project_count { if ix + 1 == project_count {
end_y end_y
} else { } else {
bounds.max_y() bounds.max_y()
}, },
), ),
), ),
background: Some(tree_branch_color), background: Some(tree_branch_color),
border: gpui::Border::default(), border: gpui::Border::default(),
corner_radius: 0., corner_radius: 0.,
}); });
cx.scene.push_quad(gpui::Quad { cx.scene.push_quad(gpui::Quad {
bounds: RectF::from_points( bounds: RectF::from_points(
vec2f(start_x, end_y), vec2f(start_x, end_y),
vec2f(end_x, end_y + tree_branch_width), vec2f(end_x, end_y + tree_branch_width),
), ),
background: Some(tree_branch_color), background: Some(tree_branch_color),
border: gpui::Border::default(), border: gpui::Border::default(),
corner_radius: 0., corner_radius: 0.,
}); });
}) })
.constrained() .constrained()
.with_width(host_avatar_height) .with_width(host_avatar_height)
.boxed(), .boxed(),
) )
.with_child({ .with_child({
let is_host = Some(collaborator.user.id) == current_user_id; let is_host = Some(contact.user.id) == current_user_id;
let is_guest = !is_host let is_guest = !is_host
&& project && project
.guests .guests
.iter() .iter()
.any(|guest| Some(guest.id) == current_user_id); .any(|guest| Some(guest.id) == current_user_id);
let is_shared = project.is_shared; let is_shared = project.is_shared;
let app_state = app_state.clone(); let app_state = app_state.clone();
MouseEventHandler::new::<ContactsPanel, _, _>( MouseEventHandler::new::<ContactsPanel, _, _>(
project_id as usize, project_id as usize,
cx, cx,
|mouse_state, _| { |mouse_state, _| {
let style = match (project.is_shared, mouse_state.hovered) { let style = match (project.is_shared, mouse_state.hovered) {
(false, false) => &theme.unshared_project, (false, false) => &theme.unshared_project,
(false, true) => &theme.hovered_unshared_project, (false, true) => &theme.hovered_unshared_project,
(true, false) => &theme.shared_project, (true, false) => &theme.shared_project,
(true, true) => &theme.hovered_shared_project, (true, true) => &theme.hovered_shared_project,
}; };
Flex::row() Flex::row()
.with_child( .with_child(
Label::new( Label::new(
project.worktree_root_names.join(", "), project.worktree_root_names.join(", "),
style.name.text.clone(), style.name.text.clone(),
) )
.aligned() .aligned()
.left() .left()
.contained() .contained()
.with_style(style.name.container) .with_style(style.name.container)
.boxed(), .boxed(),
) )
.with_children(project.guests.iter().filter_map( .with_children(project.guests.iter().filter_map(
|participant| { |participant| {
participant.avatar.clone().map(|avatar| { participant.avatar.clone().map(|avatar| {
Image::new(avatar) Image::new(avatar)
.with_style(style.guest_avatar) .with_style(style.guest_avatar)
.aligned() .aligned()
.left() .left()
.contained() .contained()
.with_margin_right( .with_margin_right(style.guest_avatar_spacing)
style.guest_avatar_spacing, .boxed()
) })
.boxed() },
}) ))
}, .contained()
)) .with_style(style.container)
.contained() .constrained()
.with_style(style.container) .with_height(style.height)
.constrained() .boxed()
.with_height(style.height) },
.boxed() )
}, .with_cursor_style(if is_host || is_shared {
) CursorStyle::PointingHand
.with_cursor_style(if is_host || is_shared { } else {
CursorStyle::PointingHand CursorStyle::Arrow
} else { })
CursorStyle::Arrow .on_click(move |_, cx| {
}) if !is_host && !is_guest {
.on_click(move |_, cx| { cx.dispatch_global_action(JoinProject {
if !is_host && !is_guest { project_id,
cx.dispatch_global_action(JoinProject { app_state: app_state.clone(),
project_id, });
app_state: app_state.clone(), }
}); })
} .flex(1., true)
}) .boxed()
.flex(1., true) })
.boxed() .constrained()
}) .with_height(theme.unshared_project.height)
.constrained() .boxed()
.with_height(theme.unshared_project.height) }))
.boxed()
}),
)
.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<Self>) {
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 {} pub enum Event {}
@ -252,7 +345,7 @@ impl View for ContactsPanel {
.with_style(theme.user_query_editor.container) .with_style(theme.user_query_editor.container)
.boxed(), .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(), .boxed(),
) )
.with_style(theme.container) .with_style(theme.container)

View File

@ -443,7 +443,7 @@ impl Project {
.map(|peer| peer.user_id) .map(|peer| peer.user_id)
.collect(); .collect();
user_store 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?; .await?;
let mut collaborators = HashMap::default(); let mut collaborators = HashMap::default();
for message in response.collaborators { for message in response.collaborators {

View File

@ -87,12 +87,13 @@ message Envelope {
UpdateContacts update_contacts = 75; UpdateContacts update_contacts = 75;
GetUsers get_users = 76; GetUsers get_users = 76;
GetUsersResponse get_users_response = 77; FuzzySearchUsers fuzzy_search_users = 77;
UsersResponse users_response = 78;
Follow follow = 78; Follow follow = 79;
FollowResponse follow_response = 79; FollowResponse follow_response = 80;
UpdateFollowers update_followers = 80; UpdateFollowers update_followers = 81;
Unfollow unfollow = 81; Unfollow unfollow = 82;
} }
} }
@ -538,7 +539,11 @@ message GetUsers {
repeated uint64 user_ids = 1; repeated uint64 user_ids = 1;
} }
message GetUsersResponse { message FuzzySearchUsers {
string query = 1;
}
message UsersResponse {
repeated User users = 1; repeated User users = 1;
} }

View File

@ -155,6 +155,7 @@ messages!(
(FollowResponse, Foreground), (FollowResponse, Foreground),
(FormatBuffers, Foreground), (FormatBuffers, Foreground),
(FormatBuffersResponse, Foreground), (FormatBuffersResponse, Foreground),
(FuzzySearchUsers, Foreground),
(GetChannelMessages, Foreground), (GetChannelMessages, Foreground),
(GetChannelMessagesResponse, Foreground), (GetChannelMessagesResponse, Foreground),
(GetChannels, Foreground), (GetChannels, Foreground),
@ -172,7 +173,7 @@ messages!(
(GetProjectSymbols, Background), (GetProjectSymbols, Background),
(GetProjectSymbolsResponse, Background), (GetProjectSymbolsResponse, Background),
(GetUsers, Foreground), (GetUsers, Foreground),
(GetUsersResponse, Foreground), (UsersResponse, Foreground),
(JoinChannel, Foreground), (JoinChannel, Foreground),
(JoinChannelResponse, Foreground), (JoinChannelResponse, Foreground),
(JoinProject, Foreground), (JoinProject, Foreground),
@ -236,7 +237,8 @@ request_messages!(
(GetDocumentHighlights, GetDocumentHighlightsResponse), (GetDocumentHighlights, GetDocumentHighlightsResponse),
(GetReferences, GetReferencesResponse), (GetReferences, GetReferencesResponse),
(GetProjectSymbols, GetProjectSymbolsResponse), (GetProjectSymbols, GetProjectSymbolsResponse),
(GetUsers, GetUsersResponse), (FuzzySearchUsers, UsersResponse),
(GetUsers, UsersResponse),
(JoinChannel, JoinChannelResponse), (JoinChannel, JoinChannelResponse),
(JoinProject, JoinProjectResponse), (JoinProject, JoinProjectResponse),
(OpenBufferById, OpenBufferResponse), (OpenBufferById, OpenBufferResponse),

View File

@ -234,20 +234,21 @@ pub struct CommandPalette {
pub struct ContactsPanel { pub struct ContactsPanel {
#[serde(flatten)] #[serde(flatten)]
pub container: ContainerStyle, pub container: ContainerStyle,
pub header: ContainedText,
pub user_query_editor: FieldEditor, pub user_query_editor: FieldEditor,
pub host_row_height: f32, pub row_height: f32,
pub host_avatar: ImageStyle, pub contact_avatar: ImageStyle,
pub host_username: ContainedText, pub contact_username: ContainedText,
pub tree_branch_width: f32, pub tree_branch_width: f32,
pub tree_branch_color: Color, pub tree_branch_color: Color,
pub shared_project: WorktreeRow, pub shared_project: ProjectRow,
pub hovered_shared_project: WorktreeRow, pub hovered_shared_project: ProjectRow,
pub unshared_project: WorktreeRow, pub unshared_project: ProjectRow,
pub hovered_unshared_project: WorktreeRow, pub hovered_unshared_project: ProjectRow,
} }
#[derive(Deserialize, Default)] #[derive(Deserialize, Default)]
pub struct WorktreeRow { pub struct ProjectRow {
#[serde(flatten)] #[serde(flatten)]
pub container: ContainerStyle, pub container: ContainerStyle,
pub height: f32, pub height: f32,

View File

@ -47,19 +47,25 @@ export default function(theme: Theme) {
top: 7, top: 7,
}, },
}, },
hostRowHeight: 28, rowHeight: 28,
treeBranchColor: borderColor(theme, "muted"), treeBranchColor: borderColor(theme, "muted"),
treeBranchWidth: 1, treeBranchWidth: 1,
hostAvatar: { contactAvatar: {
cornerRadius: 10, cornerRadius: 10,
width: 18, width: 18,
}, },
hostUsername: { contactUsername: {
...text(theme, "mono", "primary", { size: "sm" }), ...text(theme, "mono", "primary", { size: "sm" }),
padding: { padding: {
left: 8, left: 8,
}, },
}, },
header: {
...text(theme, "mono", "secondary", { size: "sm" }),
// padding: {
// left: 8,
// }
},
project, project,
sharedProject, sharedProject,
hoveredSharedProject: { hoveredSharedProject: {