Emit event on `Room` when a user shares a new project

Antonio Scandurra created

Change summary

crates/call/src/call.rs                |  2 
crates/call/src/room.rs                | 40 ++++++++++-
crates/client/src/user.rs              |  2 
crates/collab/src/integration_tests.rs | 87 +++++++++++++++++++++++++++
4 files changed, 120 insertions(+), 11 deletions(-)

Detailed changes

crates/call/src/call.rs 🔗

@@ -1,5 +1,5 @@
 mod participant;
-mod room;
+pub mod room;
 
 use anyhow::{anyhow, Result};
 use client::{incoming_call::IncomingCall, Client, UserStore};

crates/call/src/room.rs 🔗

@@ -1,14 +1,15 @@
 use crate::participant::{ParticipantLocation, RemoteParticipant};
 use anyhow::{anyhow, Result};
 use client::{incoming_call::IncomingCall, proto, Client, PeerId, TypedEnvelope, User, UserStore};
-use collections::HashMap;
+use collections::{HashMap, HashSet};
 use futures::StreamExt;
 use gpui::{AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Task};
 use std::sync::Arc;
 use util::ResultExt;
 
+#[derive(Clone, Debug, PartialEq, Eq)]
 pub enum Event {
-    PeerChangedActiveProject,
+    RemoteProjectShared { owner: Arc<User>, project_id: u64 },
 }
 
 pub struct Room {
@@ -158,19 +159,46 @@ impl Room {
 
             this.update(&mut cx, |this, cx| {
                 if let Some(participants) = participants.log_err() {
-                    // TODO: compute diff instead of clearing participants
-                    this.remote_participants.clear();
+                    let mut seen_participants = HashSet::default();
+
                     for (participant, user) in room.participants.into_iter().zip(participants) {
+                        let peer_id = PeerId(participant.peer_id);
+                        seen_participants.insert(peer_id);
+
+                        let existing_project_ids = this
+                            .remote_participants
+                            .get(&peer_id)
+                            .map(|existing| existing.project_ids.clone())
+                            .unwrap_or_default();
+                        for project_id in &participant.project_ids {
+                            if !existing_project_ids.contains(project_id) {
+                                cx.emit(Event::RemoteProjectShared {
+                                    owner: user.clone(),
+                                    project_id: *project_id,
+                                });
+                            }
+                        }
+
                         this.remote_participants.insert(
-                            PeerId(participant.peer_id),
+                            peer_id,
                             RemoteParticipant {
-                                user,
+                                user: user.clone(),
                                 project_ids: participant.project_ids,
                                 location: ParticipantLocation::from_proto(participant.location)
                                     .unwrap_or(ParticipantLocation::External),
                             },
                         );
                     }
+
+                    for participant_peer_id in
+                        this.remote_participants.keys().copied().collect::<Vec<_>>()
+                    {
+                        if !seen_participants.contains(&participant_peer_id) {
+                            this.remote_participants.remove(&participant_peer_id);
+                        }
+                    }
+
+                    cx.notify();
                 }
 
                 if let Some(pending_users) = pending_users.log_err() {

crates/client/src/user.rs 🔗

@@ -9,7 +9,7 @@ use rpc::proto::{RequestMessage, UsersResponse};
 use std::sync::{Arc, Weak};
 use util::TryFutureExt as _;
 
-#[derive(Debug)]
+#[derive(Default, Debug)]
 pub struct User {
     pub id: u64,
     pub github_login: String,

crates/collab/src/integration_tests.rs 🔗

@@ -5,10 +5,10 @@ use crate::{
 };
 use ::rpc::Peer;
 use anyhow::anyhow;
-use call::Room;
+use call::{room, Room};
 use client::{
     self, test::FakeHttpClient, Channel, ChannelDetails, ChannelList, Client, Connection,
-    Credentials, EstablishConnectionError, UserStore, RECEIVE_TIMEOUT,
+    Credentials, EstablishConnectionError, User, UserStore, RECEIVE_TIMEOUT,
 };
 use collections::{BTreeMap, HashMap, HashSet};
 use editor::{
@@ -40,7 +40,8 @@ use serde_json::json;
 use settings::{Formatter, Settings};
 use sqlx::types::time::OffsetDateTime;
 use std::{
-    env,
+    cell::RefCell,
+    env, mem,
     ops::Deref,
     path::{Path, PathBuf},
     rc::Rc,
@@ -556,6 +557,86 @@ async fn test_host_disconnect(
     });
 }
 
+#[gpui::test(iterations = 10)]
+async fn test_room_events(
+    deterministic: Arc<Deterministic>,
+    cx_a: &mut TestAppContext,
+    cx_b: &mut TestAppContext,
+) {
+    deterministic.forbid_parking();
+    let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
+    let client_a = server.create_client(cx_a, "user_a").await;
+    let client_b = server.create_client(cx_b, "user_b").await;
+    client_a.fs.insert_tree("/a", json!({})).await;
+    client_b.fs.insert_tree("/b", json!({})).await;
+
+    let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
+    let (project_b, _) = client_b.build_local_project("/b", cx_b).await;
+
+    let (room_id, mut rooms) = server
+        .create_rooms(&mut [(&client_a, cx_a), (&client_b, cx_b)])
+        .await;
+
+    let room_a = rooms.remove(0);
+    let room_a_events = room_events(&room_a, cx_a);
+
+    let room_b = rooms.remove(0);
+    let room_b_events = room_events(&room_b, cx_b);
+
+    let project_a_id = project_a
+        .update(cx_a, |project, cx| project.share(room_id, cx))
+        .await
+        .unwrap();
+    deterministic.run_until_parked();
+    assert_eq!(mem::take(&mut *room_a_events.borrow_mut()), vec![]);
+    assert_eq!(
+        mem::take(&mut *room_b_events.borrow_mut()),
+        vec![room::Event::RemoteProjectShared {
+            owner: Arc::new(User {
+                id: client_a.user_id().unwrap(),
+                github_login: "user_a".to_string(),
+                avatar: None,
+            }),
+            project_id: project_a_id,
+        }]
+    );
+
+    let project_b_id = project_b
+        .update(cx_b, |project, cx| project.share(room_id, cx))
+        .await
+        .unwrap();
+    deterministic.run_until_parked();
+    assert_eq!(
+        mem::take(&mut *room_a_events.borrow_mut()),
+        vec![room::Event::RemoteProjectShared {
+            owner: Arc::new(User {
+                id: client_b.user_id().unwrap(),
+                github_login: "user_b".to_string(),
+                avatar: None,
+            }),
+            project_id: project_b_id,
+        }]
+    );
+    assert_eq!(mem::take(&mut *room_b_events.borrow_mut()), vec![]);
+
+    fn room_events(
+        room: &ModelHandle<Room>,
+        cx: &mut TestAppContext,
+    ) -> Rc<RefCell<Vec<room::Event>>> {
+        let events = Rc::new(RefCell::new(Vec::new()));
+        cx.update({
+            let events = events.clone();
+            |cx| {
+                cx.subscribe(room, move |_, event, _| {
+                    events.borrow_mut().push(event.clone())
+                })
+                .detach()
+            }
+        });
+        events
+    }
+}
+
 #[gpui::test(iterations = 10)]
 async fn test_propagate_saves_and_fs_changes(
     cx_a: &mut TestAppContext,