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:
commit
c1df27c792
2 changed files with 179 additions and 134 deletions
|
@ -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 {
|
||||||
|
|
|
@ -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())
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue