Cleaned up and documented the login screen code.

This commit is contained in:
Werner Kroneman 2023-12-10 15:04:25 +01:00
parent 640db8e166
commit d7292505a5

View File

@ -28,6 +28,10 @@ pub enum LoginStatus {
Error(String), Error(String),
} }
/// A struct that represents a login attempt, containing a username, a default nick, and a password.
/// None are validated in any way.
///
/// Warning: this struct contains the user's password; keep it in memory only as short as possible.
#[derive(Clone)] #[derive(Clone)]
pub struct LoginAttempt { pub struct LoginAttempt {
pub username: String, pub username: String,
@ -45,6 +49,11 @@ impl Debug for LoginAttempt {
} }
} }
/// The props for the login screen widget, including:
/// * The xmpp address to pre-fill in the username field.
/// * The default nick to pre-fill in the default nick field.
/// * The current login state, which will change as the user attempts to log in.
/// * The event handler for when the user clicks the login button.
#[derive(Props)] #[derive(Props)]
pub struct LoginScreenProps<'a> { pub struct LoginScreenProps<'a> {
cached_username: String, cached_username: String,
@ -53,10 +62,32 @@ pub struct LoginScreenProps<'a> {
on_login_attempt: EventHandler<'a, LoginAttempt>, on_login_attempt: EventHandler<'a, LoginAttempt>,
} }
/// A login screen widget.
///
/// This widget is used to query the user for their login credentials, namely:
/// * Username (XMPP address/Jid)
/// * Default nick
/// * Password
///
/// The default nick and username may be pre-filled from the cache.
///
/// The widget itself only contains the UI, and does not know how to validate the credentials
/// or log the user in; the parent context is responsible for that.
///
/// As a small UX improvement, the widget will automatically fill in the default nick
/// based on the username, if the user has not changed the default nick.
pub fn LoginScreen<'a>(cx: Scope<'a, LoginScreenProps>) -> Element<'a> { pub fn LoginScreen<'a>(cx: Scope<'a, LoginScreenProps>) -> Element<'a> {
// The username.
let username = use_state(cx, || cx.props.cached_username.clone()); let username = use_state(cx, || cx.props.cached_username.clone());
// The default nick.
let default_nick = use_state(cx, || cx.props.cached_nick.clone()); let default_nick = use_state(cx, || cx.props.cached_nick.clone());
// The password.
let password = use_state(cx, || "".to_string()); let password = use_state(cx, || "".to_string());
// Track whether the default nick was directly changed by the user.
let default_nick_was_changed = use_state(cx, || false); let default_nick_was_changed = use_state(cx, || false);
render! { render! {
@ -64,13 +95,17 @@ pub fn LoginScreen<'a>(cx: Scope<'a, LoginScreenProps>) -> Element<'a> {
div { div {
class: "login-form", class: "login-form",
// The username input.
input { input {
"type": "text", "type": "text",
placeholder: "Username", placeholder: "XMPP Address",
value: "{username}", value: "{username}",
oninput: move |x| { oninput: move |x| {
// Set the username state variable.
username.set(x.value.clone()); username.set(x.value.clone());
// If the default nick was not changed by the user, set it to the part before the @.
if !default_nick_was_changed.get() { if !default_nick_was_changed.get() {
if let Some(before_at) = x.value.split('@').next() { if let Some(before_at) = x.value.split('@').next() {
default_nick.set(before_at.to_string()); default_nick.set(before_at.to_string());
@ -79,16 +114,21 @@ pub fn LoginScreen<'a>(cx: Scope<'a, LoginScreenProps>) -> Element<'a> {
}, },
} }
// The default nick input.
input { input {
placeholder: "Default nick", placeholder: "Default nick",
"type": "text", "type": "text",
value: "{default_nick}", value: "{default_nick}",
oninput: move |x| { oninput: move |x| {
// Store the nick in the state variable.
default_nick.set(x.value.clone()); default_nick.set(x.value.clone());
// Mark the nick as changed, so that it won't be overwritten by the username.
default_nick_was_changed.set(true); default_nick_was_changed.set(true);
}, },
} }
// The password input.
input { input {
placeholder: "Password", placeholder: "Password",
"type": "password", "type": "password",
@ -109,11 +149,11 @@ pub fn LoginScreen<'a>(cx: Scope<'a, LoginScreenProps>) -> Element<'a> {
rsx! { rsx! {
button { button {
class: match cx.props.login_state { disabled: match cx.props.login_state {
LoginStatus::LoggedOut => "login-button logged-out", LoginStatus::LoggedIn => true,
LoginStatus::LoggingIn => "login-button logging-in", LoginStatus::LoggedOut => false,
LoginStatus::LoggedIn => "login-button logged-in", LoginStatus::LoggingIn => true,
LoginStatus::Error(_) => "error", LoginStatus::Error(_) => false,
}, },
onclick: move |_| { onclick: move |_| {
@ -124,7 +164,12 @@ pub fn LoginScreen<'a>(cx: Scope<'a, LoginScreenProps>) -> Element<'a> {
}); });
}, },
"Login" match cx.props.login_state {
LoginStatus::LoggedOut => "Log in",
LoginStatus::LoggingIn => "Logging in...",
LoginStatus::LoggedIn => "Logged in",
LoginStatus::Error(_) => "Log in",
}
} }
} }