Merge pull request #2077 from zed-industries/2064-remove-contacts

Petros Amoiridis created

Remove contact from contact list

Change summary

crates/collab/src/db.rs                      | 25 +++++++----
crates/collab/src/rpc.rs                     | 22 +++++++---
crates/collab/src/tests/integration_tests.rs | 21 +++++++++
crates/collab_ui/src/contact_list.rs         | 47 ++++++++++++++++++---
crates/collab_ui/src/contact_notification.rs |  2 
5 files changed, 92 insertions(+), 25 deletions(-)

Detailed changes

crates/collab/src/db.rs 🔗

@@ -595,7 +595,16 @@ impl Database {
         .await
     }
 
-    pub async fn remove_contact(&self, requester_id: UserId, responder_id: UserId) -> Result<()> {
+    /// Returns a bool indicating whether the removed contact had originally accepted or not
+    ///
+    /// Deletes the contact identified by the requester and responder ids, and then returns
+    /// whether the deleted contact had originally accepted or was a pending contact request.
+    ///
+    /// # Arguments
+    ///
+    /// * `requester_id` - The user that initiates this request
+    /// * `responder_id` - The user that will be removed
+    pub async fn remove_contact(&self, requester_id: UserId, responder_id: UserId) -> Result<bool> {
         self.transaction(|tx| async move {
             let (id_a, id_b) = if responder_id < requester_id {
                 (responder_id, requester_id)
@@ -603,20 +612,18 @@ impl Database {
                 (requester_id, responder_id)
             };
 
-            let result = contact::Entity::delete_many()
+            let contact = contact::Entity::find()
                 .filter(
                     contact::Column::UserIdA
                         .eq(id_a)
                         .and(contact::Column::UserIdB.eq(id_b)),
                 )
-                .exec(&*tx)
-                .await?;
+                .one(&*tx)
+                .await?
+                .ok_or_else(|| anyhow!("no such contact"))?;
 
-            if result.rows_affected == 1 {
-                Ok(())
-            } else {
-                Err(anyhow!("no such contact"))?
-            }
+            contact::Entity::delete_by_id(contact.id).exec(&*tx).await?;
+            Ok(contact.accepted)
         })
         .await
     }

crates/collab/src/rpc.rs 🔗

@@ -1961,23 +1961,31 @@ async fn remove_contact(
     let requester_id = session.user_id;
     let responder_id = UserId::from_proto(request.user_id);
     let db = session.db().await;
-    db.remove_contact(requester_id, responder_id).await?;
+    let contact_accepted = db.remove_contact(requester_id, responder_id).await?;
 
     let pool = session.connection_pool().await;
     // Update outgoing contact requests of requester
     let mut update = proto::UpdateContacts::default();
-    update
-        .remove_outgoing_requests
-        .push(responder_id.to_proto());
+    if contact_accepted {
+        update.remove_contacts.push(responder_id.to_proto());
+    } else {
+        update
+            .remove_outgoing_requests
+            .push(responder_id.to_proto());
+    }
     for connection_id in pool.user_connection_ids(requester_id) {
         session.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());
+    if contact_accepted {
+        update.remove_contacts.push(requester_id.to_proto());
+    } else {
+        update
+            .remove_incoming_requests
+            .push(requester_id.to_proto());
+    }
     for connection_id in pool.user_connection_ids(responder_id) {
         session.peer.send(connection_id, update.clone())?;
     }

crates/collab/src/tests/integration_tests.rs 🔗

@@ -5291,6 +5291,27 @@ async fn test_contacts(
         [("user_b".to_string(), "online", "free")]
     );
 
+    // Test removing a contact
+    client_b
+        .user_store
+        .update(cx_b, |store, cx| {
+            store.remove_contact(client_c.user_id().unwrap(), cx)
+        })
+        .await
+        .unwrap();
+    deterministic.run_until_parked();
+    assert_eq!(
+        contacts(&client_b, cx_b),
+        [
+            ("user_a".to_string(), "offline", "free"),
+            ("user_d".to_string(), "online", "free")
+        ]
+    );
+    assert_eq!(
+        contacts(&client_c, cx_c),
+        [("user_a".to_string(), "offline", "free"),]
+    );
+
     fn contacts(
         client: &TestClient,
         cx: &TestAppContext,

crates/collab_ui/src/contact_list.rs 🔗

@@ -1,22 +1,22 @@
-use std::{mem, sync::Arc};
-
 use crate::contacts_popover;
 use call::ActiveCall;
 use client::{proto::PeerId, Contact, User, UserStore};
 use editor::{Cancel, Editor};
+use futures::StreamExt;
 use fuzzy::{match_strings, StringMatchCandidate};
 use gpui::{
     elements::*,
     geometry::{rect::RectF, vector::vec2f},
     impl_actions, impl_internal_actions,
     keymap_matcher::KeymapContext,
-    AppContext, CursorStyle, Entity, ModelHandle, MouseButton, MutableAppContext, RenderContext,
-    Subscription, View, ViewContext, ViewHandle,
+    AppContext, CursorStyle, Entity, ModelHandle, MouseButton, MutableAppContext, PromptLevel,
+    RenderContext, Subscription, View, ViewContext, ViewHandle,
 };
 use menu::{Confirm, SelectNext, SelectPrev};
 use project::Project;
 use serde::Deserialize;
 use settings::Settings;
+use std::{mem, sync::Arc};
 use theme::IconButton;
 use util::ResultExt;
 use workspace::{JoinProject, OpenSharedScreen};
@@ -299,9 +299,19 @@ impl ContactList {
     }
 
     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();
+        let user_id = request.0;
+        let user_store = self.user_store.clone();
+        let prompt_message = "Are you sure you want to remove this contact?";
+        let mut answer = cx.prompt(PromptLevel::Warning, prompt_message, &["Remove", "Cancel"]);
+        cx.spawn(|_, mut cx| async move {
+            if answer.next().await == Some(0) {
+                user_store
+                    .update(&mut cx, |store, cx| store.remove_contact(user_id, cx))
+                    .await
+                    .unwrap();
+            }
+        })
+        .detach();
     }
 
     fn respond_to_contact_request(
@@ -1051,7 +1061,7 @@ impl ContactList {
         let user_id = contact.user.id;
         let initial_project = project.clone();
         let mut element =
-            MouseEventHandler::<Contact>::new(contact.user.id as usize, cx, |_, _| {
+            MouseEventHandler::<Contact>::new(contact.user.id as usize, cx, |_, cx| {
                 Flex::row()
                     .with_children(contact.user.avatar.clone().map(|avatar| {
                         let status_badge = if contact.online {
@@ -1093,6 +1103,27 @@ impl ContactList {
                         .flex(1., true)
                         .boxed(),
                     )
+                    .with_child(
+                        MouseEventHandler::<Cancel>::new(
+                            contact.user.id as usize,
+                            cx,
+                            |mouse_state, _| {
+                                let button_style =
+                                    theme.contact_button.style_for(mouse_state, false);
+                                render_icon_button(button_style, "icons/x_mark_8.svg")
+                                    .aligned()
+                                    .flex_float()
+                                    .boxed()
+                            },
+                        )
+                        .with_padding(Padding::uniform(2.))
+                        .with_cursor_style(CursorStyle::PointingHand)
+                        .on_click(MouseButton::Left, move |_, cx| {
+                            cx.dispatch_action(RemoveContact(user_id))
+                        })
+                        .flex_float()
+                        .boxed(),
+                    )
                     .with_children(if calling {
                         Some(
                             Label::new("Calling".to_string(), theme.calling_indicator.text.clone())

crates/collab_ui/src/contact_notification.rs 🔗

@@ -48,7 +48,7 @@ impl View for ContactNotification {
             ContactEventKind::Requested => render_user_notification(
                 self.user.clone(),
                 "wants to add you as a contact",
-                Some("They won't know if you decline."),
+                Some("They won't be alerted if you decline."),
                 Dismiss(self.user.id),
                 vec![
                     (