Show notification when a new project is shared and allow joining it

Antonio Scandurra created

Change summary

crates/collab_ui/src/collab_ui.rs                   |  11 
crates/collab_ui/src/project_shared_notification.rs | 174 +++++++++++++++
crates/theme/src/theme.rs                           |   9 
crates/zed/src/main.rs                              |   2 
styles/src/styleTree/app.ts                         |   2 
styles/src/styleTree/projectSharedNotification.ts   |  22 +
6 files changed, 215 insertions(+), 5 deletions(-)

Detailed changes

crates/collab_ui/src/collab_ui.rs 🔗

@@ -1,13 +1,16 @@
 mod collab_titlebar_item;
 mod contacts_popover;
 mod incoming_call_notification;
+mod project_shared_notification;
 
-use client::UserStore;
 pub use collab_titlebar_item::CollabTitlebarItem;
-use gpui::{ModelHandle, MutableAppContext};
+use gpui::MutableAppContext;
+use std::sync::Arc;
+use workspace::AppState;
 
-pub fn init(user_store: ModelHandle<UserStore>, cx: &mut MutableAppContext) {
+pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
     contacts_popover::init(cx);
     collab_titlebar_item::init(cx);
-    incoming_call_notification::init(user_store, cx);
+    incoming_call_notification::init(app_state.user_store.clone(), cx);
+    project_shared_notification::init(app_state, cx);
 }

crates/collab_ui/src/project_shared_notification.rs 🔗

@@ -0,0 +1,174 @@
+use call::{room, ActiveCall};
+use client::User;
+use gpui::{
+    actions,
+    elements::*,
+    geometry::{rect::RectF, vector::vec2f},
+    Entity, MouseButton, MutableAppContext, RenderContext, View, ViewContext, WindowBounds,
+    WindowKind, WindowOptions,
+};
+use project::Project;
+use settings::Settings;
+use std::sync::Arc;
+use workspace::{AppState, Workspace};
+
+actions!(project_shared_notification, [JoinProject, DismissProject]);
+
+pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
+    cx.add_action(ProjectSharedNotification::join);
+    cx.add_action(ProjectSharedNotification::dismiss);
+
+    let active_call = ActiveCall::global(cx);
+    let mut _room_subscription = None;
+    cx.observe(&active_call, move |active_call, cx| {
+        if let Some(room) = active_call.read(cx).room().cloned() {
+            let app_state = app_state.clone();
+            _room_subscription = Some(cx.subscribe(&room, move |_, event, cx| match event {
+                room::Event::RemoteProjectShared { owner, project_id } => {
+                    cx.add_window(
+                        WindowOptions {
+                            bounds: WindowBounds::Fixed(RectF::new(
+                                vec2f(0., 0.),
+                                vec2f(300., 400.),
+                            )),
+                            titlebar: None,
+                            center: true,
+                            kind: WindowKind::PopUp,
+                            is_movable: false,
+                        },
+                        |_| {
+                            ProjectSharedNotification::new(
+                                *project_id,
+                                owner.clone(),
+                                app_state.clone(),
+                            )
+                        },
+                    );
+                }
+            }));
+        } else {
+            _room_subscription = None;
+        }
+    })
+    .detach();
+}
+
+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 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);
+    }
+
+    fn dismiss(&mut self, _: &DismissProject, cx: &mut ViewContext<Self>) {
+        let window_id = cx.window_id();
+        cx.remove_window(window_id);
+    }
+
+    fn render_owner(&self, cx: &mut RenderContext<Self>) -> ElementBox {
+        let theme = &cx.global::<Settings>().theme.project_shared_notification;
+        Flex::row()
+            .with_children(
+                self.owner
+                    .avatar
+                    .clone()
+                    .map(|avatar| Image::new(avatar).with_style(theme.owner_avatar).boxed()),
+            )
+            .with_child(
+                Label::new(
+                    format!("{} has shared a new project", self.owner.github_login),
+                    theme.message.text.clone(),
+                )
+                .boxed(),
+            )
+            .boxed()
+    }
+
+    fn render_buttons(&self, cx: &mut RenderContext<Self>) -> ElementBox {
+        enum Join {}
+        enum Dismiss {}
+
+        Flex::row()
+            .with_child(
+                MouseEventHandler::<Join>::new(0, cx, |_, cx| {
+                    let theme = &cx.global::<Settings>().theme.project_shared_notification;
+                    Label::new("Join".to_string(), theme.join_button.text.clone())
+                        .contained()
+                        .with_style(theme.join_button.container)
+                        .boxed()
+                })
+                .on_click(MouseButton::Left, |_, cx| {
+                    cx.dispatch_action(JoinProject);
+                })
+                .boxed(),
+            )
+            .with_child(
+                MouseEventHandler::<Dismiss>::new(0, cx, |_, cx| {
+                    let theme = &cx.global::<Settings>().theme.project_shared_notification;
+                    Label::new("Dismiss".to_string(), theme.dismiss_button.text.clone())
+                        .contained()
+                        .with_style(theme.dismiss_button.container)
+                        .boxed()
+                })
+                .on_click(MouseButton::Left, |_, cx| {
+                    cx.dispatch_action(DismissProject);
+                })
+                .boxed(),
+            )
+            .boxed()
+    }
+}
+
+impl Entity for ProjectSharedNotification {
+    type Event = ();
+}
+
+impl View for ProjectSharedNotification {
+    fn ui_name() -> &'static str {
+        "ProjectSharedNotification"
+    }
+
+    fn render(&mut self, cx: &mut RenderContext<Self>) -> gpui::ElementBox {
+        Flex::row()
+            .with_child(self.render_owner(cx))
+            .with_child(self.render_buttons(cx))
+            .boxed()
+    }
+}

