Style notification panel (#3741)

Marshall Bowers created

This PR makes a first pass at styling the notification panel.

#### Signed out

<img width="381" alt="Screenshot 2023-12-20 at 11 41 25 AM"
src="https://github.com/zed-industries/zed/assets/1486634/f045fa17-4ebc-437f-a25b-d7695d47f18b">

#### No notifications

<img width="380" alt="Screenshot 2023-12-20 at 11 44 23 AM"
src="https://github.com/zed-industries/zed/assets/1486634/3a7543f2-8cd8-4788-8059-d5663f5f6b4c">

#### Notifications

<img width="386" alt="Screenshot 2023-12-20 at 1 27 08 PM"
src="https://github.com/zed-industries/zed/assets/1486634/13b81722-c47a-4c06-b37d-e6515cbfdb9d">

Release Notes:

- N/A

Change summary

crates/collab_ui2/src/collab_panel.rs       |  67 +++---
crates/collab_ui2/src/notification_panel.rs | 242 +++++++++++++---------
2 files changed, 177 insertions(+), 132 deletions(-)

Detailed changes

crates/collab_ui2/src/collab_panel.rs 🔗

@@ -1624,40 +1624,41 @@ impl CollabPanel {
     }
 
     fn render_signed_out(&mut self, cx: &mut ViewContext<Self>) -> Div {
-        v_stack()
-            .items_center()
-            .child(v_stack().gap_6().p_4()
-                .child(
-                    Label::new("Work with your team in realtime with collaborative editing, voice, shared notes and more.")
-                )
-                .child(v_stack().gap_2()
+        let collab_blurb = "Work with your team in realtime with collaborative editing, voice, shared notes and more.";
 
-                .child(
-                Button::new("sign_in", "Sign in")
-                    .icon_color(Color::Muted)
-                    .icon(Icon::Github)
-                    .icon_position(IconPosition::Start)
-                    .style(ButtonStyle::Filled)
-                    .full_width()
-                    .on_click(cx.listener(
-                    |this, _, cx| {
-                        let client = this.client.clone();
-                        cx.spawn(|_, mut cx| async move {
-                            client
-                                .authenticate_and_connect(true, &cx)
-                                .await
-                                .notify_async_err(&mut cx);
-                        })
-                        .detach()
-                    },
-                )))
-                .child(
-                div().flex().w_full().items_center().child(
-                    Label::new("Sign in to enable collaboration.")
-                        .color(Color::Muted)
-                        .size(LabelSize::Small)
-                )),
-            ))
+        v_stack()
+            .gap_6()
+            .p_4()
+            .child(Label::new(collab_blurb))
+            .child(
+                v_stack()
+                    .gap_2()
+                    .child(
+                        Button::new("sign_in", "Sign in")
+                            .icon_color(Color::Muted)
+                            .icon(Icon::Github)
+                            .icon_position(IconPosition::Start)
+                            .style(ButtonStyle::Filled)
+                            .full_width()
+                            .on_click(cx.listener(|this, _, cx| {
+                                let client = this.client.clone();
+                                cx.spawn(|_, mut cx| async move {
+                                    client
+                                        .authenticate_and_connect(true, &cx)
+                                        .await
+                                        .notify_async_err(&mut cx);
+                                })
+                                .detach()
+                            })),
+                    )
+                    .child(
+                        div().flex().w_full().items_center().child(
+                            Label::new("Sign in to enable collaboration.")
+                                .color(Color::Muted)
+                                .size(LabelSize::Small),
+                        ),
+                    ),
+            )
     }
 
     fn render_list_entry(&mut self, ix: usize, cx: &mut ViewContext<Self>) -> AnyElement {

crates/collab_ui2/src/notification_panel.rs 🔗

@@ -6,11 +6,11 @@ use collections::HashMap;
 use db::kvp::KEY_VALUE_STORE;
 use futures::StreamExt;
 use gpui::{
-    actions, div, list, px, serde_json, AnyElement, AppContext, AsyncWindowContext, CursorStyle,
-    DismissEvent, Div, Element, EventEmitter, FocusHandle, FocusableView, InteractiveElement,
-    IntoElement, ListAlignment, ListScrollEvent, ListState, Model, ParentElement, Render, Stateful,
-    StatefulInteractiveElement, Styled, Task, View, ViewContext, VisualContext, WeakView,
-    WindowContext,
+    actions, div, img, list, px, serde_json, AnyElement, AppContext, AsyncWindowContext,
+    CursorStyle, DismissEvent, Div, Element, EventEmitter, FocusHandle, FocusableView,
+    InteractiveElement, IntoElement, ListAlignment, ListScrollEvent, ListState, Model,
+    ParentElement, Render, Stateful, StatefulInteractiveElement, Styled, Task, View, ViewContext,
+    VisualContext, WeakView, WindowContext,
 };
 use notifications::{NotificationEntry, NotificationEvent, NotificationStore};
 use project::Fs;
@@ -19,7 +19,7 @@ use serde::{Deserialize, Serialize};
 use settings::{Settings, SettingsStore};
 use std::{sync::Arc, time::Duration};
 use time::{OffsetDateTime, UtcOffset};
-use ui::{h_stack, v_stack, Avatar, Button, Clickable, Icon, IconButton, IconElement, Label};
+use ui::{h_stack, prelude::*, v_stack, Avatar, Button, Icon, IconButton, IconElement, Label};
 use util::{ResultExt, TryFutureExt};
 use workspace::{
     dock::{DockPosition, Panel, PanelEvent},
@@ -229,69 +229,88 @@ impl NotificationPanel {
         Some(
             div()
                 .id(ix)
+                .flex()
+                .flex_row()
+                .size_full()
+                .px_2()
+                .py_1()
+                .gap_2()
+                .when(can_navigate, |el| {
+                    el.cursor(CursorStyle::PointingHand).on_click({
+                        let notification = notification.clone();
+                        cx.listener(move |this, _, cx| {
+                            this.did_click_notification(&notification, cx)
+                        })
+                    })
+                })
+                .children(actor.map(|actor| {
+                    img(actor.avatar_uri.clone())
+                        .flex_none()
+                        .w_8()
+                        .h_8()
+                        .rounded_full()
+                }))
                 .child(
-                    h_stack()
-                        .children(actor.map(|actor| Avatar::new(actor.avatar_uri.clone())))
+                    v_stack()
+                        .gap_1()
+                        .size_full()
+                        .overflow_hidden()
+                        .child(Label::new(text.clone()))
                         .child(
-                            v_stack().child(Label::new(text)).child(
-                                h_stack()
-                                    .child(Label::new(format_timestamp(
+                            h_stack()
+                                .child(
+                                    Label::new(format_timestamp(
                                         timestamp,
                                         now,
                                         self.local_timezone,
-                                    )))
-                                    .children(if let Some(is_accepted) = response {
-                                        Some(div().child(Label::new(if is_accepted {
+                                    ))
+                                    .color(Color::Muted),
+                                )
+                                .children(if let Some(is_accepted) = response {
+                                    Some(div().flex().flex_grow().justify_end().child(Label::new(
+                                        if is_accepted {
                                             "You accepted"
                                         } else {
                                             "You declined"
-                                        })))
-                                    } else if needs_response {
-                                        Some(
-                                            h_stack()
-                                                .child(Button::new("decline", "Decline").on_click(
-                                                    {
-                                                        let notification = notification.clone();
-                                                        let view = cx.view().clone();
-                                                        move |_, cx| {
-                                                            view.update(cx, |this, cx| {
-                                                                this.respond_to_notification(
-                                                                    notification.clone(),
-                                                                    false,
-                                                                    cx,
-                                                                )
-                                                            });
-                                                        }
-                                                    },
-                                                ))
-                                                .child(Button::new("accept", "Accept").on_click({
-                                                    let notification = notification.clone();
-                                                    let view = cx.view().clone();
-                                                    move |_, cx| {
-                                                        view.update(cx, |this, cx| {
-                                                            this.respond_to_notification(
-                                                                notification.clone(),
-                                                                true,
-                                                                cx,
-                                                            )
-                                                        });
-                                                    }
-                                                })),
-                                        )
-                                    } else {
-                                        None
-                                    }),
-                            ),
+                                        },
+                                    )))
+                                } else if needs_response {
+                                    Some(
+                                        h_stack()
+                                            .flex_grow()
+                                            .justify_end()
+                                            .child(Button::new("decline", "Decline").on_click({
+                                                let notification = notification.clone();
+                                                let view = cx.view().clone();
+                                                move |_, cx| {
+                                                    view.update(cx, |this, cx| {
+                                                        this.respond_to_notification(
+                                                            notification.clone(),
+                                                            false,
+                                                            cx,
+                                                        )
+                                                    });
+                                                }
+                                            }))
+                                            .child(Button::new("accept", "Accept").on_click({
+                                                let notification = notification.clone();
+                                                let view = cx.view().clone();
+                                                move |_, cx| {
+                                                    view.update(cx, |this, cx| {
+                                                        this.respond_to_notification(
+                                                            notification.clone(),
+                                                            true,
+                                                            cx,
+                                                        )
+                                                    });
+                                                }
+                                            })),
+                                    )
+                                } else {
+                                    None
+                                }),
                         ),
                 )
-                .when(can_navigate, |el| {
-                    el.cursor(CursorStyle::PointingHand).on_click({
-                        let notification = notification.clone();
-                        cx.listener(move |this, _, cx| {
-                            this.did_click_notification(&notification, cx)
-                        })
-                    })
-                })
                 .into_any(),
         )
     }
@@ -439,28 +458,6 @@ impl NotificationPanel {
         false
     }
 
-    fn render_sign_in_prompt(&self) -> AnyElement {
-        Button::new(
-            "sign_in_prompt_button",
-            "Sign in to view your notifications",
-        )
-        .on_click({
-            let client = self.client.clone();
-            move |_, cx| {
-                let client = client.clone();
-                cx.spawn(move |cx| async move {
-                    client.authenticate_and_connect(true, &cx).log_err().await;
-                })
-                .detach()
-            }
-        })
-        .into_any_element()
-    }
-
-    fn render_empty_state(&self) -> AnyElement {
-        Label::new("You have no notifications").into_any_element()
-    }
-
     fn on_notification_event(
         &mut self,
         _: Model<NotificationStore>,
@@ -543,25 +540,72 @@ impl NotificationPanel {
 }
 
 impl Render for NotificationPanel {
-    type Element = AnyElement;
+    type Element = Div;
 
-    fn render(&mut self, _: &mut ViewContext<Self>) -> AnyElement {
-        if self.client.user_id().is_none() {
-            self.render_sign_in_prompt()
-        } else if self.notification_list.item_count() == 0 {
-            self.render_empty_state()
-        } else {
-            v_stack()
-                .bg(gpui::red())
-                .child(
-                    h_stack()
-                        .child(Label::new("Notifications"))
-                        .child(IconElement::new(Icon::Envelope)),
-                )
-                .child(list(self.notification_list.clone()).size_full())
-                .size_full()
-                .into_any_element()
-        }
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> Div {
+        v_stack()
+            .size_full()
+            .child(
+                h_stack()
+                    .justify_between()
+                    .px_2()
+                    .py_1()
+                    // Match the height of the tab bar so they line up.
+                    .h(rems(ui::Tab::HEIGHT_IN_REMS))
+                    .border_b_1()
+                    .border_color(cx.theme().colors().border)
+                    .child(Label::new("Notifications"))
+                    .child(IconElement::new(Icon::Envelope)),
+            )
+            .map(|this| {
+                if self.client.user_id().is_none() {
+                    this.child(
+                        v_stack()
+                            .gap_2()
+                            .p_4()
+                            .child(
+                                Button::new("sign_in_prompt_button", "Sign in")
+                                    .icon_color(Color::Muted)
+                                    .icon(Icon::Github)
+                                    .icon_position(IconPosition::Start)
+                                    .style(ButtonStyle::Filled)
+                                    .full_width()
+                                    .on_click({
+                                        let client = self.client.clone();
+                                        move |_, cx| {
+                                            let client = client.clone();
+                                            cx.spawn(move |cx| async move {
+                                                client
+                                                    .authenticate_and_connect(true, &cx)
+                                                    .log_err()
+                                                    .await;
+                                            })
+                                            .detach()
+                                        }
+                                    }),
+                            )
+                            .child(
+                                div().flex().w_full().items_center().child(
+                                    Label::new("Sign in to view notifications.")
+                                        .color(Color::Muted)
+                                        .size(LabelSize::Small),
+                                ),
+                            ),
+                    )
+                } else if self.notification_list.item_count() == 0 {
+                    this.child(
+                        v_stack().p_4().child(
+                            div().flex().w_full().items_center().child(
+                                Label::new("You have no notifications.")
+                                    .color(Color::Muted)
+                                    .size(LabelSize::Small),
+                            ),
+                        ),
+                    )
+                } else {
+                    this.child(list(self.notification_list.clone()).size_full())
+                }
+            })
     }
 }