Cargo.lock 🔗
@@ -934,6 +934,7 @@ dependencies = [
"futures",
"fuzzy",
"gpui",
+ "log",
"postage",
"serde",
"settings",
Nathan Sobo and Max Brunsfeld created
Co-Authored-By: Max Brunsfeld <maxbrunsfeld@gmail.com>
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(-)
@@ -934,6 +934,7 @@ dependencies = [
"futures",
"fuzzy",
"gpui",
+ "log",
"postage",
"serde",
"settings",
@@ -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<Arc<User>>,
}
-#[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<Self>,
) -> Task<Result<()>> {
- 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<Self>,
+ ) -> Task<Result<()>> {
+ 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<Self>,
+ ) -> Task<Result<()>> {
+ 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<T: RequestMessage>(
+ &mut self,
+ user_id: u64,
+ request: T,
+ cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
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<Output = Result<()>> {
- 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();
@@ -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();
@@ -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"] }
@@ -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<User>),
+ OutgoingRequest(Arc<User>),
+ Contact(Arc<Contact>),
+ PotentialContact(Arc<User>),
+}
pub struct ContactsPanel {
- list_state: ListState,
- contacts: Vec<Arc<Contact>>,
+ entries: Vec<ContactEntry>,
+ match_candidates: Vec<StringMatchCandidate>,
potential_contacts: Vec<Arc<User>>,
+ list_state: ListState,
user_store: ModelHandle<UserStore>,
contacts_search_task: Option<Task<Option<()>>>,
user_query_editor: ViewHandle<Editor>,
@@ -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::<Settings>().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::<Settings>().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<Self>) {
- 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>,
+ user_store: ModelHandle<UserStore>,
+ 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::<Reject, _, _>(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::<Accept, _, _>(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>,
+ user_store: ModelHandle<UserStore>,
+ 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::<Cancel, _, _>(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>,
user_store: ModelHandle<UserStore>,
@@ -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<Self>) {
+ fn query_changed(&mut self, cx: &mut ViewContext<Self>) {
+ 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<Self>) {
+ 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::<Vec<_>>();
- 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<Self>) {
@@ -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>,
+ ) {
+ self.user_store
+ .update(cx, |store, cx| {
+ store.respond_to_contact_request(action.user_id, action.accept, cx)
+ })
+ .detach();
+ }
}
pub enum Event {}
@@ -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::<Vec<_>>();
let query = query.chars().collect::<Vec<_>>();
@@ -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::<Vec<_>>();
background