⬆️ 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

@@ -6,11 +6,10 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
dioxus = "0.4.3" dioxus = "0.5.0"
dioxus-desktop = "0.4.3" dioxus-desktop = "0.5.0"
dioxus-free-icons = { version = "0.7.0", features = ["material-design-icons-navigation", "ionicons"] } dioxus-free-icons = { version = "0.8", features = ["material-design-icons-navigation", "ionicons"] }
dioxus-std = { version = "0.4.1", features = ["utils"] } dioxus-std = { git = "https://github.com/DioxusLabs/dioxus-std.git", branch = "master", features = ["utils"] }
fermi = { version = "0.4.3" }
# matrix-sdk = { version = "0.6.2", features = ["js"] } # matrix-sdk = { version = "0.6.2", features = ["js"] }
matrix-sdk = { git = "https://github.com/matrix-org/matrix-rust-sdk.git", branch = "main" , features = ["js"]} matrix-sdk = { git = "https://github.com/matrix-org/matrix-rust-sdk.git", branch = "main" , features = ["js"]}
@@ -41,3 +40,7 @@ regex = "1.10.3"
[package.metadata.turf.class_names] [package.metadata.turf.class_names]
template = "<original_name>--<id>" template = "<original_name>--<id>"
[features]
default = []
desktop = ["dioxus/desktop"]

View File

