Move logic for joining project into a global action in `collab_ui`

Antonio Scandurra created

Change summary

crates/collab_ui/src/collab_ui.rs                   | 79 ++++++++++++++
crates/collab_ui/src/incoming_call_notification.rs  | 51 +++------
crates/collab_ui/src/project_shared_notification.rs | 52 ++-------
crates/gpui/src/app.rs                              |  4 
crates/workspace/src/workspace.rs                   |  6 
5 files changed, 115 insertions(+), 77 deletions(-)

Detailed changes

crates/collab_ui/src/collab_ui.rs 🔗

@@ -3,14 +3,87 @@ mod contacts_popover;
 mod incoming_call_notification;
 mod project_shared_notification;
 
+use call::ActiveCall;
 pub use collab_titlebar_item::CollabTitlebarItem;
 use gpui::MutableAppContext;
+use project::Project;
 use std::sync::Arc;
-use workspace::AppState;
+use workspace::{AppState, JoinProject, ToggleFollow, Workspace};
 
 pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
     contacts_popover::init(cx);
     collab_titlebar_item::init(cx);
-    incoming_call_notification::init(app_state.clone(), cx);
-    project_shared_notification::init(app_state, cx);
+    incoming_call_notification::init(app_state.user_store.clone(), cx);
+    project_shared_notification::init(cx);
+
+    cx.add_global_action(move |action: &JoinProject, cx| {
+        let project_id = action.project_id;
+        let follow_user_id = action.follow_user_id;
+        let app_state = app_state.clone();
+        cx.spawn(|mut cx| async move {
+            let existing_workspace = cx.update(|cx| {
+                cx.window_ids()
+                    .filter_map(|window_id| cx.root_view::<Workspace>(window_id))
+                    .find(|workspace| {
+                        workspace.read(cx).project().read(cx).remote_id() == Some(project_id)
+                    })
+            });
+
+            let workspace = if let Some(existing_workspace) = existing_workspace {
+                existing_workspace
+            } else {
+                let project = Project::remote(
+                    project_id,
+                    app_state.client.clone(),
+                    app_state.user_store.clone(),
+                    app_state.project_store.clone(),
+                    app_state.languages.clone(),
+                    app_state.fs.clone(),
+                    cx.clone(),
+                )
+                .await?;
+
+                let (_, workspace) = cx.add_window((app_state.build_window_options)(), |cx| {
+                    let mut workspace = Workspace::new(project, app_state.default_item_factory, cx);
+                    (app_state.initialize_workspace)(&mut workspace, &app_state, cx);
+                    workspace
+                });
+                workspace
+            };
+
+            cx.activate_window(workspace.window_id());
+
+            workspace.update(&mut cx, |workspace, cx| {
+                if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
+                    let follow_peer_id = room
+                        .read(cx)
+                        .remote_participants()
+                        .iter()
+                        .find(|(_, participant)| participant.user.id == follow_user_id)
+                        .map(|(peer_id, _)| *peer_id)
+                        .or_else(|| {
+                            // If we couldn't follow the given user, follow the host instead.
+                            let collaborator = workspace
+                                .project()
+                                .read(cx)
+                                .collaborators()
+                                .values()
+                                .find(|collaborator| collaborator.replica_id == 0)?;
+                            Some(collaborator.peer_id)
+                        });
+
+                    if let Some(follow_peer_id) = follow_peer_id {
+                        if !workspace.is_following(follow_peer_id) {
+                            workspace
+                                .toggle_follow(&ToggleFollow(follow_peer_id), cx)
+                                .map(|follow| follow.detach_and_log_err(cx));
+                        }
+                    }
+                }
+            });
+
+            anyhow::Ok(())
+        })
+        .detach_and_log_err(cx);
+    });
 }

crates/collab_ui/src/incoming_call_notification.rs 🔗

@@ -1,25 +1,22 @@
-use std::sync::Arc;
-
 use call::ActiveCall;