crates/theme/src/theme.rs 🔗

@@ -30,6 +30,7 @@ pub struct Theme {
     pub breadcrumbs: ContainedText,
     pub contact_notification: ContactNotification,
     pub update_notification: UpdateNotification,
+    pub project_shared_notification: ProjectSharedNotification,
     pub tooltip: TooltipStyle,
     pub terminal: TerminalStyle,
 }
@@ -481,6 +482,14 @@ pub struct UpdateNotification {
     pub dismiss_button: Interactive<IconButton>,
 }
 
+#[derive(Deserialize, Default)]
+pub struct ProjectSharedNotification {
+    pub owner_avatar: ImageStyle,
+    pub message: ContainedText,
+    pub join_button: ContainedText,
+    pub dismiss_button: ContainedText,
+}
+
 #[derive(Clone, Deserialize, Default)]
 pub struct Editor {
     pub text_color: Color,

crates/zed/src/main.rs 🔗

@@ -107,7 +107,6 @@ fn main() {
         project::Project::init(&client);
         client::Channel::init(&client);
         client::init(client.clone(), cx);
-        collab_ui::init(user_store.clone(), cx);
         command_palette::init(cx);
         editor::init(cx);
         go_to_line::init(cx);
@@ -157,6 +156,7 @@ fn main() {
         journal::init(app_state.clone(), cx);
         theme_selector::init(app_state.clone(), cx);
         zed::init(&app_state, cx);
+        collab_ui::init(app_state.clone(), cx);
 
         cx.set_menus(menus::menus());
 

styles/src/styleTree/app.ts 🔗

@@ -14,6 +14,7 @@ import contextMenu from "./contextMenu";
 import projectDiagnostics from "./projectDiagnostics";
 import contactNotification from "./contactNotification";
 import updateNotification from "./updateNotification";
+import projectSharedNotification from "./projectSharedNotification";
 import tooltip from "./tooltip";
 import terminal from "./terminal";
 
@@ -47,6 +48,7 @@ export default function app(theme: Theme): Object {
     },
     contactNotification: contactNotification(theme),
     updateNotification: updateNotification(theme),
+    projectSharedNotification: projectSharedNotification(theme),
     tooltip: tooltip(theme),
     terminal: terminal(theme),
   };

styles/src/styleTree/projectSharedNotification.ts 🔗

@@ -0,0 +1,22 @@
+import Theme from "../themes/common/theme";
+import { text } from "./components";
+
+export default function projectSharedNotification(theme: Theme): Object {
+  const avatarSize = 12;
+  return {
+    ownerAvatar: {
+      height: avatarSize,
+      width: avatarSize,
+      cornerRadius: 6,
+    },
+    message: {
+      ...text(theme, "sans", "primary", { size: "xs" }),
+    },
+    joinButton: {
+      ...text(theme, "sans", "primary", { size: "xs" })
+    },
+    dismissButton: {
+      ...text(theme, "sans", "primary", { size: "xs" })
+    },
+  };
+}