Refine messages on waiting to join screen and include host avatar

Nathan Sobo created

Change summary

assets/themes/cave-dark.json                |  4 
assets/themes/cave-light.json               |  4 
assets/themes/dark.json                     |  4 
assets/themes/light.json                    |  4 
assets/themes/solarized-dark.json           |  4 
assets/themes/solarized-light.json          |  4 
assets/themes/sulphurpool-dark.json         |  4 
assets/themes/sulphurpool-light.json        |  4 
crates/contacts_panel/src/contacts_panel.rs | 17 ++-
crates/theme/src/theme.rs                   |  1 
crates/workspace/src/workspace.rs           | 93 +++++++++++++++++-----
script/watch-themes                         |  7 +
styles/src/styleTree/workspace.ts           |  4 
13 files changed, 125 insertions(+), 29 deletions(-)

Detailed changes

assets/themes/cave-dark.json 🔗

@@ -90,6 +90,10 @@
   },
   "workspace": {
     "background": "#26232a",
+    "joining_project_avatar": {
+      "corner_radius": 40,
+      "width": 80
+    },
     "joining_project_message": {
       "padding": 12,
       "family": "Zed Sans",

assets/themes/cave-light.json 🔗

@@ -90,6 +90,10 @@
   },
   "workspace": {
     "background": "#e2dfe7",
+    "joining_project_avatar": {
+      "corner_radius": 40,
+      "width": 80
+    },
     "joining_project_message": {
       "padding": 12,
       "family": "Zed Sans",

assets/themes/dark.json 🔗

@@ -90,6 +90,10 @@
   },
   "workspace": {
     "background": "#1c1c1c",
+    "joining_project_avatar": {
+      "corner_radius": 40,
+      "width": 80
+    },
     "joining_project_message": {
       "padding": 12,
       "family": "Zed Sans",

assets/themes/light.json 🔗

@@ -90,6 +90,10 @@
   },
   "workspace": {
     "background": "#f8f8f8",
+    "joining_project_avatar": {
+      "corner_radius": 40,
+      "width": 80
+    },
     "joining_project_message": {
       "padding": 12,
       "family": "Zed Sans",

assets/themes/solarized-dark.json 🔗

@@ -90,6 +90,10 @@
   },
   "workspace": {
     "background": "#073642",
+    "joining_project_avatar": {
+      "corner_radius": 40,
+      "width": 80
+    },
     "joining_project_message": {
       "padding": 12,
       "family": "Zed Sans",

assets/themes/solarized-light.json 🔗

@@ -90,6 +90,10 @@
   },
   "workspace": {
     "background": "#eee8d5",
+    "joining_project_avatar": {
+      "corner_radius": 40,
+      "width": 80
+    },
     "joining_project_message": {
       "padding": 12,
       "family": "Zed Sans",

assets/themes/sulphurpool-dark.json 🔗

@@ -90,6 +90,10 @@
   },
   "workspace": {
     "background": "#293256",
+    "joining_project_avatar": {
+      "corner_radius": 40,
+      "width": 80
+    },
     "joining_project_message": {
       "padding": 12,
       "family": "Zed Sans",

assets/themes/sulphurpool-light.json 🔗

@@ -90,6 +90,10 @@
   },
   "workspace": {
     "background": "#dfe2f1",
+    "joining_project_avatar": {
+      "corner_radius": 40,
+      "width": 80
+    },
     "joining_project_message": {
       "padding": 12,
       "family": "Zed Sans",

crates/contacts_panel/src/contacts_panel.rs 🔗

@@ -336,14 +336,14 @@ impl ContactsPanel {
     fn render_contact_project(
         contact: Arc<Contact>,
         current_user_id: Option<u64>,
-        project_ix: usize,
+        project_index: usize,
         app_state: Arc<AppState>,
         theme: &theme::ContactsPanel,
         is_last_project: bool,
         is_selected: bool,
         cx: &mut LayoutContext,
     ) -> ElementBox {
-        let project = &contact.projects[project_ix];
+        let project = &contact.projects[project_index];
         let project_id = project.id;
         let is_host = Some(contact.user.id) == current_user_id;
         let is_guest = !is_host
@@ -445,7 +445,8 @@ impl ContactsPanel {
         .on_click(move |_, cx| {
             if !is_host && !is_guest {
                 cx.dispatch_global_action(JoinProject {
-                    project_id,
+                    contact: contact.clone(),
+                    project_index,
                     app_state: app_state.clone(),
                 });
             }
@@ -768,12 +769,12 @@ impl ContactsPanel {
                         let section = *section;
                         self.toggle_expanded(&ToggleExpanded(section), cx);
                     }
-                    ContactEntry::ContactProject(contact, project_ix) => {
-                        cx.dispatch_global_action(JoinProject {
-                            project_id: contact.projects[*project_ix].id,
+                    ContactEntry::ContactProject(contact, project_index) => cx
+                        .dispatch_global_action(JoinProject {
+                            contact: contact.clone(),
+                            project_index: *project_index,
                             app_state: self.app_state.clone(),
-                        })
-                    }
+                        }),
                     _ => {}
                 }
             }

crates/theme/src/theme.rs 🔗

@@ -48,6 +48,7 @@ pub struct Workspace {
     pub modal: ContainerStyle,
     pub notification: ContainerStyle,
     pub notifications: Notifications,
+    pub joining_project_avatar: ImageStyle,
     pub joining_project_message: ContainedText,
 }
 

crates/workspace/src/workspace.rs 🔗

@@ -8,7 +8,8 @@ mod toolbar;
 
 use anyhow::{anyhow, Context, Result};
 use client::{
-    proto, Authenticate, ChannelList, Client, PeerId, Subscription, TypedEnvelope, User, UserStore,
+    proto, Authenticate, ChannelList, Client, Contact, PeerId, Subscription, TypedEnvelope, User,
+    UserStore,
 };
 use clock::ReplicaId;
 use collections::{hash_map, HashMap, HashSet};
@@ -97,7 +98,8 @@ pub struct ToggleFollow(pub PeerId);
 
 #[derive(Clone)]
 pub struct JoinProject {
-    pub project_id: u64,
+    pub contact: Arc<Contact>,
+    pub project_index: usize,
     pub app_state: Arc<AppState>,
 }
 
@@ -117,7 +119,13 @@ pub fn init(client: &Arc<Client>, cx: &mut MutableAppContext) {
         open_new(&action.0, cx)
     });
     cx.add_global_action(move |action: &JoinProject, cx: &mut MutableAppContext| {
-        join_project(action.project_id, &action.app_state, cx).detach();
+        join_project(
+            action.contact.clone(),
+            action.project_index,
+            &action.app_state,
+            cx,
+        )
+        .detach();
     });
 
     cx.add_async_action(Workspace::toggle_follow);
@@ -2268,12 +2276,16 @@ pub fn open_paths(
 }
 
 pub fn join_project(
-    project_id: u64,
+    contact: Arc<Contact>,
+    project_index: usize,
     app_state: &Arc<AppState>,
     cx: &mut MutableAppContext,
 ) -> Task<Result<ViewHandle<Workspace>>> {
+    let project_id = contact.projects[project_index].id;
+
     struct JoiningNotice {
-        message: &'static str,
+        avatar: Option<Arc<ImageData>>,
+        message: String,
     }
 
     impl Entity for JoiningNotice {
@@ -2287,16 +2299,28 @@ pub fn join_project(
 
         fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
             let theme = &cx.global::<Settings>().theme.workspace;
-            Text::new(
-                self.message.to_string(),
-                theme.joining_project_message.text.clone(),
-            )
-            .contained()
-            .with_style(theme.joining_project_message.container)
-            .aligned()
-            .contained()
-            .with_background_color(theme.background)
-            .boxed()
+
+            Flex::column()
+                .with_children(self.avatar.clone().map(|avatar| {
+                    Image::new(avatar)
+                        .with_style(theme.joining_project_avatar)
+                        .aligned()
+                        .boxed()
+                }))
+                .with_child(
+                    Text::new(
+                        self.message.clone(),
+                        theme.joining_project_message.text.clone(),
+                    )
+                    .contained()
+                    .with_style(theme.joining_project_message.container)
+                    .aligned()
+                    .boxed(),
+                )
+                .aligned()
+                .contained()
+                .with_background_color(theme.background)
+                .boxed()
         }
     }
 
@@ -2312,7 +2336,12 @@ pub fn join_project(
     cx.spawn(|mut cx| async move {
         let (window, joining_notice) = cx.update(|cx| {
             cx.add_window((app_state.build_window_options)(), |_| JoiningNotice {
-                message: "Loading remote project...",
+                avatar: contact.user.avatar.clone(),
+                message: format!(
+                    "Asking to join @{}'s copy of {}...",
+                    contact.user.github_login,
+                    humanize_list(&contact.projects[project_index].worktree_root_names)
+                ),
             })
         });
         let project = Project::remote(
@@ -2338,15 +2367,22 @@ pub fn join_project(
                 workspace
             })),
             Err(error @ _) => {
+                let login = &contact.user.github_login;
                 let message = match error {
                     project::JoinProjectError::HostDeclined => {
-                        "The host declined your request to join."
+                        format!("@{} declined your request.", login)
+                    }
+                    project::JoinProjectError::HostClosedProject => {
+                        format!(
+                            "@{} closed their copy of {}.",
+                            login,
+                            humanize_list(&contact.projects[project_index].worktree_root_names)
+                        )
                     }
-                    project::JoinProjectError::HostClosedProject => "The host closed the project.",
-                    project::JoinProjectError::HostWentOffline => "The host went offline.",
-                    project::JoinProjectError::Other(_) => {
-                        "An error occurred when attempting to join the project."
+                    project::JoinProjectError::HostWentOffline => {
+                        format!("@{} went offline.", login)
                     }
+                    project::JoinProjectError::Other(_) => "An error occurred.".to_string(),
                 };
                 joining_notice.update(cx, |notice, cx| {
                     notice.message = message;
@@ -2372,3 +2408,18 @@ fn open_new(app_state: &Arc<AppState>, cx: &mut MutableAppContext) {
     });
     cx.dispatch_action(window_id, vec![workspace.id()], &OpenNew(app_state.clone()));
 }
+
+fn humanize_list<'a>(items: impl IntoIterator<Item = &'a String>) -> String {
+    let mut list = String::new();
+    let mut items = items.into_iter().enumerate().peekable();
+    while let Some((ix, item)) = items.next() {
+        if ix > 0 {
+            list.push_str(", ");
+        }
+        if items.peek().is_none() {
+            list.push_str("and ");
+        }
+        list.push_str(item);
+    }
+    list
+}

styles/src/styleTree/workspace.ts 🔗

@@ -41,6 +41,10 @@ export default function workspace(theme: Theme) {
 
   return {
     background: backgroundColor(theme, 300),
+    joiningProjectAvatar: {
+      cornerRadius: 40,
+      width: 80,
+    },
     joiningProjectMessage: {
       padding: 12,
       ...text(theme, "sans", "primary", { size: "lg" })