Mail · Sorting: a label picker and a snooze menu
LabelPicker puts labels on a message: a field finds them by name, arrows move and a press or Enter turns one on or off, each row with its check and its hue. A name no label has can be made into one, and the field starts over. Its rows are the option rows every list shares; forms::marked_row leads one with a mark. The cursor shows only while the field has focus. SnoozePicker offers later today, tomorrow, this weekend and next week, each with its day and hour, or a time picked in a dialog. It shares mail::times with ScheduleSend: the names, the dialog, which now waits for a time ahead of now, and a rule that a moment comes once, so on a Sunday tomorrow morning no longer repeats as Monday morning.
This commit is contained in:
1 parent
14b5faa80c
commit
ccbc0970e5
16 files changed
+1037
-144
No files matched your search
@@ -3,12 +3,14 @@ use ely_gpui_component::{
|
||||
feedback::EmptyState,
|
||||
mail::{Mail, MailList, Mailbox, MailboxList},
|
||||
primitives::IconName,
|
||||
theme::{ActiveTheme, Radius},
|
||||
theme::{ActiveTheme, Radius, TextSize},
|
||||
typography::format,
|
||||
};
|
||||
use gpui::{AnyElement, App, IntoElement, ParentElement, SharedString, Styled, Window, div, px};
|
||||
use jiff::{SignedDuration, Timestamp};
|
||||
use jiff::{SignedDuration, Timestamp, tz::TimeZone};
|
||||
|
||||
mod read;
|
||||
mod sort;
|
||||
mod write;
|
||||
|
||||
use super::Page;
|
||||
@@ -52,8 +54,40 @@ const SCRIPT: &[Step] = &[
|
||||
Step::Key("enter"),
|
||||
Step::Wait(200),
|
||||
Step::Shot("recipient-added"),
|
||||
Step::DownAt("mail-labels", 150.0, 25.0),
|
||||
Step::UpAt("mail-labels", 150.0, 25.0),
|
||||
Step::Type("Suppliers"),
|
||||
Step::Wait(200),
|
||||
Step::Shot("label-new"),
|
||||
Step::Key("enter"),
|
||||
Step::Wait(200),
|
||||
Step::Shot("label-made"),
|
||||
Step::DownAt("mail-snooze", 50.0, 16.0),
|
||||
Step::UpAt("mail-snooze", 50.0, 16.0),
|
||||
Step::Wait(300),
|
||||
Step::Shot("snooze-open"),
|
||||
Step::Key("up"),
|
||||
Step::Key("enter"),
|
||||
Step::Wait(300),
|
||||
Step::Shot("snooze-dialog"),
|
||||
Step::Key("escape"),
|
||||
];
|
||||
|
||||
/// What a demo says it did, under it.
|
||||
fn quiet(text: SharedString, cx: &App) -> impl IntoElement + use<> {
|
||||
let theme = cx.theme();
|
||||
div()
|
||||
.text_size(theme.text_size(TextSize::Sm))
|
||||
.text_color(theme.colors.fg_muted)
|
||||
.child(text)
|
||||
}
|
||||
|
||||
/// A moment as the demos say it: its weekday and hour here.
|
||||
fn day_and_hour(at: Timestamp) -> String {
|
||||
let zone = TimeZone::try_system().expect("the gallery reads the system time zone");
|
||||
format::datetime(at, &zone, "%a %H:%M").expect("a fixed pattern formats")
|
||||
}
|
||||
|
||||
fn boxes(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||
let open = keep("mail-open-box", || SharedString::from("inbox"), window, cx);
|
||||
let shown = open.read(cx).clone();
|
||||
@@ -223,6 +257,8 @@ fn render(window: &mut Window, cx: &mut App) -> AnyElement {
|
||||
.child(read::thread(cx))
|
||||
.child(write::composer(window, cx))
|
||||
.child(write::signature(window, cx))
|
||||
.child(sort::labels(window, cx))
|
||||
.child(sort::snooze(window, cx))
|
||||
.child(write::schedule(window, cx))
|
||||
.into_any_element()
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
use ely_gpui_component::{
|
||||
mail::{Label, LabelPicker, SnoozePicker},
|
||||
theme::{ActiveTheme, Radius},
|
||||
};
|
||||
use gpui::{App, IntoElement, ParentElement, SharedString, Styled, Window, div, px};
|
||||
|
||||
use super::{day_and_hour, quiet};
|
||||
use crate::{
|
||||
probe::probe,
|
||||
ui::{change, keep, section, set},
|
||||
};
|
||||
|
||||
/// The demo's labels and the keys on.
|
||||
#[derive(Clone)]
|
||||
struct Sorting {
|
||||
labels: Vec<Label>,
|
||||
on: Vec<SharedString>,
|
||||
}
|
||||
|
||||
pub fn labels(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||
let sorting = keep(
|
||||
"mail-labels",
|
||||
|| Sorting {
|
||||
labels: vec![
|
||||
Label::new("clients", "Clients", 0),
|
||||
Label::new("studio", "Studio", 5),
|
||||
Label::new("press", "Press", 3),
|
||||
Label::new("finance", "Finance", 1),
|
||||
Label::new("site", "Site visits", 2),
|
||||
Label::new("travel", "Travel", 7),
|
||||
],
|
||||
on: vec!["clients".into(), "press".into()],
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
let now = sorting.read(cx).clone();
|
||||
let names: Vec<&str> = now
|
||||
.labels
|
||||
.iter()
|
||||
.filter(|label| now.on.contains(&label.key))
|
||||
.map(|label| label.name.as_ref())
|
||||
.collect();
|
||||
let said = match names.len() {
|
||||
0 => SharedString::from("No labels on."),
|
||||
_ => format!("On: {}.", names.join(", ")).into(),
|
||||
};
|
||||
let (changing, making) = (sorting.clone(), sorting);
|
||||
let theme = cx.theme();
|
||||
section(
|
||||
"LabelPicker",
|
||||
"The labels a message wears: the field finds them by name, arrows move, and a press or Enter turns one on or off. A name no label has can be made into one, on at once.",
|
||||
cx,
|
||||
)
|
||||
.child(probe(
|
||||
"mail-labels",
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_3()
|
||||
.child(
|
||||
div()
|
||||
.w(px(300.))
|
||||
.p_2()
|
||||
.border_1()
|
||||
.border_color(theme.colors.border)
|
||||
.rounded(theme.radius(Radius::Lg))
|
||||
.child(
|
||||
LabelPicker::new("mail-labels", now.labels.clone())
|
||||
.selected(now.on.clone())
|
||||
.on_change(move |keys, _, cx| {
|
||||
change(&changing, cx, |sorting| sorting.on = keys.to_vec())
|
||||
})
|
||||
.on_create(move |name, _, cx| {
|
||||
let hues = cx.theme().colors.chart.len();
|
||||
change(&making, cx, |sorting| {
|
||||
let made = sorting.labels.len();
|
||||
let key = SharedString::from(format!("made-{made}"));
|
||||
let free = (0..hues).find(|hue| {
|
||||
sorting.labels.iter().all(|label| label.hue != *hue)
|
||||
});
|
||||
let hue = free.unwrap_or(made % hues);
|
||||
sorting.labels.push(Label::new(key.clone(), name.clone(), hue));
|
||||
sorting.on.push(key);
|
||||
})
|
||||
}),
|
||||
),
|
||||
)
|
||||
.child(quiet(said, cx)),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn snooze(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||
let said = keep("mail-snoozed", || None::<SharedString>, window, cx);
|
||||
let shown = said.read(cx).clone();
|
||||
section(
|
||||
"SnoozePicker",
|
||||
"Put a message off until later today, tomorrow, this weekend or next week, each with its day and hour, or until a time of your own.",
|
||||
cx,
|
||||
)
|
||||
.child(probe(
|
||||
"mail-snooze",
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_4()
|
||||
.child(SnoozePicker::new("mail-snooze").on_snooze(move |at, _, cx| {
|
||||
set(&said, Some(format!("Back {}.", day_and_hour(at)).into()), cx)
|
||||
}))
|
||||
.children(shown.map(|shown| quiet(shown, cx))),
|
||||
))
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
use ely_gpui_component::{
|
||||
chat::Attachment,
|
||||
mail::{Contact, MailComposer, ScheduleSend, SignatureEditor},
|
||||
theme::{ActiveTheme, Radius, TextSize},
|
||||
theme::{ActiveTheme, Radius},
|
||||
};
|
||||
use gpui::{App, IntoElement, ParentElement, SharedString, Styled, Window, div, px};
|
||||
|
||||
use super::{day_and_hour, quiet};
|
||||
use crate::{
|
||||
probe::probe,
|
||||
ui::{change, keep, section, set},
|
||||
@@ -39,14 +40,6 @@ struct Writing {
|
||||
said: Option<SharedString>,
|
||||
}
|
||||
|
||||
fn quiet(text: SharedString, cx: &App) -> impl IntoElement + use<> {
|
||||
let theme = cx.theme();
|
||||
div()
|
||||
.text_size(theme.text_size(TextSize::Sm))
|
||||
.text_color(theme.colors.fg_muted)
|
||||
.child(text)
|
||||
}
|
||||
|
||||
pub fn composer(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||
let writing = keep(
|
||||
"mail-writing",
|
||||
@@ -83,7 +76,7 @@ pub fn composer(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||
change(&sent, cx, |writing| writing.said = Some(said.into()))
|
||||
})
|
||||
.on_schedule(move |draft, at, _, cx| {
|
||||
let said = format!("{} will go at {at}.", draft.subject);
|
||||
let said = format!("{} goes {}.", draft.subject, day_and_hour(at));
|
||||
change(&later, cx, |writing| writing.said = Some(said.into()))
|
||||
})
|
||||
.on_discard(move |_, cx| {
|
||||
@@ -164,7 +157,9 @@ pub fn schedule(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||
.child(
|
||||
ScheduleSend::new("mail-schedule")
|
||||
.on_send(move |_, cx| set(&sent, Some("Sent now.".into()), cx))
|
||||
.on_schedule(move |at, _, cx| set(&later, Some(format!("Goes at {at}.").into()), cx)),
|
||||
.on_schedule(move |at, _, cx| {
|
||||
set(&later, Some(format!("Goes {}.", day_and_hour(at)).into()), cx)
|
||||
}),
|
||||
)
|
||||
.children(shown.map(|shown| quiet(shown, cx))),
|
||||
))
|
||||
|
||||
Reference in new issue
Block a user