From e3ee19b123b919fe360f422fe407595f5fbaee5a Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 9 May 2022 11:24:05 -0600 Subject: [PATCH] Wire up UI for requesting contacts and cancelling requests Co-Authored-By: Max Brunsfeld --- crates/client/src/user.rs | 98 ++++++++++++++++++--- crates/collab/src/db.rs | 32 +++++++ crates/collab/src/rpc.rs | 50 +++++++++-- crates/contacts_panel/src/contacts_panel.rs | 48 ++++++++-- crates/rpc/proto/zed.proto | 13 ++- crates/rpc/src/proto.rs | 2 + crates/zed/src/main.rs | 1 + 7 files changed, 212 insertions(+), 32 deletions(-) diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index a32a4b179a70b60766157e761b863e14ea461898..ef38c6e2dafc3536918d4f2fdc780b90bec34142 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -5,7 +5,7 @@ use gpui::{AsyncAppContext, Entity, ImageData, ModelContext, ModelHandle, Task}; use postage::{prelude::Stream, sink::Sink, watch}; use rpc::proto::{RequestMessage, UsersResponse}; use std::{ - collections::{HashMap, HashSet}, + collections::{hash_map::Entry, HashMap, HashSet}, sync::{Arc, Weak}, }; use util::TryFutureExt as _; @@ -31,6 +31,14 @@ pub struct ProjectMetadata { pub guests: Vec>, } +#[derive(Debug, Clone, Copy)] +pub enum ContactRequestStatus { + None, + SendingRequest, + Requested, + RequestAccepted, +} + pub struct UserStore { users: HashMap>, update_contacts_tx: watch::Sender>, @@ -38,6 +46,7 @@ pub struct UserStore { contacts: Vec>, incoming_contact_requests: Vec>, outgoing_contact_requests: Vec>, + pending_contact_requests: HashMap, client: Weak, http: Arc, _maintain_contacts: Task<()>, @@ -100,6 +109,7 @@ impl UserStore { } } }), + pending_contact_requests: Default::default(), } } @@ -237,23 +247,85 @@ impl UserStore { &self.outgoing_contact_requests } - pub fn has_outgoing_contact_request(&self, user: &User) -> bool { - self.outgoing_contact_requests - .binary_search_by_key(&&user.github_login, |requested_user| { - &requested_user.github_login - }) + pub fn contact_request_status(&self, user: &User) -> ContactRequestStatus { + if self + .contacts + .binary_search_by_key(&&user.id, |contact| &contact.user.id) .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> { + pub fn request_contact( + &mut self, + responder_id: u64, + cx: &mut ModelContext, + ) -> Task> { let client = self.client.upgrade(); - async move { - client - .ok_or_else(|| anyhow!("not logged in"))? - .request(proto::RequestContact { responder_id }) - .await?; + *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( + &mut self, + user_id: u64, + cx: &mut ModelContext, + ) -> Task> { + 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( diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index fd84f39bab9041ece9fbc16fc535255c0c18b945..e5b3b31571bd2d3457aec0e92bfdc76640a071a4 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -19,6 +19,7 @@ pub trait Db: Send + Sync { async fn get_contacts(&self, 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( &self, 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( &self, responder_id: UserId, @@ -1248,6 +1273,13 @@ pub mod tests { 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( &self, responder_id: UserId, diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 548744ca767777fe954404b64fd32273af5a7ee7..6dabc63eaa103511946791efa405c77a0be00603 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -155,6 +155,7 @@ impl Server { .add_request_handler(Server::get_users) .add_request_handler(Server::fuzzy_search_users) .add_request_handler(Server::request_contact) + .add_request_handler(Server::remove_contact) .add_request_handler(Server::respond_to_contact_request) .add_request_handler(Server::join_channel) .add_message_handler(Server::leave_channel) @@ -1048,6 +1049,43 @@ impl Server { Ok(()) } + async fn remove_contact( + self: Arc, + request: TypedEnvelope, + response: Response, + ) -> 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))] // fn update_contacts_for_users<'a>( // self: &Arc, @@ -5138,15 +5176,15 @@ mod tests { // User A and User C request that user B become their contact. client_a .user_store - .read_with(cx_a, |store, _| { - store.request_contact(client_b.user_id().unwrap()) + .update(cx_a, |store, cx| { + store.request_contact(client_b.user_id().unwrap(), cx) }) .await .unwrap(); client_c .user_store - .read_with(cx_c, |store, _| { - store.request_contact(client_b.user_id().unwrap()) + .update(cx_c, |store, cx| { + store.request_contact(client_b.user_id().unwrap(), cx) }) .await .unwrap(); @@ -6460,8 +6498,8 @@ mod tests { for (client_b, cx_b) in &mut clients { client_a .user_store - .update(cx_a, |store, _| { - store.request_contact(client_b.user_id().unwrap()) + .update(cx_a, |store, cx| { + store.request_contact(client_b.user_id().unwrap(), cx) }) .await .unwrap(); diff --git a/crates/contacts_panel/src/contacts_panel.rs b/crates/contacts_panel/src/contacts_panel.rs index affdded6e5ce57c3dfa08a117f8398a71d149b68..eb64afb2d57b21ea0c9b8210616da4ad5496e9bd 100644 --- a/crates/contacts_panel/src/contacts_panel.rs +++ b/crates/contacts_panel/src/contacts_panel.rs @@ -1,4 +1,4 @@ -use client::{Contact, User, UserStore}; +use client::{Contact, ContactRequestStatus, User, UserStore}; use editor::Editor; use fuzzy::StringMatchCandidate; use gpui::{ @@ -7,8 +7,8 @@ use gpui::{ geometry::{rect::RectF, vector::vec2f}, impl_actions, platform::CursorStyle, - Element, ElementBox, Entity, LayoutContext, ModelHandle, RenderContext, Subscription, Task, - View, ViewContext, ViewHandle, + Element, ElementBox, Entity, LayoutContext, ModelHandle, MutableAppContext, RenderContext, + Subscription, Task, View, ViewContext, ViewHandle, }; use serde::Deserialize; use settings::Settings; @@ -16,7 +16,7 @@ use std::sync::Arc; use util::ResultExt; use workspace::{AppState, JoinProject}; -impl_actions!(contacts_panel, [RequestContact]); +impl_actions!(contacts_panel, [RequestContact, RemoveContact]); pub struct ContactsPanel { list_state: ListState, @@ -31,6 +31,14 @@ pub struct ContactsPanel { #[derive(Clone, Deserialize)] 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 { pub fn new(app_state: Arc, cx: &mut ViewContext) -> Self { let user_query_editor = cx.add_view(|cx| { @@ -295,7 +303,7 @@ impl ContactsPanel { ) -> ElementBox { 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() .with_children(contact.avatar.clone().map(|avatar| { @@ -321,7 +329,13 @@ impl ContactsPanel { contact.id as usize, 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()) .contained() .with_style(theme.edit_contact.container) @@ -330,12 +344,16 @@ impl ContactsPanel { .boxed() }, ) - .on_click(move |_, cx| { - if requested_contact { - } else { + .on_click(move |_, cx| match request_status { + ContactRequestStatus::None => { cx.dispatch_action(RequestContact(contact.id)); } + ContactRequestStatus::Requested => { + cx.dispatch_action(RemoveContact(contact.id)); + } + _ => {} }) + .with_cursor_style(CursorStyle::PointingHand) .boxed(), ) .constrained() @@ -415,6 +433,18 @@ impl ContactsPanel { None })); } + + fn request_contact(&mut self, request: &RequestContact, cx: &mut ViewContext) { + 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.user_store + .update(cx, |store, cx| store.remove_contact(request.0, cx)) + .detach(); + } } pub enum Event {} diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 97e0930f4fa2055d812652469f3745ba54ee42c6..55bdba975159bb5b912d849ce10afaa5fb617128 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -91,11 +91,12 @@ message Envelope { UsersResponse users_response = 78; RequestContact request_contact = 79; RespondToContactRequest respond_to_contact_request = 80; + RemoveContact remove_contact = 81; - Follow follow = 81; - FollowResponse follow_response = 82; - UpdateFollowers update_followers = 83; - Unfollow unfollow = 84; + Follow follow = 82; + FollowResponse follow_response = 83; + UpdateFollowers update_followers = 84; + Unfollow unfollow = 85; } } @@ -553,6 +554,10 @@ message RequestContact { uint64 responder_id = 1; } +message RemoveContact { + uint64 user_id = 1; +} + message RespondToContactRequest { uint64 requester_id = 1; ContactRequestResponse response = 2; diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index 2674e8a0d8e2f36715339f5779219187388200e1..0b7ba21c4a22419d1f13ba909bbaeab7e24c512b 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -82,6 +82,7 @@ messages!( (ApplyCompletionAdditionalEditsResponse, Background), (BufferReloaded, Foreground), (BufferSaved, Foreground), + (RemoveContact, Foreground), (ChannelMessageSent, Foreground), (CreateProjectEntry, Foreground), (DeleteProjectEntry, Foreground), @@ -188,6 +189,7 @@ request_messages!( (RegisterWorktree, Ack), (ReloadBuffers, ReloadBuffersResponse), (RequestContact, Ack), + (RemoveContact, Ack), (RespondToContactRequest, Ack), (RenameProjectEntry, ProjectEntryResponse), (SaveBuffer, BufferSaved), diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 53a0a92a2534ed091cb24638f2fb3e9b0269b86f..a4f85ab9bc7467b108092c01db7a26d5920098d9 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -146,6 +146,7 @@ fn main() { go_to_line::init(cx); file_finder::init(cx); chat_panel::init(cx); + contacts_panel::init(cx); outline::init(cx); project_symbols::init(cx); project_panel::init(cx);