Show requests in contacts panel

Co-Authored-By: Max Brunsfeld <maxbrunsfeld@gmail.com>
This commit is contained in:
Nathan Sobo 2022-05-09 12:48:07 -06:00
parent e9d8cc94cc
commit 40f1427885
6 changed files with 437 additions and 195 deletions

1
Cargo.lock generated
View file

@ -934,6 +934,7 @@ dependencies = [
"futures", "futures",
"fuzzy", "fuzzy",
"gpui", "gpui",
"log",
"postage", "postage",
"serde", "serde",
"settings", "settings",

View file

@ -1,6 +1,6 @@
use super::{http::HttpClient, proto, Client, Status, TypedEnvelope}; use super::{http::HttpClient, proto, Client, Status, TypedEnvelope};
use anyhow::{anyhow, Context, Result}; use anyhow::{anyhow, Context, Result};
use futures::{future, AsyncReadExt, Future}; 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 rpc::proto::{RequestMessage, UsersResponse};
@ -31,11 +31,12 @@ pub struct ProjectMetadata {
pub guests: Vec<Arc<User>>, pub guests: Vec<Arc<User>>,
} }
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ContactRequestStatus { pub enum ContactRequestStatus {
None, None,
SendingRequest, Pending,
Requested, RequestSent,
RequestReceived,
RequestAccepted, RequestAccepted,
} }
@ -192,7 +193,6 @@ impl UserStore {
Err(ix) => this.contacts.insert(ix, updated_contact), Err(ix) => this.contacts.insert(ix, updated_contact),
} }
} }
cx.notify();
// Remove incoming contact requests // Remove incoming contact requests
this.incoming_contact_requests this.incoming_contact_requests
@ -223,6 +223,8 @@ impl UserStore {
Err(ix) => this.outgoing_contact_requests.insert(ix, request), Err(ix) => this.outgoing_contact_requests.insert(ix, request),
} }
} }
cx.notify();
}); });
Ok(()) Ok(())
@ -248,7 +250,9 @@ impl UserStore {
} }
pub fn contact_request_status(&self, user: &User) -> ContactRequestStatus { 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 .contacts
.binary_search_by_key(&&user.id, |contact| &contact.user.id) .binary_search_by_key(&&user.id, |contact| &contact.user.id)
.is_ok() .is_ok()
@ -259,9 +263,13 @@ impl UserStore {
.binary_search_by_key(&&user.id, |user| &user.id) .binary_search_by_key(&&user.id, |user| &user.id)
.is_ok() .is_ok()
{ {
ContactRequestStatus::Requested ContactRequestStatus::RequestSent
} else if self.pending_contact_requests.contains_key(&user.id) { } else if self
ContactRequestStatus::SendingRequest .incoming_contact_requests
.binary_search_by_key(&&user.id, |user| &user.id)
.is_ok()
{
ContactRequestStatus::RequestReceived
} else { } else {
ContactRequestStatus::None ContactRequestStatus::None
} }
@ -272,37 +280,42 @@ impl UserStore {
responder_id: u64, responder_id: u64,
cx: &mut ModelContext<Self>, cx: &mut ModelContext<Self>,
) -> Task<Result<()>> { ) -> Task<Result<()>> {
let client = self.client.upgrade(); self.perform_contact_request(responder_id, proto::RequestContact { responder_id }, cx)
*self
.pending_contact_requests
.entry(responder_id)
.or_insert(0) += 1;
cx.notify();
cx.spawn(|this, mut cx| async move {
let request = client
.ok_or_else(|| anyhow!("can't upgrade client reference"))?
.request(proto::RequestContact { responder_id });
request.await?;
this.update(&mut cx, |this, cx| {
if let Entry::Occupied(mut request_count) =
this.pending_contact_requests.entry(responder_id)
{
*request_count.get_mut() -= 1;
if *request_count.get() == 0 {
request_count.remove();
}
}
cx.notify();
});
Ok(())
})
} }
pub fn remove_contact( pub fn remove_contact(
&mut self, &mut self,
user_id: u64, user_id: u64,
cx: &mut ModelContext<Self>, 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<()>> { ) -> Task<Result<()>> {
let client = self.client.upgrade(); let client = self.client.upgrade();
*self.pending_contact_requests.entry(user_id).or_insert(0) += 1; *self.pending_contact_requests.entry(user_id).or_insert(0) += 1;
@ -311,7 +324,7 @@ impl UserStore {
cx.spawn(|this, mut cx| async move { cx.spawn(|this, mut cx| async move {
let request = client let request = client
.ok_or_else(|| anyhow!("can't upgrade client reference"))? .ok_or_else(|| anyhow!("can't upgrade client reference"))?
.request(proto::RemoveContact { user_id }); .request(request);
request.await?; request.await?;
this.update(&mut cx, |this, cx| { this.update(&mut cx, |this, cx| {
if let Entry::Occupied(mut request_count) = 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"))] #[cfg(any(test, feature = "test-support"))]
pub fn clear_contacts(&mut self) { pub fn clear_contacts(&mut self) {
self.contacts.clear(); self.contacts.clear();

View file

@ -5237,8 +5237,8 @@ mod tests {
// User B accepts the request from user A. // User B accepts the request from user A.
client_b client_b
.user_store .user_store
.read_with(cx_b, |store, _| { .update(cx_b, |store, cx| {
store.respond_to_contact_request(client_a.user_id().unwrap(), true) store.respond_to_contact_request(client_a.user_id().unwrap(), true, cx)
}) })
.await .await
.unwrap(); .unwrap();
@ -5281,8 +5281,8 @@ mod tests {
// User B rejects the request from user C. // User B rejects the request from user C.
client_b client_b
.user_store .user_store
.read_with(cx_b, |store, _| { .update(cx_b, |store, cx| {
store.respond_to_contact_request(client_c.user_id().unwrap(), false) store.respond_to_contact_request(client_c.user_id().unwrap(), false, cx)
}) })
.await .await
.unwrap(); .unwrap();
@ -6506,8 +6506,8 @@ mod tests {
cx_a.foreground().run_until_parked(); cx_a.foreground().run_until_parked();
client_b client_b
.user_store .user_store
.update(*cx_b, |store, _| { .update(*cx_b, |store, cx| {
store.respond_to_contact_request(client_a.user_id().unwrap(), true) store.respond_to_contact_request(client_a.user_id().unwrap(), true, cx)
}) })
.await .await
.unwrap(); .unwrap();

View file

@ -17,5 +17,6 @@ theme = { path = "../theme" }
util = { path = "../util" } util = { path = "../util" }
workspace = { path = "../workspace" } workspace = { path = "../workspace" }
futures = "0.3" futures = "0.3"
log = "0.4"
postage = { version = "0.4.1", features = ["futures-traits"] } postage = { version = "0.4.1", features = ["futures-traits"] }
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }

View file

@ -1,8 +1,7 @@
use client::{Contact, ContactRequestStatus, User, UserStore}; use client::{Contact, ContactRequestStatus, User, UserStore};
use editor::Editor; use editor::Editor;
use fuzzy::StringMatchCandidate; use fuzzy::{match_strings, StringMatchCandidate};
use gpui::{ use gpui::{
anyhow,
elements::*, elements::*,
geometry::{rect::RectF, vector::vec2f}, geometry::{rect::RectF, vector::vec2f},
impl_actions, impl_actions,
@ -13,15 +12,28 @@ use gpui::{
use serde::Deserialize; use serde::Deserialize;
use settings::Settings; use settings::Settings;
use std::sync::Arc; use std::sync::Arc;
use util::ResultExt; use util::TryFutureExt;
use workspace::{AppState, JoinProject}; 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 { pub struct ContactsPanel {
list_state: ListState, entries: Vec<ContactEntry>,
contacts: Vec<Arc<Contact>>, match_candidates: Vec<StringMatchCandidate>,
potential_contacts: Vec<Arc<User>>, potential_contacts: Vec<Arc<User>>,
list_state: ListState,
user_store: ModelHandle<UserStore>, user_store: ModelHandle<UserStore>,
contacts_search_task: Option<Task<Option<()>>>, contacts_search_task: Option<Task<Option<()>>>,
user_query_editor: ViewHandle<Editor>, user_query_editor: ViewHandle<Editor>,
@ -34,9 +46,16 @@ pub struct RequestContact(pub u64);
#[derive(Clone, Deserialize)] #[derive(Clone, Deserialize)]
pub struct RemoveContact(pub u64); pub struct RemoveContact(pub u64);
#[derive(Clone, Deserialize)]
pub struct RespondToContactRequest {
pub user_id: u64,
pub accept: bool,
}
pub fn init(cx: &mut MutableAppContext) { pub fn init(cx: &mut MutableAppContext) {
cx.add_action(ContactsPanel::request_contact); cx.add_action(ContactsPanel::request_contact);
cx.add_action(ContactsPanel::remove_contact); cx.add_action(ContactsPanel::remove_contact);
cx.add_action(ContactsPanel::respond_to_contact_request);
} }
impl ContactsPanel { impl ContactsPanel {
@ -50,29 +69,26 @@ impl ContactsPanel {
cx.subscribe(&user_query_editor, |this, _, event, cx| { cx.subscribe(&user_query_editor, |this, _, event, cx| {
if let editor::Event::BufferEdited = event { if let editor::Event::BufferEdited = event {
this.filter_contacts(true, cx) this.query_changed(cx)
} }
}) })
.detach(); .detach();
Self { let mut this = Self {
list_state: ListState::new( list_state: ListState::new(0, Orientation::Top, 1000., {
1 + app_state.user_store.read(cx).contacts().len(), // Add 1 for the "Contacts" header
Orientation::Top,
1000.,
{
let this = cx.weak_handle(); let this = cx.weak_handle();
let app_state = app_state.clone(); let app_state = app_state.clone();
move |ix, cx| { move |ix, cx| {
let this = this.upgrade(cx).unwrap(); let this = this.upgrade(cx).unwrap();
let this = this.read(cx); 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 = cx.global::<Settings>().theme.clone();
let theme = &theme.contacts_panel; let theme = &theme.contacts_panel;
let current_user_id =
this.user_store.read(cx).current_user().map(|user| user.id);
if ix == 0 { match &this.entries[ix] {
Label::new("contacts".to_string(), theme.header.text.clone()) ContactEntry::Header(text) => {
Label::new(text.to_string(), theme.header.text.clone())
.contained() .contained()
.with_style(theme.header.container) .with_style(theme.header.container)
.aligned() .aligned()
@ -80,55 +96,50 @@ impl ContactsPanel {
.constrained() .constrained()
.with_height(theme.row_height) .with_height(theme.row_height)
.boxed() .boxed()
} else if ix < this.contacts.len() + 1 { }
let contact_ix = ix - 1; ContactEntry::IncomingRequest(user) => {
Self::render_contact( Self::render_incoming_contact_request(
this.contacts[contact_ix].clone(), user.clone(),
current_user_id,
app_state.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(),
this.user_store.clone(), this.user_store.clone(),
theme, theme,
cx, cx,
) )
} }
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,
), ),
contacts: app_state.user_store.read(cx).contacts().into(), ContactEntry::PotentialContact(user) => Self::render_potential_contact(
potential_contacts: Default::default(), user.clone(),
user_query_editor, this.user_store.clone(),
_maintain_contacts: cx.observe(&app_state.user_store, |this, _, cx| { theme,
this.filter_contacts(false, cx) cx,
),
}
}
}), }),
entries: Default::default(),
potential_contacts: Default::default(),
match_candidates: Default::default(),
user_query_editor,
_maintain_contacts: cx
.observe(&app_state.user_store, |this, _, cx| this.update_entries(cx)),
contacts_search_task: None, contacts_search_task: None,
user_store: app_state.user_store.clone(), user_store: app_state.user_store.clone(),
} };
} this.update_entries(cx);
this
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();
} }
fn render_contact( fn render_contact(
@ -295,6 +306,150 @@ impl ContactsPanel {
.boxed() .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( fn render_potential_contact(
contact: Arc<User>, contact: Arc<User>,
user_store: ModelHandle<UserStore>, user_store: ModelHandle<UserStore>,
@ -330,9 +485,11 @@ impl ContactsPanel {
cx, cx,
|_, _| { |_, _| {
let label = match request_status { let label = match request_status {
ContactRequestStatus::None => "+", ContactRequestStatus::None | ContactRequestStatus::RequestReceived => {
ContactRequestStatus::SendingRequest => "", "+"
ContactRequestStatus::Requested => "-", }
ContactRequestStatus::Pending => "",
ContactRequestStatus::RequestSent => "-",
ContactRequestStatus::RequestAccepted => unreachable!(), ContactRequestStatus::RequestAccepted => unreachable!(),
}; };
@ -348,7 +505,7 @@ impl ContactsPanel {
ContactRequestStatus::None => { ContactRequestStatus::None => {
cx.dispatch_action(RequestContact(contact.id)); cx.dispatch_action(RequestContact(contact.id));
} }
ContactRequestStatus::Requested => { ContactRequestStatus::RequestSent => {
cx.dispatch_action(RemoveContact(contact.id)); cx.dispatch_action(RemoveContact(contact.id));
} }
_ => {} _ => {}
@ -361,77 +518,145 @@ impl ContactsPanel {
.boxed() .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 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));
if query.is_empty() { self.contacts_search_task = Some(cx.spawn(|this, mut cx| {
self.contacts.clear(); async move {
self.contacts let potential_contacts = search_users.await?;
.extend_from_slice(self.user_store.read(cx).contacts()); this.update(&mut cx, |this, cx| {
this.potential_contacts = potential_contacts;
if query_changed { this.update_entries(cx);
self.potential_contacts.clear(); });
Ok(())
}
.log_err()
}));
} }
self.update_list_state(cx); fn update_entries(&mut self, cx: &mut ViewContext<Self>) {
return; let user_store = self.user_store.read(cx);
let query = self.user_query_editor.read(cx).text(cx);
let executor = cx.background().clone();
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())
}),
);
}
} }
let contacts = self.user_store.read(cx).contacts().to_vec(); let outgoing = user_store.outgoing_contact_requests();
let candidates = contacts 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 = user_store.contacts();
if !contacts.is_empty() {
self.match_candidates.clear();
self.match_candidates
.extend(
contacts
.iter() .iter()
.enumerate() .enumerate()
.map(|(ix, contact)| StringMatchCandidate { .map(|(ix, contact)| StringMatchCandidate {
id: ix, id: ix,
string: contact.user.github_login.clone(), string: contact.user.github_login.clone(),
char_bag: contact.user.github_login.chars().collect(), char_bag: contact.user.github_login.chars().collect(),
}) }),
.collect::<Vec<_>>();
let cancel_flag = Default::default();
let background = cx.background().clone();
let search_users = if query_changed {
self.user_store
.update(cx, |store, cx| store.fuzzy_search_users(query.clone(), cx))
} else {
Task::ready(Ok(self.potential_contacts.clone()))
};
let match_contacts = async move {
anyhow::Ok(
fuzzy::match_strings(
&candidates,
query.as_str(),
false,
100,
&cancel_flag,
background,
)
.await,
)
};
self.contacts_search_task = Some(cx.spawn(|this, mut cx| async move {
let (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; let matches = executor.block(match_strings(
this.potential_contacts &self.match_candidates,
.retain(|user| !user_store.has_contact(&user)); &query,
this.update_list_state(cx); true,
}); usize::MAX,
None &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())),
);
}
}
if !self.potential_contacts.is_empty() {
self.entries.push(ContactEntry::Header("Add Contacts"));
self.entries.extend(
self.potential_contacts
.iter()
.map(|user| ContactEntry::PotentialContact(user.clone())),
);
}
self.list_state.reset(self.entries.len());
log::info!("UPDATE ENTRIES");
dbg!(&self.entries);
cx.notify();
} }
fn request_contact(&mut self, request: &RequestContact, cx: &mut ViewContext<Self>) { 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)) .update(cx, |store, cx| store.remove_contact(request.0, cx))
.detach(); .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 {} pub enum Event {}

View file

@ -185,6 +185,18 @@ pub async fn match_strings(
return Default::default(); 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 lowercase_query = query.to_lowercase().chars().collect::<Vec<_>>();
let query = query.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 num_cpus = background.num_cpus().min(candidates.len());
let segment_size = (candidates.len() + num_cpus - 1) / num_cpus; let segment_size = (candidates.len() + num_cpus - 1) / num_cpus;
let mut segment_results = (0..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<_>>(); .collect::<Vec<_>>();
background background