Files
Ely-GPUI-Components/src/account/mail.rs
T

278 lines
8.0 KiB
Rust

use std::rc::Rc;
use gpui::{
AnyElement, App, ElementId, FontWeight, InteractiveElement, IntoElement, ParentElement,
RenderOnce, SharedString, Styled, Window, div,
};
use super::login::{Run, field};
use crate::{
buttons::{Button, ButtonVariant},
forms::{EmailInput, Enter, FormField, is_email},
primitives::{Icon, IconName},
theme::{ActiveTheme, IconSize, TextSize},
typography::Link,
};
type OnText = Rc<dyn Fn(&SharedString, &mut Window, &mut App)>;
/// The words that set one email step apart from another.
struct Words {
intro: &'static str,
send: &'static str,
sent: &'static str,
note: fn(&str) -> String,
}
/// A way out: its label and what it runs.
type Way = Option<(&'static str, Run)>;
/// One email step: its words, where it stands, and its ways out. `back` shows in both steps, `other` once sent.
struct Step {
id: ElementId,
words: Words,
sent: bool,
busy: bool,
on_send: OnText,
back: Way,
other: Way,
}
/// An address to send a link to, then where the link went with a way to send it again.
fn email_step(step: Step, window: &mut Window, cx: &mut App) -> AnyElement {
let Step {
id,
words,
sent,
busy,
on_send,
back,
other,
} = step;
let email = field(&id, "email", "name@example.com", false, window, cx);
let address: SharedString = email.read(cx).text().trim().to_string().into();
let ready = is_email(&address) && !busy;
let send: Run = {
let address = address.clone();
Rc::new(move |window, cx| {
log::info!("email step: send");
on_send(&address, window, cx);
})
};
let theme = cx.theme();
let muted = |text: SharedString| {
div().flex().child(
div()
.flex_1()
.min_w_0()
.text_size(theme.text_size(TextSize::Sm))
.text_color(theme.colors.fg_muted)
.child(text),
)
};
let back = back.map(|(label, run)| {
Link::new((id.clone(), "back"), label, move |_, window, cx| {
run(window, cx)
})
});
if sent {
let other = other.map(|(label, run)| {
Button::new((id.clone(), "other"), label)
.variant(ButtonVariant::Ghost)
.on_click(move |_, window, cx| run(window, cx))
});
return div()
.flex()
.flex_col()
.gap_4()
.child(
div()
.flex()
.items_start()
.gap_3()
.child(
Icon::new(IconName::CircleCheck)
.size(IconSize::Md)
.color(theme.colors.success),
)
.child(
div()
.flex_1()
.min_w_0()
.child(div().font_weight(FontWeight::MEDIUM).child(words.sent))
.child(muted((words.note)(&address).into())),
),
)
.child(
div()
.flex()
.flex_wrap()
.gap_2()
.child(
Button::new((id.clone(), "resend"), "Send again")
.loading(busy)
.disabled(busy)
.on_click(move |_, window, cx| send(window, cx)),
)
.children(other),
)
.children(back)
.into_any_element();
}
let enter = send.clone();
div()
.flex()
.flex_col()
.gap_4()
.capture_action(move |_: &Enter, window, cx| {
if ready {
cx.stop_propagation();
enter(window, cx);
}
})
.child(muted(words.intro.into()))
.child(FormField::new((id.clone(), "email-field"), "Email").child(EmailInput::new(&email)))
.child(
Button::new((id.clone(), "send"), words.send)
.variant(ButtonVariant::Primary)
.full_width()
.loading(busy)
.disabled(!ready)
.on_click(move |_, window, cx| send(window, cx)),
)
.children(back)
.into_any_element()
}
/// Sign in by a link sent to an address: the address, then where the link went, with a way to send it again or to use another address.
#[derive(IntoElement)]
pub struct MagicLinkForm {
id: ElementId,
sent: bool,
busy: bool,
on_send: OnText,
on_other: Option<Run>,
}
impl MagicLinkForm {
pub fn new(
id: impl Into<ElementId>,
on_send: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
) -> Self {
Self {
id: id.into(),
sent: false,
busy: false,
on_send: Rc::new(on_send),
on_other: None,
}
}
/// Once the owner has sent the link.
pub fn sent(mut self, sent: bool) -> Self {
self.sent = sent;
self
}
/// While the owner sends.
pub fn busy(mut self, busy: bool) -> Self {
self.busy = busy;
self
}
/// Shows "Use another email" once sent; the owner clears `sent`.
pub fn on_other(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
self.on_other = Some(Rc::new(handler));
self
}
}
impl RenderOnce for MagicLinkForm {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let words = Words {
intro: "We will email you a link that signs you in. No password needed.",
send: "Email me a link",
sent: "Check your email",
note: |address| {
format!("A sign-in link is on its way to {address}. Open it on this device.")
},
};
let other = self.on_other.map(|run| ("Use another email", run));
let step = Step {
id: self.id,
words,
sent: self.sent,
busy: self.busy,
on_send: self.on_send,
back: None,
other,
};
email_step(step, window, cx)
}
}
/// A forgotten password: the account's address, then word that a reset link is on its way, with a way to send it again and a way back to sign in.
#[derive(IntoElement)]
pub struct ForgotPassword {
id: ElementId,
sent: bool,
busy: bool,
on_send: OnText,
on_back: Option<Run>,
}
impl ForgotPassword {
pub fn new(
id: impl Into<ElementId>,
on_send: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
) -> Self {
Self {
id: id.into(),
sent: false,
busy: false,
on_send: Rc::new(on_send),
on_back: None,
}
}
/// Once the owner has sent the link.
pub fn sent(mut self, sent: bool) -> Self {
self.sent = sent;
self
}
/// While the owner sends.
pub fn busy(mut self, busy: bool) -> Self {
self.busy = busy;
self
}
/// Shows "Back to sign in".
pub fn on_back(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
self.on_back = Some(Rc::new(handler));
self
}
}
impl RenderOnce for ForgotPassword {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let words = Words {
intro: "Enter the email you signed up with and we will send a link to set a new password.",
send: "Send reset link",
sent: "Check your email",
note: |address| format!("If an account uses {address}, a reset link is on its way."),
};
let back = self.on_back.map(|run| ("Back to sign in", run));
let step = Step {
id: self.id,
words,
sent: self.sent,
busy: self.busy,
on_send: self.on_send,
back,
other: None,
};
email_step(step, window, cx)
}
}