Show requests in contacts panel
Co-Authored-By: Max Brunsfeld <maxbrunsfeld@gmail.com>
This commit is contained in:
parent
e9d8cc94cc
commit
40f1427885
6 changed files with 437 additions and 195 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -934,6 +934,7 @@ dependencies = [
|
||||||
"futures",
|
"futures",
|
||||||
"fuzzy",
|
"fuzzy",
|
||||||
"gpui",
|
"gpui",
|
||||||
|
"log",
|
||||||
"postage",
|
"postage",
|
||||||
"serde",
|
"serde",
|
||||||
"settings",
|
"settings",
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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"] }
|
||||||
|
|
|
@ -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 {}
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue