Improve layout and styling of contact notifications

Max Brunsfeld and Nathan Sobo created

Co-authored-by: Nathan Sobo <nathan@zed.dev>

Change summary

assets/icons/decline.svg                          |  0 
assets/themes/cave-dark.json                      | 14 ++
assets/themes/cave-light.json                     | 14 ++
assets/themes/dark.json                           | 14 ++
assets/themes/light.json                          | 14 ++
assets/themes/solarized-dark.json                 | 14 ++
assets/themes/solarized-light.json                | 14 ++
assets/themes/sulphurpool-dark.json               | 14 ++
assets/themes/sulphurpool-light.json              | 14 ++
crates/client/src/user.rs                         |  2 
crates/contacts_panel/src/contact_finder.rs       |  2 
crates/contacts_panel/src/contact_notification.rs | 67 ++++++++++------
crates/contacts_panel/src/contacts_panel.rs       |  8 +-
crates/rpc/proto/zed.proto                        |  2 
crates/theme/src/theme.rs                         |  4 
crates/workspace/src/workspace.rs                 |  2 
styles/src/styleTree/contactNotification.ts       | 17 +++-
styles/src/styleTree/workspace.ts                 |  2 
18 files changed, 152 insertions(+), 66 deletions(-)

Detailed changes

assets/themes/cave-dark.json 🔗

@@ -505,7 +505,7 @@
       }
     },
     "notifications": {
-      "width": 256,
+      "width": 380,
       "margin": {
         "right": 10,
         "bottom": 10
@@ -1698,7 +1698,8 @@
       "color": "#e2dfe7",
       "size": 12,
       "margin": {
-        "left": 4
+        "left": 8,
+        "right": 8
       }
     },
     "header_height": 18,
@@ -1707,6 +1708,7 @@
       "color": "#8b8792",
       "size": 12,
       "margin": {
+        "left": 20,
         "top": 6,
         "bottom": 6
       }
@@ -1720,6 +1722,9 @@
       "corner_radius": 6,
       "margin": {
         "left": 6
+      },
+      "hover": {
+        "background": "#26232a3d"
       }
     },
     "dismiss_button": {
@@ -1727,7 +1732,10 @@
       "icon_width": 8,
       "icon_height": 8,
       "button_width": 8,
-      "button_height": 8
+      "button_height": 8,
+      "hover": {
+        "color": "#e2dfe7"
+      }
     }
   }
 }

assets/themes/cave-light.json 🔗

@@ -505,7 +505,7 @@
       }
     },
     "notifications": {
-      "width": 256,
+      "width": 380,
       "margin": {
         "right": 10,
         "bottom": 10
@@ -1698,7 +1698,8 @@
       "color": "#26232a",
       "size": 12,
       "margin": {
-        "left": 4
+        "left": 8,
+        "right": 8
       }
     },
     "header_height": 18,
@@ -1707,6 +1708,7 @@
       "color": "#585260",
       "size": 12,
       "margin": {
+        "left": 20,
         "top": 6,
         "bottom": 6
       }
@@ -1720,6 +1722,9 @@
       "corner_radius": 6,
       "margin": {
         "left": 6
+      },
+      "hover": {
+        "background": "#e2dfe71f"
       }
     },
     "dismiss_button": {
@@ -1727,7 +1732,10 @@
       "icon_width": 8,
       "icon_height": 8,
       "button_width": 8,
-      "button_height": 8
+      "button_height": 8,
+      "hover": {
+        "color": "#26232a"
+      }
     }
   }
 }

assets/themes/dark.json 🔗

@@ -505,7 +505,7 @@
       }
     },
     "notifications": {
-      "width": 256,
+      "width": 380,
       "margin": {
         "right": 10,
         "bottom": 10
@@ -1698,7 +1698,8 @@
       "color": "#f1f1f1",
       "size": 12,
       "margin": {
-        "left": 4
+        "left": 8,
+        "right": 8
       }
     },
     "header_height": 18,
@@ -1707,6 +1708,7 @@
       "color": "#9c9c9c",
       "size": 12,
       "margin": {
+        "left": 20,
         "top": 6,
         "bottom": 6
       }
@@ -1720,6 +1722,9 @@
       "corner_radius": 6,
       "margin": {
         "left": 6
+      },
+      "hover": {
+        "background": "#070707"
       }
     },
     "dismiss_button": {
@@ -1727,7 +1732,10 @@
       "icon_width": 8,
       "icon_height": 8,
       "button_width": 8,
-      "button_height": 8
+      "button_height": 8,
+      "hover": {
+        "color": "#c6c6c6"
+      }
     }
   }
 }

assets/themes/light.json 🔗

@@ -505,7 +505,7 @@
       }
     },
     "notifications": {
-      "width": 256,
+      "width": 380,
       "margin": {
         "right": 10,
         "bottom": 10
@@ -1698,7 +1698,8 @@
       "color": "#2b2b2b",
       "size": 12,
       "margin": {
-        "left": 4
+        "left": 8,
+        "right": 8
       }
     },
     "header_height": 18,
@@ -1707,6 +1708,7 @@
       "color": "#474747",
       "size": 12,
       "margin": {
+        "left": 20,
         "top": 6,
         "bottom": 6
       }
@@ -1720,6 +1722,9 @@
       "corner_radius": 6,
       "margin": {
         "left": 6
+      },
+      "hover": {
+        "background": "#e3e3e3"
       }
     },
     "dismiss_button": {
@@ -1727,7 +1732,10 @@
       "icon_width": 8,
       "icon_height": 8,
       "button_width": 8,
-      "button_height": 8
+      "button_height": 8,
+      "hover": {
+        "color": "#393939"
+      }
     }
   }
 }

assets/themes/solarized-dark.json 🔗

@@ -505,7 +505,7 @@
       }
     },
     "notifications": {
-      "width": 256,
+      "width": 380,
       "margin": {
         "right": 10,
         "bottom": 10
@@ -1698,7 +1698,8 @@
       "color": "#eee8d5",
       "size": 12,
       "margin": {
-        "left": 4
+        "left": 8,
+        "right": 8
       }
     },
     "header_height": 18,
@@ -1707,6 +1708,7 @@
       "color": "#93a1a1",
       "size": 12,
       "margin": {
+        "left": 20,
         "top": 6,
         "bottom": 6
       }
@@ -1720,6 +1722,9 @@
       "corner_radius": 6,
       "margin": {
         "left": 6
+      },
+      "hover": {
+        "background": "#0736423d"
       }
     },
     "dismiss_button": {
@@ -1727,7 +1732,10 @@
       "icon_width": 8,
       "icon_height": 8,
       "button_width": 8,
-      "button_height": 8
+      "button_height": 8,
+      "hover": {
+        "color": "#eee8d5"
+      }
     }
   }
 }

assets/themes/solarized-light.json 🔗

@@ -505,7 +505,7 @@
       }
     },
     "notifications": {
-      "width": 256,
+      "width": 380,
       "margin": {
         "right": 10,
         "bottom": 10
@@ -1698,7 +1698,8 @@
       "color": "#073642",
       "size": 12,
       "margin": {
-        "left": 4
+        "left": 8,
+        "right": 8
       }
     },
     "header_height": 18,
@@ -1707,6 +1708,7 @@
       "color": "#586e75",
       "size": 12,
       "margin": {
+        "left": 20,
         "top": 6,
         "bottom": 6
       }
@@ -1720,6 +1722,9 @@
       "corner_radius": 6,
       "margin": {
         "left": 6
+      },
+      "hover": {
+        "background": "#eee8d51f"
       }
     },
     "dismiss_button": {
@@ -1727,7 +1732,10 @@
       "icon_width": 8,
       "icon_height": 8,
       "button_width": 8,
-      "button_height": 8
+      "button_height": 8,
+      "hover": {
+        "color": "#073642"
+      }
     }
   }
 }

assets/themes/sulphurpool-dark.json 🔗

@@ -505,7 +505,7 @@
       }
     },
     "notifications": {
-      "width": 256,
+      "width": 380,
       "margin": {
         "right": 10,
         "bottom": 10
@@ -1698,7 +1698,8 @@
       "color": "#dfe2f1",
       "size": 12,
       "margin": {
-        "left": 4
+        "left": 8,
+        "right": 8
       }
     },
     "header_height": 18,
@@ -1707,6 +1708,7 @@
       "color": "#979db4",
       "size": 12,
       "margin": {
+        "left": 20,
         "top": 6,
         "bottom": 6
       }
@@ -1720,6 +1722,9 @@
       "corner_radius": 6,
       "margin": {
         "left": 6
+      },
+      "hover": {
+        "background": "#2932563d"
       }
     },
     "dismiss_button": {
@@ -1727,7 +1732,10 @@
       "icon_width": 8,
       "icon_height": 8,
       "button_width": 8,
-      "button_height": 8
+      "button_height": 8,
+      "hover": {
+        "color": "#dfe2f1"
+      }
     }
   }
 }

assets/themes/sulphurpool-light.json 🔗

@@ -505,7 +505,7 @@
       }
     },
     "notifications": {
-      "width": 256,
+      "width": 380,
       "margin": {
         "right": 10,
         "bottom": 10
@@ -1698,7 +1698,8 @@
       "color": "#293256",
       "size": 12,
       "margin": {
-        "left": 4
+        "left": 8,
+        "right": 8
       }
     },
     "header_height": 18,
@@ -1707,6 +1708,7 @@
       "color": "#5e6687",
       "size": 12,
       "margin": {
+        "left": 20,
         "top": 6,
         "bottom": 6
       }
@@ -1720,6 +1722,9 @@
       "corner_radius": 6,
       "margin": {
         "left": 6
+      },
+      "hover": {
+        "background": "#dfe2f11f"
       }
     },
     "dismiss_button": {
@@ -1727,7 +1732,10 @@
       "icon_width": 8,
       "icon_height": 8,
       "button_width": 8,
-      "button_height": 8
+      "button_height": 8,
+      "hover": {
+        "color": "#293256"
+      }
     }
   }
 }

crates/client/src/user.rs 🔗

@@ -371,7 +371,7 @@ impl UserStore {
                 response: if accept {
                     proto::ContactRequestResponse::Accept
                 } else {
-                    proto::ContactRequestResponse::Reject
+                    proto::ContactRequestResponse::Decline
                 } as i32,
             },
             cx,

crates/contacts_panel/src/contact_finder.rs 🔗