@@ -38,5 +38,6 @@ learn Rust) graphical library should be selected. The [Dioxus](https://dioxuslab
# TODO # TODO
- [ ] Test dioxus-radio.
- [ ] Design system ? - [ ] Design system ?
- [ ] Implement MSN messenger features using Matrix.org sdk... - [ ] Implement MSN messenger features using Matrix.org sdk...

View File

@@ -3,10 +3,13 @@
// (used by [UnboundedReceiver]) by adding 'futures_util' as a dependency to your project // (used by [UnboundedReceiver]) by adding 'futures_util' as a dependency to your project
// and adding the use futures_util::stream::StreamExt; // and adding the use futures_util::stream::StreamExt;
use futures_util::stream::StreamExt; use futures_util::stream::StreamExt;
use std::cell::RefCell;
use std::{collections::HashMap, sync::Arc}; use std::{collections::HashMap, sync::Arc};
use crate::matrix_interface::client::{Client, RoomEvent};
use crate::matrix_interface::requester::{Receivers, Requester};
use crate::matrix_interface::worker_tasks::LoginStyle;
use dioxus::prelude::*; use dioxus::prelude::*;
use fermi::*;
use matrix_sdk::{ use matrix_sdk::{
room::{Room as MatrixRoom, RoomMember}, room::{Room as MatrixRoom, RoomMember},
ruma::{OwnedRoomId, OwnedUserId}, ruma::{OwnedRoomId, OwnedUserId},
@@ -15,9 +18,6 @@ use tokio::select;
use tracing::{debug, error, warn}; use tracing::{debug, error, warn};
use crate::components::chats_window::interface::Interface as ChatsWinInterface; use crate::components::chats_window::interface::Interface as ChatsWinInterface;
use crate::matrix_interface::client::{Client, RoomEvent};
use crate::matrix_interface::requester::{Receivers, Requester};
use crate::matrix_interface::worker_tasks::LoginStyle;
// #[derive(Clone, Debug)] // #[derive(Clone, Debug)]
// pub struct UserInfo { // pub struct UserInfo {
@@ -140,11 +140,9 @@ impl AppSettings {
} }
} }
pub static APP_SETTINGS: AtomRef<AppSettings> = AtomRef(|_| AppSettings::new()); async fn on_room(room_id: OwnedRoomId, room: Room, by_id_rooms: &GlobalSignal<ByIdRooms>) {
async fn on_room(room_id: OwnedRoomId, room: Room, rooms_ref: &UseAtomRef<ByIdRooms>) {
// TODO: Update rooms // TODO: Update rooms
rooms_ref by_id_rooms
.write() .write()
.insert(room_id, RefCell::<Room>::new(room)); .insert(room_id, RefCell::<Room>::new(room));
} }
@@ -152,31 +150,36 @@ async fn on_room(room_id: OwnedRoomId, room: Room, rooms_ref: &UseAtomRef<ByIdRo
async fn on_joining_invitation( async fn on_joining_invitation(
room_id: OwnedRoomId, room_id: OwnedRoomId,
room: Room, room: Room,
rooms_ref: &UseAtomRef<ByIdRooms>, by_id_rooms: &GlobalSignal<ByIdRooms>,
) { ) {
debug!( debug!(
"You're invited to join the \"{}\" room", "You're invited to join the \"{}\" room",
room.name().unwrap() room.name().unwrap()
); );
// TODO: Update rooms // TODO: Update rooms
rooms_ref by_id_rooms
.write() .write()
.insert(room_id, RefCell::<Room>::new(room)); .insert(room_id, RefCell::<Room>::new(room));
} }
pub async fn on_room_topic(room_id: OwnedRoomId, topic: String, rooms_ref: &UseAtomRef<ByIdRooms>) { async fn on_room_topic(room_id: OwnedRoomId, topic: String, by_id_rooms: &GlobalSignal<ByIdRooms>) {
if let Some(room_ref) = rooms_ref.read().get(&room_id) { if let Some(room) = by_id_rooms.read().get(&room_id) {
let mut room = room_ref.borrow_mut(); let mut room = room.borrow_mut();
room.topic = Some(RefCell::new(topic)); room.topic = Some(RefCell::new(topic));
} else { } else {
warn!("No room found with the \"{}\" id", room_id); warn!("No room found with the \"{}\" id", room_id);
} }
} }
pub async fn sync_messages(by_id_rooms: &GlobalSignal<ByIdRooms>, room_id: OwnedRoomId) {
error!("== sync_messages ==");
}
pub async fn sync_rooms( pub async fn sync_rooms(
mut rx: UnboundedReceiver<bool>, mut rx: UnboundedReceiver<bool>,
receivers: Receivers, receivers: Receivers,
rooms_ref: UseAtomRef<ByIdRooms>, by_id_rooms: &GlobalSignal<ByIdRooms>,
) { ) {
if let Some(_is_logged) = rx.next().await { if let Some(_is_logged) = rx.next().await {
let mut rooms_receiver = receivers.room_receiver.borrow_mut(); let mut rooms_receiver = receivers.room_receiver.borrow_mut();
@@ -187,9 +190,9 @@ pub async fn sync_rooms(
res = rooms_receiver.recv() => { res = rooms_receiver.recv() => {
if let Ok(room_event) = res { if let Ok(room_event) = res {
match room_event { match room_event {
RoomEvent::MemberEvent(room_id, room) => on_room(room_id, room, &rooms_ref).await, RoomEvent::MemberEvent(room_id, room) => on_room(room_id, room, &by_id_rooms).await,
RoomEvent::InviteEvent(room_id, room) => on_joining_invitation(room_id, room, &rooms_ref).await, RoomEvent::InviteEvent(room_id, room) => on_joining_invitation(room_id, room, &by_id_rooms).await,
RoomEvent::TopicEvent(room_id, topic) => on_room_topic(room_id, topic, &rooms_ref).await, RoomEvent::TopicEvent(room_id, topic) => on_room_topic(room_id, topic, &by_id_rooms).await,
}; };
} }
}, },
@@ -200,14 +203,14 @@ pub async fn sync_rooms(
pub async fn login( pub async fn login(
mut rx: UnboundedReceiver<bool>, mut rx: UnboundedReceiver<bool>,
app_settings_ref: UseAtomRef<AppSettings>, app_settings: &GlobalSignal<AppSettings>,
session_ref: UseAtomRef<Session>, session: &GlobalSignal<Session>,
) { ) {
while let Some(is_logged) = rx.next().await { while let Some(is_logged) = rx.next().await {
if !is_logged { if !is_logged {
let homeserver_url = session_ref.read().homeserver_url.clone(); let homeserver_url = session.read().homeserver_url.clone();
let username = session_ref.read().username.clone(); let username = session.read().username.clone();
let password = session_ref.read().password.clone(); let password = session.read().password.clone();
if homeserver_url.is_some() && username.is_some() && password.is_some() { if homeserver_url.is_some() && username.is_some() && password.is_some() {
let client = Client::spawn(homeserver_url.unwrap()).await; let client = Client::spawn(homeserver_url.unwrap()).await;
@@ -222,14 +225,14 @@ pub async fn login(
{ {
Ok(_) => { Ok(_) => {
debug!("successfully logged"); debug!("successfully logged");
session_ref.write().is_logged = true; session.write().is_logged = true;
} }
Err(err) => { Err(err) => {
error!("Error during login: {err}"); error!("Error during login: {err}");
// invalid_login.modify(|_| true); // invalid_login.modify(|_| true);
} }
} }
app_settings_ref.write().set_requester(RefCell::new(client)); app_settings.write().set_requester(RefCell::new(client));
} else { } else {
warn!("At least one of the following values is/are invalid: homeserver, username or password"); warn!("At least one of the following values is/are invalid: homeserver, username or password");
} }
@@ -247,7 +250,7 @@ pub struct Session {
pub is_logged: bool, pub is_logged: bool,
} }
impl Session { impl Session {
fn new() -> Self { pub fn new() -> Self {
Self { Self {
homeserver_url: None, homeserver_url: None,
username: None, username: None,
@@ -267,6 +270,8 @@ impl Session {
} }
} }
pub static ROOMS: AtomRef<ByIdRooms> = AtomRef(|_| ByIdRooms::new()); pub static APP_SETTINGS: GlobalSignal<AppSettings> = Signal::global(AppSettings::new);
pub static SESSION: AtomRef<Session> = AtomRef(|_| Session::new()); pub static ROOMS: GlobalSignal<ByIdRooms> = Signal::global(ByIdRooms::new);
pub static CHATS_WIN_INTERFACE: AtomRef<ChatsWinInterface> = AtomRef(|_| ChatsWinInterface::new()); pub static SESSION: GlobalSignal<Session> = Signal::global(Session::new);
pub static CHATS_WIN_INTERFACE: GlobalSignal<ChatsWinInterface> =
Signal::global(ChatsWinInterface::new);

View File

@@ -2,9 +2,9 @@ use dioxus::prelude::*;
turf::style_sheet!("src/components/avatar_selector.scss"); turf::style_sheet!("src/components/avatar_selector.scss");
pub fn AvatarSelector(cx: Scope) -> Element { pub fn AvatarSelector() -> Element {
cx.render(rsx! { rsx! {
style { STYLE_SHEET }, style { {STYLE_SHEET} },
div { div {
class: ClassName::AVATAR_SELECTOR, class: ClassName::AVATAR_SELECTOR,
@@ -50,5 +50,5 @@ pub fn AvatarSelector(cx: Scope) -> Element {
src: "./images/default-avatar.png", src: "./images/default-avatar.png",
}, },
}, },
}) }
} }

View File

@@ -3,8 +3,19 @@ use dioxus_free_icons::{Icon, IconShape};
turf::style_sheet!("src/components/button.scss"); turf::style_sheet!("src/components/button.scss");
#[derive(PartialEq, Clone, Props)]
struct _ButtonProps {
children: Element,
#[props(default = false)]
focus: bool,
id: Option<String>,
onclick: Option<EventHandler<MouseEvent>>,
style: String,
}
macro_rules! svg_text_icon { macro_rules! svg_text_icon {
($name:ident,$text:literal) => { ($name:ident,$text:literal) => {
#[derive(Copy, Clone, PartialEq)]
struct $name; struct $name;
impl IconShape for $name { impl IconShape for $name {
fn view_box(&self) -> String { fn view_box(&self) -> String {
@@ -15,7 +26,7 @@ macro_rules! svg_text_icon {
String::from("http://www.w3.org/2000/svg") String::from("http://www.w3.org/2000/svg")
} }
fn child_elements(&self) -> LazyNodes { fn child_elements(&self) -> Element {
rsx! { rsx! {
text { text {
x: "50%", x: "50%",
@@ -30,78 +41,64 @@ macro_rules! svg_text_icon {
macro_rules! svg_text_button { macro_rules! svg_text_button {
($name:ident,$style:ident,$icon:ident) => { ($name:ident,$style:ident,$icon:ident) => {
pub fn $name<'a>(cx: Scope<'a, ButtonProps>) -> Element<'a> { pub fn $name(props: ButtonProps) -> Element {
cx.render(rsx! { rsx! {
style { STYLE_SHEET }, style { {STYLE_SHEET} },
Button { Button {
id: cx.props.id.unwrap_or(""), id: props.id,
style: ClassName::$style, style: {ClassName::$style},
onclick: |event| { onclick: move |event| {
if let Some(cb) = &cx.props.onclick { if let Some(cb) = &props.onclick {
cb.call(event); cb.call(event);
} }
}, },
focus: cx.props.focus, focus: props.focus,
Icon { Icon {
icon: $icon, icon: $icon,
} }
} }
}) }
} }
}; };
} }
#[derive(Props)] #[derive(PartialEq, Clone, Props)]
struct _ButtonProps<'a> { pub struct ButtonProps {
children: Element<'a>,
#[props(default = false)] #[props(default = false)]
focus: bool, focus: bool,
#[props(optional)] id: Option<String>,
id: Option<&'a str>, onclick: Option<EventHandler<MouseEvent>>,
#[props(optional)] style: Option<String>,
onclick: Option<EventHandler<'a, MouseEvent>>, children: Element,
style: &'static str,
} }
fn Button<'a>(cx: Scope<'a, _ButtonProps<'a>>) -> Element<'a> { fn Button(props: ButtonProps) -> Element {
let focus = cx.props.focus; rsx! {
style { {STYLE_SHEET} },
cx.render(rsx! {
style { STYLE_SHEET },
button { button {
id: cx.props.id, id: props.id,
class: cx.props.style, class: props.style,
onmounted: move |cx| async move { onmounted: move |evt| async move {
let _ = cx.inner().set_focus(focus).await; _ = evt.set_focus(props.focus).await;
}, },
onclick: move |evt| { onclick: move |evt| {
if let Some(cb) = &cx.props.onclick { if let Some(cb) = &props.onclick {
cb.call(evt); cb.call(evt);
} }
}, },
&cx.props.children {props.children},
} },
}) }
}
#[derive(Props)]
pub struct ButtonProps<'a> {
#[props(default = false)]
focus: bool,
#[props(optional)]
id: Option<&'a str>,
#[props(optional)]
onclick: Option<EventHandler<'a, MouseEvent>>,
} }
svg_text_icon!(RegisterText, "REGISTER"); svg_text_icon!(RegisterText, "REGISTER");

View File

@@ -1,17 +1,24 @@
use dioxus::prelude::*; use dioxus::prelude::*;
use tracing::debug; use matrix_sdk::ruma::OwnedRoomId;
use tracing::error;
use super::edit_section::EditSection; use super::edit_section::EditSection;
use crate::base::{sync_messages, ROOMS};
use crate::components::avatar_selector::AvatarSelector; use crate::components::avatar_selector::AvatarSelector;
use crate::components::icons::DownArrowIcon; use crate::components::icons::DownArrowIcon;
turf::style_sheet!("src/components/chats_window/conversation.scss"); turf::style_sheet!("src/components/chats_window/conversation.scss");
pub(super) fn Conversation(cx: Scope) -> Element { #[component]
debug!("Conversation {} rendering", "TBD"); pub(super) fn Conversation(room_id: OwnedRoomId) -> Element {
error!("Conversation {} rendering", room_id);
cx.render(rsx! { let _sync_message_coro: Coroutine<()> =
style { STYLE_SHEET }, use_coroutine(|_: UnboundedReceiver<_>| sync_messages(&ROOMS, room_id));
rsx! {
style { {STYLE_SHEET} },
div { div {
class: ClassName::CONVERSATION, class: ClassName::CONVERSATION,
@@ -76,5 +83,5 @@ pub(super) fn Conversation(cx: Scope) -> Element {
}, },
}, },
}, },
}) }
} }

View File

@@ -2,9 +2,9 @@ use dioxus::prelude::*;
turf::style_sheet!("src/components/chats_window/edit_section.scss"); turf::style_sheet!("src/components/chats_window/edit_section.scss");
pub fn EditSection(cx: Scope) -> Element { pub fn EditSection() -> Element {
cx.render(rsx! { rsx! {
style { STYLE_SHEET }, style { {STYLE_SHEET} },
div { div {
class: ClassName::INPUT_AREA, class: ClassName::INPUT_AREA,
@@ -48,5 +48,5 @@ pub fn EditSection(cx: Scope) -> Element {
}, },
}, },
}, },
}) }
} }

View File

@@ -1,4 +1,5 @@
use dioxus::prelude::*; use std::cell::RefCell;
use matrix_sdk::ruma::OwnedRoomId; use matrix_sdk::ruma::OwnedRoomId;
use tokio::sync::broadcast::error::SendError; use tokio::sync::broadcast::error::SendError;
use tokio::sync::broadcast::{channel, Receiver, Sender}; use tokio::sync::broadcast::{channel, Receiver, Sender};

View File

@@ -3,10 +3,10 @@ mod edit_section;
pub mod interface; pub mod interface;
mod navbar; mod navbar;
use std::cell::RefCell;
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use dioxus::prelude::*; use dioxus::prelude::*;
use fermi::*;
use matrix_sdk::ruma::OwnedRoomId; use matrix_sdk::ruma::OwnedRoomId;
use tokio::sync::broadcast::Receiver; use tokio::sync::broadcast::Receiver;
use tracing::{debug, error}; use tracing::{debug, error};
@@ -20,32 +20,31 @@ use interface::{Interface, Tasks};
turf::style_sheet!("src/components/chats_window/chats_window.scss"); turf::style_sheet!("src/components/chats_window/chats_window.scss");
#[derive(Props, Clone, PartialEq)]
pub struct ChatsWindowProps { pub struct ChatsWindowProps {
pub receivers: Receivers, pub receivers: Receivers,
pub interface: UseAtomRef<Interface>, pub interface: Signal<Interface>,
} }
fn render_rooms_tabs<'a>( fn render_rooms_tabs(
rooms_atom_ref: &'a UseAtomRef<HashMap<OwnedRoomId, RefCell<Room>>>, by_id_rooms: &GlobalSignal<HashMap<OwnedRoomId, RefCell<Room>>>,
displayed_room_ids_ref: &'a UseRef<HashSet<OwnedRoomId>>, displayed_room_ids: Signal<HashSet<OwnedRoomId>>,
) -> Vec<LazyNodes<'a, 'a>> { ) -> Vec<Element> {
let rooms_ref = rooms_atom_ref.read(); let rooms_ref = by_id_rooms.read();
let displayed_room_ids = displayed_room_ids_ref.read(); let displayed_room_ids = displayed_room_ids.read();
rooms_ref rooms_ref
.values() .values()
.filter(|room| displayed_room_ids.contains(&room.borrow().id())) .filter(|room| displayed_room_ids.contains(&room.borrow().id()))
.map(|room| -> LazyNodes { .map(|room| {
let room = room.borrow(); let room = room.borrow();
let room_name = room.name().unwrap_or(room.id().to_string()); let room_name = room.name().unwrap_or(room.id().to_string());
rsx!( rsx!(
div { div {
class: ClassName::TAB, class: ClassName::TAB,
button { button {
img { img {
src: "./images/status_online.png", src: "./images/status_online.png",
}, },
"{room_name}", "{room_name}",
}, },
}, },
@@ -54,9 +53,25 @@ fn render_rooms_tabs<'a>(
.collect() .collect()
} }
async fn handle_controls<'a>( fn render_rooms_conversations(
receiver_ref: &'a RefCell<Receiver<Tasks>>, by_id_rooms: &GlobalSignal<HashMap<OwnedRoomId, RefCell<Room>>>,
displayed_room_ids_ref: &'a UseRef<HashSet<OwnedRoomId>>, displayed_room_ids: Signal<HashSet<OwnedRoomId>>,
) -> Vec<Element> {
let rooms_ref = by_id_rooms.read();
let displayed_room_ids = displayed_room_ids.read();
rooms_ref
.values()
.filter(|room| displayed_room_ids.contains(&room.borrow().id()))
.map(|room| {
let room_id = room.borrow().id();
rsx!(Conversation { room_id: room_id },)
})
.collect()
}
async fn handle_controls(
receiver_ref: &RefCell<Receiver<Tasks>>,
mut displayed_room_ids: Signal<HashSet<OwnedRoomId>>,
) { ) {
loop { loop {
let result = receiver_ref.borrow_mut().recv().await; let result = receiver_ref.borrow_mut().recv().await;
@@ -64,7 +79,7 @@ async fn handle_controls<'a>(
Ok(task) => match task { Ok(task) => match task {
Tasks::ToggleRoom(room_id) => { Tasks::ToggleRoom(room_id) => {
error!("ON TOGGLE ROOM {}", room_id); error!("ON TOGGLE ROOM {}", room_id);
let mut displayed_room_ids = displayed_room_ids_ref.write(); let mut displayed_room_ids = displayed_room_ids.write();
match displayed_room_ids.take(&room_id) { match displayed_room_ids.take(&room_id) {
Some(_) => { Some(_) => {
error!("{} room already dispayed... close it", room_id); error!("{} room already dispayed... close it", room_id);
@@ -81,45 +96,41 @@ async fn handle_controls<'a>(
} }
} }
pub fn ChatsWindow(cx: Scope<ChatsWindowProps>) -> Element { pub fn ChatsWindow(props: ChatsWindowProps) -> Element {
debug!("ChatsWindow rendering"); debug!("ChatsWindow rendering");
use_init_atom_root(cx); let receivers = &props.receivers;
let interface_ref = &props.interface;
let receivers = &cx.props.receivers; let displayed_room_ids = use_signal(HashSet::<OwnedRoomId>::new);
let interface_ref = &cx.props.interface;
let rooms_ref = use_atom_ref(cx, &ROOMS); let sync_rooms_coro = use_coroutine(|rx| {
let displayed_room_ids = use_ref(cx, HashSet::<OwnedRoomId>::new);
let sync_rooms_coro = use_coroutine(cx, |rx| {
to_owned![receivers]; to_owned![receivers];
sync_rooms(rx, receivers, rooms_ref.clone()) sync_rooms(rx, receivers, &ROOMS)
}); });
sync_rooms_coro.send(true); sync_rooms_coro.send(true);
let _: &Coroutine<()> = use_coroutine(cx, |_: UnboundedReceiver<_>| { let _: Coroutine<()> = use_coroutine(|_: UnboundedReceiver<_>| {
to_owned![interface_ref, displayed_room_ids]; to_owned![interface_ref, displayed_room_ids];
async move { async move {
let interface = interface_ref.read(); let interface = interface_ref.read();
let receiver = &interface.receiver(); let receiver = &interface.receiver();
handle_controls(receiver, &displayed_room_ids).await handle_controls(receiver, displayed_room_ids).await
} }
}); });
let rendered_rooms_tabs = render_rooms_tabs(rooms_ref, displayed_room_ids); let rendered_rooms_tabs = render_rooms_tabs(&ROOMS, displayed_room_ids);
let rendered_rooms_conversations = render_rooms_conversations(&ROOMS, displayed_room_ids);
cx.render(rsx! { rsx! {
style { STYLE_SHEET }, style { {STYLE_SHEET} },
div { div {
class: ClassName::CHATS_WINDOW, class: ClassName::CHATS_WINDOW,
div { div {
class: ClassName::TABS, class: ClassName::TABS,
{rendered_rooms_tabs.into_iter()},
rendered_rooms_tabs.into_iter(),
}, },
div { div {
@@ -133,13 +144,11 @@ pub fn ChatsWindow(cx: Scope<ChatsWindowProps>) -> Element {
p { p {
class: ClassName::ROOM_NAME, class: ClassName::ROOM_NAME,
"MON POTE", "MON POTE",
}, },
p { p {
class: ClassName::ROOM_TOPIC, class: ClassName::ROOM_TOPIC,
"LE STATUT A MON POTE", "LE STATUT A MON POTE",
}, },
}, },
@@ -147,8 +156,8 @@ pub fn ChatsWindow(cx: Scope<ChatsWindowProps>) -> Element {
Navbar {}, Navbar {},
}, },
Conversation {}, {rendered_rooms_conversations.into_iter()},
}, },
}, },
}) }
} }

View File

@@ -3,11 +3,11 @@ use tracing::debug;
turf::style_sheet!("src/components/chats_window/navbar.scss"); turf::style_sheet!("src/components/chats_window/navbar.scss");
pub fn Navbar(cx: Scope) -> Element { pub fn Navbar() -> Element {
debug!("Navbar rendering"); debug!("Navbar rendering");
cx.render(rsx! { rsx! {
style { STYLE_SHEET }, style { {STYLE_SHEET} },
div { div {
class: ClassName::NAVBAR, class: ClassName::NAVBAR,
@@ -46,5 +46,5 @@ pub fn Navbar(cx: Scope) -> Element {
style: "background: url(./images/settings.png) center no-repeat", style: "background: url(./images/settings.png) center no-repeat",
}, },
}, },
}) }
} }

View File

@@ -1,3 +1,5 @@
use std::rc::Rc;
use dioxus::prelude::*; use dioxus::prelude::*;
use tracing::debug; use tracing::debug;
@@ -7,19 +9,18 @@ use crate::components::contacts_window::contacts_section::{
turf::style_sheet!("src/components/contacts_window/contacts.scss"); turf::style_sheet!("src/components/contacts_window/contacts.scss");
pub fn Contacts(cx: Scope) -> Element { pub fn Contacts() -> Element {
debug!("Contacts rendering"); debug!("Contacts rendering");
// TODO: Test overflow // TODO: Test overflow
// TODO: Add offline users ? // TODO: Add offline users ?
cx.render(rsx! { rsx! {
style { STYLE_SHEET }, style { {STYLE_SHEET} },
div { div {
class: ClassName::CONTACTS, class: ClassName::CONTACTS,
ContactsSection {name: "Groups", filter: Rc::new(filter_room_conversations)},
ContactsSection {name: "Groups", filter: &filter_room_conversations}, ContactsSection {name: "Available", filter: Rc::new(filter_people_conversations)},
ContactsSection {name: "Available", filter: &filter_people_conversations},
}, },
}) }
} }

View File

@@ -1,33 +1,37 @@
use std::cell::RefCell;
use std::rc::Rc;
use dioxus::prelude::*; use dioxus::prelude::*;
use dioxus_free_icons::icons::io_icons::IoChevronDown; use dioxus_free_icons::icons::io_icons::IoChevronDown;
use dioxus_free_icons::Icon; use dioxus_free_icons::Icon;
use fermi::prelude::*;
use matrix_sdk::{ruma::OwnedRoomId, RoomState}; use matrix_sdk::{ruma::OwnedRoomId, RoomState};
use tracing::{debug, warn}; use tracing::debug;
use crate::base::{ByIdRooms, Room, CHATS_WIN_INTERFACE, ROOMS}; use crate::base::{ByIdRooms, Room, CHATS_WIN_INTERFACE, ROOMS};
use crate::components::chats_window::interface::Interface as ChatsWindowInterface; use crate::components::chats_window::interface::Interface as ChatsWindowInterface;
turf::style_sheet!("src/components/contacts_window/contacts_section.scss"); turf::style_sheet!("src/components/contacts_window/contacts_section.scss");
fn ContactsArrow(cx: Scope) -> Element { fn ContactsArrow() -> Element {
cx.render(rsx! { rsx! {
style { STYLE_SHEET }, style { {STYLE_SHEET} },
Icon { Icon {
icon: IoChevronDown, icon: IoChevronDown,
}, },
}) }
} }
static NO_NAME_REPR: &str = "No name"; static NO_NAME_REPR: &str = "No name";
static NO_SUBJECT_REPR: &str = "No subject"; static NO_SUBJECT_REPR: &str = "No subject";
pub(super) fn filter_people_conversations(rooms_atom: UseAtomRef<ByIdRooms>) -> Vec<RefCell<Room>> { pub(super) fn filter_people_conversations(
let rooms = rooms_atom.read(); by_id_rooms: &GlobalSignal<ByIdRooms>,
let mut filtered_rooms = Vec::<RefCell<Room>>::with_capacity(rooms.len()); ) -> Vec<RefCell<Room>> {
let by_id_rooms = by_id_rooms.read();
for room in rooms.values() { let mut filtered_rooms = Vec::<RefCell<Room>>::with_capacity(by_id_rooms.len());
for room in by_id_rooms.values() {
let is_direct = room.borrow().is_direct.unwrap(); let is_direct = room.borrow().is_direct.unwrap();
if !is_direct { if !is_direct {
filtered_rooms.push(room.to_owned()); filtered_rooms.push(room.to_owned());
@@ -36,11 +40,14 @@ pub(super) fn filter_people_conversations(rooms_atom: UseAtomRef<ByIdRooms>) ->
filtered_rooms filtered_rooms
} }
pub(super) fn filter_room_conversations(rooms_atom: UseAtomRef<ByIdRooms>) -> Vec<RefCell<Room>> { pub(super) fn filter_room_conversations(
let rooms = rooms_atom.read(); by_id_rooms: &GlobalSignal<ByIdRooms>,
let mut filtered_rooms = Vec::<RefCell<Room>>::with_capacity(rooms.len()); ) -> Vec<RefCell<Room>> {
let by_id_rooms = by_id_rooms.read();
for room in rooms.values() { let mut filtered_rooms = Vec::<RefCell<Room>>::with_capacity(by_id_rooms.len());
for room in by_id_rooms.values() {
let is_direct = room.borrow().is_direct.unwrap(); let is_direct = room.borrow().is_direct.unwrap();
if is_direct { if is_direct {
filtered_rooms.push(room.to_owned()); filtered_rooms.push(room.to_owned());
@@ -52,30 +59,33 @@ pub(super) fn filter_room_conversations(rooms_atom: UseAtomRef<ByIdRooms>) -> Ve
// TODO: Handle errors // TODO: Handle errors
fn on_clicked_room( fn on_clicked_room(
room_id: &OwnedRoomId, room_id: &OwnedRoomId,
chats_window_interface: &UseAtomRef<ChatsWindowInterface>, chats_window_interface: &GlobalSignal<ChatsWindowInterface>,
) { ) {
let _ = chats_window_interface.read().toggle_room(room_id.clone()); let _ = chats_window_interface.read().toggle_room(room_id.clone());
} }
#[component] #[derive(Props, Clone)]
pub fn ContactsSection<'a>( pub struct ContactsSectionProps {
cx: Scope, name: String,
name: &'a str, filter: Rc<dyn Fn(&GlobalSignal<ByIdRooms>) -> Vec<RefCell<Room>>>,
filter: &'a dyn Fn(UseAtomRef<ByIdRooms>) -> Vec<RefCell<Room>>, }
) -> Element { impl PartialEq for ContactsSectionProps {
fn eq(&self, other: &Self) -> bool {
self.name == other.name && Rc::ptr_eq(&self.filter, &other.filter)
}
}
pub fn ContactsSection(props: ContactsSectionProps) -> Element {
debug!("ContactsSection rendering"); debug!("ContactsSection rendering");
let rooms_atom_ref = use_atom_ref(cx, &ROOMS); let contacts = props.filter.to_owned()(&ROOMS);
let chats_window_interface_ref = use_atom_ref(cx, &CHATS_WIN_INTERFACE);
let contacts = filter(rooms_atom_ref.clone());
let contacts_len = contacts.len(); let contacts_len = contacts.len();
let show = use_state(cx, || false); let mut show = use_signal(|| false);
let classes = [ let classes = [
ClassName::SECTION, ClassName::SECTION,
if **show { ClassName::ACTIVE } else { "" }, if *show.read() { ClassName::ACTIVE } else { "" },
] ]
.join(" "); .join(" ");
@@ -102,40 +112,44 @@ pub fn ContactsSection<'a>(
} }
); );
rsx!(li { rsx! {
onclick: move |_| on_clicked_room(&room_id, chats_window_interface_ref), li {
onclick: move |_| on_clicked_room(&room_id, &CHATS_WIN_INTERFACE),
img { img {
src: "./images/status_online.png", src: "./images/status_online.png",
}, },
p { p {
formatted, {formatted},
}, },
p { p {
style: "color: darkgrey;", style: "color: darkgrey;",
room_topic, {room_topic},
}, },
}) }
}
}); });
cx.render(rsx! { rsx! {
style { STYLE_SHEET }, style { {STYLE_SHEET} },
div { div {
class: "{classes}", class: "{classes}",
p { p {
class: ClassName::HEADER, class: ClassName::HEADER,
onclick: move |_| show.set(!show), onclick: move |_| {
let state = *show.read();
show.set(!state)
},
ContactsArrow {}, ContactsArrow {},
format!("{name} ({contacts_len})"), {format!("{} ({contacts_len})", props.name)},
}, },
ul { ul {
rendered_contacts.into_iter(), {rendered_contacts.into_iter()},
}, },
}, },
}) }
} }

View File

@@ -10,11 +10,11 @@ use crate::components::contacts_window::user_infos::UserInfos;
turf::style_sheet!("src/components/contacts_window/contacts_window.scss"); turf::style_sheet!("src/components/contacts_window/contacts_window.scss");
pub fn ContactsWindow(cx: Scope) -> Element { pub fn ContactsWindow() -> Element {
debug!("ContactsWindow rendering"); debug!("ContactsWindow rendering");
cx.render(rsx! { rsx! {
style { STYLE_SHEET }, style { {STYLE_SHEET} },
div { div {
class: ClassName::CONTACTS_WINDOW, class: ClassName::CONTACTS_WINDOW,
@@ -94,5 +94,5 @@ pub fn ContactsWindow(cx: Scope) -> Element {
class: ClassName::FOOTER, class: ClassName::FOOTER,
}, },
}, },
}) }
} }

View File

@@ -1,8 +1,6 @@
use dioxus::prelude::*; use dioxus::prelude::*;
// use fermi::*;
use tracing::debug; use tracing::debug;
// use crate::base::APP_SETTINGS;
use crate::components::avatar_selector::AvatarSelector; use crate::components::avatar_selector::AvatarSelector;
use crate::components::icons::DownArrowIcon; use crate::components::icons::DownArrowIcon;
@@ -10,7 +8,7 @@ turf::style_sheet!("src/components/contacts_window/user_infos.scss");
static MESSAGE_PLACEHOLDER: &str = "<Enter a personal message>"; static MESSAGE_PLACEHOLDER: &str = "<Enter a personal message>";
pub fn UserInfos(cx: Scope) -> Element { pub fn UserInfos() -> Element {
debug!("UserInfos rendering"); debug!("UserInfos rendering");
// let app_settings = use_atom_ref(cx, &APP_SETTINGS); // let app_settings = use_atom_ref(cx, &APP_SETTINGS);
@@ -26,6 +24,7 @@ pub fn UserInfos(cx: Scope) -> Element {
// let mut user_info_option = None; // let mut user_info_option = None;
let user_display_name_option: Option<bool> = None; let user_display_name_option: Option<bool> = None;
let user_display_name = "AIE";
// let user_id_option = &store.user_id; // let user_id_option = &store.user_id;
// if user_id_option.is_some() { // if user_id_option.is_some() {
@@ -36,8 +35,8 @@ pub fn UserInfos(cx: Scope) -> Element {
// } // }
// } // }
cx.render(rsx! { rsx! {
style { STYLE_SHEET }, style { {STYLE_SHEET} },
div { div {
class: ClassName::USER_INFO, class: ClassName::USER_INFO,
@@ -67,11 +66,11 @@ pub fn UserInfos(cx: Scope) -> Element {
class: ClassName::USER_MESSAGE, class: ClassName::USER_MESSAGE,
p { p {
// TODO: Handle user message // TODO: Handle user message
MESSAGE_PLACEHOLDER, {MESSAGE_PLACEHOLDER},
} }
DownArrowIcon {}, DownArrowIcon {},
}, },
}, },
}, },
}) }
} }

View File

@@ -2,14 +2,13 @@ use dioxus::prelude::*;
turf::style_sheet!("src/components/header.scss"); turf::style_sheet!("src/components/header.scss");
pub fn Header(cx: Scope) -> Element { pub fn Header() -> Element {
cx.render(rsx! { rsx! {
style { STYLE_SHEET }, style { {STYLE_SHEET} },
div { div {
class: ClassName::ROOT, class: ClassName::ROOT,
img { img {
// src: "./assets/live-logo2.png"
src: "./images/logo-msn.png" src: "./images/logo-msn.png"
} }
svg { svg {
@@ -22,5 +21,5 @@ pub fn Header(cx: Scope) -> Element {
}, },
}, },
} }
}) }
} }

View File

@@ -8,15 +8,15 @@ include!(concat!(env!("OUT_DIR"), "/style_vars.rs"));
use style::{COLOR_PRIMARY_100, COLOR_TERNARY_100}; use style::{COLOR_PRIMARY_100, COLOR_TERNARY_100};
pub fn DownArrowIcon(cx: Scope) -> Element { pub fn DownArrowIcon() -> Element {
cx.render(rsx! { rsx! {
style { STYLE_SHEET }, style { {STYLE_SHEET} },
Icon { Icon {
class: ClassName::DOWN_ARROW_ICON, class: ClassName::DOWN_ARROW_ICON,
icon: MdArrowDropDown, icon: MdArrowDropDown,
} }
}) }
} }
const _PYRAMID_OFFSET_X: f64 = 1.0; const _PYRAMID_OFFSET_X: f64 = 1.0;
@@ -39,13 +39,14 @@ const _PYRAMID_CENTRAL_EDGE_Y_LEN: f64 = _PYRAMID_CENTRAL_EDGE_E2_Y - _PYRAMID_E
const _PYRAMID_RIGHT_EDGE_E2_X: f64 = 130.0 + _PYRAMID_OFFSET_X; const _PYRAMID_RIGHT_EDGE_E2_X: f64 = 130.0 + _PYRAMID_OFFSET_X;
const _PYRAMID_RIGHT_EDGE_E2_Y: f64 = _PYRAMID_LEFT_EDGE_E2_Y; const _PYRAMID_RIGHT_EDGE_E2_Y: f64 = _PYRAMID_LEFT_EDGE_E2_Y;
struct PyramidShape<'a> { #[derive(PartialEq, Clone)]
color: &'a str, struct PyramidShape {
color: String,
ratio: f64, ratio: f64,
progress_color: &'a str, progress_color: String,
} }
impl<'a> IconShape for PyramidShape<'a> { impl IconShape for PyramidShape {
fn view_box(&self) -> String { fn view_box(&self) -> String {
let height = _PYRAMID_CENTRAL_EDGE_E2_Y + _PYRAMID_STROKE_WIDTH; let height = _PYRAMID_CENTRAL_EDGE_E2_Y + _PYRAMID_STROKE_WIDTH;
let width = _PYRAMID_RIGHT_EDGE_E2_X + _PYRAMID_STROKE_WIDTH; let width = _PYRAMID_RIGHT_EDGE_E2_X + _PYRAMID_STROKE_WIDTH;
@@ -56,7 +57,7 @@ impl<'a> IconShape for PyramidShape<'a> {
String::from("http://www.w3.org/2000/svg") String::from("http://www.w3.org/2000/svg")
} }
fn child_elements(&self) -> LazyNodes { fn child_elements(&self) -> Element {
let inverted_ratio = 1.0 - self.ratio; let inverted_ratio = 1.0 - self.ratio;
let central_edge_ratio_e2_y = let central_edge_ratio_e2_y =
@@ -107,24 +108,26 @@ impl<'a> IconShape for PyramidShape<'a> {
} }
} }
#[derive(Props)] #[derive(PartialEq, Clone, Props)]
pub struct PyramidProps<'a> { pub struct PyramidProps {
color: Option<String>,
#[props(default = 0.5)] #[props(default = 0.5)]
color: Option<&'a str>,
ratio: f64, ratio: f64,
progress_color: Option<&'a str>, progress_color: Option<String>,
} }
pub fn Pyramid<'a>(cx: Scope<'a, PyramidProps<'a>>) -> Element<'a> { pub fn Pyramid(props: PyramidProps) -> Element {
let progress_color = cx.props.progress_color.unwrap_or(COLOR_PRIMARY_100); let color = props.color.unwrap_or(COLOR_PRIMARY_100.to_string());
let color = cx.props.color.unwrap_or(COLOR_TERNARY_100); let progress_color = props
.progress_color
.unwrap_or(COLOR_TERNARY_100.to_string());
cx.render(rsx! { rsx! {
style { STYLE_SHEET }, style { {STYLE_SHEET} },
Icon { Icon {
class: ClassName::PYRAMID_ICON, class: ClassName::PYRAMID_ICON,
icon: PyramidShape { ratio: cx.props.ratio, color, progress_color }, icon: PyramidShape { ratio: props.ratio, color, progress_color },
} }
}) }
} }

View File

@@ -6,12 +6,11 @@ use super::wallpaper::Wallpaper;
turf::style_sheet!("src/components/loading.scss"); turf::style_sheet!("src/components/loading.scss");
#[component] pub fn LoadingPage() -> Element {
pub fn LoadingPage(cx: Scope) -> Element {
debug!("LoadingPage rendering"); debug!("LoadingPage rendering");
cx.render(rsx! { rsx! {
style { STYLE_SHEET }, style { {STYLE_SHEET} },
div { div {
class: ClassName::LOADING, class: ClassName::LOADING,
@@ -23,5 +22,5 @@ pub fn LoadingPage(cx: Scope) -> Element {
Spinner {}, Spinner {},
} }
} }
}) }
} }

View File

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

View File

@@ -1,19 +1,15 @@
use dioxus::prelude::*; use dioxus::prelude::*;
use fermi::*;
use tracing::debug; use tracing::debug;
use crate::base::SESSION; use crate::base::SESSION;
use crate::components::contacts_window::ContactsWindow; use crate::components::contacts_window::ContactsWindow;
pub fn MainWindow(cx: Scope) -> Element { pub fn MainWindow() -> Element {
debug!("MainWindow rendering"); debug!("MainWindow rendering");
let session_ref = use_atom_ref(cx, &SESSION); rsx! {
let is_logged = session_ref.read().is_logged; if SESSION.read().is_logged {
ContactsWindow {}
cx.render(rsx! {
if is_logged {
rsx!(ContactsWindow {})
} }
}) }
} }

View File

@@ -14,7 +14,7 @@ use style::{COLOR_CRITICAL_100, COLOR_SUCCESS_100, COLOR_WARNING_100};
turf::style_sheet!("src/components/modal.scss"); turf::style_sheet!("src/components/modal.scss");
#[derive(Clone, Eq, PartialEq, Hash)] #[derive(Clone, Copy, Eq, PartialEq, Hash)]
pub enum Severity { pub enum Severity {
Ok, Ok,
Warning, Warning,
@@ -39,14 +39,14 @@ struct DicebearConfig<'a> {
lips: Vec<u32>, lips: Vec<u32>,
} }
#[derive(Props)] #[derive(Props, Clone, PartialEq)]
pub struct ModalProps<'a> { pub struct ModalProps {
pub severity: Severity, pub severity: Severity,
#[props(optional)] #[props(optional)]
pub title: Option<&'a str>, pub title: Option<String>,
pub children: Element<'a>, pub children: Element,
#[props(optional)] #[props(optional)]
pub on_confirm: Option<EventHandler<'a, MouseEvent>>, pub on_confirm: Option<EventHandler<MouseEvent>>,
} }
fn dicebear_variants() -> &'static HashMap<Severity, DicebearConfig<'static>> { fn dicebear_variants() -> &'static HashMap<Severity, DicebearConfig<'static>> {
@@ -96,10 +96,10 @@ fn render_dicebear_variants(values: &[u32]) -> String {
.join(",") .join(",")
} }
async fn generate_random_figure(url: &String, severity: Severity) -> Option<String> { async fn generate_random_figure(url: &str, severity: &Severity) -> Option<String> {
let mut res: Option<String> = None; let mut res: Option<String> = None;
let config = match dicebear_variants().get(&severity) { let config = match dicebear_variants().get(severity) {
Some(config) => config, Some(config) => config,
None => { None => {
error!("No dicebear configuration found for \"{severity}\""); error!("No dicebear configuration found for \"{severity}\"");
@@ -144,17 +144,14 @@ async fn generate_random_figure(url: &String, severity: Severity) -> Option<Stri
} }
#[component] #[component]
pub fn Modal<'a>(cx: Scope<'a, ModalProps<'a>>) -> Element<'a> { pub fn Modal(props: ModalProps) -> Element {
// TODO: Use configuration file // TODO: Use configuration file
let url = "dicebear.tools.adrien.run".to_string(); let url = "dicebear.tools.adrien.run";
let severity = cx.props.severity.clone(); let random_figure_future =
use_resource(move || async move { generate_random_figure(url, &props.severity).await });
let random_figure_future = use_future(cx, &url, |url| async move { let figure = match &*random_figure_future.read_unchecked() {
generate_random_figure(&url, severity).await
});
let figure = match random_figure_future.value() {
Some(Some(svg)) => Some(rsx! { Some(Some(svg)) => Some(rsx! {
div { div {
class: ClassName::MODAL_CONTENT_ICON_PLACEHOLDER, class: ClassName::MODAL_CONTENT_ICON_PLACEHOLDER,
@@ -163,7 +160,7 @@ pub fn Modal<'a>(cx: Scope<'a, ModalProps<'a>>) -> Element<'a> {
}), }),
Some(None) => { Some(None) => {
warn!("No profile image set or generated, display the placeholder"); warn!("No profile image set or generated, display the placeholder");
let path = match cx.props.severity { let path = match &props.severity {
Severity::Ok => "./images/modal-default-ok-icon.svg", Severity::Ok => "./images/modal-default-ok-icon.svg",
Severity::Warning => "./images/modal-default-warning-icon.svg", Severity::Warning => "./images/modal-default-warning-icon.svg",
Severity::Critical => "./images/modal-default-critical-icon.svg", Severity::Critical => "./images/modal-default-critical-icon.svg",
@@ -180,7 +177,7 @@ pub fn Modal<'a>(cx: Scope<'a, ModalProps<'a>>) -> Element<'a> {
None => None, None => None,
}; };
let button_class = match cx.props.severity { let button_class = match &props.severity {
Severity::Ok => SuccessButton, Severity::Ok => SuccessButton,
Severity::Warning => WarningButton, Severity::Warning => WarningButton,
Severity::Critical => ErrorButton, Severity::Critical => ErrorButton,
@@ -188,8 +185,8 @@ pub fn Modal<'a>(cx: Scope<'a, ModalProps<'a>>) -> Element<'a> {
figure.as_ref()?; figure.as_ref()?;
cx.render(rsx! { rsx! {
style { STYLE_SHEET }, style { {STYLE_SHEET} },
div { div {
class: ClassName::MODAL, class: ClassName::MODAL,
@@ -204,20 +201,19 @@ pub fn Modal<'a>(cx: Scope<'a, ModalProps<'a>>) -> Element<'a> {
div { div {
class: ClassName::MODAL_CONTENT_TITLE, class: ClassName::MODAL_CONTENT_TITLE,
cx.props.title, {props.title},
}, },
div { div {
class: ClassName::MODAL_CONTENT_MSG, class: ClassName::MODAL_CONTENT_MSG,
{props.children},
&cx.props.children,
}, },
div { div {
class: ClassName::MODAL_CONTENT_BUTTONS, class: ClassName::MODAL_CONTENT_BUTTONS,
button_class { button_class {
onclick: move |evt| { onclick: move |evt| {
if let Some(cb) = &cx.props.on_confirm { if let Some(cb) = &props.on_confirm {
cb.call(evt); cb.call(evt);
} }
}, },
@@ -225,5 +221,5 @@ pub fn Modal<'a>(cx: Scope<'a, ModalProps<'a>>) -> Element<'a> {
}, },
}, },
}, },
}) }
} }

View File

@@ -3,7 +3,7 @@ use dioxus_free_icons::{Icon, IconShape};
turf::style_sheet!("src/components/spinner.scss"); turf::style_sheet!("src/components/spinner.scss");
#[derive(Copy, Clone, Debug)] #[derive(Clone, PartialEq)]
struct _Spinner; struct _Spinner;
impl IconShape for _Spinner { impl IconShape for _Spinner {
fn view_box(&self) -> String { fn view_box(&self) -> String {
@@ -12,7 +12,7 @@ impl IconShape for _Spinner {
fn xmlns(&self) -> String { fn xmlns(&self) -> String {
String::from("http://www.w3.org/2000/svg") String::from("http://www.w3.org/2000/svg")
} }
fn child_elements(&self) -> LazyNodes { fn child_elements(&self) -> Element {
rsx! { rsx! {
path { path {
"stroke-linejoin": "round", "stroke-linejoin": "round",
@@ -23,23 +23,23 @@ impl IconShape for _Spinner {
} }
} }
#[derive(PartialEq, Props)] #[derive(PartialEq, Clone, Props)]
pub struct SpinnerProps { pub struct SpinnerProps {
#[props(default = true)] #[props(default = true)]
animate: bool, animate: bool,
} }
pub fn Spinner(cx: Scope<SpinnerProps>) -> Element { pub fn Spinner(props: SpinnerProps) -> Element {
cx.render(rsx! { rsx! {
style { STYLE_SHEET }, style { {STYLE_SHEET} },
div { div {
class: ClassName::SPINNER, class: ClassName::SPINNER,
Icon { Icon {
class: if cx.props.animate { "" } else { ClassName::PAUSED }, class: if props.animate { "" } else { ClassName::PAUSED },
icon: _Spinner, icon: _Spinner,
} }
} }
}) }
} }

View File

@@ -9,15 +9,15 @@ turf::style_sheet!("src/components/text_input.scss");
pub trait InputPropsData {} pub trait InputPropsData {}
#[derive(Props)] #[derive(Props, Clone, PartialEq)]
pub struct InputProps<'a, D: InputPropsData + 'a> { pub struct InputProps<D: InputPropsData + 'static + std::cmp::PartialEq> {
value: Option<&'a str>, value: Option<String>,
placeholder: Option<&'a str>, placeholder: Option<String>,
oninput: Option<EventHandler<'a, Event<FormData>>>, oninput: Option<EventHandler<Event<FormData>>>,
state: Option<&'a UseRef<D>>, state: Option<Signal<D>>,
} }
#[derive(PartialEq)] #[derive(Clone, PartialEq)]
pub struct TextInputState { pub struct TextInputState {
pub is_valid: bool, pub is_valid: bool,
pub helper_text: Option<String>, pub helper_text: Option<String>,
@@ -50,11 +50,11 @@ impl Default for TextInputState {
impl InputPropsData for TextInputState {} impl InputPropsData for TextInputState {}
pub fn TextInput<'a>(cx: Scope<'a, InputProps<'a, TextInputState>>) -> Element<'a> { pub fn TextInput(props: InputProps<TextInputState>) -> Element {
let mut criticity_class = ""; let mut criticity_class = "";
let mut helper_text = "".to_string(); let mut helper_text = "".to_string();
if let Some(state) = cx.props.state { if let Some(state) = props.state {
let state = state.read(); let state = state.read();
if !state.is_valid { if !state.is_valid {
criticity_class = ClassName::INVALID; criticity_class = ClassName::INVALID;
@@ -66,8 +66,8 @@ pub fn TextInput<'a>(cx: Scope<'a, InputProps<'a, TextInputState>>) -> Element<'
let input_classes_str = [ClassName::TEXT_INPUT_INPUT, criticity_class].join(" "); let input_classes_str = [ClassName::TEXT_INPUT_INPUT, criticity_class].join(" ");
cx.render(rsx! { rsx! {
style { STYLE_SHEET }, style { {STYLE_SHEET} },
div { div {
class: ClassName::TEXT_INPUT, class: ClassName::TEXT_INPUT,
@@ -75,11 +75,11 @@ pub fn TextInput<'a>(cx: Scope<'a, InputProps<'a, TextInputState>>) -> Element<'
input { input {
class: "{input_classes_str}", class: "{input_classes_str}",
r#type: "text", r#type: "text",
placeholder: cx.props.placeholder, placeholder: props.placeholder,
value: cx.props.value, value: props.value,
oninput: move |evt| { oninput: move |evt| {
if let Some(cb) = &cx.props.oninput { if let Some(cb) = &props.oninput {
cb.call(evt); cb.call(evt);
} }
}, },
@@ -90,14 +90,14 @@ pub fn TextInput<'a>(cx: Scope<'a, InputProps<'a, TextInputState>>) -> Element<'
p { p {
class: criticity_class, class: criticity_class,
helper_text {helper_text}
} }
} }
} }
}) }
} }
#[derive(PartialEq, Props)] #[derive(Props, Clone, PartialEq)]
pub struct PasswordInputState { pub struct PasswordInputState {
text_input_state: TextInputState, text_input_state: TextInputState,
#[props(default = 0.0)] #[props(default = 0.0)]
@@ -129,14 +129,14 @@ impl Default for PasswordInputState {
impl InputPropsData for PasswordInputState {} impl InputPropsData for PasswordInputState {}
pub fn PasswordTextInput<'a>(cx: Scope<'a, InputProps<'a, PasswordInputState>>) -> Element<'a> { pub fn PasswordTextInput(props: InputProps<PasswordInputState>) -> Element {
let mut criticity_class = ""; let mut criticity_class = "";
let mut helper_text: String = "".to_string(); let mut helper_text: String = "".to_string();
let mut score: Option<f64> = None; let mut score: Option<f64> = None;
let show_password = use_state(cx, || false); let mut show_password = use_signal(|| false);
if let Some(state) = cx.props.state { if let Some(state) = props.state {
let state = state.read(); let state = state.read();
if !state.text_input_state.is_valid { if !state.text_input_state.is_valid {
criticity_class = ClassName::INVALID; criticity_class = ClassName::INVALID;
@@ -158,55 +158,50 @@ pub fn PasswordTextInput<'a>(cx: Scope<'a, InputProps<'a, PasswordInputState>>)
.join(" "); .join(" ");
let input_classes = [ClassName::PASSWORD_TEXT_INPUT_INPUT, criticity_class].join(" "); let input_classes = [ClassName::PASSWORD_TEXT_INPUT_INPUT, criticity_class].join(" ");
cx.render(rsx! { rsx! {
style { STYLE_SHEET }, style { {STYLE_SHEET} },
div { div {
class: "{text_input_classes}", class: "{text_input_classes}",
input { input {
class: "{input_classes}", class: "{input_classes}",
r#type: if **show_password { "text" } else { "password" }, r#type: if *show_password.read() { "text" } else { "password" },
placeholder: cx.props.placeholder, placeholder: props.placeholder,
value: cx.props.value, value: props.value,
oninput: move |evt| { oninput: move |evt| {
if let Some(cb) = &cx.props.oninput { if let Some(cb) = &props.oninput {
cb.call(evt); cb.call(evt);
} }
}, },
}, },
if let Some(score) = score { if let Some(score) = score {
rsx!( div {
div { class: ClassName::PASSWORD_TEXT_INPUT_STRENGTH_LEVEL,
class: ClassName::PASSWORD_TEXT_INPUT_STRENGTH_LEVEL, Pyramid {
Pyramid { ratio: score,
ratio: score,
}
} }
) }
}, },
div { div {
class: ClassName::PASSWORD_TEXT_INPUT_SHOW_TOGGLE, class: ClassName::PASSWORD_TEXT_INPUT_SHOW_TOGGLE,
onclick: move |_| { onclick: move |_| {
show_password.set(!**show_password); let current_state = *show_password.read();
show_password.set(!current_state);
}, },
if **show_password { if *show_password.read() {
rsx!( Icon {
Icon { icon: IoEyeOff,
icon: IoEyeOff, }
}
)
} }
else { else {
rsx!( Icon {
Icon { icon: IoEye,
icon: IoEye, }
}
)
} }
}, },
@@ -215,9 +210,9 @@ pub fn PasswordTextInput<'a>(cx: Scope<'a, InputProps<'a, PasswordInputState>>)
p { p {
class: criticity_class, class: criticity_class,
helper_text {helper_text}
} }
}, },
} }
}) }
} }

View File

@@ -2,10 +2,9 @@ use dioxus::prelude::*;
turf::style_sheet!("src/components/wallpaper.scss"); turf::style_sheet!("src/components/wallpaper.scss");
#[component] pub fn Wallpaper() -> Element {
pub fn Wallpaper(cx: Scope) -> Element { rsx! {
cx.render(rsx! { style { {STYLE_SHEET} },
style { STYLE_SHEET },
div { div {
class: ClassName::WALLPAPER, class: ClassName::WALLPAPER,
@@ -13,5 +12,5 @@ pub fn Wallpaper(cx: Scope) -> Element {
class: ClassName::WALLPAPER_CONTENT, class: ClassName::WALLPAPER_CONTENT,
} }
} }
}) }
} }

View File

@@ -5,121 +5,105 @@ pub mod matrix_interface;
pub mod utils; pub mod utils;
use dioxus::prelude::*; use dioxus::prelude::*;
use dioxus_desktop::Config;
use fermi::*;
use tokio::time::{sleep, Duration}; use tokio::time::{sleep, Duration};
use tracing::{debug, Level}; use tracing::{debug, Level};
use crate::base::{login, sync_rooms, APP_SETTINGS, CHATS_WIN_INTERFACE, ROOMS, SESSION}; use crate::base::{login, sync_rooms};
use crate::components::chats_window::{ChatsWindow, ChatsWindowProps}; use crate::base::{APP_SETTINGS, ROOMS, SESSION};
use crate::components::loading::LoadingPage; use crate::components::loading::LoadingPage;
use crate::components::login::Login; use crate::components::login::Login;
use crate::components::main_window::MainWindow; use crate::components::main_window::MainWindow;
mod base; mod base;
fn App(cx: Scope) -> Element { fn app() -> Element {
debug!("*** App rendering ***"); debug!("*** App rendering ***");
use_init_atom_root(cx); let mut ready = use_signal(|| false);
let app_settings_ref = use_atom_ref(cx, &APP_SETTINGS);
let session_ref = use_atom_ref(cx, &SESSION);
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 // Dummy timer simulating the loading of the application
let _: &Coroutine<()> = use_coroutine(cx, |_: UnboundedReceiver<_>| { let _: Coroutine<()> = use_coroutine(|_: UnboundedReceiver<_>| async move {
to_owned![ready]; debug!("Not ready");
async move { sleep(Duration::from_secs(3)).await;
debug!("Not ready"); // sleep(Duration::from_secs(0)).await;
sleep(Duration::from_secs(3)).await; debug!("Ready");
// sleep(Duration::from_secs(0)).await; ready.set(true);
debug!("Ready");
ready.set(true);
}
}); });
let chats_win_state = use_state(cx, || None); let login_coro = use_coroutine(|rx| login(rx, &APP_SETTINGS, &SESSION));
let login_coro = use_coroutine(cx, |rx| {
login(rx, app_settings_ref.clone(), session_ref.clone())
});
let mut sync_rooms_coro = None; let mut sync_rooms_coro = None;
if let Some(requester) = &app_settings_ref.read().requester { if let Some(requester) = &APP_SETTINGS.read().requester {
sync_rooms_coro = Some(use_coroutine(cx, |rx| { sync_rooms_coro = Some(use_coroutine(|rx| {
sync_rooms(rx, requester.borrow().receivers.clone(), rooms_ref.clone()) sync_rooms(rx, requester.borrow().receivers.clone(), &ROOMS)
})); }));
} }
if !session_ref.read().is_logged { if !SESSION.read().is_logged {
login_coro.send(false); login_coro.send(false);
} else { } else {
if let Some(coro) = sync_rooms_coro { if let Some(coro) = sync_rooms_coro {
coro.send(true); coro.send(true);
} }
if chats_win_state.is_none() { // if chats_win_state.read().is_none() {
let chats_window = dioxus_desktop::use_window(cx); // let chats_window = dioxus_desktop::use_window(cx);
let receivers = app_settings_ref // let receivers = app_settings
.read() // .read()
.requester // .requester
.as_ref() // .as_ref()
.unwrap() // .unwrap()
.borrow() // .borrow()
.receivers // .receivers
.clone(); // .clone();
let chats_props = ChatsWindowProps { // let chats_props = ChatsWindowProps {
receivers, // receivers,
interface: chats_win_interface_ref.clone(), // interface: chats_win_interface_ref.clone(),
}; // };
let chats_dom = VirtualDom::new_with_props(ChatsWindow, chats_props); // let chats_dom = VirtualDom::new_with_props(ChatsWindow, chats_props);
let window_cfg = Config::default().with_custom_head( // let window_cfg = Config::default().with_custom_head(
r#" // r#"
<style type="text/css"> // <style type="text/css">
html, body { // html, body {
height: 100%; // height: 100%;
width: 100%; // width: 100%;
margin: 0; // margin: 0;
} // }
#main, #bodywrap { // #main, #bodywrap {
height: 100%; // height: 100%;
width: 100%; // width: 100%;
} // }
</style> // </style>
"# // "#
.to_owned(), // .to_owned(),
); // );
let chats_window_desktop_service = chats_window.new_window(chats_dom, window_cfg); // let chats_window_desktop_service = chats_window.new_window(chats_dom, window_cfg);
chats_win_state.set(Some(chats_window_desktop_service)); // chats_win_state.set(Some(chats_window_desktop_service));
} // }
} }
if **ready { if *ready.read() {
if session_ref.read().is_logged { if SESSION.read().is_logged {
debug!("Should render the MainWindow component"); debug!("Should render the MainWindow component");
cx.render(rsx! { rsx! {
MainWindow {}, MainWindow {},
}) }
} else { } else {
cx.render(rsx! { rsx! {
Login {}, Login {},
}) }
} }
} else { } else {
cx.render(rsx! { rsx! {
LoadingPage {}, LoadingPage {},
}) }
} }
} }
@@ -129,6 +113,5 @@ fn main() {
.with_max_level(Level::DEBUG) .with_max_level(Level::DEBUG)
.init(); .init();
dioxus_desktop::launch(App); launch(app);
// dioxus_web::launch(App);
} }

View File

@@ -1,6 +1,6 @@
use std::cell::RefCell;
use std::sync::Arc; use std::sync::Arc;
use dioxus::prelude::*;
use tokio::sync::broadcast; use tokio::sync::broadcast;
use tokio::sync::broadcast::Sender; use tokio::sync::broadcast::Sender;
use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver}; use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver};

View File

@@ -1,6 +1,6 @@
use std::cell::RefCell;
use std::sync::Arc; use std::sync::Arc;
use dioxus::prelude::*;
use matrix_sdk::Client as MatrixClient; use matrix_sdk::Client as MatrixClient;
use tokio::sync::broadcast::Receiver; use tokio::sync::broadcast::Receiver;
use tokio::sync::mpsc::UnboundedSender; use tokio::sync::mpsc::UnboundedSender;
@@ -12,7 +12,6 @@ use crate::utils::oneshot;
pub struct Receivers { pub struct Receivers {
pub room_receiver: RefCell<Receiver<RoomEvent>>, pub room_receiver: RefCell<Receiver<RoomEvent>>,
} }
impl Clone for Receivers { impl Clone for Receivers {
fn clone(&self) -> Self { fn clone(&self) -> Self {
Self { Self {
@@ -20,6 +19,13 @@ impl Clone for Receivers {
} }
} }
} }
impl PartialEq for Receivers {
fn eq(&self, other: &Self) -> bool {
self.room_receiver
.borrow()
.same_channel(&other.room_receiver.borrow())
}
}
pub struct Requester { pub struct Requester {
pub matrix_client: Arc<MatrixClient>, pub matrix_client: Arc<MatrixClient>,