Wire up UI for requesting contacts and cancelling requests

Co-Authored-By: Max Brunsfeld <maxbrunsfeld@gmail.com>
This commit is contained in:
Nathan Sobo 2022-05-09 11:24:05 -06:00
parent e4f1952657
commit e3ee19b123
7 changed files with 212 additions and 32 deletions

View file

@ -5,7 +5,7 @@ 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};
use std::{ use std::{
collections::{HashMap, HashSet}, collections::{hash_map::Entry, HashMap, HashSet},
sync::{Arc, Weak}, sync::{Arc, Weak},
}; };
use util::TryFutureExt as _; use util::TryFutureExt as _;
@ -31,6 +31,14 @@ pub struct ProjectMetadata {
pub guests: Vec<Arc<User>>, pub guests: Vec<Arc<User>>,
} }
#[derive(Debug, Clone, Copy)]
pub enum ContactRequestStatus {
None,
SendingRequest,
Requested,
RequestAccepted,
}
pub struct UserStore { pub struct UserStore {
users: HashMap<u64, Arc<User>>, users: HashMap<u64, Arc<User>>,
update_contacts_tx: watch::Sender<Option<proto::UpdateContacts>>, update_contacts_tx: watch::Sender<Option<proto::UpdateContacts>>,
@ -38,6 +46,7 @@ pub struct UserStore {
contacts: Vec<Arc<Contact>>, contacts: Vec<Arc<Contact>>,
incoming_contact_requests: Vec<Arc<User>>, incoming_contact_requests: Vec<Arc<User>>,
outgoing_contact_requests: Vec<Arc<User>>, outgoing_contact_requests: Vec<Arc<User>>,
pending_contact_requests: HashMap<u64, usize>,
client: Weak<Client>, client: Weak<Client>,
http: Arc<dyn HttpClient>, http: Arc<dyn HttpClient>,
_maintain_contacts: Task<()>, _maintain_contacts: Task<()>,
@ -100,6 +109,7 @@ impl UserStore {
} }
} }
}), }),
pending_contact_requests: Default::default(),
} }
} }
@ -237,23 +247,85 @@ impl UserStore {
&self.outgoing_contact_requests &self.outgoing_contact_requests
} }
pub fn has_outgoing_contact_request(&self, user: &User) -> bool { pub fn contact_request_status(&self, user: &User) -> ContactRequestStatus {
self.outgoing_contact_requests if self
.binary_search_by_key(&&user.github_login, |requested_user| { .contacts
&requested_user.github_login .binary_search_by_key(&&user.id, |contact| &contact.user.id)
})
.is_ok() .is_ok()
{
ContactRequestStatus::RequestAccepted
} else if self
.outgoing_contact_requests
.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
} else {
ContactRequestStatus::None
}
} }
pub fn request_contact(&self, responder_id: u64) -> impl Future<Output = Result<()>> { pub fn request_contact(
&mut self,
responder_id: u64,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
let client = self.client.upgrade(); let client = self.client.upgrade();
async move { *self
client .pending_contact_requests
.ok_or_else(|| anyhow!("not logged in"))? .entry(responder_id)
.request(proto::RequestContact { responder_id }) .or_insert(0) += 1;
.await?; 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(()) Ok(())
} })
}
pub fn remove_contact(
&mut self,
user_id: u64,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
let client = self.client.upgrade();
*self.pending_contact_requests.entry(user_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::RemoveContact { user_id });
request.await?;
this.update(&mut cx, |this, cx| {
if let Entry::Occupied(mut request_count) =
this.pending_contact_requests.entry(user_id)
{
*request_count.get_mut() -= 1;
if *request_count.get() == 0 {
request_count.remove();
}
}
cx.notify();
});
Ok(())
})
} }
pub fn respond_to_contact_request( pub fn respond_to_contact_request(

View file

@ -19,6 +19,7 @@ pub trait Db: Send + Sync {
async fn get_contacts(&self, id: UserId) -> Result<Contacts>; async fn get_contacts(&self, id: UserId) -> Result<Contacts>;
async fn send_contact_request(&self, requester_id: UserId, responder_id: UserId) -> Result<()>; async fn send_contact_request(&self, requester_id: UserId, responder_id: UserId) -> Result<()>;
async fn remove_contact(&self, requester_id: UserId, responder_id: UserId) -> Result<()>;
async fn dismiss_contact_request( async fn dismiss_contact_request(
&self, &self,
responder_id: UserId, responder_id: UserId,
@ -267,6 +268,30 @@ impl Db for PostgresDb {
} }
} }
async fn remove_contact(&self, requester_id: UserId, responder_id: UserId) -> Result<()> {
let (id_a, id_b, a_to_b) = if responder_id < requester_id {
(responder_id, requester_id, false)
} else {
(requester_id, responder_id, true)
};
let query = "
DELETE FROM contacts
WHERE user_id_a = $1 AND user_id_b = $2 AND a_to_b = $3;
";
let result = sqlx::query(query)
.bind(id_a.0)
.bind(id_b.0)
.bind(a_to_b)
.execute(&self.pool)
.await?;
if result.rows_affected() == 1 {
Ok(())
} else {
Err(anyhow!("no such contact"))
}
}
async fn respond_to_contact_request( async fn respond_to_contact_request(
&self, &self,
responder_id: UserId, responder_id: UserId,
@ -1248,6 +1273,13 @@ pub mod tests {
Ok(()) Ok(())
} }
async fn remove_contact(&self, requester_id: UserId, responder_id: UserId) -> Result<()> {
self.contacts.lock().retain(|contact| {
!(contact.requester_id == requester_id && contact.responder_id == responder_id)
});
Ok(())
}
async fn dismiss_contact_request( async fn dismiss_contact_request(
&self, &self,
responder_id: UserId, responder_id: UserId,

View file

@ -155,6 +155,7 @@ impl Server {
.add_request_handler(Server::get_users) .add_request_handler(Server::get_users)
.add_request_handler(Server::fuzzy_search_users) .add_request_handler(Server::fuzzy_search_users)
.add_request_handler(Server::request_contact) .add_request_handler(Server::request_contact)
.add_request_handler(Server::remove_contact)
.add_request_handler(Server::respond_to_contact_request) .add_request_handler(Server::respond_to_contact_request)
.add_request_handler(Server::join_channel) .add_request_handler(Server::join_channel)
.add_message_handler(Server::leave_channel) .add_message_handler(Server::leave_channel)
@ -1048,6 +1049,43 @@ impl Server {
Ok(()) Ok(())
} }
async fn remove_contact(
self: Arc<Server>,
request: TypedEnvelope<proto::RemoveContact>,
response: Response<proto::RemoveContact>,
) -> Result<()> {
let requester_id = self
.store()
.await
.user_id_for_connection(request.sender_id)?;
let responder_id = UserId::from_proto(request.payload.user_id);
self.app_state
.db
.remove_contact(requester_id, responder_id)
.await?;
// Update outgoing contact requests of requester
let mut update = proto::UpdateContacts::default();
update
.remove_outgoing_requests
.push(responder_id.to_proto());
for connection_id in self.store().await.connection_ids_for_user(requester_id) {
self.peer.send(connection_id, update.clone())?;
}
// Update incoming contact requests of responder
let mut update = proto::UpdateContacts::default();
update
.remove_incoming_requests
.push(requester_id.to_proto());
for connection_id in self.store().await.connection_ids_for_user(responder_id) {
self.peer.send(connection_id, update.clone())?;
}
response.send(proto::Ack {})?;
Ok(())
}
// #[instrument(skip(self, state, user_ids))] // #[instrument(skip(self, state, user_ids))]
// fn update_contacts_for_users<'a>( // fn update_contacts_for_users<'a>(
// self: &Arc<Self>, // self: &Arc<Self>,
@ -5138,15 +5176,15 @@ mod tests {
// User A and User C request that user B become their contact. // User A and User C request that user B become their contact.
client_a client_a
.user_store .user_store
.read_with(cx_a, |store, _| { .update(cx_a, |store, cx| {
store.request_contact(client_b.user_id().unwrap()) store.request_contact(client_b.user_id().unwrap(), cx)
}) })
.await .await
.unwrap(); .unwrap();
client_c client_c
.user_store .user_store
.read_with(cx_c, |store, _| { .update(cx_c, |store, cx| {
store.request_contact(client_b.user_id().unwrap()) store.request_contact(client_b.user_id().unwrap(), cx)
}) })
.await .await
.unwrap(); .unwrap();
@ -6460,8 +6498,8 @@ mod tests {
for (client_b, cx_b) in &mut clients { for (client_b, cx_b) in &mut clients {
client_a client_a
.user_store .user_store
.update(cx_a, |store, _| { .update(cx_a, |store, cx| {
store.request_contact(client_b.user_id().unwrap()) store.request_contact(client_b.user_id().unwrap(), cx)
}) })
.await .await
.unwrap(); .unwrap();

View file

@ -1,4 +1,4 @@
use client::{Contact, User, UserStore}; use client::{Contact, ContactRequestStatus, User, UserStore};
use editor::Editor; use editor::Editor;
use fuzzy::StringMatchCandidate; use fuzzy::StringMatchCandidate;
use gpui::{ use gpui::{
@ -7,8 +7,8 @@ use gpui::{
geometry::{rect::RectF, vector::vec2f}, geometry::{rect::RectF, vector::vec2f},
impl_actions, impl_actions,
platform::CursorStyle, platform::CursorStyle,
Element, ElementBox, Entity, LayoutContext, ModelHandle, RenderContext, Subscription, Task, Element, ElementBox, Entity, LayoutContext, ModelHandle, MutableAppContext, RenderContext,
View, ViewContext, ViewHandle, Subscription, Task, View, ViewContext, ViewHandle,
}; };
use serde::Deserialize; use serde::Deserialize;
use settings::Settings; use settings::Settings;
@ -16,7 +16,7 @@ use std::sync::Arc;
use util::ResultExt; use util::ResultExt;
use workspace::{AppState, JoinProject}; use workspace::{AppState, JoinProject};
impl_actions!(contacts_panel, [RequestContact]); impl_actions!(contacts_panel, [RequestContact, RemoveContact]);
pub struct ContactsPanel { pub struct ContactsPanel {
list_state: ListState, list_state: ListState,
@ -31,6 +31,14 @@ pub struct ContactsPanel {
#[derive(Clone, Deserialize)] #[derive(Clone, Deserialize)]
pub struct RequestContact(pub u64); pub struct RequestContact(pub u64);
#[derive(Clone, Deserialize)]
pub struct RemoveContact(pub u64);
pub fn init(cx: &mut MutableAppContext) {
cx.add_action(ContactsPanel::request_contact);
cx.add_action(ContactsPanel::remove_contact);
}
impl ContactsPanel { impl ContactsPanel {
pub fn new(app_state: Arc<AppState>, cx: &mut ViewContext<Self>) -> Self { pub fn new(app_state: Arc<AppState>, cx: &mut ViewContext<Self>) -> Self {
let user_query_editor = cx.add_view(|cx| { let user_query_editor = cx.add_view(|cx| {
@ -295,7 +303,7 @@ impl ContactsPanel {
) -> ElementBox { ) -> ElementBox {
enum RequestContactButton {} enum RequestContactButton {}
let requested_contact = user_store.read(cx).has_outgoing_contact_request(&contact); let request_status = user_store.read(cx).contact_request_status(&contact);
Flex::row() Flex::row()
.with_children(contact.avatar.clone().map(|avatar| { .with_children(contact.avatar.clone().map(|avatar| {
@ -321,7 +329,13 @@ impl ContactsPanel {
contact.id as usize, contact.id as usize,
cx, cx,
|_, _| { |_, _| {
let label = if requested_contact { "-" } else { "+" }; let label = match request_status {
ContactRequestStatus::None => "+",
ContactRequestStatus::SendingRequest => "",
ContactRequestStatus::Requested => "-",
ContactRequestStatus::RequestAccepted => unreachable!(),
};
Label::new(label.to_string(), theme.edit_contact.text.clone()) Label::new(label.to_string(), theme.edit_contact.text.clone())
.contained() .contained()
.with_style(theme.edit_contact.container) .with_style(theme.edit_contact.container)
@ -330,12 +344,16 @@ impl ContactsPanel {
.boxed() .boxed()
}, },
) )
.on_click(move |_, cx| { .on_click(move |_, cx| match request_status {
if requested_contact { ContactRequestStatus::None => {
} else {
cx.dispatch_action(RequestContact(contact.id)); cx.dispatch_action(RequestContact(contact.id));
} }
ContactRequestStatus::Requested => {
cx.dispatch_action(RemoveContact(contact.id));
}
_ => {}
}) })
.with_cursor_style(CursorStyle::PointingHand)
.boxed(), .boxed(),
) )
.constrained() .constrained()
@ -415,6 +433,18 @@ impl ContactsPanel {
None None
})); }));
} }
fn request_contact(&mut self, request: &RequestContact, cx: &mut ViewContext<Self>) {
self.user_store
.update(cx, |store, cx| store.request_contact(request.0, cx))
.detach();
}
fn remove_contact(&mut self, request: &RemoveContact, cx: &mut ViewContext<Self>) {
self.user_store
.update(cx, |store, cx| store.remove_contact(request.0, cx))
.detach();
}
} }
pub enum Event {} pub enum Event {}

View file

@ -91,11 +91,12 @@ message Envelope {
UsersResponse users_response = 78; UsersResponse users_response = 78;
RequestContact request_contact = 79; RequestContact request_contact = 79;
RespondToContactRequest respond_to_contact_request = 80; RespondToContactRequest respond_to_contact_request = 80;
RemoveContact remove_contact = 81;
Follow follow = 81; Follow follow = 82;
FollowResponse follow_response = 82; FollowResponse follow_response = 83;
UpdateFollowers update_followers = 83; UpdateFollowers update_followers = 84;
Unfollow unfollow = 84; Unfollow unfollow = 85;
} }
} }
@ -553,6 +554,10 @@ message RequestContact {
uint64 responder_id = 1; uint64 responder_id = 1;
} }
message RemoveContact {
uint64 user_id = 1;
}
message RespondToContactRequest { message RespondToContactRequest {
uint64 requester_id = 1; uint64 requester_id = 1;
ContactRequestResponse response = 2; ContactRequestResponse response = 2;

View file

@ -82,6 +82,7 @@ messages!(
(ApplyCompletionAdditionalEditsResponse, Background), (ApplyCompletionAdditionalEditsResponse, Background),
(BufferReloaded, Foreground), (BufferReloaded, Foreground),
(BufferSaved, Foreground), (BufferSaved, Foreground),
(RemoveContact, Foreground),
(ChannelMessageSent, Foreground), (ChannelMessageSent, Foreground),
(CreateProjectEntry, Foreground), (CreateProjectEntry, Foreground),
(DeleteProjectEntry, Foreground), (DeleteProjectEntry, Foreground),
@ -188,6 +189,7 @@ request_messages!(
(RegisterWorktree, Ack), (RegisterWorktree, Ack),
(ReloadBuffers, ReloadBuffersResponse), (ReloadBuffers, ReloadBuffersResponse),
(RequestContact, Ack), (RequestContact, Ack),
(RemoveContact, Ack),
(RespondToContactRequest, Ack), (RespondToContactRequest, Ack),
(RenameProjectEntry, ProjectEntryResponse), (RenameProjectEntry, ProjectEntryResponse),
(SaveBuffer, BufferSaved), (SaveBuffer, BufferSaved),

View file

@ -146,6 +146,7 @@ fn main() {
go_to_line::init(cx); go_to_line::init(cx);
file_finder::init(cx); file_finder::init(cx);
chat_panel::init(cx); chat_panel::init(cx);
contacts_panel::init(cx);
outline::init(cx); outline::init(cx);
project_symbols::init(cx); project_symbols::init(cx);
project_panel::init(cx); project_panel::init(cx);