-use client::incoming_call::IncomingCall;
+use client::{incoming_call::IncomingCall, UserStore};
 use futures::StreamExt;
 use gpui::{
     elements::*,
     geometry::{rect::RectF, vector::vec2f},
-    impl_internal_actions, Entity, MouseButton, MutableAppContext, RenderContext, View,
-    ViewContext, WindowBounds, WindowKind, WindowOptions,
+    impl_internal_actions, Entity, ModelHandle, MouseButton, MutableAppContext, RenderContext,
+    View, ViewContext, WindowBounds, WindowKind, WindowOptions,
 };
-use project::Project;
 use settings::Settings;
 use util::ResultExt;
-use workspace::{AppState, Workspace};
+use workspace::JoinProject;
 
 impl_internal_actions!(incoming_call_notification, [RespondToCall]);
 
-pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
+pub fn init(user_store: ModelHandle<UserStore>, cx: &mut MutableAppContext) {
     cx.add_action(IncomingCallNotification::respond_to_call);
 
-    let mut incoming_call = app_state.user_store.read(cx).incoming_call();
+    let mut incoming_call = user_store.read(cx).incoming_call();
     cx.spawn(|mut cx| async move {
         let mut notification_window = None;
         while let Some(incoming_call) = incoming_call.next().await {
@@ -36,7 +33,7 @@ pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
                         kind: WindowKind::PopUp,
                         is_movable: false,
                     },
-                    |_| IncomingCallNotification::new(incoming_call, app_state.clone()),
+                    |_| IncomingCallNotification::new(incoming_call, user_store.clone()),
                 );
                 notification_window = Some(window_id);
             }
@@ -52,47 +49,35 @@ struct RespondToCall {
 
 pub struct IncomingCallNotification {
     call: IncomingCall,
-    app_state: Arc<AppState>,
+    user_store: ModelHandle<UserStore>,
 }
 
 impl IncomingCallNotification {
-    pub fn new(call: IncomingCall, app_state: Arc<AppState>) -> Self {
-        Self { call, app_state }
+    pub fn new(call: IncomingCall, user_store: ModelHandle<UserStore>) -> Self {
+        Self { call, user_store }
     }
 
     fn respond_to_call(&mut self, action: &RespondToCall, cx: &mut ViewContext<Self>) {
         if action.accept {
-            let app_state = self.app_state.clone();
             let join = ActiveCall::global(cx)
                 .update(cx, |active_call, cx| active_call.join(&self.call, cx));
+            let caller_user_id = self.call.caller.id;
             let initial_project_id = self.call.initial_project_id;
             cx.spawn_weak(|_, mut cx| async move {
                 join.await?;
-                if let Some(initial_project_id) = initial_project_id {
-                    let project = Project::remote(
-                        initial_project_id,
-                        app_state.client.clone(),
-                        app_state.user_store.clone(),
-                        app_state.project_store.clone(),
-                        app_state.languages.clone(),
-                        app_state.fs.clone(),
-                        cx.clone(),
-                    )
-                    .await?;
-
-                    cx.add_window((app_state.build_window_options)(), |cx| {
-                        let mut workspace =
-                            Workspace::new(project, app_state.default_item_factory, cx);
-                        (app_state.initialize_workspace)(&mut workspace, &app_state, cx);
-                        workspace
+                if let Some(project_id) = initial_project_id {
+                    cx.update(|cx| {
+                        cx.dispatch_global_action(JoinProject {
+                            project_id,
+                            follow_user_id: caller_user_id,
+                        })
                     });
                 }
                 anyhow::Ok(())
             })
             .detach_and_log_err(cx);
         } else {
-            self.app_state
-                .user_store
+            self.user_store
                 .update(cx, |user_store, _| user_store.decline_call().log_err());
         }
 

crates/collab_ui/src/project_shared_notification.rs 🔗

@@ -7,14 +7,13 @@ use gpui::{
     Entity, MouseButton, MutableAppContext, RenderContext, View, ViewContext, WindowBounds,
     WindowKind, WindowOptions,
 };
-use project::Project;
 use settings::Settings;
 use std::sync::Arc;
-use workspace::{AppState, Workspace};
+use workspace::JoinProject;
 
-actions!(project_shared_notification, [JoinProject, DismissProject]);
+actions!(project_shared_notification, [DismissProject]);
 
-pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
+pub fn init(cx: &mut MutableAppContext) {
     cx.add_action(ProjectSharedNotification::join);
     cx.add_action(ProjectSharedNotification::dismiss);
 
@@ -29,7 +28,7 @@ pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
                     kind: WindowKind::PopUp,
                     is_movable: false,
                 },
-                |_| ProjectSharedNotification::new(*project_id, owner.clone(), app_state.clone()),
+                |_| ProjectSharedNotification::new(*project_id, owner.clone()),
             );
         }
     })
@@ -39,45 +38,17 @@ pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
 pub struct ProjectSharedNotification {
     project_id: u64,
     owner: Arc<User>,
-    app_state: Arc<AppState>,
 }
 
 impl ProjectSharedNotification {
-    fn new(project_id: u64, owner: Arc<User>, app_state: Arc<AppState>) -> Self {
-        Self {
-            project_id,
-            owner,
-            app_state,
-        }
+    fn new(project_id: u64, owner: Arc<User>) -> Self {
+        Self { project_id, owner }
     }
 
     fn join(&mut self, _: &JoinProject, cx: &mut ViewContext<Self>) {
-        let project_id = self.project_id;
-        let app_state = self.app_state.clone();
-        cx.spawn_weak(|_, mut cx| async move {
-            let project = Project::remote(
-                project_id,
-                app_state.client.clone(),
-                app_state.user_store.clone(),
-                app_state.project_store.clone(),
-                app_state.languages.clone(),
-                app_state.fs.clone(),
-                cx.clone(),
-            )
-            .await?;
-
-            cx.add_window((app_state.build_window_options)(), |cx| {
-                let mut workspace = Workspace::new(project, app_state.default_item_factory, cx);
-                (app_state.initialize_workspace)(&mut workspace, &app_state, cx);
-                workspace
-            });
-
-            anyhow::Ok(())
-        })
-        .detach_and_log_err(cx);
-
         let window_id = cx.window_id();
         cx.remove_window(window_id);
+        cx.propagate_action();
     }
 
     fn dismiss(&mut self, _: &DismissProject, cx: &mut ViewContext<Self>) {
@@ -108,6 +79,8 @@ impl ProjectSharedNotification {
         enum Join {}
         enum Dismiss {}
 
+        let project_id = self.project_id;
+        let owner_user_id = self.owner.id;
         Flex::row()
             .with_child(
                 MouseEventHandler::<Join>::new(0, cx, |_, cx| {
@@ -117,8 +90,11 @@ impl ProjectSharedNotification {
                         .with_style(theme.join_button.container)
                         .boxed()
                 })
-                .on_click(MouseButton::Left, |_, cx| {
-                    cx.dispatch_action(JoinProject);
+                .on_click(MouseButton::Left, move |_, cx| {
+                    cx.dispatch_action(JoinProject {
+                        project_id,
+                        follow_user_id: owner_user_id,
+                    });
                 })
                 .boxed(),
             )

crates/gpui/src/app.rs 🔗

@@ -790,6 +790,10 @@ impl AsyncAppContext {
         self.update(|cx| cx.remove_window(window_id))
     }
 
+    pub fn activate_window(&mut self, window_id: usize) {
+        self.update(|cx| cx.activate_window(window_id))
+    }
+
     pub fn platform(&self) -> Arc<dyn Platform> {
         self.0.borrow().platform()
     }

crates/workspace/src/workspace.rs 🔗

@@ -13,7 +13,7 @@ mod toolbar;
 
 use anyhow::{anyhow, Context, Result};
 use call::ActiveCall;
-use client::{proto, Client, Contact, PeerId, TypedEnvelope, UserStore};
+use client::{proto, Client, PeerId, TypedEnvelope, UserStore};
 use collections::{hash_map, HashMap, HashSet};
 use dock::{DefaultItemFactory, Dock, ToggleDockButton};
 use drag_and_drop::DragAndDrop;
@@ -116,8 +116,8 @@ pub struct ToggleFollow(pub PeerId);
 
 #[derive(Clone, PartialEq)]
 pub struct JoinProject {
-    pub contact: Arc<Contact>,
-    pub project_index: usize,
+    pub project_id: u64,
+    pub follow_user_id: u64,
 }
 
 impl_internal_actions!(