⬆️ Update the components to take the dioxus 0.5 rework into account

This commit is contained in:
2024-03-31 23:26:10 +02:00
parent aad0064a0c
commit 9071b0073c
26 changed files with 537 additions and 534 deletions

View File

@@ -1,9 +1,10 @@
use std::borrow::Cow;
use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
use const_format::formatcp;
use dioxus::prelude::*;
use fermi::*;
use rand::distributions::{Alphanumeric, DistString};
use tracing::{debug, error, warn};
use validator::{Validate, ValidateArgs, ValidateEmail, ValidationError, ValidationErrors};
@@ -46,7 +47,7 @@ const SHAPE_2_COLORS_STR: &str = formatcp!(
const SHAPE_3_COLORS_STR: &str = formatcp!(
"{COLOR_TERNARY_120},{COLOR_TERNARY_110},{COLOR_TERNARY_100},{COLOR_TERNARY_90},{COLOR_TERNARY_80}");
async fn generate_random_avatar(url: &String) -> Option<String> {
async fn generate_random_avatar(url: String) -> Option<String> {
let seed = Alphanumeric.sample_string(&mut rand::thread_rng(), 16);
let req = format!(
"https://{url}/7.x/shapes/svg?\
@@ -105,7 +106,7 @@ enum Process {
}
trait OnValidationError {
fn on_validation_error(&self, error: &ValidationError) {
fn on_validation_error(&mut self, error: &ValidationError) {
let code = error.code.to_string();
let msg = match code.as_str() {
REQUIRED_ERROR_NAME => Some(REQUIRED_ERROR_HELPER_TEXT),
@@ -115,8 +116,8 @@ trait OnValidationError {
self.invalidate(msg.to_string());
}
}
fn reset(&self);
fn invalidate(&self, helper_text: String);
fn reset(&mut self);
fn invalidate(&mut self, helper_text: String);
fn box_clone(&self) -> Box<dyn OnValidationError>;
}
@@ -128,18 +129,18 @@ impl Clone for Box<dyn OnValidationError> {
#[derive(Clone)]
struct TextInputHandler {
state_ref: UseRef<TextInputState>,
state: Signal<TextInputState>,
}
impl TextInputHandler {}
impl OnValidationError for TextInputHandler {
fn reset(&self) {
self.state_ref.write().reset();
fn reset(&mut self) {
self.state.write().reset();
}
fn invalidate(&self, helper_text: String) {
self.state_ref.write().invalidate(helper_text);
fn invalidate(&mut self, helper_text: String) {
self.state.write().invalidate(helper_text);
}
fn box_clone(&self) -> Box<dyn OnValidationError> {
@@ -149,17 +150,17 @@ impl OnValidationError for TextInputHandler {
#[derive(Clone)]
struct UrlInputHandler {
state_ref: UseRef<TextInputState>,
state: Signal<TextInputState>,
}
impl UrlInputHandler {
pub fn new(state_ref: UseRef<TextInputState>) -> Self {
Self { state_ref }
pub fn new(state: Signal<TextInputState>) -> Self {
Self { state }
}
}
impl OnValidationError for UrlInputHandler {
fn on_validation_error(&self, error: &ValidationError) {
fn on_validation_error(&mut self, error: &ValidationError) {
let code = error.code.to_string();
let msg = match code.as_str() {
REQUIRED_ERROR_NAME => Some(REQUIRED_ERROR_HELPER_TEXT),
@@ -171,12 +172,12 @@ impl OnValidationError for UrlInputHandler {
}
}
fn reset(&self) {
self.state_ref.write().reset();
fn reset(&mut self) {
self.state.write().reset();
}
fn invalidate(&self, helper_text: String) {
self.state_ref.write().invalidate(helper_text);
fn invalidate(&mut self, helper_text: String) {
self.state.write().invalidate(helper_text);
}
fn box_clone(&self) -> Box<dyn OnValidationError> {
@@ -186,17 +187,17 @@ impl OnValidationError for UrlInputHandler {
#[derive(Clone)]
struct EmailInputHandler {
state_ref: UseRef<TextInputState>,
state: Signal<TextInputState>,
}
impl EmailInputHandler {
pub fn new(state_ref: UseRef<TextInputState>) -> Self {
Self { state_ref }
pub fn new(state: Signal<TextInputState>) -> Self {
Self { state }
}
}
impl OnValidationError for EmailInputHandler {
fn on_validation_error(&self, error: &ValidationError) {
fn on_validation_error(&mut self, error: &ValidationError) {
let code = error.code.to_string();
let msg = match code.as_str() {
REQUIRED_ERROR_NAME => Some(REQUIRED_ERROR_HELPER_TEXT),
@@ -207,12 +208,12 @@ impl OnValidationError for EmailInputHandler {
self.invalidate(msg.to_string());
}
}
fn reset(&self) {
self.state_ref.write().reset();
fn reset(&mut self) {
self.state.write().reset();
}
fn invalidate(&self, helper_text: String) {
self.state_ref.write().invalidate(helper_text);
fn invalidate(&mut self, helper_text: String) {
self.state.write().invalidate(helper_text);
}
fn box_clone(&self) -> Box<dyn OnValidationError> {
@@ -222,17 +223,17 @@ impl OnValidationError for EmailInputHandler {
#[derive(Clone)]
struct PasswordInputHandler {
state_ref: UseRef<PasswordInputState>,
state: Signal<PasswordInputState>,
}
impl PasswordInputHandler {
pub fn new(state_ref: UseRef<PasswordInputState>) -> Self {
Self { state_ref }
pub fn new(state: Signal<PasswordInputState>) -> Self {
Self { state }
}
}
impl OnValidationError for PasswordInputHandler {
fn on_validation_error(&self, error: &ValidationError) {
fn on_validation_error(&mut self, error: &ValidationError) {
let code = error.code.to_string();
let msg = match code.as_str() {
REQUIRED_ERROR_NAME => Some(REQUIRED_ERROR_HELPER_TEXT),
@@ -244,7 +245,7 @@ impl OnValidationError for PasswordInputHandler {
score = guesses_log10;
}
}
self.state_ref.write().score = score;
self.state.write().score = score;
Some(TOO_WEAK_PASSWORD_ERROR_HELPER_TEXT)
}
_ => None,
@@ -254,12 +255,12 @@ impl OnValidationError for PasswordInputHandler {
}
}
fn reset(&self) {
self.state_ref.write().reset();
fn reset(&mut self) {
self.state.write().reset();
}
fn invalidate(&self, helper_text: String) {
self.state_ref.write().invalidate(helper_text);
fn invalidate(&mut self, helper_text: String) {
self.state.write().invalidate(helper_text);
}
fn box_clone(&self) -> Box<dyn OnValidationError> {
@@ -273,7 +274,9 @@ fn on_validation_errors(
) {
for (field_name, errors) in field_errors {
if let Some(handler) = handlers.get(field_name) {
errors.iter().for_each(|e| handler.on_validation_error(e));
errors
.iter()
.for_each(|e| handler.borrow_mut().on_validation_error(e));
} else if *field_name == "__all__" {
for error in *errors {
let code = error.code.to_string();
@@ -307,7 +310,7 @@ fn on_validation_errors(
if other_field_names_len > 1 { "s" } else { "" },
);
handler.invalidate(formatted);
handler.borrow_mut().invalidate(formatted);
}
}
}
@@ -319,7 +322,7 @@ fn on_validation_errors(
continue;
};
if let Some(handler) = handlers.get(field_name) {
handler.on_validation_error(error);
handler.borrow_mut().on_validation_error(error);
}
}
other => todo!("{:?}", other),
@@ -463,18 +466,15 @@ fn validate_password(password: &Option<String>, process: &Process) -> Result<(),
Ok(())
}
fn on_login(
session_ref: &UseAtomRef<Session>,
data_ref: &UseRef<Data>,
) -> Result<(), ValidationErrors> {
let login = data_ref.read();
fn on_login(session: &GlobalSignal<Session>, data: Signal<Data>) -> Result<(), ValidationErrors> {
let data = data.read();
match login.validate_with_args(&Process::Login) {
match data.validate_with_args(&Process::Login) {
Ok(_) => {
session_ref.write().update(
login.homeserver_url.clone(),
login.id.clone(),
login.password.clone(),
session.write().update(
data.homeserver_url.clone(),
data.id.clone(),
data.password.clone(),
);
Ok(())
}
@@ -483,12 +483,12 @@ fn on_login(
}
fn on_register(
_session_ref: &UseAtomRef<Session>,
data_ref: &UseRef<Data>,
_session: &GlobalSignal<Session>,
data: Signal<Data>,
) -> Result<(), ValidationErrors> {
let login = data_ref.read();
let data = data.read();
match login.validate_with_args(&Process::Registration) {
match data.validate_with_args(&Process::Registration) {
Ok(_) => {
error!("TODO: Manage registration process");
Ok(())
@@ -499,8 +499,9 @@ fn on_register(
#[derive(Clone)]
struct InputHandlers<'a> {
handlers: HashMap<&'a str, Box<dyn OnValidationError>>,
handlers: HashMap<&'a str, Rc<RefCell<dyn OnValidationError>>>,
}
impl<'a> InputHandlers<'a> {
fn new() -> Self {
Self {
@@ -508,28 +509,24 @@ impl<'a> InputHandlers<'a> {
}
}
fn insert<T: 'static + OnValidationError>(&mut self, name: &'a str, handler: T) {
self.handlers.insert(name, Box::new(handler));
self.handlers.insert(name, Rc::new(RefCell::new(handler)));
}
fn get(&self, name: &'a str) -> Option<&dyn OnValidationError> {
fn get(&self, name: &'a str) -> Option<&Rc<RefCell<(dyn OnValidationError + 'static)>>> {
if let Some(handler) = self.handlers.get(name) {
return Some(handler.as_ref());
return Some(handler);
}
None
}
fn reset_handlers(&self) {
self.handlers.values().for_each(|h| h.reset());
self.handlers.values().for_each(|h| h.borrow_mut().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
}
let value = evt.value();
$data_ref.write().$data_field = if !value.is_empty() { Some(value) } else { None }
}
};
}
@@ -537,43 +534,40 @@ macro_rules! on_input {
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(password) = &$data.peek().$data_field {
if let Some(result) = compute_password_score(password, None) {
rating = result.rating;
}
}
$state.write_silent().score = rating;
$state.write().score = rating;
};
}
fn generate_modal<'a, 'b>(
config: &'b PasswordSuggestionsModalConfig<'a>,
on_confirm: impl Fn(Event<MouseData>) + 'b,
) -> LazyNodes<'a, 'b>
where
'b: 'a,
{
fn generate_modal(
config: &PasswordSuggestionsModalConfig,
on_confirm: impl FnMut(Event<MouseData>) + 'static,
) -> Element {
let suggestions = config.suggestions.get(PASSWORD_FIELD_NAME);
let mut rendered_suggestions = Vec::<LazyNodes>::new();
let mut rendered_suggestions = Vec::<Element>::new();
if let Some(suggestions) = suggestions {
if suggestions.len() == 1 {
rendered_suggestions.push(rsx!(suggestions[0].as_str()));
rendered_suggestions.push(rsx!({ suggestions[0].as_str() }));
} else {
suggestions
.iter()
.for_each(|s| rendered_suggestions.push(rsx!(li { s.as_str() })));
.for_each(|s| rendered_suggestions.push(rsx!(li { {s.as_str()} })));
}
}
rsx! {
Modal {
severity: config.severity.clone(),
severity: config.severity,
title: config.title.as_ref(),
on_confirm: on_confirm,
div {
rendered_suggestions.into_iter()
{rendered_suggestions.into_iter()}
}
}
}
@@ -597,70 +591,64 @@ impl<'a> PasswordSuggestionsModalConfig<'a> {
}
}
#[derive(Props)]
pub struct LoginProps<'a> {
dicebear_hostname: Option<&'a str>,
#[derive(Props, Clone, PartialEq)]
pub struct LoginProps {
dicebear_hostname: Option<String>,
}
pub fn Login<'a>(cx: Scope<'a, LoginProps>) -> Element<'a> {
pub fn Login(props: LoginProps) -> Element {
debug!("Login rendering");
let session = use_atom_ref(cx, &SESSION);
let mut data = use_signal(Data::new);
let data_ref = use_ref(cx, Data::new);
let mut current_process = use_signal(|| Process::Login);
let current_process = use_state(cx, || Process::Login);
let data_lock = data.read();
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, PasswordInputState::new);
let confirm_password = data.confirm_password.as_deref().unwrap_or("");
let confirm_password_state = use_ref(cx, PasswordInputState::new);
let homeserver_url = data_lock.homeserver_url.as_deref().unwrap_or("");
let homeserver_url_state = use_signal(TextInputState::new);
let id = data_lock.id.as_deref().unwrap_or("");
let id_state = use_signal(TextInputState::new);
let password = data_lock.password.as_deref().unwrap_or("");
let mut password_state = use_signal(PasswordInputState::new);
let confirm_password = data_lock.confirm_password.as_deref().unwrap_or("");
let mut confirm_password_state = use_signal(PasswordInputState::new);
let mut handlers = InputHandlers::new();
handlers.insert(
HOMESERVER_FIELD_NAME,
UrlInputHandler::new(homeserver_url_state.clone()),
UrlInputHandler::new(homeserver_url_state),
);
handlers.insert(ID_FIELD_NAME, EmailInputHandler::new(id_state.clone()));
handlers.insert(ID_FIELD_NAME, EmailInputHandler::new(id_state));
handlers.insert(
PASSWORD_FIELD_NAME,
PasswordInputHandler::new(password_state.clone()),
PasswordInputHandler::new(password_state),
);
handlers.insert(
CONFIRM_PASSWORD_FIELD_NAME,
PasswordInputHandler::new(confirm_password_state.clone()),
PasswordInputHandler::new(confirm_password_state),
);
let spinner_animated = use_state(cx, || false);
let id_placeholder = use_state(cx, || LOGIN_ID_PLACEHOLDER);
let mut spinner_animated = use_signal(|| false);
let mut id_placeholder = use_signal(|| LOGIN_ID_PLACEHOLDER);
let url = cx
.props
let url = props
.dicebear_hostname
.unwrap_or("dicebear.tools.adrien.run")
.to_string();
.unwrap_or("dicebear.tools.adrien.run".to_string());
let random_avatar_future =
use_future(
cx,
&url,
|url| async move { generate_random_avatar(&url).await },
);
let mut random_avatar_future = use_resource(move || {
to_owned![url];
async move { generate_random_avatar(url).await }
});
let avatar = match random_avatar_future.value() {
let avatar = match &*random_avatar_future.read_unchecked() {
Some(Some(svg)) => {
rsx!(div {
class: ClassName::LOGIN_FORM_PHOTO_CONTENT,
dangerous_inner_html: svg.as_str(),
})
}
Some(None) | None => {
Some(None) => {
warn!("No profile image set or generated, display the placeholder");
rsx!(div {
class: ClassName::LOGIN_FORM_PHOTO_CONTENT,
@@ -669,9 +657,10 @@ pub fn Login<'a>(cx: Scope<'a, LoginProps>) -> Element<'a> {
}
})
}
None => None,
};
if **spinner_animated && session.read().is_logged {
if *spinner_animated.read() && SESSION.read().is_logged {
debug!("Stop spinner");
spinner_animated.set(false);
}
@@ -679,11 +668,11 @@ pub fn Login<'a>(cx: Scope<'a, LoginProps>) -> Element<'a> {
refresh_password_state!(data, password, password_state);
refresh_password_state!(data, confirm_password, confirm_password_state);
let modal_configs = use_ref(cx, Vec::<PasswordSuggestionsModalConfig>::new);
let modal_config = use_state(cx, || None::<PasswordSuggestionsModalConfig>);
let mut modal_configs = use_signal(Vec::<PasswordSuggestionsModalConfig>::new);
let mut modal_config = use_signal(|| None::<PasswordSuggestionsModalConfig>);
if modal_configs.read().len() > 0 && modal_config.is_none() {
modal_config.set(modal_configs.write_silent().pop());
if !modal_configs.read().is_empty() && modal_config.read().is_none() {
modal_config.set(modal_configs.write().pop());
}
let on_clicked_login = {
@@ -693,15 +682,15 @@ pub fn Login<'a>(cx: Scope<'a, LoginProps>) -> Element<'a> {
handlers.reset_handlers();
if **current_process == Process::Registration {
if *current_process.read() == Process::Registration {
current_process.set(Process::Login);
data_ref.write().id = None;
data.write().id = None;
return;
}
spinner_animated.set(true);
if let Err(errors) = on_login(session, data_ref) {
if let Err(errors) = on_login(&SESSION, data) {
let field_errors = errors.field_errors();
on_validation_errors(&field_errors, &handlers);
}
@@ -714,9 +703,9 @@ pub fn Login<'a>(cx: Scope<'a, LoginProps>) -> Element<'a> {
to_owned![handlers, modal_configs];
move |_| {
if **current_process == Process::Login {
if *current_process.read() == Process::Login {
current_process.set(Process::Registration);
data_ref.write().id = None;
data.write().id = None;
return;
}
@@ -724,7 +713,7 @@ pub fn Login<'a>(cx: Scope<'a, LoginProps>) -> Element<'a> {
spinner_animated.set(true);
if let Err(errors) = on_register(session, data_ref) {
if let Err(errors) = on_register(&SESSION, data) {
let field_name = PASSWORD_FIELD_NAME;
let field_errors = errors.field_errors();
@@ -770,18 +759,18 @@ pub fn Login<'a>(cx: Scope<'a, LoginProps>) -> Element<'a> {
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 {
match *current_process.read() {
Process::Registration => {
form_classes[1] = ClassName::REGISTER;
password_classes[1] = ClassName::SHOW;
confirm_password_classes[1] = ClassName::SHOW;
if **id_placeholder != REGISTER_ID_PLACEHOLDER {
if *id_placeholder.read() != REGISTER_ID_PLACEHOLDER {
id_placeholder.set(REGISTER_ID_PLACEHOLDER);
}
}
Process::Login => {
if **id_placeholder != LOGIN_ID_PLACEHOLDER {
if *id_placeholder.read() != LOGIN_ID_PLACEHOLDER {
id_placeholder.set(LOGIN_ID_PLACEHOLDER);
}
}
@@ -790,17 +779,18 @@ pub fn Login<'a>(cx: Scope<'a, LoginProps>) -> Element<'a> {
let on_modal_confirm = move |_: Event<MouseData>| {
modal_config.set(None);
};
let rendered_modal = modal_config
.get()
.read()
.as_ref()
.map(|modal_config| render!(generate_modal(modal_config, on_modal_confirm)));
.map(|modal_config| rsx!({ generate_modal(modal_config, on_modal_confirm) }));
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! {
style { STYLE_SHEET },
rsx! {
style { {STYLE_SHEET} },
Wallpaper {},
@@ -826,7 +816,7 @@ pub fn Login<'a>(cx: Scope<'a, LoginProps>) -> Element<'a> {
placeholder: "Homeserver URL",
value: "{homeserver_url}",
state: homeserver_url_state,
oninput: on_input![data_ref, homeserver_url],
oninput: on_input![data, homeserver_url],
},
},
@@ -836,7 +826,7 @@ pub fn Login<'a>(cx: Scope<'a, LoginProps>) -> Element<'a> {
placeholder: "{id_placeholder}",
value: "{id}",
state: id_state,
oninput: on_input![data_ref, id],
oninput: on_input![data, id],
},
},
@@ -846,7 +836,7 @@ pub fn Login<'a>(cx: Scope<'a, LoginProps>) -> Element<'a> {
placeholder: "Password",
value: "{password}",
state: password_state,
oninput: on_input![data_ref, password],
oninput: on_input![data, password],
},
},
@@ -857,14 +847,14 @@ pub fn Login<'a>(cx: Scope<'a, LoginProps>) -> Element<'a> {
placeholder: "Confirm Password",
value: "{confirm_password}",
state: confirm_password_state,
oninput: on_input![data_ref, confirm_password],
oninput: on_input![data, confirm_password],
}
},
div {
class: ClassName::LOGIN_FORM_SPINNER,
Spinner {
animate: **spinner_animated,
animate: *spinner_animated.read(),
},
},
@@ -885,6 +875,6 @@ pub fn Login<'a>(cx: Scope<'a, LoginProps>) -> Element<'a> {
},
},
rendered_modal,
})
{rendered_modal},
}
}