Style notification panel (#3741)

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
This commit is contained in:
Marshall Bowers 2023-12-20 14:06:33 -05:00 committed by GitHub
commit c1df27c792
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 179 additions and 134 deletions

View file

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

View file

@ -6,11 +6,11 @@ use collections::HashMap;
use db::kvp::KEY_VALUE_STORE; use db::kvp::KEY_VALUE_STORE;
use futures::StreamExt; use futures::StreamExt;
use gpui::{ use gpui::{
actions, div, list, px, serde_json, AnyElement, AppContext, AsyncWindowContext, CursorStyle, actions, div, img, list, px, serde_json, AnyElement, AppContext, AsyncWindowContext,
DismissEvent, Div, Element, EventEmitter, FocusHandle, FocusableView, InteractiveElement, CursorStyle, DismissEvent, Div, Element, EventEmitter, FocusHandle, FocusableView,
IntoElement, ListAlignment, ListScrollEvent, ListState, Model, ParentElement, Render, Stateful, InteractiveElement, IntoElement, ListAlignment, ListScrollEvent, ListState, Model,
StatefulInteractiveElement, Styled, Task, View, ViewContext, VisualContext, WeakView, ParentElement, Render, Stateful, StatefulInteractiveElement, Styled, Task, View, ViewContext,
WindowContext, VisualContext, WeakView, WindowContext,
}; };
use notifications::{NotificationEntry, NotificationEvent, NotificationStore}; use notifications::{NotificationEntry, NotificationEvent, NotificationStore};
use project::Fs; use project::Fs;
@ -19,7 +19,7 @@ use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsStore}; use settings::{Settings, SettingsStore};
use std::{sync::Arc, time::Duration}; use std::{sync::Arc, time::Duration};
use time::{OffsetDateTime, UtcOffset}; 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 util::{ResultExt, TryFutureExt};
use workspace::{ use workspace::{
dock::{DockPosition, Panel, PanelEvent}, dock::{DockPosition, Panel, PanelEvent},
@ -229,61 +229,12 @@ impl NotificationPanel {
Some( Some(
div() div()
.id(ix) .id(ix)
.child( .flex()
h_stack() .flex_row()
.children(actor.map(|actor| Avatar::new(actor.avatar_uri.clone()))) .size_full()
.child( .px_2()
v_stack().child(Label::new(text)).child( .py_1()
h_stack() .gap_2()
.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 {
"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
}),
),
),
)
.when(can_navigate, |el| { .when(can_navigate, |el| {
el.cursor(CursorStyle::PointingHand).on_click({ el.cursor(CursorStyle::PointingHand).on_click({
let notification = notification.clone(); let notification = notification.clone();
@ -292,6 +243,74 @@ impl NotificationPanel {
}) })
}) })
}) })
.children(actor.map(|actor| {
img(actor.avatar_uri.clone())
.flex_none()
.w_8()
.h_8()
.rounded_full()
}))
.child(
v_stack()
.gap_1()
.size_full()
.overflow_hidden()
.child(Label::new(text.clone()))
.child(
h_stack()
.child(
Label::new(format_timestamp(
timestamp,
now,
self.local_timezone,
))
.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()
.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
}),
),
)
.into_any(), .into_any(),
) )
} }
@ -439,28 +458,6 @@ impl NotificationPanel {
false 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( fn on_notification_event(
&mut self, &mut self,
_: Model<NotificationStore>, _: Model<NotificationStore>,
@ -543,25 +540,72 @@ impl NotificationPanel {
} }
impl Render for NotificationPanel { impl Render for NotificationPanel {
type Element = AnyElement; type Element = Div;
fn render(&mut self, _: &mut ViewContext<Self>) -> AnyElement { fn render(&mut self, cx: &mut ViewContext<Self>) -> Div {
if self.client.user_id().is_none() { v_stack()
self.render_sign_in_prompt() .size_full()
} else if self.notification_list.item_count() == 0 { .child(
self.render_empty_state() h_stack()
} else { .justify_between()
v_stack() .px_2()
.bg(gpui::red()) .py_1()
.child( // Match the height of the tab bar so they line up.
h_stack() .h(rems(ui::Tab::HEIGHT_IN_REMS))
.child(Label::new("Notifications")) .border_b_1()
.child(IconElement::new(Icon::Envelope)), .border_color(cx.theme().colors().border)
) .child(Label::new("Notifications"))
.child(list(self.notification_list.clone()).size_full()) .child(IconElement::new(Icon::Envelope)),
.size_full() )
.into_any_element() .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())
}
})
} }
} }