From 448b81b65d6dd954abc1ada3bc3ccaf19a1ebef1 Mon Sep 17 00:00:00 2001 From: Adrien Date: Sat, 30 Mar 2024 15:31:12 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Add=20Login=20component?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/login.rs | 726 ++++++++++++++++++++++++++++---------- src/components/login.scss | 7 +- src/main.rs | 41 ++- 3 files changed, 571 insertions(+), 203 deletions(-) diff --git a/src/components/login.rs b/src/components/login.rs index 8e9b7dc..78b5534 100644 --- a/src/components/login.rs +++ b/src/components/login.rs @@ -1,5 +1,4 @@ use std::borrow::Cow; -use std::collections::hash_map::Values; use std::collections::HashMap; use const_format::formatcp; @@ -13,8 +12,9 @@ use zxcvbn::zxcvbn; use crate::base::{Session, SESSION}; use super::button::{LoginButton, RegisterButton}; +use super::modal::{Modal, Severity}; use super::spinner::Spinner; -use super::text_input::{TextInput, TextInputState}; +use super::text_input::{PasswordInputState, PasswordTextInput, TextInput, TextInputState}; use super::wallpaper::Wallpaper; @@ -100,75 +100,237 @@ const REGISTER_ID_PLACEHOLDER: &'static str = "Email"; #[derive(PartialEq)] enum Process { - LOGIN, - REGISTRATION, + Login, + Registration, } -fn validate_data(data: &Data, process: &Process) -> Result<(), ValidationError> { - match process { - Process::REGISTRATION => { - let mut is_confirm_password_empty = true; - if let Some(confirm_password) = &data.confirm_password { - if confirm_password.len() > 0 { - is_confirm_password_empty = false; +trait OnValidationError { + fn on_validation_error(&self, error: &ValidationError) { + let code = error.code.to_string(); + let msg = match code.as_str() { + REQUIRED_ERROR_NAME => Some(REQUIRED_ERROR_HELPER_TEXT), + _ => None, + }; + if let Some(msg) = msg { + self.invalidate(msg.to_string()); + } + } + fn reset(&self); + fn invalidate(&self, helper_text: String); + fn box_clone(&self) -> Box; +} + +impl Clone for Box { + fn clone(&self) -> Self { + self.box_clone() + } +} + +#[derive(Clone)] +struct TextInputHandler { + state_ref: UseRef, +} + +impl TextInputHandler {} + +impl OnValidationError for TextInputHandler { + fn reset(&self) { + self.state_ref.write().reset(); + } + + fn invalidate(&self, helper_text: String) { + self.state_ref.write().invalidate(helper_text); + } + + fn box_clone(&self) -> Box { + Box::new(self.clone()) + } +} + +#[derive(Clone)] +struct UrlInputHandler { + state_ref: UseRef, +} + +impl UrlInputHandler { + pub fn new(state_ref: UseRef) -> Self { + Self { state_ref } + } +} + +impl OnValidationError for UrlInputHandler { + fn on_validation_error(&self, error: &ValidationError) { + let code = error.code.to_string(); + let msg = match code.as_str() { + REQUIRED_ERROR_NAME => Some(REQUIRED_ERROR_HELPER_TEXT), + INVALID_URL_ERROR_NAME => Some(INVALID_URL_ERROR_HELPER_TEXT), + _ => None, + }; + if let Some(msg) = msg { + self.invalidate(msg.to_string()); + } + } + + fn reset(&self) { + self.state_ref.write().reset(); + } + + fn invalidate(&self, helper_text: String) { + self.state_ref.write().invalidate(helper_text); + } + + fn box_clone(&self) -> Box { + Box::new(self.clone()) + } +} + +#[derive(Clone)] +struct EmailInputHandler { + state_ref: UseRef, +} + +impl EmailInputHandler { + pub fn new(state_ref: UseRef) -> Self { + Self { state_ref } + } +} + +impl OnValidationError for EmailInputHandler { + fn on_validation_error(&self, error: &ValidationError) { + let code = error.code.to_string(); + let msg = match code.as_str() { + REQUIRED_ERROR_NAME => Some(REQUIRED_ERROR_HELPER_TEXT), + INVALID_EMAIL_ERROR_NAME => Some(INVALID_EMAIL_ERROR_HELPER_TEXT), + _ => None, + }; + if let Some(msg) = msg { + self.invalidate(msg.to_string()); + } + } + fn reset(&self) { + self.state_ref.write().reset(); + } + + fn invalidate(&self, helper_text: String) { + self.state_ref.write().invalidate(helper_text); + } + + fn box_clone(&self) -> Box { + Box::new(self.clone()) + } +} + +#[derive(Clone)] +struct PasswordInputHandler { + state_ref: UseRef, +} + +impl PasswordInputHandler { + pub fn new(state_ref: UseRef) -> Self { + Self { state_ref } + } +} + +impl OnValidationError for PasswordInputHandler { + fn on_validation_error(&self, error: &ValidationError) { + let code = error.code.to_string(); + let msg = match code.as_str() { + REQUIRED_ERROR_NAME => Some(REQUIRED_ERROR_HELPER_TEXT), + INVALID_EMAIL_ERROR_NAME => Some(INVALID_EMAIL_ERROR_HELPER_TEXT), + TOO_WEAK_PASSWORD_ERROR_NAME => { + let mut score = 0.0; + if let Some(guesses_log10) = error.params.get("guesses_log10") { + if let Some(guesses_log10) = guesses_log10.as_f64() { + score = guesses_log10; + } + } + self.state_ref.write().score = score; + Some(TOO_WEAK_PASSWORD_ERROR_HELPER_TEXT) + } + _ => None, + }; + if let Some(msg) = msg { + self.invalidate(msg.to_string()); + } + } + + fn reset(&self) { + self.state_ref.write().reset(); + } + + fn invalidate(&self, helper_text: String) { + self.state_ref.write().invalidate(helper_text); + } + + fn box_clone(&self) -> Box { + Box::new(self.clone()) + } +} + +fn on_validation_errors( + field_errors: &HashMap<&str, &Vec>, + handlers: &InputHandlers, +) { + for (field_name, errors) in field_errors { + if let Some(handler) = handlers.get(field_name) { + errors + .into_iter() + .for_each(|e| handler.on_validation_error(e)); + } else if *field_name == "__all__" { + for error in *errors { + let code = error.code.to_string(); + match code.as_str() { + FIELDS_MISMATCH_ERROR_NAME => { + let values = error.params.values(); + + let field_names = values + .into_iter() + .filter_map(|v| v.as_str()) + .collect::>(); + + for field_name in field_names.iter() { + if let Some(handler) = handlers.get(*field_name) { + let other_field_names = field_names + .iter() + .filter(|f| **f != *field_name) + .map(|f| *f) + .collect::>(); + let other_field_names_len = other_field_names.len(); + + let formatted = format!( + "This field must match with {}{}{} field{}", + other_field_names[0..other_field_names_len - 1].join(", "), + if other_field_names_len > 1 { + " and " + } else { + "" + }, + other_field_names[other_field_names_len - 1], + if other_field_names_len > 1 { "s" } else { "" }, + ); + + handler.invalidate(formatted); + } + } + } + REQUIRED_ERROR_NAME => { + let Some(field_value) = error.params.get("field_name") else { + continue; + }; + let Some(field_name) = field_value.as_str() else { + continue; + }; + if let Some(handler) = handlers.get(field_name) { + handler.on_validation_error(error); + } + } + other => todo!("{:?}", other), } } - if is_confirm_password_empty { - let mut err = ValidationError::new(&REQUIRED_ERROR_NAME); - err.add_param(Cow::from("field_name"), &CONFIRM_PASSWORD_FIELD_NAME); - return Err(err); - } - - if data.password != data.confirm_password { - let mut err = ValidationError::new(FIELDS_MISMATCH_ERROR_NAME); - err.add_param(Cow::from("field_name_1"), &PASSWORD_FIELD_NAME); - err.add_param(Cow::from("field_name_2"), &CONFIRM_PASSWORD_FIELD_NAME); - return Err(err); - } - } - Process::LOGIN => {} - } - Ok(()) -} - -fn validate_id(id: &Option, process: &Process) -> Result<(), ValidationError> { - if *process == Process::REGISTRATION { - if !id.validate_email() { - let err = ValidationError::new(&INVALID_EMAIL_ERROR_NAME); - return Err(err); + } else { + error!("No validation state found for \"{field_name}\" field name"); } } - - Ok(()) -} - -fn validate_password(password: &Option, process: &Process) -> Result<(), ValidationError> { - if *process == Process::REGISTRATION { - if let Some(password) = password { - let estimate = zxcvbn(password, &[]).unwrap(); - - let score = estimate.score(); - - // TODO: Give the limit using the configuration - if score <= 2 { - let mut err = ValidationError::new(TOO_WEAK_PASSWORD_ERROR_NAME); - err.add_param(Cow::from("score"), &score); - - if let Some(feedback) = estimate.feedback() { - let suggestions = feedback - .suggestions() - .iter() - .map(|s| s.to_string()) - .collect::>(); - err.add_param(Cow::from("suggestions"), &suggestions); - } - - return Err(err); - } - } - } - - Ok(()) } #[derive(Debug, Validate)] @@ -199,13 +361,119 @@ impl Data { } } +fn validate_data(data: &Data, process: &Process) -> Result<(), ValidationError> { + match process { + Process::Registration => { + let mut is_confirm_password_empty = true; + if let Some(confirm_password) = &data.confirm_password { + if confirm_password.len() > 0 { + is_confirm_password_empty = false; + } + } + if is_confirm_password_empty { + let mut err = ValidationError::new(&REQUIRED_ERROR_NAME); + err.add_param(Cow::from("field_name"), &CONFIRM_PASSWORD_FIELD_NAME); + return Err(err); + } + + if data.password != data.confirm_password { + let mut err = ValidationError::new(FIELDS_MISMATCH_ERROR_NAME); + err.add_param(Cow::from("field_name_1"), &PASSWORD_FIELD_NAME); + err.add_param(Cow::from("field_name_2"), &CONFIRM_PASSWORD_FIELD_NAME); + return Err(err); + } + } + Process::Login => {} + } + Ok(()) +} + +fn validate_id(id: &Option, process: &Process) -> Result<(), ValidationError> { + if *process == Process::Registration { + if !id.validate_email() { + let err = ValidationError::new(&INVALID_EMAIL_ERROR_NAME); + return Err(err); + } + } + + Ok(()) +} + +struct PasswordValidationResult { + score: u8, + rating: f64, // 0 <= rating <= 1 + suggestions: Vec, +} +impl PasswordValidationResult { + pub fn new() -> Self { + PasswordValidationResult { + score: 0, + rating: 0.0, + suggestions: Vec::::new(), + } + } +} + +fn compute_password_score( + password: &String, + with_suggestions: Option, +) -> Option { + let Ok(estimate) = zxcvbn(password, &[]) else { + return None; + }; + + let mut result = PasswordValidationResult::new(); + result.score = estimate.score(); + + let mut rating = estimate.guesses_log10() * 4.5 / 100.0; + if rating > 1.0 { + rating = 1.0; + }; + result.rating = rating; + + let with_suggestions = with_suggestions.unwrap_or(false); + if with_suggestions { + if let Some(feedback) = estimate.feedback() { + for suggestion in feedback.suggestions() { + result.suggestions.push(suggestion.to_string()); + } + } + } + + Some(result) +} + +fn validate_password(password: &Option, process: &Process) -> Result<(), ValidationError> { + if *process == Process::Registration { + if let Some(password) = password { + if let Some(result) = compute_password_score(password, Some(true)) { + // TODO: To configuration? + if result.score <= 2 { + let mut err = ValidationError::new(TOO_WEAK_PASSWORD_ERROR_NAME); + err.add_param(Cow::from("score"), &result.score); + err.add_param(Cow::from("rating"), &result.rating); + err.add_param(Cow::from("suggestions"), &result.suggestions); + + return Err(err); + } + } else { + error!("Unable to compute the password score"); + } + } else { + warn!("Password parameter is None"); + } + } + + Ok(()) +} + fn on_login( session_ref: &UseAtomRef, data_ref: &UseRef, ) -> Result<(), ValidationErrors> { let login = data_ref.read(); - match login.validate_with_args(&Process::LOGIN) { + match login.validate_with_args(&Process::Login) { Ok(_) => { session_ref.write().update( login.homeserver_url.clone(), @@ -224,7 +492,7 @@ fn on_register( ) -> Result<(), ValidationErrors> { let login = data_ref.read(); - match login.validate_with_args(&Process::REGISTRATION) { + match login.validate_with_args(&Process::Registration) { Ok(_) => { error!("TODO: Manage registration process"); Ok(()) @@ -233,96 +501,104 @@ fn on_register( } } -fn on_validation_errors( - field_errors: &HashMap<&str, &Vec>, - by_field_name_states: &HashMap<&'static str, &UseRef>, -) { - for (field_name, errors) in field_errors { - if let Some(state) = by_field_name_states.get(field_name) { - for error in *errors { - let code = error.code.to_string(); - match code.as_str() { - REQUIRED_ERROR_NAME => { - state - .write() - .invalidate(REQUIRED_ERROR_HELPER_TEXT.to_string()); - } - INVALID_URL_ERROR_NAME => { - state - .write() - .invalidate(INVALID_URL_ERROR_HELPER_TEXT.to_string()); - } - INVALID_EMAIL_ERROR_NAME => { - state - .write() - .invalidate(INVALID_EMAIL_ERROR_HELPER_TEXT.to_string()); - } - TOO_WEAK_PASSWORD_ERROR_NAME => { - state - .write() - .invalidate(TOO_WEAK_PASSWORD_ERROR_HELPER_TEXT.to_string()); - } - other => todo!("{:?}", other), - } - } - } else if *field_name == "__all__" { - for error in *errors { - let code = error.code.to_string(); - match code.as_str() { - FIELDS_MISMATCH_ERROR_NAME => { - let values = error.params.values(); - let field_names = values - .into_iter() - .filter_map(|v| v.as_str()) - .collect::>(); - for field_name in field_names.iter() { - if let Some(state) = by_field_name_states.get(*field_name) { - let other_field_names = field_names - .iter() - .filter(|f| **f != *field_name) - .map(|f| *f) - .collect::>(); - let other_field_names_len = other_field_names.len(); - let formatted = format!( - "This field must match with {}{}{} field{}", - other_field_names[0..other_field_names_len - 1].join(", "), - if other_field_names_len > 1 { - " and " - } else { - "" - }, - other_field_names[other_field_names_len - 1], - if other_field_names_len > 1 { "s" } else { "" }, - ); +#[derive(Clone)] +struct InputHandlers<'a> { + handlers: HashMap<&'a str, Box>, +} +impl<'a> InputHandlers<'a> { + fn new() -> Self { + Self { + handlers: HashMap::new(), + } + } + fn insert(&mut self, name: &'a str, handler: T) { + self.handlers.insert(name, Box::new(handler)); + } - if state.read().is_valid { - state.write().invalidate(formatted); - } - } - } - } - REQUIRED_ERROR_NAME => { - if let Some(field_value) = error.params.get("field_name") { - if let Some(field_name) = field_value.as_str() { - if let Some(state) = by_field_name_states.get(field_name) { - state - .write() - .invalidate(REQUIRED_ERROR_HELPER_TEXT.to_string()); - } - } - } - } - other => todo!("{:?}", other), - } + fn get(&self, name: &'a str) -> Option<&dyn OnValidationError> { + if let Some(handler) = self.handlers.get(name) { + return Some(handler.as_ref()); + } + None + } + fn reset_handlers(&self) { + self.handlers.values().for_each(|h| h.reset()); + } +} + +macro_rules! on_input { + ($data_ref:ident, $data_field:ident) => { + move |evt: FormEvent| { + $data_ref.write().$data_field = if evt.value.len() > 0 { + Some(evt.value.clone()) + } else { + None } + } + }; +} + +macro_rules! refresh_password_state { + ($data:ident, $data_field:ident, $state:ident) => { + let mut rating = 0.0; + if let Some(password) = &$data.$data_field { + if let Some(result) = compute_password_score(password, None) { + rating = result.rating; + } + } + $state.write_silent().score = rating; + }; +} + +fn generate_modal<'a, 'b>( + config: &'b PasswordSuggestionsModalConfig<'a>, + on_confirm: impl Fn(Event) + 'b, +) -> LazyNodes<'a, 'b> +where + 'b: 'a, +{ + let suggestions = config.suggestions.get(PASSWORD_FIELD_NAME); + + let mut rendered_suggestions = Vec::::new(); + if let Some(suggestions) = suggestions { + if suggestions.len() == 1 { + rendered_suggestions.push(rsx!(suggestions[0].as_str())); } else { - error!("No validation state found for \"{field_name}\" field name"); + suggestions + .iter() + .for_each(|s| rendered_suggestions.push(rsx!(li { s.as_str() }))); + } + } + + rsx! { + Modal { + severity: config.severity.clone(), + title: config.title.as_ref(), + on_confirm: on_confirm, + + div { + rendered_suggestions.into_iter() + } } } } -fn reset_states(states: Values<&'static str, &UseRef>) { - let _ = states.for_each(|s| s.write().reset()); +type Suggestions<'a> = HashMap<&'a str, Vec>; + +#[derive(Clone)] +struct PasswordSuggestionsModalConfig<'a> { + severity: Severity, + title: String, + suggestions: Suggestions<'a>, +} +impl<'a> PasswordSuggestionsModalConfig<'a> { + fn new(severity: Option, title: String) -> Self { + Self { + severity: severity.unwrap_or(Severity::Critical), + title, + suggestions: HashMap::new(), + } + } } #[derive(Props)] @@ -337,24 +613,33 @@ pub fn Login<'a>(cx: Scope<'a, LoginProps>) -> Element<'a> { let data_ref = use_ref(cx, Data::new); - let current_process = use_state(cx, || Process::LOGIN); + let current_process = use_state(cx, || Process::Login); let data = data_ref.read(); + let homeserver_url = data.homeserver_url.as_deref().unwrap_or(""); let homeserver_url_state = use_ref(cx, TextInputState::new); let id = data.id.as_deref().unwrap_or(""); let id_state = use_ref(cx, TextInputState::new); let password = data.password.as_deref().unwrap_or(""); - let password_state = use_ref(cx, TextInputState::new); + let password_state = use_ref(cx, PasswordInputState::new); let confirm_password = data.confirm_password.as_deref().unwrap_or(""); - let confirm_password_state = use_ref(cx, TextInputState::new); + let confirm_password_state = use_ref(cx, PasswordInputState::new); - let by_field_name_states: HashMap<&'static str, &UseRef> = HashMap::from([ - (HOMESERVER_FIELD_NAME, homeserver_url_state), - (ID_FIELD_NAME, id_state), - (PASSWORD_FIELD_NAME, password_state), - (CONFIRM_PASSWORD_FIELD_NAME, confirm_password_state), - ]); + let mut handlers = InputHandlers::new(); + handlers.insert( + HOMESERVER_FIELD_NAME, + UrlInputHandler::new(homeserver_url_state.clone()), + ); + handlers.insert(ID_FIELD_NAME, EmailInputHandler::new(id_state.clone())); + handlers.insert( + PASSWORD_FIELD_NAME, + PasswordInputHandler::new(password_state.clone()), + ); + handlers.insert( + CONFIRM_PASSWORD_FIELD_NAME, + PasswordInputHandler::new(confirm_password_state.clone()), + ); let spinner_animated = use_state(cx, || false); let id_placeholder = use_state(cx, || LOGIN_ID_PLACEHOLDER); @@ -395,14 +680,25 @@ pub fn Login<'a>(cx: Scope<'a, LoginProps>) -> Element<'a> { spinner_animated.set(false); } - let on_login = { - to_owned![by_field_name_states]; + refresh_password_state!(data, password, password_state); + refresh_password_state!(data, confirm_password, confirm_password_state); + + let modal_configs = use_ref(cx, Vec::::new); + let modal_config = use_state(cx, || None::); + + if modal_configs.read().len() > 0 && modal_config.is_none() { + modal_config.set(modal_configs.write_silent().pop()); + } + + let on_clicked_login = { + to_owned![handlers]; move |_| { - reset_states(by_field_name_states.values()); - if **current_process == Process::REGISTRATION { - current_process.set(Process::LOGIN); + handlers.reset_handlers(); + + if **current_process == Process::Registration { + current_process.set(Process::Login); data_ref.write().id = None; return; } @@ -411,55 +707,101 @@ pub fn Login<'a>(cx: Scope<'a, LoginProps>) -> Element<'a> { if let Err(errors) = on_login(&session, &data_ref) { let field_errors = errors.field_errors(); - on_validation_errors(&field_errors, &by_field_name_states); + on_validation_errors(&field_errors, &handlers); } spinner_animated.set(false); } }; - let on_register = { - to_owned![by_field_name_states]; + let on_clicked_register = { + to_owned![handlers, modal_configs]; move |_| { - reset_states(by_field_name_states.values()); - - if **current_process == Process::LOGIN { - current_process.set(Process::REGISTRATION); + if **current_process == Process::Login { + current_process.set(Process::Registration); data_ref.write().id = None; return; } + handlers.reset_handlers(); + spinner_animated.set(true); if let Err(errors) = on_register(&session, &data_ref) { + let field_name = PASSWORD_FIELD_NAME; + let field_errors = errors.field_errors(); - on_validation_errors(&field_errors, &by_field_name_states); + if let Some(errors) = field_errors.get(field_name) { + let mut suggestions_msgs = Vec::::new(); + errors + .iter() + .filter(|e| e.code == TOO_WEAK_PASSWORD_ERROR_NAME) + .for_each(|e| { + if let Some(suggestions_value) = e.params.get("suggestions") { + if let Some(suggestion) = suggestions_value.as_array() { + let msgs = suggestion.iter().map(|msg| { + let msg = msg.to_string(); + let without_quote_marks = &msg[1..msg.len() - 1]; + without_quote_marks.to_string() + }); + suggestions_msgs.extend(msgs); + } + } + }); + + if suggestions_msgs.len() > 0 { + let mut modal_config = PasswordSuggestionsModalConfig::new( + None, + "Let's talk about your password".to_string(), + ); + modal_config + .suggestions + .insert(field_name, suggestions_msgs); + + modal_configs.write().push(modal_config); + } + } + + on_validation_errors(&field_errors, &handlers); } spinner_animated.set(false); } }; - let mut form_classes: [&str; 2] = [ClassName::FORM, ""]; - let mut confirm_password_classes: [&str; 2] = [ClassName::CONFIRM_PASSWORD, ""]; + let mut form_classes: [&str; 2] = [ClassName::LOGIN_FORM, ""]; + let mut password_classes: [&str; 2] = [ClassName::LOGIN_FORM_PASSWORD, ""]; + let mut confirm_password_classes: [&str; 2] = [ClassName::LOGIN_FORM_CONFIRM_PASSWORD, ""]; + match **current_process { - Process::REGISTRATION => { + Process::Registration => { form_classes[1] = ClassName::REGISTER; + password_classes[1] = ClassName::SHOW; confirm_password_classes[1] = ClassName::SHOW; if **id_placeholder != REGISTER_ID_PLACEHOLDER { id_placeholder.set(REGISTER_ID_PLACEHOLDER); } } - Process::LOGIN => { + Process::Login => { if **id_placeholder != LOGIN_ID_PLACEHOLDER { id_placeholder.set(LOGIN_ID_PLACEHOLDER); } } } + let on_modal_confirm = move |_: Event| { + modal_config.set(None); + }; + let rendered_modal = if let Some(modal_config) = modal_config.get() { + Some(render!(generate_modal(modal_config, on_modal_confirm))) + } else { + None + }; + let form_classes_str = form_classes.join(" "); + let password_classes_str = password_classes.join(" "); let confirm_password_classes_str = confirm_password_classes.join(" "); cx.render(rsx! { @@ -486,50 +828,42 @@ pub fn Login<'a>(cx: Scope<'a, LoginProps>) -> Element<'a> { div { class: ClassName::LOGIN_FORM_HOMESERVER, TextInput { - id: "hs_url", - r#type: "text", placeholder: "Homeserver URL", value: "{homeserver_url}", state: homeserver_url_state, - oninput: move |evt: FormEvent| data_ref.write().homeserver_url = if evt.value.len() > 0 { - Some(evt.value.clone()) } else { None }, + oninput: on_input![data_ref, homeserver_url], }, }, div { class: ClassName::LOGIN_FORM_ID, TextInput { - r#type: "text", placeholder: "{id_placeholder}", value: "{id}", state: id_state, - oninput: move |evt: FormEvent| data_ref.write().id = if evt.value.len() > 0 { - Some(evt.value.clone()) } else { None }, + oninput: on_input![data_ref, id], }, }, div { - class: ClassName::PASSWORD, - TextInput { - r#type: "password", + class: "{password_classes_str}", + PasswordTextInput { placeholder: "Password", value: "{password}", state: password_state, - oninput: move |evt: FormEvent| data_ref.write().password = if evt.value.len() > 0 { - Some(evt.value.clone()) } else { None }, + oninput: on_input![data_ref, password], }, + }, div { class: "{confirm_password_classes_str}", - TextInput { - r#type: "password", + PasswordTextInput { placeholder: "Confirm Password", value: "{confirm_password}", state: confirm_password_state, - oninput: move |evt: FormEvent| data_ref.write().confirm_password = if evt.value.len() > 0 { - Some(evt.value.clone()) } else { None }, - }, + oninput: on_input![data_ref, confirm_password], + } }, div { @@ -542,7 +876,7 @@ pub fn Login<'a>(cx: Scope<'a, LoginProps>) -> Element<'a> { div { class: ClassName::LOGIN_FORM_REGISTER_BUTTON, RegisterButton { - onclick: on_register, + onclick: on_clicked_register, }, }, @@ -550,10 +884,12 @@ pub fn Login<'a>(cx: Scope<'a, LoginProps>) -> Element<'a> { class: ClassName::LOGIN_FORM_LOGIN_BUTTON, LoginButton { focus: true, - onclick: on_login, + onclick: on_clicked_login, }, }, }, }, + + rendered_modal, }) } diff --git a/src/components/login.scss b/src/components/login.scss index 29e22f5..1ceed48 100644 --- a/src/components/login.scss +++ b/src/components/login.scss @@ -85,7 +85,7 @@ overflow: hidden; &__content { - height: calc(100% + (2 * $border-big-width)); + height: calc(100% + (2 * $border-normal-width)); aspect-ratio: 1; } } @@ -113,6 +113,11 @@ &__spinner { grid-area: spinner; + + svg { + width: 100%; + height: 100%; + } } button { diff --git a/src/main.rs b/src/main.rs index f6149db..7465a14 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,10 +7,13 @@ pub mod utils; use dioxus::prelude::*; use dioxus_desktop::Config; use fermi::*; +use tokio::time::{sleep, Duration}; use tracing::{debug, Level}; use crate::base::{login, sync_rooms, APP_SETTINGS, CHATS_WIN_INTERFACE, ROOMS, SESSION}; use crate::components::chats_window::{ChatsWindow, ChatsWindowProps}; +use crate::components::loading::LoadingPage; +use crate::components::login::Login; use crate::components::main_window::MainWindow; mod base; @@ -25,6 +28,20 @@ fn App(cx: Scope) -> Element { let rooms_ref = use_atom_ref(cx, &ROOMS); let chats_win_interface_ref = use_atom_ref(cx, &CHATS_WIN_INTERFACE); + let ready = use_state(cx, || false); + + // Dummy timer simulating the loading of the application + let _: &Coroutine<()> = use_coroutine(cx, |_: UnboundedReceiver<_>| { + to_owned![ready]; + async move { + debug!("Not ready"); + sleep(Duration::from_secs(3)).await; + // sleep(Duration::from_secs(0)).await; + debug!("Ready"); + ready.set(true); + } + }); + let chats_win_state = use_state(cx, || None); let login_coro = use_coroutine(cx, |rx| { @@ -88,13 +105,25 @@ fn App(cx: Scope) -> Element { } } - cx.render(rsx! { - MainWindow {} - }) + if **ready { + if session_ref.read().is_logged { + debug!("Should render the MainWindow component"); + cx.render(rsx! { + MainWindow {}, + }) + } else { + cx.render(rsx! { + Login {}, + }) + } + } else { + cx.render(rsx! { + LoadingPage {}, + }) + } } -#[tokio::main] -async fn main() -> anyhow::Result<()> { +fn main() { tracing_subscriber::fmt() // .pretty() .with_max_level(Level::DEBUG) @@ -102,6 +131,4 @@ async fn main() -> anyhow::Result<()> { dioxus_desktop::launch(App); // dioxus_web::launch(App); - - Ok(()) }