Cargo.lock 🔗
@@ -935,6 +935,7 @@ dependencies = [
"postage",
"settings",
"theme",
+ "util",
"workspace",
]
Max Brunsfeld and Nathan Sobo created
Co-authored-by: Nathan Sobo <nathan@zed.dev>
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(-)
@@ -935,6 +935,7 @@ dependencies = [
"postage",
"settings",
"theme",
+ "util",
"workspace",
]
@@ -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,
@@ -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,
@@ -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,
@@ -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,
@@ -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,
@@ -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,
@@ -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,
@@ -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,
@@ -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(),
@@ -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<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 user_ids: Vec<u64>,
cx: &mut ModelContext<Self>,
- ) -> Task<Result<()>> {
- let rpc = self.client.clone();
- let http = self.http.clone();
+ ) -> Task<Result<Vec<Arc<User>>>> {
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<Self>,
+ ) -> Task<Result<Vec<Arc<User>>>> {
+ 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<Option<Arc<User>>> {
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 {
- async fn new(message: proto::User, http: &dyn HttpClient) -> Self {
- User {
+ async fn new(message: proto::User, http: &dyn HttpClient) -> Arc<Self> {
+ Arc::new(User {
id: message.id,
github_login: message.github_login,
avatar: fetch_avatar(http, &message.avatar_url).warn_on_err().await,
- }
+ })
}
}
@@ -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<Server>,
request: TypedEnvelope<proto::GetUsers>,
- ) -> Result<proto::GetUsersResponse> {
+ ) -> Result<proto::UsersResponse> {
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<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))]
@@ -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"] }
@@ -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<Arc<User>>,
user_store: ModelHandle<UserStore>,
+ contacts_search_task: Option<Task<Option<()>>>,
user_query_editor: ViewHandle<Editor>,
_maintain_contacts: Subscription,
}
impl ContactsPanel {
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 {
- 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::<Settings>().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<UserStore>, cx: &mut ViewContext<Self>) {
- self.contacts
- .reset(self.user_store.read(cx).contacts().len());
+ fn update_contacts(&mut self, cx: &mut ViewContext<Self>) {
+ 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<u64>,
app_state: Arc<AppState>,
+ theme: &theme::ContactsPanel,
cx: &mut LayoutContext,
) -> ElementBox {
- let theme = cx.global::<Settings>().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::<ContactsPanel, _, _>(
- 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::<ContactsPanel, _, _>(
+ 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<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 {}
@@ -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)
@@ -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 {
@@ -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;
}
@@ -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),
@@ -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,
@@ -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: {