@@ -118,7 +118,7 @@ impl PickerDelegate for ContactFinder {
                 "icons/accept.svg"
             }
             ContactRequestStatus::RequestSent | ContactRequestStatus::RequestAccepted => {
-                "icons/reject.svg"
+                "icons/decline.svg"
             }
         };
         let button_style = if self.user_store.read(cx).is_contact_request_pending(&user) {

crates/contacts_panel/src/contact_notification.rs 🔗

@@ -6,6 +6,8 @@ use gpui::{
 use settings::Settings;
 use workspace::Notification;
 
+use crate::render_icon_button;
+
 impl_internal_actions!(contact_notifications, [Dismiss, RespondToContactRequest]);
 
 pub fn init(cx: &mut MutableAppContext) {
@@ -31,7 +33,7 @@ pub enum Event {
     Dismiss,
 }
 
-enum Reject {}
+enum Decline {}
 enum Accept {}
 
 impl Entity for ContactNotification {
@@ -87,7 +89,7 @@ impl ContactNotification {
         let user_id = user.id;
 
         Flex::column()
-            .with_child(self.render_header("added you", theme, cx))
+            .with_child(self.render_header("wants to add you as a contact.", theme, cx))
             .with_child(
                 Label::new(
                     "They won't know if you decline.".to_string(),
@@ -100,13 +102,14 @@ impl ContactNotification {
             .with_child(
                 Flex::row()
                     .with_child(
-                        MouseEventHandler::new::<Reject, _, _>(
+                        MouseEventHandler::new::<Decline, _, _>(
                             self.event.user.id as usize,
                             cx,
-                            |_, _| {
-                                Label::new("Reject".to_string(), theme.button.text.clone())
+                            |state, _| {
+                                let button = theme.button.style_for(state, false);
+                                Label::new("Decline".to_string(), button.text.clone())
                                     .contained()
-                                    .with_style(theme.button.container)
+                                    .with_style(button.container)
                                     .boxed()
                             },
                         )
@@ -120,10 +123,11 @@ impl ContactNotification {
                         .boxed(),
                     )
                     .with_child(
-                        MouseEventHandler::new::<Accept, _, _>(user.id as usize, cx, |_, _| {
-                            Label::new("Accept".to_string(), theme.button.text.clone())
+                        MouseEventHandler::new::<Accept, _, _>(user.id as usize, cx, |state, _| {
+                            let button = theme.button.style_for(state, false);
+                            Label::new("Accept".to_string(), button.text.clone())
                                 .contained()
-                                .with_style(theme.button.container)
+                                .with_style(button.container)
                                 .boxed()
                         })
                         .with_cursor_style(CursorStyle::PointingHand)
@@ -163,42 +167,51 @@ impl ContactNotification {
                 Image::new(avatar)
                     .with_style(theme.header_avatar)
                     .aligned()
-                    .left()
+                    .constrained()
+                    .with_height(
+                        cx.font_cache()
+                            .line_height(theme.header_message.text.font_size),
+                    )
+                    .aligned()
+                    .top()
                     .boxed()
             }))
             .with_child(
-                Label::new(
+                Text::new(
                     format!("{} {}", user.github_login, message),
                     theme.header_message.text.clone(),
                 )
                 .contained()
                 .with_style(theme.header_message.container)
                 .aligned()
+                .top()
+                .left()
+                .flex(1., true)
                 .boxed(),
             )
             .with_child(
-                MouseEventHandler::new::<Dismiss, _, _>(user.id as usize, cx, |_, _| {
-                    Svg::new("icons/reject.svg")
-                        .with_color(theme.dismiss_button.color)
-                        .constrained()
-                        .with_width(theme.dismiss_button.icon_width)
-                        .aligned()
-                        .contained()
-                        .with_style(theme.dismiss_button.container)
-                        .constrained()
-                        .with_width(theme.dismiss_button.button_width)
-                        .with_height(theme.dismiss_button.button_width)
-                        .aligned()
-                        .boxed()
+                MouseEventHandler::new::<Dismiss, _, _>(user.id as usize, cx, |state, _| {
+                    render_icon_button(
+                        theme.dismiss_button.style_for(state, false),
+                        "icons/decline.svg",
+                    )
+                    .boxed()
                 })
                 .with_cursor_style(CursorStyle::PointingHand)
+                .with_padding(Padding::uniform(5.))
                 .on_click(move |_, cx| cx.dispatch_action(Dismiss(user_id)))
+                .aligned()
+                .constrained()
+                .with_height(
+                    cx.font_cache()
+                        .line_height(theme.header_message.text.font_size),
+                )
+                .aligned()
+                .top()
                 .flex_float()
                 .boxed(),
             )
-            .constrained()
-            .with_height(theme.header_height)
-            .boxed()
+            .named("contact notification header")
     }
 
     fn dismiss(&mut self, _: &Dismiss, cx: &mut ViewContext<Self>) {

crates/contacts_panel/src/contacts_panel.rs 🔗

@@ -344,7 +344,7 @@ impl ContactsPanel {
         is_incoming: bool,
         cx: &mut LayoutContext,
     ) -> ElementBox {
-        enum Reject {}
+        enum Decline {}
         enum Accept {}
         enum Cancel {}
 
@@ -373,13 +373,13 @@ impl ContactsPanel {
 
         if is_incoming {
             row.add_children([
-                MouseEventHandler::new::<Reject, _, _>(user.id as usize, cx, |mouse_state, _| {
+                MouseEventHandler::new::<Decline, _, _>(user.id as usize, cx, |mouse_state, _| {
                     let button_style = if is_contact_request_pending {
                         &theme.disabled_contact_button
                     } else {
                         &theme.contact_button.style_for(mouse_state, false)
                     };
-                    render_icon_button(button_style, "icons/reject.svg")
+                    render_icon_button(button_style, "icons/decline.svg")
                         .aligned()
                         .flex_float()
                         .boxed()
@@ -421,7 +421,7 @@ impl ContactsPanel {
                     } else {
                         &theme.contact_button.style_for(mouse_state, false)
                     };
-                    render_icon_button(button_style, "icons/reject.svg")
+                    render_icon_button(button_style, "icons/decline.svg")
                         .aligned()
                         .flex_float()
                         .boxed()

crates/rpc/proto/zed.proto 🔗

@@ -564,7 +564,7 @@ message RespondToContactRequest {
 
 enum ContactRequestResponse {
     Accept = 0;
-    Reject = 1;
+    Decline = 1;
     Block = 2;
     Dismiss = 3;
 }

crates/theme/src/theme.rs 🔗

@@ -362,8 +362,8 @@ pub struct ContactNotification {
     pub header_message: ContainedText,
     pub header_height: f32,
     pub body_message: ContainedText,
-    pub button: ContainedText,
-    pub dismiss_button: IconButton,
+    pub button: Interactive<ContainedText>,
+    pub dismiss_button: Interactive<IconButton>,
 }
 
 #[derive(Clone, Deserialize, Default)]

crates/workspace/src/workspace.rs 🔗

@@ -1769,7 +1769,7 @@ impl Workspace {
                             .boxed()
                     }))
                     .constrained()
-                    .with_width(250.)
+                    .with_width(theme.notifications.width)
                     .contained()
                     .with_style(theme.notifications.container)
                     .aligned()

styles/src/styleTree/contactNotification.ts 🔗

@@ -1,21 +1,24 @@
 import Theme from "../themes/theme";
 import { backgroundColor, iconColor, text } from "./components";
 
+const avatarSize = 12;
+const headerPadding = 8;
+
 export default function contactNotification(theme: Theme): Object {
   return {
     headerAvatar: {
-      height: 12,
-      width: 12,
+      height: avatarSize,
+      width: avatarSize,
       cornerRadius: 6,
     },
     headerMessage: {
       ...text(theme, "sans", "primary", { size: "xs" }),
-      margin: { left: 4 }
+      margin: { left: headerPadding, right: headerPadding }
     },
     headerHeight: 18,
     bodyMessage: {
       ...text(theme, "sans", "secondary", { size: "xs" }),
-      margin: { top: 6, bottom: 6 },
+      margin: { left: avatarSize + headerPadding, top: 6, bottom: 6 },
     },
     button: {
       ...text(theme, "sans", "primary", { size: "xs" }),
@@ -23,6 +26,9 @@ export default function contactNotification(theme: Theme): Object {
       padding: 4,
       cornerRadius: 6,
       margin: { left: 6 },
+      hover: {
+        background: backgroundColor(theme, "on300", "hovered")
+      }
     },
     dismissButton: {
       color: iconColor(theme, "secondary"),
@@ -30,6 +36,9 @@ export default function contactNotification(theme: Theme): Object {
       iconHeight: 8,
       buttonWidth: 8,
       buttonHeight: 8,
+      hover: {
+        color: iconColor(theme, "primary")
+      }
     }
   }
 }

styles/src/styleTree/workspace.ts 🔗

@@ -159,7 +159,7 @@ export default function workspace(theme: Theme) {
       shadow: shadow(theme),
     },
     notifications: {
-      width: 256,
+      width: 380,
       margin: { right: 10, bottom: 10 },
     }
   };