Mail · Writing: the composer, recipients that suggest, a signature, send later
MailComposer writes a message: To, and Cc and Bcc once asked for, as tag fields that suggest contacts and mark what is not an address; the subject and the words; the files, each with a way off; the signature under the words. Send wakes once there is someone to send to and every address is one; it hands over the draft now or at a time, and Discard lets it go. A new start from the owner begins the draft over. forms::TagInput now suggests: the rows that hold the typed words float under it through the list Combobox uses; Up, Down and Enter pick, and Escape sets them aside. RecipientInput points to it. ScheduleSend offers tomorrow morning, tomorrow afternoon, Monday morning, or a day and hour picked in a dialog. SignatureEditor writes the lines and shows how they sit under a message; it tells the owner's echo from a new signature, so the caret stays. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
48118499ae
commit
84c3dc16de
14 files changed
+1425
-12
No files matched your search
@@ -9,6 +9,7 @@ use gpui::{AnyElement, App, IntoElement, ParentElement, SharedString, Styled, Wi
|
||||
use jiff::{SignedDuration, Timestamp};
|
||||
|
||||
mod read;
|
||||
mod write;
|
||||
|
||||
use super::Page;
|
||||
use crate::{
|
||||
@@ -43,6 +44,14 @@ const SCRIPT: &[Step] = &[
|
||||
Step::UpAt("mail-thread", 200.0, 120.0),
|
||||
Step::Wait(300),
|
||||
Step::Shot("thread-opened"),
|
||||
Step::DownAt("mail-composer", 270.0, 31.0),
|
||||
Step::UpAt("mail-composer", 270.0, 31.0),
|
||||
Step::Type("an"),
|
||||
Step::Wait(200),
|
||||
Step::Shot("recipient-suggested"),
|
||||
Step::Key("enter"),
|
||||
Step::Wait(200),
|
||||
Step::Shot("recipient-added"),
|
||||
];
|
||||
|
||||
fn boxes(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||
@@ -212,5 +221,8 @@ fn render(window: &mut Window, cx: &mut App) -> AnyElement {
|
||||
.child(zero(cx))
|
||||
.child(read::reader(cx))
|
||||
.child(read::thread(cx))
|
||||
.child(write::composer(window, cx))
|
||||
.child(write::signature(window, cx))
|
||||
.child(write::schedule(window, cx))
|
||||
.into_any_element()
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
use ely_gpui_component::{
|
||||
chat::Attachment,
|
||||
mail::{Contact, MailComposer, ScheduleSend, SignatureEditor},
|
||||
theme::{ActiveTheme, Radius, TextSize},
|
||||
};
|
||||
use gpui::{App, IntoElement, ParentElement, SharedString, Styled, Window, div, px};
|
||||
|
||||
use crate::{
|
||||
probe::probe,
|
||||
ui::{change, keep, section, set},
|
||||
};
|
||||
|
||||
const SIGNATURE: &str = "Chloé Martin\nLead designer · Atrium Studio";
|
||||
|
||||
fn contacts() -> [Contact; 4] {
|
||||
[
|
||||
Contact::new("Ana Lima", "ana@atrium.studio"),
|
||||
Contact::new("Ben Ito", "ben@atrium.studio"),
|
||||
Contact::new("Chloé Martin", "chloe@atrium.studio"),
|
||||
Contact::new("Dev Rao", "dev@atrium.studio"),
|
||||
]
|
||||
}
|
||||
|
||||
fn file(key: &str, name: &str, bytes: u64) -> Attachment {
|
||||
Attachment {
|
||||
key: SharedString::from(key.to_string()),
|
||||
name: SharedString::from(name.to_string()),
|
||||
bytes,
|
||||
preview: None,
|
||||
progress: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// The demo composer: which one it is, its files, and what became of the last draft.
|
||||
#[derive(Clone)]
|
||||
struct Writing {
|
||||
round: usize,
|
||||
files: Vec<Attachment>,
|
||||
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",
|
||||
|| Writing {
|
||||
round: 0,
|
||||
files: vec![file("plan", "atrium-plan.pdf", 2_400_000)],
|
||||
said: None,
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
let now = writing.read(cx).clone();
|
||||
let (sent, later, gone, attach, remove) = (
|
||||
writing.clone(),
|
||||
writing.clone(),
|
||||
writing.clone(),
|
||||
writing.clone(),
|
||||
writing,
|
||||
);
|
||||
let theme = cx.theme();
|
||||
let frame = div()
|
||||
.w(px(560.))
|
||||
.p_4()
|
||||
.border_1()
|
||||
.border_color(theme.colors.border)
|
||||
.rounded(theme.radius(Radius::Lg))
|
||||
.child(
|
||||
MailComposer::new(("mail-composer", now.round))
|
||||
.contacts(contacts())
|
||||
.signature(SIGNATURE)
|
||||
.files(now.files.clone())
|
||||
.on_send(move |draft, _, cx| {
|
||||
let said = format!("Sent to {}.", draft.to.join(", "));
|
||||
change(&sent, cx, |writing| writing.said = Some(said.into()))
|
||||
})
|
||||
.on_schedule(move |draft, at, _, cx| {
|
||||
let said = format!("{} will go at {at}.", draft.subject);
|
||||
change(&later, cx, |writing| writing.said = Some(said.into()))
|
||||
})
|
||||
.on_discard(move |_, cx| {
|
||||
change(&gone, cx, |writing| {
|
||||
writing.round += 1;
|
||||
writing.said = Some("Discarded.".into());
|
||||
})
|
||||
})
|
||||
.on_attach(move |_, cx| {
|
||||
change(&attach, cx, |writing| {
|
||||
let ix = writing.files.len();
|
||||
writing.files.push(file(
|
||||
&format!("photo-{ix}"),
|
||||
&format!("stair-hall-{ix}.jpg"),
|
||||
3_100_000,
|
||||
));
|
||||
})
|
||||
})
|
||||
.on_remove(move |key, _, cx| {
|
||||
change(&remove, cx, |writing| {
|
||||
writing.files.retain(|each| each.key != *key)
|
||||
})
|
||||
}),
|
||||
);
|
||||
section(
|
||||
"MailComposer / RecipientInput → TagInput",
|
||||
"To, and Cc and Bcc once asked for, suggest people as you type and mark what is not an address. Then the subject, the words, the files and the signature. Send wakes once there is someone to send to, now or at a time; Discard lets the draft go.",
|
||||
cx,
|
||||
)
|
||||
.child(probe(
|
||||
"mail-composer",
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_3()
|
||||
.child(frame)
|
||||
.children(now.said.map(|said| quiet(said, cx))),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn signature(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||
let words = keep(
|
||||
"mail-signature",
|
||||
|| SharedString::from(SIGNATURE),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
let shown = words.read(cx).clone();
|
||||
section(
|
||||
"SignatureEditor",
|
||||
"A signature's lines in a field, and under it how they will sit below a message.",
|
||||
cx,
|
||||
)
|
||||
.child(probe(
|
||||
"mail-signature",
|
||||
div().w(px(420.)).child(
|
||||
SignatureEditor::new("mail-signature", shown)
|
||||
.on_change(move |next, _, cx| set(&words, next.clone(), cx)),
|
||||
),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn schedule(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||
let said = keep("mail-schedule-said", || None::<SharedString>, window, cx);
|
||||
let shown = said.read(cx).clone();
|
||||
let (sent, later) = (said.clone(), said);
|
||||
section(
|
||||
"ScheduleSend",
|
||||
"Send now, or open the arrow for tomorrow morning, tomorrow afternoon, Monday morning, or a day and hour of your own.",
|
||||
cx,
|
||||
)
|
||||
.child(probe(
|
||||
"mail-schedule",
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_4()
|
||||
.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)),
|
||||
)
|
||||
.children(shown.map(|shown| quiet(shown, cx))),
|
||||
))
|
||||
}
|
||||
Reference in new issue
Block a user